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/models/components.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
""",
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
|
97
|
+
WHERE component_type = ?
|
|
100
98
|
ORDER BY name
|
|
101
|
-
""",
|
|
99
|
+
""",
|
|
100
|
+
(component_type.upper(),),
|
|
101
|
+
)
|
|
102
102
|
else:
|
|
103
|
-
cursor = conn.execute(
|
|
104
|
-
|
|
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
|
-
"""
|
|
110
|
-
|
|
109
|
+
"""
|
|
110
|
+
)
|
|
111
|
+
|
|
111
112
|
components = []
|
|
112
113
|
for row in cursor.fetchall():
|
|
113
|
-
components.append(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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() !=
|
|
148
|
-
carrier_id = get_default_carrier_id(conn, request.
|
|
149
|
-
elif request.component_type.upper() ==
|
|
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
|
-
|
|
158
|
+
component_type, name, longitude, latitude,
|
|
156
159
|
carrier_id, bus_id, bus0_id, bus1_id,
|
|
157
160
|
created_at, updated_at
|
|
158
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
159
|
-
""",
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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(
|
|
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(
|
|
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
|
-
""",
|
|
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,
|
|
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
|
-
|
|
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
|
|
387
|
-
""",
|
|
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
|
|
392
|
-
|
|
399
|
+
raise ComponentNotFound(f"Component '{name}' not found")
|
|
400
|
+
|
|
393
401
|
return Component(
|
|
394
402
|
id=row[0],
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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,
|
|
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,
|
|
428
|
+
component = get_component_by_name(conn, name)
|
|
423
429
|
return component.id
|
|
424
430
|
|
|
425
431
|
|
|
426
|
-
def component_exists(conn: sqlite3.Connection,
|
|
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
|
-
|
|
440
|
-
|
|
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.
|
|
471
|
-
""",
|
|
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
|
-
|
|
482
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}.get(component_type.upper(),
|
|
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
|
-
|
|
509
|
-
|
|
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
|
-
|
|
518
|
-
|
|
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
|
-
|
|
527
|
-
|
|
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(
|
|
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
|
|
557
|
+
WHERE bus_id = ? AND component_type = 'UNMET_LOAD'
|
|
548
558
|
LIMIT 1
|
|
549
|
-
""",
|
|
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,
|
|
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
|
-
|
|
576
|
+
component_type, name, carrier_id, bus_id,
|
|
564
577
|
created_at, updated_at
|
|
565
|
-
) VALUES (
|
|
566
|
-
""",
|
|
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(
|
|
575
|
-
|
|
576
|
-
|
|
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
|
|
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
|
|
585
|
-
"""
|
|
586
|
-
|
|
607
|
+
WHERE component_type = 'BUS'
|
|
608
|
+
"""
|
|
609
|
+
)
|
|
610
|
+
|
|
587
611
|
return {row[0]: row[1] for row in cursor.fetchall()}
|