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