Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 3 additions & 19 deletions src/power_grid_model_ds/_core/fancypy.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import numpy as np

from power_grid_model_ds._core.utils.misc import array_equal_with_nan

if TYPE_CHECKING:
from power_grid_model_ds._core.model.arrays.base.array import FancyArray

Expand Down Expand Up @@ -44,23 +46,5 @@ def sort(array: "FancyArray", axis=-1, kind=None, order=None) -> "FancyArray":
def array_equal(array1: "FancyArray", array2: "FancyArray", equal_nan: bool = True) -> bool:
"""Return True if two arrays are equal."""
if equal_nan:
return _array_equal_with_nan(array1, array2)
return array_equal_with_nan(array1.data, array2.data)
return np.array_equal(array1.data, array2.data)


def _array_equal_with_nan(array1: "FancyArray", array2: "FancyArray") -> bool:
# np.array_equal does not work with NaN values in structured arrays, so we need to compare column by column.
# related issue: https://github.com/numpy/numpy/issues/21539

if array1.columns != array2.columns:
return False

for column in array1.columns:
column_dtype = array1.dtype[column]
if np.issubdtype(column_dtype, np.str_):
if not np.array_equal(array1[column], array2[column]):
return False
continue
if not np.array_equal(array1[column], array2[column], equal_nan=True):
return False
return True
8 changes: 8 additions & 0 deletions src/power_grid_model_ds/_core/model/arrays/base/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,11 @@ def as_df(self: Self):
if pandas is None:
raise ImportError("pandas is not installed")
return pandas.DataFrame(self._data)

@classmethod
def from_extended(cls: Type[Self], extended: Self) -> Self:
"""Create an instance from an extended array."""
if not isinstance(extended, cls):
raise TypeError(f"Extended array must be of type {cls.__name__}, got {type(extended).__name__}")
dtype = cls.get_dtype()
return cls(data=np.array(extended[list(dtype.names)], dtype=dtype))
18 changes: 18 additions & 0 deletions src/power_grid_model_ds/_core/model/grids/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,24 @@ def set_feeder_ids(self):
set_is_feeder(grid=self)
set_feeder_ids(grid=self)

@classmethod
def from_extended(cls, extended: "Grid") -> "Grid":
"""Create a grid from an extended Grid object."""
new_grid = cls.empty()

# Add nodes first, so that branches can reference them
new_grid.append(new_grid.node.__class__.from_extended(extended.node))

for field in dataclasses.fields(cls):
if field.name == "node":
continue # already added
if issubclass(field.type, FancyArray):
extended_array = getattr(extended, field.name)
new_array = field.type.from_extended(extended_array)
new_grid.append(new_array, check_max_id=False)

return new_grid


def _add_branch_array(branch: BranchArray | Branch3Array, grid: Grid):
"""Add a branch array to the grid"""
Expand Down
21 changes: 21 additions & 0 deletions src/power_grid_model_ds/_core/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,24 @@ def get_inherited_attrs(cls: Type, *private_attributes):
retrieved_attributes[private_attr] = attr_dict

return retrieved_attributes


def array_equal_with_nan(array1: np.ndarray, array2: np.ndarray) -> bool:
"""Compare two structured arrays for equality, treating NaN values as equal.

np.array_equal does not work with NaN values in structured arrays, so we need to compare column by column.
related issue: https://github.com/numpy/numpy/issues/21539
"""
if array1.dtype.names != array2.dtype.names:
return False

columns: Sequence[str] = array1.dtype.names
for column in columns:
column_dtype = array1.dtype[column]
if np.issubdtype(column_dtype, np.str_):
if not np.array_equal(array1[column], array2[column]):
return False
continue
if not np.array_equal(array1[column], array2[column], equal_nan=True):
return False
return True
17 changes: 17 additions & 0 deletions tests/fixtures/arrays.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import numpy as np
from numpy._typing import NDArray

from power_grid_model_ds._core.model.arrays import LineArray, NodeArray
from power_grid_model_ds._core.model.arrays.base.array import FancyArray
from power_grid_model_ds._core.model.dtypes.sensors import NDArray3

Expand Down Expand Up @@ -57,3 +58,19 @@ class FancyTestArray3(FancyArray):

test_float1: NDArray3[np.float64]
test_float2: NDArray3[np.float64]


class ExtendedNodeArray(NodeArray):
"""Extends the node array with an output value"""

_defaults = {"u": 0}

u: NDArray[np.float64]


class ExtendedLineArray(LineArray):
"""Extends the line array with an output value"""

_defaults = {"i_from": 0}

i_from: NDArray[np.float64]
5 changes: 4 additions & 1 deletion tests/fixtures/grid_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
from dataclasses import dataclass

from power_grid_model_ds._core.model.grids.base import Grid
from tests.fixtures.arrays import ExtendedLineArray, ExtendedNodeArray


@dataclass
class ExtendedGrid(Grid):
"""Grid with an extra container"""
"""ExtendedGrid class for testing purposes."""

node: ExtendedNodeArray
line: ExtendedLineArray
extra_value: int = 123
16 changes: 7 additions & 9 deletions tests/fixtures/grids.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@

from power_grid_model_ds._core.model.arrays import (
LineArray,
LinkArray,
NodeArray,
SourceArray,
SymLoadArray,
ThreeWindingTransformerArray,
TransformerArray,
)
from power_grid_model_ds._core.model.enums.nodes import NodeType
from power_grid_model_ds._core.model.grids.base import Grid
Expand Down Expand Up @@ -44,18 +42,18 @@ def build_basic_grid(grid: T) -> T:
# ***

# Add Substations
substation = NodeArray(id=[101], u_rated=[10_500.0], node_type=[NodeType.SUBSTATION_NODE.value])
substation = grid.node.__class__(id=[101], u_rated=[10_500.0], node_type=[NodeType.SUBSTATION_NODE.value])
grid.append(substation, check_max_id=False)

# Add Nodes
nodes = NodeArray(
nodes = grid.node.__class__(
id=[102, 103, 104, 105, 106],
u_rated=[10_500.0] * 4 + [400.0],
)
grid.append(nodes, check_max_id=False)

# Add Lines
lines = LineArray(
lines = grid.line.__class__(
id=[201, 202, 203, 204],
from_status=[1, 1, 0, 1],
to_status=[1, 1, 0, 1],
Expand All @@ -70,7 +68,7 @@ def build_basic_grid(grid: T) -> T:
grid.append(lines, check_max_id=False)

# Add a transformer
transformer = TransformerArray.empty(1)
transformer = grid.transformer.__class__.empty(1)
transformer.id = 301
transformer.from_status = 1
transformer.to_status = 1
Expand All @@ -80,7 +78,7 @@ def build_basic_grid(grid: T) -> T:
grid.append(transformer, check_max_id=False)

# Add a link
link = LinkArray.empty(1)
link = grid.link.__class__.empty(1)
link.id = 601
link.from_status = 1
link.to_status = 1
Expand All @@ -90,7 +88,7 @@ def build_basic_grid(grid: T) -> T:
grid.append(link, check_max_id=False)

# Loads
loads = SymLoadArray(
loads = grid.sym_load.__class__(
id=[401, 402, 403, 404],
node=[102, 103, 104, 105],
type=[1] * 4,
Expand All @@ -101,7 +99,7 @@ def build_basic_grid(grid: T) -> T:
grid.append(loads, check_max_id=False)

# Add Source
source = SourceArray(id=[501], node=[101], status=[1], u_ref=[0.0])
source = grid.source.__class__(id=[501], node=[101], status=[1], u_ref=[0.0])
grid.append(source, check_max_id=False)
grid.check_ids()

Expand Down
17 changes: 1 addition & 16 deletions tests/integration/loadflow/test_power_grid_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,12 @@
)
from power_grid_model_ds._core.model.arrays.pgm_arrays import TransformerTapRegulatorArray
from power_grid_model_ds._core.model.grids.base import Grid
from tests.fixtures.arrays import ExtendedLineArray, ExtendedNodeArray
from tests.unit.model.grids.test_custom_grid import CustomGrid

# pylint: disable=missing-function-docstring,missing-class-docstring


class ExtendedNodeArray(NodeArray):
"""Extends the node array with an output value"""

_defaults = {"u": 0}

u: NDArray[np.float64]


class ExtendedLineArray(LineArray):
"""Extends the line array with an output value"""

_defaults = {"i_from": 0}

i_from: NDArray[np.float64]


def test_load_flow_on_random():
"""Tests the power flow on a randomly configured grid"""
grid_generator = RadialGridGenerator(grid_class=Grid, nr_nodes=5, nr_sources=1, nr_nops=0)
Expand Down
18 changes: 16 additions & 2 deletions tests/unit/model/arrays/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@

from power_grid_model_ds._core import fancypy as fp
from power_grid_model_ds._core.model.arrays.base.array import FancyArray
from power_grid_model_ds._core.model.arrays.pgm_arrays import TransformerArray
from power_grid_model_ds._core.model.arrays.pgm_arrays import LineArray, TransformerArray
from power_grid_model_ds._core.model.constants import EMPTY_ID, empty
from power_grid_model_ds._core.utils.misc import array_equal_with_nan
from tests.conftest import FancyTestArray
from tests.fixtures.arrays import FancyTestArray3
from tests.fixtures.arrays import ExtendedLineArray, FancyTestArray3

# pylint: disable=missing-function-docstring

Expand Down Expand Up @@ -289,3 +290,16 @@ def test_overflow_value():
with pytest.raises(OverflowError):
transformer.tap_min = -167
assert transformer.tap_min == -128


def test_from_extended_array():
extended_array = ExtendedLineArray.empty(3)
extended_array.id = [1, 2, 3]
extended_array.from_node = [4, 5, 6]
extended_array.to_node = [7, 8, 9]
extended_array.from_status = [1, 0, 1]
extended_array.from_status = [0, 1, 0]

array = LineArray.from_extended(extended_array)
assert not isinstance(array, ExtendedLineArray)
array_equal_with_nan(array.data, extended_array[array.columns])
16 changes: 16 additions & 0 deletions tests/unit/model/grids/test_grid_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import numpy as np
import pytest
from numpy.ma.testutils import assert_array_equal

from power_grid_model_ds._core.model.arrays import (
LineArray,
Expand All @@ -20,6 +21,7 @@
from power_grid_model_ds._core.model.constants import EMPTY_ID
from power_grid_model_ds._core.model.grids.base import Grid
from tests.fixtures.grid_classes import ExtendedGrid
from tests.fixtures.grids import build_basic_grid

# pylint: disable=missing-function-docstring,missing-class-docstring

Expand Down Expand Up @@ -50,6 +52,20 @@ def test_initialize_empty_extended_grid():
assert isinstance(grid, ExtendedGrid)


def test_from_extended_grid():
extended_grid = build_basic_grid(ExtendedGrid.empty())
grid = Grid.from_extended(extended_grid)
assert not isinstance(grid, ExtendedGrid)
assert_array_equal(grid.line.data, extended_grid.line.data[grid.line.columns])
assert grid.node.size
assert grid.branches.size
assert grid.graphs.active_graph.nr_nodes == len(grid.node)
assert grid.graphs.complete_graph.nr_nodes == len(grid.branches)

assert extended_grid.id_counter == grid.id_counter
assert extended_grid.max_id == grid.max_id


def test_grid_build(basic_grid: Grid):
grid = basic_grid

Expand Down
8 changes: 7 additions & 1 deletion tests/unit/utils/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import numpy as np

from power_grid_model_ds._core.utils.misc import is_sequence
from power_grid_model_ds._core.utils.misc import array_equal_with_nan, is_sequence

# pylint: disable=missing-function-docstring

Expand All @@ -31,3 +31,9 @@ def test_dict_is_not_a_sequence():

def test_string_is_not_a_sequence():
assert not is_sequence("abc")


def test_array_equal_with_nan():
array1 = np.array([(1, 2.0, "a"), (3, np.nan, "b")], dtype=[("col1", "i4"), ("col2", "f4"), ("col3", "U1")])
array2 = np.array([(1, 2.0, "a"), (3, np.nan, "b")], dtype=[("col1", "i4"), ("col2", "f4"), ("col3", "U1")])
assert array_equal_with_nan(array1, array2)
Loading