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