pyconvexity 0.4.9.post1__py3-none-any.whl → 0.5.0.post1__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/_version.py +1 -1
- pyconvexity/solvers/pypsa/solver.py +11 -373
- pyconvexity/solvers/pypsa/storage.py +20 -28
- {pyconvexity-0.4.9.post1.dist-info → pyconvexity-0.5.0.post1.dist-info}/METADATA +1 -1
- {pyconvexity-0.4.9.post1.dist-info → pyconvexity-0.5.0.post1.dist-info}/RECORD +7 -7
- {pyconvexity-0.4.9.post1.dist-info → pyconvexity-0.5.0.post1.dist-info}/WHEEL +0 -0
- {pyconvexity-0.4.9.post1.dist-info → pyconvexity-0.5.0.post1.dist-info}/top_level.txt +0 -0
pyconvexity/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.5.0.post1"
|
|
@@ -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
|
"""
|
|
@@ -829,6 +829,7 @@ class ResultStorage:
|
|
|
829
829
|
# 1. GENERATORS - Generation dispatch (including UNMET_LOAD)
|
|
830
830
|
if hasattr(network, "generators_t") and hasattr(network.generators_t, "p"):
|
|
831
831
|
# Get generator-carrier mapping (include both GENERATOR and UNMET_LOAD)
|
|
832
|
+
# Use LEFT JOIN to include UNMET_LOAD components even if they don't have a carrier_id
|
|
832
833
|
cursor = conn.execute(
|
|
833
834
|
"""
|
|
834
835
|
SELECT c.name as component_name,
|
|
@@ -837,7 +838,7 @@ class ResultStorage:
|
|
|
837
838
|
ELSE carr.name
|
|
838
839
|
END as carrier_name
|
|
839
840
|
FROM components c
|
|
840
|
-
JOIN carriers carr ON c.carrier_id = carr.id
|
|
841
|
+
LEFT JOIN carriers carr ON c.carrier_id = carr.id
|
|
841
842
|
WHERE c.component_type IN ('GENERATOR', 'UNMET_LOAD')
|
|
842
843
|
"""
|
|
843
844
|
)
|
|
@@ -1006,19 +1007,16 @@ class ResultStorage:
|
|
|
1006
1007
|
end_year = build_year + lifetime - 1
|
|
1007
1008
|
return current_year <= end_year
|
|
1008
1009
|
|
|
1009
|
-
# 1. GENERATORS - Capital costs (
|
|
1010
|
+
# 1. GENERATORS - Capital costs (excluding UNMET_LOAD)
|
|
1010
1011
|
if hasattr(network, "generators") and not network.generators.empty:
|
|
1011
|
-
# Get generator info: carrier, capital_cost, build_year, lifetime
|
|
1012
|
+
# Get generator info: carrier, capital_cost, build_year, lifetime
|
|
1013
|
+
# EXCLUDE UNMET_LOAD - their capital cost is not meaningful (usually $0)
|
|
1012
1014
|
cursor = conn.execute(
|
|
1013
1015
|
"""
|
|
1014
|
-
SELECT c.name as component_name,
|
|
1015
|
-
CASE
|
|
1016
|
-
WHEN c.component_type = 'UNMET_LOAD' THEN 'Unmet Load'
|
|
1017
|
-
ELSE carr.name
|
|
1018
|
-
END as carrier_name
|
|
1016
|
+
SELECT c.name as component_name, carr.name as carrier_name
|
|
1019
1017
|
FROM components c
|
|
1020
|
-
JOIN carriers carr ON c.carrier_id = carr.id
|
|
1021
|
-
WHERE c.component_type
|
|
1018
|
+
LEFT JOIN carriers carr ON c.carrier_id = carr.id
|
|
1019
|
+
WHERE c.component_type = 'GENERATOR'
|
|
1022
1020
|
"""
|
|
1023
1021
|
)
|
|
1024
1022
|
generator_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
@@ -1297,19 +1295,16 @@ class ResultStorage:
|
|
|
1297
1295
|
# Operational costs = dispatch (MWh) × marginal_cost (currency/MWh)
|
|
1298
1296
|
# Only for components that are active in this year
|
|
1299
1297
|
|
|
1300
|
-
# 1. GENERATORS - Operational costs (
|
|
1298
|
+
# 1. GENERATORS - Operational costs (excluding UNMET_LOAD)
|
|
1301
1299
|
if hasattr(network, "generators_t") and hasattr(network.generators_t, "p"):
|
|
1302
|
-
# Get generator info: carrier, marginal_cost, build_year, lifetime
|
|
1300
|
+
# Get generator info: carrier, marginal_cost, build_year, lifetime
|
|
1301
|
+
# EXCLUDE UNMET_LOAD - their marginal cost is a penalty price, not a real operational cost
|
|
1303
1302
|
cursor = conn.execute(
|
|
1304
1303
|
"""
|
|
1305
|
-
SELECT c.name as component_name,
|
|
1306
|
-
CASE
|
|
1307
|
-
WHEN c.component_type = 'UNMET_LOAD' THEN 'Unmet Load'
|
|
1308
|
-
ELSE carr.name
|
|
1309
|
-
END as carrier_name
|
|
1304
|
+
SELECT c.name as component_name, carr.name as carrier_name
|
|
1310
1305
|
FROM components c
|
|
1311
|
-
JOIN carriers carr ON c.carrier_id = carr.id
|
|
1312
|
-
WHERE c.component_type
|
|
1306
|
+
LEFT JOIN carriers carr ON c.carrier_id = carr.id
|
|
1307
|
+
WHERE c.component_type = 'GENERATOR'
|
|
1313
1308
|
"""
|
|
1314
1309
|
)
|
|
1315
1310
|
generator_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
@@ -1539,19 +1534,16 @@ class ResultStorage:
|
|
|
1539
1534
|
|
|
1540
1535
|
# Calculate capacity by carrier for this specific year
|
|
1541
1536
|
|
|
1542
|
-
# 4. GENERATORS - Power capacity (MW) (
|
|
1537
|
+
# 4. GENERATORS - Power capacity (MW) (excluding UNMET_LOAD)
|
|
1543
1538
|
if hasattr(network, "generators") and not network.generators.empty:
|
|
1544
|
-
# Get generator-carrier mapping
|
|
1539
|
+
# Get generator-carrier mapping for capacity
|
|
1540
|
+
# EXCLUDE UNMET_LOAD - their capacity (often infinite) is not meaningful for capacity stats
|
|
1545
1541
|
cursor = conn.execute(
|
|
1546
1542
|
"""
|
|
1547
|
-
SELECT c.name as component_name,
|
|
1548
|
-
CASE
|
|
1549
|
-
WHEN c.component_type = 'UNMET_LOAD' THEN 'Unmet Load'
|
|
1550
|
-
ELSE carr.name
|
|
1551
|
-
END as carrier_name
|
|
1543
|
+
SELECT c.name as component_name, carr.name as carrier_name
|
|
1552
1544
|
FROM components c
|
|
1553
|
-
JOIN carriers carr ON c.carrier_id = carr.id
|
|
1554
|
-
WHERE c.component_type
|
|
1545
|
+
LEFT JOIN carriers carr ON c.carrier_id = carr.id
|
|
1546
|
+
WHERE c.component_type = 'GENERATOR'
|
|
1555
1547
|
"""
|
|
1556
1548
|
)
|
|
1557
1549
|
generator_carriers = {row[0]: row[1] for row in cursor.fetchall()}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
pyconvexity/__init__.py,sha256=P67QJ8npf-QWmBX12im__eICLoRz8cByQ5OJXiyIBmA,5706
|
|
2
|
-
pyconvexity/_version.py,sha256=
|
|
2
|
+
pyconvexity/_version.py,sha256=J1kBZAgpuK-WFUqCCzGZrBt454s_I5sVWXeEXZ7DyLQ,28
|
|
3
3
|
pyconvexity/dashboard.py,sha256=7x04Hr-EwzTAf-YJdHzfV83Gf2etltwtzwh_bCYJ5lk,8579
|
|
4
4
|
pyconvexity/timeseries.py,sha256=QdKbiqjAlxkJATyKm2Kelx1Ea2PsAnnCYfVLU5VER1Y,11085
|
|
5
5
|
pyconvexity/core/__init__.py,sha256=gdyyHNqOc4h9Nfe9u6NA936GNzH6coGNCMgBvvvOnGE,1196
|
|
@@ -34,11 +34,11 @@ pyconvexity/solvers/pypsa/batch_loader.py,sha256=ZgOcZqMnMS3TOYTq2Ly2O4cuwhNNAic
|
|
|
34
34
|
pyconvexity/solvers/pypsa/builder.py,sha256=1ZU68Wtl_jQSXHzspKQDkR6bxAVU1nKvPfnPUl0aO3k,23256
|
|
35
35
|
pyconvexity/solvers/pypsa/clearing_price.py,sha256=HdAk7GPfJFVI4t6mL0zQGEOMAvuyfpl0yNCnah1ZGH0,29164
|
|
36
36
|
pyconvexity/solvers/pypsa/constraints.py,sha256=20WliFDhPQGMAsS4VOTU8LZJpsFpLVRHpNsZW49GTcc,16397
|
|
37
|
-
pyconvexity/solvers/pypsa/solver.py,sha256=
|
|
38
|
-
pyconvexity/solvers/pypsa/storage.py,sha256=
|
|
37
|
+
pyconvexity/solvers/pypsa/solver.py,sha256=pNI9ch0vO5q-8mWc3RHTscWB_ymj4s2lVJQ_e2nbzHY,44417
|
|
38
|
+
pyconvexity/solvers/pypsa/storage.py,sha256=nbONOBnunq3tyexa5yDUsT9xdxieUfrqhoM76_2HIGg,94956
|
|
39
39
|
pyconvexity/validation/__init__.py,sha256=VJNZlFoWABsWwUKktNk2jbtXIepH5omvC0WtsTS7o3o,583
|
|
40
40
|
pyconvexity/validation/rules.py,sha256=GiNadc8hvbWBr09vUkGiLLTmSdvtNSeGLFwvCjlikYY,9241
|
|
41
|
-
pyconvexity-0.
|
|
42
|
-
pyconvexity-0.
|
|
43
|
-
pyconvexity-0.
|
|
44
|
-
pyconvexity-0.
|
|
41
|
+
pyconvexity-0.5.0.post1.dist-info/METADATA,sha256=ww2Vp1jYb4upZigNRSZ4augdGBQWBGfokrvFyPm_Vq8,4973
|
|
42
|
+
pyconvexity-0.5.0.post1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
43
|
+
pyconvexity-0.5.0.post1.dist-info/top_level.txt,sha256=wFPEDXVaebR3JO5Tt3HNse-ws5aROCcxEco15d6j64s,12
|
|
44
|
+
pyconvexity-0.5.0.post1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|