From 0d7010aff70454a94689f656ee0806868600f130 Mon Sep 17 00:00:00 2001 From: Cizz22 Date: Mon, 29 Jun 2026 12:42:36 +0700 Subject: [PATCH] feat: add forecast trending endpoint and ForecastTrendPoint model for simulation data --- src/aeros_simulation/router.py | 37 ++++++++++++++++++++---- src/aeros_simulation/schema.py | 10 +++++++ src/aeros_simulation/service.py | 50 ++++++++++++++++++++++++++++----- 3 files changed, 84 insertions(+), 13 deletions(-) diff --git a/src/aeros_simulation/router.py b/src/aeros_simulation/router.py index d027849..4ad4c5c 100644 --- a/src/aeros_simulation/router.py +++ b/src/aeros_simulation/router.py @@ -24,6 +24,7 @@ from src.aeros_project.service import get_project from temporal.workflow import SimulationWorkflow from .schema import ( AhmMetricInput, + ForecastTrendPoint, SimulationCalcResult, SimulationCalcResultQuery, SimulationInput, @@ -42,6 +43,7 @@ from .service import ( get_all, get_custom_parameters, get_default_simulation, + get_forecast_trending, get_simulation_by_id, get_simulation_with_calc_result, get_simulation_with_plot_result, @@ -171,11 +173,10 @@ async def run_yearly_simulation( overhaul_duration = 1200 - if next_overhaul: + if next_overhaul and last_overhaul: next_oh_start = date_to_utc(next_overhaul["start_date"]) next_oh_duration_hours = next_overhaul["duration_oh"] * 24 - overhaul_interval = int( (next_oh_start - last_oh_dt).total_seconds() // 3600 ) @@ -523,12 +524,15 @@ async def get_forecast_eaf( oh_date_str = oh_record.value_string try: - # Ensure oh_date is naive oh_date = datetime.fromisoformat(oh_date_str).replace(tzinfo=None) - next_oh_date = datetime.fromisoformat(next_oh.value_string).replace(tzinfo=None) except Exception: oh_date = datetime(now.year, 1, 1).replace(tzinfo=None) + try: + next_oh_date = datetime.fromisoformat(next_oh.value_string).replace(tzinfo=None) + except Exception: + next_oh_date = oh_date + timedelta(hours=8760 * 2) # fallback: 2 years after last OH + oh_interval = hours_between(oh_date, next_oh_date) duration = 168 if time_range == "weekly" else 720 end_date = now + timedelta(hours=duration) @@ -579,8 +583,29 @@ async def get_forecast_eaf( data=str(simulation_id), message=forecast_msg ) - - + + +@router.get("/forecast/trending", response_model=StandardResponse[List[ForecastTrendPoint]]) +async def get_forecast_trending_endpoint( + db_session: DbSession, + time_range: str = Query(..., alias="range", regex="^(weekly|monthly)$"), + limit: int = Query(12, ge=1, le=52), +): + """ + Return EAF/EFOR/SOF/EDH trending for the last N completed weekly or monthly + forecast simulations, sorted oldest → newest for chart rendering. + + Query params: + range — "weekly" or "monthly" + limit — max data points (default 12, max 52) + """ + results = await get_forecast_trending( + db_session=db_session, time_range=time_range, limit=limit + ) + return StandardResponse( + data=results, + message=f"{time_range.capitalize()} EAF/EFOR trending retrieved ({len(results)} points)", + ) @router.get("/custom_parameters", response_model=StandardResponse[list]) diff --git a/src/aeros_simulation/schema.py b/src/aeros_simulation/schema.py index f29e605..7804daa 100644 --- a/src/aeros_simulation/schema.py +++ b/src/aeros_simulation/schema.py @@ -130,6 +130,16 @@ class YearlySimulationInput(DefultBase): year: int +class ForecastTrendPoint(DefultBase): + date: Optional[datetime] = None + simulation_id: str + simulation_name: str + eaf: Optional[float] = None + efor: Optional[float] = None + sof: Optional[float] = None + edh: Optional[float] = None + + class SimulationQueryModel(CommonParams): status: Optional[str] = Field(None, max_length=20) diff --git a/src/aeros_simulation/service.py b/src/aeros_simulation/service.py index 7f467d4..fed0044 100644 --- a/src/aeros_simulation/service.py +++ b/src/aeros_simulation/service.py @@ -526,9 +526,7 @@ async def process_calc_results_streaming( # Parse nodeResultOuts array incrementally calc_results = ijson.items(response_stream, 'nodeResultOuts.item') - - raise Exception(calc_results) - + async for result in calc_results: calc_obj = await create_calc_result_object( simulation_id, result, available_nodes, eq_update, db_session @@ -843,10 +841,7 @@ async def save_simulation_result( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) ) - # db_session.add_all(calc_objects) - # db_session.add_all(plot_objects) - - raise Exception(plot_result) + db_session.add_all(plot_objects) simulation = await get_simulation_by_id( db_session=db_session, simulation_id=simulation_id @@ -1045,3 +1040,44 @@ async def update_simulation(*, db_session: DbSession, simulation_id: UUID, data: query = update(AerosSimulation).where(AerosSimulation.id == simulation_id).values(**data) await db_session.execute(query) await db_session.commit() + + +async def get_forecast_trending( + *, db_session: DbSession, time_range: str, limit: int = 12 +) -> list: + """ + Return EAF/EFOR/SOF/EDH for the last `limit` completed weekly or monthly + forecast simulations, oldest first (for chronological chart rendering). + """ + name_prefix = f"Simulation_{time_range}_" + plant_node_name = "- TJB - Unit 3 -" + + query = ( + select(AerosSimulation, AerosSimulationCalcResult) + .join( + AerosSimulationCalcResult, + AerosSimulationCalcResult.aeros_simulation_id == AerosSimulation.id, + ) + .join(AerosNode, AerosNode.id == AerosSimulationCalcResult.aeros_node_id) + .where(AerosSimulation.status == "completed") + .where(AerosSimulation.simulation_name.like(f"{name_prefix}%")) + .where(AerosNode.node_name == plant_node_name) + .order_by(AerosSimulation.completed_at.desc()) + .limit(limit) + ) + + result = await db_session.execute(query) + rows = result.all() + + return [ + { + "date": sim.completed_at, + "simulation_id": str(sim.id), + "simulation_name": sim.simulation_name, + "eaf": calc.eaf, + "efor": calc.efor, + "sof": calc.sof, + "edh": calc.derating_hours, + } + for sim, calc in reversed(rows) # oldest → newest for chart x-axis + ]