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.

Files changed (44) hide show
  1. pyconvexity/__init__.py +241 -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/dashboard.py +265 -0
  8. pyconvexity/data/README.md +101 -0
  9. pyconvexity/data/__init__.py +17 -0
  10. pyconvexity/data/loaders/__init__.py +3 -0
  11. pyconvexity/data/loaders/cache.py +213 -0
  12. pyconvexity/data/schema/01_core_schema.sql +420 -0
  13. pyconvexity/data/schema/02_data_metadata.sql +120 -0
  14. pyconvexity/data/schema/03_validation_data.sql +507 -0
  15. pyconvexity/data/sources/__init__.py +5 -0
  16. pyconvexity/data/sources/gem.py +442 -0
  17. pyconvexity/io/__init__.py +26 -0
  18. pyconvexity/io/excel_exporter.py +1226 -0
  19. pyconvexity/io/excel_importer.py +1381 -0
  20. pyconvexity/io/netcdf_exporter.py +191 -0
  21. pyconvexity/io/netcdf_importer.py +1802 -0
  22. pyconvexity/models/__init__.py +195 -0
  23. pyconvexity/models/attributes.py +730 -0
  24. pyconvexity/models/carriers.py +159 -0
  25. pyconvexity/models/components.py +611 -0
  26. pyconvexity/models/network.py +503 -0
  27. pyconvexity/models/results.py +148 -0
  28. pyconvexity/models/scenarios.py +234 -0
  29. pyconvexity/solvers/__init__.py +29 -0
  30. pyconvexity/solvers/pypsa/__init__.py +30 -0
  31. pyconvexity/solvers/pypsa/api.py +446 -0
  32. pyconvexity/solvers/pypsa/batch_loader.py +296 -0
  33. pyconvexity/solvers/pypsa/builder.py +655 -0
  34. pyconvexity/solvers/pypsa/clearing_price.py +678 -0
  35. pyconvexity/solvers/pypsa/constraints.py +405 -0
  36. pyconvexity/solvers/pypsa/solver.py +1442 -0
  37. pyconvexity/solvers/pypsa/storage.py +2096 -0
  38. pyconvexity/timeseries.py +330 -0
  39. pyconvexity/validation/__init__.py +25 -0
  40. pyconvexity/validation/rules.py +312 -0
  41. pyconvexity-0.4.8.dist-info/METADATA +148 -0
  42. pyconvexity-0.4.8.dist-info/RECORD +44 -0
  43. pyconvexity-0.4.8.dist-info/WHEEL +5 -0
  44. pyconvexity-0.4.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,446 @@
1
+ """
2
+ High-level API for PyPSA solver integration.
3
+
4
+ Provides user-friendly functions for the most common workflows.
5
+ """
6
+
7
+ from typing import Dict, Any, Optional, Callable
8
+
9
+ from pyconvexity.core.database import database_context
10
+ from pyconvexity.solvers.pypsa.builder import NetworkBuilder
11
+ from pyconvexity.solvers.pypsa.solver import NetworkSolver
12
+ from pyconvexity.solvers.pypsa.storage import ResultStorage
13
+ from pyconvexity.solvers.pypsa.constraints import ConstraintApplicator
14
+
15
+
16
+ def solve_network(
17
+ db_path: str,
18
+ scenario_id: Optional[int] = None,
19
+ solver_name: str = "highs",
20
+ solver_options: Optional[Dict[str, Any]] = None,
21
+ constraints_dsl: Optional[str] = None,
22
+ discount_rate: Optional[float] = None,
23
+ progress_callback: Optional[Callable[[int, str], None]] = None,
24
+ return_detailed_results: bool = True,
25
+ custom_solver_config: Optional[Dict[str, Any]] = None,
26
+ include_unmet_loads: bool = True,
27
+ verbose: bool = False,
28
+ ) -> Dict[str, Any]:
29
+ """
30
+ Complete solve workflow: build PyPSA network from database, solve, store results (single network per database).
31
+
32
+ This is the main high-level function that most users should use. It handles
33
+ the complete workflow of loading data from database, building a PyPSA network,
34
+ solving it, and storing results back to the database.
35
+
36
+ Args:
37
+ db_path: Path to the database file
38
+ scenario_id: Optional scenario ID (NULL for base network)
39
+ solver_name: Solver to use (default: "highs"). Use "custom" for custom_solver_config.
40
+ solver_options: Optional solver-specific options
41
+ constraints_dsl: Optional DSL constraints to apply
42
+ discount_rate: Optional discount rate for multi-period optimization
43
+ progress_callback: Optional callback for progress updates (progress: int, message: str)
44
+ return_detailed_results: If True, return comprehensive results; if False, return simple status
45
+ custom_solver_config: Optional custom solver configuration when solver_name="custom"
46
+ Format: {"solver": "actual_solver_name", "solver_options": {...}}
47
+ Example: {"solver": "gurobi", "solver_options": {"Method": 2, "Crossover": 0}}
48
+ include_unmet_loads: Whether to include unmet load components in the network (default: True)
49
+ verbose: Enable detailed logging output (default: False)
50
+
51
+ Returns:
52
+ Dictionary with solve results - comprehensive if return_detailed_results=True, simple status otherwise
53
+
54
+ Raises:
55
+ DatabaseError: If database operations fail
56
+ ValidationError: If network data is invalid
57
+ ImportError: If PyPSA is not available
58
+ """
59
+ if progress_callback:
60
+ progress_callback(0, "Starting network solve...")
61
+
62
+ with database_context(db_path) as conn:
63
+ # Load network configuration with scenario awareness
64
+ from pyconvexity.models import get_network_config
65
+
66
+ network_config = get_network_config(conn, scenario_id)
67
+ if progress_callback:
68
+ progress_callback(8, "Loaded network configuration")
69
+
70
+ # Use configuration values with parameter overrides
71
+ # Note: network_config already has default of 0.0 from get_network_config()
72
+ effective_discount_rate = (
73
+ discount_rate
74
+ if discount_rate is not None
75
+ else network_config.get("discount_rate")
76
+ )
77
+
78
+ # Build network
79
+ if progress_callback:
80
+ progress_callback(10, "Building PyPSA network...")
81
+
82
+ builder = NetworkBuilder(verbose=verbose)
83
+ network = builder.build_network(
84
+ conn, scenario_id, progress_callback, include_unmet_loads
85
+ )
86
+
87
+ if progress_callback:
88
+ progress_callback(
89
+ 50,
90
+ f"Network built: {len(network.buses)} buses, {len(network.generators)} generators",
91
+ )
92
+
93
+ # Create constraint applicator and apply constraints BEFORE solve
94
+ constraint_applicator = ConstraintApplicator()
95
+
96
+ # Apply constraints before solving (network modifications like GlobalConstraints)
97
+ if progress_callback:
98
+ progress_callback(60, "Applying constraints...")
99
+
100
+ constraint_applicator.apply_constraints(
101
+ conn, network, scenario_id, constraints_dsl
102
+ )
103
+
104
+ # Solve network
105
+ if progress_callback:
106
+ progress_callback(70, f"Solving with {solver_name}...")
107
+
108
+ solver = NetworkSolver(verbose=verbose)
109
+ solve_result = solver.solve_network(
110
+ network,
111
+ solver_name=solver_name,
112
+ solver_options=solver_options,
113
+ discount_rate=effective_discount_rate, # Use effective discount rate from config
114
+ conn=conn,
115
+ scenario_id=scenario_id,
116
+ constraint_applicator=constraint_applicator,
117
+ custom_solver_config=custom_solver_config,
118
+ )
119
+
120
+ if progress_callback:
121
+ progress_callback(85, "Storing results...")
122
+
123
+ # Store results - ALWAYS store results regardless of return_detailed_results flag
124
+ storage = ResultStorage(verbose=verbose)
125
+ storage_result = storage.store_results(conn, network, solve_result, scenario_id)
126
+
127
+ if progress_callback:
128
+ progress_callback(95, "Solve completed successfully")
129
+
130
+ # Optimize database after successful solve (if solve was successful)
131
+ if solve_result.get("success", False):
132
+ try:
133
+ if progress_callback:
134
+ progress_callback(98, "Optimizing database...")
135
+
136
+ from pyconvexity.core.database import (
137
+ should_optimize_database,
138
+ optimize_database,
139
+ )
140
+
141
+ # Only optimize if there's significant free space (>5% threshold for post-solve)
142
+ if should_optimize_database(conn, free_space_threshold_percent=5.0):
143
+ optimize_database(conn)
144
+
145
+ except Exception:
146
+ # Don't fail the solve if optimization fails
147
+ pass
148
+
149
+ if progress_callback:
150
+ progress_callback(100, "Complete")
151
+
152
+ # Return simple status if requested (for sidecar/async usage)
153
+ # Results are now stored in database regardless of this flag
154
+ if not return_detailed_results:
155
+ return {
156
+ "success": solve_result.get("success", False),
157
+ "message": (
158
+ "Solve completed successfully"
159
+ if solve_result.get("success")
160
+ else "Solve failed"
161
+ ),
162
+ "error": (
163
+ solve_result.get("error")
164
+ if not solve_result.get("success")
165
+ else None
166
+ ),
167
+ "scenario_id": scenario_id,
168
+ }
169
+
170
+ # Combine results in comprehensive format for detailed analysis
171
+ comprehensive_result = {
172
+ **solve_result,
173
+ "storage_stats": storage_result,
174
+ "scenario_id": scenario_id,
175
+ }
176
+
177
+ # Transform to include sidecar-compatible format
178
+ return _transform_to_comprehensive_format(comprehensive_result)
179
+
180
+
181
+ def build_pypsa_network(
182
+ db_path: str,
183
+ scenario_id: Optional[int] = None,
184
+ progress_callback: Optional[Callable[[int, str], None]] = None,
185
+ verbose: bool = False,
186
+ ) -> "pypsa.Network":
187
+ """
188
+ Build PyPSA network object from database (single network per database).
189
+
190
+ This function loads all network data from the database and constructs
191
+ a PyPSA Network object ready for solving or analysis. Useful when you
192
+ want to inspect or modify the network before solving.
193
+
194
+ Args:
195
+ db_path: Path to the database file
196
+ scenario_id: Optional scenario ID (NULL for base network)
197
+ progress_callback: Optional callback for progress updates
198
+ verbose: Enable detailed logging output (default: False)
199
+
200
+ Returns:
201
+ PyPSA Network object ready for solving
202
+
203
+ Raises:
204
+ DatabaseError: If database operations fail
205
+ ValidationError: If network data is invalid
206
+ ImportError: If PyPSA is not available
207
+ """
208
+ with database_context(db_path) as conn:
209
+ builder = NetworkBuilder(verbose=verbose)
210
+ return builder.build_network(conn, scenario_id, progress_callback)
211
+
212
+
213
+ def solve_pypsa_network(
214
+ network: "pypsa.Network",
215
+ db_path: str,
216
+ scenario_id: Optional[int] = None,
217
+ solver_name: str = "highs",
218
+ solver_options: Optional[Dict[str, Any]] = None,
219
+ discount_rate: Optional[float] = None,
220
+ store_results: bool = True,
221
+ progress_callback: Optional[Callable[[int, str], None]] = None,
222
+ custom_solver_config: Optional[Dict[str, Any]] = None,
223
+ verbose: bool = False,
224
+ ) -> Dict[str, Any]:
225
+ """
226
+ Solve PyPSA network and optionally store results back to database (single network per database).
227
+
228
+ This function takes an existing PyPSA network (e.g., from build_pypsa_network),
229
+ solves it, and optionally stores the results back to the database.
230
+
231
+ Args:
232
+ network: PyPSA Network object to solve
233
+ db_path: Path to the database file (needed for result storage)
234
+ scenario_id: Optional scenario ID (NULL for base network)
235
+ solver_name: Solver to use (default: "highs"). Use "custom" for custom_solver_config.
236
+ solver_options: Optional solver-specific options
237
+ discount_rate: Optional discount rate for multi-period optimization (default: 0.0)
238
+ store_results: Whether to store results back to database (default: True)
239
+ progress_callback: Optional callback for progress updates
240
+ custom_solver_config: Optional custom solver configuration when solver_name="custom"
241
+ Format: {"solver": "actual_solver_name", "solver_options": {...}}
242
+ verbose: Enable detailed logging output (default: False)
243
+
244
+ Returns:
245
+ Dictionary with solve results and statistics
246
+
247
+ Raises:
248
+ DatabaseError: If database operations fail (when store_results=True)
249
+ ImportError: If PyPSA is not available
250
+ """
251
+ if progress_callback:
252
+ progress_callback(0, f"Solving network with {solver_name}...")
253
+
254
+ # Solve network
255
+ solver = NetworkSolver(verbose=verbose)
256
+ solve_result = solver.solve_network(
257
+ network,
258
+ solver_name=solver_name,
259
+ solver_options=solver_options,
260
+ discount_rate=discount_rate,
261
+ custom_solver_config=custom_solver_config,
262
+ )
263
+
264
+ if progress_callback:
265
+ progress_callback(70, "Solve completed")
266
+
267
+ # Store results if requested
268
+ if store_results:
269
+ if progress_callback:
270
+ progress_callback(80, "Storing results...")
271
+
272
+ with database_context(db_path) as conn:
273
+ storage = ResultStorage(verbose=verbose)
274
+ storage_result = storage.store_results(
275
+ conn, network, solve_result, scenario_id
276
+ )
277
+ solve_result["storage_stats"] = storage_result
278
+
279
+ if progress_callback:
280
+ progress_callback(100, "Complete")
281
+
282
+ return solve_result
283
+
284
+
285
+ def load_network_components(
286
+ db_path: str, scenario_id: Optional[int] = None
287
+ ) -> Dict[str, Any]:
288
+ """
289
+ Load all network components and attributes as structured data (single network per database).
290
+
291
+ This low-level function loads network data without building a PyPSA network.
292
+ Useful for analysis, validation, or building custom network representations.
293
+
294
+ Args:
295
+ db_path: Path to the database file
296
+ scenario_id: Optional scenario ID (NULL for base network)
297
+
298
+ Returns:
299
+ Dictionary containing all network components and metadata
300
+
301
+ Raises:
302
+ DatabaseError: If database operations fail
303
+ """
304
+ with database_context(db_path) as conn:
305
+ builder = NetworkBuilder()
306
+ return builder.load_network_data(conn, scenario_id)
307
+
308
+
309
+ def apply_constraints(
310
+ network: "pypsa.Network",
311
+ db_path: str,
312
+ scenario_id: Optional[int] = None,
313
+ constraints_dsl: Optional[str] = None,
314
+ ) -> None:
315
+ """
316
+ Apply custom constraints to PyPSA network (single network per database).
317
+
318
+ This function applies database-stored constraints and optional DSL constraints
319
+ to an existing PyPSA network. Modifies the network in-place.
320
+
321
+ Args:
322
+ network: PyPSA Network object to modify
323
+ db_path: Path to the database file
324
+ scenario_id: Optional scenario ID (NULL for base network)
325
+ constraints_dsl: Optional DSL constraints string
326
+
327
+ Raises:
328
+ DatabaseError: If database operations fail
329
+ ValidationError: If constraints are invalid
330
+ """
331
+ with database_context(db_path) as conn:
332
+ constraint_applicator = ConstraintApplicator()
333
+ constraint_applicator.apply_constraints(
334
+ conn, network, scenario_id, constraints_dsl
335
+ )
336
+
337
+
338
+ def store_solve_results(
339
+ network: "pypsa.Network",
340
+ db_path: str,
341
+ scenario_id: Optional[int],
342
+ solve_metadata: Dict[str, Any],
343
+ verbose: bool = False,
344
+ ) -> Dict[str, Any]:
345
+ """
346
+ Store PyPSA solve results back to database (single network per database).
347
+
348
+ This low-level function stores solve results from a PyPSA network back
349
+ to the database. Useful when you want full control over the solving process
350
+ but still want to store results in the standard format.
351
+
352
+ Args:
353
+ network: Solved PyPSA Network object
354
+ db_path: Path to the database file
355
+ scenario_id: Scenario ID for result storage (NULL for base network)
356
+ solve_metadata: Dictionary with solve metadata (solver_name, solve_time, etc.)
357
+ verbose: Enable detailed logging output (default: False)
358
+
359
+ Returns:
360
+ Dictionary with storage statistics
361
+
362
+ Raises:
363
+ DatabaseError: If database operations fail
364
+ """
365
+ with database_context(db_path) as conn:
366
+ storage = ResultStorage(verbose=verbose)
367
+ return storage.store_results(conn, network, solve_metadata, scenario_id)
368
+
369
+
370
+ def _transform_to_comprehensive_format(
371
+ pyconvexity_result: Dict[str, Any],
372
+ ) -> Dict[str, Any]:
373
+ """
374
+ Transform pyconvexity result to comprehensive format that includes both
375
+ the original structure and sidecar-compatible fields.
376
+
377
+ This ensures compatibility with existing sidecar code while providing
378
+ a clean API for direct pyconvexity users.
379
+
380
+ Args:
381
+ pyconvexity_result: Result from pyconvexity solve operations
382
+
383
+ Returns:
384
+ Comprehensive result with both original and sidecar-compatible fields
385
+ """
386
+ try:
387
+ # Extract basic solve information
388
+ success = pyconvexity_result.get("success", False)
389
+ status = pyconvexity_result.get("status", "unknown")
390
+ solve_time = pyconvexity_result.get("solve_time", 0.0)
391
+ objective_value = pyconvexity_result.get("objective_value")
392
+
393
+ # Extract storage stats
394
+ storage_stats = pyconvexity_result.get("storage_stats", {})
395
+ component_stats = storage_stats.get("component_stats", {})
396
+ network_stats = storage_stats.get("network_stats", {})
397
+
398
+ # Create comprehensive result that includes both formats
399
+ comprehensive_result = {
400
+ # Original pyconvexity format (for direct users)
401
+ **pyconvexity_result,
402
+ # Sidecar-compatible format (for backward compatibility)
403
+ "network_statistics": {
404
+ "total_generation_mwh": network_stats.get("total_generation_mwh", 0.0),
405
+ "total_load_mwh": network_stats.get("total_load_mwh", 0.0),
406
+ "unmet_load_mwh": network_stats.get("unmet_load_mwh", 0.0),
407
+ "total_cost": network_stats.get("total_cost", objective_value or 0.0),
408
+ "num_buses": network_stats.get("num_buses", 0),
409
+ "num_generators": network_stats.get("num_generators", 0),
410
+ "num_loads": network_stats.get("num_loads", 0),
411
+ "num_lines": network_stats.get("num_lines", 0),
412
+ "num_links": network_stats.get("num_links", 0),
413
+ },
414
+ "component_storage_stats": {
415
+ "stored_bus_results": component_stats.get("stored_bus_results", 0),
416
+ "stored_generator_results": component_stats.get(
417
+ "stored_generator_results", 0
418
+ ),
419
+ "stored_unmet_load_results": component_stats.get(
420
+ "stored_unmet_load_results", 0
421
+ ),
422
+ "stored_load_results": component_stats.get("stored_load_results", 0),
423
+ "stored_line_results": component_stats.get("stored_line_results", 0),
424
+ "stored_link_results": component_stats.get("stored_link_results", 0),
425
+ "stored_storage_unit_results": component_stats.get(
426
+ "stored_storage_unit_results", 0
427
+ ),
428
+ "stored_store_results": component_stats.get("stored_store_results", 0),
429
+ "skipped_attributes": component_stats.get("skipped_attributes", 0),
430
+ "errors": component_stats.get("errors", 0),
431
+ },
432
+ # Additional compatibility fields
433
+ "multi_period": pyconvexity_result.get("multi_period", False),
434
+ "years": pyconvexity_result.get("years", []),
435
+ }
436
+
437
+ return comprehensive_result
438
+
439
+ except Exception as e:
440
+ # Return original result with error info if transformation fails
441
+ return {
442
+ **pyconvexity_result,
443
+ "transformation_error": str(e),
444
+ "network_statistics": {},
445
+ "component_storage_stats": {},
446
+ }