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

chore: bump deps #223

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
20 changes: 0 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,26 +45,6 @@ Please make sure to update tests as appropriate.

[RUNBOOK](runbook.md)

## Interpolating new fraction data

Bridge data from products on a different scale than the one defined at `api/calculators/bridge.py:45` can be added to
the LCM optimizer as long as the data gets interpolated into the same scale.

That can be done like this;

1. Add a file at `./api/test_data/interpolate_input.csv`
2. Have the first column be called "Size" and have 101 measuring points of the products
3. Add one column for each product, where the header is the name of the product.
```csv
Size,Prod1,Prod2
0.01,0,0
0.011482,0,0
...
10000,100,100
```
4. Run `docker-compose build api && docker-compose run api python calculators/fraction_interpolator.py`
5. One result file for each product will be created in `./api/test_data/`

## Radix
Two different environments in Radix are used: one for test (deploys from branch "test") and one for production (deploy from branch "master")

Expand Down
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")
43 changes: 41 additions & 2 deletions api/src/controllers/products.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from azure.common import AzureConflictHttpError
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 +16,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 +42,36 @@ 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,
}

try:
get_table_service().insert_entity(Config.CUSTOM_PRODUCT_TABLE, table_entry)
except AzureConflictHttpError:
return Response("A product with that name already exists", 400)
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
6 changes: 3 additions & 3 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 All @@ -28,7 +28,7 @@
"source-map-explorer": "^2.5.3"
},
"scripts": {
"start": "export NODE_OPTIONS=--openssl-legacy-provider && react-scripts start",
"start": "export NODE_OPTIONS=--openssl-legacy-provider && WDS_SOCKET_PORT=80 react-scripts start",
"build": "export NODE_OPTIONS=--openssl-legacy-provider && react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
Expand Down
Loading
Loading