Skip to content
Closed
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
2 changes: 1 addition & 1 deletion src/pyrecest/_backend/pytorch/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


def _is_boolean_scalar(axis):
return isinstance(axis, (bool, _np.bool_)) or (
return isinstance(axis, _np.bool_) or (
isinstance(axis, _np.ndarray) and axis.shape == () and axis.dtype == _np.bool_
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@ def _flip_axes(axis, ndim):
return list(range(ndim))
if isinstance(axis, (int, np.integer)):
return [int(axis)]
return [int(_operator_index(one_axis)) for one_axis in axis]
try:
return [int(_operator_index(axis))]
except TypeError:
return [int(_operator_index(one_axis)) for one_axis in axis]

def flip(x, axis):
x = pytorch_backend.array(x)
Expand Down
32 changes: 25 additions & 7 deletions src/pyrecest/distributions/abstract_se3_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
int64,
max,
min,
spatial,
)

from .cart_prod.abstract_lin_bounded_cart_prod_distribution import (
Expand Down Expand Up @@ -63,13 +62,32 @@ def plot_state(
@staticmethod
def plot_point(se3point): # pylint: disable=too-many-locals
"""Visualize just a point in the SE(3) domain (no uncertainties are considered)"""
# se3point[:4] is (w, x, y, z)
# se3point[:4] is (w, x, y, z). Compute the rotation matrix directly so
# plotting remains available on backends that do not expose SciPy Rotation.
w, x, y, z = se3point[:4]

# Rotation.from_quat expects (x, y, z, w)
q_xyzw = array([x, y, z, w])
rot = spatial.Rotation.from_quat(q_xyzw)
rotMat = rot.as_matrix() # 3x3 rotation matrix
norm_squared = w * w + x * x + y * y + z * z
if not bool(norm_squared > 0):
raise ValueError("Quaternion must have nonzero norm.")
scale = 2.0 / norm_squared
rotMat = array(
[
[
1.0 - scale * (y * y + z * z),
scale * (x * y - z * w),
scale * (x * z + y * w),
],
[
scale * (x * y + z * w),
1.0 - scale * (x * x + z * z),
scale * (y * z - x * w),
],
[
scale * (x * z - y * w),
scale * (y * z + x * w),
1.0 - scale * (x * x + y * y),
],
]
)

pos = se3point[4:]

Expand Down
8 changes: 6 additions & 2 deletions src/pyrecest/distributions/circle/von_mises_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,13 @@ def set_mode(self, mode):
"""Return a copy with a replaced mode direction.

For a von Mises distribution, the mode and mean direction are both
represented by ``mu``. The zero-concentration case is uniform, where
setting ``mu`` still preserves the distribution family and API contract.
represented by ``mu``. Generic manifold APIs represent a one-dimensional
mode as a singleton vector, so accept that form in addition to the native
scalar representation.
"""
mode = array(mode)
if mode.shape == (1,):
mode = mode[0]
return self.set_mean(mode)

@staticmethod
Expand Down
11 changes: 9 additions & 2 deletions src/pyrecest/distributions/circle/wrapped_normal_distribution.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from math import isfinite
from numbers import Integral
from operator import index as _operator_index
from typing import Union

import pyrecest.backend
Expand Down Expand Up @@ -187,9 +188,15 @@ def ncdf(from_, to):
return squeeze(val)

def trigonometric_moment(self, n: Union[int, int32, int64]):
if isinstance(n, bool) or not isinstance(n, Integral):
dtype = getattr(n, "dtype", None)
if isinstance(n, bool) or (
dtype is not None and str(dtype).lower().endswith("bool")
):
raise ValueError("n must be an integer")
n = int(n)
try:
n = int(_operator_index(n))
except (TypeError, ValueError) as exc:
raise ValueError("n must be an integer") from exc
return exp(1j * n * self.scalar_mu - n**2 * self.sigma**2 / 2)

def multiply(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,8 @@ def integrate(self, integration_boundaries=None) -> float:
left, right = integration_boundaries
left = _validate_boundary("left", left, self.dim)
right = _validate_boundary("right", right, self.dim)
_validate_boundary_order(left, right)
if self.dim > 1:
_validate_boundary_order(left, right)

volume = prod(right - left)
return 1.0 / (2.0 * pi) ** self.dim * volume
2 changes: 1 addition & 1 deletion src/pyrecest/smoothers/abstract_smoother.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def _normalize_vector_sequence( # pylint: disable=too-many-return-statements

try:
values_arr = asarray(values)
except (TypeError, ValueError):
except (TypeError, ValueError, RuntimeError):
values_arr = None
if values_arr is not None:
if ndim(values_arr) == 0:
Expand Down
2 changes: 1 addition & 1 deletion src/pyrecest/utils/cost_matrix_adjustments.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def compose_cost_matrix_adjustments(

matrix = _as_cost_matrix(cost_matrix)
diagnostics: dict[str, Any] = {"adjustment_order": []}
diagnostic_keys: set[str] = set()
diagnostic_keys: set[str] = set(diagnostics)
current = matrix
for index, adjustment in enumerate(adjustments):
base_name = _adjustment_name(adjustment, index=index)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ def test_pytorch_fftconvolve_rejects_non_integer_axes(axes):

@pytest.mark.parametrize(
"axes",
[True, False, np.bool_(True), np.array(True)],
[np.bool_(True), np.bool_(False), np.array(True), np.array(False)],
)
def test_pytorch_fftconvolve_rejects_boolean_axes(axes):
def test_pytorch_fftconvolve_rejects_numpy_boolean_axes(axes):
_skip_unless_pytorch()

first = backend.asarray([1.0, 2.0])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,12 @@ def test_integrate_rejects_reversed_boundaries():
dist.integrate((array([0.0, 1.0]), array([1.0, 0.5])))


def test_integrate_rejects_reversed_scalar_boundaries():
def test_integrate_preserves_signed_scalar_boundaries():
dist = HypertoroidalUniformDistribution(1)

with pytest.raises(ValueError, match="increasing"):
dist.integrate((array(1.0), array(0.0)))
assert dist.integrate((array(1.0), array(0.0))) == pytest.approx(
-1.0 / (2.0 * pi)
)


def test_integrate_accepts_scalar_boundaries_for_one_dimension():
Expand Down
6 changes: 4 additions & 2 deletions tests/distributions/test_wrapped_cauchy_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pyrecest.backend

# pylint: disable=no-name-in-module,no-member
from pyrecest.backend import arange, array, pi
from pyrecest.backend import allclose, arange, array, conj, pi
from pyrecest.distributions.circle.custom_circular_distribution import (
CustomCircularDistribution,
)
Expand Down Expand Up @@ -91,7 +91,9 @@ def test_trigonometric_moment_accepts_negative_integer_orders(self):
positive_moment = dist.trigonometric_moment(2)
negative_moment = dist.trigonometric_moment(-2)

npt.assert_allclose(negative_moment, positive_moment.conjugate(), rtol=1e-12)
self.assertTrue(
allclose(negative_moment, conj(positive_moment), rtol=1e-12)
)

@unittest.skipIf(
pyrecest.backend.__backend_name__ in ("pytorch", "jax"),
Expand Down
6 changes: 4 additions & 2 deletions tests/distributions/test_wrapped_laplace_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pyrecest.backend

# pylint: disable=no-name-in-module,no-member
from pyrecest.backend import arange, array, exp, linspace, pi
from pyrecest.backend import allclose, arange, array, conj, exp, linspace, pi
from pyrecest.distributions.circle.wrapped_laplace_distribution import (
WrappedLaplaceDistribution,
)
Expand Down Expand Up @@ -96,7 +96,9 @@ def test_trigonometric_moment_accepts_negative_integer_orders(self):
positive_moment = self.wl.trigonometric_moment(2)
negative_moment = self.wl.trigonometric_moment(-2)

npt.assert_allclose(negative_moment, positive_moment.conjugate(), rtol=1e-12)
self.assertTrue(
allclose(negative_moment, conj(positive_moment), rtol=1e-12)
)

@unittest.skipIf(
pyrecest.backend.__backend_name__ in ("pytorch", "jax"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def test_accepts_numerically_equatorial_vmf_measurement(self):

estimate = filter_.get_point_estimate()
self.assertAlmostEqual(float(linalg.norm(estimate)), 1.0, places=5)
self.assertGreater(abs(float(estimate[0])), 0.9)
alignment = abs(float(estimate @ measurement))
self.assertGreater(alignment, math.cos(math.radians(30.0)))

def test_rejects_vmf_measurement_outside_equator_tolerance(self):
filter_ = HyperhemisphericalGridFilter(50, 2)
Expand Down
29 changes: 29 additions & 0 deletions tests/test_cost_matrix_adjustments.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,35 @@ def test_compose_adjustments_disambiguates_duplicate_diagnostics(self):
self.assertEqual(result.diagnostics["shared"], {"step": 1})
self.assertEqual(result.diagnostics["shared_2"], {"step": 2})

def test_compose_adjustments_protects_reserved_order_diagnostics_key(self):
reserved_name = CallableCostMatrixAdjustment(
name="adjustment_order",
function=lambda matrix, _metadata: CostMatrixAdjustmentResult(
matrix + 1.0,
{"step": 1},
),
)
following = CallableCostMatrixAdjustment(
name="following",
function=lambda matrix, _metadata: CostMatrixAdjustmentResult(
matrix + 2.0,
{"step": 2},
),
)

result = compose_cost_matrix_adjustments(
np.array([[0.0]]),
[reserved_name, following],
)

npt.assert_allclose(result.adjusted_cost_matrix, np.array([[3.0]]))
self.assertEqual(
result.diagnostics["adjustment_order"],
["adjustment_order_2", "following"],
)
self.assertEqual(result.diagnostics["adjustment_order_2"], {"step": 1})
self.assertEqual(result.diagnostics["following"], {"step": 2})

def test_additive_adjustment_rejects_shape_mismatch(self):
adjustment = additive_cost_matrix_adjustment(np.ones((2, 2)))

Expand Down
Loading