feat: implement optimum time constraint module with new router, service, workflow, and optimizer logic
parent
07711f9876
commit
d51ba8dd8d
@ -0,0 +1,26 @@
|
|||||||
|
import asyncio
|
||||||
|
from src.database.core import engine
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
async def alter_table():
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
try:
|
||||||
|
await conn.execute(text("ALTER TABLE oh_tr_calculation_data ADD COLUMN plant_results JSONB"))
|
||||||
|
print("Added plant_results")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"plant_results exists: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await conn.execute(text("ALTER TABLE oh_tr_calculation_data ADD COLUMN fleet_statistics JSONB"))
|
||||||
|
print("Added fleet_statistics")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"fleet_statistics exists: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await conn.execute(text("ALTER TABLE oh_tr_calculation_data ADD COLUMN analysis_metadata JSONB"))
|
||||||
|
print("Added analysis_metadata")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"analysis_metadata exists: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(alter_table())
|
||||||
@ -0,0 +1,155 @@
|
|||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
with open('/home/atra/Development/be-optimumoh/src/calculation_time_constrains/service.py', 'r') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
# Find the point where it broke
|
||||||
|
# Line 159 is ' \'month_index\': i + 1,\n'
|
||||||
|
# We want to keep up to line 159 (index 158)
|
||||||
|
|
||||||
|
new_lines = lines[:159]
|
||||||
|
new_lines.append(" 'source': source\n")
|
||||||
|
new_lines.append(" }\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" return monthly_data\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" async def get_simulation_results(self, simulation_id: str = \"default\"):\n")
|
||||||
|
new_lines.append(" \"\"\"Get simulation results for Birnbaum importance calculations\"\"\"\n")
|
||||||
|
new_lines.append(" headers = {\n")
|
||||||
|
new_lines.append(" \"Authorization\": f\"Bearer {self.token}\",\n")
|
||||||
|
new_lines.append(" \"Content-Type\": \"application/json\"\n")
|
||||||
|
new_lines.append(" }\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" calc_result_url = f\"{self.api_base_url}/aeros/simulation/result/calc/{simulation_id}?nodetype=RegularNode\"\n")
|
||||||
|
new_lines.append(" plant_result_url = f\"{self.api_base_url}/aeros/simulation/result/calc/{simulation_id}/plant\"\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" async with httpx.AsyncClient(timeout=300.0) as client:\n")
|
||||||
|
new_lines.append(" calc_task = client.get(calc_result_url, headers=headers)\n")
|
||||||
|
new_lines.append(" plant_task = client.get(plant_result_url, headers=headers)\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" calc_response, plant_response = await asyncio.gather(calc_task, plant_task)\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" calc_response.raise_for_status()\n")
|
||||||
|
new_lines.append(" plant_response.raise_for_status()\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" calc_data = calc_response.json()[\"data\"]\n")
|
||||||
|
new_lines.append(" plant_data = plant_response.json()[\"data\"]\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" return {\n")
|
||||||
|
new_lines.append(" \"calc_result\": calc_data,\n")
|
||||||
|
new_lines.append(" \"plant_result\": plant_data\n")
|
||||||
|
new_lines.append(" }\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" def _calculate_equipment_costs_with_spareparts(self, failures_prediction: Dict, birnbaum_importance: float,\n")
|
||||||
|
new_lines.append(" preventive_cost: float, failure_replacement_cost: float, ecs,\n")
|
||||||
|
new_lines.append(" location_tag: str, planned_overhauls: List = None, loss_production_permonth=0) -> List[Dict]:\n")
|
||||||
|
new_lines.append(" \"\"\"Calculate costs for each month including sparepart costs and availability\"\"\"\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" if not failures_prediction:\n")
|
||||||
|
new_lines.append(" self.logger.warning(f\"No failure prediction data for {location_tag}\")\n")
|
||||||
|
new_lines.append(" return []\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" results = []\n")
|
||||||
|
new_lines.append(" months = list(failures_prediction.keys())\n")
|
||||||
|
new_lines.append(" num_months = len(months)\n")
|
||||||
|
new_lines.append(" failure_counts = []\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" monthly_risk_cost_per_failure = 0\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" if ecs:\n")
|
||||||
|
new_lines.append(" is_trip = 1 if ecs.get(\"Diskripsi Operasional Akibat Equip. Failure\") == \"Trip\" else 0\n")
|
||||||
|
new_lines.append(" is_series = 0 if not birnbaum_importance else math.floor(birnbaum_importance)\n")
|
||||||
|
new_lines.append(" if is_trip:\n")
|
||||||
|
new_lines.append(" downtime = ecs.get(\"Estimasi Waktu Maint. / Downtime / Gangguan (Jam)\")\n")
|
||||||
|
new_lines.append(" monthly_risk_cost_per_failure = 660 * 1000000 * is_trip * downtime * is_series\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" for month_key in months:\n")
|
||||||
|
new_lines.append(" data = failures_prediction[month_key]\n")
|
||||||
|
new_lines.append(" failure_counts.append(data['cumulative_failures'])\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" for i in range(num_months):\n")
|
||||||
|
new_lines.append(" month_index = i + 1\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # Use only months within the analysis window\n")
|
||||||
|
new_lines.append(" if month_index > self.time_window_months:\n")
|
||||||
|
new_lines.append(" continue\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" # Check sparepart availability for this month\n")
|
||||||
|
new_lines.append(" sparepart_analysis = self._analyze_sparepart_availability(\n")
|
||||||
|
new_lines.append(" location_tag, month_index - 1, planned_overhauls or []\n")
|
||||||
|
new_lines.append(" )\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # THEORY: Total Expected Cost per Unit Time (CPUT)\n")
|
||||||
|
new_lines.append(" # Reference: Maintenance Optimization Models (Age/Block Replacement)\n")
|
||||||
|
new_lines.append(" # C(T) = [Total Preventive Cost + Total Expected Corrective Cost(T)] / T\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # 1. Total Expected Corrective Cost until month_index (Expected number of failures * cost per failure)\n")
|
||||||
|
new_lines.append(" # In NHPP model, Expected Failures E[N(T)] = Cumulative Failures\n")
|
||||||
|
new_lines.append(" total_expected_failure_cost = failure_counts[i] * (failure_replacement_cost + monthly_risk_cost_per_failure)\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # 2. Total Preventive Cost (One-time cost at month_index)\n")
|
||||||
|
new_lines.append(" # Includes labor, materials, and procurement delays\n")
|
||||||
|
new_lines.append(" procurement_cost = sparepart_analysis['total_procurement_cost']\n")
|
||||||
|
new_lines.append(" total_preventive_cost = preventive_cost + procurement_cost\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # 3. Expected Total Cycle Cost\n")
|
||||||
|
new_lines.append(" total_cycle_cost = total_expected_failure_cost + total_preventive_cost\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # 4. Expected Cost Per Unit Time (Optimization Target)\n")
|
||||||
|
new_lines.append(" # This value forms the U-shaped curve\n")
|
||||||
|
new_lines.append(" cput = total_cycle_cost / month_index\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # Store both absolute and amortized components for visualization\n")
|
||||||
|
new_lines.append(" results.append({\n")
|
||||||
|
new_lines.append(" 'month': month_index,\n")
|
||||||
|
new_lines.append(" 'number_of_failures': failure_counts[i],\n")
|
||||||
|
new_lines.append(" 'is_actual': failures_prediction[months[i]].get('source') == 'actual',\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # Amortized components (for the \"U\" chart)\n")
|
||||||
|
new_lines.append(" 'failure_cost': total_expected_failure_cost / month_index,\n")
|
||||||
|
new_lines.append(" 'preventive_cost': preventive_cost / month_index,\n")
|
||||||
|
new_lines.append(" 'procurement_cost': procurement_cost / month_index,\n")
|
||||||
|
new_lines.append(" 'total_cost': cput,\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # Absolute values (for breakdown analysis)\n")
|
||||||
|
new_lines.append(" 'absolute_failure_cost': total_expected_failure_cost,\n")
|
||||||
|
new_lines.append(" 'absolute_overhaul_cost': preventive_cost,\n")
|
||||||
|
new_lines.append(" 'absolute_procurement_cost': procurement_cost,\n")
|
||||||
|
new_lines.append(" 'total_cycle_cost': total_cycle_cost,\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" 'is_after_planned_oh': month_index > self.planned_oh_months,\n")
|
||||||
|
new_lines.append(" 'delay_months': max(0, month_index - self.planned_oh_months),\n")
|
||||||
|
new_lines.append(" 'procurement_details': sparepart_analysis,\n")
|
||||||
|
new_lines.append(" 'sparepart_available': sparepart_analysis['available'],\n")
|
||||||
|
new_lines.append(" 'sparepart_status': sparepart_analysis['message'],\n")
|
||||||
|
new_lines.append(" 'can_proceed': sparepart_analysis['can_proceed_with_delays']\n")
|
||||||
|
new_lines.append(" })\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" return results\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" def _analyze_sparepart_availability(self, equipment_tag: str, target_month: int, \n")
|
||||||
|
new_lines.append(" planned_overhauls: List) -> Dict:\n")
|
||||||
|
new_lines.append(" \"\"\"Analyze sparepart availability for equipment at target month\"\"\"\n")
|
||||||
|
new_lines.append(" if not self.sparepart_manager:\n")
|
||||||
|
new_lines.append(" return {\n")
|
||||||
|
new_lines.append(" 'available': True,\n")
|
||||||
|
new_lines.append(" 'message': 'Sparepart manager not initialized',\n")
|
||||||
|
new_lines.append(" 'total_procurement_cost': 0,\n")
|
||||||
|
new_lines.append(" 'procurement_needed': [],\n")
|
||||||
|
new_lines.append(" 'can_proceed_with_delays': True\n")
|
||||||
|
new_lines.append(" }\n")
|
||||||
|
new_lines.append(" \n")
|
||||||
|
new_lines.append(" # Convert planned overhauls to format expected by sparepart manager\n")
|
||||||
|
new_lines.append(" other_overhauls = [(eq_tag, month) for eq_tag, month in planned_overhauls\n")
|
||||||
|
new_lines.append(" if eq_tag != equipment_tag and month <= target_month]\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
new_lines.append(" return self.sparepart_manager.check_sparepart_availability(\n")
|
||||||
|
new_lines.append(" equipment_tag, target_month, other_overhauls\n")
|
||||||
|
new_lines.append(" )\n")
|
||||||
|
new_lines.append("\n")
|
||||||
|
|
||||||
|
new_lines.extend(lines[159:])
|
||||||
|
|
||||||
|
with open('/home/atra/Development/be-optimumoh/src/calculation_time_constrains/service.py', 'w') as f:
|
||||||
|
f.writelines(new_lines)
|
||||||
@ -0,0 +1,263 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open('src/calculation_time_constrains/service.py', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Replacement 1: run_simulation_with_spareparts
|
||||||
|
replacement_1 = """ finally:
|
||||||
|
await optimum_oh_model._close_session()
|
||||||
|
|
||||||
|
# Re-fetch calculation_data with equipment_results to ensure they are loaded
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
calculation_query = await db_session.execute(
|
||||||
|
select(CalculationData)
|
||||||
|
.options(selectinload(CalculationData.equipment_results), selectinload(CalculationData.parameter))
|
||||||
|
.where(CalculationData.id == calculation.id)
|
||||||
|
)
|
||||||
|
scope_calculation = calculation_query.scalar_one_or_none()
|
||||||
|
|
||||||
|
data_num = scope_calculation.max_interval
|
||||||
|
all_equipment = scope_calculation.equipment_results
|
||||||
|
included_equipment = [eq for eq in all_equipment if eq.is_included]
|
||||||
|
|
||||||
|
calculation_results = []
|
||||||
|
fleet_statistics = {
|
||||||
|
'total_equipment': len(all_equipment),
|
||||||
|
'included_equipment': len(included_equipment),
|
||||||
|
'excluded_equipment': len(all_equipment) - len(included_equipment),
|
||||||
|
'equipment_with_sparepart_constraints': 0,
|
||||||
|
'total_procurement_items': 0,
|
||||||
|
'critical_procurement_items': 0,
|
||||||
|
'total_spareparts': 745
|
||||||
|
}
|
||||||
|
|
||||||
|
avg_failure_cost = sum((eq.material_cost or 0) + (3 * 111000 * 3) for eq in all_equipment) / len(all_equipment) if all_equipment else 0
|
||||||
|
|
||||||
|
rbd_marginal_fails = [0] * data_num
|
||||||
|
try:
|
||||||
|
if scope_calculation.rbd_simulation_id:
|
||||||
|
from src.config import RBD_SERVICE_API
|
||||||
|
import httpx
|
||||||
|
plant_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{scope_calculation.rbd_simulation_id}/plant"
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.get(
|
||||||
|
plant_result_url,
|
||||||
|
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
plant_data = response.json().get("data", {})
|
||||||
|
timestamp_outs = plant_data.get("timestamp_outs", [])
|
||||||
|
if timestamp_outs:
|
||||||
|
from src.calculation_time_constrains.utils import create_time_series_data, calculate_failures_per_month
|
||||||
|
hourly_data = create_time_series_data(timestamp_outs, max_hours=data_num * 720)
|
||||||
|
cumulative_rbd_fails = calculate_failures_per_month(hourly_data)
|
||||||
|
rbd_fails_map = {m['month']: m['failures'] for m in cumulative_rbd_fails}
|
||||||
|
prev_fail = 0
|
||||||
|
for m in range(1, data_num + 1):
|
||||||
|
curr_fail = rbd_fails_map.get(m, prev_fail)
|
||||||
|
rbd_marginal_fails[m-1] = curr_fail - prev_fail
|
||||||
|
prev_fail = curr_fail
|
||||||
|
except Exception as e:
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.warning(f"Failed to fetch plant simulation: {e}")
|
||||||
|
|
||||||
|
cumulative_plant_failures = 0
|
||||||
|
import numpy as np
|
||||||
|
from .schema import CalculationResultsRead
|
||||||
|
for month_index in range(data_num):
|
||||||
|
historical_marginal_fail = 0
|
||||||
|
for eq in all_equipment:
|
||||||
|
if eq.is_actual and month_index < len(eq.is_actual) and eq.is_actual[month_index]:
|
||||||
|
curr_fail = eq.daily_failures[month_index] if month_index < len(eq.daily_failures) else 0
|
||||||
|
prev_fail = eq.daily_failures[month_index-1] if month_index > 0 and (month_index - 1) < len(eq.daily_failures) else 0
|
||||||
|
historical_marginal_fail += max(0, curr_fail - prev_fail)
|
||||||
|
|
||||||
|
marginal_fail = rbd_marginal_fails[month_index] + historical_marginal_fail
|
||||||
|
cumulative_plant_failures += marginal_fail
|
||||||
|
|
||||||
|
month_result = {
|
||||||
|
"overhaul_cost": 0.0,
|
||||||
|
"corrective_cost": 0.0,
|
||||||
|
"procurement_cost": 0.0,
|
||||||
|
"num_failures": cumulative_plant_failures,
|
||||||
|
"day": month_index + 1,
|
||||||
|
"month": month_index + 1,
|
||||||
|
"procurement_details": {},
|
||||||
|
"sparepart_summary": {
|
||||||
|
"total_procurement_cost": 0.0,
|
||||||
|
"equipment_requiring_procurement": 0,
|
||||||
|
"critical_shortages": 0,
|
||||||
|
"existing_orders_value": 0.0,
|
||||||
|
"new_orders_required": 0,
|
||||||
|
"urgent_procurements": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
equipment_requiring_procurement = 0
|
||||||
|
total_existing_orders_value = 0.0
|
||||||
|
total_new_orders_value = 0.0
|
||||||
|
critical_shortages = 0
|
||||||
|
urgent_procurements = 0
|
||||||
|
|
||||||
|
for eq in all_equipment:
|
||||||
|
if month_index >= len(eq.procurement_details):
|
||||||
|
continue
|
||||||
|
procurement_detail = eq.procurement_details[month_index]
|
||||||
|
if (procurement_detail and isinstance(procurement_detail, dict) and procurement_detail.get("procurement_needed")):
|
||||||
|
equipment_requiring_procurement += 1
|
||||||
|
pr_po_summary = procurement_detail.get("pr_po_summary", {})
|
||||||
|
existing_orders_value = pr_po_summary.get("total_existing_value", 0)
|
||||||
|
total_existing_orders_value += existing_orders_value
|
||||||
|
new_orders_value = pr_po_summary.get("total_new_orders_value", 0)
|
||||||
|
total_new_orders_value += new_orders_value
|
||||||
|
critical_missing = procurement_detail.get("critical_missing_parts", 0)
|
||||||
|
if critical_missing > 0:
|
||||||
|
critical_shortages += 1
|
||||||
|
recommendations = procurement_detail.get("recommendations", [])
|
||||||
|
urgent_items = [r for r in recommendations if r.get("priority") == "CRITICAL"]
|
||||||
|
if urgent_items:
|
||||||
|
urgent_procurements += 1
|
||||||
|
is_included_eq = False if eq.is_initial else eq.is_included
|
||||||
|
month_result["procurement_details"][eq.location_tag] = {
|
||||||
|
"is_included": is_included_eq,
|
||||||
|
"location_tag": eq.location_tag,
|
||||||
|
"details": procurement_detail.get("procurement_needed", []),
|
||||||
|
"detailed_message": procurement_detail.get("detailed_message", ""),
|
||||||
|
"pr_po_summary": pr_po_summary,
|
||||||
|
"recommendations": recommendations,
|
||||||
|
"sparepart_available": procurement_detail.get("sparepart_available", True),
|
||||||
|
"can_proceed": procurement_detail.get("can_proceed_with_delays", True),
|
||||||
|
"critical_missing_parts": critical_missing,
|
||||||
|
"existing_orders_value": existing_orders_value,
|
||||||
|
"new_orders_value": new_orders_value
|
||||||
|
}
|
||||||
|
if eq.is_included:
|
||||||
|
if (month_index < len(eq.overhaul_costs) and month_index < len(eq.procurement_costs)):
|
||||||
|
month_result["overhaul_cost"] += float(eq.overhaul_costs[month_index])
|
||||||
|
month_result["procurement_cost"] += float(eq.procurement_costs[month_index])
|
||||||
|
|
||||||
|
month_result["corrective_cost"] = (cumulative_plant_failures * avg_failure_cost) / (month_index + 1)
|
||||||
|
month_result["sparepart_summary"].update({
|
||||||
|
"total_procurement_cost": month_result["procurement_cost"],
|
||||||
|
"equipment_requiring_procurement": equipment_requiring_procurement,
|
||||||
|
"critical_shortages": critical_shortages,
|
||||||
|
"existing_orders_value": total_existing_orders_value,
|
||||||
|
"new_orders_required": len([eq for eq in all_equipment if month_index < len(eq.procurement_details) and eq.procurement_details[month_index] and eq.procurement_details[month_index].get("procurement_needed")]),
|
||||||
|
"urgent_procurements": urgent_procurements
|
||||||
|
})
|
||||||
|
month_result["total_cost"] = month_result["corrective_cost"] + month_result["overhaul_cost"] + month_result["procurement_cost"]
|
||||||
|
calculation_results.append(month_result)
|
||||||
|
|
||||||
|
optimum_day = np.argmin([month["total_cost"] for month in calculation_results])
|
||||||
|
scope_calculation.optimum_oh_day = int(optimum_day)
|
||||||
|
|
||||||
|
fleet_statistics['equipment_with_sparepart_constraints'] = len([
|
||||||
|
eq for eq in all_equipment if any(detail and detail.get("procurement_needed") for detail in eq.procurement_details if detail)
|
||||||
|
])
|
||||||
|
fleet_statistics['total_procurement_items'] = sum([
|
||||||
|
len(detail.get("procurement_needed", [])) for eq in all_equipment for detail in eq.procurement_details if detail and isinstance(detail, dict)
|
||||||
|
])
|
||||||
|
|
||||||
|
analysis_metadata = {
|
||||||
|
"planned_month": (scope.start_date.year - prev_oh_scope.end_date.year) * 12 + (scope.start_date.month - prev_oh_scope.end_date.month) if prev_oh_scope and scope else 0,
|
||||||
|
"total_fleet_procurement_cost": sum([eq.procurement_costs[int(scope_calculation.optimum_oh_day)] for eq in all_equipment if eq.procurement_costs]),
|
||||||
|
"last_oh_date": prev_oh_scope.end_date.isoformat() if prev_oh_scope else None,
|
||||||
|
"next_oh_date": scope.start_date.isoformat() if scope else None,
|
||||||
|
"optimal_stat": None
|
||||||
|
}
|
||||||
|
|
||||||
|
calc_results_read = [CalculationResultsRead(**r) for r in calculation_results]
|
||||||
|
optimal_analysis = _analyze_optimal_timing(
|
||||||
|
calc_results_read, scope_calculation.optimum_oh_day, prev_oh_scope, scope
|
||||||
|
)
|
||||||
|
|
||||||
|
scope_calculation.plant_results = calculation_results
|
||||||
|
scope_calculation.fleet_statistics = fleet_statistics
|
||||||
|
scope_calculation.analysis_metadata = analysis_metadata
|
||||||
|
scope_calculation.optimum_analysis = optimal_analysis
|
||||||
|
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": calculation.id,
|
||||||
|
"optimum": optimal_analysis
|
||||||
|
}"""
|
||||||
|
|
||||||
|
pattern_1 = re.compile(r" finally:\n await optimum_oh_model\._close_session\(\).*?return \{\n \"id\": calculation_data\.id,\n \"optimum\": stats\n \}", re.DOTALL)
|
||||||
|
if pattern_1.search(content):
|
||||||
|
content = pattern_1.sub(replacement_1, content)
|
||||||
|
else:
|
||||||
|
print("Could not find Replacement 1 target")
|
||||||
|
|
||||||
|
# Replacement 2: get_calculation_result
|
||||||
|
replacement_2 = """async def get_calculation_result(db_session: DbSession, calculation_id: str, token, include_risk_cost):
|
||||||
|
\"\"\"
|
||||||
|
Get calculation results from DB, returning pre-calculated plant and equipment results.
|
||||||
|
\"\"\"
|
||||||
|
try:
|
||||||
|
# Get calculation data with equipment results
|
||||||
|
calculation_query = await db_session.execute(
|
||||||
|
select(CalculationData)
|
||||||
|
.options(selectinload(CalculationData.equipment_results))
|
||||||
|
.where(CalculationData.id == calculation_id)
|
||||||
|
)
|
||||||
|
scope_calculation = calculation_query.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not scope_calculation:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Calculation with id {calculation_id} does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
|
scope_overhaul = await get_scope(
|
||||||
|
db_session=db_session,
|
||||||
|
overhaul_session_id=scope_calculation.overhaul_session_id
|
||||||
|
)
|
||||||
|
if not scope_overhaul:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Overhaul scope for session {scope_calculation.overhaul_session_id} does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse pre-calculated plant results
|
||||||
|
plant_results_raw = scope_calculation.plant_results or []
|
||||||
|
calculation_results = [CalculationResultsRead(**r) for r in plant_results_raw]
|
||||||
|
|
||||||
|
# Return comprehensive result
|
||||||
|
return CalculationTimeConstrainsRead(
|
||||||
|
id=scope_calculation.id,
|
||||||
|
reference=scope_calculation.overhaul_session_id,
|
||||||
|
scope=scope_overhaul.maintenance_type.name,
|
||||||
|
results=calculation_results,
|
||||||
|
optimum_oh=scope_calculation.optimum_oh_day,
|
||||||
|
optimum_oh_month=scope_calculation.optimum_oh_day + 1 if scope_calculation.optimum_oh_day is not None else None,
|
||||||
|
equipment_results=scope_calculation.equipment_results,
|
||||||
|
fleet_statistics=scope_calculation.fleet_statistics or {},
|
||||||
|
optimal_analysis=scope_calculation.optimum_analysis or {},
|
||||||
|
analysis_metadata=scope_calculation.analysis_metadata or {}
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.error(f"Error in get_calculation_result for calculation_id {calculation_id}: {str(e)}")
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Internal error processing calculation results: {str(e)}",
|
||||||
|
)"""
|
||||||
|
|
||||||
|
pattern_2 = re.compile(r"async def get_calculation_result\(db_session: DbSession, calculation_id: str, token, include_risk_cost\):.*?raise HTTPException\(\n status_code=status\.HTTP_500_INTERNAL_SERVER_ERROR,\n detail=f\"Internal error processing calculation results: \{str\(e\)\}\",\n \)", re.DOTALL)
|
||||||
|
if pattern_2.search(content):
|
||||||
|
content = pattern_2.sub(replacement_2, content)
|
||||||
|
else:
|
||||||
|
print("Could not find Replacement 2 target")
|
||||||
|
|
||||||
|
with open('src/calculation_time_constrains/service.py', 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print("Patch applied.")
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from temporalio.client import Client
|
||||||
|
from temporalio.worker import Worker
|
||||||
|
|
||||||
|
from temporal.temporal_workflows import OptimumOHCalculationWorkflow
|
||||||
|
from temporal.temporal_workflows import create_optimum_oh_calculation, request_rbd_simulation, run_optimum_oh_calculation
|
||||||
|
|
||||||
|
TEMPORAL_URL = os.environ.get("TEMPORAL_URL", "http://192.168.1.86:7233")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
client = await Client.connect(TEMPORAL_URL)
|
||||||
|
|
||||||
|
try:
|
||||||
|
worker = Worker(
|
||||||
|
client,
|
||||||
|
task_queue="oh-sim-queue",
|
||||||
|
workflows=[OptimumOHCalculationWorkflow],
|
||||||
|
activities=[
|
||||||
|
create_optimum_oh_calculation,
|
||||||
|
request_rbd_simulation,
|
||||||
|
run_optimum_oh_calculation
|
||||||
|
],
|
||||||
|
max_concurrent_workflow_tasks=50,
|
||||||
|
max_concurrent_activities=12
|
||||||
|
)
|
||||||
|
await worker.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Worker failed: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@ -0,0 +1 @@
|
|||||||
|
# Empty init file
|
||||||
@ -0,0 +1,172 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
|
||||||
|
from src.auth.service import Token
|
||||||
|
from src.config import TC_RBD_ID
|
||||||
|
from src.database.core import DbSession, CollectorDbSession
|
||||||
|
from src.overhaul_scope.service import get_all
|
||||||
|
from src.standard_scope.model import StandardScope
|
||||||
|
from src.workorder.model import MasterWorkOrder
|
||||||
|
|
||||||
|
from src.calculation_time_constrains.schema import (
|
||||||
|
CalculationTimeConstrainsParametersCreate,
|
||||||
|
CalculationTimeConstrainsParametersRead,
|
||||||
|
CalculationTimeConstrainsParametersRetrive,
|
||||||
|
)
|
||||||
|
from src.optimum_time_constraint.service import (
|
||||||
|
get_calculation_data_by_id,
|
||||||
|
get_calculation_result,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_create_calculation_parameters(
|
||||||
|
*, db_session: DbSession, calculation_id: Optional[str] = None
|
||||||
|
):
|
||||||
|
if calculation_id is not None:
|
||||||
|
calculation = await get_calculation_data_by_id(
|
||||||
|
calculation_id=calculation_id, db_session=db_session
|
||||||
|
)
|
||||||
|
|
||||||
|
if not calculation:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="A data with this id does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return CalculationTimeConstrainsParametersRead(
|
||||||
|
costPerFailure=calculation.parameter.avg_failure_cost,
|
||||||
|
overhaulCost=calculation.parameter.overhaul_cost,
|
||||||
|
reference=calculation,
|
||||||
|
)
|
||||||
|
|
||||||
|
stmt = (
|
||||||
|
select(
|
||||||
|
StandardScope,
|
||||||
|
func.avg(MasterWorkOrder.total_cost_max).label("average_cost"),
|
||||||
|
)
|
||||||
|
.outerjoin(MasterWorkOrder, StandardScope.location_tag == MasterWorkOrder.location_tag)
|
||||||
|
.group_by(StandardScope.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
results = await db_session.execute(stmt)
|
||||||
|
costFailure = results.all()
|
||||||
|
scopes = await get_all(db_session=db_session)
|
||||||
|
avaiableScopes = {scope.id: scope.scope_name for scope in scopes}
|
||||||
|
costFailurePerScope = {
|
||||||
|
avaiableScopes.get(costPerFailure[0]): costPerFailure[1]
|
||||||
|
for costPerFailure in costFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
return CalculationTimeConstrainsParametersRetrive(
|
||||||
|
costPerFailure=costFailurePerScope,
|
||||||
|
availableScopes=avaiableScopes.values(),
|
||||||
|
recommendedScope="A",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_calculation(
|
||||||
|
*,
|
||||||
|
token: str,
|
||||||
|
db_session: DbSession,
|
||||||
|
collector_db_session: CollectorDbSession,
|
||||||
|
calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate,
|
||||||
|
created_by: str,
|
||||||
|
simulation_id
|
||||||
|
):
|
||||||
|
from temporalio.client import Client
|
||||||
|
from src.config import TEMPORAL_URL
|
||||||
|
from temporal.temporal_workflows import OptimumOHCalculationWorkflow
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
temporal_client = await Client.connect(TEMPORAL_URL)
|
||||||
|
|
||||||
|
workflow_id = f"optimum-oh-calc-{uuid.uuid4()}"
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"token": token.token if hasattr(token, 'token') else str(token),
|
||||||
|
"calculation_in": calculation_time_constrains_in.model_dump(mode="json"),
|
||||||
|
"created_by": created_by
|
||||||
|
}
|
||||||
|
|
||||||
|
handle = await temporal_client.start_workflow(
|
||||||
|
OptimumOHCalculationWorkflow.run,
|
||||||
|
args,
|
||||||
|
id=workflow_id,
|
||||||
|
task_queue="oh-task-queue"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"data": workflow_id,
|
||||||
|
"status": "success",
|
||||||
|
"message": "Calculation workflow started successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def recalculate_calculation(
|
||||||
|
*,
|
||||||
|
token: str,
|
||||||
|
db_session: DbSession,
|
||||||
|
collector_db_session: CollectorDbSession,
|
||||||
|
calculation_id: str,
|
||||||
|
simulation_id: Optional[str] = None
|
||||||
|
):
|
||||||
|
calculation_data = await get_calculation_data_by_id(
|
||||||
|
db_session=db_session, calculation_id=calculation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not calculation_data:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Calculation not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
rbd_simulation_id = simulation_id or calculation_data.rbd_simulation_id or TC_RBD_ID
|
||||||
|
|
||||||
|
from sqlalchemy import delete
|
||||||
|
from src.calculation_time_constrains.model import CalculationEquipmentResult, CalculationResult
|
||||||
|
|
||||||
|
await db_session.execute(
|
||||||
|
delete(CalculationResult).where(CalculationResult.calculation_data_id == calculation_data.id)
|
||||||
|
)
|
||||||
|
await db_session.execute(
|
||||||
|
delete(CalculationEquipmentResult).where(CalculationEquipmentResult.calculation_data_id == calculation_data.id)
|
||||||
|
)
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
from src.optimum_time_constraint.optimizer import run_simulation_with_spareparts
|
||||||
|
await run_simulation_with_spareparts(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation=calculation_data,
|
||||||
|
token=token,
|
||||||
|
collector_db_session=collector_db_session,
|
||||||
|
simulation_id=rbd_simulation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return await get_calculation_result(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation_id=calculation_id,
|
||||||
|
token=token,
|
||||||
|
include_risk_cost=1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_or_create_scope_equipment_calculation(
|
||||||
|
*,
|
||||||
|
db_session: DbSession,
|
||||||
|
scope_calculation_id,
|
||||||
|
calculation_time_constrains_in: Optional[CalculationTimeConstrainsParametersCreate]
|
||||||
|
):
|
||||||
|
scope_calculation = await get_calculation_data_by_id(
|
||||||
|
db_session=db_session, calculation_id=scope_calculation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not scope_calculation:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="A data with this id does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return scope_calculation.id
|
||||||
@ -0,0 +1,917 @@
|
|||||||
|
import asyncio
|
||||||
|
import calendar
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
from datetime import date, timedelta
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import httpx
|
||||||
|
import numpy as np
|
||||||
|
import requests
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from src.config import REALIBILITY_SERVICE_API, RBD_SERVICE_API
|
||||||
|
from src.database.core import CollectorDbSession, DbSession
|
||||||
|
from src.calculation_time_constrains.model import CalculationData, CalculationEquipmentResult
|
||||||
|
from src.calculation_time_constrains.schema import CalculationResultsRead
|
||||||
|
from src.calculation_time_constrains.utils import (
|
||||||
|
calculate_failures_per_month,
|
||||||
|
create_time_series_data,
|
||||||
|
get_months_between,
|
||||||
|
)
|
||||||
|
from src.overhaul_scope.service import get as get_scope, get_prev_oh
|
||||||
|
from src.sparepart.service import load_sparepart_data_from_db
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OptimumCostModelWithSpareparts:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
token: str,
|
||||||
|
last_oh_date: date,
|
||||||
|
next_oh_date: date,
|
||||||
|
sparepart_manager,
|
||||||
|
time_window_months: Optional[int] = None,
|
||||||
|
base_url: str = "http://192.168.1.82:8000",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize the Optimum Cost Model with sparepart management
|
||||||
|
"""
|
||||||
|
self.api_base_url = base_url
|
||||||
|
self.token = token
|
||||||
|
self.last_oh_date = last_oh_date
|
||||||
|
self.next_oh_date = next_oh_date
|
||||||
|
self.session = None
|
||||||
|
self.sparepart_manager = sparepart_manager
|
||||||
|
|
||||||
|
# Calculate planned overhaul interval in months
|
||||||
|
self.planned_oh_months = get_months_between(last_oh_date, next_oh_date)
|
||||||
|
|
||||||
|
# Set analysis time window: next OH + 6 months
|
||||||
|
self.time_window_months = time_window_months or (self.planned_oh_months + 6)
|
||||||
|
|
||||||
|
# Pre-calculate date range for API calls
|
||||||
|
self.date_range = self._generate_date_range()
|
||||||
|
|
||||||
|
self.logger = log
|
||||||
|
|
||||||
|
def _generate_date_range(self) -> List[datetime.datetime]:
|
||||||
|
"""Generate date range for analysis based on time window"""
|
||||||
|
dates = []
|
||||||
|
current_date = datetime.datetime.combine(self.last_oh_date, datetime.datetime.min.time())
|
||||||
|
end_date = current_date + timedelta(days=self.time_window_months * 30)
|
||||||
|
|
||||||
|
while current_date <= end_date:
|
||||||
|
dates.append(current_date)
|
||||||
|
current_date += timedelta(days=31)
|
||||||
|
|
||||||
|
return dates
|
||||||
|
|
||||||
|
async def _create_session(self):
|
||||||
|
"""Create aiohttp session with connection pooling"""
|
||||||
|
if self.session is None:
|
||||||
|
timeout = aiohttp.ClientTimeout(total=300)
|
||||||
|
connector = aiohttp.TCPConnector(
|
||||||
|
limit=500,
|
||||||
|
limit_per_host=200,
|
||||||
|
ttl_dns_cache=300,
|
||||||
|
use_dns_cache=True,
|
||||||
|
force_close=False,
|
||||||
|
enable_cleanup_closed=True,
|
||||||
|
)
|
||||||
|
self.session = aiohttp.ClientSession(
|
||||||
|
timeout=timeout,
|
||||||
|
connector=connector,
|
||||||
|
headers={"Authorization": f"Bearer {self.token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _close_session(self):
|
||||||
|
"""Close aiohttp session"""
|
||||||
|
if self.session:
|
||||||
|
await self.session.close()
|
||||||
|
self.session = None
|
||||||
|
|
||||||
|
async def get_failures_prediction(self, location_tag: str):
|
||||||
|
"""Get failure predictions for equipment from Reliability Predict service"""
|
||||||
|
|
||||||
|
start_date = self.last_oh_date.strftime("%Y-%m-%d")
|
||||||
|
end_date_val = self.next_oh_date + timedelta(days=6 * 30)
|
||||||
|
end_date = end_date_val.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
predict_url = f"{REALIBILITY_SERVICE_API}/main/predict/{location_tag}/{start_date}/{end_date}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
predict_url,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {self.token}",
|
||||||
|
},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
prediction_data = response.json()
|
||||||
|
except (requests.RequestException, ValueError) as e:
|
||||||
|
self.logger.error(f"Failed to fetch prediction data for {location_tag}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
predictions = (
|
||||||
|
prediction_data.get("data", {}).get("predictions")
|
||||||
|
if prediction_data.get("data")
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if not predictions:
|
||||||
|
self.logger.warning(f"No prediction data available for {location_tag}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
monthly_data = {}
|
||||||
|
cumulative = 0.0
|
||||||
|
for i, pred in enumerate(predictions):
|
||||||
|
month_key = pred["month"]
|
||||||
|
monthly_fail = pred["predicted_failures"]
|
||||||
|
source = pred.get("source", "predicted")
|
||||||
|
cumulative += monthly_fail
|
||||||
|
monthly_data[month_key] = {
|
||||||
|
"cumulative_failures": cumulative,
|
||||||
|
"monthly_failures": monthly_fail,
|
||||||
|
"month_index": i + 1,
|
||||||
|
"source": source,
|
||||||
|
}
|
||||||
|
|
||||||
|
return monthly_data
|
||||||
|
|
||||||
|
async def get_simulation_results(self, simulation_id: str = "default"):
|
||||||
|
"""Get simulation results for Birnbaum importance calculations"""
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
calc_result_url = f"{self.api_base_url}/aeros/simulation/result/calc/{simulation_id}?nodetype=RegularNode"
|
||||||
|
plant_result_url = f"{self.api_base_url}/aeros/simulation/result/calc/{simulation_id}/plant"
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=300.0) as client:
|
||||||
|
calc_task = client.get(calc_result_url, headers=headers)
|
||||||
|
plant_task = client.get(plant_result_url, headers=headers)
|
||||||
|
|
||||||
|
calc_response, plant_response = await asyncio.gather(calc_task, plant_task)
|
||||||
|
|
||||||
|
calc_response.raise_for_status()
|
||||||
|
plant_response.raise_for_status()
|
||||||
|
|
||||||
|
calc_data = calc_response.json()["data"]
|
||||||
|
plant_data = plant_response.json()["data"]
|
||||||
|
|
||||||
|
return {"calc_result": calc_data, "plant_result": plant_data}
|
||||||
|
|
||||||
|
def _calculate_equipment_costs_with_spareparts(
|
||||||
|
self,
|
||||||
|
failures_prediction: Dict,
|
||||||
|
birnbaum_importance: float,
|
||||||
|
preventive_cost: float,
|
||||||
|
failure_replacement_cost: float,
|
||||||
|
ecs,
|
||||||
|
location_tag: str,
|
||||||
|
planned_overhauls: List = None,
|
||||||
|
loss_production_permonth=0,
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""Calculate costs for each month including sparepart costs and availability"""
|
||||||
|
|
||||||
|
if not failures_prediction:
|
||||||
|
self.logger.warning(f"No failure prediction data for {location_tag}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
results = []
|
||||||
|
months = list(failures_prediction.keys())
|
||||||
|
num_months = len(months)
|
||||||
|
failure_counts = []
|
||||||
|
|
||||||
|
monthly_risk_cost_per_failure = 0
|
||||||
|
|
||||||
|
if ecs:
|
||||||
|
is_trip = 1 if ecs.get("Diskripsi Operasional Akibat Equip. Failure") == "Trip" else 0
|
||||||
|
is_series = 0 if not birnbaum_importance else math.floor(birnbaum_importance)
|
||||||
|
if is_trip:
|
||||||
|
downtime = ecs.get("Estimasi Waktu Maint. / Downtime / Gangguan (Jam)")
|
||||||
|
monthly_risk_cost_per_failure = 660 * 1000000 * is_trip * downtime * is_series
|
||||||
|
|
||||||
|
for month_key in months:
|
||||||
|
data = failures_prediction[month_key]
|
||||||
|
failure_counts.append(data["cumulative_failures"])
|
||||||
|
|
||||||
|
for i in range(num_months):
|
||||||
|
month_index = i + 1
|
||||||
|
|
||||||
|
if month_index > self.time_window_months:
|
||||||
|
continue
|
||||||
|
|
||||||
|
sparepart_analysis = self._analyze_sparepart_availability(
|
||||||
|
location_tag, month_index - 1, planned_overhauls or []
|
||||||
|
)
|
||||||
|
|
||||||
|
total_expected_failure_cost = failure_counts[i] * (
|
||||||
|
failure_replacement_cost + monthly_risk_cost_per_failure
|
||||||
|
)
|
||||||
|
|
||||||
|
procurement_cost = sparepart_analysis["total_procurement_cost"]
|
||||||
|
total_preventive_cost = preventive_cost + procurement_cost
|
||||||
|
|
||||||
|
total_cycle_cost = total_expected_failure_cost + total_preventive_cost
|
||||||
|
|
||||||
|
cput = total_cycle_cost / month_index
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"month": month_index,
|
||||||
|
"number_of_failures": failure_counts[i],
|
||||||
|
"is_actual": failures_prediction[months[i]].get("source") == "actual",
|
||||||
|
"failure_cost": total_expected_failure_cost / month_index,
|
||||||
|
"preventive_cost": preventive_cost / month_index,
|
||||||
|
"procurement_cost": procurement_cost / month_index,
|
||||||
|
"total_cost": cput,
|
||||||
|
"absolute_failure_cost": total_expected_failure_cost,
|
||||||
|
"absolute_overhaul_cost": preventive_cost,
|
||||||
|
"absolute_procurement_cost": procurement_cost,
|
||||||
|
"total_cycle_cost": total_cycle_cost,
|
||||||
|
"is_after_planned_oh": month_index > self.planned_oh_months,
|
||||||
|
"delay_months": max(0, month_index - self.planned_oh_months),
|
||||||
|
"procurement_details": sparepart_analysis,
|
||||||
|
"sparepart_available": sparepart_analysis["available"],
|
||||||
|
"sparepart_status": sparepart_analysis["message"],
|
||||||
|
"can_proceed": sparepart_analysis["can_proceed_with_delays"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _analyze_sparepart_availability(
|
||||||
|
self, equipment_tag: str, target_month: int, planned_overhauls: List
|
||||||
|
) -> Dict:
|
||||||
|
"""Analyze sparepart availability for equipment at target month"""
|
||||||
|
if not self.sparepart_manager:
|
||||||
|
return {
|
||||||
|
"available": True,
|
||||||
|
"message": "Sparepart manager not initialized",
|
||||||
|
"total_procurement_cost": 0,
|
||||||
|
"procurement_needed": [],
|
||||||
|
"can_proceed_with_delays": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
other_overhauls = [
|
||||||
|
(eq_tag, month)
|
||||||
|
for eq_tag, month in planned_overhauls
|
||||||
|
if eq_tag != equipment_tag and month <= target_month
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.sparepart_manager.check_sparepart_availability(
|
||||||
|
equipment_tag, target_month, other_overhauls
|
||||||
|
)
|
||||||
|
|
||||||
|
def _find_optimal_timing_with_spareparts(
|
||||||
|
self, cost_results: List[Dict], location_tag: str
|
||||||
|
) -> Optional[Dict]:
|
||||||
|
"""Find optimal timing considering sparepart constraints"""
|
||||||
|
if not cost_results:
|
||||||
|
return None
|
||||||
|
|
||||||
|
feasible_results = [r for r in cost_results if r["can_proceed"]]
|
||||||
|
|
||||||
|
min_cost = float("inf")
|
||||||
|
optimal_result = None
|
||||||
|
|
||||||
|
for i, result in enumerate(cost_results):
|
||||||
|
if result in feasible_results and result["total_cost"] < min_cost:
|
||||||
|
min_cost = result["total_cost"]
|
||||||
|
optimal_result = result
|
||||||
|
|
||||||
|
if optimal_result is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self._create_optimal_result(optimal_result, location_tag, "OPTIMAL")
|
||||||
|
|
||||||
|
def _create_optimal_result(
|
||||||
|
self, optimal_result: Dict, location_tag: str, status: str
|
||||||
|
) -> Dict:
|
||||||
|
"""Create standardized optimal result dictionary"""
|
||||||
|
return {
|
||||||
|
"location_tag": location_tag,
|
||||||
|
"optimal_month": optimal_result["month"],
|
||||||
|
"optimal_index": optimal_result["month"] - 1,
|
||||||
|
"optimal_cost": optimal_result["total_cost"],
|
||||||
|
"failure_cost": optimal_result["failure_cost"],
|
||||||
|
"preventive_cost": optimal_result["preventive_cost"],
|
||||||
|
"procurement_cost": optimal_result["procurement_cost"],
|
||||||
|
"number_of_failures": optimal_result["number_of_failures"],
|
||||||
|
"is_delayed": optimal_result["is_after_planned_oh"],
|
||||||
|
"delay_months": optimal_result["delay_months"],
|
||||||
|
"planned_oh_month": self.planned_oh_months,
|
||||||
|
"planned_cost": None,
|
||||||
|
"cost_vs_planned": None,
|
||||||
|
"savings_from_delay": 0,
|
||||||
|
"cost_of_delay": 0,
|
||||||
|
"sparepart_available": optimal_result["sparepart_available"],
|
||||||
|
"sparepart_status": optimal_result["sparepart_status"],
|
||||||
|
"procurement_details": optimal_result["procurement_details"],
|
||||||
|
"optimization_status": status,
|
||||||
|
"all_monthly_costs": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def calculate_cost_all_equipment_with_spareparts(
|
||||||
|
self,
|
||||||
|
db_session,
|
||||||
|
collector_db_session,
|
||||||
|
equipments: List,
|
||||||
|
calculation,
|
||||||
|
preventive_cost: float,
|
||||||
|
simulation_id: str = "default",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Calculate optimal overhaul timing for entire fleet considering sparepart constraints
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
f"Starting fleet optimization with reliability prediction for {len(equipments)} equipment"
|
||||||
|
)
|
||||||
|
max_interval = self.time_window_months
|
||||||
|
|
||||||
|
# Phase 1: Calculate individual optimal timings without considering interactions
|
||||||
|
individual_results = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open("src/calculation_time_constrains/full_equipment_with_downtime_opdesc.json", "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
ecs_tags = {eq["Location"]: eq for eq in data}
|
||||||
|
except FileNotFoundError:
|
||||||
|
ecs_tags = {}
|
||||||
|
|
||||||
|
for equipment in equipments:
|
||||||
|
location_tag = equipment.location_tag
|
||||||
|
contribution_factor = 1.0
|
||||||
|
ecs = ecs_tags.get(location_tag, None)
|
||||||
|
|
||||||
|
monthly_data = await self.get_failures_prediction(location_tag)
|
||||||
|
|
||||||
|
if not monthly_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
equipment_preventive_cost = equipment.overhaul_cost + equipment.service_cost
|
||||||
|
failure_replacement_cost = equipment.material_cost + (3 * 111000 * 3)
|
||||||
|
|
||||||
|
cost_results = self._calculate_equipment_costs_with_spareparts(
|
||||||
|
failures_prediction=monthly_data,
|
||||||
|
birnbaum_importance=contribution_factor,
|
||||||
|
preventive_cost=equipment_preventive_cost,
|
||||||
|
failure_replacement_cost=failure_replacement_cost,
|
||||||
|
location_tag=location_tag,
|
||||||
|
planned_overhauls=[],
|
||||||
|
ecs=ecs,
|
||||||
|
loss_production_permonth=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not cost_results:
|
||||||
|
continue
|
||||||
|
|
||||||
|
optimal_timing = self._find_optimal_timing_with_spareparts(cost_results, location_tag)
|
||||||
|
|
||||||
|
if optimal_timing:
|
||||||
|
optimal_timing["all_monthly_costs"] = cost_results
|
||||||
|
individual_results[location_tag] = optimal_timing
|
||||||
|
self.logger.info(
|
||||||
|
f"Individual optimal for {location_tag}: Month {optimal_timing['optimal_month']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Phase 2: Optimize considering sparepart interactions
|
||||||
|
self.logger.info("Phase 2: Optimizing with sparepart interactions...")
|
||||||
|
|
||||||
|
improved_plan = self._optimize_fleet_with_sparepart_constraints(
|
||||||
|
individual_results, equipments, simulation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Phase 3: Generate final results and database objects
|
||||||
|
fleet_results = []
|
||||||
|
total_corrective_costs = np.zeros(max_interval)
|
||||||
|
total_preventive_costs = np.zeros(max_interval)
|
||||||
|
total_procurement_costs = np.zeros(max_interval)
|
||||||
|
total_costs = np.zeros(max_interval)
|
||||||
|
|
||||||
|
total_fleet_procurement_cost = 0
|
||||||
|
|
||||||
|
for equipment in equipments:
|
||||||
|
location_tag = equipment.location_tag
|
||||||
|
|
||||||
|
if location_tag not in individual_results:
|
||||||
|
continue
|
||||||
|
|
||||||
|
equipment_timing = next(
|
||||||
|
(month for tag, month in improved_plan if tag == location_tag),
|
||||||
|
individual_results[location_tag]["optimal_month"],
|
||||||
|
)
|
||||||
|
|
||||||
|
cost_data = individual_results[location_tag]["all_monthly_costs"][equipment_timing - 1]
|
||||||
|
|
||||||
|
all_costs = individual_results[location_tag]["all_monthly_costs"]
|
||||||
|
|
||||||
|
corrective_costs = [r["failure_cost"] for r in all_costs]
|
||||||
|
preventive_costs = [r["preventive_cost"] for r in all_costs]
|
||||||
|
procurement_costs = [r["procurement_cost"] for r in all_costs]
|
||||||
|
failures = [r["number_of_failures"] for r in all_costs]
|
||||||
|
total_costs_equipment = [r["total_cost"] for r in all_costs]
|
||||||
|
procurement_details = [r["procurement_details"] for r in all_costs]
|
||||||
|
|
||||||
|
def pad_array(arr, target_length):
|
||||||
|
if len(arr) < target_length:
|
||||||
|
return arr + [arr[-1]] * (target_length - len(arr))
|
||||||
|
return arr[:target_length]
|
||||||
|
|
||||||
|
corrective_costs = pad_array(corrective_costs, max_interval)
|
||||||
|
preventive_costs = pad_array(preventive_costs, max_interval)
|
||||||
|
procurement_costs = pad_array(procurement_costs, max_interval)
|
||||||
|
failures = pad_array(failures, max_interval)
|
||||||
|
total_costs_equipment = pad_array(total_costs_equipment, max_interval)
|
||||||
|
procurement_details = pad_array(procurement_details, max_interval)
|
||||||
|
|
||||||
|
is_actual_list = [r.get("is_actual", False) for r in all_costs]
|
||||||
|
is_actual_list = pad_array(is_actual_list, max_interval)
|
||||||
|
|
||||||
|
equipment_result = CalculationEquipmentResult(
|
||||||
|
corrective_costs=corrective_costs,
|
||||||
|
overhaul_costs=preventive_costs,
|
||||||
|
procurement_costs=procurement_costs,
|
||||||
|
daily_failures=failures,
|
||||||
|
is_actual=is_actual_list,
|
||||||
|
location_tag=equipment.location_tag,
|
||||||
|
material_cost=equipment.material_cost,
|
||||||
|
service_cost=equipment.service_cost,
|
||||||
|
optimum_day=equipment_timing - 1,
|
||||||
|
calculation_data_id=calculation.id,
|
||||||
|
procurement_details=procurement_details,
|
||||||
|
)
|
||||||
|
|
||||||
|
fleet_results.append(equipment_result)
|
||||||
|
|
||||||
|
total_corrective_costs += np.array(corrective_costs)
|
||||||
|
total_preventive_costs += np.array(preventive_costs)
|
||||||
|
total_procurement_costs += np.array(procurement_costs)
|
||||||
|
total_costs += np.array(total_costs_equipment)
|
||||||
|
|
||||||
|
total_fleet_procurement_cost += cost_data["procurement_cost"]
|
||||||
|
|
||||||
|
fleet_optimal_index = np.argmin(total_costs)
|
||||||
|
|
||||||
|
calculation.optimum_oh_day = int(fleet_optimal_index)
|
||||||
|
calculation.max_interval = max_interval
|
||||||
|
calculation.rbd_simulation_id = simulation_id
|
||||||
|
|
||||||
|
db_session.add_all(fleet_results)
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
return int(fleet_optimal_index)
|
||||||
|
|
||||||
|
def _optimize_fleet_with_sparepart_constraints(
|
||||||
|
self, individual_results: Dict, equipments: List, simulation_id: str
|
||||||
|
) -> List[Tuple[str, int]]:
|
||||||
|
"""
|
||||||
|
Optimize fleet overhaul timing considering sparepart sharing constraints
|
||||||
|
"""
|
||||||
|
current_plan = [(tag, result["optimal_month"]) for tag, result in individual_results.items()]
|
||||||
|
current_plan.sort(key=lambda x: x[1])
|
||||||
|
|
||||||
|
improved_plan = []
|
||||||
|
processed_equipment = []
|
||||||
|
|
||||||
|
for equipment_tag, optimal_month in current_plan:
|
||||||
|
sparepart_analysis = self.sparepart_manager.check_sparepart_availability(
|
||||||
|
equipment_tag, optimal_month - 1, processed_equipment
|
||||||
|
)
|
||||||
|
|
||||||
|
if sparepart_analysis["available"] or sparepart_analysis["can_proceed_with_delays"]:
|
||||||
|
improved_plan.append((equipment_tag, optimal_month))
|
||||||
|
processed_equipment.append((equipment_tag, optimal_month))
|
||||||
|
else:
|
||||||
|
alternative_month = self._find_alternative_timing(
|
||||||
|
equipment_tag,
|
||||||
|
optimal_month,
|
||||||
|
individual_results[equipment_tag]["all_monthly_costs"],
|
||||||
|
processed_equipment,
|
||||||
|
)
|
||||||
|
|
||||||
|
if alternative_month:
|
||||||
|
improved_plan.append((equipment_tag, alternative_month))
|
||||||
|
processed_equipment.append((equipment_tag, alternative_month))
|
||||||
|
else:
|
||||||
|
improved_plan.append((equipment_tag, optimal_month))
|
||||||
|
processed_equipment.append((equipment_tag, optimal_month))
|
||||||
|
|
||||||
|
return improved_plan
|
||||||
|
|
||||||
|
def _find_alternative_timing(
|
||||||
|
self,
|
||||||
|
equipment_tag: str,
|
||||||
|
preferred_month: int,
|
||||||
|
cost_results: List[Dict],
|
||||||
|
processed_equipment: List[Tuple[str, int]],
|
||||||
|
) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Find alternative timing when preferred month has sparepart constraints
|
||||||
|
"""
|
||||||
|
search_range = 6
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
|
||||||
|
for offset in range(-search_range // 2, search_range // 2 + 1):
|
||||||
|
candidate_month = preferred_month + offset
|
||||||
|
|
||||||
|
if candidate_month <= 0 or candidate_month > len(cost_results):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if candidate_month == preferred_month:
|
||||||
|
continue
|
||||||
|
|
||||||
|
sparepart_analysis = self.sparepart_manager.check_sparepart_availability(
|
||||||
|
equipment_tag, candidate_month - 1, processed_equipment
|
||||||
|
)
|
||||||
|
|
||||||
|
if sparepart_analysis["available"] or sparepart_analysis["can_proceed_with_delays"]:
|
||||||
|
cost_data = cost_results[candidate_month - 1]
|
||||||
|
candidates.append((candidate_month, cost_data["total_cost"]))
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
|
||||||
|
candidates.sort(key=lambda x: x[1])
|
||||||
|
return candidates[0][0]
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
await self._create_session()
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
await self._close_session()
|
||||||
|
|
||||||
|
|
||||||
|
def _analyze_optimal_timing(
|
||||||
|
calculation_results: List, optimum_oh_day: int, prev_oh_scope, scope_overhaul
|
||||||
|
) -> Dict:
|
||||||
|
"""Analyze optimal timing and provide recommendations"""
|
||||||
|
|
||||||
|
if not calculation_results:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
optimal_result = None
|
||||||
|
if 0 <= optimum_oh_day < len(calculation_results):
|
||||||
|
optimal_result = calculation_results[optimum_oh_day]
|
||||||
|
|
||||||
|
planned_oh_months = None
|
||||||
|
if prev_oh_scope and scope_overhaul:
|
||||||
|
planned_oh_months = get_months_between(prev_oh_scope.end_date, scope_overhaul.start_date)
|
||||||
|
|
||||||
|
timing_recommendation = "OPTIMAL"
|
||||||
|
if planned_oh_months:
|
||||||
|
if optimum_oh_day + 1 < planned_oh_months:
|
||||||
|
timing_recommendation = "EARLY"
|
||||||
|
elif optimum_oh_day + 1 > planned_oh_months:
|
||||||
|
timing_recommendation = "DELAYED"
|
||||||
|
else:
|
||||||
|
timing_recommendation = "ON_SCHEDULE"
|
||||||
|
|
||||||
|
cost_trend = "STABLE"
|
||||||
|
if len(calculation_results) > 1:
|
||||||
|
early_costs = [r.total_cost for r in calculation_results[: len(calculation_results) // 3]]
|
||||||
|
late_costs = [r.total_cost for r in calculation_results[-len(calculation_results) // 3 :]]
|
||||||
|
|
||||||
|
avg_early = sum(early_costs) / len(early_costs) if early_costs else 0
|
||||||
|
avg_late = sum(late_costs) / len(late_costs) if late_costs else 0
|
||||||
|
|
||||||
|
if avg_late > avg_early * 1.2:
|
||||||
|
cost_trend = "INCREASING"
|
||||||
|
elif avg_late < avg_early * 0.8:
|
||||||
|
cost_trend = "DECREASING"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"optimal_month": optimum_oh_day + 1,
|
||||||
|
"planned_month": planned_oh_months,
|
||||||
|
"timing_recommendation": timing_recommendation,
|
||||||
|
"optimal_total_cost": optimal_result.total_cost if optimal_result else 0,
|
||||||
|
"optimal_breakdown": {
|
||||||
|
"corrective_cost": optimal_result.corrective_cost if optimal_result else 0,
|
||||||
|
"overhaul_cost": optimal_result.overhaul_cost if optimal_result else 0,
|
||||||
|
"procurement_cost": optimal_result.procurement_cost if optimal_result else 0,
|
||||||
|
"num_failures": optimal_result.num_failures if optimal_result else 0,
|
||||||
|
},
|
||||||
|
"cost_trend": cost_trend,
|
||||||
|
"months_from_planned": (optimum_oh_day + 1 - planned_oh_months)
|
||||||
|
if planned_oh_months
|
||||||
|
else None,
|
||||||
|
"cost_savings_vs_planned": None,
|
||||||
|
"sparepart_impact": {
|
||||||
|
"equipment_with_constraints": optimal_result.sparepart_summary["equipment_requiring_procurement"]
|
||||||
|
if optimal_result
|
||||||
|
else 0,
|
||||||
|
"critical_shortages": optimal_result.sparepart_summary["critical_shortages"]
|
||||||
|
if optimal_result
|
||||||
|
else 0,
|
||||||
|
"procurement_investment": optimal_result.sparepart_summary["total_procurement_cost"]
|
||||||
|
if optimal_result
|
||||||
|
else 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def run_simulation_with_spareparts(
|
||||||
|
*,
|
||||||
|
db_session: DbSession,
|
||||||
|
calculation,
|
||||||
|
token: str,
|
||||||
|
collector_db_session: CollectorDbSession,
|
||||||
|
time_window_months: Optional[int] = None,
|
||||||
|
simulation_id: str = "default",
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Run complete overhaul optimization simulation with sparepart management
|
||||||
|
"""
|
||||||
|
from src.optimum_time_constraint.service import get_calculation_data_by_id
|
||||||
|
from src.overhaul_activity.service import get_standard_scope_by_session_id
|
||||||
|
|
||||||
|
equipments = await get_standard_scope_by_session_id(
|
||||||
|
db_session=db_session,
|
||||||
|
overhaul_session_id=calculation.overhaul_session_id,
|
||||||
|
collector_db=collector_db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
scope = await get_scope(
|
||||||
|
db_session=db_session, overhaul_session_id=calculation.overhaul_session_id
|
||||||
|
)
|
||||||
|
prev_oh_scope = await get_prev_oh(db_session=db_session, overhaul_session=scope)
|
||||||
|
|
||||||
|
calculation_data = await get_calculation_data_by_id(
|
||||||
|
db_session=db_session, calculation_id=calculation.id
|
||||||
|
)
|
||||||
|
|
||||||
|
time_window_months = get_months_between(prev_oh_scope.end_date, scope.start_date) + 6
|
||||||
|
|
||||||
|
sparepart_manager = await load_sparepart_data_from_db(
|
||||||
|
scope=scope,
|
||||||
|
prev_oh_scope=prev_oh_scope,
|
||||||
|
db_session=collector_db_session,
|
||||||
|
app_db_session=db_session,
|
||||||
|
analysis_window_months=time_window_months,
|
||||||
|
)
|
||||||
|
|
||||||
|
optimum_oh_model = OptimumCostModelWithSpareparts(
|
||||||
|
token=token,
|
||||||
|
last_oh_date=prev_oh_scope.end_date,
|
||||||
|
next_oh_date=scope.start_date,
|
||||||
|
base_url=RBD_SERVICE_API,
|
||||||
|
sparepart_manager=sparepart_manager,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
fleet_optimal_index = await optimum_oh_model.calculate_cost_all_equipment_with_spareparts(
|
||||||
|
db_session=db_session,
|
||||||
|
collector_db_session=collector_db_session,
|
||||||
|
equipments=equipments,
|
||||||
|
calculation=calculation_data,
|
||||||
|
preventive_cost=calculation_data.parameter.overhaul_cost,
|
||||||
|
simulation_id=simulation_id,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
await optimum_oh_model._close_session()
|
||||||
|
|
||||||
|
calculation_query = await db_session.execute(
|
||||||
|
select(CalculationData)
|
||||||
|
.options(
|
||||||
|
selectinload(CalculationData.equipment_results),
|
||||||
|
selectinload(CalculationData.parameter),
|
||||||
|
)
|
||||||
|
.where(CalculationData.id == calculation.id)
|
||||||
|
)
|
||||||
|
scope_calculation = calculation_query.scalar_one_or_none()
|
||||||
|
|
||||||
|
data_num = scope_calculation.max_interval
|
||||||
|
all_equipment = scope_calculation.equipment_results
|
||||||
|
included_equipment = [eq for eq in all_equipment if eq.is_included]
|
||||||
|
|
||||||
|
calculation_results = []
|
||||||
|
fleet_statistics = {
|
||||||
|
"total_equipment": len(all_equipment),
|
||||||
|
"included_equipment": len(included_equipment),
|
||||||
|
"excluded_equipment": len(all_equipment) - len(included_equipment),
|
||||||
|
"equipment_with_sparepart_constraints": 0,
|
||||||
|
"total_procurement_items": 0,
|
||||||
|
"critical_procurement_items": 0,
|
||||||
|
"total_spareparts": 745,
|
||||||
|
}
|
||||||
|
|
||||||
|
avg_failure_cost = (
|
||||||
|
sum((eq.material_cost or 0) + (3 * 111000 * 3) for eq in all_equipment) / len(all_equipment)
|
||||||
|
if all_equipment
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
rbd_marginal_fails = [0] * data_num
|
||||||
|
try:
|
||||||
|
if scope_calculation.rbd_simulation_id:
|
||||||
|
plant_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{scope_calculation.rbd_simulation_id}/plant"
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.get(
|
||||||
|
plant_result_url,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
plant_data = response.json().get("data", {})
|
||||||
|
timestamp_outs = plant_data.get("timestamp_outs", [])
|
||||||
|
if timestamp_outs:
|
||||||
|
hourly_data = create_time_series_data(
|
||||||
|
timestamp_outs, max_hours=data_num * 720
|
||||||
|
)
|
||||||
|
cumulative_rbd_fails = calculate_failures_per_month(hourly_data)
|
||||||
|
rbd_fails_map = {m["month"]: m["failures"] for m in cumulative_rbd_fails}
|
||||||
|
prev_fail = 0
|
||||||
|
for m in range(1, data_num + 1):
|
||||||
|
curr_fail = rbd_fails_map.get(m, prev_fail)
|
||||||
|
rbd_marginal_fails[m - 1] = curr_fail - prev_fail
|
||||||
|
prev_fail = curr_fail
|
||||||
|
except Exception as e:
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.warning(f"Failed to fetch plant simulation: {e}")
|
||||||
|
|
||||||
|
cumulative_plant_failures = 0
|
||||||
|
|
||||||
|
for month_index in range(data_num):
|
||||||
|
historical_marginal_fail = 0
|
||||||
|
for eq in all_equipment:
|
||||||
|
if eq.is_actual and month_index < len(eq.is_actual) and eq.is_actual[month_index]:
|
||||||
|
curr_fail = (
|
||||||
|
eq.daily_failures[month_index]
|
||||||
|
if month_index < len(eq.daily_failures)
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
prev_fail = (
|
||||||
|
eq.daily_failures[month_index - 1]
|
||||||
|
if month_index > 0 and (month_index - 1) < len(eq.daily_failures)
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
historical_marginal_fail += max(0, curr_fail - prev_fail)
|
||||||
|
|
||||||
|
marginal_fail = rbd_marginal_fails[month_index] + historical_marginal_fail
|
||||||
|
cumulative_plant_failures += marginal_fail
|
||||||
|
|
||||||
|
month_result = {
|
||||||
|
"overhaul_cost": 0.0,
|
||||||
|
"corrective_cost": 0.0,
|
||||||
|
"procurement_cost": 0.0,
|
||||||
|
"num_failures": cumulative_plant_failures,
|
||||||
|
"day": month_index + 1,
|
||||||
|
"month": month_index + 1,
|
||||||
|
"procurement_details": {},
|
||||||
|
"sparepart_summary": {
|
||||||
|
"total_procurement_cost": 0.0,
|
||||||
|
"equipment_requiring_procurement": 0,
|
||||||
|
"critical_shortages": 0,
|
||||||
|
"existing_orders_value": 0.0,
|
||||||
|
"new_orders_required": 0,
|
||||||
|
"urgent_procurements": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
equipment_requiring_procurement = 0
|
||||||
|
total_existing_orders_value = 0.0
|
||||||
|
total_new_orders_value = 0.0
|
||||||
|
critical_shortages = 0
|
||||||
|
urgent_procurements = 0
|
||||||
|
|
||||||
|
for eq in all_equipment:
|
||||||
|
if month_index >= len(eq.procurement_details):
|
||||||
|
continue
|
||||||
|
procurement_detail = eq.procurement_details[month_index]
|
||||||
|
if (
|
||||||
|
procurement_detail
|
||||||
|
and isinstance(procurement_detail, dict)
|
||||||
|
and procurement_detail.get("procurement_needed")
|
||||||
|
):
|
||||||
|
equipment_requiring_procurement += 1
|
||||||
|
pr_po_summary = procurement_detail.get("pr_po_summary", {})
|
||||||
|
existing_orders_value = pr_po_summary.get("total_existing_value", 0)
|
||||||
|
total_existing_orders_value += existing_orders_value
|
||||||
|
new_orders_value = pr_po_summary.get("total_new_orders_value", 0)
|
||||||
|
total_new_orders_value += new_orders_value
|
||||||
|
critical_missing = procurement_detail.get("critical_missing_parts", 0)
|
||||||
|
if critical_missing > 0:
|
||||||
|
critical_shortages += 1
|
||||||
|
recommendations = procurement_detail.get("recommendations", [])
|
||||||
|
urgent_items = [
|
||||||
|
r for r in recommendations if r.get("priority") == "CRITICAL"
|
||||||
|
]
|
||||||
|
if urgent_items:
|
||||||
|
urgent_procurements += 1
|
||||||
|
is_included_eq = False if eq.is_initial else eq.is_included
|
||||||
|
month_result["procurement_details"][eq.location_tag] = {
|
||||||
|
"is_included": is_included_eq,
|
||||||
|
"location_tag": eq.location_tag,
|
||||||
|
"details": procurement_detail.get("procurement_needed", []),
|
||||||
|
"detailed_message": procurement_detail.get("detailed_message", ""),
|
||||||
|
"pr_po_summary": pr_po_summary,
|
||||||
|
"recommendations": recommendations,
|
||||||
|
"sparepart_available": procurement_detail.get("sparepart_available", True),
|
||||||
|
"can_proceed": procurement_detail.get("can_proceed_with_delays", True),
|
||||||
|
"critical_missing_parts": critical_missing,
|
||||||
|
"existing_orders_value": existing_orders_value,
|
||||||
|
"new_orders_value": new_orders_value,
|
||||||
|
}
|
||||||
|
if eq.is_included:
|
||||||
|
if month_index < len(eq.overhaul_costs) and month_index < len(
|
||||||
|
eq.procurement_costs
|
||||||
|
):
|
||||||
|
month_result["overhaul_cost"] += float(eq.overhaul_costs[month_index])
|
||||||
|
month_result["procurement_cost"] += float(eq.procurement_costs[month_index])
|
||||||
|
|
||||||
|
month_result["corrective_cost"] = (
|
||||||
|
cumulative_plant_failures * avg_failure_cost
|
||||||
|
) / (month_index + 1)
|
||||||
|
month_result["sparepart_summary"].update(
|
||||||
|
{
|
||||||
|
"total_procurement_cost": month_result["procurement_cost"],
|
||||||
|
"equipment_requiring_procurement": equipment_requiring_procurement,
|
||||||
|
"critical_shortages": critical_shortages,
|
||||||
|
"existing_orders_value": total_existing_orders_value,
|
||||||
|
"new_orders_required": len(
|
||||||
|
[
|
||||||
|
eq
|
||||||
|
for eq in all_equipment
|
||||||
|
if month_index < len(eq.procurement_details)
|
||||||
|
and eq.procurement_details[month_index]
|
||||||
|
and eq.procurement_details[month_index].get("procurement_needed")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"urgent_procurements": urgent_procurements,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
month_result["total_cost"] = (
|
||||||
|
month_result["corrective_cost"]
|
||||||
|
+ month_result["overhaul_cost"]
|
||||||
|
+ month_result["procurement_cost"]
|
||||||
|
)
|
||||||
|
calculation_results.append(month_result)
|
||||||
|
|
||||||
|
optimum_day = np.argmin([month["total_cost"] for month in calculation_results])
|
||||||
|
scope_calculation.optimum_oh_day = int(optimum_day)
|
||||||
|
|
||||||
|
fleet_statistics["equipment_with_sparepart_constraints"] = len(
|
||||||
|
[
|
||||||
|
eq
|
||||||
|
for eq in all_equipment
|
||||||
|
if any(
|
||||||
|
detail and detail.get("procurement_needed")
|
||||||
|
for detail in eq.procurement_details
|
||||||
|
if detail
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
fleet_statistics["total_procurement_items"] = sum(
|
||||||
|
[
|
||||||
|
len(detail.get("procurement_needed", []))
|
||||||
|
for eq in all_equipment
|
||||||
|
for detail in eq.procurement_details
|
||||||
|
if detail and isinstance(detail, dict)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
analysis_metadata = {
|
||||||
|
"planned_month": (scope.start_date.year - prev_oh_scope.end_date.year) * 12
|
||||||
|
+ (scope.start_date.month - prev_oh_scope.end_date.month)
|
||||||
|
if prev_oh_scope and scope
|
||||||
|
else 0,
|
||||||
|
"total_fleet_procurement_cost": sum(
|
||||||
|
[
|
||||||
|
eq.procurement_costs[int(scope_calculation.optimum_oh_day)]
|
||||||
|
for eq in all_equipment
|
||||||
|
if eq.procurement_costs
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"last_oh_date": prev_oh_scope.end_date.isoformat() if prev_oh_scope else None,
|
||||||
|
"next_oh_date": scope.start_date.isoformat() if scope else None,
|
||||||
|
"optimal_stat": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
calc_results_read = [CalculationResultsRead(**r) for r in calculation_results]
|
||||||
|
optimal_analysis = _analyze_optimal_timing(
|
||||||
|
calc_results_read, scope_calculation.optimum_oh_day, prev_oh_scope, scope
|
||||||
|
)
|
||||||
|
|
||||||
|
scope_calculation.plant_results = calculation_results
|
||||||
|
scope_calculation.fleet_statistics = fleet_statistics
|
||||||
|
scope_calculation.analysis_metadata = analysis_metadata
|
||||||
|
scope_calculation.optimum_analysis = optimal_analysis
|
||||||
|
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
return {"id": calculation.id, "optimum": optimal_analysis}
|
||||||
@ -0,0 +1,233 @@
|
|||||||
|
from typing import Annotated, List, Optional, Union
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.params import Query
|
||||||
|
|
||||||
|
from src.auth.service import CurrentUser, InternalKey, Token
|
||||||
|
from src.config import DEFAULT_TC_ID
|
||||||
|
from src.database.core import DbSession, CollectorDbSession
|
||||||
|
from src.models import StandardResponse
|
||||||
|
|
||||||
|
from src.optimum_time_constraint.flows import (
|
||||||
|
create_calculation,
|
||||||
|
get_create_calculation_parameters,
|
||||||
|
get_or_create_scope_equipment_calculation,
|
||||||
|
recalculate_calculation,
|
||||||
|
)
|
||||||
|
from src.calculation_time_constrains.schema import (
|
||||||
|
CalculationResultsRead,
|
||||||
|
CalculationSelectedEquipmentUpdate,
|
||||||
|
CalculationTimeConstrainsCreate,
|
||||||
|
CalculationTimeConstrainsParametersCreate,
|
||||||
|
CalculationTimeConstrainsParametersRead,
|
||||||
|
CalculationTimeConstrainsParametersRetrive,
|
||||||
|
CalculationTimeConstrainsRead,
|
||||||
|
CreateCalculationQuery,
|
||||||
|
EquipmentResult,
|
||||||
|
CalculationTimeConstrainsReadNoResult,
|
||||||
|
)
|
||||||
|
from src.optimum_time_constraint.service import (
|
||||||
|
bulk_update_equipment,
|
||||||
|
get_calculation_result,
|
||||||
|
get_calculation_result_by_day,
|
||||||
|
get_calculation_by_assetnum,
|
||||||
|
get_all_calculations,
|
||||||
|
refresh_spareparts_service,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
get_calculation = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"", response_model=StandardResponse[Union[dict, CalculationTimeConstrainsRead]]
|
||||||
|
)
|
||||||
|
async def create_calculation_time_constrains(
|
||||||
|
token: Token,
|
||||||
|
db_session: DbSession,
|
||||||
|
collector_db_session: CollectorDbSession,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate,
|
||||||
|
params: Annotated[CreateCalculationQuery, Query()],
|
||||||
|
):
|
||||||
|
"""Save calculation time constrains Here"""
|
||||||
|
scope_calculation_id = params.scope_calculation_id
|
||||||
|
simulation_id = params.simulation_id
|
||||||
|
|
||||||
|
if scope_calculation_id:
|
||||||
|
results = await get_or_create_scope_equipment_calculation(
|
||||||
|
db_session=db_session,
|
||||||
|
scope_calculation_id=scope_calculation_id,
|
||||||
|
calculation_time_constrains_in=calculation_time_constrains_in,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
results = await create_calculation(
|
||||||
|
token=token,
|
||||||
|
db_session=db_session,
|
||||||
|
collector_db_session=collector_db_session,
|
||||||
|
calculation_time_constrains_in=calculation_time_constrains_in,
|
||||||
|
created_by=current_user.name,
|
||||||
|
simulation_id=simulation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(data=results, message="Data created successfully")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"", response_model=StandardResponse[List[CalculationTimeConstrainsReadNoResult]]
|
||||||
|
)
|
||||||
|
async def get_all_simulation_calculations(
|
||||||
|
db_session: DbSession,
|
||||||
|
token: Token,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
):
|
||||||
|
"""Get all calculation time constrains Here"""
|
||||||
|
|
||||||
|
calculations = await get_all_calculations(db_session=db_session)
|
||||||
|
|
||||||
|
return StandardResponse(
|
||||||
|
data=calculations,
|
||||||
|
message="Data retrieved successfully",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/parameters",
|
||||||
|
response_model=StandardResponse[
|
||||||
|
Union[
|
||||||
|
CalculationTimeConstrainsParametersRetrive,
|
||||||
|
CalculationTimeConstrainsParametersRead,
|
||||||
|
]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def get_calculation_parameters(
|
||||||
|
db_session: DbSession, calculation_id: Optional[str] = Query(default=None)
|
||||||
|
):
|
||||||
|
"""Get all calculation parameter."""
|
||||||
|
|
||||||
|
parameters = await get_create_calculation_parameters(
|
||||||
|
db_session=db_session, calculation_id=calculation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(
|
||||||
|
data=parameters,
|
||||||
|
message="Data retrieved successfully",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@get_calculation.get(
|
||||||
|
"/{calculation_id}", response_model=StandardResponse[CalculationTimeConstrainsRead]
|
||||||
|
)
|
||||||
|
async def get_calculation_results(
|
||||||
|
db_session: DbSession,
|
||||||
|
calculation_id,
|
||||||
|
token: InternalKey,
|
||||||
|
include_risk_cost: int = Query(1, alias="risk_cost"),
|
||||||
|
):
|
||||||
|
if calculation_id == "default":
|
||||||
|
calculation_id = DEFAULT_TC_ID
|
||||||
|
|
||||||
|
results = await get_calculation_result(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation_id=calculation_id,
|
||||||
|
token=token,
|
||||||
|
include_risk_cost=include_risk_cost,
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(
|
||||||
|
data=results,
|
||||||
|
message="Data retrieved successfully",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{calculation_id}/{assetnum}", response_model=StandardResponse[EquipmentResult]
|
||||||
|
)
|
||||||
|
async def get_calculation_per_equipment(db_session: DbSession, calculation_id, assetnum):
|
||||||
|
results = await get_calculation_by_assetnum(
|
||||||
|
db_session=db_session, assetnum=assetnum, calculation_id=calculation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(
|
||||||
|
data=results,
|
||||||
|
message="Data retrieved successfully",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/{calculation_id}/simulation",
|
||||||
|
response_model=StandardResponse[CalculationResultsRead],
|
||||||
|
)
|
||||||
|
async def get_simulation_result(
|
||||||
|
db_session: DbSession,
|
||||||
|
calculation_id,
|
||||||
|
calculation_simuation_in: CalculationTimeConstrainsCreate,
|
||||||
|
):
|
||||||
|
simulation_result = await get_calculation_result_by_day(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation_id=calculation_id,
|
||||||
|
simulation_day=calculation_simuation_in.intervalDays,
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(
|
||||||
|
data=simulation_result, message="Data retrieved successfully"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/update/{calculation_id}", response_model=StandardResponse[List[str]])
|
||||||
|
async def update_selected_equipment(
|
||||||
|
db_session: DbSession,
|
||||||
|
calculation_id,
|
||||||
|
calculation_time_constrains_in: List[CalculationSelectedEquipmentUpdate],
|
||||||
|
):
|
||||||
|
if calculation_id == "default":
|
||||||
|
calculation_id = "3b9a73a2-bde6-418c-9e2f-19046f501a05"
|
||||||
|
|
||||||
|
results = await bulk_update_equipment(
|
||||||
|
db=db_session,
|
||||||
|
selected_equipments=calculation_time_constrains_in,
|
||||||
|
calculation_data_id=calculation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(
|
||||||
|
data=results,
|
||||||
|
message="Data retrieved successfully",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{calculation_id}/refresh-spareparts", response_model=StandardResponse[dict])
|
||||||
|
async def refresh_spareparts(
|
||||||
|
db_session: DbSession,
|
||||||
|
collector_db_session: CollectorDbSession,
|
||||||
|
calculation_id: str,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
):
|
||||||
|
"""Refresh sparepart availability for an existing calculation"""
|
||||||
|
|
||||||
|
await refresh_spareparts_service(
|
||||||
|
db_session=db_session,
|
||||||
|
collector_db_session=collector_db_session,
|
||||||
|
calculation_id=calculation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(data={}, message="Spareparts refreshed successfully")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{calculation_id}/recalculate", response_model=StandardResponse[CalculationTimeConstrainsRead])
|
||||||
|
async def recalculate_calculation_api(
|
||||||
|
db_session: DbSession,
|
||||||
|
collector_db_session: CollectorDbSession,
|
||||||
|
calculation_id: str,
|
||||||
|
token: Token,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
):
|
||||||
|
"""Recalculate an existing simulation with fresh data"""
|
||||||
|
|
||||||
|
results = await recalculate_calculation(
|
||||||
|
token=token,
|
||||||
|
db_session=db_session,
|
||||||
|
collector_db_session=collector_db_session,
|
||||||
|
calculation_id=calculation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return StandardResponse(data=results, message="Calculation updated with fresh data")
|
||||||
@ -0,0 +1,252 @@
|
|||||||
|
import datetime
|
||||||
|
from typing import Coroutine, List, Optional, Tuple, Dict
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy import and_, case, func, select, update
|
||||||
|
from sqlalchemy.orm import joinedload, selectinload
|
||||||
|
|
||||||
|
from src.database.core import DbSession, CollectorDbSession
|
||||||
|
from src.workorder.model import MasterWorkOrder
|
||||||
|
from src.calculation_time_constrains.model import (CalculationData, CalculationEquipmentResult, CalculationResult)
|
||||||
|
from src.calculation_time_constrains.schema import (
|
||||||
|
CalculationResultsRead,
|
||||||
|
CalculationSelectedEquipmentUpdate,
|
||||||
|
CalculationTimeConstrainsParametersCreate,
|
||||||
|
CalculationTimeConstrainsRead
|
||||||
|
)
|
||||||
|
from src.overhaul_scope.service import get as get_scope, get_prev_oh
|
||||||
|
|
||||||
|
|
||||||
|
async def create_param_and_data(
|
||||||
|
*,
|
||||||
|
db_session: DbSession,
|
||||||
|
calculation_param_in: CalculationTimeConstrainsParametersCreate,
|
||||||
|
created_by: str,
|
||||||
|
parameter_id: Optional[UUID] = None,
|
||||||
|
):
|
||||||
|
"""Creates a new document."""
|
||||||
|
if calculation_param_in.ohSessionId is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="overhaul_session_id is required",
|
||||||
|
)
|
||||||
|
|
||||||
|
calculationData = await CalculationData.create_with_param(
|
||||||
|
db=db_session,
|
||||||
|
overhaul_session_id=calculation_param_in.ohSessionId,
|
||||||
|
avg_failure_cost=calculation_param_in.costPerFailure,
|
||||||
|
overhaul_cost=calculation_param_in.overhaulCost,
|
||||||
|
created_by=created_by,
|
||||||
|
params_id=parameter_id,
|
||||||
|
)
|
||||||
|
return calculationData
|
||||||
|
|
||||||
|
|
||||||
|
async def get_calculation_result(db_session: DbSession, calculation_id: str, token, include_risk_cost):
|
||||||
|
"""
|
||||||
|
Get calculation results from DB, returning pre-calculated plant and equipment results.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get calculation data with equipment results
|
||||||
|
calculation_query = await db_session.execute(
|
||||||
|
select(CalculationData)
|
||||||
|
.options(selectinload(CalculationData.equipment_results))
|
||||||
|
.where(CalculationData.id == calculation_id)
|
||||||
|
)
|
||||||
|
scope_calculation = calculation_query.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not scope_calculation:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Calculation with id {calculation_id} does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
|
scope_overhaul = await get_scope(
|
||||||
|
db_session=db_session,
|
||||||
|
overhaul_session_id=scope_calculation.overhaul_session_id
|
||||||
|
)
|
||||||
|
if not scope_overhaul:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Overhaul scope for session {scope_calculation.overhaul_session_id} does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse pre-calculated plant results
|
||||||
|
plant_results_raw = scope_calculation.plant_results or []
|
||||||
|
calculation_results = [CalculationResultsRead(**r) for r in plant_results_raw]
|
||||||
|
|
||||||
|
# Return comprehensive result
|
||||||
|
return CalculationTimeConstrainsRead(
|
||||||
|
id=scope_calculation.id,
|
||||||
|
reference=scope_calculation.overhaul_session_id,
|
||||||
|
scope=scope_overhaul.maintenance_type.name,
|
||||||
|
results=calculation_results,
|
||||||
|
optimum_oh=scope_calculation.optimum_oh_day,
|
||||||
|
optimum_oh_month=scope_calculation.optimum_oh_day + 1 if scope_calculation.optimum_oh_day is not None else None,
|
||||||
|
equipment_results=scope_calculation.equipment_results,
|
||||||
|
fleet_statistics=scope_calculation.fleet_statistics or {},
|
||||||
|
optimal_analysis=scope_calculation.optimum_analysis or {},
|
||||||
|
analysis_metadata=scope_calculation.analysis_metadata or {}
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.error(f"Error in get_calculation_result for calculation_id {calculation_id}: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Internal error processing calculation results: {str(e)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_calculation_data_by_id(db_session: DbSession, calculation_id) -> CalculationData:
|
||||||
|
stmt = (
|
||||||
|
select(CalculationData)
|
||||||
|
.filter(CalculationData.id == calculation_id)
|
||||||
|
.options(
|
||||||
|
joinedload(CalculationData.equipment_results),
|
||||||
|
joinedload(CalculationData.parameter),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
return result.unique().scalar()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_all_calculations(db_session: DbSession) -> List[CalculationData]:
|
||||||
|
stmt = (
|
||||||
|
select(CalculationData)
|
||||||
|
.options(selectinload(CalculationData.session))
|
||||||
|
.where(
|
||||||
|
CalculationData.optimum_oh_day.isnot(None),
|
||||||
|
CalculationData.max_interval.isnot(None),
|
||||||
|
CalculationData.optimum_analysis.isnot(None),
|
||||||
|
)
|
||||||
|
.order_by(CalculationData.created_at.desc())
|
||||||
|
)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_calculation_by_assetnum(*, db_session: DbSession, assetnum: str, calculation_id: str):
|
||||||
|
stmt = (
|
||||||
|
select(CalculationEquipmentResult)
|
||||||
|
.where(CalculationEquipmentResult.assetnum == assetnum)
|
||||||
|
.where(CalculationEquipmentResult.calculation_data_id == calculation_id)
|
||||||
|
)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
return result.scalar()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_calculation_by_reference_and_parameter(
|
||||||
|
*, db_session: DbSession, calculation_reference_id, parameter_id
|
||||||
|
):
|
||||||
|
stmt = select(CalculationData).filter(
|
||||||
|
and_(
|
||||||
|
CalculationData.reference_id == calculation_reference_id,
|
||||||
|
CalculationData.parameter_id == parameter_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
return result.scalar()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_calculation_result_by_day(*, db_session: DbSession, calculation_id, simulation_day):
|
||||||
|
stmt = select(CalculationData).filter(CalculationData.id == calculation_id)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
calculation_data = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not calculation_data or not calculation_data.plant_results:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for res in calculation_data.plant_results:
|
||||||
|
if res.get("day") == simulation_day:
|
||||||
|
return res
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_avg_cost_by_asset(*, db_session: DbSession, assetnum: str):
|
||||||
|
stmt = select(func.avg(MasterWorkOrder.total_cost_max).label("average_cost")).where(
|
||||||
|
MasterWorkOrder.assetnum == assetnum
|
||||||
|
)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def bulk_update_equipment(
|
||||||
|
*,
|
||||||
|
db: DbSession,
|
||||||
|
selected_equipments: List[CalculationSelectedEquipmentUpdate],
|
||||||
|
calculation_data_id: UUID,
|
||||||
|
):
|
||||||
|
case_mappings = {asset.location_tag: asset.is_included for asset in selected_equipments}
|
||||||
|
location_tags = list(case_mappings.keys())
|
||||||
|
|
||||||
|
when_clauses = [
|
||||||
|
(CalculationEquipmentResult.location_tag == location_tag, is_included)
|
||||||
|
for location_tag, is_included in case_mappings.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
stmt = (
|
||||||
|
update(CalculationEquipmentResult)
|
||||||
|
.where(CalculationEquipmentResult.calculation_data_id == calculation_data_id)
|
||||||
|
.where(CalculationEquipmentResult.location_tag.in_(location_tags))
|
||||||
|
.values(
|
||||||
|
{
|
||||||
|
"is_included": case(*when_clauses),
|
||||||
|
"is_initial": False
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.execute(stmt)
|
||||||
|
await db.commit()
|
||||||
|
return location_tags
|
||||||
|
|
||||||
|
|
||||||
|
async def refresh_spareparts_service(db_session: DbSession, collector_db_session: CollectorDbSession, calculation_id: str):
|
||||||
|
stmt = select(CalculationData).where(CalculationData.id == calculation_id).options(
|
||||||
|
joinedload(CalculationData.equipment_results)
|
||||||
|
)
|
||||||
|
result = await db_session.execute(stmt)
|
||||||
|
calculation = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not calculation:
|
||||||
|
raise HTTPException(status_code=404, detail="Calculation not found")
|
||||||
|
|
||||||
|
scope = await get_scope(db_session=db_session, overhaul_session_id=calculation.overhaul_session_id)
|
||||||
|
prev_oh_scope = await get_prev_oh(db_session=db_session, overhaul_session=scope)
|
||||||
|
|
||||||
|
last_oh_date = prev_oh_scope.end_date
|
||||||
|
next_oh_date = scope.start_date
|
||||||
|
time_window_months = ((next_oh_date.year - last_oh_date.year) * 12 +
|
||||||
|
(next_oh_date.month - last_oh_date.month) + 6)
|
||||||
|
|
||||||
|
from src.sparepart.service import load_sparepart_data_from_db
|
||||||
|
sparepart_manager = await load_sparepart_data_from_db(
|
||||||
|
scope=scope,
|
||||||
|
prev_oh_scope=prev_oh_scope,
|
||||||
|
db_session=collector_db_session,
|
||||||
|
app_db_session=db_session,
|
||||||
|
analysis_window_months=time_window_months
|
||||||
|
)
|
||||||
|
|
||||||
|
for eq in calculation.equipment_results:
|
||||||
|
procurement_details = []
|
||||||
|
procurement_costs = []
|
||||||
|
for month_index in range(time_window_months):
|
||||||
|
status = sparepart_manager.check_sparepart_availability(eq.location_tag, month_index)
|
||||||
|
procurement_details.append(status)
|
||||||
|
|
||||||
|
if not status['available']:
|
||||||
|
procurement_costs.append(status['total_procurement_cost'])
|
||||||
|
else:
|
||||||
|
procurement_costs.append(0.0)
|
||||||
|
|
||||||
|
eq.procurement_details = procurement_details
|
||||||
|
eq.procurement_costs = procurement_costs
|
||||||
|
|
||||||
|
await db_session.commit()
|
||||||
@ -0,0 +1,140 @@
|
|||||||
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
|
import httpx
|
||||||
|
from temporalio import activity, workflow
|
||||||
|
|
||||||
|
from src.config import RBD_SERVICE_API
|
||||||
|
from src.database.core import get_main_session, get_collector_session
|
||||||
|
from src.optimum_time_constraint.service import (
|
||||||
|
create_param_and_data,
|
||||||
|
get_calculation_data_by_id
|
||||||
|
)
|
||||||
|
from src.optimum_time_constraint.optimizer import run_simulation_with_spareparts
|
||||||
|
from src.calculation_time_constrains.schema import CalculationTimeConstrainsParametersCreate
|
||||||
|
from src.overhaul_scope.service import get as get_scope, get_prev_oh
|
||||||
|
from src.calculation_time_constrains.utils import get_months_between
|
||||||
|
from src.calculation_target_reliability.utils import wait_for_workflow
|
||||||
|
|
||||||
|
@activity.defn
|
||||||
|
async def create_optimum_oh_calculation(args: dict) -> str:
|
||||||
|
token = args["token"]
|
||||||
|
calc_in_dict = args["calculation_in"]
|
||||||
|
created_by = args["created_by"]
|
||||||
|
|
||||||
|
calc_in = CalculationTimeConstrainsParametersCreate(**calc_in_dict)
|
||||||
|
|
||||||
|
async with get_main_session() as db_session:
|
||||||
|
calc_data = await create_param_and_data(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation_param_in=calc_in,
|
||||||
|
created_by=created_by
|
||||||
|
)
|
||||||
|
return str(calc_data.id)
|
||||||
|
|
||||||
|
@activity.defn
|
||||||
|
async def request_rbd_simulation(args: dict) -> str:
|
||||||
|
calc_id = args["calc_id"]
|
||||||
|
token = args["token"]
|
||||||
|
|
||||||
|
async with get_main_session() as db_session:
|
||||||
|
calc_data = await get_calculation_data_by_id(db_session=db_session, calculation_id=calc_id)
|
||||||
|
|
||||||
|
scope = await get_scope(db_session=db_session, overhaul_session_id=calc_data.overhaul_session_id)
|
||||||
|
prev_oh_scope = await get_prev_oh(db_session=db_session, overhaul_session=scope)
|
||||||
|
|
||||||
|
time_window_months = get_months_between(prev_oh_scope.end_date, scope.start_date) + 6
|
||||||
|
sim_duration_hours = time_window_months * 720
|
||||||
|
|
||||||
|
sim_input = {
|
||||||
|
"SchematicName": "- TJB - Unit 3 -",
|
||||||
|
"SimulationName": f"OptimumOH_Calc_{calc_id}",
|
||||||
|
"SimDuration": sim_duration_hours,
|
||||||
|
"DurationUnit": "UHour",
|
||||||
|
"OffSet": 0,
|
||||||
|
"SimSeed": 99,
|
||||||
|
"SimNumRun": 1,
|
||||||
|
"OverhaulInterval": 0,
|
||||||
|
"MaintenanceOutages": 0,
|
||||||
|
"IsDefault": False,
|
||||||
|
"OverhaulDuration": 0,
|
||||||
|
"AhmJobId": None
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
resp = await client.post(
|
||||||
|
f"{RBD_SERVICE_API}/aeros/simulation/run",
|
||||||
|
json=sim_input,
|
||||||
|
headers={"Authorization": f"Bearer {token}"}
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
sim_id = resp.json()["data"]
|
||||||
|
|
||||||
|
calc_data.rbd_simulation_id = sim_id
|
||||||
|
return str(sim_id)
|
||||||
|
|
||||||
|
@activity.defn
|
||||||
|
async def wait_for_rbd_simulation(sim_id: str) -> str:
|
||||||
|
await wait_for_workflow(sim_id)
|
||||||
|
return sim_id
|
||||||
|
|
||||||
|
@activity.defn
|
||||||
|
async def run_optimum_oh_calculation(args: dict) -> dict:
|
||||||
|
calc_id = args["calc_id"]
|
||||||
|
token = args["token"]
|
||||||
|
sim_id = args["sim_id"]
|
||||||
|
|
||||||
|
async with get_main_session() as db_session:
|
||||||
|
async with get_collector_session() as collector_db:
|
||||||
|
calc_data = await get_calculation_data_by_id(db_session=db_session, calculation_id=calc_id)
|
||||||
|
results = await run_simulation_with_spareparts(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation=calc_data,
|
||||||
|
token=token,
|
||||||
|
collector_db_session=collector_db,
|
||||||
|
simulation_id=sim_id
|
||||||
|
)
|
||||||
|
return {"id": str(results["id"]), "optimum": results["optimum"]}
|
||||||
|
|
||||||
|
@workflow.defn
|
||||||
|
class OptimumOHCalculationWorkflow:
|
||||||
|
def __init__(self):
|
||||||
|
self.done = False
|
||||||
|
self.result = None
|
||||||
|
|
||||||
|
@workflow.signal
|
||||||
|
def notify_done(self, result: dict):
|
||||||
|
self.done = True
|
||||||
|
self.result = result
|
||||||
|
|
||||||
|
@workflow.run
|
||||||
|
async def run(self, args: dict) -> dict:
|
||||||
|
calc_id = await workflow.execute_activity(
|
||||||
|
create_optimum_oh_calculation,
|
||||||
|
args,
|
||||||
|
start_to_close_timeout=timedelta(minutes=1)
|
||||||
|
)
|
||||||
|
|
||||||
|
args["calc_id"] = calc_id
|
||||||
|
|
||||||
|
sim_id = await workflow.execute_activity(
|
||||||
|
request_rbd_simulation,
|
||||||
|
args,
|
||||||
|
start_to_close_timeout=timedelta(minutes=2)
|
||||||
|
)
|
||||||
|
|
||||||
|
args["sim_id"] = sim_id
|
||||||
|
|
||||||
|
# await workflow.execute_activity(
|
||||||
|
# wait_for_rbd_simulation,
|
||||||
|
# sim_id,
|
||||||
|
# start_to_close_timeout=timedelta(hours=2)
|
||||||
|
# )
|
||||||
|
await workflow.wait_condition(lambda: self.done)
|
||||||
|
|
||||||
|
result = await workflow.execute_activity(
|
||||||
|
run_optimum_oh_calculation,
|
||||||
|
args,
|
||||||
|
start_to_close_timeout=timedelta(minutes=30)
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
@ -0,0 +1,159 @@
|
|||||||
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
|
from temporalio import activity, workflow
|
||||||
|
|
||||||
|
# with workflow.unsafe.imports_passed_through():
|
||||||
|
# import httpx
|
||||||
|
# from src.config import RBD_SERVICE_API
|
||||||
|
# from src.database.core import get_main_session, get_collector_session
|
||||||
|
# from src.optimum_time_constraint.service import (
|
||||||
|
# create_param_and_data,
|
||||||
|
# get_calculation_data_by_id
|
||||||
|
# )
|
||||||
|
# from src.optimum_time_constraint.optimizer import run_simulation_with_spareparts
|
||||||
|
# from src.overhaul_scope.service import get as get_scope, get_prev_oh
|
||||||
|
# from src.calculation_time_constrains.utils import get_months_between
|
||||||
|
# from src.calculation_target_reliability.utils import wait_for_workflow
|
||||||
|
|
||||||
|
@activity.defn
|
||||||
|
async def create_optimum_oh_calculation(args: dict) -> str:
|
||||||
|
from src.calculation_time_constrains.schema import CalculationTimeConstrainsParametersCreate
|
||||||
|
from src.calculation_time_constrains.service import create_param_and_data
|
||||||
|
from src.database.core import get_main_session
|
||||||
|
|
||||||
|
token = args["token"]
|
||||||
|
calc_in_dict = args["calculation_in"]
|
||||||
|
created_by = args["created_by"]
|
||||||
|
|
||||||
|
calc_in = CalculationTimeConstrainsParametersCreate(**calc_in_dict)
|
||||||
|
|
||||||
|
async with get_main_session() as db_session:
|
||||||
|
calc_data = await create_param_and_data(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation_param_in=calc_in,
|
||||||
|
created_by=created_by
|
||||||
|
)
|
||||||
|
return str(calc_data.id)
|
||||||
|
|
||||||
|
@activity.defn
|
||||||
|
async def request_rbd_simulation(args: dict) -> dict:
|
||||||
|
from src.calculation_time_constrains.service import get_calculation_data_by_id
|
||||||
|
from src.database.core import get_main_session
|
||||||
|
from src.overhaul_scope.service import get as get_scope, get_prev_oh
|
||||||
|
from src.calculation_time_constrains.utils import get_months_between
|
||||||
|
from src.calculation_target_reliability.utils import wait_for_workflow
|
||||||
|
import httpx
|
||||||
|
from src.config import RBD_SERVICE_API
|
||||||
|
|
||||||
|
calc_id = args["calc_id"]
|
||||||
|
token = args["token"]
|
||||||
|
callback_workflow_id = args["callback_workflow_id"]
|
||||||
|
|
||||||
|
async with get_main_session() as db_session:
|
||||||
|
calc_data = await get_calculation_data_by_id(db_session=db_session, calculation_id=calc_id)
|
||||||
|
|
||||||
|
scope = await get_scope(db_session=db_session, overhaul_session_id=calc_data.overhaul_session_id)
|
||||||
|
prev_oh_scope = await get_prev_oh(db_session=db_session, overhaul_session=scope)
|
||||||
|
|
||||||
|
# Calculate time window
|
||||||
|
time_window_months = get_months_between(prev_oh_scope.end_date, scope.start_date) + 6
|
||||||
|
sim_duration_hours = time_window_months * 720
|
||||||
|
|
||||||
|
sim_input = {
|
||||||
|
"SchematicName": "- TJB - Unit 3 -",
|
||||||
|
"SimulationName": f"OptimumOH_Calc_{calc_id}",
|
||||||
|
"SimDuration": sim_duration_hours,
|
||||||
|
"DurationUnit": "UHour",
|
||||||
|
"OffSet": 0,
|
||||||
|
"SimSeed": 99,
|
||||||
|
"SimNumRun": 1,
|
||||||
|
"OverhaulInterval": sim_duration_hours + 1,
|
||||||
|
"MaintenanceOutages": 0,
|
||||||
|
"IsDefault": False,
|
||||||
|
"OverhaulDuration": 1200,
|
||||||
|
"AhmJobId": None,
|
||||||
|
"CallbackWorkflowId": callback_workflow_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
resp = await client.post(
|
||||||
|
f"{RBD_SERVICE_API}/aeros/simulation/run",
|
||||||
|
json=sim_input,
|
||||||
|
headers={"Authorization": f"Bearer {token}"}
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
sim_id = resp.json()["data"]
|
||||||
|
|
||||||
|
calc_data.rbd_simulation_id = sim_id
|
||||||
|
return sim_id
|
||||||
|
|
||||||
|
# @activity.defn
|
||||||
|
# async def wait_for_rbd_simulation(sim_id: str) -> str:
|
||||||
|
|
||||||
|
# await wait_for_workflow(sim_id)
|
||||||
|
# return sim_id
|
||||||
|
|
||||||
|
@activity.defn
|
||||||
|
async def run_optimum_oh_calculation(args: dict) -> dict:
|
||||||
|
from src.database.core import get_main_session, get_collector_session
|
||||||
|
from src.calculation_time_constrains.service import get_calculation_data_by_id, run_simulation_with_spareparts
|
||||||
|
from src.config import RBD_SERVICE_API
|
||||||
|
|
||||||
|
calc_id = args["calc_id"]
|
||||||
|
token = args["token"]
|
||||||
|
sim_id = args["sim_id"]
|
||||||
|
|
||||||
|
async with get_main_session() as db_session:
|
||||||
|
async with get_collector_session() as collector_db:
|
||||||
|
calc_data = await get_calculation_data_by_id(db_session=db_session, calculation_id=calc_id)
|
||||||
|
results = await run_simulation_with_spareparts(
|
||||||
|
db_session=db_session,
|
||||||
|
calculation=calc_data,
|
||||||
|
token=token,
|
||||||
|
collector_db_session=collector_db,
|
||||||
|
simulation_id=sim_id,
|
||||||
|
rbd_service_api=RBD_SERVICE_API
|
||||||
|
)
|
||||||
|
# return simplified result since temporal handles JSON
|
||||||
|
return {"id": str(results["id"]), "optimum": results["optimum"]}
|
||||||
|
|
||||||
|
@workflow.defn
|
||||||
|
class OptimumOHCalculationWorkflow:
|
||||||
|
def __init__(self):
|
||||||
|
self.done = False
|
||||||
|
|
||||||
|
@workflow.signal
|
||||||
|
def notify_done(self):
|
||||||
|
self.done = True
|
||||||
|
|
||||||
|
@workflow.run
|
||||||
|
async def run(self, args: dict) -> dict:
|
||||||
|
# 1. Create calculation
|
||||||
|
calc_id = await workflow.execute_activity(
|
||||||
|
create_optimum_oh_calculation,
|
||||||
|
args,
|
||||||
|
start_to_close_timeout=timedelta(minutes=1)
|
||||||
|
)
|
||||||
|
|
||||||
|
args["calc_id"] = calc_id
|
||||||
|
|
||||||
|
# 2. Request RBD simulation
|
||||||
|
sim_id = await workflow.execute_activity(
|
||||||
|
request_rbd_simulation,
|
||||||
|
args,
|
||||||
|
start_to_close_timeout=timedelta(minutes=2)
|
||||||
|
)
|
||||||
|
|
||||||
|
args["sim_id"] = sim_id
|
||||||
|
|
||||||
|
# Wait for RBD simulation to finish using signal
|
||||||
|
await workflow.wait_condition(lambda: self.done)
|
||||||
|
|
||||||
|
# 4. Run Optimum OH calculation
|
||||||
|
result = await workflow.execute_activity(
|
||||||
|
run_optimum_oh_calculation,
|
||||||
|
args,
|
||||||
|
start_to_close_timeout=timedelta(minutes=30)
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
Loading…
Reference in New Issue