pyconvexity 0.3.8.post4__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.

pyconvexity/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.3.8post4"
1
+ __version__ = "0.3.8post5"
@@ -361,6 +361,57 @@ BEGIN
361
361
  WHERE id = NEW.component_id;
362
362
  END;
363
363
 
364
+ -- ============================================================================
365
+ -- COMPONENT GEOMETRIES - Optional GeoJSON geometries for spatial representation
366
+ -- ============================================================================
367
+
368
+ -- Component geometries - stores optional GeoJSON geometries for components
369
+ -- Enables real spatial representation (e.g., actual line routes, generator footprints)
370
+ CREATE TABLE component_geometries (
371
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
372
+ component_id INTEGER NOT NULL UNIQUE,
373
+
374
+ -- GeoJSON geometry stored as JSON text
375
+ -- Supports: Point, LineString, Polygon, MultiPolygon, MultiPoint, MultiLineString, GeometryCollection
376
+ geometry TEXT NOT NULL,
377
+
378
+ -- Cache the geometry type for faster queries and validation
379
+ geometry_type TEXT NOT NULL CHECK (geometry_type IN (
380
+ 'Point', 'LineString', 'Polygon', 'MultiPolygon',
381
+ 'MultiPoint', 'MultiLineString', 'GeometryCollection'
382
+ )),
383
+
384
+ -- Cache bounding box for spatial indexing and quick filtering
385
+ bbox_min_lng REAL,
386
+ bbox_min_lat REAL,
387
+ bbox_max_lng REAL,
388
+ bbox_max_lat REAL,
389
+
390
+ -- Metadata
391
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
392
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
393
+
394
+ CONSTRAINT fk_geometry_component
395
+ FOREIGN KEY (component_id) REFERENCES components(id) ON DELETE CASCADE
396
+ );
397
+
398
+ -- Indexes for efficient geometry queries
399
+ CREATE INDEX idx_component_geometries_component ON component_geometries(component_id);
400
+ CREATE INDEX idx_component_geometries_type ON component_geometries(geometry_type);
401
+ CREATE INDEX idx_component_geometries_bbox ON component_geometries(
402
+ bbox_min_lng, bbox_min_lat, bbox_max_lng, bbox_max_lat
403
+ );
404
+
405
+ -- Trigger to update timestamp on geometry changes
406
+ CREATE TRIGGER update_component_geometries_timestamp
407
+ BEFORE UPDATE ON component_geometries
408
+ FOR EACH ROW
409
+ BEGIN
410
+ UPDATE component_geometries
411
+ SET updated_at = CURRENT_TIMESTAMP
412
+ WHERE id = NEW.id;
413
+ END;
414
+
364
415
  -- ============================================================================
365
416
  -- NETWORK CONFIGURATION
366
417
  -- ============================================================================
@@ -0,0 +1,73 @@
1
+ -- ============================================================================
2
+ -- MIGRATION: Add Component Geometries Support
3
+ -- Adds the component_geometries table to existing databases
4
+ -- This migration is safe and backwards compatible - existing functionality is unchanged
5
+ -- ============================================================================
6
+
7
+ -- Check if the table already exists before creating
8
+ CREATE TABLE IF NOT EXISTS component_geometries (
9
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10
+ component_id INTEGER NOT NULL UNIQUE,
11
+
12
+ -- GeoJSON geometry stored as JSON text
13
+ -- Supports: Point, LineString, Polygon, MultiPolygon, MultiPoint, MultiLineString, GeometryCollection
14
+ geometry TEXT NOT NULL,
15
+
16
+ -- Cache the geometry type for faster queries and validation
17
+ geometry_type TEXT NOT NULL CHECK (geometry_type IN (
18
+ 'Point', 'LineString', 'Polygon', 'MultiPolygon',
19
+ 'MultiPoint', 'MultiLineString', 'GeometryCollection'
20
+ )),
21
+
22
+ -- Cache bounding box for spatial indexing and quick filtering
23
+ bbox_min_lng REAL,
24
+ bbox_min_lat REAL,
25
+ bbox_max_lng REAL,
26
+ bbox_max_lat REAL,
27
+
28
+ -- Metadata
29
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
30
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
31
+
32
+ CONSTRAINT fk_geometry_component
33
+ FOREIGN KEY (component_id) REFERENCES components(id) ON DELETE CASCADE
34
+ );
35
+
36
+ -- Create indexes if they don't exist
37
+ CREATE INDEX IF NOT EXISTS idx_component_geometries_component
38
+ ON component_geometries(component_id);
39
+
40
+ CREATE INDEX IF NOT EXISTS idx_component_geometries_type
41
+ ON component_geometries(geometry_type);
42
+
43
+ CREATE INDEX IF NOT EXISTS idx_component_geometries_bbox
44
+ ON component_geometries(bbox_min_lng, bbox_min_lat, bbox_max_lng, bbox_max_lat);
45
+
46
+ -- Create trigger for automatic timestamp updates
47
+ DROP TRIGGER IF EXISTS update_component_geometries_timestamp;
48
+
49
+ CREATE TRIGGER update_component_geometries_timestamp
50
+ BEFORE UPDATE ON component_geometries
51
+ FOR EACH ROW
52
+ BEGIN
53
+ UPDATE component_geometries
54
+ SET updated_at = CURRENT_TIMESTAMP
55
+ WHERE id = NEW.id;
56
+ END;
57
+
58
+ -- Update schema version
59
+ UPDATE system_metadata
60
+ SET value = '2.5.0',
61
+ updated_at = CURRENT_TIMESTAMP,
62
+ description = 'Database schema version - Added component_geometries table'
63
+ WHERE key = 'schema_version';
64
+
65
+ -- Add metadata about the migration
66
+ INSERT OR REPLACE INTO system_metadata (key, value, description)
67
+ VALUES ('geometries_migration_applied', datetime('now'), 'Timestamp when component geometries migration was applied');
68
+
69
+ -- Verify the migration
70
+ SELECT
71
+ 'Migration completed successfully!' as message,
72
+ COUNT(*) as existing_geometries
73
+ FROM component_geometries;
@@ -7,7 +7,8 @@ Contains high-level operations for networks, components, and attributes.
7
7
  from pyconvexity.models.components import (
8
8
  get_component_type, get_component, list_components_by_type,
9
9
  insert_component, create_component, update_component, delete_component,
10
- list_component_attributes, get_default_carrier_id, get_bus_name_to_id_map
10
+ list_component_attributes, get_default_carrier_id, get_bus_name_to_id_map,
11
+ get_component_by_name, get_component_id, component_exists, get_component_carrier_map
11
12
  )
12
13
 
13
14
  from pyconvexity.models.attributes import (
@@ -17,18 +18,50 @@ from pyconvexity.models.attributes import (
17
18
  from pyconvexity.models.network import (
18
19
  create_network, get_network_info, get_network_time_periods, list_networks,
19
20
  create_carrier, list_carriers, get_network_config, set_network_config,
20
- get_component_counts, get_master_scenario_id, resolve_scenario_id
21
+ get_component_counts, get_master_scenario_id, resolve_scenario_id,
22
+ get_first_network, get_network_by_name
21
23
  )
22
24
 
25
+ # Import from new modules
23
26
  from pyconvexity.models.scenarios import (
24
- create_scenario, list_scenarios, get_scenario, delete_scenario
27
+ list_scenarios as list_scenarios_new,
28
+ get_scenario_by_name, get_scenario_by_id, get_master_scenario
25
29
  )
26
30
 
31
+ from pyconvexity.models.results import (
32
+ get_solve_results, get_yearly_results
33
+ )
34
+
35
+ from pyconvexity.models.carriers import (
36
+ list_carriers as list_carriers_new,
37
+ get_carrier_by_name, get_carrier_by_id, get_carrier_colors
38
+ )
39
+
40
+ # Try to import old scenarios functions if they exist
41
+ try:
42
+ from pyconvexity.models.scenarios_old import (
43
+ create_scenario, list_scenarios as list_scenarios_old,
44
+ get_scenario, delete_scenario
45
+ )
46
+ # Use old functions as primary for backward compatibility
47
+ list_scenarios_primary = list_scenarios_old
48
+ except ImportError:
49
+ # Old module doesn't exist, use new functions
50
+ list_scenarios_primary = list_scenarios_new
51
+ # Create dummy functions for backward compatibility
52
+ def create_scenario(*args, **kwargs):
53
+ raise NotImplementedError("create_scenario not yet implemented in new API")
54
+ def get_scenario(*args, **kwargs):
55
+ return get_scenario_by_id(*args, **kwargs)
56
+ def delete_scenario(*args, **kwargs):
57
+ raise NotImplementedError("delete_scenario not yet implemented in new API")
58
+
27
59
  __all__ = [
28
60
  # Component operations
29
61
  "get_component_type", "get_component", "list_components_by_type",
30
62
  "insert_component", "create_component", "update_component", "delete_component",
31
63
  "list_component_attributes", "get_default_carrier_id", "get_bus_name_to_id_map",
64
+ "get_component_by_name", "get_component_id", "component_exists", "get_component_carrier_map",
32
65
 
33
66
  # Attribute operations
34
67
  "set_static_attribute", "set_timeseries_attribute", "get_attribute", "delete_attribute",
@@ -37,7 +70,18 @@ __all__ = [
37
70
  "create_network", "get_network_info", "get_network_time_periods", "list_networks",
38
71
  "create_carrier", "list_carriers", "get_network_config", "set_network_config",
39
72
  "get_component_counts", "get_master_scenario_id", "resolve_scenario_id",
73
+ "get_first_network", "get_network_by_name",
74
+
75
+ # Scenario operations (backward compatible)
76
+ "create_scenario", "list_scenarios_primary", "get_scenario", "delete_scenario",
77
+ "list_scenarios_new", "get_scenario_by_name", "get_scenario_by_id", "get_master_scenario",
40
78
 
41
- # Scenario operations
42
- "create_scenario", "list_scenarios", "get_scenario", "delete_scenario",
79
+ # Results operations
80
+ "get_solve_results", "get_yearly_results",
81
+
82
+ # Carrier operations
83
+ "list_carriers_new", "get_carrier_by_name", "get_carrier_by_id", "get_carrier_colors",
43
84
  ]
85
+
86
+ # Expose primary list_scenarios for convenience
87
+ list_scenarios = list_scenarios_primary
@@ -0,0 +1,156 @@
1
+ """
2
+ Carrier management operations for PyConvexity.
3
+
4
+ Provides operations for querying carriers and their properties.
5
+ """
6
+
7
+ import sqlite3
8
+ import logging
9
+ from typing import Dict, List, Optional
10
+ from dataclasses import dataclass
11
+
12
+ from pyconvexity.core.errors import ValidationError
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class Carrier:
19
+ """Represents an energy carrier in the network."""
20
+ id: int
21
+ network_id: int
22
+ name: str
23
+ co2_emissions: float
24
+ color: Optional[str]
25
+ nice_name: Optional[str]
26
+
27
+
28
+ def list_carriers(conn: sqlite3.Connection, network_id: int) -> List[Carrier]:
29
+ """
30
+ List all carriers for a network.
31
+
32
+ Args:
33
+ conn: Database connection
34
+ network_id: Network ID
35
+
36
+ Returns:
37
+ List of Carrier objects ordered by name
38
+ """
39
+ cursor = conn.execute("""
40
+ SELECT id, network_id, name, co2_emissions, color, nice_name
41
+ FROM carriers
42
+ WHERE network_id = ?
43
+ ORDER BY name
44
+ """, (network_id,))
45
+
46
+ carriers = []
47
+ for row in cursor.fetchall():
48
+ carriers.append(Carrier(
49
+ id=row[0],
50
+ network_id=row[1],
51
+ name=row[2],
52
+ co2_emissions=row[3] or 0.0,
53
+ color=row[4],
54
+ nice_name=row[5]
55
+ ))
56
+
57
+ return carriers
58
+
59
+
60
+ def get_carrier_by_name(conn: sqlite3.Connection, network_id: int, name: str) -> Carrier:
61
+ """
62
+ Get a carrier by name.
63
+
64
+ Args:
65
+ conn: Database connection
66
+ network_id: Network ID
67
+ name: Carrier name
68
+
69
+ Returns:
70
+ Carrier object
71
+
72
+ Raises:
73
+ ValidationError: If carrier doesn't exist
74
+ """
75
+ cursor = conn.execute("""
76
+ SELECT id, network_id, name, co2_emissions, color, nice_name
77
+ FROM carriers
78
+ WHERE network_id = ? AND name = ?
79
+ """, (network_id, name))
80
+
81
+ row = cursor.fetchone()
82
+ if not row:
83
+ raise ValidationError(f"Carrier '{name}' not found for network {network_id}")
84
+
85
+ return Carrier(
86
+ id=row[0],
87
+ network_id=row[1],
88
+ name=row[2],
89
+ co2_emissions=row[3] or 0.0,
90
+ color=row[4],
91
+ nice_name=row[5]
92
+ )
93
+
94
+
95
+ def get_carrier_by_id(conn: sqlite3.Connection, carrier_id: int) -> Carrier:
96
+ """
97
+ Get a carrier by ID.
98
+
99
+ Args:
100
+ conn: Database connection
101
+ carrier_id: Carrier ID
102
+
103
+ Returns:
104
+ Carrier object
105
+
106
+ Raises:
107
+ ValidationError: If carrier doesn't exist
108
+ """
109
+ cursor = conn.execute("""
110
+ SELECT id, network_id, name, co2_emissions, color, nice_name
111
+ FROM carriers
112
+ WHERE id = ?
113
+ """, (carrier_id,))
114
+
115
+ row = cursor.fetchone()
116
+ if not row:
117
+ raise ValidationError(f"Carrier with ID {carrier_id} not found")
118
+
119
+ return Carrier(
120
+ id=row[0],
121
+ network_id=row[1],
122
+ name=row[2],
123
+ co2_emissions=row[3] or 0.0,
124
+ color=row[4],
125
+ nice_name=row[5]
126
+ )
127
+
128
+
129
+ def get_carrier_colors(conn: sqlite3.Connection, network_id: int) -> Dict[str, str]:
130
+ """
131
+ Get carrier colors for visualization.
132
+
133
+ Args:
134
+ conn: Database connection
135
+ network_id: Network ID
136
+
137
+ Returns:
138
+ Dictionary mapping carrier names to color strings
139
+ """
140
+ cursor = conn.execute("""
141
+ SELECT name, color
142
+ FROM carriers
143
+ WHERE network_id = ?
144
+ """, (network_id,))
145
+
146
+ colors = {}
147
+ for row in cursor.fetchall():
148
+ if row[1]: # Only include if color is defined
149
+ colors[row[0]] = row[1]
150
+
151
+ # Add default color for Unmet Load if not present
152
+ if 'Unmet Load' not in colors:
153
+ colors['Unmet Load'] = '#FF0000'
154
+
155
+ return colors
156
+
@@ -364,6 +364,126 @@ def list_component_attributes(
364
364
  return [row[0] for row in cursor.fetchall()]
365
365
 
366
366
 
367
+ def get_component_by_name(conn: sqlite3.Connection, network_id: int, name: str) -> Component:
368
+ """
369
+ Get a component by name.
370
+
371
+ Args:
372
+ conn: Database connection
373
+ network_id: Network ID
374
+ name: Component name
375
+
376
+ Returns:
377
+ Component object
378
+
379
+ Raises:
380
+ ComponentNotFound: If component doesn't exist
381
+ """
382
+ cursor = conn.execute("""
383
+ SELECT id, network_id, component_type, name, longitude, latitude,
384
+ carrier_id, bus_id, bus0_id, bus1_id
385
+ FROM components
386
+ WHERE network_id = ? AND name = ?
387
+ """, (network_id, name))
388
+
389
+ row = cursor.fetchone()
390
+ if not row:
391
+ raise ComponentNotFound(f"Component '{name}' not found in network {network_id}")
392
+
393
+ return Component(
394
+ id=row[0],
395
+ network_id=row[1],
396
+ component_type=row[2],
397
+ name=row[3],
398
+ longitude=row[4],
399
+ latitude=row[5],
400
+ carrier_id=row[6],
401
+ bus_id=row[7],
402
+ bus0_id=row[8],
403
+ bus1_id=row[9]
404
+ )
405
+
406
+
407
+ def get_component_id(conn: sqlite3.Connection, network_id: int, name: str) -> int:
408
+ """
409
+ Get component ID by name.
410
+
411
+ Args:
412
+ conn: Database connection
413
+ network_id: Network ID
414
+ name: Component name
415
+
416
+ Returns:
417
+ Component ID
418
+
419
+ Raises:
420
+ ComponentNotFound: If component doesn't exist
421
+ """
422
+ component = get_component_by_name(conn, network_id, name)
423
+ return component.id
424
+
425
+
426
+ def component_exists(conn: sqlite3.Connection, network_id: int, name: str) -> bool:
427
+ """
428
+ Check if a component exists.
429
+
430
+ Args:
431
+ conn: Database connection
432
+ network_id: Network ID
433
+ name: Component name
434
+
435
+ Returns:
436
+ True if component exists, False otherwise
437
+ """
438
+ cursor = conn.execute("""
439
+ SELECT 1 FROM components WHERE network_id = ? AND name = ?
440
+ """, (network_id, name))
441
+
442
+ return cursor.fetchone() is not None
443
+
444
+
445
+ def get_component_carrier_map(
446
+ conn: sqlite3.Connection,
447
+ network_id: int,
448
+ component_type: Optional[str] = None
449
+ ) -> Dict[str, str]:
450
+ """
451
+ Get mapping from component names to carrier names.
452
+
453
+ Args:
454
+ conn: Database connection
455
+ network_id: Network ID
456
+ component_type: Optional component type filter
457
+
458
+ Returns:
459
+ Dictionary mapping component names to carrier names
460
+ """
461
+ if component_type:
462
+ cursor = conn.execute("""
463
+ SELECT c.name,
464
+ CASE
465
+ WHEN c.component_type = 'UNMET_LOAD' THEN 'Unmet Load'
466
+ ELSE carr.name
467
+ END as carrier_name
468
+ FROM components c
469
+ LEFT JOIN carriers carr ON c.carrier_id = carr.id
470
+ WHERE c.network_id = ? AND c.component_type = ?
471
+ """, (network_id, component_type.upper()))
472
+ else:
473
+ cursor = conn.execute("""
474
+ SELECT c.name,
475
+ CASE
476
+ WHEN c.component_type = 'UNMET_LOAD' THEN 'Unmet Load'
477
+ ELSE carr.name
478
+ END as carrier_name
479
+ FROM components c
480
+ LEFT JOIN carriers carr ON c.carrier_id = carr.id
481
+ WHERE c.network_id = ?
482
+ """, (network_id,))
483
+
484
+ return {row[0]: row[1] for row in cursor.fetchall()}
485
+
486
+
367
487
  # Helper functions
368
488
 
369
489
  def get_default_carrier_id(
@@ -176,6 +176,72 @@ def list_networks(conn: sqlite3.Connection) -> List[Dict[str, Any]]:
176
176
  return networks
177
177
 
178
178
 
179
+ def get_first_network(conn: sqlite3.Connection) -> Optional[Dict[str, Any]]:
180
+ """
181
+ Get the first network (useful for single-network databases).
182
+
183
+ Args:
184
+ conn: Database connection
185
+
186
+ Returns:
187
+ Network dictionary or None if no networks exist
188
+ """
189
+ cursor = conn.execute("""
190
+ SELECT id, name, description, created_at, updated_at, time_interval, time_start, time_end
191
+ FROM networks
192
+ ORDER BY created_at DESC
193
+ LIMIT 1
194
+ """)
195
+
196
+ row = cursor.fetchone()
197
+ if not row:
198
+ return None
199
+
200
+ return {
201
+ "id": row[0],
202
+ "name": row[1],
203
+ "description": row[2],
204
+ "created_at": row[3],
205
+ "updated_at": row[4],
206
+ "time_resolution": row[5],
207
+ "start_time": row[6],
208
+ "end_time": row[7],
209
+ }
210
+
211
+
212
+ def get_network_by_name(conn: sqlite3.Connection, name: str) -> Optional[Dict[str, Any]]:
213
+ """
214
+ Get a network by name.
215
+
216
+ Args:
217
+ conn: Database connection
218
+ name: Network name
219
+
220
+ Returns:
221
+ Network dictionary or None if not found
222
+ """
223
+ cursor = conn.execute("""
224
+ SELECT id, name, description, created_at, updated_at, time_interval, time_start, time_end
225
+ FROM networks
226
+ WHERE name = ?
227
+ """, (name,))
228
+
229
+ row = cursor.fetchone()
230
+ if not row:
231
+ return None
232
+
233
+ return {
234
+ "id": row[0],
235
+ "name": row[1],
236
+ "description": row[2],
237
+ "created_at": row[3],
238
+ "updated_at": row[4],
239
+ "time_resolution": row[5],
240
+ "start_time": row[6],
241
+ "end_time": row[7],
242
+ }
243
+
244
+
179
245
  def create_carrier(
180
246
  conn: sqlite3.Connection,
181
247
  network_id: int,
@@ -309,7 +375,7 @@ def get_network_config(
309
375
  # Apply system defaults for missing parameters
310
376
  defaults = {
311
377
  'unmet_load_active': True,
312
- 'discount_rate': 0.01,
378
+ 'discount_rate': 0.0, # No discounting by default
313
379
  'solver_name': 'default'
314
380
  }
315
381
 
@@ -0,0 +1,138 @@
1
+ """
2
+ Results and statistics operations for PyConvexity.
3
+
4
+ Provides operations for querying solve results and statistics.
5
+ """
6
+
7
+ import sqlite3
8
+ import json
9
+ import logging
10
+ from typing import Dict, Any, Optional
11
+ from dataclasses import dataclass
12
+
13
+ from pyconvexity.core.errors import ValidationError
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class SolveResults:
20
+ """Represents solve results for a scenario."""
21
+ network_statistics: Dict[str, Any]
22
+ metadata: Dict[str, Any]
23
+ status: str
24
+ objective_value: Optional[float]
25
+ solve_time: float
26
+
27
+
28
+ @dataclass
29
+ class YearlyResults:
30
+ """Represents yearly solve results."""
31
+ year: int
32
+ network_statistics: Dict[str, Any]
33
+ metadata: Dict[str, Any]
34
+
35
+
36
+ def get_solve_results(
37
+ conn: sqlite3.Connection,
38
+ network_id: int,
39
+ scenario_id: Optional[int] = None
40
+ ) -> Optional[SolveResults]:
41
+ """
42
+ Get overall solve results for a scenario.
43
+
44
+ Args:
45
+ conn: Database connection
46
+ network_id: Network ID
47
+ scenario_id: Scenario ID (uses master scenario if None)
48
+
49
+ Returns:
50
+ SolveResults object or None if no results found
51
+ """
52
+ # Resolve scenario ID if not provided
53
+ if scenario_id is None:
54
+ from pyconvexity.models.scenarios import get_master_scenario
55
+ scenario = get_master_scenario(conn, network_id)
56
+ scenario_id = scenario.id
57
+
58
+ cursor = conn.execute("""
59
+ SELECT results_json, metadata_json, solve_status, objective_value, solve_time_seconds
60
+ FROM network_solve_results
61
+ WHERE network_id = ? AND scenario_id = ?
62
+ ORDER BY created_at DESC
63
+ LIMIT 1
64
+ """, (network_id, scenario_id))
65
+
66
+ row = cursor.fetchone()
67
+ if not row:
68
+ return None
69
+
70
+ try:
71
+ results_json = json.loads(row[0]) if row[0] else {}
72
+ metadata_json = json.loads(row[1]) if row[1] else {}
73
+
74
+ # Extract network_statistics from results_json
75
+ network_statistics = results_json.get('network_statistics', {})
76
+
77
+ return SolveResults(
78
+ network_statistics=network_statistics,
79
+ metadata=metadata_json,
80
+ status=row[2] or 'unknown',
81
+ objective_value=row[3],
82
+ solve_time=row[4] or 0.0
83
+ )
84
+ except json.JSONDecodeError as e:
85
+ logger.error(f"Error parsing JSON for scenario {scenario_id}: {e}")
86
+ return None
87
+
88
+
89
+ def get_yearly_results(
90
+ conn: sqlite3.Connection,
91
+ network_id: int,
92
+ scenario_id: Optional[int] = None
93
+ ) -> Dict[int, YearlyResults]:
94
+ """
95
+ Get year-by-year solve results for a scenario.
96
+
97
+ Args:
98
+ conn: Database connection
99
+ network_id: Network ID
100
+ scenario_id: Scenario ID (uses master scenario if None)
101
+
102
+ Returns:
103
+ Dictionary mapping years to YearlyResults objects
104
+ """
105
+ # Resolve scenario ID if not provided
106
+ if scenario_id is None:
107
+ from pyconvexity.models.scenarios import get_master_scenario
108
+ scenario = get_master_scenario(conn, network_id)
109
+ scenario_id = scenario.id
110
+
111
+ cursor = conn.execute("""
112
+ SELECT year, results_json, metadata_json
113
+ FROM network_solve_results_by_year
114
+ WHERE network_id = ? AND scenario_id = ?
115
+ ORDER BY year
116
+ """, (network_id, scenario_id))
117
+
118
+ yearly_results = {}
119
+ for row in cursor.fetchall():
120
+ year = row[0]
121
+ try:
122
+ results_json = json.loads(row[1]) if row[1] else {}
123
+ metadata_json = json.loads(row[2]) if row[2] else {}
124
+
125
+ # Extract network_statistics from results_json
126
+ network_statistics = results_json.get('network_statistics', {})
127
+
128
+ yearly_results[year] = YearlyResults(
129
+ year=year,
130
+ network_statistics=network_statistics,
131
+ metadata=metadata_json
132
+ )
133
+ except json.JSONDecodeError as e:
134
+ logger.error(f"Error parsing JSON for year {year}: {e}")
135
+ continue
136
+
137
+ return yearly_results
138
+
@@ -1,177 +1,165 @@
1
1
  """
2
2
  Scenario management operations for PyConvexity.
3
3
 
4
- Provides operations for creating and managing scenarios within networks.
4
+ Provides operations for listing, querying, and managing scenarios.
5
5
  """
6
6
 
7
7
  import sqlite3
8
8
  import logging
9
9
  from typing import List, Optional
10
- from datetime import datetime
10
+ from dataclasses import dataclass
11
11
 
12
- from pyconvexity.core.errors import ValidationError, DatabaseError
12
+ from pyconvexity.core.errors import ValidationError
13
13
 
14
14
  logger = logging.getLogger(__name__)
15
15
 
16
16
 
17
- def create_scenario(
18
- conn: sqlite3.Connection,
19
- network_id: int,
20
- name: str,
21
- description: Optional[str] = None,
22
- is_master: bool = False,
23
- ) -> int:
17
+ @dataclass
18
+ class Scenario:
19
+ """Represents a scenario in the network."""
20
+ id: int
21
+ network_id: int
22
+ name: str
23
+ description: Optional[str]
24
+ is_master: bool
25
+ created_at: str
26
+ updated_at: Optional[str] = None
27
+
28
+
29
+ def list_scenarios(conn: sqlite3.Connection, network_id: int) -> List[Scenario]:
24
30
  """
25
- Create a new scenario for a network.
31
+ List all scenarios for a network.
26
32
 
27
33
  Args:
28
34
  conn: Database connection
29
- network_id: ID of the network
30
- name: Name of the scenario
31
- description: Optional description
32
- is_master: Whether this is a master scenario (default False)
35
+ network_id: Network ID
33
36
 
34
37
  Returns:
35
- ID of the newly created scenario
36
-
37
- Raises:
38
- ValidationError: If network doesn't exist or scenario name conflicts
39
- DatabaseError: If creation fails
38
+ List of Scenario objects ordered by master first, then by creation date
40
39
  """
40
+ cursor = conn.execute("""
41
+ SELECT id, network_id, name, description, is_master, created_at, updated_at
42
+ FROM scenarios
43
+ WHERE network_id = ?
44
+ ORDER BY is_master DESC, created_at
45
+ """, (network_id,))
41
46
 
42
- # Validate network exists
43
- cursor = conn.execute("SELECT COUNT(*) FROM networks WHERE id = ?", (network_id,))
44
- if cursor.fetchone()[0] == 0:
45
- raise ValidationError(f"Network with ID {network_id} not found")
46
-
47
- # Check for name conflicts within the network
48
- cursor = conn.execute(
49
- "SELECT COUNT(*) FROM scenarios WHERE network_id = ? AND name = ?",
50
- (network_id, name)
51
- )
52
- if cursor.fetchone()[0] > 0:
53
- raise ValidationError(f"Scenario with name '{name}' already exists in network {network_id}")
54
-
55
- # Insert the scenario (database triggers will handle master scenario uniqueness)
56
- cursor = conn.execute(
57
- "INSERT INTO scenarios (network_id, name, description, is_master, created_at) "
58
- "VALUES (?, ?, ?, ?, datetime('now'))",
59
- (network_id, name, description, is_master)
60
- )
61
-
62
- scenario_id = cursor.lastrowid
63
- if not scenario_id:
64
- raise DatabaseError("Failed to create scenario")
47
+ scenarios = []
48
+ for row in cursor.fetchall():
49
+ scenarios.append(Scenario(
50
+ id=row[0],
51
+ network_id=row[1],
52
+ name=row[2],
53
+ description=row[3],
54
+ is_master=bool(row[4]),
55
+ created_at=row[5],
56
+ updated_at=row[6]
57
+ ))
65
58
 
66
- logger.info(f"Created scenario '{name}' (ID: {scenario_id}) for network {network_id}")
67
- return scenario_id
59
+ return scenarios
68
60
 
69
61
 
70
- def list_scenarios(conn: sqlite3.Connection, network_id: int) -> List[dict]:
62
+ def get_scenario_by_name(conn: sqlite3.Connection, network_id: int, name: str) -> Scenario:
71
63
  """
72
- List all scenarios for a network.
64
+ Get a scenario by name.
73
65
 
74
66
  Args:
75
67
  conn: Database connection
76
- network_id: ID of the network
68
+ network_id: Network ID
69
+ name: Scenario name
77
70
 
78
71
  Returns:
79
- List of scenario dictionaries with keys: id, network_id, name, description, is_master, created_at
72
+ Scenario object
80
73
 
81
74
  Raises:
82
- DatabaseError: If query fails
75
+ ValidationError: If scenario doesn't exist
83
76
  """
77
+ cursor = conn.execute("""
78
+ SELECT id, network_id, name, description, is_master, created_at, updated_at
79
+ FROM scenarios
80
+ WHERE network_id = ? AND name = ?
81
+ """, (network_id, name))
84
82
 
85
- cursor = conn.execute(
86
- "SELECT id, network_id, name, description, is_master, created_at "
87
- "FROM scenarios "
88
- "WHERE network_id = ? "
89
- "ORDER BY is_master DESC, created_at ASC",
90
- (network_id,)
83
+ row = cursor.fetchone()
84
+ if not row:
85
+ raise ValidationError(f"Scenario '{name}' not found for network {network_id}")
86
+
87
+ return Scenario(
88
+ id=row[0],
89
+ network_id=row[1],
90
+ name=row[2],
91
+ description=row[3],
92
+ is_master=bool(row[4]),
93
+ created_at=row[5],
94
+ updated_at=row[6]
91
95
  )
92
-
93
- scenarios = []
94
- for row in cursor.fetchall():
95
- scenarios.append({
96
- 'id': row[0],
97
- 'network_id': row[1],
98
- 'name': row[2],
99
- 'description': row[3],
100
- 'is_master': bool(row[4]),
101
- 'created_at': row[5],
102
- })
103
-
104
- logger.debug(f"Found {len(scenarios)} scenarios for network {network_id}")
105
- return scenarios
106
96
 
107
97
 
108
- def get_scenario(conn: sqlite3.Connection, scenario_id: int) -> dict:
98
+ def get_scenario_by_id(conn: sqlite3.Connection, scenario_id: int) -> Scenario:
109
99
  """
110
- Get a specific scenario by ID.
100
+ Get a scenario by ID.
111
101
 
112
102
  Args:
113
103
  conn: Database connection
114
- scenario_id: ID of the scenario
104
+ scenario_id: Scenario ID
115
105
 
116
106
  Returns:
117
- Scenario dictionary with keys: id, network_id, name, description, is_master, created_at
107
+ Scenario object
118
108
 
119
109
  Raises:
120
- ValidationError: If scenario not found
121
- DatabaseError: If query fails
110
+ ValidationError: If scenario doesn't exist
122
111
  """
123
-
124
- cursor = conn.execute(
125
- "SELECT id, network_id, name, description, is_master, created_at "
126
- "FROM scenarios "
127
- "WHERE id = ?",
128
- (scenario_id,)
129
- )
112
+ cursor = conn.execute("""
113
+ SELECT id, network_id, name, description, is_master, created_at, updated_at
114
+ FROM scenarios
115
+ WHERE id = ?
116
+ """, (scenario_id,))
130
117
 
131
118
  row = cursor.fetchone()
132
119
  if not row:
133
120
  raise ValidationError(f"Scenario with ID {scenario_id} not found")
134
121
 
135
- return {
136
- 'id': row[0],
137
- 'network_id': row[1],
138
- 'name': row[2],
139
- 'description': row[3],
140
- 'is_master': bool(row[4]),
141
- 'created_at': row[5],
142
- }
122
+ return Scenario(
123
+ id=row[0],
124
+ network_id=row[1],
125
+ name=row[2],
126
+ description=row[3],
127
+ is_master=bool(row[4]),
128
+ created_at=row[5],
129
+ updated_at=row[6]
130
+ )
143
131
 
144
132
 
145
- def delete_scenario(conn: sqlite3.Connection, scenario_id: int) -> None:
133
+ def get_master_scenario(conn: sqlite3.Connection, network_id: int) -> Scenario:
146
134
  """
147
- Delete a scenario (cannot delete master scenarios).
135
+ Get the master scenario for a network.
148
136
 
149
137
  Args:
150
138
  conn: Database connection
151
- scenario_id: ID of the scenario to delete
139
+ network_id: Network ID
140
+
141
+ Returns:
142
+ Scenario object for the master scenario
152
143
 
153
144
  Raises:
154
- ValidationError: If scenario not found or is master scenario
155
- DatabaseError: If deletion fails
145
+ ValidationError: If master scenario doesn't exist
156
146
  """
157
-
158
- # Check if scenario exists and is not master
159
- cursor = conn.execute(
160
- "SELECT is_master FROM scenarios WHERE id = ?",
161
- (scenario_id,)
162
- )
147
+ cursor = conn.execute("""
148
+ SELECT id, network_id, name, description, is_master, created_at, updated_at
149
+ FROM scenarios
150
+ WHERE network_id = ? AND is_master = TRUE
151
+ """, (network_id,))
163
152
 
164
153
  row = cursor.fetchone()
165
154
  if not row:
166
- raise ValidationError(f"Scenario with ID {scenario_id} not found")
167
-
168
- if row[0]: # is_master
169
- raise ValidationError("Cannot delete master scenario")
170
-
171
- # Delete the scenario (this will cascade to delete related component attributes)
172
- cursor = conn.execute("DELETE FROM scenarios WHERE id = ?", (scenario_id,))
173
-
174
- if cursor.rowcount == 0:
175
- raise DatabaseError("Failed to delete scenario")
176
-
177
- logger.info(f"Deleted scenario {scenario_id}")
155
+ raise ValidationError(f"No master scenario found for network {network_id}")
156
+
157
+ return Scenario(
158
+ id=row[0],
159
+ network_id=row[1],
160
+ name=row[2],
161
+ description=row[3],
162
+ is_master=bool(row[4]),
163
+ created_at=row[5],
164
+ updated_at=row[6]
165
+ )
@@ -68,7 +68,9 @@ def solve_network(
68
68
  progress_callback(8, "Loaded network configuration")
69
69
 
70
70
  # Use configuration values with parameter overrides
71
- effective_discount_rate = discount_rate if discount_rate is not None else network_config.get('discount_rate', 0.05)
71
+ # Note: network_config already has default of 0.0 from get_network_config()
72
+ effective_discount_rate = discount_rate if discount_rate is not None else network_config.get('discount_rate')
73
+ logger.info(f"Using discount rate: {effective_discount_rate} (from {'parameter override' if discount_rate is not None else 'network config'})")
72
74
 
73
75
  # Build network
74
76
  if progress_callback:
@@ -205,6 +207,7 @@ def solve_pypsa_network(
205
207
  scenario_id: Optional[int] = None,
206
208
  solver_name: str = "highs",
207
209
  solver_options: Optional[Dict[str, Any]] = None,
210
+ discount_rate: Optional[float] = None,
208
211
  store_results: bool = True,
209
212
  progress_callback: Optional[Callable[[int, str], None]] = None,
210
213
  custom_solver_config: Optional[Dict[str, Any]] = None
@@ -222,6 +225,7 @@ def solve_pypsa_network(
222
225
  scenario_id: Optional scenario ID
223
226
  solver_name: Solver to use (default: "highs"). Use "custom" for custom_solver_config.
224
227
  solver_options: Optional solver-specific options
228
+ discount_rate: Optional discount rate for multi-period optimization (default: 0.0)
225
229
  store_results: Whether to store results back to database (default: True)
226
230
  progress_callback: Optional callback for progress updates
227
231
  custom_solver_config: Optional custom solver configuration when solver_name="custom"
@@ -243,6 +247,7 @@ def solve_pypsa_network(
243
247
  network,
244
248
  solver_name=solver_name,
245
249
  solver_options=solver_options,
250
+ discount_rate=discount_rate,
246
251
  custom_solver_config=custom_solver_config
247
252
  )
248
253
 
@@ -133,12 +133,14 @@ class NetworkSolver:
133
133
  # Get solver configuration
134
134
  actual_solver_name, solver_config = self._get_solver_config(solver_name, solver_options, custom_solver_config)
135
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}")
136
140
 
137
141
  years = list(network.investment_periods)
138
- effective_discount_rate = discount_rate if discount_rate is not None else 0.05 # Default 5%
139
142
 
140
143
  logger.info(f"Multi-period optimization with {len(years)} periods: {years}")
141
- logger.info(f"Discount rate: {effective_discount_rate}")
142
144
 
143
145
  # Calculate investment period weightings with discount rate
144
146
  self._calculate_investment_weightings(network, effective_discount_rate)
@@ -402,17 +404,7 @@ class NetworkSolver:
402
404
  mosek_default_options = {
403
405
  'solver_options': {
404
406
  'MSK_DPAR_MIO_REL_GAP_CONST': 0.05, # MIP relative gap tolerance (5% to match Gurobi)
405
- 'MSK_IPAR_MIO_MAX_TIME': 3600, # Max time 1 hour
406
- # Safe MIP performance improvements
407
- 'MSK_IPAR_MIO_HEURISTIC_LEVEL': 2, # Moderate heuristics (safe, helps find good solutions faster)
408
- 'MSK_IPAR_MIO_SYMMETRY_LEVEL': 2, # Moderate symmetry detection (safe, can dramatically speed up symmetric problems)
409
- 'MSK_IPAR_MIO_PRESOLVE_AGGREGATOR_USE': 1, # MIP presolve aggregator (safe, helps reduce problem size)
410
- # Logging
411
- 'MSK_IPAR_LOG': 4, # Moderate logging (was 10)
412
- 'MSK_IPAR_LOG_INTPNT': 1, # Log interior-point progress
413
- 'MSK_IPAR_LOG_SIM': 4, # Log simplex progress
414
- 'MSK_IPAR_LOG_MIO': 2, # Reduced MIP logging (was 4)
415
- 'MSK_IPAR_LOG_MIO_FREQ': 10, # Log MIP every 10 seconds
407
+ 'MSK_IPAR_MIO_MAX_TIME': 36000, # Max time 1 hour
416
408
  }
417
409
  }
418
410
  if solver_options:
@@ -434,16 +426,7 @@ class NetworkSolver:
434
426
  'MSK_DPAR_MIO_REL_GAP_CONST': 0.05, # Match Gurobi 5% MIP gap
435
427
  'MSK_IPAR_MIO_NODE_OPTIMIZER': 4, # Use interior-point for MIP nodes
436
428
  'MSK_IPAR_MIO_ROOT_OPTIMIZER': 4, # Use interior-point for MIP root
437
- # Safe MIP performance improvements
438
- 'MSK_IPAR_MIO_HEURISTIC_LEVEL': 4, # Aggressive heuristics (was 2, match Gurobi's aggressive approach)
439
- 'MSK_IPAR_MIO_SYMMETRY_LEVEL': 2, # Moderate symmetry detection
440
- 'MSK_IPAR_MIO_PRESOLVE_AGGREGATOR_USE': 1, # MIP presolve aggregator
441
- 'MSK_DPAR_MIO_MAX_TIME': 3600, # Max time 1 hour
442
- # Logging
443
- 'MSK_IPAR_LOG': 4, # Moderate logging
444
- 'MSK_IPAR_LOG_INTPNT': 1, # Log interior-point progress
445
- 'MSK_IPAR_LOG_MIO': 2, # Reduced MIP logging
446
- 'MSK_IPAR_LOG_MIO_FREQ': 10, # Log MIP every 10 seconds
429
+ 'MSK_DPAR_MIO_MAX_TIME': 36000, # Max time 10 hour
447
430
  }
448
431
  }
449
432
  if solver_options:
@@ -459,22 +442,9 @@ class NetworkSolver:
459
442
  'MSK_DPAR_INTPNT_TOL_PFEAS': 1e-5, # Match Gurobi (was 1e-6)
460
443
  'MSK_DPAR_INTPNT_TOL_DFEAS': 1e-5, # Match Gurobi (was 1e-6)
461
444
  'MSK_IPAR_NUM_THREADS': 0, # Use all available cores (0 = auto)
462
- 'MSK_IPAR_PRESOLVE_USE': 1, # Force presolve
463
- 'MSK_IPAR_PRESOLVE_LINDEP_USE': 1, # Linear dependency check
464
445
  'MSK_DPAR_MIO_REL_GAP_CONST': 0.05, # Match Gurobi 5% MIP gap (was 1e-6)
465
- 'MSK_IPAR_MIO_NODE_OPTIMIZER': 4, # Use interior-point for MIP nodes
466
446
  'MSK_IPAR_MIO_ROOT_OPTIMIZER': 4, # Use interior-point for MIP root
467
- # Safe MIP performance improvements
468
- 'MSK_IPAR_MIO_HEURISTIC_LEVEL': 2, # Moderate heuristics (safe, helps find good solutions faster)
469
- 'MSK_IPAR_MIO_SYMMETRY_LEVEL': 2, # Moderate symmetry detection (safe, can dramatically speed up symmetric problems)
470
- 'MSK_IPAR_MIO_PRESOLVE_AGGREGATOR_USE': 1, # MIP presolve aggregator (safe, helps reduce problem size)
471
- 'MSK_DPAR_MIO_MAX_TIME': 3600, # Max time 1 hour (safety limit)
472
- # Logging
473
- 'MSK_IPAR_LOG': 4, # Moderate logging (was 10)
474
- 'MSK_IPAR_LOG_INTPNT': 1, # Log interior-point progress
475
- 'MSK_IPAR_LOG_MIO': 2, # Reduced MIP logging (was 4)
476
- 'MSK_IPAR_LOG_MIO_FREQ': 10, # Log MIP every 10 seconds
477
- # 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)
478
448
  }
479
449
  }
480
450
  if solver_options:
@@ -487,21 +457,11 @@ class NetworkSolver:
487
457
  'solver_options': {
488
458
  'MSK_IPAR_NUM_THREADS': 0, # Use all available cores (0 = automatic)
489
459
  'MSK_IPAR_PRESOLVE_USE': 1, # Force presolve
490
- 'MSK_IPAR_SIM_SCALING': 2, # Aggressive scaling (2 = MSK_SCALING_AGGRESSIVE)
491
460
  'MSK_DPAR_MIO_REL_GAP_CONST': 0.05, # Match Gurobi 5% MIP gap (was 1e-6)
492
461
  'MSK_IPAR_MIO_NODE_OPTIMIZER': 1, # Use dual simplex for MIP nodes (1 = MSK_OPTIMIZER_DUAL_SIMPLEX)
493
462
  'MSK_IPAR_MIO_ROOT_OPTIMIZER': 1, # Use dual simplex for MIP root
494
- # Safe MIP performance improvements
495
- 'MSK_IPAR_MIO_HEURISTIC_LEVEL': 2, # Moderate heuristics (safe, helps find good solutions faster)
496
- 'MSK_IPAR_MIO_SYMMETRY_LEVEL': 2, # Moderate symmetry detection (safe, can dramatically speed up symmetric problems)
497
- 'MSK_IPAR_MIO_PRESOLVE_AGGREGATOR_USE': 1, # MIP presolve aggregator (safe, helps reduce problem size)
498
- 'MSK_DPAR_MIO_MAX_TIME': 3600, # Max time 1 hour (safety limit)
499
- # Logging
500
- 'MSK_IPAR_LOG': 4, # Moderate logging (was 10)
501
- 'MSK_IPAR_LOG_SIM': 4, # Log simplex progress
502
- 'MSK_IPAR_LOG_MIO': 2, # Reduced MIP logging (was 4)
503
- 'MSK_IPAR_LOG_MIO_FREQ': 10, # Log MIP every 10 seconds
504
- # 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
+
505
465
  }
506
466
  }
507
467
  if solver_options:
@@ -515,18 +475,8 @@ class NetworkSolver:
515
475
  mosek_defaults = {
516
476
  'solver_options': {
517
477
  'MSK_DPAR_MIO_REL_GAP_CONST': 0.05, # Match Gurobi 5% MIP gap (was 1e-4)
518
- 'MSK_IPAR_MIO_MAX_TIME': 3600, # Max time 1 hour
478
+ 'MSK_IPAR_MIO_MAX_TIME': 36000, # Max time 1 hour
519
479
  'MSK_IPAR_NUM_THREADS': 0, # Use all cores (0 = auto)
520
- # CRITICAL: Use interior-point for MIP (much faster than simplex)
521
- 'MSK_IPAR_MIO_NODE_OPTIMIZER': 4, # Use interior-point for MIP nodes (4 = MSK_OPTIMIZER_INTPNT)
522
- 'MSK_IPAR_MIO_ROOT_OPTIMIZER': 4, # Use interior-point for MIP root (4 = MSK_OPTIMIZER_INTPNT)
523
- # Safe MIP performance improvements
524
- 'MSK_IPAR_MIO_HEURISTIC_LEVEL': 2, # Moderate heuristics (safe, helps find good solutions faster)
525
- 'MSK_IPAR_MIO_SYMMETRY_LEVEL': 2, # Moderate symmetry detection (safe, can dramatically speed up symmetric problems)
526
- 'MSK_IPAR_MIO_PRESOLVE_AGGREGATOR_USE': 1, # MIP presolve aggregator (safe, helps reduce problem size)
527
- # Logging
528
- 'MSK_IPAR_LOG': 4, # Moderate logging
529
- 'MSK_IPAR_LOG_MIO': 2, # Log MIP occasionally
530
480
  }
531
481
  }
532
482
  if solver_options:
@@ -556,8 +506,8 @@ class NetworkSolver:
556
506
  'LpMethod': 2, # Barrier method
557
507
  'Crossover': 0, # Skip crossover for speed
558
508
  'RelGap': 0.05, # 5% MIP gap (match Gurobi)
559
- 'TimeLimit': 3600, # 1 hour time limit
560
- 'Threads': 2, # 4 threads (memory-conscious)
509
+ 'TimeLimit': 7200, # 1 hour time limit
510
+ 'Threads': -1, # 4 threads (memory-conscious)
561
511
  'Presolve': 3, # Aggressive presolve
562
512
  'Scaling': 1, # Enable scaling
563
513
  'FeasTol': 1e-5, # Match Gurobi feasibility
@@ -581,7 +531,7 @@ class NetworkSolver:
581
531
  'BarHomogeneous': 1, # Use homogeneous self-dual form
582
532
  'RelGap': 0.05, # 5% MIP gap
583
533
  'TimeLimit': 3600, # 1 hour
584
- 'Threads': 2, # 4 threads (memory-conscious)
534
+ 'Threads': -1, # 4 threads (memory-conscious)
585
535
  'Presolve': 3, # Aggressive presolve
586
536
  'Scaling': 1, # Enable scaling
587
537
  'FeasTol': 1e-5,
@@ -603,16 +553,12 @@ class NetworkSolver:
603
553
  'LpMethod': 2, # Barrier method
604
554
  'Crossover': 1, # Enable crossover for better solutions
605
555
  'RelGap': 0.05, # 5% MIP gap (relaxed for faster solves)
606
- 'TimeLimit': 3600, # 1 hour
607
- 'Threads': 2, # Use all cores
608
- 'Presolve': 3, # Aggressive presolve
556
+ 'TimeLimit': 36000, # 10 hour
557
+ 'Threads': -1, # Use all cores
558
+ 'Presolve': 2, # Aggressive presolve
609
559
  'Scaling': 1, # Enable scaling
610
- 'FeasTol': 1e-6, # Tighter feasibility
611
- 'DualTol': 1e-6, # Tighter dual tolerance
612
- # MIP performance settings
613
- 'CutLevel': 3, # Aggressive cuts
614
- 'HeurLevel': 3, # Aggressive heuristics
615
- 'StrongBranching': 2, # Normal strong branching
560
+ 'FeasTol': 1e-4, # Tighter feasibility
561
+ 'DualTol': 1e-4, # Tighter dual tolerance
616
562
  }
617
563
  }
618
564
  if solver_options:
@@ -626,7 +572,7 @@ class NetworkSolver:
626
572
  'LpMethod': 1, # Dual simplex method
627
573
  'RelGap': 0.05, # 5% MIP gap
628
574
  'TimeLimit': 3600, # 1 hour
629
- 'Threads': 2, # Use all cores
575
+ 'Threads': -1, # Use all cores
630
576
  'Presolve': 3, # Aggressive presolve
631
577
  'Scaling': 1, # Enable scaling
632
578
  'FeasTol': 1e-6,
@@ -648,7 +594,7 @@ class NetworkSolver:
648
594
  'LpMethod': 4, # Concurrent (simplex + barrier)
649
595
  'RelGap': 0.05, # 5% MIP gap
650
596
  'TimeLimit': 3600, # 1 hour
651
- 'Threads': 2, # Use all cores
597
+ 'Threads': -1, # Use all cores
652
598
  'Presolve': 3, # Aggressive presolve
653
599
  'Scaling': 1, # Enable scaling
654
600
  'FeasTol': 1e-5,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyconvexity
3
- Version: 0.3.8.post4
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=QpKwBQB8nrOjAVlaA3W8ReZysT4se7cws6FTiK5PyJQ,27
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=Kjw4_AUegvF7CH3yx3kLgn9nm-xTgrNsyTCvjSWtNTk,17614
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
41
  pyconvexity/solvers/pypsa/constraints.py,sha256=qosBSNe0pr4va4dMmQFM-ifJCNGAkhS1R2gerNmhaiQ,16266
39
- pyconvexity/solvers/pypsa/solver.py,sha256=Yqf4_q9k5UAKm8XJwYPgGDrnGU_JYLFJsNP9OJ-fNz0,77061
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.post4.dist-info/METADATA,sha256=24YZdnl-SFo5ZSe93PseCUi8hNB4fS1KxV3BBP3LJdU,4886
44
- pyconvexity-0.3.8.post4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
45
- pyconvexity-0.3.8.post4.dist-info/top_level.txt,sha256=wFPEDXVaebR3JO5Tt3HNse-ws5aROCcxEco15d6j64s,12
46
- pyconvexity-0.3.8.post4.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,,