Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
104 changes: 79 additions & 25 deletions spharpy/classes/sh.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'``.
Expand All @@ -33,7 +38,6 @@ class SphericalHarmonicDefinition:

def __init__(
self,
n_max=0,
basis_type="real",
channel_convention="ACN",
normalization="N3D",
Expand All @@ -43,38 +47,20 @@ 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
self.basis_type = basis_type
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):
Expand Down Expand Up @@ -158,14 +144,82 @@ 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.
"""
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.
Expand Down
10 changes: 9 additions & 1 deletion tests/test_sh_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down