pyconvexity 0.4.8__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 +241 -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/dashboard.py +265 -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 +507 -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 +191 -0
- pyconvexity/io/netcdf_importer.py +1802 -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 +30 -0
- pyconvexity/solvers/pypsa/api.py +446 -0
- pyconvexity/solvers/pypsa/batch_loader.py +296 -0
- pyconvexity/solvers/pypsa/builder.py +655 -0
- pyconvexity/solvers/pypsa/clearing_price.py +678 -0
- pyconvexity/solvers/pypsa/constraints.py +405 -0
- pyconvexity/solvers/pypsa/solver.py +1442 -0
- pyconvexity/solvers/pypsa/storage.py +2096 -0
- pyconvexity/timeseries.py +330 -0
- pyconvexity/validation/__init__.py +25 -0
- pyconvexity/validation/rules.py +312 -0
- pyconvexity-0.4.8.dist-info/METADATA +148 -0
- pyconvexity-0.4.8.dist-info/RECORD +44 -0
- pyconvexity-0.4.8.dist-info/WHEEL +5 -0
- pyconvexity-0.4.8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constraint application functionality for PyPSA networks.
|
|
3
|
+
|
|
4
|
+
Handles loading and applying custom constraints from the database.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import numpy as np
|
|
10
|
+
from typing import Dict, Any, Optional, List
|
|
11
|
+
|
|
12
|
+
from pyconvexity.models import list_components_by_type, get_attribute
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConstraintApplicator:
|
|
18
|
+
"""
|
|
19
|
+
Handles loading and applying custom constraints to PyPSA networks.
|
|
20
|
+
|
|
21
|
+
This class manages both pre-optimization constraints (applied to network structure)
|
|
22
|
+
and optimization-time constraints (applied during solving via extra_functionality).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def apply_constraints(
|
|
26
|
+
self,
|
|
27
|
+
conn,
|
|
28
|
+
network: "pypsa.Network",
|
|
29
|
+
scenario_id: Optional[int] = None,
|
|
30
|
+
constraints_dsl: Optional[str] = None,
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Apply all constraints to the network (single network per database).
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
conn: Database connection
|
|
37
|
+
network: PyPSA Network object
|
|
38
|
+
scenario_id: Optional scenario ID (NULL for base network)
|
|
39
|
+
constraints_dsl: Optional DSL constraints string
|
|
40
|
+
"""
|
|
41
|
+
# Apply database constraints
|
|
42
|
+
self._apply_database_constraints(conn, network, scenario_id)
|
|
43
|
+
|
|
44
|
+
# Apply DSL constraints if provided
|
|
45
|
+
if constraints_dsl:
|
|
46
|
+
self._apply_dsl_constraints(network, constraints_dsl)
|
|
47
|
+
|
|
48
|
+
def _detect_constraint_type(self, constraint_code: str) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Detect if constraint is network-modification or model-constraint type.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
constraint_code: The constraint code to analyze
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
"model_constraint" or "network_modification"
|
|
57
|
+
"""
|
|
58
|
+
# Type 2 indicators (model constraints) - need access to optimization model
|
|
59
|
+
model_indicators = [
|
|
60
|
+
"n.optimize.create_model()",
|
|
61
|
+
"m.variables",
|
|
62
|
+
"m.add_constraints",
|
|
63
|
+
"gen_p =",
|
|
64
|
+
"constraint_expr =",
|
|
65
|
+
"LinearExpression",
|
|
66
|
+
"linopy",
|
|
67
|
+
"Generator-p",
|
|
68
|
+
"lhs <=",
|
|
69
|
+
"constraint_expr =",
|
|
70
|
+
"model.variables",
|
|
71
|
+
"model.add_constraints",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
# Type 1 indicators (network modifications) - modify network directly
|
|
75
|
+
network_indicators = [
|
|
76
|
+
"n.generators.loc",
|
|
77
|
+
"n.add(",
|
|
78
|
+
"n.buses.",
|
|
79
|
+
"n.lines.",
|
|
80
|
+
"network.generators.loc",
|
|
81
|
+
"network.add(",
|
|
82
|
+
"network.buses.",
|
|
83
|
+
"network.lines.",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
# Check for model constraint indicators first (more specific)
|
|
87
|
+
if any(indicator in constraint_code for indicator in model_indicators):
|
|
88
|
+
return "model_constraint"
|
|
89
|
+
elif any(indicator in constraint_code for indicator in network_indicators):
|
|
90
|
+
return "network_modification"
|
|
91
|
+
else:
|
|
92
|
+
# Default to network_modification for safety (existing behavior)
|
|
93
|
+
return "network_modification"
|
|
94
|
+
|
|
95
|
+
def _apply_database_constraints(
|
|
96
|
+
self, conn, network: "pypsa.Network", scenario_id: Optional[int]
|
|
97
|
+
):
|
|
98
|
+
"""Load and apply custom constraints from the database in priority order (single network per database)."""
|
|
99
|
+
try:
|
|
100
|
+
# Load all constraints for this network
|
|
101
|
+
constraints = list_components_by_type(conn, "CONSTRAINT")
|
|
102
|
+
|
|
103
|
+
if not constraints:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
# Load constraint attributes and filter active ones
|
|
107
|
+
active_constraints = []
|
|
108
|
+
model_constraints = []
|
|
109
|
+
network_constraints = []
|
|
110
|
+
|
|
111
|
+
for constraint in constraints:
|
|
112
|
+
try:
|
|
113
|
+
# Get constraint attributes
|
|
114
|
+
is_active = get_attribute(
|
|
115
|
+
conn, constraint.id, "is_active", scenario_id
|
|
116
|
+
)
|
|
117
|
+
priority = get_attribute(
|
|
118
|
+
conn, constraint.id, "priority", scenario_id
|
|
119
|
+
)
|
|
120
|
+
constraint_code = get_attribute(
|
|
121
|
+
conn, constraint.id, "constraint_code", scenario_id
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Check if constraint is active
|
|
125
|
+
if is_active.variant == "Static":
|
|
126
|
+
# Extract boolean value from StaticValue
|
|
127
|
+
is_active_bool = False
|
|
128
|
+
if "Boolean" in is_active.static_value.data:
|
|
129
|
+
is_active_bool = is_active.static_value.data["Boolean"]
|
|
130
|
+
|
|
131
|
+
if is_active_bool:
|
|
132
|
+
# Extract code value
|
|
133
|
+
code_val = ""
|
|
134
|
+
if constraint_code.variant == "Static":
|
|
135
|
+
if "String" in constraint_code.static_value.data:
|
|
136
|
+
code_val = constraint_code.static_value.data[
|
|
137
|
+
"String"
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
# Extract priority value
|
|
141
|
+
priority_val = 0
|
|
142
|
+
if priority.variant == "Static":
|
|
143
|
+
if "Integer" in priority.static_value.data:
|
|
144
|
+
priority_val = priority.static_value.data["Integer"]
|
|
145
|
+
elif "Float" in priority.static_value.data:
|
|
146
|
+
priority_val = int(
|
|
147
|
+
priority.static_value.data["Float"]
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
constraint_dict = {
|
|
151
|
+
"id": constraint.id,
|
|
152
|
+
"name": constraint.name,
|
|
153
|
+
"priority": priority_val,
|
|
154
|
+
"code": code_val,
|
|
155
|
+
"constraint_code": code_val, # For compatibility
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Detect constraint type and separate them
|
|
159
|
+
constraint_type = self._detect_constraint_type(code_val)
|
|
160
|
+
if constraint_type == "model_constraint":
|
|
161
|
+
model_constraints.append(constraint_dict)
|
|
162
|
+
logger.info(
|
|
163
|
+
f"Detected model constraint: {constraint.name}"
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
network_constraints.append(constraint_dict)
|
|
167
|
+
logger.info(
|
|
168
|
+
f"Detected network constraint: {constraint.name}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.warning(f"Failed to load constraint {constraint.name}: {e}")
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
if not model_constraints and not network_constraints:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
logger.info(
|
|
179
|
+
f"Constraint breakdown: {len(model_constraints)} model constraints, {len(network_constraints)} network constraints"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Apply network constraints ONLY (they modify the network structure before solve)
|
|
183
|
+
# Model constraints will be applied later by the solver via extra_functionality
|
|
184
|
+
if network_constraints:
|
|
185
|
+
network_constraints.sort(key=lambda x: x["priority"])
|
|
186
|
+
for constraint in network_constraints:
|
|
187
|
+
try:
|
|
188
|
+
logger.info(
|
|
189
|
+
f"Executing network constraint '{constraint['name']}' (priority {constraint['priority']})"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Execute the constraint code in the normal Python environment
|
|
193
|
+
exec_globals = {
|
|
194
|
+
"n": network,
|
|
195
|
+
"network": network,
|
|
196
|
+
"pd": pd,
|
|
197
|
+
"np": np,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Execute the constraint code
|
|
201
|
+
exec(constraint["code"], exec_globals)
|
|
202
|
+
|
|
203
|
+
except Exception as e:
|
|
204
|
+
error_msg = f"Failed to execute network constraint '{constraint['name']}': {e}"
|
|
205
|
+
logger.error(error_msg, exc_info=True)
|
|
206
|
+
# Continue with other constraints instead of failing the entire solve
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
# Skip model constraints here - they will be applied by the solver during optimization
|
|
210
|
+
# via extra_functionality to ensure they have access to the actual optimization model
|
|
211
|
+
if model_constraints:
|
|
212
|
+
logger.info(
|
|
213
|
+
f"Skipping {len(model_constraints)} model constraints - will be applied during solve"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.error(f"Failed to apply custom constraints: {e}", exc_info=True)
|
|
218
|
+
|
|
219
|
+
def _apply_dsl_constraints(self, network: "pypsa.Network", constraints_dsl: str):
|
|
220
|
+
"""
|
|
221
|
+
Apply DSL constraints to the network.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
network: PyPSA Network object
|
|
225
|
+
constraints_dsl: DSL constraints string
|
|
226
|
+
"""
|
|
227
|
+
try:
|
|
228
|
+
logger.info("Applying DSL constraints")
|
|
229
|
+
logger.debug(f"DSL Code: {constraints_dsl}")
|
|
230
|
+
|
|
231
|
+
# Execute DSL constraints
|
|
232
|
+
exec_globals = {
|
|
233
|
+
"n": network,
|
|
234
|
+
"network": network,
|
|
235
|
+
"pd": pd,
|
|
236
|
+
"np": np,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
exec(constraints_dsl, exec_globals)
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.error(f"Failed to apply DSL constraints: {e}", exc_info=True)
|
|
243
|
+
|
|
244
|
+
def get_optimization_constraints(
|
|
245
|
+
self, conn, scenario_id: Optional[int] = None
|
|
246
|
+
) -> List[Dict[str, Any]]:
|
|
247
|
+
"""
|
|
248
|
+
Get ALL active constraints for optimization-time application (single network per database).
|
|
249
|
+
The solver will determine which are model constraints vs network constraints.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
conn: Database connection
|
|
253
|
+
scenario_id: Optional scenario ID (NULL for base network)
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
List of all active constraints
|
|
257
|
+
"""
|
|
258
|
+
try:
|
|
259
|
+
# Load all constraints for this network
|
|
260
|
+
constraints = list_components_by_type(conn, "CONSTRAINT")
|
|
261
|
+
|
|
262
|
+
if not constraints:
|
|
263
|
+
return []
|
|
264
|
+
|
|
265
|
+
# Load constraint attributes and filter active ones
|
|
266
|
+
optimization_constraints = []
|
|
267
|
+
for constraint in constraints:
|
|
268
|
+
try:
|
|
269
|
+
# Get constraint attributes
|
|
270
|
+
is_active = get_attribute(
|
|
271
|
+
conn, constraint.id, "is_active", scenario_id
|
|
272
|
+
)
|
|
273
|
+
priority = get_attribute(
|
|
274
|
+
conn, constraint.id, "priority", scenario_id
|
|
275
|
+
)
|
|
276
|
+
constraint_code = get_attribute(
|
|
277
|
+
conn, constraint.id, "constraint_code", scenario_id
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Check if constraint is active
|
|
281
|
+
if is_active.variant == "Static":
|
|
282
|
+
# Extract boolean value from StaticValue
|
|
283
|
+
is_active_bool = False
|
|
284
|
+
if "Boolean" in is_active.static_value.data:
|
|
285
|
+
is_active_bool = is_active.static_value.data["Boolean"]
|
|
286
|
+
|
|
287
|
+
if is_active_bool:
|
|
288
|
+
# Extract code value
|
|
289
|
+
code_val = ""
|
|
290
|
+
if constraint_code.variant == "Static":
|
|
291
|
+
if "String" in constraint_code.static_value.data:
|
|
292
|
+
code_val = constraint_code.static_value.data[
|
|
293
|
+
"String"
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
# Extract priority value
|
|
297
|
+
priority_val = 0
|
|
298
|
+
if priority.variant == "Static":
|
|
299
|
+
if "Integer" in priority.static_value.data:
|
|
300
|
+
priority_val = priority.static_value.data["Integer"]
|
|
301
|
+
elif "Float" in priority.static_value.data:
|
|
302
|
+
priority_val = int(
|
|
303
|
+
priority.static_value.data["Float"]
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
optimization_constraints.append(
|
|
307
|
+
{
|
|
308
|
+
"id": constraint.id,
|
|
309
|
+
"name": constraint.name,
|
|
310
|
+
"priority": priority_val,
|
|
311
|
+
"constraint_code": code_val, # Use consistent key name
|
|
312
|
+
"code": code_val, # Keep both for compatibility
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logger.warning(f"Failed to load constraint {constraint.name}: {e}")
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
# Sort constraints by priority (lower numbers first)
|
|
321
|
+
optimization_constraints.sort(key=lambda x: x["priority"])
|
|
322
|
+
return optimization_constraints
|
|
323
|
+
|
|
324
|
+
except Exception as e:
|
|
325
|
+
logger.error(f"Failed to get optimization constraints: {e}", exc_info=True)
|
|
326
|
+
return []
|
|
327
|
+
|
|
328
|
+
def apply_optimization_constraints(
|
|
329
|
+
self, network: "pypsa.Network", snapshots, constraints: List[Dict[str, Any]]
|
|
330
|
+
):
|
|
331
|
+
"""
|
|
332
|
+
Apply constraints during optimization (called via extra_functionality).
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
network: PyPSA Network object
|
|
336
|
+
snapshots: Network snapshots
|
|
337
|
+
constraints: List of constraint dictionaries
|
|
338
|
+
"""
|
|
339
|
+
try:
|
|
340
|
+
for constraint in constraints:
|
|
341
|
+
try:
|
|
342
|
+
logger.info(
|
|
343
|
+
f"Applying optimization constraint '{constraint['name']}' (priority {constraint['priority']})"
|
|
344
|
+
)
|
|
345
|
+
logger.debug(f"Code: {constraint['code']}")
|
|
346
|
+
|
|
347
|
+
# Execute the constraint code with network and snapshots available
|
|
348
|
+
exec_globals = {
|
|
349
|
+
"net": network,
|
|
350
|
+
"network": network,
|
|
351
|
+
"n": network,
|
|
352
|
+
"snapshots": snapshots,
|
|
353
|
+
"pd": pd,
|
|
354
|
+
"np": np,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
# Execute the constraint code
|
|
358
|
+
exec(constraint["code"], exec_globals)
|
|
359
|
+
|
|
360
|
+
except Exception as e:
|
|
361
|
+
error_msg = f"Failed to execute optimization constraint '{constraint['name']}': {e}"
|
|
362
|
+
logger.error(error_msg, exc_info=True)
|
|
363
|
+
# Continue with other constraints instead of failing the entire solve
|
|
364
|
+
continue
|
|
365
|
+
|
|
366
|
+
except Exception as e:
|
|
367
|
+
logger.error(
|
|
368
|
+
f"Failed to apply optimization constraints: {e}", exc_info=True
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def apply_optimization_constraint(
|
|
372
|
+
self, network: "pypsa.Network", snapshots, constraint: Dict[str, Any]
|
|
373
|
+
):
|
|
374
|
+
"""
|
|
375
|
+
Apply a single optimization constraint during solve.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
network: PyPSA Network object
|
|
379
|
+
snapshots: Network snapshots
|
|
380
|
+
constraint: Single constraint dictionary
|
|
381
|
+
"""
|
|
382
|
+
try:
|
|
383
|
+
logger.info(
|
|
384
|
+
f"Applying optimization constraint '{constraint.get('name', 'unknown')}' (priority {constraint.get('priority', 0)})"
|
|
385
|
+
)
|
|
386
|
+
logger.debug(f"Code: {constraint.get('code', '')}")
|
|
387
|
+
|
|
388
|
+
# Execute the constraint code with network and snapshots available
|
|
389
|
+
exec_globals = {
|
|
390
|
+
"net": network,
|
|
391
|
+
"network": network,
|
|
392
|
+
"n": network,
|
|
393
|
+
"snapshots": snapshots,
|
|
394
|
+
"pd": pd,
|
|
395
|
+
"np": np,
|
|
396
|
+
"xr": __import__("xarray"),
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
# Execute the constraint code
|
|
400
|
+
exec(constraint.get("code", ""), exec_globals)
|
|
401
|
+
|
|
402
|
+
except Exception as e:
|
|
403
|
+
error_msg = f"Failed to execute optimization constraint '{constraint.get('name', 'unknown')}': {e}"
|
|
404
|
+
logger.error(error_msg, exc_info=True)
|
|
405
|
+
raise # Re-raise so solver can handle it
|