diff --git a/docs/reference/numpy/array-likes.md b/docs/reference/numpy/array-likes.md
index 0288fda5..303e4603 100644
--- a/docs/reference/numpy/array-likes.md
+++ b/docs/reference/numpy/array-likes.md
@@ -309,6 +309,10 @@ matrix-likes, and cuboid-likes, and the `To{}` aliases for "bare" scalar types.
wasn't generic, making it impossible to distinguish between `np.False_` and `np.True_`
using static typing.
+!!! note
+
+ For 4-d array-likes there are only the generic `ToArray4D` and `ToArrayStrict4D`.
+
!!! note
The `ToArrayStrict{1,2,3}D` types are generic since `optype 0.9.1`, analogous to
diff --git a/docs/reference/numpy/shape.md b/docs/reference/numpy/shape.md
index a66fb01c..e5494ca7 100644
--- a/docs/reference/numpy/shape.md
+++ b/docs/reference/numpy/shape.md
@@ -44,9 +44,9 @@ their differences become clear:
-Additionally, there are the four `Array{0,1,2,3}D` aliases, which are
-equivalent to `Array` with `tuple[()]`, `tuple[int]`, `tuple[int, int]` and
-`tuple[int, int, int]` as shape-type, respectively.
+Additionally, there are the five `Array{0,1,2,3,4}D` aliases, which are
+equivalent to `Array` with `tuple[()]`, `tuple[int]`, `tuple[int, int]`,
+`tuple[int, int, int]` and `tuple[int, int, int, int]` as shape-type, respectively.
!!! info
@@ -102,12 +102,12 @@ In the same way as `ArrayND` for `ndarray` (shown for reference), its subtypes
-For masked arrays with specific `ndim`, you could also use one of the four
-`MArray{0,1,2,3}D` aliases.
+For masked arrays with specific `ndim`, you could also use one of the five
+`MArray{0,1,2,3,4}D` aliases.
## Array typeguards
-To check whether a given object is an instance of `Array{0,1,2,3,N}D`, in a way that
+To check whether a given object is an instance of `Array{0,1,2,3,4,N}D`, in a way that
static type-checkers also understand it, the following [PEP 742][PEP742] typeguards can
be used:
@@ -146,6 +146,11 @@ be used:
Array3D[ST] |
tuple[int, int, int] |
+
+ is_array_4d |
+ Array4D[ST] |
+ tuple[int, int, int, int] |
+
These functions additionally accept an optional `dtype` argument, that can either be
diff --git a/optype/numpy/_array.py b/optype/numpy/_array.py
index 534dd776..a557c2fb 100644
--- a/optype/numpy/_array.py
+++ b/optype/numpy/_array.py
@@ -13,11 +13,11 @@
from optype._utils import set_module
__all__ = [ # ruff: ignore[unsorted-dunder-all]
- "Array", "Array0D", "Array1D", "Array2D", "Array3D", "ArrayND",
- "MArray", "MArray0D", "MArray1D", "MArray2D", "MArray3D",
+ "Array", "Array0D", "Array1D", "Array2D", "Array3D", "Array4D", "ArrayND",
+ "MArray", "MArray0D", "MArray1D", "MArray2D", "MArray3D", "MArray4D",
"Matrix",
"CanArray", "CanArray0",
- "CanArrayND", "CanArray0D", "CanArray1D", "CanArray2D", "CanArray3D",
+ "CanArrayND", "CanArray0D", "CanArray1D", "CanArray2D", "CanArray3D", "CanArray4D",
"CanArrayFinalize", "CanArrayWrap",
"HasArrayInterface", "HasArrayPriority",
] # fmt: skip
@@ -148,6 +148,11 @@ def __array__(self, /) -> np.ndarray[_NDT_co, np.dtype[_SCT0_co]]: ...
np.ndarray[tuple[int, int, int], np.dtype[_SCT]],
type_params=(_SCT,),
)
+Array4D = TypeAliasType(
+ "Array4D",
+ np.ndarray[tuple[int, int, int, int], np.dtype[_SCT]],
+ type_params=(_SCT,),
+)
MArray0D = TypeAliasType(
"MArray0D",
@@ -169,6 +174,11 @@ def __array__(self, /) -> np.ndarray[_NDT_co, np.dtype[_SCT0_co]]: ...
np.ma.MaskedArray[tuple[int, int, int], np.dtype[_SCT]],
type_params=(_SCT,),
)
+MArray4D = TypeAliasType(
+ "MArray4D",
+ np.ma.MaskedArray[tuple[int, int, int, int], np.dtype[_SCT]],
+ type_params=(_SCT,),
+)
###########################
@@ -233,6 +243,18 @@ def __len__(self, /) -> int: ...
def __array__(self, /) -> np.ndarray[tuple[int, int, int], np.dtype[_SCT_co]]: ...
+@runtime_checkable
+@set_module("optype.numpy")
+class CanArray4D(Protocol[_SCT_co]):
+ """The 4-d variant of `optype.numpy.CanArrayND`."""
+
+ def __len__(self, /) -> int: ...
+ def __array__(
+ self,
+ /,
+ ) -> np.ndarray[tuple[int, int, int, int], np.dtype[_SCT_co]]: ...
+
+
# this is almost always a `ndarray`, but setting a `bound` might break in some
# edge cases
_T_contra = TypeVar("_T_contra", contravariant=True, default=Any)
diff --git a/optype/numpy/_is.py b/optype/numpy/_is.py
index 7594a14a..2c1fa4d7 100644
--- a/optype/numpy/_is.py
+++ b/optype/numpy/_is.py
@@ -8,7 +8,7 @@
import numpy as np
-from ._array import Array0D, Array1D, Array2D, Array3D, ArrayND
+from ._array import Array0D, Array1D, Array2D, Array3D, Array4D, ArrayND
from ._dtype import ToDType
__all__ = [
@@ -16,6 +16,7 @@
"is_array_1d",
"is_array_2d",
"is_array_3d",
+ "is_array_4d",
"is_array_nd",
"is_dtype",
"is_sctype",
@@ -100,3 +101,12 @@ def is_array_3d(
) -> TypeIs[Array3D[ScalarT]]:
"""Checks if `a` is a 3-d `ndarray` of the given dtype (defaults to `generic`)."""
return is_array_nd(a, dtype) and a.ndim == 3
+
+
+def is_array_4d(
+ a: Any,
+ /,
+ dtype: ToDType[ScalarT] | None = None,
+) -> TypeIs[Array4D[ScalarT]]:
+ """Checks if `a` is a 4-d `ndarray` of the given dtype (defaults to `generic`)."""
+ return is_array_nd(a, dtype) and a.ndim == 4
diff --git a/optype/numpy/_to.py b/optype/numpy/_to.py
index 9883d5a9..cda75e66 100644
--- a/optype/numpy/_to.py
+++ b/optype/numpy/_to.py
@@ -27,6 +27,7 @@
"ToArray1D", "ToArrayStrict1D",
"ToArray2D", "ToArrayStrict2D",
"ToArray3D", "ToArrayStrict3D",
+ "ToArray4D", "ToArrayStrict4D",
"ToArrayND",
"ToFalse", "ToTrue",
@@ -159,6 +160,11 @@ def __array__(self, /) -> np.ndarray[Any, np.dtype[SCT_co]]: ...
nptc.CanArray[tuple[int, int, int], np.dtype[SCT]],
type_params=(SCT,),
)
+ _CanArrayStrict4D = TypeAliasType(
+ "_CanArrayStrict4D",
+ nptc.CanArray[tuple[int, int, int, int], np.dtype[SCT]],
+ type_params=(SCT,),
+ )
else:
@@ -174,6 +180,12 @@ def __array__(self) -> np.ndarray[tuple[int, int], np.dtype[SCT_co]]: ...
class _CanArrayStrict3D(Protocol[SCT_co]):
def __array__(self) -> np.ndarray[tuple[int, int, int], np.dtype[SCT_co]]: ...
+ @runtime_checkable
+ class _CanArrayStrict4D(Protocol[SCT_co]):
+ def __array__(
+ self,
+ ) -> np.ndarray[tuple[int, int, int, int], np.dtype[SCT_co]]: ...
+
type _To1D1[SCT: np.generic] = _CanArrayND[SCT] | Seq[SCT]
type _To1D2[T, SCT: np.generic] = _CanArrayND[SCT] | Seq[T | SCT]
@@ -184,6 +196,8 @@ def __array__(self) -> np.ndarray[tuple[int, int, int], np.dtype[SCT_co]]: ...
type _To3D1[SCT: np.generic] = _CanArrayND[SCT] | Seq[_To2D1[SCT]]
type _To3D2[T, SCT: np.generic] = _CanArrayND[SCT] | Seq[_To2D2[T, SCT]]
+type _To4D2[T, SCT: np.generic] = _CanArrayND[SCT] | Seq[_To3D2[T, SCT]]
+
type _ToND1[SCT: np.generic] = _CanArrayND[SCT] | SeqND[_CanArray[SCT]]
type _ToND2[T, SCT: np.generic] = _CanArrayND[SCT] | SeqND[T | _CanArray[SCT]]
@@ -200,6 +214,10 @@ def __array__(self) -> np.ndarray[tuple[int, int, int], np.dtype[SCT_co]]: ...
_CanArrayStrict3D[SCT] | Seq[_ToStrict2D2[T, SCT]]
)
+type _ToStrict4D2[T, SCT: np.generic] = (
+ _CanArrayStrict4D[SCT] | Seq[_ToStrict3D2[T, SCT]]
+)
+
###
@@ -237,6 +255,7 @@ def __array__(self) -> np.ndarray[tuple[int, int, int], np.dtype[SCT_co]]: ...
ToArray1D = TypeAliasType("ToArray1D", _To1D2[T, SCT], type_params=(T, SCT))
ToArray2D = TypeAliasType("ToArray2D", _To2D2[T, SCT], type_params=(T, SCT))
ToArray3D = TypeAliasType("ToArray3D", _To3D2[T, SCT], type_params=(T, SCT))
+ToArray4D = TypeAliasType("ToArray4D", _To4D2[T, SCT], type_params=(T, SCT))
ToArrayND = TypeAliasType("ToArrayND", _ToND2[T, SCT], type_params=(T, SCT))
type ToFalse = nptc.LiteralFalse | Literal[0]
@@ -390,6 +409,11 @@ def __array__(self) -> np.ndarray[tuple[int, int, int], np.dtype[SCT_co]]: ...
_ToStrict3D2[T, SCT],
type_params=(T, SCT),
)
+ToArrayStrict4D = TypeAliasType(
+ "ToArrayStrict4D",
+ _ToStrict4D2[T, SCT],
+ type_params=(T, SCT),
+)
type ToBoolStrict1D = _ToStrict1D2[_PyBool, np.bool]
type ToBoolStrict2D = _ToStrict2D2[_PyBool, np.bool]
diff --git a/tests/numpy/test_is.py b/tests/numpy/test_is.py
index 26d29d7c..5c4641ce 100644
--- a/tests/numpy/test_is.py
+++ b/tests/numpy/test_is.py
@@ -51,7 +51,7 @@ def test_is_sctype(sctype: type[np.generic]) -> None:
)
@pytest.mark.parametrize("dtype", DTYPES, ids=str)
@pytest.mark.parametrize("ndtype", NDARRAY_TYPES)
-def test_is_array(
+def test_is_array( # ruff: ignore[too-many-statements]
ndtype: type[onp.Array],
dtype: onp.DType,
dtype_map: Callable[[onp.DType], onp.DType | type[np.generic]],
@@ -63,42 +63,49 @@ def test_is_array(
assert not onp.is_array_1d(arr[0])
assert not onp.is_array_2d(arr[0])
assert not onp.is_array_3d(arr[0])
+ assert not onp.is_array_4d(arr[0])
assert onp.is_array_nd(arr[1])
assert not onp.is_array_0d(arr[1])
assert onp.is_array_1d(arr[1])
assert not onp.is_array_2d(arr[1])
assert not onp.is_array_3d(arr[1])
+ assert not onp.is_array_4d(arr[1])
assert onp.is_array_nd(arr[2])
assert not onp.is_array_0d(arr[2])
assert not onp.is_array_1d(arr[2])
assert onp.is_array_2d(arr[2])
assert not onp.is_array_3d(arr[2])
+ assert not onp.is_array_4d(arr[2])
assert onp.is_array_nd(arr[2].view(np.matrix))
assert not onp.is_array_0d(arr[2].view(np.matrix))
assert not onp.is_array_1d(arr[2].view(np.matrix))
assert onp.is_array_2d(arr[2].view(np.matrix))
assert not onp.is_array_3d(arr[2].view(np.matrix))
+ assert not onp.is_array_4d(arr[2].view(np.matrix))
assert onp.is_array_nd(arr[3])
assert not onp.is_array_0d(arr[3])
assert not onp.is_array_1d(arr[3])
assert not onp.is_array_2d(arr[3])
assert onp.is_array_3d(arr[3])
+ assert not onp.is_array_4d(arr[3])
assert onp.is_array_nd(arr[4])
assert not onp.is_array_0d(arr[4])
assert not onp.is_array_1d(arr[4])
assert not onp.is_array_2d(arr[4])
assert not onp.is_array_3d(arr[4])
+ assert onp.is_array_4d(arr[4])
dtype_is = dtype_map(dtype)
assert onp.is_array_0d(arr[0], dtype=dtype_is)
assert onp.is_array_1d(arr[1], dtype=dtype_is)
assert onp.is_array_2d(arr[2], dtype=dtype_is)
assert onp.is_array_3d(arr[3], dtype=dtype_is)
+ assert onp.is_array_4d(arr[4], dtype=dtype_is)
assert onp.is_array_nd(arr[4], dtype=dtype_is)
dtype_not = np.dtype("?") if dtype.char != "?" else np.dtype("B")
@@ -106,4 +113,5 @@ def test_is_array(
assert not onp.is_array_1d(arr[1], dtype=dtype_not)
assert not onp.is_array_2d(arr[2], dtype=dtype_not)
assert not onp.is_array_3d(arr[3], dtype=dtype_not)
+ assert not onp.is_array_4d(arr[4], dtype=dtype_not)
assert not onp.is_array_nd(arr[4], dtype=dtype_not)
diff --git a/tests/numpy/test_to.pyi b/tests/numpy/test_to.pyi
index ac5a323f..82959f49 100644
--- a/tests/numpy/test_to.pyi
+++ b/tests/numpy/test_to.pyi
@@ -101,6 +101,8 @@ type _Arr3_c_co = onp.Array3D[_Sca_c_co] | Seq[_Arr2_c_co]
type _Arr3_f8_co = onp.Array3D[_Sca_f8_co] | Seq[_Arr2_f8_co]
type _Arr3_c16_co = onp.Array3D[_Sca_c16_co] | Seq[_Arr2_c16_co]
+type _Arr4_x = onp.Array4D[_Sca_x] | Seq[_Arr3_x]
+
x_: _Val_x
b_: _Val_b
i_: _Val_i
@@ -166,6 +168,8 @@ c_co_3d: _Arr3_c_co
f8_co_3d: _Arr3_f8_co
c16_co_3d: _Arr3_c16_co
+x_4d: _Arr4_x
+
# scalar
def sca_sca() -> None:
@@ -358,6 +362,19 @@ def s3d_a3d() -> None:
f8_co__f: onp.ToFloat64Strict3D = f_co_3d # type: ignore[assignment] # pyright: ignore[reportAssignmentType]
c16_co__c: onp.ToComplex128Strict3D = c_co_3d # type: ignore[assignment] # pyright: ignore[reportAssignmentType]
+# 4-d
+
+def a4d() -> None:
+ x__sca: onp.ToArray4D = x_ # type: ignore[assignment] # pyright: ignore[reportAssignmentType]
+ x__a3d: onp.ToArray4D = x_3d # type: ignore[assignment] # pyright: ignore[reportAssignmentType]
+ x__a4d: onp.ToArray4D = x_4d
+ x__s4d: onp.ToArrayStrict4D = x_4d
+
+def nd_4d() -> None:
+ to_x_4d: onp.ToArray4D
+
+ x__x: onp.ToArrayND = to_x_4d
+
# n-d
def nd_sca() -> None: