pyconvexity 0.5.0__tar.gz → 0.5.1__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.
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/PKG-INFO +1 -1
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/pyproject.toml +2 -2
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/__init__.py +9 -0
- pyconvexity-0.5.1/src/pyconvexity/_version.py +1 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/solvers/pypsa/solver.py +11 -373
- pyconvexity-0.5.1/src/pyconvexity/transformations/__init__.py +15 -0
- pyconvexity-0.5.1/src/pyconvexity/transformations/api.py +93 -0
- pyconvexity-0.5.1/src/pyconvexity/transformations/time_axis.py +721 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity.egg-info/PKG-INFO +1 -1
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity.egg-info/SOURCES.txt +3 -0
- pyconvexity-0.5.0/src/pyconvexity/_version.py +0 -1
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/README.md +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/setup.cfg +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/core/__init__.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/core/database.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/core/errors.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/core/types.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/dashboard.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/data/README.md +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/data/__init__.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/data/loaders/__init__.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/data/loaders/cache.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/data/schema/01_core_schema.sql +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/data/schema/02_data_metadata.sql +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/data/schema/03_validation_data.sql +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/data/sources/__init__.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/data/sources/gem.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/io/__init__.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/io/excel_exporter.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/io/excel_importer.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/io/netcdf_exporter.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/io/netcdf_importer.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/models/__init__.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/models/attributes.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/models/carriers.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/models/components.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/models/network.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/models/results.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/models/scenarios.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/solvers/__init__.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/solvers/pypsa/__init__.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/solvers/pypsa/api.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/solvers/pypsa/batch_loader.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/solvers/pypsa/builder.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/solvers/pypsa/clearing_price.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/solvers/pypsa/constraints.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/solvers/pypsa/storage.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/timeseries.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/validation/__init__.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity/validation/rules.py +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity.egg-info/dependency_links.txt +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity.egg-info/requires.txt +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/src/pyconvexity.egg-info/top_level.txt +0 -0
- {pyconvexity-0.5.0 → pyconvexity-0.5.1}/tests/test_core_types.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyconvexity"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.1"
|
|
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 = 88
|
|
82
82
|
|
|
83
83
|
[tool.mypy]
|
|
84
|
-
python_version = "0.5.
|
|
84
|
+
python_version = "0.5.1"
|
|
85
85
|
warn_return_any = true
|
|
86
86
|
warn_unused_configs = true
|
|
87
87
|
disallow_untyped_defs = true
|
|
@@ -239,3 +239,12 @@ try:
|
|
|
239
239
|
except ImportError:
|
|
240
240
|
# NetCDF dependencies not available
|
|
241
241
|
pass
|
|
242
|
+
|
|
243
|
+
# Transformation operations
|
|
244
|
+
try:
|
|
245
|
+
from pyconvexity.transformations import modify_time_axis
|
|
246
|
+
|
|
247
|
+
__all__.append("modify_time_axis")
|
|
248
|
+
except ImportError:
|
|
249
|
+
# Transformation dependencies (pandas, numpy) not available
|
|
250
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.1"
|
|
@@ -38,81 +38,6 @@ class NetworkSolver:
|
|
|
38
38
|
"Please ensure it is installed correctly in the environment."
|
|
39
39
|
) from e
|
|
40
40
|
|
|
41
|
-
def _get_user_settings_path(self):
|
|
42
|
-
"""Get the path to the user settings file (same location as Tauri uses)"""
|
|
43
|
-
try:
|
|
44
|
-
import platform
|
|
45
|
-
import os
|
|
46
|
-
from pathlib import Path
|
|
47
|
-
|
|
48
|
-
system = platform.system()
|
|
49
|
-
if system == "Darwin": # macOS
|
|
50
|
-
home = Path.home()
|
|
51
|
-
app_data_dir = (
|
|
52
|
-
home / "Library" / "Application Support" / "com.convexity.desktop"
|
|
53
|
-
)
|
|
54
|
-
elif system == "Windows":
|
|
55
|
-
app_data_dir = (
|
|
56
|
-
Path(os.environ.get("APPDATA", "")) / "com.convexity.desktop"
|
|
57
|
-
)
|
|
58
|
-
else: # Linux
|
|
59
|
-
home = Path.home()
|
|
60
|
-
app_data_dir = home / ".local" / "share" / "com.convexity.desktop"
|
|
61
|
-
|
|
62
|
-
settings_file = app_data_dir / "user_settings.json"
|
|
63
|
-
return settings_file if settings_file.exists() else None
|
|
64
|
-
|
|
65
|
-
except Exception as e:
|
|
66
|
-
return None
|
|
67
|
-
|
|
68
|
-
def _resolve_default_solver(self) -> str:
|
|
69
|
-
"""Resolve 'default' solver to user's preferred solver"""
|
|
70
|
-
try:
|
|
71
|
-
import json
|
|
72
|
-
|
|
73
|
-
settings_path = self._get_user_settings_path()
|
|
74
|
-
if not settings_path:
|
|
75
|
-
return "highs"
|
|
76
|
-
|
|
77
|
-
with open(settings_path, "r") as f:
|
|
78
|
-
user_settings = json.load(f)
|
|
79
|
-
|
|
80
|
-
# Get default solver from user settings
|
|
81
|
-
default_solver = user_settings.get("default_solver", "highs")
|
|
82
|
-
|
|
83
|
-
# Validate that it's a known solver
|
|
84
|
-
known_solvers = [
|
|
85
|
-
"highs",
|
|
86
|
-
"gurobi",
|
|
87
|
-
"gurobi (barrier)",
|
|
88
|
-
"gurobi (barrier homogeneous)",
|
|
89
|
-
"gurobi (barrier+crossover balanced)",
|
|
90
|
-
"gurobi (dual simplex)",
|
|
91
|
-
"mosek",
|
|
92
|
-
"mosek (default)",
|
|
93
|
-
"mosek (barrier)",
|
|
94
|
-
"mosek (barrier+crossover)",
|
|
95
|
-
"mosek (dual simplex)",
|
|
96
|
-
"copt",
|
|
97
|
-
"copt (barrier)",
|
|
98
|
-
"copt (barrier homogeneous)",
|
|
99
|
-
"copt (barrier+crossover)",
|
|
100
|
-
"copt (dual simplex)",
|
|
101
|
-
"copt (concurrent)",
|
|
102
|
-
"cplex",
|
|
103
|
-
"glpk",
|
|
104
|
-
"cbc",
|
|
105
|
-
"scip",
|
|
106
|
-
]
|
|
107
|
-
|
|
108
|
-
if default_solver in known_solvers:
|
|
109
|
-
return default_solver
|
|
110
|
-
else:
|
|
111
|
-
return "highs"
|
|
112
|
-
|
|
113
|
-
except Exception as e:
|
|
114
|
-
return "highs"
|
|
115
|
-
|
|
116
41
|
def solve_network(
|
|
117
42
|
self,
|
|
118
43
|
network: "pypsa.Network",
|
|
@@ -269,24 +194,19 @@ class NetworkSolver:
|
|
|
269
194
|
custom_solver_config: Optional[Dict[str, Any]] = None,
|
|
270
195
|
) -> tuple[str, Optional[Dict[str, Any]]]:
|
|
271
196
|
"""
|
|
272
|
-
Get the actual solver name and options for
|
|
197
|
+
Get the actual solver name and options for solver configurations.
|
|
273
198
|
|
|
274
199
|
Args:
|
|
275
|
-
solver_name: The solver name (e.g., '
|
|
200
|
+
solver_name: The solver name (e.g., 'highs', 'gurobi', 'custom')
|
|
276
201
|
solver_options: Optional additional solver options
|
|
277
|
-
custom_solver_config:
|
|
202
|
+
custom_solver_config: Custom solver configuration (from frontend)
|
|
278
203
|
Format: {"solver": "actual_solver_name", "solver_options": {...}}
|
|
279
204
|
|
|
280
205
|
Returns:
|
|
281
206
|
Tuple of (actual_solver_name, solver_options_dict)
|
|
282
207
|
"""
|
|
283
|
-
# Handle "custom" solver with custom configuration
|
|
284
|
-
if solver_name == "custom":
|
|
285
|
-
if not custom_solver_config:
|
|
286
|
-
raise ValueError(
|
|
287
|
-
"custom_solver_config must be provided when solver_name='custom'"
|
|
288
|
-
)
|
|
289
|
-
|
|
208
|
+
# Handle "custom" solver with custom configuration from frontend
|
|
209
|
+
if solver_name == "custom" and custom_solver_config:
|
|
290
210
|
if "solver" not in custom_solver_config:
|
|
291
211
|
raise ValueError(
|
|
292
212
|
"custom_solver_config must contain 'solver' key with the actual solver name"
|
|
@@ -307,294 +227,12 @@ class NetworkSolver:
|
|
|
307
227
|
|
|
308
228
|
return actual_solver, merged_options
|
|
309
229
|
|
|
310
|
-
#
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
# Handle special Gurobi configurations
|
|
317
|
-
if solver_name == "gurobi (barrier)":
|
|
318
|
-
gurobi_barrier_options = {
|
|
319
|
-
"solver_options": {
|
|
320
|
-
"Method": 2, # Barrier
|
|
321
|
-
"Crossover": 0, # Skip crossover
|
|
322
|
-
"MIPGap": 0.05, # 5% gap
|
|
323
|
-
"Threads": 0, # Use all cores (0 = auto)
|
|
324
|
-
"Presolve": 2, # Aggressive presolve
|
|
325
|
-
"ConcurrentMIP": 1, # Parallel root strategies
|
|
326
|
-
"BarConvTol": 1e-4, # Relaxed barrier convergence
|
|
327
|
-
"FeasibilityTol": 1e-5,
|
|
328
|
-
"OptimalityTol": 1e-5,
|
|
329
|
-
"NumericFocus": 1, # Improve stability
|
|
330
|
-
"PreSparsify": 1,
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
# Merge with any additional options
|
|
334
|
-
if solver_options:
|
|
335
|
-
gurobi_barrier_options.update(solver_options)
|
|
336
|
-
return "gurobi", gurobi_barrier_options
|
|
337
|
-
|
|
338
|
-
elif solver_name == "gurobi (barrier homogeneous)":
|
|
339
|
-
gurobi_barrier_homogeneous_options = {
|
|
340
|
-
"solver_options": {
|
|
341
|
-
"Method": 2, # Barrier
|
|
342
|
-
"Crossover": 0, # Skip crossover
|
|
343
|
-
"MIPGap": 0.05,
|
|
344
|
-
"Threads": 0, # Use all cores (0 = auto)
|
|
345
|
-
"Presolve": 2,
|
|
346
|
-
"ConcurrentMIP": 1,
|
|
347
|
-
"BarConvTol": 1e-4,
|
|
348
|
-
"FeasibilityTol": 1e-5,
|
|
349
|
-
"OptimalityTol": 1e-5,
|
|
350
|
-
"NumericFocus": 1,
|
|
351
|
-
"PreSparsify": 1,
|
|
352
|
-
"BarHomogeneous": 1, # Enable homogeneous barrier algorithm
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
if solver_options:
|
|
356
|
-
gurobi_barrier_homogeneous_options.update(solver_options)
|
|
357
|
-
return "gurobi", gurobi_barrier_homogeneous_options
|
|
358
|
-
|
|
359
|
-
elif solver_name == "gurobi (barrier+crossover balanced)":
|
|
360
|
-
gurobi_options_balanced = {
|
|
361
|
-
"solver_options": {
|
|
362
|
-
"Method": 2,
|
|
363
|
-
"Crossover": 1, # Dual crossover
|
|
364
|
-
"MIPGap": 0.01,
|
|
365
|
-
"Threads": 0, # Use all cores (0 = auto)
|
|
366
|
-
"Presolve": 2,
|
|
367
|
-
"Heuristics": 0.1,
|
|
368
|
-
"Cuts": 2,
|
|
369
|
-
"ConcurrentMIP": 1,
|
|
370
|
-
"BarConvTol": 1e-6,
|
|
371
|
-
"FeasibilityTol": 1e-6,
|
|
372
|
-
"OptimalityTol": 1e-6,
|
|
373
|
-
"NumericFocus": 1,
|
|
374
|
-
"PreSparsify": 1,
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
if solver_options:
|
|
378
|
-
gurobi_options_balanced.update(solver_options)
|
|
379
|
-
return "gurobi", gurobi_options_balanced
|
|
380
|
-
|
|
381
|
-
elif solver_name == "gurobi (dual simplex)":
|
|
382
|
-
gurobi_dual_options = {
|
|
383
|
-
"solver_options": {
|
|
384
|
-
"Method": 1, # Dual simplex method
|
|
385
|
-
"Threads": 0, # Use all available cores
|
|
386
|
-
"Presolve": 2, # Aggressive presolve
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
if solver_options:
|
|
390
|
-
gurobi_dual_options.update(solver_options)
|
|
391
|
-
return "gurobi", gurobi_dual_options
|
|
392
|
-
|
|
393
|
-
# Handle special Mosek configurations
|
|
394
|
-
elif solver_name == "mosek (default)":
|
|
395
|
-
# No custom options - let Mosek use its default configuration
|
|
396
|
-
mosek_default_options = {
|
|
397
|
-
"solver_options": {
|
|
398
|
-
"MSK_DPAR_MIO_REL_GAP_CONST": 0.05, # MIP relative gap tolerance (5% to match Gurobi)
|
|
399
|
-
"MSK_DPAR_MIO_MAX_TIME": 36000, # Max time 10 hours
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
if solver_options:
|
|
403
|
-
mosek_default_options["solver_options"].update(solver_options)
|
|
404
|
-
return "mosek", mosek_default_options
|
|
405
|
-
|
|
406
|
-
elif solver_name == "mosek (barrier)":
|
|
407
|
-
mosek_barrier_options = {
|
|
408
|
-
"solver_options": {
|
|
409
|
-
"MSK_IPAR_INTPNT_BASIS": 0, # Skip crossover (barrier-only) - 0 = MSK_BI_NEVER
|
|
410
|
-
"MSK_DPAR_INTPNT_TOL_REL_GAP": 1e-4, # Match Gurobi barrier tolerance
|
|
411
|
-
"MSK_DPAR_INTPNT_TOL_PFEAS": 1e-5, # Match Gurobi primal feasibility
|
|
412
|
-
"MSK_DPAR_INTPNT_TOL_DFEAS": 1e-5, # Match Gurobi dual feasibility
|
|
413
|
-
# Removed MSK_DPAR_INTPNT_TOL_INFEAS - was 1000x tighter than other tolerances!
|
|
414
|
-
"MSK_IPAR_NUM_THREADS": 0, # Use all available cores (0 = auto)
|
|
415
|
-
"MSK_IPAR_PRESOLVE_USE": 2, # Aggressive presolve (match Gurobi Presolve=2)
|
|
416
|
-
"MSK_DPAR_MIO_REL_GAP_CONST": 0.05, # Match Gurobi 5% MIP gap
|
|
417
|
-
"MSK_IPAR_MIO_ROOT_OPTIMIZER": 4, # Use interior-point for MIP root
|
|
418
|
-
"MSK_DPAR_MIO_MAX_TIME": 36000, # Max time 10 hour
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
if solver_options:
|
|
422
|
-
mosek_barrier_options["solver_options"].update(solver_options)
|
|
423
|
-
return "mosek", mosek_barrier_options
|
|
424
|
-
|
|
425
|
-
elif solver_name == "mosek (barrier+crossover)":
|
|
426
|
-
mosek_barrier_crossover_options = {
|
|
427
|
-
"solver_options": {
|
|
428
|
-
"MSK_IPAR_INTPNT_BASIS": 1, # Always crossover (1 = MSK_BI_ALWAYS)
|
|
429
|
-
"MSK_DPAR_INTPNT_TOL_REL_GAP": 1e-4, # Match Gurobi barrier tolerance (was 1e-6)
|
|
430
|
-
"MSK_DPAR_INTPNT_TOL_PFEAS": 1e-5, # Match Gurobi (was 1e-6)
|
|
431
|
-
"MSK_DPAR_INTPNT_TOL_DFEAS": 1e-5, # Match Gurobi (was 1e-6)
|
|
432
|
-
"MSK_IPAR_NUM_THREADS": 0, # Use all available cores (0 = auto)
|
|
433
|
-
"MSK_DPAR_MIO_REL_GAP_CONST": 0.05, # Match Gurobi 5% MIP gap (was 1e-6)
|
|
434
|
-
"MSK_IPAR_MIO_ROOT_OPTIMIZER": 4, # Use interior-point for MIP root
|
|
435
|
-
"MSK_DPAR_MIO_MAX_TIME": 36000, # Max time 10 hour (safety limit)
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
if solver_options:
|
|
439
|
-
mosek_barrier_crossover_options["solver_options"].update(solver_options)
|
|
440
|
-
return "mosek", mosek_barrier_crossover_options
|
|
441
|
-
|
|
442
|
-
elif solver_name == "mosek (dual simplex)":
|
|
443
|
-
mosek_dual_options = {
|
|
444
|
-
"solver_options": {
|
|
445
|
-
"MSK_IPAR_NUM_THREADS": 0, # Use all available cores (0 = automatic)
|
|
446
|
-
"MSK_IPAR_PRESOLVE_USE": 1, # Force presolve
|
|
447
|
-
"MSK_DPAR_MIO_REL_GAP_CONST": 0.05, # Match Gurobi 5% MIP gap (was 1e-6)
|
|
448
|
-
"MSK_IPAR_MIO_ROOT_OPTIMIZER": 1, # Use dual simplex for MIP root
|
|
449
|
-
"MSK_DPAR_MIO_MAX_TIME": 36000, # Max time 10 hour (safety limit)
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
if solver_options:
|
|
453
|
-
mosek_dual_options["solver_options"].update(solver_options)
|
|
454
|
-
return "mosek", mosek_dual_options
|
|
455
|
-
|
|
456
|
-
# Check if this is a known valid solver name
|
|
457
|
-
elif solver_name == "mosek":
|
|
458
|
-
# Add default MILP-friendly settings for plain Mosek
|
|
459
|
-
mosek_defaults = {
|
|
460
|
-
"solver_options": {
|
|
461
|
-
"MSK_DPAR_MIO_REL_GAP_CONST": 0.05, # Match Gurobi 5% MIP gap (was 1e-4)
|
|
462
|
-
"MSK_DPAR_MIO_MAX_TIME": 36000, # Max time 10 hours
|
|
463
|
-
"MSK_IPAR_NUM_THREADS": 0, # Use all cores (0 = auto)
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
if solver_options:
|
|
467
|
-
mosek_defaults["solver_options"].update(solver_options)
|
|
468
|
-
return solver_name, mosek_defaults
|
|
469
|
-
|
|
470
|
-
elif solver_name == "gurobi":
|
|
471
|
-
# Add default MILP-friendly settings for plain Gurobi (for consistency)
|
|
472
|
-
gurobi_defaults = {
|
|
473
|
-
"solver_options": {
|
|
474
|
-
"MIPGap": 1e-4, # 0.01% gap
|
|
475
|
-
"TimeLimit": 3600, # 1 hour
|
|
476
|
-
"Threads": 0, # Use all cores
|
|
477
|
-
"OutputFlag": 1, # Enable output
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
if solver_options:
|
|
481
|
-
gurobi_defaults["solver_options"].update(solver_options)
|
|
482
|
-
return solver_name, gurobi_defaults
|
|
483
|
-
|
|
484
|
-
# Handle special COPT configurations
|
|
485
|
-
elif solver_name == "copt (barrier)":
|
|
486
|
-
copt_barrier_options = {
|
|
487
|
-
"solver_options": {
|
|
488
|
-
"LpMethod": 2, # Barrier method
|
|
489
|
-
"Crossover": 0, # Skip crossover for speed
|
|
490
|
-
"RelGap": 0.05, # 5% MIP gap (match Gurobi)
|
|
491
|
-
"TimeLimit": 7200, # 1 hour time limit
|
|
492
|
-
"Threads": -1, # 4 threads (memory-conscious)
|
|
493
|
-
"Presolve": 3, # Aggressive presolve
|
|
494
|
-
"Scaling": 1, # Enable scaling
|
|
495
|
-
"FeasTol": 1e-5, # Match Gurobi feasibility
|
|
496
|
-
"DualTol": 1e-5, # Match Gurobi dual tolerance
|
|
497
|
-
# MIP performance settings
|
|
498
|
-
"CutLevel": 2, # Normal cut generation
|
|
499
|
-
"HeurLevel": 3, # Aggressive heuristics
|
|
500
|
-
"StrongBranching": 1, # Fast strong branching
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
if solver_options:
|
|
504
|
-
copt_barrier_options["solver_options"].update(solver_options)
|
|
505
|
-
return "copt", copt_barrier_options
|
|
506
|
-
|
|
507
|
-
elif solver_name == "copt (barrier homogeneous)":
|
|
508
|
-
copt_barrier_homogeneous_options = {
|
|
509
|
-
"solver_options": {
|
|
510
|
-
"LpMethod": 2, # Barrier method
|
|
511
|
-
"Crossover": 0, # Skip crossover
|
|
512
|
-
"BarHomogeneous": 1, # Use homogeneous self-dual form
|
|
513
|
-
"RelGap": 0.05, # 5% MIP gap
|
|
514
|
-
"TimeLimit": 3600, # 1 hour
|
|
515
|
-
"Threads": -1, # 4 threads (memory-conscious)
|
|
516
|
-
"Presolve": 3, # Aggressive presolve
|
|
517
|
-
"Scaling": 1, # Enable scaling
|
|
518
|
-
"FeasTol": 1e-5,
|
|
519
|
-
"DualTol": 1e-5,
|
|
520
|
-
# MIP performance settings
|
|
521
|
-
"CutLevel": 2, # Normal cuts
|
|
522
|
-
"HeurLevel": 3, # Aggressive heuristics
|
|
523
|
-
"StrongBranching": 1, # Fast strong branching
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
if solver_options:
|
|
527
|
-
copt_barrier_homogeneous_options["solver_options"].update(
|
|
528
|
-
solver_options
|
|
529
|
-
)
|
|
530
|
-
return "copt", copt_barrier_homogeneous_options
|
|
531
|
-
|
|
532
|
-
elif solver_name == "copt (barrier+crossover)":
|
|
533
|
-
copt_barrier_crossover_options = {
|
|
534
|
-
"solver_options": {
|
|
535
|
-
"LpMethod": 2, # Barrier method
|
|
536
|
-
"Crossover": 1, # Enable crossover for better solutions
|
|
537
|
-
"RelGap": 0.05, # 5% MIP gap (relaxed for faster solves)
|
|
538
|
-
"TimeLimit": 36000, # 10 hour
|
|
539
|
-
"Threads": -1, # Use all cores
|
|
540
|
-
"Presolve": 2, # Aggressive presolve
|
|
541
|
-
"Scaling": 1, # Enable scaling
|
|
542
|
-
"FeasTol": 1e-4, # Tighter feasibility
|
|
543
|
-
"DualTol": 1e-4, # Tighter dual tolerance
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
if solver_options:
|
|
547
|
-
copt_barrier_crossover_options["solver_options"].update(solver_options)
|
|
548
|
-
return "copt", copt_barrier_crossover_options
|
|
549
|
-
|
|
550
|
-
elif solver_name == "copt (dual simplex)":
|
|
551
|
-
copt_dual_simplex_options = {
|
|
552
|
-
"solver_options": {
|
|
553
|
-
"LpMethod": 1, # Dual simplex method
|
|
554
|
-
"RelGap": 0.05, # 5% MIP gap
|
|
555
|
-
"TimeLimit": 3600, # 1 hour
|
|
556
|
-
"Threads": -1, # Use all cores
|
|
557
|
-
"Presolve": 3, # Aggressive presolve
|
|
558
|
-
"Scaling": 1, # Enable scaling
|
|
559
|
-
"FeasTol": 1e-6,
|
|
560
|
-
"DualTol": 1e-6,
|
|
561
|
-
# MIP performance settings
|
|
562
|
-
"CutLevel": 2, # Normal cuts
|
|
563
|
-
"HeurLevel": 2, # Normal heuristics
|
|
564
|
-
"StrongBranching": 1, # Fast strong branching
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
if solver_options:
|
|
568
|
-
copt_dual_simplex_options["solver_options"].update(solver_options)
|
|
569
|
-
return "copt", copt_dual_simplex_options
|
|
570
|
-
|
|
571
|
-
elif solver_name == "copt (concurrent)":
|
|
572
|
-
copt_concurrent_options = {
|
|
573
|
-
"solver_options": {
|
|
574
|
-
"LpMethod": 4, # Concurrent (simplex + barrier)
|
|
575
|
-
"RelGap": 0.05, # 5% MIP gap
|
|
576
|
-
"TimeLimit": 3600, # 1 hour
|
|
577
|
-
"Threads": -1, # Use all cores
|
|
578
|
-
"Presolve": 3, # Aggressive presolve
|
|
579
|
-
"Scaling": 1, # Enable scaling
|
|
580
|
-
"FeasTol": 1e-5,
|
|
581
|
-
"DualTol": 1e-5,
|
|
582
|
-
# MIP performance settings
|
|
583
|
-
"CutLevel": 2, # Normal cuts
|
|
584
|
-
"HeurLevel": 3, # Aggressive heuristics
|
|
585
|
-
"StrongBranching": 1, # Fast strong branching
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
if solver_options:
|
|
589
|
-
copt_concurrent_options["solver_options"].update(solver_options)
|
|
590
|
-
return "copt", copt_concurrent_options
|
|
591
|
-
|
|
592
|
-
elif solver_name in ["highs", "cplex", "glpk", "cbc", "scip", "copt"]:
|
|
593
|
-
return solver_name, solver_options
|
|
594
|
-
|
|
595
|
-
else:
|
|
596
|
-
# Unknown solver name - fall back to highs
|
|
597
|
-
return "highs", solver_options
|
|
230
|
+
# For all other cases, pass through solver name and options directly
|
|
231
|
+
# The frontend is responsible for resolving presets and defaults
|
|
232
|
+
if solver_options:
|
|
233
|
+
return solver_name, {"solver_options": solver_options}
|
|
234
|
+
|
|
235
|
+
return solver_name, None
|
|
598
236
|
|
|
599
237
|
def _detect_constraint_type(self, constraint_code: str) -> str:
|
|
600
238
|
"""
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transformations module for PyConvexity.
|
|
3
|
+
|
|
4
|
+
Provides functions for transforming network data, including:
|
|
5
|
+
- Time axis modification (truncation, resampling)
|
|
6
|
+
- Future: network merging, scenario duplication, etc.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pyconvexity.transformations.api import modify_time_axis
|
|
10
|
+
from pyconvexity.transformations.time_axis import TimeAxisModifier
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"modify_time_axis",
|
|
14
|
+
"TimeAxisModifier",
|
|
15
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-level API for network transformations.
|
|
3
|
+
|
|
4
|
+
Provides user-friendly functions for transforming network data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Callable, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from pyconvexity.transformations.time_axis import TimeAxisModifier
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def modify_time_axis(
|
|
13
|
+
source_db_path: str,
|
|
14
|
+
target_db_path: str,
|
|
15
|
+
new_start: str,
|
|
16
|
+
new_end: str,
|
|
17
|
+
new_resolution_minutes: int,
|
|
18
|
+
new_network_name: Optional[str] = None,
|
|
19
|
+
convert_timeseries: bool = True,
|
|
20
|
+
progress_callback: Optional[Callable[[float, str], None]] = None,
|
|
21
|
+
) -> Dict[str, Any]:
|
|
22
|
+
"""
|
|
23
|
+
Create a new database with modified time axis and resampled timeseries data.
|
|
24
|
+
|
|
25
|
+
This function copies a network database while adjusting the time axis -
|
|
26
|
+
useful for truncating time periods, changing resolution, or both.
|
|
27
|
+
All timeseries data is automatically resampled to match the new time axis.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
source_db_path: Path to source database
|
|
31
|
+
target_db_path: Path to target database (will be created)
|
|
32
|
+
new_start: Start datetime as ISO string (e.g., "2024-01-01 00:00:00")
|
|
33
|
+
new_end: End datetime as ISO string (e.g., "2024-12-31 23:00:00")
|
|
34
|
+
new_resolution_minutes: New time resolution in minutes (e.g., 60 for hourly)
|
|
35
|
+
new_network_name: Optional new name for the network
|
|
36
|
+
convert_timeseries: If True, resample timeseries data to new time axis.
|
|
37
|
+
If False, wipe all timeseries attributes (useful for creating templates)
|
|
38
|
+
progress_callback: Optional callback for progress updates.
|
|
39
|
+
Called with (progress: float, message: str) where progress is 0-100.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dictionary with results and statistics:
|
|
43
|
+
- success: bool - Whether the operation completed successfully
|
|
44
|
+
- source_db_path: str - Path to source database
|
|
45
|
+
- target_db_path: str - Path to created target database
|
|
46
|
+
- new_periods_count: int - Number of time periods in new database
|
|
47
|
+
- new_resolution_minutes: int - Resolution in minutes
|
|
48
|
+
- new_start: str - Start time
|
|
49
|
+
- new_end: str - End time
|
|
50
|
+
- processing_stats: dict - Detailed processing statistics
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: If time parameters are invalid (end before start, negative resolution)
|
|
54
|
+
FileNotFoundError: If source database doesn't exist
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
# Truncate a yearly model to one week with hourly resolution
|
|
58
|
+
result = modify_time_axis(
|
|
59
|
+
source_db_path="full_year_model.db",
|
|
60
|
+
target_db_path="one_week_model.db",
|
|
61
|
+
new_start="2024-01-01 00:00:00",
|
|
62
|
+
new_end="2024-01-07 23:00:00",
|
|
63
|
+
new_resolution_minutes=60,
|
|
64
|
+
new_network_name="One Week Test Model",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if result["success"]:
|
|
68
|
+
print(f"Created {result['target_db_path']} with {result['new_periods_count']} periods")
|
|
69
|
+
|
|
70
|
+
Example with progress tracking:
|
|
71
|
+
def on_progress(progress: float, message: str):
|
|
72
|
+
print(f"[{progress:.0f}%] {message}")
|
|
73
|
+
|
|
74
|
+
result = modify_time_axis(
|
|
75
|
+
source_db_path="original.db",
|
|
76
|
+
target_db_path="resampled.db",
|
|
77
|
+
new_start="2024-01-01",
|
|
78
|
+
new_end="2024-06-30",
|
|
79
|
+
new_resolution_minutes=60,
|
|
80
|
+
progress_callback=on_progress,
|
|
81
|
+
)
|
|
82
|
+
"""
|
|
83
|
+
modifier = TimeAxisModifier()
|
|
84
|
+
return modifier.modify_time_axis(
|
|
85
|
+
source_db_path=source_db_path,
|
|
86
|
+
target_db_path=target_db_path,
|
|
87
|
+
new_start=new_start,
|
|
88
|
+
new_end=new_end,
|
|
89
|
+
new_resolution_minutes=new_resolution_minutes,
|
|
90
|
+
new_network_name=new_network_name,
|
|
91
|
+
convert_timeseries=convert_timeseries,
|
|
92
|
+
progress_callback=progress_callback,
|
|
93
|
+
)
|