pyconvexity 0.4.0__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 (43) 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 -663
  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/schema/04_scenario_schema.sql +0 -122
  39. pyconvexity/data/schema/migrate_add_geometries.sql +0 -73
  40. pyconvexity-0.4.0.dist-info/METADATA +0 -138
  41. pyconvexity-0.4.0.dist-info/RECORD +0 -44
  42. {pyconvexity-0.4.0.dist-info → pyconvexity-0.4.1.dist-info}/WHEEL +0 -0
  43. {pyconvexity-0.4.0.dist-info → pyconvexity-0.4.1.dist-info}/top_level.txt +0 -0
@@ -8,29 +8,27 @@ with proper validation and error handling.
8
8
  import sqlite3
9
9
  from typing import Dict, Any, Optional, List
10
10
 
11
- from pyconvexity.core.types import (
12
- Component, CreateComponentRequest, StaticValue
13
- )
14
- from pyconvexity.core.errors import (
15
- ComponentNotFound, ValidationError, DatabaseError
16
- )
11
+ from pyconvexity.core.types import Component, CreateComponentRequest, StaticValue
12
+ from pyconvexity.core.errors import ComponentNotFound, ValidationError, DatabaseError
17
13
 
18
14
 
19
15
  def get_component_type(conn: sqlite3.Connection, component_id: int) -> str:
20
16
  """
21
17
  Get component type for a component ID.
22
-
18
+
23
19
  Args:
24
20
  conn: Database connection
25
21
  component_id: ID of the component
26
-
22
+
27
23
  Returns:
28
24
  Component type string (e.g., "BUS", "GENERATOR")
29
-
25
+
30
26
  Raises:
31
27
  ComponentNotFound: If component doesn't exist
32
28
  """
33
- cursor = conn.execute("SELECT component_type FROM components WHERE id = ?", (component_id,))
29
+ cursor = conn.execute(
30
+ "SELECT component_type FROM components WHERE id = ?", (component_id,)
31
+ )
34
32
  row = cursor.fetchone()
35
33
  if not row:
36
34
  raise ComponentNotFound(component_id)
@@ -39,104 +37,108 @@ def get_component_type(conn: sqlite3.Connection, component_id: int) -> str:
39
37
 
40
38
  def get_component(conn: sqlite3.Connection, component_id: int) -> Component:
41
39
  """
42
- Get component by ID.
43
-
40
+ Get component by ID (single network per database).
41
+
44
42
  Args:
45
43
  conn: Database connection
46
44
  component_id: ID of the component
47
-
45
+
48
46
  Returns:
49
47
  Component object with all fields populated
50
-
48
+
51
49
  Raises:
52
50
  ComponentNotFound: If component doesn't exist
53
51
  """
54
- cursor = conn.execute("""
55
- SELECT id, network_id, component_type, name, longitude, latitude,
52
+ cursor = conn.execute(
53
+ """
54
+ SELECT id, component_type, name, longitude, latitude,
56
55
  carrier_id, bus_id, bus0_id, bus1_id
57
56
  FROM components WHERE id = ?
58
- """, (component_id,))
59
-
57
+ """,
58
+ (component_id,),
59
+ )
60
+
60
61
  row = cursor.fetchone()
61
62
  if not row:
62
63
  raise ComponentNotFound(component_id)
63
-
64
+
64
65
  return Component(
65
66
  id=row[0],
66
- network_id=row[1],
67
- component_type=row[2],
68
- name=row[3],
69
- longitude=row[4],
70
- latitude=row[5],
71
- carrier_id=row[6],
72
- bus_id=row[7],
73
- bus0_id=row[8],
74
- bus1_id=row[9]
67
+ component_type=row[1],
68
+ name=row[2],
69
+ longitude=row[3],
70
+ latitude=row[4],
71
+ carrier_id=row[5],
72
+ bus_id=row[6],
73
+ bus0_id=row[7],
74
+ bus1_id=row[8],
75
75
  )
76
76
 
77
77
 
78
78
  def list_components_by_type(
79
- conn: sqlite3.Connection,
80
- network_id: int,
81
- component_type: Optional[str] = None
79
+ conn: sqlite3.Connection, component_type: Optional[str] = None
82
80
  ) -> List[Component]:
83
81
  """
84
- List components by type.
85
-
82
+ List components by type (single network per database).
83
+
86
84
  Args:
87
85
  conn: Database connection
88
- network_id: Network ID to filter by
89
86
  component_type: Optional component type filter (e.g., "BUS", "GENERATOR")
90
-
87
+
91
88
  Returns:
92
89
  List of Component objects
93
90
  """
94
91
  if component_type:
95
- cursor = conn.execute("""
96
- SELECT id, network_id, component_type, name, longitude, latitude,
92
+ cursor = conn.execute(
93
+ """
94
+ SELECT id, component_type, name, longitude, latitude,
97
95
  carrier_id, bus_id, bus0_id, bus1_id
98
96
  FROM components
99
- WHERE network_id = ? AND component_type = ?
97
+ WHERE component_type = ?
100
98
  ORDER BY name
101
- """, (network_id, component_type.upper()))
99
+ """,
100
+ (component_type.upper(),),
101
+ )
102
102
  else:
103
- cursor = conn.execute("""
104
- SELECT id, network_id, component_type, name, longitude, latitude,
103
+ cursor = conn.execute(
104
+ """
105
+ SELECT id, component_type, name, longitude, latitude,
105
106
  carrier_id, bus_id, bus0_id, bus1_id
106
107
  FROM components
107
- WHERE network_id = ?
108
108
  ORDER BY component_type, name
109
- """, (network_id,))
110
-
109
+ """
110
+ )
111
+
111
112
  components = []
112
113
  for row in cursor.fetchall():
113
- components.append(Component(
114
- id=row[0],
115
- network_id=row[1],
116
- component_type=row[2],
117
- name=row[3],
118
- longitude=row[4],
119
- latitude=row[5],
120
- carrier_id=row[6],
121
- bus_id=row[7],
122
- bus0_id=row[8],
123
- bus1_id=row[9]
124
- ))
125
-
114
+ components.append(
115
+ Component(
116
+ id=row[0],
117
+ component_type=row[1],
118
+ name=row[2],
119
+ longitude=row[3],
120
+ latitude=row[4],
121
+ carrier_id=row[5],
122
+ bus_id=row[6],
123
+ bus0_id=row[7],
124
+ bus1_id=row[8],
125
+ )
126
+ )
127
+
126
128
  return components
127
129
 
128
130
 
129
131
  def insert_component(conn: sqlite3.Connection, request: CreateComponentRequest) -> int:
130
132
  """
131
- Insert a new component.
132
-
133
+ Insert a new component (single network per database).
134
+
133
135
  Args:
134
136
  conn: Database connection
135
137
  request: Component creation request with all necessary fields
136
-
138
+
137
139
  Returns:
138
140
  ID of the newly created component
139
-
141
+
140
142
  Raises:
141
143
  DatabaseError: If insertion fails
142
144
  ValidationError: If required fields are missing
@@ -144,58 +146,59 @@ def insert_component(conn: sqlite3.Connection, request: CreateComponentRequest)
144
146
  # Determine carrier_id - use provided value or auto-assign default
145
147
  # CONSTRAINT components must have carrier_id=None per database schema
146
148
  carrier_id = request.carrier_id
147
- if carrier_id is None and request.component_type.upper() != 'CONSTRAINT':
148
- carrier_id = get_default_carrier_id(conn, request.network_id, request.component_type)
149
- elif request.component_type.upper() == 'CONSTRAINT':
149
+ if carrier_id is None and request.component_type.upper() != "CONSTRAINT":
150
+ carrier_id = get_default_carrier_id(conn, request.component_type)
151
+ elif request.component_type.upper() == "CONSTRAINT":
150
152
  carrier_id = None # Explicitly keep None for constraints
151
-
153
+
152
154
  # Insert the component
153
- cursor = conn.execute("""
155
+ cursor = conn.execute(
156
+ """
154
157
  INSERT INTO components (
155
- network_id, component_type, name, longitude, latitude,
158
+ component_type, name, longitude, latitude,
156
159
  carrier_id, bus_id, bus0_id, bus1_id,
157
160
  created_at, updated_at
158
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
159
- """, (
160
- request.network_id,
161
- request.component_type,
162
- request.name,
163
- request.longitude,
164
- request.latitude,
165
- carrier_id,
166
- request.bus_id,
167
- request.bus0_id,
168
- request.bus1_id
169
- ))
170
-
161
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
162
+ """,
163
+ (
164
+ request.component_type,
165
+ request.name,
166
+ request.longitude,
167
+ request.latitude,
168
+ carrier_id,
169
+ request.bus_id,
170
+ request.bus0_id,
171
+ request.bus1_id,
172
+ ),
173
+ )
174
+
171
175
  component_id = cursor.lastrowid
172
176
  if not component_id:
173
177
  raise DatabaseError("Failed to create component")
174
-
178
+
175
179
  # If description is provided, store it as an attribute
176
180
  if request.description:
177
181
  from pyconvexity.models.attributes import set_static_attribute
178
- set_static_attribute(conn, component_id, "description", StaticValue(request.description))
179
-
182
+
183
+ set_static_attribute(
184
+ conn, component_id, "description", StaticValue(request.description)
185
+ )
186
+
180
187
  # If this is a BUS, ensure unmet load exists
181
188
  if request.component_type.upper() == "BUS":
182
- # Get unmet_load_active flag from network
183
- cursor = conn.execute("""
184
- SELECT COALESCE(unmet_load_active, 1) FROM networks WHERE id = ?
185
- """, (request.network_id,))
186
-
187
- row = cursor.fetchone()
188
- # Explicitly convert to boolean to avoid int/bool type confusion
189
- unmet_load_active = bool(row[0]) if row else True
190
-
191
- ensure_unmet_load_for_bus(conn, request.network_id, component_id, request.name, unmet_load_active)
192
-
189
+ # Get unmet_load_active flag from network config
190
+ from pyconvexity.models.network import get_network_config
191
+
192
+ network_config = get_network_config(conn)
193
+ unmet_load_active = network_config.get("unmet_load_active", True)
194
+
195
+ ensure_unmet_load_for_bus(conn, component_id, request.name, unmet_load_active)
196
+
193
197
  return component_id
194
198
 
195
199
 
196
200
  def create_component(
197
201
  conn: sqlite3.Connection,
198
- network_id: int,
199
202
  component_type: str,
200
203
  name: str,
201
204
  description: Optional[str] = None,
@@ -204,14 +207,13 @@ def create_component(
204
207
  carrier_id: Optional[int] = None,
205
208
  bus_id: Optional[int] = None,
206
209
  bus0_id: Optional[int] = None,
207
- bus1_id: Optional[int] = None
210
+ bus1_id: Optional[int] = None,
208
211
  ) -> int:
209
212
  """
210
- Create a component and return its ID - convenience function.
211
-
213
+ Create a component and return its ID - convenience function (single network per database).
214
+
212
215
  Args:
213
216
  conn: Database connection
214
- network_id: Network ID
215
217
  component_type: Type of component (e.g., "BUS", "GENERATOR")
216
218
  name: Component name
217
219
  description: Optional description
@@ -221,12 +223,11 @@ def create_component(
221
223
  bus_id: Optional bus ID (for single-bus components)
222
224
  bus0_id: Optional first bus ID (for two-bus components)
223
225
  bus1_id: Optional second bus ID (for two-bus components)
224
-
226
+
225
227
  Returns:
226
228
  ID of the newly created component
227
229
  """
228
230
  request = CreateComponentRequest(
229
- network_id=network_id,
230
231
  component_type=component_type,
231
232
  name=name,
232
233
  description=description,
@@ -235,7 +236,7 @@ def create_component(
235
236
  carrier_id=carrier_id,
236
237
  bus_id=bus_id,
237
238
  bus0_id=bus0_id,
238
- bus1_id=bus1_id
239
+ bus1_id=bus1_id,
239
240
  )
240
241
  return insert_component(conn, request)
241
242
 
@@ -250,11 +251,11 @@ def update_component(
250
251
  carrier_id: Optional[int] = None,
251
252
  bus_id: Optional[int] = None,
252
253
  bus0_id: Optional[int] = None,
253
- bus1_id: Optional[int] = None
254
+ bus1_id: Optional[int] = None,
254
255
  ) -> None:
255
256
  """
256
257
  Update a component.
257
-
258
+
258
259
  Args:
259
260
  conn: Database connection
260
261
  component_id: ID of component to update
@@ -266,14 +267,14 @@ def update_component(
266
267
  bus_id: New bus ID (optional)
267
268
  bus0_id: New first bus ID (optional)
268
269
  bus1_id: New second bus ID (optional)
269
-
270
+
270
271
  Raises:
271
272
  ComponentNotFound: If component doesn't exist
272
273
  """
273
274
  # Build dynamic SQL based on what fields are being updated
274
275
  set_clauses = []
275
276
  params = []
276
-
277
+
277
278
  if name is not None:
278
279
  set_clauses.append("name = ?")
279
280
  params.append(name)
@@ -295,21 +296,22 @@ def update_component(
295
296
  if bus1_id is not None:
296
297
  set_clauses.append("bus1_id = ?")
297
298
  params.append(bus1_id)
298
-
299
+
299
300
  # Update component table fields if any changes
300
301
  if set_clauses:
301
302
  set_clauses.append("updated_at = datetime('now')")
302
303
  params.append(component_id)
303
-
304
+
304
305
  sql = f"UPDATE components SET {', '.join(set_clauses)} WHERE id = ?"
305
306
  cursor = conn.execute(sql, params)
306
-
307
+
307
308
  if cursor.rowcount == 0:
308
309
  raise ComponentNotFound(component_id)
309
-
310
+
310
311
  # Handle description as an attribute
311
312
  if description is not None:
312
313
  from pyconvexity.models.attributes import set_static_attribute, delete_attribute
314
+
313
315
  if description == "":
314
316
  # Remove description attribute if empty
315
317
  try:
@@ -318,148 +320,152 @@ def update_component(
318
320
  pass # Already doesn't exist
319
321
  else:
320
322
  # Set description as attribute
321
- set_static_attribute(conn, component_id, "description", StaticValue(description))
323
+ set_static_attribute(
324
+ conn, component_id, "description", StaticValue(description)
325
+ )
322
326
 
323
327
 
324
328
  def delete_component(conn: sqlite3.Connection, component_id: int) -> None:
325
329
  """
326
330
  Delete a component and all its attributes.
327
-
331
+
328
332
  Args:
329
333
  conn: Database connection
330
334
  component_id: ID of component to delete
331
-
335
+
332
336
  Raises:
333
337
  ComponentNotFound: If component doesn't exist
334
338
  """
335
339
  # First delete all component attributes
336
- conn.execute("DELETE FROM component_attributes WHERE component_id = ?", (component_id,))
337
-
340
+ conn.execute(
341
+ "DELETE FROM component_attributes WHERE component_id = ?", (component_id,)
342
+ )
343
+
338
344
  # Then delete the component itself
339
345
  cursor = conn.execute("DELETE FROM components WHERE id = ?", (component_id,))
340
-
346
+
341
347
  if cursor.rowcount == 0:
342
348
  raise ComponentNotFound(component_id)
343
349
 
344
350
 
345
- def list_component_attributes(
346
- conn: sqlite3.Connection,
347
- component_id: int
348
- ) -> List[str]:
351
+ def list_component_attributes(conn: sqlite3.Connection, component_id: int) -> List[str]:
349
352
  """
350
353
  List all attribute names for a component.
351
-
354
+
352
355
  Args:
353
356
  conn: Database connection
354
357
  component_id: Component ID
355
-
358
+
356
359
  Returns:
357
360
  List of attribute names
358
361
  """
359
- cursor = conn.execute("""
362
+ cursor = conn.execute(
363
+ """
360
364
  SELECT attribute_name FROM component_attributes
361
365
  WHERE component_id = ? ORDER BY attribute_name
362
- """, (component_id,))
363
-
366
+ """,
367
+ (component_id,),
368
+ )
369
+
364
370
  return [row[0] for row in cursor.fetchall()]
365
371
 
366
372
 
367
- def get_component_by_name(conn: sqlite3.Connection, network_id: int, name: str) -> Component:
373
+ def get_component_by_name(conn: sqlite3.Connection, name: str) -> Component:
368
374
  """
369
- Get a component by name.
370
-
375
+ Get a component by name (single network per database).
376
+
371
377
  Args:
372
378
  conn: Database connection
373
- network_id: Network ID
374
379
  name: Component name
375
-
380
+
376
381
  Returns:
377
382
  Component object
378
-
383
+
379
384
  Raises:
380
385
  ComponentNotFound: If component doesn't exist
381
386
  """
382
- cursor = conn.execute("""
383
- SELECT id, network_id, component_type, name, longitude, latitude,
387
+ cursor = conn.execute(
388
+ """
389
+ SELECT id, component_type, name, longitude, latitude,
384
390
  carrier_id, bus_id, bus0_id, bus1_id
385
391
  FROM components
386
- WHERE network_id = ? AND name = ?
387
- """, (network_id, name))
388
-
392
+ WHERE name = ?
393
+ """,
394
+ (name,),
395
+ )
396
+
389
397
  row = cursor.fetchone()
390
398
  if not row:
391
- raise ComponentNotFound(f"Component '{name}' not found in network {network_id}")
392
-
399
+ raise ComponentNotFound(f"Component '{name}' not found")
400
+
393
401
  return Component(
394
402
  id=row[0],
395
- network_id=row[1],
396
- component_type=row[2],
397
- name=row[3],
398
- longitude=row[4],
399
- latitude=row[5],
400
- carrier_id=row[6],
401
- bus_id=row[7],
402
- bus0_id=row[8],
403
- bus1_id=row[9]
403
+ component_type=row[1],
404
+ name=row[2],
405
+ longitude=row[3],
406
+ latitude=row[4],
407
+ carrier_id=row[5],
408
+ bus_id=row[6],
409
+ bus0_id=row[7],
410
+ bus1_id=row[8],
404
411
  )
405
412
 
406
413
 
407
- def get_component_id(conn: sqlite3.Connection, network_id: int, name: str) -> int:
414
+ def get_component_id(conn: sqlite3.Connection, name: str) -> int:
408
415
  """
409
- Get component ID by name.
410
-
416
+ Get component ID by name (single network per database).
417
+
411
418
  Args:
412
419
  conn: Database connection
413
- network_id: Network ID
414
420
  name: Component name
415
-
421
+
416
422
  Returns:
417
423
  Component ID
418
-
424
+
419
425
  Raises:
420
426
  ComponentNotFound: If component doesn't exist
421
427
  """
422
- component = get_component_by_name(conn, network_id, name)
428
+ component = get_component_by_name(conn, name)
423
429
  return component.id
424
430
 
425
431
 
426
- def component_exists(conn: sqlite3.Connection, network_id: int, name: str) -> bool:
432
+ def component_exists(conn: sqlite3.Connection, name: str) -> bool:
427
433
  """
428
- Check if a component exists.
429
-
434
+ Check if a component exists (single network per database).
435
+
430
436
  Args:
431
437
  conn: Database connection
432
- network_id: Network ID
433
438
  name: Component name
434
-
439
+
435
440
  Returns:
436
441
  True if component exists, False otherwise
437
442
  """
438
- cursor = conn.execute("""
439
- SELECT 1 FROM components WHERE network_id = ? AND name = ?
440
- """, (network_id, name))
441
-
443
+ cursor = conn.execute(
444
+ """
445
+ SELECT 1 FROM components WHERE name = ?
446
+ """,
447
+ (name,),
448
+ )
449
+
442
450
  return cursor.fetchone() is not None
443
451
 
444
452
 
445
453
  def get_component_carrier_map(
446
- conn: sqlite3.Connection,
447
- network_id: int,
448
- component_type: Optional[str] = None
454
+ conn: sqlite3.Connection, component_type: Optional[str] = None
449
455
  ) -> Dict[str, str]:
450
456
  """
451
- Get mapping from component names to carrier names.
452
-
457
+ Get mapping from component names to carrier names (single network per database).
458
+
453
459
  Args:
454
460
  conn: Database connection
455
- network_id: Network ID
456
461
  component_type: Optional component type filter
457
-
462
+
458
463
  Returns:
459
464
  Dictionary mapping component names to carrier names
460
465
  """
461
466
  if component_type:
462
- cursor = conn.execute("""
467
+ cursor = conn.execute(
468
+ """
463
469
  SELECT c.name,
464
470
  CASE
465
471
  WHEN c.component_type = 'UNMET_LOAD' THEN 'Unmet Load'
@@ -467,10 +473,13 @@ def get_component_carrier_map(
467
473
  END as carrier_name
468
474
  FROM components c
469
475
  LEFT JOIN carriers carr ON c.carrier_id = carr.id
470
- WHERE c.network_id = ? AND c.component_type = ?
471
- """, (network_id, component_type.upper()))
476
+ WHERE c.component_type = ?
477
+ """,
478
+ (component_type.upper(),),
479
+ )
472
480
  else:
473
- cursor = conn.execute("""
481
+ cursor = conn.execute(
482
+ """
474
483
  SELECT c.name,
475
484
  CASE
476
485
  WHEN c.component_type = 'UNMET_LOAD' THEN 'Unmet Load'
@@ -478,110 +487,125 @@ def get_component_carrier_map(
478
487
  END as carrier_name
479
488
  FROM components c
480
489
  LEFT JOIN carriers carr ON c.carrier_id = carr.id
481
- WHERE c.network_id = ?
482
- """, (network_id,))
483
-
490
+ """
491
+ )
492
+
484
493
  return {row[0]: row[1] for row in cursor.fetchall()}
485
494
 
486
495
 
487
496
  # Helper functions
488
497
 
489
- def get_default_carrier_id(
490
- conn: sqlite3.Connection,
491
- network_id: int,
492
- component_type: str
493
- ) -> int:
494
- """Get default carrier ID for a component type."""
498
+
499
+ def get_default_carrier_id(conn: sqlite3.Connection, component_type: str) -> int:
500
+ """Get default carrier ID for a component type (single network per database)."""
495
501
  # Default carrier names based on PyPSA conventions
496
502
  default_carrier_name = {
497
- 'BUS': 'AC',
498
- 'GENERATOR': 'electricity',
499
- 'LOAD': 'electricity',
500
- 'STORAGE_UNIT': 'electricity',
501
- 'STORE': 'electricity',
502
- 'LINE': 'AC',
503
- 'LINK': 'AC'
504
- }.get(component_type.upper(), 'AC')
505
-
503
+ "BUS": "AC",
504
+ "GENERATOR": "electricity",
505
+ "LOAD": "electricity",
506
+ "STORAGE_UNIT": "electricity",
507
+ "STORE": "electricity",
508
+ "LINE": "AC",
509
+ "LINK": "AC",
510
+ }.get(component_type.upper(), "AC")
511
+
506
512
  # Try to find the default carrier
507
- cursor = conn.execute("""
508
- SELECT id FROM carriers WHERE network_id = ? AND name = ? LIMIT 1
509
- """, (network_id, default_carrier_name))
510
-
513
+ cursor = conn.execute(
514
+ """
515
+ SELECT id FROM carriers WHERE name = ? LIMIT 1
516
+ """,
517
+ (default_carrier_name,),
518
+ )
519
+
511
520
  row = cursor.fetchone()
512
521
  if row:
513
522
  return row[0]
514
-
523
+
515
524
  # If not found, try AC
516
- cursor = conn.execute("""
517
- SELECT id FROM carriers WHERE network_id = ? AND name = 'AC' LIMIT 1
518
- """, (network_id,))
519
-
525
+ cursor = conn.execute(
526
+ """
527
+ SELECT id FROM carriers WHERE name = 'AC' LIMIT 1
528
+ """
529
+ )
530
+
520
531
  row = cursor.fetchone()
521
532
  if row:
522
533
  return row[0]
523
-
534
+
524
535
  # If still not found, get any carrier
525
- cursor = conn.execute("""
526
- SELECT id FROM carriers WHERE network_id = ? LIMIT 1
527
- """, (network_id,))
528
-
536
+ cursor = conn.execute(
537
+ """
538
+ SELECT id FROM carriers LIMIT 1
539
+ """
540
+ )
541
+
529
542
  row = cursor.fetchone()
530
543
  if row:
531
544
  return row[0]
532
-
533
- raise DatabaseError(f"No carriers found in network {network_id}")
545
+
546
+ raise DatabaseError("No carriers found in database")
534
547
 
535
548
 
536
549
  def ensure_unmet_load_for_bus(
537
- conn: sqlite3.Connection,
538
- network_id: int,
539
- bus_id: int,
540
- bus_name: str,
541
- unmet_load_active: bool
550
+ conn: sqlite3.Connection, bus_id: int, bus_name: str, unmet_load_active: bool
542
551
  ) -> None:
543
- """Ensure there is exactly one UNMET_LOAD per bus."""
552
+ """Ensure there is exactly one UNMET_LOAD per bus (single network per database)."""
544
553
  # Check if unmet load already exists for this bus
545
- cursor = conn.execute("""
554
+ cursor = conn.execute(
555
+ """
546
556
  SELECT id FROM components
547
- WHERE network_id = ? AND bus_id = ? AND component_type = 'UNMET_LOAD'
557
+ WHERE bus_id = ? AND component_type = 'UNMET_LOAD'
548
558
  LIMIT 1
549
- """, (network_id, bus_id))
550
-
559
+ """,
560
+ (bus_id,),
561
+ )
562
+
551
563
  if cursor.fetchone():
552
564
  return # Already exists
553
-
565
+
554
566
  # Get default carrier for generators (unmet loads are treated as generators)
555
- carrier_id = get_default_carrier_id(conn, network_id, "GENERATOR")
556
-
567
+ carrier_id = get_default_carrier_id(conn, "GENERATOR")
568
+
557
569
  # Insert unmet load component - sanitize bus name for PyPSA compatibility
558
570
  # Remove spaces, periods, and other problematic characters
559
571
  sanitized_bus_name = bus_name.replace(" ", "_").replace(".", "_").replace("-", "_")
560
572
  name = f"unmet_load_{sanitized_bus_name}"
561
- cursor = conn.execute("""
573
+ cursor = conn.execute(
574
+ """
562
575
  INSERT INTO components (
563
- network_id, component_type, name, carrier_id, bus_id,
576
+ component_type, name, carrier_id, bus_id,
564
577
  created_at, updated_at
565
- ) VALUES (?, 'UNMET_LOAD', ?, ?, ?, datetime('now'), datetime('now'))
566
- """, (network_id, name, carrier_id, bus_id))
567
-
578
+ ) VALUES ('UNMET_LOAD', ?, ?, ?, datetime('now'), datetime('now'))
579
+ """,
580
+ (name, carrier_id, bus_id),
581
+ )
582
+
568
583
  unmet_load_id = cursor.lastrowid
569
-
584
+
570
585
  # Set fixed attributes for unmet load
571
586
  from pyconvexity.models.attributes import set_static_attribute
587
+
572
588
  set_static_attribute(conn, unmet_load_id, "marginal_cost", StaticValue(1e6))
573
589
  set_static_attribute(conn, unmet_load_id, "p_nom", StaticValue(1e6))
574
- set_static_attribute(conn, unmet_load_id, "p_max_pu", StaticValue(1.0)) # Can run at full capacity
575
- set_static_attribute(conn, unmet_load_id, "p_min_pu", StaticValue(0.0)) # Can be turned off
576
- set_static_attribute(conn, unmet_load_id, "sign", StaticValue(1.0)) # Positive power sign (generation)
590
+ set_static_attribute(
591
+ conn, unmet_load_id, "p_max_pu", StaticValue(1.0)
592
+ ) # Can run at full capacity
593
+ set_static_attribute(
594
+ conn, unmet_load_id, "p_min_pu", StaticValue(0.0)
595
+ ) # Can be turned off
596
+ set_static_attribute(
597
+ conn, unmet_load_id, "sign", StaticValue(1.0)
598
+ ) # Positive power sign (generation)
577
599
  set_static_attribute(conn, unmet_load_id, "active", StaticValue(unmet_load_active))
578
600
 
579
601
 
580
- def get_bus_name_to_id_map(conn: sqlite3.Connection, network_id: int) -> Dict[str, int]:
581
- """Get mapping from bus names to component IDs."""
582
- cursor = conn.execute("""
602
+ def get_bus_name_to_id_map(conn: sqlite3.Connection) -> Dict[str, int]:
603
+ """Get mapping from bus names to component IDs (single network per database)."""
604
+ cursor = conn.execute(
605
+ """
583
606
  SELECT name, id FROM components
584
- WHERE network_id = ? AND component_type = 'BUS'
585
- """, (network_id,))
586
-
607
+ WHERE component_type = 'BUS'
608
+ """
609
+ )
610
+
587
611
  return {row[0]: row[1] for row in cursor.fetchall()}