From 6aeaef415fba083c9c16047c69943a9346fd1a21 Mon Sep 17 00:00:00 2001 From: Jan Wagner Date: Wed, 29 Jul 2026 08:01:14 +0200 Subject: [PATCH 1/3] fix: distinguish duplicate CDecay statements --- CHANGELOG.md | 8 ++++ README.md | 3 +- docs/examples/decfile_parsing.rst | 7 ++- docs/getting_started/quickstart.rst | 2 +- src/decaylanguage/dec/dec.py | 20 +++++++- src/decaylanguage/dec/validate.py | 24 +++++++++- tests/dec/test_dec.py | 19 ++++++++ tests/dec/test_validate.py | 73 ++++++++++++++++++++++++++++- 8 files changed, 147 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3191fbca..0e9d2b95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Next + +* Decay-file validation: + - Renamed diagnostic `DLW003` to `decay-cdecay-conflict` to describe a + particle defined by both `Decay` and `CDecay`. + - Added `DLW006` (`duplicate-cdecay`) for repeated `CDecay` statements; + later occurrences are now ignored. + ## Version 1.1.0 (2026-07-27) * Parsing of decay files (aka .dec files): diff --git a/README.md b/README.md index 7767b7d5..c4e10716 100644 --- a/README.md +++ b/README.md @@ -224,9 +224,10 @@ Available diagnostics: | `DLP001` | `parse-error` | The file could not be read or parsed by `DecFileParser`. | | `DLW001` | `duplicate-decay` | A particle has multiple `Decay` blocks; only the first is retained. | | `DLW002` | `missing-copydecay-source` | A `CopyDecay` statement references a missing `Decay` source. | -| `DLW003` | `duplicate-cdecay` | A particle is defined with both `Decay` and `CDecay`; `CDecay` is ignored. | +| `DLW003` | `decay-cdecay-conflict` | A particle is defined with both `Decay` and `CDecay`; `CDecay` is ignored. | | `DLW004` | `missing-cdecay-source` | A `CDecay` statement has no corresponding `Decay` source. | | `DLW005` | `self-conjugate-cdecay` | A `CDecay` statement targets a self-conjugate particle. | +| `DLW006` | `duplicate-cdecay` | A particle has multiple `CDecay` statements; only the first is retained. | | `DLW999` | `parser-warning` | An otherwise unclassified warning was emitted by `DecFileParser`. | When the hook finds a problem, pre-commit prints the validator output. A parser diff --git a/docs/examples/decfile_parsing.rst b/docs/examples/decfile_parsing.rst index ce7f357e..3370cc77 100644 --- a/docs/examples/decfile_parsing.rst +++ b/docs/examples/decfile_parsing.rst @@ -94,7 +94,7 @@ available diagnostics, which are the following: - ``missing-copydecay-source`` - A ``CopyDecay`` statement references a missing ``Decay`` source. * - ``DLW003`` - - ``duplicate-cdecay`` + - ``decay-cdecay-conflict`` - A particle is defined with both ``Decay`` and ``CDecay``; ``CDecay`` is ignored. * - ``DLW004`` - ``missing-cdecay-source`` @@ -102,6 +102,9 @@ available diagnostics, which are the following: * - ``DLW005`` - ``self-conjugate-cdecay`` - A ``CDecay`` statement targets a self-conjugate particle. + * - ``DLW006`` + - ``duplicate-cdecay`` + - A particle has multiple ``CDecay`` statements; only the first is retained. * - ``DLW999`` - ``parser-warning`` - An otherwise unclassified warning was emitted by ``DecFileParser``. @@ -122,7 +125,7 @@ Parser warnings are reported more compactly: DecayLanguage: 2 diagnostic(s) in 1 file(s) tests/data/duplicate-decays.dec: DLW001 duplicate-decay: duplicate Decay block(s): Sigma(1775)0; later definitions ignored - tests/data/duplicate-decays.dec: DLW003 duplicate-cdecay: both Decay and CDecay defined: anti-Sigma(1775)0; CDecay ignored + tests/data/duplicate-decays.dec: DLW003 decay-cdecay-conflict: both Decay and CDecay defined: anti-Sigma(1775)0; CDecay ignored summary: DLW001=1, DLW003=1 By default, at most 100 diagnostics are printed before the remaining diagnostics diff --git a/docs/getting_started/quickstart.rst b/docs/getting_started/quickstart.rst index 9619a774..12f5dc2c 100644 --- a/docs/getting_started/quickstart.rst +++ b/docs/getting_started/quickstart.rst @@ -38,7 +38,7 @@ On failure, the validator prints output such as: DecayLanguage: 2 diagnostic(s) in 1 file(s) tests/data/duplicate-decays.dec: DLW001 duplicate-decay: duplicate Decay block(s): Sigma(1775)0; later definitions ignored - tests/data/duplicate-decays.dec: DLW003 duplicate-cdecay: both Decay and CDecay defined: anti-Sigma(1775)0; CDecay ignored + tests/data/duplicate-decays.dec: DLW003 decay-cdecay-conflict: both Decay and CDecay defined: anti-Sigma(1775)0; CDecay ignored summary: DLW001=1, DLW003=1 Use ``decaylanguage-validate --list-diagnostics`` to list the available diff --git a/src/decaylanguage/dec/dec.py b/src/decaylanguage/dec/dec.py index 908d33a5..150a0349 100644 --- a/src/decaylanguage/dec/dec.py +++ b/src/decaylanguage/dec/dec.py @@ -85,7 +85,7 @@ class MissingCopyDecaySourceWarning(DecFileWarning): code = "DLW002" -class DuplicateCDecayWarning(DecFileWarning): +class DecayAndCDecayWarning(DecFileWarning): code = "DLW003" @@ -97,6 +97,10 @@ class SelfConjugateCDecayWarning(DecFileWarning): code = "DLW005" +class DuplicateCDecayWarning(DecFileWarning): + code = "DLW006" + + @cache def _build_lark_parser( grammar: str, @@ -749,6 +753,18 @@ def _add_charge_conjugate_decays(self) -> None: assert self._parsed_decays is not None + # As for duplicate Decay blocks, retain only the first CDecay + # statement for each particle. + counts = Counter(mother_names_ccdecays) + duplicate_cdecays = [name for name, count in counts.items() if count > 1] + if duplicate_cdecays: + msg = """The following particle(s) is(are) redefined in the input .dec file with 'CDecay': {}! +All but the first occurrence will be discarded/removed ...""".format( + ", ".join(duplicate_cdecays) + ) + warnings.warn(msg, DuplicateCDecayWarning, stacklevel=2) + mother_names_ccdecays = list(dict.fromkeys(mother_names_ccdecays)) + # Cross-check - make sure charge conjugate decays are not defined # with both 'Decay' and 'CDecay' statements! mother_names_decays = [ @@ -760,7 +776,7 @@ def _add_charge_conjugate_decays(self) -> None: str_duplicates = ", ".join(d for d in duplicates) msg = f"""The following particles are defined in the input .dec file with both 'Decay' and 'CDecay': {str_duplicates}! The 'CDecay' definition(s) will be ignored ...""" - warnings.warn(msg, DuplicateCDecayWarning, stacklevel=2) + warnings.warn(msg, DecayAndCDecayWarning, stacklevel=2) # If that's the case, proceed using the decay definitions specified # via the 'Decay' statement, hence discard/remove the definition diff --git a/src/decaylanguage/dec/validate.py b/src/decaylanguage/dec/validate.py index 92c239f2..e39e3278 100644 --- a/src/decaylanguage/dec/validate.py +++ b/src/decaylanguage/dec/validate.py @@ -69,7 +69,7 @@ class Diagnostic: ) DLW003 = DiagnosticRule( "DLW003", - "duplicate-cdecay", + "decay-cdecay-conflict", "A particle is defined with both Decay and CDecay; CDecay is ignored.", ) DLW004 = DiagnosticRule( @@ -82,13 +82,27 @@ class Diagnostic: "self-conjugate-cdecay", "A CDecay statement targets a self-conjugate particle.", ) +DLW006 = DiagnosticRule( + "DLW006", + "duplicate-cdecay", + "A particle has multiple CDecay statements; only the first is retained.", +) DLW999 = DiagnosticRule( "DLW999", "parser-warning", "An otherwise unclassified warning emitted by DecFileParser.", ) -DIAGNOSTIC_RULES = (DLP001, DLW001, DLW002, DLW003, DLW004, DLW005, DLW999) +DIAGNOSTIC_RULES = ( + DLP001, + DLW001, + DLW002, + DLW003, + DLW004, + DLW005, + DLW006, + DLW999, +) _RULES_BY_CODE = {rule.code: rule for rule in DIAGNOSTIC_RULES} _DEFAULT_MAX_DIAGNOSTICS = 100 @@ -252,6 +266,12 @@ def _compact_warning_message(rule: DiagnosticRule, message: str) -> str: ) if particle is not None: return f"CDecay targets self-conjugate particle: {particle}" + if rule is DLW006: + particles = _search_message(message, r"with 'CDecay': (?P.*?)!") + if particles is not None: + return ( + f"duplicate CDecay statement(s): {particles}; later statements ignored" + ) return message diff --git a/tests/dec/test_dec.py b/tests/dec/test_dec.py index 8344e14c..66eff761 100644 --- a/tests/dec/test_dec.py +++ b/tests/dec/test_dec.py @@ -19,6 +19,7 @@ DecayNotFound, DecFileNotParsed, DecFileParser, + DuplicateCDecayWarning, get_branching_fraction, get_decay_mother_name, get_final_state_particle_names, @@ -554,6 +555,24 @@ def test_duplicate_decay_definitions(): assert p.list_decay_mother_names() == ["Sigma(1775)0", "anti-Sigma(1775)0"] +def test_duplicate_cdecay_definitions_are_only_applied_once(): + p = DecFileParser.from_string( + """Decay D0 +1.0 K- pi+ PHSP; +Enddecay +CDecay anti-D0 +CDecay anti-D0 +End +""" + ) + + with pytest.warns(DuplicateCDecayWarning, match="CDecay") as caught: + p.parse() + + assert len(caught) == 1 + assert p.list_decay_mother_names() == ["D0", "anti-D0"] + + def test_list_decay_modes(): p = DecFileParser(DIR / "../data/test_example_Dst.dec") p.parse() diff --git a/tests/dec/test_validate.py b/tests/dec/test_validate.py index 36f1b36b..4866cd6e 100644 --- a/tests/dec/test_validate.py +++ b/tests/dec/test_validate.py @@ -13,6 +13,7 @@ import decaylanguage.dec.validate as validate_module from decaylanguage.dec.dec import ( + DecayAndCDecayWarning, DuplicateCDecayWarning, DuplicateDecayWarning, MissingCDecaySourceWarning, @@ -55,7 +56,7 @@ "missing Decay source for CopyDecay: D0", ), ( - DuplicateCDecayWarning, + DecayAndCDecayWarning, ( "The following particles are defined in the input .dec file with both " "'Decay' and 'CDecay': D0! The 'CDecay' definition(s) will be ignored ..." @@ -63,6 +64,16 @@ "DLW003", "both Decay and CDecay defined: D0; CDecay ignored", ), + ( + DuplicateCDecayWarning, + ( + "The following particle(s) is(are) redefined in the input .dec file " + "with 'CDecay': D0! All but the first occurrence will be " + "discarded/removed ..." + ), + "DLW006", + "duplicate CDecay statement(s): D0; later statements ignored", + ), ( MissingCDecaySourceWarning, ( @@ -140,6 +151,66 @@ def test_validate_files_reports_self_conjugate_cdecay(tmp_path: Path) -> None: assert diagnostics[0].message == "CDecay targets self-conjugate particle: pi0" +def test_validate_files_reports_duplicate_cdecay(tmp_path: Path) -> None: + path = tmp_path / "duplicate-cdecay.dec" + path.write_text( + """Decay D0 +1.0 K- pi+ PHSP; +Enddecay +CDecay anti-D0 +CDecay anti-D0 +End +""", + encoding="utf_8", + ) + + diagnostics = validate_files([path]) + + assert [diagnostic.code for diagnostic in diagnostics] == ["DLW006"] + assert diagnostics[0].message == ( + "duplicate CDecay statement(s): anti-D0; later statements ignored" + ) + + +@pytest.mark.parametrize( + ("decay_file", "codes"), + [ + ( + """Decay D0 +1.0 K- pi+ PHSP; +Enddecay +Decay anti-D0 +1.0 K+ pi- PHSP; +Enddecay +CDecay anti-D0 +CDecay anti-D0 +End +""", + ["DLW006", "DLW003"], + ), + ( + """Decay D0 +1.0 K- pi+ PHSP; +Enddecay +CDecay B0 +CDecay B0 +End +""", + ["DLW006", "DLW004"], + ), + ], +) +def test_duplicate_cdecay_is_independent_of_other_cdecay_diagnostics( + tmp_path: Path, decay_file: str, codes: list[str] +) -> None: + path = tmp_path / "combined-cdecay-errors.dec" + path.write_text(decay_file, encoding="utf_8") + + diagnostics = validate_files([path]) + + assert [diagnostic.code for diagnostic in diagnostics] == codes + + def test_validate_files_can_ignore_exact_code() -> None: diagnostics = validate_files( [DIR / "../data/duplicate-decays.dec"], From 1b5ed8f64678e9ba873d0a107aa267c14f7635fa Mon Sep 17 00:00:00 2001 From: Jan Wagner Date: Tue, 4 Aug 2026 12:51:24 +0200 Subject: [PATCH 2/3] fix: reorder decay validator diagnostics --- CHANGELOG.md | 3 ++- README.md | 4 ++-- docs/examples/decfile_parsing.rst | 8 ++++---- src/decaylanguage/dec/dec.py | 4 ++-- src/decaylanguage/dec/validate.py | 20 ++++++++++---------- tests/dec/test_validate.py | 10 +++++----- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e9d2b95..b8958a61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,9 @@ * Decay-file validation: - Renamed diagnostic `DLW003` to `decay-cdecay-conflict` to describe a particle defined by both `Decay` and `CDecay`. - - Added `DLW006` (`duplicate-cdecay`) for repeated `CDecay` statements; + - Added `DLW002` (`duplicate-cdecay`) for repeated `CDecay` statements; later occurrences are now ignored. + - Renumbered `missing-copydecay-source` to `DLW006`. ## Version 1.1.0 (2026-07-27) diff --git a/README.md b/README.md index c4e10716..2f203761 100644 --- a/README.md +++ b/README.md @@ -223,11 +223,11 @@ Available diagnostics: | --- | --- | --- | | `DLP001` | `parse-error` | The file could not be read or parsed by `DecFileParser`. | | `DLW001` | `duplicate-decay` | A particle has multiple `Decay` blocks; only the first is retained. | -| `DLW002` | `missing-copydecay-source` | A `CopyDecay` statement references a missing `Decay` source. | +| `DLW002` | `duplicate-cdecay` | A particle has multiple `CDecay` statements; only the first is retained. | | `DLW003` | `decay-cdecay-conflict` | A particle is defined with both `Decay` and `CDecay`; `CDecay` is ignored. | | `DLW004` | `missing-cdecay-source` | A `CDecay` statement has no corresponding `Decay` source. | | `DLW005` | `self-conjugate-cdecay` | A `CDecay` statement targets a self-conjugate particle. | -| `DLW006` | `duplicate-cdecay` | A particle has multiple `CDecay` statements; only the first is retained. | +| `DLW006` | `missing-copydecay-source` | A `CopyDecay` statement references a missing `Decay` source. | | `DLW999` | `parser-warning` | An otherwise unclassified warning was emitted by `DecFileParser`. | When the hook finds a problem, pre-commit prints the validator output. A parser diff --git a/docs/examples/decfile_parsing.rst b/docs/examples/decfile_parsing.rst index 3370cc77..8b5a864b 100644 --- a/docs/examples/decfile_parsing.rst +++ b/docs/examples/decfile_parsing.rst @@ -91,8 +91,8 @@ available diagnostics, which are the following: - ``duplicate-decay`` - A particle has multiple ``Decay`` blocks; only the first is retained. * - ``DLW002`` - - ``missing-copydecay-source`` - - A ``CopyDecay`` statement references a missing ``Decay`` source. + - ``duplicate-cdecay`` + - A particle has multiple ``CDecay`` statements; only the first is retained. * - ``DLW003`` - ``decay-cdecay-conflict`` - A particle is defined with both ``Decay`` and ``CDecay``; ``CDecay`` is ignored. @@ -103,8 +103,8 @@ available diagnostics, which are the following: - ``self-conjugate-cdecay`` - A ``CDecay`` statement targets a self-conjugate particle. * - ``DLW006`` - - ``duplicate-cdecay`` - - A particle has multiple ``CDecay`` statements; only the first is retained. + - ``missing-copydecay-source`` + - A ``CopyDecay`` statement references a missing ``Decay`` source. * - ``DLW999`` - ``parser-warning`` - An otherwise unclassified warning was emitted by ``DecFileParser``. diff --git a/src/decaylanguage/dec/dec.py b/src/decaylanguage/dec/dec.py index 150a0349..03b013f4 100644 --- a/src/decaylanguage/dec/dec.py +++ b/src/decaylanguage/dec/dec.py @@ -81,7 +81,7 @@ class DuplicateDecayWarning(DecFileWarning): code = "DLW001" -class MissingCopyDecaySourceWarning(DecFileWarning): +class DuplicateCDecayWarning(DecFileWarning): code = "DLW002" @@ -97,7 +97,7 @@ class SelfConjugateCDecayWarning(DecFileWarning): code = "DLW005" -class DuplicateCDecayWarning(DecFileWarning): +class MissingCopyDecaySourceWarning(DecFileWarning): code = "DLW006" diff --git a/src/decaylanguage/dec/validate.py b/src/decaylanguage/dec/validate.py index e39e3278..92c998ae 100644 --- a/src/decaylanguage/dec/validate.py +++ b/src/decaylanguage/dec/validate.py @@ -64,8 +64,8 @@ class Diagnostic: ) DLW002 = DiagnosticRule( "DLW002", - "missing-copydecay-source", - "A CopyDecay statement references a missing Decay source.", + "duplicate-cdecay", + "A particle has multiple CDecay statements; only the first is retained.", ) DLW003 = DiagnosticRule( "DLW003", @@ -84,8 +84,8 @@ class Diagnostic: ) DLW006 = DiagnosticRule( "DLW006", - "duplicate-cdecay", - "A particle has multiple CDecay statements; only the first is retained.", + "missing-copydecay-source", + "A CopyDecay statement references a missing Decay source.", ) DLW999 = DiagnosticRule( "DLW999", @@ -248,9 +248,11 @@ def _compact_warning_message(rule: DiagnosticRule, message: str) -> str: if particles is not None: return f"duplicate Decay block(s): {particles}; later definitions ignored" if rule is DLW002: - particles = _search_message(message, r"not found: (?P.*?)\.") + particles = _search_message(message, r"with 'CDecay': (?P.*?)!") if particles is not None: - return f"missing Decay source for CopyDecay: {particles}" + return ( + f"duplicate CDecay statement(s): {particles}; later statements ignored" + ) if rule is DLW003: particles = _search_message(message, r"'CDecay': (?P.*?)!") if particles is not None: @@ -267,11 +269,9 @@ def _compact_warning_message(rule: DiagnosticRule, message: str) -> str: if particle is not None: return f"CDecay targets self-conjugate particle: {particle}" if rule is DLW006: - particles = _search_message(message, r"with 'CDecay': (?P.*?)!") + particles = _search_message(message, r"not found: (?P.*?)\.") if particles is not None: - return ( - f"duplicate CDecay statement(s): {particles}; later statements ignored" - ) + return f"missing Decay source for CopyDecay: {particles}" return message diff --git a/tests/dec/test_validate.py b/tests/dec/test_validate.py index 4866cd6e..2dbfee29 100644 --- a/tests/dec/test_validate.py +++ b/tests/dec/test_validate.py @@ -52,7 +52,7 @@ "following particle(s) not found: D0. Skipping creation of these " "copied decay trees." ), - "DLW002", + "DLW006", "missing Decay source for CopyDecay: D0", ), ( @@ -71,7 +71,7 @@ "with 'CDecay': D0! All but the first occurrence will be " "discarded/removed ..." ), - "DLW006", + "DLW002", "duplicate CDecay statement(s): D0; later statements ignored", ), ( @@ -166,7 +166,7 @@ def test_validate_files_reports_duplicate_cdecay(tmp_path: Path) -> None: diagnostics = validate_files([path]) - assert [diagnostic.code for diagnostic in diagnostics] == ["DLW006"] + assert [diagnostic.code for diagnostic in diagnostics] == ["DLW002"] assert diagnostics[0].message == ( "duplicate CDecay statement(s): anti-D0; later statements ignored" ) @@ -186,7 +186,7 @@ def test_validate_files_reports_duplicate_cdecay(tmp_path: Path) -> None: CDecay anti-D0 End """, - ["DLW006", "DLW003"], + ["DLW002", "DLW003"], ), ( """Decay D0 @@ -196,7 +196,7 @@ def test_validate_files_reports_duplicate_cdecay(tmp_path: Path) -> None: CDecay B0 End """, - ["DLW006", "DLW004"], + ["DLW002", "DLW004"], ), ], ) From 82289c53a3e6c02093bdec6e0f6695441b41e177 Mon Sep 17 00:00:00 2001 From: Jan Wagner Date: Tue, 4 Aug 2026 12:51:47 +0200 Subject: [PATCH 3/3] fix: clarify duplicate decay warning wording --- src/decaylanguage/dec/dec.py | 4 ++-- tests/dec/test_validate.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/decaylanguage/dec/dec.py b/src/decaylanguage/dec/dec.py index 03b013f4..830c6cf0 100644 --- a/src/decaylanguage/dec/dec.py +++ b/src/decaylanguage/dec/dec.py @@ -759,7 +759,7 @@ def _add_charge_conjugate_decays(self) -> None: duplicate_cdecays = [name for name, count in counts.items() if count > 1] if duplicate_cdecays: msg = """The following particle(s) is(are) redefined in the input .dec file with 'CDecay': {}! -All but the first occurrence will be discarded/removed ...""".format( +All but the first occurrence(s) will be discarded/removed ...""".format( ", ".join(duplicate_cdecays) ) warnings.warn(msg, DuplicateCDecayWarning, stacklevel=2) @@ -862,7 +862,7 @@ def _check_parsed_decays(self) -> None: # Issue a helpful warning if duplicates are found if duplicates: msg = """The following particle(s) is(are) redefined in the input .dec file with 'Decay': {}! -All but the first occurrence will be discarded/removed ...""".format( +All but the first occurrence(s) will be discarded/removed ...""".format( ", ".join(duplicates) ) diff --git a/tests/dec/test_validate.py b/tests/dec/test_validate.py index 2dbfee29..cad64550 100644 --- a/tests/dec/test_validate.py +++ b/tests/dec/test_validate.py @@ -39,7 +39,7 @@ DuplicateDecayWarning, ( "The following particle(s) is(are) redefined in the input .dec file " - "with 'Decay': D0! All but the first occurrence will be " + "with 'Decay': D0! All but the first occurrence(s) will be " "discarded/removed ..." ), "DLW001", @@ -68,7 +68,7 @@ DuplicateCDecayWarning, ( "The following particle(s) is(are) redefined in the input .dec file " - "with 'CDecay': D0! All but the first occurrence will be " + "with 'CDecay': D0! All but the first occurrence(s) will be " "discarded/removed ..." ), "DLW002",