pyconvexity 0.3.8.post4__tar.gz → 0.3.8.post6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyconvexity might be problematic. Click here for more details.

Files changed (58) hide show
  1. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/PKG-INFO +1 -1
  2. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/pyproject.toml +2 -2
  3. pyconvexity-0.3.8.post6/src/pyconvexity/_version.py +1 -0
  4. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/data/schema/01_core_schema.sql +51 -0
  5. pyconvexity-0.3.8.post6/src/pyconvexity/data/schema/migrate_add_geometries.sql +73 -0
  6. pyconvexity-0.3.8.post6/src/pyconvexity/models/__init__.py +87 -0
  7. pyconvexity-0.3.8.post6/src/pyconvexity/models/carriers.py +156 -0
  8. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/models/components.py +120 -0
  9. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/models/network.py +67 -1
  10. pyconvexity-0.3.8.post6/src/pyconvexity/models/results.py +138 -0
  11. pyconvexity-0.3.8.post6/src/pyconvexity/models/scenarios.py +160 -0
  12. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/solvers/pypsa/api.py +6 -1
  13. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/solvers/pypsa/solver.py +20 -74
  14. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity.egg-info/PKG-INFO +1 -1
  15. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity.egg-info/SOURCES.txt +3 -0
  16. pyconvexity-0.3.8.post4/src/pyconvexity/_version.py +0 -1
  17. pyconvexity-0.3.8.post4/src/pyconvexity/models/__init__.py +0 -43
  18. pyconvexity-0.3.8.post4/src/pyconvexity/models/scenarios.py +0 -177
  19. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/README.md +0 -0
  20. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/setup.cfg +0 -0
  21. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/__init__.py +0 -0
  22. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/core/__init__.py +0 -0
  23. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/core/database.py +0 -0
  24. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/core/errors.py +0 -0
  25. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/core/types.py +0 -0
  26. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/data/README.md +0 -0
  27. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/data/__init__.py +0 -0
  28. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/data/__pycache__/__init__.cpython-313.pyc +0 -0
  29. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/data/loaders/__init__.py +0 -0
  30. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/data/loaders/__pycache__/__init__.cpython-313.pyc +0 -0
  31. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/data/loaders/__pycache__/cache.cpython-313.pyc +0 -0
  32. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/data/loaders/cache.py +0 -0
  33. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/data/schema/02_data_metadata.sql +0 -0
  34. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/data/schema/03_validation_data.sql +0 -0
  35. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/data/schema/04_scenario_schema.sql +0 -0
  36. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/data/sources/__init__.py +0 -0
  37. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/data/sources/__pycache__/__init__.cpython-313.pyc +0 -0
  38. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/data/sources/__pycache__/gem.cpython-313.pyc +0 -0
  39. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/data/sources/gem.py +0 -0
  40. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/io/__init__.py +0 -0
  41. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/io/excel_exporter.py +0 -0
  42. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/io/excel_importer.py +0 -0
  43. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/io/netcdf_exporter.py +0 -0
  44. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/io/netcdf_importer.py +0 -0
  45. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/models/attributes.py +0 -0
  46. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/solvers/__init__.py +0 -0
  47. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/solvers/pypsa/__init__.py +0 -0
  48. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/solvers/pypsa/batch_loader.py +0 -0
  49. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/solvers/pypsa/builder.py +0 -0
  50. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/solvers/pypsa/constraints.py +0 -0
  51. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/solvers/pypsa/storage.py +0 -0
  52. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/timeseries.py +0 -0
  53. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/validation/__init__.py +0 -0
  54. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity/validation/rules.py +0 -0
  55. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity.egg-info/dependency_links.txt +0 -0
  56. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity.egg-info/requires.txt +0 -0
  57. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/src/pyconvexity.egg-info/top_level.txt +0 -0
  58. {pyconvexity-0.3.8.post4 → pyconvexity-0.3.8.post6}/tests/test_core_types.py +0 -0
@@ -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.post6
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyconvexity"
7
- version = "0.3.8post4"
7
+ version = "0.3.8post6"
8
8
  description = "Python library for energy system modeling and optimization with PyPSA"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -81,7 +81,7 @@ profile = "black"
81
81
  line_length = 100
82
82
 
83
83
  [tool.mypy]
84
- python_version = "0.3.8post4"
84
+ python_version = "0.3.8post6"
85
85
  warn_return_any = true
86
86
  warn_unused_configs = true
87
87
  disallow_untyped_defs = true
@@ -0,0 +1 @@
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;
@@ -0,0 +1,87 @@
1
+ """
2
+ Model management module for PyConvexity.
3
+
4
+ Contains high-level operations for networks, components, and attributes.
5
+ """
6
+
7
+ from pyconvexity.models.components import (
8
+ get_component_type, get_component, list_components_by_type,
9
+ insert_component, create_component, update_component, delete_component,
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
12
+ )
13
+
14
+ from pyconvexity.models.attributes import (
15
+ set_static_attribute, set_timeseries_attribute, get_attribute, delete_attribute
16
+ )
17
+
18
+ from pyconvexity.models.network import (
19
+ create_network, get_network_info, get_network_time_periods, list_networks,
20
+ create_carrier, list_carriers, get_network_config, set_network_config,
21
+ get_component_counts, get_master_scenario_id, resolve_scenario_id,
22
+ get_first_network, get_network_by_name
23
+ )
24
+
25
+ # Import from new modules
26
+ from pyconvexity.models.scenarios import (
27
+ list_scenarios as list_scenarios_new,
28
+ get_scenario_by_name, get_scenario_by_id, get_master_scenario
29
+ )
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
+
59
+ __all__ = [
60
+ # Component operations
61
+ "get_component_type", "get_component", "list_components_by_type",
62
+ "insert_component", "create_component", "update_component", "delete_component",
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",
65
+
66
+ # Attribute operations
67
+ "set_static_attribute", "set_timeseries_attribute", "get_attribute", "delete_attribute",
68
+
69
+ # Network operations
70
+ "create_network", "get_network_info", "get_network_time_periods", "list_networks",
71
+ "create_carrier", "list_carriers", "get_network_config", "set_network_config",
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",
78
+
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",
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