@ -64,8 +64,8 @@ class OptimumCostModelWithSpareparts:
# Calculate planned overhaul interval in months
# Calculate planned overhaul interval in months
self . planned_oh_months = self . _get_months_between ( last_oh_date , next_oh_date )
self . planned_oh_months = self . _get_months_between ( last_oh_date , next_oh_date )
# Set analysis time window (default: 1.5x planned interval)
# Set analysis time window : next OH + 6 months
self . time_window_months = time_window_months or int ( self . planned_oh_months * 1.2 )
self . time_window_months = self . planned_oh_months + 6
# Pre-calculate date range for API calls
# Pre-calculate date range for API calls
self . date_range = self . _generate_date_range ( )
self . date_range = self . _generate_date_range ( )
@ -112,13 +112,21 @@ class OptimumCostModelWithSpareparts:
await self . session . close ( )
await self . session . close ( )
self . session = None
self . session = None
async def get_failures_prediction ( self , simulation_id : str , location_tag : str , birnbaum_importance : float , use_location_tag : int = 1 ) :
async def get_failures_prediction ( self , location_tag : str ) :
""" Get failure predictions for equipment from simulation service """
""" Get failure predictions for equipment from Reliability Predict service """
plot_result_url = f " { self . api_base_url } /aeros/simulation/result/plot/ { simulation_id } / { location_tag } ?use_location_tag= { use_location_tag } "
# Calculate start and end dates
start_date = self . last_oh_date . strftime ( ' % Y- % m- %d ' )
# next_oh + 6 months
end_date_val = self . next_oh_date + timedelta ( days = 6 * 30 )
end_date = end_date_val . strftime ( ' % Y- % m- %d ' )
# Use RELIABILITY_SERVICE_API instead of RBD_SERVICE_API
predict_url = f " { REALIBILITY_SERVICE_API } /main/predict/ { location_tag } / { start_date } / { end_date } "
try :
try :
response = requests . get (
response = requests . get (
plot_result_url ,
p redic t_url,
headers = {
headers = {
" Content-Type " : " application/json " ,
" Content-Type " : " application/json " ,
" Authorization " : f " Bearer { self . token } " ,
" Authorization " : f " Bearer { self . token } " ,
@ -131,14 +139,26 @@ class OptimumCostModelWithSpareparts:
self . logger . error ( f " Failed to fetch prediction data for { location_tag } : { e } " )
self . logger . error ( f " Failed to fetch prediction data for { location_tag } : { e } " )
return None
return None
p lot_data = prediction_data . get ( ' data ' , { } ) . get ( ' timestamp_out s' ) if prediction_data . get ( " data " ) else None
p redictions = prediction_data . get ( ' data ' , { } ) . get ( ' prediction s' ) if prediction_data . get ( " data " ) else None
if not p lot_data :
if not p redictions :
self . logger . warning ( f " No p lot data available for { location_tag } " )
self . logger . warning ( f " No p rediction data available for { location_tag } " )
return None
return None
time_series = create_time_series_data ( plot_data , ( self . time_window_months * 24 * 31 ) )
# Convert to dictionary format expected by the model
monthly_data = analyze_monthly_metrics ( time_series , self . last_oh_date )
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
return monthly_data
@ -197,44 +217,59 @@ class OptimumCostModelWithSpareparts:
data = failures_prediction [ month_key ]
data = failures_prediction [ month_key ]
failure_counts . append ( data [ ' cumulative_failures ' ] )
failure_counts . append ( data [ ' cumulative_failures ' ] )
# Calculate costs for each month including sparepart considerations
results = [ ]
for i in range ( num_months ) :
for i in range ( num_months ) :
month_index = i + 1
month_index = i + 1
# Use only months within the analysis window
# Basic failure and preventive costs
if month_index > self . time_window_months :
failure_cost = ( failure_counts [ i ] * ( failure_replacement_cost + monthly_risk_cost_per_failure ) )
continue
preventive_cost_month = preventive_cost / month_index
# Check sparepart availability for this month
# Check sparepart availability for this month
sparepart_analysis = self . _analyze_sparepart_availability (
sparepart_analysis = self . _analyze_sparepart_availability (
location_tag , month_index - 1 , planned_overhauls or [ ]
location_tag , month_index - 1 , planned_overhauls or [ ]
)
)
# THEORY: Total Expected Cost per Unit Time (CPUT)
# Reference: Maintenance Optimization Models (Age/Block Replacement)
# C(T) = [Total Preventive Cost + Total Expected Corrective Cost(T)] / T
# Calculate procurement costs if spareparts are needed
# 1. Total Expected Corrective Cost until month_index (Expected number of failures * cost per failure)
# In NHPP model, Expected Failures E[N(T)] = Cumulative Failures
total_expected_failure_cost = failure_counts [ i ] * ( failure_replacement_cost + monthly_risk_cost_per_failure )
# 2. Total Preventive Cost (One-time cost at month_index)
# Includes labor, materials, and procurement delays
procurement_cost = sparepart_analysis [ ' total_procurement_cost ' ]
procurement_cost = sparepart_analysis [ ' total_procurement_cost ' ]
procurement_details = sparepart_analysis
total_preventive_cost = preventive_cost + procurement_cost
# Adjust total cost based on sparepart availability
# 3. Expected Total Cycle Cost
if not sparepart_analysis [ ' available ' ] :
total_cycle_cost = total_expected_failure_cost + total_preventive_cost
total_cost = failure_cost + preventive_cost_month + procurement_cost
else :
# 4. Expected Cost Per Unit Time (Optimization Target)
# All spareparts availabl e
# This value forms the U-shaped curv e
total_cost = failure_cost + preventive_cost_month + procurement_cost
cput = total_cycle_cost / month_index
# Store both absolute and amortized components for visualization
results . append ( {
results . append ( {
' month ' : month_index ,
' month ' : month_index ,
' number_of_failures ' : failure_counts [ i ] ,
' number_of_failures ' : failure_counts [ i ] ,
' failure_cost ' : failure_cost ,
' is_actual ' : failures_prediction [ months [ i ] ] . get ( ' source ' ) == ' actual ' ,
' preventive_cost ' : preventive_cost_month ,
' procurement_cost ' : procurement_cost ,
# Amortized components (for the "U" chart)
' total_cost ' : total_cost ,
' failure_cost ' : total_expected_failure_cost / month_index ,
' preventive_cost ' : preventive_cost / month_index ,
' procurement_cost ' : procurement_cost / month_index ,
' total_cost ' : cput ,
# Absolute values (for breakdown analysis)
' 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 ,
' is_after_planned_oh ' : month_index > self . planned_oh_months ,
' delay_months ' : max ( 0 , month_index - self . planned_oh_months ) ,
' delay_months ' : max ( 0 , month_index - self . planned_oh_months ) ,
' procurement_details ' : procurement_details ,
' procurement_details ' : sparepart_analysi s,
' sparepart_available ' : sparepart_analysis [ ' available ' ] ,
' sparepart_available ' : sparepart_analysis [ ' available ' ] ,
' sparepart_status ' : sparepart_analysis [ ' message ' ] ,
' sparepart_status ' : sparepart_analysis [ ' message ' ] ,
' can_proceed ' : sparepart_analysis [ ' can_proceed_with_delays ' ]
' can_proceed ' : sparepart_analysis [ ' can_proceed_with_delays ' ]
@ -336,22 +371,9 @@ class OptimumCostModelWithSpareparts:
Calculate optimal overhaul timing for entire fleet considering sparepart constraints
Calculate optimal overhaul timing for entire fleet considering sparepart constraints
"""
"""
self . logger . info ( f " Starting fleet optimization with sparepart management for { len ( equipments ) } equipment " )
self . logger . info ( f " Starting fleet optimization with reliability prediction for { len ( equipments ) } equipment " )
max_interval = self . time_window_months
max_interval = self . time_window_months
total_simulation_period = max_interval
# Get Birnbaum importance values
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 = { }
# Phase 1: Calculate individual optimal timings without considering interactions
# Phase 1: Calculate individual optimal timings without considering interactions
individual_results = { }
individual_results = { }
@ -363,30 +385,20 @@ class OptimumCostModelWithSpareparts:
eq [ " Location " ] : eq
eq [ " Location " ] : eq
for eq in data
for eq in data
}
}
plant_monthly_metrics = await self . get_failures_prediction ( simulation_id = simulation_id , location_tag = " plant " , use_location_tag = 0 , birnbaum_importance = 0 )
REFERENCE_CAPACITY = 630 # or 550
# Loss production estimate (default or from metadata)
COST_PER_MWH = 1_000_000 # rupiah
# 660 MW * 500,000 IDR/MWh * total_downtime (placeholder if RBD is gone)
# We'll use a per-failure risk cost instead if available
# plant_capacity_loss_money = [metrics['derated_mwh'] * COST_PER_MWH for metrics in plant_monthly_metrics.values()]
loss_production = 0 # Will be handled per equipment
# cumulative_loss_money = np.cumsum(plant_capacity_loss_money)
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 * 500_000 ) * ( np . exp ( k * ( loss_production_per_month / total_simulation_period ) ) - 1 ) / ( np . exp ( k ) - 1 )
for equipment in equipments :
for equipment in equipments :
location_tag = equipment . location_tag
location_tag = equipment . location_tag
contribution_factor = equipment_birnbaum . get ( location_tag , 0.0 )
contribution_factor = 1.0 # Default importance without RBD
ecs = ecs_tags . get ( location_tag , None )
ecs = ecs_tags . get ( location_tag , None )
# try:
# Get failure predictions
# # Get failure predictions
monthly_data = await self . get_failures_prediction ( location_tag )
monthly_data = await self . get_failures_prediction ( simulation_id , location_tag , contribution_factor )
if not monthly_data :
if not monthly_data :
continue
continue
@ -430,14 +442,14 @@ class OptimumCostModelWithSpareparts:
# Iteratively improve the plan considering sparepart constraints
# Iteratively improve the plan considering sparepart constraints
improved_plan = self . _optimize_fleet_with_sparepart_constraints (
improved_plan = self . _optimize_fleet_with_sparepart_constraints (
individual_results , equipments , equipment_birnbaum, simulation_id
individual_results , equipments , simulation_id
)
)
# Phase 3: Generate final results and database objects
# Phase 3: Generate final results and database objects
fleet_results = [ ]
fleet_results = [ ]
total_corrective_costs = np . zeros ( max_interval ) + loss_exp [ 0 : max_interval ]
total_corrective_costs = np . zeros ( max_interval )
total_preventive_costs = np . zeros ( max_interval )
total_preventive_costs = np . zeros ( max_interval )
total_procurement_costs = np . zeros ( max_interval )
total_procurement_costs = np . zeros ( max_interval )
total_costs = np . zeros ( max_interval )
total_costs = np . zeros ( max_interval )
@ -481,12 +493,16 @@ class OptimumCostModelWithSpareparts:
total_costs_equipment = pad_array ( total_costs_equipment , max_interval )
total_costs_equipment = pad_array ( total_costs_equipment , max_interval )
procurement_details = pad_array ( procurement_details , 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 )
# Create database result object
# Create database result object
equipment_result = CalculationEquipmentResult (
equipment_result = CalculationEquipmentResult (
corrective_costs = corrective_costs ,
corrective_costs = corrective_costs ,
overhaul_costs = preventive_costs ,
overhaul_costs = preventive_costs ,
procurement_costs = procurement_costs ,
procurement_costs = procurement_costs ,
daily_failures = failures ,
daily_failures = failures ,
is_actual = is_actual_list ,
location_tag = equipment . location_tag ,
location_tag = equipment . location_tag ,
material_cost = equipment . material_cost ,
material_cost = equipment . material_cost ,
service_cost = equipment . service_cost ,
service_cost = equipment . service_cost ,
@ -509,8 +525,8 @@ class OptimumCostModelWithSpareparts:
self . logger . info ( f " Final timing for { location_tag } : Month { equipment_timing } "
self . logger . info ( f " Final timing for { location_tag } : Month { equipment_timing } "
f " (Procurement cost: $ { cost_data [ ' procurement_cost ' ] : ,.2f } ) " )
f " (Procurement cost: $ { cost_data [ ' procurement_cost ' ] : ,.2f } ) " )
# Calculate fleet optimal interval
# Calculate fleet optimal interval (Month T that minimizes the sum of all equipment CPUTs)
fleet_optimal_index = np . argmin ( total_co rrective_costs + total_preventive_co sts)
fleet_optimal_index = np . argmin ( total_co sts)
fleet_optimal_cost = total_costs [ fleet_optimal_index ]
fleet_optimal_cost = total_costs [ fleet_optimal_index ]
# Generate procurement optimization report
# Generate procurement optimization report
@ -534,7 +550,7 @@ class OptimumCostModelWithSpareparts:
return fleet_optimal_index
return fleet_optimal_index
def _optimize_fleet_with_sparepart_constraints ( self , individual_results : Dict , equipments : List ,
def _optimize_fleet_with_sparepart_constraints ( self , individual_results : Dict , equipments : List ,
equipment_birnbaum: Dict , simulation_id: str ) - > List [ Tuple [ str , int ] ] :
simulation_id: str ) - > List [ Tuple [ str , int ] ] :
"""
"""
Optimize fleet overhaul timing considering sparepart sharing constraints
Optimize fleet overhaul timing considering sparepart sharing constraints
"""
"""
@ -677,6 +693,7 @@ async def run_simulation_with_spareparts(*, db_session, calculation, token: str,
collector_db = collector_db_session
collector_db = collector_db_session
)
)
scope = await get_scope ( 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 )
prev_oh_scope = await get_prev_oh ( db_session = db_session , overhaul_session = scope )
@ -684,11 +701,11 @@ async def run_simulation_with_spareparts(*, db_session, calculation, token: str,
db_session = db_session , calculation_id = calculation . id
db_session = db_session , calculation_id = calculation . id
)
)
time_window_months = 60
# time window is months between prev oh -> next oh + 6
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 )
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 )
# Initialize optimization model with sparepart management
# Initialize optimization model with sparepart management
optimum_oh_model = OptimumCostModelWithSpareparts (
optimum_oh_model = OptimumCostModelWithSpareparts (
token = token ,
token = token ,
@ -824,7 +841,6 @@ async def get_calculation_result(db_session: DbSession, calculation_id: str, tok
# Filter included equipment for performance
# Filter included equipment for performance
included_equipment = [ eq for eq in scope_calculation . equipment_results if eq . is_included ]
included_equipment = [ eq for eq in scope_calculation . equipment_results if eq . is_included ]
all_equipment = scope_calculation . equipment_results
all_equipment = scope_calculation . equipment_results
# all_spareparts = await get_spareparts_paginated(db_session=db_session, collector_db_session=collector_db_session )
# Pre-calculate aggregated statistics
# Pre-calculate aggregated statistics
calculation_results = [ ]
calculation_results = [ ]
@ -838,30 +854,11 @@ async def get_calculation_result(db_session: DbSession, calculation_id: str, tok
' total_spareparts ' : 745
' total_spareparts ' : 745
}
}
# The code `plant_monthly_metrics` is likely a function or a variable in Python. Without
# Process each month
# seeing the implementation of the code, it is not possible to determine exactly what it is
# doing. It could be used to store monthly metrics for a plant, calculate metrics, or perform
# some other operation related to plant data.
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 * 500_000 * include_risk_cost ) * ( np . exp ( k * ( loss_production_per_month / total_simulation_period ) ) - 1 ) / ( np . exp ( k ) - 1 )
# REFERENCE_CAPACITY = 630 # or 550
# COST_PER_MWH = 1_000_000 # rupiah
# plant_capacity_loss_money = [metrics['derated_mwh'] * COST_PER_MWH for metrics in plant_monthly_metrics.values()]
# cumulative_loss_money = np.cumsum(plant_capacity_loss_money)
# # raise Exception(plant_monthly_metrics)
# Process each monthself
for month_index in range ( data_num ) :
for month_index in range ( data_num ) :
month_result = {
month_result = {
" overhaul_cost " : 0.0 ,
" overhaul_cost " : 0.0 ,
" corrective_cost " : float ( loss_exp [ month_index ] ) ,
" corrective_cost " : 0.0 ,
" procurement_cost " : 0.0 ,
" procurement_cost " : 0.0 ,
" num_failures " : 0.0 ,
" num_failures " : 0.0 ,
" day " : month_index + 1 ,
" day " : month_index + 1 ,
@ -988,6 +985,14 @@ async def get_calculation_result(db_session: DbSession, calculation_id: str, tok
if detail and isinstance ( detail , dict )
if detail and isinstance ( detail , dict )
] )
] )
# Calculate metadata for the chart
analysis_metadata = {
" planned_month " : ( scope_overhaul . start_date . year - prev_oh_scope . end_date . year ) * 12 + ( scope_overhaul . start_date . month - prev_oh_scope . end_date . month ) if prev_oh_scope and scope_overhaul 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_overhaul . start_date . isoformat ( ) if scope_overhaul else None
}
# Calculate optimal timing analysis
# Calculate optimal timing analysis
optimal_analysis = _analyze_optimal_timing (
optimal_analysis = _analyze_optimal_timing (
calculation_results , scope_calculation . optimum_oh_day , prev_oh_scope , scope_overhaul
calculation_results , scope_calculation . optimum_oh_day , prev_oh_scope , scope_overhaul
@ -1004,15 +1009,7 @@ async def get_calculation_result(db_session: DbSession, calculation_id: str, tok
equipment_results = scope_calculation . equipment_results ,
equipment_results = scope_calculation . equipment_results ,
fleet_statistics = fleet_statistics ,
fleet_statistics = fleet_statistics ,
optimal_analysis = optimal_analysis ,
optimal_analysis = optimal_analysis ,
analysis_metadata = {
analysis_metadata = 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 :
except HTTPException :
@ -1490,3 +1487,53 @@ async def bulk_update_equipment(
await db . commit ( )
await db . commit ( )
return location_tags
return location_tags
async def refresh_spareparts_service ( db_session : DbSession , collector_db_session : CollectorDbSession , calculation_id : str ) :
# 1. Fetch calculation data
stmt = select ( CalculationData ) . where ( CalculationData . id == calculation_id ) . options (
joinedload ( CalculationData . equipment_results )
)
result = await db_session . execute ( stmt )
calculation = result . scalar_one_or_none ( )
if not calculation :
raise HTTPException ( status_code = 404 , detail = " Calculation not found " )
# 2. Get prev_oh_scope and scope (needed for load_sparepart_data_from_db)
from src . overhaul_scope . service import get as get_scope , get_prev_oh
scope = await get_scope ( db_session = db_session , id = calculation . overhaul_session_id )
prev_oh_scope = await get_prev_oh ( db_session = db_session , session_id = calculation . overhaul_session_id )
# 3. Calculate time window
last_oh_date = prev_oh_scope . end_date
next_oh_date = scope . start_date
time_window_months = ( ( next_oh_date . year - last_oh_date . year ) * 12 +
( next_oh_date . month - last_oh_date . month ) + 6 )
# 4. Load sparepart manager with fresh data from DB
from src . sparepart . service import load_sparepart_data_from_db
sparepart_manager = await load_sparepart_data_from_db (
scope = scope ,
prev_oh_scope = prev_oh_scope ,
db_session = collector_db_session ,
app_db_session = db_session ,
analysis_window_months = time_window_months
)
# 5. For each equipment and each month, refresh availability
for eq in calculation . equipment_results :
procurement_details = [ ]
procurement_costs = [ ]
for month_index in range ( time_window_months ) :
status = sparepart_manager . check_sparepart_availability ( eq . location_tag , month_index )
procurement_details . append ( status )
if not status [ ' available ' ] :
procurement_costs . append ( status [ ' total_procurement_cost ' ] )
else :
procurement_costs . append ( 0.0 )
eq . procurement_details = procurement_details
eq . procurement_costs = procurement_costs
await db_session . commit ( )