pyconvexity 0.4.6__tar.gz → 0.4.8__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 (51) hide show
  1. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/PKG-INFO +1 -1
  2. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/pyproject.toml +2 -2
  3. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/__init__.py +15 -0
  4. pyconvexity-0.4.8/src/pyconvexity/_version.py +1 -0
  5. pyconvexity-0.4.8/src/pyconvexity/dashboard.py +265 -0
  6. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/data/schema/01_core_schema.sql +1 -1
  7. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/data/schema/03_validation_data.sql +2 -2
  8. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/io/netcdf_exporter.py +1 -7
  9. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/io/netcdf_importer.py +82 -113
  10. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/solvers/pypsa/__init__.py +6 -0
  11. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/solvers/pypsa/api.py +4 -18
  12. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/solvers/pypsa/batch_loader.py +1 -12
  13. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/solvers/pypsa/builder.py +3 -23
  14. pyconvexity-0.4.8/src/pyconvexity/solvers/pypsa/clearing_price.py +678 -0
  15. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/solvers/pypsa/solver.py +4 -71
  16. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/solvers/pypsa/storage.py +95 -47
  17. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity.egg-info/PKG-INFO +1 -1
  18. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity.egg-info/SOURCES.txt +2 -0
  19. pyconvexity-0.4.6/src/pyconvexity/_version.py +0 -1
  20. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/README.md +0 -0
  21. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/setup.cfg +0 -0
  22. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/core/__init__.py +0 -0
  23. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/core/database.py +0 -0
  24. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/core/errors.py +0 -0
  25. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/core/types.py +0 -0
  26. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/data/README.md +0 -0
  27. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/data/__init__.py +0 -0
  28. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/data/loaders/__init__.py +0 -0
  29. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/data/loaders/cache.py +0 -0
  30. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/data/schema/02_data_metadata.sql +0 -0
  31. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/data/sources/__init__.py +0 -0
  32. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/data/sources/gem.py +0 -0
  33. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/io/__init__.py +0 -0
  34. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/io/excel_exporter.py +0 -0
  35. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/io/excel_importer.py +0 -0
  36. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/models/__init__.py +0 -0
  37. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/models/attributes.py +0 -0
  38. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/models/carriers.py +0 -0
  39. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/models/components.py +0 -0
  40. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/models/network.py +0 -0
  41. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/models/results.py +0 -0
  42. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/models/scenarios.py +0 -0
  43. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/solvers/__init__.py +0 -0
  44. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/solvers/pypsa/constraints.py +0 -0
  45. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/timeseries.py +0 -0
  46. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/validation/__init__.py +0 -0
  47. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity/validation/rules.py +0 -0
  48. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity.egg-info/dependency_links.txt +0 -0
  49. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity.egg-info/requires.txt +0 -0
  50. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/src/pyconvexity.egg-info/top_level.txt +0 -0
  51. {pyconvexity-0.4.6 → pyconvexity-0.4.8}/tests/test_core_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyconvexity
3
- Version: 0.4.6
3
+ Version: 0.4.8
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.4.6"
7
+ version = "0.4.8"
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 = 88
82
82
 
83
83
  [tool.mypy]
84
- python_version = "0.4.6"
84
+ python_version = "0.4.8"
85
85
  warn_return_any = true
86
86
  warn_unused_configs = true
87
87
  disallow_untyped_defs = true
@@ -96,6 +96,15 @@ from pyconvexity.timeseries import (
96
96
  numpy_to_timeseries,
97
97
  )
98
98
 
99
+ # Dashboard configuration for Convexity app
100
+ from pyconvexity.dashboard import (
101
+ DashboardConfig,
102
+ set_dashboard_config,
103
+ get_dashboard_config,
104
+ clear_dashboard_config,
105
+ auto_layout,
106
+ )
107
+
99
108
  # High-level API functions
100
109
  __all__ = [
101
110
  # Version info
@@ -171,6 +180,12 @@ __all__ = [
171
180
  "get_multiple_timeseries",
172
181
  "timeseries_to_numpy",
173
182
  "numpy_to_timeseries",
183
+ # Dashboard configuration
184
+ "DashboardConfig",
185
+ "set_dashboard_config",
186
+ "get_dashboard_config",
187
+ "clear_dashboard_config",
188
+ "auto_layout",
174
189
  ]
175
190
 
176
191
  # Data module imports
@@ -0,0 +1 @@
1
+ __version__ = "0.4.8"
@@ -0,0 +1,265 @@
1
+ """
2
+ Dashboard configuration for Convexity app visualization.
3
+
4
+ Allows programmatic configuration of the analytics dashboard layout
5
+ that will be displayed when the model is loaded in the Convexity app.
6
+
7
+ Example:
8
+ >>> from pyconvexity.dashboard import set_dashboard_config, DashboardConfig, auto_layout
9
+ >>>
10
+ >>> charts = [
11
+ ... {
12
+ ... "id": "dispatch-1",
13
+ ... "title": "Generation by Carrier",
14
+ ... "visible": True,
15
+ ... "view": {
16
+ ... "timeseries": {
17
+ ... "component": "Generator",
18
+ ... "attribute": "p",
19
+ ... "group_by": "carrier"
20
+ ... }
21
+ ... }
22
+ ... },
23
+ ... {
24
+ ... "id": "lmp-1",
25
+ ... "title": "Locational Marginal Prices",
26
+ ... "visible": True,
27
+ ... "view": {
28
+ ... "timeseries": {
29
+ ... "component": "Bus",
30
+ ... "attribute": "marginal_price"
31
+ ... }
32
+ ... }
33
+ ... }
34
+ ... ]
35
+ >>>
36
+ >>> config = DashboardConfig(charts=charts, layout=auto_layout(charts))
37
+ >>> set_dashboard_config(conn, config)
38
+ """
39
+
40
+ import json
41
+ import sqlite3
42
+ from dataclasses import dataclass, field, asdict
43
+ from typing import Any, Dict, List, Optional
44
+
45
+
46
+ @dataclass
47
+ class DashboardConfig:
48
+ """
49
+ Dashboard configuration for the Convexity app analytics view.
50
+
51
+ Attributes:
52
+ charts: List of chart configurations. Each chart is a dict with:
53
+ - id: Unique identifier (e.g., "dispatch-1", "lmp-main")
54
+ - title: Display title
55
+ - visible: Whether chart is shown
56
+ - view: Chart type configuration (see below)
57
+ - filters: Optional chart-specific filters
58
+
59
+ layout: List of layout positions. Each position is a dict with:
60
+ - i: Chart ID (must match a chart's id)
61
+ - x: Grid column (0-11)
62
+ - y: Grid row
63
+ - w: Width in grid units (max 12)
64
+ - h: Height in grid units
65
+
66
+ selected_scenario_id: Pre-selected scenario ID (optional)
67
+ selected_ensemble_name: Pre-selected ensemble name (optional)
68
+ selected_bus_id: Pre-selected bus ID for filtering (optional)
69
+
70
+ Chart View Types:
71
+ Timeseries (dispatch, LMP, etc.):
72
+ {"timeseries": {"component": "Generator", "attribute": "p", "group_by": "carrier"}}
73
+ {"timeseries": {"component": "Bus", "attribute": "marginal_price"}}
74
+ {"timeseries": {"component": "Load", "attribute": "p"}}
75
+
76
+ Network map:
77
+ {"network": {"network": true}}
78
+
79
+ Statistics/Summary:
80
+ {"statistic": {"statistic": "optimal_capacity", "metric": "capacity"}}
81
+ {"statistic": {"statistic": "total_cost"}}
82
+ """
83
+ charts: List[Dict[str, Any]]
84
+ layout: List[Dict[str, Any]]
85
+ selected_scenario_id: Optional[int] = None
86
+ selected_ensemble_name: Optional[str] = None
87
+ selected_bus_id: Optional[int] = None
88
+
89
+ def to_dict(self) -> Dict[str, Any]:
90
+ """Convert to dictionary for JSON serialization."""
91
+ result = {
92
+ "charts": self.charts,
93
+ "layout": self.layout,
94
+ }
95
+ # Only include optional fields if set (matches Rust's skip_serializing_if)
96
+ if self.selected_scenario_id is not None:
97
+ result["selected_scenario_id"] = self.selected_scenario_id
98
+ if self.selected_ensemble_name is not None:
99
+ result["selected_ensemble_name"] = self.selected_ensemble_name
100
+ if self.selected_bus_id is not None:
101
+ result["selected_bus_id"] = self.selected_bus_id
102
+ return result
103
+
104
+
105
+ def set_dashboard_config(conn: sqlite3.Connection, config: DashboardConfig) -> int:
106
+ """
107
+ Save dashboard configuration to the database.
108
+
109
+ This configuration will be loaded by the Convexity app when the model
110
+ is opened, setting up the analytics dashboard with the specified charts
111
+ and layout.
112
+
113
+ Args:
114
+ conn: Database connection
115
+ config: Dashboard configuration
116
+
117
+ Returns:
118
+ Row ID of the stored configuration
119
+
120
+ Example:
121
+ >>> config = DashboardConfig(
122
+ ... charts=[{"id": "dispatch-1", "title": "Dispatch", "visible": True,
123
+ ... "view": {"timeseries": {"component": "Generator", "attribute": "p", "group_by": "carrier"}}}],
124
+ ... layout=[{"i": "dispatch-1", "x": 0, "y": 0, "w": 12, "h": 40}]
125
+ ... )
126
+ >>> set_dashboard_config(conn, config)
127
+ """
128
+ data_json = json.dumps(config.to_dict())
129
+ data_bytes = data_json.encode('utf-8')
130
+
131
+ # Check if analytics config exists
132
+ cursor = conn.execute(
133
+ "SELECT id FROM network_data_store WHERE category = 'analytics_view' AND name = 'default'"
134
+ )
135
+ row = cursor.fetchone()
136
+
137
+ if row:
138
+ # Update existing
139
+ row_id = row[0]
140
+ conn.execute(
141
+ "UPDATE network_data_store SET data = ?, updated_at = datetime('now') WHERE id = ?",
142
+ (data_bytes, row_id)
143
+ )
144
+ else:
145
+ # Insert new
146
+ conn.execute(
147
+ """INSERT INTO network_data_store (category, name, data_format, data, created_at, updated_at)
148
+ VALUES ('analytics_view', 'default', 'json', ?, datetime('now'), datetime('now'))""",
149
+ (data_bytes,)
150
+ )
151
+ row_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
152
+
153
+ conn.commit()
154
+ return row_id
155
+
156
+
157
+ def get_dashboard_config(conn: sqlite3.Connection) -> Optional[DashboardConfig]:
158
+ """
159
+ Get the current dashboard configuration from the database.
160
+
161
+ Args:
162
+ conn: Database connection
163
+
164
+ Returns:
165
+ DashboardConfig if one exists, None otherwise
166
+ """
167
+ cursor = conn.execute(
168
+ """SELECT data FROM network_data_store
169
+ WHERE category = 'analytics_view'
170
+ ORDER BY updated_at DESC LIMIT 1"""
171
+ )
172
+ row = cursor.fetchone()
173
+
174
+ if not row:
175
+ return None
176
+
177
+ data_bytes = row[0]
178
+ if isinstance(data_bytes, bytes):
179
+ data_str = data_bytes.decode('utf-8')
180
+ else:
181
+ data_str = data_bytes
182
+
183
+ data = json.loads(data_str)
184
+
185
+ return DashboardConfig(
186
+ charts=data.get("charts", []),
187
+ layout=data.get("layout", []),
188
+ selected_scenario_id=data.get("selected_scenario_id"),
189
+ selected_ensemble_name=data.get("selected_ensemble_name"),
190
+ selected_bus_id=data.get("selected_bus_id"),
191
+ )
192
+
193
+
194
+ def auto_layout(charts: List[Dict[str, Any]], cols: int = 12) -> List[Dict[str, Any]]:
195
+ """
196
+ Automatically generate layout positions for charts.
197
+
198
+ Places charts in a vertical stack, each taking full width.
199
+ Timeseries charts get height 40, others get height 20.
200
+
201
+ Args:
202
+ charts: List of chart configurations
203
+ cols: Grid columns (default 12)
204
+
205
+ Returns:
206
+ List of layout position dicts
207
+
208
+ Example:
209
+ >>> charts = [
210
+ ... {"id": "dispatch-1", "title": "Dispatch", "visible": True,
211
+ ... "view": {"timeseries": {...}}},
212
+ ... {"id": "network-1", "title": "Network", "visible": True,
213
+ ... "view": {"network": {"network": True}}}
214
+ ... ]
215
+ >>> layout = auto_layout(charts)
216
+ >>> # Returns: [{"i": "dispatch-1", "x": 0, "y": 0, "w": 12, "h": 40}, ...]
217
+ """
218
+ layout = []
219
+ y = 0
220
+
221
+ for chart in charts:
222
+ if not chart.get("visible", True):
223
+ continue
224
+
225
+ chart_id = chart["id"]
226
+ view = chart.get("view", {})
227
+
228
+ # Determine chart dimensions based on type
229
+ if "timeseries" in view:
230
+ w, h = 12, 40
231
+ elif "network" in view:
232
+ w, h = 6, 40
233
+ elif "statistic" in view:
234
+ w, h = 6, 20
235
+ else:
236
+ w, h = 12, 20
237
+
238
+ layout.append({
239
+ "i": chart_id,
240
+ "x": 0,
241
+ "y": y,
242
+ "w": w,
243
+ "h": h,
244
+ })
245
+
246
+ y += h
247
+
248
+ return layout
249
+
250
+
251
+ def clear_dashboard_config(conn: sqlite3.Connection) -> bool:
252
+ """
253
+ Remove any existing dashboard configuration.
254
+
255
+ Args:
256
+ conn: Database connection
257
+
258
+ Returns:
259
+ True if a config was deleted, False if none existed
260
+ """
261
+ cursor = conn.execute(
262
+ "DELETE FROM network_data_store WHERE category = 'analytics_view' AND name = 'default'"
263
+ )
264
+ conn.commit()
265
+ return cursor.rowcount > 0
@@ -54,7 +54,7 @@ CREATE TABLE carriers (
54
54
  nice_name TEXT, -- Display name
55
55
  max_growth REAL DEFAULT NULL, -- MW - can be infinite
56
56
  max_relative_growth REAL DEFAULT 0.0, -- MW
57
- curtailable BOOLEAN DEFAULT FALSE, -- Whether the carrier can be curtailed
57
+ curtailable BOOLEAN DEFAULT TRUE, -- Whether the carrier can be curtailed
58
58
 
59
59
  -- Metadata
60
60
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -33,8 +33,8 @@ INSERT INTO attribute_validation_rules (component_type, attribute_name, display_
33
33
  ('BUS', 'q', 'Reactive Power', 'float', 'MVar', '0', 'timeseries', FALSE, FALSE, 'reactive power (positive if net generation at bus)', NULL, NULL, 'electrical', TRUE),
34
34
  ('BUS', 'v_mag_pu', 'Voltage Magnitude', 'float', 'per unit', '1', 'timeseries', FALSE, FALSE, 'Voltage magnitude, per unit of v_nom', NULL, NULL, 'electrical', TRUE),
35
35
  ('BUS', 'v_ang', 'Voltage Angle', 'float', 'radians', '0', 'timeseries', FALSE, FALSE, 'Voltage angle', NULL, NULL, 'electrical', TRUE),
36
- ('BUS', 'marginal_price', 'Marginal Price', 'float', 'currency/MWh', '0', 'timeseries', FALSE, FALSE, 'Locational marginal price from LOPF from power balance constraint', NULL, NULL, 'costs', TRUE),
37
- ('BUS', 'market_price', 'Market Price', 'float', 'currency/MWh', '0', 'timeseries', FALSE, FALSE, 'Custom calculated market price for the bus (independent of PyPSA marginal cost)', NULL, NULL, 'costs', TRUE);
36
+ ('BUS', 'marginal_price', 'Marginal Price', 'float', 'currency/MWh', '0', 'timeseries', FALSE, FALSE, 'Locational marginal price from LOPF from power balance constraint (shadow price). Includes effects of UC constraints, ramping limits, and other binding constraints.', NULL, NULL, 'costs', TRUE),
37
+ ('BUS', 'clearing_price', 'Clearing Price', 'float', 'currency/MWh', '0', 'timeseries', FALSE, FALSE, 'Pay-as-clear price: marginal cost of the cheapest available source (generator, storage, or import via uncongested link) with spare capacity. Calculated as min of: (1) local generators/storage with spare capacity, (2) adjacent bus local marginal + link cost adjusted for efficiency, for uncongested links, (3) unmet load price in scarcity. Differs from marginal_price which is the LP shadow price.', NULL, NULL, 'costs', TRUE);
38
38
 
39
39
  -- ============================================================================
40
40
  -- GENERATOR ATTRIBUTES
@@ -3,22 +3,18 @@ NetCDF exporter for PyConvexity energy system models.
3
3
  Exports networks to PyPSA NetCDF format using existing PyPSA infrastructure.
4
4
  """
5
5
 
6
- import logging
7
6
  from typing import Dict, Any, Optional, Callable
8
7
  from pathlib import Path
9
8
 
10
9
  # Import existing PyPSA functionality from pyconvexity
11
- from pyconvexity.core.database import open_connection
12
10
  from pyconvexity.solvers.pypsa import build_pypsa_network
13
11
 
14
- logger = logging.getLogger(__name__)
15
-
16
12
 
17
13
  class NetCDFModelExporter:
18
14
  """Export network model to PyPSA NetCDF format"""
19
15
 
20
16
  def __init__(self):
21
- self.logger = logging.getLogger(__name__)
17
+ pass
22
18
 
23
19
  def export_to_netcdf(
24
20
  self,
@@ -78,7 +74,6 @@ class NetCDFModelExporter:
78
74
  }
79
75
 
80
76
  except Exception as e:
81
- self.logger.error(f"NetCDF export failed: {e}", exc_info=True)
82
77
  if progress_callback:
83
78
  progress_callback(None, f"Export failed: {str(e)}")
84
79
  raise
@@ -141,7 +136,6 @@ class NetCDFModelExporter:
141
136
  }
142
137
 
143
138
  except Exception as e:
144
- self.logger.error(f"CSV export failed: {e}", exc_info=True)
145
139
  if progress_callback:
146
140
  progress_callback(None, f"Export failed: {str(e)}")
147
141
  raise