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.
Files changed (48) hide show
  1. pyconvexity/__init__.py +87 -46
  2. pyconvexity/_version.py +1 -1
  3. pyconvexity/core/__init__.py +3 -5
  4. pyconvexity/core/database.py +111 -103
  5. pyconvexity/core/errors.py +16 -10
  6. pyconvexity/core/types.py +61 -54
  7. pyconvexity/data/__init__.py +0 -1
  8. pyconvexity/data/loaders/cache.py +65 -64
  9. pyconvexity/data/schema/01_core_schema.sql +134 -234
  10. pyconvexity/data/schema/02_data_metadata.sql +38 -168
  11. pyconvexity/data/schema/03_validation_data.sql +327 -264
  12. pyconvexity/data/sources/gem.py +169 -139
  13. pyconvexity/io/__init__.py +4 -10
  14. pyconvexity/io/excel_exporter.py +694 -480
  15. pyconvexity/io/excel_importer.py +817 -545
  16. pyconvexity/io/netcdf_exporter.py +66 -61
  17. pyconvexity/io/netcdf_importer.py +850 -619
  18. pyconvexity/models/__init__.py +109 -59
  19. pyconvexity/models/attributes.py +197 -178
  20. pyconvexity/models/carriers.py +70 -67
  21. pyconvexity/models/components.py +260 -236
  22. pyconvexity/models/network.py +202 -284
  23. pyconvexity/models/results.py +65 -55
  24. pyconvexity/models/scenarios.py +58 -88
  25. pyconvexity/solvers/__init__.py +5 -5
  26. pyconvexity/solvers/pypsa/__init__.py +3 -3
  27. pyconvexity/solvers/pypsa/api.py +150 -134
  28. pyconvexity/solvers/pypsa/batch_loader.py +165 -162
  29. pyconvexity/solvers/pypsa/builder.py +390 -291
  30. pyconvexity/solvers/pypsa/constraints.py +184 -162
  31. pyconvexity/solvers/pypsa/solver.py +968 -666
  32. pyconvexity/solvers/pypsa/storage.py +1377 -671
  33. pyconvexity/timeseries.py +63 -60
  34. pyconvexity/validation/__init__.py +14 -6
  35. pyconvexity/validation/rules.py +95 -84
  36. pyconvexity-0.4.1.dist-info/METADATA +46 -0
  37. pyconvexity-0.4.1.dist-info/RECORD +42 -0
  38. pyconvexity/data/__pycache__/__init__.cpython-313.pyc +0 -0
  39. pyconvexity/data/loaders/__pycache__/__init__.cpython-313.pyc +0 -0
  40. pyconvexity/data/loaders/__pycache__/cache.cpython-313.pyc +0 -0
  41. pyconvexity/data/schema/04_scenario_schema.sql +0 -122
  42. pyconvexity/data/schema/migrate_add_geometries.sql +0 -73
  43. pyconvexity/data/sources/__pycache__/__init__.cpython-313.pyc +0 -0
  44. pyconvexity/data/sources/__pycache__/gem.cpython-313.pyc +0 -0
  45. pyconvexity-0.3.8.post7.dist-info/METADATA +0 -138
  46. pyconvexity-0.3.8.post7.dist-info/RECORD +0 -49
  47. {pyconvexity-0.3.8.post7.dist-info → pyconvexity-0.4.1.dist-info}/WHEEL +0 -0
  48. {pyconvexity-0.3.8.post7.dist-info → pyconvexity-0.4.1.dist-info}/top_level.txt +0 -0
@@ -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 / "examples" / "data" / "raw" / "global-energy-monitor" / "Global-Integrated-Power-August-2025.xlsx",
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("../examples/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
+ ),
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 / "examples" / "schema" / "gem_mapping.yaml",
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, 'r') as f:
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
- 'technology_mapping': {
89
+ "technology_mapping": {
76
90
  # Nuclear
77
- "pressurized water reactor": ["nuclear", "nuclear", "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
- 'type_fallback': {
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
- 'default_mapping': ["unknown", "unspecified", "unspecified"]
129
+ "default_mapping": ["unknown", "unspecified", "unspecified"],
117
130
  }
118
131
 
119
- def _map_technology_to_carriers(technology: str, gem_type: str, mapping_config: Dict[str, Any]) -> tuple:
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['technology_mapping']
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['type_fallback']
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['default_mapping']
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
- 'country': country,
209
- 'technology_types': technology_types,
210
- 'min_capacity_mw': min_capacity_mw,
211
- 'status_filter': status_filter
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('gem_generators', cache_filters)
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='Power facilities')
226
-
242
+ df = pd.read_excel(gem_file, sheet_name="Power facilities")
243
+
227
244
  # Apply status filter
228
- df = df[df['Status'].isin(status_filter)]
229
-
245
+ df = df[df["Status"].isin(status_filter)]
246
+
230
247
  # Filter out captive industry use
231
- df = df[~df['Captive Industry Use'].isin(['power', 'heat', 'both'])]
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['Country/area'],
236
- to='ISO3',
237
- not_found='pass'
252
+ names=df["Country/area"], to="ISO3", not_found="pass"
238
253
  )
239
- df['country_iso_3'] = country_codes_3
240
-
254
+ df["country_iso_3"] = country_codes_3
255
+
241
256
  # Filter by country
242
- df = df[df['country_iso_3'] == country]
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(columns={
250
- 'Plant / Project name': 'plant_name',
251
- 'Capacity (MW)': 'capacity_mw',
252
- 'Start year': 'start_year',
253
- 'Latitude': 'latitude',
254
- 'Longitude': 'longitude'
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['start_year'] = pd.to_numeric(df['start_year'], errors='coerce')
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['category'] = None
264
- df['carrier'] = None
265
- df['type'] = None
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['Technology'], row['Type'], mapping_config
286
+ row["Technology"], row["Type"], mapping_config
270
287
  )
271
- df.at[idx, 'category'] = category
272
- df.at[idx, 'carrier'] = carrier
273
- df.at[idx, 'type'] = tech_type
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['carrier'].isin(technology_types)]
278
-
294
+ df = df[df["carrier"].isin(technology_types)]
295
+
279
296
  if min_capacity_mw > 0:
280
- df = df[df['capacity_mw'] >= min_capacity_mw]
281
-
297
+ df = df[df["capacity_mw"] >= min_capacity_mw]
298
+
282
299
  # Aggregate plants by key attributes
283
300
  df = (
284
- df.groupby(['country_iso_3', 'category', 'carrier', 'type', 'plant_name'])
285
- .agg({
286
- 'capacity_mw': 'sum',
287
- 'start_year': 'first',
288
- 'latitude': 'first',
289
- 'longitude': 'first',
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
- 'plant_name',
297
- 'country_iso_3',
298
- 'category',
299
- 'carrier',
300
- 'type',
301
- 'capacity_mw',
302
- 'start_year',
303
- 'latitude',
304
- 'longitude',
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('gem_generators', result_df, cache_filters)
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 ['region', 'nearest_bus', 'bus', 'bus_name']:
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['carrier'])
359
-
378
+ carrier_id = carrier_mapping.get(gen["carrier"])
379
+
360
380
  # Make generator name unique
361
- base_name = gen['plant_name']
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['latitude'] if pd.notna(gen['latitude']) else None,
376
- longitude=gen['longitude'] if pd.notna(gen['longitude']) else None,
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(conn, component_id, "p_nom", StaticValue(float(gen['capacity_mw'])))
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['carrier'] == 'gas':
407
+ if gen["carrier"] == "gas":
387
408
  marginal_cost = 50.0 # €/MWh for gas
388
- elif gen['carrier'] == 'coal':
409
+ elif gen["carrier"] == "coal":
389
410
  marginal_cost = 35.0 # €/MWh for coal
390
- elif gen['carrier'] == 'biomass':
411
+ elif gen["carrier"] == "biomass":
391
412
  marginal_cost = 45.0 # €/MWh for biomass
392
- elif gen['carrier'] == 'nuclear':
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(conn, component_id, "marginal_cost", StaticValue(marginal_cost))
397
-
398
- if pd.notna(gen['start_year']):
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(conn, component_id, "build_year", StaticValue(int(gen['start_year'])))
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(f"Created generator {component_id}: {gen['plant_name']} ({gen['capacity_mw']} MW)")
410
-
411
- logger.info(f"Added {len(created_ids)} generators to network {network_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")
412
442
  return created_ids
@@ -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