You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #852, and a concrete first consumer for #189.
The problem
MultipoleProfilePotential (#852) is the first potential in galax that
carries an explicit symmetry, and it does so with an ad-hoc stringly-typed
field:
with the accepted values None, "spherical", "axisymmetric", "triaxial" interpreted by a single function, lm_keys(l_max, symmetry),
which returns the (l, m) modes that can be non-zero. Anything else raises ValueError("Unknown symmetry ...").
That works for the one thing it was built for — pruning modes that vanish —
but it is the wrong long-term shape:
Stringly typed. No IDE completion, no static checking, typos caught
only at runtime and only inside one function.
It encodes exactly one consequence of symmetry. Mode pruning. It says
nothing about input types, evaluation shortcuts, or composition.
The taxonomy is already awkward."triaxial" here means "octant
symmetry" — even l, even m >= 0 — i.e. reflection symmetry in all
three planes. That is a stronger statement than "triaxial" usually means,
and it is not the same axis as "axisymmetric", which after ✨ feat(potential): add MultipoleProfilePotential #852 correctly
keeps alll at m = 0 (axisymmetry constrains m, not l; assuming
it also kills odd l was a real bug, fixed in that PR). The two options
are not points on one scale, and a flag makes that explicit where a string
hides it.
#189 proposes that potentials know their symmetry so that, for example, a
spherical potential can be called with a 1-D radius and the machinery
promotes it to 3-D Cartesian internally. Composed as a flag rather than an
enum of named cases, the same declaration can drive several things at once:
which (l, m) modes can be non-zero (what lm_keys does today);
which input types are safe — r for spherical, (R, z) for axisymmetric, xyz otherwise — and how to promote them;
evaluation shortcuts (a spherical expansion needs no harmonic table at
all);
how symmetry composes under the xfm wrappers, which currently can silently
destroy it: TriaxialInThePotential applied to a spherical base is no
longer spherical, and nothing today notices.
Reflection symmetry about each plane, azimuthal symmetry and full spherical
symmetry are independent bits, which is what makes a flag the right
representation and what the current four-string enumeration cannot express.
Reimplement lm_keys(l_max, symmetry) against it. The current mapping is
the specification to preserve:
no symmetry -> all (l, m), -l <= m <= l
spherical -> (0, 0) only
axisymmetric -> m = 0, all l
octant/triaxial -> even l, even m >= 0
Have from_density / from_potential take the flag, and — where the
source is itself a galax potential — default to its declared symmetry
rather than None. Today the caller must supply it by hand and gets a
full expansion if they forget, which is correct but wasteful.
Keep __check_init__'s consistency check (lm_keys == lm_keys(l_max, symmetry)), retargeted at the flag.
Follow-up to #852, and a concrete first consumer for #189.
The problem
MultipoleProfilePotential(#852) is the first potential ingalaxthatcarries an explicit symmetry, and it does so with an ad-hoc stringly-typed
field:
with the accepted values
None,"spherical","axisymmetric","triaxial"interpreted by a single function,lm_keys(l_max, symmetry),which returns the
(l, m)modes that can be non-zero. Anything else raisesValueError("Unknown symmetry ...").That works for the one thing it was built for — pruning modes that vanish —
but it is the wrong long-term shape:
only at runtime and only inside one function.
nothing about input types, evaluation shortcuts, or composition.
a package that will then have two.
"triaxial"here means "octantsymmetry" — even
l, evenm >= 0— i.e. reflection symmetry in allthree planes. That is a stronger statement than "triaxial" usually means,
and it is not the same axis as
"axisymmetric", which after ✨ feat(potential): add MultipoleProfilePotential #852 correctlykeeps all
latm = 0(axisymmetry constrainsm, notl; assumingit also kills odd
lwas a real bug, fixed in that PR). The two optionsare not points on one scale, and a flag makes that explicit where a string
hides it.
What #189 would give it
#189 proposes that potentials know their symmetry so that, for example, a
spherical potential can be called with a 1-D radius and the machinery
promotes it to 3-D Cartesian internally. Composed as a flag rather than an
enum of named cases, the same declaration can drive several things at once:
(l, m)modes can be non-zero (whatlm_keysdoes today);rfor spherical,(R, z)for axisymmetric,xyzotherwise — and how to promote them;all);
xfmwrappers, which currently can silentlydestroy it:
TriaxialInThePotentialapplied to a spherical base is nolonger spherical, and nothing today notices.
Reflection symmetry about each plane, azimuthal symmetry and full spherical
symmetry are independent bits, which is what makes a flag the right
representation and what the current four-string enumeration cannot express.
The migration for this PR's code
Once #189 exists:
symmetry: str | Nonewith the flag type.lm_keys(l_max, symmetry)against it. The current mapping isthe specification to preserve:
(l, m),-l <= m <= l(0, 0)onlym = 0, allll, evenm >= 0from_density/from_potentialtake the flag, and — where thesource is itself a
galaxpotential — default to its declared symmetryrather than
None. Today the caller must supply it by hand and gets afull expansion if they forget, which is correct but wasteful.
__check_init__'s consistency check (lm_keys == lm_keys(l_max, symmetry)), retargeted at the flag.Notes
MultipoleProfilePotential'ssymmetryargument.Worth doing before it has downstream users; ✨ feat(potential): add MultipoleProfilePotential #852 is the moment it becomes
public.
galax.potential.multipole_profile.lm_keysis public, so its signature ispart of the change.
needed a symmetry mechanism, and the general design belongs with Support symmetries in potentials #189
rather than being invented inside one potential.