|
|
|
|
@ -494,6 +494,96 @@ async def get_simulation_report(db_session: DbSession, simulation_id: str):
|
|
|
|
|
headers=headers,
|
|
|
|
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/forecast/eaf", response_model=StandardResponse[dict])
|
|
|
|
|
async def get_forecast_eaf(
|
|
|
|
|
db_session: DbSession,
|
|
|
|
|
time_range: str = Query(..., alias="range", regex="^(weekly|monthly)$"),
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
Generate a forecasted EAF value for the next week or month.
|
|
|
|
|
Calculates offset from 'Last Overhaul Date' in master data.
|
|
|
|
|
"""
|
|
|
|
|
from src.dashboard_model.service import _get_or_create_master_data
|
|
|
|
|
from .schema import SimulationInput
|
|
|
|
|
|
|
|
|
|
# 1. Get current time
|
|
|
|
|
now = datetime.now()
|
|
|
|
|
|
|
|
|
|
# 2. Determine Last Overhaul Date from Master Data
|
|
|
|
|
oh_record = await _get_or_create_master_data(db_session, "Last Overhaul Date", 0)
|
|
|
|
|
|
|
|
|
|
# If not set, use a fallback (e.g., start of current year)
|
|
|
|
|
if not oh_record.value_string:
|
|
|
|
|
oh_date_str = f"{now.year}-01-01T00:00:00"
|
|
|
|
|
else:
|
|
|
|
|
oh_date_str = oh_record.value_string
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
oh_date = datetime.fromisoformat(oh_date_str)
|
|
|
|
|
except Exception:
|
|
|
|
|
oh_date = datetime(now.year, 1, 1)
|
|
|
|
|
|
|
|
|
|
# 3. Calculate Age in Hours (Offset)
|
|
|
|
|
# Using the helper from utils.py
|
|
|
|
|
offset_hours = hours_between(oh_date, now)
|
|
|
|
|
|
|
|
|
|
# 4. Define Forecast Window
|
|
|
|
|
duration = 168 if time_range == "weekly" else 720
|
|
|
|
|
|
|
|
|
|
# 5. Create Simulation Request
|
|
|
|
|
sim_in = SimulationInput(
|
|
|
|
|
SchematicName="- TJB - Unit 3 -",
|
|
|
|
|
SimulationName=f"Simulation{time_range}_{now.strftime('%Y%m%d')}",
|
|
|
|
|
SimDuration=duration,
|
|
|
|
|
DurationUnit="UHour",
|
|
|
|
|
OffSet=offset_hours,
|
|
|
|
|
SimSeed=1,
|
|
|
|
|
SimNumRun=1,
|
|
|
|
|
IsDefault=False
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# 6. Run simulation (blocking call to engine)
|
|
|
|
|
try:
|
|
|
|
|
raw_result = await execute_simulation(
|
|
|
|
|
db_session=db_session,
|
|
|
|
|
sim_data=sim_in.model_dump(),
|
|
|
|
|
is_saved=False
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# 7. Extract Root Node Results
|
|
|
|
|
node_results = raw_result.get("nodeResultOuts", [])
|
|
|
|
|
plant_result = next((r for r in node_results if r.get("nodeName") == "- TJB - Unit 3 -"), None)
|
|
|
|
|
|
|
|
|
|
if not plant_result:
|
|
|
|
|
# Fallback to any schematic result if name doesn't match perfectly
|
|
|
|
|
plant_result = next((r for r in node_results if r.get("nodeType") == "Schematic"), None)
|
|
|
|
|
|
|
|
|
|
if not plant_result:
|
|
|
|
|
raise HTTPException(status_code=500, detail="Plant node results not found in simulation output")
|
|
|
|
|
|
|
|
|
|
# 8. Return predicted metrics
|
|
|
|
|
eaf = plant_result.get("eaf")
|
|
|
|
|
total_dt = plant_result.get("totalDowntime", 0)
|
|
|
|
|
total_ut = plant_result.get("totalUpTime", 0)
|
|
|
|
|
efor = (total_dt / (total_dt + total_ut)) * 100 if (total_dt + total_ut) > 0 else 0
|
|
|
|
|
|
|
|
|
|
return StandardResponse(
|
|
|
|
|
data={
|
|
|
|
|
"range": time_range,
|
|
|
|
|
"eaf_predicted": float(eaf) if eaf is not None else 0.0,
|
|
|
|
|
"efor_predicted": float(efor),
|
|
|
|
|
"reliability_age_hours": offset_hours,
|
|
|
|
|
"sim_duration_hours": duration,
|
|
|
|
|
"reference_oh_date": oh_date_str
|
|
|
|
|
},
|
|
|
|
|
message="Forecast generated successfully"
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
import logging
|
|
|
|
|
logging.getLogger(__name__).error(f"Forecast failed: {str(e)}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Simulation failed: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|