Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## Next

* Decay-file validation:
- Renamed diagnostic `DLW003` to `decay-cdecay-conflict` to describe a
particle defined by both `Decay` and `CDecay`.
- 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)

* Parsing of decay files (aka .dec files):
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +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. |
| `DLW003` | `duplicate-cdecay` | A particle is defined with both `Decay` and `CDecay`; `CDecay` is ignored. |
| `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` | `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
Expand Down
11 changes: 7 additions & 4 deletions docs/examples/decfile_parsing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,20 @@ 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.
* - ``DLW003``
- ``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``
- ``missing-copydecay-source``
- A ``CopyDecay`` statement references a missing ``Decay`` source.
* - ``DLW999``
- ``parser-warning``
- An otherwise unclassified warning was emitted by ``DecFileParser``.
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/getting_started/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 20 additions & 4 deletions src/decaylanguage/dec/dec.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@ class DuplicateDecayWarning(DecFileWarning):
code = "DLW001"


class MissingCopyDecaySourceWarning(DecFileWarning):
class DuplicateCDecayWarning(DecFileWarning):
code = "DLW002"


class DuplicateCDecayWarning(DecFileWarning):
class DecayAndCDecayWarning(DecFileWarning):
code = "DLW003"


Expand All @@ -97,6 +97,10 @@ class SelfConjugateCDecayWarning(DecFileWarning):
code = "DLW005"


class MissingCopyDecaySourceWarning(DecFileWarning):
code = "DLW006"


@cache
def _build_lark_parser(
grammar: str,
Expand Down Expand Up @@ -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(s) 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 = [
Expand All @@ -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
Expand Down Expand Up @@ -846,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)
)

Expand Down
32 changes: 26 additions & 6 deletions src/decaylanguage/dec/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ 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",
"duplicate-cdecay",
"decay-cdecay-conflict",
"A particle is defined with both Decay and CDecay; CDecay is ignored.",
)
DLW004 = DiagnosticRule(
Expand All @@ -82,13 +82,27 @@ class Diagnostic:
"self-conjugate-cdecay",
"A CDecay statement targets a self-conjugate particle.",
)
DLW006 = DiagnosticRule(
"DLW006",
"missing-copydecay-source",
"A CopyDecay statement references a missing Decay source.",
)
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

Expand Down Expand Up @@ -234,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>.*?)\.")
particles = _search_message(message, r"with 'CDecay': (?P<particles>.*?)!")
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<particles>.*?)!")
if particles is not None:
Expand All @@ -252,6 +268,10 @@ 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"not found: (?P<particles>.*?)\.")
if particles is not None:
return f"missing Decay source for CopyDecay: {particles}"
return message


Expand Down
19 changes: 19 additions & 0 deletions tests/dec/test_dec.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
DecayNotFound,
DecFileNotParsed,
DecFileParser,
DuplicateCDecayWarning,
get_branching_fraction,
get_decay_mother_name,
get_final_state_particle_names,
Expand Down Expand Up @@ -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()
Expand Down
77 changes: 74 additions & 3 deletions tests/dec/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import decaylanguage.dec.validate as validate_module
from decaylanguage.dec.dec import (
DecayAndCDecayWarning,
DuplicateCDecayWarning,
DuplicateDecayWarning,
MissingCDecaySourceWarning,
Expand All @@ -38,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",
Expand All @@ -51,18 +52,28 @@
"following particle(s) not found: D0. Skipping creation of these "
"copied decay trees."
),
"DLW002",
"DLW006",
"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 ..."
),
"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(s) will be "
"discarded/removed ..."
),
"DLW002",
"duplicate CDecay statement(s): D0; later statements ignored",
),
(
MissingCDecaySourceWarning,
(
Expand Down Expand Up @@ -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] == ["DLW002"]
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
""",
["DLW002", "DLW003"],
),
(
"""Decay D0
1.0 K- pi+ PHSP;
Enddecay
CDecay B0
CDecay B0
End
""",
["DLW002", "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"],
Expand Down