Skip to content

Port ToroidalVMMatrixDistribution (bivariate von Mises, matrix version) from libDirectional#1605

Merged
FlorianPfaff merged 7 commits intomainfrom
copilot/port-toroidal-vm-matrix-distribution
Apr 2, 2026
Merged

Port ToroidalVMMatrixDistribution (bivariate von Mises, matrix version) from libDirectional#1605
FlorianPfaff merged 7 commits intomainfrom
copilot/port-toroidal-vm-matrix-distribution

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 1, 2026

Ports the ToroidalVMMatrixDistribution class from MATLAB/libDirectional to Python, implementing the bivariate von Mises distribution with a 2×2 correlation matrix A (Mardia 1975; Kurz & Hanebeck MFI 2015).

New distribution: ToroidalVMMatrixDistribution

Parameters: mu (2,), kappa (2,), A (2×2)

PDF:

f(x) = C · exp(κ₁cos(x₁−μ₁) + κ₂cos(x₂−μ₂)
             + cos(x₁−μ₁)·A₀₀·cos(x₂−μ₂) + cos(x₁−μ₁)·A₀₁·sin(x₂−μ₂)
             + sin(x₁−μ₁)·A₁₀·cos(x₂−μ₂) + sin(x₁−μ₁)·A₁₁·sin(x₂−μ₂))
  • Normalization: Taylor series (≤8 terms) when κ ≤ 1.5 and max|A| ≤ 1; falls back to numerical dblquad for higher concentrations
  • multiply(other): Exact closed-form product via matrix rotation formula (combines mu/kappa as in the 1D von Mises case, transforms A via a 4×4 rotation matrix)
  • marginalize_to_1d(dimension): Analytic marginal using the identity ∫exp(α·cos θ + β·sin θ)dθ = 2π·I₀(√(α²+β²)), correct for both dimension=0 and dimension=1
  • shift(angles): Returns copy with mu shifted mod 2π
mu = array([1.0, 2.0])
kappa = array([0.7, 1.4])
A = array([[0.5, 0.1], [-0.1, 0.3]])
dist = ToroidalVMMatrixDistribution(mu, kappa, A)

dist2 = dist.multiply(dist)           # exact product
marginal = dist.marginalize_to_1d(0)  # CustomCircularDistribution

Copilot AI and others added 2 commits April 1, 2026 07:45
Port MATLAB ToroidalVMMatrixDistribution to Python. Implements:
- PDF with kappa concentration and A correlation matrix
- Series approximation normalization for low concentrations (n=7 terms)
- Numerical normalization (dblquad) for high concentrations
- multiply(): exact product of two distributions
- marginalize_to_1d(): Bessel-function-based analytic marginal
- shift(): shift mu parameters

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: FlorianPfaff <6773539+FlorianPfaff@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: FlorianPfaff <6773539+FlorianPfaff@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 1, 2026

MegaLinter analysis: Success

Descriptor Linter Files Fixed Errors Warnings Elapsed time
✅ COPYPASTE jscpd yes no no 7.32s
✅ JSON prettier 2 0 0 0 0.8s
✅ JSON v8r 2 0 0 3.08s
✅ MARKDOWN markdownlint 1 0 0 0 0.66s
✅ MARKDOWN markdown-table-formatter 1 0 0 0 0.2s
✅ PYTHON bandit 252 0 0 3.11s
✅ PYTHON black 252 1 0 0 4.65s
✅ PYTHON flake8 252 0 0 1.78s
✅ PYTHON isort 252 1 0 0 0.48s
✅ PYTHON mypy 252 0 0 4.02s
✅ PYTHON pylint 252 0 0 71.11s
✅ PYTHON ruff 252 1 0 0 0.04s
✅ REPOSITORY checkov yes no no 22.89s
✅ REPOSITORY gitleaks yes no no 4.01s
✅ REPOSITORY git_diff yes no no 0.02s
✅ REPOSITORY secretlint yes no no 5.54s
✅ REPOSITORY syft yes no no 3.27s
✅ REPOSITORY trivy-sbom yes no no 1.7s
✅ REPOSITORY trufflehog yes no no 16.29s
✅ YAML prettier 4 0 0 0 0.45s
✅ YAML v8r 4 0 0 5.5s
✅ YAML yamllint 4 0 0 0.47s

See detailed reports in MegaLinter artifacts

Your project could benefit from a custom flavor, which would allow you to run only the linters you need, and thus improve runtime performances. (Skip this info by defining FLAVOR_SUGGESTIONS: false)

  • Documentation: Custom Flavors
  • Command: npx mega-linter-runner@9.4.0 --custom-flavor-setup --custom-flavor-linters PYTHON_PYLINT,PYTHON_BLACK,PYTHON_FLAKE8,PYTHON_ISORT,PYTHON_BANDIT,PYTHON_MYPY,PYTHON_RUFF,COPYPASTE_JSCPD,JSON_V8R,JSON_PRETTIER,MARKDOWN_MARKDOWNLINT,MARKDOWN_MARKDOWN_TABLE_FORMATTER,REPOSITORY_CHECKOV,REPOSITORY_GIT_DIFF,REPOSITORY_GITLEAKS,REPOSITORY_SECRETLINT,REPOSITORY_SYFT,REPOSITORY_TRIVY_SBOM,REPOSITORY_TRUFFLEHOG,YAML_PRETTIER,YAML_YAMLLINT,YAML_V8R

MegaLinter is graciously provided by OX Security
Show us your support by starring ⭐ the repository

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 1, 2026

Test Results

   12 files     12 suites   3h 20m 51s ⏱️
  597 tests   597 ✅     0 💤 0 ❌
7 376 runs  6 128 ✅ 1 248 💤 0 ❌

Results for commit 94c91a3.

♻️ This comment has been updated with latest results.

@FlorianPfaff
Copy link
Copy Markdown
Owner

@copilot My pyrecest.backend can be numpy, pytorch, or Jax (all unified under a numpy-like interface). Please use this and never import anything from numpy.

Copilot AI and others added 2 commits April 1, 2026 17:55
…bution and its tests

Agent-Logs-Url: https://github.com/FlorianPfaff/PyRecEst/sessions/c69a6a6e-41ad-4f91-a595-1258f8d4c196

Co-authored-by: FlorianPfaff <6773539+FlorianPfaff@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 1, 2026

@copilot My pyrecest.backend can be numpy, pytorch, or Jax (all unified under a numpy-like interface). Please use this and never import anything from numpy.

Done in commit 7768c8c. Both toroidal_vm_matrix_distribution.py and its test file now use pyrecest.backend exclusively — import numpy as np has been removed and all numpy calls (np.cos, np.sin, np.arctan2, np.sqrt, np.max, np.abs, np.linalg.solve, np.pi, np.array) are replaced with the corresponding pyrecest.backend imports (cos, sin, arctan2, sqrt, max, abs, linalg.solve, pi, array). The test file retains numpy.testing as npt only for assertion utilities, consistent with other test files in the repo.

@FlorianPfaff
Copy link
Copy Markdown
Owner

@copilot Can you investigate and fix those two test failures?
=========================== short test summary info ============================
FAILED pyrecest/tests/distributions/test_toroidal_vm_matrix_distribution.py::TestToroidalVMMatrixDistribution::test_marginalize_to_1d_dim0 - TypeError: only 0-dimensional arrays can be converted to Python scalars
FAILED pyrecest/tests/distributions/test_toroidal_vm_matrix_distribution.py::TestToroidalVMMatrixDistribution::test_marginalize_to_1d_dim1 - TypeError: only 0-dimensional arrays can be converted to Python scalars
=== 2 failed, 595 passed, 1 warning, 37 subtests passed in 245.08s (0:04:05) ===

…odule in closures

Agent-Logs-Url: https://github.com/FlorianPfaff/PyRecEst/sessions/87bf7f69-bcb1-447d-a7d3-8799c6d86712

Co-authored-by: FlorianPfaff <6773539+FlorianPfaff@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 2, 2026

@copilot Can you investigate and fix those two test failures?
=========================== short test summary info ============================
FAILED pyrecest/tests/distributions/test_toroidal_vm_ma...

Fixed in commit 9eeea76.

The root cause: integrate_fun_over_domain_part calls the pdf via lambda *args: self.pdf(array(args)), which wraps the scalar integration variable in a 1-element array array([x]). The closures in marginalize_to_1d were using math.cos/sin/sqrt/exp, which only accept Python scalars — newer numpy raises TypeError: only 0-dimensional arrays can be converted to Python scalars when given a 1-D array.

Fix: replaced math.cos/sin/sqrt/exp in the two integration closures with the already-imported backend functions cos/sin/sqrt/exp, which handle arrays natively. Also removed the now-unused bare import math (keeping from math import factorial for _norm_const_approx).

@FlorianPfaff
Copy link
Copy Markdown
Owner

@copilot Are all these conversions to float really necessary?

…tribution

Agent-Logs-Url: https://github.com/FlorianPfaff/PyRecEst/sessions/e29fd9a4-61f5-434b-b521-e472ea09896d

Co-authored-by: FlorianPfaff <6773539+FlorianPfaff@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 2, 2026

@copilot Are all these conversions to float really necessary?

No, they weren't necessary. Removed all of them in commit 94c91a3 — the backend returns numpy-like scalars that work natively in arithmetic, comparisons, and scipy calls. The only case that needed attention was the dblquad lambda, which now uses .item() (the idiomatic way to extract a Python scalar from a 0-D array) instead of float().

@FlorianPfaff FlorianPfaff merged commit 17460d2 into main Apr 2, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants