feat: enhance forecast trending retrieval with improved simulation querying and fallback logic

main
Cizz22 1 week ago
parent dd8fe1ecd7
commit 042e11b06a

@ -1046,42 +1046,35 @@ async def get_forecast_trending(
*, db_session: DbSession, time_range: str, limit: int = 12 *, db_session: DbSession, time_range: str, limit: int = 12
) -> list: ) -> list:
""" """
Return EAF/EFOR/SOF/EDH for the last `limit` completed weekly or monthly Return EAF/EFOR/SOF/EDH for the last `limit` completed simulations.
forecast simulations, oldest first (for chronological chart rendering).
Strategy: Strategy:
1. Try simulations named Simulation_weekly_* / Simulation_monthly_* (scheduled forecasts). 1. Try simulations named Simulation_weekly_* / Simulation_monthly_* (forecast schedule).
2. If fewer than `limit` found, fall back to any completed simulation, grouped 2. If fewer than `limit` found, fall back to any completed simulation grouped by
by week or month bucket (latest sim per bucket), so the chart is populated week/month bucket so the chart is always populated.
even before automated forecast simulations exist.
Each simulation's plant metrics are fetched via get_plant_calc_result (same path the
dashboard uses) rather than a JOIN, avoiding silent row-drops when CalcResults aren't
yet committed under the plant node name.
""" """
plant_node_name = "- TJB - Unit 3 -"
name_prefix = f"Simulation_{time_range}_" name_prefix = f"Simulation_{time_range}_"
base_join = ( async def _sims_query(extra_where=None):
select(AerosSimulation, AerosSimulationCalcResult) q = (
.join( select(AerosSimulation)
AerosSimulationCalcResult,
AerosSimulationCalcResult.aeros_simulation_id == AerosSimulation.id,
)
.join(AerosNode, AerosNode.id == AerosSimulationCalcResult.aeros_node_id)
.where(AerosSimulation.status == "completed") .where(AerosSimulation.status == "completed")
.where(AerosNode.node_name == plant_node_name)
)
# First pass: named forecast simulations
named_q = (
base_join
.where(AerosSimulation.simulation_name.like(f"{name_prefix}%"))
.order_by(AerosSimulation.completed_at.desc()) .order_by(AerosSimulation.completed_at.desc())
.limit(limit)
) )
result = await db_session.execute(named_q) if extra_where is not None:
named_rows = result.all() q = q.where(extra_where)
return q
def _format(rows): async def _fetch_plant(sim) -> dict | None:
return [ try:
{ calc = await get_plant_calc_result(db_session=db_session, simulation_id=sim.id)
if calc is None:
return None
return {
"date": sim.completed_at, "date": sim.completed_at,
"simulation_id": str(sim.id), "simulation_id": str(sim.id),
"simulation_name": sim.simulation_name, "simulation_name": sim.simulation_name,
@ -1090,32 +1083,48 @@ async def get_forecast_trending(
"sof": calc.sof, "sof": calc.sof,
"edh": calc.derating_hours, "edh": calc.derating_hours,
} }
for sim, calc in rows except Exception:
] return None
if len(named_rows) >= limit:
return _format(reversed(named_rows))
# Fallback: any completed sim, group by week/month bucket (latest per bucket) # --- Pass 1: named forecast simulations ---
fallback_q = ( named_q = await _sims_query(
base_join AerosSimulation.simulation_name.like(f"{name_prefix}%")
.order_by(AerosSimulation.completed_at.desc())
.limit(limit * 20)
) )
named_q = named_q.limit(limit)
result = await db_session.execute(named_q)
named_sims = result.scalars().all()
points = []
for sim in reversed(named_sims): # oldest → newest
pt = await _fetch_plant(sim)
if pt:
points.append(pt)
if len(points) >= limit:
return points
# --- Pass 2: any completed sim, bucket by week/month ---
fallback_q = await _sims_query()
fallback_q = fallback_q.limit(limit * 20)
result = await db_session.execute(fallback_q) result = await db_session.execute(fallback_q)
all_rows = result.all() all_sims = result.scalars().all()
buckets: dict = {} buckets: dict = {}
for sim, calc in all_rows: for sim in all_sims:
ts = sim.completed_at ts = sim.completed_at
if ts is None: if ts is None:
continue continue
if time_range == "weekly": bucket = ts.strftime("%G-W%V") if time_range == "weekly" else ts.strftime("%Y-%m")
bucket = ts.strftime("%G-W%V") # ISO week
else:
bucket = ts.strftime("%Y-%m")
if bucket not in buckets: if bucket not in buckets:
buckets[bucket] = (sim, calc) buckets[bucket] = sim
sorted_sims = [sim for _, sim in sorted(buckets.items(), key=lambda x: x[0])]
sorted_sims = sorted_sims[-limit:] # keep last `limit` buckets
fallback_points = []
for sim in sorted_sims:
pt = await _fetch_plant(sim)
if pt:
fallback_points.append(pt)
sorted_items = sorted(buckets.items(), key=lambda x: x[0])[-limit:] return fallback_points if fallback_points else points
return _format((sim, calc) for _, (sim, calc) in sorted_items)

Loading…
Cancel
Save