Skip to content

Add support for parsing decay descriptors - #573

Open
admorris wants to merge 13 commits into
scikit-hep:mainfrom
admorris:descriptor_parsing
Open

Add support for parsing decay descriptors#573
admorris wants to merge 13 commits into
scikit-hep:mainfrom
admorris:descriptor_parsing

Conversation

@admorris

Copy link
Copy Markdown
Contributor

Implements the rest of #200

Added DecayChain.from_string method

Decay descriptors are parsed with Lark. A Transformer class converts them into DecayChainDict objects, which are then used to initialise DecayChain objects.

Custom descriptor formats can be used by pointing to another .lark file in an argument of DecayChain.from_string. These pretty much only have the freedom to modify ARROW, LPAR and RPAR. The rest of the structure is assumed by the Transformer. i.e. I did not find a way to support sub-decays written with the mother outside of braces like A -> B (-> C D) E

One glaring limitation (which is inherent to DecayChain/_build_decay_modes) is that sub-decays of identically named particles are not supported: e.g.. "B_s0 -> (phi -> K+ K-) (phi -> K+ K-)" will result in an exception. This could possibly be handled by adding internal/hidden uniqueness when duplicates are encountered.

@eduardo-rodrigues

Copy link
Copy Markdown
Member

Hi @admorris, I am not forgetting to check this. It's just that I have been working on urgent and important suff. Will get back to you very soon.

@eduardo-rodrigues eduardo-rodrigues added the enhancement New feature or request label May 1, 2026
Comment thread src/decaylanguage/data/descriptor.lark Outdated
Comment thread src/decaylanguage/data/descriptor.lark
Comment thread src/decaylanguage/decay/decay.py
Comment thread src/decaylanguage/decay/decay.py
return cls(mother, decay_modes)

@classmethod
def from_string(

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.

At some point it would make sense to "synchronise" this from_string function with the existing to_string one, since they should effectively be the "mirror of each other". Else one would name this function to from_descriptor. WDYT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The pytest test_from_string_to_string demonstrates they mirror eachother in the specific case of using the default grammar.

What if this function is renamed from_descriptor, then from_string just invokes from_descriptor with the default grammar?

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.

Yes, that sounds good to me.

Comment thread src/decaylanguage/decay/decay.py
Comment thread src/decaylanguage/decay/decay.py
Comment thread src/decaylanguage/decay/decay.py Outdated
Comment thread tests/data/descriptor_alt.lark

@eduardo-rodrigues eduardo-rodrigues 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.

Thank you so much for this, @admorris! It's a really nice enhancement 👍.

I left a few little suggestions but this is looking excellent anway.

I am well aware of the limitation you point out. It does annoy me.

@eduardo-rodrigues

Copy link
Copy Markdown
Member

Hey @admorris, let me know your thoughts on this enhancement and whether you would prefer to have a follow-up for the limitations and some of the matters discussed above.

BTW, for the issue with parsing parenthesis for particle names. One thing that differentiates parentheses for particle names wrt parenthesis denoting decays is that the latter always contain an arrow inside, so between ( and ).

@eduardo-rodrigues

Copy link
Copy Markdown
Member

Hey. So aside the trivial comment above, I think there are only 2 things to get sorted:$

  • Add some more info/doc to the descriptor.lark file as it got reasonably complicated to deal with particle names involving parentheses.
  • Enhance the tests to include a couple of complicated particle names with parentheses, so names of the sort psi(2S) or Upsilon_3(1D) or anti-Lambda_b(5920)0.

@eduardo-rodrigues

Copy link
Copy Markdown
Member

Hi @admorris, you may have seen the major improvements made to the package recently, thanks to @henryiii. We are moving towards a 1.0 release ...

Let me know if you will be able to pick up the work here after a rebase.

FYI you can now do the following:

In [2]: dm_Bs = DecayMode(1, 'phi phi')
   ...: dm_phi = DecayMode(1, 'K+ K-')
   ...: dc = DecayChain('Bs', {'Bs':dm_Bs, 'phi':dm_phi})

In [3]: dc_dict = dc.to_dict()

In [4]: DecayChain.from_dict(dc_dict).to_dict() == dc_dict
Out[4]: True

In [5]: dc.to_string()
Out[5]: 'Bs -> (phi -> K+ K-) (phi -> K+ K-)'

@admorris

Copy link
Copy Markdown
Contributor Author

Very good! I will have a look

@admorris
admorris force-pushed the descriptor_parsing branch from 2943a59 to c8eb6de Compare June 16, 2026 17:38
@henryiii

Copy link
Copy Markdown
Member

Whenever you are ready, I can do an Opus 4.8 or GPT 5.5 review of the PR if that sounds helpful. (if you are using one of those, or already ran a review with one, let me know and I'll do the other one, otherwise I'll do Opus because I've got higher limits on that)

@eduardo-rodrigues

Copy link
Copy Markdown
Member

Whenever you are ready, I can do an Opus 4.8 or GPT 5.5 review of the PR if that sounds helpful. (if you are using one of those, or already ran a review with one, let me know and I'll do the other one, otherwise I'll do Opus because I've got higher limits on that)

Hello @henryiii, I will get back to this PR now that version 1.0 is out. Before I do a "physics" review, if you can then run an Opus or similar review, that will be great. Thank you.

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.62338% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.38%. Comparing base (42bd8ab) to head (9447a1b).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/decaylanguage/decay/decay.py 76.62% 18 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #573      +/-   ##
==========================================
- Coverage   97.62%   96.38%   -1.25%     
==========================================
  Files           6        6              
  Lines        1222     1299      +77     
==========================================
+ Hits         1193     1252      +59     
- Misses         29       47      +18     

☔ 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.

@eduardo-rodrigues

Copy link
Copy Markdown
Member

Whenever you are ready, I can do an Opus 4.8 or GPT 5.5 review of the PR if that sounds helpful. (if you are using one of those, or already ran a review with one, let me know and I'll do the other one, otherwise I'll do Opus because I've got higher limits on that)

Hello @henryiii, I will get back to this PR now that version 1.0 is out. Before I do a "physics" review, if you can then run an Opus or similar review, that will be great. Thank you.

Hi @henryiii, a lot has been happening recently. I would like to get back to this now that the other developments are available in the latest 1.x releases. Very happy, then, to have an AI review from you. Let me know, thanks.

@henryiii

henryiii commented Aug 5, 2026

Copy link
Copy Markdown
Member

Claude Fable standard review:

🤖 AI text below 🤖

Review of PR #573 — Add support for parsing decay descriptors

Overview

Adds DecayChain.from_string, the inverse of to_string, closing out #200. A new data/descriptor.lark grammar parses descriptors like "D*+ -> (D0 -> K+ pi-) pi+"; a Lark Transformer (_DescriptorTreeToDict) converts the parse tree into a DecayChainDict fed to the existing from_dict. Custom grammars can be supplied via grammar_file. __eq__/__hash__ are added to DecayMode and DecayChain to support the equality-based tests.

The design fits the codebase well — same Lark-grammar-in-data/ pattern as dec/ and modeling/, and reusing from_dict keeps the new surface small. I verified the grammar handles the tricky cases correctly: K*(892)0 (parenthesized mass suffix, with or without a sub-decay), flat descriptors with no sub-decay, and trailing suffixes all parse right. The explicit whitespace handling (instead of %ignore) is justified — it keeps K_1 (1270) from gluing into one particle.

Issues

1. Error contract is leaky for same-name sub-decays (verified by running the PR code). The docstring promises ValueError for malformed input, but only LarkError and transformer exceptions are wrapped; whatever from_dict raises escapes as-is. I tested:

  • "B_s0 -> (phi -> K+ K-) (phi -> K+ K-)" (identical duplicates) actually works fine, contrary to the limitation stated in the PR description.
  • "B_s0 -> (phi -> K+ K-) (phi -> pi+ pi- pi0)" (same name, different modes) raises RuntimeError: Input is not a single decay chain! — a confusing message that doesn't tell the user the real problem, and the wrong exception type per the docstring.

Suggest wrapping the from_dict call too, re-raising as ValueError with a message naming the duplicated particle, and documenting the limitation in the from_string docstring rather than only in the PR body.

2. Misleading error for a broken custom grammar (decay.py:973). Lark(grammar, ...) is constructed inside the try that catches LarkError, so a syntax error in a user's custom grammar file is reported as "Failed to parse descriptor '...'". Construct the parser outside that try (or catch GrammarError separately). Also worth documenting that custom grammars must be LALR-compatible, and that a bad grammar_file path raises FileNotFoundError.

3. Parser is rebuilt on every call. LALR table construction runs per from_string invocation. Anyone parsing descriptors in a loop pays it repeatedly. A module-level @functools.lru_cache keyed on the grammar text (or path) is a two-line fix.

4. RST underline too short (data/README.rst). The ``descriptor.lark`` title is 19 chars but the underline is 18 dashes (verified) — docutils emits a "title underline too short" warning.

5. Missing CHANGELOG.md entry. Project convention per CLAUDE.md, and doubly warranted here: adding __eq__ to DecayMode/DecayChain changes public behavior (equality was identity-based before), which users should hear about in release notes, not just the new from_string feature.

Test coverage

The positive-path tests are good — repurposing the existing formatter examples for round-trip tests is exactly right. Gaps:

  • No negative tests: nothing asserts ValueError for malformed descriptors ("D0 ->", empty string, unbalanced parens).
  • No flat-descriptor test ("D0 -> K- pi+" — no sub-decay). It works; a test would keep it working.
  • No test for the same-name sub-decay cases from issue 1, in either the working or failing variant.
  • No test for a suffix particle like K*(892)0, which is the case the pgroup_paren machinery exists for — currently that whole code path is untested.

Minor / style

  • Lark(grammar, parser="lalr", transformer=None, lexer="auto"): transformer=None and lexer="auto" are the defaults — drop them. (Alternatively, passing the transformer directly to a LALR Lark is Lark's intended fast path, though the current two-step form gives cleaner error separation.)
  • __hash__ raising TypeError works, but the conventional __hash__ = None also makes isinstance(x, collections.abc.Hashable) return False; the current form leaves instances looking hashable to ABC checks. If it was done to satisfy strict mypy, a brief comment would save the next reader the puzzle.
  • psuffix returning "" (decay.py transformer): the fallback is unreachable — psuffix always has exactly one non-ws child; return self._item_text(items[0]) would be more honest.
  • grammar_file handling: Path(grammar_file).read_text(...) covers both str and Path without the isinstance branch.
  • The alternative test grammar's PARTICLE regex contains an invisible combining macron (U+0304) in its character class — presumably intentional for -style names, but worth a comment since it's indistinguishable from a stray character.

Verdict

Solid, well-scoped feature that follows the project's grammar-driven architecture, and the grammar itself held up to every edge case I threw at it. The main asks before merge: tighten the error contract for same-name sub-decays (issue 1 — the actual behavior differs from what the PR description claims, in both directions), fix the misleading custom-grammar error path, add the missing negative/edge tests, and the changelog entry plus one-character RST fix.

@eduardo-rodrigues

Copy link
Copy Markdown
Member

Super, thank you much, @henryiii. There's now some bits of work to do :).

"D0 -> (K_S0 -> pi+ pi-) (pi0 -> gamma gamma)",
"D*+ -> (D0 -> (K_S0 -> pi+ pi-) (pi0 -> gamma gamma)) pi+",
"B0 -> (D- -> K+ pi- pi-) (tau+ -> anti-nu_tau pi+ pi+ pi-) nu_tau",
"B_s0 -> (phi -> K+ K-) (phi' -> pi+ pi- pi0)",

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.

Add here "B_s0 -> (phi -> K+ K-) (phi -> K+ K-)" as per my comment above on the limitation being a thing of the past.

@eduardo-rodrigues

Copy link
Copy Markdown
Member

Hello @admorris, cc @henryiii. The review is very good. There are things that we could leave for a follow-up enhancement. Other small things like better coverage would be useful, though. If you're really low on time let me know and I can try and step in in a follow-up - would avoid trying to work on your fork, which would require a sync with main.

"D*+ -> (D0 -> (K_S0 -> pi+ pi-) (pi0 -> gamma gamma)) pi+",
"B0 -> (D- -> K+ pi- pi-) (tau+ -> anti-nu_tau pi+ pi+ pi-) nu_tau",
"B_s0 -> (phi -> K+ K-) (phi' -> pi+ pi- pi0)",
],

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.

Suggested change
],
"B_s0 -> (phi -> K+ K-) (phi -> K+ K-)",
],

@pytest.mark.parametrize(
"descriptor",
[
"D0 -> (K_S0 -> pi+ pi-) (pi0 -> gamma gamma)",

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.

Suggested change
"D0 -> (K_S0 -> pi+ pi-) (pi0 -> gamma gamma)",
"D0 -> K- pi+",
"D0 -> (K_S0 -> pi+ pi-) (pi0 -> gamma gamma)",

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants