pyconvexity 0.4.2.post1__py3-none-any.whl → 0.4.6__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/_version.py +1 -1
- pyconvexity/data/schema/03_validation_data.sql +2 -1
- pyconvexity/models/network.py +93 -2
- pyconvexity/solvers/pypsa/api.py +15 -10
- pyconvexity/solvers/pypsa/builder.py +16 -12
- pyconvexity/solvers/pypsa/solver.py +19 -231
- pyconvexity/solvers/pypsa/storage.py +22 -254
- pyconvexity-0.4.6.dist-info/METADATA +148 -0
- {pyconvexity-0.4.2.post1.dist-info → pyconvexity-0.4.6.dist-info}/RECORD +11 -11
- pyconvexity-0.4.2.post1.dist-info/METADATA +0 -47
- {pyconvexity-0.4.2.post1.dist-info → pyconvexity-0.4.6.dist-info}/WHEEL +0 -0
- {pyconvexity-0.4.2.post1.dist-info → pyconvexity-0.4.6.dist-info}/top_level.txt +0 -0
|
@@ -29,6 +29,15 @@ class ResultStorage:
|
|
|
29
29
|
and storing them back to the database with proper validation and error handling.
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
+
def __init__(self, verbose: bool = False):
|
|
33
|
+
"""
|
|
34
|
+
Initialize ResultStorage.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
verbose: Enable detailed logging output
|
|
38
|
+
"""
|
|
39
|
+
self.verbose = verbose
|
|
40
|
+
|
|
32
41
|
def store_results(
|
|
33
42
|
self,
|
|
34
43
|
conn,
|
|
@@ -51,71 +60,30 @@ class ResultStorage:
|
|
|
51
60
|
run_id = solve_result.get("run_id", str(uuid.uuid4()))
|
|
52
61
|
|
|
53
62
|
try:
|
|
54
|
-
logger.info("=" * 80)
|
|
55
|
-
logger.info("📊 STARTING RESULT STORAGE")
|
|
56
|
-
logger.info("=" * 80)
|
|
57
|
-
|
|
58
63
|
# Store component results
|
|
59
|
-
logger.info("📝 Step 1: Storing component results (timeseries data)...")
|
|
60
64
|
component_stats = self._store_component_results(conn, network, scenario_id)
|
|
61
|
-
logger.info(f"✅ Component results stored: {component_stats}")
|
|
62
65
|
|
|
63
66
|
# Calculate network statistics first
|
|
64
|
-
logger.info("📊 Step 2: Calculating network statistics...")
|
|
65
67
|
network_stats = self._calculate_network_statistics(
|
|
66
68
|
conn, network, solve_result
|
|
67
69
|
)
|
|
68
|
-
logger.info(f"✅ Network statistics calculated")
|
|
69
|
-
logger.info(
|
|
70
|
-
f" - Total generation: {network_stats.get('core_summary', {}).get('total_generation_mwh', 0):.2f} MWh"
|
|
71
|
-
)
|
|
72
|
-
logger.info(
|
|
73
|
-
f" - Total cost: {network_stats.get('core_summary', {}).get('total_cost', 0):.2f}"
|
|
74
|
-
)
|
|
75
70
|
|
|
76
71
|
# Store solve summary with network statistics
|
|
77
|
-
logger.info(
|
|
78
|
-
"💾 Step 3: Storing solve summary to network_solve_results table..."
|
|
79
|
-
)
|
|
80
72
|
self._store_solve_summary(conn, solve_result, scenario_id, network_stats)
|
|
81
|
-
# Explicit commit after storing summary
|
|
82
73
|
conn.commit()
|
|
83
|
-
logger.info("✅ Solve summary stored and committed successfully")
|
|
84
74
|
|
|
85
75
|
# Store year-based statistics if available
|
|
86
76
|
year_stats_stored = 0
|
|
87
77
|
if solve_result.get("year_statistics"):
|
|
88
|
-
logger.info(
|
|
89
|
-
f"📅 Step 4: Storing year-based statistics ({len(solve_result['year_statistics'])} years)..."
|
|
90
|
-
)
|
|
91
78
|
year_stats_stored = self._store_year_based_statistics(
|
|
92
79
|
conn, network, solve_result["year_statistics"], scenario_id
|
|
93
80
|
)
|
|
94
|
-
# Explicit commit after storing year statistics
|
|
95
81
|
conn.commit()
|
|
96
|
-
logger.info(
|
|
97
|
-
f"✅ Year-based statistics stored and committed: {year_stats_stored} years"
|
|
98
|
-
)
|
|
99
|
-
else:
|
|
100
|
-
logger.warning(
|
|
101
|
-
"⚠️ No year_statistics in solve_result - skipping year-based storage"
|
|
102
|
-
)
|
|
103
|
-
logger.info(
|
|
104
|
-
f" Available solve_result keys: {list(solve_result.keys())}"
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
logger.info("=" * 80)
|
|
108
|
-
logger.info("✅ RESULT STORAGE COMPLETED SUCCESSFULLY")
|
|
109
|
-
logger.info("=" * 80)
|
|
110
82
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
"SELECT COUNT(*) FROM network_solve_results WHERE scenario_id IS ? ",
|
|
114
|
-
(scenario_id,) if scenario_id is not None else (None,),
|
|
115
|
-
)
|
|
116
|
-
count = cursor.fetchone()[0]
|
|
83
|
+
total_gen = network_stats.get("core_summary", {}).get("total_generation_mwh", 0)
|
|
84
|
+
total_cost = network_stats.get("core_summary", {}).get("total_cost", 0)
|
|
117
85
|
logger.info(
|
|
118
|
-
f"
|
|
86
|
+
f"Results stored: {total_gen:.0f} MWh generation, {total_cost:.0f} cost, {year_stats_stored} years"
|
|
119
87
|
)
|
|
120
88
|
|
|
121
89
|
return {
|
|
@@ -127,10 +95,7 @@ class ResultStorage:
|
|
|
127
95
|
}
|
|
128
96
|
|
|
129
97
|
except Exception as e:
|
|
130
|
-
logger.error("
|
|
131
|
-
logger.error(f"❌ RESULT STORAGE FAILED: {e}")
|
|
132
|
-
logger.error("=" * 80)
|
|
133
|
-
logger.exception("Full traceback:")
|
|
98
|
+
logger.error(f"Result storage failed: {e}")
|
|
134
99
|
return {
|
|
135
100
|
"component_stats": {},
|
|
136
101
|
"network_stats": {},
|
|
@@ -382,22 +347,12 @@ class ResultStorage:
|
|
|
382
347
|
):
|
|
383
348
|
"""Store solve summary to network_solve_results table (single network per database)."""
|
|
384
349
|
try:
|
|
385
|
-
# Debug logging
|
|
386
|
-
logger.info(
|
|
387
|
-
f"📝 _store_solve_summary called with scenario_id={scenario_id} (type={type(scenario_id)})"
|
|
388
|
-
)
|
|
389
|
-
|
|
390
350
|
# Prepare solve summary data
|
|
391
351
|
solver_name = solve_result.get("solver_name", "unknown")
|
|
392
352
|
solve_status = solve_result.get("status", "unknown")
|
|
393
353
|
objective_value = solve_result.get("objective_value")
|
|
394
354
|
solve_time = solve_result.get("solve_time", 0.0)
|
|
395
355
|
|
|
396
|
-
logger.info(f" solver_name: {solver_name}")
|
|
397
|
-
logger.info(f" solve_status: {solve_status}")
|
|
398
|
-
logger.info(f" objective_value: {objective_value}")
|
|
399
|
-
logger.info(f" solve_time: {solve_time}")
|
|
400
|
-
|
|
401
356
|
# Create enhanced solve result with network statistics for serialization
|
|
402
357
|
enhanced_solve_result = {
|
|
403
358
|
**solve_result,
|
|
@@ -406,31 +361,20 @@ class ResultStorage:
|
|
|
406
361
|
|
|
407
362
|
# Delete existing result for this scenario first (handles NULL scenario_id correctly)
|
|
408
363
|
if scenario_id is None:
|
|
409
|
-
|
|
410
|
-
delete_result = conn.execute(
|
|
364
|
+
conn.execute(
|
|
411
365
|
"DELETE FROM network_solve_results WHERE scenario_id IS NULL"
|
|
412
366
|
)
|
|
413
|
-
logger.info(f" Deleted {delete_result.rowcount} existing rows")
|
|
414
367
|
else:
|
|
415
|
-
|
|
416
|
-
f"🗑️ Deleting existing results for scenario_id={scenario_id}"
|
|
417
|
-
)
|
|
418
|
-
delete_result = conn.execute(
|
|
368
|
+
conn.execute(
|
|
419
369
|
"DELETE FROM network_solve_results WHERE scenario_id = ?",
|
|
420
370
|
(scenario_id,),
|
|
421
371
|
)
|
|
422
|
-
logger.info(f" Deleted {delete_result.rowcount} existing rows")
|
|
423
372
|
|
|
424
|
-
logger.info(f"💾 Inserting solve results for scenario_id={scenario_id}")
|
|
425
|
-
logger.info(
|
|
426
|
-
f" Serializing results_json ({len(str(enhanced_solve_result))} chars)..."
|
|
427
|
-
)
|
|
428
373
|
results_json = self._serialize_results_json(enhanced_solve_result)
|
|
429
|
-
logger.info(f" Serializing metadata_json...")
|
|
430
374
|
metadata_json = self._serialize_metadata_json(enhanced_solve_result)
|
|
431
375
|
|
|
432
376
|
# Insert new solve results summary
|
|
433
|
-
|
|
377
|
+
conn.execute(
|
|
434
378
|
"""
|
|
435
379
|
INSERT INTO network_solve_results (
|
|
436
380
|
scenario_id, solver_name, solve_type, solve_status,
|
|
@@ -449,20 +393,8 @@ class ResultStorage:
|
|
|
449
393
|
),
|
|
450
394
|
)
|
|
451
395
|
|
|
452
|
-
logger.info(f"✅ Inserted solve summary (rowid={insert_result.lastrowid})")
|
|
453
|
-
|
|
454
|
-
# Verify insertion
|
|
455
|
-
verify_cursor = conn.execute(
|
|
456
|
-
"SELECT COUNT(*), solver_name, solve_status FROM network_solve_results WHERE scenario_id IS ?",
|
|
457
|
-
(scenario_id,) if scenario_id is not None else (None,),
|
|
458
|
-
)
|
|
459
|
-
verify_result = verify_cursor.fetchone()
|
|
460
|
-
logger.info(
|
|
461
|
-
f"🔍 Verification: {verify_result[0]} row(s) with solver={verify_result[1]}, status={verify_result[2]}"
|
|
462
|
-
)
|
|
463
|
-
|
|
464
396
|
except Exception as e:
|
|
465
|
-
logger.error(f"
|
|
397
|
+
logger.error(f"Failed to store solve summary: {e}")
|
|
466
398
|
raise # Re-raise to trigger rollback
|
|
467
399
|
|
|
468
400
|
def _calculate_network_statistics(
|
|
@@ -605,30 +537,19 @@ class ResultStorage:
|
|
|
605
537
|
This is the primary calculation - per-year stats will be calculated separately.
|
|
606
538
|
"""
|
|
607
539
|
try:
|
|
608
|
-
logger.info("🔍 _calculate_carrier_statistics: Starting calculation...")
|
|
609
|
-
|
|
610
540
|
# Calculate all-year statistics directly from the network
|
|
611
541
|
# Extract years from network snapshots
|
|
612
542
|
if hasattr(network.snapshots, "levels"):
|
|
613
543
|
# Multi-period optimization - get years from period level
|
|
614
544
|
period_values = network.snapshots.get_level_values(0)
|
|
615
545
|
years = sorted(period_values.unique())
|
|
616
|
-
logger.info(
|
|
617
|
-
f" Detected multi-period optimization with years: {years}"
|
|
618
|
-
)
|
|
619
546
|
elif hasattr(network.snapshots, "year"):
|
|
620
547
|
years = sorted(network.snapshots.year.unique())
|
|
621
|
-
logger.info(f" Detected DatetimeIndex with years: {years}")
|
|
622
548
|
elif hasattr(network, "_available_years"):
|
|
623
549
|
years = network._available_years
|
|
624
|
-
logger.info(f" Using _available_years: {years}")
|
|
625
550
|
else:
|
|
626
551
|
years = [2020] # Fallback
|
|
627
|
-
logger.warning(f"
|
|
628
|
-
|
|
629
|
-
logger.info(
|
|
630
|
-
f"📅 Calculating all-year carrier statistics for {len(years)} years: {years}"
|
|
631
|
-
)
|
|
552
|
+
logger.warning(f"No year information found, using fallback: {years}")
|
|
632
553
|
|
|
633
554
|
# Calculate per-year statistics first
|
|
634
555
|
all_year_stats = {
|
|
@@ -694,51 +615,10 @@ class ResultStorage:
|
|
|
694
615
|
year_stats["energy_capacity_by_carrier"].get(carrier, 0.0)
|
|
695
616
|
)
|
|
696
617
|
|
|
697
|
-
logger.info(f"Calculated all-year carrier statistics:")
|
|
698
|
-
logger.info(
|
|
699
|
-
f" Total dispatch: {sum(all_year_stats['dispatch_by_carrier'].values()):.2f} MWh"
|
|
700
|
-
)
|
|
701
|
-
logger.info(
|
|
702
|
-
f" Total emissions: {sum(all_year_stats['emissions_by_carrier'].values()):.2f} tonnes CO2"
|
|
703
|
-
)
|
|
704
|
-
logger.info(
|
|
705
|
-
f" Total capital cost: {sum(all_year_stats['capital_cost_by_carrier'].values()):.2f} USD"
|
|
706
|
-
)
|
|
707
|
-
logger.info(
|
|
708
|
-
f" Total operational cost: {sum(all_year_stats['operational_cost_by_carrier'].values()):.2f} USD"
|
|
709
|
-
)
|
|
710
|
-
logger.info(
|
|
711
|
-
f" Final power capacity: {sum(all_year_stats['power_capacity_by_carrier'].values()):.2f} MW"
|
|
712
|
-
)
|
|
713
|
-
|
|
714
|
-
# Detailed logging for cost and emission statistics by carrier
|
|
715
|
-
logger.info("=" * 80)
|
|
716
|
-
logger.info("📊 DETAILED CARRIER STATISTICS (RAW VALUES FROM MODEL):")
|
|
717
|
-
logger.info("=" * 80)
|
|
718
|
-
for carrier in sorted(all_carriers_with_unmet):
|
|
719
|
-
dispatch = all_year_stats['dispatch_by_carrier'].get(carrier, 0.0)
|
|
720
|
-
emissions = all_year_stats['emissions_by_carrier'].get(carrier, 0.0)
|
|
721
|
-
capital_cost = all_year_stats['capital_cost_by_carrier'].get(carrier, 0.0)
|
|
722
|
-
operational_cost = all_year_stats['operational_cost_by_carrier'].get(carrier, 0.0)
|
|
723
|
-
total_cost = all_year_stats['total_system_cost_by_carrier'].get(carrier, 0.0)
|
|
724
|
-
power_cap = all_year_stats['power_capacity_by_carrier'].get(carrier, 0.0)
|
|
725
|
-
energy_cap = all_year_stats['energy_capacity_by_carrier'].get(carrier, 0.0)
|
|
726
|
-
|
|
727
|
-
if dispatch > 0.001 or emissions > 0.001 or capital_cost > 0.001 or operational_cost > 0.001:
|
|
728
|
-
logger.info(f" {carrier}:")
|
|
729
|
-
logger.info(f" - Dispatch: {dispatch:.2f} MWh")
|
|
730
|
-
logger.info(f" - Emissions: {emissions:.2f} tonnes CO2")
|
|
731
|
-
logger.info(f" - Capital Cost: {capital_cost:.2f} USD")
|
|
732
|
-
logger.info(f" - Operational Cost: {operational_cost:.2f} USD")
|
|
733
|
-
logger.info(f" - Total System Cost: {total_cost:.2f} USD")
|
|
734
|
-
logger.info(f" - Power Capacity: {power_cap:.2f} MW")
|
|
735
|
-
logger.info(f" - Energy Capacity: {energy_cap:.2f} MWh")
|
|
736
|
-
logger.info("=" * 80)
|
|
737
|
-
|
|
738
618
|
return all_year_stats
|
|
739
619
|
|
|
740
620
|
except Exception as e:
|
|
741
|
-
logger.error(f"Failed to calculate carrier statistics: {e}"
|
|
621
|
+
logger.error(f"Failed to calculate carrier statistics: {e}")
|
|
742
622
|
return {
|
|
743
623
|
"dispatch_by_carrier": {},
|
|
744
624
|
"power_capacity_by_carrier": {},
|
|
@@ -836,19 +716,15 @@ class ResultStorage:
|
|
|
836
716
|
)
|
|
837
717
|
|
|
838
718
|
stored_count += 1
|
|
839
|
-
logger.info(f"Stored year-based statistics for year {year}")
|
|
840
719
|
|
|
841
720
|
except Exception as e:
|
|
842
721
|
logger.error(f"Failed to store statistics for year {year}: {e}")
|
|
843
722
|
continue
|
|
844
723
|
|
|
845
|
-
logger.info(
|
|
846
|
-
f"Successfully stored year-based statistics for {stored_count} years"
|
|
847
|
-
)
|
|
848
724
|
return stored_count
|
|
849
725
|
|
|
850
726
|
except Exception as e:
|
|
851
|
-
logger.error(f"Failed to store year-based statistics: {e}"
|
|
727
|
+
logger.error(f"Failed to store year-based statistics: {e}")
|
|
852
728
|
return 0
|
|
853
729
|
|
|
854
730
|
def _calculate_year_carrier_statistics(
|
|
@@ -859,7 +735,6 @@ class ResultStorage:
|
|
|
859
735
|
For now, only calculate capacity statistics.
|
|
860
736
|
"""
|
|
861
737
|
try:
|
|
862
|
-
logger.info(f" 🔍 Calculating statistics for year {year}...")
|
|
863
738
|
# Initialize carrier statistics
|
|
864
739
|
carrier_stats = {
|
|
865
740
|
"dispatch_by_carrier": {},
|
|
@@ -1136,24 +1011,6 @@ class ResultStorage:
|
|
|
1136
1011
|
|
|
1137
1012
|
# Calculate annualized capital cost for this year
|
|
1138
1013
|
annual_capital_cost = capacity_mw * capital_cost_per_mw
|
|
1139
|
-
|
|
1140
|
-
# Log capital cost calculation details with unit check
|
|
1141
|
-
if annual_capital_cost > 0.001:
|
|
1142
|
-
logger.info(
|
|
1143
|
-
f" 💰 Capital cost calc for {gen_name} ({carrier_name}) in year {year}: "
|
|
1144
|
-
f"{capacity_mw:.2f} MW × {capital_cost_per_mw:.2f} currency/MW = {annual_capital_cost:.2f} currency"
|
|
1145
|
-
)
|
|
1146
|
-
# Check if values seem too small (might be in kUSD instead of USD)
|
|
1147
|
-
if capital_cost_per_mw > 0 and capital_cost_per_mw < 1:
|
|
1148
|
-
logger.warning(
|
|
1149
|
-
f" ⚠️ WARNING: capital_cost_per_mw ({capital_cost_per_mw:.2f}) seems very small. "
|
|
1150
|
-
f"Expected USD/MW but might be in kUSD/MW (thousands)?"
|
|
1151
|
-
)
|
|
1152
|
-
if annual_capital_cost > 0 and annual_capital_cost < 1000:
|
|
1153
|
-
logger.warning(
|
|
1154
|
-
f" ⚠️ WARNING: annual_capital_cost ({annual_capital_cost:.2f}) seems very small. "
|
|
1155
|
-
f"Expected USD but might be in kUSD (thousands)?"
|
|
1156
|
-
)
|
|
1157
1014
|
|
|
1158
1015
|
if carrier_name in carrier_stats["capital_cost_by_carrier"]:
|
|
1159
1016
|
carrier_stats["capital_cost_by_carrier"][
|
|
@@ -1459,24 +1316,6 @@ class ResultStorage:
|
|
|
1459
1316
|
|
|
1460
1317
|
# Calculate operational cost for this year
|
|
1461
1318
|
operational_cost = generation_mwh * marginal_cost
|
|
1462
|
-
|
|
1463
|
-
# Log operational cost calculation details with unit check
|
|
1464
|
-
if operational_cost > 0.001:
|
|
1465
|
-
logger.info(
|
|
1466
|
-
f" 💰 Operational cost calc for {gen_name} ({carrier_name}) in year {year}: "
|
|
1467
|
-
f"{generation_mwh:.2f} MWh × {marginal_cost:.2f} currency/MWh = {operational_cost:.2f} currency"
|
|
1468
|
-
)
|
|
1469
|
-
# Check if values seem too small (might be in kUSD instead of USD)
|
|
1470
|
-
if marginal_cost > 0 and marginal_cost < 1:
|
|
1471
|
-
logger.warning(
|
|
1472
|
-
f" ⚠️ WARNING: marginal_cost ({marginal_cost:.2f}) seems very small. "
|
|
1473
|
-
f"Expected USD/MWh but might be in kUSD/MWh (thousands)?"
|
|
1474
|
-
)
|
|
1475
|
-
if operational_cost > 0 and operational_cost < 1000:
|
|
1476
|
-
logger.warning(
|
|
1477
|
-
f" ⚠️ WARNING: operational_cost ({operational_cost:.2f}) seems very small. "
|
|
1478
|
-
f"Expected USD but might be in kUSD (thousands)?"
|
|
1479
|
-
)
|
|
1480
1319
|
|
|
1481
1320
|
if (
|
|
1482
1321
|
carrier_name
|
|
@@ -1899,50 +1738,10 @@ class ResultStorage:
|
|
|
1899
1738
|
carrier_name
|
|
1900
1739
|
] += capacity_mw
|
|
1901
1740
|
|
|
1902
|
-
logger.info(f"Calculated year {year} carrier statistics:")
|
|
1903
|
-
logger.info(
|
|
1904
|
-
f" Dispatch: {sum(carrier_stats['dispatch_by_carrier'].values()):.2f} MWh"
|
|
1905
|
-
)
|
|
1906
|
-
logger.info(
|
|
1907
|
-
f" Emissions: {sum(carrier_stats['emissions_by_carrier'].values()):.2f} tonnes CO2"
|
|
1908
|
-
)
|
|
1909
|
-
logger.info(
|
|
1910
|
-
f" Capital cost: {sum(carrier_stats['capital_cost_by_carrier'].values()):.2f} USD"
|
|
1911
|
-
)
|
|
1912
|
-
logger.info(
|
|
1913
|
-
f" Operational cost: {sum(carrier_stats['operational_cost_by_carrier'].values()):.2f} USD"
|
|
1914
|
-
)
|
|
1915
|
-
logger.info(
|
|
1916
|
-
f" Total system cost: {sum(carrier_stats['total_system_cost_by_carrier'].values()):.2f} USD"
|
|
1917
|
-
)
|
|
1918
|
-
logger.info(
|
|
1919
|
-
f" Power capacity: {sum(carrier_stats['power_capacity_by_carrier'].values()):.2f} MW"
|
|
1920
|
-
)
|
|
1921
|
-
logger.info(
|
|
1922
|
-
f" Energy capacity: {sum(carrier_stats['energy_capacity_by_carrier'].values()):.2f} MWh"
|
|
1923
|
-
)
|
|
1924
|
-
|
|
1925
|
-
# Detailed logging for year-specific statistics
|
|
1926
|
-
logger.info(f" 📊 Year {year} detailed carrier stats (RAW VALUES):")
|
|
1927
|
-
for carrier in sorted(all_carriers_with_unmet):
|
|
1928
|
-
dispatch = carrier_stats['dispatch_by_carrier'].get(carrier, 0.0)
|
|
1929
|
-
emissions = carrier_stats['emissions_by_carrier'].get(carrier, 0.0)
|
|
1930
|
-
capital_cost = carrier_stats['capital_cost_by_carrier'].get(carrier, 0.0)
|
|
1931
|
-
operational_cost = carrier_stats['operational_cost_by_carrier'].get(carrier, 0.0)
|
|
1932
|
-
total_cost = carrier_stats['total_system_cost_by_carrier'].get(carrier, 0.0)
|
|
1933
|
-
|
|
1934
|
-
if dispatch > 0.001 or emissions > 0.001 or capital_cost > 0.001 or operational_cost > 0.001:
|
|
1935
|
-
logger.info(f" {carrier}: dispatch={dispatch:.2f} MWh, emissions={emissions:.2f} tCO2, "
|
|
1936
|
-
f"capital={capital_cost:.2f} USD, operational={operational_cost:.2f} USD, "
|
|
1937
|
-
f"total={total_cost:.2f} USD")
|
|
1938
|
-
|
|
1939
1741
|
return carrier_stats
|
|
1940
1742
|
|
|
1941
1743
|
except Exception as e:
|
|
1942
|
-
logger.error(
|
|
1943
|
-
f"Failed to calculate year {year} carrier statistics: {e}",
|
|
1944
|
-
exc_info=True,
|
|
1945
|
-
)
|
|
1744
|
+
logger.error(f"Failed to calculate year {year} carrier statistics: {e}")
|
|
1946
1745
|
return {
|
|
1947
1746
|
"dispatch_by_carrier": {},
|
|
1948
1747
|
"power_capacity_by_carrier": {},
|
|
@@ -2000,10 +1799,8 @@ class ResultStorage:
|
|
|
2000
1799
|
)
|
|
2001
1800
|
|
|
2002
1801
|
year_results = cursor.fetchall()
|
|
2003
|
-
logger.info(f"Found {len(year_results)} year-based results to sum")
|
|
2004
1802
|
|
|
2005
1803
|
if not year_results:
|
|
2006
|
-
logger.warning(f"No year-based results found")
|
|
2007
1804
|
return totals
|
|
2008
1805
|
|
|
2009
1806
|
# For capacity: use the LAST YEAR only (final capacity state)
|
|
@@ -2031,10 +1828,6 @@ class ResultStorage:
|
|
|
2031
1828
|
value or 0
|
|
2032
1829
|
)
|
|
2033
1830
|
|
|
2034
|
-
logger.info(
|
|
2035
|
-
f"Used last year ({last_year}) capacity as all-year capacity"
|
|
2036
|
-
)
|
|
2037
|
-
|
|
2038
1831
|
except Exception as e:
|
|
2039
1832
|
logger.error(f"Failed to process last year ({last_year}) results: {e}")
|
|
2040
1833
|
|
|
@@ -2091,35 +1884,10 @@ class ResultStorage:
|
|
|
2091
1884
|
logger.error(f"Failed to process year {year} results: {e}")
|
|
2092
1885
|
continue
|
|
2093
1886
|
|
|
2094
|
-
logger.info(f"Summed carrier statistics across {len(year_results)} years:")
|
|
2095
|
-
logger.info(
|
|
2096
|
-
f" Final power capacity: {sum(totals['power_capacity_by_carrier'].values()):.2f} MW"
|
|
2097
|
-
)
|
|
2098
|
-
logger.info(
|
|
2099
|
-
f" Final energy capacity: {sum(totals['energy_capacity_by_carrier'].values()):.2f} MWh"
|
|
2100
|
-
)
|
|
2101
|
-
logger.info(
|
|
2102
|
-
f" Total dispatch: {sum(totals['dispatch_by_carrier'].values()):.2f} MWh"
|
|
2103
|
-
)
|
|
2104
|
-
logger.info(
|
|
2105
|
-
f" Total emissions: {sum(totals['emissions_by_carrier'].values()):.2f} tonnes CO2"
|
|
2106
|
-
)
|
|
2107
|
-
logger.info(
|
|
2108
|
-
f" Total capital cost: {sum(totals['capital_cost_by_carrier'].values()):.2f} USD"
|
|
2109
|
-
)
|
|
2110
|
-
logger.info(
|
|
2111
|
-
f" Total operational cost: {sum(totals['operational_cost_by_carrier'].values()):.2f} USD"
|
|
2112
|
-
)
|
|
2113
|
-
logger.info(
|
|
2114
|
-
f" Total system cost: {sum(totals['total_system_cost_by_carrier'].values()):.2f} USD"
|
|
2115
|
-
)
|
|
2116
|
-
|
|
2117
1887
|
return totals
|
|
2118
1888
|
|
|
2119
1889
|
except Exception as e:
|
|
2120
|
-
logger.error(
|
|
2121
|
-
f"Failed to sum year-based carrier statistics: {e}", exc_info=True
|
|
2122
|
-
)
|
|
1890
|
+
logger.error(f"Failed to sum year-based carrier statistics: {e}")
|
|
2123
1891
|
# Return empty structure on error
|
|
2124
1892
|
return {
|
|
2125
1893
|
"dispatch_by_carrier": {},
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyconvexity
|
|
3
|
+
Version: 0.4.6
|
|
4
|
+
Summary: Python library for energy system modeling and optimization with PyPSA
|
|
5
|
+
Author-email: Convexity Team <info@convexity.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/bayesian-energy/pyconvexity
|
|
8
|
+
Project-URL: Repository, https://github.com/bayesian-energy/pyconvexity
|
|
9
|
+
Project-URL: Issues, https://github.com/bayesian-energy/pyconvexity/issues
|
|
10
|
+
Project-URL: Documentation, https://pyconvexity.readthedocs.io
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: pandas>=1.5.0
|
|
23
|
+
Requires-Dist: numpy>=1.21.0
|
|
24
|
+
Requires-Dist: pyarrow>=10.0.0
|
|
25
|
+
Provides-Extra: pypsa
|
|
26
|
+
Requires-Dist: pypsa>=0.25.0; extra == "pypsa"
|
|
27
|
+
Requires-Dist: networkx; extra == "pypsa"
|
|
28
|
+
Requires-Dist: scipy; extra == "pypsa"
|
|
29
|
+
Requires-Dist: xarray; extra == "pypsa"
|
|
30
|
+
Provides-Extra: excel
|
|
31
|
+
Requires-Dist: openpyxl>=3.0.0; extra == "excel"
|
|
32
|
+
Requires-Dist: xlsxwriter>=3.0.0; extra == "excel"
|
|
33
|
+
Provides-Extra: netcdf
|
|
34
|
+
Requires-Dist: netcdf4>=1.6.0; extra == "netcdf"
|
|
35
|
+
Requires-Dist: xarray>=2022.3.0; extra == "netcdf"
|
|
36
|
+
Provides-Extra: data
|
|
37
|
+
Requires-Dist: country-converter>=1.0.0; extra == "data"
|
|
38
|
+
Requires-Dist: pyyaml>=6.0.0; extra == "data"
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
41
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
42
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
43
|
+
Requires-Dist: isort>=5.10.0; extra == "dev"
|
|
44
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
45
|
+
Requires-Dist: pre-commit>=2.20.0; extra == "dev"
|
|
46
|
+
Provides-Extra: all
|
|
47
|
+
Requires-Dist: pyconvexity[data,excel,netcdf,pypsa]; extra == "all"
|
|
48
|
+
|
|
49
|
+
# pyconvexity
|
|
50
|
+
|
|
51
|
+
Python library for energy system modeling and optimization with PyPSA.
|
|
52
|
+
|
|
53
|
+
[](https://badge.fury.io/py/pyconvexity)
|
|
54
|
+
[](https://www.python.org/downloads/)
|
|
55
|
+
[](https://opensource.org/licenses/MIT)
|
|
56
|
+
|
|
57
|
+
## Overview
|
|
58
|
+
|
|
59
|
+
pyconvexity is the Python library that powers [Convexity](https://bayesian.energy/convexity), providing programmatic access to energy system modeling and optimization. It stores models in SQLite databases and integrates with PyPSA for solving.
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install pyconvexity
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
With optional dependencies:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install pyconvexity[pypsa] # PyPSA solver integration
|
|
71
|
+
pip install pyconvexity[excel] # Excel import/export
|
|
72
|
+
pip install pyconvexity[all] # All optional dependencies
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Quick Start
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import pyconvexity as px
|
|
79
|
+
|
|
80
|
+
# Create a new model database
|
|
81
|
+
px.create_database_with_schema("my_model.db")
|
|
82
|
+
|
|
83
|
+
# Create a network
|
|
84
|
+
with px.database_context("my_model.db") as conn:
|
|
85
|
+
network_req = px.CreateNetworkRequest(
|
|
86
|
+
name="My Network",
|
|
87
|
+
start_time="2024-01-01 00:00:00",
|
|
88
|
+
end_time="2024-01-01 23:00:00",
|
|
89
|
+
time_resolution="PT1H",
|
|
90
|
+
)
|
|
91
|
+
px.create_network(conn, network_req)
|
|
92
|
+
|
|
93
|
+
# Create components
|
|
94
|
+
carrier_id = px.create_carrier(conn, name="AC")
|
|
95
|
+
bus_id = px.create_component(conn, "BUS", "Main Bus", carrier_id=carrier_id)
|
|
96
|
+
|
|
97
|
+
conn.commit()
|
|
98
|
+
|
|
99
|
+
# Solve the network
|
|
100
|
+
result = px.solve_network("my_model.db", solver_name="highs")
|
|
101
|
+
print(f"Success: {result['success']}, Objective: {result['objective_value']}")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Documentation
|
|
105
|
+
|
|
106
|
+
Full documentation is available at: **[docs.bayesian.energy](https://docs.bayesian.energy/convexity/user-guide/advanced-features/pyconvexity/)**
|
|
107
|
+
|
|
108
|
+
- [API Reference](https://docs.bayesian.energy/convexity/user-guide/advanced-features/pyconvexity/api/api-reference)
|
|
109
|
+
- [Examples & Tutorials](https://docs.bayesian.energy/convexity/user-guide/advanced-features/pyconvexity/examples/example-1/)
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
### Setup
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git clone https://github.com/bayesian-energy/pyconvexity.git
|
|
117
|
+
cd pyconvexity
|
|
118
|
+
pip install -e ".[dev,all]"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Running Tests
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
pytest
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Documentation Deployment
|
|
128
|
+
|
|
129
|
+
API documentation is auto-generated and synced to [bayesian-docs](https://github.com/bayesian-energy/bayesian-docs).
|
|
130
|
+
|
|
131
|
+
**Generate API docs locally:**
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
python scripts/generate_api_docs.py --output-dir api-docs
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**How it works:**
|
|
138
|
+
1. On push to `main` or release, the `sync-docs.yml` workflow runs
|
|
139
|
+
2. It generates API markdown from Python docstrings
|
|
140
|
+
3. Commits the generated docs to the `bayesian-docs` repository
|
|
141
|
+
|
|
142
|
+
**Setup for maintainers:**
|
|
143
|
+
- Add `DOCS_DEPLOY_TOKEN` secret to the repo (personal access token with `repo` scope for bayesian-docs)
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
148
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
pyconvexity/__init__.py,sha256=TJvgkEaMHFFWhIGaOdjMgc4rHVIxHKni3Ggg1YAQziE,5347
|
|
2
|
-
pyconvexity/_version.py,sha256=
|
|
2
|
+
pyconvexity/_version.py,sha256=bbBpXE_PBbo_SaI807mDML0QJywD0_ufCDPgAMlDHaE,22
|
|
3
3
|
pyconvexity/timeseries.py,sha256=QdKbiqjAlxkJATyKm2Kelx1Ea2PsAnnCYfVLU5VER1Y,11085
|
|
4
4
|
pyconvexity/core/__init__.py,sha256=gdyyHNqOc4h9Nfe9u6NA936GNzH6coGNCMgBvvvOnGE,1196
|
|
5
5
|
pyconvexity/core/database.py,sha256=vwCmuN0B0xwImh6L0bFR4vNWHw_wVfYSG1KwsUjK4iY,14831
|
|
@@ -11,7 +11,7 @@ pyconvexity/data/loaders/__init__.py,sha256=6xPtOmH2n1mNby7ZjA-2Mk9F48Q246RNsyMn
|
|
|
11
11
|
pyconvexity/data/loaders/cache.py,sha256=R-DUIiFpphjyi5EitcUZwzwUdZeqN6poYVyuNpKzB4g,7040
|
|
12
12
|
pyconvexity/data/schema/01_core_schema.sql,sha256=xCCxUl65IGYxaMxAbgZVS7jVceRUJ_wOMw5c7cm6_Ww,16903
|
|
13
13
|
pyconvexity/data/schema/02_data_metadata.sql,sha256=BbpTkH1s7IbZQkDBRF2kL_UR9tzMEWDBYS3VBkwDRu0,4323
|
|
14
|
-
pyconvexity/data/schema/03_validation_data.sql,sha256=
|
|
14
|
+
pyconvexity/data/schema/03_validation_data.sql,sha256=Y4iZ1RzqMzeaBWalecT6BRd8I3MSW5nclXJI3cE_JAQ,88663
|
|
15
15
|
pyconvexity/data/sources/__init__.py,sha256=Dn6_oS7wB-vLjMj2YeXlmIl6hNjACbicimSabKxIWnc,108
|
|
16
16
|
pyconvexity/data/sources/gem.py,sha256=v8OYCMsb2t-8u-YmK8vzMsgI9ArUAOAXMZZQOFpJ-nI,14923
|
|
17
17
|
pyconvexity/io/__init__.py,sha256=FCyvRDfBUrrNei-y5JVod6MMN1bkPMSSfE0fpKi1aKQ,751
|
|
@@ -23,20 +23,20 @@ pyconvexity/models/__init__.py,sha256=WqDSq1Mst7iJsFytausruoM562FKlOKV0Egmnpm290
|
|
|
23
23
|
pyconvexity/models/attributes.py,sha256=RpH3rBoHD33xBSXUEfaD-CvRj3JruolCwexo6HPMGC8,23388
|
|
24
24
|
pyconvexity/models/carriers.py,sha256=L_WuDMW13k8aaA-obsDPxjmpZgZELiIAZuNtxq7YLpg,3447
|
|
25
25
|
pyconvexity/models/components.py,sha256=vxznTAvAzF99ILcc1DdLtj4K6k8aqclwlH1VhLcFAMM,17570
|
|
26
|
-
pyconvexity/models/network.py,sha256=
|
|
26
|
+
pyconvexity/models/network.py,sha256=P2Cuxv2lX9gWDwIBDDFqLs1sznhAXYquYNYstaMPjfU,14352
|
|
27
27
|
pyconvexity/models/results.py,sha256=6j1H4AwVmp94L97gl_sGnE8izMxkU5o89guKIU8JdtE,4169
|
|
28
28
|
pyconvexity/models/scenarios.py,sha256=-0UPUDXf6r9mFriA-z2fD5KKMARm2PUBjLba49S9mCI,5867
|
|
29
29
|
pyconvexity/solvers/__init__.py,sha256=t1gOUTqbYDCtIvKPqGVY1fjKwqJi2Od9bGeIO7bPvJE,667
|
|
30
30
|
pyconvexity/solvers/pypsa/__init__.py,sha256=nudu0AOYEfPhpGHZ1Q9pUgjGeeIJd_zeULc975iyluE,555
|
|
31
|
-
pyconvexity/solvers/pypsa/api.py,sha256=
|
|
31
|
+
pyconvexity/solvers/pypsa/api.py,sha256=OXGfVsv76HIXMJ4b_5LSiWW1OL5Xiq5EnUzaArTuaTI,18196
|
|
32
32
|
pyconvexity/solvers/pypsa/batch_loader.py,sha256=w525_lqanKtARKgicajhBiDwIJzkaU_HbkL1I82gDqg,12361
|
|
33
|
-
pyconvexity/solvers/pypsa/builder.py,sha256=
|
|
33
|
+
pyconvexity/solvers/pypsa/builder.py,sha256=SOr77SmDCYKNxfb-wGHYRX5A0M7zdv_E2Sjyf82uS6Y,24208
|
|
34
34
|
pyconvexity/solvers/pypsa/constraints.py,sha256=20WliFDhPQGMAsS4VOTU8LZJpsFpLVRHpNsZW49GTcc,16397
|
|
35
|
-
pyconvexity/solvers/pypsa/solver.py,sha256=
|
|
36
|
-
pyconvexity/solvers/pypsa/storage.py,sha256=
|
|
35
|
+
pyconvexity/solvers/pypsa/solver.py,sha256=oXX2kqU9AWg8GgsMOV0fYqv93XjIk8Kh-fKWbQSzaig,63239
|
|
36
|
+
pyconvexity/solvers/pypsa/storage.py,sha256=9edLe46f62jvpai1mNQEKypV77L26VyFdvKgQLCEA3o,94635
|
|
37
37
|
pyconvexity/validation/__init__.py,sha256=VJNZlFoWABsWwUKktNk2jbtXIepH5omvC0WtsTS7o3o,583
|
|
38
38
|
pyconvexity/validation/rules.py,sha256=GiNadc8hvbWBr09vUkGiLLTmSdvtNSeGLFwvCjlikYY,9241
|
|
39
|
-
pyconvexity-0.4.
|
|
40
|
-
pyconvexity-0.4.
|
|
41
|
-
pyconvexity-0.4.
|
|
42
|
-
pyconvexity-0.4.
|
|
39
|
+
pyconvexity-0.4.6.dist-info/METADATA,sha256=YEKMDpHrBrlWJZ8VA1DJgHrLN2oLXZNl3aojYwebRfg,4967
|
|
40
|
+
pyconvexity-0.4.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
41
|
+
pyconvexity-0.4.6.dist-info/top_level.txt,sha256=wFPEDXVaebR3JO5Tt3HNse-ws5aROCcxEco15d6j64s,12
|
|
42
|
+
pyconvexity-0.4.6.dist-info/RECORD,,
|