You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
178 lines
6.0 KiB
Python
178 lines
6.0 KiB
Python
from collections import defaultdict
|
|
import random
|
|
from typing import Optional
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy import Delete, Select
|
|
|
|
from src.auth.service import CurrentUser
|
|
from src.contribution_util import calculate_contribution_accurate
|
|
from src.database.core import CollectorDbSession, DbSession
|
|
# from src.scope_equipment.model import ScopeEquipment
|
|
# from src.scope_equipment.service import get_by_scope_name
|
|
from src.overhaul_activity.service import get_all_by_session_id, get_standard_scope_by_session_id
|
|
|
|
# async def get_all_budget_constrains(
|
|
# *, db_session: DbSession, session_id: str, cost_threshold: float = 100000000
|
|
# ):
|
|
|
|
# At the module level, add this dictionary to store persistent EAF values
|
|
from collections import defaultdict
|
|
from uuid import UUID
|
|
from typing import List, Dict, Tuple
|
|
|
|
from src.database.core import CollectorDbSession, DbSession
|
|
from src.overhaul_activity.service import get_standard_scope_by_session_id
|
|
from src.contribution_util import calculate_contribution_accurate
|
|
|
|
|
|
async def get_all_budget_constrains(
|
|
*,
|
|
db_session: DbSession,
|
|
collector_db: CollectorDbSession,
|
|
session_id: UUID,
|
|
simulation_result: Dict,
|
|
cost_threshold: float = 100_000_000,
|
|
use_optimal: bool = True, # default = optimal (knapsack)
|
|
) -> Tuple[List[Dict], List[Dict]]:
|
|
"""
|
|
Select equipment under budget constraint using contribution + cost efficiency.
|
|
Returns (priority_list, consequence_list).
|
|
"""
|
|
|
|
calc_result = simulation_result["calc_result"]
|
|
plant_result = simulation_result["plant_result"]
|
|
|
|
equipments = await get_standard_scope_by_session_id(
|
|
db_session=db_session,
|
|
overhaul_session_id=session_id,
|
|
collector_db=collector_db,
|
|
)
|
|
if not equipments:
|
|
return [], []
|
|
|
|
# Flatten results
|
|
eq_results = calc_result if isinstance(calc_result, list) else [calc_result]
|
|
|
|
# Calculate contribution map (node_name → contribution)
|
|
equipments_eaf_contribution = calculate_asset_eaf_contributions(
|
|
plant_result=plant_result, eq_results=eq_results
|
|
)
|
|
|
|
# Build base result list
|
|
result = []
|
|
for equipment in equipments:
|
|
contribution = equipments_eaf_contribution.get(equipment.location_tag, 0.0)
|
|
total_cost = (equipment.overhaul_cost or 0) + (equipment.service_cost or 0)
|
|
|
|
result.append(
|
|
{
|
|
"id": equipment.id,
|
|
"location_tag": equipment.location_tag,
|
|
"name": equipment.equipment_name,
|
|
"total_cost": total_cost,
|
|
"eaf_contribution": contribution,
|
|
}
|
|
)
|
|
|
|
# Normalize contribution so sum = 1.0
|
|
total_contrib = sum(item["eaf_contribution"] for item in result) or 1.0
|
|
for item in result:
|
|
item["contribution_norm"] = item["eaf_contribution"] / total_contrib
|
|
|
|
# Calculate efficiency and composite score
|
|
for item in result:
|
|
cost = item["total_cost"] or 1.0
|
|
efficiency = item["contribution_norm"] / cost
|
|
item["priority_score"] = item["contribution_norm"]
|
|
|
|
# Choose method
|
|
if use_optimal:
|
|
priority_list, consequence_list = knapsack_selection(result, cost_threshold)
|
|
else:
|
|
priority_list, consequence_list = greedy_selection(result, cost_threshold)
|
|
|
|
return priority_list, consequence_list
|
|
|
|
|
|
def calculate_asset_eaf_contributions(plant_result, eq_results):
|
|
"""
|
|
Calculate each asset's negative contribution to plant EAF.
|
|
Key assumption: eq_results have aeros_node.node_name matching equipment.location_tag.
|
|
"""
|
|
results = defaultdict(float)
|
|
for asset in eq_results:
|
|
node_name = asset.get("aeros_node", {}).get("node_name")
|
|
if node_name:
|
|
results[node_name] = asset.get("contribution_factor", 0.0)
|
|
return results
|
|
|
|
|
|
def greedy_selection(equipments: List[dict], budget: float) -> Tuple[List[dict], List[dict]]:
|
|
"""Greedy fallback: select items by score until budget is used."""
|
|
# Sort by priority_score descending
|
|
equipments_sorted = sorted(equipments, key=lambda x: x["priority_score"], reverse=True)
|
|
total_cost = 0
|
|
selected, excluded = [], []
|
|
|
|
for eq in equipments_sorted:
|
|
if total_cost + eq["cost"] <= budget:
|
|
selected.append(eq)
|
|
total_cost += eq["cost"]
|
|
else:
|
|
excluded.append(eq)
|
|
|
|
return selected, excluded
|
|
|
|
|
|
def knapsack_selection(equipments: List[dict], budget: float, scale: int = 10_000_000) -> Tuple[List[dict], List[dict]]:
|
|
"""
|
|
Select equipment optimally within budget using 0/1 knapsack DP.
|
|
Uses scaling + 1D DP optimization to avoid MemoryError.
|
|
Falls back to greedy if W is too large.
|
|
"""
|
|
n = len(equipments)
|
|
|
|
# Scale costs + budget
|
|
costs = [int(eq["total_cost"] // scale) for eq in equipments]
|
|
values = [eq["priority_score"] for eq in equipments]
|
|
W = int(budget // scale)
|
|
|
|
# Fallback if W is still too large
|
|
if W > 1_000_000:
|
|
print("too big")
|
|
return greedy_selection(equipments, budget)
|
|
|
|
# 1D DP array
|
|
dp = [0.0] * (W + 1)
|
|
keep = [[False] * (W + 1) for _ in range(n)] # track selection choices
|
|
|
|
for i in range(n):
|
|
cost, value = costs[i], values[i]
|
|
for w in range(W, cost - 1, -1):
|
|
if dp[w - cost] + value >= dp[w]: # <= FIXED HERE
|
|
dp[w] = dp[w - cost] + value
|
|
keep[i][w] = True
|
|
|
|
# Backtrack to find selected items
|
|
selected, excluded = [], []
|
|
w = W
|
|
for i in range(n - 1, -1, -1):
|
|
if keep[i][w]:
|
|
selected.append(equipments[i])
|
|
w -= costs[i]
|
|
else:
|
|
excluded.append(equipments[i])
|
|
|
|
# Optional: fill leftover budget with zero-priority items
|
|
remaining_budget = budget - sum(eq["total_cost"] for eq in selected)
|
|
if remaining_budget > 0:
|
|
for eq in excluded[:]:
|
|
if eq["total_cost"] <= remaining_budget:
|
|
selected.append(eq)
|
|
excluded.remove(eq)
|
|
remaining_budget -= eq["total_cost"]
|
|
|
|
return selected, excluded
|
|
|
|
|