power-grid-model 1.11.33__py3-none-win_amd64.whl → 1.12.70__py3-none-win_amd64.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.
Files changed (67) hide show
  1. power_grid_model/__init__.py +54 -54
  2. power_grid_model/_core/__init__.py +3 -3
  3. power_grid_model/_core/buffer_handling.py +493 -478
  4. power_grid_model/_core/data_handling.py +195 -141
  5. power_grid_model/_core/data_types.py +143 -132
  6. power_grid_model/_core/dataset_definitions.py +109 -108
  7. power_grid_model/_core/enum.py +226 -226
  8. power_grid_model/_core/error_handling.py +206 -205
  9. power_grid_model/_core/errors.py +130 -126
  10. power_grid_model/_core/index_integer.py +17 -17
  11. power_grid_model/_core/options.py +71 -70
  12. power_grid_model/_core/power_grid_core.py +563 -581
  13. power_grid_model/_core/power_grid_dataset.py +535 -534
  14. power_grid_model/_core/power_grid_meta.py +257 -243
  15. power_grid_model/_core/power_grid_model.py +969 -687
  16. power_grid_model/_core/power_grid_model_c/__init__.py +3 -0
  17. power_grid_model/_core/power_grid_model_c/bin/power_grid_model_c.dll +0 -0
  18. power_grid_model/_core/power_grid_model_c/get_pgm_dll_path.py +63 -0
  19. power_grid_model/_core/power_grid_model_c/include/power_grid_model_c/basics.h +255 -0
  20. power_grid_model/_core/power_grid_model_c/include/power_grid_model_c/buffer.h +108 -0
  21. power_grid_model/_core/power_grid_model_c/include/power_grid_model_c/dataset.h +316 -0
  22. power_grid_model/_core/power_grid_model_c/include/power_grid_model_c/dataset_definitions.h +1052 -0
  23. power_grid_model/_core/power_grid_model_c/include/power_grid_model_c/handle.h +99 -0
  24. power_grid_model/_core/power_grid_model_c/include/power_grid_model_c/meta_data.h +189 -0
  25. power_grid_model/_core/power_grid_model_c/include/power_grid_model_c/model.h +125 -0
  26. power_grid_model/_core/power_grid_model_c/include/power_grid_model_c/options.h +142 -0
  27. power_grid_model/_core/power_grid_model_c/include/power_grid_model_c/serialization.h +118 -0
  28. power_grid_model/_core/power_grid_model_c/include/power_grid_model_c.h +36 -0
  29. power_grid_model/_core/power_grid_model_c/include/power_grid_model_cpp/basics.hpp +65 -0
  30. power_grid_model/_core/power_grid_model_c/include/power_grid_model_cpp/buffer.hpp +61 -0
  31. power_grid_model/_core/power_grid_model_c/include/power_grid_model_cpp/dataset.hpp +220 -0
  32. power_grid_model/_core/power_grid_model_c/include/power_grid_model_cpp/handle.hpp +108 -0
  33. power_grid_model/_core/power_grid_model_c/include/power_grid_model_cpp/meta_data.hpp +84 -0
  34. power_grid_model/_core/power_grid_model_c/include/power_grid_model_cpp/model.hpp +63 -0
  35. power_grid_model/_core/power_grid_model_c/include/power_grid_model_cpp/options.hpp +52 -0
  36. power_grid_model/_core/power_grid_model_c/include/power_grid_model_cpp/serialization.hpp +124 -0
  37. power_grid_model/_core/power_grid_model_c/include/power_grid_model_cpp/utils.hpp +81 -0
  38. power_grid_model/_core/power_grid_model_c/include/power_grid_model_cpp.hpp +19 -0
  39. power_grid_model/_core/power_grid_model_c/lib/cmake/power_grid_model/power_grid_modelConfig.cmake +37 -0
  40. power_grid_model/_core/power_grid_model_c/lib/cmake/power_grid_model/power_grid_modelConfigVersion.cmake +65 -0
  41. power_grid_model/_core/power_grid_model_c/lib/cmake/power_grid_model/power_grid_modelTargets-release.cmake +19 -0
  42. power_grid_model/_core/power_grid_model_c/lib/cmake/power_grid_model/power_grid_modelTargets.cmake +144 -0
  43. power_grid_model/_core/power_grid_model_c/lib/power_grid_model_c.lib +0 -0
  44. power_grid_model/_core/power_grid_model_c/share/LICENSE +292 -0
  45. power_grid_model/_core/power_grid_model_c/share/README.md +15 -0
  46. power_grid_model/_core/serialization.py +317 -317
  47. power_grid_model/_core/typing.py +20 -20
  48. power_grid_model/_core/utils.py +798 -789
  49. power_grid_model/data_types.py +321 -321
  50. power_grid_model/enum.py +27 -27
  51. power_grid_model/errors.py +37 -37
  52. power_grid_model/typing.py +43 -43
  53. power_grid_model/utils.py +473 -469
  54. power_grid_model/validation/__init__.py +25 -25
  55. power_grid_model/validation/_rules.py +1171 -1174
  56. power_grid_model/validation/_validation.py +1172 -1173
  57. power_grid_model/validation/assertions.py +93 -93
  58. power_grid_model/validation/errors.py +602 -589
  59. power_grid_model/validation/utils.py +313 -312
  60. {power_grid_model-1.11.33.dist-info → power_grid_model-1.12.70.dist-info}/METADATA +178 -180
  61. power_grid_model-1.12.70.dist-info/RECORD +65 -0
  62. {power_grid_model-1.11.33.dist-info → power_grid_model-1.12.70.dist-info}/WHEEL +1 -1
  63. power_grid_model-1.12.70.dist-info/entry_points.txt +3 -0
  64. power_grid_model/_core/_power_grid_core.dll +0 -0
  65. power_grid_model-1.11.33.dist-info/RECORD +0 -36
  66. power_grid_model-1.11.33.dist-info/top_level.txt +0 -1
  67. {power_grid_model-1.11.33.dist-info → power_grid_model-1.12.70.dist-info}/licenses/LICENSE +0 -0
@@ -1,589 +1,602 @@
1
- # SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
2
- #
3
- # SPDX-License-Identifier: MPL-2.0
4
-
5
- """
6
- Error classes
7
- """
8
-
9
- import re
10
- from abc import ABC
11
- from enum import Enum
12
- from typing import Any, Iterable, Type
13
-
14
- from power_grid_model import ComponentType
15
- from power_grid_model._core.dataset_definitions import DatasetType
16
-
17
-
18
- class ValidationError(ABC):
19
- """
20
- The Validation Error is an abstract base class which should be extended by all validation errors. It supplies
21
- three public member variables: component, field and ids; storing information about the origin of the validation
22
- error. Error classes can extend the public members. For example:
23
-
24
- NotBetweenError(ValidationError):
25
- component = 'vehicle'
26
- field = 'direction'
27
- id = [3, 14, 15, 92, 65, 35]
28
- ref_value = (-3.1416, 3.1416)
29
-
30
- For convenience, a human readable representation of the error is supplied using the str() function.
31
- I.e. print(str(error)) will print a human readable error message like:
32
-
33
- Field `direction` is not between -3.1416 and 3.1416 for 6 vehicles
34
-
35
- """
36
-
37
- component: ComponentType | list[ComponentType] | None = None
38
- """
39
- The component, or components, to which the error applies.
40
- """
41
-
42
- field: str | list[str] | list[tuple[ComponentType, str]] | None = None
43
- """
44
- The field, or fields, to which the error applies. A field can also be a tuple (component, field) when multiple
45
- components are being addressed.
46
- """
47
-
48
- ids: list[int] | list[tuple[ComponentType, int]] | None = None
49
- """
50
- The object identifiers to which the error applies. A field object identifier can also be a tuple (component, id)
51
- when multiple components are being addressed.
52
- """
53
-
54
- _message: str = "An unknown validation error occurred."
55
-
56
- _delimiter: str = " and "
57
-
58
- @property
59
- def component_str(self) -> str:
60
- """
61
- A string representation of the component to which this error applies
62
- """
63
- if self.component is None:
64
- return str(None)
65
- if isinstance(self.component, list):
66
- return "/".join(component.value for component in self.component)
67
- return self.component.value
68
-
69
- @property
70
- def field_str(self) -> str:
71
- """
72
- A string representation of the field to which this error applies
73
- """
74
-
75
- def _unpack(field: str | tuple[ComponentType, str]) -> str:
76
- if isinstance(field, str):
77
- return f"'{field}'"
78
- return ".".join(field)
79
-
80
- if isinstance(self.field, list):
81
- return self._delimiter.join(_unpack(field) for field in self.field)
82
- return _unpack(self.field) if self.field else str(self.field)
83
-
84
- def get_context(self, id_lookup: list[str] | dict[int, str] | None = None) -> dict[str, Any]:
85
- """
86
- Returns a dictionary that supplies (human readable) information about this error. Each member variable is
87
- included in the dictionary. If a function {field_name}_str() exists, the value is overwritten by that function.
88
-
89
- Args:
90
- id_lookup: A list or dict (int->str) containing textual object ids
91
- """
92
- context = self.__dict__.copy()
93
- if id_lookup:
94
- if isinstance(id_lookup, list):
95
- id_lookup = dict(enumerate(id_lookup))
96
- context["ids"] = (
97
- {i: id_lookup.get(i[1] if isinstance(i, tuple) else i) for i in self.ids} if self.ids else set()
98
- )
99
- for key in context:
100
- if hasattr(self, key + "_str"):
101
- context[key] = str(getattr(self, key + "_str"))
102
- return context
103
-
104
- def __str__(self) -> str:
105
- n_objects = len(self.ids) if self.ids else 0
106
- context = self.get_context()
107
- context["n"] = n_objects
108
- context["objects"] = context.get("component", "object")
109
- if n_objects != 1:
110
- context["objects"] = re.sub(r"([a-z_]+)", r"\1s", context["objects"])
111
- return self._message.format(**context).strip()
112
-
113
- def __repr__(self) -> str:
114
- context = " ".join(f"{key}={value}" for key, value in self.get_context().items())
115
- return f"<{type(self).__name__}: {context}>"
116
-
117
- def __eq__(self, other):
118
- return (
119
- type(self) is type(other)
120
- and self.component == other.component
121
- and self.field == other.field
122
- and self.ids == other.ids
123
- )
124
-
125
-
126
- class SingleFieldValidationError(ValidationError):
127
- """
128
- Base class for an error that applies to a single field in a single component
129
- """
130
-
131
- _message = "Field {field} is not valid for {n} {objects}."
132
- component: ComponentType
133
- field: str
134
- ids: list[int] | None
135
-
136
- def __init__(self, component: ComponentType, field: str, ids: Iterable[int] | None):
137
- """
138
- Args:
139
- component: Component name
140
- field: Field name
141
- ids: List of component IDs (not row indices)
142
- """
143
- self.component = component
144
- self.field = field
145
- self.ids = sorted(ids) if ids is not None else None
146
-
147
-
148
- class MultiFieldValidationError(ValidationError):
149
- """
150
- Base class for an error that applies to multiple fields in a single component
151
- """
152
-
153
- _message = "Combination of fields {field} is not valid for {n} {objects}."
154
- component: ComponentType
155
- field: list[str]
156
- ids: list[int]
157
-
158
- def __init__(self, component: ComponentType, fields: list[str], ids: list[int]):
159
- """
160
- Args:
161
- component: Component name
162
- fields: List of field names
163
- ids: List of component IDs (not row indices)
164
- """
165
- self.component = component
166
- self.field = sorted(fields)
167
- self.ids = sorted(ids)
168
-
169
- if len(self.field) < 2:
170
- raise ValueError(f"{type(self).__name__} expects at least two fields: {self.field}")
171
-
172
-
173
- class MultiComponentValidationError(ValidationError):
174
- """
175
- Base class for an error that applies to multiple components, and, subsequently, multiple fields.
176
- Even if both fields have the same name, they are considered to be different fields and notated as such.
177
- E.g. the two fields `id` fields of the `node` and `line` component: [('node', 'id'), ('line', 'id')].
178
- """
179
-
180
- component: list[ComponentType]
181
- field: list[tuple[ComponentType, str]]
182
- ids: list[tuple[ComponentType, int]]
183
- _message = "Fields {field} are not valid for {n} {objects}."
184
-
185
- def __init__(self, fields: list[tuple[ComponentType, str]], ids: list[tuple[ComponentType, int]]):
186
- """
187
- Args:
188
- fields: List of field names, formatted as tuples (component, field)
189
- ids: List of component IDs (not row indices), formatted as tuples (component, id)
190
- """
191
- self.component = sorted(set(component for component, _ in fields), key=str)
192
- self.field = sorted(fields)
193
- self.ids = sorted(ids)
194
-
195
- if len(self.field) < 2:
196
- raise ValueError(f"{type(self).__name__} expects at least two fields: {self.field}")
197
- if len(self.component) < 2:
198
- raise ValueError(f"{type(self).__name__} expects at least two components: {self.component}")
199
-
200
-
201
- class NotIdenticalError(SingleFieldValidationError):
202
- """
203
- The value is not unique within a single column in a dataset
204
- E.g. When two nodes share the same id.
205
- """
206
-
207
- _message = "Field {field} is not unique for {n} {objects}: {num_unique} different values."
208
- values: list[Any]
209
- unique: set[Any]
210
- num_unique: int
211
-
212
- def __init__(self, component: ComponentType, field: str, ids: Iterable[int], values: list[Any]):
213
- super().__init__(component, field, ids)
214
- self.values = values
215
- self.unique = set(self.values)
216
- self.num_unique = len(self.unique)
217
-
218
-
219
- class NotUniqueError(SingleFieldValidationError):
220
- """
221
- The value is not unique within a single column in a dataset
222
- E.g. When two nodes share the same id.
223
- """
224
-
225
- _message = "Field {field} is not unique for {n} {objects}."
226
-
227
-
228
- class MultiComponentNotUniqueError(MultiComponentValidationError):
229
- """
230
- The value is not unique between multiple columns in multiple components
231
- E.g. When a node and a line share the same id.
232
- """
233
-
234
- _message = "Fields {field} are not unique for {n} {objects}."
235
-
236
-
237
- class InvalidValueError(SingleFieldValidationError):
238
- """
239
- The value is not a valid value in the supplied list of supported values.
240
- E.g. an enum value that is not supported for a specific feature.
241
- """
242
-
243
- _message = "Field {field} contains invalid values for {n} {objects}."
244
- values: list
245
-
246
- def __init__(self, component: ComponentType, field: str, ids: list[int], values: list):
247
- super().__init__(component, field, ids)
248
- self.values = values
249
-
250
- @property
251
- def values_str(self) -> str:
252
- """
253
- A string representation of the field to which this error applies.
254
- """
255
- return ",".join(v.name if isinstance(v, Enum) else v for v in self.values)
256
-
257
- def __eq__(self, other):
258
- return super().__eq__(other) and self.values == other.values
259
-
260
-
261
- class InvalidEnumValueError(SingleFieldValidationError):
262
- """
263
- The value is not a valid value in the supplied enumeration type.
264
- E.g. a sym_load has a non existing LoadGenType.
265
- """
266
-
267
- _message = "Field {field} contains invalid {enum} values for {n} {objects}."
268
- enum: Type[Enum] | list[Type[Enum]]
269
-
270
- def __init__(self, component: ComponentType, field: str, ids: list[int], enum: Type[Enum] | list[Type[Enum]]):
271
- super().__init__(component, field, ids)
272
- self.enum = enum
273
-
274
- @property
275
- def enum_str(self) -> str:
276
- """
277
- A string representation of the field to which this error applies.
278
- """
279
- if isinstance(self.enum, list):
280
- return ",".join(e.__name__ for e in self.enum)
281
-
282
- return self.enum.__name__
283
-
284
- def __eq__(self, other):
285
- return super().__eq__(other) and self.enum == other.enum
286
-
287
-
288
- class SameValueError(MultiFieldValidationError):
289
- """
290
- The value of two fields is equal.
291
- E.g. A line has the same from_node as to_node.
292
- """
293
-
294
- _message = "Same value for {field} for {n} {objects}."
295
-
296
-
297
- class NotBooleanError(SingleFieldValidationError):
298
- """
299
- Invalid boolean value. Boolean fields don't really exist in our data structure, they are 1-byte signed integers and
300
- should contain either a 0 (=False) or a 1 (=True).
301
- """
302
-
303
- _message = "Field {field} is not a boolean (0 or 1) for {n} {objects}."
304
-
305
-
306
- class MissingValueError(SingleFieldValidationError):
307
- """
308
- A required value was missing, i.e. NaN.
309
- """
310
-
311
- _message = "Field {field} is missing for {n} {objects}."
312
-
313
-
314
- class IdNotInDatasetError(SingleFieldValidationError):
315
- """
316
- An object identifier does not exist in the original data.
317
- E.g. An update data set contains a record for a line that doesn't exist in the input data set.
318
- """
319
-
320
- _message = "ID does not exist in {ref_dataset} for {n} {objects}."
321
- ref_dataset: DatasetType
322
-
323
- def __init__(self, component: ComponentType, ids: list[int], ref_dataset: DatasetType):
324
- super().__init__(component=component, field="id", ids=ids)
325
- self.ref_dataset = ref_dataset
326
-
327
- def __eq__(self, other):
328
- return super().__eq__(other) and self.ref_dataset == other.ref_dataset
329
-
330
-
331
- class InvalidIdError(SingleFieldValidationError):
332
- """
333
- An object identifier does not refer to the right type of object.
334
- E.g. An line's from_node refers to a source.
335
-
336
- Filters can have been applied to check a subset of the records. E.g. This error may apply only to power_sensors
337
- that are said to be connected to a source (filter: measured_terminal_type=source). This useful to spot validation
338
- mistakes, due to ambiguity.
339
-
340
- E.g. when a sym_power_sensor is connected to a load, but measured_terminal_type is accidentally set to 'source',
341
- the error is:
342
-
343
- "Field `measured_object` does not contain a valid source id for 1 sym_power_sensor. (measured_terminal_type=source)"
344
-
345
- """
346
-
347
- _message = "Field {field} does not contain a valid {ref_components} id for {n} {objects}. {filters}"
348
- ref_components: list[ComponentType]
349
-
350
- def __init__(
351
- self,
352
- component: ComponentType,
353
- field: str,
354
- ids: list[int] | None = None,
355
- ref_components: ComponentType | list[ComponentType] | None = None,
356
- filters: dict[str, Any] | None = None,
357
- ):
358
- super().__init__(component=component, field=field, ids=ids)
359
- ref_components = ref_components if ref_components is not None else []
360
- self.ref_components = [ref_components] if isinstance(ref_components, (str, ComponentType)) else ref_components
361
- self.filters = filters if filters else None
362
-
363
- @property
364
- def ref_components_str(self):
365
- """
366
- A string representation of the components to which this error applies
367
- """
368
- return "/".join(self.ref_components)
369
-
370
- @property
371
- def filters_str(self):
372
- """
373
- A string representation of the filters that have been applied to the data to which this error refers
374
- """
375
- if not self.filters:
376
- return ""
377
- filters = ", ".join(f"{k}={v.name if isinstance(v, Enum) else v}" for k, v in self.filters.items())
378
- return f"({filters})"
379
-
380
- def __eq__(self, other):
381
- return super().__eq__(other) and self.ref_components == other.ref_components and self.filters == other.filters
382
-
383
-
384
- class TwoValuesZeroError(MultiFieldValidationError):
385
- """
386
- A record has a 0.0 value in two fields at the same time.
387
- E.g. A line's `r1`, `x1` are both 0.
388
- """
389
-
390
- _message = "Fields {field} are both zero for {n} {objects}."
391
-
392
-
393
- class ComparisonError(SingleFieldValidationError):
394
- """
395
- Base class for comparison errors.
396
- E.g. A transformer's `i0` is not greater or equal to it's `p0` divided by it's `sn`
397
- """
398
-
399
- _message = "Invalid {field}, compared to {ref_value} for {n} {objects}."
400
-
401
- RefType = int | float | str | tuple[int | float | str, ...]
402
-
403
- def __init__(self, component: ComponentType, field: str, ids: list[int], ref_value: "ComparisonError.RefType"):
404
- super().__init__(component, field, ids)
405
- self.ref_value = ref_value
406
-
407
- @property
408
- def ref_value_str(self):
409
- """
410
- A string representation of the reference value. E.g. 'zero', 'one', 'field_a and field_b' or '123'.
411
- """
412
- if isinstance(self.ref_value, tuple):
413
- return self._delimiter.join(map(str, self.ref_value))
414
- if self.ref_value == 0:
415
- return "zero"
416
- if self.ref_value == 1:
417
- return "one"
418
- return str(self.ref_value)
419
-
420
- def __eq__(self, other):
421
- return super().__eq__(other) and self.ref_value == other.ref_value
422
-
423
-
424
- class NotGreaterThanError(ComparisonError):
425
- """
426
- The value of a field is not greater than a reference value or expression.
427
- """
428
-
429
- _message = "Field {field} is not greater than {ref_value} for {n} {objects}."
430
-
431
-
432
- class NotGreaterOrEqualError(ComparisonError):
433
- """
434
- The value of a field is not greater or equal to a reference value or expression.
435
- """
436
-
437
- _message = "Field {field} is not greater than (or equal to) {ref_value} for {n} {objects}."
438
-
439
-
440
- class NotLessThanError(ComparisonError):
441
- """
442
- The value of a field is not less than a reference value or expression.
443
- """
444
-
445
- _message = "Field {field} is not smaller than {ref_value} for {n} {objects}."
446
-
447
-
448
- class NotLessOrEqualError(ComparisonError):
449
- """
450
- The value of a field is not smaller or equal to a reference value or expression.
451
- """
452
-
453
- _message = "Field {field} is not smaller than (or equal to) {ref_value} for {n} {objects}."
454
-
455
-
456
- class NotBetweenError(ComparisonError):
457
- """
458
- The value of a field is not between two a reference values or expressions (exclusive).
459
- """
460
-
461
- _message = "Field {field} is not between {ref_value} for {n} {objects}."
462
-
463
-
464
- class NotBetweenOrAtError(ComparisonError):
465
- """
466
- The value of a field is not between two a reference values or expressions (inclusive).
467
- """
468
-
469
- _message = "Field {field} is not between (or at) {ref_value} for {n} {objects}."
470
-
471
-
472
- class InfinityError(SingleFieldValidationError):
473
- """
474
- The value of a field is infinite.
475
- """
476
-
477
- _message = "Field {field} is infinite for {n} {objects}."
478
-
479
-
480
- class TransformerClockError(MultiFieldValidationError):
481
- """
482
- Invalid clock number.
483
- """
484
-
485
- _message = (
486
- "Invalid clock number for {n} {objects}. "
487
- "If one side has wye winding and the other side has not, the clock number should be odd. "
488
- "If either both or none of the sides have wye winding, the clock number should be even."
489
- )
490
-
491
-
492
- class FaultPhaseError(MultiFieldValidationError):
493
- """
494
- The fault phase does not match the fault type.
495
- """
496
-
497
- _message = "The fault phase is not applicable to the corresponding fault type for {n} {objects}."
498
-
499
-
500
- class PQSigmaPairError(MultiFieldValidationError):
501
- """
502
- The combination of p_sigma and q_sigma is not valid. They should be both present or both absent.
503
- """
504
-
505
- _message = "The combination of p_sigma and q_sigma is not valid for {n} {objects}."
506
-
507
-
508
- class InvalidAssociatedEnumValueError(MultiFieldValidationError):
509
- """
510
- The value is not a valid value in combination with the other specified attributes.
511
- E.g. When a transformer tap regulator has a branch3 control side but regulates a transformer.
512
- """
513
-
514
- _message = "The combination of fields {field} results in invalid {enum} values for {n} {objects}."
515
- enum: Type[Enum] | list[Type[Enum]]
516
-
517
- def __init__(
518
- self,
519
- component: ComponentType,
520
- fields: list[str],
521
- ids: list[int],
522
- enum: Type[Enum] | list[Type[Enum]],
523
- ):
524
- """
525
- Args:
526
- component: Component name
527
- fields: List of field names
528
- ids: List of component IDs (not row indices)
529
- enum: The supported enum values
530
- """
531
- super().__init__(component, fields, ids)
532
- self.enum = enum
533
-
534
- @property
535
- def enum_str(self) -> str:
536
- """
537
- A string representation of the field to which this error applies.
538
- """
539
- if isinstance(self.enum, list):
540
- return ",".join(e.__name__ for e in self.enum)
541
-
542
- return self.enum.__name__
543
-
544
- def __eq__(self, other):
545
- return super().__eq__(other) and self.enum == other.enum
546
-
547
-
548
- class UnsupportedMeasuredTerminalType(InvalidValueError):
549
- """
550
- The measured terminal type is not a supported value.
551
-
552
- Supported values are in the supplied list of values.
553
- """
554
-
555
- _message = "measured_terminal_type contains unsupported values for {n} {objects}."
556
-
557
-
558
- class MixedCurrentAngleMeasurementTypeError(MultiFieldValidationError):
559
- """
560
- Mixed current angle measurement type error.
561
- """
562
-
563
- _message = (
564
- "Mixture of different current angle measurement types on the same terminal for {n} {objects}. "
565
- "If multiple current sensors measure the same terminal of the same object, all angle measurement types must be "
566
- "the same. Mixing local_angle and global_angle current measurements on the same terminal is not supported."
567
- )
568
-
569
-
570
- class MixedPowerCurrentSensorError(MultiComponentValidationError):
571
- """
572
- Mixed power and current sensor error.
573
- """
574
-
575
- _message = (
576
- "Mixture of power and current sensors on the same terminal for {n} {objects}. "
577
- "If multiple sensors measure the same terminal of the same object, all sensors must measure the same quantity."
578
- )
579
-
580
-
581
- class MissingVoltageAngleMeasurementError(MultiComponentValidationError):
582
- """
583
- Missing voltage angle measurement error.
584
- """
585
-
586
- _message = (
587
- "Missing voltage angle measurement for {n} {objects}. "
588
- "If a voltage sensor measures the voltage of a terminal, it must also measure the voltage angle."
589
- )
1
+ # SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
2
+ #
3
+ # SPDX-License-Identifier: MPL-2.0
4
+
5
+ """
6
+ Error classes
7
+ """
8
+
9
+ import re
10
+ from abc import ABC
11
+ from collections.abc import Iterable
12
+ from enum import Enum
13
+ from typing import Any
14
+
15
+ from power_grid_model import ComponentType
16
+ from power_grid_model._core.dataset_definitions import DatasetType
17
+
18
+ _MIN_FIELDS = 2
19
+ _MIN_COMPONENTS = 2
20
+
21
+
22
+ class ValidationError(ABC):
23
+ """
24
+ The Validation Error is an abstract base class which should be extended by all validation errors. It supplies
25
+ three public member variables: component, field and ids; storing information about the origin of the validation
26
+ error. Error classes can extend the public members. For example:
27
+
28
+ NotBetweenError(ValidationError):
29
+ component = 'vehicle'
30
+ field = 'direction'
31
+ id = [3, 14, 15, 92, 65, 35]
32
+ ref_value = (-3.1416, 3.1416)
33
+
34
+ For convenience, a human readable representation of the error is supplied using the str() function.
35
+ I.e. print(str(error)) will print a human readable error message like:
36
+
37
+ Field `direction` is not between -3.1416 and 3.1416 for 6 vehicles
38
+
39
+ """
40
+
41
+ component: ComponentType | list[ComponentType] | None = None
42
+ """
43
+ The component, or components, to which the error applies.
44
+ """
45
+
46
+ field: str | list[str] | list[tuple[ComponentType, str]] | None = None
47
+ """
48
+ The field, or fields, to which the error applies. A field can also be a tuple (component, field) when multiple
49
+ components are being addressed.
50
+ """
51
+
52
+ ids: list[int] | list[tuple[ComponentType, int]] | None = None
53
+ """
54
+ The object identifiers to which the error applies. A field object identifier can also be a tuple (component, id)
55
+ when multiple components are being addressed.
56
+ """
57
+
58
+ _message: str = "An unknown validation error occurred."
59
+
60
+ _delimiter: str = " and "
61
+
62
+ __hash__ = None # type: ignore[assignment]
63
+
64
+ @property
65
+ def component_str(self) -> str:
66
+ """
67
+ A string representation of the component to which this error applies
68
+ """
69
+ if self.component is None:
70
+ return str(None)
71
+ if isinstance(self.component, list):
72
+ return "/".join(component.value for component in self.component)
73
+ return self.component.value
74
+
75
+ @property
76
+ def field_str(self) -> str:
77
+ """
78
+ A string representation of the field to which this error applies
79
+ """
80
+
81
+ def _unpack(field: str | tuple[ComponentType, str]) -> str:
82
+ if isinstance(field, str):
83
+ return f"'{field}'"
84
+ return ".".join(field)
85
+
86
+ if isinstance(self.field, list):
87
+ return self._delimiter.join(_unpack(field) for field in self.field)
88
+ return _unpack(self.field) if self.field else str(self.field)
89
+
90
+ def get_context(self, id_lookup: list[str] | dict[int, str] | None = None) -> dict[str, Any]:
91
+ """
92
+ Returns a dictionary that supplies (human readable) information about this error. Each member variable is
93
+ included in the dictionary. If a function {field_name}_str() exists, the value is overwritten by that function.
94
+
95
+ Args:
96
+ id_lookup: A list or dict (int->str) containing textual object ids
97
+ """
98
+ context = self.__dict__.copy()
99
+ if id_lookup:
100
+ if isinstance(id_lookup, list):
101
+ id_lookup = dict(enumerate(id_lookup))
102
+ context["ids"] = (
103
+ {i: id_lookup.get(i[1] if isinstance(i, tuple) else i) for i in self.ids} if self.ids else set()
104
+ )
105
+ for key in context:
106
+ if hasattr(self, key + "_str"):
107
+ context[key] = str(getattr(self, key + "_str"))
108
+ return context
109
+
110
+ def __str__(self) -> str:
111
+ n_objects = len(self.ids) if self.ids else 0
112
+ context = self.get_context()
113
+ context["n"] = n_objects
114
+ context["objects"] = context.get("component", "object")
115
+ if n_objects != 1:
116
+ context["objects"] = re.sub(r"([a-z_]+)", r"\1s", context["objects"])
117
+ return self._message.format(**context).strip()
118
+
119
+ def __repr__(self) -> str:
120
+ context = " ".join(f"{key}={value}" for key, value in self.get_context().items())
121
+ return f"<{type(self).__name__}: {context}>"
122
+
123
+ def __eq__(self, other):
124
+ return (
125
+ type(self) is type(other)
126
+ and self.component == other.component
127
+ and self.field == other.field
128
+ and self.ids == other.ids
129
+ )
130
+
131
+
132
+ class SingleFieldValidationError(ValidationError):
133
+ """
134
+ Base class for an error that applies to a single field in a single component
135
+ """
136
+
137
+ _message = "Field {field} is not valid for {n} {objects}."
138
+ component: ComponentType
139
+ field: str
140
+ ids: list[int] | None
141
+
142
+ def __init__(self, component: ComponentType, field: str, ids: Iterable[int] | None):
143
+ """
144
+ Args:
145
+ component: Component name
146
+ field: Field name
147
+ ids: List of component IDs (not row indices)
148
+ """
149
+ self.component = component
150
+ self.field = field
151
+ self.ids = sorted(ids) if ids is not None else None
152
+
153
+
154
+ class MultiFieldValidationError(ValidationError):
155
+ """
156
+ Base class for an error that applies to multiple fields in a single component
157
+ """
158
+
159
+ _message = "Combination of fields {field} is not valid for {n} {objects}."
160
+ component: ComponentType
161
+ field: list[str]
162
+ ids: list[int]
163
+
164
+ def __init__(self, component: ComponentType, fields: list[str], ids: list[int]):
165
+ """
166
+ Args:
167
+ component: Component name
168
+ fields: List of field names
169
+ ids: List of component IDs (not row indices)
170
+ """
171
+ self.component = component
172
+ self.field = sorted(fields)
173
+ self.ids = sorted(ids)
174
+
175
+ if len(self.field) < _MIN_FIELDS:
176
+ raise ValueError(f"{type(self).__name__} expects at least 2 fields: {self.field}")
177
+
178
+
179
+ class MultiComponentValidationError(ValidationError):
180
+ """
181
+ Base class for an error that applies to multiple components, and, subsequently, multiple fields.
182
+ Even if both fields have the same name, they are considered to be different fields and notated as such.
183
+ E.g. the two fields `id` fields of the `node` and `line` component: [('node', 'id'), ('line', 'id')].
184
+ """
185
+
186
+ component: list[ComponentType]
187
+ field: list[tuple[ComponentType, str]]
188
+ ids: list[tuple[ComponentType, int]]
189
+ _message = "Fields {field} are not valid for {n} {objects}."
190
+
191
+ def __init__(self, fields: list[tuple[ComponentType, str]], ids: list[tuple[ComponentType, int]]):
192
+ """
193
+ Args:
194
+ fields: List of field names, formatted as tuples (component, field)
195
+ ids: List of component IDs (not row indices), formatted as tuples (component, id)
196
+ """
197
+ self.component = sorted({component for component, _ in fields}, key=str)
198
+ self.field = sorted(fields)
199
+ self.ids = sorted(ids)
200
+
201
+ if len(self.field) < _MIN_FIELDS:
202
+ raise ValueError(f"{type(self).__name__} expects at least {_MIN_FIELDS} fields: {self.field}")
203
+ if len(self.component) < _MIN_COMPONENTS:
204
+ raise ValueError(f"{type(self).__name__} expects at least {_MIN_COMPONENTS} components: {self.component}")
205
+
206
+
207
+ class NotIdenticalError(SingleFieldValidationError):
208
+ """
209
+ The value is not unique within a single column in a dataset
210
+ E.g. When two nodes share the same id.
211
+ """
212
+
213
+ _message = "Field {field} is not unique for {n} {objects}: {num_unique} different values."
214
+ values: list[Any]
215
+ unique: set[Any]
216
+ num_unique: int
217
+
218
+ def __init__(self, component: ComponentType, field: str, ids: Iterable[int], values: list[Any]):
219
+ super().__init__(component, field, ids)
220
+ self.values = values
221
+ self.unique = set(self.values)
222
+ self.num_unique = len(self.unique)
223
+
224
+
225
+ class NotUniqueError(SingleFieldValidationError):
226
+ """
227
+ The value is not unique within a single column in a dataset
228
+ E.g. When two nodes share the same id.
229
+ """
230
+
231
+ _message = "Field {field} is not unique for {n} {objects}."
232
+
233
+
234
+ class MultiComponentNotUniqueError(MultiComponentValidationError):
235
+ """
236
+ The value is not unique between multiple columns in multiple components
237
+ E.g. When a node and a line share the same id.
238
+ """
239
+
240
+ _message = "Fields {field} are not unique for {n} {objects}."
241
+
242
+
243
+ class InvalidValueError(SingleFieldValidationError):
244
+ """
245
+ The value is not a valid value in the supplied list of supported values.
246
+ E.g. an enum value that is not supported for a specific feature.
247
+ """
248
+
249
+ _message = "Field {field} contains invalid values for {n} {objects}."
250
+ values: list
251
+ __hash__ = None # type: ignore[assignment]
252
+
253
+ def __init__(self, component: ComponentType, field: str, ids: list[int], values: list):
254
+ super().__init__(component, field, ids)
255
+ self.values = values
256
+
257
+ @property
258
+ def values_str(self) -> str:
259
+ """
260
+ A string representation of the field to which this error applies.
261
+ """
262
+ return ",".join(v.name if isinstance(v, Enum) else v for v in self.values)
263
+
264
+ def __eq__(self, other):
265
+ return super().__eq__(other) and self.values == other.values
266
+
267
+
268
+ class InvalidEnumValueError(SingleFieldValidationError):
269
+ """
270
+ The value is not a valid value in the supplied enumeration type.
271
+ E.g. a sym_load has a non existing LoadGenType.
272
+ """
273
+
274
+ _message = "Field {field} contains invalid {enum} values for {n} {objects}."
275
+ enum: type[Enum] | list[type[Enum]]
276
+ __hash__ = None # type: ignore[assignment]
277
+
278
+ def __init__(self, component: ComponentType, field: str, ids: list[int], enum: type[Enum] | list[type[Enum]]):
279
+ super().__init__(component, field, ids)
280
+ self.enum = enum
281
+
282
+ @property
283
+ def enum_str(self) -> str:
284
+ """
285
+ A string representation of the field to which this error applies.
286
+ """
287
+ if isinstance(self.enum, list):
288
+ return ",".join(e.__name__ for e in self.enum)
289
+
290
+ return self.enum.__name__
291
+
292
+ def __eq__(self, other):
293
+ return super().__eq__(other) and self.enum == other.enum
294
+
295
+
296
+ class SameValueError(MultiFieldValidationError):
297
+ """
298
+ The value of two fields is equal.
299
+ E.g. A line has the same from_node as to_node.
300
+ """
301
+
302
+ _message = "Same value for {field} for {n} {objects}."
303
+
304
+
305
+ class NotBooleanError(SingleFieldValidationError):
306
+ """
307
+ Invalid boolean value. Boolean fields don't really exist in our data structure, they are 1-byte signed integers and
308
+ should contain either a 0 (=False) or a 1 (=True).
309
+ """
310
+
311
+ _message = "Field {field} is not a boolean (0 or 1) for {n} {objects}."
312
+
313
+
314
+ class MissingValueError(SingleFieldValidationError):
315
+ """
316
+ A required value was missing, i.e. NaN.
317
+ """
318
+
319
+ _message = "Field {field} is missing for {n} {objects}."
320
+
321
+
322
+ class IdNotInDatasetError(SingleFieldValidationError):
323
+ """
324
+ An object identifier does not exist in the original data.
325
+ E.g. An update data set contains a record for a line that doesn't exist in the input data set.
326
+ """
327
+
328
+ _message = "ID does not exist in {ref_dataset} for {n} {objects}."
329
+ ref_dataset: DatasetType
330
+ __hash__ = None # type: ignore[assignment]
331
+
332
+ def __init__(self, component: ComponentType, ids: list[int], ref_dataset: DatasetType):
333
+ super().__init__(component=component, field="id", ids=ids)
334
+ self.ref_dataset = ref_dataset
335
+
336
+ def __eq__(self, other):
337
+ return super().__eq__(other) and self.ref_dataset == other.ref_dataset
338
+
339
+
340
+ class InvalidIdError(SingleFieldValidationError):
341
+ """
342
+ An object identifier does not refer to the right type of object.
343
+ E.g. An line's from_node refers to a source.
344
+
345
+ Filters can have been applied to check a subset of the records. E.g. This error may apply only to power_sensors
346
+ that are said to be connected to a source (filter: measured_terminal_type=source). This useful to spot validation
347
+ mistakes, due to ambiguity.
348
+
349
+ E.g. when a sym_power_sensor is connected to a load, but measured_terminal_type is accidentally set to 'source',
350
+ the error is:
351
+
352
+ "Field `measured_object` does not contain a valid source id for 1 sym_power_sensor. (measured_terminal_type=source)"
353
+
354
+ """
355
+
356
+ _message = "Field {field} does not contain a valid {ref_components} id for {n} {objects}. {filters}"
357
+ ref_components: list[ComponentType]
358
+ __hash__ = None # type: ignore[assignment]
359
+
360
+ def __init__(
361
+ self,
362
+ component: ComponentType,
363
+ field: str,
364
+ ids: list[int] | None = None,
365
+ ref_components: ComponentType | list[ComponentType] | None = None,
366
+ filters: dict[str, Any] | None = None,
367
+ ):
368
+ super().__init__(component=component, field=field, ids=ids)
369
+ ref_components = ref_components if ref_components is not None else []
370
+ self.ref_components = [ref_components] if isinstance(ref_components, (str, ComponentType)) else ref_components
371
+ self.filters = filters if filters else None
372
+
373
+ @property
374
+ def ref_components_str(self):
375
+ """
376
+ A string representation of the components to which this error applies
377
+ """
378
+ return "/".join(self.ref_components)
379
+
380
+ @property
381
+ def filters_str(self):
382
+ """
383
+ A string representation of the filters that have been applied to the data to which this error refers
384
+ """
385
+ if not self.filters:
386
+ return ""
387
+ filters = ", ".join(f"{k}={v.name if isinstance(v, Enum) else v}" for k, v in self.filters.items())
388
+ return f"({filters})"
389
+
390
+ def __eq__(self, other):
391
+ return super().__eq__(other) and self.ref_components == other.ref_components and self.filters == other.filters
392
+
393
+
394
+ class TwoValuesZeroError(MultiFieldValidationError):
395
+ """
396
+ A record has a 0.0 value in two fields at the same time.
397
+ E.g. A line's `r1`, `x1` are both 0.
398
+ """
399
+
400
+ _message = "Fields {field} are both zero for {n} {objects}."
401
+
402
+
403
+ class ComparisonError(SingleFieldValidationError):
404
+ """
405
+ Base class for comparison errors.
406
+ E.g. A transformer's `i0` is not greater or equal to it's `p0` divided by it's `sn`
407
+ """
408
+
409
+ _message = "Invalid {field}, compared to {ref_value} for {n} {objects}."
410
+
411
+ RefType = int | float | str | tuple[int | float | str, ...]
412
+
413
+ __hash__ = None # type: ignore[assignment]
414
+
415
+ def __init__(self, component: ComponentType, field: str, ids: list[int], ref_value: "ComparisonError.RefType"):
416
+ super().__init__(component, field, ids)
417
+ self.ref_value = ref_value
418
+
419
+ @property
420
+ def ref_value_str(self):
421
+ """
422
+ A string representation of the reference value. E.g. 'zero', 'one', 'field_a and field_b' or '123'.
423
+ """
424
+ if isinstance(self.ref_value, tuple):
425
+ return self._delimiter.join(map(str, self.ref_value))
426
+ if self.ref_value == 0:
427
+ return "zero"
428
+ if self.ref_value == 1:
429
+ return "one"
430
+ return str(self.ref_value)
431
+
432
+ def __eq__(self, other):
433
+ return super().__eq__(other) and self.ref_value == other.ref_value
434
+
435
+
436
+ class NotGreaterThanError(ComparisonError):
437
+ """
438
+ The value of a field is not greater than a reference value or expression.
439
+ """
440
+
441
+ _message = "Field {field} is not greater than {ref_value} for {n} {objects}."
442
+
443
+
444
+ class NotGreaterOrEqualError(ComparisonError):
445
+ """
446
+ The value of a field is not greater or equal to a reference value or expression.
447
+ """
448
+
449
+ _message = "Field {field} is not greater than (or equal to) {ref_value} for {n} {objects}."
450
+
451
+
452
+ class NotLessThanError(ComparisonError):
453
+ """
454
+ The value of a field is not less than a reference value or expression.
455
+ """
456
+
457
+ _message = "Field {field} is not smaller than {ref_value} for {n} {objects}."
458
+
459
+
460
+ class NotLessOrEqualError(ComparisonError):
461
+ """
462
+ The value of a field is not smaller or equal to a reference value or expression.
463
+ """
464
+
465
+ _message = "Field {field} is not smaller than (or equal to) {ref_value} for {n} {objects}."
466
+
467
+
468
+ class NotBetweenError(ComparisonError):
469
+ """
470
+ The value of a field is not between two a reference values or expressions (exclusive).
471
+ """
472
+
473
+ _message = "Field {field} is not between {ref_value} for {n} {objects}."
474
+
475
+
476
+ class NotBetweenOrAtError(ComparisonError):
477
+ """
478
+ The value of a field is not between two a reference values or expressions (inclusive).
479
+ """
480
+
481
+ _message = "Field {field} is not between (or at) {ref_value} for {n} {objects}."
482
+
483
+
484
+ class InfinityError(SingleFieldValidationError):
485
+ """
486
+ The value of a field is infinite.
487
+ """
488
+
489
+ _message = "Field {field} is infinite for {n} {objects}."
490
+
491
+
492
+ class TransformerClockError(MultiFieldValidationError):
493
+ """
494
+ Invalid clock number.
495
+ """
496
+
497
+ _message = (
498
+ "Invalid clock number for {n} {objects}. "
499
+ "If one side has wye winding and the other side has not, the clock number should be odd. "
500
+ "If either both or none of the sides have wye winding, the clock number should be even."
501
+ )
502
+
503
+
504
+ class FaultPhaseError(MultiFieldValidationError):
505
+ """
506
+ The fault phase does not match the fault type.
507
+ """
508
+
509
+ _message = "The fault phase is not applicable to the corresponding fault type for {n} {objects}."
510
+
511
+
512
+ class PQSigmaPairError(MultiFieldValidationError):
513
+ """
514
+ The combination of p_sigma and q_sigma is not valid. They should be both present or both absent.
515
+ """
516
+
517
+ _message = "The combination of p_sigma and q_sigma is not valid for {n} {objects}."
518
+
519
+
520
+ class InvalidAssociatedEnumValueError(MultiFieldValidationError):
521
+ """
522
+ The value is not a valid value in combination with the other specified attributes.
523
+ E.g. When a transformer tap regulator has a branch3 control side but regulates a transformer.
524
+ """
525
+
526
+ _message = "The combination of fields {field} results in invalid {enum} values for {n} {objects}."
527
+ enum: type[Enum] | list[type[Enum]]
528
+ __hash__ = None # type: ignore[assignment]
529
+
530
+ def __init__(
531
+ self,
532
+ component: ComponentType,
533
+ fields: list[str],
534
+ ids: list[int],
535
+ enum: type[Enum] | list[type[Enum]],
536
+ ):
537
+ """
538
+ Args:
539
+ component: Component name
540
+ fields: List of field names
541
+ ids: List of component IDs (not row indices)
542
+ enum: The supported enum values
543
+ """
544
+ super().__init__(component, fields, ids)
545
+ self.enum = enum
546
+
547
+ @property
548
+ def enum_str(self) -> str:
549
+ """
550
+ A string representation of the field to which this error applies.
551
+ """
552
+ if isinstance(self.enum, list):
553
+ return ",".join(e.__name__ for e in self.enum)
554
+
555
+ return self.enum.__name__
556
+
557
+ def __eq__(self, other):
558
+ return super().__eq__(other) and self.enum == other.enum
559
+
560
+
561
+ class UnsupportedMeasuredTerminalType(InvalidValueError):
562
+ """
563
+ The measured terminal type is not a supported value.
564
+
565
+ Supported values are in the supplied list of values.
566
+ """
567
+
568
+ _message = "measured_terminal_type contains unsupported values for {n} {objects}."
569
+
570
+
571
+ class MixedCurrentAngleMeasurementTypeError(MultiFieldValidationError):
572
+ """
573
+ Mixed current angle measurement type error.
574
+ """
575
+
576
+ _message = (
577
+ "Mixture of different current angle measurement types on the same terminal for {n} {objects}. "
578
+ "If multiple current sensors measure the same terminal of the same object, all angle measurement types must be "
579
+ "the same. Mixing local_angle and global_angle current measurements on the same terminal is not supported."
580
+ )
581
+
582
+
583
+ class MixedPowerCurrentSensorError(MultiComponentValidationError):
584
+ """
585
+ Mixed power and current sensor error.
586
+ """
587
+
588
+ _message = (
589
+ "Mixture of power and current sensors on the same terminal for {n} {objects}. "
590
+ "If multiple sensors measure the same terminal of the same object, all sensors must measure the same quantity."
591
+ )
592
+
593
+
594
+ class MissingVoltageAngleMeasurementError(MultiComponentValidationError):
595
+ """
596
+ Missing voltage angle measurement error.
597
+ """
598
+
599
+ _message = (
600
+ "Missing voltage angle measurement for {n} {objects}. "
601
+ "If a voltage sensor measures the voltage of a terminal, it must also measure the voltage angle."
602
+ )