From dd75ef257b429a525b45bd38daff09ca1c46759a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:53:54 +0000 Subject: [PATCH 1/3] Changes before error encountered Agent-Logs-Url: https://github.com/FlorianPfaff/PyRecEst/sessions/a6fbc6c8-a141-4bd2-a54c-833a26347f70 Co-authored-by: FlorianPfaff <6773539+FlorianPfaff@users.noreply.github.com> --- pyrecest/distributions/__init__.py | 4 + .../toroidal_von_mises_cosine_distribution.py | 96 +++++++++++++++ ..._toroidal_von_mises_cosine_distribution.py | 116 ++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 pyrecest/distributions/hypertorus/toroidal_von_mises_cosine_distribution.py create mode 100644 pyrecest/tests/distributions/test_toroidal_von_mises_cosine_distribution.py diff --git a/pyrecest/distributions/__init__.py b/pyrecest/distributions/__init__.py index 72efac1ad..d51a2a519 100644 --- a/pyrecest/distributions/__init__.py +++ b/pyrecest/distributions/__init__.py @@ -181,6 +181,9 @@ from .hypertorus.toroidal_dirac_distribution import ToroidalDiracDistribution from .hypertorus.toroidal_mixture import ToroidalMixture from .hypertorus.toroidal_uniform_distribution import ToroidalUniformDistribution +from .hypertorus.toroidal_von_mises_cosine_distribution import ( + ToroidalVonMisesCosineDistribution, +) from .hypertorus.toroidal_von_mises_sine_distribution import ( ToroidalVonMisesSineDistribution, ) @@ -316,6 +319,7 @@ "ToroidalDiracDistribution", "ToroidalMixture", "ToroidalUniformDistribution", + "ToroidalVonMisesCosineDistribution", "ToroidalVonMisesSineDistribution", "ToroidalWrappedNormalDistribution", "AbstractHyperrectangularDistribution", diff --git a/pyrecest/distributions/hypertorus/toroidal_von_mises_cosine_distribution.py b/pyrecest/distributions/hypertorus/toroidal_von_mises_cosine_distribution.py new file mode 100644 index 000000000..7af63caf9 --- /dev/null +++ b/pyrecest/distributions/hypertorus/toroidal_von_mises_cosine_distribution.py @@ -0,0 +1,96 @@ +# pylint: disable=redefined-builtin,no-name-in-module,no-member +import numpy as np +from pyrecest.backend import all, array, cos, exp, mod, pi, sum +from scipy.special import iv + +from .abstract_toroidal_distribution import AbstractToroidalDistribution + +_SERIES_TERMS = 10 + + +class ToroidalVonMisesCosineDistribution(AbstractToroidalDistribution): + """Bivariate von Mises distribution, cosine model. + + Corresponds to A = [-kappa3, 0; 0, -kappa3]. + + References: + Mardia, K. V.; Taylor, C. C. & Subramaniam, G. K. + Protein Bioinformatics and Mixtures of Bivariate von Mises Distributions + for Angular Data Biometrics, 2007, 63, 505-512 + + Mardia, K. V. & Frellsen, J. in Hamelryck, T.; Mardia, K. & + Ferkinghoff-Borg, J. (Eds.) + Statistics of Bivariate von Mises Distributions + Bayesian Methods in Structural Bioinformatics, + Springer Berlin Heidelberg, 2012, 159-178 + """ + + def __init__(self, mu, kappa, kappa3): + AbstractToroidalDistribution.__init__(self) + assert mu.shape == (2,) + assert kappa.shape == (2,) + assert kappa3.shape == () + assert all(kappa >= 0.0) + + self.mu = mod(mu, 2.0 * pi) + self.kappa = kappa + self.kappa3 = kappa3 + + self.C = 1.0 / self.norm_const + + @property + def norm_const(self): + def s(p): + return iv(p, self.kappa[0]) * iv(p, self.kappa[1]) * iv(p, -self.kappa3) + + Cinv = 4.0 * pi**2 * ( + s(0) + 2.0 * sum(array([s(p) for p in range(1, _SERIES_TERMS + 1)])) + ) + return Cinv + + def pdf(self, xs): + assert xs.shape[-1] == 2 + p = self.C * exp( + self.kappa[0] * cos(xs[..., 0] - self.mu[0]) + + self.kappa[1] * cos(xs[..., 1] - self.mu[1]) + - self.kappa3 * cos(xs[..., 0] - self.mu[0] - xs[..., 1] + self.mu[1]) + ) + return p + + def trigonometric_moment(self, n): + if n == 1: + def s1(m): + return ( + (iv(m + 1, self.kappa[0]) + iv(m - 1, self.kappa[0])) + * iv(m, self.kappa[1]) + * iv(m, -self.kappa3) + ) + + def s2(m): + return ( + iv(m, self.kappa[0]) + * (iv(m + 1, self.kappa[1]) + iv(m - 1, self.kappa[1])) + * iv(m, -self.kappa3) + ) + + def s(p): + return iv(p, self.kappa[0]) * iv(p, self.kappa[1]) * iv(p, -self.kappa3) + + terms = range(1, _SERIES_TERMS + 1) + s1_sum = s1(0) / 2.0 + sum(array([s1(m) for m in terms])) + s2_sum = s2(0) / 2.0 + sum(array([s2(m) for m in terms])) + s_sum = s(0) + 2.0 * sum(array([s(p) for p in terms])) + + # Use numpy directly here because the result is inherently complex + # and pyrecest.backend does not support complex-valued arrays. + m1 = float(s1_sum) / float(s_sum) * np.exp(1j * n * float(self.mu[0])) + m2 = float(s2_sum) / float(s_sum) * np.exp(1j * n * float(self.mu[1])) + return np.array([m1, m2]) + return self.trigonometric_moment_numerical(n) + + def shift(self, shift_by): + assert shift_by.shape == (self.dim,) + tvm = ToroidalVonMisesCosineDistribution( + mod(self.mu + shift_by, 2.0 * pi), self.kappa, self.kappa3 + ) + return tvm diff --git a/pyrecest/tests/distributions/test_toroidal_von_mises_cosine_distribution.py b/pyrecest/tests/distributions/test_toroidal_von_mises_cosine_distribution.py new file mode 100644 index 000000000..6cbd77392 --- /dev/null +++ b/pyrecest/tests/distributions/test_toroidal_von_mises_cosine_distribution.py @@ -0,0 +1,116 @@ +import unittest + +import matplotlib +import numpy.testing as npt + +# pylint: disable=no-name-in-module,no-member +import pyrecest.backend +from parameterized import parameterized + +# pylint: disable=no-name-in-module,no-member +from pyrecest.backend import arange, array, column_stack, cos, exp, pi +from pyrecest.distributions.hypertorus.toroidal_von_mises_cosine_distribution import ( + ToroidalVonMisesCosineDistribution, +) + +matplotlib.pyplot.close("all") +matplotlib.use("Agg") + + +class ToroidalVMCosineDistributionTest(unittest.TestCase): + def setUp(self): + self.mu = array([1.0, 2.0]) + self.kappa = array([0.7, 1.4]) + self.kappa3 = array(0.5) + self.tvm = ToroidalVonMisesCosineDistribution( + self.mu, self.kappa, self.kappa3 + ) + + def test_instance(self): + self.assertIsInstance(self.tvm, ToroidalVonMisesCosineDistribution) + + def test_mu_kappa_kappa3(self): + npt.assert_allclose(self.tvm.mu, self.mu) + npt.assert_allclose(self.tvm.kappa, self.kappa) + self.assertEqual(self.tvm.kappa3, self.kappa3) + + @unittest.skipIf( + pyrecest.backend.__backend_name__ in ("pytorch", "jax"), + reason="Not supported on this backend", + ) + def test_integral(self): + self.assertAlmostEqual(self.tvm.integrate(), 1.0, delta=1e-5) + + @unittest.skipIf( + pyrecest.backend.__backend_name__ == "jax", + reason="Not supported on this backend", + ) + def test_trigonometric_moment_numerical(self): + npt.assert_allclose( + self.tvm.trigonometric_moment_numerical(0), array([1.0, 1.0]) + ) + + def test_plot_2d(self): + self.tvm.plot() + + # jscpd:ignore-start + # pylint: disable=R0801 + def _unnormalized_pdf(self, xs): + return exp( + self.kappa[0] * cos(xs[..., 0] - self.mu[0]) + + self.kappa[1] * cos(xs[..., 1] - self.mu[1]) + - self.kappa3 + * cos(xs[..., 0] - self.mu[0] - xs[..., 1] + self.mu[1]) + ) + + # jscpd:ignore-end + + @parameterized.expand( + [ + (array([3.0, 2.0]),), + (array([1.0, 4.0]),), + (array([5.0, 6.0]),), + (array([-3.0, 11.0]),), + (array([[5.0, 1.0], [6.0, 3.0]]),), + ( + column_stack( + (arange(0.0, 2.0 * pi, 0.1), arange(1.0 * pi, 3.0 * pi, 0.1)) + ), + ), + ] + ) + def test_pdf(self, x): + C = self.tvm.C + + def pdf(x): + return self._unnormalized_pdf(x) * C + + expected = pdf(x) + + npt.assert_allclose(self.tvm.pdf(x), expected) + + @unittest.skipIf( + pyrecest.backend.__backend_name__ == "jax", + reason="Not supported on this backend", + ) + def test_trigonometric_moment_analytical(self): + m_analytical = self.tvm.trigonometric_moment(1) + m_numerical = self.tvm.trigonometric_moment_numerical(1) + npt.assert_allclose(m_analytical, m_numerical, rtol=1e-8) + + def test_shift(self): + shift_by = array([4.0, 2.0]) + tvm2 = self.tvm.shift(shift_by) + self.assertIsInstance(tvm2, ToroidalVonMisesCosineDistribution) + x_test = column_stack( + (arange(0.0, 2.0 * pi, 0.3), arange(0.0, 2.0 * pi, 0.3)) + ) + npt.assert_allclose( + tvm2.pdf(x_test), + self.tvm.pdf(x_test - shift_by), + atol=1e-10, + ) + + +if __name__ == "__main__": + unittest.main() From b9f051bd6d620dda84b326b2aa72557350c8a2a8 Mon Sep 17 00:00:00 2001 From: Florian Pfaff Date: Wed, 1 Apr 2026 17:40:38 +0200 Subject: [PATCH 2/3] Fixed test case --- .../test_toroidal_von_mises_cosine_distribution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrecest/tests/distributions/test_toroidal_von_mises_cosine_distribution.py b/pyrecest/tests/distributions/test_toroidal_von_mises_cosine_distribution.py index 6cbd77392..2d9a11921 100644 --- a/pyrecest/tests/distributions/test_toroidal_von_mises_cosine_distribution.py +++ b/pyrecest/tests/distributions/test_toroidal_von_mises_cosine_distribution.py @@ -108,7 +108,7 @@ def test_shift(self): npt.assert_allclose( tvm2.pdf(x_test), self.tvm.pdf(x_test - shift_by), - atol=1e-10, + atol=1e-10, rtol=1e-6 ) From 02879f6459655d2e185eaa065217736742639889 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:24:24 +0000 Subject: [PATCH 3/3] Extract ToroidalBivarVMTestMixin to share tests between sine and cosine test classes Agent-Logs-Url: https://github.com/FlorianPfaff/PyRecEst/sessions/21f93465-a948-420c-adf1-090db0d9281e Co-authored-by: FlorianPfaff <6773539+FlorianPfaff@users.noreply.github.com> --- pyrecest/distributions/__init__.py | 2 - ..._toroidal_von_mises_cosine_distribution.py | 56 ++----------------- ...st_toroidal_von_mises_sine_distribution.py | 55 +++++++++--------- 3 files changed, 35 insertions(+), 78 deletions(-) diff --git a/pyrecest/distributions/__init__.py b/pyrecest/distributions/__init__.py index 415b4a98d..53c8c3e85 100644 --- a/pyrecest/distributions/__init__.py +++ b/pyrecest/distributions/__init__.py @@ -184,8 +184,6 @@ from .hypertorus.toroidal_uniform_distribution import ToroidalUniformDistribution from .hypertorus.toroidal_von_mises_cosine_distribution import ( ToroidalVonMisesCosineDistribution, -from .hypertorus.toroidal_vm_rivest_distribution import ( - ToroidalVMRivestDistribution, ) from .hypertorus.toroidal_von_mises_sine_distribution import ( ToroidalVonMisesSineDistribution, diff --git a/pyrecest/tests/distributions/test_toroidal_von_mises_cosine_distribution.py b/pyrecest/tests/distributions/test_toroidal_von_mises_cosine_distribution.py index 2d9a11921..4f0e23aa0 100644 --- a/pyrecest/tests/distributions/test_toroidal_von_mises_cosine_distribution.py +++ b/pyrecest/tests/distributions/test_toroidal_von_mises_cosine_distribution.py @@ -5,19 +5,21 @@ # pylint: disable=no-name-in-module,no-member import pyrecest.backend -from parameterized import parameterized # pylint: disable=no-name-in-module,no-member from pyrecest.backend import arange, array, column_stack, cos, exp, pi from pyrecest.distributions.hypertorus.toroidal_von_mises_cosine_distribution import ( ToroidalVonMisesCosineDistribution, ) +from pyrecest.tests.distributions.test_toroidal_von_mises_sine_distribution import ( + ToroidalBivarVMTestMixin, +) matplotlib.pyplot.close("all") matplotlib.use("Agg") -class ToroidalVMCosineDistributionTest(unittest.TestCase): +class ToroidalVMCosineDistributionTest(ToroidalBivarVMTestMixin, unittest.TestCase): def setUp(self): self.mu = array([1.0, 2.0]) self.kappa = array([0.7, 1.4]) @@ -34,27 +36,6 @@ def test_mu_kappa_kappa3(self): npt.assert_allclose(self.tvm.kappa, self.kappa) self.assertEqual(self.tvm.kappa3, self.kappa3) - @unittest.skipIf( - pyrecest.backend.__backend_name__ in ("pytorch", "jax"), - reason="Not supported on this backend", - ) - def test_integral(self): - self.assertAlmostEqual(self.tvm.integrate(), 1.0, delta=1e-5) - - @unittest.skipIf( - pyrecest.backend.__backend_name__ == "jax", - reason="Not supported on this backend", - ) - def test_trigonometric_moment_numerical(self): - npt.assert_allclose( - self.tvm.trigonometric_moment_numerical(0), array([1.0, 1.0]) - ) - - def test_plot_2d(self): - self.tvm.plot() - - # jscpd:ignore-start - # pylint: disable=R0801 def _unnormalized_pdf(self, xs): return exp( self.kappa[0] * cos(xs[..., 0] - self.mu[0]) @@ -63,32 +44,6 @@ def _unnormalized_pdf(self, xs): * cos(xs[..., 0] - self.mu[0] - xs[..., 1] + self.mu[1]) ) - # jscpd:ignore-end - - @parameterized.expand( - [ - (array([3.0, 2.0]),), - (array([1.0, 4.0]),), - (array([5.0, 6.0]),), - (array([-3.0, 11.0]),), - (array([[5.0, 1.0], [6.0, 3.0]]),), - ( - column_stack( - (arange(0.0, 2.0 * pi, 0.1), arange(1.0 * pi, 3.0 * pi, 0.1)) - ), - ), - ] - ) - def test_pdf(self, x): - C = self.tvm.C - - def pdf(x): - return self._unnormalized_pdf(x) * C - - expected = pdf(x) - - npt.assert_allclose(self.tvm.pdf(x), expected) - @unittest.skipIf( pyrecest.backend.__backend_name__ == "jax", reason="Not supported on this backend", @@ -108,7 +63,8 @@ def test_shift(self): npt.assert_allclose( tvm2.pdf(x_test), self.tvm.pdf(x_test - shift_by), - atol=1e-10, rtol=1e-6 + atol=1e-10, + rtol=1e-6, ) diff --git a/pyrecest/tests/distributions/test_toroidal_von_mises_sine_distribution.py b/pyrecest/tests/distributions/test_toroidal_von_mises_sine_distribution.py index e3e54ff5c..de959cf50 100644 --- a/pyrecest/tests/distributions/test_toroidal_von_mises_sine_distribution.py +++ b/pyrecest/tests/distributions/test_toroidal_von_mises_sine_distribution.py @@ -17,28 +17,18 @@ matplotlib.use("Agg") -class ToroidalVMSineDistributionTest(unittest.TestCase): - def setUp(self): - self.mu = array([1.0, 2.0]) - self.kappa = array([0.7, 1.4]) - self.lambda_ = array(0.5) - self.tvm = ToroidalVonMisesSineDistribution(self.mu, self.kappa, self.lambda_) +class ToroidalBivarVMTestMixin: + """Shared tests for bivariate toroidal von Mises distribution test classes. - def test_instance(self): - # sanity check - self.assertIsInstance(self.tvm, ToroidalVonMisesSineDistribution) - - def test_mu_kappa_lambda(self): - npt.assert_allclose(self.tvm.mu, self.mu) - npt.assert_allclose(self.tvm.kappa, self.kappa) - self.assertEqual(self.tvm.lambda_, self.lambda_) + Subclasses must implement ``setUp`` (setting ``self.mu``, ``self.kappa``, + and ``self.tvm``) and ``_unnormalized_pdf``. + """ @unittest.skipIf( pyrecest.backend.__backend_name__ in ("pytorch", "jax"), reason="Not supported on this backend", ) def test_integral(self): - # test integral self.assertAlmostEqual(self.tvm.integrate(), 1.0, delta=1e-5) @unittest.skipIf( @@ -53,17 +43,6 @@ def test_trigonometric_moment_numerical(self): def test_plot_2d(self): self.tvm.plot() - # jscpd:ignore-start - # pylint: disable=R0801 - def _unnormalized_pdf(self, xs): - return exp( - self.kappa[0] * cos(xs[..., 0] - self.mu[0]) - + self.kappa[1] * cos(xs[..., 1] - self.mu[1]) - + self.lambda_ * sin(xs[..., 0] - self.mu[0]) * sin(xs[..., 1] - self.mu[1]) - ) - - # jscpd:ignore-end - @parameterized.expand( [ (array([3.0, 2.0]),), @@ -89,5 +68,29 @@ def pdf(x): npt.assert_allclose(self.tvm.pdf(x), expected) +class ToroidalVMSineDistributionTest(ToroidalBivarVMTestMixin, unittest.TestCase): + def setUp(self): + self.mu = array([1.0, 2.0]) + self.kappa = array([0.7, 1.4]) + self.lambda_ = array(0.5) + self.tvm = ToroidalVonMisesSineDistribution(self.mu, self.kappa, self.lambda_) + + def test_instance(self): + # sanity check + self.assertIsInstance(self.tvm, ToroidalVonMisesSineDistribution) + + def test_mu_kappa_lambda(self): + npt.assert_allclose(self.tvm.mu, self.mu) + npt.assert_allclose(self.tvm.kappa, self.kappa) + self.assertEqual(self.tvm.lambda_, self.lambda_) + + def _unnormalized_pdf(self, xs): + return exp( + self.kappa[0] * cos(xs[..., 0] - self.mu[0]) + + self.kappa[1] * cos(xs[..., 1] - self.mu[1]) + + self.lambda_ * sin(xs[..., 0] - self.mu[0]) * sin(xs[..., 1] - self.mu[1]) + ) + + if __name__ == "__main__": unittest.main()