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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import numpy as np
import pyrecest.backend
from pyrecest.backend import (
amax,
arange,
array,
exp,
Expand Down Expand Up @@ -144,11 +145,12 @@ def __init__(self, w):
if any(bool(weight < 0.0) for weight in w):
raise ValueError("Weights must be nonnegative")

mean_weight = mean(w)
if not bool(mean_weight > 0.0):
weight_scale = amax(w)
if not bool(weight_scale > 0.0):
raise ValueError("Weights must have positive total mass")
scaled_weights = w / weight_scale

self.w = w / (mean_weight * 2.0 * pi)
self.w = scaled_weights / (mean(scaled_weights) * 2.0 * pi)

def pdf(self, xs):
"""Evaluate the pdf at each point in xs.
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
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
23 changes: 23 additions & 0 deletions tests/distributions/test_piecewise_constant_weight_overflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import numpy as np
import numpy.testing as npt

from pyrecest.backend import array, to_numpy
from pyrecest.distributions.circle.piecewise_constant_distribution import (
PiecewiseConstantDistribution,
)


def test_maximum_finite_weights_normalize_without_overflow():
backend_dtype = np.asarray(to_numpy(array([1.0], dtype=float))).dtype
maximum_finite_weight = np.finfo(backend_dtype).max

distribution = PiecewiseConstantDistribution(
array([maximum_finite_weight, maximum_finite_weight / 2.0], dtype=float)
)

npt.assert_allclose(
np.asarray(to_numpy(distribution.w)),
np.array([2.0 / (3.0 * np.pi), 1.0 / (3.0 * np.pi)]),
rtol=1.0e-6,
atol=0.0,
)
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
Loading