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.
918 lines
36 KiB
Python
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}
|