diff --git a/src/coordinax/_src/base/charts.py b/src/coordinax/_src/base/charts.py index 58705cfac..a1b9022e0 100644 --- a/src/coordinax/_src/base/charts.py +++ b/src/coordinax/_src/base/charts.py @@ -364,6 +364,22 @@ def __init_subclass__(cls, **kw: Any) -> None: cls._coord_dimensions = _get_tuple(args[2]) break + # Check the component count matches the declared dimension flag, + # but only when the chart mixes in an `AbstractDimensionalFlag` with + # a fixed integer `n` (skip the variable-`n` flag, e.g. `CartND` + # whose `_chart_ndim` is `"N"`, and charts with no flag at all). + ndim = getattr(cls, "_chart_ndim", None) + if ( + isinstance(ndim, int) + and hasattr(cls, "_components") + and len(cls._components) != ndim + ): + msg = ( + f"{cls.__name__} is declared {ndim}D but has " + f"{len(cls._components)} components {cls._components}" + ) + raise TypeError(msg) + super().__init_subclass__(**kw) # AbstractChart has. @property @@ -389,9 +405,15 @@ class AbstractDimensionalFlag: """ + #: Declared coordinate dimension of the flag (set when ``n`` is given). + _chart_ndim: ClassVar[int | L["N"]] + def __init_subclass__(cls, n: int | L["N"] | None = None, **kw: Any) -> None: if n is not None: DIMENSIONAL_FLAGS[n] = cls + # Record the declared dimension so concrete fixed-component charts + # can validate their component count against it. + cls._chart_ndim = n # Enforce that this is a subclass of AbstractChart unless it's an # abstract base class (name starts with "Abstract") diff --git a/src/coordinax/_src/charts/d0.py b/src/coordinax/_src/charts/d0.py index ee3a576e2..1f00b243f 100644 --- a/src/coordinax/_src/charts/d0.py +++ b/src/coordinax/_src/charts/d0.py @@ -31,8 +31,6 @@ class Abstract0D(AbstractDimensionalFlag, n=0): A 0D representation has no coordinate component. """ - # TODO: add a check it's 0D - @override def __init_subclass__(cls, n: int | L["N"] | None = None, **kw: Any) -> None: # Enforce that this is a subclass of AbstractChart diff --git a/src/coordinax/_src/charts/d1.py b/src/coordinax/_src/charts/d1.py index 9b7e81bc2..f5270404c 100644 --- a/src/coordinax/_src/charts/d1.py +++ b/src/coordinax/_src/charts/d1.py @@ -49,8 +49,6 @@ class Abstract1D(AbstractDimensionalFlag, n=1): Cartesian $(x)$ or radial $(r)$ coordinates. """ - # TODO: add a check it's 1D - @override def __init_subclass__(cls, n: int | L["N"] | None = None, **kw: Any) -> None: # Enforce that this is a subclass of AbstractChart diff --git a/src/coordinax/_src/charts/d2.py b/src/coordinax/_src/charts/d2.py index 6505200dd..41f0e8144 100644 --- a/src/coordinax/_src/charts/d2.py +++ b/src/coordinax/_src/charts/d2.py @@ -35,8 +35,6 @@ class Abstract2D(AbstractDimensionalFlag, n=2): two angular coordinates but represents a curved surface. """ - # TODO: add a check it's 2D - @override def __init_subclass__(cls, n: int | L["N"] | None = None, **kw: Any) -> None: # Enforce that this is a subclass of AbstractChart diff --git a/src/coordinax/_src/charts/d3.py b/src/coordinax/_src/charts/d3.py index 4ab0ec702..be7e60a76 100644 --- a/src/coordinax/_src/charts/d3.py +++ b/src/coordinax/_src/charts/d3.py @@ -54,8 +54,6 @@ class Abstract3D(AbstractDimensionalFlag, n=3): (cylindrical, spherical), or other three-dimensional manifolds. """ - # TODO: add a check it's 3D - @override def __init_subclass__(cls, n: int | L["N"] | None = None, **kw: Any) -> None: # Enforce that this is a subclass of AbstractChart diff --git a/src/coordinax/_src/charts/d4.py b/src/coordinax/_src/charts/d4.py index 2afb1a6a2..19d203ac6 100644 --- a/src/coordinax/_src/charts/d4.py +++ b/src/coordinax/_src/charts/d4.py @@ -17,8 +17,6 @@ class Abstract4D(AbstractDimensionalFlag, n=4): example is the Minkowski spacetime chart ``(ct, x, y, z)``. """ - # TODO: add a check it's 4D - @override def __init_subclass__(cls, n: int | L["N"] | None = None, **kw: Any) -> None: # Enforce that this is a subclass of AbstractChart diff --git a/src/coordinax/_src/charts/d6.py b/src/coordinax/_src/charts/d6.py index 7030d828c..7b6cc9431 100644 --- a/src/coordinax/_src/charts/d6.py +++ b/src/coordinax/_src/charts/d6.py @@ -28,8 +28,6 @@ class Abstract6D(AbstractDimensionalFlag, n=6): Examples include Cartesian representations in arbitrary dimensions. """ - # TODO: add a check it's 6D - @override def __init_subclass__(cls, n: int | L["N"] | None = None, **kw: Any) -> None: # Enforce that this is a subclass of AbstractChart diff --git a/src/coordinax/vectors/_src/__init__.py b/src/coordinax/vectors/_src/__init__.py index 15c600441..b6dc3203a 100644 --- a/src/coordinax/vectors/_src/__init__.py +++ b/src/coordinax/vectors/_src/__init__.py @@ -1,6 +1,5 @@ """Vectors.""" -from .api import * from .base import * from .bundle import * from .constants import * diff --git a/src/coordinax/vectors/_src/api.py b/src/coordinax/vectors/_src/api.py deleted file mode 100644 index 67c432c2f..000000000 --- a/src/coordinax/vectors/_src/api.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Copyright (c) 2023 coordinax maintainers. All rights reserved.""" - -__all__ = ("normalize_vector",) - -from typing import Any - -import plum - - -@plum.dispatch.abstract -def normalize_vector(x: Any, /) -> Any: - """Return the unit vector.""" - raise NotImplementedError # pragma: no cover diff --git a/tests/unit/charts/test_base.py b/tests/unit/charts/test_base.py index 589877dee..96d243a50 100644 --- a/tests/unit/charts/test_base.py +++ b/tests/unit/charts/test_base.py @@ -181,3 +181,50 @@ def test_flags_must_subclass_chart(self) -> None: for _flag_cls in cxc.DIMENSIONAL_FLAGS.values(): # All registered flags should have chart subclasses pass # Registration enforces this + + def test_component_count_must_match_declared_dimension(self) -> None: + """A concrete chart's component count must match its dimension flag.""" + import dataclasses + + from typing import Literal + + import jax.tree_util as jtu + + from coordinax._src.base import ( + MT, + AbstractFixedComponentsChart, + chart_dataclass_decorator, + ) + from coordinax._src.charts.d3 import Abstract3D, Cart3D + from coordinax._src.custom_types import Len + from coordinax._src.euclidean.manifold import R3 + + two_keys = tuple[Literal["a"], Literal["b"]] + two_dims = tuple[Len, Len] + + # A 2-component chart declared 3D (via Abstract3D) must be rejected. + with pytest.raises(TypeError, match="declared 3D but has 2 components"): + + @jtu.register_static + @chart_dataclass_decorator + class _Bad3D( + AbstractFixedComponentsChart[MT, two_keys, two_dims], Abstract3D + ): + _: dataclasses.KW_ONLY + M: MT = R3 + + @property + def cartesian(self): + return Cart3D(M=self.M) + + def test_component_count_matches_for_predefined_charts(self) -> None: + """Every predefined chart's ndim matches its component count.""" + charts = [ + obj + for name in dir(cxc) + if not name.startswith("_") + and isinstance(obj := getattr(cxc, name), cxc.AbstractChart) + ] + assert charts, "no predefined chart instances discovered" + for chart in charts: + assert chart.ndim == len(chart.components), chart