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/network.py
CHANGED
|
@@ -11,484 +11,402 @@ import logging
|
|
|
11
11
|
from typing import Dict, Any, Optional, List
|
|
12
12
|
from datetime import datetime, timezone
|
|
13
13
|
|
|
14
|
-
from pyconvexity.core.types import
|
|
15
|
-
|
|
16
|
-
)
|
|
17
|
-
from pyconvexity.core.errors import (
|
|
18
|
-
ValidationError, DatabaseError
|
|
19
|
-
)
|
|
14
|
+
from pyconvexity.core.types import CreateNetworkRequest, TimePeriod, Network
|
|
15
|
+
from pyconvexity.core.errors import ValidationError, DatabaseError
|
|
20
16
|
|
|
21
17
|
logger = logging.getLogger(__name__)
|
|
22
18
|
|
|
23
19
|
|
|
24
|
-
def create_network(conn: sqlite3.Connection, request: CreateNetworkRequest) ->
|
|
20
|
+
def create_network(conn: sqlite3.Connection, request: CreateNetworkRequest) -> None:
|
|
25
21
|
"""
|
|
26
|
-
Create
|
|
27
|
-
|
|
22
|
+
Create network metadata (single network per database).
|
|
23
|
+
|
|
28
24
|
Args:
|
|
29
25
|
conn: Database connection
|
|
30
26
|
request: Network creation request
|
|
31
|
-
|
|
32
|
-
Returns:
|
|
33
|
-
ID of the newly created network
|
|
34
|
-
|
|
27
|
+
|
|
35
28
|
Raises:
|
|
36
29
|
ValidationError: If required fields are missing
|
|
37
30
|
DatabaseError: If creation fails
|
|
38
31
|
"""
|
|
39
|
-
|
|
32
|
+
|
|
40
33
|
# Validate required fields
|
|
41
34
|
if not request.start_time:
|
|
42
35
|
raise ValidationError("start_time is required")
|
|
43
36
|
if not request.end_time:
|
|
44
37
|
raise ValidationError("end_time is required")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
|
|
39
|
+
# Insert into network_metadata table (single row per database)
|
|
40
|
+
conn.execute(
|
|
41
|
+
"""
|
|
42
|
+
INSERT INTO network_metadata (name, description, time_start, time_end, time_interval, created_at, updated_at)
|
|
48
43
|
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
49
|
-
""",
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# No need to call create_master_scenario() manually
|
|
63
|
-
|
|
64
|
-
return network_id
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def get_network_info(conn: sqlite3.Connection, network_id: int) -> Dict[str, Any]:
|
|
44
|
+
""",
|
|
45
|
+
(
|
|
46
|
+
request.name,
|
|
47
|
+
request.description
|
|
48
|
+
or f"Created on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
|
49
|
+
request.start_time,
|
|
50
|
+
request.end_time,
|
|
51
|
+
request.time_resolution or "PT1H", # ISO 8601 duration format
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_network_info(conn: sqlite3.Connection) -> Dict[str, Any]:
|
|
68
57
|
"""
|
|
69
|
-
Get network information.
|
|
70
|
-
|
|
58
|
+
Get network information (single network per database).
|
|
59
|
+
|
|
71
60
|
Args:
|
|
72
61
|
conn: Database connection
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
|
|
75
63
|
Returns:
|
|
76
64
|
Dictionary with network information
|
|
77
|
-
|
|
65
|
+
|
|
78
66
|
Raises:
|
|
79
|
-
ValidationError: If network doesn't exist
|
|
67
|
+
ValidationError: If network metadata doesn't exist
|
|
80
68
|
"""
|
|
81
|
-
cursor = conn.execute(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
69
|
+
cursor = conn.execute(
|
|
70
|
+
"""
|
|
71
|
+
SELECT name, description, time_start, time_end, time_interval, created_at, updated_at
|
|
72
|
+
FROM network_metadata
|
|
73
|
+
LIMIT 1
|
|
74
|
+
"""
|
|
75
|
+
)
|
|
76
|
+
|
|
87
77
|
row = cursor.fetchone()
|
|
88
78
|
if not row:
|
|
89
|
-
raise ValidationError(
|
|
90
|
-
|
|
79
|
+
raise ValidationError("No network metadata found in database")
|
|
80
|
+
|
|
91
81
|
return {
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"updated_at": row[7]
|
|
82
|
+
"name": row[0],
|
|
83
|
+
"description": row[1],
|
|
84
|
+
"time_start": row[2],
|
|
85
|
+
"time_end": row[3],
|
|
86
|
+
"time_interval": row[4],
|
|
87
|
+
"created_at": row[5],
|
|
88
|
+
"updated_at": row[6],
|
|
100
89
|
}
|
|
101
90
|
|
|
102
91
|
|
|
103
|
-
def get_network_time_periods(
|
|
104
|
-
conn: sqlite3.Connection,
|
|
105
|
-
network_id: int
|
|
106
|
-
) -> List[TimePeriod]:
|
|
92
|
+
def get_network_time_periods(conn: sqlite3.Connection) -> List[TimePeriod]:
|
|
107
93
|
"""
|
|
108
|
-
Get network time periods using optimized storage.
|
|
109
|
-
|
|
94
|
+
Get network time periods using optimized storage (single network per database).
|
|
95
|
+
|
|
110
96
|
Args:
|
|
111
97
|
conn: Database connection
|
|
112
|
-
|
|
113
|
-
|
|
98
|
+
|
|
114
99
|
Returns:
|
|
115
100
|
List of TimePeriod objects ordered by period_index
|
|
116
101
|
"""
|
|
117
|
-
cursor = conn.execute(
|
|
102
|
+
cursor = conn.execute(
|
|
103
|
+
"""
|
|
118
104
|
SELECT period_count, start_timestamp, interval_seconds
|
|
119
105
|
FROM network_time_periods
|
|
120
|
-
|
|
121
|
-
"""
|
|
122
|
-
|
|
106
|
+
LIMIT 1
|
|
107
|
+
"""
|
|
108
|
+
)
|
|
109
|
+
|
|
123
110
|
row = cursor.fetchone()
|
|
124
111
|
if not row:
|
|
125
112
|
return [] # No time periods defined
|
|
126
|
-
|
|
113
|
+
|
|
127
114
|
period_count, start_timestamp, interval_seconds = row
|
|
128
|
-
|
|
115
|
+
|
|
129
116
|
# Generate all time periods computationally
|
|
130
117
|
periods = []
|
|
131
118
|
for period_index in range(period_count):
|
|
132
119
|
timestamp = start_timestamp + (period_index * interval_seconds)
|
|
133
|
-
|
|
120
|
+
|
|
134
121
|
# Format timestamp as string for compatibility - ALWAYS use UTC to avoid DST duplicates
|
|
135
122
|
dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
|
136
123
|
formatted_time = dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
137
|
-
|
|
138
|
-
periods.append(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
124
|
+
|
|
125
|
+
periods.append(
|
|
126
|
+
TimePeriod(
|
|
127
|
+
timestamp=timestamp,
|
|
128
|
+
period_index=period_index,
|
|
129
|
+
formatted_time=formatted_time,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
144
133
|
return periods
|
|
145
134
|
|
|
146
135
|
|
|
147
136
|
def list_networks(conn: sqlite3.Connection) -> List[Dict[str, Any]]:
|
|
148
137
|
"""
|
|
149
|
-
|
|
150
|
-
|
|
138
|
+
Get network information (returns single network in list for backward compatibility).
|
|
139
|
+
|
|
151
140
|
Args:
|
|
152
141
|
conn: Database connection
|
|
153
|
-
|
|
142
|
+
|
|
154
143
|
Returns:
|
|
155
|
-
List
|
|
144
|
+
List with single network dictionary (for backward compatibility)
|
|
156
145
|
"""
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
networks = []
|
|
164
|
-
for row in cursor.fetchall():
|
|
165
|
-
networks.append({
|
|
166
|
-
"id": row[0],
|
|
167
|
-
"name": row[1],
|
|
168
|
-
"description": row[2],
|
|
169
|
-
"created_at": row[3],
|
|
170
|
-
"updated_at": row[4],
|
|
171
|
-
"time_resolution": row[5], # time_interval from DB mapped to time_resolution
|
|
172
|
-
"start_time": row[6], # time_start from DB mapped to start_time
|
|
173
|
-
"end_time": row[7], # time_end from DB mapped to end_time
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
return networks
|
|
146
|
+
try:
|
|
147
|
+
network_info = get_network_info(conn)
|
|
148
|
+
return [network_info]
|
|
149
|
+
except ValidationError:
|
|
150
|
+
return []
|
|
177
151
|
|
|
178
152
|
|
|
179
153
|
def get_first_network(conn: sqlite3.Connection) -> Optional[Dict[str, Any]]:
|
|
180
154
|
"""
|
|
181
|
-
Get
|
|
182
|
-
|
|
155
|
+
Get network (for backward compatibility with single-network-per-database).
|
|
156
|
+
|
|
183
157
|
Args:
|
|
184
158
|
conn: Database connection
|
|
185
|
-
|
|
159
|
+
|
|
186
160
|
Returns:
|
|
187
|
-
Network dictionary or None if no
|
|
161
|
+
Network dictionary or None if no network exists
|
|
188
162
|
"""
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
ORDER BY created_at DESC
|
|
193
|
-
LIMIT 1
|
|
194
|
-
""")
|
|
195
|
-
|
|
196
|
-
row = cursor.fetchone()
|
|
197
|
-
if not row:
|
|
163
|
+
try:
|
|
164
|
+
return get_network_info(conn)
|
|
165
|
+
except ValidationError:
|
|
198
166
|
return None
|
|
199
|
-
|
|
200
|
-
return {
|
|
201
|
-
"id": row[0],
|
|
202
|
-
"name": row[1],
|
|
203
|
-
"description": row[2],
|
|
204
|
-
"created_at": row[3],
|
|
205
|
-
"updated_at": row[4],
|
|
206
|
-
"time_resolution": row[5],
|
|
207
|
-
"start_time": row[6],
|
|
208
|
-
"end_time": row[7],
|
|
209
|
-
}
|
|
210
167
|
|
|
211
168
|
|
|
212
|
-
def get_network_by_name(
|
|
169
|
+
def get_network_by_name(
|
|
170
|
+
conn: sqlite3.Connection, name: str
|
|
171
|
+
) -> Optional[Dict[str, Any]]:
|
|
213
172
|
"""
|
|
214
|
-
Get
|
|
215
|
-
|
|
173
|
+
Get network by name (for backward compatibility - checks if name matches).
|
|
174
|
+
|
|
216
175
|
Args:
|
|
217
176
|
conn: Database connection
|
|
218
|
-
name: Network name
|
|
219
|
-
|
|
177
|
+
name: Network name to match
|
|
178
|
+
|
|
220
179
|
Returns:
|
|
221
|
-
Network dictionary
|
|
180
|
+
Network dictionary if name matches, None otherwise
|
|
222
181
|
"""
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
row = cursor.fetchone()
|
|
230
|
-
if not row:
|
|
182
|
+
try:
|
|
183
|
+
network_info = get_network_info(conn)
|
|
184
|
+
if network_info.get("name") == name:
|
|
185
|
+
return network_info
|
|
186
|
+
return None
|
|
187
|
+
except ValidationError:
|
|
231
188
|
return None
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
"id": row[0],
|
|
235
|
-
"name": row[1],
|
|
236
|
-
"description": row[2],
|
|
237
|
-
"created_at": row[3],
|
|
238
|
-
"updated_at": row[4],
|
|
239
|
-
"time_resolution": row[5],
|
|
240
|
-
"start_time": row[6],
|
|
241
|
-
"end_time": row[7],
|
|
242
|
-
}
|
|
243
189
|
|
|
244
190
|
|
|
245
191
|
def create_carrier(
|
|
246
|
-
conn: sqlite3.Connection,
|
|
247
|
-
|
|
248
|
-
name: str,
|
|
192
|
+
conn: sqlite3.Connection,
|
|
193
|
+
name: str,
|
|
249
194
|
co2_emissions: float = 0.0,
|
|
250
195
|
color: Optional[str] = None,
|
|
251
|
-
nice_name: Optional[str] = None
|
|
196
|
+
nice_name: Optional[str] = None,
|
|
252
197
|
) -> int:
|
|
253
198
|
"""
|
|
254
|
-
Create a carrier record and return carrier ID.
|
|
255
|
-
|
|
199
|
+
Create a carrier record and return carrier ID (single network per database).
|
|
200
|
+
|
|
256
201
|
Args:
|
|
257
202
|
conn: Database connection
|
|
258
|
-
network_id: Network ID
|
|
259
203
|
name: Carrier name
|
|
260
204
|
co2_emissions: CO2 emissions factor
|
|
261
205
|
color: Display color
|
|
262
206
|
nice_name: Human-readable name
|
|
263
|
-
|
|
207
|
+
|
|
264
208
|
Returns:
|
|
265
209
|
ID of the newly created carrier
|
|
266
210
|
"""
|
|
267
|
-
cursor = conn.execute(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
211
|
+
cursor = conn.execute(
|
|
212
|
+
"""
|
|
213
|
+
INSERT INTO carriers (name, co2_emissions, color, nice_name)
|
|
214
|
+
VALUES (?, ?, ?, ?)
|
|
215
|
+
""",
|
|
216
|
+
(name, co2_emissions, color, nice_name),
|
|
217
|
+
)
|
|
218
|
+
|
|
272
219
|
carrier_id = cursor.lastrowid
|
|
273
220
|
if not carrier_id:
|
|
274
221
|
raise DatabaseError("Failed to create carrier")
|
|
275
|
-
|
|
222
|
+
|
|
276
223
|
return carrier_id
|
|
277
224
|
|
|
278
225
|
|
|
279
|
-
def list_carriers(conn: sqlite3.Connection
|
|
226
|
+
def list_carriers(conn: sqlite3.Connection) -> List[Dict[str, Any]]:
|
|
280
227
|
"""
|
|
281
|
-
List all carriers
|
|
282
|
-
|
|
228
|
+
List all carriers (single network per database).
|
|
229
|
+
|
|
283
230
|
Args:
|
|
284
231
|
conn: Database connection
|
|
285
|
-
|
|
286
|
-
|
|
232
|
+
|
|
287
233
|
Returns:
|
|
288
234
|
List of carrier dictionaries
|
|
289
235
|
"""
|
|
290
|
-
cursor = conn.execute(
|
|
291
|
-
|
|
236
|
+
cursor = conn.execute(
|
|
237
|
+
"""
|
|
238
|
+
SELECT id, name, co2_emissions, color, nice_name
|
|
292
239
|
FROM carriers
|
|
293
|
-
WHERE network_id = ?
|
|
294
240
|
ORDER BY name
|
|
295
|
-
"""
|
|
296
|
-
|
|
241
|
+
"""
|
|
242
|
+
)
|
|
243
|
+
|
|
297
244
|
carriers = []
|
|
298
245
|
for row in cursor.fetchall():
|
|
299
|
-
carriers.append(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
246
|
+
carriers.append(
|
|
247
|
+
{
|
|
248
|
+
"id": row[0],
|
|
249
|
+
"name": row[1],
|
|
250
|
+
"co2_emissions": row[2],
|
|
251
|
+
"color": row[3],
|
|
252
|
+
"nice_name": row[4],
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
|
|
308
256
|
return carriers
|
|
309
257
|
|
|
310
258
|
|
|
311
259
|
def get_network_config(
|
|
312
|
-
conn: sqlite3.Connection,
|
|
313
|
-
network_id: int,
|
|
314
|
-
scenario_id: Optional[int] = None
|
|
260
|
+
conn: sqlite3.Connection, scenario_id: Optional[int] = None
|
|
315
261
|
) -> Dict[str, Any]:
|
|
316
262
|
"""
|
|
317
|
-
Get network configuration with scenario-aware fallback.
|
|
318
|
-
|
|
263
|
+
Get network configuration with scenario-aware fallback (single network per database).
|
|
264
|
+
|
|
319
265
|
Priority order:
|
|
320
266
|
1. Scenario-specific config (network_config WHERE scenario_id = X)
|
|
321
267
|
2. Network default config (network_config WHERE scenario_id IS NULL)
|
|
322
|
-
3.
|
|
323
|
-
|
|
324
|
-
|
|
268
|
+
3. System default value
|
|
269
|
+
|
|
325
270
|
Args:
|
|
326
271
|
conn: Database connection
|
|
327
|
-
network_id: Network ID
|
|
328
272
|
scenario_id: Optional scenario ID
|
|
329
|
-
|
|
273
|
+
|
|
330
274
|
Returns:
|
|
331
275
|
Dictionary with network configuration
|
|
332
276
|
"""
|
|
333
277
|
config = {}
|
|
334
|
-
|
|
278
|
+
|
|
335
279
|
# Load from network_config table with scenario fallback
|
|
336
|
-
cursor = conn.execute(
|
|
280
|
+
cursor = conn.execute(
|
|
281
|
+
"""
|
|
337
282
|
SELECT param_name, param_type, param_value
|
|
338
283
|
FROM network_config
|
|
339
|
-
WHERE
|
|
284
|
+
WHERE (scenario_id = ? OR scenario_id IS NULL)
|
|
340
285
|
ORDER BY scenario_id DESC NULLS LAST -- Scenario-specific values first
|
|
341
|
-
""",
|
|
342
|
-
|
|
286
|
+
""",
|
|
287
|
+
(scenario_id,),
|
|
288
|
+
)
|
|
289
|
+
|
|
343
290
|
seen_params = set()
|
|
344
291
|
for row in cursor.fetchall():
|
|
345
292
|
param_name, param_type, param_value = row
|
|
346
|
-
|
|
293
|
+
|
|
347
294
|
# Skip if we already have this parameter (scenario-specific takes precedence)
|
|
348
295
|
if param_name in seen_params:
|
|
349
296
|
continue
|
|
350
297
|
seen_params.add(param_name)
|
|
351
|
-
|
|
298
|
+
|
|
352
299
|
# Parse value based on type
|
|
353
300
|
try:
|
|
354
|
-
if param_type ==
|
|
355
|
-
config[param_name] = param_value.lower() ==
|
|
356
|
-
elif param_type ==
|
|
301
|
+
if param_type == "boolean":
|
|
302
|
+
config[param_name] = param_value.lower() == "true"
|
|
303
|
+
elif param_type == "real":
|
|
357
304
|
config[param_name] = float(param_value)
|
|
358
|
-
elif param_type ==
|
|
305
|
+
elif param_type == "integer":
|
|
359
306
|
config[param_name] = int(param_value)
|
|
360
|
-
elif param_type ==
|
|
307
|
+
elif param_type == "json":
|
|
361
308
|
config[param_name] = json.loads(param_value)
|
|
362
309
|
else: # string
|
|
363
310
|
config[param_name] = param_value
|
|
364
311
|
except (ValueError, json.JSONDecodeError) as e:
|
|
365
312
|
logger.warning(f"Failed to parse config parameter {param_name}: {e}")
|
|
366
313
|
continue
|
|
367
|
-
|
|
368
|
-
# Fallback to legacy column for unmet_load_active if not in config table
|
|
369
|
-
if 'unmet_load_active' not in config:
|
|
370
|
-
cursor = conn.execute("SELECT unmet_load_active FROM networks WHERE id = ?", (network_id,))
|
|
371
|
-
row = cursor.fetchone()
|
|
372
|
-
if row and row[0] is not None:
|
|
373
|
-
config['unmet_load_active'] = bool(row[0])
|
|
374
|
-
|
|
314
|
+
|
|
375
315
|
# Apply system defaults for missing parameters
|
|
376
316
|
defaults = {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
317
|
+
"unmet_load_active": True,
|
|
318
|
+
"discount_rate": 0.0, # No discounting by default
|
|
319
|
+
"solver_name": "default",
|
|
380
320
|
}
|
|
381
|
-
|
|
321
|
+
|
|
382
322
|
for param, default_value in defaults.items():
|
|
383
323
|
if param not in config:
|
|
384
324
|
config[param] = default_value
|
|
385
|
-
|
|
325
|
+
|
|
386
326
|
return config
|
|
387
327
|
|
|
388
328
|
|
|
389
329
|
def set_network_config(
|
|
390
330
|
conn: sqlite3.Connection,
|
|
391
|
-
network_id: int,
|
|
392
331
|
param_name: str,
|
|
393
332
|
param_value: Any,
|
|
394
333
|
param_type: str,
|
|
395
334
|
scenario_id: Optional[int] = None,
|
|
396
|
-
description: Optional[str] = None
|
|
335
|
+
description: Optional[str] = None,
|
|
397
336
|
) -> None:
|
|
398
337
|
"""
|
|
399
|
-
Set network configuration parameter.
|
|
400
|
-
|
|
338
|
+
Set network configuration parameter (single network per database).
|
|
339
|
+
|
|
401
340
|
Args:
|
|
402
341
|
conn: Database connection
|
|
403
|
-
network_id: Network ID
|
|
404
342
|
param_name: Parameter name
|
|
405
343
|
param_value: Parameter value
|
|
406
344
|
param_type: Parameter type ('boolean', 'real', 'integer', 'string', 'json')
|
|
407
|
-
scenario_id: Optional scenario ID
|
|
345
|
+
scenario_id: Optional scenario ID (NULL for base network)
|
|
408
346
|
description: Optional parameter description
|
|
409
|
-
|
|
347
|
+
|
|
410
348
|
Raises:
|
|
411
349
|
ValidationError: If parameter type is invalid or serialization fails
|
|
412
350
|
"""
|
|
413
|
-
|
|
351
|
+
|
|
414
352
|
# Validate parameter type
|
|
415
|
-
valid_types = {
|
|
353
|
+
valid_types = {"boolean", "real", "integer", "string", "json"}
|
|
416
354
|
if param_type not in valid_types:
|
|
417
|
-
raise ValidationError(
|
|
418
|
-
|
|
355
|
+
raise ValidationError(
|
|
356
|
+
f"Invalid parameter type: {param_type}. Must be one of {valid_types}"
|
|
357
|
+
)
|
|
358
|
+
|
|
419
359
|
# Serialize value based on type
|
|
420
360
|
try:
|
|
421
|
-
if param_type ==
|
|
361
|
+
if param_type == "boolean":
|
|
422
362
|
serialized = str(param_value).lower()
|
|
423
|
-
if serialized not in {
|
|
424
|
-
raise ValidationError(
|
|
425
|
-
|
|
363
|
+
if serialized not in {"true", "false"}:
|
|
364
|
+
raise ValidationError(
|
|
365
|
+
f"Boolean parameter must be True/False, got: {param_value}"
|
|
366
|
+
)
|
|
367
|
+
elif param_type == "real":
|
|
426
368
|
serialized = str(float(param_value))
|
|
427
|
-
elif param_type ==
|
|
369
|
+
elif param_type == "integer":
|
|
428
370
|
serialized = str(int(param_value))
|
|
429
|
-
elif param_type ==
|
|
371
|
+
elif param_type == "json":
|
|
430
372
|
serialized = json.dumps(param_value)
|
|
431
373
|
else: # string
|
|
432
374
|
serialized = str(param_value)
|
|
433
375
|
except (ValueError, TypeError) as e:
|
|
434
|
-
raise ValidationError(
|
|
435
|
-
|
|
376
|
+
raise ValidationError(
|
|
377
|
+
f"Failed to serialize parameter {param_name} as {param_type}: {e}"
|
|
378
|
+
)
|
|
379
|
+
|
|
436
380
|
# Insert or update parameter
|
|
437
|
-
conn.execute(
|
|
381
|
+
conn.execute(
|
|
382
|
+
"""
|
|
438
383
|
INSERT OR REPLACE INTO network_config
|
|
439
|
-
(
|
|
440
|
-
VALUES (?, ?, ?, ?, ?,
|
|
441
|
-
""",
|
|
384
|
+
(scenario_id, param_name, param_type, param_value, param_description, updated_at)
|
|
385
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
|
386
|
+
""",
|
|
387
|
+
(scenario_id, param_name, param_type, serialized, description),
|
|
388
|
+
)
|
|
442
389
|
|
|
443
390
|
|
|
444
|
-
def get_component_counts(conn: sqlite3.Connection
|
|
391
|
+
def get_component_counts(conn: sqlite3.Connection) -> Dict[str, int]:
|
|
445
392
|
"""
|
|
446
|
-
Get component counts by type
|
|
447
|
-
|
|
393
|
+
Get component counts by type (single network per database).
|
|
394
|
+
|
|
448
395
|
Args:
|
|
449
396
|
conn: Database connection
|
|
450
|
-
|
|
451
|
-
|
|
397
|
+
|
|
452
398
|
Returns:
|
|
453
399
|
Dictionary mapping component types to counts
|
|
454
400
|
"""
|
|
455
|
-
cursor = conn.execute(
|
|
401
|
+
cursor = conn.execute(
|
|
402
|
+
"""
|
|
456
403
|
SELECT component_type, COUNT(*) FROM components
|
|
457
|
-
|
|
458
|
-
"""
|
|
459
|
-
|
|
404
|
+
GROUP BY component_type
|
|
405
|
+
"""
|
|
406
|
+
)
|
|
407
|
+
|
|
460
408
|
counts = {}
|
|
461
409
|
for row in cursor.fetchall():
|
|
462
410
|
counts[row[0].lower()] = row[1]
|
|
463
|
-
|
|
464
|
-
return counts
|
|
465
|
-
|
|
466
411
|
|
|
467
|
-
|
|
468
|
-
"""Get the master scenario ID for a network"""
|
|
469
|
-
cursor = conn.cursor()
|
|
470
|
-
cursor.execute(
|
|
471
|
-
"SELECT id FROM scenarios WHERE network_id = ? AND is_master = TRUE",
|
|
472
|
-
(network_id,)
|
|
473
|
-
)
|
|
474
|
-
result = cursor.fetchone()
|
|
475
|
-
if not result:
|
|
476
|
-
raise ValidationError(f"No master scenario found for network {network_id}")
|
|
477
|
-
return result[0]
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
def resolve_scenario_id(conn: sqlite3.Connection, component_id: int, scenario_id: Optional[int]) -> int:
|
|
481
|
-
"""Resolve scenario ID - if None, get master scenario ID"""
|
|
482
|
-
if scenario_id is not None:
|
|
483
|
-
return scenario_id
|
|
484
|
-
|
|
485
|
-
# Get network_id from component, then get master scenario
|
|
486
|
-
cursor = conn.cursor()
|
|
487
|
-
cursor.execute("SELECT network_id FROM components WHERE id = ?", (component_id,))
|
|
488
|
-
result = cursor.fetchone()
|
|
489
|
-
if not result:
|
|
490
|
-
from pyconvexity.core.errors import ComponentNotFound
|
|
491
|
-
raise ComponentNotFound(component_id)
|
|
492
|
-
|
|
493
|
-
network_id = result[0]
|
|
494
|
-
return get_master_scenario_id(conn, network_id)
|
|
412
|
+
return counts
|