|
|
|
@ -41,6 +41,8 @@ from datetime import datetime, date
|
|
|
|
import asyncio
|
|
|
|
import asyncio
|
|
|
|
import json
|
|
|
|
import json
|
|
|
|
# from src.utils import save_to_pastebin
|
|
|
|
# from src.utils import save_to_pastebin
|
|
|
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client = httpx.AsyncClient(timeout=300.0)
|
|
|
|
client = httpx.AsyncClient(timeout=300.0)
|
|
|
|
|
|
|
|
|
|
|
|
@ -677,6 +679,7 @@ class OptimumCostModelWithSpareparts:
|
|
|
|
await self._close_session()
|
|
|
|
await self._close_session()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Service to calculate equipment data chart
|
|
|
|
# Service to calculate equipment data chart
|
|
|
|
async def calculate_equipment_data_chart(
|
|
|
|
async def calculate_equipment_data_chart(
|
|
|
|
*,
|
|
|
|
*,
|
|
|
|
@ -739,6 +742,29 @@ async def calculate_equipment_data_chart(
|
|
|
|
await optimum_oh_model._close_session()
|
|
|
|
await optimum_oh_model._close_session()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def npv_formula(rate, values):
|
|
|
|
|
|
|
|
res = 0
|
|
|
|
|
|
|
|
for i, val in enumerate(values):
|
|
|
|
|
|
|
|
res += val / ((1 + rate) ** (i + 1))
|
|
|
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pmt_formula(rate, nper, pv_val, fv_val=0):
|
|
|
|
|
|
|
|
if nper == 0:
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
if rate == 0:
|
|
|
|
|
|
|
|
return -(pv_val + fv_val) / nper
|
|
|
|
|
|
|
|
term = (1 + rate) ** nper
|
|
|
|
|
|
|
|
res = (pv_val * term + fv_val) * rate / (1 - term)
|
|
|
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pv_formula(rate, nper, pmt_val, fv_val=0):
|
|
|
|
|
|
|
|
if rate == 0:
|
|
|
|
|
|
|
|
return -(fv_val + pmt_val * nper)
|
|
|
|
|
|
|
|
term = (1 + rate) ** nper
|
|
|
|
|
|
|
|
res = (fv_val / term + pmt_val * ((1 - (1/term)) / rate))
|
|
|
|
|
|
|
|
return -res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Service to calculate plant data chart
|
|
|
|
# Service to calculate plant data chart
|
|
|
|
async def calculate_plant_data_chart(
|
|
|
|
async def calculate_plant_data_chart(
|
|
|
|
@ -790,8 +816,10 @@ async def calculate_plant_data_chart(
|
|
|
|
# raise Exception(fleet_statistics)
|
|
|
|
# raise Exception(fleet_statistics)
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
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
|
|
|
|
|
|
|
|
benefit_loss_perhour = 867000000
|
|
|
|
rbd_marginal_fails = [0] * data_num
|
|
|
|
rbd_marginal_fails = [0] * data_num
|
|
|
|
|
|
|
|
rbd_marginal_oos_hours = [0] * data_num
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if simulation_id:
|
|
|
|
if simulation_id:
|
|
|
|
from src.config import RBD_SERVICE_API
|
|
|
|
from src.config import RBD_SERVICE_API
|
|
|
|
@ -806,17 +834,31 @@ async def calculate_plant_data_chart(
|
|
|
|
plant_data = response.json().get("data", {})
|
|
|
|
plant_data = response.json().get("data", {})
|
|
|
|
timestamp_outs = plant_data.get("timestamp_outs", [])
|
|
|
|
timestamp_outs = plant_data.get("timestamp_outs", [])
|
|
|
|
if timestamp_outs:
|
|
|
|
if timestamp_outs:
|
|
|
|
from src.calculation_time_constrains.utils import create_time_series_data, calculate_failures_per_month
|
|
|
|
from src.calculation_time_constrains.utils import create_time_series_data, calculate_failures_per_month, calculate_oos_per_month
|
|
|
|
hourly_data = create_time_series_data(timestamp_outs, max_hours=data_num * 720)
|
|
|
|
hourly_data = create_time_series_data(timestamp_outs, max_hours=data_num * 720)
|
|
|
|
cumulative_rbd_fails = calculate_failures_per_month(hourly_data)
|
|
|
|
cumulative_rbd_fails = calculate_failures_per_month(hourly_data)
|
|
|
|
|
|
|
|
cumulative_rbd_oos = calculate_oos_per_month(hourly_data)
|
|
|
|
|
|
|
|
|
|
|
|
rbd_fails_map = {m['month']: m['failures'] for m in cumulative_rbd_fails}
|
|
|
|
rbd_fails_map = {m['month']: m['failures'] for m in cumulative_rbd_fails}
|
|
|
|
|
|
|
|
rbd_oos_map = {m['month']: m['oos_hours'] for m in cumulative_rbd_oos}
|
|
|
|
|
|
|
|
|
|
|
|
prev_fail = 0
|
|
|
|
prev_fail = 0
|
|
|
|
|
|
|
|
prev_oos = 0
|
|
|
|
for m in range(1, data_num + 1):
|
|
|
|
for m in range(1, data_num + 1):
|
|
|
|
curr_fail = rbd_fails_map.get(m, prev_fail)
|
|
|
|
curr_fail = rbd_fails_map.get(m, prev_fail)
|
|
|
|
rbd_marginal_fails[m-1] = curr_fail - prev_fail
|
|
|
|
rbd_marginal_fails[m-1] = curr_fail - prev_fail
|
|
|
|
prev_fail = curr_fail
|
|
|
|
prev_fail = curr_fail
|
|
|
|
|
|
|
|
rbd_marginal_oos_hours[m-1] = rbd_oos_map.get(m, 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cumulative_plant_failures = 0
|
|
|
|
cumulative_plant_failures = 0
|
|
|
|
|
|
|
|
cumulative_benefit_loss = 0
|
|
|
|
|
|
|
|
corrective_costs = []
|
|
|
|
|
|
|
|
overhaul_costs = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
import numpy as np
|
|
|
|
from .schema import CalculationResultsRead
|
|
|
|
from .schema import CalculationResultsRead
|
|
|
|
for month_index in range(data_num):
|
|
|
|
for month_index in range(data_num):
|
|
|
|
@ -836,6 +878,7 @@ async def calculate_plant_data_chart(
|
|
|
|
"corrective_cost": 0.0,
|
|
|
|
"corrective_cost": 0.0,
|
|
|
|
"procurement_cost": 0.0,
|
|
|
|
"procurement_cost": 0.0,
|
|
|
|
"num_failures": cumulative_plant_failures,
|
|
|
|
"num_failures": cumulative_plant_failures,
|
|
|
|
|
|
|
|
|
|
|
|
"day": month_index + 1,
|
|
|
|
"day": month_index + 1,
|
|
|
|
"month": month_index + 1,
|
|
|
|
"month": month_index + 1,
|
|
|
|
"procurement_details": {},
|
|
|
|
"procurement_details": {},
|
|
|
|
@ -892,7 +935,25 @@ async def calculate_plant_data_chart(
|
|
|
|
month_result["overhaul_cost"] += float(eq.overhaul_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["procurement_cost"] += float(eq.procurement_costs[month_index])
|
|
|
|
|
|
|
|
|
|
|
|
month_result["corrective_cost"] = (cumulative_plant_failures * avg_failure_cost) / (month_index + 1)
|
|
|
|
marginal_fail = rbd_marginal_fails[month_index] + historical_marginal_fail
|
|
|
|
|
|
|
|
benefit_loss = float(rbd_marginal_oos_hours[month_index] * benefit_loss_perhour)
|
|
|
|
|
|
|
|
cumulative_benefit_loss += benefit_loss
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Monthly marginal corrective cost (cash flow for NPV)
|
|
|
|
|
|
|
|
marginal_corrective_cost = (marginal_fail * avg_failure_cost) + benefit_loss
|
|
|
|
|
|
|
|
corrective_costs.append(marginal_corrective_cost)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Calculate NPV of the corrective cost series (using 4.533% as 0.04533)
|
|
|
|
|
|
|
|
corrective_npv_val = npv_formula(0.04533, corrective_costs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Calculate PMT to get the annualized/periodic equivalent cost (using 4.05% as 0.0405)
|
|
|
|
|
|
|
|
month_result["corrective_cost"] = -pmt_formula(0.0405, month_index + 1, corrective_npv_val)
|
|
|
|
|
|
|
|
# Levelize the one-time Overhaul + Procurement Cost
|
|
|
|
|
|
|
|
oh_procurement_sum = month_result["overhaul_cost"] + month_result["procurement_cost"]
|
|
|
|
|
|
|
|
# PV of a single payment in the future
|
|
|
|
|
|
|
|
# Monthly levelized Overhaul cost
|
|
|
|
|
|
|
|
month_result["overhaul_cost"] = oh_procurement_sum
|
|
|
|
|
|
|
|
|
|
|
|
month_result["sparepart_summary"].update({
|
|
|
|
month_result["sparepart_summary"].update({
|
|
|
|
"total_procurement_cost": month_result["procurement_cost"],
|
|
|
|
"total_procurement_cost": month_result["procurement_cost"],
|
|
|
|
"equipment_requiring_procurement": equipment_requiring_procurement,
|
|
|
|
"equipment_requiring_procurement": equipment_requiring_procurement,
|
|
|
|
@ -901,12 +962,15 @@ async def calculate_plant_data_chart(
|
|
|
|
"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")]),
|
|
|
|
"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
|
|
|
|
"urgent_procurements": urgent_procurements
|
|
|
|
})
|
|
|
|
})
|
|
|
|
month_result["total_cost"] = month_result["corrective_cost"] + month_result["overhaul_cost"] + month_result["procurement_cost"]
|
|
|
|
month_result["total_cost"] = month_result["corrective_cost"] + month_result["overhaul_cost"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
calculation_results.append(month_result)
|
|
|
|
calculation_results.append(month_result)
|
|
|
|
|
|
|
|
|
|
|
|
optimum_day = np.argmin([month["total_cost"] for month in calculation_results])
|
|
|
|
optimum_day = np.argmin([month["total_cost"] for month in calculation_results])
|
|
|
|
scope_calculation.optimum_oh_day = int(optimum_day)
|
|
|
|
scope_calculation.optimum_oh_day = int(optimum_day)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fleet_statistics['equipment_with_sparepart_constraints'] = len([
|
|
|
|
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)
|
|
|
|
eq for eq in all_equipment if any(detail and detail.get("procurement_needed") for detail in eq.procurement_details if detail)
|
|
|
|
])
|
|
|
|
])
|
|
|
|
@ -930,6 +994,8 @@ async def calculate_plant_data_chart(
|
|
|
|
scope_calculation.plant_results = calculation_results
|
|
|
|
scope_calculation.plant_results = calculation_results
|
|
|
|
scope_calculation.fleet_statistics = fleet_statistics
|
|
|
|
scope_calculation.fleet_statistics = fleet_statistics
|
|
|
|
scope_calculation.analysis_metadata = analysis_metadata
|
|
|
|
scope_calculation.analysis_metadata = analysis_metadata
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scope_calculation.optimum_analysis = optimal_analysis
|
|
|
|
scope_calculation.optimum_analysis = optimal_analysis
|
|
|
|
|
|
|
|
|
|
|
|
await db_session.commit()
|
|
|
|
await db_session.commit()
|
|
|
|
@ -940,6 +1006,46 @@ async def calculate_plant_data_chart(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_simulation_chart(plant_results: List[Dict], optimum_oh_day: Optional[int], title_id: str, output_path: str = "plant_data_chart.png"):
|
|
|
|
|
|
|
|
"""Helper function to generate and save simulation chart"""
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
if not plant_results:
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
months = [r['month'] for r in plant_results]
|
|
|
|
|
|
|
|
total_costs = [r['total_cost'] for r in plant_results]
|
|
|
|
|
|
|
|
corrective_costs = [r['corrective_cost'] for r in plant_results]
|
|
|
|
|
|
|
|
overhaul_costs = [r['overhaul_cost'] for r in plant_results]
|
|
|
|
|
|
|
|
procurement_costs = [r['procurement_cost'] for r in plant_results]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
plt.figure(figsize=(12, 6))
|
|
|
|
|
|
|
|
plt.plot(months, total_costs, label='Total Cost', marker='o', linewidth=2, color='blue')
|
|
|
|
|
|
|
|
plt.plot(months, corrective_costs, label='Corrective Cost', linestyle='--', color='green')
|
|
|
|
|
|
|
|
plt.plot(months, overhaul_costs, label='Overhaul Cost', linestyle='--', color='orange')
|
|
|
|
|
|
|
|
plt.plot(months, procurement_costs, label='Procurement Cost', linestyle='--', color='red')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Add a vertical line for the optimum
|
|
|
|
|
|
|
|
if optimum_oh_day is not None:
|
|
|
|
|
|
|
|
optimum_month = optimum_oh_day + 1
|
|
|
|
|
|
|
|
plt.axvline(x=optimum_month, color='purple', linestyle=':', label=f'Optimum (Month {optimum_month})')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
plt.xlabel('Month')
|
|
|
|
|
|
|
|
plt.ylabel('Cost')
|
|
|
|
|
|
|
|
plt.title(f'Plant Overhaul Optimization Results\nID: {title_id}')
|
|
|
|
|
|
|
|
plt.legend()
|
|
|
|
|
|
|
|
plt.grid(True, alpha=0.3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
plt.savefig(output_path)
|
|
|
|
|
|
|
|
plt.close()
|
|
|
|
|
|
|
|
log.info(f"Simulation chart saved to {output_path}")
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
log.error(f"Failed to generate simulation chart: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def run_simulation_with_spareparts(*, db_session, calculation, token: str, collector_db_session,
|
|
|
|
async def run_simulation_with_spareparts(*, db_session, calculation, token: str, collector_db_session,
|
|
|
|
time_window_months: Optional[int] = None,
|
|
|
|
time_window_months: Optional[int] = None,
|
|
|
|
simulation_id: str = "default", rbd_service_api = None) -> Dict:
|
|
|
|
simulation_id: str = "default", rbd_service_api = None) -> Dict:
|
|
|
|
@ -948,32 +1054,41 @@ async def run_simulation_with_spareparts(*, db_session, calculation, token: str,
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# 1. Calculate equipment data chart
|
|
|
|
# 1. Calculate equipment data chart
|
|
|
|
await calculate_equipment_data_chart(
|
|
|
|
# await calculate_equipment_data_chart(
|
|
|
|
|
|
|
|
# db_session=db_session,
|
|
|
|
|
|
|
|
# calculation=calculation,
|
|
|
|
|
|
|
|
# token=token,
|
|
|
|
|
|
|
|
# collector_db_session=collector_db_session,
|
|
|
|
|
|
|
|
# simulation_id=simulation_id,
|
|
|
|
|
|
|
|
# rbd_service_api=rbd_service_api
|
|
|
|
|
|
|
|
# )
|
|
|
|
|
|
|
|
# 2. Calculate plant data chart
|
|
|
|
|
|
|
|
plant_summary = await calculate_plant_data_chart(
|
|
|
|
db_session=db_session,
|
|
|
|
db_session=db_session,
|
|
|
|
calculation=calculation,
|
|
|
|
calculation_id=str("2ad790c6-beb1-4be9-b79c-1b4c13771a07"),
|
|
|
|
token=token,
|
|
|
|
token=token,
|
|
|
|
collector_db_session=collector_db_session,
|
|
|
|
simulation_id=simulation_id
|
|
|
|
simulation_id=simulation_id,
|
|
|
|
|
|
|
|
rbd_service_api=rbd_service_api
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# print(f"Calculation ID: {calculation.id}")
|
|
|
|
# 3. Generate and save plant data chart as PNG
|
|
|
|
# print(f"Token: {token}")
|
|
|
|
# Fetch the latest calculation data to get plant_results
|
|
|
|
# print(f"Simulation ID: {simulation_id}")
|
|
|
|
calculation_data = await get_calculation_data_by_id(db_session, "2ad790c6-beb1-4be9-b79c-1b4c13771a07")
|
|
|
|
# print(f"RBD Service API: {rbd_service_api}")
|
|
|
|
|
|
|
|
# print(f"Max Interval: {calculation.max_interval}")
|
|
|
|
|
|
|
|
# print(f"Last OH Date: {calculation.last_oh_date}")
|
|
|
|
|
|
|
|
# print(f"Next OH Date: {calculation.next_oh_date}")
|
|
|
|
|
|
|
|
# print(f"Equipment Results: {calculation.equipment_results}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 2. Calculate plant data chart
|
|
|
|
|
|
|
|
await calculate_plant_data_chart(
|
|
|
|
generate_simulation_chart(
|
|
|
|
db_session=db_session,
|
|
|
|
plant_results=calculation_data.plant_results,
|
|
|
|
calculation_id=str(calculation.id),
|
|
|
|
optimum_oh_day=calculation_data.optimum_oh_day,
|
|
|
|
token=token,
|
|
|
|
title_id=simulation_id
|
|
|
|
simulation_id=simulation_id
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
raise Exception("test")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return plant_summary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -1037,6 +1152,14 @@ async def get_calculation_result(db_session: DbSession, calculation_id: str, tok
|
|
|
|
plant_results_raw = scope_calculation.plant_results or []
|
|
|
|
plant_results_raw = scope_calculation.plant_results or []
|
|
|
|
calculation_results = [CalculationResultsRead(**r) for r in plant_results_raw]
|
|
|
|
calculation_results = [CalculationResultsRead(**r) for r in plant_results_raw]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Generate chart
|
|
|
|
|
|
|
|
generate_simulation_chart(
|
|
|
|
|
|
|
|
plant_results=scope_calculation.plant_results,
|
|
|
|
|
|
|
|
optimum_oh_day=scope_calculation.optimum_oh_day,
|
|
|
|
|
|
|
|
title_id=calculation_id
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Return comprehensive result
|
|
|
|
# Return comprehensive result
|
|
|
|
return CalculationTimeConstrainsRead(
|
|
|
|
return CalculationTimeConstrainsRead(
|
|
|
|
id=scope_calculation.id,
|
|
|
|
id=scope_calculation.id,
|
|
|
|
@ -1111,8 +1234,8 @@ def _analyze_optimal_timing(calculation_results: List, optimum_oh_day: int,
|
|
|
|
cost_trend = "DECREASING"
|
|
|
|
cost_trend = "DECREASING"
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
"optimal_month": optimum_oh_day + 1,
|
|
|
|
"optimal_month": int(optimum_oh_day + 1),
|
|
|
|
"planned_month": planned_oh_months,
|
|
|
|
"planned_month": int(planned_oh_months),
|
|
|
|
"timing_recommendation": timing_recommendation,
|
|
|
|
"timing_recommendation": timing_recommendation,
|
|
|
|
"optimal_total_cost": optimal_result.total_cost if optimal_result else 0,
|
|
|
|
"optimal_total_cost": optimal_result.total_cost if optimal_result else 0,
|
|
|
|
"optimal_breakdown": {
|
|
|
|
"optimal_breakdown": {
|
|
|
|
@ -1122,7 +1245,7 @@ def _analyze_optimal_timing(calculation_results: List, optimum_oh_day: int,
|
|
|
|
"num_failures": optimal_result.num_failures if optimal_result else 0
|
|
|
|
"num_failures": optimal_result.num_failures if optimal_result else 0
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"cost_trend": cost_trend,
|
|
|
|
"cost_trend": cost_trend,
|
|
|
|
"months_from_planned": (optimum_oh_day + 1 - planned_oh_months) if planned_oh_months else None,
|
|
|
|
"months_from_planned": int((optimum_oh_day + 1) - planned_oh_months) if planned_oh_months else None,
|
|
|
|
"cost_savings_vs_planned": None, # Would need planned month cost to calculate
|
|
|
|
"cost_savings_vs_planned": None, # Would need planned month cost to calculate
|
|
|
|
"sparepart_impact": {
|
|
|
|
"sparepart_impact": {
|
|
|
|
"equipment_with_constraints": optimal_result.sparepart_summary["equipment_requiring_procurement"] if optimal_result else 0,
|
|
|
|
"equipment_with_constraints": optimal_result.sparepart_summary["equipment_requiring_procurement"] if optimal_result else 0,
|
|
|
|
|