Cizz22 3 months ago
parent 1409f486ad
commit 22bc48eef2

@ -40,7 +40,8 @@ async def get_target_reliability(
oh_session_id: Optional[str] = Query(None), oh_session_id: Optional[str] = Query(None),
eaf_input: float = Query(99.8), eaf_input: float = Query(99.8),
duration: int = Query(17520), duration: int = Query(17520),
simulation_id: Optional[str] = Query(None) simulation_id: Optional[str] = Query(None),
po_duration = Query(1200)
): ):
"""Get all scope pagination.""" """Get all scope pagination."""
if not oh_session_id: if not oh_session_id:
@ -66,7 +67,7 @@ async def get_target_reliability(
) )
simulation_id = simulation.get("data") simulation_id = simulation.get("data")
await wait_for_workflow(simulation_id=simulation_id.get("data")) await wait_for_workflow(simulation_id=simulation_id)
else: else:
await wait_for_workflow(simulation_id=simulation_id) await wait_for_workflow(simulation_id=simulation_id)
@ -82,7 +83,9 @@ async def get_target_reliability(
db_session=db_session, db_session=db_session,
oh_session_id=oh_session_id, oh_session_id=oh_session_id,
collector_db=collector_db, collector_db=collector_db,
simulation_id=simulation_id simulation_id=simulation_id,
duration=duration,
po_duration=po_duration
) )

@ -56,9 +56,11 @@ class OptimizationResult(OverhaulBase):
target_plant_eaf: float target_plant_eaf: float
possible_plant_eaf:float possible_plant_eaf:float
eaf_gap: float eaf_gap: float
warning_message:Optional[str]
asset_contributions: List[dict] asset_contributions: List[dict]
optimization_success: bool = False optimization_success: bool = False
simulation_id: Optional[str] = None simulation_id: Optional[str] = None
# { # {

@ -1,3 +1,4 @@
import math
from typing import Optional, List from typing import Optional, List
from dataclasses import dataclass from dataclasses import dataclass
from sqlalchemy import Delete, Select from sqlalchemy import Delete, Select
@ -77,57 +78,61 @@ async def get_simulation_results(*, simulation_id: str, token: str):
"plant_result": plant_data "plant_result": plant_data
} }
def calculate_asset_eaf_contributions(plant_result, eq_results, standard_scope, eaf_gap): def calculate_asset_eaf_contributions(plant_result, eq_results, standard_scope, eaf_gap, scheduled_outage):
""" """
Calculate each asset's contribution to plant EAF with realistic improvement potential. Calculate each asset's contribution to plant EAF with realistic, fair improvement allocation.
Ranking: The total EAF gap is distributed among assets proportionally to their contribution potential.
1. Highest contribution (Birnbaum Importance)
2. Tie-breaker: Contribution per downtime (efficiency)
""" """
eaf_gap_fraction = eaf_gap / 100.0 if eaf_gap > 1.0 else eaf_gap eaf_gap_fraction = eaf_gap / 100.0 if eaf_gap > 1.0 else eaf_gap
total_hours = plant_result.get("total_uptime") + plant_result.get("total_downtime")
plant_operating_fraction = (total_hours - scheduled_outage) / total_hours
MIN_BIRNBAUM_IMPORTANCE = 0.0005 REALISTIC_MAX_TECHNICAL = 0.995
REALISTIC_MAX_AVAILABILITY = 0.995 # 99.5% REALISTIC_MAX_AVAILABILITY = REALISTIC_MAX_TECHNICAL * plant_operating_fraction
MIN_IMPROVEMENT_PERCENT = 0.0001 MIN_IMPROVEMENT_PERCENT = 0.0001
min_improvement_fraction = MIN_IMPROVEMENT_PERCENT / 100.0 min_improvement_fraction = MIN_IMPROVEMENT_PERCENT / 100.0
results = [] results = []
weighted_assets = []
# Step 1: Collect eligible assets and their weights
for asset in eq_results: for asset in eq_results:
asset_name = asset.get("aeros_node").get("node_name") asset_name = asset.get("aeros_node").get("node_name")
num_of_events = asset.get("num_events")
if asset_name not in standard_scope:
if asset_name not in standard_scope or num_of_events < 2:
continue continue
contribution_factor = asset.get("contribution_factor", 0.0)
birbaum = asset.get("contribution", 0.0)
current_availability = asset.get("availability", 0.0)
downtime = asset.get("total_downtime", 0.0)
max_possible_improvement = REALISTIC_MAX_AVAILABILITY - current_availability if REALISTIC_MAX_AVAILABILITY > current_availability else REALISTIC_MAX_TECHNICAL
raw_weight = birbaum * contribution_factor
weight = math.sqrt(raw_weight)
weighted_assets.append((asset, weight, max_possible_improvement))
# Step 2: Compute total weight
total_weight = sum(w for _, w, _ in weighted_assets) or 1.0
# Step 3: Distribute improvement proportionally to weight
for asset, weight, max_possible_improvement in weighted_assets:
asset_name = asset.get("aeros_node").get("node_name")
contribution_factor = asset.get("contribution_factor", 0.0) contribution_factor = asset.get("contribution_factor", 0.0)
birbaum = asset.get("contribution", 0.0) birbaum = asset.get("contribution", 0.0)
current_availability = asset.get("availability", 0.0) current_availability = asset.get("availability", 0.0)
downtime = asset.get("total_downtime", 0.0) downtime = asset.get("total_downtime", 0.0)
# Filter 1: Importance too low # Proportional improvement share
if contribution_factor < MIN_BIRNBAUM_IMPORTANCE: required_improvement = eaf_gap_fraction * (weight / total_weight)
continue required_improvement = min(required_improvement, max_possible_improvement)
required_improvement = max(required_improvement, min_improvement_fraction)
# Max possible availability improvement
max_possible_improvement = REALISTIC_MAX_AVAILABILITY - current_availability
if max_possible_improvement <= 0:
continue
# Inject standard each equipment
required_improvement = 0.01
improvement_impact = required_improvement * contribution_factor improvement_impact = required_improvement * contribution_factor
# Filter 2: Improvement too small
if improvement_impact < MIN_IMPROVEMENT_PERCENT:
continue
# Contribution efficiency (secondary metric) # Secondary metric: efficiency
efficiency = birbaum / downtime if downtime > 0 else birbaum efficiency = birbaum / downtime if downtime > 0 else birbaum
contribution = AssetWeight( contribution = AssetWeight(
@ -138,30 +143,30 @@ def calculate_asset_eaf_contributions(plant_result, eq_results, standard_scope,
improvement_impact=improvement_impact, improvement_impact=improvement_impact,
num_of_failures=asset.get("num_events", 0), num_of_failures=asset.get("num_events", 0),
down_time=downtime, down_time=downtime,
efficiency= efficiency, efficiency=efficiency,
birbaum=birbaum birbaum=birbaum
) )
results.append(contribution) results.append(contribution)
# Sort: 1) contribution (desc), 2) efficiency (desc) # Step 4: Sort by Birnbaum importance
results.sort(key=lambda x: (x.birbaum), reverse=True) results.sort(key=lambda x: x.birbaum, reverse=True)
return results return results
def project_eaf_improvement(asset: AssetWeight, improvement_factor: float = 0.3) -> float: def project_eaf_improvement(asset: AssetWeight, improvement_factor: float = 0.3) -> float:
""" """
Project EAF improvement after maintenance Project EAF improvement after maintenance
This is a simplified model - you should replace with your actual prediction logic This is a simplified model - you should replace with your actual prediction logic
""" """
current_downtime_pct = 100 - asset.eaf current_downtime_pct = 100 - asset.eaf
# Assume maintenance reduces downtime by improvement_factor
improved_downtime_pct = current_downtime_pct * (1 - improvement_factor) improved_downtime_pct = current_downtime_pct * (1 - improvement_factor)
projected_eaf = 100 - improved_downtime_pct projected_eaf = 100 - improved_downtime_pct
return min(projected_eaf, 99.9) # Cap at 99.9% return min(projected_eaf, 99.9) # Cap at 99.9%
async def identify_worst_eaf_contributors( async def identify_worst_eaf_contributors(
*, *,
simulation_result, simulation_result,
@ -170,24 +175,56 @@ async def identify_worst_eaf_contributors(
oh_session_id: str, oh_session_id: str,
collector_db: CollectorDbSession, collector_db: CollectorDbSession,
simulation_id: str, simulation_id: str,
duration:int,
po_duration: int
): ):
""" """
Identify equipment that contributes most to plant EAF reduction Identify equipment that contributes most to plant EAF reduction
in order to reach a target EAF. and evaluate if target EAF is physically achievable.
""" """
# Extract results # Extract results
calc_result = simulation_result["calc_result"] calc_result = simulation_result["calc_result"]
plant_result = simulation_result["plant_result"] plant_result = simulation_result["plant_result"]
# Ensure list of equipment
eq_results = calc_result if isinstance(calc_result, list) else [calc_result] eq_results = calc_result if isinstance(calc_result, list) else [calc_result]
# Current plant EAF and gap # Current plant EAF and gap
current_plant_eaf = plant_result.get("eaf", 0) current_plant_eaf = plant_result.get("eaf", 0)
total_hours = duration
scheduled_outage = int(po_duration)
max_eaf_possible = (total_hours - scheduled_outage) / total_hours * 100
# Check if target EAF exceeds theoretical maximum
warning_message = None
if target_eaf > max_eaf_possible:
impossible_gap = target_eaf - max_eaf_possible
required_scheduled_hours = total_hours * (1 - target_eaf / 100)
required_reduction = scheduled_outage - required_scheduled_hours
warning_message = (
f"⚠️ Target EAF {target_eaf:.2f}% exceeds theoretical maximum {max_eaf_possible:.2f}%.\n"
f"To achieve it, planned outage must be reduced by approximately "
f"{required_reduction:.1f} hours (from {scheduled_outage:.0f}h → {required_scheduled_hours:.0f}h)."
)
# Cap target EAF to max achievable for calculation
target_eaf = max_eaf_possible
eaf_gap = (target_eaf - current_plant_eaf) / 100.0 eaf_gap = (target_eaf - current_plant_eaf) / 100.0
if eaf_gap <= 0:
return OptimizationResult(
current_plant_eaf=current_plant_eaf,
target_plant_eaf=target_eaf,
possible_plant_eaf=current_plant_eaf,
eaf_gap=0,
warning_message=warning_message or "Target already achieved or exceeded.",
asset_contributions=[],
optimization_success=True,
simulation_id=simulation_id,
)
# Get standard scope (equipment allowed for overhaul/optimization) # Get standard scope
standard_scope = await get_standard_scope_by_session_id( standard_scope = await get_standard_scope_by_session_id(
db_session=db_session, db_session=db_session,
overhaul_session_id=oh_session_id, overhaul_session_id=oh_session_id,
@ -197,35 +234,39 @@ async def identify_worst_eaf_contributors(
# Compute contributions # Compute contributions
asset_contributions = calculate_asset_eaf_contributions( asset_contributions = calculate_asset_eaf_contributions(
plant_result, eq_results, standard_scope_location_tags, eaf_gap=eaf_gap plant_result,
eq_results,
standard_scope_location_tags,
eaf_gap,
scheduled_outage
) )
project_eaf_improvement = 0.0 # Greedy selection to fill EAF gap
project_eaf_improvement_total = 0.0
selected_eq = [] selected_eq = []
# Greedy select until gap is closed
for asset in asset_contributions: for asset in asset_contributions:
if project_eaf_improvement >= eaf_gap: if project_eaf_improvement_total >= eaf_gap:
break break
if (project_eaf_improvement_total + asset.improvement_impact) <= eaf_gap:
if (project_eaf_improvement + asset.improvement_impact) <= eaf_gap:
selected_eq.append(asset) selected_eq.append(asset)
project_eaf_improvement += asset.improvement_impact project_eaf_improvement_total += asset.improvement_impact
else: else:
# allow overshoot tolerance by skipping large ones, continue with smaller ones
continue continue
possible_eaf_plant = current_plant_eaf + project_eaf_improvement*100
selected_eq.sort(key=lambda x: (x.birbaum), reverse=True)
# Build output with efficiency included possible_eaf_plant = current_plant_eaf + project_eaf_improvement_total * 100
possible_eaf_plant = min(possible_eaf_plant, max_eaf_possible)
selected_eq.sort(key=lambda x: x.birbaum, reverse=True)
# Final return
return OptimizationResult( return OptimizationResult(
current_plant_eaf=current_plant_eaf, current_plant_eaf=current_plant_eaf,
target_plant_eaf=target_eaf, target_plant_eaf=target_eaf,
possible_plant_eaf=possible_eaf_plant, possible_plant_eaf=possible_eaf_plant,
eaf_gap=eaf_gap, eaf_gap=eaf_gap,
warning_message=warning_message,
asset_contributions=[ asset_contributions=[
{ {
"node": asset.node, "node": asset.node,
@ -236,10 +277,10 @@ async def identify_worst_eaf_contributors(
"system_impact": asset.improvement_impact, "system_impact": asset.improvement_impact,
"num_of_failures": asset.num_of_failures, "num_of_failures": asset.num_of_failures,
"down_time": asset.down_time, "down_time": asset.down_time,
"efficiency": asset.efficiency, "efficiency": asset.efficiency,
} }
for asset in selected_eq for asset in selected_eq
], ],
optimization_success=(current_plant_eaf + project_eaf_improvement) >= target_eaf, optimization_success=(current_plant_eaf + project_eaf_improvement_total * 100) >= target_eaf,
simulation_id=simulation_id, simulation_id=simulation_id,
) )

@ -68,7 +68,7 @@ class OptimumCostModelWithSpareparts:
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 (default: 1.5x planned interval)
self.time_window_months = time_window_months or int(self.planned_oh_months * 1.5) self.time_window_months = time_window_months or int(self.planned_oh_months * 1.3)
# 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()
@ -174,7 +174,7 @@ class OptimumCostModelWithSpareparts:
def _calculate_equipment_costs_with_spareparts(self, failures_prediction: Dict, birnbaum_importance: float, def _calculate_equipment_costs_with_spareparts(self, failures_prediction: Dict, birnbaum_importance: float,
preventive_cost: float, failure_replacement_cost: float, ecs, preventive_cost: float, failure_replacement_cost: float, ecs,
location_tag: str, planned_overhauls: List = None) -> List[Dict]: location_tag: str, planned_overhauls: List = None, loss_production_permonth=0) -> List[Dict]:
"""Calculate costs for each month including sparepart costs and availability""" """Calculate costs for each month including sparepart costs and availability"""
if not failures_prediction: if not failures_prediction:
@ -349,6 +349,11 @@ class OptimumCostModelWithSpareparts:
imp['aeros_node']['node_name']: imp['contribution_factor'] imp['aeros_node']['node_name']: imp['contribution_factor']
for imp in importance_results["calc_result"] for imp in importance_results["calc_result"]
} }
loss_production_permonth = {
imp['aeros_node']['node_name']: (imp['ideal_production'] - imp['production']) / 60
for imp in importance_results["calc_result"]
}
except Exception as e: except Exception as e:
self.logger.error(f"Failed to get simulation results: {e}") self.logger.error(f"Failed to get simulation results: {e}")
equipment_birnbaum = {} equipment_birnbaum = {}
@ -376,6 +381,8 @@ class OptimumCostModelWithSpareparts:
location_tag = equipment.location_tag location_tag = equipment.location_tag
contribution_factor = equipment_birnbaum.get(location_tag, 0.0) contribution_factor = equipment_birnbaum.get(location_tag, 0.0)
ecs = ecs_tags.get(location_tag, None) ecs = ecs_tags.get(location_tag, None)
loss_production = loss_production_permonth.get(location_tag, 0) * 960000
# try: # try:
# # Get failure predictions # # Get failure predictions
@ -395,7 +402,8 @@ class OptimumCostModelWithSpareparts:
failure_replacement_cost=failure_replacement_cost, failure_replacement_cost=failure_replacement_cost,
location_tag=location_tag, location_tag=location_tag,
planned_overhauls=[] , # Empty in first pass planned_overhauls=[] , # Empty in first pass
ecs=ecs ecs=ecs,
loss_production_permonth=loss_production
) )
if not cost_results: if not cost_results:
@ -696,17 +704,18 @@ async def run_simulation_with_spareparts(*, db_session, calculation, token: str,
time_window_months = 60 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, 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, 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,
last_oh_date=prev_oh_scope.end_date, last_oh_date=prev_oh_scope.end_date,
next_oh_date=scope.start_date, next_oh_date=scope.start_date,
time_window_months=time_window_months,
base_url=RBD_SERVICE_API, base_url=RBD_SERVICE_API,
sparepart_manager=sparepart_manager sparepart_manager=sparepart_manager
) )
try: try:
# Run fleet optimization with sparepart management # Run fleet optimization with sparepart management
results = await optimum_oh_model.calculate_cost_all_equipment_with_spareparts( results = await optimum_oh_model.calculate_cost_all_equipment_with_spareparts(

@ -792,9 +792,11 @@ async def load_sparepart_data_from_db(scope, prev_oh_scope, db_session, analysis
# 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)
analysis_start_date = prev_oh_scope.end_date analysis_start_date = prev_oh_scope.end_date
analysis_window_months = int(((scope.start_date - prev_oh_scope.end_date).days / 30) * 1.5) if not analysis_window_months else analysis_window_months analysis_window_months = int(((scope.start_date - prev_oh_scope.end_date).days / 30) * 1.3) if not analysis_window_months else analysis_window_months
sparepart_manager = SparepartManager(analysis_start_date, analysis_window_months) sparepart_manager = SparepartManager(analysis_start_date, analysis_window_months)
start_date = prev_oh_scope.end_date
end_date = scope.start_date
# Load sparepart stocks # Load sparepart stocks
# Example query - adjust based on your schema # Example query - adjust based on your schema
@ -832,7 +834,7 @@ async def load_sparepart_data_from_db(scope, prev_oh_scope, db_session, analysis
wonum, wonum,
asset_location asset_location
FROM public.wo_staging_maximo_2 FROM public.wo_staging_maximo_2
WHERE worktype = 'OH' AND asset_location IS NOT NULL WHERE worktype = 'OH' AND asset_location IS NOT NULL and asset_unit IN ('3', '00')
), ),
sparepart_usage AS ( sparepart_usage AS (
-- Get sparepart usage for OH work orders -- Get sparepart usage for OH work orders

Loading…
Cancel
Save