Skip to content

Commit 8bd0ba3

Browse files
more ux for calibrations
1 parent d06bfcf commit 8bd0ba3

File tree

15 files changed

+959
-23
lines changed

15 files changed

+959
-23
lines changed

core/pioreactor/calibrations/protocols/od_fusion_standards.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,8 +463,8 @@ def advance(self, ctx: SessionContext) -> SessionStep | None:
463463
sigma_floor=fit.sigma_floor,
464464
)
465465

466-
ctx.store_estimator(estimator, pt.OD_FUSED_DEVICE)
467-
ctx.complete({"title": "Fusion estimator saved."})
466+
result = ctx.store_estimator(estimator, pt.OD_FUSED_DEVICE)
467+
ctx.complete({"title": "Fusion estimator saved.", **result})
468468
return CalibrationComplete()
469469

470470

core/pioreactor/web/api.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,15 @@ def get_all_active_estimators(pioreactor_unit: str) -> DelayedResponseReturnValu
14361436
return create_task_response(task)
14371437

14381438

1439+
@api_bp.route("/workers/<pioreactor_unit>/estimators", methods=["GET"])
1440+
def get_all_estimators(pioreactor_unit: str) -> DelayedResponseReturnValue:
1441+
if pioreactor_unit == UNIVERSAL_IDENTIFIER:
1442+
task = broadcast_get_across_workers("/unit_api/estimators")
1443+
else:
1444+
task = tasks.multicast_get("/unit_api/estimators", [pioreactor_unit])
1445+
return create_task_response(task)
1446+
1447+
14391448
@api_bp.route("/workers/<pioreactor_unit>/zipped_calibrations", methods=["GET"])
14401449
def get_all_calibrations_as_yamls(pioreactor_unit: str) -> ResponseReturnValue:
14411450
if pioreactor_unit == UNIVERSAL_IDENTIFIER:
@@ -1603,6 +1612,24 @@ def get_calibration(pioreactor_unit: str, device: str, cal_name: str) -> Delayed
16031612
return create_task_response(task)
16041613

16051614

1615+
@api_bp.route("/workers/<pioreactor_unit>/estimators/<device>", methods=["GET"])
1616+
def get_estimators_by_device(pioreactor_unit: str, device: str) -> DelayedResponseReturnValue:
1617+
if pioreactor_unit == UNIVERSAL_IDENTIFIER:
1618+
task = broadcast_get_across_workers(f"/unit_api/estimators/{device}")
1619+
else:
1620+
task = tasks.multicast_get(f"/unit_api/estimators/{device}", [pioreactor_unit])
1621+
return create_task_response(task)
1622+
1623+
1624+
@api_bp.route("/workers/<pioreactor_unit>/estimators/<device>/<estimator_name>", methods=["GET"])
1625+
def get_estimator(pioreactor_unit: str, device: str, estimator_name: str) -> DelayedResponseReturnValue:
1626+
if pioreactor_unit == UNIVERSAL_IDENTIFIER:
1627+
task = broadcast_get_across_workers(f"/unit_api/estimators/{device}/{estimator_name}")
1628+
else:
1629+
task = tasks.multicast_get(f"/unit_api/estimators/{device}/{estimator_name}", [pioreactor_unit])
1630+
return create_task_response(task)
1631+
1632+
16061633
@api_bp.route("/workers/<pioreactor_unit>/calibrations/<device>", methods=["POST"])
16071634
def create_calibration(pioreactor_unit: str, device: str) -> DelayedResponseReturnValue:
16081635
yaml_data = request.get_json()["calibration_data"]
@@ -1769,6 +1796,15 @@ def set_active_calibration(pioreactor_unit, device, cal_name) -> DelayedResponse
17691796
return create_task_response(task)
17701797

17711798

1799+
@api_bp.route("/workers/<pioreactor_unit>/active_estimators/<device>/<estimator_name>", methods=["PATCH"])
1800+
def set_active_estimator(pioreactor_unit, device, estimator_name) -> DelayedResponseReturnValue:
1801+
if pioreactor_unit == UNIVERSAL_IDENTIFIER:
1802+
task = broadcast_patch_across_workers(f"/unit_api/active_estimators/{device}/{estimator_name}")
1803+
else:
1804+
task = tasks.multicast_patch(f"/unit_api/active_estimators/{device}/{estimator_name}", [pioreactor_unit])
1805+
return create_task_response(task)
1806+
1807+
17721808
@api_bp.route("/workers/<pioreactor_unit>/active_calibrations/<device>", methods=["DELETE"])
17731809
def remove_active_status_calibration(pioreactor_unit, device) -> DelayedResponseReturnValue:
17741810
if pioreactor_unit == UNIVERSAL_IDENTIFIER:
@@ -1778,6 +1814,15 @@ def remove_active_status_calibration(pioreactor_unit, device) -> DelayedResponse
17781814
return create_task_response(task)
17791815

17801816

1817+
@api_bp.route("/workers/<pioreactor_unit>/active_estimators/<device>", methods=["DELETE"])
1818+
def remove_active_status_estimator(pioreactor_unit, device) -> DelayedResponseReturnValue:
1819+
if pioreactor_unit == UNIVERSAL_IDENTIFIER:
1820+
task = broadcast_delete_across_workers(f"/unit_api/active_estimators/{device}")
1821+
else:
1822+
task = tasks.multicast_delete(f"/unit_api/active_estimators/{device}", [pioreactor_unit])
1823+
return create_task_response(task)
1824+
1825+
17811826
@api_bp.route("/workers/<pioreactor_unit>/calibrations/<device>/<cal_name>", methods=["DELETE"])
17821827
def delete_calibration(pioreactor_unit, device, cal_name) -> DelayedResponseReturnValue:
17831828
if pioreactor_unit == UNIVERSAL_IDENTIFIER:
@@ -1787,6 +1832,15 @@ def delete_calibration(pioreactor_unit, device, cal_name) -> DelayedResponseRetu
17871832
return create_task_response(task)
17881833

17891834

1835+
@api_bp.route("/workers/<pioreactor_unit>/estimators/<device>/<estimator_name>", methods=["DELETE"])
1836+
def delete_estimator(pioreactor_unit, device, estimator_name) -> DelayedResponseReturnValue:
1837+
if pioreactor_unit == UNIVERSAL_IDENTIFIER:
1838+
task = broadcast_delete_across_workers(f"/unit_api/estimators/{device}/{estimator_name}")
1839+
else:
1840+
task = tasks.multicast_delete(f"/unit_api/estimators/{device}/{estimator_name}", [pioreactor_unit])
1841+
return create_task_response(task)
1842+
1843+
17901844
## PLUGINS
17911845

17921846

core/pioreactor/web/static/asset-manifest.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"files": {
33
"main.css": "/static/static/css/main.9c7a48b7.css",
4-
"main.js": "/static/static/js/main.52ca6c09.js",
4+
"main.js": "/static/static/js/main.1e4ef9e0.js",
55
"static/media/pioreactor_cloud.webp": "/static/static/media/pioreactor_cloud.b15b29e435797dc69d76.webp",
66
"static/media/roboto-all-500-normal.woff": "/static/static/media/roboto-all-500-normal.0ab669b7a0d19b178f57.woff",
77
"static/media/roboto-all-700-normal.woff": "/static/static/media/roboto-all-700-normal.a457fde362a540fcadff.woff",
@@ -30,10 +30,10 @@
3030
"static/media/roboto-greek-ext-700-normal.woff2": "/static/static/media/roboto-greek-ext-700-normal.bd9854c751441ccc1a70.woff2",
3131
"index.html": "/static/index.html",
3232
"main.9c7a48b7.css.map": "/static/static/css/main.9c7a48b7.css.map",
33-
"main.52ca6c09.js.map": "/static/static/js/main.52ca6c09.js.map"
33+
"main.1e4ef9e0.js.map": "/static/static/js/main.1e4ef9e0.js.map"
3434
},
3535
"entrypoints": [
3636
"static/css/main.9c7a48b7.css",
37-
"static/js/main.52ca6c09.js"
37+
"static/js/main.1e4ef9e0.js"
3838
]
3939
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/static/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Pioreactor"/><link rel="apple-touch-icon" href="/static/logo192.png"/><link rel="manifest" href="/static/manifest.json"/><script defer="defer" src="/static/static/js/main.52ca6c09.js"></script><link href="/static/static/css/main.9c7a48b7.css" rel="stylesheet"></head><body><div id="root"></div></body></html>
1+
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/static/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Pioreactor"/><link rel="apple-touch-icon" href="/static/logo192.png"/><link rel="manifest" href="/static/manifest.json"/><script defer="defer" src="/static/static/js/main.1e4ef9e0.js"></script><link href="/static/static/css/main.9c7a48b7.css" rel="stylesheet"></head><body><div id="root"></div></body></html>

core/pioreactor/web/static/static/js/main.52ca6c09.js renamed to core/pioreactor/web/static/static/js/main.1e4ef9e0.js

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/pioreactor/web/static/static/js/main.52ca6c09.js.LICENSE.txt renamed to core/pioreactor/web/static/static/js/main.1e4ef9e0.js.LICENSE.txt

File renamed without changes.

core/pioreactor/web/static/static/js/main.52ca6c09.js.map renamed to core/pioreactor/web/static/static/js/main.1e4ef9e0.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/pioreactor/web/unit_api.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,33 @@ def get_all_active_estimators() -> ResponseReturnValue:
867867
return attach_cache_control(jsonify(all_estimators), max_age=10)
868868

869869

870+
@unit_api_bp.route("/estimators", methods=["GET"])
871+
def get_all_estimators() -> ResponseReturnValue:
872+
estimator_dir = ESTIMATOR_PATH
873+
874+
if not estimator_dir.exists():
875+
return attach_cache_control(jsonify({}), max_age=10)
876+
877+
all_estimators: dict[str, list] = {}
878+
879+
with local_persistent_storage("active_estimators") as cache:
880+
for file in sorted(estimator_dir.glob("*/*.yaml")):
881+
try:
882+
device = file.parent.name
883+
estimator = to_builtins(yaml_decode(file.read_bytes(), type=AllEstimators))
884+
estimator["is_active"] = cache.get(device) == estimator.get("estimator_name")
885+
estimator["pioreactor_unit"] = HOSTNAME
886+
estimator["device"] = device
887+
if device in all_estimators:
888+
all_estimators[device].append(estimator)
889+
else:
890+
all_estimators[device] = [estimator]
891+
except Exception as e:
892+
publish_to_error_log(f"Error reading {file.stem}: {e}", "get_all_estimators")
893+
894+
return attach_cache_control(jsonify(all_estimators), max_age=10)
895+
896+
870897
@unit_api_bp.route("/zipped_calibrations", methods=["GET"])
871898
def get_all_calibrations_as_zipped_yaml() -> ResponseReturnValue:
872899
calibration_dir = CALIBRATION_PATH
@@ -1092,6 +1119,7 @@ def get_estimators_by_device(device: str) -> ResponseReturnValue:
10921119
estimator = to_builtins(yaml_decode(file.read_bytes(), type=AllEstimators))
10931120
estimator["is_active"] = c.get(device) == estimator.get("estimator_name")
10941121
estimator["pioreactor_unit"] = HOSTNAME
1122+
estimator["device"] = device
10951123
estimators.append(estimator)
10961124
except Exception as e:
10971125
publish_to_error_log(f"Error reading {file.stem}: {e}", "get_estimators_by_device")
@@ -1111,6 +1139,7 @@ def get_estimator(device: str, estimator_name: str) -> ResponseReturnValue:
11111139
estimator = to_builtins(yaml_decode(estimator_path.read_bytes(), type=AllEstimators))
11121140
estimator["is_active"] = c.get(device) == estimator.get("estimator_name")
11131141
estimator["pioreactor_unit"] = HOSTNAME
1142+
estimator["device"] = device
11141143
return attach_cache_control(jsonify(estimator), max_age=10)
11151144
except Exception as e:
11161145
publish_to_error_log(f"Error reading {estimator_path.stem}: {e}", "get_estimator")
@@ -1132,3 +1161,42 @@ def remove_active_status_calibration(device: str) -> ResponseReturnValue:
11321161
c.pop(device)
11331162

11341163
return {"status": "success"}, 200
1164+
1165+
1166+
@unit_api_bp.route("/active_estimators/<device>/<estimator_name>", methods=["PATCH"])
1167+
def set_active_estimator(device: str, estimator_name: str) -> ResponseReturnValue:
1168+
with local_persistent_storage("active_estimators") as c:
1169+
c[device] = estimator_name
1170+
1171+
return {"status": "success"}, 200
1172+
1173+
1174+
@unit_api_bp.route("/active_estimators/<device>", methods=["DELETE"])
1175+
def remove_active_status_estimator(device: str) -> ResponseReturnValue:
1176+
with local_persistent_storage("active_estimators") as c:
1177+
if device in c:
1178+
c.pop(device)
1179+
1180+
return {"status": "success"}, 200
1181+
1182+
1183+
@unit_api_bp.route("/estimators/<device>/<estimator_name>", methods=["DELETE"])
1184+
def delete_estimator(device: str, estimator_name: str) -> ResponseReturnValue:
1185+
estimator_path = ESTIMATOR_PATH / device / f"{estimator_name}.yaml"
1186+
1187+
if not estimator_path.exists():
1188+
abort_with(404, description=f"Estimator '{estimator_name}' not found for device '{device}'.")
1189+
1190+
try:
1191+
estimator_path.unlink()
1192+
with local_persistent_storage("active_estimators") as cache:
1193+
if cache.get(device) == estimator_name:
1194+
cache.pop(device)
1195+
1196+
return (
1197+
jsonify({"msg": f"Estimator '{estimator_name}' for device '{device}' deleted successfully."}),
1198+
200,
1199+
)
1200+
except Exception as e:
1201+
publish_to_error_log(f"Error deleting estimator: {e}", "delete_estimator")
1202+
abort_with(500, description="Failed to delete estimator.")

frontend/src/App.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Pioreactors from "./Pioreactors";
1313
import Pioreactor from "./Pioreactor";
1414
import StartNewExperiment from "./StartNewExperiment";
1515
import SingleCalibrationPage from "./SingleCalibrationPage";
16+
import SingleEstimatorPage from "./SingleEstimatorPage";
1617
import CreateExperimentProfile from "./CreateExperimentProfile";
1718
import EditExperimentProfile from "./EditExperimentProfile";
1819
import EditConfig from "./EditConfig";
@@ -26,6 +27,7 @@ import SystemLogs from "./SystemLogs";
2627
//import Analysis from "./Analysis";
2728
import Experiments from "./Experiments";
2829
import Calibrations from "./Calibrations";
30+
import Estimators from "./Estimators";
2931
import Protocols from "./Protocols";
3032
import SideNavAndHeader from "./components/SideNavAndHeader";
3133
import MissingWorkerModelModal from "./components/MissingWorkerModelModal";
@@ -137,6 +139,10 @@ function MainSite() {
137139
<Route path="/calibrations/:pioreactorUnit/" element={<Calibrations title="Pioreactor ~ Calibrations"/>}/>
138140
<Route path="/calibrations/:pioreactorUnit/:device" element={<Calibrations title="Pioreactor ~ Calibrations"/>}/>
139141
<Route path="/calibrations/:pioreactorUnit/:device/:calibrationName" element={<SingleCalibrationPage title="Pioreactor ~ Calibration"/>}/>
142+
<Route path="/estimators" element={<Estimators title="Pioreactor ~ Estimators"/>}/>
143+
<Route path="/estimators/:pioreactorUnit/" element={<Estimators title="Pioreactor ~ Estimators"/>}/>
144+
<Route path="/estimators/:pioreactorUnit/:device" element={<Estimators title="Pioreactor ~ Estimators"/>}/>
145+
<Route path="/estimators/:pioreactorUnit/:device/:estimatorName" element={<SingleEstimatorPage title="Pioreactor ~ Estimator"/>}/>
140146
<Route path="/protocols" element={<Protocols title="Pioreactor ~ Protocols"/>}/>
141147
<Route path="/protocols/:pioreactorUnit" element={<Protocols title="Pioreactor ~ Protocols"/>}/>
142148
<Route path="/protocols/:pioreactorUnit/:device" element={<Protocols title="Pioreactor ~ Protocols"/>}/>

0 commit comments

Comments
 (0)