Spaces:
Sleeping
Sleeping
| 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: | |
| 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: | |
| 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}") | |
| def _parse_json(path_str): | |
| with open(path_str, 'r') as f: | |
| return json.load(f) | |
| 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) | |
| } |