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.
609 lines
45 KiB
Python
609 lines
45 KiB
Python
import datetime
|
|
from typing import List, Optional, Tuple, Dict
|
|
from uuid import UUID
|
|
import calendar
|
|
import httpx
|
|
from src.config import REALIBILITY_SERVICE_API, RBD_SERVICE_API
|
|
import numpy as np
|
|
import requests
|
|
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
|
|
from src.overhaul_activity.service import get_all as get_all_by_session_id
|
|
from src.overhaul_scope.service import get as get_scope, get_prev_oh
|
|
from src.sparepart.service import load_sparepart_data_from_db
|
|
from src.workorder.model import MasterWorkOrder
|
|
from .model import CalculationData, CalculationEquipmentResult, CalculationResult
|
|
from .schema import CalculationResultsRead, CalculationSelectedEquipmentUpdate, CalculationTimeConstrainsParametersCreate, CalculationTimeConstrainsRead, OptimumResult
|
|
from .utils import analyze_monthly_metrics, create_time_series_data, get_months_between, plant_simulation_metrics
|
|
import math
|
|
from src.overhaul_activity.service import get_standard_scope_by_session_id
|
|
from datetime import timedelta
|
|
import logging
|
|
import aiohttp
|
|
from datetime import datetime, date
|
|
import asyncio
|
|
import json
|
|
from src.config import BASE_URL, REALIBILITY_SERVICE_API
|
|
client = httpx.AsyncClient(timeout=300.0)
|
|
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=f'{BASE_URL}'):
|
|
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
|
|
self.planned_oh_months = self._get_months_between(last_oh_date, next_oh_date)
|
|
self.time_window_months = time_window_months or int(self.planned_oh_months * 1.2)
|
|
self.date_range = self._generate_date_range()
|
|
self.logger = log
|
|
|
|
def _get_months_between(self, start_date: date, end_date: date) -> int:
|
|
return (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month)
|
|
|
|
def _generate_date_range(self) -> List[datetime]:
|
|
dates = []
|
|
current_date = datetime.combine(self.last_oh_date, 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):
|
|
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):
|
|
if self.session:
|
|
await self.session.close()
|
|
self.session = None
|
|
|
|
async def get_failures_prediction(self, simulation_id: str, location_tag: str, birnbaum_importance: float, use_location_tag: int=1):
|
|
plot_result_url = f'{self.api_base_url}/aeros/simulation/result/plot/{simulation_id}/{location_tag}?use_location_tag={use_location_tag}'
|
|
try:
|
|
response = requests.get(plot_result_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
|
|
plot_data = prediction_data.get('data', {}).get('timestamp_outs') if prediction_data.get('data') else None
|
|
if not plot_data:
|
|
self.logger.warning(f'No plot data available for {location_tag}')
|
|
return None
|
|
time_series = create_time_series_data(plot_data, self.time_window_months * 24 * 31)
|
|
monthly_data = analyze_monthly_metrics(time_series, self.last_oh_date)
|
|
return monthly_data
|
|
|
|
async def get_simulation_results(self, simulation_id: str='default'):
|
|
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]:
|
|
if not failures_prediction:
|
|
self.logger.warning(f'No failure prediction data for {location_tag}')
|
|
return []
|
|
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'])
|
|
results = []
|
|
for i in range(num_months):
|
|
month_index = i + 1
|
|
failure_cost = failure_counts[i] * (failure_replacement_cost + monthly_risk_cost_per_failure)
|
|
preventive_cost_month = preventive_cost / month_index
|
|
sparepart_analysis = self._analyze_sparepart_availability(location_tag, month_index - 1, planned_overhauls or [])
|
|
procurement_cost = sparepart_analysis['total_procurement_cost']
|
|
procurement_details = sparepart_analysis
|
|
if not sparepart_analysis['available']:
|
|
total_cost = failure_cost + preventive_cost_month + procurement_cost
|
|
else:
|
|
total_cost = failure_cost + preventive_cost_month + procurement_cost
|
|
results.append({'month': month_index, 'number_of_failures': failure_counts[i], 'failure_cost': failure_cost, 'preventive_cost': preventive_cost_month, 'procurement_cost': procurement_cost, 'total_cost': total_cost, 'is_after_planned_oh': month_index > self.planned_oh_months, 'delay_months': max(0, month_index - self.planned_oh_months), 'procurement_details': procurement_details, '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:
|
|
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]:
|
|
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:
|
|
planned_cost = None
|
|
cost_vs_planned = None
|
|
if self.planned_oh_months <= len(optimal_result.get('all_monthly_costs', [])):
|
|
pass
|
|
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': planned_cost, 'cost_vs_planned': cost_vs_planned, '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'):
|
|
self.logger.info(f'Starting fleet optimization with sparepart management for {len(equipments)} equipment')
|
|
max_interval = self.time_window_months
|
|
try:
|
|
importance_results = await self.get_simulation_results(simulation_id)
|
|
equipment_birnbaum = {imp['aeros_node']['node_name']: imp['contribution_factor'] for imp in importance_results['calc_result']}
|
|
loss_production = importance_results['plant_result']['total_downtime'] * 660
|
|
except Exception as e:
|
|
self.logger.error(f'Failed to get simulation results: {e}')
|
|
equipment_birnbaum = {}
|
|
individual_results = {}
|
|
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}
|
|
await self.get_failures_prediction(simulation_id=simulation_id, location_tag='plant', use_location_tag=0, birnbaum_importance=0)
|
|
total_simulation_period = int((importance_results['plant_result']['total_downtime'] + importance_results['plant_result']['total_uptime']) / 720)
|
|
loss_production_per_month = np.arange(0, total_simulation_period)
|
|
k = 5
|
|
loss_exp = importance_results['plant_result']['total_downtime'] * 660 * 500000 * (np.exp(k * (loss_production_per_month / total_simulation_period)) - 1) / (np.exp(k) - 1)
|
|
for equipment in equipments:
|
|
location_tag = equipment.location_tag
|
|
contribution_factor = equipment_birnbaum.get(location_tag, 0.0)
|
|
ecs = ecs_tags.get(location_tag, None)
|
|
monthly_data = await self.get_failures_prediction(simulation_id, location_tag, contribution_factor)
|
|
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=loss_production)
|
|
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']}")
|
|
self.logger.info('Phase 2: Optimizing with sparepart interactions...')
|
|
[(tag, result['optimal_month']) for tag, result in individual_results.items()]
|
|
improved_plan = self._optimize_fleet_with_sparepart_constraints(individual_results, equipments, equipment_birnbaum, simulation_id)
|
|
fleet_results = []
|
|
total_corrective_costs = np.zeros(max_interval) + loss_exp[0: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)
|
|
equipment_result = CalculationEquipmentResult(corrective_costs=corrective_costs, overhaul_costs=preventive_costs, procurement_costs=procurement_costs, daily_failures=failures, 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']
|
|
self.logger.info(f"Final timing for {location_tag}: Month {equipment_timing} (Procurement cost: ${cost_data['procurement_cost']:,.2f})")
|
|
fleet_optimal_index = np.argmin(total_corrective_costs + total_preventive_costs)
|
|
fleet_optimal_cost = total_costs[fleet_optimal_index]
|
|
self.sparepart_manager.optimize_procurement_timing(improved_plan)
|
|
calculation.optimum_oh_day = fleet_optimal_index
|
|
calculation.max_interval = max_interval
|
|
calculation.rbd_simulation_id = simulation_id
|
|
db_session.add_all(fleet_results)
|
|
await db_session.commit()
|
|
self.logger.info(f'Fleet optimization with spareparts completed:')
|
|
self.logger.info(f' - Fleet optimal month: {fleet_optimal_index + 1}')
|
|
self.logger.info(f' - Fleet optimal cost: ${fleet_optimal_cost:,.2f}')
|
|
self.logger.info(f' - Total procurement cost: ${total_fleet_procurement_cost:,.2f}')
|
|
self.logger.info(f" - Equipment with sparepart constraints: {len([r for r in individual_results.values() if not r['sparepart_available']])}")
|
|
return fleet_optimal_index
|
|
|
|
def _optimize_fleet_with_sparepart_constraints(self, individual_results: Dict, equipments: List, equipment_birnbaum: Dict, simulation_id: str) -> List[Tuple[str, int]]:
|
|
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))
|
|
self.logger.info(f'Confirmed optimal timing for {equipment_tag}: Month {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))
|
|
self.logger.info(f'Alternative timing for {equipment_tag}: Month {alternative_month} (was {optimal_month})')
|
|
else:
|
|
improved_plan.append((equipment_tag, optimal_month))
|
|
processed_equipment.append((equipment_tag, optimal_month))
|
|
self.logger.warning(f'Forced timing for {equipment_tag}: Month {optimal_month} (requires emergency procurement)')
|
|
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]:
|
|
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]
|
|
|
|
def generate_sparepart_report(self, results: Dict) -> str:
|
|
individual_results = results['individual_results']
|
|
procurement_plan = results['procurement_plan']
|
|
report = f"\n SPAREPART ANALYSIS REPORT\n {'=' * 50}\n\n FLEET SUMMARY:\n - Total equipment analyzed: {len(individual_results)}\n - Total procurement cost: ${results['total_fleet_procurement_cost']:,.2f}\n - Equipment requiring procurement: {len([r for r in individual_results.values() if r['procurement_cost'] > 0])}\n\n PROCUREMENT SUMMARY:\n - Total procurement items: {(procurement_plan['summary']['total_items'] if 'summary' in procurement_plan else 0)}\n - Critical items: {(procurement_plan['summary']['critical_items'] if 'summary' in procurement_plan else 0)}\n - Unique spareparts: {(procurement_plan['summary']['unique_spareparts'] if 'summary' in procurement_plan else 0)}\n - Suppliers involved: {(procurement_plan['summary']['suppliers_involved'] if 'summary' in procurement_plan else 0)}\n\n EQUIPMENT DETAILS:\n "
|
|
for tag, result in individual_results.items():
|
|
status = '✓ Available' if result['sparepart_available'] else '⚠ Procurement needed'
|
|
report += f"- {tag}: Month {result['optimal_month']} - {status}"
|
|
if result['procurement_cost'] > 0:
|
|
report += f" (${result['procurement_cost']:,.2f})"
|
|
report += '\n'
|
|
if procurement_plan.get('procurement_by_month'):
|
|
report += '\nPROCUREMENT SCHEDULE:\n'
|
|
for month, items in procurement_plan['procurement_by_month'].items():
|
|
month_cost = sum((item['total_cost'] for item in items))
|
|
report += f'Month {month + 1}: {len(items)} items - ${month_cost:,.2f}\n'
|
|
return report
|
|
|
|
async def __aenter__(self):
|
|
await self._create_session()
|
|
return self
|
|
|
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
await self._close_session()
|
|
|
|
async def run_simulation_with_spareparts(*, db_session, calculation, token: str, collector_db_session, time_window_months: Optional[int]=None, simulation_id: str='default') -> Dict:
|
|
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 = 60
|
|
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)
|
|
simulation_ids = [simulation_id]
|
|
try:
|
|
optimum_collection = []
|
|
for sim_id in simulation_ids:
|
|
results = 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=sim_id)
|
|
optimum_collection.append(results)
|
|
finally:
|
|
await optimum_oh_model._close_session()
|
|
np_optimum = np.array(optimum_collection)
|
|
stats = {'min': float(np.min(np_optimum)), 'max': float(np.max(np_optimum)), 'mean': float(np.mean(np_optimum)), 'median': float(np.median(np_optimum)), 'std': float(np.std(np_optimum)), 'variance': float(np.var(np_optimum)), 'range': float(np.ptp(np_optimum))}
|
|
calculation_data.optimum_analysis = stats
|
|
await db_session.commit()
|
|
return {'id': calculation_data.id, 'optimum': stats}
|
|
|
|
async def create_param_and_data(*, db_session: DbSession, calculation_param_in: CalculationTimeConstrainsParametersCreate, created_by: str, parameter_id: Optional[UUID]=None):
|
|
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):
|
|
try:
|
|
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.')
|
|
prev_oh_scope = await get_prev_oh(db_session=db_session, overhaul_session=scope_overhaul)
|
|
data_num = scope_calculation.max_interval
|
|
if data_num <= 0:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid max_interval in calculation data.')
|
|
included_equipment = [eq for eq in scope_calculation.equipment_results if eq.is_included]
|
|
all_equipment = scope_calculation.equipment_results
|
|
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}
|
|
plant_monthly_metrics = await plant_simulation_metrics(simulation_id=scope_calculation.rbd_simulation_id, location_tag='plant', use_location_tag=0, token=token, last_oh_date=prev_oh_scope.end_date, max_interval=scope_calculation.max_interval)
|
|
total_simulation_period = int((plant_monthly_metrics['total_downtime'] + plant_monthly_metrics['total_uptime']) / 720)
|
|
loss_production_per_month = np.arange(0, total_simulation_period)
|
|
k = 5
|
|
loss_exp = plant_monthly_metrics['total_downtime'] * 660 * 500000 * include_risk_cost * (np.exp(k * (loss_production_per_month / total_simulation_period)) - 1) / (np.exp(k) - 1)
|
|
for month_index in range(data_num):
|
|
month_result = {'overhaul_cost': 0.0, 'corrective_cost': float(loss_exp[month_index]), 'procurement_cost': 0.0, 'num_failures': 0.0, '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.corrective_costs) and month_index < len(eq.overhaul_costs) and (month_index < len(eq.procurement_costs)) and (month_index < len(eq.daily_failures)):
|
|
month_result['corrective_cost'] += float(eq.corrective_costs[month_index])
|
|
month_result['overhaul_cost'] += float(eq.overhaul_costs[month_index])
|
|
month_result['procurement_cost'] += float(eq.procurement_costs[month_index])
|
|
month_result['num_failures'] += float(eq.daily_failures[month_index])
|
|
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']
|
|
calculation_results.append(CalculationResultsRead(**month_result))
|
|
optimum_day = np.argmin([month.total_cost for month in calculation_results])
|
|
scope_calculation.optimum_oh_day = int(optimum_day)
|
|
await db_session.commit()
|
|
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)])
|
|
optimal_analysis = _analyze_optimal_timing(calculation_results, scope_calculation.optimum_oh_day, prev_oh_scope, scope_overhaul)
|
|
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, equipment_results=scope_calculation.equipment_results, fleet_statistics=fleet_statistics, optimal_analysis=optimal_analysis, analysis_metadata={'max_interval_months': data_num, 'last_overhaul_date': prev_oh_scope.end_date.isoformat() if prev_oh_scope else None, 'next_planned_overhaul': scope_overhaul.start_date.isoformat(), 'calculation_type': 'sparepart_optimized' if fleet_statistics['equipment_with_sparepart_constraints'] > 0 else 'standard', 'total_equipment_analyzed': len(all_equipment), 'included_in_optimization': len(included_equipment), 'optimal_stat': scope_calculation.optimum_analysis})
|
|
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)}')
|
|
|
|
def _analyze_optimal_timing(calculation_results: List, optimum_oh_day: int, prev_oh_scope, scope_overhaul) -> Dict:
|
|
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 = (scope_overhaul.start_date.year - prev_oh_scope.end_date.year) * 12 + (scope_overhaul.start_date.month - prev_oh_scope.end_date.month)
|
|
min(calculation_results, key=lambda x: x.total_cost)
|
|
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 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_number_of_failures(location_tag, start_date, end_date, token, max_interval=24):
|
|
url_prediction = f"{REALIBILITY_SERVICE_API}/main/number-of-failures/{location_tag}/{start_date.strftime('%Y-%m-%d')}/{end_date.strftime('%Y-%m-%d')}"
|
|
results = {}
|
|
try:
|
|
response = requests.get(url_prediction, headers={'Content-Type': 'application/json', 'Authorization': f'Bearer {token}'}, timeout=10)
|
|
response.raise_for_status()
|
|
prediction_data = response.json()
|
|
except (requests.RequestException, ValueError) as e:
|
|
raise Exception(f'Failed to fetch or parse prediction data: {e}')
|
|
if not prediction_data or 'data' not in prediction_data or (not isinstance(prediction_data['data'], list)):
|
|
raise Exception('Invalid or empty prediction data format.')
|
|
last_data = prediction_data['data'][-1]
|
|
last_data_date = datetime.strptime(last_data['date'], '%d %b %Y')
|
|
results[datetime.date(last_data_date.year, last_data_date.month, last_data_date.day)] = round(last_data['num_fail']) if last_data['num_fail'] is not None else 0
|
|
for item in prediction_data['data']:
|
|
try:
|
|
date = datetime.strptime(item['date'], '%d %b %Y')
|
|
last_day = calendar.monthrange(date.year, date.month)[1]
|
|
value = item.get('num_fail', 0)
|
|
if date.day == last_day:
|
|
if date.month == start_date.month and date.year == start_date.year:
|
|
results[date.date()] = 0
|
|
else:
|
|
results[date.date()] = 0 if value <= 0 else int(value)
|
|
except (KeyError, ValueError):
|
|
continue
|
|
current = start_date.replace(day=1)
|
|
for _ in range(max_interval):
|
|
last_day = calendar.monthrange(current.year, current.month)[1]
|
|
last_day_date = datetime.date(current.year, current.month, last_day)
|
|
if last_day_date not in results:
|
|
results[last_day_date] = 0
|
|
if current.month == 12:
|
|
current = current.replace(year=current.year + 1, month=1)
|
|
else:
|
|
current = current.replace(month=current.month + 1)
|
|
results = dict(sorted(results.items()))
|
|
return results
|
|
|
|
async def get_equipment_foh(location_tag: str, token: str):
|
|
url_mdt = f'{REALIBILITY_SERVICE_API}/asset/mdt/{location_tag}'
|
|
try:
|
|
response = requests.get(url_mdt, headers={'Content-Type': 'application/json', 'Authorization': f'Bearer {token}'}, timeout=10)
|
|
response.raise_for_status()
|
|
result = response.json()
|
|
except (requests.RequestException, ValueError) as e:
|
|
raise Exception(f'Failed to fetch or parse mdt data: {e}')
|
|
mdt_data = result['data']['hours']
|
|
return mdt_data
|
|
|
|
def simulate_equipment_overhaul(equipment, preventive_cost, predicted_num_failures, interval_months, forced_outage_hours_value, total_months=24):
|
|
total_preventive_cost = 0
|
|
total_corrective_cost = 0
|
|
months_since_overhaul = 0
|
|
failures_by_month = {i: val for i, (date, val) in enumerate(sorted(predicted_num_failures.items()))}
|
|
cost_per_failure = equipment.material_cost
|
|
for month in range(total_months):
|
|
if months_since_overhaul >= interval_months:
|
|
total_preventive_cost += preventive_cost
|
|
months_since_overhaul = 0
|
|
if months_since_overhaul == 0:
|
|
expected_failures = 0
|
|
equivalent_force_derated_hours = 0
|
|
failure_cost = expected_failures * cost_per_failure + (forced_outage_hours_value + equivalent_force_derated_hours) * equipment.service_cost
|
|
total_corrective_cost += failure_cost
|
|
else:
|
|
expected_failures = failures_by_month.get(months_since_overhaul, 0)
|
|
equivalent_force_derated_hours = 0
|
|
failure_cost = expected_failures * cost_per_failure + (forced_outage_hours_value + equivalent_force_derated_hours) * equipment.service_cost
|
|
total_corrective_cost += failure_cost
|
|
months_since_overhaul += 1
|
|
monthly_preventive_cost = total_preventive_cost / total_months
|
|
monthly_corrective_cost = total_corrective_cost / total_months
|
|
monthly_total_cost = monthly_preventive_cost + monthly_corrective_cost
|
|
return {'interval': interval_months, 'preventive_cost': monthly_preventive_cost, 'corrective_cost': monthly_corrective_cost, 'total_cost': monthly_total_cost}
|
|
|
|
async def create_calculation_result_service(db_session: DbSession, calculation: CalculationData, token: str) -> CalculationTimeConstrainsRead:
|
|
equipments = await get_all_by_session_id(db_session=db_session, overhaul_session_id=calculation.overhaul_session_id)
|
|
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)
|
|
if prev_oh_scope:
|
|
start_date = datetime.combine(prev_oh_scope.end_date + datetime.timedelta(days=1), datetime.time.min)
|
|
end_date = datetime.combine(scope.start_date, datetime.time.min)
|
|
else:
|
|
start_date = datetime.combine(scope.start_date, datetime.time.min)
|
|
end_date = datetime.combine(scope.end_date, datetime.time.min)
|
|
max_interval = get_months_between(start_date, end_date)
|
|
overhaul_cost = calculation_data.parameter.overhaul_cost / len(equipments)
|
|
results = []
|
|
total_corrective_costs = np.zeros(max_interval)
|
|
total_overhaul_costs = np.zeros(max_interval)
|
|
total_daily_failures = np.zeros(max_interval)
|
|
total_costs = np.zeros(max_interval)
|
|
for eq in equipments:
|
|
equipment_results = []
|
|
corrective_costs = []
|
|
overhaul_costs = []
|
|
total = []
|
|
predicted_num_failures = await get_number_of_failures(location_tag=eq.location_tag, start_date=start_date, end_date=end_date, token=token)
|
|
foh_value = await get_equipment_foh(location_tag=eq.location_tag, token=token)
|
|
for interval in range(1, max_interval + 1):
|
|
result = simulate_equipment_overhaul(eq, overhaul_cost, predicted_num_failures, interval, foh_value, total_months=max_interval)
|
|
corrective_costs.append(result['corrective_cost'])
|
|
overhaul_costs.append(result['preventive_cost'])
|
|
total.append(result['total_cost'])
|
|
equipment_results.append(result)
|
|
optimal_result = min(equipment_results, key=lambda x: x['total_cost'])
|
|
results.append(CalculationEquipmentResult(corrective_costs=corrective_costs, overhaul_costs=overhaul_costs, daily_failures=[failure for _, failure in predicted_num_failures.items()], assetnum=eq.assetnum, material_cost=eq.material_cost, service_cost=eq.service_cost, optimum_day=optimal_result['interval'], calculation_data_id=calculation.id, master_equipment=eq.master_equipment))
|
|
if len(predicted_num_failures.values()) < max_interval:
|
|
raise Exception(eq.equipment.assetnum)
|
|
total_corrective_costs += np.array(corrective_costs)
|
|
total_overhaul_costs += np.array(overhaul_costs)
|
|
total_daily_failures += np.array([failure for _, failure in predicted_num_failures.items()])
|
|
total_costs += np.array(total_costs)
|
|
db_session.add_all(results)
|
|
total_costs_point = total_corrective_costs + total_overhaul_costs
|
|
optimum_oh_index = np.argmin(total_costs_point)
|
|
numbers_of_failure = sum(total_daily_failures[:optimum_oh_index])
|
|
optimum = OptimumResult(overhaul_cost=float(total_overhaul_costs[optimum_oh_index]), corrective_cost=float(total_corrective_costs[optimum_oh_index]), num_failures=int(numbers_of_failure), days=int(optimum_oh_index + 1))
|
|
calculation.optimum_oh_day = optimum.days
|
|
await db_session.commit()
|
|
return CalculationTimeConstrainsRead(id=calculation.id, reference=calculation.overhaul_session_id, scope=scope.type, results=[], optimum_oh=optimum, equipment_results=results)
|
|
|
|
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(CalculationResult).filter(and_(CalculationResult.day == simulation_day, CalculationResult.calculation_data_id == calculation_id))
|
|
result = await db_session.execute(stmt)
|
|
return result.scalar()
|
|
|
|
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
|