From d6769d40a66ab5936444688a270cebe5bb956dd7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 20:27:24 +0000 Subject: [PATCH 1/5] Add PiecewiseConstantDistribution for circular distributions Agent-Logs-Url: https://github.com/FlorianPfaff/PyRecEst/sessions/63fdf26d-4651-4b25-82ef-a43575b8bf7c Co-authored-by: FlorianPfaff <6773539+FlorianPfaff@users.noreply.github.com> --- pyrecest/distributions/__init__.py | 1 + .../circle/piecewise_constant_distribution.py | 199 ++++++++++++++++++ .../test_piecewise_constant_distribution.py | 117 ++++++++++ 3 files changed, 317 insertions(+) create mode 100644 pyrecest/distributions/circle/piecewise_constant_distribution.py create mode 100644 pyrecest/tests/distributions/test_piecewise_constant_distribution.py diff --git a/pyrecest/distributions/__init__.py b/pyrecest/distributions/__init__.py index 72efac1ad..f7136d62a 100644 --- a/pyrecest/distributions/__init__.py +++ b/pyrecest/distributions/__init__.py @@ -84,6 +84,7 @@ SineSkewedWrappedCauchyDistribution, SineSkewedWrappedNormalDistribution, ) +from .circle.piecewise_constant_distribution import PiecewiseConstantDistribution from .circle.von_mises_distribution import VonMisesDistribution from .circle.wrapped_cauchy_distribution import WrappedCauchyDistribution from .circle.wrapped_laplace_distribution import WrappedLaplaceDistribution diff --git a/pyrecest/distributions/circle/piecewise_constant_distribution.py b/pyrecest/distributions/circle/piecewise_constant_distribution.py new file mode 100644 index 000000000..3df9b6fbf --- /dev/null +++ b/pyrecest/distributions/circle/piecewise_constant_distribution.py @@ -0,0 +1,199 @@ +import numpy as np + +# pylint: disable=no-name-in-module,no-member +from pyrecest.backend import mod, pi + +from .abstract_circular_distribution import AbstractCircularDistribution + + +class PiecewiseConstantDistribution(AbstractCircularDistribution): + """Piecewise constant (i.e. discrete) circular distribution, similar to a histogram. + + The circle [0, 2*pi) is divided into n equal intervals, each with a constant + probability density weight. + + Gerhard Kurz, Florian Pfaff, Uwe D. Hanebeck, + Discrete Recursive Bayesian Filtering on Intervals and the Unit Circle + Proceedings of the 2016 IEEE International Conference on Multisensor Fusion + and Integration for Intelligent Systems (MFI 2016), + Baden-Baden, Germany, September 2016. + """ + + def __init__(self, w): + """Initialize with a weight vector that is automatically normalized. + + Parameters + ---------- + w : array_like, shape (n,) + Weight for each interval (will be normalized to form a valid pdf). + """ + AbstractCircularDistribution.__init__(self) + w = np.asarray(w, dtype=float).ravel() + assert w.ndim == 1 and w.size > 0 + self.w = w / (np.mean(w) * 2.0 * np.pi) + + def pdf(self, xs): + """Evaluate the pdf at each point in xs. + + Parameters + ---------- + xs : array_like, shape (n,) + Points at which to evaluate the pdf. + + Returns + ------- + p : ndarray, shape (n,) + Pdf values at each point. + """ + assert xs.ndim == 1 + xs_mod = np.asarray(mod(xs, 2.0 * pi), dtype=float) + n = len(self.w) + idx = np.minimum( + np.floor(xs_mod / (2.0 * np.pi) * n).astype(int), n - 1 + ) + return self.w[idx] + + def trigonometric_moment(self, n): + """Calculate the n-th trigonometric moment analytically. + + Parameters + ---------- + n : int + Moment order. + + Returns + ------- + m : complex + n-th trigonometric moment. + """ + if n == 0: + return 1.0 + 0j + num = len(self.w) + interv = np.zeros(num, dtype=complex) + for j in range(1, num + 1): + l = PiecewiseConstantDistribution.left_border(j, num) + r = PiecewiseConstantDistribution.right_border(j, num) + c = PiecewiseConstantDistribution.interval_center(j, num) + w_j = float(self.pdf(np.array([c]))[0]) + interv[j - 1] = w_j * (np.exp(1j * n * r) - np.exp(1j * n * l)) + return complex(-1j / n * np.sum(interv)) + + def entropy(self): + """Calculate the entropy analytically. + + Returns + ------- + e : float + Entropy of the distribution. + """ + n = len(self.w) + return float(-2.0 * np.pi / n * np.sum(self.w * np.log(self.w))) + + def sample(self, n): + """Draw n random samples from the distribution. + + Parameters + ---------- + n : int + Number of samples to draw. + + Returns + ------- + samples : ndarray, shape (n,) + Samples in [0, 2*pi). + """ + num_intervals = len(self.w) + interval_width = 2.0 * np.pi / num_intervals + # Each interval has probability w[j] * interval_width, which sums to 1 by + # construction. Divide by sum anyway to guard against floating-point drift. + interval_probs = self.w * interval_width + interval_probs /= interval_probs.sum() + interval_indices = np.random.choice(num_intervals, size=n, p=interval_probs) + return interval_indices * interval_width + np.random.uniform( + 0.0, interval_width, size=n + ) + + @staticmethod + def left_border(m, n): + """Left border of the m-th interval (1-indexed) for n total intervals. + + Parameters + ---------- + m : int + Interval index (1-indexed). + n : int + Total number of intervals. + + Returns + ------- + float + Left border of the m-th interval. + """ + assert 1 <= m <= n + return 2.0 * np.pi / n * (m - 1) + + @staticmethod + def right_border(m, n): + """Right border of the m-th interval (1-indexed) for n total intervals. + + Parameters + ---------- + m : int + Interval index (1-indexed). + n : int + Total number of intervals. + + Returns + ------- + float + Right border of the m-th interval. + """ + assert 1 <= m <= n + return 2.0 * np.pi / n * m + + @staticmethod + def interval_center(m, n): + """Center of the m-th interval (1-indexed) for n total intervals. + + Parameters + ---------- + m : int + Interval index (1-indexed). + n : int + Total number of intervals. + + Returns + ------- + float + Center of the m-th interval. + """ + assert 1 <= m <= n + return 2.0 * np.pi / n * (m - 0.5) + + @staticmethod + def calculate_parameters_numerically(pdf_func, n): + """Calculate weights by numerically integrating a given pdf over each interval. + + Parameters + ---------- + pdf_func : callable + Pdf of a circular density; accepts a 1-D array and returns a 1-D array. + n : int + Number of discretization intervals. + + Returns + ------- + w : ndarray, shape (n,) + Weights of the corresponding PiecewiseConstantDistribution. + """ + from scipy.integrate import quad # pylint: disable=import-outside-toplevel + + assert n >= 1 + w = np.zeros(n) + for j in range(1, n + 1): + l = PiecewiseConstantDistribution.left_border(j, n) + r = PiecewiseConstantDistribution.right_border(j, n) + w[j - 1] = quad( + lambda x: float(pdf_func(np.array([x]))), l, r + )[0] + return w diff --git a/pyrecest/tests/distributions/test_piecewise_constant_distribution.py b/pyrecest/tests/distributions/test_piecewise_constant_distribution.py new file mode 100644 index 000000000..94eec4e1a --- /dev/null +++ b/pyrecest/tests/distributions/test_piecewise_constant_distribution.py @@ -0,0 +1,117 @@ +import unittest + +import numpy as np +import numpy.testing as npt + +# pylint: disable=no-name-in-module,no-member +from pyrecest.backend import array +from pyrecest.distributions.circle.piecewise_constant_distribution import ( + PiecewiseConstantDistribution, +) +from pyrecest.distributions.circle.wrapped_normal_distribution import ( + WrappedNormalDistribution, +) + + +class PiecewiseConstantDistributionTest(unittest.TestCase): + def setUp(self): + self.w = np.array([1, 2, 3, 4, 5, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1], dtype=float) + self.normal = 1.0 / (2.0 * np.pi * np.mean(self.w)) + self.dist = PiecewiseConstantDistribution(self.w) + + def test_pdf(self): + npt.assert_allclose( + self.dist.pdf(np.array([0.0])), np.array([1 * self.normal]), rtol=1e-10 + ) + npt.assert_allclose( + self.dist.pdf(np.array([4.2])), np.array([5 * self.normal]), rtol=1e-10 + ) + npt.assert_allclose( + self.dist.pdf(np.array([10.9])), np.array([4 * self.normal]), rtol=1e-10 + ) + + def test_integral_normalized(self): + """Verify the distribution integrates to 1 via the exact sum.""" + n = len(self.dist.w) + npt.assert_allclose( + np.sum(self.dist.w) * (2.0 * np.pi / n), 1.0, rtol=1e-10 + ) + + def test_integral_partial(self): + """Verify partial integrals sum to 1 using a fine grid.""" + M = 1_000_000 + xs = np.linspace(0, 2.0 * np.pi, M, endpoint=False) + dx = 2.0 * np.pi / M + pdf_vals = self.dist.pdf(xs) + first_half = np.sum(pdf_vals[xs < np.pi]) * dx + second_half = np.sum(pdf_vals[xs >= np.pi]) * dx + npt.assert_allclose(first_half + second_half, 1.0, rtol=1e-4) + + def test_trigonometric_moment(self): + """Verify analytical trigonometric moments using a fine-grid reference.""" + M = 1_000_000 + xs = np.linspace(0, 2.0 * np.pi, M, endpoint=False) + pdf_vals = self.dist.pdf(xs) + dx = 2.0 * np.pi / M + for n_moment in [1, 2, 3]: + expected = np.sum(pdf_vals * np.exp(1j * n_moment * xs)) * dx + with self.subTest(n=n_moment): + npt.assert_allclose( + self.dist.trigonometric_moment(n_moment), expected, rtol=1e-4 + ) + + def test_interval_borders(self): + self.assertAlmostEqual( + PiecewiseConstantDistribution.left_border(1, 2), 0.0 * 2.0 * np.pi + ) + self.assertAlmostEqual( + PiecewiseConstantDistribution.interval_center(1, 2), + 1.0 / 4.0 * 2.0 * np.pi, + ) + self.assertAlmostEqual( + PiecewiseConstantDistribution.right_border(1, 2), + 1.0 / 2.0 * 2.0 * np.pi, + ) + self.assertAlmostEqual( + PiecewiseConstantDistribution.left_border(2, 2), + 1.0 / 2.0 * 2.0 * np.pi, + ) + self.assertAlmostEqual( + PiecewiseConstantDistribution.interval_center(2, 2), + 3.0 / 4.0 * 2.0 * np.pi, + ) + self.assertAlmostEqual( + PiecewiseConstantDistribution.right_border(2, 2), 1.0 * 2.0 * np.pi + ) + + def test_calculate_parameters_numerically(self): + """More samples should yield better moment matching.""" + wn = WrappedNormalDistribution(array(2.0), array(1.3)) + w1 = PiecewiseConstantDistribution.calculate_parameters_numerically(wn.pdf, 40) + w2 = PiecewiseConstantDistribution.calculate_parameters_numerically(wn.pdf, 45) + w3 = PiecewiseConstantDistribution.calculate_parameters_numerically(wn.pdf, 50) + p1 = PiecewiseConstantDistribution(w1) + p2 = PiecewiseConstantDistribution(w2) + p3 = PiecewiseConstantDistribution(w3) + delta1 = abs(wn.trigonometric_moment(1) - p1.trigonometric_moment(1)) + delta2 = abs(wn.trigonometric_moment(1) - p2.trigonometric_moment(1)) + delta3 = abs(wn.trigonometric_moment(1) - p3.trigonometric_moment(1)) + self.assertLessEqual(delta2, delta1) + self.assertLessEqual(delta3, delta2) + + def test_entropy(self): + """Verify analytical entropy against the direct formula.""" + w = self.dist.w + n = len(w) + expected = -2.0 * np.pi / n * np.sum(w * np.log(w)) + npt.assert_allclose(self.dist.entropy(), expected, rtol=1e-10) + + def test_sample(self): + samples = self.dist.sample(100) + self.assertEqual(len(samples), 100) + self.assertTrue(np.all(samples >= 0.0)) + self.assertTrue(np.all(samples < 2.0 * np.pi)) + + +if __name__ == "__main__": + unittest.main() From 771fe82839e46878da828f71e45622222aa76fa8 Mon Sep 17 00:00:00 2001 From: Florian Pfaff Date: Wed, 1 Apr 2026 16:51:26 +0200 Subject: [PATCH 2/5] Support all backends for pwc distribution --- .../circle/piecewise_constant_distribution.py | 40 +++++++-------- .../test_piecewise_constant_distribution.py | 49 +++++++++---------- 2 files changed, 43 insertions(+), 46 deletions(-) diff --git a/pyrecest/distributions/circle/piecewise_constant_distribution.py b/pyrecest/distributions/circle/piecewise_constant_distribution.py index 3df9b6fbf..5a7e97be8 100644 --- a/pyrecest/distributions/circle/piecewise_constant_distribution.py +++ b/pyrecest/distributions/circle/piecewise_constant_distribution.py @@ -1,7 +1,5 @@ -import numpy as np - # pylint: disable=no-name-in-module,no-member -from pyrecest.backend import mod, pi +from pyrecest.backend import mod, pi, array, mean, floor, zeros, exp, sum, log, random from .abstract_circular_distribution import AbstractCircularDistribution @@ -28,9 +26,9 @@ def __init__(self, w): Weight for each interval (will be normalized to form a valid pdf). """ AbstractCircularDistribution.__init__(self) - w = np.asarray(w, dtype=float).ravel() + w = array(w, dtype=float).ravel() assert w.ndim == 1 and w.size > 0 - self.w = w / (np.mean(w) * 2.0 * np.pi) + self.w = w / (mean(w) * 2.0 * pi) def pdf(self, xs): """Evaluate the pdf at each point in xs. @@ -46,10 +44,10 @@ def pdf(self, xs): Pdf values at each point. """ assert xs.ndim == 1 - xs_mod = np.asarray(mod(xs, 2.0 * pi), dtype=float) + xs_mod = array(mod(xs, 2.0 * pi), dtype=float) n = len(self.w) - idx = np.minimum( - np.floor(xs_mod / (2.0 * np.pi) * n).astype(int), n - 1 + idx = array( + [min(int(floor(xs_mod[i] / (2.0 * pi) * n)), n - 1) for i in range(n)] ) return self.w[idx] @@ -69,14 +67,14 @@ def trigonometric_moment(self, n): if n == 0: return 1.0 + 0j num = len(self.w) - interv = np.zeros(num, dtype=complex) + interv = zeros(num, dtype=complex) for j in range(1, num + 1): l = PiecewiseConstantDistribution.left_border(j, num) r = PiecewiseConstantDistribution.right_border(j, num) c = PiecewiseConstantDistribution.interval_center(j, num) - w_j = float(self.pdf(np.array([c]))[0]) - interv[j - 1] = w_j * (np.exp(1j * n * r) - np.exp(1j * n * l)) - return complex(-1j / n * np.sum(interv)) + w_j = float(self.pdf(array([c]))[0]) + interv[j - 1] = w_j * (exp(1j * n * r) - exp(1j * n * l)) + return complex(-1j / n * sum(interv)) def entropy(self): """Calculate the entropy analytically. @@ -87,7 +85,7 @@ def entropy(self): Entropy of the distribution. """ n = len(self.w) - return float(-2.0 * np.pi / n * np.sum(self.w * np.log(self.w))) + return float(-2.0 * pi / n * sum(self.w * log(self.w))) def sample(self, n): """Draw n random samples from the distribution. @@ -103,13 +101,13 @@ def sample(self, n): Samples in [0, 2*pi). """ num_intervals = len(self.w) - interval_width = 2.0 * np.pi / num_intervals + interval_width = 2.0 * pi / num_intervals # Each interval has probability w[j] * interval_width, which sums to 1 by # construction. Divide by sum anyway to guard against floating-point drift. interval_probs = self.w * interval_width interval_probs /= interval_probs.sum() - interval_indices = np.random.choice(num_intervals, size=n, p=interval_probs) - return interval_indices * interval_width + np.random.uniform( + interval_indices = random.choice(num_intervals, size=n, p=interval_probs) + return interval_indices * interval_width + random.uniform( 0.0, interval_width, size=n ) @@ -130,7 +128,7 @@ def left_border(m, n): Left border of the m-th interval. """ assert 1 <= m <= n - return 2.0 * np.pi / n * (m - 1) + return 2.0 * pi / n * (m - 1) @staticmethod def right_border(m, n): @@ -149,7 +147,7 @@ def right_border(m, n): Right border of the m-th interval. """ assert 1 <= m <= n - return 2.0 * np.pi / n * m + return 2.0 * pi / n * m @staticmethod def interval_center(m, n): @@ -168,7 +166,7 @@ def interval_center(m, n): Center of the m-th interval. """ assert 1 <= m <= n - return 2.0 * np.pi / n * (m - 0.5) + return 2.0 * pi / n * (m - 0.5) @staticmethod def calculate_parameters_numerically(pdf_func, n): @@ -189,11 +187,11 @@ def calculate_parameters_numerically(pdf_func, n): from scipy.integrate import quad # pylint: disable=import-outside-toplevel assert n >= 1 - w = np.zeros(n) + w = zeros(n) for j in range(1, n + 1): l = PiecewiseConstantDistribution.left_border(j, n) r = PiecewiseConstantDistribution.right_border(j, n) w[j - 1] = quad( - lambda x: float(pdf_func(np.array([x]))), l, r + lambda x: float(pdf_func(array([x]))), l, r )[0] return w diff --git a/pyrecest/tests/distributions/test_piecewise_constant_distribution.py b/pyrecest/tests/distributions/test_piecewise_constant_distribution.py index 94eec4e1a..f65e9f61e 100644 --- a/pyrecest/tests/distributions/test_piecewise_constant_distribution.py +++ b/pyrecest/tests/distributions/test_piecewise_constant_distribution.py @@ -1,10 +1,9 @@ import unittest -import numpy as np import numpy.testing as npt -# pylint: disable=no-name-in-module,no-member -from pyrecest.backend import array +# pylint: disable=no-name-in-module,no-member,redefined-builtin +from pyrecest.backend import linspace, sum, exp, log, mean, pi, array from pyrecest.distributions.circle.piecewise_constant_distribution import ( PiecewiseConstantDistribution, ) @@ -15,46 +14,46 @@ class PiecewiseConstantDistributionTest(unittest.TestCase): def setUp(self): - self.w = np.array([1, 2, 3, 4, 5, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1], dtype=float) - self.normal = 1.0 / (2.0 * np.pi * np.mean(self.w)) + self.w = array([1, 2, 3, 4, 5, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1], dtype=float) + self.normal = 1.0 / (2.0 * pi * mean(self.w)) self.dist = PiecewiseConstantDistribution(self.w) def test_pdf(self): npt.assert_allclose( - self.dist.pdf(np.array([0.0])), np.array([1 * self.normal]), rtol=1e-10 + self.dist.pdf(array([0.0])), array([1 * self.normal]), rtol=1e-10 ) npt.assert_allclose( - self.dist.pdf(np.array([4.2])), np.array([5 * self.normal]), rtol=1e-10 + self.dist.pdf(array([4.2])), array([5 * self.normal]), rtol=1e-10 ) npt.assert_allclose( - self.dist.pdf(np.array([10.9])), np.array([4 * self.normal]), rtol=1e-10 + self.dist.pdf(array([10.9])), array([4 * self.normal]), rtol=1e-10 ) def test_integral_normalized(self): """Verify the distribution integrates to 1 via the exact sum.""" n = len(self.dist.w) npt.assert_allclose( - np.sum(self.dist.w) * (2.0 * np.pi / n), 1.0, rtol=1e-10 + sum(self.dist.w) * (2.0 * pi / n), 1.0, rtol=1e-10 ) def test_integral_partial(self): """Verify partial integrals sum to 1 using a fine grid.""" M = 1_000_000 - xs = np.linspace(0, 2.0 * np.pi, M, endpoint=False) - dx = 2.0 * np.pi / M + xs = linspace(0, 2.0 * pi, M, endpoint=False) + dx = 2.0 * pi / M pdf_vals = self.dist.pdf(xs) - first_half = np.sum(pdf_vals[xs < np.pi]) * dx - second_half = np.sum(pdf_vals[xs >= np.pi]) * dx + first_half = sum(pdf_vals[xs < pi]) * dx + second_half = sum(pdf_vals[xs >= pi]) * dx npt.assert_allclose(first_half + second_half, 1.0, rtol=1e-4) def test_trigonometric_moment(self): """Verify analytical trigonometric moments using a fine-grid reference.""" M = 1_000_000 - xs = np.linspace(0, 2.0 * np.pi, M, endpoint=False) + xs = linspace(0, 2.0 * pi, M, endpoint=False) pdf_vals = self.dist.pdf(xs) - dx = 2.0 * np.pi / M + dx = 2.0 * pi / M for n_moment in [1, 2, 3]: - expected = np.sum(pdf_vals * np.exp(1j * n_moment * xs)) * dx + expected = sum(pdf_vals * exp(1j * n_moment * xs)) * dx with self.subTest(n=n_moment): npt.assert_allclose( self.dist.trigonometric_moment(n_moment), expected, rtol=1e-4 @@ -62,26 +61,26 @@ def test_trigonometric_moment(self): def test_interval_borders(self): self.assertAlmostEqual( - PiecewiseConstantDistribution.left_border(1, 2), 0.0 * 2.0 * np.pi + PiecewiseConstantDistribution.left_border(1, 2), 0.0 * 2.0 * pi ) self.assertAlmostEqual( PiecewiseConstantDistribution.interval_center(1, 2), - 1.0 / 4.0 * 2.0 * np.pi, + 1.0 / 4.0 * 2.0 * pi, ) self.assertAlmostEqual( PiecewiseConstantDistribution.right_border(1, 2), - 1.0 / 2.0 * 2.0 * np.pi, + 1.0 / 2.0 * 2.0 * pi, ) self.assertAlmostEqual( PiecewiseConstantDistribution.left_border(2, 2), - 1.0 / 2.0 * 2.0 * np.pi, + 1.0 / 2.0 * 2.0 * pi, ) self.assertAlmostEqual( PiecewiseConstantDistribution.interval_center(2, 2), - 3.0 / 4.0 * 2.0 * np.pi, + 3.0 / 4.0 * 2.0 * pi, ) self.assertAlmostEqual( - PiecewiseConstantDistribution.right_border(2, 2), 1.0 * 2.0 * np.pi + PiecewiseConstantDistribution.right_border(2, 2), 1.0 * 2.0 * pi ) def test_calculate_parameters_numerically(self): @@ -103,14 +102,14 @@ def test_entropy(self): """Verify analytical entropy against the direct formula.""" w = self.dist.w n = len(w) - expected = -2.0 * np.pi / n * np.sum(w * np.log(w)) + expected = -2.0 * pi / n * sum(w * log(w)) npt.assert_allclose(self.dist.entropy(), expected, rtol=1e-10) def test_sample(self): samples = self.dist.sample(100) self.assertEqual(len(samples), 100) - self.assertTrue(np.all(samples >= 0.0)) - self.assertTrue(np.all(samples < 2.0 * np.pi)) + self.assertTrue(all(samples >= 0.0)) + self.assertTrue(all(samples < 2.0 * pi)) if __name__ == "__main__": From a6234b832a351a6a28906f62c6521cf67bc233ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 18:24:59 +0000 Subject: [PATCH 3/5] Fix linter errors: redefined-builtin, F401, E741 (ambiguous 'l'), and pdf index bug Agent-Logs-Url: https://github.com/FlorianPfaff/PyRecEst/sessions/6e33f469-e06f-4f76-906e-7de00e354a5e Co-authored-by: FlorianPfaff <6773539+FlorianPfaff@users.noreply.github.com> --- pyrecest/distributions/__init__.py | 2 +- .../circle/piecewise_constant_distribution.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyrecest/distributions/__init__.py b/pyrecest/distributions/__init__.py index f7136d62a..771e942f5 100644 --- a/pyrecest/distributions/__init__.py +++ b/pyrecest/distributions/__init__.py @@ -84,7 +84,7 @@ SineSkewedWrappedCauchyDistribution, SineSkewedWrappedNormalDistribution, ) -from .circle.piecewise_constant_distribution import PiecewiseConstantDistribution +from .circle.piecewise_constant_distribution import PiecewiseConstantDistribution # noqa: F401 from .circle.von_mises_distribution import VonMisesDistribution from .circle.wrapped_cauchy_distribution import WrappedCauchyDistribution from .circle.wrapped_laplace_distribution import WrappedLaplaceDistribution diff --git a/pyrecest/distributions/circle/piecewise_constant_distribution.py b/pyrecest/distributions/circle/piecewise_constant_distribution.py index 5a7e97be8..e495080fa 100644 --- a/pyrecest/distributions/circle/piecewise_constant_distribution.py +++ b/pyrecest/distributions/circle/piecewise_constant_distribution.py @@ -1,4 +1,4 @@ -# pylint: disable=no-name-in-module,no-member +# pylint: disable=no-name-in-module,no-member,redefined-builtin from pyrecest.backend import mod, pi, array, mean, floor, zeros, exp, sum, log, random from .abstract_circular_distribution import AbstractCircularDistribution @@ -44,10 +44,10 @@ def pdf(self, xs): Pdf values at each point. """ assert xs.ndim == 1 + n_intervals = len(self.w) xs_mod = array(mod(xs, 2.0 * pi), dtype=float) - n = len(self.w) idx = array( - [min(int(floor(xs_mod[i] / (2.0 * pi) * n)), n - 1) for i in range(n)] + [min(int(floor(x / (2.0 * pi) * n_intervals)), n_intervals - 1) for x in xs_mod] ) return self.w[idx] @@ -69,11 +69,11 @@ def trigonometric_moment(self, n): num = len(self.w) interv = zeros(num, dtype=complex) for j in range(1, num + 1): - l = PiecewiseConstantDistribution.left_border(j, num) + left = PiecewiseConstantDistribution.left_border(j, num) r = PiecewiseConstantDistribution.right_border(j, num) c = PiecewiseConstantDistribution.interval_center(j, num) w_j = float(self.pdf(array([c]))[0]) - interv[j - 1] = w_j * (exp(1j * n * r) - exp(1j * n * l)) + interv[j - 1] = w_j * (exp(1j * n * r) - exp(1j * n * left)) return complex(-1j / n * sum(interv)) def entropy(self): @@ -189,9 +189,9 @@ def calculate_parameters_numerically(pdf_func, n): assert n >= 1 w = zeros(n) for j in range(1, n + 1): - l = PiecewiseConstantDistribution.left_border(j, n) + left = PiecewiseConstantDistribution.left_border(j, n) r = PiecewiseConstantDistribution.right_border(j, n) w[j - 1] = quad( - lambda x: float(pdf_func(array([x]))), l, r + lambda x: float(pdf_func(array([x]))), left, r )[0] return w From 64b5dd60b21220e18268e56f30f3fd7a1f710a94 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:39:44 +0000 Subject: [PATCH 4/5] Replace .size with .shape[0] for cross-backend compatibility Agent-Logs-Url: https://github.com/FlorianPfaff/PyRecEst/sessions/c71323fb-44d8-4dde-8144-38e76494136f Co-authored-by: FlorianPfaff <6773539+FlorianPfaff@users.noreply.github.com> --- .../distributions/circle/piecewise_constant_distribution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrecest/distributions/circle/piecewise_constant_distribution.py b/pyrecest/distributions/circle/piecewise_constant_distribution.py index e495080fa..c3b8755e6 100644 --- a/pyrecest/distributions/circle/piecewise_constant_distribution.py +++ b/pyrecest/distributions/circle/piecewise_constant_distribution.py @@ -27,7 +27,7 @@ def __init__(self, w): """ AbstractCircularDistribution.__init__(self) w = array(w, dtype=float).ravel() - assert w.ndim == 1 and w.size > 0 + assert w.ndim == 1 and w.shape[0] > 0 self.w = w / (mean(w) * 2.0 * pi) def pdf(self, xs): From 6814f0d07ec94fa80fbee69f89a035a6673238fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:05:12 +0000 Subject: [PATCH 5/5] Add PiecewiseConstantDistribution to __all__ in distributions __init__.py Agent-Logs-Url: https://github.com/FlorianPfaff/PyRecEst/sessions/f37fba1a-be63-4f27-953c-a60f6d784efd Co-authored-by: FlorianPfaff <6773539+FlorianPfaff@users.noreply.github.com> --- pyrecest/distributions/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyrecest/distributions/__init__.py b/pyrecest/distributions/__init__.py index 771e942f5..de52d8b32 100644 --- a/pyrecest/distributions/__init__.py +++ b/pyrecest/distributions/__init__.py @@ -84,7 +84,7 @@ SineSkewedWrappedCauchyDistribution, SineSkewedWrappedNormalDistribution, ) -from .circle.piecewise_constant_distribution import PiecewiseConstantDistribution # noqa: F401 +from .circle.piecewise_constant_distribution import PiecewiseConstantDistribution from .circle.von_mises_distribution import VonMisesDistribution from .circle.wrapped_cauchy_distribution import WrappedCauchyDistribution from .circle.wrapped_laplace_distribution import WrappedLaplaceDistribution @@ -272,6 +272,7 @@ "SineSkewedVonMisesDistribution", "SineSkewedWrappedCauchyDistribution", "SineSkewedWrappedNormalDistribution", + "PiecewiseConstantDistribution", "VonMisesDistribution", "WrappedCauchyDistribution", "WrappedLaplaceDistribution",