diff --git a/src/aeros_simulation/ramp_engine.py b/src/aeros_simulation/ramp_engine.py index 2e19f84..b9886ed 100644 --- a/src/aeros_simulation/ramp_engine.py +++ b/src/aeros_simulation/ramp_engine.py @@ -323,6 +323,13 @@ def _availability_percent(value: Any) -> float: return availability +def _availability_fraction(value: Any) -> float: + availability = _as_float(value, 0) or 0 + if abs(availability) > 1: + availability /= 100 + return availability + + def _get_number(data: dict, keys: list[str], default: float = 0) -> float: for key in keys: if key in data and data[key] is not None: @@ -332,6 +339,11 @@ def _get_number(data: dict, keys: list[str], default: float = 0) -> float: return default +def _result_number(value: Any, default: float = 0) -> float: + parsed = _as_float(value) + return default if parsed is None else parsed + + def _ramp_model_id_from_sim_data(sim_data: dict) -> str | None: model_id = ( sim_data.get("RampModelId") @@ -527,9 +539,11 @@ def _calc_result( total_uptime = _get_number(metrics, ["total_uptime", "system_uptime", "uptime"]) total_downtime = _get_number(metrics, ["total_downtime", "system_downtime", "downtime"]) period_hours = total_uptime + total_downtime - availability = _availability_percent(_first_present(metrics, "availability", "system_availability", "production_availability")) + availability_source = _first_present(metrics, "availability", "system_availability", "production_availability") + availability = _availability_fraction(availability_source) if not availability and period_hours: - availability = total_uptime / period_hours * 100 + availability = total_uptime / period_hours + eaf = _availability_percent(availability) efor = total_downtime / period_hours * 100 if period_hours else 0 production = _get_number(metrics, ["production", "total_productions", "total_production", "actual_production"]) @@ -571,16 +585,19 @@ def _calc_result( stg_output=0, average_level=0, potential_production=ideal_production, - eaf=availability, + eaf=eaf, efor=efor, derating_hours=effective_loss, - beta=reliability.get("beta"), - eta=reliability.get("eta"), - mttr=reliability.get("mttr"), - parameters=parameters or reliability.get("parameters"), + beta=_result_number(reliability.get("beta")), + eta=_result_number(reliability.get("eta")), + mttr=int(_result_number(reliability.get("mttr"))), + parameters=parameters or reliability.get("parameters") or {}, contribution=_get_number(metrics, ["contribution"]), criticality=_get_number(metrics, ["criticality"]), contribution_factor=_get_number(metrics, ["contribution_factor"]), + total_mo_downtime=_get_number(metrics, ["total_mo_downtime"]), + total_po_downtime=_get_number(metrics, ["total_po_downtime"]), + sof=_get_number(metrics, ["sof"]), ) diff --git a/src/aeros_simulation/schema.py b/src/aeros_simulation/schema.py index f8533bb..00adeb8 100644 --- a/src/aeros_simulation/schema.py +++ b/src/aeros_simulation/schema.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import List, Optional from uuid import UUID -from pydantic import Field +from pydantic import Field, field_validator from src.database.schema import CommonParams from src.database.service import CommonParameters @@ -86,6 +86,20 @@ class SimulationCalc(DefultBase): contribution_factor: Optional[float] sof: Optional[float] + @field_validator( + "eaf_konkin", + "efor", + "derating_hours", + "contribution", + "criticality", + "contribution_factor", + "sof", + mode="before", + ) + @classmethod + def default_numeric_compat_fields(cls, value): + return 0 if value is None else value + class SimulationPlot(DefultBase): id: UUID max_flow_rate: float diff --git a/tests/unit/test_ramp_engine.py b/tests/unit/test_ramp_engine.py index 2ddc489..2e2facb 100644 --- a/tests/unit/test_ramp_engine.py +++ b/tests/unit/test_ramp_engine.py @@ -8,6 +8,7 @@ from src.aeros_simulation.ramp_engine import ( build_ramp_distribution_overrides, selected_simulation_engine, ) +from src.aeros_simulation.schema import SimulationCalc def test_selected_simulation_engine_defaults_to_aeros(): @@ -83,7 +84,70 @@ def test_ramp_calc_result_uses_numeric_defaults_for_aeros_only_fields(): assert calc.stg_input == 0 assert calc.stg_output == 0 assert calc.average_level == 0 - assert calc.availability == 95.0 + assert calc.availability == 0.95 + assert calc.eaf == 95.0 + assert calc.sof == 0 + assert calc.total_mo_downtime == 0 + assert calc.total_po_downtime == 0 + + +def test_simulation_calc_response_defaults_nullable_numbers_for_fe(): + payload = { + "id": uuid4(), + "total_downtime": 5, + "total_uptime": 95, + "num_events": 2, + "production": 10, + "production_std": 0, + "ideal_production": 12, + "availability": 0.95, + "efficiency": 83.3, + "effective_loss": 2, + "num_cm": 2, + "cm_waiting_time": 0, + "total_cm_downtime": 5, + "num_pm": 0, + "total_pm_downtime": 0, + "num_ip": 0, + "total_ip_downtime": 0, + "num_oh": 0, + "total_oh_downtime": 0, + "t_wait_for_crew": 0, + "t_wait_for_spare": 0, + "duration_at_full": 95, + "duration_above_hh": 0, + "duration_above_h": 0, + "duration_below_l": 0, + "duration_below_ll": 0, + "duration_at_empty": 5, + "stg_input": 0, + "eaf": 95, + "eaf_konkin": None, + "efor": None, + "derating_hours": None, + "aeros_node": { + "id": uuid4(), + "node_type": "SchematicNode", + "node_id": 1, + "node_name": "- TJB - Unit 3 -", + "structure_name": "- TJB - Unit 3 -", + "schematic_name": "- TJB - Unit 3 -", + "schematic_id": uuid4(), + "model_image": [], + "equipment": None, + }, + "contribution": None, + "criticality": None, + "contribution_factor": None, + "sof": None, + } + + calc = SimulationCalc.model_validate(payload) + + assert calc.eaf_konkin == 0 + assert calc.efor == 0 + assert calc.derating_hours == 0 + assert calc.sof == 0 def test_build_ramp_distribution_overrides_from_rbd_reliability():