You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
be-optimumoh/src/optimum_time_constraint/optimizer.py

918 lines
36 KiB
Python

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}