Skip to content

Port SphericalHarmonicsFilter from MATLAB to Python#1588

Open
Copilot wants to merge 4 commits intomainfrom
copilot/add-spherical-harmonics-filter
Open

Port SphericalHarmonicsFilter from MATLAB to Python#1588
Copilot wants to merge 4 commits intomainfrom
copilot/add-spherical-harmonics-filter

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 31, 2026

The feature/sharmonics_filter branch contained a stub SphericalHarmonicsFilter and test file but was missing the implementation of the required distribution operations (convolve, multiply, rotate, from_distribution_numerical_fast).

Distribution methods added to SphericalHarmonicsDistributionComplex

  • from_distribution_numerical_fast(dist, degree, transformation) — evaluates dist.pdf on a pyshtools Driscoll-Healy grid and recovers SH coefficients via discrete SHT; replaces the slow integral-based variant
  • convolve(other) — zonal spherical convolution:
    • identity: direct frequency-domain formula h_{l,m} = √(4π/(2l+1)) · f_{l,m} · g_{l,0}
    • sqrt: grid-based with a 2× finer intermediate grid (avoids aliasing from squaring )
  • multiply(other) — grid-based pointwise product; for sqrt transformation √p · √q = √(pq) so no extra step needed
  • rotate(alpha, beta, gamma) — ZYZ Euler-angle rotation via pyshtools.SHCoeffs.rotate(..., body=True)
  • Private helpers: _coeff_mat_to_pysh, _pysh_to_coeff_mat, _get_dh_grid_cartesian, _eval_on_grid, _fit_from_grid

SphericalHarmonicsFilter (new file)

Complete implementation following the MATLAB reference:

filt = SphericalHarmonicsFilter(degree=21, transformation='sqrt')
filt.set_state(SphericalHarmonicsDistributionComplex.from_distribution_numerical_fast(prior, 21, 'sqrt'))
filt.predict_identity(noise_shd)           # zonal convolution
filt.update_identity(meas_shd, z)          # multiply + optional rotation
filt.update_nonlinear(likelihood_fn, z)    # grid-based Bayes update
filt.update_nonlinear_multiple(lks, zs)    # batch update with multiple likelihoods
mean = filt.get_estimate_mean()

Both identity and sqrt transformations produce the same underlying posterior (verified to atol=5e-15 in test_prediction_sqrt_vs_id).

Minor additions

  • VonMisesFisherFilter: added set_state / get_estimate_mean helpers needed by the shared test
  • pyrecest/filters/__init__.py: exported VonMisesFisherFilter
  • test_spherical_harmonics_filter.py: four tests ported from the MATLAB reference suite

Copilot AI and others added 2 commits March 31, 2026 20:27
…ibution_numerical_fast

Agent-Logs-Url: https://github.com/FlorianPfaff/PyRecEst/sessions/a02e218a-d9bc-4d03-9c38-8776e308de70

Co-authored-by: FlorianPfaff <6773539+FlorianPfaff@users.noreply.github.com>
…scale factor comment

Agent-Logs-Url: https://github.com/FlorianPfaff/PyRecEst/sessions/a02e218a-d9bc-4d03-9c38-8776e308de70

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

github-actions bot commented Mar 31, 2026

MegaLinter analysis: Error

Descriptor Linter Files Fixed Errors Warnings Elapsed time
✅ COPYPASTE jscpd yes no no 8.11s
✅ JSON prettier 2 0 0 0 0.4s
✅ JSON v8r 2 0 0 2.43s
✅ MARKDOWN markdownlint 1 0 0 0 0.72s
✅ MARKDOWN markdown-table-formatter 1 0 0 0 0.21s
✅ PYTHON bandit 251 0 0 3.17s
✅ PYTHON black 251 3 0 0 4.74s
❌ PYTHON flake8 251 8 0 1.84s
✅ PYTHON isort 251 3 0 0 0.49s
✅ PYTHON mypy 251 0 0 4.18s
❌ PYTHON pylint 251 20 0 72.11s
❌ PYTHON ruff 251 3 7 0 0.11s
✅ REPOSITORY checkov yes no no 22.02s
✅ REPOSITORY gitleaks yes no no 4.62s
✅ REPOSITORY git_diff yes no no 0.04s
✅ REPOSITORY secretlint yes no no 5.62s
✅ REPOSITORY syft yes no no 3.26s
✅ REPOSITORY trivy-sbom yes no no 1.73s
✅ REPOSITORY trufflehog yes no no 17.58s
✅ YAML prettier 4 0 0 0 0.48s
✅ YAML v8r 4 0 0 4.94s
✅ YAML yamllint 4 0 0 0.46s

Detailed Issues

❌ PYTHON / flake8 - 8 errors
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:7:1: F811 redefinition of unused 'sin' from line 7
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:223:21: F821 undefined name 'np'
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:344:17: E741 ambiguous variable name 'l'
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:379:17: E741 ambiguous variable name 'l'
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:435:9: F401 'pyshtools as pysh' imported but unused
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:440:13: F821 undefined name 'np'
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:441:13: F821 undefined name 'np'
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:442:13: F821 undefined name 'np'
❌ PYTHON / pylint - 20 errors
************* Module pyrecest.distributions.hypersphere_subset.spherical_harmonics_distribution_complex
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:7:0: W0404: Reimport 'sin' (imported line 7) (reimported)
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:223:20: E0602: Undefined variable 'np' (undefined-variable)
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:327:4: R0914: Too many local variables (21/15) (too-many-locals)
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:365:21: W0212: Access to a protected member _eval_on_grid of a client class (protected-access)
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:391:21: W0212: Access to a protected member _eval_on_grid of a client class (protected-access)
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:420:17: W0212: Access to a protected member _eval_on_grid of a client class (protected-access)
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:445:12: E0602: Undefined variable 'np' (undefined-variable)
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:446:12: E0602: Undefined variable 'np' (undefined-variable)
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:447:12: E0602: Undefined variable 'np' (undefined-variable)
pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:440:8: W0611: Unused pyshtools imported as pysh (unused-import)
************* Module pyrecest.filters.spherical_harmonics_filter
pyrecest/filters/spherical_harmonics_filter.py:4:0: W0622: Redefining built-in 'abs' (redefined-builtin)
pyrecest/filters/spherical_harmonics_filter.py:196:4: R0914: Too many local variables (16/15) (too-many-locals)
pyrecest/filters/spherical_harmonics_filter.py:203:12: W0212: Access to a protected member _get_dh_grid_cartesian of a client class (protected-access)
pyrecest/filters/spherical_harmonics_filter.py:232:29: W0212: Access to a protected member _fit_from_grid of a client class (protected-access)
************* Module pyrecest.tests.filters.test_spherical_harmonics_filter
pyrecest/tests/filters/test_spherical_harmonics_filter.py:65:74: W0640: Cell variable sigma_x defined in loop (cell-var-from-loop)
pyrecest/tests/filters/test_spherical_harmonics_filter.py:66:74: W0640: Cell variable sigma_y defined in loop (cell-var-from-loop)
pyrecest/tests/filters/test_spherical_harmonics_filter.py:67:74: W0640: Cell variable sigma_z defined in loop (cell-var-from-loop)
pyrecest/tests/filters/test_spherical_harmonics_filter.py:71:54: W0640: Cell variable sigma_x defined in loop (cell-var-from-loop)
pyrecest/tests/filters/test_spherical_harmonics_filter.py:72:54: W0640: Cell variable sigma_y defined in loop (cell-var-from-loop)
pyrecest/tests/filters/test_spherical_harmonics_filter.py:73:54: W0640: Cell variable sigma_z defined in loop (cell-var-from-loop)

-----------------------------------
Your code has been rated at 9.97/10
❌ PYTHON / ruff - 7 errors
F821 Undefined name `np`
   --> pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:222:21
    |
220 |     def _pysh_to_coeff_mat(clm, degree):
221 |         """Convert a pyshtools SHComplexCoeffs object to our coeff_mat."""
222 |         coeff_mat = np.zeros((degree + 1, 2 * degree + 1), dtype=complex)
    |                     ^^
223 |         max_n = min(clm.lmax, degree)
224 |         for n in range(max_n + 1):
    |

E741 Ambiguous variable name: `l`
   --> pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:343:17
    |
341 |             # Direct frequency-domain formula: h_{l,m} = sqrt(4π/(2l+1)) * f_{l,m} * g_{l,0}
342 |             h_lm = zeros_like(self.coeff_mat)
343 |             for l in range(degree + 1):
    |                 ^
344 |                 factor = sqrt(4.0 * pi / (2 * l + 1)) * other.coeff_mat[l, l]
345 |                 for m in range(-l, l + 1):
    |

E741 Ambiguous variable name: `l`
   --> pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:378:17
    |
376 |             # Convolution formula on the identity coefficients
377 |             r_lm = zeros_like(p_lm)
378 |             for l in range(degree + 1):
    |                 ^
379 |                 factor = sqrt(4.0 * pi / (2 * l + 1)) * q_lm[l, l]
380 |                 for m in range(-l, l + 1):
    |

F821 Undefined name `np`
   --> pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:438:13
    |
436 |         clm = self._coeff_mat_to_pysh(self.coeff_mat, degree)
437 |         clm_rot = clm.rotate(
438 |             np.degrees(alpha),
    |             ^^
439 |             np.degrees(beta),
440 |             np.degrees(gamma),
    |

F821 Undefined name `np`
   --> pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:439:13
    |
437 |         clm_rot = clm.rotate(
438 |             np.degrees(alpha),
439 |             np.degrees(beta),
    |             ^^
440 |             np.degrees(gamma),
441 |             degrees=True,
    |

F821 Undefined name `np`
   --> pyrecest/distributions/hypersphere_subset/spherical_harmonics_distribution_complex.py:440:13
    |
438 |             np.degrees(alpha),
439 |             np.degrees(beta),
440 |             np.degrees(gamma),
    |             ^^
441 |             degrees=True,
442 |             body=True,
    |

Found 7 errors (1 fixed, 6 remaining).

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 Mar 31, 2026

Test Results

0 tests   0 ✅  0s ⏱️
0 suites  0 💤
0 files    0 ❌

Results for commit cead808.

♻️ This comment has been updated with latest results.

@FlorianPfaff FlorianPfaff force-pushed the copilot/add-spherical-harmonics-filter branch from dd20732 to cead808 Compare April 2, 2026 17:33
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