diff --git a/src/aeros_simulation/router.py b/src/aeros_simulation/router.py index 36f57e6..2c835df 100644 --- a/src/aeros_simulation/router.py +++ b/src/aeros_simulation/router.py @@ -14,6 +14,7 @@ from .schema import ( SimulationCalcResult, SimulationInput, SimulationPagination, + SimulationPlot, SimulationPlotResult, SimulationCalc, SimulationData, @@ -144,7 +145,7 @@ async def get_simulation_result_plant(db_session: DbSession, simulation_id): @router.get( "/result/plot/{simulation_id}", - response_model=StandardResponse[SimulationPlotResult], + response_model=StandardResponse[List[SimulationPlot]], ) async def get_simulation_result_plot(db_session: DbSession, simulation_id): """Get simulation result.""" @@ -157,6 +158,24 @@ async def get_simulation_result_plot(db_session: DbSession, simulation_id): "status": "success", "message": "Simulation result retrieved successfully", } + + +@router.get( + "/result/plot/{simulation_id}/{node_id}", + response_model=StandardResponse[SimulationPlot], +) +async def get_simulation_result_plot_per_node(db_session: DbSession, simulation_id, node_id): + """Get simulation result.""" + simulation_result = await get_simulation_with_plot_result( + db_session=db_session, simulation_id=simulation_id, node_id=node_id + ) + + + return { + "data": simulation_result, + "status": "success", + "message": "Simulation result retrieved successfully", + } @router.get("/result/ranking/{simulation_id}", response_model=StandardResponse[List[SimulationRankingParameters]]) async def get_simulation_result_ranking(db_session: DbSession, simulation_id): diff --git a/src/aeros_simulation/service.py b/src/aeros_simulation/service.py index 40d8aaa..85cdf53 100644 --- a/src/aeros_simulation/service.py +++ b/src/aeros_simulation/service.py @@ -26,7 +26,7 @@ from .model import ( ) from src.aeros_equipment.model import AerosEquipment, AerosEquipmentCustomParameterData from src.aeros_equipment.schema import EquipmentWithCustomParameters -from .schema import SimulationInput, SimulationRankingParameters +from .schema import SimulationInput, SimulationPlotResult, SimulationRankingParameters from .utils import calculate_eaf client = httpx.AsyncClient(timeout=300.0) @@ -232,27 +232,56 @@ async def get_result_ranking(*, db_session: DbSession, simulation_id: UUID): async def get_simulation_with_plot_result( - *, db_session: DbSession, simulation_id: UUID, node_type: Optional[str] = None + *, db_session: DbSession, simulation_id: UUID, node_type: Optional[str] = None, node_id: Optional[str] = None ): """Get a simulation by id.""" - query = ( - select(AerosSimulation) - .where(AerosSimulation.id == simulation_id) - .options( - selectinload(AerosSimulation.plot_results).options( - selectinload(AerosSimulationPlotResult.aeros_node) - ) - ) - ) - - if node_type: - query = query.join( - AerosNode, AerosNode.id == AerosSimulation.plot_results.aeros_node_id - ).filter(AerosNode.node_type == node_type) - - simulation = await db_session.execute(query) - return simulation.scalar() + # query = ( + # select(AerosSimulation) + # .where(AerosSimulation.id == simulation_id) + # .options( + # selectinload(AerosSimulation.plot_results).options( + # selectinload(AerosSimulationPlotResult.aeros_node) + # ) + # ) + # ) + + # if node_type: + # query = query.join( + # AerosNode, AerosNode.id == AerosSimulation.plot_results.aeros_node_id + # ).filter(AerosNode.node_type == node_type) + + # if node_id: + # if node_id == 'plant': + # query = query.join( + # AerosNode, AerosNode.id == AerosSimulation.plot_results.aeros_node_id + # ).filter(AerosNode.node_name == '- TJB - Unit 3 -') + # else: + # query = query.join( + # AerosNode, AerosNode.id == AerosSimulation.plot_results.aeros_node_id + # ).filter(AerosNode.id == node_id) + + # simulation = await db_session.execute(query) + # return simulation.scalar() + + query = select(AerosSimulationPlotResult).where( + AerosSimulationPlotResult.aeros_simulation_id == simulation_id + ).options(selectinload(AerosSimulationPlotResult.aeros_node)) + + if node_id: + if node_id == 'plant': + query = query.join( + AerosNode, AerosNode.id == AerosSimulationPlotResult.aeros_node_id + ).filter(AerosNode.node_name == '- TJB - Unit 3 -') + else: + query = query.join( + AerosNode, AerosNode.id == AerosSimulationPlotResult.aeros_node_id + ).filter(AerosNode.id == node_id) + + res = await db_session.execute(query) + return res.scalar_one_or_none() + simulation_plots = await db_session.execute(query) + return simulation_plots.scalars().all() async def get_calc_result_by( *, db_session: DbSession, simulation_id: UUID, node_name: Optional[str] = None @@ -548,9 +577,33 @@ async def save_simulation_result( } calc_objects = [] plot_objects = [] + + try: + for result in plot_result: + node_type = "RegularNode" if result["nodeType"] == "RegularNode" else "SchematicNode" + node = avaiable_nodes.get(f"{node_type}:{result['nodeName']}", None) + if not node: + if result["nodeType"] != "RegularNode" and result["nodeType"] != "Schematic": + continue + node = await get_or_save_node( + db_session=db_session, node_data=result, type="plot" + ) + + plot_result = AerosSimulationPlotResult( + aeros_simulation_id=simulation_id, + aeros_node_id=node.id, + max_flow_rate=result["maxFlowrate"], + storage_capacity=result["storageCapacity"], + point_availabilities=result["pointAvailabilities"], + point_flowrates=result["pointFlowrates"], + timestamp_outs=result["timeStampOuts"], + ) + + plot_objects.append(plot_result) + for result in calc_result: node_type = "RegularNode" if result["nodeType"] == "RegularNode" else "SchematicNode" node = avaiable_nodes.get(f"{node_type}:{result['nodeName']}", None) @@ -561,6 +614,9 @@ async def save_simulation_result( "mttr": 0, "parameters": {} }) + + plot_data = next(plot for plot in plot_objects if plot.aeros_node_id == node.id) if node else {} + if not node: @@ -574,7 +630,9 @@ async def save_simulation_result( available_hours=result["totalUpTime"], period_hours=result["totalUpTime"] + result["totalDowntime"], actual_production=result["production"], - ideal_production=result["idealProduction"] + ideal_production=result["idealProduction"], + downtime_hours = result["totalDowntime"], + plot_data=plot_data ) efor = (result["totalDowntime"] / (result["totalDowntime"] + result["totalUpTime"]))*100 if (result["totalDowntime"] + result["totalUpTime"]) > 0 else 0 @@ -623,27 +681,7 @@ async def save_simulation_result( calc_objects.append(calc_result) - for result in plot_result: - node_type = "RegularNode" if result["nodeType"] == "RegularNode" else "SchematicNode" - node = avaiable_nodes.get(f"{node_type}:{result['nodeName']}", None) - if not node: - if result["nodeType"] != "RegularNode" and result["nodeType"] != "Schematic": - continue - node = await get_or_save_node( - db_session=db_session, node_data=result, type="plot" - ) - - plot_result = AerosSimulationPlotResult( - aeros_simulation_id=simulation_id, - aeros_node_id=node.id, - max_flow_rate=result["maxFlowrate"], - storage_capacity=result["storageCapacity"], - point_availabilities=result["pointAvailabilities"], - point_flowrates=result["pointFlowrates"], - timestamp_outs=result["timeStampOuts"], - ) - - plot_objects.append(plot_result) + except Exception as e: simulation = await get_simulation_by_id( diff --git a/src/aeros_simulation/utils.py b/src/aeros_simulation/utils.py index cacf278..2f8dd62 100644 --- a/src/aeros_simulation/utils.py +++ b/src/aeros_simulation/utils.py @@ -2,7 +2,9 @@ def calculate_eaf( available_hours: float, period_hours: float, actual_production: float, - ideal_production: float + ideal_production: float, + downtime_hours, + plot_data ): """ Calculate EAF using the time-based method from PLN document @@ -20,18 +22,19 @@ def calculate_eaf( """ try: - # Calculate lost production - lost_production = ideal_production - actual_production - max_capacity = 660 - # Calculate total equivalent derate hours - total_equivalent_derate_hours = lost_production / max_capacity - - # Calculate EAF - effective_available_hours = available_hours - total_equivalent_derate_hours - return (effective_available_hours / period_hours) * 100 if period_hours > 0 else 0, total_equivalent_derate_hours + # Calculate lost production + lost_production = ideal_production - actual_production + max_capacity = plot_data.max_flow_rate + # Calculate total equivalent derate and outage hours + total_equivalent_derate_and_outage_hours = lost_production / max_capacity if max_capacity > 0 else 0 + total_equivalent_derate = total_equivalent_derate_and_outage_hours - downtime_hours + + # Calculate EAF + effective_available_hours = available_hours - total_equivalent_derate + return (effective_available_hours / period_hours) * 100 if period_hours > 0 else 0, total_equivalent_derate except Exception as e: - print("Error calculating EAF:", str(e)) - raise + print("Error calculating EAF:", e) + raise # def calculate_efor( # forced_outage_hours: float, @@ -54,3 +57,33 @@ def calculate_eaf( # numerator_efor = forced_outage_hours + equivalent_forced_derate_hours # denominator_efor = forced_outage_hours + service_hours + synchronous_hours + equivalent_forced_derate_hours_rs # return (numerator_efor / denominator_efor * 100) if denominator_efor > 0 else 0 + + + +def calculate_derating(data_list, max_flow_rate: float = 660) -> float: + """ + Calculate total time when flow rate is below maximum. + + Method 2: Time intervals AFTER each measurement point. + This assumes each data point represents the start of a period with that flow rate. + """ + # Sort data by cumulative time to ensure proper order + sorted_data = sorted(data_list, key=lambda x: x['cumulativeTime']) + + total_time_below_max = 0.0 + + print("=== Method 2: Time intervals AFTER each measurement ===") + + for i in range(len(sorted_data) - 1): + current_data = sorted_data[i] + next_data = sorted_data[i + 1] + + # If current flow rate is below max, add this time interval + if current_data['flowRate'] < max_flow_rate and current_data['flowRate'] != 0: + # Time interval until next measurement + time_interval = next_data['cumulativeTime'] - current_data['cumulativeTime'] + + total_time_below_max += time_interval + print(f"Derating hours: {time_interval:.2f}") + + return total_time_below_max \ No newline at end of file