pyconvexity 0.3.8__tar.gz → 0.3.8.post2__tar.gz

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 (53) hide show
  1. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/PKG-INFO +1 -1
  2. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/pyproject.toml +2 -2
  3. pyconvexity-0.3.8.post2/src/pyconvexity/_version.py +1 -0
  4. pyconvexity-0.3.8.post2/src/pyconvexity/data/__pycache__/__init__.cpython-313.pyc +0 -0
  5. pyconvexity-0.3.8.post2/src/pyconvexity/data/loaders/__pycache__/__init__.cpython-313.pyc +0 -0
  6. pyconvexity-0.3.8.post2/src/pyconvexity/data/loaders/__pycache__/cache.cpython-313.pyc +0 -0
  7. pyconvexity-0.3.8.post2/src/pyconvexity/data/sources/__pycache__/__init__.cpython-313.pyc +0 -0
  8. pyconvexity-0.3.8.post2/src/pyconvexity/data/sources/__pycache__/gem.cpython-313.pyc +0 -0
  9. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/solvers/pypsa/api.py +2 -6
  10. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/solvers/pypsa/constraints.py +178 -43
  11. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/solvers/pypsa/solver.py +75 -4
  12. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity.egg-info/PKG-INFO +1 -1
  13. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity.egg-info/SOURCES.txt +5 -0
  14. pyconvexity-0.3.8/src/pyconvexity/_version.py +0 -1
  15. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/README.md +0 -0
  16. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/setup.cfg +0 -0
  17. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/__init__.py +0 -0
  18. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/core/__init__.py +0 -0
  19. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/core/database.py +0 -0
  20. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/core/errors.py +0 -0
  21. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/core/types.py +0 -0
  22. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/data/README.md +0 -0
  23. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/data/__init__.py +0 -0
  24. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/data/loaders/__init__.py +0 -0
  25. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/data/loaders/cache.py +0 -0
  26. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/data/schema/01_core_schema.sql +0 -0
  27. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/data/schema/02_data_metadata.sql +0 -0
  28. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/data/schema/03_validation_data.sql +0 -0
  29. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/data/schema/04_scenario_schema.sql +0 -0
  30. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/data/sources/__init__.py +0 -0
  31. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/data/sources/gem.py +0 -0
  32. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/io/__init__.py +0 -0
  33. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/io/excel_exporter.py +0 -0
  34. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/io/excel_importer.py +0 -0
  35. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/io/netcdf_exporter.py +0 -0
  36. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/io/netcdf_importer.py +0 -0
  37. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/models/__init__.py +0 -0
  38. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/models/attributes.py +0 -0
  39. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/models/components.py +0 -0
  40. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/models/network.py +0 -0
  41. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/models/scenarios.py +0 -0
  42. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/solvers/__init__.py +0 -0
  43. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/solvers/pypsa/__init__.py +0 -0
  44. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/solvers/pypsa/batch_loader.py +0 -0
  45. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/solvers/pypsa/builder.py +0 -0
  46. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/solvers/pypsa/storage.py +0 -0
  47. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/timeseries.py +0 -0
  48. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/validation/__init__.py +0 -0
  49. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity/validation/rules.py +0 -0
  50. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity.egg-info/dependency_links.txt +0 -0
  51. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity.egg-info/requires.txt +0 -0
  52. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/src/pyconvexity.egg-info/top_level.txt +0 -0
  53. {pyconvexity-0.3.8 → pyconvexity-0.3.8.post2}/tests/test_core_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyconvexity
3
- Version: 0.3.8
3
+ Version: 0.3.8.post2
4
4
  Summary: Python library for energy system modeling and optimization with PyPSA
5
5
  Author-email: Convexity Team <info@convexity.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyconvexity"
7
- version = "0.3.8"
7
+ version = "0.3.8.post2"
8
8
  description = "Python library for energy system modeling and optimization with PyPSA"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -81,7 +81,7 @@ profile = "black"
81
81
  line_length = 100
82
82
 
83
83
  [tool.mypy]
84
- python_version = "0.3.8"
84
+ python_version = "0.3.8.post2"
85
85
  warn_return_any = true
86
86
  warn_unused_configs = true
87
87
  disallow_untyped_defs = true
@@ -0,0 +1 @@
1
+ __version__ = "0.3.8.post2"
@@ -76,14 +76,10 @@ def solve_network(
76
76
  if progress_callback:
77
77
  progress_callback(50, f"Network built: {len(network.buses)} buses, {len(network.generators)} generators")
78
78
 
79
- # Apply constraints
79
+ # Create constraint applicator (constraints will be applied during solve via extra_functionality)
80
80
  constraint_applicator = ConstraintApplicator()
81
- if constraints_dsl or progress_callback:
82
- if progress_callback:
83
- progress_callback(60, "Applying constraints...")
84
- constraint_applicator.apply_constraints(conn, network_id, network, scenario_id, constraints_dsl)
85
81
 
86
- # Solve network
82
+ # Solve network (constraints are applied during optimization)
87
83
  if progress_callback:
88
84
  progress_callback(70, f"Solving with {solver_name}...")
89
85
 
@@ -47,6 +47,53 @@ class ConstraintApplicator:
47
47
  if constraints_dsl:
48
48
  self._apply_dsl_constraints(network, constraints_dsl)
49
49
 
50
+ def _detect_constraint_type(self, constraint_code: str) -> str:
51
+ """
52
+ Detect if constraint is network-modification or model-constraint type.
53
+
54
+ Args:
55
+ constraint_code: The constraint code to analyze
56
+
57
+ Returns:
58
+ "model_constraint" or "network_modification"
59
+ """
60
+ # Type 2 indicators (model constraints) - need access to optimization model
61
+ model_indicators = [
62
+ 'n.optimize.create_model()',
63
+ 'm.variables',
64
+ 'm.add_constraints',
65
+ 'gen_p =',
66
+ 'constraint_expr =',
67
+ 'LinearExpression',
68
+ 'linopy',
69
+ 'Generator-p',
70
+ 'lhs <=',
71
+ 'constraint_expr =',
72
+ 'model.variables',
73
+ 'model.add_constraints'
74
+ ]
75
+
76
+ # Type 1 indicators (network modifications) - modify network directly
77
+ network_indicators = [
78
+ 'n.generators.loc',
79
+ 'n.add(',
80
+ 'n.buses.',
81
+ 'n.lines.',
82
+ 'network.generators.loc',
83
+ 'network.add(',
84
+ 'network.buses.',
85
+ 'network.lines.'
86
+ ]
87
+
88
+ # Check for model constraint indicators first (more specific)
89
+ if any(indicator in constraint_code for indicator in model_indicators):
90
+ return "model_constraint"
91
+ elif any(indicator in constraint_code for indicator in network_indicators):
92
+ return "network_modification"
93
+ else:
94
+ # Default to network_modification for safety (existing behavior)
95
+ return "network_modification"
96
+
50
97
  def _apply_database_constraints(
51
98
  self,
52
99
  conn,
@@ -64,6 +111,9 @@ class ConstraintApplicator:
64
111
 
65
112
  # Load constraint attributes and filter active ones
66
113
  active_constraints = []
114
+ model_constraints = []
115
+ network_constraints = []
116
+
67
117
  for constraint in constraints:
68
118
  try:
69
119
  # Get constraint attributes
@@ -79,16 +129,12 @@ class ConstraintApplicator:
79
129
  is_active_bool = is_active.static_value.data["Boolean"]
80
130
 
81
131
  if is_active_bool:
82
- # Extract code value first to check if it's an optimization constraint
132
+ # Extract code value
83
133
  code_val = ""
84
134
  if constraint_code.variant == "Static":
85
135
  if "String" in constraint_code.static_value.data:
86
136
  code_val = constraint_code.static_value.data["String"]
87
137
 
88
- # Skip optimization-time constraints in pre-optimization phase
89
- if 'net.model' in code_val or 'network.model' in code_val or 'n.model' in code_val:
90
- continue
91
-
92
138
  # Extract priority value
93
139
  priority_val = 0
94
140
  if priority.variant == "Static":
@@ -97,48 +143,137 @@ class ConstraintApplicator:
97
143
  elif "Float" in priority.static_value.data:
98
144
  priority_val = int(priority.static_value.data["Float"])
99
145
 
100
- active_constraints.append({
146
+ constraint_dict = {
101
147
  'id': constraint.id,
102
148
  'name': constraint.name,
103
149
  'priority': priority_val,
104
- 'code': code_val
105
- })
150
+ 'code': code_val,
151
+ 'constraint_code': code_val # For compatibility
152
+ }
153
+
154
+ # Detect constraint type and separate them
155
+ constraint_type = self._detect_constraint_type(code_val)
156
+ if constraint_type == "model_constraint":
157
+ model_constraints.append(constraint_dict)
158
+ logger.info(f"Detected model constraint: {constraint.name}")
159
+ else:
160
+ network_constraints.append(constraint_dict)
161
+ logger.info(f"Detected network constraint: {constraint.name}")
106
162
 
107
163
  except Exception as e:
108
164
  logger.warning(f"Failed to load constraint {constraint.name}: {e}")
109
165
  continue
110
166
 
111
- if not active_constraints:
167
+ if not model_constraints and not network_constraints:
112
168
  return
113
169
 
114
- # Sort constraints by priority (lower numbers first)
115
- active_constraints.sort(key=lambda x: x['priority'])
170
+ logger.info(f"Constraint breakdown: {len(model_constraints)} model constraints, {len(network_constraints)} network constraints")
171
+
172
+ # Apply network constraints first (they modify the network structure)
173
+ if network_constraints:
174
+ network_constraints.sort(key=lambda x: x['priority'])
175
+ for constraint in network_constraints:
176
+ try:
177
+ logger.info(f"Executing network constraint '{constraint['name']}' (priority {constraint['priority']})")
178
+
179
+ # Execute the constraint code in the normal Python environment
180
+ exec_globals = {
181
+ 'n': network,
182
+ 'network': network,
183
+ 'pd': pd,
184
+ 'np': np,
185
+ }
186
+
187
+ # Execute the constraint code
188
+ exec(constraint['code'], exec_globals)
189
+
190
+ except Exception as e:
191
+ error_msg = f"Failed to execute network constraint '{constraint['name']}': {e}"
192
+ logger.error(error_msg, exc_info=True)
193
+ # Continue with other constraints instead of failing the entire solve
194
+ continue
195
+
196
+ # Apply model constraints (they need access to the optimization model)
197
+ if model_constraints:
198
+ self._apply_model_constraints(network, model_constraints)
199
+
200
+ except Exception as e:
201
+ logger.error(f"Failed to apply custom constraints: {e}", exc_info=True)
202
+
203
+ def _apply_model_constraints(self, network: 'pypsa.Network', model_constraints: list):
204
+ """
205
+ Apply model constraints that need access to the optimization model.
206
+
207
+ This creates the optimization model, applies constraints to it, and then
208
+ replaces PyPSA's solve method to use the pre-constrained model.
209
+
210
+ Args:
211
+ network: PyPSA Network object
212
+ model_constraints: List of model constraint dictionaries
213
+ """
214
+ try:
215
+ logger.info(f"Applying {len(model_constraints)} model constraints...")
216
+
217
+ # Create the optimization model (same as PyPSA would do internally)
218
+ logger.info("Creating optimization model for constraint application...")
219
+ model = network.optimize.create_model()
220
+ logger.info(f"Created optimization model with {len(model.variables)} variable groups")
116
221
 
117
- # Execute constraints in order
118
- for constraint in active_constraints:
222
+ # Sort constraints by priority
223
+ sorted_constraints = sorted(model_constraints, key=lambda x: x['priority'])
224
+
225
+ # Apply each model constraint
226
+ for constraint in sorted_constraints:
119
227
  try:
120
- logger.info(f"Executing constraint '{constraint['name']}' (priority {constraint['priority']})")
121
- logger.debug(f"Code: {constraint['code']}")
228
+ constraint_code = constraint['constraint_code']
229
+ constraint_name = constraint['name']
122
230
 
123
- # Execute the constraint code in the normal Python environment
124
- # The network object 'n' is available in the global scope
231
+ logger.info(f"Applying model constraint '{constraint_name}' (priority {constraint['priority']})")
232
+
233
+ # Create execution environment with network, model, and utilities
125
234
  exec_globals = {
126
235
  'n': network,
236
+ 'network': network,
237
+ 'model': model,
238
+ 'm': model,
239
+ 'snapshots': network.snapshots,
127
240
  'pd': pd,
128
241
  'np': np,
242
+ 'xr': __import__('xarray'), # Import xarray for DataArray operations
129
243
  }
130
244
 
131
245
  # Execute the constraint code
132
- exec(constraint['code'], exec_globals)
246
+ exec(constraint_code, exec_globals)
247
+ logger.info(f"Successfully applied model constraint '{constraint_name}'")
133
248
 
134
249
  except Exception as e:
135
- error_msg = f"Failed to execute constraint '{constraint['name']}': {e}"
250
+ error_msg = f"Failed to apply model constraint '{constraint.get('name', 'unknown')}': {e}"
136
251
  logger.error(error_msg, exc_info=True)
137
- # Continue with other constraints instead of failing the entire solve
252
+ # Continue with other constraints instead of failing
138
253
  continue
139
-
254
+
255
+ # Store the constrained model for the solver to use
256
+ # We'll replace PyPSA's solve_model method to use our pre-constrained model
257
+ logger.info("Replacing PyPSA's solve method to use pre-constrained model...")
258
+
259
+ # Store original methods
260
+ original_optimize = network.optimize
261
+ original_solve_model = original_optimize.solve_model
262
+
263
+ # Create a wrapper that uses our pre-constrained model
264
+ def constrained_solve_model(*args, **kwargs):
265
+ """Use the pre-constrained model instead of creating a new one."""
266
+ logger.info("Using pre-constrained model for solve...")
267
+ return original_solve_model(model, *args, **kwargs)
268
+
269
+ # Replace the solve_model method
270
+ network.optimize.solve_model = constrained_solve_model
271
+
272
+ logger.info(f"Successfully applied {len(model_constraints)} model constraints")
273
+
140
274
  except Exception as e:
141
- logger.error(f"Failed to apply custom constraints: {e}", exc_info=True)
275
+ logger.error(f"Failed to apply model constraints: {e}", exc_info=True)
276
+ # Don't re-raise - let the solve continue without constraints rather than fail completely
142
277
 
143
278
  def _apply_dsl_constraints(self, network: 'pypsa.Network', constraints_dsl: str):
144
279
  """
@@ -172,7 +307,8 @@ class ConstraintApplicator:
172
307
  scenario_id: Optional[int] = None
173
308
  ) -> List[Dict[str, Any]]:
174
309
  """
175
- Get constraints that need to be applied during optimization (via extra_functionality).
310
+ Get ALL active constraints for optimization-time application.
311
+ The solver will determine which are model constraints vs network constraints.
176
312
 
177
313
  Args:
178
314
  conn: Database connection
@@ -180,7 +316,7 @@ class ConstraintApplicator:
180
316
  scenario_id: Optional scenario ID
181
317
 
182
318
  Returns:
183
- List of optimization constraints
319
+ List of all active constraints
184
320
  """
185
321
  try:
186
322
  # Load all constraints for this network
@@ -189,7 +325,7 @@ class ConstraintApplicator:
189
325
  if not constraints:
190
326
  return []
191
327
 
192
- # Load constraint attributes and filter active optimization-time ones
328
+ # Load constraint attributes and filter active ones
193
329
  optimization_constraints = []
194
330
  for constraint in constraints:
195
331
  try:
@@ -212,26 +348,24 @@ class ConstraintApplicator:
212
348
  if "String" in constraint_code.static_value.data:
213
349
  code_val = constraint_code.static_value.data["String"]
214
350
 
215
- # Check if this is an optimization-time constraint
216
- # (contains model access patterns like 'net.model' or 'network.model')
217
- if 'net.model' in code_val or 'network.model' in code_val or 'n.model' in code_val:
218
- # Extract priority value
219
- priority_val = 0
220
- if priority.variant == "Static":
221
- if "Integer" in priority.static_value.data:
222
- priority_val = priority.static_value.data["Integer"]
223
- elif "Float" in priority.static_value.data:
224
- priority_val = int(priority.static_value.data["Float"])
225
-
226
- optimization_constraints.append({
227
- 'id': constraint.id,
228
- 'name': constraint.name,
229
- 'priority': priority_val,
230
- 'code': code_val
231
- })
351
+ # Extract priority value
352
+ priority_val = 0
353
+ if priority.variant == "Static":
354
+ if "Integer" in priority.static_value.data:
355
+ priority_val = priority.static_value.data["Integer"]
356
+ elif "Float" in priority.static_value.data:
357
+ priority_val = int(priority.static_value.data["Float"])
358
+
359
+ optimization_constraints.append({
360
+ 'id': constraint.id,
361
+ 'name': constraint.name,
362
+ 'priority': priority_val,
363
+ 'constraint_code': code_val, # Use consistent key name
364
+ 'code': code_val # Keep both for compatibility
365
+ })
232
366
 
233
367
  except Exception as e:
234
- logger.warning(f"Failed to load optimization constraint {constraint.name}: {e}")
368
+ logger.warning(f"Failed to load constraint {constraint.name}: {e}")
235
369
  continue
236
370
 
237
371
  # Sort constraints by priority (lower numbers first)
@@ -310,6 +444,7 @@ class ConstraintApplicator:
310
444
  'snapshots': snapshots,
311
445
  'pd': pd,
312
446
  'np': np,
447
+ 'xr': __import__('xarray'),
313
448
  }
314
449
 
315
450
  # Execute the constraint code
@@ -139,13 +139,39 @@ class NetworkSolver:
139
139
  if conn and network_id:
140
140
  self._set_snapshot_weightings_after_multiperiod(conn, network_id, network)
141
141
 
142
- # Prepare optimization constraints (extra_functionality)
142
+ # Prepare optimization constraints with type detection
143
143
  extra_functionality = None
144
+ model_constraints = []
145
+ network_constraints = []
146
+
144
147
  if conn and network_id and constraint_applicator:
145
148
  optimization_constraints = constraint_applicator.get_optimization_constraints(conn, network_id, scenario_id)
146
149
  if optimization_constraints:
147
- logger.info(f"Applying {len(optimization_constraints)} optimization-time constraints")
148
- extra_functionality = self._create_extra_functionality(optimization_constraints, constraint_applicator)
150
+ logger.info(f"Found {len(optimization_constraints)} optimization constraints")
151
+
152
+ # Separate constraints by type
153
+ for constraint in optimization_constraints:
154
+ constraint_code = constraint.get('constraint_code', '')
155
+ constraint_type = self._detect_constraint_type(constraint_code)
156
+ constraint_name = constraint.get('name', 'unknown')
157
+
158
+ if constraint_type == "model_constraint":
159
+ model_constraints.append(constraint)
160
+ logger.info(f"Detected model constraint: {constraint_name}")
161
+ else:
162
+ network_constraints.append(constraint)
163
+ logger.info(f"Detected network constraint: {constraint_name}")
164
+
165
+ logger.info(f"Constraint breakdown: {len(model_constraints)} model constraints, {len(network_constraints)} network constraints")
166
+
167
+ # Create extra_functionality for ALL constraints (both model and network)
168
+ all_constraints = model_constraints + network_constraints
169
+ if all_constraints:
170
+ extra_functionality = self._create_extra_functionality(all_constraints, constraint_applicator)
171
+ logger.info(f"Prepared {len(all_constraints)} constraints for optimization-time application")
172
+
173
+ # NOTE: Model constraints are now applied DURING solve via extra_functionality
174
+ # This ensures they are applied to the actual model PyPSA creates, not a separate model
149
175
 
150
176
  # Solver diagnostics
151
177
  logger.info(f"=== PYPSA SOLVER DIAGNOSTICS ===")
@@ -342,7 +368,7 @@ class NetworkSolver:
342
368
  return 'gurobi', gurobi_dual_options
343
369
 
344
370
  # Check if this is a known valid solver name
345
- elif solver_name in ['highs', 'gurobi', 'cplex', 'glpk', 'cbc', 'scip']:
371
+ elif solver_name in ['highs', 'gurobi', 'cplex', 'glpk', 'cbc', 'scip', 'copt', 'mosek']:
346
372
  return solver_name, solver_options
347
373
 
348
374
  else:
@@ -351,6 +377,51 @@ class NetworkSolver:
351
377
  return 'highs', solver_options
352
378
 
353
379
 
380
+ def _detect_constraint_type(self, constraint_code: str) -> str:
381
+ """
382
+ Detect if constraint is network-modification or model-constraint type.
383
+
384
+ Args:
385
+ constraint_code: The constraint code to analyze
386
+
387
+ Returns:
388
+ "model_constraint" or "network_modification"
389
+ """
390
+ # Type 2 indicators (model constraints) - need access to optimization model
391
+ model_indicators = [
392
+ 'n.optimize.create_model()',
393
+ 'm.variables',
394
+ 'm.add_constraints',
395
+ 'gen_p =',
396
+ 'constraint_expr =',
397
+ 'LinearExpression',
398
+ 'linopy',
399
+ 'Generator-p',
400
+ 'lhs <=',
401
+ 'constraint_expr ='
402
+ ]
403
+
404
+ # Type 1 indicators (network modifications) - modify network directly
405
+ network_indicators = [
406
+ 'n.generators.loc',
407
+ 'n.add(',
408
+ 'n.buses.',
409
+ 'n.lines.',
410
+ 'network.generators.loc',
411
+ 'network.add(',
412
+ 'network.buses.',
413
+ 'network.lines.'
414
+ ]
415
+
416
+ # Check for model constraint indicators first (more specific)
417
+ if any(indicator in constraint_code for indicator in model_indicators):
418
+ return "model_constraint"
419
+ elif any(indicator in constraint_code for indicator in network_indicators):
420
+ return "network_modification"
421
+ else:
422
+ # Default to network_modification for safety (existing behavior)
423
+ return "network_modification"
424
+
354
425
  def _create_extra_functionality(self, optimization_constraints: list, constraint_applicator) -> callable:
355
426
  """
356
427
  Create extra_functionality function for optimization-time constraints.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyconvexity
3
- Version: 0.3.8
3
+ Version: 0.3.8.post2
4
4
  Summary: Python library for energy system modeling and optimization with PyPSA
5
5
  Author-email: Convexity Team <info@convexity.com>
6
6
  License: MIT
@@ -14,14 +14,19 @@ src/pyconvexity/core/errors.py
14
14
  src/pyconvexity/core/types.py
15
15
  src/pyconvexity/data/README.md
16
16
  src/pyconvexity/data/__init__.py
17
+ src/pyconvexity/data/__pycache__/__init__.cpython-313.pyc
17
18
  src/pyconvexity/data/loaders/__init__.py
18
19
  src/pyconvexity/data/loaders/cache.py
20
+ src/pyconvexity/data/loaders/__pycache__/__init__.cpython-313.pyc
21
+ src/pyconvexity/data/loaders/__pycache__/cache.cpython-313.pyc
19
22
  src/pyconvexity/data/schema/01_core_schema.sql
20
23
  src/pyconvexity/data/schema/02_data_metadata.sql
21
24
  src/pyconvexity/data/schema/03_validation_data.sql
22
25
  src/pyconvexity/data/schema/04_scenario_schema.sql
23
26
  src/pyconvexity/data/sources/__init__.py
24
27
  src/pyconvexity/data/sources/gem.py
28
+ src/pyconvexity/data/sources/__pycache__/__init__.cpython-313.pyc
29
+ src/pyconvexity/data/sources/__pycache__/gem.cpython-313.pyc
25
30
  src/pyconvexity/io/__init__.py
26
31
  src/pyconvexity/io/excel_exporter.py
27
32
  src/pyconvexity/io/excel_importer.py
@@ -1 +0,0 @@
1
- __version__ = "0.3.8"
File without changes
File without changes