pyconvexity 0.4.3__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 +226 -0
- pyconvexity/_version.py +1 -0
- pyconvexity/core/__init__.py +60 -0
- pyconvexity/core/database.py +485 -0
- pyconvexity/core/errors.py +106 -0
- pyconvexity/core/types.py +400 -0
- pyconvexity/data/README.md +101 -0
- pyconvexity/data/__init__.py +17 -0
- pyconvexity/data/loaders/__init__.py +3 -0
- pyconvexity/data/loaders/cache.py +213 -0
- pyconvexity/data/schema/01_core_schema.sql +420 -0
- pyconvexity/data/schema/02_data_metadata.sql +120 -0
- pyconvexity/data/schema/03_validation_data.sql +506 -0
- pyconvexity/data/sources/__init__.py +5 -0
- pyconvexity/data/sources/gem.py +442 -0
- pyconvexity/io/__init__.py +26 -0
- pyconvexity/io/excel_exporter.py +1226 -0
- pyconvexity/io/excel_importer.py +1381 -0
- pyconvexity/io/netcdf_exporter.py +197 -0
- pyconvexity/io/netcdf_importer.py +1833 -0
- pyconvexity/models/__init__.py +195 -0
- pyconvexity/models/attributes.py +730 -0
- pyconvexity/models/carriers.py +159 -0
- pyconvexity/models/components.py +611 -0
- pyconvexity/models/network.py +503 -0
- pyconvexity/models/results.py +148 -0
- pyconvexity/models/scenarios.py +234 -0
- pyconvexity/solvers/__init__.py +29 -0
- pyconvexity/solvers/pypsa/__init__.py +24 -0
- pyconvexity/solvers/pypsa/api.py +460 -0
- pyconvexity/solvers/pypsa/batch_loader.py +307 -0
- pyconvexity/solvers/pypsa/builder.py +675 -0
- pyconvexity/solvers/pypsa/constraints.py +405 -0
- pyconvexity/solvers/pypsa/solver.py +1509 -0
- pyconvexity/solvers/pypsa/storage.py +2048 -0
- pyconvexity/timeseries.py +330 -0
- pyconvexity/validation/__init__.py +25 -0
- pyconvexity/validation/rules.py +312 -0
- pyconvexity-0.4.3.dist-info/METADATA +47 -0
- pyconvexity-0.4.3.dist-info/RECORD +42 -0
- pyconvexity-0.4.3.dist-info/WHEEL +5 -0
- pyconvexity-0.4.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Global Energy Monitor (GEM) data integration for PyConvexity.
|
|
3
|
+
|
|
4
|
+
This module provides functions to load power plant data from GEM's Global Integrated Power dataset
|
|
5
|
+
and integrate it with PyConvexity models.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sqlite3
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import yaml
|
|
11
|
+
import country_converter as coco
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Dict, Any, Optional, List
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
from pyconvexity.core.types import CreateComponentRequest, StaticValue
|
|
17
|
+
from pyconvexity.models.components import create_component
|
|
18
|
+
from pyconvexity.models.attributes import set_static_attribute
|
|
19
|
+
from pyconvexity.data.loaders.cache import DataCache
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Default path to GEM data - can be overridden
|
|
24
|
+
DEFAULT_GEM_DATA_PATH = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_gem_data_path() -> Path:
|
|
28
|
+
"""Get the path to GEM data file."""
|
|
29
|
+
global DEFAULT_GEM_DATA_PATH
|
|
30
|
+
|
|
31
|
+
if DEFAULT_GEM_DATA_PATH:
|
|
32
|
+
return Path(DEFAULT_GEM_DATA_PATH)
|
|
33
|
+
|
|
34
|
+
# Try to find the examples data
|
|
35
|
+
possible_paths = [
|
|
36
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
37
|
+
/ "examples"
|
|
38
|
+
/ "data"
|
|
39
|
+
/ "raw"
|
|
40
|
+
/ "global-energy-monitor"
|
|
41
|
+
/ "Global-Integrated-Power-August-2025.xlsx",
|
|
42
|
+
Path("data/raw/global-energy-monitor/Global-Integrated-Power-August-2025.xlsx"),
|
|
43
|
+
Path(
|
|
44
|
+
"../examples/data/raw/global-energy-monitor/Global-Integrated-Power-August-2025.xlsx"
|
|
45
|
+
),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
for path in possible_paths:
|
|
49
|
+
if path.exists():
|
|
50
|
+
return path
|
|
51
|
+
|
|
52
|
+
raise FileNotFoundError(
|
|
53
|
+
"GEM data file not found. Please set the path using set_gem_data_path() or "
|
|
54
|
+
"ensure the file exists at one of the expected locations."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def set_gem_data_path(path: str):
|
|
59
|
+
"""Set the path to the GEM data file."""
|
|
60
|
+
global DEFAULT_GEM_DATA_PATH
|
|
61
|
+
DEFAULT_GEM_DATA_PATH = path
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _load_gem_mapping() -> Dict[str, Any]:
|
|
65
|
+
"""Load the GEM to carriers mapping configuration."""
|
|
66
|
+
# Try to find the mapping file
|
|
67
|
+
possible_paths = [
|
|
68
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
69
|
+
/ "examples"
|
|
70
|
+
/ "schema"
|
|
71
|
+
/ "gem_mapping.yaml",
|
|
72
|
+
Path("schema/gem_mapping.yaml"),
|
|
73
|
+
Path("../examples/schema/gem_mapping.yaml"),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
for mapping_file in possible_paths:
|
|
77
|
+
if mapping_file.exists():
|
|
78
|
+
with open(mapping_file, "r") as f:
|
|
79
|
+
return yaml.safe_load(f)
|
|
80
|
+
|
|
81
|
+
# Fallback to embedded mapping if file not found
|
|
82
|
+
logger.warning("GEM mapping file not found, using embedded mapping")
|
|
83
|
+
return _get_embedded_gem_mapping()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _get_embedded_gem_mapping() -> Dict[str, Any]:
|
|
87
|
+
"""Embedded GEM mapping as fallback."""
|
|
88
|
+
return {
|
|
89
|
+
"technology_mapping": {
|
|
90
|
+
# Nuclear
|
|
91
|
+
"pressurized water reactor": [
|
|
92
|
+
"nuclear",
|
|
93
|
+
"nuclear",
|
|
94
|
+
"pressurized-water-reactor",
|
|
95
|
+
],
|
|
96
|
+
"boiling water reactor": ["nuclear", "nuclear", "boiling-water-reactor"],
|
|
97
|
+
"small modular reactor": ["nuclear", "nuclear", "small-modular-reactor"],
|
|
98
|
+
# Thermal coal
|
|
99
|
+
"subcritical": ["thermal", "coal", "subcritical"],
|
|
100
|
+
"supercritical": ["thermal", "coal", "supercritical"],
|
|
101
|
+
"ultra-supercritical": ["thermal", "coal", "supercritical"],
|
|
102
|
+
# Thermal gas
|
|
103
|
+
"combined cycle": ["thermal", "gas", "combined-cycle"],
|
|
104
|
+
"gas turbine": ["thermal", "gas", "gas-turbine"],
|
|
105
|
+
# Renewables
|
|
106
|
+
"PV": ["renewables", "solar", "photovoltaic"],
|
|
107
|
+
"Solar Thermal": ["renewables", "solar", "thermal"],
|
|
108
|
+
"Onshore": ["renewables", "wind", "onshore"],
|
|
109
|
+
"Offshore hard mount": ["renewables", "wind", "offshore"],
|
|
110
|
+
"Offshore floating": ["renewables", "wind", "offshore"],
|
|
111
|
+
"run-of-river": ["renewables", "hydro", "run-of-river"],
|
|
112
|
+
"pumped storage": ["storage", "pumped-hydro", "unspecified"],
|
|
113
|
+
# Storage
|
|
114
|
+
"battery": ["storage", "battery", "lithium-ion"],
|
|
115
|
+
# Bioenergy
|
|
116
|
+
"biomass": ["bioenergy", "biomass", "unspecified"],
|
|
117
|
+
"biogas": ["bioenergy", "biogas", "unspecified"],
|
|
118
|
+
},
|
|
119
|
+
"type_fallback": {
|
|
120
|
+
"nuclear": ["nuclear", "nuclear", "unspecified"],
|
|
121
|
+
"coal": ["thermal", "coal", "unspecified"],
|
|
122
|
+
"oil/gas": ["thermal", "gas", "unspecified"],
|
|
123
|
+
"wind": ["renewables", "wind", "unspecified"],
|
|
124
|
+
"solar": ["renewables", "solar", "unspecified"],
|
|
125
|
+
"geothermal": ["renewables", "geothermal", "unspecified"],
|
|
126
|
+
"hydropower": ["renewables", "hydro", "unspecified"],
|
|
127
|
+
"bioenergy": ["bioenergy", "biomass", "unspecified"],
|
|
128
|
+
},
|
|
129
|
+
"default_mapping": ["unknown", "unspecified", "unspecified"],
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _map_technology_to_carriers(
|
|
134
|
+
technology: str, gem_type: str, mapping_config: Dict[str, Any]
|
|
135
|
+
) -> tuple:
|
|
136
|
+
"""
|
|
137
|
+
Map GEM technology to carriers schema.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
technology: GEM Technology field value
|
|
141
|
+
gem_type: GEM Type field value
|
|
142
|
+
mapping_config: Loaded mapping configuration
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
tuple: (category, carrier, type)
|
|
146
|
+
"""
|
|
147
|
+
# Clean the technology string
|
|
148
|
+
if pd.isna(technology):
|
|
149
|
+
technology = "unknown"
|
|
150
|
+
else:
|
|
151
|
+
technology = str(technology).strip()
|
|
152
|
+
|
|
153
|
+
# Try to map using technology mapping
|
|
154
|
+
tech_mapping = mapping_config["technology_mapping"]
|
|
155
|
+
if technology in tech_mapping:
|
|
156
|
+
mapped = tech_mapping[technology]
|
|
157
|
+
if len(mapped) == 2:
|
|
158
|
+
return mapped[0], mapped[1], "unspecified"
|
|
159
|
+
elif len(mapped) == 3:
|
|
160
|
+
return mapped[0], mapped[1], mapped[2]
|
|
161
|
+
|
|
162
|
+
# Fallback to type mapping
|
|
163
|
+
type_fallback = mapping_config["type_fallback"]
|
|
164
|
+
if pd.notna(gem_type) and str(gem_type).strip() in type_fallback:
|
|
165
|
+
mapped = type_fallback[str(gem_type).strip()]
|
|
166
|
+
if len(mapped) == 2:
|
|
167
|
+
return mapped[0], mapped[1], "unspecified"
|
|
168
|
+
elif len(mapped) == 3:
|
|
169
|
+
return mapped[0], mapped[1], mapped[2]
|
|
170
|
+
|
|
171
|
+
# Default mapping
|
|
172
|
+
default = mapping_config["default_mapping"]
|
|
173
|
+
return default[0], default[1], default[2]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def get_generators_from_gem(
|
|
177
|
+
country: str,
|
|
178
|
+
gem_data_path: Optional[str] = None,
|
|
179
|
+
technology_types: Optional[List[str]] = None,
|
|
180
|
+
min_capacity_mw: float = 0.0,
|
|
181
|
+
status_filter: Optional[List[str]] = None,
|
|
182
|
+
use_cache: bool = True,
|
|
183
|
+
) -> pd.DataFrame:
|
|
184
|
+
"""
|
|
185
|
+
Load generator data from GEM for a specific country.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
country: ISO 3-letter country code (e.g., "USA", "DEU", "CHN")
|
|
189
|
+
gem_data_path: Optional path to GEM Excel file (overrides default)
|
|
190
|
+
technology_types: Optional list of technology types to filter by
|
|
191
|
+
min_capacity_mw: Minimum capacity in MW (default: 0.0)
|
|
192
|
+
status_filter: Optional list of status values (default: ["operating", "construction"])
|
|
193
|
+
use_cache: Whether to use cached data (default: True)
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
pandas.DataFrame: Generator data with columns:
|
|
197
|
+
- plant_name: Name of the power plant
|
|
198
|
+
- country_iso_3: ISO 3-letter country code
|
|
199
|
+
- category: Energy category (nuclear, thermal, renewables, etc.)
|
|
200
|
+
- carrier: Energy carrier (coal, gas, solar, wind, etc.)
|
|
201
|
+
- type: Technology type (subcritical, combined-cycle, photovoltaic, etc.)
|
|
202
|
+
- capacity_mw: Capacity in megawatts
|
|
203
|
+
- start_year: Year the plant started operation
|
|
204
|
+
- latitude: Latitude coordinate
|
|
205
|
+
- longitude: Longitude coordinate
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
FileNotFoundError: If GEM data file cannot be found
|
|
209
|
+
ValueError: If country code is invalid
|
|
210
|
+
"""
|
|
211
|
+
if gem_data_path:
|
|
212
|
+
set_gem_data_path(gem_data_path)
|
|
213
|
+
|
|
214
|
+
# Validate country code
|
|
215
|
+
country = country.upper()
|
|
216
|
+
if len(country) != 3:
|
|
217
|
+
raise ValueError(f"Country code must be 3 letters, got: {country}")
|
|
218
|
+
|
|
219
|
+
# Set default status filter
|
|
220
|
+
if status_filter is None:
|
|
221
|
+
status_filter = ["operating", "construction"]
|
|
222
|
+
|
|
223
|
+
# Create cache key
|
|
224
|
+
cache_filters = {
|
|
225
|
+
"country": country,
|
|
226
|
+
"technology_types": technology_types,
|
|
227
|
+
"min_capacity_mw": min_capacity_mw,
|
|
228
|
+
"status_filter": status_filter,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# Try to get cached data
|
|
232
|
+
cache = DataCache()
|
|
233
|
+
if use_cache:
|
|
234
|
+
cached_data = cache.get_cached_data("gem_generators", cache_filters)
|
|
235
|
+
if cached_data is not None:
|
|
236
|
+
return cached_data
|
|
237
|
+
|
|
238
|
+
# Load and process data
|
|
239
|
+
logger.info(f"Loading GEM data for country: {country}")
|
|
240
|
+
|
|
241
|
+
gem_file = _get_gem_data_path()
|
|
242
|
+
df = pd.read_excel(gem_file, sheet_name="Power facilities")
|
|
243
|
+
|
|
244
|
+
# Apply status filter
|
|
245
|
+
df = df[df["Status"].isin(status_filter)]
|
|
246
|
+
|
|
247
|
+
# Filter out captive industry use
|
|
248
|
+
df = df[~df["Captive Industry Use"].isin(["power", "heat", "both"])]
|
|
249
|
+
|
|
250
|
+
# Convert country names to ISO codes
|
|
251
|
+
country_codes_3 = coco.convert(
|
|
252
|
+
names=df["Country/area"], to="ISO3", not_found="pass"
|
|
253
|
+
)
|
|
254
|
+
df["country_iso_3"] = country_codes_3
|
|
255
|
+
|
|
256
|
+
# Filter by country
|
|
257
|
+
df = df[df["country_iso_3"] == country]
|
|
258
|
+
|
|
259
|
+
if len(df) == 0:
|
|
260
|
+
logger.warning(f"No generators found for country: {country}")
|
|
261
|
+
return pd.DataFrame()
|
|
262
|
+
|
|
263
|
+
# Rename columns
|
|
264
|
+
df = df.rename(
|
|
265
|
+
columns={
|
|
266
|
+
"Plant / Project name": "plant_name",
|
|
267
|
+
"Capacity (MW)": "capacity_mw",
|
|
268
|
+
"Start year": "start_year",
|
|
269
|
+
"Latitude": "latitude",
|
|
270
|
+
"Longitude": "longitude",
|
|
271
|
+
}
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Clean start_year column - convert non-numeric values to NaN
|
|
275
|
+
df["start_year"] = pd.to_numeric(df["start_year"], errors="coerce")
|
|
276
|
+
|
|
277
|
+
# Load mapping configuration and apply technology mapping
|
|
278
|
+
mapping_config = _load_gem_mapping()
|
|
279
|
+
|
|
280
|
+
df["category"] = None
|
|
281
|
+
df["carrier"] = None
|
|
282
|
+
df["type"] = None
|
|
283
|
+
|
|
284
|
+
for idx, row in df.iterrows():
|
|
285
|
+
category, carrier, tech_type = _map_technology_to_carriers(
|
|
286
|
+
row["Technology"], row["Type"], mapping_config
|
|
287
|
+
)
|
|
288
|
+
df.at[idx, "category"] = category
|
|
289
|
+
df.at[idx, "carrier"] = carrier
|
|
290
|
+
df.at[idx, "type"] = tech_type
|
|
291
|
+
|
|
292
|
+
# Apply filters
|
|
293
|
+
if technology_types:
|
|
294
|
+
df = df[df["carrier"].isin(technology_types)]
|
|
295
|
+
|
|
296
|
+
if min_capacity_mw > 0:
|
|
297
|
+
df = df[df["capacity_mw"] >= min_capacity_mw]
|
|
298
|
+
|
|
299
|
+
# Aggregate plants by key attributes
|
|
300
|
+
df = (
|
|
301
|
+
df.groupby(["country_iso_3", "category", "carrier", "type", "plant_name"])
|
|
302
|
+
.agg(
|
|
303
|
+
{
|
|
304
|
+
"capacity_mw": "sum",
|
|
305
|
+
"start_year": "first",
|
|
306
|
+
"latitude": "first",
|
|
307
|
+
"longitude": "first",
|
|
308
|
+
}
|
|
309
|
+
)
|
|
310
|
+
.reset_index()
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Select final columns
|
|
314
|
+
result_df = df[
|
|
315
|
+
[
|
|
316
|
+
"plant_name",
|
|
317
|
+
"country_iso_3",
|
|
318
|
+
"category",
|
|
319
|
+
"carrier",
|
|
320
|
+
"type",
|
|
321
|
+
"capacity_mw",
|
|
322
|
+
"start_year",
|
|
323
|
+
"latitude",
|
|
324
|
+
"longitude",
|
|
325
|
+
]
|
|
326
|
+
]
|
|
327
|
+
|
|
328
|
+
# Cache the result
|
|
329
|
+
if use_cache:
|
|
330
|
+
cache.cache_data("gem_generators", result_df, cache_filters)
|
|
331
|
+
|
|
332
|
+
logger.info(f"Loaded {len(result_df)} generators for {country}")
|
|
333
|
+
return result_df
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def add_gem_generators_to_network(
|
|
337
|
+
conn: sqlite3.Connection,
|
|
338
|
+
generators_df: pd.DataFrame,
|
|
339
|
+
bus_mapping: Optional[Dict[str, int]] = None,
|
|
340
|
+
carrier_mapping: Optional[Dict[str, int]] = None,
|
|
341
|
+
) -> List[int]:
|
|
342
|
+
"""
|
|
343
|
+
Add GEM generators to a PyConvexity network.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
conn: Database connection
|
|
347
|
+
generators_df: DataFrame from get_generators_from_gem()
|
|
348
|
+
bus_mapping: Optional mapping from region/location to bus IDs
|
|
349
|
+
carrier_mapping: Optional mapping from carrier names to carrier IDs
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
List of created generator component IDs
|
|
353
|
+
|
|
354
|
+
Raises:
|
|
355
|
+
ValueError: If required data is missing
|
|
356
|
+
"""
|
|
357
|
+
if generators_df.empty:
|
|
358
|
+
logger.warning("No generators to add")
|
|
359
|
+
return []
|
|
360
|
+
|
|
361
|
+
created_ids = []
|
|
362
|
+
name_counter = {} # Track duplicate names
|
|
363
|
+
|
|
364
|
+
for _, gen in generators_df.iterrows():
|
|
365
|
+
# Determine bus_id (simplified - could be enhanced with spatial mapping)
|
|
366
|
+
bus_id = None
|
|
367
|
+
if bus_mapping:
|
|
368
|
+
# Try different possible column names for bus assignment
|
|
369
|
+
for col_name in ["region", "nearest_bus", "bus", "bus_name"]:
|
|
370
|
+
if col_name in gen and pd.notna(gen[col_name]):
|
|
371
|
+
bus_id = bus_mapping.get(gen[col_name])
|
|
372
|
+
if bus_id:
|
|
373
|
+
break
|
|
374
|
+
|
|
375
|
+
# Determine carrier_id
|
|
376
|
+
carrier_id = None
|
|
377
|
+
if carrier_mapping:
|
|
378
|
+
carrier_id = carrier_mapping.get(gen["carrier"])
|
|
379
|
+
|
|
380
|
+
# Make generator name unique
|
|
381
|
+
base_name = gen["plant_name"]
|
|
382
|
+
if base_name in name_counter:
|
|
383
|
+
name_counter[base_name] += 1
|
|
384
|
+
unique_name = f"{base_name}_{name_counter[base_name]}"
|
|
385
|
+
else:
|
|
386
|
+
name_counter[base_name] = 0
|
|
387
|
+
unique_name = base_name
|
|
388
|
+
|
|
389
|
+
# Create generator component
|
|
390
|
+
component_id = create_component(
|
|
391
|
+
conn=conn,
|
|
392
|
+
component_type="GENERATOR",
|
|
393
|
+
name=unique_name,
|
|
394
|
+
latitude=gen["latitude"] if pd.notna(gen["latitude"]) else None,
|
|
395
|
+
longitude=gen["longitude"] if pd.notna(gen["longitude"]) else None,
|
|
396
|
+
carrier_id=carrier_id,
|
|
397
|
+
bus_id=bus_id,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Set generator attributes
|
|
401
|
+
set_static_attribute(
|
|
402
|
+
conn, component_id, "p_nom", StaticValue(float(gen["capacity_mw"]))
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# Set marginal costs based on technology
|
|
406
|
+
marginal_cost = 0.0 # Default for renewables (wind, solar, hydro)
|
|
407
|
+
if gen["carrier"] == "gas":
|
|
408
|
+
marginal_cost = 50.0 # €/MWh for gas
|
|
409
|
+
elif gen["carrier"] == "coal":
|
|
410
|
+
marginal_cost = 35.0 # €/MWh for coal
|
|
411
|
+
elif gen["carrier"] == "biomass":
|
|
412
|
+
marginal_cost = 45.0 # €/MWh for biomass
|
|
413
|
+
elif gen["carrier"] == "nuclear":
|
|
414
|
+
marginal_cost = 15.0 # €/MWh for nuclear
|
|
415
|
+
# Wind, solar, hydro, pumped-hydro remain at 0.0
|
|
416
|
+
|
|
417
|
+
set_static_attribute(
|
|
418
|
+
conn, component_id, "marginal_cost", StaticValue(marginal_cost)
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
if pd.notna(gen["start_year"]):
|
|
422
|
+
try:
|
|
423
|
+
set_static_attribute(
|
|
424
|
+
conn,
|
|
425
|
+
component_id,
|
|
426
|
+
"build_year",
|
|
427
|
+
StaticValue(int(gen["start_year"])),
|
|
428
|
+
)
|
|
429
|
+
except:
|
|
430
|
+
pass # Skip if build_year attribute doesn't exist
|
|
431
|
+
|
|
432
|
+
# Technology metadata would be stored here if the database had a metadata table
|
|
433
|
+
# For now, the Ireland demo will use the original generator dataframe for technology info
|
|
434
|
+
|
|
435
|
+
created_ids.append(component_id)
|
|
436
|
+
|
|
437
|
+
logger.debug(
|
|
438
|
+
f"Created generator {component_id}: {gen['plant_name']} ({gen['capacity_mw']} MW)"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
logger.info(f"Added {len(created_ids)} generators to network")
|
|
442
|
+
return created_ids
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input/Output operations for PyConvexity.
|
|
3
|
+
|
|
4
|
+
Provides functionality for importing and exporting energy system models
|
|
5
|
+
in various formats including Excel, CSV, and other data formats.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Import main classes for easy access
|
|
9
|
+
try:
|
|
10
|
+
from .excel_exporter import ExcelModelExporter
|
|
11
|
+
from .excel_importer import ExcelModelImporter
|
|
12
|
+
|
|
13
|
+
__all__ = ["ExcelModelExporter", "ExcelModelImporter"]
|
|
14
|
+
except ImportError:
|
|
15
|
+
# Excel dependencies not available
|
|
16
|
+
__all__ = []
|
|
17
|
+
|
|
18
|
+
# NetCDF I/O functionality
|
|
19
|
+
try:
|
|
20
|
+
from .netcdf_exporter import NetCDFModelExporter
|
|
21
|
+
from .netcdf_importer import NetCDFModelImporter
|
|
22
|
+
|
|
23
|
+
__all__.extend(["NetCDFModelExporter", "NetCDFModelImporter"])
|
|
24
|
+
except ImportError:
|
|
25
|
+
# NetCDF/PyPSA dependencies not available
|
|
26
|
+
pass
|