File size: 12,382 Bytes
27cb60a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
"""
Phase 1D: Comprehensive FBMC Border Query for Asset-Specific Outages
=====================================================================

Queries all FBMC borders systematically to maximize CNEC coverage.

Approach:
1. Define all FBMC bidding zone EIC codes
2. Query transmission outages for all border pairs
3. Parse XML to extract Asset_RegisteredResource.mRID from each
4. Aggregate all extracted EICs and match against 200 CNEC list
5. Report coverage statistics

Expected outcome: 40-80% CNEC coverage (80-165 features)
"""

import os
import sys
from pathlib import Path
import pandas as pd
import polars as pl
import zipfile
from io import BytesIO
import xml.etree.ElementTree as ET
from dotenv import load_dotenv
from entsoe import EntsoePandasClient
import time

sys.path.append(str(Path(__file__).parent.parent))
load_dotenv()

API_KEY = os.getenv('ENTSOE_API_KEY')
client = EntsoePandasClient(api_key=API_KEY)

print("="*80)
print("PHASE 1D: COMPREHENSIVE FBMC BORDER QUERY")
print("="*80)
print()

# ============================================================================
# FBMC Bidding Zones (EIC Codes)
# ============================================================================

FBMC_ZONES = {
    'AT': '10YAT-APG------L',      # Austria
    'BE': '10YBE----------2',       # Belgium
    'HR': '10YHR-HEP------M',       # Croatia
    'CZ': '10YCZ-CEPS-----N',       # Czech Republic
    'FR': '10YFR-RTE------C',       # France
    'DE_LU': '10Y1001A1001A82H',    # Germany-Luxembourg
    'HU': '10YHU-MAVIR----U',       # Hungary
    'NL': '10YNL----------L',       # Netherlands
    'PL': '10YPL-AREA-----S',       # Poland
    'RO': '10YRO-TEL------P',       # Romania
    'SK': '10YSK-SEPS-----K',       # Slovakia
    'SI': '10YSI-ELES-----O',       # Slovenia
    'CH': '10YCH-SWISSGRIDZ'        # Switzerland (also part of FBMC)
}

# ============================================================================
# FBMC Border Pairs (Known Interconnections)
# ============================================================================
# Based on European transmission network topology

FBMC_BORDERS = [
    # Germany-Luxembourg borders
    ('DE_LU', 'FR'),
    ('DE_LU', 'BE'),
    ('DE_LU', 'NL'),
    ('DE_LU', 'AT'),
    ('DE_LU', 'CZ'),
    ('DE_LU', 'PL'),
    ('DE_LU', 'CH'),

    # France borders
    ('FR', 'BE'),
    ('FR', 'CH'),

    # Austria borders
    ('AT', 'CZ'),
    ('AT', 'HU'),
    ('AT', 'SI'),
    ('AT', 'CH'),

    # Czech Republic borders
    ('CZ', 'SK'),
    ('CZ', 'PL'),

    # Poland borders
    ('PL', 'SK'),

    # Slovakia borders
    ('SK', 'HU'),

    # Hungary borders
    ('HU', 'RO'),
    ('HU', 'HR'),
    ('HU', 'SI'),

    # Slovenia borders
    ('SI', 'HR'),

    # Belgium borders
    ('BE', 'NL'),
]

print(f"FBMC Bidding Zones: {len(FBMC_ZONES)}")
print(f"Border Pairs to Query: {len(FBMC_BORDERS)}")
print()

# ============================================================================
# Load CNEC EIC Codes
# ============================================================================

print("Loading 200 CNEC EIC codes...")
cnec_file = Path(__file__).parent.parent / 'data' / 'processed' / 'critical_cnecs_all.csv'
cnec_df = pl.read_csv(cnec_file)
cnec_eics = cnec_df.select('cnec_eic').to_series().to_list()
print(f"[OK] Loaded {len(cnec_eics)} CNEC EICs")
print()

# ============================================================================
# Query All Borders for Transmission Outages
# ============================================================================

print("-"*80)
print("QUERYING ALL FBMC BORDERS")
print("-"*80)
print()

all_extracted_eics = []
border_results = {}

start_time = time.time()
query_count = 0

for i, (zone1, zone2) in enumerate(FBMC_BORDERS, 1):
    border_name = f"{zone1} -> {zone2}"
    print(f"[{i}/{len(FBMC_BORDERS)}] {border_name}...")

    try:
        # Query transmission outages for this border
        response = client._base_request(
            params={
                'documentType': 'A78',  # Transmission unavailability
                'in_Domain': FBMC_ZONES[zone2],
                'out_Domain': FBMC_ZONES[zone1]
            },
            start=pd.Timestamp('2025-09-23', tz='UTC'),
            end=pd.Timestamp('2025-09-30', tz='UTC')
        )

        outages_zip = response.content
        query_count += 1

        # Parse ZIP and extract Asset_RegisteredResource.mRID
        border_eics = []

        with zipfile.ZipFile(BytesIO(outages_zip), 'r') as zf:
            xml_files = [f for f in zf.namelist() if f.endswith('.xml')]

            for xml_file in xml_files:
                with zf.open(xml_file) as xf:
                    xml_content = xf.read()
                    root = ET.fromstring(xml_content)

                    # Get namespace
                    nsmap = dict([node for _, node in ET.iterparse(BytesIO(xml_content), events=['start-ns'])])
                    ns_uri = nsmap.get('', None)

                    # Find TimeSeries elements
                    if ns_uri:
                        timeseries_found = root.findall('.//{' + ns_uri + '}TimeSeries')
                    else:
                        timeseries_found = root.findall('.//TimeSeries')

                    for ts in timeseries_found:
                        # Extract Asset_RegisteredResource.mRID
                        if ns_uri:
                            reg_resource = ts.find('.//{' + ns_uri + '}Asset_RegisteredResource')
                        else:
                            reg_resource = ts.find('.//Asset_RegisteredResource')

                        if reg_resource is not None:
                            if ns_uri:
                                mrid_elem = reg_resource.find('.//{' + ns_uri + '}mRID')
                            else:
                                mrid_elem = reg_resource.find('.//mRID')

                            if mrid_elem is not None:
                                eic_code = mrid_elem.text
                                border_eics.append(eic_code)

        # Store results
        unique_border_eics = list(set(border_eics))
        border_matches = [eic for eic in unique_border_eics if eic in cnec_eics]

        border_results[border_name] = {
            'total_eics': len(unique_border_eics),
            'cnec_matches': len(border_matches),
            'matched_eics': border_matches
        }

        all_extracted_eics.extend(border_eics)

        print(f"  EICs extracted: {len(unique_border_eics)}, CNEC matches: {len(border_matches)}")

        # Rate limiting: 27 requests per minute
        if i < len(FBMC_BORDERS):
            time.sleep(2.2)

    except Exception as e:
        print(f"  [FAIL] {e}")
        border_results[border_name] = {
            'total_eics': 0,
            'cnec_matches': 0,
            'matched_eics': [],
            'error': str(e)
        }

total_time = time.time() - start_time

print()
print("="*80)
print("AGGREGATED RESULTS")
print("="*80)
print()

# Aggregate statistics
unique_eics = list(set(all_extracted_eics))
cnec_matches = [eic for eic in unique_eics if eic in cnec_eics]
match_rate = len(cnec_matches) / len(cnec_eics) * 100

print(f"Query Statistics:")
print(f"  Borders queried: {query_count}")
print(f"  Total time: {total_time / 60:.1f} minutes")
print(f"  Avg time per border: {total_time / query_count:.1f} seconds")
print()

print(f"EIC Extraction Results:")
print(f"  Total asset EICs extracted: {len(all_extracted_eics)} (with duplicates)")
print(f"  Unique asset EICs: {len(unique_eics)}")
print()

print(f"CNEC Matching Results:")
print(f"  CNEC EICs matched: {len(cnec_matches)} / {len(cnec_eics)}")
print(f"  Match rate: {match_rate:.1f}%")
print()

# ============================================================================
# Detailed Border Breakdown
# ============================================================================

print("-"*80)
print("BORDER-BY-BORDER BREAKDOWN")
print("-"*80)
print()

# Sort borders by number of CNEC matches (descending)
sorted_borders = sorted(
    border_results.items(),
    key=lambda x: x[1]['cnec_matches'],
    reverse=True
)

for border_name, result in sorted_borders:
    if result['cnec_matches'] > 0:
        print(f"{border_name}:")
        print(f"  Total EICs: {result['total_eics']}")
        print(f"  CNEC matches: {result['cnec_matches']}")

        # Show matched CNEC names
        for eic in result['matched_eics'][:5]:  # First 5
            try:
                cnec_name = cnec_df.filter(pl.col('cnec_eic') == eic).select('cnec_name').item(0, 0)
                print(f"    - {eic}: {cnec_name}")
            except:
                print(f"    - {eic}")

        if result['cnec_matches'] > 5:
            print(f"    ... and {result['cnec_matches'] - 5} more")
        print()

print()

# ============================================================================
# Coverage Analysis
# ============================================================================

print("="*80)
print("COVERAGE ANALYSIS")
print("="*80)
print()

if match_rate >= 80:
    print(f"[EXCELLENT] {match_rate:.1f}% CNEC coverage achieved!")
    print(f">> Can implement {len(cnec_matches)}-feature asset-specific outages")
    print(f">> Exceeds 80% target - comprehensive coverage")
elif match_rate >= 40:
    print(f"[GOOD] {match_rate:.1f}% CNEC coverage achieved!")
    print(f">> Can implement {len(cnec_matches)}-feature asset-specific outages")
    print(f">> Meets 40-80% target range")
elif match_rate >= 20:
    print(f"[PARTIAL] {match_rate:.1f}% CNEC coverage")
    print(f">> Can implement {len(cnec_matches)}-feature asset-specific outages")
    print(f">> Below 40% target but still useful")
else:
    print(f"[LIMITED] {match_rate:.1f}% CNEC coverage")
    print(f">> Only {len(cnec_matches)} CNECs matched")
    print(f">> May need to investigate EIC code mapping or alternative approaches")

print()

# ============================================================================
# Non-Matching EICs (for investigation)
# ============================================================================

non_matches = [eic for eic in unique_eics if eic not in cnec_eics]
if non_matches:
    print("-"*80)
    print("NON-MATCHING TRANSMISSION ELEMENT EICs")
    print("-"*80)
    print()
    print(f"Total non-matching EICs: {len(non_matches)}")
    print()
    print("Sample non-matching EICs (first 20):")
    for eic in non_matches[:20]:
        print(f"  - {eic}")
    if len(non_matches) > 20:
        print(f"  ... and {len(non_matches) - 20} more")
    print()
    print("These are transmission elements NOT in the 200 CNEC list.")
    print("They may be:")
    print("  1. Non-critical transmission lines (not in JAO CNEC list)")
    print("  2. Internal lines (not cross-border)")
    print("  3. Different EIC code format (JAO vs ENTSO-E)")

print()

# ============================================================================
# SUMMARY & NEXT STEPS
# ============================================================================

print("="*80)
print("PHASE 1D SUMMARY")
print("="*80)
print()

print(f"Asset-Specific Transmission Outages: {len(cnec_matches)} features")
print(f"  Coverage: {match_rate:.1f}% of 200 CNECs")
print(f"  Implementation: Parse border-level XML, filter to CNEC EICs")
print()

print("Combined ENTSO-E Features (Estimated):")
print(f"  - Generation (12 zones × 8 types): 96 features")
print(f"  - Demand (12 zones): 12 features")
print(f"  - Day-ahead prices (12 zones): 12 features")
print(f"  - Hydro reservoirs (7 zones): 7 features")
print(f"  - Pumped storage generation (7 zones): 7 features")
print(f"  - Load forecasts (12 zones): 12 features")
print(f"  - Transmission outages (asset-specific): {len(cnec_matches)} features")
print(f"  - Generation outages (nuclear): ~20 features")
print(f"  TOTAL ENTSO-E: {146 + len(cnec_matches)} features")
print()

print("Combined with JAO (726 features):")
print(f"  GRAND TOTAL: {726 + 146 + len(cnec_matches)} features")
print()

print("="*80)
print("NEXT STEPS:")
print("1. Extend collect_entsoe.py with XML parsing method")
print("2. Implement process_entsoe_features.py for outage encoding")
print("3. Collect 24-month historical ENTSO-E data")
print("4. Create ENTSO-E features EDA notebook")
print("5. Merge JAO + ENTSO-E features")
print("="*80)