pyconvexity 0.3.8.post3__py3-none-any.whl → 0.3.8.post5__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.

@@ -77,6 +77,8 @@ class NetworkSolver:
77
77
  known_solvers = ['highs', 'gurobi', 'gurobi (barrier)', 'gurobi (barrier homogeneous)',
78
78
  'gurobi (barrier+crossover balanced)', 'gurobi (dual simplex)',
79
79
  'mosek', 'mosek (default)', 'mosek (barrier)', 'mosek (barrier+crossover)', 'mosek (dual simplex)',
80
+ 'copt', 'copt (barrier)', 'copt (barrier homogeneous)', 'copt (barrier+crossover)',
81
+ 'copt (dual simplex)', 'copt (concurrent)',
80
82
  'cplex', 'glpk', 'cbc', 'scip']
81
83
 
82
84
  if default_solver in known_solvers:
@@ -99,17 +101,21 @@ class NetworkSolver:
99
101
  conn=None,
100
102
  network_id: Optional[int] = None,
101
103
  scenario_id: Optional[int] = None,
102
- constraint_applicator=None
104
+ constraint_applicator=None,
105
+ custom_solver_config: Optional[Dict[str, Any]] = None
103
106
  ) -> Dict[str, Any]:
104
107
  """
105
108
  Solve PyPSA network and return results.
106
109
 
107
110
  Args:
108
111
  network: PyPSA Network object to solve
109
- solver_name: Solver to use (default: "highs")
112
+ solver_name: Solver to use (default: "highs"). Use "custom" for custom_solver_config.
110
113
  solver_options: Optional solver-specific options
111
114
  discount_rate: Optional discount rate for multi-period optimization
112
115
  job_id: Optional job ID for tracking
116
+ custom_solver_config: Optional custom solver configuration when solver_name="custom"
117
+ Format: {"solver": "actual_solver_name", "solver_options": {...}}
118
+ Example: {"solver": "gurobi", "solver_options": {"Method": 2, "Crossover": 0}}
113
119
 
114
120
  Returns:
115
121
  Dictionary with solve results and metadata
@@ -125,14 +131,16 @@ class NetworkSolver:
125
131
 
126
132
  try:
127
133
  # Get solver configuration
128
- actual_solver_name, solver_config = self._get_solver_config(solver_name, solver_options)
134
+ actual_solver_name, solver_config = self._get_solver_config(solver_name, solver_options, custom_solver_config)
129
135
 
136
+ # Resolve discount rate - fallback to 0.0 if None
137
+ # Note: API layer (api.py) handles fetching from network_config before calling this
138
+ effective_discount_rate = discount_rate if discount_rate is not None else 0.0
139
+ logger.info(f"Discount rate for solve: {effective_discount_rate}")
130
140
 
131
141
  years = list(network.investment_periods)
132
- effective_discount_rate = discount_rate if discount_rate is not None else 0.05 # Default 5%
133
142
 
134
143
  logger.info(f"Multi-period optimization with {len(years)} periods: {years}")
135
- logger.info(f"Discount rate: {effective_discount_rate}")
136
144
 
137
145
  # Calculate investment period weightings with discount rate
138
146
  self._calculate_investment_weightings(network, effective_discount_rate)
@@ -141,17 +149,17 @@ class NetworkSolver:
141
149
  if conn and network_id:
142
150
  self._set_snapshot_weightings_after_multiperiod(conn, network_id, network)
143
151
 
144
- # Prepare optimization constraints with type detection
152
+ # Prepare optimization constraints - ONLY model constraints
153
+ # Network constraints were already applied before solve in api.py
145
154
  extra_functionality = None
146
155
  model_constraints = []
147
- network_constraints = []
148
156
 
149
157
  if conn and network_id and constraint_applicator:
150
158
  optimization_constraints = constraint_applicator.get_optimization_constraints(conn, network_id, scenario_id)
151
159
  if optimization_constraints:
152
160
  logger.info(f"Found {len(optimization_constraints)} optimization constraints")
153
161
 
154
- # Separate constraints by type
162
+ # Filter for model constraints only (network constraints already applied)
155
163
  for constraint in optimization_constraints:
156
164
  constraint_code = constraint.get('constraint_code', '')
157
165
  constraint_type = self._detect_constraint_type(constraint_code)
@@ -159,21 +167,19 @@ class NetworkSolver:
159
167
 
160
168
  if constraint_type == "model_constraint":
161
169
  model_constraints.append(constraint)
162
- logger.info(f"Detected model constraint: {constraint_name}")
170
+ logger.info(f"Will apply model constraint during solve: {constraint_name}")
163
171
  else:
164
- network_constraints.append(constraint)
165
- logger.info(f"Detected network constraint: {constraint_name}")
172
+ logger.info(f"Skipping network constraint (already applied): {constraint_name}")
166
173
 
167
- logger.info(f"Constraint breakdown: {len(model_constraints)} model constraints, {len(network_constraints)} network constraints")
174
+ logger.info(f"Will apply {len(model_constraints)} model constraints during optimization")
168
175
 
169
- # Create extra_functionality for ALL constraints (both model and network)
170
- all_constraints = model_constraints + network_constraints
171
- if all_constraints:
172
- extra_functionality = self._create_extra_functionality(all_constraints, constraint_applicator)
173
- logger.info(f"Prepared {len(all_constraints)} constraints for optimization-time application")
176
+ # Create extra_functionality for model constraints only
177
+ if model_constraints:
178
+ extra_functionality = self._create_extra_functionality(model_constraints, constraint_applicator)
179
+ logger.info(f"Prepared {len(model_constraints)} model constraints for optimization-time application")
174
180
 
175
- # NOTE: Model constraints are now applied DURING solve via extra_functionality
176
- # This ensures they are applied to the actual model PyPSA creates, not a separate model
181
+ # NOTE: Model constraints are applied DURING solve via extra_functionality
182
+ # Network constraints were already applied to the network structure before solve
177
183
 
178
184
  # Solver diagnostics
179
185
  logger.info(f"=== PYPSA SOLVER DIAGNOSTICS ===")
@@ -273,17 +279,40 @@ class NetworkSolver:
273
279
  "objective_value": None
274
280
  }
275
281
 
276
- def _get_solver_config(self, solver_name: str, solver_options: Optional[Dict[str, Any]] = None) -> tuple[str, Optional[Dict[str, Any]]]:
282
+ def _get_solver_config(self, solver_name: str, solver_options: Optional[Dict[str, Any]] = None,
283
+ custom_solver_config: Optional[Dict[str, Any]] = None) -> tuple[str, Optional[Dict[str, Any]]]:
277
284
  """
278
285
  Get the actual solver name and options for special solver configurations.
279
286
 
280
287
  Args:
281
- solver_name: The solver name (e.g., 'gurobi (barrier)', 'highs')
288
+ solver_name: The solver name (e.g., 'gurobi (barrier)', 'highs', 'custom')
282
289
  solver_options: Optional additional solver options
290
+ custom_solver_config: Optional custom solver configuration for solver_name='custom'
291
+ Format: {"solver": "actual_solver_name", "solver_options": {...}}
283
292
 
284
293
  Returns:
285
294
  Tuple of (actual_solver_name, solver_options_dict)
286
295
  """
296
+ # Handle "custom" solver with custom configuration
297
+ if solver_name == 'custom':
298
+ if not custom_solver_config:
299
+ raise ValueError("custom_solver_config must be provided when solver_name='custom'")
300
+
301
+ if 'solver' not in custom_solver_config:
302
+ raise ValueError("custom_solver_config must contain 'solver' key with the actual solver name")
303
+
304
+ actual_solver = custom_solver_config['solver']
305
+ custom_options = custom_solver_config.get('solver_options', {})
306
+
307
+ # Merge with any additional solver_options passed separately
308
+ if solver_options:
309
+ merged_options = {'solver_options': {**custom_options, **solver_options}}
310
+ else:
311
+ merged_options = {'solver_options': custom_options} if custom_options else None
312
+
313
+ logger.info(f"Using custom solver configuration: {actual_solver} with options: {custom_options}")
314
+ return actual_solver, merged_options
315
+
287
316
  # Handle "default" solver
288
317
  if solver_name == 'default':
289
318
  # Try to read user's default solver preference
@@ -298,7 +327,7 @@ class NetworkSolver:
298
327
  'Method': 2, # Barrier
299
328
  'Crossover': 0, # Skip crossover
300
329
  'MIPGap': 0.05, # 5% gap
301
- 'Threads': 4, # Use all cores
330
+ 'Threads': 0, # Use all cores (0 = auto)
302
331
  'Presolve': 2, # Aggressive presolve
303
332
  'ConcurrentMIP': 1, # Parallel root strategies
304
333
  'BarConvTol': 1e-4, # Relaxed barrier convergence
@@ -319,7 +348,7 @@ class NetworkSolver:
319
348
  'Method': 2, # Barrier
320
349
  'Crossover': 0, # Skip crossover
321
350
  'MIPGap': 0.05,
322
- 'Threads': 4,
351
+ 'Threads': 0, # Use all cores (0 = auto)
323
352
  'Presolve': 2,
324
353
  'ConcurrentMIP': 1,
325
354
  'BarConvTol': 1e-4,
@@ -340,7 +369,7 @@ class NetworkSolver:
340
369
  'Method': 2,
341
370
  'Crossover': 1, # Dual crossover
342
371
  'MIPGap': 0.01,
343
- 'Threads': 4,
372
+ 'Threads': 0, # Use all cores (0 = auto)
344
373
  'Presolve': 2,
345
374
  'Heuristics': 0.1,
346
375
  'Cuts': 2,
@@ -374,67 +403,53 @@ class NetworkSolver:
374
403
  # No custom options - let Mosek use its default configuration
375
404
  mosek_default_options = {
376
405
  'solver_options': {
377
- 'MSK_IPAR_LOG': 10, # Enable full logging (10 = verbose)
378
- 'MSK_IPAR_LOG_INTPNT': 1, # Log interior-point progress
379
- 'MSK_IPAR_LOG_SIM': 4, # Log simplex progress
380
- 'MSK_IPAR_LOG_MIO': 4, # Log MIP progress (4 = full)
381
- 'MSK_IPAR_LOG_MIO_FREQ': 10, # Log MIP every 10 seconds
406
+ 'MSK_DPAR_MIO_REL_GAP_CONST': 0.05, # MIP relative gap tolerance (5% to match Gurobi)
407
+ 'MSK_IPAR_MIO_MAX_TIME': 36000, # Max time 1 hour
382
408
  }
383
409
  }
384
410
  if solver_options:
385
411
  mosek_default_options['solver_options'].update(solver_options)
386
- logger.info(f"Using Mosek with default configuration (auto-select optimizer)")
412
+ logger.info(f"Using Mosek with default configuration (auto-select optimizer) and moderate MIP strategies")
387
413
  return 'mosek', mosek_default_options
388
414
 
389
415
  elif solver_name == 'mosek (barrier)':
390
416
  mosek_barrier_options = {
391
417
  'solver_options': {
392
418
  'MSK_IPAR_INTPNT_BASIS': 0, # Skip crossover (barrier-only) - 0 = MSK_BI_NEVER
393
- 'MSK_DPAR_INTPNT_TOL_REL_GAP': 1e-5, # Relaxed relative gap tolerance
394
- 'MSK_DPAR_INTPNT_TOL_PFEAS': 1e-6, # Primal feasibility tolerance
395
- 'MSK_DPAR_INTPNT_TOL_DFEAS': 1e-6, # Dual feasibility tolerance
396
- 'MSK_DPAR_INTPNT_TOL_INFEAS': 1e-8, # Infeasibility tolerance
397
- 'MSK_IPAR_NUM_THREADS': 4, # Number of threads
398
- 'MSK_IPAR_PRESOLVE_USE': 1, # Force presolve (1 = ON)
399
- 'MSK_IPAR_PRESOLVE_LINDEP_USE': 1, # Linear dependency check (1 = ON)
400
- 'MSK_DPAR_MIO_REL_GAP_CONST': 1e-5, # MIP relative gap tolerance
401
- 'MSK_IPAR_MIO_NODE_OPTIMIZER': 4, # Use interior-point for MIP nodes (4 = MSK_OPTIMIZER_INTPNT)
402
- 'MSK_IPAR_MIO_ROOT_OPTIMIZER': 4, # Use interior-point for MIP root (4 = MSK_OPTIMIZER_INTPNT)
403
- 'MSK_IPAR_LOG': 10, # Enable full logging (10 = verbose)
404
- 'MSK_IPAR_LOG_INTPNT': 1, # Log interior-point progress
405
- 'MSK_IPAR_LOG_MIO': 4, # Log MIP progress (4 = full)
406
- 'MSK_IPAR_LOG_MIO_FREQ': 10, # Log MIP every 10 seconds
407
- # Note: Don't force MSK_IPAR_OPTIMIZER - let Mosek choose based on problem type (LP vs MILP)
419
+ 'MSK_DPAR_INTPNT_TOL_REL_GAP': 1e-4, # Match Gurobi barrier tolerance
420
+ 'MSK_DPAR_INTPNT_TOL_PFEAS': 1e-5, # Match Gurobi primal feasibility
421
+ 'MSK_DPAR_INTPNT_TOL_DFEAS': 1e-5, # Match Gurobi dual feasibility
422
+ # Removed MSK_DPAR_INTPNT_TOL_INFEAS - was 1000x tighter than other tolerances!
423
+ 'MSK_IPAR_NUM_THREADS': 0, # Use all available cores (0 = auto)
424
+ 'MSK_IPAR_PRESOLVE_USE': 2, # Aggressive presolve (match Gurobi Presolve=2)
425
+ 'MSK_IPAR_PRESOLVE_LINDEP_USE': 1, # Linear dependency check
426
+ 'MSK_DPAR_MIO_REL_GAP_CONST': 0.05, # Match Gurobi 5% MIP gap
427
+ 'MSK_IPAR_MIO_NODE_OPTIMIZER': 4, # Use interior-point for MIP nodes
428
+ 'MSK_IPAR_MIO_ROOT_OPTIMIZER': 4, # Use interior-point for MIP root
429
+ 'MSK_DPAR_MIO_MAX_TIME': 36000, # Max time 10 hour
408
430
  }
409
431
  }
410
432
  if solver_options:
411
433
  mosek_barrier_options['solver_options'].update(solver_options)
412
- logger.info(f"Using Mosek Barrier (no crossover) configuration with verbose logging")
434
+ logger.info(f"Using Mosek Barrier with aggressive presolve and relaxed tolerances")
413
435
  return 'mosek', mosek_barrier_options
414
436
 
415
437
  elif solver_name == 'mosek (barrier+crossover)':
416
438
  mosek_barrier_crossover_options = {
417
439
  'solver_options': {
418
440
  'MSK_IPAR_INTPNT_BASIS': 1, # Always crossover (1 = MSK_BI_ALWAYS)
419
- 'MSK_DPAR_INTPNT_TOL_REL_GAP': 1e-6, # Tighter relative gap tolerance
420
- 'MSK_DPAR_INTPNT_TOL_PFEAS': 1e-6, # Primal feasibility tolerance
421
- 'MSK_DPAR_INTPNT_TOL_DFEAS': 1e-6, # Dual feasibility tolerance
422
- 'MSK_IPAR_NUM_THREADS': 4, # Number of threads
423
- 'MSK_IPAR_PRESOLVE_USE': 1, # Force presolve
424
- 'MSK_IPAR_PRESOLVE_LINDEP_USE': 1, # Linear dependency check
425
- 'MSK_DPAR_MIO_REL_GAP_CONST': 1e-6, # MIP relative gap tolerance
426
- 'MSK_IPAR_MIO_NODE_OPTIMIZER': 4, # Use interior-point for MIP nodes
441
+ 'MSK_DPAR_INTPNT_TOL_REL_GAP': 1e-4, # Match Gurobi barrier tolerance (was 1e-6)
442
+ 'MSK_DPAR_INTPNT_TOL_PFEAS': 1e-5, # Match Gurobi (was 1e-6)
443
+ 'MSK_DPAR_INTPNT_TOL_DFEAS': 1e-5, # Match Gurobi (was 1e-6)
444
+ 'MSK_IPAR_NUM_THREADS': 0, # Use all available cores (0 = auto)
445
+ 'MSK_DPAR_MIO_REL_GAP_CONST': 0.05, # Match Gurobi 5% MIP gap (was 1e-6)
427
446
  'MSK_IPAR_MIO_ROOT_OPTIMIZER': 4, # Use interior-point for MIP root
428
- 'MSK_IPAR_LOG': 10, # Enable full logging (10 = verbose)
429
- 'MSK_IPAR_LOG_INTPNT': 1, # Log interior-point progress
430
- 'MSK_IPAR_LOG_MIO': 4, # Log MIP progress (4 = full)
431
- 'MSK_IPAR_LOG_MIO_FREQ': 10, # Log MIP every 10 seconds
432
- # Note: Don't force MSK_IPAR_OPTIMIZER - let Mosek choose based on problem type
447
+ 'MSK_DPAR_MIO_MAX_TIME': 36000, # Max time 10 hour (safety limit)
433
448
  }
434
449
  }
435
450
  if solver_options:
436
451
  mosek_barrier_crossover_options['solver_options'].update(solver_options)
437
- logger.info(f"Using Mosek Barrier+Crossover configuration with verbose logging")
452
+ logger.info(f"Using Mosek Barrier+Crossover configuration with Gurobi-matched tolerances and moderate MIP strategies")
438
453
  return 'mosek', mosek_barrier_crossover_options
439
454
 
440
455
  elif solver_name == 'mosek (dual simplex)':
@@ -442,20 +457,16 @@ class NetworkSolver:
442
457
  'solver_options': {
443
458
  'MSK_IPAR_NUM_THREADS': 0, # Use all available cores (0 = automatic)
444
459
  'MSK_IPAR_PRESOLVE_USE': 1, # Force presolve
445
- 'MSK_IPAR_SIM_SCALING': 2, # Aggressive scaling (2 = MSK_SCALING_AGGRESSIVE)
446
- 'MSK_DPAR_MIO_REL_GAP_CONST': 1e-6, # MIP relative gap tolerance
460
+ 'MSK_DPAR_MIO_REL_GAP_CONST': 0.05, # Match Gurobi 5% MIP gap (was 1e-6)
447
461
  'MSK_IPAR_MIO_NODE_OPTIMIZER': 1, # Use dual simplex for MIP nodes (1 = MSK_OPTIMIZER_DUAL_SIMPLEX)
448
462
  'MSK_IPAR_MIO_ROOT_OPTIMIZER': 1, # Use dual simplex for MIP root
449
- 'MSK_IPAR_LOG': 10, # Enable full logging (10 = verbose)
450
- 'MSK_IPAR_LOG_SIM': 4, # Log simplex progress (4 = full)
451
- 'MSK_IPAR_LOG_MIO': 4, # Log MIP progress (4 = full)
452
- 'MSK_IPAR_LOG_MIO_FREQ': 10, # Log MIP every 10 seconds
453
- # Note: For pure LP, set optimizer; for MILP, only set node/root optimizers
463
+ 'MSK_DPAR_MIO_MAX_TIME': 36000, # Max time 10 hour (safety limit)
464
+
454
465
  }
455
466
  }
456
467
  if solver_options:
457
468
  mosek_dual_options['solver_options'].update(solver_options)
458
- logger.info(f"Using Mosek Dual Simplex configuration with verbose logging")
469
+ logger.info(f"Using Mosek Dual Simplex configuration with Gurobi-matched tolerances and moderate MIP strategies")
459
470
  return 'mosek', mosek_dual_options
460
471
 
461
472
  # Check if this is a known valid solver name
@@ -463,16 +474,14 @@ class NetworkSolver:
463
474
  # Add default MILP-friendly settings for plain Mosek
464
475
  mosek_defaults = {
465
476
  'solver_options': {
466
- 'MSK_DPAR_MIO_REL_GAP_CONST': 1e-4, # MIP relative gap tolerance (10^-4 = 0.01%)
467
- 'MSK_IPAR_MIO_MAX_TIME': 3600, # Max time 1 hour
477
+ 'MSK_DPAR_MIO_REL_GAP_CONST': 0.05, # Match Gurobi 5% MIP gap (was 1e-4)
478
+ 'MSK_IPAR_MIO_MAX_TIME': 36000, # Max time 1 hour
468
479
  'MSK_IPAR_NUM_THREADS': 0, # Use all cores (0 = auto)
469
- 'MSK_IPAR_LOG': 4, # Moderate logging
470
- 'MSK_IPAR_LOG_MIO': 2, # Log MIP occasionally
471
480
  }
472
481
  }
473
482
  if solver_options:
474
483
  mosek_defaults['solver_options'].update(solver_options)
475
- logger.info(f"Using Mosek with default MILP-friendly settings")
484
+ logger.info(f"Using Mosek with barrier method for MIP (interior-point for root/nodes)")
476
485
  return solver_name, mosek_defaults
477
486
 
478
487
  elif solver_name == 'gurobi':
@@ -490,6 +499,117 @@ class NetworkSolver:
490
499
  logger.info(f"Using Gurobi with default MILP-friendly settings")
491
500
  return solver_name, gurobi_defaults
492
501
 
502
+ # Handle special COPT configurations
503
+ elif solver_name == 'copt (barrier)':
504
+ copt_barrier_options = {
505
+ 'solver_options': {
506
+ 'LpMethod': 2, # Barrier method
507
+ 'Crossover': 0, # Skip crossover for speed
508
+ 'RelGap': 0.05, # 5% MIP gap (match Gurobi)
509
+ 'TimeLimit': 7200, # 1 hour time limit
510
+ 'Threads': -1, # 4 threads (memory-conscious)
511
+ 'Presolve': 3, # Aggressive presolve
512
+ 'Scaling': 1, # Enable scaling
513
+ 'FeasTol': 1e-5, # Match Gurobi feasibility
514
+ 'DualTol': 1e-5, # Match Gurobi dual tolerance
515
+ # MIP performance settings
516
+ 'CutLevel': 2, # Normal cut generation
517
+ 'HeurLevel': 3, # Aggressive heuristics
518
+ 'StrongBranching': 1, # Fast strong branching
519
+ }
520
+ }
521
+ if solver_options:
522
+ copt_barrier_options['solver_options'].update(solver_options)
523
+ logger.info(f"Using COPT Barrier configuration (fast interior-point method)")
524
+ return 'copt', copt_barrier_options
525
+
526
+ elif solver_name == 'copt (barrier homogeneous)':
527
+ copt_barrier_homogeneous_options = {
528
+ 'solver_options': {
529
+ 'LpMethod': 2, # Barrier method
530
+ 'Crossover': 0, # Skip crossover
531
+ 'BarHomogeneous': 1, # Use homogeneous self-dual form
532
+ 'RelGap': 0.05, # 5% MIP gap
533
+ 'TimeLimit': 3600, # 1 hour
534
+ 'Threads': -1, # 4 threads (memory-conscious)
535
+ 'Presolve': 3, # Aggressive presolve
536
+ 'Scaling': 1, # Enable scaling
537
+ 'FeasTol': 1e-5,
538
+ 'DualTol': 1e-5,
539
+ # MIP performance settings
540
+ 'CutLevel': 2, # Normal cuts
541
+ 'HeurLevel': 3, # Aggressive heuristics
542
+ 'StrongBranching': 1, # Fast strong branching
543
+ }
544
+ }
545
+ if solver_options:
546
+ copt_barrier_homogeneous_options['solver_options'].update(solver_options)
547
+ logger.info(f"Using COPT Barrier Homogeneous configuration")
548
+ return 'copt', copt_barrier_homogeneous_options
549
+
550
+ elif solver_name == 'copt (barrier+crossover)':
551
+ copt_barrier_crossover_options = {
552
+ 'solver_options': {
553
+ 'LpMethod': 2, # Barrier method
554
+ 'Crossover': 1, # Enable crossover for better solutions
555
+ 'RelGap': 0.05, # 5% MIP gap (relaxed for faster solves)
556
+ 'TimeLimit': 36000, # 10 hour
557
+ 'Threads': -1, # Use all cores
558
+ 'Presolve': 2, # Aggressive presolve
559
+ 'Scaling': 1, # Enable scaling
560
+ 'FeasTol': 1e-4, # Tighter feasibility
561
+ 'DualTol': 1e-4, # Tighter dual tolerance
562
+ }
563
+ }
564
+ if solver_options:
565
+ copt_barrier_crossover_options['solver_options'].update(solver_options)
566
+ logger.info(f"Using COPT Barrier+Crossover configuration (balanced performance)")
567
+ return 'copt', copt_barrier_crossover_options
568
+
569
+ elif solver_name == 'copt (dual simplex)':
570
+ copt_dual_simplex_options = {
571
+ 'solver_options': {
572
+ 'LpMethod': 1, # Dual simplex method
573
+ 'RelGap': 0.05, # 5% MIP gap
574
+ 'TimeLimit': 3600, # 1 hour
575
+ 'Threads': -1, # Use all cores
576
+ 'Presolve': 3, # Aggressive presolve
577
+ 'Scaling': 1, # Enable scaling
578
+ 'FeasTol': 1e-6,
579
+ 'DualTol': 1e-6,
580
+ # MIP performance settings
581
+ 'CutLevel': 2, # Normal cuts
582
+ 'HeurLevel': 2, # Normal heuristics
583
+ 'StrongBranching': 1, # Fast strong branching
584
+ }
585
+ }
586
+ if solver_options:
587
+ copt_dual_simplex_options['solver_options'].update(solver_options)
588
+ logger.info(f"Using COPT Dual Simplex configuration (robust method)")
589
+ return 'copt', copt_dual_simplex_options
590
+
591
+ elif solver_name == 'copt (concurrent)':
592
+ copt_concurrent_options = {
593
+ 'solver_options': {
594
+ 'LpMethod': 4, # Concurrent (simplex + barrier)
595
+ 'RelGap': 0.05, # 5% MIP gap
596
+ 'TimeLimit': 3600, # 1 hour
597
+ 'Threads': -1, # Use all cores
598
+ 'Presolve': 3, # Aggressive presolve
599
+ 'Scaling': 1, # Enable scaling
600
+ 'FeasTol': 1e-5,
601
+ 'DualTol': 1e-5,
602
+ # MIP performance settings
603
+ 'CutLevel': 2, # Normal cuts
604
+ 'HeurLevel': 3, # Aggressive heuristics
605
+ 'StrongBranching': 1, # Fast strong branching
606
+ }
607
+ }
608
+ if solver_options:
609
+ copt_concurrent_options['solver_options'].update(solver_options)
610
+ logger.info(f"Using COPT Concurrent configuration (parallel simplex + barrier)")
611
+ return 'copt', copt_concurrent_options
612
+
493
613
  elif solver_name in ['highs', 'cplex', 'glpk', 'cbc', 'scip', 'copt']:
494
614
  return solver_name, solver_options
495
615
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyconvexity
3
- Version: 0.3.8.post3
3
+ Version: 0.3.8.post5
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
@@ -1,5 +1,5 @@
1
1
  pyconvexity/__init__.py,sha256=eiAFroO4n-z8F0jTLpJgBIO7vtSxu9ovu3G2N-qqpUo,4783
2
- pyconvexity/_version.py,sha256=jxRGlI_N8vedEkv7nqYjYQvDc31-O3CwZMs1KZ9Odq8,28
2
+ pyconvexity/_version.py,sha256=hAXE8ndUYHyK3gOK1hBR1ri9H4YX_KTmcufHouTLgTo,27
3
3
  pyconvexity/timeseries.py,sha256=4p1Tdpa1otqDvCq2zppA4tw660sF_XWb8Xobib-cCms,11340
4
4
  pyconvexity/core/__init__.py,sha256=MgVa5rrRWIi2w1UI1P4leiBntvHeeOPv0Thm0DEXBHo,1209
5
5
  pyconvexity/core/database.py,sha256=M02q4UkJqAPeTXuwng9I7kHm16reJ7eq7wccWxnhE5I,15227
@@ -12,10 +12,11 @@ pyconvexity/data/loaders/__init__.py,sha256=6xPtOmH2n1mNby7ZjA-2Mk9F48Q246RNsyMn
12
12
  pyconvexity/data/loaders/cache.py,sha256=nnz8bV3slSehOT0alexFga9tM1XoJqWHBGqaXvz132U,7299
13
13
  pyconvexity/data/loaders/__pycache__/__init__.cpython-313.pyc,sha256=AuT3aXy3v5gssxdD1_CBaKqNAVmDt6GBwFSyAe3jHow,265
14
14
  pyconvexity/data/loaders/__pycache__/cache.cpython-313.pyc,sha256=9_xMQN6AciMzbzhCmWAzvEKRXfRINmfRsO8Dyg0_CUQ,9804
15
- pyconvexity/data/schema/01_core_schema.sql,sha256=Ww3eD71JGIBNw-t_eVJ6TVGju-sEDzpLqyRGqGDje54,18871
15
+ pyconvexity/data/schema/01_core_schema.sql,sha256=2kkEevAhXJtNnC-wca2bnyw0m11mjheh4g9MPZpwBAc,20865
16
16
  pyconvexity/data/schema/02_data_metadata.sql,sha256=oOfwa3PLY2_8rxKDD4cpDeqP5I_PdahcF8m6cSKStJM,10732
17
17
  pyconvexity/data/schema/03_validation_data.sql,sha256=1rKFi9y6jQ2OnfH32jnIKnZ5WtB8eG43hz0OVJhwn3w,58325
18
18
  pyconvexity/data/schema/04_scenario_schema.sql,sha256=sL4PySJNHIthXsnoJ2T5pdXUbpAi94ld0XGuU8LwNuQ,4641
19
+ pyconvexity/data/schema/migrate_add_geometries.sql,sha256=ljTz2ZIvfRkHCjJiUbZJr7PvUxPv3UeLl3ADb9U7dWc,2710
19
20
  pyconvexity/data/sources/__init__.py,sha256=Dn6_oS7wB-vLjMj2YeXlmIl6hNjACbicimSabKxIWnc,108
20
21
  pyconvexity/data/sources/gem.py,sha256=Ft2pAYsWe1V9poRge2Q4xdNt15XkG-USSR0XR9KFmsY,14935
21
22
  pyconvexity/data/sources/__pycache__/__init__.cpython-313.pyc,sha256=9x5FyLxmTE5ZRaEFNSF375KBd_rDLY6pGHGSWPpcxxA,313
@@ -25,22 +26,24 @@ pyconvexity/io/excel_exporter.py,sha256=pjgvTs5vq9K61mNOVutEzaH5Zx4FgrDG4Xc_YmXh
25
26
  pyconvexity/io/excel_importer.py,sha256=M7YcBqKUVzOMoR5HN-v8M2UnZgHRfhqgXBMUVD10-IQ,56898
26
27
  pyconvexity/io/netcdf_exporter.py,sha256=AMM-uXBj8sh86n5m57aZ6S7LulAyIx_HM-eM-26BrWQ,7428
27
28
  pyconvexity/io/netcdf_importer.py,sha256=nv4CYYqnbCBeznwCU_JGBMTbg-BGNpXKlsqbu2R8fTU,72152
28
- pyconvexity/models/__init__.py,sha256=-CEdfjwOp-6XvR4vVyV1Z6umF1axs82zzvv7VRZNcys,1690
29
+ pyconvexity/models/__init__.py,sha256=N8YqEntbF5NrxIgUk1Knj9FiOzmMtD5Kywc6THJVeFk,3528
29
30
  pyconvexity/models/attributes.py,sha256=LTvYF0hl56HeLjS8ZVocZWLhbLRTNhmZ5gUKxf93eSE,18254
30
- pyconvexity/models/components.py,sha256=yccDW9ROtjsk5eIO38Tr420VUj9KeV03IVLrfmZgj3c,14942
31
- pyconvexity/models/network.py,sha256=ePydR3l60-AaOBbrA4uld3hu3X9sB7GOSyBYMh3_rBA,13117
32
- pyconvexity/models/scenarios.py,sha256=6-devNWZccnFeQr3IsP19GkO6Ixp914RKD-6lIduN-A,5164
31
+ pyconvexity/models/carriers.py,sha256=-nmasYvsaUeYPY1B0QdzfF_eph2HUFb5n3KF3CFd-YI,3700
32
+ pyconvexity/models/components.py,sha256=wWRdX6vErZrQhhLTnHBLDOnkmLjbHY2e9J9ITZJi3F8,18287
33
+ pyconvexity/models/network.py,sha256=2oEZOeVotyAs-SJl-b73zJKzSBvJEa6n1ryM0wV-Nko,14762
34
+ pyconvexity/models/results.py,sha256=9IKgO4bve94OGHgUGUcxvFpySGRW8-K3Wwvv9RXEF2k,4031
35
+ pyconvexity/models/scenarios.py,sha256=oF_xSOjrKMmUTzSR4oBAKKqWyudOGewW6DvZBa5IKXw,4125
33
36
  pyconvexity/solvers/__init__.py,sha256=zoVf6T2Tmyj2XOeiVbEvaIMOX584orqCz1q9t1oXy0M,674
34
37
  pyconvexity/solvers/pypsa/__init__.py,sha256=KZqYDo7CvwB-5Kp784xxxtdn5kRcmn3gGSRlaQdDA4c,554
35
- pyconvexity/solvers/pypsa/api.py,sha256=05IN6KgjpVNJ3TrH0gv1F2-GLRnffLQOc3rsT1ZEql8,16629
38
+ pyconvexity/solvers/pypsa/api.py,sha256=si2VAvotQKk-hcNtT3bIWV0CE4EzSER94mxehPFm7M8,18015
36
39
  pyconvexity/solvers/pypsa/batch_loader.py,sha256=eQb8B11akQYtH3aK93WAOoXEI-ktk4imATw9gaYDNR4,13547
37
40
  pyconvexity/solvers/pypsa/builder.py,sha256=WrimcBvG4mNFLTrLq7131Ku0AXY_0oRKxfI81ywc5Cs,24460
38
- pyconvexity/solvers/pypsa/constraints.py,sha256=MycS-pvuYIEEa0s2tkskiydX_HhAKNTnsQdVc842u50,19792
39
- pyconvexity/solvers/pypsa/solver.py,sha256=2EB1EL6VRtYJ-KAk00vecEn8egMfdjOUaMfFuT8aPgA,65763
41
+ pyconvexity/solvers/pypsa/constraints.py,sha256=qosBSNe0pr4va4dMmQFM-ifJCNGAkhS1R2gerNmhaiQ,16266
42
+ pyconvexity/solvers/pypsa/solver.py,sha256=7jaksRKMaQuFYWb7Pl7rw7Pu0kO5DPysQX2JtWdUbBc,72074
40
43
  pyconvexity/solvers/pypsa/storage.py,sha256=T-0qEryiEy_8G4KiscPoiiWvTPd_OGqpLczW0_Xm85E,87331
41
44
  pyconvexity/validation/__init__.py,sha256=_6SVqXkaDFqmagub_O064Zm_QIdBrOra-Gvvbo9vM4I,549
42
45
  pyconvexity/validation/rules.py,sha256=6Kak12BVfUpjmgB5B7Wre55eGc5e1dvIdFca-vN-IFI,9296
43
- pyconvexity-0.3.8.post3.dist-info/METADATA,sha256=bqoTl9zga7X365E-kBanrXKrvepNvxvFrSSEEqBwTkg,4886
44
- pyconvexity-0.3.8.post3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
45
- pyconvexity-0.3.8.post3.dist-info/top_level.txt,sha256=wFPEDXVaebR3JO5Tt3HNse-ws5aROCcxEco15d6j64s,12
46
- pyconvexity-0.3.8.post3.dist-info/RECORD,,
46
+ pyconvexity-0.3.8.post5.dist-info/METADATA,sha256=Sls9yFmzwrnujuODybzOPn5Z7s-8DoikRaHfto5sbuM,4886
47
+ pyconvexity-0.3.8.post5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
48
+ pyconvexity-0.3.8.post5.dist-info/top_level.txt,sha256=wFPEDXVaebR3JO5Tt3HNse-ws5aROCcxEco15d6j64s,12
49
+ pyconvexity-0.3.8.post5.dist-info/RECORD,,