Skip to content

Commit 98e1fd3

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/add-basic-arrays
2 parents cc8a0c6 + 54a4061 commit 98e1fd3

File tree

10 files changed

+82
-52
lines changed

10 files changed

+82
-52
lines changed

.github/workflows/check-code-quality.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
uses: actions/checkout@v5
3131

3232
- name: Install uv
33-
uses: astral-sh/setup-uv@v6
33+
uses: astral-sh/setup-uv@v7
3434
if: ${{ always() }}
3535
with:
3636
activate-environment: true

.github/workflows/ci.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
uses: actions/checkout@v5
3939

4040
- name: Install uv
41-
uses: astral-sh/setup-uv@v6
41+
uses: astral-sh/setup-uv@v7
4242
if: ${{ always() }}
4343
with:
4444
activate-environment: true
@@ -62,7 +62,7 @@ jobs:
6262
uv build
6363
6464
- name: Store built wheel file
65-
uses: actions/upload-artifact@v4
65+
uses: actions/upload-artifact@v5
6666
with:
6767
name: power-grid-model-ds
6868
path: dist/
@@ -81,7 +81,7 @@ jobs:
8181
uses: actions/checkout@v5
8282

8383
- name: Install uv
84-
uses: astral-sh/setup-uv@v6
84+
uses: astral-sh/setup-uv@v7
8585
if: ${{ always() }}
8686
with:
8787
activate-environment: true
@@ -91,7 +91,7 @@ jobs:
9191
run: uv tool install poethepoet
9292

9393
- name: Load built wheel file
94-
uses: actions/download-artifact@v4
94+
uses: actions/download-artifact@v6
9595
with:
9696
name: power-grid-model-ds
9797
path: dist/
@@ -117,7 +117,7 @@ jobs:
117117
uses: actions/checkout@v5
118118

119119
- name: Load built wheel file
120-
uses: actions/download-artifact@v4
120+
uses: actions/download-artifact@v6
121121
with:
122122
name: power-grid-model-ds
123123
path: dist/

.github/workflows/citations.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ jobs:
3030
runs-on: ubuntu-24.04
3131
steps:
3232
- name: checkout
33-
uses: actions/checkout@v4
33+
uses: actions/checkout@v5
34+
3435
- name: Validate CITATION.cff
3536
uses: dieghernan/cff-validator@v4

.github/workflows/reuse-compliance.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ jobs:
2020
- name: checkout
2121
uses: actions/checkout@v5
2222
- name: REUSE Compliance Check
23-
uses: fsfe/reuse-action@v5
23+
uses: fsfe/reuse-action@v6

.github/workflows/sonar.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
uses: actions/checkout@v5
2727

2828
- name: Install uv
29-
uses: astral-sh/setup-uv@v6
29+
uses: astral-sh/setup-uv@v7
3030
if: ${{ always() }}
3131
with:
3232
activate-environment: true
@@ -46,7 +46,7 @@ jobs:
4646
4747
- name: SonarCloud Scan
4848
if: ${{ (github.event_name == 'push') || (github.event.pull_request.head.repo.owner.login == 'PowerGridModel') }}
49-
uses: SonarSource/sonarqube-scan-action@v5
49+
uses: SonarSource/sonarqube-scan-action@v6
5050
env:
5151
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5252
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

docs/model_interface.ipynb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,8 @@
265265
"outputs": [],
266266
"source": [
267267
"all_paths = grid.graphs.active_graph.get_all_paths(56, 41)\n",
268-
"components = grid.graphs.active_graph.get_components(substation_nodes=np.array([1, 2, 3]))\n",
268+
"with grid.graphs.active_graph.tmp_remove_nodes([1, 2, 3]):\n",
269+
" components = grid.graphs.active_graph.get_components()\n",
269270
"connected = grid.graphs.active_graph.get_connected(node_id=56)"
270271
]
271272
}

src/power_grid_model_ds/_core/model/arrays/base/array.py

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from collections import namedtuple
77
from copy import copy
88
from functools import lru_cache
9-
from typing import Any, Iterable, Literal, Type, TypeVar
9+
from typing import Any, Iterable, Literal, Type, TypeVar, overload
1010

1111
import numpy as np
1212
from numpy.typing import ArrayLike, NDArray
@@ -59,14 +59,14 @@ class FancyArray(ABC):
5959
_defaults: dict[str, Any] = {}
6060
_str_lengths: dict[str, int] = {}
6161

62-
def __init__(self: Self, *args, data: NDArray | None = None, **kwargs):
62+
def __init__(self, *args, data: NDArray | None = None, **kwargs):
6363
if data is None:
6464
self._data = build_array(*args, dtype=self.get_dtype(), defaults=self.get_defaults(), **kwargs)
6565
else:
6666
self._data = data
6767

6868
@property
69-
def data(self: Self) -> NDArray:
69+
def data(self) -> NDArray:
7070
return self._data
7171

7272
@classmethod
@@ -110,7 +110,7 @@ def get_dtype(cls):
110110
dtype_list.append((name, dtype))
111111
return np.dtype(dtype_list)
112112

113-
def __repr__(self: Self) -> str:
113+
def __repr__(self) -> str:
114114
try:
115115
data = getattr(self, "data")
116116
if data.size > 3:
@@ -125,7 +125,7 @@ def __str__(self) -> str:
125125
def __len__(self) -> int:
126126
return len(self._data)
127127

128-
def __iter__(self: Self):
128+
def __iter__(self):
129129
for record in self._data:
130130
yield self.__class__(data=np.array([record]))
131131

@@ -152,20 +152,33 @@ def __setattr__(self: Self, attr: str, value: object) -> None:
152152
except (AttributeError, ValueError) as error:
153153
raise AttributeError(f"Cannot set attribute {attr} on {self.__class__.__name__}") from error
154154

155-
def __getitem__(self: Self, item):
156-
"""Used by for-loops, slicing [0:3], column-access ['id'], row-access [0], multi-column access.
157-
Note: If a single item is requested, return a named tuple instead of a np.void object.
158-
"""
159-
160-
result = self._data.__getitem__(item)
161-
162-
if isinstance(item, (list, tuple)) and (len(item) == 0 or np.array(item).dtype.type is np.bool_):
163-
return self.__class__(data=result)
164-
if isinstance(item, (str, list, tuple)):
165-
return result
166-
if isinstance(result, np.void):
167-
return self.__class__(data=np.array([result]))
168-
return self.__class__(data=result)
155+
@overload
156+
def __getitem__(
157+
self: Self, item: slice | int | NDArray[np.bool_] | list[bool] | NDArray[np.int_] | list[int]
158+
) -> Self: ...
159+
160+
@overload
161+
def __getitem__(self, item: str | NDArray[np.str_] | list[str]) -> NDArray[Any]: ...
162+
163+
def __getitem__(self, item):
164+
if isinstance(item, slice | int):
165+
new_data = self._data[item]
166+
if new_data.shape == ():
167+
new_data = np.array([new_data])
168+
return self.__class__(data=new_data)
169+
if isinstance(item, str):
170+
return self._data[item]
171+
if (isinstance(item, np.ndarray) and item.size == 0) or (isinstance(item, list | tuple) and len(item) == 0):
172+
return self.__class__(data=self._data[[]])
173+
if isinstance(item, list | np.ndarray):
174+
item_array = np.array(item)
175+
if item_array.dtype == np.bool_ or np.issubdtype(item_array.dtype, np.int_):
176+
return self.__class__(data=self._data[item_array])
177+
if np.issubdtype(item_array.dtype, np.str_):
178+
return self._data[item_array.tolist()]
179+
raise NotImplementedError(
180+
f"FancyArray[{type(item).__name__}] is not supported. Try FancyArray.data[{type(item).__name__}] instead."
181+
)
169182

170183
def __setitem__(self: Self, key, value):
171184
if isinstance(value, FancyArray):
@@ -177,16 +190,18 @@ def __contains__(self: Self, item: Self) -> bool:
177190
return item.data in self._data
178191
return False
179192

180-
def __hash__(self: Self):
193+
def __hash__(self):
181194
return hash(f"{self.__class__} {self}")
182195

183-
def __eq__(self: Self, other):
184-
return self._data.__eq__(other.data)
196+
def __eq__(self, other):
197+
if not isinstance(other, self.__class__):
198+
return False
199+
return self.data.__eq__(other.data)
185200

186-
def __copy__(self: Self):
201+
def __copy__(self):
187202
return self.__class__(data=copy(self._data))
188203

189-
def copy(self: Self):
204+
def copy(self):
190205
"""Return a copy of this array including its data"""
191206
return copy(self)
192207

@@ -281,15 +296,15 @@ def get(
281296
return self.__class__(data=apply_get(*args, array=self._data, mode_=mode_, **kwargs))
282297

283298
def filter_mask(
284-
self: Self,
299+
self,
285300
*args: int | Iterable[int] | np.ndarray,
286301
mode_: Literal["AND", "OR"] = "AND",
287302
**kwargs: Any | list[Any] | np.ndarray,
288303
) -> np.ndarray:
289304
return get_filter_mask(*args, array=self._data, mode_=mode_, **kwargs)
290305

291306
def exclude_mask(
292-
self: Self,
307+
self,
293308
*args: int | Iterable[int] | np.ndarray,
294309
mode_: Literal["AND", "OR"] = "AND",
295310
**kwargs: Any | list[Any] | np.ndarray,
@@ -299,7 +314,7 @@ def exclude_mask(
299314
def re_order(self: Self, new_order: ArrayLike, column: str = "id") -> Self:
300315
return self.__class__(data=re_order(self._data, new_order, column=column))
301316

302-
def update_by_id(self: Self, ids: ArrayLike, allow_missing: bool = False, **kwargs) -> None:
317+
def update_by_id(self, ids: ArrayLike, allow_missing: bool = False, **kwargs) -> None:
303318
try:
304319
_ = update_by_id(self._data, ids, allow_missing, **kwargs)
305320
except ValueError as error:
@@ -312,13 +327,13 @@ def get_updated_by_id(self: Self, ids: ArrayLike, allow_missing: bool = False, *
312327
except ValueError as error:
313328
raise ValueError(f"Cannot update {self.__class__.__name__}. {error}") from error
314329

315-
def check_ids(self: Self, return_duplicates: bool = False) -> NDArray | None:
330+
def check_ids(self, return_duplicates: bool = False) -> NDArray | None:
316331
return check_ids(self._data, return_duplicates=return_duplicates)
317332

318-
def as_table(self: Self, column_width: int | str = "auto", rows: int = 10) -> str:
333+
def as_table(self, column_width: int | str = "auto", rows: int = 10) -> str:
319334
return convert_array_to_string(self, column_width=column_width, rows=rows)
320335

321-
def as_df(self: Self):
336+
def as_df(self):
322337
"""Convert to pandas DataFrame"""
323338
if pandas is None:
324339
raise ImportError("pandas is not installed")

src/power_grid_model_ds/_core/model/graphs/models/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ def __init__(self, active_only=False) -> None:
2929

3030
@property
3131
@abstractmethod
32-
def nr_nodes(self):
32+
def nr_nodes(self) -> int:
3333
"""Returns the number of nodes in the graph"""
3434

3535
@property
3636
@abstractmethod
37-
def nr_branches(self):
37+
def nr_branches(self) -> int:
3838
"""Returns the number of branches in the graph"""
3939

4040
@property
@@ -251,7 +251,7 @@ def get_all_paths(self, ext_start_node_id: int, ext_end_node_id: int) -> list[li
251251
return [self._internals_to_externals(path) for path in internal_paths]
252252

253253
def get_components(self) -> list[list[int]]:
254-
"""Returns all separate components when the substation_nodes are removed of the graph as lists
254+
"""Returns all separate components of the graph as lists
255255
256256
If you want to get the components of the graph without certain nodes,
257257
use the `tmp_remove_nodes` context manager.

src/power_grid_model_ds/_core/model/graphs/models/rustworkx.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ def __init__(self, active_only=False) -> None:
2626
self._external_to_internal: dict[int, int] = {}
2727

2828
@property
29-
def nr_nodes(self):
29+
def nr_nodes(self) -> int:
3030
return self._graph.num_nodes()
3131

3232
@property
33-
def nr_branches(self):
33+
def nr_branches(self) -> int:
3434
return self._graph.num_edges()
3535

3636
@property

tests/unit/model/arrays/test_array.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ def test_getitem_unique_multiple_columns(fancy_test_array: FancyTestArray):
7474
assert np.array_equal(np.unique(fancy_test_array[columns]), fancy_test_array[columns])
7575

7676

77+
def test_getitem_array_index(fancy_test_array: FancyTestArray):
78+
assert fancy_test_array[0].data.tolist() == fancy_test_array.data[0:1].tolist()
79+
80+
81+
def test_getitem_array_nested_index(fancy_test_array: FancyTestArray):
82+
nested_array = fancy_test_array[0][0][0][0][0][0]
83+
assert isinstance(nested_array, FancyArray)
84+
assert nested_array.data.shape == (1,)
85+
assert nested_array.data.tolist() == fancy_test_array.data[0:1].tolist()
86+
87+
7788
def test_getitem_array_slice(fancy_test_array: FancyTestArray):
7889
assert fancy_test_array.data[0:2].tolist() == fancy_test_array[0:2].tolist()
7990

@@ -84,18 +95,20 @@ def test_getitem_with_array_mask(fancy_test_array: FancyTestArray):
8495
assert np.array_equal(fancy_test_array.data[mask], fancy_test_array[mask].data)
8596

8697

87-
def test_getitem_with_tuple_mask(fancy_test_array: FancyTestArray):
88-
mask = (True, False, True)
89-
assert isinstance(fancy_test_array[mask], FancyArray)
90-
assert np.array_equal(fancy_test_array.data[mask], fancy_test_array[mask].data)
91-
92-
9398
def test_getitem_with_list_mask(fancy_test_array: FancyTestArray):
9499
mask = [True, False, True]
95100
assert isinstance(fancy_test_array[mask], FancyArray)
96101
assert np.array_equal(fancy_test_array.data[mask], fancy_test_array[mask].data)
97102

98103

104+
def test_getitem_with_tuple_mask(fancy_test_array: FancyTestArray):
105+
# Numpy gives unexpected results with tuple masks. Therefore, we raise NotImplementedError here.
106+
# e.g: np.array([1,2,3])[(True, False, True)] returns an empty array (array([], shape=(0, 3), dtype=int64)
107+
mask = (True, False, True)
108+
with pytest.raises(NotImplementedError):
109+
fancy_test_array[mask] # type: ignore[call-overload] # noqa
110+
111+
99112
def test_getitem_with_empty_list_mask():
100113
array = FancyTestArray()
101114
mask = []

0 commit comments

Comments
 (0)