File size: 10,162 Bytes
308b322
30267f8
308b322
 
 
30267f8
 
1f66689
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308b322
 
682e246
 
 
30267f8
 
682e246
 
 
 
 
 
 
 
 
 
 
30267f8
682e246
30267f8
682e246
308b322
 
 
1f66689
308b322
 
 
 
 
 
 
1f66689
682e246
308b322
 
682e246
308b322
1f66689
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2012887
1f66689
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2012887
1f66689
308b322
 
 
 
 
30267f8
 
682e246
1f66689
 
308b322
1f66689
 
 
308b322
 
 
 
1f66689
308b322
 
682e246
 
1f66689
682e246
 
 
 
 
 
1f66689
682e246
1f66689
682e246
1f66689
308b322
1f66689
 
 
308b322
1f66689
 
 
308b322
1f66689
 
 
308b322
 
 
 
 
 
 
 
 
1f66689
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
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)
    }