Skip to content

FEAT: derive C-parity sign for conjugate chains - #203

Draft
Zeyna777 wants to merge 13 commits into
bose_symmeriztionfrom
c-parity-sign
Draft

Zeyna777 wants to merge 13 commits into
bose_symmeriztionfrom
c-parity-sign

Conversation

@Zeyna777

@Zeyna777 Zeyna777 commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Implements the derivation described in that issue as a new ampform_dpd.cparity module. The physics, the reference values and the cross-checks live in the issue; what follows is how it is implemented.

✨ New features

New module ampform_dpd.cparity, which derives the relative sign between charge-conjugate decay chains and applies it to an AmplitudeModel:

function purpose
get_conjugate_state_map / is_c_symmetric the gate: the permutation of the final-state IDs, or False if charge conjugation maps the decay to a different process
get_conjugate_coupling_sign the sign $\xi$, per chain and per basis
get_exchange_phase the vertex phase $\eta_v$ on its own
get_conjugate_chain_pairs the chains that are tied to each other, particle first
get_c_forbidden_chains the selection rule for chains that charge conjugation maps onto themselves
relate_conjugate_couplings the coupling substitutions, sign on the production coupling
symmetrize_conjugate_couplings applies them to an AmplitudeModel and drops the dependent parameters
get_antiparticle_name the name of the charge conjugate of a particle

Details worth knowing when reading the diff:

  • C-parities and antiparticles are read from a QRules ParticleCollection, defaulting to load_particles(), so nothing about the decay has to be supplied by hand.
  • The basis is a public alias CouplingBasis = Literal["LS", "helicity"]. symmetrize_conjugate_couplings() infers it from the decay couplings of the model itself, which makes it work for $LS$ couplings, helicity couplings, mixed bases and the single-coefficient form of formulate() alike. A sign derived in one basis must never be applied to the couplings of the other, so this is not left to the caller.
  • The sign is distributed over the two vertices of a chain by putting it entirely on the production coupling. Only the product along a chain is observable, so this is a choice; it keeps the decay couplings of a resonance and its charge conjugate identical.
  • It is substituted into the model expression as a literal $+$ or $-$ rather than a coupled parameter, and the couplings that are no longer free are removed from parameter_defaults.
  • In the helicity basis the two indices of the decay node are swapped along with the substitution, because charge conjugation delivers the decay products in the order in which the partner chain lists them the other way round.
  • Chains that the selection rule forbids are reported as a UserWarning and left in the model; symmetrize_conjugate_couplings() raises ValueError if charge conjugation does not map the final state onto itself at all.

📝 Documentation

New page docs/cparity.ipynb walks through the derivation on the real implementation: the gate, the sign factor by factor in both bases, the selection rule with its two cross-checks, the coupling substitutions, and Dalitz plots showing that the sign leaves both bands untouched and only moves the interference where they cross, that tying restores mirror symmetry, and, in the $J/\psi \to 3\pi$ section, when mirror symmetry is and is not able to tell the two signs apart.

Notes

  • No existing behavior is touched: cparity is a new module, and everything else in the diff is test fixtures, documentation configuration and spell-check entries.
  • tests/test_cparity.py checks the tie against the amplitudes themselves, not only against the formula: charge conjugation relabels the final state without touching a momentum, so the intensity of a tied model has to be invariant under the induced permutation of the Mandelstam variables. Untied couplings give an asymmetry of order $10^{-1}$, the tie brings it to order $10^{-16}$.
  • The $LS$ pair-ordering mismatch that this work uncovered (LS Clebsch-Gordan factors use a different pair ordering than the isobar Wigner-d function #202) has meanwhile been fixed in FIX: use cyclic pair ordering for LS couplings #207, which is merged; the $LS$ basis is therefore no longer subject to the caveat that earlier revisions of this branch carried.

Squash commit messages

* DOC: add notebook on charge-conjugation symmetrization

@Zeyna777 Zeyna777 added ✨ Feature New feature added to the package 📝 Docs Improvements or additions to documentation labels Aug 3, 2026
@Zeyna777
Zeyna777 requested a review from redeboer August 3, 2026 14:20
@Zeyna777 Zeyna777 self-assigned this Aug 3, 2026
@redeboer

redeboer commented Aug 3, 2026

Copy link
Copy Markdown
Member

47a94e9: it would be good to check in a separate branch/PR whether #202 is really a bug. We can then also test this downstream in the polarimetry repo, because that does floating-point comparisons to a published model.

@redeboer
redeboer changed the base branch from main to fix-ls-pair-ordering August 3, 2026 20:12
Base automatically changed from fix-ls-pair-ordering to main August 4, 2026 18:55
@redeboer redeboer changed the title FEAT: derive C-parity sign for charge-conjugate decay chains FEAT: derive C-parity sign for conjugate chains Sep 9, 2026
@Zeyna777

Zeyna777 commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Bug: get_exchange_phase is missing the Fermi-statistics factor

TL;DR — the re-ordering phase $\eta_v$ accounts for the orbital and spin-coupling parts of a two-particle exchange, but not the statistics part. It is invisible unless both children of an isobar are fermions, i.e. an $X \to p\bar{p}$ chain. Consequence: get_c_forbidden_chains is inverted for such chains. Every conjugate-pair sign — i.e. everything relate_conjugate_couplings / symmetrize_conjugate_couplings actually applies to a model — is unaffected and correct.

Where it comes from

Writing a two-particle state with the two children swapped costs three independent factors:

origin identity factor
orbital $Y_{lm}(-\hat{p}) = (-1)^l Y_{lm}(\hat{p})$ $(-1)^{l}$
spin coupling CG exchange symmetry $(-1)^{s_i+s_j-S}$
statistics two fermionic creation operators anticommute $(-1)^{(2s_i)(2s_j)}$

Eq. exchange-phase (and nb-exchange-phase in the notebook) has the first two. It is the same third factor that appears in the textbook derivation $C(f\bar{f}) = (-1)^{l}\cdot(-1)^{S+1}\cdot(-1) = (-1)^{l+S}$ — space exchange, spin exchange, and Fermi statistics.

The check that pins it down

For a chain that C maps onto itself, the spectator is self-conjugate, which forces the isobar's two children to be a particle–antiparticle pair. The selection rule $s=+1$ then requires $\eta$ to come out equal to the C-parity of that pair — a PDG number, independent of any phase convention:

pair $C$ (PDG) $\eta$ on this branch
$\pi^{+}\pi^{-}$ $(-1)^{l}$ $(-1)^{l}$ ✅
$p\bar{p}$ $(-1)^{l+S}$ $-(-1)^{l+S}$ ❌

QRules' own c_parity_conservation is a fully independent implementation and hard-codes exactly those two formulas ($(-1)^{l+S}$ for a fermion–antifermion pair, $(-1)^{l}$ for a spinless boson pair), so calling it on the same vertices reproduces the table.

Reproducer

$J/\psi \to \pi^0 p\bar{p}$: an $X \to p\bar{p}$ recoiling against the $\pi^0$ needs $C_X = C_\psi C_{\pi^0} = -1$, which allows the $\rho^0$ (${}^3S_1$) and forbids the $f_2(1270)$ (${}^3P_2$). QRules will not generate a meson isobar for this final state, so the chains are hand-built:

from ampform_dpd.adapter.qrules import load_particles
from ampform_dpd.cparity import get_c_forbidden_chains
from ampform_dpd.decay import (
    IsobarNode, LSCoupling, Particle, State, ThreeBodyDecay, ThreeBodyDecayChain,
)

db = load_particles()

def mk(name, index=None):
    p = db[name]
    kw = dict(name=p.name, latex=p.name, spin=p.spin, parity=p.parity,
              mass=p.mass, width=p.width)
    return State(index=index, **kw) if index is not None else Particle(**kw)

psi, pi0, p, pbar = mk("J/psi(1S)", 0), mk("pi0", 1), mk("p", 2), mk("p~", 3)

def chain(resonance, L, S):  # spectator 1 => cyclic pair (2, 3) = (p, pbar)
    return ThreeBodyDecayChain(IsobarNode(
        parent=psi, child2=pi0, interaction=LSCoupling(1, 1),
        child1=IsobarNode(parent=mk(resonance), child1=p, child2=pbar,
                          interaction=LSCoupling(L, S)),
    ))

decay = ThreeBodyDecay(
    states={0: psi, 1: pi0, 2: p, 3: pbar},
    chains=[chain("rho(770)0", 0, 1),    # 3S1 -> C = (-1)^(L+S) = -1
            chain("f(2)(1270)", 1, 1)],  # 3P2 -> C = (-1)^(L+S) = +1
)
print("reported forbidden:", [c.resonance.name for c in get_c_forbidden_chains(decay)])
reported forbidden: ['rho(770)0']
correct  forbidden: ['f(2)(1270)']

Both rows are inverted: the physically allowed $C=-1$ state is reported as forbidden, and the $C=+1$ state that C conservation actually excludes is cleared.

Suggested fix

--- a/src/ampform_dpd/cparity.py
+++ b/src/ampform_dpd/cparity.py
@@ def get_exchange_phase
              " momentum coupling"
          )
          raise ValueError(msg)
-    return (-1) ** int(exponent)
+    statistics_phase = (-1) ** int(4 * child1.spin * child2.spin)
+    return statistics_phase * (-1) ** int(exponent)

$4 s_i s_j = (2s_i)(2s_j)$ is always an integer and is odd exactly when both children are fermions.

All 43 tests still pass with this applied — which is itself the finding. No fixture puts two fermions in one isobar: the $J/\psi \to \eta p\bar{p}$ fixture has isobars $(\eta, p)$ and $(\bar{p}, \eta)$ (one boson each), and the $J/\psi \to 3\pi$ fixture is all bosons. it_agrees_with_qrules is a genuinely strong cross-check, but only for the bosonic branch.

Also needs

  • the extra factor in Eq. exchange-phase (module docstring) and nb-exchange-phase (notebook)
  • rewording the "$C^2 = 1$" sentence quoted above
  • a regression test with a fermion–antifermion isobar, hand-built as in the reproducer (cf. it_requires_ls_couplings_in_the_ls_basis)

Not affected

  • Conjugate pairs. A pair requires a non-self-conjugate spectator, which puts at most one of the two fermions inside the isobar, so the statistics factor is always $+1$ there. The signs actually substituted into a model are correct as they stand.
  • $s = C_\psi C_\eta (-1)^{l} = P_{N^*}$ for $J/\psi \to \eta p\bar{p}$, and the $\rho^{\pm}$ result for $J/\psi \to 3\pi$.
  • The state map, the cyclic-ordering claim, both bases' angular-momentum phases, the helicity-index swap, and parking the sign on the production coupling.

@redeboer

redeboer commented Sep 10, 2026 •

Copy link
Copy Markdown
Member

#203 (comment) Some thoughts:

  • Sounds like this is a separate issue? I vaguely remember that particle symmetrization (whether fermion or boson) is implemented in AmpForm.
  • It that's the case, I am not sure about the suggested fix. Why is it in cparity.py?
  • Note that Bose symmetrization is implemented in FEAT: add TR-036 on D⁺ → π⁺π⁺π⁻ Dalitz model report#44 as a hack.

After offline discussion with @Zeyna777:

@redeboer
redeboer removed this pull request from stack #208 September 15, 2026 18:58
@redeboer
redeboer marked this pull request as draft September 15, 2026 19:00
@redeboer
redeboer changed the base branch from main to bose_symmeriztion September 15, 2026 19:22
@redeboer
redeboer added this pull request to stack #247 September 15, 2026 19:23

@redeboer redeboer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First review without inspecting the diff too much yet:

  • Implement #249
  • Update that document with Fermi statistics factor
  • Verify if the implementation still agrees with the imported note(s)
  • Fix the CI problems

redeboer and others added 5 commits September 16, 2026 17:42
Generalises the sign convention of ComPWA/jpsi-nstar#573 from J/psi -> p pbar
eta to an arbitrary three-body decay, as a new `ampform_dpd.cparity` module.

The sign is assembled from the three factors of the general derivation: the
C-parity of the initial state, the C-parities of the final-state particles that
charge conjugation leaves in place, and the exchange phase of every isobar
vertex whose children charge conjugation re-orders. In the cyclic pair ordering
of the DPD paper only the decay vertex can be re-ordered, and its phase is
(-1)^(l+s_i+s_j-S) for LS couplings and (-1)^(J_R-s_i-s_j) for helicity
couplings, so the two bases give different signs.

Public API:

- `get_conjugate_state_map` / `is_c_symmetric`: the gate, i.e. the permutation
  of the final-state IDs induced by charge conjugation.
- `get_conjugate_coupling_sign`: the sign itself, per chain and per basis.
- `get_conjugate_chain_pairs`: the chains that are tied to each other.
- `get_c_forbidden_chains`: the selection rule for chains that charge
  conjugation maps onto themselves.
- `relate_conjugate_couplings` / `symmetrize_conjugate_couplings`: apply the
  tie to an `AmplitudeModel`, for LS, helicity, mixed and single-coefficient
  couplings, with the sign on the production coupling.

C-parities and antiparticles are read from a QRules `ParticleCollection`, so no
decay-specific input is needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Walks through the general derivation and shows it at work on the actual
`ampform_dpd.cparity` implementation:

- the gate, i.e. which decays charge conjugation constrains at all, with
  J/psi -> K0 Sigma+ pbar as the counter-example;
- the sign of J/psi -> eta p pbar factor by factor, in both bases, showing
  that it collapses to the parity of the N* in the LS basis and that the
  helicity basis disagrees for the 1/2+ state;
- the selection rule for chains that are mapped onto themselves, with the
  rho+- / rho0 / f2(1270) case of J/psi -> pi0 pi- pi+ and its independent
  isospin and QRules cross-checks;
- the coupling substitutions on an AmplitudeModel;
- Dalitz plots: the sign leaves both bands untouched and only moves the
  interference between the conjugate chains, and tying the couplings restores
  the mirror symmetry that untied couplings break;
- why mirror symmetry cannot arbitrate the sign, and the convention caveats,
  including a plot of the LS-basis pair-ordering asymmetry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The relative sign between waves of different l comes out wrong when the tie is
applied to an LS model whose conjugate pair involves subsystem 2, because the
builder writes that subsystem's Clebsch-Gordan factors in a different pair
ordering than the derivation assumes. Records that in the module docstring, the
xfail reason and the notebook, and points all three at the issue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A model made of a conjugate pair alone is mirror-symmetric for either sign,
since an overall factor s drops out of the modulus. Adding a chain that charge
conjugation maps onto itself breaks that degeneracy, because the self-mapped
chain interferes with the pair. J/psi -> 3pi with rho+- and rho0 is the
smallest example: the derived sign leaves the intensity mirror-symmetric to
6e-14, the flipped one to 3e-01.
Lena Poepping and others added 8 commits September 16, 2026 17:42
The notebook and the module docstring defined the same three math labels, and
both are rendered into the documentation, so Sphinx reported duplicate labels
and the build failed on warnings. The canonical names stay with the module,
whose functions cross-reference them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Charge conjugation interchanges a particle-antiparticle pair, and putting the two
creation operators back in the order in which the amplitude defines its final state
costs (-1)^(2s). Without it the sign is inverted wherever that pair is fermionic: for
J/psi -> eta p pbar, QRules keeps the C=-1 omega -> p pbar chain and drops the C=+1
f2(1270), while get_c_forbidden_chains() claimed the opposite. The signs of that
channel become s_LS = -P_N* and s_hel = +1. All-boson channels are unaffected, which
is why the existing cross-checks did not catch this.

Two further fixes:

- The sign now goes on the decay coupling in the LS basis, where it depends on (l, S)
  and several decay waves of one resonance share a production coupling, and LS
  couplings are matched by their (l, S) indices so that two waves cannot overwrite
  each other's sign.
- A self-mapped chain in the helicity basis ties its own decay couplings as
  H[j,i] = s H[i,j] rather than vanishing. get_c_forbidden_chains() reports that
  instead of condemning the whole chain, since the tie cannot be applied as a
  substitution while the helicity indices are still summation variables.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📝 Docs Improvements or additions to documentation ✨ Feature New feature added to the package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Publish the C-parity sign derivation with executable checks Tie the couplings of charge-conjugate decay chains, with the correct C-parity sign

3 participants