diff --git a/spharpy/classes/sh.py b/spharpy/classes/sh.py index 3d67813b..4c3ba392 100644 --- a/spharpy/classes/sh.py +++ b/spharpy/classes/sh.py @@ -4,15 +4,20 @@ import numpy as np import pyfar as pf import spharpy as sy +from abc import ABC, abstractmethod -class SphericalHarmonicDefinition: - """Class storing the definition of spherical harmonics. +class _SphericalHarmonicBase(ABC): + """Base class defining properties to parametrize spherical harmonics. + + This base class serves as a base for all classes requiring a definition of + the spherical harmonics without explicitly setting a spherical harmonic + order. This class is intended for cases where the spherical harmonic order + is implemented as a read-only property in child classes, for example when + the order is implicitly defined by other parameters or inferred from data. Attributes ---------- - n_max : int, optional - Maximum spherical harmonic order. The default is ``0``. basis_type : str Type of spherical harmonic basis, either ``'real'`` or ``'complex'``. The default is ``'real'``. @@ -33,7 +38,6 @@ class SphericalHarmonicDefinition: def __init__( self, - n_max=0, basis_type="real", channel_convention="ACN", normalization="N3D", @@ -43,7 +47,6 @@ def __init__( self._channel_convention = None self._condon_shortley = None self._normalization = None - self._n_max = 0 # basis_type needs to be initialized first, since the default for the # Condon-Shortley phase depends on the basis type @@ -51,30 +54,13 @@ def __init__( self.condon_shortley = condon_shortley # n_max needs to be initialized before channel_convention and # normalization, since both have restrictions on n_max - self.n_max = n_max self.channel_convention = channel_convention self.normalization = normalization @property + @abstractmethod def n_max(self): """Get or set the spherical harmonic order.""" - return self._n_max - - @n_max.setter - def n_max(self, value : int): - """Get or set the spherical harmonic order.""" - if value < 0 or value % 1 != 0: - raise ValueError("n_max must be a positive integer") - if self.channel_convention == "FuMa" and value > 3: - raise ValueError( - "n_max > 3 is not allowed with 'FuMa' channel convention") - if self.normalization == "maxN" and value > 3: - raise ValueError( - "n_max > 3 is not allowed with 'maxN' normalization") - - if self._n_max != value: - self._n_max = value - self._on_property_change() @property def condon_shortley(self): @@ -158,7 +144,7 @@ def normalization(self, value): self._normalization = value self._on_property_change() - def _on_property_change(self): + def _on_property_change(self): # noqa: B027 """Method called when a class property changes. This method can be overridden in child classes to re-compute dependent properties. @@ -166,6 +152,74 @@ def _on_property_change(self): pass +class SphericalHarmonicDefinition(_SphericalHarmonicBase): + """Class storing the (discrete) definition of spherical harmonics. + + This class can serve as a container to create related objects, e.g., + spherical harmonic basis matrices for given sampling points, transforms, + or other spherical harmonic related data and computations. + + Attributes + ---------- + n_max : int, optional + Maximum spherical harmonic order. The default is ``0``. + basis_type : str + Type of spherical harmonic basis, either ``'real'`` or ``'complex'``. + The default is ``'real'``. + channel_convention : str, optional + Channel ordering convention, either ``'ACN'`` or ``'FuMa'``. + The default is ``'ACN'``. Note that ``'FuMa'`` is only supported up to + 3rd order. + normalization : str, optional + Normalization convention, either ``'N3D'``, ``'NM'``, ``'maxN'``, + ``'SN3D'``, or ``'SNM'``. The default is ``'N3D'``. Note that + ``'maxN'`` is only supported up to 3rd order. + condon_shortley : bool, str, optional + Condon-Shortley phase term. If ``True``, Condon-Shortley is included, + if ``False`` it is not included. The default is ``'auto'``, which + corresponds to ``True`` for type ``complex`` and ``False`` for type + ``real``. + """ + + def __init__( + self, + n_max=0, + basis_type="real", + channel_convention="ACN", + normalization="N3D", + condon_shortley="auto", + ): + self._n_max = 0 + super().__init__( + basis_type=basis_type, + channel_convention=channel_convention, + normalization=normalization, + condon_shortley=condon_shortley, + ) + self.n_max = n_max + + @property + def n_max(self): + """Get or set the spherical harmonic order.""" + return self._n_max + + @n_max.setter + def n_max(self, value : int): + """Get or set the spherical harmonic order.""" + if value < 0 or value % 1 != 0: + raise ValueError("n_max must be a positive integer") + if self.channel_convention == "FuMa" and value > 3: + raise ValueError( + "n_max > 3 is not allowed with 'FuMa' channel convention") + if self.normalization == "maxN" and value > 3: + raise ValueError( + "n_max > 3 is not allowed with 'maxN' normalization") + + if self._n_max != value: + self._n_max = value + self._on_property_change() + + class SphericalHarmonics(SphericalHarmonicDefinition): r""" Compute spherical harmonic basis matrices, their inverses, and gradients. diff --git a/tests/test_sh_class.py b/tests/test_sh_class.py index b6afdf23..4e33e0fc 100644 --- a/tests/test_sh_class.py +++ b/tests/test_sh_class.py @@ -79,9 +79,17 @@ def test_setter_channel_convention_fuma_error(): """Test error when setting FUMA channel convention with n_max > 3.""" sph_harm = SphericalHarmonicDefinition(n_max=4) - with pytest.raises(ValueError, match='n_max > 3 is not allowed with'): + message = 'n_max > 3 is not allowed with' + + with pytest.raises(ValueError, match=message): sph_harm.channel_convention = "FuMa" + with pytest.raises(ValueError, match=message): + SphericalHarmonicDefinition( + n_max=4, + channel_convention="FuMa", + ) + def test_setter_channel_convention_definition_invalid(): """Test error handling for invalid channel convention values."""