pyconvexity 0.4.8__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/__init__.py +241 -0
- pyconvexity/_version.py +1 -0
- pyconvexity/core/__init__.py +60 -0
- pyconvexity/core/database.py +485 -0
- pyconvexity/core/errors.py +106 -0
- pyconvexity/core/types.py +400 -0
- pyconvexity/dashboard.py +265 -0
- pyconvexity/data/README.md +101 -0
- pyconvexity/data/__init__.py +17 -0
- pyconvexity/data/loaders/__init__.py +3 -0
- pyconvexity/data/loaders/cache.py +213 -0
- pyconvexity/data/schema/01_core_schema.sql +420 -0
- pyconvexity/data/schema/02_data_metadata.sql +120 -0
- pyconvexity/data/schema/03_validation_data.sql +507 -0
- pyconvexity/data/sources/__init__.py +5 -0
- pyconvexity/data/sources/gem.py +442 -0
- pyconvexity/io/__init__.py +26 -0
- pyconvexity/io/excel_exporter.py +1226 -0
- pyconvexity/io/excel_importer.py +1381 -0
- pyconvexity/io/netcdf_exporter.py +191 -0
- pyconvexity/io/netcdf_importer.py +1802 -0
- pyconvexity/models/__init__.py +195 -0
- pyconvexity/models/attributes.py +730 -0
- pyconvexity/models/carriers.py +159 -0
- pyconvexity/models/components.py +611 -0
- pyconvexity/models/network.py +503 -0
- pyconvexity/models/results.py +148 -0
- pyconvexity/models/scenarios.py +234 -0
- pyconvexity/solvers/__init__.py +29 -0
- pyconvexity/solvers/pypsa/__init__.py +30 -0
- pyconvexity/solvers/pypsa/api.py +446 -0
- pyconvexity/solvers/pypsa/batch_loader.py +296 -0
- pyconvexity/solvers/pypsa/builder.py +655 -0
- pyconvexity/solvers/pypsa/clearing_price.py +678 -0
- pyconvexity/solvers/pypsa/constraints.py +405 -0
- pyconvexity/solvers/pypsa/solver.py +1442 -0
- pyconvexity/solvers/pypsa/storage.py +2096 -0
- pyconvexity/timeseries.py +330 -0
- pyconvexity/validation/__init__.py +25 -0
- pyconvexity/validation/rules.py +312 -0
- pyconvexity-0.4.8.dist-info/METADATA +148 -0
- pyconvexity-0.4.8.dist-info/RECORD +44 -0
- pyconvexity-0.4.8.dist-info/WHEEL +5 -0
- pyconvexity-0.4.8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-level timeseries API for PyConvexity.
|
|
3
|
+
|
|
4
|
+
This module provides the main interface for working with timeseries data,
|
|
5
|
+
matching the efficient patterns used in the Rust implementation.
|
|
6
|
+
|
|
7
|
+
Key Features:
|
|
8
|
+
- Ultra-fast binary serialization (matches Rust exactly)
|
|
9
|
+
- Array-based data structures for maximum performance
|
|
10
|
+
- Unified API for getting/setting timeseries data
|
|
11
|
+
- Backward compatibility with legacy point-based format
|
|
12
|
+
- Efficient sampling and filtering operations
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import sqlite3
|
|
16
|
+
from typing import List, Optional, Union
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from pyconvexity.core.database import database_context
|
|
20
|
+
from pyconvexity.core.types import Timeseries, TimeseriesMetadata
|
|
21
|
+
from pyconvexity.models.attributes import (
|
|
22
|
+
get_timeseries as _get_timeseries,
|
|
23
|
+
get_timeseries_metadata as _get_timeseries_metadata,
|
|
24
|
+
set_timeseries_attribute,
|
|
25
|
+
serialize_values_to_binary,
|
|
26
|
+
deserialize_values_from_binary,
|
|
27
|
+
get_timeseries_length_from_binary,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ============================================================================
|
|
32
|
+
# HIGH-LEVEL TIMESERIES API
|
|
33
|
+
# ============================================================================
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_timeseries(
|
|
37
|
+
db_path: str,
|
|
38
|
+
component_id: int,
|
|
39
|
+
attribute_name: str,
|
|
40
|
+
scenario_id: Optional[int] = None,
|
|
41
|
+
start_index: Optional[int] = None,
|
|
42
|
+
end_index: Optional[int] = None,
|
|
43
|
+
max_points: Optional[int] = None,
|
|
44
|
+
) -> Timeseries:
|
|
45
|
+
"""
|
|
46
|
+
Get timeseries data with efficient array-based format.
|
|
47
|
+
|
|
48
|
+
This is the main function for retrieving timeseries data. It returns
|
|
49
|
+
a Timeseries object with values as a flat array for maximum performance.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
db_path: Path to the database file
|
|
53
|
+
component_id: Component ID
|
|
54
|
+
attribute_name: Name of the attribute (e.g., 'p', 'p_set', 'marginal_cost')
|
|
55
|
+
scenario_id: Scenario ID (uses master scenario if None)
|
|
56
|
+
start_index: Start index for range queries (optional)
|
|
57
|
+
end_index: End index for range queries (optional)
|
|
58
|
+
max_points: Maximum number of points for sampling (optional)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Timeseries object with efficient array-based data
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
>>> ts = get_timeseries("model.db", component_id=123, attribute_name="p")
|
|
65
|
+
>>> print(f"Length: {ts.length}, Values: {ts.values[:5]}")
|
|
66
|
+
Length: 8760, Values: [100.5, 95.2, 87.3, 92.1, 88.7]
|
|
67
|
+
|
|
68
|
+
# Get a subset of the data
|
|
69
|
+
>>> ts_subset = get_timeseries("model.db", 123, "p", start_index=100, end_index=200)
|
|
70
|
+
>>> print(f"Subset length: {ts_subset.length}")
|
|
71
|
+
Subset length: 100
|
|
72
|
+
|
|
73
|
+
# Sample large datasets
|
|
74
|
+
>>> ts_sampled = get_timeseries("model.db", 123, "p", max_points=1000)
|
|
75
|
+
>>> print(f"Sampled from {ts.length} to {ts_sampled.length} points")
|
|
76
|
+
"""
|
|
77
|
+
with database_context(db_path, read_only=True) as conn:
|
|
78
|
+
return _get_timeseries(
|
|
79
|
+
conn,
|
|
80
|
+
component_id,
|
|
81
|
+
attribute_name,
|
|
82
|
+
scenario_id,
|
|
83
|
+
start_index,
|
|
84
|
+
end_index,
|
|
85
|
+
max_points,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_timeseries_metadata(
|
|
90
|
+
db_path: str,
|
|
91
|
+
component_id: int,
|
|
92
|
+
attribute_name: str,
|
|
93
|
+
scenario_id: Optional[int] = None,
|
|
94
|
+
) -> TimeseriesMetadata:
|
|
95
|
+
"""
|
|
96
|
+
Get timeseries metadata without loading the full data.
|
|
97
|
+
|
|
98
|
+
This is useful for checking the size and properties of a timeseries
|
|
99
|
+
before deciding whether to load the full data.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
db_path: Path to the database file
|
|
103
|
+
component_id: Component ID
|
|
104
|
+
attribute_name: Name of the attribute
|
|
105
|
+
scenario_id: Scenario ID (uses master scenario if None)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
TimeseriesMetadata with length and type information
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
>>> meta = get_timeseries_metadata("model.db", 123, "p")
|
|
112
|
+
>>> print(f"Length: {meta.length}, Type: {meta.data_type}, Unit: {meta.unit}")
|
|
113
|
+
Length: 8760, Type: float, Unit: MW
|
|
114
|
+
"""
|
|
115
|
+
with database_context(db_path, read_only=True) as conn:
|
|
116
|
+
return _get_timeseries_metadata(conn, component_id, attribute_name, scenario_id)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def set_timeseries(
|
|
120
|
+
db_path: str,
|
|
121
|
+
component_id: int,
|
|
122
|
+
attribute_name: str,
|
|
123
|
+
values: Union[List[float], np.ndarray, Timeseries],
|
|
124
|
+
scenario_id: Optional[int] = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Set timeseries data using efficient array-based format.
|
|
128
|
+
|
|
129
|
+
This is the main function for storing timeseries data. It accepts
|
|
130
|
+
various input formats and stores them efficiently in the database.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
db_path: Path to the database file
|
|
134
|
+
component_id: Component ID
|
|
135
|
+
attribute_name: Name of the attribute
|
|
136
|
+
values: Timeseries values as list, numpy array, or Timeseries object
|
|
137
|
+
scenario_id: Scenario ID (uses master scenario if None)
|
|
138
|
+
|
|
139
|
+
Example:
|
|
140
|
+
# Set from a list
|
|
141
|
+
>>> values = [100.5, 95.2, 87.3, 92.1, 88.7]
|
|
142
|
+
>>> set_timeseries("model.db", 123, "p_set", values)
|
|
143
|
+
|
|
144
|
+
# Set from numpy array
|
|
145
|
+
>>> import numpy as np
|
|
146
|
+
>>> values = np.random.normal(100, 10, 8760) # Hourly data for a year
|
|
147
|
+
>>> set_timeseries("model.db", 123, "p_max_pu", values)
|
|
148
|
+
|
|
149
|
+
# Set from existing Timeseries object
|
|
150
|
+
>>> ts = get_timeseries("model.db", 456, "p")
|
|
151
|
+
>>> set_timeseries("model.db", 123, "p_set", ts)
|
|
152
|
+
"""
|
|
153
|
+
# Convert input to list of floats
|
|
154
|
+
if isinstance(values, Timeseries):
|
|
155
|
+
values_list = values.values
|
|
156
|
+
elif isinstance(values, np.ndarray):
|
|
157
|
+
values_list = values.tolist()
|
|
158
|
+
elif isinstance(values, list):
|
|
159
|
+
values_list = [float(v) for v in values]
|
|
160
|
+
else:
|
|
161
|
+
raise ValueError("values must be List[float], numpy.ndarray, or Timeseries")
|
|
162
|
+
|
|
163
|
+
with database_context(db_path) as conn:
|
|
164
|
+
set_timeseries_attribute(
|
|
165
|
+
conn, component_id, attribute_name, values_list, scenario_id
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_multiple_timeseries(
|
|
170
|
+
db_path: str, requests: List[dict], max_points: Optional[int] = None
|
|
171
|
+
) -> List[Timeseries]:
|
|
172
|
+
"""
|
|
173
|
+
Get multiple timeseries efficiently in a single database connection.
|
|
174
|
+
|
|
175
|
+
This is more efficient than calling get_timeseries multiple times
|
|
176
|
+
when you need to load many timeseries from the same database.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
db_path: Path to the database file
|
|
180
|
+
requests: List of dicts with keys: component_id, attribute_name, scenario_id (optional)
|
|
181
|
+
max_points: Maximum number of points for sampling (applied to all)
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
List of Timeseries objects in the same order as requests
|
|
185
|
+
|
|
186
|
+
Example:
|
|
187
|
+
>>> requests = [
|
|
188
|
+
... {"component_id": 123, "attribute_name": "p"},
|
|
189
|
+
... {"component_id": 124, "attribute_name": "p"},
|
|
190
|
+
... {"component_id": 125, "attribute_name": "p", "scenario_id": 2}
|
|
191
|
+
... ]
|
|
192
|
+
>>> timeseries_list = get_multiple_timeseries("model.db", requests)
|
|
193
|
+
>>> print(f"Loaded {len(timeseries_list)} timeseries")
|
|
194
|
+
"""
|
|
195
|
+
results = []
|
|
196
|
+
|
|
197
|
+
with database_context(db_path, read_only=True) as conn:
|
|
198
|
+
for request in requests:
|
|
199
|
+
component_id = request["component_id"]
|
|
200
|
+
attribute_name = request["attribute_name"]
|
|
201
|
+
scenario_id = request.get("scenario_id")
|
|
202
|
+
|
|
203
|
+
ts = _get_timeseries(
|
|
204
|
+
conn, component_id, attribute_name, scenario_id, None, None, max_points
|
|
205
|
+
)
|
|
206
|
+
results.append(ts)
|
|
207
|
+
|
|
208
|
+
return results
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ============================================================================
|
|
212
|
+
# UTILITY FUNCTIONS
|
|
213
|
+
# ============================================================================
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def timeseries_to_numpy(timeseries: Timeseries) -> np.ndarray:
|
|
217
|
+
"""
|
|
218
|
+
Convert Timeseries to numpy array for scientific computing.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
timeseries: Timeseries object
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
numpy array with float32 dtype for memory efficiency
|
|
225
|
+
|
|
226
|
+
Example:
|
|
227
|
+
>>> ts = get_timeseries("model.db", 123, "p")
|
|
228
|
+
>>> arr = timeseries_to_numpy(ts)
|
|
229
|
+
>>> print(f"Mean: {arr.mean():.2f}, Std: {arr.std():.2f}")
|
|
230
|
+
"""
|
|
231
|
+
return np.array(timeseries.values, dtype=np.float32)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def numpy_to_timeseries(
|
|
235
|
+
array: np.ndarray,
|
|
236
|
+
data_type: str = "float",
|
|
237
|
+
unit: Optional[str] = None,
|
|
238
|
+
is_input: bool = True,
|
|
239
|
+
) -> Timeseries:
|
|
240
|
+
"""
|
|
241
|
+
Convert numpy array to Timeseries object.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
array: numpy array of values
|
|
245
|
+
data_type: Data type string (default: "float")
|
|
246
|
+
unit: Unit string (optional)
|
|
247
|
+
is_input: Whether this is input data (default: True)
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Timeseries object
|
|
251
|
+
|
|
252
|
+
Example:
|
|
253
|
+
>>> import numpy as np
|
|
254
|
+
>>> arr = np.random.normal(100, 10, 8760)
|
|
255
|
+
>>> ts = numpy_to_timeseries(arr, unit="MW")
|
|
256
|
+
>>> print(f"Created timeseries with {ts.length} points")
|
|
257
|
+
"""
|
|
258
|
+
values = array.tolist() if hasattr(array, "tolist") else list(array)
|
|
259
|
+
return Timeseries(
|
|
260
|
+
values=[float(v) for v in values],
|
|
261
|
+
length=len(values),
|
|
262
|
+
start_index=0,
|
|
263
|
+
data_type=data_type,
|
|
264
|
+
unit=unit,
|
|
265
|
+
is_input=is_input,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def validate_timeseries_alignment(
|
|
270
|
+
db_path: str, values: Union[List[float], np.ndarray, Timeseries]
|
|
271
|
+
) -> dict:
|
|
272
|
+
"""
|
|
273
|
+
Validate that timeseries data aligns with network time periods.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
db_path: Path to the database file
|
|
277
|
+
values: Timeseries values to validate
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Dictionary with validation results
|
|
281
|
+
|
|
282
|
+
Example:
|
|
283
|
+
>>> values = [100.0] * 8760 # Hourly data for a year
|
|
284
|
+
>>> result = validate_timeseries_alignment("model.db", 1, values)
|
|
285
|
+
>>> if result["is_valid"]:
|
|
286
|
+
... print("Timeseries is properly aligned")
|
|
287
|
+
... else:
|
|
288
|
+
... print(f"Alignment issues: {result['issues']}")
|
|
289
|
+
"""
|
|
290
|
+
# Convert to list of floats
|
|
291
|
+
if isinstance(values, Timeseries):
|
|
292
|
+
values_list = values.values
|
|
293
|
+
elif isinstance(values, np.ndarray):
|
|
294
|
+
values_list = values.tolist()
|
|
295
|
+
elif isinstance(values, list):
|
|
296
|
+
values_list = [float(v) for v in values]
|
|
297
|
+
else:
|
|
298
|
+
raise ValueError("values must be List[float], numpy.ndarray, or Timeseries")
|
|
299
|
+
|
|
300
|
+
with database_context(db_path, read_only=True) as conn:
|
|
301
|
+
# Get network time periods
|
|
302
|
+
from pyconvexity.models.network import get_network_time_periods
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
time_periods = get_network_time_periods(conn)
|
|
306
|
+
expected_length = len(time_periods)
|
|
307
|
+
actual_length = len(values_list)
|
|
308
|
+
|
|
309
|
+
is_valid = actual_length == expected_length
|
|
310
|
+
issues = []
|
|
311
|
+
|
|
312
|
+
if actual_length < expected_length:
|
|
313
|
+
issues.append(f"Missing {expected_length - actual_length} time periods")
|
|
314
|
+
elif actual_length > expected_length:
|
|
315
|
+
issues.append(f"Extra {actual_length - expected_length} time periods")
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
"is_valid": is_valid,
|
|
319
|
+
"expected_length": expected_length,
|
|
320
|
+
"actual_length": actual_length,
|
|
321
|
+
"issues": issues,
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
except Exception as e:
|
|
325
|
+
return {
|
|
326
|
+
"is_valid": False,
|
|
327
|
+
"expected_length": 0,
|
|
328
|
+
"actual_length": len(values_list),
|
|
329
|
+
"issues": [f"Failed to get network time periods: {e}"],
|
|
330
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation module for PyConvexity.
|
|
3
|
+
|
|
4
|
+
Contains data validation rules and type checking functionality.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pyconvexity.validation.rules import (
|
|
8
|
+
get_validation_rule,
|
|
9
|
+
list_validation_rules,
|
|
10
|
+
get_all_validation_rules,
|
|
11
|
+
validate_static_value,
|
|
12
|
+
validate_timeseries_alignment,
|
|
13
|
+
parse_default_value,
|
|
14
|
+
get_attribute_setter_info,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"get_validation_rule",
|
|
19
|
+
"list_validation_rules",
|
|
20
|
+
"get_all_validation_rules",
|
|
21
|
+
"validate_static_value",
|
|
22
|
+
"validate_timeseries_alignment",
|
|
23
|
+
"parse_default_value",
|
|
24
|
+
"get_attribute_setter_info",
|
|
25
|
+
]
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation rules and operations for PyConvexity.
|
|
3
|
+
|
|
4
|
+
Provides validation logic for component attributes, data types, and timeseries alignment.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sqlite3
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Dict, Any, Optional, List
|
|
11
|
+
|
|
12
|
+
from pyconvexity.core.types import (
|
|
13
|
+
ValidationRule,
|
|
14
|
+
StaticValue,
|
|
15
|
+
TimePeriod,
|
|
16
|
+
TimeseriesValidationResult,
|
|
17
|
+
)
|
|
18
|
+
from pyconvexity.core.errors import ValidationError, InvalidDataType
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_validation_rule(
|
|
24
|
+
conn: sqlite3.Connection, component_type: str, attribute_name: str
|
|
25
|
+
) -> ValidationRule:
|
|
26
|
+
"""
|
|
27
|
+
Get validation rule for a specific component type and attribute.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
conn: Database connection
|
|
31
|
+
component_type: Type of component (e.g., "BUS", "GENERATOR")
|
|
32
|
+
attribute_name: Name of the attribute
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
ValidationRule object with all validation information
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ValidationError: If no validation rule is found
|
|
39
|
+
"""
|
|
40
|
+
cursor = conn.execute(
|
|
41
|
+
"""
|
|
42
|
+
SELECT component_type, attribute_name, data_type, unit, default_value, allowed_storage_types,
|
|
43
|
+
is_required, is_input, description
|
|
44
|
+
FROM attribute_validation_rules
|
|
45
|
+
WHERE component_type = ? AND attribute_name = ?
|
|
46
|
+
""",
|
|
47
|
+
(component_type, attribute_name),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
row = cursor.fetchone()
|
|
51
|
+
if not row:
|
|
52
|
+
raise ValidationError(
|
|
53
|
+
f"No validation rule found for {component_type}.{attribute_name}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
allowed_storage_types = row[5]
|
|
57
|
+
allows_static = allowed_storage_types in ("static", "static_or_timeseries")
|
|
58
|
+
allows_timeseries = allowed_storage_types in ("timeseries", "static_or_timeseries")
|
|
59
|
+
|
|
60
|
+
# Parse default value
|
|
61
|
+
default_value = None
|
|
62
|
+
if row[4]: # default_value_string
|
|
63
|
+
default_value = parse_default_value(row[4])
|
|
64
|
+
|
|
65
|
+
return ValidationRule(
|
|
66
|
+
component_type=row[0],
|
|
67
|
+
attribute_name=row[1],
|
|
68
|
+
data_type=row[2],
|
|
69
|
+
unit=row[3],
|
|
70
|
+
default_value_string=row[4],
|
|
71
|
+
allowed_storage_types=allowed_storage_types,
|
|
72
|
+
allows_static=allows_static,
|
|
73
|
+
allows_timeseries=allows_timeseries,
|
|
74
|
+
is_required=bool(row[6]),
|
|
75
|
+
is_input=bool(row[7]),
|
|
76
|
+
description=row[8],
|
|
77
|
+
default_value=default_value,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def list_validation_rules(
|
|
82
|
+
conn: sqlite3.Connection, component_type: str
|
|
83
|
+
) -> List[ValidationRule]:
|
|
84
|
+
"""
|
|
85
|
+
List validation rules for a component type.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
conn: Database connection
|
|
89
|
+
component_type: Type of component
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
List of ValidationRule objects
|
|
93
|
+
"""
|
|
94
|
+
cursor = conn.execute(
|
|
95
|
+
"""
|
|
96
|
+
SELECT component_type, attribute_name, data_type, unit, default_value, allowed_storage_types,
|
|
97
|
+
is_required, is_input, description
|
|
98
|
+
FROM attribute_validation_rules
|
|
99
|
+
WHERE component_type = ?
|
|
100
|
+
ORDER BY attribute_name
|
|
101
|
+
""",
|
|
102
|
+
(component_type,),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
rules = []
|
|
106
|
+
for row in cursor.fetchall():
|
|
107
|
+
allowed_storage_types = row[5]
|
|
108
|
+
allows_static = allowed_storage_types in ("static", "static_or_timeseries")
|
|
109
|
+
allows_timeseries = allowed_storage_types in (
|
|
110
|
+
"timeseries",
|
|
111
|
+
"static_or_timeseries",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Parse default value
|
|
115
|
+
default_value = None
|
|
116
|
+
if row[4]: # default_value_string
|
|
117
|
+
default_value = parse_default_value(row[4])
|
|
118
|
+
|
|
119
|
+
rules.append(
|
|
120
|
+
ValidationRule(
|
|
121
|
+
component_type=row[0],
|
|
122
|
+
attribute_name=row[1],
|
|
123
|
+
data_type=row[2],
|
|
124
|
+
unit=row[3],
|
|
125
|
+
default_value_string=row[4],
|
|
126
|
+
allowed_storage_types=allowed_storage_types,
|
|
127
|
+
allows_static=allows_static,
|
|
128
|
+
allows_timeseries=allows_timeseries,
|
|
129
|
+
is_required=bool(row[6]),
|
|
130
|
+
is_input=bool(row[7]),
|
|
131
|
+
description=row[8],
|
|
132
|
+
default_value=default_value,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return rules
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_all_validation_rules(conn: sqlite3.Connection) -> Dict[str, Any]:
|
|
140
|
+
"""
|
|
141
|
+
Get all validation rules from the database.
|
|
142
|
+
This replaces the need to load the entire JSON file into memory.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
conn: Database connection
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Dictionary mapping component types to their validation rules
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
cursor = conn.execute(
|
|
152
|
+
"""
|
|
153
|
+
SELECT component_type, attribute_name, data_type, unit, default_value, allowed_storage_types,
|
|
154
|
+
is_required, is_input, description
|
|
155
|
+
FROM attribute_validation_rules
|
|
156
|
+
"""
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
rules = {}
|
|
160
|
+
for row in cursor.fetchall():
|
|
161
|
+
component_type = row[0]
|
|
162
|
+
attribute_name = row[1]
|
|
163
|
+
data_type = row[2]
|
|
164
|
+
unit = row[3]
|
|
165
|
+
default_value = row[4]
|
|
166
|
+
allowed_storage_types = row[5]
|
|
167
|
+
is_required = bool(row[6])
|
|
168
|
+
is_input = bool(row[7])
|
|
169
|
+
description = row[8]
|
|
170
|
+
|
|
171
|
+
if component_type not in rules:
|
|
172
|
+
rules[component_type] = {}
|
|
173
|
+
|
|
174
|
+
rules[component_type][attribute_name] = {
|
|
175
|
+
"data_type": data_type,
|
|
176
|
+
"unit": unit,
|
|
177
|
+
"default_value": default_value,
|
|
178
|
+
"allowed_storage_types": allowed_storage_types,
|
|
179
|
+
"is_required": is_required,
|
|
180
|
+
"is_input": is_input,
|
|
181
|
+
"description": description,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return rules
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.error(f"Error getting all validation rules: {e}")
|
|
187
|
+
return {}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def validate_static_value(value: StaticValue, rule: ValidationRule) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Validate static value against rule.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
value: StaticValue to validate
|
|
196
|
+
rule: ValidationRule to validate against
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
InvalidDataType: If value type doesn't match rule
|
|
200
|
+
"""
|
|
201
|
+
value_type = value.data_type()
|
|
202
|
+
|
|
203
|
+
if value_type != rule.data_type:
|
|
204
|
+
raise InvalidDataType(expected=rule.data_type, actual=value_type)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def validate_timeseries_alignment(
|
|
208
|
+
conn: sqlite3.Connection, timeseries: List[float]
|
|
209
|
+
) -> TimeseriesValidationResult:
|
|
210
|
+
"""
|
|
211
|
+
Validate timeseries alignment with network periods (single network per database).
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
conn: Database connection
|
|
215
|
+
timeseries: List of timeseries points to validate
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
TimeseriesValidationResult with validation details
|
|
219
|
+
"""
|
|
220
|
+
# Get network time periods
|
|
221
|
+
from pyconvexity.models.network import get_network_time_periods
|
|
222
|
+
|
|
223
|
+
network_periods = get_network_time_periods(conn)
|
|
224
|
+
network_period_indices = {p.period_index for p in network_periods}
|
|
225
|
+
|
|
226
|
+
# Get provided period indices
|
|
227
|
+
provided_period_indices = {p.period_index for p in timeseries}
|
|
228
|
+
|
|
229
|
+
# Find missing and extra periods
|
|
230
|
+
missing_periods = list(network_period_indices - provided_period_indices)
|
|
231
|
+
extra_periods = list(provided_period_indices - network_period_indices)
|
|
232
|
+
|
|
233
|
+
is_valid = len(missing_periods) == 0 and len(extra_periods) == 0
|
|
234
|
+
|
|
235
|
+
return TimeseriesValidationResult(
|
|
236
|
+
is_valid=is_valid,
|
|
237
|
+
missing_periods=missing_periods,
|
|
238
|
+
extra_periods=extra_periods,
|
|
239
|
+
total_network_periods=len(network_periods),
|
|
240
|
+
provided_periods=len(timeseries),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def parse_default_value(s: str) -> Optional[StaticValue]:
|
|
245
|
+
"""
|
|
246
|
+
Parse default value string.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
s: String representation of default value
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
StaticValue object or None if parsing fails
|
|
253
|
+
"""
|
|
254
|
+
# Try to parse as JSON first
|
|
255
|
+
try:
|
|
256
|
+
value = json.loads(s)
|
|
257
|
+
if isinstance(value, float):
|
|
258
|
+
return StaticValue(value)
|
|
259
|
+
elif isinstance(value, int):
|
|
260
|
+
return StaticValue(value)
|
|
261
|
+
elif isinstance(value, bool):
|
|
262
|
+
return StaticValue(value)
|
|
263
|
+
elif isinstance(value, str):
|
|
264
|
+
return StaticValue(value)
|
|
265
|
+
else:
|
|
266
|
+
return None
|
|
267
|
+
except (json.JSONDecodeError, ValueError):
|
|
268
|
+
# Fallback to string
|
|
269
|
+
return StaticValue(s)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def get_attribute_setter_info(
|
|
273
|
+
conn: sqlite3.Connection,
|
|
274
|
+
component_type: str,
|
|
275
|
+
attribute_name: str,
|
|
276
|
+
) -> Dict[str, Any]:
|
|
277
|
+
"""
|
|
278
|
+
Get the appropriate function name for setting an attribute.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
conn: Database connection
|
|
282
|
+
component_type: Type of component
|
|
283
|
+
attribute_name: Name of the attribute
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Dictionary with setter function information
|
|
287
|
+
|
|
288
|
+
Raises:
|
|
289
|
+
ValidationError: If attribute or data type is unknown
|
|
290
|
+
"""
|
|
291
|
+
rule = get_validation_rule(conn, component_type, attribute_name)
|
|
292
|
+
|
|
293
|
+
function_name = {
|
|
294
|
+
"float": "set_float_attribute",
|
|
295
|
+
"int": "set_integer_attribute",
|
|
296
|
+
"boolean": "set_boolean_attribute",
|
|
297
|
+
"string": "set_string_attribute",
|
|
298
|
+
}.get(rule.data_type)
|
|
299
|
+
|
|
300
|
+
if not function_name:
|
|
301
|
+
raise ValidationError(f"Unknown data type: {rule.data_type}")
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
"function_name": function_name,
|
|
305
|
+
"data_type": rule.data_type,
|
|
306
|
+
"allows_static": rule.allows_static,
|
|
307
|
+
"allows_timeseries": rule.allows_timeseries,
|
|
308
|
+
"is_required": rule.is_required,
|
|
309
|
+
"default_value": rule.default_value_string,
|
|
310
|
+
"unit": rule.unit,
|
|
311
|
+
"description": rule.description,
|
|
312
|
+
}
|