Skip to content

Commit f5f9519

Browse files
authored
Merge pull request #126 from alliander-opensource/feature/extra_info
Remove extra_info
2 parents dd649e6 + 2396b17 commit f5f9519

File tree

8 files changed

+72
-194
lines changed

8 files changed

+72
-194
lines changed

.clang-format

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ AllowShortLoopsOnASingleLine: false
1515
BreakBeforeBraces: Custom
1616
BraceWrapping:
1717
BeforeCatch: true
18-
BeforeElse: true
18+
BeforeElse: true
19+

.pre-commit-config.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ repos:
1919
rev: v0.971
2020
hooks:
2121
- id: mypy
22-
files: ^(src|tests|scripts)/.+\.py$
2322
- repo: local
2423
hooks:
2524
- id: pylint
@@ -35,3 +34,8 @@ repos:
3534
language: system
3635
pass_filenames: false
3736
always_run: true
37+
- repo: https://github.com/pre-commit/mirrors-clang-format
38+
rev: v14.0.6
39+
hooks:
40+
- id: clang-format
41+
types_or: [ c++ ]

include/power_grid_model/auxiliary/meta_data.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,16 @@ struct MetaData {
208208
throw UnknownAttributeName{attr_name};
209209
}
210210

211+
bool has_attr(std::string const& attr_name) const {
212+
try {
213+
find_attr(attr_name);
214+
}
215+
catch (const UnknownAttributeName&) {
216+
return false;
217+
}
218+
return true;
219+
}
220+
211221
void* get_position(void* ptr, Idx position) const {
212222
return reinterpret_cast<char*>(ptr) + position * size;
213223
}

src/power_grid_model/data_types.py

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@
66
have been defined and explained in this file
77
"""
88

9-
from typing import Any, Dict, List, Tuple, Union
9+
from typing import Dict, List, Tuple, Union
1010

1111
import numpy as np
1212

13-
# When we're dropping python 3.8, we should introduce
14-
# SingleArray = np.ndarray (ndim=1)
15-
# DenseBatchArray = np.ndarray (ndim=2)
13+
# When we're dropping python 3.8, we should introduce proper NumPy type hinting
1614

1715
SparseBatchArray = Dict[str, np.ndarray]
1816
"""
@@ -97,15 +95,14 @@
9795
nominal: 123
9896
real: 10500.0
9997
asym: (10400.0, 10500.0, 10600.0)
100-
10198
"""
10299

103-
Component = Dict[str, AttributeValue]
100+
Component = Dict[str, Union[AttributeValue, str]]
104101
"""
105102
A component, when represented in native python format, is a dictionary, where the keys are the attributes and the values
106-
are the corresponding values.
103+
are the corresponding values. It is allowed to add extra fields, containing either an AttributeValue or a string.
107104
108-
Example: {"id": 1, "u_rated": 10500.0}
105+
Example: {"id": 1, "u_rated": 10500.0, "original_id": "Busbar #1"}
109106
"""
110107

111108
ComponentList = List[Component]
@@ -166,16 +163,3 @@
166163
}
167164
]
168165
"""
169-
170-
ExtraInfo = Dict[int, Any]
171-
"""
172-
Extra info is a dictionary that contains information about the objects. It is indexed on the object IDs and the
173-
actual information can be anything.
174-
175-
Example:
176-
{
177-
1: "First Node",
178-
2: "Second Node",
179-
3: {"name": "Cable", "material": "Aluminum"}
180-
}
181-
"""

src/power_grid_model/utils.py

Lines changed: 35 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import json
1010
from pathlib import Path
11-
from typing import IO, Any, List, Optional, Union, cast
11+
from typing import IO, Any, List, Optional, cast
1212

1313
import numpy as np
1414

@@ -17,11 +17,8 @@
1717
BatchArray,
1818
BatchDataset,
1919
BatchList,
20-
BatchPythonDataset,
2120
ComponentList,
2221
Dataset,
23-
ExtraInfo,
24-
Nominal,
2522
PythonDataset,
2623
SingleDataset,
2724
SinglePythonDataset,
@@ -101,12 +98,13 @@ def convert_list_to_batch_data(list_data: BatchList) -> BatchDataset:
10198
return batch_data
10299

103100

104-
def convert_python_to_numpy(data: PythonDataset, data_type: str) -> Dataset:
101+
def convert_python_to_numpy(data: PythonDataset, data_type: str, ignore_extra: bool = False) -> Dataset:
105102
"""
106103
Convert native python data to internal numpy
107104
Args:
108105
data: data in dict or list
109106
data_type: type of data: input, update, sym_output, or asym_output
107+
ignore_extra: Allow (and ignore) extra attributes in the data
110108
111109
Returns:
112110
A single or batch dataset for power-grid-model
@@ -118,23 +116,27 @@ def convert_python_to_numpy(data: PythonDataset, data_type: str) -> Dataset:
118116
# data for all batches in converted into a proper and compact numpy structure.
119117
if isinstance(data, list):
120118
list_data = [
121-
convert_python_single_dataset_to_single_dataset(json_dict, data_type=data_type) for json_dict in data
119+
convert_python_single_dataset_to_single_dataset(json_dict, data_type=data_type, ignore_extra=ignore_extra)
120+
for json_dict in data
122121
]
123122
return convert_list_to_batch_data(list_data)
124123

125124
# Otherwise this should be a normal (non-batch) structure, with a list of objects (dictionaries) per component.
126125
if not isinstance(data, dict):
127126
raise TypeError("Data should be either a list or a dictionary!")
128127

129-
return convert_python_single_dataset_to_single_dataset(data=data, data_type=data_type)
128+
return convert_python_single_dataset_to_single_dataset(data=data, data_type=data_type, ignore_extra=ignore_extra)
130129

131130

132-
def convert_python_single_dataset_to_single_dataset(data: SinglePythonDataset, data_type: str) -> SingleDataset:
131+
def convert_python_single_dataset_to_single_dataset(
132+
data: SinglePythonDataset, data_type: str, ignore_extra: bool = False
133+
) -> SingleDataset:
133134
"""
134135
Convert native python data to internal numpy
135136
Args:
136137
data: data in dict
137138
data_type: type of data: input, update, sym_output, or asym_output
139+
ignore_extra: Allow (and ignore) extra attributes in the data
138140
139141
Returns:
140142
A single dataset for power-grid-model
@@ -143,18 +145,23 @@ def convert_python_single_dataset_to_single_dataset(data: SinglePythonDataset, d
143145

144146
dataset: SingleDataset = {}
145147
for component, objects in data.items():
146-
dataset[component] = convert_component_list_to_numpy(objects=objects, component=component, data_type=data_type)
148+
dataset[component] = convert_component_list_to_numpy(
149+
objects=objects, component=component, data_type=data_type, ignore_extra=ignore_extra
150+
)
147151

148152
return dataset
149153

150154

151-
def convert_component_list_to_numpy(objects: ComponentList, component: str, data_type: str) -> np.ndarray:
155+
def convert_component_list_to_numpy(
156+
objects: ComponentList, component: str, data_type: str, ignore_extra: bool = False
157+
) -> np.ndarray:
152158
"""
153159
Convert native python data to internal numpy
154160
Args:
155161
objects: data in dict
156162
component: the name of the component
157163
data_type: type of data: input, update, sym_output, or asym_output
164+
ignore_extra: Allow (and ignore) extra attributes in the data
158165
159166
Returns:
160167
A single numpy array
@@ -168,19 +175,19 @@ def convert_component_list_to_numpy(objects: ComponentList, component: str, data
168175
# As each object is a separate dictionary, and the attributes may differ per object, we need to check
169176
# all attributes. Non-existing attributes
170177
for attribute, value in obj.items():
171-
if attribute == "extra":
172-
# The "extra" attribute is a special one. It can store any type of information associated with
173-
# an object, but it will not be used in the calculations. Therefore it is not included in the
174-
# numpy array, so we can skip this attribute
175-
continue
176178

179+
# If an attribute doesn't exist, the user should explicitly state that she/he is ok with extra
180+
# information in the data. This is to protect the user from overlooking errors.
177181
if attribute not in array.dtype.names:
178-
# If an attribute doesn't exist, the user made a mistake. Let's be merciless in that case,
179-
# for their own good.
180-
raise ValueError(f"Invalid attribute '{attribute}' for {component} {data_type} data.")
181-
182-
# Now just assign the value and raise an error if the value cannot be stored in the specific
183-
# numpy array data format for this attribute.
182+
if ignore_extra:
183+
continue
184+
raise ValueError(
185+
f"Invalid attribute '{attribute}' for {component} {data_type} data. "
186+
"(Use ignore_extra=True to ignore the extra data and suppress this exception)"
187+
)
188+
189+
# Assign the value and raise an error if the value cannot be stored in the specific numpy array data format
190+
# for this attribute.
184191
try:
185192
array[i][attribute] = value
186193
except ValueError as ex:
@@ -419,19 +426,20 @@ def convert_single_dataset_to_python_single_dataset(data: SingleDataset) -> Sing
419426
}
420427

421428

422-
def import_json_data(json_file: Path, data_type: str) -> Dataset:
429+
def import_json_data(json_file: Path, data_type: str, ignore_extra: bool = False) -> Dataset:
423430
"""
424431
import json data
425432
Args:
426433
json_file: path to the json file
427434
data_type: type of data: input, update, sym_output, or asym_output
435+
ignore_extra: Allow (and ignore) extra attributes in the json file
428436
429437
Returns:
430438
A single or batch dataset for power-grid-model
431439
"""
432440
with open(json_file, mode="r", encoding="utf-8") as file_pointer:
433-
json_data = json.load(file_pointer)
434-
return convert_python_to_numpy(json_data, data_type)
441+
data = json.load(file_pointer)
442+
return convert_python_to_numpy(data=data, data_type=data_type, ignore_extra=ignore_extra)
435443

436444

437445
def import_input_data(json_file: Path) -> SingleDataset:
@@ -461,29 +469,19 @@ def import_update_data(json_file: Path) -> BatchDataset:
461469
return cast(BatchDataset, import_json_data(json_file=json_file, data_type="update"))
462470

463471

464-
def export_json_data(
465-
json_file: Path,
466-
data: Dataset,
467-
indent: Optional[int] = 2,
468-
compact: bool = False,
469-
extra_info: Optional[Union[ExtraInfo, List[ExtraInfo]]] = None,
470-
):
472+
def export_json_data(json_file: Path, data: Dataset, indent: Optional[int] = 2, compact: bool = False):
471473
"""
472474
export json data
473475
Args:
474476
json_file: path to json file
475477
data: a single or batch dataset for power-grid-model
476478
indent: indent of the file, default 2
477479
compact: write components on a single line
478-
extra_info: extra information (in any json-serializable format), indexed on the object ids
479-
e.g. a string representing the original id, or a dictionary storing even more information.
480480
481481
Returns:
482482
Save to file
483483
"""
484484
json_data = convert_dataset_to_python_dataset(data)
485-
if extra_info is not None:
486-
inject_extra_info(data=json_data, extra_info=extra_info)
487485

488486
with open(json_file, mode="w", encoding="utf-8") as file_pointer:
489487
if compact and indent:
@@ -494,70 +492,14 @@ def export_json_data(
494492
json.dump(json_data, file_pointer, indent=indent)
495493

496494

497-
def inject_extra_info(data: PythonDataset, extra_info: Union[ExtraInfo, List[ExtraInfo]]):
498-
"""
499-
Injects extra info to the objects by ID
500-
501-
Args:
502-
data: Power Grid Model Python data, as written to pgm json files.
503-
extra_info: A dictionary indexed by object id. The value may be anything.
504-
505-
"""
506-
if isinstance(data, list):
507-
_inject_extra_info_batch(data=data, extra_info=extra_info)
508-
elif isinstance(data, dict):
509-
_inject_extra_info_single(data=data, extra_info=cast(ExtraInfo, extra_info))
510-
else:
511-
raise TypeError("Invalid data type")
512-
513-
514-
def _inject_extra_info_single(data: SinglePythonDataset, extra_info: ExtraInfo):
515-
"""
516-
Injects extra info to the objects by ID
517-
518-
Args:
519-
data: Power Grid Model Python data, as written to pgm json files.
520-
extra_info: A dictionary indexed by object id. The value may be anything.
521-
522-
"""
523-
if not isinstance(extra_info, dict):
524-
raise TypeError("Invalid extra info data type")
525-
526-
for _, objects in data.items():
527-
for obj in objects:
528-
if obj["id"] in extra_info:
529-
# IDs are always nominal values, so let's tell the type checker:
530-
obj_id = cast(Nominal, obj["id"])
531-
obj["extra"] = extra_info[obj_id]
532-
533-
534-
def _inject_extra_info_batch(data: BatchPythonDataset, extra_info: Union[ExtraInfo, List[ExtraInfo]]):
535-
"""
536-
Injects extra info to the objects by ID
537-
538-
Args:
539-
data: Power Grid Model Python data, as written to pgm json files.
540-
extra_info: A dictionary indexed by object id. The value may be anything.
541-
542-
"""
543-
if isinstance(extra_info, list):
544-
# If both data and extra_info are lists, expect one extra info set per batch
545-
for batch, info in zip(data, extra_info):
546-
_inject_extra_info_single(batch, info)
547-
else:
548-
# If only data is a list, copy extra_info for each batch
549-
for batch in data:
550-
_inject_extra_info_single(batch, extra_info)
551-
552-
553495
def compact_json_dump(data: Any, io_stream: IO[str], indent: int, max_level: int, level: int = 0):
554496
"""Custom compact JSON writer that is intended to put data belonging to a single object on a single line.
555497
556498
For example:
557499
{
558500
"node": [
559-
{"id": 0, "u_rated": 10500.0, "extra": {"original_id": 123}},
560-
{"id": 1, "u_rated": 10500.0, "extra": {"original_id": 456}},
501+
{"id": 0, "u_rated": 10500.0},
502+
{"id": 1, "u_rated": 10500.0},
561503
],
562504
"line": [
563505
{"id": 2, "node_from": 0, "node_to": 1, ...}

tests/cpp_unit_tests/test_validation.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ struct Buffer {
5050
void parse_single_object(void* ptr, json const& j, MetaData const& meta, Idx position) {
5151
meta.set_nan(ptr, position);
5252
for (auto const& it : j.items()) {
53-
// skip extra info
54-
if (it.key() == "extra") {
53+
// Allow and skip unknown attributes
54+
if (!meta.has_attr(it.key())) {
5555
continue;
5656
}
57-
DataAttribute const& attr = meta.find_attr(it.key());
57+
DataAttribute attr = meta.find_attr(it.key());
5858
if (attr.numpy_type == "i1") {
5959
int8_t const value = it.value().get<int8_t>();
6060
meta.set_attr(ptr, &value, attr, position);

0 commit comments

Comments
 (0)