Skip to content

Commit 06e04f1

Browse files
add error handling, finalize tests
Signed-off-by: Laurynas Jagutis <[email protected]>
1 parent 8b5ba14 commit 06e04f1

File tree

5 files changed

+199
-13
lines changed

5 files changed

+199
-13
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ def generate_build_ext(pkg_dir: Path, pkg_name: str):
214214
# list of extensions
215215
exts = [
216216
CTypesExtension(
217-
name="power_grid_model_io_native._core.power_grid_model_io_core",
217+
name="power_grid_model_io_native._core._power_grid_model_io_core",
218218
sources=sources,
219219
include_dirs=include_dirs,
220220
library_dirs=library_dirs,
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <[email protected]>
2+
#
3+
# SPDX-License-Identifier: MPL-2.0
4+
5+
"""
6+
Error handling
7+
"""
8+
9+
import re
10+
11+
from power_grid_model.errors import (
12+
AutomaticTapCalculationError,
13+
ConflictID,
14+
ConflictVoltage,
15+
IDNotFound,
16+
IDWrongType,
17+
InvalidArguments,
18+
InvalidBranch,
19+
InvalidBranch3,
20+
InvalidCalculationMethod,
21+
InvalidMeasuredObject,
22+
InvalidRegulatedObject,
23+
InvalidShortCircuitPhaseOrType,
24+
InvalidTransformerClock,
25+
IterationDiverge,
26+
MaxIterationReached,
27+
MissingCaseForEnumError,
28+
NotObservableError,
29+
PowerGridBatchError,
30+
PowerGridDatasetError,
31+
PowerGridError,
32+
PowerGridNotImplementedError,
33+
PowerGridSerializationError,
34+
PowerGridUnreachableHitError,
35+
SparseMatrixError,
36+
)
37+
38+
from power_grid_model_io_native._core.power_grid_model_io_core import pgm_io_core as pgmic
39+
40+
VALIDATOR_MSG = "\nTry validate_input_data() or validate_batch_data() to validate your data.\n"
41+
# error codes
42+
PGM_NO_ERROR = 0
43+
PGM_REGULAR_ERROR = 1
44+
PGM_BATCH_ERROR = 2
45+
PGM_SERIALIZATION_ERROR = 3
46+
47+
_MISSING_CASE_FOR_ENUM_RE = re.compile(r" is not implemented for (.+) #(-?\d+)!\n")
48+
_INVALID_ARGUMENTS_RE = re.compile(r" is not implemented for ") # multiple different flavors
49+
_CONFLICT_VOLTAGE_RE = re.compile(
50+
r"Conflicting voltage for line (-?\d+)\n voltage at from node (-?\d+) is (.*)\n"
51+
r" voltage at to node (-?\d+) is (.*)\n"
52+
)
53+
_INVALID_BRANCH_RE = re.compile(r"Branch (-?\d+) has the same from- and to-node (-?\d+),\n This is not allowed!\n")
54+
_INVALID_BRANCH3_RE = re.compile(
55+
r"Branch3 (-?\d+) is connected to the same node at least twice. Node 1\/2\/3: (-?\d+)\/(-?\d+)\/(-?\d+),\n"
56+
r" This is not allowed!\n"
57+
)
58+
_INVALID_TRANSFORMER_CLOCK_RE = re.compile(r"Invalid clock for transformer (-?\d+), clock (-?\d+)\n")
59+
_SPARSE_MATRIX_ERROR_RE = re.compile(r"Sparse matrix error") # multiple different flavors
60+
_NOT_OBSERVABLE_ERROR_RE = re.compile(r"Not enough measurements available for state estimation.\n")
61+
_ITERATION_DIVERGE_RE = re.compile(r"Iteration failed to converge") # potentially multiple different flavors
62+
_MAX_ITERATION_REACHED_RE = re.compile(r"Maximum number of iterations reached")
63+
_CONFLICT_ID_RE = re.compile(r"Conflicting id detected: (-?\d+)\n")
64+
_ID_NOT_FOUND_RE = re.compile(r"The id cannot be found: (-?\d+)\n")
65+
_INVALID_MEASURED_OBJECT_RE = re.compile(r"(\w+) measurement is not supported for object of type (\w+)")
66+
_INVALID_REGULATED_OBJECT_RE = re.compile(
67+
r"(\w+) regulator is not supported for object "
68+
) # potentially multiple different flavors
69+
_AUTOMATIC_TAP_CALCULATION_ERROR_RE = re.compile(
70+
r"Automatic tap changing regulator with tap_side at LV side is not supported. Found at id (-?\d+)\n"
71+
)
72+
_ID_WRONG_TYPE_RE = re.compile(r"Wrong type for object with id (-?\d+)\n")
73+
_INVALID_CALCULATION_METHOD_RE = re.compile(r"The calculation method is invalid for this calculation!")
74+
_INVALID_SHORT_CIRCUIT_PHASE_OR_TYPE_RE = re.compile(r"short circuit type") # multiple different flavors
75+
_POWER_GRID_DATASET_ERROR_RE = re.compile(r"Dataset error: ") # multiple different flavors
76+
_POWER_GRID_UNREACHABLE_HIT_RE = re.compile(r"Unreachable code hit when executing ") # multiple different flavors
77+
_POWER_GRID_SEARCH_OPT_INCMPT_RE = re.compile(r"Search method is incompatible with optimization strategy: ")
78+
_POWER_GRID_NOT_IMPLEMENTED_ERROR_RE = re.compile(r"The functionality is either not supported or not yet implemented!")
79+
80+
_ERROR_MESSAGE_PATTERNS = {
81+
_MISSING_CASE_FOR_ENUM_RE: MissingCaseForEnumError,
82+
_INVALID_ARGUMENTS_RE: InvalidArguments,
83+
_CONFLICT_VOLTAGE_RE: ConflictVoltage,
84+
_INVALID_BRANCH_RE: InvalidBranch,
85+
_INVALID_BRANCH3_RE: InvalidBranch3,
86+
_INVALID_TRANSFORMER_CLOCK_RE: InvalidTransformerClock,
87+
_SPARSE_MATRIX_ERROR_RE: SparseMatrixError,
88+
_NOT_OBSERVABLE_ERROR_RE: NotObservableError,
89+
_ITERATION_DIVERGE_RE: IterationDiverge,
90+
_MAX_ITERATION_REACHED_RE: MaxIterationReached,
91+
_CONFLICT_ID_RE: ConflictID,
92+
_ID_NOT_FOUND_RE: IDNotFound,
93+
_INVALID_MEASURED_OBJECT_RE: InvalidMeasuredObject,
94+
_INVALID_REGULATED_OBJECT_RE: InvalidRegulatedObject,
95+
_AUTOMATIC_TAP_CALCULATION_ERROR_RE: AutomaticTapCalculationError,
96+
_ID_WRONG_TYPE_RE: IDWrongType,
97+
_INVALID_CALCULATION_METHOD_RE: InvalidCalculationMethod,
98+
_INVALID_SHORT_CIRCUIT_PHASE_OR_TYPE_RE: InvalidShortCircuitPhaseOrType,
99+
_POWER_GRID_DATASET_ERROR_RE: PowerGridDatasetError,
100+
_POWER_GRID_UNREACHABLE_HIT_RE: PowerGridUnreachableHitError,
101+
_POWER_GRID_SEARCH_OPT_INCMPT_RE: PowerGridUnreachableHitError,
102+
_POWER_GRID_NOT_IMPLEMENTED_ERROR_RE: PowerGridNotImplementedError,
103+
}
104+
105+
106+
def _interpret_error(message: str, decode_error: bool = True) -> PowerGridError:
107+
if decode_error:
108+
for pattern, type_ in _ERROR_MESSAGE_PATTERNS.items():
109+
if pattern.search(message) is not None:
110+
return type_(message)
111+
112+
return PowerGridError(message)
113+
114+
115+
def find_error(batch_size: int = 1, decode_error: bool = True) -> RuntimeError | None:
116+
"""
117+
Check if there is an error and return it
118+
119+
Args:
120+
batch_size: (int, optional): Size of batch. Defaults to 1.
121+
decode_error (bool, optional): Decode the error message(s) to derived error classes. Defaults to True
122+
123+
Returns: error object, can be none
124+
125+
"""
126+
error_code: int = pgmic.error_code()
127+
if error_code == PGM_NO_ERROR:
128+
return None
129+
if error_code == PGM_REGULAR_ERROR:
130+
error_message = pgmic.error_message()
131+
error_message += VALIDATOR_MSG
132+
return _interpret_error(error_message, decode_error=decode_error)
133+
if error_code == PGM_BATCH_ERROR:
134+
_ = batch_size
135+
error_message = "There are errors in the batch calculation." + VALIDATOR_MSG
136+
error = PowerGridBatchError(error_message)
137+
return error
138+
if error_code == PGM_SERIALIZATION_ERROR:
139+
return PowerGridSerializationError(pgmic.error_message())
140+
return RuntimeError("Unknown error!")
141+
142+
143+
def assert_no_error(batch_size: int = 1, decode_error: bool = True):
144+
"""
145+
Assert there is no error in the last operation
146+
If there is an error, raise it
147+
148+
Args:
149+
batch_size (int, optional): Size of batch. Defaults to 1.
150+
decode_error (bool, optional): Decode the error message(s) to derived error classes. Defaults to True
151+
152+
Returns:
153+
154+
"""
155+
error = find_error(batch_size=batch_size, decode_error=decode_error)
156+
if error is not None:
157+
raise error
158+
159+
160+
def handle_errors(
161+
continue_on_batch_error: bool, batch_size: int = 1, decode_error: bool = True
162+
) -> PowerGridBatchError | None:
163+
"""
164+
Handle any errors in the way that is specified.
165+
166+
Args:
167+
continue_on_batch_error (bool): Return the error when the error type is a batch error instead of reraising it.
168+
batch_size (int, optional): Size of batch. Defaults to 1.
169+
decode_error (bool, optional): Decode the error message(s) to derived error classes. Defaults to True
170+
171+
Raises:
172+
error: Any errors previously encountered, unless it was a batch error and continue_on_batch_error was True.
173+
174+
Returns:
175+
PowerGridBatchError | None: None if there were no errors, or the previously encountered
176+
error if it was a batch error and continue_on_batch_error was True.
177+
"""
178+
error: RuntimeError | None = find_error(batch_size=batch_size, decode_error=decode_error)
179+
if error is None:
180+
return None
181+
182+
if continue_on_batch_error and isinstance(error, PowerGridBatchError):
183+
# continue on batch error
184+
return error
185+
186+
# raise normal error
187+
raise error

src/power_grid_model_io_native/_core/power_grid_model_io_core.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from inspect import signature
1010
from itertools import chain
1111
from pathlib import Path
12-
from typing import Callable, Optional
12+
from typing import Callable
1313

1414
from power_grid_model._core.power_grid_core import CharPtr, CStr, IdxC
1515

@@ -40,9 +40,9 @@ def _load_core() -> CDLL:
4040
4141
"""
4242
if platform.system() == "Windows":
43-
dll_file = "power_grid_model_io_core.dll"
43+
dll_file = "_power_grid_model_io_core.dll"
4444
else:
45-
dll_file = "power_grid_model_io_core.so"
45+
dll_file = "_power_grid_model_io_core.so"
4646
cdll = CDLL(str(Path(__file__).parent / dll_file))
4747
# assign return types
4848
# handle
@@ -118,9 +118,9 @@ class PowerGridModelIoCore:
118118
"""
119119

120120
_handle: HandlePtr
121-
_instance: Optional["PowerGridModelIoCore"] = None
121+
_instance: "PowerGridModelIoCore | None" = None
122122

123-
# singleton of power grid core
123+
# singleton of power grid model io core
124124
def __new__(cls, *args, **kwargs):
125125
if cls._instance is None:
126126
cls._instance = super().__new__(cls, *args, **kwargs)

src/power_grid_model_io_native/_core/vnf_converter.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
Power grid model io native converter for vnf files
77
"""
88

9-
from power_grid_model._core.error_handling import assert_no_error
10-
9+
from power_grid_model_io_native._core.error_handling import assert_no_error
1110
from power_grid_model_io_native._core.power_grid_model_io_core import PgmVnfConverterPtr, pgm_io_core as pgmic
1211

1312

tests/unit/test_vnf_converter.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@
44

55
import pytest
66

7+
from power_grid_model_io_native._core.error_handling import InvalidArguments, assert_no_error
78
from power_grid_model_io_native._core.vnf_converter import PgmVnfConverter
89

910

1011
def test_pgmvnfconverter_constructor_without_experimental_features():
1112
"""A test case for creating pgmvnfconverter without experimental features"""
12-
converter = PgmVnfConverter("", 0)
13-
with pytest.raises(OSError):
14-
_ = converter.get_pgm_input_data()
13+
with pytest.raises(InvalidArguments):
14+
_ = PgmVnfConverter("", 0)
1515

1616

1717
def test_pgmvnfconverter_constructor_with_experimental_features():
1818
"""A test case for creating pgmvnfconverter with experimental features"""
19-
converter = PgmVnfConverter("", 1)
20-
_ = converter.get_pgm_input_data()
19+
_ = PgmVnfConverter("", 1)
20+
assert_no_error()
2121

2222

2323
def test_get_pgm_input_data():

0 commit comments

Comments
 (0)