Skip to content

potential/backend: coerce scalar coords for backend-compatible scalar-only methods#990

Merged
jobovy merged 1 commit into
feat/backendsfrom
backend/scalar-only-coerce-inputs
Jun 21, 2026
Merged

potential/backend: coerce scalar coords for backend-compatible scalar-only methods#990
jobovy merged 1 commit into
feat/backendsfrom
backend/scalar-only-coerce-inputs

Conversation

@jobovy

@jobovy jobovy commented Jun 20, 2026

Copy link
Copy Markdown
Owner

What

Coerce scalar coordinates onto the active backend inside check_potential_inputs_not_arrays, for _backend_compatible potentials only.

The scalar-only compute methods of migrated potentials (RotateAndTiltWrapperPotential, DoubleExponentialDiskPotential) still do real backend arithmetic on their coordinates — xp.isinf(R), xp.cos(phi), xp.sin(phi) — which torch rejects on a plain python float under a forced backend:

TypeError: isinf(): argument 'input' (position 1) must be Tensor, not float
   at RotateAndTiltWrapperPotential.py:174  (Rinf = xp.isinf(R))

This was the root cause of the mockRotatedAndTilted* test_amp_mult_divide torch failures (the wrapper's normalize() and _evaluate reach the scalar-only methods with python-float R/z/phi).

 if (... array-shape check ...):
     raise TypeError(...)
+if getattr(self, "_backend_compatible", False):
+    xp = get_namespace(R, z, phi)
+    R, z, phi = coerce_coords(xp, R, z, phi)
 return func(self, R, z, phi, t)

The gate (caught by adversarial review)

An earlier unconditional version of this coercion regressed the unmigrated AnyAxisymmetricRazorThinDiskPotential — its bare scipy.integrate.quad / numpy internals reject a 0-d tensor ('<' not supported between numpy.ndarray and Tensor), turning ~9 currently-green forced-torch tests red. The fix mirrors the existing potential_physical_input gate (conversion.py: if xp is not numpy and _check_backend_compatible(Pot)): only coerce for _backend_compatible potentials, so unmigrated scalar-only potentials keep their python-float inputs. t is left uncoerced (it may be a hashable cache key downstream).

Result

  • Flips the RotateAndTilt-wrapped test_amp_mult_divide torch entries: mockRotatedAndTiltedMWP14WrapperPotential[wInclination] standalone; the TriaxialLogHalo-wrapped one composes with potential/backend: centrally coerce forced-backend scalar leaks #987's phi-default coercion (all 4 pass on feat/backends+potential/backend: centrally coerce forced-backend scalar leaks #987).
  • numpy byte-identical: coerce_coords is an object-identical pass-through when xp is numpy, and the gate skips it entirely for unmigrated potentials.
  • No regression: test_backend_disk.py+test_backend_diskexpansion.py (DoubleExp) 331✓, test_backend_wrappers.py 480✓, AnyAxiRazorThin forced-torch restored to baseline (the one ledgered test_normalize_potential xfail unchanged).
  • Ledger untouched (flipped entries become XPASS, green via strict=False; pruned in the separate ledger-regen PR).

Tests

  • test_scalar_only_python_float_inputs_forced_backend (test_backend_wrappers.py): passes plain python floats to RotateAndTilt's _evaluate/_Rforce/_zforce/_phitorque/_dens under a forced backend, so the decorator coercion is what's under test. Fails at the isinf line without the fix.
  • test_scalar_only_gate_spares_unmigrated_potential (test_backend_conventions.py): asserts AnyAxisymmetricRazorThinDiskPotential (not _backend_compatible) still evaluates under a forced backend — the regression guard for the gate.

The new coercion lines run on the numpy path too (pass-through), so they are covered by existing numpy tests.

🤖 Generated with Claude Code

…-only methods

The @check_potential_inputs_not_arrays scalar-only compute methods (e.g.
RotateAndTiltWrapperPotential, DoubleExponentialDiskPotential) still do real
backend arithmetic on their coordinates -- xp.isinf(R), xp.cos(phi),
xp.sin(phi) -- which torch rejects on a plain python float under a forced
backend (TypeError: isinf(): argument 'input' ... must be Tensor, not float).
Coerce R, z, phi onto the active backend once, in the shared decorator,
mirroring the potential_physical_input boundary -- but ONLY for
_backend_compatible potentials, so an unmigrated scalar-only potential
(AnyAxisymmetricRazorThinDiskPotential, whose internals are bare
scipy.integrate.quad / numpy) keeps its python-float inputs and does not
regress under a forced backend. t is left uncoerced (it may be a hashable
cache key downstream).

Flips the RotateAndTilt-wrapped test_amp_mult_divide torch entries
(mockRotatedAndTiltedMWP14WrapperPotential[wInclination] standalone; the
TriaxialLogHalo-wrapped one composes with #987's phi-default coercion). The
numpy path is byte-identical (coerce_coords is an object-identical
pass-through when xp is numpy; the gate skips it entirely for unmigrated
potentials). Adds a forced-backend regression test (plain python floats ->
decorator coerces, fails at the isinf line without the fix) and a gate-guard
test (unmigrated AnyAxiRazorThin must stay on numpy under a forced backend).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@jobovy jobovy enabled auto-merge (squash) June 20, 2026 21:46
@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.93%. Comparing base (e0d454d) to head (a64daf6).
⚠️ Report is 1 commits behind head on feat/backends.

Additional details and impacted files
@@              Coverage Diff               @@
##           feat/backends     #990   +/-   ##
==============================================
  Coverage          99.93%   99.93%           
==============================================
  Files                254      254           
  Lines              39833    39836    +3     
  Branches             834      840    +6     
==============================================
+ Hits               39808    39811    +3     
  Misses                25       25           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

Copy link
Copy Markdown
Contributor

All-backend test status (jax / torch)

Commit f6d832fd6aaca28a1c2024d33c7258fbfa6f127d

Green is achieved via the checked-in xfail-ledger (tests/backend_xfail.txt, applied xfail(strict=False)), so the metric to watch is the shrinking xfail count (burndown), not a raw pass count. A FAIL/ERR is an un-ledgered regression (reds the run). Because the ledger is non-strict, a now-passing ledgered test is a plain pass here (no per-push XPASS); burndown candidates -- in both directions -- are surfaced by the scheduled regen run, which rewrites the ledger from real outcomes. deferred is a separate burndown: tests skipped because they are unrunnable under the backend until the port is vectorized (see tests/backend_slow_skip.txt), e.g. the jax spherical-DF sampling/quadrature tests pending the Track F DF migration.

Overall: jax: 1057 passed · 265 xfail · 725 deferred | torch: 883 passed · 1160 xfail · 1 deferred

Ledger size: 2357 entries (jax=284, torch=2073).

Test shard jax torch
actionAngle ✅ 112 pass · 89 xfail ✅ 37 pass · 164 xfail
sphericaldf ✅ 164 pass · 26 xfail · 28 deferred ✅ 8 pass · 210 xfail
conversion + util + misc ✅ 86 pass · 5 xfail · 1 deferred ✅ 42 pass · 50 xfail
potential + scf + multipole — (no result) — (no result)
quantity + coords ✅ 287 pass · 49 xfail ✅ 206 pass · 130 xfail
orbit (energy/Jacobi + from_name) ✅ 0 pass · 0 xfail · 115 deferred ✅ 66 pass · 49 xfail
orbit + orbits (main) ✅ 0 pass · 0 xfail · 578 deferred ✅ 265 pass · 310 xfail
evolveddiskdf ✅ 35 pass · 0 xfail ✅ 32 pass · 3 xfail
jeans + dynamfric ✅ 17 pass · 2 xfail · 2 deferred ✅ 7 pass · 13 xfail · 1 deferred
qdf + pv2qdf + streamgapdf_impulse + noninertial ✅ 57 pass · 75 xfail · 1 deferred ✅ 14 pass · 119 xfail
streamgapdf ✅ 28 pass · 2 xfail ✅ 28 pass · 2 xfail
diskdf ✅ 129 pass · 0 xfail ✅ 112 pass · 17 xfail
streamdf + streamspraydf + streamTrack ✅ 142 pass · 17 xfail ✅ 66 pass · 93 xfail
Per-shard counts
Test shard backend pass xfail deferred XPASS fail error
actionAngle jax 112 89 0 0 0 0
actionAngle torch 37 164 0 0 0 0
sphericaldf jax 164 26 28 0 0 0
sphericaldf torch 8 210 0 0 0 0
conversion + util + misc jax 86 5 1 0 0 0
conversion + util + misc torch 42 50 0 0 0 0
potential + scf + multipole jax
potential + scf + multipole torch
quantity + coords jax 287 49 0 0 0 0
quantity + coords torch 206 130 0 0 0 0
orbit (energy/Jacobi + from_name) jax 0 0 115 0 0 0
orbit (energy/Jacobi + from_name) torch 66 49 0 0 0 0
orbit + orbits (main) jax 0 0 578 0 0 0
orbit + orbits (main) torch 265 310 0 0 0 0
evolveddiskdf jax 35 0 0 0 0 0
evolveddiskdf torch 32 3 0 0 0 0
jeans + dynamfric jax 17 2 2 0 0 0
jeans + dynamfric torch 7 13 1 0 0 0
qdf + pv2qdf + streamgapdf_impulse + noninertial jax 57 75 1 0 0 0
qdf + pv2qdf + streamgapdf_impulse + noninertial torch 14 119 0 0 0 0
streamgapdf jax 28 2 0 0 0 0
streamgapdf torch 28 2 0 0 0 0
diskdf jax 129 0 0 0 0 0
diskdf torch 112 17 0 0 0 0
streamdf + streamspraydf + streamTrack jax 142 17 0 0 0 0
streamdf + streamspraydf + streamTrack torch 66 93 0 0 0 0

@jobovy jobovy merged commit 38dad76 into feat/backends Jun 21, 2026
146 checks passed
@jobovy jobovy deleted the backend/scalar-only-coerce-inputs branch June 21, 2026 01:11
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.

1 participant