feat: implement financial calculation formulas and simulation chart generation for plant overhaul optimization

main
Cizz22 2 months ago
parent d51ba8dd8d
commit 4380cfa134

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

@ -59,6 +59,8 @@ class CalculationResultsRead(CalculationTimeConstrainsBase):
sparepart_summary: dict sparepart_summary: dict
class OptimumResult(CalculationTimeConstrainsBase): class OptimumResult(CalculationTimeConstrainsBase):
overhaul_cost: float overhaul_cost: float
corrective_cost: float corrective_cost: float

@ -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,

@ -100,6 +100,48 @@ def calculate_failures_per_month(hourly_data):
return result return result
def calculate_oos_per_month(hourly_data):
"""
Calculate the discrete OOS hours for each month from hourly data.
A failure is defined as when currentEQStatus = "OoS".
Args:
hourly_data: List of dicts with 'cumulativeTime', 'flowRate', and 'currentEQStatus' keys
Returns:
List of dicts with 'month' and 'oos_hours' keys (discrete monthly count)
"""
monthly_data = {}
for data_point in hourly_data:
hour = data_point['cumulativeTime']
current_eq_status = data_point['currentEQStatus']
# Calculate which month this hour belongs to (1-based)
# Assuming 30 days per month = 720 hours per month
month = ((hour - 1) // 720) + 1
if month not in monthly_data:
monthly_data[month] = 0
if current_eq_status == "OoS":
monthly_data[month] += 1
# Convert to list format
result = []
if monthly_data:
max_month = max(monthly_data.keys())
for month in range(1, max_month + 1):
result.append({
'month': month,
'oos_hours': monthly_data.get(month, 0)
})
return result
import pandas as pd import pandas as pd
import datetime import datetime

Loading…
Cancel
Save