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
|
@@ -10,7 +10,7 @@ import pandas as pd
|
|
|
10
10
|
import numpy as np
|
|
11
11
|
from typing import Dict, Any, Optional, Callable
|
|
12
12
|
|
|
13
|
-
from pyconvexity.core.types import StaticValue
|
|
13
|
+
from pyconvexity.core.types import StaticValue
|
|
14
14
|
from pyconvexity.models import (
|
|
15
15
|
list_components_by_type, set_static_attribute, set_timeseries_attribute
|
|
16
16
|
)
|
|
@@ -210,25 +210,20 @@ class ResultStorage:
|
|
|
210
210
|
if component_series.isna().all():
|
|
211
211
|
continue
|
|
212
212
|
|
|
213
|
-
# Convert to
|
|
214
|
-
|
|
215
|
-
for
|
|
213
|
+
# Convert to efficient values array
|
|
214
|
+
values = []
|
|
215
|
+
for value in component_series.values:
|
|
216
216
|
if pd.isna(value):
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
timeseries_points.append(TimeseriesPoint(
|
|
221
|
-
timestamp=timestamp,
|
|
222
|
-
value=float(value),
|
|
223
|
-
period_index=period_index
|
|
224
|
-
))
|
|
217
|
+
values.append(0.0) # Fill NaN with 0.0
|
|
218
|
+
else:
|
|
219
|
+
values.append(float(value))
|
|
225
220
|
|
|
226
|
-
if not
|
|
221
|
+
if not values:
|
|
227
222
|
continue
|
|
228
223
|
|
|
229
|
-
# Store using
|
|
224
|
+
# Store using efficient format
|
|
230
225
|
try:
|
|
231
|
-
set_timeseries_attribute(conn, component_id, attr_name,
|
|
226
|
+
set_timeseries_attribute(conn, component_id, attr_name, values, scenario_id)
|
|
232
227
|
stored_count += 1
|
|
233
228
|
except Exception as e:
|
|
234
229
|
# Handle validation errors gracefully
|
|
@@ -350,262 +345,43 @@ class ResultStorage:
|
|
|
350
345
|
network: 'pypsa.Network',
|
|
351
346
|
solve_result: Dict[str, Any]
|
|
352
347
|
) -> Dict[str, Any]:
|
|
353
|
-
"""Calculate network statistics
|
|
348
|
+
"""Calculate network statistics - focusing only on capacity for now."""
|
|
354
349
|
try:
|
|
355
|
-
# Calculate
|
|
356
|
-
total_generation_mwh = 0.0
|
|
357
|
-
total_load_mwh = 0.0
|
|
358
|
-
unmet_load_mwh = 0.0
|
|
359
|
-
|
|
360
|
-
# Calculate generation statistics (simple sum of all positive generator output)
|
|
361
|
-
if hasattr(network, 'generators_t') and hasattr(network.generators_t, 'p'):
|
|
362
|
-
gen_data = network.generators_t.p
|
|
363
|
-
if not gen_data.empty:
|
|
364
|
-
# Debug: Log what's in the generators DataFrame
|
|
365
|
-
logger.info(f"Generators DataFrame columns: {list(gen_data.columns)}")
|
|
366
|
-
logger.info(f"Generators DataFrame shape: {gen_data.shape}")
|
|
367
|
-
|
|
368
|
-
# Total generation - only count positive generation (ignore negative values like storage charging)
|
|
369
|
-
# CRITICAL: Apply snapshot weightings to convert MW to MWh
|
|
370
|
-
weightings = network.snapshot_weightings
|
|
371
|
-
if isinstance(weightings, pd.DataFrame):
|
|
372
|
-
if 'objective' in weightings.columns:
|
|
373
|
-
weighting_values = weightings['objective'].values
|
|
374
|
-
else:
|
|
375
|
-
weighting_values = weightings.iloc[:, 0].values
|
|
376
|
-
else:
|
|
377
|
-
weighting_values = weightings.values
|
|
378
|
-
|
|
379
|
-
# Apply weightings and clip to positive values
|
|
380
|
-
total_generation_mwh = float((gen_data.clip(lower=0).values * weighting_values[:, None]).sum())
|
|
381
|
-
|
|
382
|
-
# Debug logging
|
|
383
|
-
raw_sum = gen_data.sum().sum()
|
|
384
|
-
clipped_sum = gen_data.clip(lower=0).sum().sum()
|
|
385
|
-
|
|
386
|
-
logger.info(f"Generation calculation: raw_sum={raw_sum}, clipped_sum={clipped_sum}")
|
|
387
|
-
|
|
388
|
-
# Check for negative generator values
|
|
389
|
-
negative_gen_columns = []
|
|
390
|
-
for col in gen_data.columns:
|
|
391
|
-
if (gen_data[col] < 0).any():
|
|
392
|
-
negative_gen_columns.append(col)
|
|
393
|
-
min_val = gen_data[col].min()
|
|
394
|
-
logger.warning(f"Generator column '{col}' has negative values (min: {min_val})")
|
|
395
|
-
|
|
396
|
-
if negative_gen_columns:
|
|
397
|
-
logger.info(f"Found {len(negative_gen_columns)} generator columns with negative values: {negative_gen_columns}")
|
|
398
|
-
|
|
399
|
-
# Calculate unmet load if component type mapping available
|
|
400
|
-
if hasattr(network, '_component_type_map'):
|
|
401
|
-
unmet_load_total = 0.0
|
|
402
|
-
for gen_name, gen_type in network._component_type_map.items():
|
|
403
|
-
if gen_type == 'UNMET_LOAD' and gen_name in gen_data.columns:
|
|
404
|
-
# Unmet load should be positive (it's generation to meet unserved demand)
|
|
405
|
-
unmet_load_total += max(0, gen_data[gen_name].sum())
|
|
406
|
-
unmet_load_mwh = float(unmet_load_total)
|
|
407
|
-
|
|
408
|
-
# Calculate load statistics
|
|
409
|
-
if hasattr(network, 'loads_t') and hasattr(network.loads_t, 'p'):
|
|
410
|
-
load_data = network.loads_t.p
|
|
411
|
-
if not load_data.empty:
|
|
412
|
-
# Debug: Log what's in the loads DataFrame
|
|
413
|
-
logger.info(f"Loads DataFrame columns: {list(load_data.columns)}")
|
|
414
|
-
logger.info(f"Loads DataFrame shape: {load_data.shape}")
|
|
415
|
-
logger.info(f"Sample loads data (first 5 columns): {load_data.iloc[:3, :5].to_dict()}")
|
|
416
|
-
|
|
417
|
-
# CRITICAL: Apply snapshot weightings to convert MW to MWh
|
|
418
|
-
weightings = network.snapshot_weightings
|
|
419
|
-
if isinstance(weightings, pd.DataFrame):
|
|
420
|
-
if 'objective' in weightings.columns:
|
|
421
|
-
weighting_values = weightings['objective'].values
|
|
422
|
-
else:
|
|
423
|
-
weighting_values = weightings.iloc[:, 0].values
|
|
424
|
-
else:
|
|
425
|
-
weighting_values = weightings.values
|
|
426
|
-
|
|
427
|
-
total_load_mwh = float(abs((load_data.values * weighting_values[:, None]).sum()))
|
|
428
|
-
logger.info(f"Total load calculation with weightings: {total_load_mwh} MWh")
|
|
429
|
-
logger.info(f"Total load calculation without weightings: {abs(load_data.sum().sum())} MW")
|
|
430
|
-
|
|
431
|
-
# Check if any columns have negative values (which shouldn't be in loads)
|
|
432
|
-
negative_columns = []
|
|
433
|
-
for col in load_data.columns:
|
|
434
|
-
if (load_data[col] < 0).any():
|
|
435
|
-
negative_columns.append(col)
|
|
436
|
-
min_val = load_data[col].min()
|
|
437
|
-
logger.warning(f"Load column '{col}' has negative values (min: {min_val})")
|
|
438
|
-
|
|
439
|
-
if negative_columns:
|
|
440
|
-
logger.error(f"Found {len(negative_columns)} load columns with negative values: {negative_columns}")
|
|
441
|
-
else:
|
|
442
|
-
total_load_mwh = 0.0
|
|
443
|
-
|
|
444
|
-
# Calculate transmission losses from links (CORRECTED)
|
|
445
|
-
total_link_losses_mwh = 0.0
|
|
446
|
-
total_link_flow_mwh = 0.0
|
|
447
|
-
if hasattr(network, 'links_t') and hasattr(network.links_t, 'p0'):
|
|
448
|
-
link_p0_data = network.links_t.p0 # Power at bus0
|
|
449
|
-
link_p1_data = network.links_t.p1 # Power at bus1
|
|
450
|
-
|
|
451
|
-
if not link_p0_data.empty and not link_p1_data.empty:
|
|
452
|
-
logger.info(f"Links p0 DataFrame columns: {list(link_p0_data.columns)}")
|
|
453
|
-
logger.info(f"Links p0 DataFrame shape: {link_p0_data.shape}")
|
|
454
|
-
logger.info(f"Links p1 DataFrame columns: {list(link_p1_data.columns)}")
|
|
455
|
-
logger.info(f"Links p1 DataFrame shape: {link_p1_data.shape}")
|
|
456
|
-
|
|
457
|
-
# CORRECT calculation: For each link and timestep, calculate losses properly
|
|
458
|
-
# Losses occur when power flows through a link with efficiency < 1.0
|
|
459
|
-
# p1 = p0 * efficiency, so losses = p0 - p1 = p0 * (1 - efficiency)
|
|
460
|
-
|
|
461
|
-
link_losses_by_link = {}
|
|
462
|
-
total_losses = 0.0
|
|
463
|
-
total_flow = 0.0
|
|
464
|
-
|
|
465
|
-
for link_name in link_p0_data.columns:
|
|
466
|
-
p0_series = link_p0_data[link_name] # Power input to link
|
|
467
|
-
p1_series = link_p1_data[link_name] # Power output from link
|
|
468
|
-
|
|
469
|
-
# Calculate losses for this link across all timesteps
|
|
470
|
-
# Losses should always be positive regardless of flow direction
|
|
471
|
-
# For each timestep: losses = abs(p0) * (1 - efficiency)
|
|
472
|
-
# But we don't have efficiency here, so use: losses = abs(p0) - abs(p1)
|
|
473
|
-
|
|
474
|
-
# Calculate losses properly for each timestep
|
|
475
|
-
timestep_losses = abs(p0_series) - abs(p1_series)
|
|
476
|
-
link_losses = timestep_losses.sum()
|
|
477
|
-
link_flow = abs(p0_series).sum() # Total absolute flow through this link
|
|
478
|
-
|
|
479
|
-
link_losses_by_link[link_name] = {
|
|
480
|
-
'losses_mwh': link_losses,
|
|
481
|
-
'flow_mwh': link_flow,
|
|
482
|
-
'loss_rate': (link_losses / link_flow * 100) if link_flow > 0 else 0
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
total_losses += link_losses
|
|
486
|
-
total_flow += link_flow
|
|
487
|
-
|
|
488
|
-
# Log details for first few links
|
|
489
|
-
if len(link_losses_by_link) <= 5:
|
|
490
|
-
avg_p0 = p0_series.mean()
|
|
491
|
-
avg_p1 = p1_series.mean()
|
|
492
|
-
logger.info(f" Link '{link_name}': avg_p0={avg_p0:.1f}MW, avg_p1={avg_p1:.1f}MW, losses={link_losses:.1f}MWh, flow={link_flow:.1f}MWh")
|
|
493
|
-
|
|
494
|
-
total_link_losses_mwh = total_losses
|
|
495
|
-
total_link_flow_mwh = total_flow
|
|
496
|
-
|
|
497
|
-
# Summary statistics
|
|
498
|
-
logger.info(f"Link transmission analysis:")
|
|
499
|
-
logger.info(f" Total link flow: {total_link_flow_mwh:.1f} MWh")
|
|
500
|
-
logger.info(f" Total link losses: {total_link_losses_mwh:.1f} MWh")
|
|
501
|
-
logger.info(f" Average loss rate: {(total_link_losses_mwh/total_link_flow_mwh*100):.2f}%")
|
|
502
|
-
logger.info(f" Number of links: {len(link_losses_by_link)}")
|
|
503
|
-
|
|
504
|
-
# Show top 5 links by losses
|
|
505
|
-
top_loss_links = sorted(link_losses_by_link.items(), key=lambda x: x[1]['losses_mwh'], reverse=True)[:5]
|
|
506
|
-
logger.info(f" Top 5 links by losses:")
|
|
507
|
-
for link_name, stats in top_loss_links:
|
|
508
|
-
logger.info(f" {link_name}: {stats['losses_mwh']:.1f} MWh ({stats['loss_rate']:.2f}%)")
|
|
509
|
-
|
|
510
|
-
# Calculate storage losses if any
|
|
511
|
-
total_storage_losses_mwh = 0.0
|
|
512
|
-
storage_charging_mwh = 0.0
|
|
513
|
-
storage_discharging_mwh = 0.0
|
|
514
|
-
|
|
515
|
-
# Check for storage units
|
|
516
|
-
if hasattr(network, 'storage_units_t') and hasattr(network.storage_units_t, 'p'):
|
|
517
|
-
storage_data = network.storage_units_t.p
|
|
518
|
-
if not storage_data.empty:
|
|
519
|
-
logger.info(f"Storage units DataFrame columns: {list(storage_data.columns)}")
|
|
520
|
-
logger.info(f"Storage units DataFrame shape: {storage_data.shape}")
|
|
521
|
-
|
|
522
|
-
# Storage: positive = discharge (generation), negative = charge (consumption)
|
|
523
|
-
total_storage_power = storage_data.sum().sum()
|
|
524
|
-
storage_discharging_mwh = storage_data.clip(lower=0).sum().sum() # Positive values
|
|
525
|
-
storage_charging_mwh = abs(storage_data.clip(upper=0).sum().sum()) # Negative values made positive
|
|
526
|
-
|
|
527
|
-
logger.info(f"Storage analysis:")
|
|
528
|
-
logger.info(f" Total storage net: {total_storage_power:.1f} MWh")
|
|
529
|
-
logger.info(f" Storage discharging: {storage_discharging_mwh:.1f} MWh")
|
|
530
|
-
logger.info(f" Storage charging: {storage_charging_mwh:.1f} MWh")
|
|
531
|
-
|
|
532
|
-
# Storage losses = charging - discharging (due to round-trip efficiency)
|
|
533
|
-
total_storage_losses_mwh = storage_charging_mwh - storage_discharging_mwh
|
|
534
|
-
if total_storage_losses_mwh < 0:
|
|
535
|
-
logger.warning(f"Negative storage losses: {total_storage_losses_mwh:.1f} MWh (net discharge)")
|
|
536
|
-
total_storage_losses_mwh = 0.0 # Don't count net discharge as negative loss
|
|
537
|
-
|
|
538
|
-
# Check for other PyPSA components that might consume energy
|
|
539
|
-
other_consumption_mwh = 0.0
|
|
540
|
-
|
|
541
|
-
# Check stores
|
|
542
|
-
if hasattr(network, 'stores_t') and hasattr(network.stores_t, 'p'):
|
|
543
|
-
stores_data = network.stores_t.p
|
|
544
|
-
if not stores_data.empty:
|
|
545
|
-
stores_consumption = abs(stores_data.sum().sum())
|
|
546
|
-
other_consumption_mwh += stores_consumption
|
|
547
|
-
logger.info(f"Stores consumption: {stores_consumption:.1f} MWh")
|
|
548
|
-
|
|
549
|
-
# Total consumption (link losses already accounted for in PyPSA generation)
|
|
550
|
-
total_consumption_with_losses_mwh = (total_load_mwh + total_storage_losses_mwh + other_consumption_mwh)
|
|
551
|
-
|
|
552
|
-
# Detailed energy balance analysis
|
|
553
|
-
logger.info(f"=== DETAILED ENERGY BALANCE ANALYSIS ===")
|
|
554
|
-
logger.info(f"GENERATION SIDE:")
|
|
555
|
-
logger.info(f" Total generation: {total_generation_mwh:.1f} MWh")
|
|
556
|
-
logger.info(f" Storage discharging: {storage_discharging_mwh:.1f} MWh")
|
|
557
|
-
logger.info(f" Total supply: {total_generation_mwh + storage_discharging_mwh:.1f} MWh")
|
|
558
|
-
logger.info(f"")
|
|
559
|
-
logger.info(f"CONSUMPTION SIDE:")
|
|
560
|
-
logger.info(f" Load demand: {total_load_mwh:.1f} MWh")
|
|
561
|
-
logger.info(f" Storage charging: {storage_charging_mwh:.1f} MWh")
|
|
562
|
-
logger.info(f" Link losses: {total_link_losses_mwh:.1f} MWh (for info only - already in generation)")
|
|
563
|
-
logger.info(f" Storage losses: {total_storage_losses_mwh:.1f} MWh")
|
|
564
|
-
logger.info(f" Other consumption: {other_consumption_mwh:.1f} MWh")
|
|
565
|
-
logger.info(f" Total consumption: {total_load_mwh + storage_charging_mwh + total_storage_losses_mwh + other_consumption_mwh:.1f} MWh")
|
|
566
|
-
logger.info(f"")
|
|
567
|
-
logger.info(f"BALANCE CHECK:")
|
|
568
|
-
total_supply = total_generation_mwh + storage_discharging_mwh
|
|
569
|
-
total_consumption = total_load_mwh + storage_charging_mwh + total_storage_losses_mwh + other_consumption_mwh
|
|
570
|
-
balance_error = total_supply - total_consumption
|
|
571
|
-
logger.info(f" Supply - Consumption = {balance_error:.1f} MWh")
|
|
572
|
-
logger.info(f" Balance error %: {(balance_error/total_supply*100):.3f}%")
|
|
573
|
-
logger.info(f"=========================================")
|
|
574
|
-
|
|
575
|
-
# Calculate carrier-specific statistics first
|
|
350
|
+
# Calculate carrier-specific statistics
|
|
576
351
|
carrier_stats = self._calculate_carrier_statistics(conn, network_id, network)
|
|
577
352
|
|
|
578
|
-
# Calculate
|
|
579
|
-
total_capital_cost = sum(carrier_stats["capital_cost_by_carrier"].values())
|
|
580
|
-
total_operational_cost = sum(carrier_stats["operational_cost_by_carrier"].values())
|
|
581
|
-
total_emissions = sum(carrier_stats["emissions_by_carrier"].values())
|
|
582
|
-
|
|
583
|
-
# Calculate derived statistics
|
|
353
|
+
# Calculate basic network statistics
|
|
584
354
|
total_cost = solve_result.get('objective_value', 0.0)
|
|
585
|
-
|
|
586
|
-
|
|
355
|
+
total_generation_mwh = sum(carrier_stats.get("dispatch_by_carrier", {}).values())
|
|
356
|
+
total_emissions_tonnes = sum(carrier_stats.get("emissions_by_carrier", {}).values())
|
|
357
|
+
total_capital_cost = sum(carrier_stats.get("capital_cost_by_carrier", {}).values())
|
|
358
|
+
total_operational_cost = sum(carrier_stats.get("operational_cost_by_carrier", {}).values())
|
|
359
|
+
total_system_cost = sum(carrier_stats.get("total_system_cost_by_carrier", {}).values())
|
|
587
360
|
|
|
588
|
-
|
|
361
|
+
# Calculate unmet load statistics
|
|
362
|
+
unmet_load_mwh = carrier_stats.get("dispatch_by_carrier", {}).get("Unmet Load", 0.0)
|
|
363
|
+
total_demand_mwh = self._calculate_total_demand(network)
|
|
364
|
+
unmet_load_percentage = (unmet_load_mwh / (total_demand_mwh + 1e-6)) * 100 if total_demand_mwh > 0 else 0.0
|
|
589
365
|
|
|
590
366
|
# Create nested structure expected by frontend
|
|
591
367
|
network_statistics = {
|
|
592
368
|
"core_summary": {
|
|
593
369
|
"total_generation_mwh": total_generation_mwh,
|
|
594
|
-
"total_demand_mwh":
|
|
370
|
+
"total_demand_mwh": total_demand_mwh,
|
|
595
371
|
"total_cost": total_cost,
|
|
596
|
-
"load_factor":
|
|
372
|
+
"load_factor": (total_demand_mwh / (total_generation_mwh + 1e-6)) if total_generation_mwh > 0 else 0.0,
|
|
597
373
|
"unserved_energy_mwh": unmet_load_mwh
|
|
598
374
|
},
|
|
599
375
|
"custom_statistics": {
|
|
600
|
-
# Include carrier-specific statistics
|
|
376
|
+
# Include carrier-specific statistics (capacity, dispatch, emissions, costs)
|
|
601
377
|
**carrier_stats,
|
|
602
|
-
"total_capital_cost": total_capital_cost,
|
|
603
|
-
"total_operational_cost": total_operational_cost,
|
|
604
|
-
"total_currency_cost":
|
|
605
|
-
"total_emissions_tons_co2":
|
|
606
|
-
"average_price_per_mwh": (
|
|
378
|
+
"total_capital_cost": total_capital_cost,
|
|
379
|
+
"total_operational_cost": total_operational_cost,
|
|
380
|
+
"total_currency_cost": total_system_cost, # Use calculated system cost instead of PyPSA objective
|
|
381
|
+
"total_emissions_tons_co2": total_emissions_tonnes,
|
|
382
|
+
"average_price_per_mwh": (total_system_cost / (total_generation_mwh + 1e-6)) if total_generation_mwh > 0 else 0.0,
|
|
607
383
|
"unmet_load_percentage": unmet_load_percentage,
|
|
608
|
-
"max_unmet_load_hour_mw": 0.0
|
|
384
|
+
"max_unmet_load_hour_mw": 0.0 # TODO: Calculate max hourly unmet load later
|
|
609
385
|
},
|
|
610
386
|
"runtime_info": {
|
|
611
387
|
"component_count": (
|
|
@@ -620,7 +396,6 @@ class ResultStorage:
|
|
|
620
396
|
}
|
|
621
397
|
|
|
622
398
|
logger.info(f"Calculated network statistics: core_summary={network_statistics['core_summary']}")
|
|
623
|
-
logger.info(f"Calculated custom statistics: custom_statistics={network_statistics['custom_statistics']}")
|
|
624
399
|
return network_statistics
|
|
625
400
|
|
|
626
401
|
except Exception as e:
|
|
@@ -635,6 +410,13 @@ class ResultStorage:
|
|
|
635
410
|
"unserved_energy_mwh": 0.0
|
|
636
411
|
},
|
|
637
412
|
"custom_statistics": {
|
|
413
|
+
"dispatch_by_carrier": {},
|
|
414
|
+
"power_capacity_by_carrier": {},
|
|
415
|
+
"energy_capacity_by_carrier": {},
|
|
416
|
+
"emissions_by_carrier": {},
|
|
417
|
+
"capital_cost_by_carrier": {},
|
|
418
|
+
"operational_cost_by_carrier": {},
|
|
419
|
+
"total_system_cost_by_carrier": {},
|
|
638
420
|
"total_capital_cost": 0.0,
|
|
639
421
|
"total_operational_cost": 0.0,
|
|
640
422
|
"total_currency_cost": 0.0,
|
|
@@ -653,50 +435,188 @@ class ResultStorage:
|
|
|
653
435
|
"error": str(e)
|
|
654
436
|
}
|
|
655
437
|
|
|
656
|
-
def
|
|
657
|
-
"""
|
|
658
|
-
|
|
438
|
+
def _calculate_carrier_statistics(self, conn, network_id: int, network: 'pypsa.Network') -> Dict[str, Any]:
|
|
439
|
+
"""
|
|
440
|
+
Calculate carrier-specific statistics directly from the network.
|
|
441
|
+
This is the primary calculation - per-year stats will be calculated separately.
|
|
442
|
+
"""
|
|
659
443
|
try:
|
|
660
|
-
#
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
444
|
+
# Calculate all-year statistics directly from the network
|
|
445
|
+
# Extract years from network snapshots
|
|
446
|
+
if hasattr(network.snapshots, 'levels'):
|
|
447
|
+
# Multi-period optimization - get years from period level
|
|
448
|
+
period_values = network.snapshots.get_level_values(0)
|
|
449
|
+
years = sorted(period_values.unique())
|
|
450
|
+
elif hasattr(network.snapshots, 'year'):
|
|
451
|
+
years = sorted(network.snapshots.year.unique())
|
|
452
|
+
elif hasattr(network, '_available_years'):
|
|
453
|
+
years = network._available_years
|
|
454
|
+
else:
|
|
455
|
+
years = [2020] # Fallback
|
|
456
|
+
|
|
457
|
+
logger.info(f"Calculating all-year carrier statistics for years: {years}")
|
|
458
|
+
|
|
459
|
+
# Calculate per-year statistics first
|
|
460
|
+
all_year_stats = {
|
|
461
|
+
"dispatch_by_carrier": {},
|
|
462
|
+
"power_capacity_by_carrier": {},
|
|
463
|
+
"energy_capacity_by_carrier": {},
|
|
464
|
+
"emissions_by_carrier": {},
|
|
465
|
+
"capital_cost_by_carrier": {},
|
|
466
|
+
"operational_cost_by_carrier": {},
|
|
467
|
+
"total_system_cost_by_carrier": {}
|
|
670
468
|
}
|
|
671
|
-
|
|
469
|
+
|
|
470
|
+
# Initialize all carriers with zero values
|
|
471
|
+
cursor = conn.execute("""
|
|
472
|
+
SELECT DISTINCT name FROM carriers WHERE network_id = ?
|
|
473
|
+
""", (network_id,))
|
|
474
|
+
all_carriers = [row[0] for row in cursor.fetchall()]
|
|
475
|
+
|
|
476
|
+
# Initialize all carriers with zero values (including special "Unmet Load" carrier)
|
|
477
|
+
all_carriers_with_unmet = all_carriers + ['Unmet Load']
|
|
478
|
+
for carrier in all_carriers_with_unmet:
|
|
479
|
+
all_year_stats["dispatch_by_carrier"][carrier] = 0.0
|
|
480
|
+
all_year_stats["power_capacity_by_carrier"][carrier] = 0.0
|
|
481
|
+
all_year_stats["energy_capacity_by_carrier"][carrier] = 0.0
|
|
482
|
+
all_year_stats["emissions_by_carrier"][carrier] = 0.0
|
|
483
|
+
all_year_stats["capital_cost_by_carrier"][carrier] = 0.0
|
|
484
|
+
all_year_stats["operational_cost_by_carrier"][carrier] = 0.0
|
|
485
|
+
all_year_stats["total_system_cost_by_carrier"][carrier] = 0.0
|
|
486
|
+
|
|
487
|
+
# Calculate statistics for each year and sum them up
|
|
488
|
+
for year in years:
|
|
489
|
+
year_stats = self._calculate_year_carrier_statistics(conn, network_id, network, year)
|
|
490
|
+
|
|
491
|
+
# Sum up all the statistics (including "Unmet Load")
|
|
492
|
+
for carrier in all_carriers_with_unmet:
|
|
493
|
+
# Sum dispatch, emissions, and costs across years
|
|
494
|
+
all_year_stats["dispatch_by_carrier"][carrier] += year_stats["dispatch_by_carrier"].get(carrier, 0.0)
|
|
495
|
+
all_year_stats["emissions_by_carrier"][carrier] += year_stats["emissions_by_carrier"].get(carrier, 0.0)
|
|
496
|
+
all_year_stats["capital_cost_by_carrier"][carrier] += year_stats["capital_cost_by_carrier"].get(carrier, 0.0)
|
|
497
|
+
all_year_stats["operational_cost_by_carrier"][carrier] += year_stats["operational_cost_by_carrier"].get(carrier, 0.0)
|
|
498
|
+
all_year_stats["total_system_cost_by_carrier"][carrier] += year_stats["total_system_cost_by_carrier"].get(carrier, 0.0)
|
|
499
|
+
|
|
500
|
+
# For capacity: use the last year (final capacity state)
|
|
501
|
+
if year == years[-1]:
|
|
502
|
+
all_year_stats["power_capacity_by_carrier"][carrier] = year_stats["power_capacity_by_carrier"].get(carrier, 0.0)
|
|
503
|
+
all_year_stats["energy_capacity_by_carrier"][carrier] = year_stats["energy_capacity_by_carrier"].get(carrier, 0.0)
|
|
504
|
+
|
|
505
|
+
logger.info(f"Calculated all-year carrier statistics:")
|
|
506
|
+
logger.info(f" Total dispatch: {sum(all_year_stats['dispatch_by_carrier'].values()):.2f} MWh")
|
|
507
|
+
logger.info(f" Total emissions: {sum(all_year_stats['emissions_by_carrier'].values()):.2f} tonnes CO2")
|
|
508
|
+
logger.info(f" Total capital cost: {sum(all_year_stats['capital_cost_by_carrier'].values()):.2f} USD")
|
|
509
|
+
logger.info(f" Total operational cost: {sum(all_year_stats['operational_cost_by_carrier'].values()):.2f} USD")
|
|
510
|
+
logger.info(f" Final power capacity: {sum(all_year_stats['power_capacity_by_carrier'].values()):.2f} MW")
|
|
511
|
+
|
|
512
|
+
return all_year_stats
|
|
513
|
+
|
|
672
514
|
except Exception as e:
|
|
673
|
-
logger.
|
|
674
|
-
return
|
|
515
|
+
logger.error(f"Failed to calculate carrier statistics: {e}", exc_info=True)
|
|
516
|
+
return {
|
|
517
|
+
"dispatch_by_carrier": {},
|
|
518
|
+
"power_capacity_by_carrier": {},
|
|
519
|
+
"energy_capacity_by_carrier": {},
|
|
520
|
+
"emissions_by_carrier": {},
|
|
521
|
+
"capital_cost_by_carrier": {},
|
|
522
|
+
"operational_cost_by_carrier": {},
|
|
523
|
+
"total_system_cost_by_carrier": {}
|
|
524
|
+
}
|
|
675
525
|
|
|
676
|
-
def
|
|
677
|
-
|
|
678
|
-
|
|
526
|
+
def _store_year_based_statistics(
|
|
527
|
+
self,
|
|
528
|
+
conn,
|
|
529
|
+
network_id: int,
|
|
530
|
+
network: 'pypsa.Network',
|
|
531
|
+
year_statistics: Dict[int, Dict[str, Any]],
|
|
532
|
+
scenario_id: Optional[int]
|
|
533
|
+
) -> int:
|
|
534
|
+
"""Store year-based statistics to database"""
|
|
679
535
|
try:
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
536
|
+
import json
|
|
537
|
+
stored_count = 0
|
|
538
|
+
|
|
539
|
+
# Use master scenario if no scenario specified
|
|
540
|
+
if scenario_id is None:
|
|
541
|
+
from pyconvexity.models import get_master_scenario_id
|
|
542
|
+
scenario_id = get_master_scenario_id(conn, network_id)
|
|
543
|
+
|
|
544
|
+
# Check if network_solve_results_by_year table exists, create if not
|
|
545
|
+
conn.execute("""
|
|
546
|
+
CREATE TABLE IF NOT EXISTS network_solve_results_by_year (
|
|
547
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
548
|
+
network_id INTEGER NOT NULL,
|
|
549
|
+
scenario_id INTEGER NOT NULL,
|
|
550
|
+
year INTEGER NOT NULL,
|
|
551
|
+
results_json TEXT,
|
|
552
|
+
metadata_json TEXT,
|
|
553
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
554
|
+
FOREIGN KEY (network_id) REFERENCES networks(id),
|
|
555
|
+
FOREIGN KEY (scenario_id) REFERENCES scenarios(id),
|
|
556
|
+
UNIQUE(network_id, scenario_id, year)
|
|
557
|
+
)
|
|
558
|
+
""")
|
|
559
|
+
|
|
560
|
+
for year, stats in year_statistics.items():
|
|
561
|
+
try:
|
|
562
|
+
# Calculate proper year-specific carrier statistics
|
|
563
|
+
year_carrier_stats = self._calculate_year_carrier_statistics(conn, network_id, network, year)
|
|
564
|
+
|
|
565
|
+
# Merge year-specific carrier stats into the statistics
|
|
566
|
+
if "custom_statistics" in stats:
|
|
567
|
+
stats["custom_statistics"].update(year_carrier_stats)
|
|
568
|
+
else:
|
|
569
|
+
stats["custom_statistics"] = year_carrier_stats
|
|
570
|
+
|
|
571
|
+
# Wrap the year statistics in the same structure as overall results for consistency
|
|
572
|
+
year_result_wrapper = {
|
|
573
|
+
"success": True,
|
|
574
|
+
"year": year,
|
|
575
|
+
"network_statistics": stats
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
metadata = {
|
|
579
|
+
"year": year,
|
|
580
|
+
"network_id": network_id,
|
|
581
|
+
"scenario_id": scenario_id
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
conn.execute("""
|
|
585
|
+
INSERT OR REPLACE INTO network_solve_results_by_year
|
|
586
|
+
(network_id, scenario_id, year, results_json, metadata_json)
|
|
587
|
+
VALUES (?, ?, ?, ?, ?)
|
|
588
|
+
""", (
|
|
589
|
+
network_id,
|
|
590
|
+
scenario_id,
|
|
591
|
+
year,
|
|
592
|
+
json.dumps(year_result_wrapper, default=self._json_serializer),
|
|
593
|
+
json.dumps(metadata, default=self._json_serializer)
|
|
594
|
+
))
|
|
595
|
+
|
|
596
|
+
stored_count += 1
|
|
597
|
+
logger.info(f"Stored year-based statistics for year {year}")
|
|
598
|
+
|
|
599
|
+
except Exception as e:
|
|
600
|
+
logger.error(f"Failed to store statistics for year {year}: {e}")
|
|
601
|
+
continue
|
|
602
|
+
|
|
603
|
+
logger.info(f"Successfully stored year-based statistics for {stored_count} years")
|
|
604
|
+
return stored_count
|
|
605
|
+
|
|
689
606
|
except Exception as e:
|
|
690
|
-
logger.
|
|
691
|
-
return
|
|
607
|
+
logger.error(f"Failed to store year-based statistics: {e}", exc_info=True)
|
|
608
|
+
return 0
|
|
692
609
|
|
|
693
|
-
def
|
|
694
|
-
"""
|
|
610
|
+
def _calculate_year_carrier_statistics(self, conn, network_id: int, network: 'pypsa.Network', year: int) -> Dict[str, Any]:
|
|
611
|
+
"""
|
|
612
|
+
Calculate carrier-specific statistics for a specific year.
|
|
613
|
+
For now, only calculate capacity statistics.
|
|
614
|
+
"""
|
|
695
615
|
try:
|
|
696
|
-
# Initialize carrier statistics
|
|
616
|
+
# Initialize carrier statistics
|
|
697
617
|
carrier_stats = {
|
|
698
618
|
"dispatch_by_carrier": {},
|
|
699
|
-
"power_capacity_by_carrier": {}, # MW - Generators + Storage Units (power)
|
|
619
|
+
"power_capacity_by_carrier": {}, # MW - Generators + Storage Units (power) + Lines + Links
|
|
700
620
|
"energy_capacity_by_carrier": {}, # MWh - Stores + Storage Units (energy)
|
|
701
621
|
"emissions_by_carrier": {},
|
|
702
622
|
"capital_cost_by_carrier": {},
|
|
@@ -710,8 +630,9 @@ class ResultStorage:
|
|
|
710
630
|
""", (network_id,))
|
|
711
631
|
all_carriers = [row[0] for row in cursor.fetchall()]
|
|
712
632
|
|
|
713
|
-
# Initialize all carriers with zero values
|
|
714
|
-
|
|
633
|
+
# Initialize all carriers with zero values (including special "Unmet Load" carrier)
|
|
634
|
+
all_carriers_with_unmet = all_carriers + ['Unmet Load']
|
|
635
|
+
for carrier in all_carriers_with_unmet:
|
|
715
636
|
carrier_stats["dispatch_by_carrier"][carrier] = 0.0
|
|
716
637
|
carrier_stats["power_capacity_by_carrier"][carrier] = 0.0
|
|
717
638
|
carrier_stats["energy_capacity_by_carrier"][carrier] = 0.0
|
|
@@ -720,37 +641,41 @@ class ResultStorage:
|
|
|
720
641
|
carrier_stats["operational_cost_by_carrier"][carrier] = 0.0
|
|
721
642
|
carrier_stats["total_system_cost_by_carrier"][carrier] = 0.0
|
|
722
643
|
|
|
723
|
-
# Calculate dispatch by carrier
|
|
644
|
+
# Calculate dispatch (generation) by carrier for this specific year
|
|
724
645
|
|
|
725
|
-
# 1. GENERATORS -
|
|
646
|
+
# 1. GENERATORS - Generation dispatch (including UNMET_LOAD)
|
|
726
647
|
if hasattr(network, 'generators_t') and hasattr(network.generators_t, 'p'):
|
|
727
|
-
# Get generator-carrier mapping
|
|
648
|
+
# Get generator-carrier mapping (include both GENERATOR and UNMET_LOAD)
|
|
728
649
|
cursor = conn.execute("""
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
650
|
+
SELECT c.name as component_name,
|
|
651
|
+
CASE
|
|
652
|
+
WHEN c.component_type = 'UNMET_LOAD' THEN 'Unmet Load'
|
|
653
|
+
ELSE carr.name
|
|
654
|
+
END as carrier_name
|
|
655
|
+
FROM components c
|
|
656
|
+
JOIN carriers carr ON c.carrier_id = carr.id
|
|
657
|
+
WHERE c.network_id = ? AND c.component_type IN ('GENERATOR', 'UNMET_LOAD')
|
|
658
|
+
""", (network_id,))
|
|
735
659
|
generator_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
736
660
|
|
|
737
|
-
#
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
661
|
+
# Filter generation data for this specific year
|
|
662
|
+
year_generation = self._filter_timeseries_by_year(network.generators_t.p, network.snapshots, year)
|
|
663
|
+
if year_generation is not None and not year_generation.empty:
|
|
664
|
+
for gen_name in year_generation.columns:
|
|
665
|
+
if gen_name in generator_carriers:
|
|
666
|
+
carrier_name = generator_carriers[gen_name]
|
|
667
|
+
|
|
668
|
+
# Calculate generation for this year (ALWAYS apply snapshot weightings to convert MW to MWh)
|
|
669
|
+
year_weightings = self._get_year_weightings(network, year)
|
|
670
|
+
if year_weightings is not None:
|
|
671
|
+
generation_mwh = float((year_generation[gen_name].values * year_weightings).sum())
|
|
746
672
|
else:
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
carrier_stats["dispatch_by_carrier"][carrier_name] += generation_mwh
|
|
673
|
+
# Fallback: simple sum (will be incorrect for non-1H models)
|
|
674
|
+
generation_mwh = float(year_generation[gen_name].sum())
|
|
675
|
+
logger.warning(f"Could not apply snapshot weightings for {gen_name} in year {year} - energy may be incorrect")
|
|
676
|
+
|
|
677
|
+
if carrier_name in carrier_stats["dispatch_by_carrier"]:
|
|
678
|
+
carrier_stats["dispatch_by_carrier"][carrier_name] += generation_mwh
|
|
754
679
|
|
|
755
680
|
# 2. STORAGE_UNITS - Discharge only (positive values)
|
|
756
681
|
if hasattr(network, 'storage_units_t') and hasattr(network.storage_units_t, 'p'):
|
|
@@ -761,29 +686,26 @@ class ResultStorage:
|
|
|
761
686
|
JOIN carriers carr ON c.carrier_id = carr.id
|
|
762
687
|
WHERE c.network_id = ? AND c.component_type = 'STORAGE_UNIT'
|
|
763
688
|
""", (network_id,))
|
|
764
|
-
|
|
765
689
|
storage_unit_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
766
690
|
|
|
767
|
-
#
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
691
|
+
# Filter storage unit data for this specific year
|
|
692
|
+
year_storage = self._filter_timeseries_by_year(network.storage_units_t.p, network.snapshots, year)
|
|
693
|
+
if year_storage is not None and not year_storage.empty:
|
|
694
|
+
for su_name in year_storage.columns:
|
|
695
|
+
if su_name in storage_unit_carriers:
|
|
696
|
+
carrier_name = storage_unit_carriers[su_name]
|
|
697
|
+
|
|
698
|
+
# Calculate discharge for this year (positive values only, ALWAYS apply snapshot weightings)
|
|
699
|
+
year_weightings = self._get_year_weightings(network, year)
|
|
700
|
+
if year_weightings is not None:
|
|
701
|
+
discharge_mwh = float((year_storage[su_name].clip(lower=0).values * year_weightings).sum())
|
|
776
702
|
else:
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
discharge_mwh = float((su_power.clip(lower=0) * weighting_values).sum())
|
|
784
|
-
|
|
785
|
-
if carrier_name in carrier_stats["dispatch_by_carrier"]:
|
|
786
|
-
carrier_stats["dispatch_by_carrier"][carrier_name] += discharge_mwh
|
|
703
|
+
# Fallback: simple sum (will be incorrect for non-1H models)
|
|
704
|
+
discharge_mwh = float(year_storage[su_name].clip(lower=0).sum())
|
|
705
|
+
logger.warning(f"Could not apply snapshot weightings for storage unit {su_name} in year {year} - energy may be incorrect")
|
|
706
|
+
|
|
707
|
+
if carrier_name in carrier_stats["dispatch_by_carrier"]:
|
|
708
|
+
carrier_stats["dispatch_by_carrier"][carrier_name] += discharge_mwh
|
|
787
709
|
|
|
788
710
|
# 3. STORES - Discharge only (positive values)
|
|
789
711
|
if hasattr(network, 'stores_t') and hasattr(network.stores_t, 'p'):
|
|
@@ -794,719 +716,295 @@ class ResultStorage:
|
|
|
794
716
|
JOIN carriers carr ON c.carrier_id = carr.id
|
|
795
717
|
WHERE c.network_id = ? AND c.component_type = 'STORE'
|
|
796
718
|
""", (network_id,))
|
|
797
|
-
|
|
798
719
|
store_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
799
720
|
|
|
800
|
-
#
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
721
|
+
# Filter store data for this specific year
|
|
722
|
+
year_stores = self._filter_timeseries_by_year(network.stores_t.p, network.snapshots, year)
|
|
723
|
+
if year_stores is not None and not year_stores.empty:
|
|
724
|
+
for store_name in year_stores.columns:
|
|
725
|
+
if store_name in store_carriers:
|
|
726
|
+
carrier_name = store_carriers[store_name]
|
|
727
|
+
|
|
728
|
+
# Calculate discharge for this year (positive values only, ALWAYS apply snapshot weightings)
|
|
729
|
+
year_weightings = self._get_year_weightings(network, year)
|
|
730
|
+
if year_weightings is not None:
|
|
731
|
+
discharge_mwh = float((year_stores[store_name].clip(lower=0).values * year_weightings).sum())
|
|
809
732
|
else:
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
discharge_mwh = float((store_power.clip(lower=0) * weighting_values).sum())
|
|
817
|
-
|
|
818
|
-
if carrier_name in carrier_stats["dispatch_by_carrier"]:
|
|
819
|
-
carrier_stats["dispatch_by_carrier"][carrier_name] += discharge_mwh
|
|
733
|
+
# Fallback: simple sum (will be incorrect for non-1H models)
|
|
734
|
+
discharge_mwh = float(year_stores[store_name].clip(lower=0).sum())
|
|
735
|
+
logger.warning(f"Could not apply snapshot weightings for store {store_name} in year {year} - energy may be incorrect")
|
|
736
|
+
|
|
737
|
+
if carrier_name in carrier_stats["dispatch_by_carrier"]:
|
|
738
|
+
carrier_stats["dispatch_by_carrier"][carrier_name] += discharge_mwh
|
|
820
739
|
|
|
821
|
-
# Calculate
|
|
740
|
+
# Calculate emissions by carrier for this specific year
|
|
741
|
+
# Get emission factors for all carriers
|
|
742
|
+
cursor = conn.execute("""
|
|
743
|
+
SELECT name, co2_emissions FROM carriers WHERE network_id = ?
|
|
744
|
+
""", (network_id,))
|
|
745
|
+
emission_factors = {row[0]: row[1] for row in cursor.fetchall()}
|
|
822
746
|
|
|
823
|
-
#
|
|
824
|
-
|
|
825
|
-
#
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
FROM components c
|
|
829
|
-
JOIN carriers carr ON c.carrier_id = carr.id
|
|
830
|
-
WHERE c.network_id = ? AND c.component_type = 'GENERATOR'
|
|
831
|
-
""", (network_id,))
|
|
832
|
-
|
|
833
|
-
generator_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
747
|
+
# Calculate emissions: dispatch (MWh) × emission factor (tonnes CO2/MWh) = tonnes CO2
|
|
748
|
+
for carrier_name, dispatch_mwh in carrier_stats["dispatch_by_carrier"].items():
|
|
749
|
+
# Handle None values safely
|
|
750
|
+
if dispatch_mwh is None:
|
|
751
|
+
dispatch_mwh = 0.0
|
|
834
752
|
|
|
835
|
-
#
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
carrier_name = generator_carriers[gen_name]
|
|
839
|
-
# Use p_nom_opt if available, otherwise p_nom (POWER capacity)
|
|
840
|
-
if 'p_nom_opt' in network.generators.columns:
|
|
841
|
-
capacity_mw = float(network.generators.loc[gen_name, 'p_nom_opt'])
|
|
842
|
-
else:
|
|
843
|
-
capacity_mw = float(network.generators.loc[gen_name, 'p_nom']) if 'p_nom' in network.generators.columns else 0.0
|
|
844
|
-
|
|
845
|
-
if carrier_name in carrier_stats["power_capacity_by_carrier"]:
|
|
846
|
-
carrier_stats["power_capacity_by_carrier"][carrier_name] += capacity_mw
|
|
847
|
-
|
|
848
|
-
# 2. STORAGE_UNITS - Power capacity (MW) + Energy capacity (MWh)
|
|
849
|
-
if hasattr(network, 'storage_units') and not network.storage_units.empty:
|
|
850
|
-
# Get storage unit-carrier mapping
|
|
851
|
-
cursor = conn.execute("""
|
|
852
|
-
SELECT c.name as component_name, carr.name as carrier_name
|
|
853
|
-
FROM components c
|
|
854
|
-
JOIN carriers carr ON c.carrier_id = carr.id
|
|
855
|
-
WHERE c.network_id = ? AND c.component_type = 'STORAGE_UNIT'
|
|
856
|
-
""", (network_id,))
|
|
753
|
+
emission_factor = emission_factors.get(carrier_name, 0.0) # Default to 0 if no emission factor
|
|
754
|
+
if emission_factor is None:
|
|
755
|
+
emission_factor = 0.0
|
|
857
756
|
|
|
858
|
-
|
|
757
|
+
emissions_tonnes = dispatch_mwh * emission_factor
|
|
859
758
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
if su_name in storage_unit_carriers:
|
|
863
|
-
carrier_name = storage_unit_carriers[su_name]
|
|
864
|
-
|
|
865
|
-
# Power capacity (MW)
|
|
866
|
-
if 'p_nom_opt' in network.storage_units.columns:
|
|
867
|
-
p_nom_opt = float(network.storage_units.loc[su_name, 'p_nom_opt'])
|
|
868
|
-
else:
|
|
869
|
-
p_nom_opt = float(network.storage_units.loc[su_name, 'p_nom']) if 'p_nom' in network.storage_units.columns else 0.0
|
|
870
|
-
|
|
871
|
-
if carrier_name in carrier_stats["power_capacity_by_carrier"]:
|
|
872
|
-
carrier_stats["power_capacity_by_carrier"][carrier_name] += p_nom_opt
|
|
873
|
-
|
|
874
|
-
# Energy capacity (MWh) using max_hours (matching old solver)
|
|
875
|
-
max_hours = 1.0 # Default from validation data
|
|
876
|
-
if 'max_hours' in network.storage_units.columns:
|
|
877
|
-
max_hours = float(network.storage_units.loc[su_name, 'max_hours'])
|
|
878
|
-
energy_capacity_mwh = p_nom_opt * max_hours
|
|
879
|
-
|
|
880
|
-
if carrier_name in carrier_stats["energy_capacity_by_carrier"]:
|
|
881
|
-
carrier_stats["energy_capacity_by_carrier"][carrier_name] += energy_capacity_mwh
|
|
759
|
+
if carrier_name in carrier_stats["emissions_by_carrier"]:
|
|
760
|
+
carrier_stats["emissions_by_carrier"][carrier_name] += emissions_tonnes
|
|
882
761
|
|
|
883
|
-
#
|
|
884
|
-
|
|
885
|
-
# Get store-carrier mapping
|
|
886
|
-
cursor = conn.execute("""
|
|
887
|
-
SELECT c.name as component_name, carr.name as carrier_name
|
|
888
|
-
FROM components c
|
|
889
|
-
JOIN carriers carr ON c.carrier_id = carr.id
|
|
890
|
-
WHERE c.network_id = ? AND c.component_type = 'STORE'
|
|
891
|
-
""", (network_id,))
|
|
892
|
-
|
|
893
|
-
store_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
894
|
-
|
|
895
|
-
# Calculate energy capacity for each store
|
|
896
|
-
for store_name in network.stores.index:
|
|
897
|
-
if store_name in store_carriers:
|
|
898
|
-
carrier_name = store_carriers[store_name]
|
|
899
|
-
|
|
900
|
-
# Energy capacity (MWh) - stores don't have power capacity, only energy
|
|
901
|
-
if 'e_nom_opt' in network.stores.columns:
|
|
902
|
-
e_nom_opt = float(network.stores.loc[store_name, 'e_nom_opt'])
|
|
903
|
-
else:
|
|
904
|
-
e_nom_opt = float(network.stores.loc[store_name, 'e_nom']) if 'e_nom' in network.stores.columns else 0.0
|
|
905
|
-
|
|
906
|
-
# Stores contribute only to energy capacity
|
|
907
|
-
if carrier_name in carrier_stats["energy_capacity_by_carrier"]:
|
|
908
|
-
carrier_stats["energy_capacity_by_carrier"][carrier_name] += e_nom_opt
|
|
762
|
+
# Calculate capital costs by carrier for this specific year
|
|
763
|
+
# Capital costs are annualized and counted every year the component is active
|
|
909
764
|
|
|
910
|
-
#
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
FROM components c
|
|
916
|
-
JOIN carriers carr ON c.carrier_id = carr.id
|
|
917
|
-
WHERE c.network_id = ? AND c.component_type = 'LINE'
|
|
918
|
-
""", (network_id,))
|
|
919
|
-
|
|
920
|
-
line_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
765
|
+
# Helper function to check if component is active in this year
|
|
766
|
+
def is_component_active(build_year, lifetime, current_year):
|
|
767
|
+
"""Check if component is active in the current year based on build_year and lifetime"""
|
|
768
|
+
if pd.isna(build_year):
|
|
769
|
+
return True # No build year constraint
|
|
921
770
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
carrier_name = line_carriers[line_name]
|
|
926
|
-
|
|
927
|
-
# Apparent power capacity (MVA) - convert to MW equivalent for consistency
|
|
928
|
-
if 's_nom_opt' in network.lines.columns:
|
|
929
|
-
capacity_mva = float(network.lines.loc[line_name, 's_nom_opt'])
|
|
930
|
-
else:
|
|
931
|
-
capacity_mva = float(network.lines.loc[line_name, 's_nom']) if 's_nom' in network.lines.columns else 0.0
|
|
932
|
-
|
|
933
|
-
# Convert MVA to MW (assume power factor = 1 for simplicity)
|
|
934
|
-
capacity_mw = capacity_mva
|
|
935
|
-
|
|
936
|
-
if carrier_name in carrier_stats["power_capacity_by_carrier"]:
|
|
937
|
-
carrier_stats["power_capacity_by_carrier"][carrier_name] += capacity_mw
|
|
938
|
-
|
|
939
|
-
# 5. LINKS - Power capacity (MW)
|
|
940
|
-
if hasattr(network, 'links') and not network.links.empty:
|
|
941
|
-
# Get link-carrier mapping
|
|
942
|
-
cursor = conn.execute("""
|
|
943
|
-
SELECT c.name as component_name, carr.name as carrier_name
|
|
944
|
-
FROM components c
|
|
945
|
-
JOIN carriers carr ON c.carrier_id = carr.id
|
|
946
|
-
WHERE c.network_id = ? AND c.component_type = 'LINK'
|
|
947
|
-
""", (network_id,))
|
|
771
|
+
build_year = int(build_year)
|
|
772
|
+
if build_year > current_year:
|
|
773
|
+
return False # Not built yet
|
|
948
774
|
|
|
949
|
-
|
|
775
|
+
if pd.isna(lifetime) or lifetime == float('inf'):
|
|
776
|
+
return True # Infinite lifetime
|
|
950
777
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
carrier_name = link_carriers[link_name]
|
|
955
|
-
|
|
956
|
-
# Power capacity (MW)
|
|
957
|
-
if 'p_nom_opt' in network.links.columns:
|
|
958
|
-
capacity_mw = float(network.links.loc[link_name, 'p_nom_opt'])
|
|
959
|
-
else:
|
|
960
|
-
capacity_mw = float(network.links.loc[link_name, 'p_nom']) if 'p_nom' in network.links.columns else 0.0
|
|
961
|
-
|
|
962
|
-
if carrier_name in carrier_stats["power_capacity_by_carrier"]:
|
|
963
|
-
carrier_stats["power_capacity_by_carrier"][carrier_name] += capacity_mw
|
|
964
|
-
|
|
965
|
-
# Calculate emissions by carrier
|
|
966
|
-
cursor = conn.execute("""
|
|
967
|
-
SELECT name, co2_emissions
|
|
968
|
-
FROM carriers
|
|
969
|
-
WHERE network_id = ? AND co2_emissions IS NOT NULL
|
|
970
|
-
ORDER BY name
|
|
971
|
-
""", (network_id,))
|
|
972
|
-
|
|
973
|
-
emission_factors = {}
|
|
974
|
-
for row in cursor.fetchall():
|
|
975
|
-
carrier_name, co2_emissions = row
|
|
976
|
-
emission_factors[carrier_name] = co2_emissions
|
|
778
|
+
lifetime = int(lifetime)
|
|
779
|
+
end_year = build_year + lifetime - 1
|
|
780
|
+
return current_year <= end_year
|
|
977
781
|
|
|
978
|
-
#
|
|
979
|
-
for carrier, dispatch_mwh in carrier_stats["dispatch_by_carrier"].items():
|
|
980
|
-
emission_factor = emission_factors.get(carrier, 0.0)
|
|
981
|
-
emissions = dispatch_mwh * emission_factor
|
|
982
|
-
carrier_stats["emissions_by_carrier"][carrier] = emissions
|
|
983
|
-
|
|
984
|
-
# Calculate cost statistics by carrier (all component types)
|
|
985
|
-
|
|
986
|
-
# 1. GENERATORS - Operational and capital costs
|
|
782
|
+
# 1. GENERATORS - Capital costs (including UNMET_LOAD)
|
|
987
783
|
if hasattr(network, 'generators') and not network.generators.empty:
|
|
988
|
-
# Get generator
|
|
784
|
+
# Get generator info: carrier, capital_cost, build_year, lifetime (include UNMET_LOAD)
|
|
989
785
|
cursor = conn.execute("""
|
|
990
|
-
SELECT c.name as component_name,
|
|
786
|
+
SELECT c.name as component_name,
|
|
787
|
+
CASE
|
|
788
|
+
WHEN c.component_type = 'UNMET_LOAD' THEN 'Unmet Load'
|
|
789
|
+
ELSE carr.name
|
|
790
|
+
END as carrier_name
|
|
991
791
|
FROM components c
|
|
992
792
|
JOIN carriers carr ON c.carrier_id = carr.id
|
|
993
|
-
WHERE c.network_id = ? AND c.component_type
|
|
793
|
+
WHERE c.network_id = ? AND c.component_type IN ('GENERATOR', 'UNMET_LOAD')
|
|
994
794
|
""", (network_id,))
|
|
995
|
-
|
|
996
795
|
generator_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
997
796
|
|
|
998
|
-
# Calculate operational costs based on dispatch and marginal costs
|
|
999
|
-
if hasattr(network, 'generators_t') and hasattr(network.generators_t, 'p'):
|
|
1000
|
-
for gen_name in network.generators.index:
|
|
1001
|
-
if gen_name in generator_carriers and gen_name in network.generators_t.p.columns:
|
|
1002
|
-
carrier_name = generator_carriers[gen_name]
|
|
1003
|
-
|
|
1004
|
-
# Get marginal cost for this generator
|
|
1005
|
-
marginal_cost = 0.0
|
|
1006
|
-
if 'marginal_cost' in network.generators.columns:
|
|
1007
|
-
marginal_cost = float(network.generators.loc[gen_name, 'marginal_cost'])
|
|
1008
|
-
|
|
1009
|
-
# Calculate operational cost = dispatch * marginal_cost (with weightings)
|
|
1010
|
-
weightings = network.snapshot_weightings
|
|
1011
|
-
if isinstance(weightings, pd.DataFrame):
|
|
1012
|
-
if 'objective' in weightings.columns:
|
|
1013
|
-
weighting_values = weightings['objective'].values
|
|
1014
|
-
else:
|
|
1015
|
-
weighting_values = weightings.iloc[:, 0].values
|
|
1016
|
-
else:
|
|
1017
|
-
weighting_values = weightings.values
|
|
1018
|
-
|
|
1019
|
-
dispatch_mwh = float((network.generators_t.p[gen_name].values * weighting_values).sum())
|
|
1020
|
-
operational_cost = dispatch_mwh * marginal_cost
|
|
1021
|
-
|
|
1022
|
-
if carrier_name in carrier_stats["operational_cost_by_carrier"]:
|
|
1023
|
-
carrier_stats["operational_cost_by_carrier"][carrier_name] += operational_cost
|
|
1024
|
-
|
|
1025
|
-
# Calculate annual capital costs for all operational generators (matching old solver per-year logic)
|
|
1026
|
-
for gen_name in network.generators.index:
|
|
1027
|
-
if gen_name in generator_carriers:
|
|
1028
|
-
carrier_name = generator_carriers[gen_name]
|
|
1029
|
-
|
|
1030
|
-
# Get capital cost and capacity
|
|
1031
|
-
capital_cost_per_mw = 0.0
|
|
1032
|
-
if 'capital_cost' in network.generators.columns:
|
|
1033
|
-
capital_cost_per_mw = float(network.generators.loc[gen_name, 'capital_cost'])
|
|
1034
|
-
|
|
1035
|
-
capacity_mw = 0.0
|
|
1036
|
-
if 'p_nom_opt' in network.generators.columns:
|
|
1037
|
-
capacity_mw = float(network.generators.loc[gen_name, 'p_nom_opt'])
|
|
1038
|
-
elif 'p_nom' in network.generators.columns:
|
|
1039
|
-
capacity_mw = float(network.generators.loc[gen_name, 'p_nom'])
|
|
1040
|
-
|
|
1041
|
-
# Annual capital cost for operational assets (undiscounted)
|
|
1042
|
-
annual_capital_cost = capital_cost_per_mw * capacity_mw
|
|
1043
|
-
|
|
1044
|
-
if carrier_name in carrier_stats["capital_cost_by_carrier"]:
|
|
1045
|
-
carrier_stats["capital_cost_by_carrier"][carrier_name] += annual_capital_cost
|
|
1046
|
-
|
|
1047
|
-
# Calculate operational costs including fixed costs (matching old solver)
|
|
1048
797
|
for gen_name in network.generators.index:
|
|
1049
798
|
if gen_name in generator_carriers:
|
|
1050
799
|
carrier_name = generator_carriers[gen_name]
|
|
1051
800
|
|
|
1052
|
-
#
|
|
1053
|
-
|
|
1054
|
-
if '
|
|
1055
|
-
fixed_cost_per_mw = float(network.generators.loc[gen_name, 'fixed_cost'])
|
|
1056
|
-
|
|
1057
|
-
capacity_mw = 0.0
|
|
1058
|
-
if 'p_nom_opt' in network.generators.columns:
|
|
1059
|
-
capacity_mw = float(network.generators.loc[gen_name, 'p_nom_opt'])
|
|
1060
|
-
elif 'p_nom' in network.generators.columns:
|
|
1061
|
-
capacity_mw = float(network.generators.loc[gen_name, 'p_nom'])
|
|
801
|
+
# Get build year and lifetime
|
|
802
|
+
build_year = network.generators.loc[gen_name, 'build_year'] if 'build_year' in network.generators.columns else None
|
|
803
|
+
lifetime = network.generators.loc[gen_name, 'lifetime'] if 'lifetime' in network.generators.columns else None
|
|
1062
804
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
805
|
+
# Check if component is active in this year
|
|
806
|
+
if is_component_active(build_year, lifetime, year):
|
|
807
|
+
# Get capacity and capital cost
|
|
808
|
+
if 'p_nom_opt' in network.generators.columns:
|
|
809
|
+
capacity_mw = float(network.generators.loc[gen_name, 'p_nom_opt'])
|
|
810
|
+
else:
|
|
811
|
+
capacity_mw = float(network.generators.loc[gen_name, 'p_nom']) if 'p_nom' in network.generators.columns else 0.0
|
|
812
|
+
|
|
813
|
+
capital_cost_per_mw = float(network.generators.loc[gen_name, 'capital_cost']) if 'capital_cost' in network.generators.columns else 0.0
|
|
814
|
+
|
|
815
|
+
# Calculate annualized capital cost for this year
|
|
816
|
+
annual_capital_cost = capacity_mw * capital_cost_per_mw
|
|
817
|
+
|
|
818
|
+
if carrier_name in carrier_stats["capital_cost_by_carrier"]:
|
|
819
|
+
carrier_stats["capital_cost_by_carrier"][carrier_name] += annual_capital_cost
|
|
1067
820
|
|
|
1068
|
-
# 2. STORAGE_UNITS -
|
|
821
|
+
# 2. STORAGE_UNITS - Capital costs
|
|
1069
822
|
if hasattr(network, 'storage_units') and not network.storage_units.empty:
|
|
1070
|
-
# Get storage unit-carrier mapping
|
|
1071
823
|
cursor = conn.execute("""
|
|
1072
824
|
SELECT c.name as component_name, carr.name as carrier_name
|
|
1073
825
|
FROM components c
|
|
1074
826
|
JOIN carriers carr ON c.carrier_id = carr.id
|
|
1075
827
|
WHERE c.network_id = ? AND c.component_type = 'STORAGE_UNIT'
|
|
1076
828
|
""", (network_id,))
|
|
1077
|
-
|
|
1078
829
|
storage_unit_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
1079
830
|
|
|
1080
|
-
# Calculate operational costs (marginal costs for storage units)
|
|
1081
|
-
if hasattr(network, 'storage_units_t') and hasattr(network.storage_units_t, 'p'):
|
|
1082
|
-
for su_name in network.storage_units.index:
|
|
1083
|
-
if su_name in storage_unit_carriers and su_name in network.storage_units_t.p.columns:
|
|
1084
|
-
carrier_name = storage_unit_carriers[su_name]
|
|
1085
|
-
|
|
1086
|
-
# Get marginal cost for this storage unit
|
|
1087
|
-
marginal_cost = 0.0
|
|
1088
|
-
if 'marginal_cost' in network.storage_units.columns:
|
|
1089
|
-
marginal_cost = float(network.storage_units.loc[su_name, 'marginal_cost'])
|
|
1090
|
-
|
|
1091
|
-
# Calculate operational cost = dispatch * marginal_cost (discharge only)
|
|
1092
|
-
weightings = network.snapshot_weightings
|
|
1093
|
-
if isinstance(weightings, pd.DataFrame):
|
|
1094
|
-
if 'objective' in weightings.columns:
|
|
1095
|
-
weighting_values = weightings['objective'].values
|
|
1096
|
-
else:
|
|
1097
|
-
weighting_values = weightings.iloc[:, 0].values
|
|
1098
|
-
else:
|
|
1099
|
-
weighting_values = weightings.values
|
|
1100
|
-
|
|
1101
|
-
su_power = network.storage_units_t.p[su_name]
|
|
1102
|
-
discharge_mwh = float((su_power.clip(lower=0) * weighting_values).sum())
|
|
1103
|
-
operational_cost = discharge_mwh * marginal_cost
|
|
1104
|
-
|
|
1105
|
-
if carrier_name in carrier_stats["operational_cost_by_carrier"]:
|
|
1106
|
-
carrier_stats["operational_cost_by_carrier"][carrier_name] += operational_cost
|
|
1107
|
-
|
|
1108
|
-
# Calculate fixed O&M costs for storage units (matching old solver)
|
|
1109
831
|
for su_name in network.storage_units.index:
|
|
1110
832
|
if su_name in storage_unit_carriers:
|
|
1111
833
|
carrier_name = storage_unit_carriers[su_name]
|
|
1112
834
|
|
|
1113
|
-
#
|
|
1114
|
-
|
|
1115
|
-
if '
|
|
1116
|
-
fixed_cost_per_mw = float(network.storage_units.loc[su_name, 'fixed_cost'])
|
|
835
|
+
# Get build year and lifetime
|
|
836
|
+
build_year = network.storage_units.loc[su_name, 'build_year'] if 'build_year' in network.storage_units.columns else None
|
|
837
|
+
lifetime = network.storage_units.loc[su_name, 'lifetime'] if 'lifetime' in network.storage_units.columns else None
|
|
1117
838
|
|
|
1118
|
-
|
|
1119
|
-
if
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
# Get capital cost for this storage unit
|
|
1135
|
-
capital_cost_per_mw = 0.0
|
|
1136
|
-
if 'capital_cost' in network.storage_units.columns:
|
|
1137
|
-
capital_cost_per_mw = float(network.storage_units.loc[su_name, 'capital_cost'])
|
|
1138
|
-
|
|
1139
|
-
# Get capacity
|
|
1140
|
-
capacity_mw = 0.0
|
|
1141
|
-
if 'p_nom_opt' in network.storage_units.columns:
|
|
1142
|
-
capacity_mw = float(network.storage_units.loc[su_name, 'p_nom_opt'])
|
|
1143
|
-
elif 'p_nom' in network.storage_units.columns:
|
|
1144
|
-
capacity_mw = float(network.storage_units.loc[su_name, 'p_nom'])
|
|
1145
|
-
|
|
1146
|
-
# Annual capital cost for operational assets (undiscounted)
|
|
1147
|
-
annual_capital_cost = capital_cost_per_mw * capacity_mw
|
|
1148
|
-
|
|
1149
|
-
if carrier_name in carrier_stats["capital_cost_by_carrier"]:
|
|
1150
|
-
carrier_stats["capital_cost_by_carrier"][carrier_name] += annual_capital_cost
|
|
839
|
+
# Check if component is active in this year
|
|
840
|
+
if is_component_active(build_year, lifetime, year):
|
|
841
|
+
# Get power capacity and capital cost (per MW)
|
|
842
|
+
if 'p_nom_opt' in network.storage_units.columns:
|
|
843
|
+
capacity_mw = float(network.storage_units.loc[su_name, 'p_nom_opt'])
|
|
844
|
+
else:
|
|
845
|
+
capacity_mw = float(network.storage_units.loc[su_name, 'p_nom']) if 'p_nom' in network.storage_units.columns else 0.0
|
|
846
|
+
|
|
847
|
+
capital_cost_per_mw = float(network.storage_units.loc[su_name, 'capital_cost']) if 'capital_cost' in network.storage_units.columns else 0.0
|
|
848
|
+
|
|
849
|
+
# Calculate annualized capital cost for this year
|
|
850
|
+
annual_capital_cost = capacity_mw * capital_cost_per_mw
|
|
851
|
+
|
|
852
|
+
if carrier_name in carrier_stats["capital_cost_by_carrier"]:
|
|
853
|
+
carrier_stats["capital_cost_by_carrier"][carrier_name] += annual_capital_cost
|
|
1151
854
|
|
|
1152
|
-
# 3. STORES -
|
|
855
|
+
# 3. STORES - Capital costs (per MWh)
|
|
1153
856
|
if hasattr(network, 'stores') and not network.stores.empty:
|
|
1154
|
-
# Get store-carrier mapping
|
|
1155
857
|
cursor = conn.execute("""
|
|
1156
858
|
SELECT c.name as component_name, carr.name as carrier_name
|
|
1157
859
|
FROM components c
|
|
1158
860
|
JOIN carriers carr ON c.carrier_id = carr.id
|
|
1159
861
|
WHERE c.network_id = ? AND c.component_type = 'STORE'
|
|
1160
862
|
""", (network_id,))
|
|
1161
|
-
|
|
1162
863
|
store_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
1163
864
|
|
|
1164
|
-
# Calculate operational costs (marginal costs for stores)
|
|
1165
|
-
if hasattr(network, 'stores_t') and hasattr(network.stores_t, 'p'):
|
|
1166
|
-
for store_name in network.stores.index:
|
|
1167
|
-
if store_name in store_carriers and store_name in network.stores_t.p.columns:
|
|
1168
|
-
carrier_name = store_carriers[store_name]
|
|
1169
|
-
|
|
1170
|
-
# Get marginal cost for this store
|
|
1171
|
-
marginal_cost = 0.0
|
|
1172
|
-
if 'marginal_cost' in network.stores.columns:
|
|
1173
|
-
marginal_cost = float(network.stores.loc[store_name, 'marginal_cost'])
|
|
1174
|
-
|
|
1175
|
-
# Calculate operational cost = dispatch * marginal_cost (discharge only)
|
|
1176
|
-
weightings = network.snapshot_weightings
|
|
1177
|
-
if isinstance(weightings, pd.DataFrame):
|
|
1178
|
-
if 'objective' in weightings.columns:
|
|
1179
|
-
weighting_values = weightings['objective'].values
|
|
1180
|
-
else:
|
|
1181
|
-
weighting_values = weightings.iloc[:, 0].values
|
|
1182
|
-
else:
|
|
1183
|
-
weighting_values = weightings.values
|
|
1184
|
-
|
|
1185
|
-
store_power = network.stores_t.p[store_name]
|
|
1186
|
-
discharge_mwh = float((store_power.clip(lower=0) * weighting_values).sum())
|
|
1187
|
-
operational_cost = discharge_mwh * marginal_cost
|
|
1188
|
-
|
|
1189
|
-
if carrier_name in carrier_stats["operational_cost_by_carrier"]:
|
|
1190
|
-
carrier_stats["operational_cost_by_carrier"][carrier_name] += operational_cost
|
|
1191
|
-
|
|
1192
|
-
# Calculate annual capital costs for all operational stores (based on energy capacity)
|
|
1193
865
|
for store_name in network.stores.index:
|
|
1194
866
|
if store_name in store_carriers:
|
|
1195
867
|
carrier_name = store_carriers[store_name]
|
|
1196
868
|
|
|
1197
|
-
# Get
|
|
1198
|
-
|
|
1199
|
-
if '
|
|
1200
|
-
capital_cost_per_mwh = float(network.stores.loc[store_name, 'capital_cost'])
|
|
869
|
+
# Get build year and lifetime
|
|
870
|
+
build_year = network.stores.loc[store_name, 'build_year'] if 'build_year' in network.stores.columns else None
|
|
871
|
+
lifetime = network.stores.loc[store_name, 'lifetime'] if 'lifetime' in network.stores.columns else None
|
|
1201
872
|
|
|
1202
|
-
#
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
873
|
+
# Check if component is active in this year
|
|
874
|
+
if is_component_active(build_year, lifetime, year):
|
|
875
|
+
# Get energy capacity and capital cost (per MWh)
|
|
876
|
+
if 'e_nom_opt' in network.stores.columns:
|
|
877
|
+
capacity_mwh = float(network.stores.loc[store_name, 'e_nom_opt'])
|
|
878
|
+
else:
|
|
879
|
+
capacity_mwh = float(network.stores.loc[store_name, 'e_nom']) if 'e_nom' in network.stores.columns else 0.0
|
|
880
|
+
|
|
881
|
+
capital_cost_per_mwh = float(network.stores.loc[store_name, 'capital_cost']) if 'capital_cost' in network.stores.columns else 0.0
|
|
882
|
+
|
|
883
|
+
# Calculate annualized capital cost for this year
|
|
884
|
+
annual_capital_cost = capacity_mwh * capital_cost_per_mwh
|
|
885
|
+
|
|
886
|
+
if carrier_name in carrier_stats["capital_cost_by_carrier"]:
|
|
887
|
+
carrier_stats["capital_cost_by_carrier"][carrier_name] += annual_capital_cost
|
|
1214
888
|
|
|
1215
|
-
# 4. LINES - Capital costs
|
|
889
|
+
# 4. LINES - Capital costs (per MVA)
|
|
1216
890
|
if hasattr(network, 'lines') and not network.lines.empty:
|
|
1217
|
-
# Get line-carrier mapping
|
|
1218
891
|
cursor = conn.execute("""
|
|
1219
892
|
SELECT c.name as component_name, carr.name as carrier_name
|
|
1220
893
|
FROM components c
|
|
1221
894
|
JOIN carriers carr ON c.carrier_id = carr.id
|
|
1222
895
|
WHERE c.network_id = ? AND c.component_type = 'LINE'
|
|
1223
896
|
""", (network_id,))
|
|
1224
|
-
|
|
1225
897
|
line_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
1226
898
|
|
|
1227
|
-
# Calculate capital costs for lines (based on s_nom_opt capacity)
|
|
1228
899
|
for line_name in network.lines.index:
|
|
1229
900
|
if line_name in line_carriers:
|
|
1230
901
|
carrier_name = line_carriers[line_name]
|
|
1231
902
|
|
|
1232
|
-
# Get
|
|
1233
|
-
|
|
1234
|
-
if '
|
|
1235
|
-
capital_cost_per_mva = float(network.lines.loc[line_name, 'capital_cost'])
|
|
1236
|
-
|
|
1237
|
-
# Get apparent power capacity (MVA)
|
|
1238
|
-
capacity_mva = 0.0
|
|
1239
|
-
if 's_nom_opt' in network.lines.columns:
|
|
1240
|
-
capacity_mva = float(network.lines.loc[line_name, 's_nom_opt'])
|
|
1241
|
-
elif 's_nom' in network.lines.columns:
|
|
1242
|
-
capacity_mva = float(network.lines.loc[line_name, 's_nom'])
|
|
903
|
+
# Get build year and lifetime
|
|
904
|
+
build_year = network.lines.loc[line_name, 'build_year'] if 'build_year' in network.lines.columns else None
|
|
905
|
+
lifetime = network.lines.loc[line_name, 'lifetime'] if 'lifetime' in network.lines.columns else None
|
|
1243
906
|
|
|
1244
|
-
#
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
907
|
+
# Check if component is active in this year
|
|
908
|
+
if is_component_active(build_year, lifetime, year):
|
|
909
|
+
# Get apparent power capacity and capital cost (per MVA)
|
|
910
|
+
if 's_nom_opt' in network.lines.columns:
|
|
911
|
+
capacity_mva = float(network.lines.loc[line_name, 's_nom_opt'])
|
|
912
|
+
else:
|
|
913
|
+
capacity_mva = float(network.lines.loc[line_name, 's_nom']) if 's_nom' in network.lines.columns else 0.0
|
|
914
|
+
|
|
915
|
+
capital_cost_per_mva = float(network.lines.loc[line_name, 'capital_cost']) if 'capital_cost' in network.lines.columns else 0.0
|
|
916
|
+
|
|
917
|
+
# Calculate annualized capital cost for this year
|
|
918
|
+
annual_capital_cost = capacity_mva * capital_cost_per_mva
|
|
919
|
+
|
|
920
|
+
if carrier_name in carrier_stats["capital_cost_by_carrier"]:
|
|
921
|
+
carrier_stats["capital_cost_by_carrier"][carrier_name] += annual_capital_cost
|
|
1249
922
|
|
|
1250
|
-
# 5. LINKS - Capital
|
|
923
|
+
# 5. LINKS - Capital costs (per MW)
|
|
1251
924
|
if hasattr(network, 'links') and not network.links.empty:
|
|
1252
|
-
# Get link-carrier mapping
|
|
1253
925
|
cursor = conn.execute("""
|
|
1254
926
|
SELECT c.name as component_name, carr.name as carrier_name
|
|
1255
927
|
FROM components c
|
|
1256
928
|
JOIN carriers carr ON c.carrier_id = carr.id
|
|
1257
929
|
WHERE c.network_id = ? AND c.component_type = 'LINK'
|
|
1258
930
|
""", (network_id,))
|
|
1259
|
-
|
|
1260
931
|
link_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
1261
932
|
|
|
1262
|
-
# Calculate operational costs (marginal costs for links)
|
|
1263
|
-
if hasattr(network, 'links_t') and hasattr(network.links_t, 'p0'):
|
|
1264
|
-
for link_name in network.links.index:
|
|
1265
|
-
if link_name in link_carriers and link_name in network.links_t.p0.columns:
|
|
1266
|
-
carrier_name = link_carriers[link_name]
|
|
1267
|
-
|
|
1268
|
-
# Get marginal cost for this link
|
|
1269
|
-
marginal_cost = 0.0
|
|
1270
|
-
if 'marginal_cost' in network.links.columns:
|
|
1271
|
-
marginal_cost = float(network.links.loc[link_name, 'marginal_cost'])
|
|
1272
|
-
|
|
1273
|
-
# Calculate operational cost = flow * marginal_cost (use absolute flow)
|
|
1274
|
-
weightings = network.snapshot_weightings
|
|
1275
|
-
if isinstance(weightings, pd.DataFrame):
|
|
1276
|
-
if 'objective' in weightings.columns:
|
|
1277
|
-
weighting_values = weightings['objective'].values
|
|
1278
|
-
else:
|
|
1279
|
-
weighting_values = weightings.iloc[:, 0].values
|
|
1280
|
-
else:
|
|
1281
|
-
weighting_values = weightings.values
|
|
1282
|
-
|
|
1283
|
-
# Use absolute flow for cost calculation
|
|
1284
|
-
link_flow = abs(network.links_t.p0[link_name])
|
|
1285
|
-
flow_mwh = float((link_flow * weighting_values).sum())
|
|
1286
|
-
operational_cost = flow_mwh * marginal_cost
|
|
1287
|
-
|
|
1288
|
-
if carrier_name in carrier_stats["operational_cost_by_carrier"]:
|
|
1289
|
-
carrier_stats["operational_cost_by_carrier"][carrier_name] += operational_cost
|
|
1290
|
-
|
|
1291
|
-
# Calculate capital costs for links
|
|
1292
933
|
for link_name in network.links.index:
|
|
1293
934
|
if link_name in link_carriers:
|
|
1294
935
|
carrier_name = link_carriers[link_name]
|
|
1295
936
|
|
|
1296
|
-
# Get
|
|
1297
|
-
|
|
1298
|
-
if '
|
|
1299
|
-
capital_cost_per_mw = float(network.links.loc[link_name, 'capital_cost'])
|
|
1300
|
-
|
|
1301
|
-
# Get power capacity (MW)
|
|
1302
|
-
capacity_mw = 0.0
|
|
1303
|
-
if 'p_nom_opt' in network.links.columns:
|
|
1304
|
-
capacity_mw = float(network.links.loc[link_name, 'p_nom_opt'])
|
|
1305
|
-
elif 'p_nom' in network.links.columns:
|
|
1306
|
-
capacity_mw = float(network.links.loc[link_name, 'p_nom'])
|
|
937
|
+
# Get build year and lifetime
|
|
938
|
+
build_year = network.links.loc[link_name, 'build_year'] if 'build_year' in network.links.columns else None
|
|
939
|
+
lifetime = network.links.loc[link_name, 'lifetime'] if 'lifetime' in network.links.columns else None
|
|
1307
940
|
|
|
1308
|
-
#
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
logger.info(f"Total energy capacity: {sum(carrier_stats['energy_capacity_by_carrier'].values()):.2f} MWh")
|
|
1324
|
-
logger.info(f"Total emissions: {sum(carrier_stats['emissions_by_carrier'].values()):.2f} tCO2")
|
|
1325
|
-
logger.info(f"Total capital cost: {sum(carrier_stats['capital_cost_by_carrier'].values()):.2f} USD")
|
|
1326
|
-
logger.info(f"Total operational cost: {sum(carrier_stats['operational_cost_by_carrier'].values()):.2f} USD")
|
|
1327
|
-
logger.info(f"Total system cost: {sum(carrier_stats['total_system_cost_by_carrier'].values()):.2f} USD")
|
|
1328
|
-
|
|
1329
|
-
return carrier_stats
|
|
1330
|
-
|
|
1331
|
-
except Exception as e:
|
|
1332
|
-
logger.error(f"Failed to calculate carrier statistics: {e}", exc_info=True)
|
|
1333
|
-
return {
|
|
1334
|
-
"dispatch_by_carrier": {},
|
|
1335
|
-
"power_capacity_by_carrier": {},
|
|
1336
|
-
"energy_capacity_by_carrier": {},
|
|
1337
|
-
"emissions_by_carrier": {},
|
|
1338
|
-
"capital_cost_by_carrier": {},
|
|
1339
|
-
"operational_cost_by_carrier": {},
|
|
1340
|
-
"total_system_cost_by_carrier": {}
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
def _store_year_based_statistics(
|
|
1344
|
-
self,
|
|
1345
|
-
conn,
|
|
1346
|
-
network_id: int,
|
|
1347
|
-
network: 'pypsa.Network',
|
|
1348
|
-
year_statistics: Dict[int, Dict[str, Any]],
|
|
1349
|
-
scenario_id: Optional[int]
|
|
1350
|
-
) -> int:
|
|
1351
|
-
"""Store year-based statistics to database"""
|
|
1352
|
-
try:
|
|
1353
|
-
import json
|
|
1354
|
-
stored_count = 0
|
|
1355
|
-
|
|
1356
|
-
# Use master scenario if no scenario specified
|
|
1357
|
-
if scenario_id is None:
|
|
1358
|
-
from pyconvexity.models import get_master_scenario_id
|
|
1359
|
-
scenario_id = get_master_scenario_id(conn, network_id)
|
|
1360
|
-
|
|
1361
|
-
# Check if network_solve_results_by_year table exists, create if not
|
|
1362
|
-
conn.execute("""
|
|
1363
|
-
CREATE TABLE IF NOT EXISTS network_solve_results_by_year (
|
|
1364
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1365
|
-
network_id INTEGER NOT NULL,
|
|
1366
|
-
scenario_id INTEGER NOT NULL,
|
|
1367
|
-
year INTEGER NOT NULL,
|
|
1368
|
-
results_json TEXT,
|
|
1369
|
-
metadata_json TEXT,
|
|
1370
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
1371
|
-
FOREIGN KEY (network_id) REFERENCES networks(id),
|
|
1372
|
-
FOREIGN KEY (scenario_id) REFERENCES scenarios(id),
|
|
1373
|
-
UNIQUE(network_id, scenario_id, year)
|
|
1374
|
-
)
|
|
1375
|
-
""")
|
|
1376
|
-
|
|
1377
|
-
for year, stats in year_statistics.items():
|
|
1378
|
-
try:
|
|
1379
|
-
# Calculate proper year-specific carrier statistics
|
|
1380
|
-
year_carrier_stats = self._calculate_year_carrier_statistics(conn, network_id, network, year)
|
|
1381
|
-
|
|
1382
|
-
# Merge year-specific carrier stats into the statistics
|
|
1383
|
-
if "custom_statistics" in stats:
|
|
1384
|
-
stats["custom_statistics"].update(year_carrier_stats)
|
|
1385
|
-
else:
|
|
1386
|
-
stats["custom_statistics"] = year_carrier_stats
|
|
1387
|
-
|
|
1388
|
-
# Wrap the year statistics in the same structure as overall results for consistency
|
|
1389
|
-
year_result_wrapper = {
|
|
1390
|
-
"success": True,
|
|
1391
|
-
"year": year,
|
|
1392
|
-
"network_statistics": stats
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
metadata = {
|
|
1396
|
-
"year": year,
|
|
1397
|
-
"network_id": network_id,
|
|
1398
|
-
"scenario_id": scenario_id
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
conn.execute("""
|
|
1402
|
-
INSERT OR REPLACE INTO network_solve_results_by_year
|
|
1403
|
-
(network_id, scenario_id, year, results_json, metadata_json)
|
|
1404
|
-
VALUES (?, ?, ?, ?, ?)
|
|
1405
|
-
""", (
|
|
1406
|
-
network_id,
|
|
1407
|
-
scenario_id,
|
|
1408
|
-
year,
|
|
1409
|
-
json.dumps(year_result_wrapper, default=self._json_serializer),
|
|
1410
|
-
json.dumps(metadata, default=self._json_serializer)
|
|
1411
|
-
))
|
|
1412
|
-
|
|
1413
|
-
stored_count += 1
|
|
1414
|
-
logger.info(f"Stored year-based statistics for year {year}")
|
|
1415
|
-
|
|
1416
|
-
except Exception as e:
|
|
1417
|
-
logger.error(f"Failed to store statistics for year {year}: {e}")
|
|
1418
|
-
continue
|
|
1419
|
-
|
|
1420
|
-
logger.info(f"Successfully stored year-based statistics for {stored_count} years")
|
|
1421
|
-
return stored_count
|
|
1422
|
-
|
|
1423
|
-
except Exception as e:
|
|
1424
|
-
logger.error(f"Failed to store year-based statistics: {e}", exc_info=True)
|
|
1425
|
-
return 0
|
|
1426
|
-
|
|
1427
|
-
def _calculate_year_carrier_statistics(self, conn, network_id: int, network: 'pypsa.Network', year: int) -> Dict[str, Any]:
|
|
1428
|
-
"""
|
|
1429
|
-
Calculate carrier-specific statistics for a specific year with proper database access.
|
|
1430
|
-
|
|
1431
|
-
CRITICAL: This method now consistently applies snapshot weightings to ALL energy calculations
|
|
1432
|
-
to convert MW to MWh, matching the old PyPSA solver behavior. This is essential for
|
|
1433
|
-
multi-hourly models (e.g., 3-hourly models where each timestep = 3 hours).
|
|
1434
|
-
"""
|
|
1435
|
-
try:
|
|
1436
|
-
# Initialize carrier statistics (separate power and energy capacity like old solver)
|
|
1437
|
-
carrier_stats = {
|
|
1438
|
-
"dispatch_by_carrier": {},
|
|
1439
|
-
"power_capacity_by_carrier": {}, # MW - Generators + Storage Units (power)
|
|
1440
|
-
"energy_capacity_by_carrier": {}, # MWh - Stores + Storage Units (energy)
|
|
1441
|
-
"emissions_by_carrier": {},
|
|
1442
|
-
"capital_cost_by_carrier": {},
|
|
1443
|
-
"operational_cost_by_carrier": {},
|
|
1444
|
-
"total_system_cost_by_carrier": {}
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
# Get all carriers from database
|
|
1448
|
-
cursor = conn.execute("""
|
|
1449
|
-
SELECT DISTINCT name FROM carriers WHERE network_id = ?
|
|
1450
|
-
""", (network_id,))
|
|
1451
|
-
all_carriers = [row[0] for row in cursor.fetchall()]
|
|
1452
|
-
|
|
1453
|
-
# Initialize all carriers with zero values
|
|
1454
|
-
for carrier in all_carriers:
|
|
1455
|
-
carrier_stats["dispatch_by_carrier"][carrier] = 0.0
|
|
1456
|
-
carrier_stats["power_capacity_by_carrier"][carrier] = 0.0
|
|
1457
|
-
carrier_stats["energy_capacity_by_carrier"][carrier] = 0.0
|
|
1458
|
-
carrier_stats["emissions_by_carrier"][carrier] = 0.0
|
|
1459
|
-
carrier_stats["capital_cost_by_carrier"][carrier] = 0.0
|
|
1460
|
-
carrier_stats["operational_cost_by_carrier"][carrier] = 0.0
|
|
1461
|
-
carrier_stats["total_system_cost_by_carrier"][carrier] = 0.0
|
|
1462
|
-
|
|
1463
|
-
# Get generator-carrier mapping from database
|
|
1464
|
-
cursor = conn.execute("""
|
|
1465
|
-
SELECT c.name as component_name, carr.name as carrier_name
|
|
1466
|
-
FROM components c
|
|
1467
|
-
JOIN carriers carr ON c.carrier_id = carr.id
|
|
1468
|
-
WHERE c.network_id = ? AND c.component_type = 'GENERATOR'
|
|
1469
|
-
""", (network_id,))
|
|
1470
|
-
generator_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
941
|
+
# Check if component is active in this year
|
|
942
|
+
if is_component_active(build_year, lifetime, year):
|
|
943
|
+
# Get power capacity and capital cost (per MW)
|
|
944
|
+
if 'p_nom_opt' in network.links.columns:
|
|
945
|
+
capacity_mw = float(network.links.loc[link_name, 'p_nom_opt'])
|
|
946
|
+
else:
|
|
947
|
+
capacity_mw = float(network.links.loc[link_name, 'p_nom']) if 'p_nom' in network.links.columns else 0.0
|
|
948
|
+
|
|
949
|
+
capital_cost_per_mw = float(network.links.loc[link_name, 'capital_cost']) if 'capital_cost' in network.links.columns else 0.0
|
|
950
|
+
|
|
951
|
+
# Calculate annualized capital cost for this year
|
|
952
|
+
annual_capital_cost = capacity_mw * capital_cost_per_mw
|
|
953
|
+
|
|
954
|
+
if carrier_name in carrier_stats["capital_cost_by_carrier"]:
|
|
955
|
+
carrier_stats["capital_cost_by_carrier"][carrier_name] += annual_capital_cost
|
|
1471
956
|
|
|
1472
|
-
# Calculate
|
|
957
|
+
# Calculate operational costs by carrier for this specific year
|
|
958
|
+
# Operational costs = dispatch (MWh) × marginal_cost (currency/MWh)
|
|
959
|
+
# Only for components that are active in this year
|
|
1473
960
|
|
|
1474
|
-
# 1. GENERATORS -
|
|
961
|
+
# 1. GENERATORS - Operational costs (including UNMET_LOAD)
|
|
1475
962
|
if hasattr(network, 'generators_t') and hasattr(network.generators_t, 'p'):
|
|
963
|
+
# Get generator info: carrier, marginal_cost, build_year, lifetime (include UNMET_LOAD)
|
|
964
|
+
cursor = conn.execute("""
|
|
965
|
+
SELECT c.name as component_name,
|
|
966
|
+
CASE
|
|
967
|
+
WHEN c.component_type = 'UNMET_LOAD' THEN 'Unmet Load'
|
|
968
|
+
ELSE carr.name
|
|
969
|
+
END as carrier_name
|
|
970
|
+
FROM components c
|
|
971
|
+
JOIN carriers carr ON c.carrier_id = carr.id
|
|
972
|
+
WHERE c.network_id = ? AND c.component_type IN ('GENERATOR', 'UNMET_LOAD')
|
|
973
|
+
""", (network_id,))
|
|
974
|
+
generator_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
975
|
+
|
|
1476
976
|
# Filter generation data for this specific year
|
|
1477
977
|
year_generation = self._filter_timeseries_by_year(network.generators_t.p, network.snapshots, year)
|
|
1478
978
|
if year_generation is not None and not year_generation.empty:
|
|
1479
979
|
for gen_name in year_generation.columns:
|
|
1480
980
|
if gen_name in generator_carriers:
|
|
1481
981
|
carrier_name = generator_carriers[gen_name]
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
if
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
weighting_values = weightings.iloc[:, 0].values
|
|
1494
|
-
else:
|
|
1495
|
-
weighting_values = weightings.values
|
|
1496
|
-
# Apply weightings to the filtered year data
|
|
1497
|
-
if len(weighting_values) == len(year_generation):
|
|
1498
|
-
generation_mwh = float((year_generation[gen_name].values * weighting_values).sum())
|
|
982
|
+
|
|
983
|
+
# Get build year and lifetime
|
|
984
|
+
build_year = network.generators.loc[gen_name, 'build_year'] if 'build_year' in network.generators.columns else None
|
|
985
|
+
lifetime = network.generators.loc[gen_name, 'lifetime'] if 'lifetime' in network.generators.columns else None
|
|
986
|
+
|
|
987
|
+
# Check if component is active in this year
|
|
988
|
+
if is_component_active(build_year, lifetime, year):
|
|
989
|
+
# Calculate generation for this year (already calculated above, but need to recalculate for operational costs)
|
|
990
|
+
year_weightings = self._get_year_weightings(network, year)
|
|
991
|
+
if year_weightings is not None:
|
|
992
|
+
generation_mwh = float((year_generation[gen_name].values * year_weightings).sum())
|
|
1499
993
|
else:
|
|
1500
|
-
# Last resort: simple sum (will be incorrect for non-1H models)
|
|
1501
994
|
generation_mwh = float(year_generation[gen_name].sum())
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
995
|
+
|
|
996
|
+
# Get marginal cost
|
|
997
|
+
marginal_cost = float(network.generators.loc[gen_name, 'marginal_cost']) if 'marginal_cost' in network.generators.columns else 0.0
|
|
998
|
+
|
|
999
|
+
# Calculate operational cost for this year
|
|
1000
|
+
operational_cost = generation_mwh * marginal_cost
|
|
1001
|
+
|
|
1002
|
+
if carrier_name in carrier_stats["operational_cost_by_carrier"]:
|
|
1003
|
+
carrier_stats["operational_cost_by_carrier"][carrier_name] += operational_cost
|
|
1506
1004
|
|
|
1507
|
-
# 2. STORAGE_UNITS -
|
|
1005
|
+
# 2. STORAGE_UNITS - Operational costs (discharge only)
|
|
1508
1006
|
if hasattr(network, 'storage_units_t') and hasattr(network.storage_units_t, 'p'):
|
|
1509
|
-
# Get storage unit
|
|
1007
|
+
# Get storage unit info: carrier, marginal_cost, build_year, lifetime
|
|
1510
1008
|
cursor = conn.execute("""
|
|
1511
1009
|
SELECT c.name as component_name, carr.name as carrier_name
|
|
1512
1010
|
FROM components c
|
|
@@ -1521,33 +1019,32 @@ class ResultStorage:
|
|
|
1521
1019
|
for su_name in year_storage.columns:
|
|
1522
1020
|
if su_name in storage_unit_carriers:
|
|
1523
1021
|
carrier_name = storage_unit_carriers[su_name]
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
if
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
weighting_values = weightings.iloc[:, 0].values
|
|
1536
|
-
else:
|
|
1537
|
-
weighting_values = weightings.values
|
|
1538
|
-
# Apply weightings to the filtered year data
|
|
1539
|
-
if len(weighting_values) == len(year_storage):
|
|
1540
|
-
discharge_mwh = float((year_storage[su_name].clip(lower=0).values * weighting_values).sum())
|
|
1022
|
+
|
|
1023
|
+
# Get build year and lifetime
|
|
1024
|
+
build_year = network.storage_units.loc[su_name, 'build_year'] if 'build_year' in network.storage_units.columns else None
|
|
1025
|
+
lifetime = network.storage_units.loc[su_name, 'lifetime'] if 'lifetime' in network.storage_units.columns else None
|
|
1026
|
+
|
|
1027
|
+
# Check if component is active in this year
|
|
1028
|
+
if is_component_active(build_year, lifetime, year):
|
|
1029
|
+
# Calculate discharge for this year (positive values only)
|
|
1030
|
+
year_weightings = self._get_year_weightings(network, year)
|
|
1031
|
+
if year_weightings is not None:
|
|
1032
|
+
discharge_mwh = float((year_storage[su_name].clip(lower=0).values * year_weightings).sum())
|
|
1541
1033
|
else:
|
|
1542
1034
|
discharge_mwh = float(year_storage[su_name].clip(lower=0).sum())
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1035
|
+
|
|
1036
|
+
# Get marginal cost
|
|
1037
|
+
marginal_cost = float(network.storage_units.loc[su_name, 'marginal_cost']) if 'marginal_cost' in network.storage_units.columns else 0.0
|
|
1038
|
+
|
|
1039
|
+
# Calculate operational cost for this year
|
|
1040
|
+
operational_cost = discharge_mwh * marginal_cost
|
|
1041
|
+
|
|
1042
|
+
if carrier_name in carrier_stats["operational_cost_by_carrier"]:
|
|
1043
|
+
carrier_stats["operational_cost_by_carrier"][carrier_name] += operational_cost
|
|
1547
1044
|
|
|
1548
|
-
# 3. STORES -
|
|
1045
|
+
# 3. STORES - Operational costs (discharge only)
|
|
1549
1046
|
if hasattr(network, 'stores_t') and hasattr(network.stores_t, 'p'):
|
|
1550
|
-
# Get store
|
|
1047
|
+
# Get store info: carrier, marginal_cost, build_year, lifetime
|
|
1551
1048
|
cursor = conn.execute("""
|
|
1552
1049
|
SELECT c.name as component_name, carr.name as carrier_name
|
|
1553
1050
|
FROM components c
|
|
@@ -1562,34 +1059,56 @@ class ResultStorage:
|
|
|
1562
1059
|
for store_name in year_stores.columns:
|
|
1563
1060
|
if store_name in store_carriers:
|
|
1564
1061
|
carrier_name = store_carriers[store_name]
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
if
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
weighting_values = weightings.iloc[:, 0].values
|
|
1577
|
-
else:
|
|
1578
|
-
weighting_values = weightings.values
|
|
1579
|
-
# Apply weightings to the filtered year data
|
|
1580
|
-
if len(weighting_values) == len(year_stores):
|
|
1581
|
-
discharge_mwh = float((year_stores[store_name].clip(lower=0).values * weighting_values).sum())
|
|
1062
|
+
|
|
1063
|
+
# Get build year and lifetime
|
|
1064
|
+
build_year = network.stores.loc[store_name, 'build_year'] if 'build_year' in network.stores.columns else None
|
|
1065
|
+
lifetime = network.stores.loc[store_name, 'lifetime'] if 'lifetime' in network.stores.columns else None
|
|
1066
|
+
|
|
1067
|
+
# Check if component is active in this year
|
|
1068
|
+
if is_component_active(build_year, lifetime, year):
|
|
1069
|
+
# Calculate discharge for this year (positive values only)
|
|
1070
|
+
year_weightings = self._get_year_weightings(network, year)
|
|
1071
|
+
if year_weightings is not None:
|
|
1072
|
+
discharge_mwh = float((year_stores[store_name].clip(lower=0).values * year_weightings).sum())
|
|
1582
1073
|
else:
|
|
1583
1074
|
discharge_mwh = float(year_stores[store_name].clip(lower=0).sum())
|
|
1584
|
-
|
|
1075
|
+
|
|
1076
|
+
# Get marginal cost
|
|
1077
|
+
marginal_cost = float(network.stores.loc[store_name, 'marginal_cost']) if 'marginal_cost' in network.stores.columns else 0.0
|
|
1078
|
+
|
|
1079
|
+
# Calculate operational cost for this year
|
|
1080
|
+
operational_cost = discharge_mwh * marginal_cost
|
|
1585
1081
|
|
|
1586
|
-
if carrier_name in carrier_stats["
|
|
1587
|
-
carrier_stats["
|
|
1082
|
+
if carrier_name in carrier_stats["operational_cost_by_carrier"]:
|
|
1083
|
+
carrier_stats["operational_cost_by_carrier"][carrier_name] += operational_cost
|
|
1084
|
+
|
|
1085
|
+
# Calculate total system costs by carrier for this specific year
|
|
1086
|
+
# Total system cost = capital cost + operational cost
|
|
1087
|
+
for carrier_name in carrier_stats["capital_cost_by_carrier"]:
|
|
1088
|
+
capital_cost = carrier_stats["capital_cost_by_carrier"][carrier_name]
|
|
1089
|
+
operational_cost = carrier_stats["operational_cost_by_carrier"][carrier_name]
|
|
1090
|
+
total_system_cost = capital_cost + operational_cost
|
|
1091
|
+
|
|
1092
|
+
if carrier_name in carrier_stats["total_system_cost_by_carrier"]:
|
|
1093
|
+
carrier_stats["total_system_cost_by_carrier"][carrier_name] = total_system_cost
|
|
1588
1094
|
|
|
1589
|
-
# Calculate
|
|
1095
|
+
# Calculate capacity by carrier for this specific year
|
|
1590
1096
|
|
|
1591
|
-
#
|
|
1097
|
+
# 4. GENERATORS - Power capacity (MW) (including UNMET_LOAD)
|
|
1592
1098
|
if hasattr(network, 'generators') and not network.generators.empty:
|
|
1099
|
+
# Get generator-carrier mapping (include UNMET_LOAD)
|
|
1100
|
+
cursor = conn.execute("""
|
|
1101
|
+
SELECT c.name as component_name,
|
|
1102
|
+
CASE
|
|
1103
|
+
WHEN c.component_type = 'UNMET_LOAD' THEN 'Unmet Load'
|
|
1104
|
+
ELSE carr.name
|
|
1105
|
+
END as carrier_name
|
|
1106
|
+
FROM components c
|
|
1107
|
+
JOIN carriers carr ON c.carrier_id = carr.id
|
|
1108
|
+
WHERE c.network_id = ? AND c.component_type IN ('GENERATOR', 'UNMET_LOAD')
|
|
1109
|
+
""", (network_id,))
|
|
1110
|
+
generator_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
1111
|
+
|
|
1593
1112
|
for gen_name in network.generators.index:
|
|
1594
1113
|
if gen_name in generator_carriers:
|
|
1595
1114
|
carrier_name = generator_carriers[gen_name]
|
|
@@ -1610,8 +1129,8 @@ class ResultStorage:
|
|
|
1610
1129
|
|
|
1611
1130
|
if carrier_name in carrier_stats["power_capacity_by_carrier"]:
|
|
1612
1131
|
carrier_stats["power_capacity_by_carrier"][carrier_name] += capacity_mw
|
|
1613
|
-
|
|
1614
|
-
# 2. STORAGE_UNITS -
|
|
1132
|
+
|
|
1133
|
+
# 2. STORAGE_UNITS - Power capacity (MW) + Energy capacity (MWh)
|
|
1615
1134
|
if hasattr(network, 'storage_units') and not network.storage_units.empty:
|
|
1616
1135
|
# Get storage unit-carrier mapping
|
|
1617
1136
|
cursor = conn.execute("""
|
|
@@ -1634,16 +1153,25 @@ class ResultStorage:
|
|
|
1634
1153
|
is_available = False
|
|
1635
1154
|
|
|
1636
1155
|
if is_available:
|
|
1637
|
-
#
|
|
1156
|
+
# Power capacity (MW)
|
|
1638
1157
|
if 'p_nom_opt' in network.storage_units.columns:
|
|
1639
|
-
|
|
1158
|
+
p_nom_opt = float(network.storage_units.loc[su_name, 'p_nom_opt'])
|
|
1640
1159
|
else:
|
|
1641
|
-
|
|
1160
|
+
p_nom_opt = float(network.storage_units.loc[su_name, 'p_nom']) if 'p_nom' in network.storage_units.columns else 0.0
|
|
1642
1161
|
|
|
1643
1162
|
if carrier_name in carrier_stats["power_capacity_by_carrier"]:
|
|
1644
|
-
carrier_stats["power_capacity_by_carrier"][carrier_name] +=
|
|
1163
|
+
carrier_stats["power_capacity_by_carrier"][carrier_name] += p_nom_opt
|
|
1164
|
+
|
|
1165
|
+
# Energy capacity (MWh) using max_hours
|
|
1166
|
+
max_hours = 1.0 # Default
|
|
1167
|
+
if 'max_hours' in network.storage_units.columns:
|
|
1168
|
+
max_hours = float(network.storage_units.loc[su_name, 'max_hours'])
|
|
1169
|
+
energy_capacity_mwh = p_nom_opt * max_hours
|
|
1170
|
+
|
|
1171
|
+
if carrier_name in carrier_stats["energy_capacity_by_carrier"]:
|
|
1172
|
+
carrier_stats["energy_capacity_by_carrier"][carrier_name] += energy_capacity_mwh
|
|
1645
1173
|
|
|
1646
|
-
# 3. STORES -
|
|
1174
|
+
# 3. STORES - Energy capacity (MWh) only
|
|
1647
1175
|
if hasattr(network, 'stores') and not network.stores.empty:
|
|
1648
1176
|
# Get store-carrier mapping
|
|
1649
1177
|
cursor = conn.execute("""
|
|
@@ -1666,17 +1194,16 @@ class ResultStorage:
|
|
|
1666
1194
|
is_available = False
|
|
1667
1195
|
|
|
1668
1196
|
if is_available:
|
|
1669
|
-
#
|
|
1197
|
+
# Energy capacity (MWh)
|
|
1670
1198
|
if 'e_nom_opt' in network.stores.columns:
|
|
1671
1199
|
capacity_mwh = float(network.stores.loc[store_name, 'e_nom_opt'])
|
|
1672
1200
|
else:
|
|
1673
1201
|
capacity_mwh = float(network.stores.loc[store_name, 'e_nom']) if 'e_nom' in network.stores.columns else 0.0
|
|
1674
1202
|
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
# 4. LINES - Year-specific apparent power capacity
|
|
1203
|
+
if carrier_name in carrier_stats["energy_capacity_by_carrier"]:
|
|
1204
|
+
carrier_stats["energy_capacity_by_carrier"][carrier_name] += capacity_mwh
|
|
1205
|
+
|
|
1206
|
+
# 4. LINES - Apparent power capacity (MVA -> MW)
|
|
1680
1207
|
if hasattr(network, 'lines') and not network.lines.empty:
|
|
1681
1208
|
# Get line-carrier mapping
|
|
1682
1209
|
cursor = conn.execute("""
|
|
@@ -1699,19 +1226,18 @@ class ResultStorage:
|
|
|
1699
1226
|
is_available = False
|
|
1700
1227
|
|
|
1701
1228
|
if is_available:
|
|
1702
|
-
#
|
|
1229
|
+
# Apparent power capacity (MVA -> MW, assume power factor = 1)
|
|
1703
1230
|
if 's_nom_opt' in network.lines.columns:
|
|
1704
1231
|
capacity_mva = float(network.lines.loc[line_name, 's_nom_opt'])
|
|
1705
1232
|
else:
|
|
1706
1233
|
capacity_mva = float(network.lines.loc[line_name, 's_nom']) if 's_nom' in network.lines.columns else 0.0
|
|
1707
1234
|
|
|
1708
|
-
# Convert MVA to MW
|
|
1709
|
-
capacity_mw = capacity_mva
|
|
1235
|
+
capacity_mw = capacity_mva # Convert MVA to MW
|
|
1710
1236
|
|
|
1711
1237
|
if carrier_name in carrier_stats["power_capacity_by_carrier"]:
|
|
1712
1238
|
carrier_stats["power_capacity_by_carrier"][carrier_name] += capacity_mw
|
|
1713
1239
|
|
|
1714
|
-
# 5. LINKS -
|
|
1240
|
+
# 5. LINKS - Power capacity (MW)
|
|
1715
1241
|
if hasattr(network, 'links') and not network.links.empty:
|
|
1716
1242
|
# Get link-carrier mapping
|
|
1717
1243
|
cursor = conn.execute("""
|
|
@@ -1734,7 +1260,7 @@ class ResultStorage:
|
|
|
1734
1260
|
is_available = False
|
|
1735
1261
|
|
|
1736
1262
|
if is_available:
|
|
1737
|
-
#
|
|
1263
|
+
# Power capacity (MW)
|
|
1738
1264
|
if 'p_nom_opt' in network.links.columns:
|
|
1739
1265
|
capacity_mw = float(network.links.loc[link_name, 'p_nom_opt'])
|
|
1740
1266
|
else:
|
|
@@ -1743,376 +1269,158 @@ class ResultStorage:
|
|
|
1743
1269
|
if carrier_name in carrier_stats["power_capacity_by_carrier"]:
|
|
1744
1270
|
carrier_stats["power_capacity_by_carrier"][carrier_name] += capacity_mw
|
|
1745
1271
|
|
|
1746
|
-
|
|
1272
|
+
logger.info(f"Calculated year {year} carrier statistics:")
|
|
1273
|
+
logger.info(f" Dispatch: {sum(carrier_stats['dispatch_by_carrier'].values()):.2f} MWh")
|
|
1274
|
+
logger.info(f" Emissions: {sum(carrier_stats['emissions_by_carrier'].values()):.2f} tonnes CO2")
|
|
1275
|
+
logger.info(f" Capital cost: {sum(carrier_stats['capital_cost_by_carrier'].values()):.2f} USD")
|
|
1276
|
+
logger.info(f" Operational cost: {sum(carrier_stats['operational_cost_by_carrier'].values()):.2f} USD")
|
|
1277
|
+
logger.info(f" Total system cost: {sum(carrier_stats['total_system_cost_by_carrier'].values()):.2f} USD")
|
|
1278
|
+
logger.info(f" Power capacity: {sum(carrier_stats['power_capacity_by_carrier'].values()):.2f} MW")
|
|
1279
|
+
logger.info(f" Energy capacity: {sum(carrier_stats['energy_capacity_by_carrier'].values()):.2f} MWh")
|
|
1280
|
+
|
|
1281
|
+
return carrier_stats
|
|
1282
|
+
|
|
1283
|
+
except Exception as e:
|
|
1284
|
+
logger.error(f"Failed to calculate year {year} carrier statistics: {e}", exc_info=True)
|
|
1285
|
+
return {
|
|
1286
|
+
"dispatch_by_carrier": {},
|
|
1287
|
+
"power_capacity_by_carrier": {},
|
|
1288
|
+
"energy_capacity_by_carrier": {},
|
|
1289
|
+
"emissions_by_carrier": {},
|
|
1290
|
+
"capital_cost_by_carrier": {},
|
|
1291
|
+
"operational_cost_by_carrier": {},
|
|
1292
|
+
"total_system_cost_by_carrier": {}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
def _sum_year_based_carrier_statistics(self, conn, network_id: int) -> Dict[str, Any]:
|
|
1296
|
+
"""
|
|
1297
|
+
Sum up per-year carrier statistics for accurate multi-year totals.
|
|
1298
|
+
For capacity: take the LAST YEAR (final capacity) instead of maximum.
|
|
1299
|
+
"""
|
|
1300
|
+
try:
|
|
1301
|
+
import json
|
|
1302
|
+
|
|
1303
|
+
# Initialize totals
|
|
1304
|
+
totals = {
|
|
1305
|
+
"dispatch_by_carrier": {},
|
|
1306
|
+
"power_capacity_by_carrier": {},
|
|
1307
|
+
"energy_capacity_by_carrier": {},
|
|
1308
|
+
"emissions_by_carrier": {},
|
|
1309
|
+
"capital_cost_by_carrier": {},
|
|
1310
|
+
"operational_cost_by_carrier": {},
|
|
1311
|
+
"total_system_cost_by_carrier": {}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
# Get all carriers from database
|
|
1747
1315
|
cursor = conn.execute("""
|
|
1748
|
-
SELECT name
|
|
1749
|
-
FROM carriers
|
|
1750
|
-
WHERE network_id = ? AND co2_emissions IS NOT NULL
|
|
1751
|
-
ORDER BY name
|
|
1316
|
+
SELECT DISTINCT name FROM carriers WHERE network_id = ?
|
|
1752
1317
|
""", (network_id,))
|
|
1318
|
+
all_carriers = [row[0] for row in cursor.fetchall()]
|
|
1753
1319
|
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1320
|
+
# Initialize all carriers with zero values (including special "Unmet Load" carrier)
|
|
1321
|
+
all_carriers_with_unmet = all_carriers + ['Unmet Load']
|
|
1322
|
+
for carrier in all_carriers_with_unmet:
|
|
1323
|
+
totals["dispatch_by_carrier"][carrier] = 0.0
|
|
1324
|
+
totals["power_capacity_by_carrier"][carrier] = 0.0
|
|
1325
|
+
totals["energy_capacity_by_carrier"][carrier] = 0.0
|
|
1326
|
+
totals["emissions_by_carrier"][carrier] = 0.0
|
|
1327
|
+
totals["capital_cost_by_carrier"][carrier] = 0.0
|
|
1328
|
+
totals["operational_cost_by_carrier"][carrier] = 0.0
|
|
1329
|
+
totals["total_system_cost_by_carrier"][carrier] = 0.0
|
|
1330
|
+
|
|
1331
|
+
# Get all year-based results, ordered by year
|
|
1332
|
+
cursor = conn.execute("""
|
|
1333
|
+
SELECT year, results_json FROM network_solve_results_by_year
|
|
1334
|
+
WHERE network_id = ?
|
|
1335
|
+
ORDER BY year
|
|
1336
|
+
""", (network_id,))
|
|
1764
1337
|
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
# but operational costs are incurred when generating
|
|
1338
|
+
year_results = cursor.fetchall()
|
|
1339
|
+
logger.info(f"Found {len(year_results)} year-based results to sum for network {network_id}")
|
|
1768
1340
|
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
if gen_name in generator_carriers:
|
|
1773
|
-
carrier_name = generator_carriers[gen_name]
|
|
1774
|
-
|
|
1775
|
-
# Operational costs = year_dispatch * marginal_cost
|
|
1776
|
-
if 'marginal_cost' in network.generators.columns:
|
|
1777
|
-
year_dispatch = 0.0
|
|
1778
|
-
if hasattr(network, 'generators_t') and hasattr(network.generators_t, 'p'):
|
|
1779
|
-
year_generation = self._filter_timeseries_by_year(network.generators_t.p, network.snapshots, year)
|
|
1780
|
-
if year_generation is not None and gen_name in year_generation.columns:
|
|
1781
|
-
year_weightings = self._get_year_weightings(network, year)
|
|
1782
|
-
if year_weightings is not None:
|
|
1783
|
-
year_dispatch = float((year_generation[gen_name].values * year_weightings).sum())
|
|
1784
|
-
else:
|
|
1785
|
-
# Fallback: use all-year weightings if year-specific not available
|
|
1786
|
-
weightings = network.snapshot_weightings
|
|
1787
|
-
if isinstance(weightings, pd.DataFrame):
|
|
1788
|
-
if 'objective' in weightings.columns:
|
|
1789
|
-
weighting_values = weightings['objective'].values
|
|
1790
|
-
else:
|
|
1791
|
-
weighting_values = weightings.iloc[:, 0].values
|
|
1792
|
-
else:
|
|
1793
|
-
weighting_values = weightings.values
|
|
1794
|
-
# Apply weightings to the filtered year data
|
|
1795
|
-
if len(weighting_values) == len(year_generation):
|
|
1796
|
-
year_dispatch = float((year_generation[gen_name].values * weighting_values).sum())
|
|
1797
|
-
else:
|
|
1798
|
-
year_dispatch = float(year_generation[gen_name].sum())
|
|
1799
|
-
logger.warning(f"Could not apply snapshot weightings for operational cost calc of {gen_name} in year {year} - cost may be incorrect")
|
|
1800
|
-
|
|
1801
|
-
marginal_cost = float(network.generators.loc[gen_name, 'marginal_cost'])
|
|
1802
|
-
operational_cost = year_dispatch * marginal_cost
|
|
1803
|
-
|
|
1804
|
-
if carrier_name in carrier_stats["operational_cost_by_carrier"]:
|
|
1805
|
-
carrier_stats["operational_cost_by_carrier"][carrier_name] += operational_cost
|
|
1806
|
-
|
|
1807
|
-
# Capital costs - include if asset is operational in this year (matching old solver)
|
|
1808
|
-
if 'capital_cost' in network.generators.columns:
|
|
1809
|
-
# Check if this generator is operational in this year
|
|
1810
|
-
is_operational = True
|
|
1811
|
-
if 'build_year' in network.generators.columns:
|
|
1812
|
-
build_year = network.generators.loc[gen_name, 'build_year']
|
|
1813
|
-
if pd.notna(build_year) and int(build_year) > year:
|
|
1814
|
-
is_operational = False # Not built yet
|
|
1815
|
-
|
|
1816
|
-
if is_operational:
|
|
1817
|
-
capital_cost_per_mw = float(network.generators.loc[gen_name, 'capital_cost'])
|
|
1818
|
-
|
|
1819
|
-
if 'p_nom_opt' in network.generators.columns:
|
|
1820
|
-
capacity_mw = float(network.generators.loc[gen_name, 'p_nom_opt'])
|
|
1821
|
-
elif 'p_nom' in network.generators.columns:
|
|
1822
|
-
capacity_mw = float(network.generators.loc[gen_name, 'p_nom'])
|
|
1823
|
-
else:
|
|
1824
|
-
capacity_mw = 0.0
|
|
1825
|
-
|
|
1826
|
-
# Annual capital cost for operational assets (undiscounted)
|
|
1827
|
-
annual_capital_cost = capacity_mw * capital_cost_per_mw
|
|
1828
|
-
|
|
1829
|
-
if carrier_name in carrier_stats["capital_cost_by_carrier"]:
|
|
1830
|
-
carrier_stats["capital_cost_by_carrier"][carrier_name] += annual_capital_cost
|
|
1341
|
+
if not year_results:
|
|
1342
|
+
logger.warning(f"No year-based results found for network {network_id}")
|
|
1343
|
+
return totals
|
|
1831
1344
|
|
|
1832
|
-
#
|
|
1833
|
-
|
|
1834
|
-
# Get storage unit-carrier mapping
|
|
1835
|
-
cursor = conn.execute("""
|
|
1836
|
-
SELECT c.name as component_name, carr.name as carrier_name
|
|
1837
|
-
FROM components c
|
|
1838
|
-
JOIN carriers carr ON c.carrier_id = carr.id
|
|
1839
|
-
WHERE c.network_id = ? AND c.component_type = 'STORAGE_UNIT'
|
|
1840
|
-
""", (network_id,))
|
|
1841
|
-
storage_unit_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
1842
|
-
|
|
1843
|
-
for su_name in network.storage_units.index:
|
|
1844
|
-
if su_name in storage_unit_carriers:
|
|
1845
|
-
carrier_name = storage_unit_carriers[su_name]
|
|
1846
|
-
|
|
1847
|
-
# Operational costs = year_discharge * marginal_cost
|
|
1848
|
-
if 'marginal_cost' in network.storage_units.columns:
|
|
1849
|
-
year_discharge = 0.0
|
|
1850
|
-
if hasattr(network, 'storage_units_t') and hasattr(network.storage_units_t, 'p'):
|
|
1851
|
-
year_storage = self._filter_timeseries_by_year(network.storage_units_t.p, network.snapshots, year)
|
|
1852
|
-
if year_storage is not None and su_name in year_storage.columns:
|
|
1853
|
-
year_weightings = self._get_year_weightings(network, year)
|
|
1854
|
-
if year_weightings is not None:
|
|
1855
|
-
year_discharge = float((year_storage[su_name].clip(lower=0).values * year_weightings).sum())
|
|
1856
|
-
else:
|
|
1857
|
-
# Fallback: use all-year weightings if year-specific not available
|
|
1858
|
-
weightings = network.snapshot_weightings
|
|
1859
|
-
if isinstance(weightings, pd.DataFrame):
|
|
1860
|
-
if 'objective' in weightings.columns:
|
|
1861
|
-
weighting_values = weightings['objective'].values
|
|
1862
|
-
else:
|
|
1863
|
-
weighting_values = weightings.iloc[:, 0].values
|
|
1864
|
-
else:
|
|
1865
|
-
weighting_values = weightings.values
|
|
1866
|
-
# Apply weightings to the filtered year data
|
|
1867
|
-
if len(weighting_values) == len(year_storage):
|
|
1868
|
-
year_discharge = float((year_storage[su_name].clip(lower=0).values * weighting_values).sum())
|
|
1869
|
-
else:
|
|
1870
|
-
year_discharge = float(year_storage[su_name].clip(lower=0).sum())
|
|
1871
|
-
logger.warning(f"Could not apply snapshot weightings for operational cost calc of storage unit {su_name} in year {year} - cost may be incorrect")
|
|
1872
|
-
|
|
1873
|
-
marginal_cost = float(network.storage_units.loc[su_name, 'marginal_cost'])
|
|
1874
|
-
operational_cost = year_discharge * marginal_cost
|
|
1875
|
-
|
|
1876
|
-
if carrier_name in carrier_stats["operational_cost_by_carrier"]:
|
|
1877
|
-
carrier_stats["operational_cost_by_carrier"][carrier_name] += operational_cost
|
|
1878
|
-
|
|
1879
|
-
# Capital costs - include if asset is operational in this year (matching old solver)
|
|
1880
|
-
if 'capital_cost' in network.storage_units.columns:
|
|
1881
|
-
# Check if this storage unit is operational in this year
|
|
1882
|
-
is_operational = True
|
|
1883
|
-
if 'build_year' in network.storage_units.columns:
|
|
1884
|
-
build_year = network.storage_units.loc[su_name, 'build_year']
|
|
1885
|
-
if pd.notna(build_year) and int(build_year) > year:
|
|
1886
|
-
is_operational = False # Not built yet
|
|
1887
|
-
|
|
1888
|
-
if is_operational:
|
|
1889
|
-
capital_cost_per_mw = float(network.storage_units.loc[su_name, 'capital_cost'])
|
|
1890
|
-
|
|
1891
|
-
if 'p_nom_opt' in network.storage_units.columns:
|
|
1892
|
-
capacity_mw = float(network.storage_units.loc[su_name, 'p_nom_opt'])
|
|
1893
|
-
elif 'p_nom' in network.storage_units.columns:
|
|
1894
|
-
capacity_mw = float(network.storage_units.loc[su_name, 'p_nom'])
|
|
1895
|
-
else:
|
|
1896
|
-
capacity_mw = 0.0
|
|
1897
|
-
|
|
1898
|
-
# Annual capital cost for operational assets (undiscounted)
|
|
1899
|
-
annual_capital_cost = capacity_mw * capital_cost_per_mw
|
|
1900
|
-
|
|
1901
|
-
if carrier_name in carrier_stats["capital_cost_by_carrier"]:
|
|
1902
|
-
carrier_stats["capital_cost_by_carrier"][carrier_name] += annual_capital_cost
|
|
1345
|
+
# For capacity: use the LAST YEAR only (final capacity state)
|
|
1346
|
+
last_year, last_results_json = year_results[-1]
|
|
1903
1347
|
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
SELECT c.name as component_name, carr.name as carrier_name
|
|
1909
|
-
FROM components c
|
|
1910
|
-
JOIN carriers carr ON c.carrier_id = carr.id
|
|
1911
|
-
WHERE c.network_id = ? AND c.component_type = 'STORE'
|
|
1912
|
-
""", (network_id,))
|
|
1913
|
-
store_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
1348
|
+
try:
|
|
1349
|
+
results = json.loads(last_results_json)
|
|
1350
|
+
network_stats = results.get('network_statistics', {})
|
|
1351
|
+
custom_stats = network_stats.get('custom_statistics', {})
|
|
1914
1352
|
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
if 'marginal_cost' in network.stores.columns:
|
|
1921
|
-
year_discharge = 0.0
|
|
1922
|
-
if hasattr(network, 'stores_t') and hasattr(network.stores_t, 'p'):
|
|
1923
|
-
year_stores = self._filter_timeseries_by_year(network.stores_t.p, network.snapshots, year)
|
|
1924
|
-
if year_stores is not None and store_name in year_stores.columns:
|
|
1925
|
-
year_weightings = self._get_year_weightings(network, year)
|
|
1926
|
-
if year_weightings is not None:
|
|
1927
|
-
year_discharge = float((year_stores[store_name].clip(lower=0).values * year_weightings).sum())
|
|
1928
|
-
else:
|
|
1929
|
-
# Fallback: use all-year weightings if year-specific not available
|
|
1930
|
-
weightings = network.snapshot_weightings
|
|
1931
|
-
if isinstance(weightings, pd.DataFrame):
|
|
1932
|
-
if 'objective' in weightings.columns:
|
|
1933
|
-
weighting_values = weightings['objective'].values
|
|
1934
|
-
else:
|
|
1935
|
-
weighting_values = weightings.iloc[:, 0].values
|
|
1936
|
-
else:
|
|
1937
|
-
weighting_values = weightings.values
|
|
1938
|
-
# Apply weightings to the filtered year data
|
|
1939
|
-
if len(weighting_values) == len(year_stores):
|
|
1940
|
-
year_discharge = float((year_stores[store_name].clip(lower=0).values * weighting_values).sum())
|
|
1941
|
-
else:
|
|
1942
|
-
year_discharge = float(year_stores[store_name].clip(lower=0).sum())
|
|
1943
|
-
logger.warning(f"Could not apply snapshot weightings for operational cost calc of store {store_name} in year {year} - cost may be incorrect")
|
|
1944
|
-
|
|
1945
|
-
marginal_cost = float(network.stores.loc[store_name, 'marginal_cost'])
|
|
1946
|
-
operational_cost = year_discharge * marginal_cost
|
|
1947
|
-
|
|
1948
|
-
if carrier_name in carrier_stats["operational_cost_by_carrier"]:
|
|
1949
|
-
carrier_stats["operational_cost_by_carrier"][carrier_name] += operational_cost
|
|
1950
|
-
|
|
1951
|
-
# Capital costs - include if asset is operational in this year (matching old solver)
|
|
1952
|
-
if 'capital_cost' in network.stores.columns:
|
|
1953
|
-
# Check if this store is operational in this year
|
|
1954
|
-
is_operational = True
|
|
1955
|
-
if 'build_year' in network.stores.columns:
|
|
1956
|
-
build_year = network.stores.loc[store_name, 'build_year']
|
|
1957
|
-
if pd.notna(build_year) and int(build_year) > year:
|
|
1958
|
-
is_operational = False # Not built yet
|
|
1959
|
-
|
|
1960
|
-
if is_operational:
|
|
1961
|
-
capital_cost_per_mwh = float(network.stores.loc[store_name, 'capital_cost'])
|
|
1962
|
-
|
|
1963
|
-
if 'e_nom_opt' in network.stores.columns:
|
|
1964
|
-
capacity_mwh = float(network.stores.loc[store_name, 'e_nom_opt'])
|
|
1965
|
-
elif 'e_nom' in network.stores.columns:
|
|
1966
|
-
capacity_mwh = float(network.stores.loc[store_name, 'e_nom'])
|
|
1967
|
-
else:
|
|
1968
|
-
capacity_mwh = 0.0
|
|
1969
|
-
|
|
1970
|
-
# Annual capital cost for operational assets (undiscounted)
|
|
1971
|
-
annual_capital_cost = capacity_mwh * capital_cost_per_mwh
|
|
1972
|
-
|
|
1973
|
-
if carrier_name in carrier_stats["capital_cost_by_carrier"]:
|
|
1974
|
-
carrier_stats["capital_cost_by_carrier"][carrier_name] += annual_capital_cost
|
|
1975
|
-
|
|
1976
|
-
# 4. LINES - Year-specific capital costs (only count if built in this year)
|
|
1977
|
-
if hasattr(network, 'lines') and not network.lines.empty:
|
|
1978
|
-
# Get line-carrier mapping
|
|
1979
|
-
cursor = conn.execute("""
|
|
1980
|
-
SELECT c.name as component_name, carr.name as carrier_name
|
|
1981
|
-
FROM components c
|
|
1982
|
-
JOIN carriers carr ON c.carrier_id = carr.id
|
|
1983
|
-
WHERE c.network_id = ? AND c.component_type = 'LINE'
|
|
1984
|
-
""", (network_id,))
|
|
1985
|
-
line_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
1353
|
+
# Use last year's capacity as the all-year capacity
|
|
1354
|
+
power_capacity_by_carrier = custom_stats.get('power_capacity_by_carrier', {})
|
|
1355
|
+
for carrier, value in power_capacity_by_carrier.items():
|
|
1356
|
+
if carrier in totals["power_capacity_by_carrier"]:
|
|
1357
|
+
totals["power_capacity_by_carrier"][carrier] = float(value or 0)
|
|
1986
1358
|
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
# Capital costs - include if asset is operational in this year (matching old solver)
|
|
1992
|
-
if 'capital_cost' in network.lines.columns:
|
|
1993
|
-
# Check if this line is operational in this year
|
|
1994
|
-
is_operational = True
|
|
1995
|
-
if 'build_year' in network.lines.columns:
|
|
1996
|
-
build_year = network.lines.loc[line_name, 'build_year']
|
|
1997
|
-
if pd.notna(build_year) and int(build_year) > year:
|
|
1998
|
-
is_operational = False # Not built yet
|
|
1999
|
-
|
|
2000
|
-
if is_operational:
|
|
2001
|
-
capital_cost_per_mva = float(network.lines.loc[line_name, 'capital_cost'])
|
|
2002
|
-
|
|
2003
|
-
if 's_nom_opt' in network.lines.columns:
|
|
2004
|
-
capacity_mva = float(network.lines.loc[line_name, 's_nom_opt'])
|
|
2005
|
-
elif 's_nom' in network.lines.columns:
|
|
2006
|
-
capacity_mva = float(network.lines.loc[line_name, 's_nom'])
|
|
2007
|
-
else:
|
|
2008
|
-
capacity_mva = 0.0
|
|
2009
|
-
|
|
2010
|
-
# Annual capital cost for operational assets (undiscounted)
|
|
2011
|
-
annual_capital_cost = capacity_mva * capital_cost_per_mva
|
|
2012
|
-
|
|
2013
|
-
if carrier_name in carrier_stats["capital_cost_by_carrier"]:
|
|
2014
|
-
carrier_stats["capital_cost_by_carrier"][carrier_name] += annual_capital_cost
|
|
2015
|
-
|
|
2016
|
-
# 5. LINKS - Year-specific operational and capital costs
|
|
2017
|
-
if hasattr(network, 'links') and not network.links.empty:
|
|
2018
|
-
# Get link-carrier mapping
|
|
2019
|
-
cursor = conn.execute("""
|
|
2020
|
-
SELECT c.name as component_name, carr.name as carrier_name
|
|
2021
|
-
FROM components c
|
|
2022
|
-
JOIN carriers carr ON c.carrier_id = carr.id
|
|
2023
|
-
WHERE c.network_id = ? AND c.component_type = 'LINK'
|
|
2024
|
-
""", (network_id,))
|
|
2025
|
-
link_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
1359
|
+
energy_capacity_by_carrier = custom_stats.get('energy_capacity_by_carrier', {})
|
|
1360
|
+
for carrier, value in energy_capacity_by_carrier.items():
|
|
1361
|
+
if carrier in totals["energy_capacity_by_carrier"]:
|
|
1362
|
+
totals["energy_capacity_by_carrier"][carrier] = float(value or 0)
|
|
2026
1363
|
|
|
2027
|
-
|
|
2028
|
-
if hasattr(network, 'links_t') and hasattr(network.links_t, 'p0'):
|
|
2029
|
-
for link_name in network.links.index:
|
|
2030
|
-
if link_name in link_carriers:
|
|
2031
|
-
carrier_name = link_carriers[link_name]
|
|
2032
|
-
|
|
2033
|
-
# Get marginal cost for this link
|
|
2034
|
-
marginal_cost = 0.0
|
|
2035
|
-
if 'marginal_cost' in network.links.columns:
|
|
2036
|
-
marginal_cost = float(network.links.loc[link_name, 'marginal_cost'])
|
|
2037
|
-
|
|
2038
|
-
# Calculate operational cost = year_flow * marginal_cost
|
|
2039
|
-
year_flow = 0.0
|
|
2040
|
-
if link_name in network.links_t.p0.columns:
|
|
2041
|
-
year_links = self._filter_timeseries_by_year(network.links_t.p0, network.snapshots, year)
|
|
2042
|
-
if year_links is not None and link_name in year_links.columns:
|
|
2043
|
-
year_weightings = self._get_year_weightings(network, year)
|
|
2044
|
-
if year_weightings is not None:
|
|
2045
|
-
year_flow = float((abs(year_links[link_name]).values * year_weightings).sum())
|
|
2046
|
-
else:
|
|
2047
|
-
# Fallback: use all-year weightings if year-specific not available
|
|
2048
|
-
weightings = network.snapshot_weightings
|
|
2049
|
-
if isinstance(weightings, pd.DataFrame):
|
|
2050
|
-
if 'objective' in weightings.columns:
|
|
2051
|
-
weighting_values = weightings['objective'].values
|
|
2052
|
-
else:
|
|
2053
|
-
weighting_values = weightings.iloc[:, 0].values
|
|
2054
|
-
else:
|
|
2055
|
-
weighting_values = weightings.values
|
|
2056
|
-
# Apply weightings to the filtered year data
|
|
2057
|
-
if len(weighting_values) == len(year_links):
|
|
2058
|
-
year_flow = float((abs(year_links[link_name]).values * weighting_values).sum())
|
|
2059
|
-
else:
|
|
2060
|
-
year_flow = float(abs(year_links[link_name]).sum())
|
|
2061
|
-
logger.warning(f"Could not apply snapshot weightings for operational cost calc of link {link_name} in year {year} - cost may be incorrect")
|
|
2062
|
-
|
|
2063
|
-
operational_cost = year_flow * marginal_cost
|
|
2064
|
-
|
|
2065
|
-
if carrier_name in carrier_stats["operational_cost_by_carrier"]:
|
|
2066
|
-
carrier_stats["operational_cost_by_carrier"][carrier_name] += operational_cost
|
|
1364
|
+
logger.info(f"Used last year ({last_year}) capacity as all-year capacity")
|
|
2067
1365
|
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
if link_name in link_carriers:
|
|
2071
|
-
carrier_name = link_carriers[link_name]
|
|
2072
|
-
|
|
2073
|
-
# Capital costs - include if asset is operational in this year (matching old solver)
|
|
2074
|
-
if 'capital_cost' in network.links.columns:
|
|
2075
|
-
# Check if this link is operational in this year
|
|
2076
|
-
is_operational = True
|
|
2077
|
-
if 'build_year' in network.links.columns:
|
|
2078
|
-
build_year = network.links.loc[link_name, 'build_year']
|
|
2079
|
-
if pd.notna(build_year) and int(build_year) > year:
|
|
2080
|
-
is_operational = False # Not built yet
|
|
2081
|
-
|
|
2082
|
-
if is_operational:
|
|
2083
|
-
capital_cost_per_mw = float(network.links.loc[link_name, 'capital_cost'])
|
|
2084
|
-
|
|
2085
|
-
if 'p_nom_opt' in network.links.columns:
|
|
2086
|
-
capacity_mw = float(network.links.loc[link_name, 'p_nom_opt'])
|
|
2087
|
-
elif 'p_nom' in network.links.columns:
|
|
2088
|
-
capacity_mw = float(network.links.loc[link_name, 'p_nom'])
|
|
2089
|
-
else:
|
|
2090
|
-
capacity_mw = 0.0
|
|
2091
|
-
|
|
2092
|
-
# Annual capital cost for operational assets (undiscounted)
|
|
2093
|
-
annual_capital_cost = capacity_mw * capital_cost_per_mw
|
|
2094
|
-
|
|
2095
|
-
if carrier_name in carrier_stats["capital_cost_by_carrier"]:
|
|
2096
|
-
carrier_stats["capital_cost_by_carrier"][carrier_name] += annual_capital_cost
|
|
1366
|
+
except Exception as e:
|
|
1367
|
+
logger.error(f"Failed to process last year ({last_year}) results: {e}")
|
|
2097
1368
|
|
|
2098
|
-
#
|
|
2099
|
-
for
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
1369
|
+
# For other stats (dispatch, emissions, costs): sum across all years
|
|
1370
|
+
for year, results_json in year_results:
|
|
1371
|
+
try:
|
|
1372
|
+
results = json.loads(results_json)
|
|
1373
|
+
network_stats = results.get('network_statistics', {})
|
|
1374
|
+
custom_stats = network_stats.get('custom_statistics', {})
|
|
1375
|
+
|
|
1376
|
+
# Sum dispatch (energy values - sum across years)
|
|
1377
|
+
dispatch_by_carrier = custom_stats.get('dispatch_by_carrier', {})
|
|
1378
|
+
for carrier, value in dispatch_by_carrier.items():
|
|
1379
|
+
if carrier in totals["dispatch_by_carrier"]:
|
|
1380
|
+
totals["dispatch_by_carrier"][carrier] += float(value or 0)
|
|
1381
|
+
|
|
1382
|
+
# Sum emissions (cumulative across years)
|
|
1383
|
+
emissions_by_carrier = custom_stats.get('emissions_by_carrier', {})
|
|
1384
|
+
for carrier, value in emissions_by_carrier.items():
|
|
1385
|
+
if carrier in totals["emissions_by_carrier"]:
|
|
1386
|
+
totals["emissions_by_carrier"][carrier] += float(value or 0)
|
|
1387
|
+
|
|
1388
|
+
# Sum capital costs (cumulative across years)
|
|
1389
|
+
capital_cost_by_carrier = custom_stats.get('capital_cost_by_carrier', {})
|
|
1390
|
+
for carrier, value in capital_cost_by_carrier.items():
|
|
1391
|
+
if carrier in totals["capital_cost_by_carrier"]:
|
|
1392
|
+
totals["capital_cost_by_carrier"][carrier] += float(value or 0)
|
|
1393
|
+
|
|
1394
|
+
# Sum operational costs (cumulative across years)
|
|
1395
|
+
operational_cost_by_carrier = custom_stats.get('operational_cost_by_carrier', {})
|
|
1396
|
+
for carrier, value in operational_cost_by_carrier.items():
|
|
1397
|
+
if carrier in totals["operational_cost_by_carrier"]:
|
|
1398
|
+
totals["operational_cost_by_carrier"][carrier] += float(value or 0)
|
|
1399
|
+
|
|
1400
|
+
# Sum total system costs (cumulative across years)
|
|
1401
|
+
total_system_cost_by_carrier = custom_stats.get('total_system_cost_by_carrier', {})
|
|
1402
|
+
for carrier, value in total_system_cost_by_carrier.items():
|
|
1403
|
+
if carrier in totals["total_system_cost_by_carrier"]:
|
|
1404
|
+
totals["total_system_cost_by_carrier"][carrier] += float(value or 0)
|
|
1405
|
+
|
|
1406
|
+
except Exception as e:
|
|
1407
|
+
logger.error(f"Failed to process year {year} results: {e}")
|
|
1408
|
+
continue
|
|
2103
1409
|
|
|
2104
|
-
logger.info(f"
|
|
2105
|
-
logger.info(f"
|
|
2106
|
-
logger.info(f"
|
|
2107
|
-
logger.info(f"
|
|
2108
|
-
logger.info(f"
|
|
2109
|
-
logger.info(f"
|
|
2110
|
-
logger.info(f"
|
|
1410
|
+
logger.info(f"Summed carrier statistics across {len(year_results)} years:")
|
|
1411
|
+
logger.info(f" Final power capacity: {sum(totals['power_capacity_by_carrier'].values()):.2f} MW")
|
|
1412
|
+
logger.info(f" Final energy capacity: {sum(totals['energy_capacity_by_carrier'].values()):.2f} MWh")
|
|
1413
|
+
logger.info(f" Total dispatch: {sum(totals['dispatch_by_carrier'].values()):.2f} MWh")
|
|
1414
|
+
logger.info(f" Total emissions: {sum(totals['emissions_by_carrier'].values()):.2f} tonnes CO2")
|
|
1415
|
+
logger.info(f" Total capital cost: {sum(totals['capital_cost_by_carrier'].values()):.2f} USD")
|
|
1416
|
+
logger.info(f" Total operational cost: {sum(totals['operational_cost_by_carrier'].values()):.2f} USD")
|
|
1417
|
+
logger.info(f" Total system cost: {sum(totals['total_system_cost_by_carrier'].values()):.2f} USD")
|
|
2111
1418
|
|
|
2112
|
-
return
|
|
1419
|
+
return totals
|
|
2113
1420
|
|
|
2114
1421
|
except Exception as e:
|
|
2115
|
-
logger.error(f"Failed to
|
|
1422
|
+
logger.error(f"Failed to sum year-based carrier statistics: {e}", exc_info=True)
|
|
1423
|
+
# Return empty structure on error
|
|
2116
1424
|
return {
|
|
2117
1425
|
"dispatch_by_carrier": {},
|
|
2118
1426
|
"power_capacity_by_carrier": {},
|
|
@@ -2123,10 +1431,46 @@ class ResultStorage:
|
|
|
2123
1431
|
"total_system_cost_by_carrier": {}
|
|
2124
1432
|
}
|
|
2125
1433
|
|
|
1434
|
+
def _serialize_results_json(self, solve_result: Dict[str, Any]) -> str:
|
|
1435
|
+
"""Serialize solve results to JSON string."""
|
|
1436
|
+
import json
|
|
1437
|
+
try:
|
|
1438
|
+
# Create a clean results dictionary
|
|
1439
|
+
results = {
|
|
1440
|
+
"success": solve_result.get("success", False),
|
|
1441
|
+
"status": solve_result.get("status", "unknown"),
|
|
1442
|
+
"solve_time": solve_result.get("solve_time", 0.0),
|
|
1443
|
+
"objective_value": solve_result.get("objective_value"),
|
|
1444
|
+
"solver_name": solve_result.get("solver_name", "unknown"),
|
|
1445
|
+
"run_id": solve_result.get("run_id"),
|
|
1446
|
+
"network_statistics": solve_result.get("network_statistics", {}),
|
|
1447
|
+
"pypsa_result": solve_result.get("pypsa_result", {})
|
|
1448
|
+
}
|
|
1449
|
+
return json.dumps(results, default=self._json_serializer)
|
|
1450
|
+
except Exception as e:
|
|
1451
|
+
logger.warning(f"Failed to serialize results JSON: {e}")
|
|
1452
|
+
return json.dumps({"error": "serialization_failed"})
|
|
1453
|
+
|
|
1454
|
+
def _serialize_metadata_json(self, solve_result: Dict[str, Any]) -> str:
|
|
1455
|
+
"""Serialize solve metadata to JSON string."""
|
|
1456
|
+
import json
|
|
1457
|
+
try:
|
|
1458
|
+
metadata = {
|
|
1459
|
+
"solver_name": solve_result.get("solver_name", "unknown"),
|
|
1460
|
+
"run_id": solve_result.get("run_id"),
|
|
1461
|
+
"multi_period": solve_result.get("multi_period", False),
|
|
1462
|
+
"years": solve_result.get("years", []),
|
|
1463
|
+
"network_name": solve_result.get("network_name"),
|
|
1464
|
+
"num_snapshots": solve_result.get("num_snapshots", 0)
|
|
1465
|
+
}
|
|
1466
|
+
return json.dumps(metadata, default=self._json_serializer)
|
|
1467
|
+
except Exception as e:
|
|
1468
|
+
logger.warning(f"Failed to serialize metadata JSON: {e}")
|
|
1469
|
+
return json.dumps({"error": "serialization_failed"})
|
|
1470
|
+
|
|
2126
1471
|
def _filter_timeseries_by_year(self, timeseries_df: 'pd.DataFrame', snapshots: 'pd.Index', year: int) -> 'pd.DataFrame':
|
|
2127
|
-
"""Filter timeseries data by year
|
|
1472
|
+
"""Filter timeseries data by year"""
|
|
2128
1473
|
try:
|
|
2129
|
-
|
|
2130
1474
|
# Handle MultiIndex case (multi-period optimization)
|
|
2131
1475
|
if hasattr(snapshots, 'levels'):
|
|
2132
1476
|
period_values = snapshots.get_level_values(0)
|
|
@@ -2149,9 +1493,8 @@ class ResultStorage:
|
|
|
2149
1493
|
return None
|
|
2150
1494
|
|
|
2151
1495
|
def _get_year_weightings(self, network: 'pypsa.Network', year: int) -> 'np.ndarray':
|
|
2152
|
-
"""Get snapshot weightings for a specific year
|
|
1496
|
+
"""Get snapshot weightings for a specific year"""
|
|
2153
1497
|
try:
|
|
2154
|
-
|
|
2155
1498
|
# Filter snapshot weightings by year
|
|
2156
1499
|
if hasattr(network.snapshots, 'levels'):
|
|
2157
1500
|
period_values = network.snapshots.get_level_values(0)
|
|
@@ -2185,6 +1528,30 @@ class ResultStorage:
|
|
|
2185
1528
|
logger.error(f"Failed to get year weightings for year {year}: {e}")
|
|
2186
1529
|
return None
|
|
2187
1530
|
|
|
1531
|
+
def _calculate_total_demand(self, network: 'pypsa.Network') -> float:
|
|
1532
|
+
"""Calculate total demand from loads in the network"""
|
|
1533
|
+
try:
|
|
1534
|
+
total_demand = 0.0
|
|
1535
|
+
|
|
1536
|
+
# Calculate demand from loads
|
|
1537
|
+
if hasattr(network, 'loads_t') and hasattr(network.loads_t, 'p'):
|
|
1538
|
+
# Apply snapshot weightings to convert MW to MWh
|
|
1539
|
+
weightings = network.snapshot_weightings
|
|
1540
|
+
if isinstance(weightings, pd.DataFrame):
|
|
1541
|
+
if 'objective' in weightings.columns:
|
|
1542
|
+
weighting_values = weightings['objective'].values
|
|
1543
|
+
else:
|
|
1544
|
+
weighting_values = weightings.iloc[:, 0].values
|
|
1545
|
+
else:
|
|
1546
|
+
weighting_values = weightings.values
|
|
1547
|
+
|
|
1548
|
+
total_demand = float((network.loads_t.p.values * weighting_values[:, None]).sum())
|
|
1549
|
+
|
|
1550
|
+
return total_demand
|
|
1551
|
+
|
|
1552
|
+
except Exception as e:
|
|
1553
|
+
logger.error(f"Failed to calculate total demand: {e}")
|
|
1554
|
+
return 0.0
|
|
2188
1555
|
|
|
2189
1556
|
def _json_serializer(self, obj):
|
|
2190
1557
|
"""Convert numpy/pandas types to JSON serializable types"""
|
|
@@ -2204,4 +1571,4 @@ class ResultStorage:
|
|
|
2204
1571
|
elif hasattr(obj, 'item'): # Handle numpy scalars
|
|
2205
1572
|
return obj.item()
|
|
2206
1573
|
else:
|
|
2207
|
-
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
|
|
1574
|
+
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
|