Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4c0d47c
Add is equal methods
Thijss Dec 10, 2025
08c87be
Move logic to parent class
Thijss Dec 10, 2025
e54c04d
Add tests and piggyback move some existing tests
Thijss Dec 10, 2025
7d55785
Add reuse compliance
Thijss Dec 10, 2025
f407842
Revert changes
Thijss Dec 10, 2025
5fae063
Revert changes
Thijss Dec 10, 2025
64e2690
Fix logger
Thijss Dec 10, 2025
8059ec9
Remove redundant set() calls
Thijss Dec 10, 2025
9d8af65
Reduce cognitive complexity
Thijss Dec 10, 2025
cb6715e
cleanup
Thijss Dec 10, 2025
cafd113
remove Grid.is_equal
Thijss Dec 11, 2025
1b673a7
Add reuse compliance
Thijss Dec 11, 2025
6ea6821
revert order
Thijss Dec 11, 2025
85a96e3
cleanup
Thijss Dec 11, 2025
a140758
Merge remote-tracking branch 'origin/main' into feat/add-is-equal
Thijss Jan 14, 2026
7b5606f
Refactor: Move tests so that code folder structure is matched
Thijss Jan 14, 2026
6d666e4
cleanup
Thijss Jan 14, 2026
6eec020
remove module that is not part of this PR
Thijss Jan 14, 2026
06d4068
Merge branch 'refactor-tests' into feat/add-is-equal
Thijss Jan 14, 2026
9e9ec06
remove duplicate test modules
Thijss Jan 14, 2026
270eb4c
Merge branch 'refactor-tests' into feat/add-is-equal
Thijss Jan 14, 2026
ab5cc13
fix import
Thijss Jan 14, 2026
0dfd30b
Merge branch 'refactor-tests' into feat/add-is-equal
Thijss Jan 14, 2026
d5f4568
revert to main
Thijss Jan 14, 2026
52f37b5
merge main
Thijss Jan 14, 2026
c569c8f
Apply suggestion from @Thijss
Thijss Jan 14, 2026
4ba6ad0
rename parameter
Thijss Jan 14, 2026
166b960
docs: add docs on equals method
jaapschoutenalliander Jan 14, 2026
23ae7ce
Merge branch 'feat/add-is-equal' of https://github.com/PowerGridModel…
jaapschoutenalliander Jan 14, 2026
0d04de6
rename parameter
Thijss Jan 14, 2026
3308643
Merge branch 'feat/add-is-equal' of https://github.com/PowerGridModel…
Thijss Jan 14, 2026
51dc096
provide updated parameter
Thijss Jan 14, 2026
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
64 changes: 45 additions & 19 deletions docs/examples/model/grid_examples.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
{
"cell_type": "code",
"execution_count": 1,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -43,7 +43,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -67,7 +67,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand Down Expand Up @@ -101,7 +101,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand Down Expand Up @@ -129,7 +129,7 @@
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand Down Expand Up @@ -162,7 +162,7 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand Down Expand Up @@ -192,7 +192,7 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -213,7 +213,7 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -233,7 +233,7 @@
},
{
"cell_type": "code",
"execution_count": 9,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -256,7 +256,7 @@
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -277,7 +277,7 @@
},
{
"cell_type": "code",
"execution_count": 11,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -302,7 +302,7 @@
},
{
"cell_type": "code",
"execution_count": 12,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -323,7 +323,7 @@
},
{
"cell_type": "code",
"execution_count": 13,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -348,7 +348,7 @@
},
{
"cell_type": "code",
"execution_count": 14,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -373,7 +373,7 @@
},
{
"cell_type": "code",
"execution_count": 15,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -396,7 +396,7 @@
},
{
"cell_type": "code",
"execution_count": 16,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -417,7 +417,7 @@
},
{
"cell_type": "code",
"execution_count": 17,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -431,11 +431,37 @@
"source": [
"After deactivating the branch, its `from_status` or `to_status` is set to inactive, and the branch is removed from the active graph.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Comparing Grids\n",
"\n",
"Use Python's equality operator to quickly check whether two grids contain identical array data. The comparison inspects every array stored on the container and ignores differences inside `grid.graphs`, so it is perfect for validating serialized data or fixtures. If any field differs, the expression immediately returns `False`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from power_grid_model_ds import Grid\n",
"\n",
"reference_grid = Grid.from_txt(\"1 2\", \"2 3 line\")\n",
"candidate_grid = Grid.from_txt(\"1 2\", \"2 3 line\")\n",
"\n",
"print(f\"Grid equality (arrays only) before changes: {reference_grid == candidate_grid}\")\n",
"\n",
"candidate_grid.make_inactive(candidate_grid.line.get(4))\n",
"print(f\"Grid equality after deactivating line 4: {reference_grid == candidate_grid}\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"display_name": ".venv (3.12.6)",
"language": "python",
"name": "python3"
},
Expand All @@ -449,7 +475,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
"version": "3.12.6"
}
},
"nbformat": 4,
Expand Down
6 changes: 6 additions & 0 deletions src/power_grid_model_ds/_core/model/containers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from power_grid_model_ds._core.model.arrays.base.array import FancyArray
from power_grid_model_ds._core.model.arrays.base.errors import RecordDoesNotExist
from power_grid_model_ds._core.model.constants import EMPTY_ID
from power_grid_model_ds._core.model.containers.helpers import container_equal

Self = TypeVar("Self", bound="FancyArrayContainer")

Expand All @@ -29,6 +30,11 @@ class FancyArrayContainer:

_id_counter: int

def __eq__(self, other) -> bool:
if not isinstance(other, self.__class__):
return False
return container_equal(self, other, ignore_extras=False, early_exit=True)

@property
def id_counter(self):
"""Returns the private _id_counter field (as read-only)"""
Expand Down
102 changes: 102 additions & 0 deletions src/power_grid_model_ds/_core/model/containers/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <[email protected]>
#
# SPDX-License-Identifier: MPL-2.0
import logging
from dataclasses import Field, fields
from typing import TYPE_CHECKING

from power_grid_model_ds._core.model.arrays.base.array import FancyArray
from power_grid_model_ds._core.utils.misc import array_equal_with_nan

if TYPE_CHECKING:
from power_grid_model_ds._core.model.grids.base import FancyArrayContainer

logger = logging.getLogger("power_grid_model_ds.fancypy") # public location of the container_equal function


def container_equal(
container_a: "FancyArrayContainer",
container_b: "FancyArrayContainer",
ignore_extras: bool = False,
early_exit: bool = True,
fields_to_ignore: list[str] = None,
) -> bool:
"""
Compares two containers for equality.

Args:
container_a: The first container to compare.
container_b: The second container to compare.
ignore_extras:
If True,
ignores fields present in one container_a but not in container_b.
ignores extra columns in arrays in container_b that are not present in container_a.
early_exit: If True, returns False on the first detected difference. False to log all differences as debug.
fields_to_ignore: A list of field names to exclude from comparison.

Returns:
True if the containers are equal, False otherwise.
"""
fields_to_ignore = fields_to_ignore or []
is_equal = True

for field in fields(container_a):
if field.name in fields_to_ignore or (ignore_extras and not hasattr(container_b, field.name)):
continue

if not _fields_are_equal(container_a, container_b, field, ignore_extras):
is_equal = False
if early_exit:
return False

if not ignore_extras and _check_for_extra_fields(container_a, container_b):
return False

return is_equal


def _fields_are_equal(
container_a: "FancyArrayContainer", container_b: "FancyArrayContainer", field: "Field", ignore_extras: bool
) -> bool:
"""Compares a single field between two containers."""
value_a = getattr(container_a, field.name)
value_b = getattr(container_b, field.name)
class_name = container_a.__class__.__name__

if isinstance(value_a, FancyArray):
if not _check_array_equal(value_a, value_b, ignore_extras):
logger.debug(f"Array field '{field.name}' differs between {class_name}s.")
return False
elif value_a != value_b:
logger.debug(f"Field '{field.name}' differs between {class_name}s.")
return False

return True


def _check_for_extra_fields(container_a: "FancyArrayContainer", container_b: "FancyArrayContainer") -> bool:
"""Checks if container_b has extra fields not present in container_a."""
fields_a = {f.name for f in fields(container_a)}
fields_b = {f.name for f in fields(container_b)}
extra_fields = fields_b - fields_a

if extra_fields:
logger.debug(f"Container {container_b.__class__.__name__} has extra fields: {extra_fields}")
return True
return False


def _check_array_equal(array_a: FancyArray, array_b: FancyArray, ignore_extras: bool) -> bool:
"""
Compares two FancyArrays, optionally ignoring extra columns in array_b.
NaN values are treated as equal.
"""
data_a = array_a.data
data_b = array_b.data

if ignore_extras:
common_columns = [col for col in array_a.columns if col in array_b.columns]
data_a = array_a[common_columns]
data_b = array_b[common_columns]

return array_equal_with_nan(data_a, data_b)
17 changes: 12 additions & 5 deletions src/power_grid_model_ds/_core/model/grids/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from dataclasses import dataclass
from pathlib import Path
from typing import Self, Type, TypeVar
from typing import Any, Self, Type, TypeVar

import numpy as np
import numpy.typing as npt
Expand All @@ -29,14 +29,12 @@
)
from power_grid_model_ds._core.model.arrays.base.array import FancyArray
from power_grid_model_ds._core.model.containers.base import FancyArrayContainer
from power_grid_model_ds._core.model.containers.helpers import container_equal
from power_grid_model_ds._core.model.graphs.container import GraphContainer
from power_grid_model_ds._core.model.graphs.models import RustworkxGraphModel
from power_grid_model_ds._core.model.graphs.models.base import BaseGraphModel
from power_grid_model_ds._core.model.grids._feeders import set_feeder_ids
from power_grid_model_ds._core.model.grids._helpers import (
create_empty_grid,
create_grid_from_extended_grid,
)
from power_grid_model_ds._core.model.grids._helpers import create_empty_grid, create_grid_from_extended_grid
from power_grid_model_ds._core.model.grids._modify import (
add_array_to_grid,
add_branch,
Expand Down Expand Up @@ -109,6 +107,15 @@ def __str__(self) -> str:
"""
return serialize_to_str(self)

def __eq__(self, other: Any) -> bool:
"""Check if two grids are equal.

Note: differences in graphs are ignored in this comparison.
"""
if not isinstance(other, self.__class__):
return False
return container_equal(self, other, ignore_extras=False, early_exit=True, fields_to_ignore=["graphs"])

@classmethod
def empty(cls: Type[G], graph_model: type[BaseGraphModel] = RustworkxGraphModel) -> G:
"""Create an empty grid
Expand Down
Loading