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.

Files changed (42) hide show
  1. pyconvexity/__init__.py +226 -0
  2. pyconvexity/_version.py +1 -0
  3. pyconvexity/core/__init__.py +60 -0
  4. pyconvexity/core/database.py +485 -0
  5. pyconvexity/core/errors.py +106 -0
  6. pyconvexity/core/types.py +400 -0
  7. pyconvexity/data/README.md +101 -0
  8. pyconvexity/data/__init__.py +17 -0
  9. pyconvexity/data/loaders/__init__.py +3 -0
  10. pyconvexity/data/loaders/cache.py +213 -0
  11. pyconvexity/data/schema/01_core_schema.sql +420 -0
  12. pyconvexity/data/schema/02_data_metadata.sql +120 -0
  13. pyconvexity/data/schema/03_validation_data.sql +506 -0
  14. pyconvexity/data/sources/__init__.py +5 -0
  15. pyconvexity/data/sources/gem.py +442 -0
  16. pyconvexity/io/__init__.py +26 -0
  17. pyconvexity/io/excel_exporter.py +1226 -0
  18. pyconvexity/io/excel_importer.py +1381 -0
  19. pyconvexity/io/netcdf_exporter.py +197 -0
  20. pyconvexity/io/netcdf_importer.py +1833 -0
  21. pyconvexity/models/__init__.py +195 -0
  22. pyconvexity/models/attributes.py +730 -0
  23. pyconvexity/models/carriers.py +159 -0
  24. pyconvexity/models/components.py +611 -0
  25. pyconvexity/models/network.py +503 -0
  26. pyconvexity/models/results.py +148 -0
  27. pyconvexity/models/scenarios.py +234 -0
  28. pyconvexity/solvers/__init__.py +29 -0
  29. pyconvexity/solvers/pypsa/__init__.py +24 -0
  30. pyconvexity/solvers/pypsa/api.py +460 -0
  31. pyconvexity/solvers/pypsa/batch_loader.py +307 -0
  32. pyconvexity/solvers/pypsa/builder.py +675 -0
  33. pyconvexity/solvers/pypsa/constraints.py +405 -0
  34. pyconvexity/solvers/pypsa/solver.py +1509 -0
  35. pyconvexity/solvers/pypsa/storage.py +2048 -0
  36. pyconvexity/timeseries.py +330 -0
  37. pyconvexity/validation/__init__.py +25 -0
  38. pyconvexity/validation/rules.py +312 -0
  39. pyconvexity-0.4.3.dist-info/METADATA +47 -0
  40. pyconvexity-0.4.3.dist-info/RECORD +42 -0
  41. pyconvexity-0.4.3.dist-info/WHEEL +5 -0
  42. pyconvexity-0.4.3.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