diff --git a/src/pyrecest/distributions/circle/circular_fourier_distribution.py b/src/pyrecest/distributions/circle/circular_fourier_distribution.py index f05366f2d..d8b316709 100644 --- a/src/pyrecest/distributions/circle/circular_fourier_distribution.py +++ b/src/pyrecest/distributions/circle/circular_fourier_distribution.py @@ -3,6 +3,7 @@ from typing import Union import matplotlib.pyplot as plt +import numpy as np # pylint: disable=redefined-builtin,no-name-in-module,no-member # pylint: disable=no-name-in-module,no-member @@ -52,6 +53,21 @@ def _ensure_odd_n(n) -> None: _validate_odd_n(n) +def _validate_scaling_flag(value, name: str) -> bool: + """Return a real boolean scaling flag without accepting truthy values.""" + if np.ma.is_masked(value): + raise TypeError(f"{name} must be a boolean.") + if isinstance(value, (bool, np.bool_)): + return bool(value) + try: + value_array = np.asarray(value) + except (TypeError, ValueError, RuntimeError, OverflowError) as exc: + raise TypeError(f"{name} must be a boolean.") from exc + if value_array.shape == () and value_array.dtype == np.bool_: + return bool(value_array.item()) + raise TypeError(f"{name} must be a boolean.") + + class CircularFourierDistribution(AbstractCircularDistribution): """Circular distribution represented by a Fourier series. @@ -120,7 +136,9 @@ def __init__( else: raise ValueError("Need to provide either c or a and b.") - self.multiplied_by_n = multiplied_by_n + self.multiplied_by_n = _validate_scaling_flag( + multiplied_by_n, "multiplied_by_n" + ) self.transformation = transformation def __sub__( @@ -365,6 +383,9 @@ def from_distribution( transformation: str = "sqrt", store_values_multiplied_by_n: bool = True, ) -> "CircularFourierDistribution": + store_values_multiplied_by_n = _validate_scaling_flag( + store_values_multiplied_by_n, "store_values_multiplied_by_n" + ) n = _validate_odd_n(n) if isinstance(distribution, CircularDiracDistribution): if transformation != "identity": @@ -412,6 +433,9 @@ def from_function_values( transformation: str = "sqrt", store_values_multiplied_by_n: bool = True, ) -> "CircularFourierDistribution": + store_values_multiplied_by_n = _validate_scaling_flag( + store_values_multiplied_by_n, "store_values_multiplied_by_n" + ) n_values = fvals.shape[0] _ensure_odd_n(n_values) c = fft.rfft(fvals) diff --git a/tests/distributions/test_circular_fourier_bool_validation.py b/tests/distributions/test_circular_fourier_bool_validation.py new file mode 100644 index 000000000..c39256d36 --- /dev/null +++ b/tests/distributions/test_circular_fourier_bool_validation.py @@ -0,0 +1,59 @@ +import numpy as np +import pytest + +from pyrecest.backend import array +from pyrecest.distributions import CircularFourierDistribution, VonMisesDistribution + + +@pytest.mark.parametrize("invalid_flag", ["False", "True", 0, 1, None]) +def test_constructor_rejects_non_boolean_multiplied_by_n(invalid_flag): + with pytest.raises(TypeError, match="multiplied_by_n must be a boolean"): + CircularFourierDistribution( + c=array([1.0]), + n=1, + transformation="identity", + multiplied_by_n=invalid_flag, + ) + + +@pytest.mark.parametrize("invalid_flag", ["False", "True", 0, 1, None]) +def test_function_value_factory_rejects_non_boolean_scaling_flag(invalid_flag): + with pytest.raises( + TypeError, match="store_values_multiplied_by_n must be a boolean" + ): + CircularFourierDistribution.from_function_values( + array([1.0, 1.0, 1.0]), + transformation="identity", + store_values_multiplied_by_n=invalid_flag, + ) + + +def test_distribution_factory_rejects_truthy_string_scaling_flag(): + distribution = VonMisesDistribution(array(0.0), array(1.0)) + + with pytest.raises( + TypeError, match="store_values_multiplied_by_n must be a boolean" + ): + CircularFourierDistribution.from_distribution( + distribution, + n=3, + transformation="identity", + store_values_multiplied_by_n="False", + ) + + +def test_numpy_boolean_scaling_flags_are_accepted(): + direct = CircularFourierDistribution( + c=array([1.0]), + n=1, + transformation="identity", + multiplied_by_n=np.bool_(False), + ) + from_values = CircularFourierDistribution.from_function_values( + array([1.0, 1.0, 1.0]), + transformation="identity", + store_values_multiplied_by_n=np.bool_(False), + ) + + assert direct.multiplied_by_n is False + assert from_values.multiplied_by_n is False