pyconvexity 0.1.3__py3-none-any.whl → 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyconvexity might be problematic. Click here for more details.
- pyconvexity/__init__.py +27 -2
- pyconvexity/_version.py +1 -2
- pyconvexity/core/__init__.py +0 -2
- pyconvexity/core/database.py +158 -0
- pyconvexity/core/types.py +105 -18
- pyconvexity/data/__pycache__/__init__.cpython-313.pyc +0 -0
- pyconvexity/data/loaders/__pycache__/__init__.cpython-313.pyc +0 -0
- pyconvexity/data/loaders/__pycache__/cache.cpython-313.pyc +0 -0
- pyconvexity/data/schema/01_core_schema.sql +12 -12
- pyconvexity/data/schema/02_data_metadata.sql +17 -321
- pyconvexity/data/sources/__pycache__/__init__.cpython-313.pyc +0 -0
- pyconvexity/data/sources/__pycache__/gem.cpython-313.pyc +0 -0
- pyconvexity/data/sources/gem.py +5 -5
- pyconvexity/io/excel_exporter.py +34 -13
- pyconvexity/io/excel_importer.py +48 -51
- pyconvexity/io/netcdf_importer.py +1054 -51
- pyconvexity/models/attributes.py +209 -72
- pyconvexity/models/network.py +17 -15
- pyconvexity/solvers/pypsa/api.py +24 -1
- pyconvexity/solvers/pypsa/batch_loader.py +37 -44
- pyconvexity/solvers/pypsa/builder.py +62 -152
- pyconvexity/solvers/pypsa/solver.py +104 -253
- pyconvexity/solvers/pypsa/storage.py +740 -1373
- pyconvexity/timeseries.py +327 -0
- pyconvexity/validation/rules.py +2 -2
- {pyconvexity-0.1.3.dist-info → pyconvexity-0.1.4.dist-info}/METADATA +1 -1
- pyconvexity-0.1.4.dist-info/RECORD +46 -0
- pyconvexity-0.1.3.dist-info/RECORD +0 -45
- {pyconvexity-0.1.3.dist-info → pyconvexity-0.1.4.dist-info}/WHEEL +0 -0
- {pyconvexity-0.1.3.dist-info → pyconvexity-0.1.4.dist-info}/top_level.txt +0 -0
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Network building functionality for PyPSA solver integration.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
Simplified to always use MultiIndex format for consistent multi-period optimization.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
+
import json
|
|
8
9
|
import pandas as pd
|
|
9
10
|
from typing import Dict, Any, Optional, Callable
|
|
10
11
|
|
|
11
12
|
from pyconvexity.models import (
|
|
12
|
-
list_components_by_type, get_network_time_periods,
|
|
13
|
+
list_components_by_type, get_network_time_periods, get_network_info
|
|
13
14
|
)
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
@@ -19,9 +20,8 @@ class NetworkBuilder:
|
|
|
19
20
|
"""
|
|
20
21
|
Builds PyPSA networks from database data.
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
a properly configured PyPSA Network object.
|
|
23
|
+
Simplified to always create MultiIndex snapshots for consistent multi-period optimization,
|
|
24
|
+
even for single-year models.
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
27
|
def __init__(self):
|
|
@@ -36,13 +36,8 @@ class NetworkBuilder:
|
|
|
36
36
|
) from e
|
|
37
37
|
|
|
38
38
|
# Import batch loader for efficient data loading
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
self.batch_loader = PyPSABatchLoader()
|
|
42
|
-
except ImportError:
|
|
43
|
-
# Fallback to individual loading if batch loader not available
|
|
44
|
-
self.batch_loader = None
|
|
45
|
-
logger.warning("PyPSABatchLoader not available, using individual component loading")
|
|
39
|
+
from pyconvexity.solvers.pypsa.batch_loader import PyPSABatchLoader
|
|
40
|
+
self.batch_loader = PyPSABatchLoader()
|
|
46
41
|
|
|
47
42
|
def build_network(
|
|
48
43
|
self,
|
|
@@ -87,7 +82,7 @@ class NetworkBuilder:
|
|
|
87
82
|
if progress_callback:
|
|
88
83
|
progress_callback(20, "Loading components...")
|
|
89
84
|
|
|
90
|
-
# Load all components
|
|
85
|
+
# Load all components using efficient batch loader
|
|
91
86
|
self._load_components(conn, network_id, network, scenario_id, progress_callback)
|
|
92
87
|
|
|
93
88
|
# NOTE: Snapshot weightings will be set AFTER multi-period optimization setup
|
|
@@ -184,35 +179,44 @@ class NetworkBuilder:
|
|
|
184
179
|
}
|
|
185
180
|
|
|
186
181
|
def _set_time_index(self, conn, network_id: int, network: 'pypsa.Network'):
|
|
187
|
-
"""Set time index from network time periods."""
|
|
182
|
+
"""Set time index from network time periods - always create MultiIndex for consistency."""
|
|
188
183
|
try:
|
|
189
184
|
time_periods = get_network_time_periods(conn, network_id)
|
|
190
|
-
if time_periods:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
# Set the snapshots to the timestamps
|
|
195
|
-
network.set_snapshots(timestamps)
|
|
185
|
+
if not time_periods:
|
|
186
|
+
logger.error("No time periods found for network")
|
|
187
|
+
return
|
|
196
188
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
189
|
+
# Convert to pandas DatetimeIndex
|
|
190
|
+
timestamps = [pd.Timestamp(tp.formatted_time) for tp in time_periods]
|
|
191
|
+
|
|
192
|
+
# Extract unique years for investment periods
|
|
193
|
+
years = sorted(list(set([ts.year for ts in timestamps])))
|
|
194
|
+
logger.info(f"Found {len(years)} investment periods: {years}")
|
|
195
|
+
|
|
196
|
+
# Always create MultiIndex following PyPSA multi-investment tutorial format
|
|
197
|
+
# First level: investment periods (years), Second level: timesteps
|
|
198
|
+
multi_snapshots = []
|
|
199
|
+
for ts in timestamps:
|
|
200
|
+
multi_snapshots.append((ts.year, ts))
|
|
201
|
+
|
|
202
|
+
multi_index = pd.MultiIndex.from_tuples(multi_snapshots, names=['period', 'timestep'])
|
|
203
|
+
|
|
204
|
+
# Verify MultiIndex is unique (should always be true now with UTC timestamps)
|
|
205
|
+
if not multi_index.is_unique:
|
|
206
|
+
raise ValueError(f"Created MultiIndex is not unique! Check timestamp generation.")
|
|
207
|
+
|
|
208
|
+
logger.info(f"Created MultiIndex with {len(multi_index)} snapshots")
|
|
209
|
+
network.set_snapshots(multi_index)
|
|
210
|
+
|
|
211
|
+
# Set investment periods for multi-period optimization
|
|
212
|
+
network.investment_periods = years
|
|
213
|
+
|
|
214
|
+
# Store years for statistics
|
|
215
|
+
network._available_years = years
|
|
216
|
+
|
|
217
|
+
logger.info(f"Created MultiIndex with {len(multi_index)} snapshots across {len(years)} periods")
|
|
218
|
+
logger.info(f"Investment periods: {network.investment_periods}")
|
|
212
219
|
|
|
213
|
-
else:
|
|
214
|
-
logger.warning("No time periods found for network, year-based statistics will not be available")
|
|
215
|
-
network._available_years = []
|
|
216
220
|
except Exception as e:
|
|
217
221
|
logger.error(f"Failed to set time index: {e}")
|
|
218
222
|
network._available_years = []
|
|
@@ -260,15 +264,11 @@ class NetworkBuilder:
|
|
|
260
264
|
scenario_id: Optional[int],
|
|
261
265
|
progress_callback: Optional[Callable[[int, str], None]] = None
|
|
262
266
|
):
|
|
263
|
-
"""Load all network components."""
|
|
264
|
-
# Load component connections
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
carrier_id_to_name = connections['carrier_id_to_name']
|
|
269
|
-
else:
|
|
270
|
-
bus_id_to_name = self._build_bus_id_to_name_map(conn, network_id)
|
|
271
|
-
carrier_id_to_name = self._build_carrier_id_to_name_map(conn, network_id)
|
|
267
|
+
"""Load all network components using batch loader."""
|
|
268
|
+
# Load component connections
|
|
269
|
+
connections = self.batch_loader.batch_load_component_connections(conn, network_id)
|
|
270
|
+
bus_id_to_name = connections['bus_id_to_name']
|
|
271
|
+
carrier_id_to_name = connections['carrier_id_to_name']
|
|
272
272
|
|
|
273
273
|
# Component type mapping for later identification
|
|
274
274
|
component_type_map = {}
|
|
@@ -307,17 +307,14 @@ class NetworkBuilder:
|
|
|
307
307
|
# Store component type mapping on network
|
|
308
308
|
network._component_type_map = component_type_map
|
|
309
309
|
|
|
310
|
+
|
|
310
311
|
def _load_buses(self, conn, network_id: int, network: 'pypsa.Network', scenario_id: Optional[int], component_type_map: Dict[str, str]):
|
|
311
312
|
"""Load bus components."""
|
|
312
313
|
buses = list_components_by_type(conn, network_id, 'BUS')
|
|
313
314
|
bus_ids = [bus.id for bus in buses]
|
|
314
315
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
bus_timeseries = self.batch_loader.batch_load_component_timeseries(conn, bus_ids, scenario_id)
|
|
318
|
-
else:
|
|
319
|
-
bus_attributes = self._load_component_attributes_individually(conn, bus_ids, scenario_id)
|
|
320
|
-
bus_timeseries = {}
|
|
316
|
+
bus_attributes = self.batch_loader.batch_load_component_attributes(conn, bus_ids, scenario_id)
|
|
317
|
+
bus_timeseries = self.batch_loader.batch_load_component_timeseries(conn, bus_ids, scenario_id)
|
|
321
318
|
|
|
322
319
|
for bus in buses:
|
|
323
320
|
attrs = bus_attributes.get(bus.id, {})
|
|
@@ -338,12 +335,8 @@ class NetworkBuilder:
|
|
|
338
335
|
|
|
339
336
|
generator_ids = [gen.id for gen in all_generators]
|
|
340
337
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
generator_timeseries = self.batch_loader.batch_load_component_timeseries(conn, generator_ids, scenario_id)
|
|
344
|
-
else:
|
|
345
|
-
generator_attributes = self._load_component_attributes_individually(conn, generator_ids, scenario_id)
|
|
346
|
-
generator_timeseries = {}
|
|
338
|
+
generator_attributes = self.batch_loader.batch_load_component_attributes(conn, generator_ids, scenario_id)
|
|
339
|
+
generator_timeseries = self.batch_loader.batch_load_component_timeseries(conn, generator_ids, scenario_id)
|
|
347
340
|
|
|
348
341
|
for gen in all_generators:
|
|
349
342
|
attrs = generator_attributes.get(gen.id, {})
|
|
@@ -373,12 +366,8 @@ class NetworkBuilder:
|
|
|
373
366
|
loads = list_components_by_type(conn, network_id, 'LOAD')
|
|
374
367
|
load_ids = [load.id for load in loads]
|
|
375
368
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
load_timeseries = self.batch_loader.batch_load_component_timeseries(conn, load_ids, scenario_id)
|
|
379
|
-
else:
|
|
380
|
-
load_attributes = self._load_component_attributes_individually(conn, load_ids, scenario_id)
|
|
381
|
-
load_timeseries = {}
|
|
369
|
+
load_attributes = self.batch_loader.batch_load_component_attributes(conn, load_ids, scenario_id)
|
|
370
|
+
load_timeseries = self.batch_loader.batch_load_component_timeseries(conn, load_ids, scenario_id)
|
|
382
371
|
|
|
383
372
|
for load in loads:
|
|
384
373
|
attrs = load_attributes.get(load.id, {})
|
|
@@ -405,12 +394,8 @@ class NetworkBuilder:
|
|
|
405
394
|
lines = list_components_by_type(conn, network_id, 'LINE')
|
|
406
395
|
line_ids = [line.id for line in lines]
|
|
407
396
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
line_timeseries = self.batch_loader.batch_load_component_timeseries(conn, line_ids, scenario_id)
|
|
411
|
-
else:
|
|
412
|
-
line_attributes = self._load_component_attributes_individually(conn, line_ids, scenario_id)
|
|
413
|
-
line_timeseries = {}
|
|
397
|
+
line_attributes = self.batch_loader.batch_load_component_attributes(conn, line_ids, scenario_id)
|
|
398
|
+
line_timeseries = self.batch_loader.batch_load_component_timeseries(conn, line_ids, scenario_id)
|
|
414
399
|
|
|
415
400
|
for line in lines:
|
|
416
401
|
attrs = line_attributes.get(line.id, {})
|
|
@@ -439,12 +424,8 @@ class NetworkBuilder:
|
|
|
439
424
|
links = list_components_by_type(conn, network_id, 'LINK')
|
|
440
425
|
link_ids = [link.id for link in links]
|
|
441
426
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
link_timeseries = self.batch_loader.batch_load_component_timeseries(conn, link_ids, scenario_id)
|
|
445
|
-
else:
|
|
446
|
-
link_attributes = self._load_component_attributes_individually(conn, link_ids, scenario_id)
|
|
447
|
-
link_timeseries = {}
|
|
427
|
+
link_attributes = self.batch_loader.batch_load_component_attributes(conn, link_ids, scenario_id)
|
|
428
|
+
link_timeseries = self.batch_loader.batch_load_component_timeseries(conn, link_ids, scenario_id)
|
|
448
429
|
|
|
449
430
|
for link in links:
|
|
450
431
|
attrs = link_attributes.get(link.id, {})
|
|
@@ -473,12 +454,8 @@ class NetworkBuilder:
|
|
|
473
454
|
storage_units = list_components_by_type(conn, network_id, 'STORAGE_UNIT')
|
|
474
455
|
storage_ids = [storage.id for storage in storage_units]
|
|
475
456
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
storage_timeseries = self.batch_loader.batch_load_component_timeseries(conn, storage_ids, scenario_id)
|
|
479
|
-
else:
|
|
480
|
-
storage_attributes = self._load_component_attributes_individually(conn, storage_ids, scenario_id)
|
|
481
|
-
storage_timeseries = {}
|
|
457
|
+
storage_attributes = self.batch_loader.batch_load_component_attributes(conn, storage_ids, scenario_id)
|
|
458
|
+
storage_timeseries = self.batch_loader.batch_load_component_timeseries(conn, storage_ids, scenario_id)
|
|
482
459
|
|
|
483
460
|
for storage in storage_units:
|
|
484
461
|
attrs = storage_attributes.get(storage.id, {})
|
|
@@ -505,12 +482,8 @@ class NetworkBuilder:
|
|
|
505
482
|
stores = list_components_by_type(conn, network_id, 'STORE')
|
|
506
483
|
store_ids = [store.id for store in stores]
|
|
507
484
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
store_timeseries = self.batch_loader.batch_load_component_timeseries(conn, store_ids, scenario_id)
|
|
511
|
-
else:
|
|
512
|
-
store_attributes = self._load_component_attributes_individually(conn, store_ids, scenario_id)
|
|
513
|
-
store_timeseries = {}
|
|
485
|
+
store_attributes = self.batch_loader.batch_load_component_attributes(conn, store_ids, scenario_id)
|
|
486
|
+
store_timeseries = self.batch_loader.batch_load_component_timeseries(conn, store_ids, scenario_id)
|
|
514
487
|
|
|
515
488
|
for store in stores:
|
|
516
489
|
attrs = store_attributes.get(store.id, {})
|
|
@@ -591,66 +564,3 @@ class NetworkBuilder:
|
|
|
591
564
|
cursor = conn.execute("SELECT id, name FROM carriers WHERE network_id = ?", (network_id,))
|
|
592
565
|
return {row[0]: row[1] for row in cursor.fetchall()}
|
|
593
566
|
|
|
594
|
-
def _load_component_attributes_individually(self, conn, component_ids: list, scenario_id: Optional[int]) -> Dict[int, Dict[str, Any]]:
|
|
595
|
-
"""Fallback method to load component attributes individually."""
|
|
596
|
-
from pyconvexity.models import get_attribute, list_component_attributes
|
|
597
|
-
from pyconvexity.core.types import AttributeValue
|
|
598
|
-
from pyconvexity.models.network import get_network_time_periods
|
|
599
|
-
import pandas as pd
|
|
600
|
-
|
|
601
|
-
# Get network time periods for proper timestamp alignment
|
|
602
|
-
network_time_periods = None
|
|
603
|
-
if component_ids:
|
|
604
|
-
cursor = conn.execute("SELECT network_id FROM components WHERE id = ? LIMIT 1", (component_ids[0],))
|
|
605
|
-
result = cursor.fetchone()
|
|
606
|
-
if result:
|
|
607
|
-
network_id = result[0]
|
|
608
|
-
try:
|
|
609
|
-
network_time_periods = get_network_time_periods(conn, network_id)
|
|
610
|
-
except Exception as e:
|
|
611
|
-
logger.warning(f"Failed to load network time periods: {e}")
|
|
612
|
-
|
|
613
|
-
attributes = {}
|
|
614
|
-
for comp_id in component_ids:
|
|
615
|
-
try:
|
|
616
|
-
attr_names = list_component_attributes(conn, comp_id)
|
|
617
|
-
comp_attrs = {}
|
|
618
|
-
for attr_name in attr_names:
|
|
619
|
-
try:
|
|
620
|
-
attr_value = get_attribute(conn, comp_id, attr_name, scenario_id)
|
|
621
|
-
if attr_value is not None:
|
|
622
|
-
# Handle different attribute value types
|
|
623
|
-
if hasattr(attr_value, 'static_value') and attr_value.static_value is not None:
|
|
624
|
-
# Static value
|
|
625
|
-
comp_attrs[attr_name] = attr_value.static_value.value()
|
|
626
|
-
elif hasattr(attr_value, 'timeseries_value') and attr_value.timeseries_value is not None:
|
|
627
|
-
# Timeseries value - convert to pandas Series with proper timestamps
|
|
628
|
-
timeseries_points = attr_value.timeseries_value
|
|
629
|
-
if timeseries_points:
|
|
630
|
-
# Sort by period_index to ensure correct order
|
|
631
|
-
timeseries_points.sort(key=lambda x: x.period_index)
|
|
632
|
-
values = [point.value for point in timeseries_points]
|
|
633
|
-
|
|
634
|
-
# Create proper timestamps for PyPSA alignment
|
|
635
|
-
if network_time_periods:
|
|
636
|
-
timestamps = []
|
|
637
|
-
for point in timeseries_points:
|
|
638
|
-
if point.period_index < len(network_time_periods):
|
|
639
|
-
tp = network_time_periods[point.period_index]
|
|
640
|
-
timestamps.append(pd.Timestamp(tp.formatted_time))
|
|
641
|
-
else:
|
|
642
|
-
logger.warning(f"Period index {point.period_index} out of range for network time periods")
|
|
643
|
-
timestamps.append(pd.Timestamp.now()) # Fallback
|
|
644
|
-
comp_attrs[attr_name] = pd.Series(values, index=timestamps)
|
|
645
|
-
else:
|
|
646
|
-
# Fallback: use period_index as index
|
|
647
|
-
period_indices = [point.period_index for point in timeseries_points]
|
|
648
|
-
comp_attrs[attr_name] = pd.Series(values, index=period_indices)
|
|
649
|
-
except Exception as e:
|
|
650
|
-
logger.debug(f"Failed to load attribute {attr_name} for component {comp_id}: {e}")
|
|
651
|
-
attributes[comp_id] = comp_attrs
|
|
652
|
-
except Exception as e:
|
|
653
|
-
logger.warning(f"Failed to load attributes for component {comp_id}: {e}")
|
|
654
|
-
attributes[comp_id] = {}
|
|
655
|
-
|
|
656
|
-
return attributes
|