pyconvexity 0.3.8.post4__py3-none-any.whl → 0.3.8.post6__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/data/schema/01_core_schema.sql +51 -0
- pyconvexity/data/schema/migrate_add_geometries.sql +73 -0
- pyconvexity/models/__init__.py +49 -5
- pyconvexity/models/carriers.py +156 -0
- pyconvexity/models/components.py +120 -0
- pyconvexity/models/network.py +67 -1
- pyconvexity/models/results.py +138 -0
- pyconvexity/models/scenarios.py +97 -114
- pyconvexity/solvers/pypsa/api.py +6 -1
- pyconvexity/solvers/pypsa/solver.py +20 -74
- {pyconvexity-0.3.8.post4.dist-info → pyconvexity-0.3.8.post6.dist-info}/METADATA +1 -1
- {pyconvexity-0.3.8.post4.dist-info → pyconvexity-0.3.8.post6.dist-info}/RECORD +15 -12
- {pyconvexity-0.3.8.post4.dist-info → pyconvexity-0.3.8.post6.dist-info}/WHEEL +0 -0
- {pyconvexity-0.3.8.post4.dist-info → pyconvexity-0.3.8.post6.dist-info}/top_level.txt +0 -0
pyconvexity/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.3.
|
|
1
|
+
__version__ = "0.3.8post6"
|
|
@@ -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;
|
pyconvexity/models/__init__.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
42
|
-
"
|
|
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
|
+
|
pyconvexity/models/components.py
CHANGED
|
@@ -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(
|
pyconvexity/models/network.py
CHANGED
|
@@ -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.
|
|
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 solved_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
|
+
|
pyconvexity/models/scenarios.py
CHANGED
|
@@ -1,177 +1,160 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Scenario management operations for PyConvexity.
|
|
3
3
|
|
|
4
|
-
Provides operations for
|
|
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
|
|
10
|
+
from dataclasses import dataclass
|
|
11
11
|
|
|
12
|
-
from pyconvexity.core.errors import ValidationError
|
|
12
|
+
from pyconvexity.core.errors import ValidationError
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
|
|
27
|
+
|
|
28
|
+
def list_scenarios(conn: sqlite3.Connection, network_id: int) -> List[Scenario]:
|
|
24
29
|
"""
|
|
25
|
-
|
|
30
|
+
List all scenarios for a network.
|
|
26
31
|
|
|
27
32
|
Args:
|
|
28
33
|
conn: Database connection
|
|
29
|
-
network_id: ID
|
|
30
|
-
name: Name of the scenario
|
|
31
|
-
description: Optional description
|
|
32
|
-
is_master: Whether this is a master scenario (default False)
|
|
34
|
+
network_id: Network ID
|
|
33
35
|
|
|
34
36
|
Returns:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
Raises:
|
|
38
|
-
ValidationError: If network doesn't exist or scenario name conflicts
|
|
39
|
-
DatabaseError: If creation fails
|
|
37
|
+
List of Scenario objects ordered by master first, then by creation date
|
|
40
38
|
"""
|
|
39
|
+
cursor = conn.execute("""
|
|
40
|
+
SELECT id, network_id, name, description, is_master, created_at
|
|
41
|
+
FROM scenarios
|
|
42
|
+
WHERE network_id = ?
|
|
43
|
+
ORDER BY is_master DESC, created_at
|
|
44
|
+
""", (network_id,))
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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")
|
|
46
|
+
scenarios = []
|
|
47
|
+
for row in cursor.fetchall():
|
|
48
|
+
scenarios.append(Scenario(
|
|
49
|
+
id=row[0],
|
|
50
|
+
network_id=row[1],
|
|
51
|
+
name=row[2],
|
|
52
|
+
description=row[3],
|
|
53
|
+
is_master=bool(row[4]),
|
|
54
|
+
created_at=row[5]
|
|
55
|
+
))
|
|
65
56
|
|
|
66
|
-
|
|
67
|
-
return scenario_id
|
|
57
|
+
return scenarios
|
|
68
58
|
|
|
69
59
|
|
|
70
|
-
def
|
|
60
|
+
def get_scenario_by_name(conn: sqlite3.Connection, network_id: int, name: str) -> Scenario:
|
|
71
61
|
"""
|
|
72
|
-
|
|
62
|
+
Get a scenario by name.
|
|
73
63
|
|
|
74
64
|
Args:
|
|
75
65
|
conn: Database connection
|
|
76
|
-
network_id: ID
|
|
66
|
+
network_id: Network ID
|
|
67
|
+
name: Scenario name
|
|
77
68
|
|
|
78
69
|
Returns:
|
|
79
|
-
|
|
70
|
+
Scenario object
|
|
80
71
|
|
|
81
72
|
Raises:
|
|
82
|
-
|
|
73
|
+
ValidationError: If scenario doesn't exist
|
|
83
74
|
"""
|
|
75
|
+
cursor = conn.execute("""
|
|
76
|
+
SELECT id, network_id, name, description, is_master, created_at
|
|
77
|
+
FROM scenarios
|
|
78
|
+
WHERE network_id = ? AND name = ?
|
|
79
|
+
""", (network_id, name))
|
|
84
80
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
row = cursor.fetchone()
|
|
82
|
+
if not row:
|
|
83
|
+
raise ValidationError(f"Scenario '{name}' not found for network {network_id}")
|
|
84
|
+
|
|
85
|
+
return Scenario(
|
|
86
|
+
id=row[0],
|
|
87
|
+
network_id=row[1],
|
|
88
|
+
name=row[2],
|
|
89
|
+
description=row[3],
|
|
90
|
+
is_master=bool(row[4]),
|
|
91
|
+
created_at=row[5]
|
|
91
92
|
)
|
|
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
93
|
|
|
107
94
|
|
|
108
|
-
def
|
|
95
|
+
def get_scenario_by_id(conn: sqlite3.Connection, scenario_id: int) -> Scenario:
|
|
109
96
|
"""
|
|
110
|
-
Get a
|
|
97
|
+
Get a scenario by ID.
|
|
111
98
|
|
|
112
99
|
Args:
|
|
113
100
|
conn: Database connection
|
|
114
|
-
scenario_id: ID
|
|
101
|
+
scenario_id: Scenario ID
|
|
115
102
|
|
|
116
103
|
Returns:
|
|
117
|
-
Scenario
|
|
104
|
+
Scenario object
|
|
118
105
|
|
|
119
106
|
Raises:
|
|
120
|
-
ValidationError: If scenario
|
|
121
|
-
DatabaseError: If query fails
|
|
107
|
+
ValidationError: If scenario doesn't exist
|
|
122
108
|
"""
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
(scenario_id,)
|
|
129
|
-
)
|
|
109
|
+
cursor = conn.execute("""
|
|
110
|
+
SELECT id, network_id, name, description, is_master, created_at
|
|
111
|
+
FROM scenarios
|
|
112
|
+
WHERE id = ?
|
|
113
|
+
""", (scenario_id,))
|
|
130
114
|
|
|
131
115
|
row = cursor.fetchone()
|
|
132
116
|
if not row:
|
|
133
117
|
raise ValidationError(f"Scenario with ID {scenario_id} not found")
|
|
134
118
|
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
119
|
+
return Scenario(
|
|
120
|
+
id=row[0],
|
|
121
|
+
network_id=row[1],
|
|
122
|
+
name=row[2],
|
|
123
|
+
description=row[3],
|
|
124
|
+
is_master=bool(row[4]),
|
|
125
|
+
created_at=row[5]
|
|
126
|
+
)
|
|
143
127
|
|
|
144
128
|
|
|
145
|
-
def
|
|
129
|
+
def get_master_scenario(conn: sqlite3.Connection, network_id: int) -> Scenario:
|
|
146
130
|
"""
|
|
147
|
-
|
|
131
|
+
Get the master scenario for a network.
|
|
148
132
|
|
|
149
133
|
Args:
|
|
150
134
|
conn: Database connection
|
|
151
|
-
|
|
135
|
+
network_id: Network ID
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Scenario object for the master scenario
|
|
152
139
|
|
|
153
140
|
Raises:
|
|
154
|
-
ValidationError: If scenario
|
|
155
|
-
DatabaseError: If deletion fails
|
|
141
|
+
ValidationError: If master scenario doesn't exist
|
|
156
142
|
"""
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
)
|
|
143
|
+
cursor = conn.execute("""
|
|
144
|
+
SELECT id, network_id, name, description, is_master, created_at
|
|
145
|
+
FROM scenarios
|
|
146
|
+
WHERE network_id = ? AND is_master = TRUE
|
|
147
|
+
""", (network_id,))
|
|
163
148
|
|
|
164
149
|
row = cursor.fetchone()
|
|
165
150
|
if not row:
|
|
166
|
-
raise ValidationError(f"
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
logger.info(f"Deleted scenario {scenario_id}")
|
|
151
|
+
raise ValidationError(f"No master scenario found for network {network_id}")
|
|
152
|
+
|
|
153
|
+
return Scenario(
|
|
154
|
+
id=row[0],
|
|
155
|
+
network_id=row[1],
|
|
156
|
+
name=row[2],
|
|
157
|
+
description=row[3],
|
|
158
|
+
is_master=bool(row[4]),
|
|
159
|
+
created_at=row[5]
|
|
160
|
+
)
|
pyconvexity/solvers/pypsa/api.py
CHANGED
|
@@ -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
|
-
|
|
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':
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
495
|
-
|
|
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':
|
|
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':
|
|
560
|
-
'Threads':
|
|
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':
|
|
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':
|
|
607
|
-
'Threads':
|
|
608
|
-
'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-
|
|
611
|
-
'DualTol': 1e-
|
|
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':
|
|
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':
|
|
597
|
+
'Threads': -1, # Use all cores
|
|
652
598
|
'Presolve': 3, # Aggressive presolve
|
|
653
599
|
'Scaling': 1, # Enable scaling
|
|
654
600
|
'FeasTol': 1e-5,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
pyconvexity/__init__.py,sha256=eiAFroO4n-z8F0jTLpJgBIO7vtSxu9ovu3G2N-qqpUo,4783
|
|
2
|
-
pyconvexity/_version.py,sha256=
|
|
2
|
+
pyconvexity/_version.py,sha256=jO5GlIb68CD0eDfWJLp-lwidoi1UzGwf4_unGDQim90,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=
|
|
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
|
|
29
|
+
pyconvexity/models/__init__.py,sha256=N8YqEntbF5NrxIgUk1Knj9FiOzmMtD5Kywc6THJVeFk,3528
|
|
29
30
|
pyconvexity/models/attributes.py,sha256=LTvYF0hl56HeLjS8ZVocZWLhbLRTNhmZ5gUKxf93eSE,18254
|
|
30
|
-
pyconvexity/models/
|
|
31
|
-
pyconvexity/models/
|
|
32
|
-
pyconvexity/models/
|
|
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=zEbrFj7ReEBnKJz_Kpf__HfIuHoVsZvLpMoL4RobCYE,4030
|
|
35
|
+
pyconvexity/models/scenarios.py,sha256=6pTfMusCQ6bwPUUFR_Wi1PUk3ZgCwG21uoTy667mZaM,3928
|
|
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=
|
|
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=
|
|
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.
|
|
44
|
-
pyconvexity-0.3.8.
|
|
45
|
-
pyconvexity-0.3.8.
|
|
46
|
-
pyconvexity-0.3.8.
|
|
46
|
+
pyconvexity-0.3.8.post6.dist-info/METADATA,sha256=fKPUW65mNFxOCENHZuic30W1bbph8wORCzM56uFoP-U,4886
|
|
47
|
+
pyconvexity-0.3.8.post6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
48
|
+
pyconvexity-0.3.8.post6.dist-info/top_level.txt,sha256=wFPEDXVaebR3JO5Tt3HNse-ws5aROCcxEco15d6j64s,12
|
|
49
|
+
pyconvexity-0.3.8.post6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|