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