flow_problem / src /evaluation /validator.py
xxxm1r0xxx's picture
refactor: apply SOLID principles to validator logic and remove magic strings/numbers
1f66689
Raw
History Blame Contribute Delete
10.2 kB
import json
import logging
import pandas as pd
import numpy as np
logger = logging.getLogger(__name__)
TOLERANCE = 1e-3
MAX_ERRORS_TO_SHOW = 10
class DataParser:
@staticmethod
def parse_russian_float(val):
if pd.isna(val): return 0.0
if isinstance(val, (int, float)): return float(val)
val = str(val).replace(' ', '').replace(',', '.')
return float(val)
class SolutionParser:
@classmethod
def parse(cls, file_path):
path_str = str(file_path)
if path_str.endswith('.json'):
return cls._parse_json(path_str)
elif path_str.endswith('.csv'):
return cls._parse_csv(path_str)
else:
raise ValueError(f"Unsupported file format: {path_str}")
@staticmethod
def _parse_json(path_str):
with open(path_str, 'r') as f:
return json.load(f)
@staticmethod
def _parse_csv(path_str):
df = pd.read_csv(path_str)
df.columns = [c.lower().strip() for c in df.columns]
rename_map = {
'commodity': 'commodity_id',
'id': 'commodity_id',
'start': 'u',
'end': 'v',
'value': 'flow'
}
df = df.rename(columns={k: v for k, v in rename_map.items() if k in df.columns})
return {
"metadata": {},
"flows": df.to_dict('records')
}
def parse_solution_file(file_path):
return SolutionParser.parse(file_path)
def load_network_data(capacity_path='data/permissible_flow_capacity.csv',
production_path='data/production_volumes.csv',
graph_path='data/graph.graph'):
"""Loads network capacity limits, production volumes, and valid graph edges."""
logger.info(f"Loading network data from {capacity_path}, {production_path}, and {graph_path}")
valid_edges = set()
try:
with open(graph_path, 'r') as f:
graph_data = json.load(f)
vertices = graph_data.get('vertices', [])
for e in graph_data.get('edges', []):
v1, v2 = e.get('vertex1'), e.get('vertex2')
if v1 is not None and v2 is not None and v1 < len(vertices) and v2 < len(vertices):
n1 = str(vertices[v1].get('name')).strip()
n2 = str(vertices[v2].get('name')).strip()
valid_edges.add('-'.join(sorted([n1, n2])))
logger.info(f"Loaded {len(valid_edges)} valid edges from graph topology.")
except Exception as e:
logger.warning(f"Could not load valid edges from {graph_path}: {e}")
df_cap = pd.read_csv(capacity_path, sep=';')
df_cap['u'] = df_cap['Код участка (начало)'].astype(str).str.strip()
df_cap['v'] = df_cap['Код участка (окончание)'].astype(str).str.strip()
df_cap['capacity'] = df_cap['Допустимая мощность потока (кВт)'].apply(DataParser.parse_russian_float)
df_cap['physical_edge'] = df_cap.apply(lambda r: '-'.join(sorted([r['u'], r['v']])), axis=1)
capacities = df_cap.groupby('physical_edge')['capacity'].sum().to_dict()
df_prod = pd.read_csv(production_path, sep=',')
df_prod['commodity_id'] = df_prod['Номер строки'].astype(int)
df_prod['source'] = df_prod['Источник потока'].astype(str).str.strip()
df_prod['target'] = df_prod['Потребитель'].astype(str).str.strip()
df_prod['volume'] = df_prod['Поток, кВт'].apply(DataParser.parse_russian_float)
df_prod = df_prod[df_prod['target'].str.lower() != 'итого']
demands = df_prod.set_index('commodity_id')[['source', 'target', 'volume']].to_dict('index')
return capacities, demands, valid_edges
class FlowValidator:
def __init__(self, capacities, demands, valid_edges, atol=TOLERANCE):
self.capacities = capacities
self.demands = demands
self.valid_edges = valid_edges
self.atol = atol
self.errors = []
self.delivered_flows = {}
def _add_error(self, msg):
self.errors.append(msg)
def validate_capacity(self, df_flows):
df_flows['physical_edge'] = df_flows.apply(lambda r: '-'.join(sorted([r['u'], r['v']])), axis=1)
edge_loads = df_flows.groupby('physical_edge')['abs_flow'].sum()
for edge, load in edge_loads.items():
if self.valid_edges and edge not in self.valid_edges:
self._add_error(f"Edge {edge} used in solution does not exist in the physical graph")
continue
if edge in self.capacities:
cap = self.capacities[edge]
if load > cap + self.atol:
self._add_error(f"Capacity exceeded on edge {edge}: load {load:.2f} > limit {cap:.2f}")
return edge_loads
def validate_conservation(self, df_flows):
for cid, group in df_flows.groupby('commodity_id'):
if cid not in self.demands:
self._add_error(f"Unknown commodity {cid}")
continue
req = self.demands[cid]
in_flows = group.groupby('v')['flow'].sum()
out_flows = group.groupby('u')['flow'].sum()
nodes = set(in_flows.index).union(set(out_flows.index))
for node in nodes:
net_flow = in_flows.get(node, 0.0) - out_flows.get(node, 0.0)
if node == req['source']:
if net_flow > self.atol:
self._add_error(f"Source {node} for commodity {cid} has positive net flow ({net_flow:.2f})")
elif node == req['target']:
if net_flow < -self.atol:
self._add_error(f"Target {node} for commodity {cid} has negative net flow ({net_flow:.2f})")
self.delivered_flows[cid] = net_flow
else:
if abs(net_flow) > self.atol:
self._add_error(f"Conservation failed at transit node {node} for commodity {cid} (net flow: {net_flow:.2f})")
if len(self.errors) >= MAX_ERRORS_TO_SHOW:
return
def calculate_metrics(self, df_flows, edge_loads):
alphas = {}
total_flow = 0.0
for cid, req in self.demands.items():
f_k = self.delivered_flows.get(cid, 0.0)
a_k = min(f_k / req['volume'] if req['volume'] > 0 else 0.0, 1.0)
alphas[cid] = a_k
total_flow += f_k
min_alpha = min(alphas.values()) if alphas else 0.0
if min_alpha < self.atol:
zero_commodities = [str(cid) for cid, a in alphas.items() if a < self.atol]
self._add_error(f"Обнаружен нулевой или пренебрежимо малый поток для продуктов: {', '.join(zero_commodities[:10])}" + ("..." if len(zero_commodities) > 10 else ""))
return None, None, None
bottlenecks = [edge for edge, load in edge_loads.items() if edge in self.capacities and load >= self.capacities[edge] - self.atol]
std_devs = []
if bottlenecks:
for b_edge in bottlenecks:
b_flows = df_flows[(df_flows['physical_edge'] == b_edge) & (df_flows['abs_flow'] > self.atol)]
cids_on_edge = b_flows['commodity_id'].unique()
if len(cids_on_edge) > 1:
edge_alphas = [alphas[cid] for cid in cids_on_edge]
std_devs.append(np.std(edge_alphas))
prop_error = np.mean(std_devs) if std_devs else 0.0
return total_flow, min_alpha, prop_error
def evaluate_solution(solution_data, capacities, demands, valid_edges, atol=TOLERANCE):
flows = solution_data.get('flows', [])
metadata = solution_data.get('metadata', {})
exec_time = metadata.get('execution_time_sec', 0.0)
algo_name = metadata.get('algorithm_name', 'Unknown')
logger.info(f"Starting evaluation for algorithm: '{algo_name}' with {len(flows)} flows.")
def invalid_result(error_msgs):
if len(error_msgs) >= MAX_ERRORS_TO_SHOW:
error_msgs = error_msgs[:MAX_ERRORS_TO_SHOW] + ["...and more errors (truncated)."]
return {
"algorithm_name": algo_name, "status": "Invalid",
"error": "\n".join(error_msgs), "total_flow": 0.0,
"min_alpha": 0.0, "proportionality_error": float('inf'),
"execution_time_sec": exec_time
}
if not flows:
return invalid_result(["No flows provided in 'flows' key."])
df_flows = pd.DataFrame(flows)
required_cols = {'commodity_id', 'u', 'v', 'flow'}
if not required_cols.issubset(df_flows.columns):
return invalid_result([f"Solution missing required keys: {', '.join(required_cols - set(df_flows.columns))}"])
try:
df_flows['u'] = df_flows['u'].astype(str).str.strip()
df_flows['v'] = df_flows['v'].astype(str).str.strip()
df_flows['commodity_id'] = df_flows['commodity_id'].astype(int)
df_flows['flow'] = df_flows['flow'].astype(float)
df_flows['abs_flow'] = df_flows['flow'].abs()
except Exception as e:
return invalid_result([f"Error parsing flow data types: {e}"])
validator = FlowValidator(capacities, demands, valid_edges, atol)
edge_loads = validator.validate_capacity(df_flows)
if validator.errors:
return invalid_result(validator.errors)
validator.validate_conservation(df_flows)
if validator.errors:
return invalid_result(validator.errors)
total_flow, min_alpha, prop_error = validator.calculate_metrics(df_flows, edge_loads)
if validator.errors:
return invalid_result(validator.errors)
return {
"algorithm_name": algo_name,
"status": "Valid",
"error": "",
"total_flow": float(total_flow),
"min_alpha": float(min_alpha),
"proportionality_error": float(prop_error),
"execution_time_sec": float(exec_time)
}