Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/create product #224

Merged
merged 2 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions api/src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from controllers.combination import bridge_from_combination
from controllers.optimal_bridge import bridgeRequestHandler
from controllers.optimizer import optimizer_request_handler
from controllers.products import products_get
from controllers.products import products_get, products_post
from controllers.report import create_report
from util.authentication import authorize
from util.sync_share_point_az import sync_all
Expand All @@ -22,10 +22,17 @@ def init_api():
app = init_api()


@app.route("/api/products", methods=["GET"])
@app.route("/api/products", methods=["GET", "POST"])
@authorize
def products():
return products_get()
def products() -> Response:
if request.method == "GET":
return products_get()
if request.method == "POST":
name: str = request.json.get("productName")
supplier: str = request.json.get("productSupplier")
product_data: [[float, float]] = request.json.get("productData")
return products_post(name, supplier, product_data)
raise ValueError("Invalid method for endpoint")


@app.route("/api/report", methods=["POST"])
Expand Down
79 changes: 30 additions & 49 deletions api/src/calculators/fraction_interpolator.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,44 @@
import csv
from copy import copy
from bisect import bisect_left

from numpy import log

from calculators.bridge import SIZE_STEPS


def lookup_smaller(table: dict, value: float):
n = [i for i in table.keys() if i <= value]
return max(n)
def find_closest_bigger_index(array: list[float], target: float) -> int:
index = bisect_left(array, target)
return index + 1


def lookup_bigger(table: dict, value: float):
n = [i for i in table.keys() if i >= value]
return min(n)
def log_interpolate_or_extrapolate(xMin: float, yMin: float, xMax: float, yMax: float, z: float) -> float:
increase = (log(z) - log(xMin)) / (log(xMax) - log(xMin))
return increase * (yMax - yMin) + yMin


def fraction_interpolator(x: list[float], y: list[float], z: list[float]) -> list[float]:
table_dict = dict(zip(x, y, strict=False))
max_x = max(x)
def fraction_interpolator_and_extrapolator(
xArray: list[float], yArray: list[float], zArray: list[float] = SIZE_STEPS
) -> list[float]:
sizes_dict = {size: 0 for size in zArray} # Populate size_dict with 0 values
starting_index = find_closest_bigger_index(zArray, min(xArray)) - 1

z_table = {}
for _z in z:
if _z > max_x:
break
smaller_x = lookup_smaller(table_dict, _z)
bigger_x = lookup_bigger(table_dict, _z)
z_table[_z] = {"x1": smaller_x, "x2": bigger_x, "y1": table_dict[smaller_x], "y2": table_dict[bigger_x]}
for zIndex, z in enumerate(zArray[starting_index:]):
if z < xArray[0]: # Don't extrapolate down from first measuring point
continue
# If z is above the range of xArray, use the last two points for extrapolation
elif z > xArray[-1]:
yz = log_interpolate_or_extrapolate(xArray[-2], yArray[-2], xArray[-1], yArray[-1], z)
else:
# Find the interval that z falls into for interpolation
for i in range(1, len(xArray)):
if xArray[i - 1] <= z <= xArray[i]:
yz = log_interpolate_or_extrapolate(xArray[i - 1], yArray[i - 1], xArray[i], yArray[i], z)
break

for zz, values in z_table.items():
x1 = values["x1"]
x2 = values["x2"]
y1 = values["y1"]
y2 = values["y2"]
if yz > 100: # 100% volume has been reached. Stop extrapolation. Set all remaining to 100%
for key in zArray[zIndex + starting_index :]:
sizes_dict[key] = 100
return list(sizes_dict.values())

values["j"] = (y2 - y1) / (log(x2) - log(x1)) * (log(zz) - log(x1)) + y1
return [round(v["j"], 3) for v in z_table.values()]
sizes_dict[z] = round(yz, ndigits=3)


def from_csv_to_csv():
with open("test_data/interpolate_input.csv") as csvfile:
reader = csv.DictReader(csvfile)
fields_copy = copy(reader.fieldnames)
fields_copy.pop(0)
products = {name: [] for name in fields_copy}
a_x = []
for line in reader:
a_x.append(float(line["Size"]))
for n in products:
products[n].append(float(line[n]))

for name in products:
b_y = fraction_interpolator(x=a_x, y=products[name], z=SIZE_STEPS)
with open(f"test_data/{name}.csv", "w+") as newcsvfile:
writer = csv.DictWriter(newcsvfile, fieldnames=["Size", "Cumulative"])
writer.writeheader()
for step, interpol_value in zip(SIZE_STEPS, b_y, strict=False):
writer.writerow({"Size": step, "Cumulative": interpol_value})


if __name__ == "__main__":
from_csv_to_csv()
return list(sizes_dict.values())
2 changes: 2 additions & 0 deletions api/src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@


class Config:
AUTH_DISABLED = os.getenv("AUTH_DISABLED", "false")
TABLE_ACCOUNT_NAME = os.getenv("TABLE_ACCOUNT_NAME", "lcmdevstorage")
TABLE_KEY = os.getenv("TABLE_KEY")
BLOB_CONTAINER_NAME = "lcm-file-blobs"
Expand All @@ -19,4 +20,5 @@ class Config:
DEFAULT_MAX_ITERATIONS = 100
HOME_DIR = str(Path(__file__).parent.absolute())
PRODUCT_TABLE_NAME = "products"
CUSTOM_PRODUCT_TABLE = "interpolatedproducts"
named_supplier = ("Baker Hughes", "Halliburton", "Schlumberger")
39 changes: 37 additions & 2 deletions api/src/controllers/products.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from cachetools import TTLCache, cached
from flask import Response

from calculators.fraction_interpolator import fraction_interpolator_and_extrapolator
from config import Config
from util.azure_table import get_service
from util.azure_table import get_table_service, sanitize_row_key


def sort_products(products: dict[str, dict]):
Expand All @@ -14,7 +15,11 @@ def sort_products(products: dict[str, dict]):

@cached(cache=TTLCache(maxsize=128, ttl=300))
def products_get():
products = get_service().query_entities(Config.PRODUCT_TABLE_NAME)
table_service = get_table_service()

share_point_products = table_service.query_entities(Config.PRODUCT_TABLE_NAME)
interpolated_products = table_service.query_entities(Config.CUSTOM_PRODUCT_TABLE)
products = [*share_point_products, *interpolated_products]

products_response = {}
for p in products:
Expand All @@ -36,3 +41,33 @@ def products_get():

sorted_products = sort_products(products_response)
return sorted_products


def products_post(product_name: str, supplier_name: str, product_data: [[float, float]]) -> Response:
product_id = sanitize_row_key(product_name)

for p in product_data:
if not len(p) == 2:
return Response("Invalid product data. Must be two valid numbers for each line", 400)

if not isinstance(p[0], float | int) or not isinstance(p[1], float | int):
return Response("Invalid product data. Must be two valid numbers for each line", 400)

sizes = [p[0] for p in product_data]
cumulative = [p[1] for p in product_data]
table_entry = {
"PartitionKey": Config.CUSTOM_PRODUCT_TABLE,
"RowKey": product_id,
"id": product_id,
"title": product_name,
"supplier": supplier_name,
"cumulative": str(fraction_interpolator_and_extrapolator(sizes, cumulative)),
"sack_size": 25,
"environment": "Green",
"cost": 100,
"co2": 1000,
}

get_table_service().insert_entity(Config.CUSTOM_PRODUCT_TABLE, table_entry)
products_get.cache_clear()
return table_entry
109 changes: 105 additions & 4 deletions api/src/tests/test_interpolator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import unittest

from calculators.fraction_interpolator import fraction_interpolator
from calculators.fraction_interpolator import fraction_interpolator_and_extrapolator


class InterpolatorTest(unittest.TestCase):
Expand Down Expand Up @@ -39,6 +39,107 @@ def test_interpolator():
35.88136905,
]

b_x = [39.8, 60.2, 104.7]
b_y = fraction_interpolator(x=a_x, y=a_y, z=b_x)
assert b_y == [0.214, 1.464, 16.634]
b_y = fraction_interpolator_and_extrapolator(xArray=a_x, yArray=a_y)
assert b_y == [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0.192,
0.229,
0.496,
0.875,
1.376,
2.122,
4.313,
8.304,
13.633,
19.459,
25.537,
30.036,
32.986,
34.748,
35.978,
37.233,
38.454,
39.729,
40.968,
42.215,
43.449,
44.698,
45.938,
47.186,
48.422,
49.668,
50.913,
52.167,
53.403,
54.638,
55.914,
57.149,
58.385,
59.646,
60.871,
62.119,
63.366,
]
2 changes: 2 additions & 0 deletions api/src/util/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ def decode_jwt(token):
def authorize(f):
@wraps(f)
def wrap(*args, **kwargs):
if Config.AUTH_DISABLED == "true":
return f(*args, **kwargs)
if "Authorization" not in request.headers:
abort(401, "Missing 'Authorization' header")
try:
Expand Down
2 changes: 1 addition & 1 deletion api/src/util/azure_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def sanitize_row_key(value: str) -> str:
return value.replace("/", "-").replace("\\", "-").replace("#", "").replace("?", "-").replace(" ", "").lower()


def get_service():
def get_table_service() -> TableService:
return TableService(account_name=Config.TABLE_ACCOUNT_NAME, account_key=Config.TABLE_KEY)


Expand Down
1 change: 1 addition & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ services:
restart: unless-stopped
environment:
ENVIRONMENT: development
AUTH_DISABLED: "true"
FLASK_DEBUG: "true"
TABLE_KEY: ${TABLE_KEY}
ports:
Expand Down
4 changes: 2 additions & 2 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@equinor/eds-core-react": "^0.34.0",
"@equinor/eds-icons": "^0.19.3",
"@equinor/eds-core-react": "^0.36.1",
"@equinor/eds-icons": "^0.21.0",
"@equinor/eds-tokens": "^0.9.2",
"axios": "^1.6.2",
"react": "^18.2.0",
Expand Down
4 changes: 4 additions & 0 deletions web/src/Api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axios from 'axios'
import { TNewProduct } from './Types'

const BASE_PATH = '/api'

Expand Down Expand Up @@ -39,6 +40,9 @@ class ProductsApi {
async getProductsApi(token: string) {
return axios.get(`${BASE_PATH}/products`, { headers: { Authorization: `Bearer ${token}` } })
}
async postProductsApi(token: string, newProduct: TNewProduct) {
return axios.post(`${BASE_PATH}/products`, newProduct, { headers: { Authorization: `Bearer ${token}` } })
}
}

class FractionsApi {
Expand Down
2 changes: 0 additions & 2 deletions web/src/Components/Bridging/Graphs/BridgeGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ type BridgeGraphProps = {

const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
console.log(payload)
console.log(label)
return (
<div style={{ backgroundColor: 'white', border: '1px solid gray', padding: '5px', borderRadius: '2px' }}>
<div style={{ opacity: '50%' }}>{`Particle size : ${label}µm`}</div>
Expand Down
2 changes: 1 addition & 1 deletion web/src/Components/Bridging/InputContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ const InputContainer = ({
<Typography variant='h3'>Optimal Bridge:</Typography>
<div style={{ marginInlineStart: '0.5rem' }}>
{[10, 50, 90].map(d => (
<p>
<p key={d}>
D{d}: {findDValue(optimalBridgeGraphData, d, 'Bridge')}
{'\u00B5m'}
</p>
Expand Down
2 changes: 1 addition & 1 deletion web/src/Components/Combinations/CombinationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ export const CombinationCard = ({
setD90(findDValue(graphData, 90, combination.name))
})
.catch(error => {
ErrorToast(`${error.response.data}`, error.response.status)
console.error('fetch error' + error)
ErrorToast(`${error.response.data}`, error.response.status)
})
}, [combination, allProducts])

Expand Down
Loading
Loading