-
Notifications
You must be signed in to change notification settings - Fork 6
feat: grid serialization #100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 12 commits
9c3a161
1491e14
6e11a3d
d401ef6
f629e6a
b77f8e3
108b934
a6f0cbe
af87381
89fa6ee
e838550
942e22b
ec8c44c
c588156
8bcf0e7
b1da441
c35e081
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,104 @@ | ||||||
| # SPDX-FileCopyrightText: Contributors to the Power Grid Model project <[email protected]> | ||||||
| # | ||||||
| # SPDX-License-Identifier: MPL-2.0 | ||||||
|
|
||||||
| """Serialization utilities for Grid objects using power-grid-model serialization with extensions support.""" | ||||||
|
|
||||||
| import dataclasses | ||||||
| import json | ||||||
| import logging | ||||||
| from pathlib import Path | ||||||
| from typing import Dict, Optional | ||||||
|
|
||||||
| from power_grid_model_ds._core.model.arrays.base.array import FancyArray | ||||||
| from power_grid_model_ds._core.model.grids.base import Grid | ||||||
|
|
||||||
| logger = logging.getLogger(__name__) | ||||||
|
|
||||||
|
|
||||||
| def _restore_grid_values(grid, input_data: Dict) -> None: | ||||||
| """Restore arrays to the grid.""" | ||||||
| for attr_name, attr_values in input_data.items(): | ||||||
| if not hasattr(grid, attr_name): | ||||||
| continue | ||||||
|
|
||||||
| if not issubclass(getattr(grid, attr_name).__class__, FancyArray): | ||||||
| setattr(grid, attr_name, attr_values) | ||||||
|
||||||
| continue | ||||||
|
|
||||||
| try: | ||||||
| array_field = grid.find_array_field(getattr(grid, attr_name).__class__) | ||||||
| matched_columns = { | ||||||
Thijss marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| col: attr_values["data"][col] for col in array_field.type().columns if col in attr_values["data"] | ||||||
| } | ||||||
| restored_array = array_field.type(**matched_columns) | ||||||
| setattr(grid, attr_name, restored_array) | ||||||
| except (AttributeError, KeyError, ValueError, TypeError) as e: | ||||||
| # Handle restoration failures: | ||||||
| # - KeyError: missing "dtype" or "data" keys | ||||||
| # - ValueError/TypeError: invalid dtype string or data conversion | ||||||
| # - AttributeError: grid methods/attributes missing | ||||||
| logger.warning(f"Failed to restore '{attr_name}': {e}") | ||||||
|
|
||||||
|
|
||||||
| def save_grid_to_json( | ||||||
|
||||||
| def save_grid_to_json( | |
| def serialize( |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would crash here if not FancyArray for now instead of skipping. Or maybe this should be an option to skip?
jaapschoutenalliander marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we just add an kwargs argument and pass it to json.dump? Instead of picking indent specifically.
json.dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False, **kw)
jaapschoutenalliander marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps just:
| def load_grid_from_json(path: Path, target_grid_class=None): | |
| def deserialize(path: Path, target_grid_class=None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now that would work, but if we later add another serialisation option having load_grid_from... works better
jaapschoutenalliander marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ik mis tests voor wanneer het crasht. |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,215 @@ | ||||||||||||
| # SPDX-FileCopyrightText: Contributors to the Power Grid Model project <[email protected]> | ||||||||||||
| # | ||||||||||||
| # SPDX-License-Identifier: MPL-2.0 | ||||||||||||
|
|
||||||||||||
| """Comprehensive unit tests for Grid serialization with power-grid-model compatibility.""" | ||||||||||||
|
|
||||||||||||
| from dataclasses import dataclass | ||||||||||||
| from pathlib import Path | ||||||||||||
| from tempfile import TemporaryDirectory | ||||||||||||
|
|
||||||||||||
| import numpy as np | ||||||||||||
| import pytest | ||||||||||||
| from numpy.typing import NDArray | ||||||||||||
|
|
||||||||||||
| from power_grid_model_ds import Grid | ||||||||||||
| from power_grid_model_ds._core.model.arrays.base.array import FancyArray | ||||||||||||
| from power_grid_model_ds._core.utils.serialization import ( | ||||||||||||
| load_grid_from_json, | ||||||||||||
| save_grid_to_json, | ||||||||||||
| ) | ||||||||||||
| from power_grid_model_ds.arrays import LineArray | ||||||||||||
| from power_grid_model_ds.arrays import NodeArray as BaseNodeArray | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class ExtendedNodeArray(BaseNodeArray): | ||||||||||||
| """Test array with extended columns""" | ||||||||||||
|
|
||||||||||||
| _defaults = {"u": 0.0, "analysis_flag": 0} | ||||||||||||
| u: NDArray[np.float64] | ||||||||||||
| analysis_flag: NDArray[np.int32] | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class ExtendedLineArray(LineArray): | ||||||||||||
| """Test array with extended columns""" | ||||||||||||
|
|
||||||||||||
| _defaults = {"i_from": 0.0, "loading_factor": 0.0} | ||||||||||||
| i_from: NDArray[np.float64] | ||||||||||||
| loading_factor: NDArray[np.float64] | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @dataclass | ||||||||||||
| class ExtendedGrid(Grid): | ||||||||||||
| """Test grid with extended arrays""" | ||||||||||||
|
|
||||||||||||
| node: ExtendedNodeArray | ||||||||||||
| line: ExtendedLineArray | ||||||||||||
|
|
||||||||||||
| value_extension: float = 0.0 | ||||||||||||
| str_extension: str = "default" | ||||||||||||
| complex_extension: list = None | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @pytest.fixture | ||||||||||||
| def temp_dir(): | ||||||||||||
| """Temporary directory fixture""" | ||||||||||||
| with TemporaryDirectory() as tmp_dir: | ||||||||||||
| yield Path(tmp_dir) | ||||||||||||
jaapschoutenalliander marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @pytest.fixture | ||||||||||||
| def basic_grid(): | ||||||||||||
| """Basic grid fixture""" | ||||||||||||
| return Grid.from_txt("1 2", "2 3", "S10 1") | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @pytest.fixture | ||||||||||||
| def extended_grid(): | ||||||||||||
| """Extended grid fixture with additional columns""" | ||||||||||||
| grid = ExtendedGrid.empty() | ||||||||||||
| nodes = ExtendedNodeArray( | ||||||||||||
| id=[1, 2, 3], u_rated=[10500, 10500, 10500], u=[10450, 10400, 10350], analysis_flag=[1, 0, 1] | ||||||||||||
| ) | ||||||||||||
| lines = ExtendedLineArray( | ||||||||||||
| id=[10, 11], | ||||||||||||
| from_node=[1, 2], | ||||||||||||
| to_node=[2, 3], | ||||||||||||
| from_status=[1, 1], | ||||||||||||
| to_status=[1, 1], | ||||||||||||
| r1=[0.1, 0.15], | ||||||||||||
| x1=[0.2, 0.25], | ||||||||||||
| c1=[1e-6, 1.2e-6], | ||||||||||||
| tan1=[0.0, 0.0], | ||||||||||||
| i_n=[400, 350], | ||||||||||||
| i_from=[150.5, 120.3], | ||||||||||||
| loading_factor=[0.75, 0.68], | ||||||||||||
| ) | ||||||||||||
| grid.append(nodes) | ||||||||||||
| grid.append(lines) | ||||||||||||
| return grid | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class TestSerializationFormats: | ||||||||||||
jaapschoutenalliander marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||
| """Test serialization across different formats and configurations""" | ||||||||||||
|
|
||||||||||||
| def test_basic_serialization_roundtrip(self, basic_grid: Grid, temp_dir: Path): | ||||||||||||
| """Test basic serialization roundtrip for all formats""" | ||||||||||||
| path = temp_dir / "test.json" | ||||||||||||
| result_path = save_grid_to_json(basic_grid, path) | ||||||||||||
| assert result_path.exists() | ||||||||||||
|
|
||||||||||||
| # Load and verify | ||||||||||||
| loaded_grid = load_grid_from_json(path, target_grid_class=Grid) | ||||||||||||
| assert loaded_grid.node.size == basic_grid.node.size | ||||||||||||
| assert loaded_grid.line.size == basic_grid.line.size | ||||||||||||
| assert list(loaded_grid.node.id) == list(basic_grid.node.id) | ||||||||||||
|
||||||||||||
| assert loaded_grid.node.size == basic_grid.node.size | |
| assert loaded_grid.line.size == basic_grid.line.size | |
| assert list(loaded_grid.node.id) == list(basic_grid.node.id) | |
| assert_array_equal(loaded_grid.node.data, basic_grid.node.data) | |
| assert_array_equal(loaded_grid.line.data, basic_grid.line.data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated to use the array_equal from pgm_ds where applicable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
commenting here, but it is about the uv.lock. I don't think we should expect any changes?
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.