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) }