Skip to content

Commit cf468fd

Browse files
adding estimators to api and UI
1 parent c8ec542 commit cf468fd

File tree

13 files changed

+475
-34
lines changed

13 files changed

+475
-34
lines changed

core/pioreactor/background_jobs/od_reading.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -810,7 +810,7 @@ def __init__(self) -> None:
810810
super().__init__()
811811
self.models: dict[pt.PdChannel, Callable] = {}
812812
self.verifiers: dict[pt.PdChannel, Callable] = {}
813-
self.has_logged_warning = False
813+
self._last_bound_warning_at: float | None = None
814814

815815
def hydate_models(self, calibration_data: structs.ODCalibration | None) -> None:
816816
if calibration_data is None:
@@ -889,31 +889,35 @@ def clamp_to_recorded_extrema() -> pt.OD:
889889
try:
890890
return calibration_data.y_to_x(observed_voltage, enforce_bounds=True)
891891
except exc.NoSolutionsFoundError:
892-
if not self.has_logged_warning:
892+
if self._should_warn_about_bounds():
893893
self.logger.warning(
894894
f"No solution found for calibrated signal. Trimming signal. Calibrated for OD=[{min_OD:0.3g}, {max_OD:0.3g}], V=[{min_voltage:0.3g}, {max_voltage:0.3g}]. Observed {observed_voltage:0.3f}V, which would map outside the allowed values."
895895
)
896-
self.has_logged_warning = True
897896
return clamp_to_recorded_extrema()
898897
except exc.SolutionBelowDomainError:
899-
if not self.has_logged_warning:
898+
if self._should_warn_about_bounds():
900899
below_or_above = "below" if observed_voltage <= min_voltage else "outside of"
901900
self.logger.warning(
902901
f"Signal {below_or_above} suggested calibration range. Trimming signal. Calibrated for OD=[{min_OD:0.3g}, {max_OD:0.3g}], V=[{min_voltage:0.3g}, {max_voltage:0.3g}]. Observed {observed_voltage:0.3f}V, which would map outside the allowed values."
903902
)
904-
self.has_logged_warning = True
905903
return clamp_to_recorded_extrema()
906904
except exc.SolutionAboveDomainError:
907-
if not self.has_logged_warning:
905+
if self._should_warn_about_bounds():
908906
above_or_outside = "above" if observed_voltage >= max_voltage else "outside of"
909907
self.logger.warning(
910908
f"Signal {above_or_outside} suggested calibration range. Trimming signal. Calibrated for OD=[{min_OD:0.3g}, {max_OD:0.3g}], V=[{min_voltage:0.3g}, {max_voltage:0.3g}]. Observed {observed_voltage:0.3f}V, which would map outside the allowed values."
911909
)
912-
self.has_logged_warning = True
913910
return clamp_to_recorded_extrema()
914911

915912
return _calibrate_signal, _verify
916913

914+
def _should_warn_about_bounds(self) -> bool:
915+
now = time()
916+
if self._last_bound_warning_at is None or (now - self._last_bound_warning_at) > 300:
917+
self._last_bound_warning_at = now
918+
return True
919+
return False
920+
917921
def __call__(self, od_readings: structs.ODReadings) -> structs.ODReadings:
918922
calibrated_od_readings = structs.ODReadings(ods={}, timestamp=od_readings.timestamp)
919923
for channel in od_readings.ods:
@@ -957,6 +961,7 @@ class CachedEstimatorTransformer(LoggerMixin, EstimatorTransformerProtocol):
957961
def __init__(self) -> None:
958962
super().__init__()
959963
self.estimator: structs.ODFusionEstimator | None = None
964+
self._last_bound_warning_at: float | None = None
960965

961966
def hydrate_estimator(self, estimator: structs.ODFusionEstimator | None) -> None:
962967
if estimator is None:
@@ -973,12 +978,34 @@ def __call__(self, raw_od_readings: structs.ODReadings) -> structs.ODFused | Non
973978
}
974979
try:
975980
od_fused_value = compute_fused_od(self.estimator, fused_inputs)
981+
if self._should_warn_about_bounds(od_fused_value):
982+
self.logger.warning(
983+
"Fused OD estimate hit estimator bounds: estimator=%s min_logc=%s max_logc=%s od_fused=%s ref_normalization=%s",
984+
self.estimator.estimator_name,
985+
self.estimator.min_logc,
986+
self.estimator.max_logc,
987+
od_fused_value,
988+
config.get("od_reading.config", "ref_normalization", fallback="classic"),
989+
)
976990
except Exception as e:
977991
self.logger.debug(f"Failed to compute fused OD: {e}", exc_info=True)
978992
return None
979993

980994
return structs.ODFused(od_fused=od_fused_value, timestamp=raw_od_readings.timestamp)
981995

996+
def _should_warn_about_bounds(self, od_fused_value: float) -> bool:
997+
if self.estimator is None:
998+
return False
999+
1000+
lower_bound = 10**self.estimator.min_logc
1001+
upper_bound = 10**self.estimator.max_logc
1002+
if (od_fused_value <= lower_bound * 1.001) or (od_fused_value >= upper_bound * 0.999):
1003+
now = time()
1004+
if self._last_bound_warning_at is None or (now - self._last_bound_warning_at) > 300:
1005+
self._last_bound_warning_at = now
1006+
return True
1007+
return False
1008+
9821009

9831010
class ODReader(BackgroundJob):
9841011
"""

core/pioreactor/calibrations/protocols/od_fusion_standards.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ def render(self, ctx: SessionContext) -> CalibrationStep:
300300
f"Place standard vial {standard_index}",
301301
(
302302
f"Place standard vial {standard_index} with a stir bar into the Pioreactor. "
303-
"We will take four readings, then you will remove it."
303+
"We will take OD readings, then you will remove it."
304304
),
305305
)
306306
step.metadata = {

core/pioreactor/web/api.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,6 +1427,15 @@ def get_all_active_calibrations(pioreactor_unit: str) -> DelayedResponseReturnVa
14271427
return create_task_response(task)
14281428

14291429

1430+
@api_bp.route("/workers/<pioreactor_unit>/active_estimators", methods=["GET"])
1431+
def get_all_active_estimators(pioreactor_unit: str) -> DelayedResponseReturnValue:
1432+
if pioreactor_unit == UNIVERSAL_IDENTIFIER:
1433+
task = broadcast_get_across_workers("/unit_api/active_estimators")
1434+
else:
1435+
task = tasks.multicast_get("/unit_api/active_estimators", [pioreactor_unit])
1436+
return create_task_response(task)
1437+
1438+
14301439
@api_bp.route("/workers/<pioreactor_unit>/zipped_calibrations", methods=["GET"])
14311440
def get_all_calibrations_as_yamls(pioreactor_unit: str) -> ResponseReturnValue:
14321441
if pioreactor_unit == UNIVERSAL_IDENTIFIER:

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.ab57c232.js",
4+
"main.js": "/static/static/js/main.12bd0d12.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.ab57c232.js.map": "/static/static/js/main.ab57c232.js.map"
33+
"main.12bd0d12.js.map": "/static/static/js/main.12bd0d12.js.map"
3434
},
3535
"entrypoints": [
3636
"static/css/main.9c7a48b7.css",
37-
"static/js/main.ab57c232.js"
37+
"static/js/main.12bd0d12.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.ab57c232.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.12bd0d12.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.ab57c232.js renamed to core/pioreactor/web/static/static/js/main.12bd0d12.js

Lines changed: 19 additions & 19 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.ab57c232.js.LICENSE.txt renamed to core/pioreactor/web/static/static/js/main.12bd0d12.js.LICENSE.txt

File renamed without changes.

core/pioreactor/web/static/static/js/main.ab57c232.js.map renamed to core/pioreactor/web/static/static/js/main.12bd0d12.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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,34 @@ def get_all_active_calibrations() -> ResponseReturnValue:
839839
return attach_cache_control(jsonify(all_calibrations), max_age=10)
840840

841841

842+
@unit_api_bp.route("/active_estimators", methods=["GET"])
843+
def get_all_active_estimators() -> ResponseReturnValue:
844+
estimator_dir = ESTIMATOR_PATH
845+
846+
if not estimator_dir.exists():
847+
return attach_cache_control(jsonify({}), max_age=10)
848+
849+
all_estimators: dict[str, dict] = {}
850+
851+
with local_persistent_storage("active_estimators") as cache:
852+
for device in cache.iterkeys():
853+
estimator_name = cache[device]
854+
estimator_file_path = estimator_dir / device / f"{estimator_name}.yaml"
855+
if not estimator_file_path.exists():
856+
continue
857+
try:
858+
estimator = to_builtins(yaml_decode(estimator_file_path.read_bytes(), type=AllEstimators))
859+
estimator["is_active"] = True
860+
estimator["pioreactor_unit"] = HOSTNAME
861+
all_estimators[device] = estimator
862+
except Exception as e:
863+
publish_to_error_log(
864+
f"Error reading {estimator_file_path.stem}: {e}", "get_all_active_estimators"
865+
)
866+
867+
return attach_cache_control(jsonify(all_estimators), max_age=10)
868+
869+
842870
@unit_api_bp.route("/zipped_calibrations", methods=["GET"])
843871
def get_all_calibrations_as_zipped_yaml() -> ResponseReturnValue:
844872
calibration_dir = CALIBRATION_PATH

0 commit comments

Comments
 (0)