From 0a9ae6a40fc358029eff736ba611228bab9a96ab Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Mon, 7 Sep 2026 15:13:54 +0200
Subject: [PATCH 01/27] ENH: add pdg package dependency and update cspell words
---
.cspell.json | 3 +++
pyproject.toml | 1 +
uv.lock | 15 +++++++++++++++
3 files changed, 19 insertions(+)
diff --git a/.cspell.json b/.cspell.json
index 38d8c4a6..7a901fc6 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -88,6 +88,8 @@
"macos",
"mathrm",
"maxdepth",
+ "mcid",
+ "mcids",
"meijerg",
"mimetype",
"modindex",
@@ -161,6 +163,7 @@
"determinator",
"determinators",
"docstrings",
+ "eigenstates",
"façade",
"fermionic",
"flatté",
diff --git a/pyproject.toml b/pyproject.toml
index 8ee11c44..a02b3776 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,6 +39,7 @@ dependencies = [
"attrs >=20.1.0", # on_setattr and https://www.attrs.org/en/stable/api.html#next-gen
"frozendict",
"jsonschema",
+ "pdg >=2026.0,<2027",
"particle",
"python-constraint2",
"tqdm >=4.24.0", # autonotebook
diff --git a/uv.lock b/uv.lock
index 0465e062..8a6c2f3e 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1954,6 +1954,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
]
+[[package]]
+name = "pdg"
+version = "2026.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "sqlalchemy" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/77/07/d582d4ae647b301ff499a04db4cdaa85681c3bc86c1739b1a4f5c765f4af/pdg-2026.0.tar.gz", hash = "sha256:15b5c2971448608b0f78796c33c16ca3ccf0d593028803702f6e66cfa62e565f", size = 8244310, upload-time = "2026-06-01T22:10:28.028Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c2/9d/d37fbde4b234bad0e788982f550e53c06439a386452a42436e928ecae115/pdg-2026.0-py3-none-any.whl", hash = "sha256:681e11f8c9a5accb1cb41ccb87bf7efd398adec8ea39ddb894630b17b45c8e8a", size = 8289676, upload-time = "2026-06-01T22:10:25.672Z" },
+]
+
[[package]]
name = "pexpect"
version = "4.9.0"
@@ -2584,6 +2597,7 @@ dependencies = [
{ name = "frozendict" },
{ name = "jsonschema" },
{ name = "particle" },
+ { name = "pdg" },
{ name = "python-constraint2", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "python-constraint2", version = "2.7.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "pyyaml" },
@@ -2709,6 +2723,7 @@ requires-dist = [
{ name = "graphviz", marker = "extra == 'viz'" },
{ name = "jsonschema" },
{ name = "particle" },
+ { name = "pdg", specifier = ">=2026.0,<2027" },
{ name = "python-constraint2" },
{ name = "pyyaml" },
{ name = "tqdm", specifier = ">=4.24.0" },
From e2212d9cbfebe54d582a97e996544e4b764c0602 Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Mon, 7 Sep 2026 15:14:32 +0200
Subject: [PATCH 02/27] ENH: implement PDG particle loading and conversion
functionality
---
src/qrules/_pdg.py | 270 +++++++++++++++++++++++++++++++++
tests/unit/test_pdg_adapter.py | 114 ++++++++++++++
2 files changed, 384 insertions(+)
create mode 100644 src/qrules/_pdg.py
create mode 100644 tests/unit/test_pdg_adapter.py
diff --git a/src/qrules/_pdg.py b/src/qrules/_pdg.py
new file mode 100644
index 00000000..0e22efc3
--- /dev/null
+++ b/src/qrules/_pdg.py
@@ -0,0 +1,270 @@
+"""Convert records from the official PDG API to QRules particles."""
+
+from __future__ import annotations
+
+from fractions import Fraction
+from typing import TYPE_CHECKING
+
+import pdg
+
+from qrules.particle import Particle, ParticleCollection, Spin
+from qrules.quantum_numbers import Parity
+
+if TYPE_CHECKING:
+ from collections.abc import Iterator
+
+ from pdg.api import PdgApi
+ from pdg.particle import PdgParticle
+
+
+# These states are also excluded by the existing Scikit-HEP based loader. The neutral
+# kaon mass eigenstates do not have a definite flavor, while the B(s2) entries have
+# inconsistent isospin information for QRules' particle model.
+_SKIPPED_MC_IDS = {-535, 130, 310, 535}
+
+
+class _UnsupportedParticleError(ValueError):
+ """Raised when a PDG entry cannot be represented by QRules."""
+
+
+def load_pdg() -> ParticleCollection:
+ """Load particle definitions from the official PDG database."""
+ api = pdg.connect()
+ particles = ParticleCollection()
+ for source_particle in _iter_particles(api):
+ try:
+ particle = _convert_particle(source_particle)
+ except _UnsupportedParticleError:
+ continue
+ particles.add(particle)
+ return particles
+
+
+def _iter_particles(api: PdgApi) -> Iterator[PdgParticle]:
+ """Iterate over unique, charge-specific particles with a Monte Carlo ID."""
+ mc_ids: set[int] = set()
+ for particle_group in api.get_particles():
+ source_particles = (
+ particle_group if isinstance(particle_group, list) else [particle_group]
+ )
+ for source_particle in source_particles:
+ mcid = source_particle.mcid
+ if mcid is not None:
+ mc_ids.add(mcid)
+ for mcid in sorted(mc_ids):
+ yield api.get_particle_by_mcid(mcid)
+
+
+def _convert_particle(source: PdgParticle) -> Particle:
+ mcid = source.mcid
+ if mcid is None:
+ msg = f"Particle {source.name} has no Monte Carlo ID"
+ raise _UnsupportedParticleError(msg)
+ if mcid in _SKIPPED_MC_IDS or abs(mcid) >= 1_000_000_000:
+ msg = f"Particle {source.name} is not supported"
+ raise _UnsupportedParticleError(msg)
+
+ charge = _to_integer_charge(source.charge)
+ spin = _to_spin(
+ source.quantum_J,
+ mcid,
+ is_hadron=source.is_baryon or source.is_meson,
+ )
+ mass = _to_mass(source)
+ width = _to_width(source)
+
+ strangeness, charmness, bottomness, topness = _flavor_quantum_numbers(
+ mcid,
+ is_baryon=source.is_baryon,
+ is_meson=source.is_meson,
+ )
+ baryon_number = _baryon_number(mcid, is_baryon=source.is_baryon)
+ lepton_numbers = _lepton_numbers(mcid, is_lepton=source.is_lepton)
+ isospin = _to_isospin(
+ source.quantum_I,
+ charge=charge,
+ baryon_number=baryon_number,
+ flavor_numbers=(strangeness, charmness, bottomness, topness),
+ )
+ parity = _to_parity(source.quantum_P)
+ if source.is_lepton:
+ # QRules convention: fermions and antifermions have opposite intrinsic parity.
+ parity = Parity(+1 if mcid > 0 else -1)
+ c_parity = _to_parity(source.quantum_C) if source.self_conjugate else None
+
+ return Particle(
+ name=source.name,
+ pid=mcid,
+ spin=spin,
+ mass=mass,
+ width=width,
+ charge=charge,
+ isospin=isospin,
+ strangeness=strangeness,
+ charmness=charmness,
+ bottomness=bottomness,
+ topness=topness,
+ baryon_number=baryon_number,
+ electron_lepton_number=lepton_numbers[0],
+ muon_lepton_number=lepton_numbers[1],
+ tau_lepton_number=lepton_numbers[2],
+ parity=parity,
+ c_parity=c_parity,
+ g_parity=_to_parity(source.quantum_G),
+ )
+
+
+def _to_integer_charge(value: float) -> int:
+ if not float(value).is_integer():
+ msg = f"QRules does not support fractional charge {value}"
+ raise _UnsupportedParticleError(msg)
+ return int(value)
+
+
+def _to_spin(value: str | None, mcid: int, *, is_hadron: bool) -> Fraction:
+ spin = _to_fraction(value)
+ if spin is not None:
+ return spin
+
+ # For hadrons, the final digit of an MC ID is 2J+1. This preserves definite
+ # spins encoded in IDs where the current RPP text reports a range or "?".
+ spin_code = abs(mcid) % 10
+ if is_hadron and spin_code > 0:
+ return Fraction(spin_code - 1, 2)
+ msg = f"Cannot determine spin for MC ID {mcid} from {value!r}"
+ raise _UnsupportedParticleError(msg)
+
+
+def _to_mass(source: PdgParticle) -> float:
+ for candidate in _particle_and_antiparticle(source):
+ if candidate.has_mass_entry:
+ mass = candidate.mass
+ if mass is not None:
+ return mass
+ mass = _range_central_value(candidate, quantity="mass")
+ if mass is not None:
+ return mass
+ if abs(source.mcid) in {12, 14, 16, 21, 22}:
+ return 0.0
+ msg = f"Particle {source.name} has no supported mass value"
+ raise _UnsupportedParticleError(msg)
+
+
+def _to_width(source: PdgParticle) -> float:
+ for candidate in _particle_and_antiparticle(source):
+ if candidate.has_width_entry or candidate.has_lifetime_entry:
+ width = candidate.width
+ if width is not None:
+ return width
+ width = _range_central_value(candidate, quantity="width")
+ if width is not None:
+ return width
+ return 0.0
+
+
+def _particle_and_antiparticle(source: PdgParticle) -> tuple[PdgParticle, ...]:
+ if source.self_conjugate:
+ return (source,)
+ return source, source.antiparticle
+
+
+def _range_central_value(
+ source: PdgParticle,
+ *,
+ quantity: str,
+) -> float | None:
+ properties = source.masses() if quantity == "mass" else source.widths()
+ prop = source.best(properties, f"{source.name} {quantity}")
+ summary = prop.best_summary()
+ if summary is None or summary.is_lower_limit or summary.is_upper_limit:
+ return None
+ return summary.get_value("GeV")
+
+
+def _to_fraction(value: str | None) -> Fraction | None:
+ if value is None:
+ return None
+ try:
+ return Fraction(value)
+ except ValueError:
+ return None
+
+
+def _to_parity(value: str | None) -> Parity | None:
+ if value == "+":
+ return Parity(+1)
+ if value == "-":
+ return Parity(-1)
+ return None
+
+
+def _baryon_number(mcid: int, *, is_baryon: bool) -> int:
+ if not is_baryon:
+ return 0
+ return +1 if mcid > 0 else -1
+
+
+def _lepton_numbers(mcid: int, *, is_lepton: bool) -> tuple[int, int, int]:
+ if not is_lepton:
+ return 0, 0, 0
+ lepton_number = +1 if mcid > 0 else -1
+ generation = (abs(mcid) - 11) // 2
+ values = [0, 0, 0]
+ if generation not in range(len(values)):
+ return 0, 0, 0
+ values[generation] = lepton_number
+ return values[0], values[1], values[2]
+
+
+def _flavor_quantum_numbers(
+ mcid: int,
+ *,
+ is_baryon: bool,
+ is_meson: bool,
+) -> tuple[int, int, int, int]:
+ """Derive S, C, B' and T from the quark digits in a standard MC ID."""
+ abs_mcid = abs(mcid)
+ quark3 = (abs_mcid // 10) % 10
+ quark2 = (abs_mcid // 100) % 10
+ quark1 = (abs_mcid // 1_000) % 10
+ net_quarks = dict.fromkeys((3, 4, 5, 6), 0)
+ particle_sign = +1 if mcid > 0 else -1
+
+ if is_baryon:
+ for flavor in (quark1, quark2, quark3):
+ if flavor in net_quarks:
+ net_quarks[flavor] += particle_sign
+ elif is_meson and quark2 != quark3:
+ # For a positive meson ID, the heavier flavor is a quark when it is
+ # up-type and an antiquark when it is down-type. A negative ID reverses
+ # the assignment.
+ heavier_sign = particle_sign if quark2 % 2 == 0 else -particle_sign
+ lighter_sign = -heavier_sign
+ if quark2 in net_quarks:
+ net_quarks[quark2] += heavier_sign
+ if quark3 in net_quarks:
+ net_quarks[quark3] += lighter_sign
+
+ return (
+ -net_quarks[3],
+ +net_quarks[4],
+ -net_quarks[5],
+ +net_quarks[6],
+ )
+
+
+def _to_isospin(
+ value: str | None,
+ *,
+ charge: int,
+ baryon_number: int,
+ flavor_numbers: tuple[int, int, int, int],
+) -> Spin | None:
+ magnitude = _to_fraction(value)
+ if magnitude is None:
+ return None
+ projection = Fraction(
+ 2 * charge - baryon_number - sum(flavor_numbers),
+ 2,
+ )
+ return Spin(magnitude, projection)
diff --git a/tests/unit/test_pdg_adapter.py b/tests/unit/test_pdg_adapter.py
new file mode 100644
index 00000000..9b0caa55
--- /dev/null
+++ b/tests/unit/test_pdg_adapter.py
@@ -0,0 +1,114 @@
+from fractions import Fraction
+
+import pytest
+
+from qrules._pdg import load_pdg as load_official_pdg
+from qrules.particle import ParticleCollection
+from qrules.particle import load_pdg as load_scikit_hep_pdg
+from qrules.quantum_numbers import Parity
+
+
+@pytest.fixture(scope="module")
+def official_particles() -> ParticleCollection:
+ return load_official_pdg()
+
+
+@pytest.fixture(scope="module")
+def scikit_hep_particles() -> ParticleCollection:
+ return load_scikit_hep_pdg()
+
+
+def test_current_mcids_are_covered(
+ official_particles: ParticleCollection,
+ scikit_hep_particles: ParticleCollection,
+):
+ official_mcids = {particle.pid for particle in official_particles}
+ current_mcids = {particle.pid for particle in scikit_hep_particles}
+ assert current_mcids <= official_mcids
+
+
+@pytest.mark.parametrize(
+ ("mcid", "name"),
+ [
+ (12, "nu_e"),
+ (-2212, "pbar"),
+ (443, "J/psi(1S)"),
+ (9010221, "f_0(980)0"),
+ (5122, "Lambda_b()0"),
+ ],
+)
+def test_uses_official_names(
+ official_particles: ParticleCollection,
+ mcid: int,
+ name: str,
+):
+ assert official_particles.find(mcid).name == name
+
+
+def test_pion_quantum_numbers(official_particles: ParticleCollection):
+ pion = official_particles.find(211)
+ assert pion.spin == 0
+ assert pion.charge == +1
+ assert pion.isospin is not None
+ assert pion.isospin.magnitude == 1
+ assert pion.isospin.projection == +1
+ assert pion.parity == Parity(-1)
+ assert pion.c_parity is None
+ assert pion.g_parity == Parity(-1)
+
+
+def test_antiproton_quantum_numbers(official_particles: ParticleCollection):
+ antiproton = official_particles.find(-2212)
+ assert antiproton.spin == Fraction(1, 2)
+ assert antiproton.charge == -1
+ assert antiproton.baryon_number == -1
+ assert antiproton.isospin is not None
+ assert antiproton.isospin.projection == Fraction(-1, 2)
+ assert antiproton.parity == Parity(-1)
+
+
+@pytest.mark.parametrize(
+ ("mcid", "lepton_numbers"),
+ [
+ (11, (+1, 0, 0)),
+ (-12, (-1, 0, 0)),
+ (13, (0, +1, 0)),
+ (-14, (0, -1, 0)),
+ (15, (0, 0, +1)),
+ (-16, (0, 0, -1)),
+ ],
+)
+def test_lepton_numbers(
+ official_particles: ParticleCollection,
+ mcid: int,
+ lepton_numbers: tuple[int, int, int],
+):
+ particle = official_particles.find(mcid)
+ assert (
+ particle.electron_lepton_number,
+ particle.muon_lepton_number,
+ particle.tau_lepton_number,
+ ) == lepton_numbers
+
+
+def test_flavor_numbers_match_current_loader(
+ official_particles: ParticleCollection,
+ scikit_hep_particles: ParticleCollection,
+):
+ for current in scikit_hep_particles:
+ official = official_particles.find(current.pid)
+ assert (
+ official.strangeness,
+ official.charmness,
+ official.bottomness,
+ official.topness,
+ ) == (
+ current.strangeness,
+ current.charmness,
+ current.bottomness,
+ current.topness,
+ )
+
+
+def test_prefers_official_spin(official_particles: ParticleCollection):
+ assert official_particles.find(104122).spin == Fraction(3, 2)
From b3562809726b8a6a2b64b08f07289c9537e226b7 Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Mon, 7 Sep 2026 15:37:35 +0200
Subject: [PATCH 03/27] ENH: implement central value extraction for PDG width
ranges in particle loading
---
src/qrules/_pdg.py | 24 +++++++++++++++++++++++-
tests/unit/test_pdg_adapter.py | 15 +++++++++++++++
2 files changed, 38 insertions(+), 1 deletion(-)
diff --git a/src/qrules/_pdg.py b/src/qrules/_pdg.py
index 0e22efc3..79ee6bf1 100644
--- a/src/qrules/_pdg.py
+++ b/src/qrules/_pdg.py
@@ -2,10 +2,12 @@
from __future__ import annotations
+import re
from fractions import Fraction
from typing import TYPE_CHECKING
import pdg
+from pdg.units import convert
from qrules.particle import Particle, ParticleCollection, Spin
from qrules.quantum_numbers import Parity
@@ -178,7 +180,27 @@ def _range_central_value(
summary = prop.best_summary()
if summary is None or summary.is_lower_limit or summary.is_upper_limit:
return None
- return summary.get_value("GeV")
+ value = summary.get_value("GeV")
+ if value is not None:
+ return value
+ range_value = _central_value_from_range(summary.value_text)
+ if range_value is None:
+ return None
+ return convert(range_value, summary.units, "GeV")
+
+
+def _central_value_from_range(value: str) -> float | None:
+ """Select the preferred value, or midpoint, from a PDG range."""
+ components = re.split(r"\s+to\s+", value.strip(), flags=re.IGNORECASE)
+ if len(components) not in {2, 3}:
+ return None
+ try:
+ numbers = [float(component) for component in components]
+ except ValueError:
+ return None
+ if len(numbers) == 3:
+ return numbers[1]
+ return sum(numbers) / 2
def _to_fraction(value: str | None) -> Fraction | None:
diff --git a/tests/unit/test_pdg_adapter.py b/tests/unit/test_pdg_adapter.py
index 9b0caa55..11ff10b9 100644
--- a/tests/unit/test_pdg_adapter.py
+++ b/tests/unit/test_pdg_adapter.py
@@ -112,3 +112,18 @@ def test_flavor_numbers_match_current_loader(
def test_prefers_official_spin(official_particles: ParticleCollection):
assert official_particles.find(104122).spin == Fraction(3, 2)
+
+
+@pytest.mark.parametrize(
+ ("mcid", "width"),
+ [
+ (9010221, 0.055), # 10 to 100 MeV
+ (2224, 0.117), # 114 to 117 to 120 MeV
+ ],
+)
+def test_uses_central_value_for_width_ranges(
+ official_particles: ParticleCollection,
+ mcid: int,
+ width: float,
+):
+ assert official_particles.find(mcid).width == pytest.approx(width)
From 1a8f6f9990ab8bafcc79ab189be2fc2bfb23ec04 Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Mon, 7 Sep 2026 16:25:27 +0200
Subject: [PATCH 04/27] ENH: add LaTeX name generation for PDG particles and
corresponding tests
---
src/qrules/_pdg.py | 6 +++
src/qrules/_pdg_latex.py | 82 ++++++++++++++++++++++++++++++++++
tests/unit/test_pdg_adapter.py | 23 ++++++++++
tests/unit/test_pdg_latex.py | 46 +++++++++++++++++++
4 files changed, 157 insertions(+)
create mode 100644 src/qrules/_pdg_latex.py
create mode 100644 tests/unit/test_pdg_latex.py
diff --git a/src/qrules/_pdg.py b/src/qrules/_pdg.py
index 79ee6bf1..002bc38e 100644
--- a/src/qrules/_pdg.py
+++ b/src/qrules/_pdg.py
@@ -9,6 +9,7 @@
import pdg
from pdg.units import convert
+from qrules._pdg_latex import create_latex_name
from qrules.particle import Particle, ParticleCollection, Spin
from qrules.quantum_numbers import Parity
@@ -96,6 +97,11 @@ def _convert_particle(source: PdgParticle) -> Particle:
return Particle(
name=source.name,
+ latex=create_latex_name(
+ source.name,
+ isospin=source.quantum_I,
+ self_conjugate=source.self_conjugate,
+ ),
pid=mcid,
spin=spin,
mass=mass,
diff --git a/src/qrules/_pdg_latex.py b/src/qrules/_pdg_latex.py
new file mode 100644
index 00000000..4f94188c
--- /dev/null
+++ b/src/qrules/_pdg_latex.py
@@ -0,0 +1,82 @@
+"""Convert official PDG ASCII particle names to LaTeX."""
+
+from __future__ import annotations
+
+import re
+
+_PARTICLE_NAME_PATTERN = re.compile(
+ r"^(?P[A-Za-z]+(?:/[A-Za-z]+)?)"
+ r"(?P(?:[_^](?:[A-Za-z0-9]+|\*|'))*)"
+ r"(?P\([^()]*\))?"
+ r"(?P\+\+|--|\+|-|0)?$"
+)
+_SCRIPT_PATTERN = re.compile(r"(?P[_^])(?P[A-Za-z0-9]+|\*|')")
+
+_LATEX_SYMBOLS = {
+ "Delta": R"\Delta",
+ "Lambda": R"\Lambda",
+ "Omega": R"\Omega",
+ "Sigma": R"\Sigma",
+ "Upsilon": R"\Upsilon",
+ "Xi": R"\Xi",
+ "chi": R"\chi",
+ "eta": R"\eta",
+ "gamma": R"\gamma",
+ "mu": R"\mu",
+ "nu": R"\nu",
+ "omega": R"\omega",
+ "phi": R"\phi",
+ "pi": R"\pi",
+ "psi": R"\psi",
+ "rho": R"\rho",
+ "tau": R"\tau",
+}
+
+
+def create_latex_name(
+ name: str,
+ *,
+ isospin: str | None,
+ self_conjugate: bool,
+) -> str | None:
+ """Convert a canonical PDG name, returning `None` for unsupported syntax."""
+ match = _PARTICLE_NAME_PATTERN.fullmatch(name)
+ if match is None:
+ return None
+
+ latex = _render_base(match.group("base"))
+
+ subscripts: list[str] = []
+ superscripts: list[str] = []
+ for script in _SCRIPT_PATTERN.finditer(match.group("scripts")):
+ value = script.group("value")
+ if script.group("kind") == "_":
+ subscripts.append(_LATEX_SYMBOLS.get(value, value))
+ else:
+ superscripts.append(R"\prime" if value == "'" else value)
+ latex += "".join(Rf"_{{{value}}}" for value in subscripts)
+
+ charge = match.group("charge")
+ if charge == "0" and self_conjugate and isospin == "0":
+ charge = None
+ qualifier = match.group("qualifier")
+ if qualifier in {None, "()"} and charge is not None and superscripts:
+ superscripts.append(charge)
+ charge = None
+ if superscripts:
+ latex += Rf"^{{{''.join(superscripts)}}}"
+ if qualifier != "()" and qualifier is not None:
+ latex += qualifier
+ if charge is not None:
+ latex += Rf"^{{{charge}}}"
+ return latex
+
+
+def _render_base(base: str) -> str:
+ is_antiparticle = base.endswith("bar")
+ if is_antiparticle:
+ base = base.removesuffix("bar")
+ latex = "/".join(_LATEX_SYMBOLS.get(part, part) for part in base.split("/"))
+ if is_antiparticle:
+ latex = Rf"\overline{{{latex}}}"
+ return latex
diff --git a/tests/unit/test_pdg_adapter.py b/tests/unit/test_pdg_adapter.py
index 11ff10b9..369d9b53 100644
--- a/tests/unit/test_pdg_adapter.py
+++ b/tests/unit/test_pdg_adapter.py
@@ -45,6 +45,29 @@ def test_uses_official_names(
assert official_particles.find(mcid).name == name
+def test_all_particles_have_latex(official_particles: ParticleCollection):
+ assert all(particle.latex is not None for particle in official_particles)
+
+
+@pytest.mark.parametrize(
+ ("mcid", "latex"),
+ [
+ (12, R"\nu_{e}"),
+ (211, R"\pi^{+}"),
+ (-2212, R"\overline{p}"),
+ (443, R"J/\psi(1S)"),
+ (9010221, R"f_{0}(980)"),
+ (5122, R"\Lambda_{b}^{0}"),
+ ],
+)
+def test_uses_generated_latex(
+ official_particles: ParticleCollection,
+ mcid: int,
+ latex: str,
+):
+ assert official_particles.find(mcid).latex == latex
+
+
def test_pion_quantum_numbers(official_particles: ParticleCollection):
pion = official_particles.find(211)
assert pion.spin == 0
diff --git a/tests/unit/test_pdg_latex.py b/tests/unit/test_pdg_latex.py
new file mode 100644
index 00000000..6d2ea2b0
--- /dev/null
+++ b/tests/unit/test_pdg_latex.py
@@ -0,0 +1,46 @@
+import pytest
+
+from qrules._pdg_latex import create_latex_name
+
+
+@pytest.mark.parametrize(
+ ("name", "isospin", "self_conjugate", "latex"),
+ [
+ ("pi+", "1", False, R"\pi^{+}"),
+ ("pi0", "1", True, R"\pi^{0}"),
+ ("pbar", "1/2", False, R"\overline{p}"),
+ ("nubar_mu", None, False, R"\overline{\nu}_{\mu}"),
+ ("J/psi(1S)", "0", True, R"J/\psi(1S)"),
+ ("f_0(980)0", "0", True, R"f_{0}(980)"),
+ ("Lambda_b()0", "0", False, R"\Lambda_{b}^{0}"),
+ ("D_0^*(2300)+", "1/2", False, R"D_{0}^{*}(2300)^{+}"),
+ ("D_s^*()+", "0", False, R"D_{s}^{*+}"),
+ ("Xibar_c^'()0", "1/2", False, R"\overline{\Xi}_{c}^{\prime0}"),
+ ("eta^'(958)0", "0", True, R"\eta^{\prime}(958)"),
+ ],
+)
+def test_create_latex_name(
+ name: str,
+ isospin: str | None,
+ self_conjugate: bool,
+ latex: str,
+):
+ assert (
+ create_latex_name(
+ name,
+ isospin=isospin,
+ self_conjugate=self_conjugate,
+ )
+ == latex
+ )
+
+
+def test_rejects_unsupported_name_syntax():
+ assert (
+ create_latex_name(
+ "not a PDG name",
+ isospin=None,
+ self_conjugate=False,
+ )
+ is None
+ )
From 81d5d003d1e4946eef29d0216d316fe047892a44 Mon Sep 17 00:00:00 2001
From: GitHub
Date: Mon, 7 Sep 2026 14:37:41 +0000
Subject: [PATCH 05/27] MAINT: implement updates from formatters
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index a02b3776..a59eeb9a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,8 +39,8 @@ dependencies = [
"attrs >=20.1.0", # on_setattr and https://www.attrs.org/en/stable/api.html#next-gen
"frozendict",
"jsonschema",
- "pdg >=2026.0,<2027",
"particle",
+ "pdg >=2026.0,<2027",
"python-constraint2",
"tqdm >=4.24.0", # autonotebook
]
From bdb46e7a7d44978f5d628afdcb548a6eea3d9b6d Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Mon, 7 Sep 2026 16:57:13 +0200
Subject: [PATCH 06/27] MAINT: minor formatting improvements
---
.cspell.json | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.cspell.json b/.cspell.json
index 7a901fc6..587c9e4d 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -38,6 +38,7 @@
"pyproject.toml"
],
"ignoreWords": [
+ "Xibar",
"adrs",
"ampform",
"arange",
@@ -104,6 +105,7 @@
"noreply",
"nrows",
"nsimplify",
+ "nubar",
"pandoc",
"pbar",
"permalinks",
From 974bac0da76a5946a23745d40abb15638154c3d4 Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Mon, 7 Sep 2026 17:51:11 +0200
Subject: [PATCH 07/27] ENH: improve loading speed from pdg database by
removing redundant database queries
---
src/qrules/_pdg.py | 22 ++++++++++--------
tests/unit/test_pdg_adapter.py | 42 ++++++++++++++++++++++++++++++++++
2 files changed, 55 insertions(+), 9 deletions(-)
diff --git a/src/qrules/_pdg.py b/src/qrules/_pdg.py
index 002bc38e..fd5007a0 100644
--- a/src/qrules/_pdg.py
+++ b/src/qrules/_pdg.py
@@ -7,6 +7,7 @@
from typing import TYPE_CHECKING
import pdg
+from pdg.errors import PdgNoDataError
from pdg.units import convert
from qrules._pdg_latex import create_latex_name
@@ -145,13 +146,15 @@ def _to_spin(value: str | None, mcid: int, *, is_hadron: bool) -> Fraction:
def _to_mass(source: PdgParticle) -> float:
for candidate in _particle_and_antiparticle(source):
- if candidate.has_mass_entry:
+ try:
mass = candidate.mass
- if mass is not None:
- return mass
- mass = _range_central_value(candidate, quantity="mass")
- if mass is not None:
- return mass
+ except PdgNoDataError:
+ continue
+ if mass is not None:
+ return mass
+ mass = _range_central_value(candidate, quantity="mass")
+ if mass is not None:
+ return mass
if abs(source.mcid) in {12, 14, 16, 21, 22}:
return 0.0
msg = f"Particle {source.name} has no supported mass value"
@@ -160,10 +163,11 @@ def _to_mass(source: PdgParticle) -> float:
def _to_width(source: PdgParticle) -> float:
for candidate in _particle_and_antiparticle(source):
- if candidate.has_width_entry or candidate.has_lifetime_entry:
- width = candidate.width
- if width is not None:
+ width = candidate.width
+ if width is not None:
+ if width > 0:
return width
+ else:
width = _range_central_value(candidate, quantity="width")
if width is not None:
return width
diff --git a/tests/unit/test_pdg_adapter.py b/tests/unit/test_pdg_adapter.py
index 369d9b53..04fc9407 100644
--- a/tests/unit/test_pdg_adapter.py
+++ b/tests/unit/test_pdg_adapter.py
@@ -1,7 +1,10 @@
from fractions import Fraction
+from unittest.mock import MagicMock, PropertyMock
import pytest
+from pdg.errors import PdgNoDataError
+from qrules._pdg import _to_mass, _to_width
from qrules._pdg import load_pdg as load_official_pdg
from qrules.particle import ParticleCollection
from qrules.particle import load_pdg as load_scikit_hep_pdg
@@ -137,6 +140,45 @@ def test_prefers_official_spin(official_particles: ParticleCollection):
assert official_particles.find(104122).spin == Fraction(3, 2)
+def test_uses_measured_mass_and_width(official_particles: ParticleCollection):
+ rho = official_particles.find(113)
+ assert rho.mass == pytest.approx(0.7752611563582926)
+ assert rho.width == pytest.approx(0.14739133387028722)
+
+
+def test_derives_width_from_lifetime(official_particles: ParticleCollection):
+ muon = official_particles.find(13)
+ assert muon.width == pytest.approx(2.9959292110062035e-19)
+
+
+def test_uses_zero_width_for_stable_particle(
+ official_particles: ParticleCollection,
+):
+ assert official_particles.find(22).width == 0.0
+
+
+def test_uses_antiparticle_mass_if_particle_has_no_mass():
+ source = MagicMock(self_conjugate=False, mcid=1, name="particle")
+ source.has_mass_entry = False
+ type(source).mass = PropertyMock(side_effect=PdgNoDataError("no mass"))
+ source.antiparticle.has_mass_entry = True
+ source.antiparticle.mass = 0.5
+
+ assert _to_mass(source) == 0.5
+
+
+def test_uses_antiparticle_width_if_particle_has_no_decay_data():
+ source = MagicMock(self_conjugate=False)
+ source.has_width_entry = False
+ source.has_lifetime_entry = False
+ source.width = 0.0
+ source.antiparticle.has_width_entry = True
+ source.antiparticle.has_lifetime_entry = False
+ source.antiparticle.width = 0.25
+
+ assert _to_width(source) == 0.25
+
+
@pytest.mark.parametrize(
("mcid", "width"),
[
From a354e9370a7dace6198e1a26be771f28158dab82 Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Tue, 8 Sep 2026 14:01:45 +0200
Subject: [PATCH 08/27] ENH: exchange particle loading from particle package to
pdg package, add caching for PDG particle loading and remove unused
dependencies
---
.cspell.json | 3 +
pyproject.toml | 1 -
src/qrules/_pdg.py | 19 ++++-
src/qrules/particle.py | 187 ++---------------------------------------
tests/conftest.py | 11 ---
uv.lock | 24 ------
6 files changed, 26 insertions(+), 219 deletions(-)
diff --git a/.cspell.json b/.cspell.json
index 587c9e4d..034794db 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -38,6 +38,8 @@
"pyproject.toml"
],
"ignoreWords": [
+ "Dbar",
+ "Kbar",
"Xibar",
"adrs",
"ampform",
@@ -95,6 +97,7 @@
"mimetype",
"modindex",
"mystnb",
+ "nbar",
"nbconvert",
"nbformat",
"nbody",
diff --git a/pyproject.toml b/pyproject.toml
index a59eeb9a..a9420b29 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,7 +39,6 @@ dependencies = [
"attrs >=20.1.0", # on_setattr and https://www.attrs.org/en/stable/api.html#next-gen
"frozendict",
"jsonschema",
- "particle",
"pdg >=2026.0,<2027",
"python-constraint2",
"tqdm >=4.24.0", # autonotebook
diff --git a/src/qrules/_pdg.py b/src/qrules/_pdg.py
index fd5007a0..3e204869 100644
--- a/src/qrules/_pdg.py
+++ b/src/qrules/_pdg.py
@@ -4,6 +4,7 @@
import re
from fractions import Fraction
+from functools import cache
from typing import TYPE_CHECKING
import pdg
@@ -32,16 +33,26 @@ class _UnsupportedParticleError(ValueError):
def load_pdg() -> ParticleCollection:
- """Load particle definitions from the official PDG database."""
+ """Load particle definitions from the official PDG database.
+
+ The converted particle definitions are cached, while each call returns a new
+ collection that callers can modify independently.
+ """
+ return ParticleCollection(_load_pdg_particles())
+
+
+@cache
+def _load_pdg_particles() -> tuple[Particle, ...]:
+ """Load and cache immutable particle definitions from the PDG database."""
api = pdg.connect()
- particles = ParticleCollection()
+ particles = []
for source_particle in _iter_particles(api):
try:
particle = _convert_particle(source_particle)
except _UnsupportedParticleError:
continue
- particles.add(particle)
- return particles
+ particles.append(particle)
+ return tuple(particles)
def _iter_particles(api: PdgApi) -> Iterator[PdgParticle]:
diff --git a/src/qrules/particle.py b/src/qrules/particle.py
index df64e50b..79dc6d0d 100644
--- a/src/qrules/particle.py
+++ b/src/qrules/particle.py
@@ -18,7 +18,6 @@
from difflib import get_close_matches
from fractions import Fraction
from functools import total_ordering
-from math import copysign
from typing import TYPE_CHECKING, Any
import attrs
@@ -39,8 +38,6 @@
from attrs import Attribute
from IPython.lib.pretty import RepresentationPrinter
- from particle import Particle as PdgDatabase
- from particle.particle import enums
_LOGGER = logging.getLogger(__name__)
@@ -241,9 +238,10 @@ def _repr_pretty_(self, p: RepresentationPrinter, cycle: bool) -> None:
def _get_name_root(name: str) -> str:
"""Strip a string (particularly the `.Particle.name`) of specifications."""
- name_root = name
- name_root = re.sub(r"\(.+\)", "", name_root)
- return re.sub(r"[\*\+\-~\d']", "", name_root)
+ name_root = re.sub(r"\([^)]*\)", "", name)
+ name_root = name_root.split("_", maxsplit=1)[0]
+ name_root = re.sub(r"[\^\*\+\-~\d']", "", name_root)
+ return name_root.removesuffix("bar")
ParticleWithSpin = tuple[Particle, Fraction]
@@ -497,178 +495,9 @@ def create_antiparticle(
def load_pdg() -> ParticleCollection:
"""Create a `.ParticleCollection` with all entries from the PDG.
- PDG info is imported from the `scikit-hep/particle
- `_ package.
+ PDG info is imported from the official `PDG Python API
+ `_.
"""
- from particle import Particle as PdgDatabase # ruff: ignore[import-outside-top-level]
-
- all_pdg_particles = PdgDatabase.findall(
- lambda item: (
- item.charge is not None
- and float(item.charge).is_integer() # remove quarks
- and item.J is not None # remove new physics and nuclei
- and abs(item.pdgid) < 1e9 # p and n as nucleus
- and item.name not in __skip_particles
- and not (item.mass is None and not item.name.startswith("nu"))
- )
- )
- particle_collection = ParticleCollection()
- for pdg_particle in all_pdg_particles:
- new_particle = __convert_pdg_instance(pdg_particle)
- particle_collection.add(new_particle)
- return particle_collection
-
-
-__skip_particles = {
- "K(L)0", # no isospin projection
- "K(S)0", # no isospin projection
- "B(s2)*(5840)0", # isospin(0.5, 0.0) ?
- "B(s2)*(5840)~0", # isospin(0.5, 0.0) ?
-}
-
-
-def __sign(value: float) -> int:
- return int(copysign(1, value))
-
-
-# cspell:ignore pdgid
-def __convert_pdg_instance(pdg_particle: PdgDatabase) -> Particle:
- def convert_mass_width(value: float | None) -> float:
- if value is None:
- return 0.0
- return float(value) / 1e3 # https://github.com/ComPWA/qrules/issues/14
-
- def convert_spin(value: Fraction | float | None) -> float:
- if value is None:
- msg = f"PDG instance has no spin:\n{pdg_particle}"
- raise ValueError(msg)
- return float(value)
-
- if pdg_particle.charge is None:
- msg = f"PDG instance has no charge:\n{pdg_particle}"
- raise ValueError(msg)
- quark_numbers = __compute_quark_numbers(pdg_particle)
- lepton_numbers = __compute_lepton_numbers(pdg_particle)
- if pdg_particle.pdgid.is_lepton: # convention: C(fermion)=+1
- parity: Parity | None = Parity(__sign(pdg_particle.pdgid))
- else:
- parity = __create_parity(pdg_particle.P)
- latex = None
- if pdg_particle.latex_name != "Unknown":
- latex = str(pdg_particle.latex_name)
- return Particle(
- name=str(pdg_particle.name),
- latex=latex,
- pid=int(pdg_particle.pdgid),
- mass=convert_mass_width(pdg_particle.mass),
- width=convert_mass_width(pdg_particle.width),
- charge=int(pdg_particle.charge),
- spin=convert_spin(pdg_particle.J),
- strangeness=quark_numbers[0],
- charmness=quark_numbers[1],
- bottomness=quark_numbers[2],
- topness=quark_numbers[3],
- baryon_number=__compute_baryonnumber(pdg_particle),
- electron_lepton_number=lepton_numbers[0],
- muon_lepton_number=lepton_numbers[1],
- tau_lepton_number=lepton_numbers[2],
- isospin=__create_isospin(pdg_particle),
- parity=parity,
- c_parity=__create_parity(pdg_particle.C),
- g_parity=__create_parity(pdg_particle.G),
- )
-
-
-def __compute_quark_numbers(
- pdg_particle: PdgDatabase,
-) -> tuple[int, int, int, int]:
- strangeness = 0
- charmness = 0
- bottomness = 0
- topness = 0
- if pdg_particle.pdgid.is_hadron:
- quark_content = __filter_quark_content(pdg_particle)
- strangeness = quark_content.count("S") - quark_content.count("s")
- charmness = quark_content.count("c") - quark_content.count("C")
- bottomness = quark_content.count("B") - quark_content.count("b")
- topness = quark_content.count("t") - quark_content.count("T")
- return (
- strangeness,
- charmness,
- bottomness,
- topness,
- )
-
-
-def __compute_lepton_numbers(
- pdg_particle: PdgDatabase,
-) -> tuple[int, int, int]:
- electron_lepton_number = 0
- muon_lepton_number = 0
- tau_lepton_number = 0
- if pdg_particle.pdgid.is_lepton:
- lepton_number = int(__sign(pdg_particle.pdgid))
- if "e" in pdg_particle.name:
- electron_lepton_number = lepton_number
- elif "mu" in pdg_particle.name:
- muon_lepton_number = lepton_number
- elif "tau" in pdg_particle.name:
- tau_lepton_number = lepton_number
- return electron_lepton_number, muon_lepton_number, tau_lepton_number
-
-
-def __compute_baryonnumber(pdg_particle: PdgDatabase) -> int:
- return int(__sign(pdg_particle.pdgid) * pdg_particle.pdgid.is_baryon)
-
-
-def __create_isospin(pdg_particle: PdgDatabase) -> Spin | None:
- if pdg_particle.I is None:
- return None
- magnitude = Fraction(pdg_particle.I)
- projection = __isospin_projection_from_pdg(pdg_particle)
- return Spin(magnitude, projection)
-
-
-def __isospin_projection_from_pdg(pdg_particle: PdgDatabase, /) -> Fraction:
- if pdg_particle.charge is None:
- msg = f"PDG instance has no charge:\n{pdg_particle}"
- raise ValueError(msg)
- if "qq" in pdg_particle.quarks.lower():
- strangeness, charmness, bottomness, topness = __compute_quark_numbers(
- pdg_particle
- )
- baryon_number = __compute_baryonnumber(pdg_particle)
- projection = pdg_particle.charge - 0.5 * (
- baryon_number + strangeness + charmness + bottomness + topness
- )
- else:
- projection = 0.0
- if pdg_particle.pdgid.is_hadron:
- quark_content = __filter_quark_content(pdg_particle)
- projection += quark_content.count("u") + quark_content.count("D")
- projection -= quark_content.count("U") + quark_content.count("d")
- projection *= 0.5
- if (
- pdg_particle.I is not None
- and not float(pdg_particle.I - projection).is_integer()
- ):
- msg = f"Cannot have isospin {pdg_particle.I, projection}"
- raise ValueError(msg)
- return Fraction(projection)
-
-
-def __filter_quark_content(pdg_particle: PdgDatabase) -> str:
- matches = re.search(r"([dDuUsScCbBtT+-]{2,})", pdg_particle.quarks)
- if matches is None:
- return ""
- return matches[1]
-
-
-def __create_parity(parity_enum: enums.Parity) -> Parity | None:
- from particle.particle import enums # ruff: ignore[import-outside-top-level]
+ from qrules._pdg import load_pdg as load_official_pdg # ruff: ignore[import-outside-top-level]
- if parity_enum is None or parity_enum == enums.Parity.u:
- return None
- if parity_enum == getattr(parity_enum, "o", None): # particle < 0.14
- return None
- return Parity(int(parity_enum))
+ return load_official_pdg()
diff --git a/tests/conftest.py b/tests/conftest.py
index 897c5232..a31d1bc7 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,5 +1,3 @@
-from importlib.metadata import version
-
import pytest
from qrules import load_default_particles
@@ -19,12 +17,3 @@ def particle_database() -> ParticleCollection:
@pytest.fixture(scope="session")
def output_dir(pytestconfig) -> str:
return f"{pytestconfig.rootpath}/tests/output/"
-
-
-@pytest.fixture(scope="session")
-def skh_particle_version() -> str:
- major, minor, *_ = (int(i) for i in version("particle").split("."))
- particle_version = f"{major}.{minor}"
- if (major, minor) < (0, 11):
- pytest.skip(f"Version {particle_version} is not supported in the tests")
- return particle_version
diff --git a/uv.lock b/uv.lock
index 8a6c2f3e..8a126271 100644
--- a/uv.lock
+++ b/uv.lock
@@ -972,15 +972,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
-[[package]]
-name = "hepunits"
-version = "2.4.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e7/1f/c5f7525bf7e5d14d61750f91845f9f9350a8f33f5ac95672cec92f317038/hepunits-2.4.6.tar.gz", hash = "sha256:bca6ada937147166d66e9fa152566f2378868d798211cad6990080399560fa34", size = 17985, upload-time = "2026-06-16T09:23:36.685Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/85/10/7f9c58d1ec6a0b7f7783fe552f3593f39cda30c2e1d7a9d148ae711e748d/hepunits-2.4.6-py3-none-any.whl", hash = "sha256:089c52c3b84ef67a159b5e9ee9bdd50e1a442e3fd0c101303cc409c1e9011c4d", size = 17090, upload-time = "2026-06-16T09:23:35.35Z" },
-]
-
[[package]]
name = "httpcore"
version = "1.0.9"
@@ -1932,19 +1923,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" },
]
-[[package]]
-name = "particle"
-version = "1.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "attrs" },
- { name = "hepunits" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c3/66/09911bbb658fdffe960903c12edecab95f6cced40fef4909d1cc04bd288b/particle-1.0.0.tar.gz", hash = "sha256:49145dec1cb5044b07f3e8e902280fa050950fa845b058003e69de519bb50492", size = 285766, upload-time = "2026-06-25T14:48:26.893Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a8/92/05078b696cddbdd60963577c895d5b77e5bf829197b08677351b11761c2b/particle-1.0.0-py3-none-any.whl", hash = "sha256:fc2656f53e729be76e45430f56aa65dc20dea069565a393032016544425b64bf", size = 245760, upload-time = "2026-06-25T14:48:25.238Z" },
-]
-
[[package]]
name = "pathspec"
version = "1.1.1"
@@ -2596,7 +2574,6 @@ dependencies = [
{ name = "attrs" },
{ name = "frozendict" },
{ name = "jsonschema" },
- { name = "particle" },
{ name = "pdg" },
{ name = "python-constraint2", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "python-constraint2", version = "2.7.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
@@ -2722,7 +2699,6 @@ requires-dist = [
{ name = "frozendict" },
{ name = "graphviz", marker = "extra == 'viz'" },
{ name = "jsonschema" },
- { name = "particle" },
{ name = "pdg", specifier = ">=2026.0,<2027" },
{ name = "python-constraint2" },
{ name = "pyyaml" },
From e5d14526566970fb39184d3c8fdfc83dca3f7170 Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Tue, 8 Sep 2026 14:02:46 +0200
Subject: [PATCH 09/27] Refactor particle names and update tests for
consistency
---
src/qrules/__init__.py | 4 +-
src/qrules/particle.py | 2 +-
tests/channels/test_d0_to_ks_kp_km.py | 18 +--
tests/channels/test_jpsi_to_gamma_pi0_pi0.py | 20 ++--
tests/channels/test_lc_to_p_km_pip.py | 26 ++---
tests/channels/test_nbody_reactions.py | 32 +++---
tests/channels/test_psi2s_to_eta_k_kstar.py | 10 +-
tests/channels/test_y_to_d0_d0bar_pi0_pi0.py | 20 ++--
tests/unit/conftest.py | 2 +-
tests/unit/io/conftest.py | 2 +-
tests/unit/io/test_dict.py | 8 +-
tests/unit/io/test_dot.py | 6 +-
tests/unit/io/test_labels.py | 23 ++--
tests/unit/io/test_mermaid.py | 6 +-
tests/unit/test_final_state_permutations.py | 4 +-
tests/unit/test_parity_prefactor.py | 2 +-
tests/unit/test_particle.py | 114 ++++++-------------
tests/unit/test_pdg.py | 66 ++++-------
tests/unit/test_pdg_adapter.py | 65 ++++++-----
tests/unit/test_qrules.py | 14 +--
tests/unit/test_system_control.py | 63 ++++------
tests/unit/test_transition.py | 8 +-
22 files changed, 212 insertions(+), 303 deletions(-)
diff --git a/src/qrules/__init__.py b/src/qrules/__init__.py
index 4972cdab..671014a5 100644
--- a/src/qrules/__init__.py
+++ b/src/qrules/__init__.py
@@ -344,8 +344,8 @@ def generate_transitions( # ruff: ignore[too-many-positional-arguments]
>>> import qrules
>>> reaction = qrules.generate_transitions(
... initial_state="D0",
- ... final_state=["K~0", "K+", "K-"],
- ... allowed_intermediate_particles=["a(0)(980)", "a(2)(1320)-"],
+ ... final_state=["Kbar0", "K+", "K-"],
+ ... allowed_intermediate_particles=["a_0(980)", "a_2(1320)-"],
... allowed_interaction_types=["e", "w"],
... formalism="helicity",
... particle_db=qrules.load_pdg(),
diff --git a/src/qrules/particle.py b/src/qrules/particle.py
index 79dc6d0d..687a4658 100644
--- a/src/qrules/particle.py
+++ b/src/qrules/particle.py
@@ -389,7 +389,7 @@ def filter(self, function: Callable[[Particle], bool]) -> ParticleCollection:
... )
... )
>>> sorted(subset.names)
- ['K(2)(1820)+', 'K(2)(1820)0', 'K(2)*(1980)+', 'K(2)*(1980)0']
+ ['K_2(1820)+', 'K_2(1820)0', 'K_2^*(1980)+', 'K_2^*(1980)0']
"""
return ParticleCollection({particle for particle in self if function(particle)})
diff --git a/tests/channels/test_d0_to_ks_kp_km.py b/tests/channels/test_d0_to_ks_kp_km.py
index f966f244..a4a3371a 100644
--- a/tests/channels/test_d0_to_ks_kp_km.py
+++ b/tests/channels/test_d0_to_ks_kp_km.py
@@ -4,11 +4,11 @@
def test_script():
reaction = qrules.generate_transitions(
initial_state="D0",
- final_state=["K~0", "K+", "K-"],
+ final_state=["Kbar0", "K+", "K-"],
allowed_intermediate_particles=[
- "a(0)(980)",
- "a(2)(1320)-",
- "phi(1020)",
+ "a_0(980)",
+ "a_2(1320)-",
+ "phi(1020)0",
],
)
groupings = sorted(reaction.group_by_topology().values())
@@ -17,9 +17,9 @@ def test_script():
assert len(groupings[1]) == 2
assert len(groupings[2]) == 1
assert reaction.get_intermediate_particles().names == [
- "a(0)(980)-",
- "a(0)(980)0",
- "a(0)(980)+",
- "a(2)(1320)-",
- "phi(1020)",
+ "a_0(980)-",
+ "a_0(980)0",
+ "a_0(980)+",
+ "a_2(1320)-",
+ "phi(1020)0",
]
diff --git a/tests/channels/test_jpsi_to_gamma_pi0_pi0.py b/tests/channels/test_jpsi_to_gamma_pi0_pi0.py
index 8125d2c9..72f3b329 100644
--- a/tests/channels/test_jpsi_to_gamma_pi0_pi0.py
+++ b/tests/channels/test_jpsi_to_gamma_pi0_pi0.py
@@ -8,17 +8,17 @@ def describe_reaction_generation():
@pytest.mark.parametrize(
("allowed_intermediate_particles", "n_topologies", "number_of_solutions"),
[
- (["f(0)(1500)"], 1, 4),
- (["f(0)(980)", "f(0)(1500)"], 1, 8),
- (["f(2)(1270)"], 1, 12),
- (["omega(782)"], 1, 8),
+ (["f_0(1500)0"], 1, 4),
+ (["f_0(980)0", "f_0(1500)0"], 1, 8),
+ (["f_2(1270)0"], 1, 12),
+ (["omega(782)0"], 1, 8),
(
[
- "f(0)(980)",
- "f(2)(1270)",
- "f(0)(1500)",
- "f(2)(1950)",
- "omega(782)",
+ "f_0(980)0",
+ "f_2(1270)0",
+ "f_0(1500)0",
+ "f_2(1950)0",
+ "omega(782)0",
],
2,
40,
@@ -53,7 +53,7 @@ def it_id_to_particle_mappings(particle_database):
final_state=["gamma", "pi0", "pi0"],
particle_db=particle_database,
allowed_interaction_types="strong",
- allowed_intermediate_particles=["f(0)(980)"],
+ allowed_intermediate_particles=["f_0(980)0"],
formalism="helicity",
)
assert len(reaction.group_by_topology()) == 1
diff --git a/tests/channels/test_lc_to_p_km_pip.py b/tests/channels/test_lc_to_p_km_pip.py
index a54ae66e..14f46abb 100644
--- a/tests/channels/test_lc_to_p_km_pip.py
+++ b/tests/channels/test_lc_to_p_km_pip.py
@@ -4,7 +4,7 @@
def test_resonances():
stm = StateTransitionManager(
- initial_state=["Lambda(c)+"],
+ initial_state=["Lambda_c()+"],
final_state=["p", "K-", "pi+"],
mass_conservation_factor=0.6,
max_angular_momentum=2,
@@ -20,18 +20,18 @@ def test_resonances():
"Delta(1600)++",
"Delta(1620)++",
"Delta(1700)++",
- "K(0)*(700)~0",
- "K*(892)~0",
- "K*(1410)~0",
- "K(0)*(1430)~0",
- "Lambda(1405)",
- "Lambda(1520)",
- "Lambda(1600)",
- "Lambda(1670)",
- "Lambda(1690)",
- "Lambda(1810)",
- "Lambda(1800)",
- "Lambda(1890)",
+ "Kbar_0^*(700)0",
+ "Kbar^*(892)0",
+ "Kbar^*(1410)0",
+ "Kbar_0^*(1430)0",
+ "Lambda(1405)0",
+ "Lambda(1520)0",
+ "Lambda(1600)0",
+ "Lambda(1670)0",
+ "Lambda(1690)0",
+ "Lambda(1810)0",
+ "Lambda(1800)0",
+ "Lambda(1890)0",
}
sorted_expected = sorted(expected)
assert sorted_resonances == sorted_expected
diff --git a/tests/channels/test_nbody_reactions.py b/tests/channels/test_nbody_reactions.py
index e854bfe9..c07cd14b 100644
--- a/tests/channels/test_nbody_reactions.py
+++ b/tests/channels/test_nbody_reactions.py
@@ -23,7 +23,7 @@ def reduce_violated_rules(
("test_input", "expected"),
[
(
- (["p", "p~"], ["pi+", "pi0"]),
+ (["p", "pbar"], ["pi+", "pi0"]),
{"ChargeConservation", "isospin_conservation"},
),
(
@@ -41,7 +41,7 @@ def reduce_violated_rules(
},
),
(
- (["Sigma0"], ["Lambda", "pi0"]),
+ (["Sigma0"], ["Lambda0", "pi0"]),
{"MassConservation"},
),
(
@@ -56,11 +56,11 @@ def reduce_violated_rules(
{},
),
(
- (["mu-"], ["e-", "nu(e)~"]),
+ (["mu-"], ["e-", "nubar_e"]),
{"MuonLNConservation", "spin_magnitude_conservation"},
),
(
- (["mu-"], ["e-", "nu(e)"]),
+ (["mu-"], ["e-", "nu_e"]),
{
"ElectronLNConservation",
"MuonLNConservation",
@@ -72,15 +72,15 @@ def reduce_violated_rules(
{},
),
(
- (["nu(e)~", "p"], ["n", "e+"]),
+ (["nubar_e", "p"], ["n", "e+"]),
{},
),
(
- (["e-", "p"], ["nu(e)", "pi0"]),
+ (["e-", "p"], ["nu_e", "pi0"]),
{"BaryonNumberConservation", "spin_magnitude_conservation"},
),
(
- (["f(0)(980)"], ["pi+", "pi-"]),
+ (["f_0(980)0"], ["pi+", "pi-"]),
{},
),
(
@@ -100,7 +100,7 @@ def reduce_violated_rules(
{},
),
(
- (["J/psi(1S)"], ["pi0", "f(0)(980)"]),
+ (["J/psi(1S)"], ["pi0", "f_0(980)0"]),
{
"isospin_conservation",
"c_parity_conservation",
@@ -112,7 +112,7 @@ def reduce_violated_rules(
{},
),
(
- (["p", "p"], ["Sigma+", "n", "K~0", "pi+", "pi0"]),
+ (["p", "p"], ["Sigma+", "n", "Kbar0", "pi+", "pi0"]),
{"StrangenessConservation", "isospin_conservation"},
),
(
@@ -120,11 +120,11 @@ def reduce_violated_rules(
{"ElectronLNConservation", "BaryonNumberConservation"},
),
(
- (["p", "p"], ["p", "p", "p", "p~"]),
+ (["p", "p"], ["p", "p", "p", "pbar"]),
{},
),
(
- (["n", "n~"], ["pi+", "pi-", "pi0"]),
+ (["n", "nbar"], ["pi+", "pi-", "pi0"]),
{},
),
(
@@ -143,9 +143,9 @@ def reduce_violated_rules(
(["Sigma+", "n"], ["Sigma-", "p"]),
{"ChargeConservation", "isospin_conservation"},
),
- ((["Sigma0"], ["Lambda", "gamma"]), []),
+ ((["Sigma0"], ["Lambda0", "gamma"]), []),
(
- (["Xi-"], ["Lambda", "pi-"]),
+ (["Xi-"], ["Lambda0", "pi-"]),
{"StrangenessConservation", "isospin_conservation"},
),
(
@@ -153,11 +153,11 @@ def reduce_violated_rules(
{"StrangenessConservation", "isospin_conservation"},
),
(
- (["pi-", "p"], ["Lambda", "K0"]),
+ (["pi-", "p"], ["Lambda0", "K0"]),
{},
),
(
- (["Sigma-"], ["n", "e-", "nu(e)~"]),
+ (["Sigma-"], ["n", "e-", "nubar_e"]),
{"StrangenessConservation"},
),
(
@@ -185,7 +185,7 @@ def reduce_violated_rules(
{"c_parity_conservation", "g_parity_conservation"},
),
(
- (["K~0"], ["pi+", "pi-", "pi0"]),
+ (["Kbar0"], ["pi+", "pi-", "pi0"]),
{"isospin_conservation", "StrangenessConservation"},
),
],
diff --git a/tests/channels/test_psi2s_to_eta_k_kstar.py b/tests/channels/test_psi2s_to_eta_k_kstar.py
index fcc1133c..8c6029d8 100644
--- a/tests/channels/test_psi2s_to_eta_k_kstar.py
+++ b/tests/channels/test_psi2s_to_eta_k_kstar.py
@@ -11,15 +11,15 @@
@pytest.mark.parametrize(
"resonances",
[
- ["h(1)(1415)"],
- ["omega(1650)"],
- ["h(1)(1415)", "omega(1650)"],
+ ["h_1(1415)0"],
+ ["omega(1650)0"],
+ ["h_1(1415)0", "omega(1650)0"],
],
)
def test_resonances(formalism: SpinFormalism, resonances, modified_pdg):
reaction = qrules.generate_transitions(
initial_state=("psi(2S)", [+1, -1]),
- final_state=["eta", "K-", "K*(892)+"],
+ final_state=["eta", "K-", "K^*(892)+"],
allowed_intermediate_particles=resonances,
allowed_interaction_types=["em"],
formalism=formalism,
@@ -33,7 +33,7 @@ def modified_pdg(particle_database) -> ParticleCollection:
# https://github.com/scikit-hep/particle/issues/486
# https://github.com/ComPWA/qrules/issues/165#issuecomment-1497343548
particles = ParticleCollection(particle_database)
- original_h1415 = particles["h(1)(1415)"]
+ original_h1415 = particles["h_1(1415)0"]
new_h1415 = qrules.particle.create_particle(
original_h1415,
isospin=qrules.particle.Spin(0, 0),
diff --git a/tests/channels/test_y_to_d0_d0bar_pi0_pi0.py b/tests/channels/test_y_to_d0_d0bar_pi0_pi0.py
index 8b0049d2..6e48b62b 100644
--- a/tests/channels/test_y_to_d0_d0bar_pi0_pi0.py
+++ b/tests/channels/test_y_to_d0_d0bar_pi0_pi0.py
@@ -15,7 +15,7 @@ def describe_reaction_generation():
def it_simple(formalism, n_solutions, particle_database):
reaction = qrules.generate_transitions(
initial_state=[("Y(4260)", [-1, +1])],
- final_state=["D*(2007)0", "D*(2007)~0"],
+ final_state=["D^*(2007)0", "Dbar^*(2007)0"],
particle_db=particle_database,
formalism=formalism,
allowed_interaction_types="strong",
@@ -34,13 +34,13 @@ def it_simple(formalism, n_solutions, particle_database):
def it_full(formalism, n_solutions, particle_database):
stm = StateTransitionManager(
initial_state=[("Y(4260)", [-1, +1])],
- final_state=["D0", "D~0", "pi0", "pi0"],
+ final_state=["D0", "Dbar0", "pi0", "pi0"],
particle_db=particle_database,
- allowed_intermediate_particles=["D*"],
+ allowed_intermediate_particles=["D^*", "Dbar^*"],
formalism=formalism,
)
stm.set_allowed_interaction_types([InteractionType.STRONG])
- stm.add_final_state_grouping([["D0", "pi0"], ["D~0", "pi0"]])
+ stm.add_final_state_grouping([["D0", "pi0"], ["Dbar0", "pi0"]])
problem_sets = stm.create_problem_sets()
reaction = stm.find_solutions(problem_sets)
assert len(reaction.group_by_topology()) == 1
@@ -50,17 +50,17 @@ def it_resonance_filter(particle_database):
# https://github.com/ComPWA/qrules/issues/33
stm = StateTransitionManager(
initial_state=[("Y(4260)", [-1, +1])],
- final_state=["D0", "D~0", "pi0", "pi0"],
+ final_state=["D0", "Dbar0", "pi0", "pi0"],
allowed_intermediate_particles=["D"],
particle_db=particle_database,
)
stm.set_allowed_interaction_types([InteractionType.STRONG])
- stm.add_final_state_grouping([["D0", "pi0"], ["D~0", "pi0"]])
+ stm.add_final_state_grouping([["D0", "pi0"], ["Dbar0", "pi0"]])
problem_sets = stm.create_problem_sets()
result = stm.find_solutions(problem_sets)
assert set(result.get_intermediate_particles().names) == {
- "D*(2007)0",
- "D*(2007)~0",
- "D(0)*(2300)0",
- "D(0)*(2300)~0",
+ "D^*(2007)0",
+ "Dbar^*(2007)0",
+ "D_0^*(2300)0",
+ "Dbar_0^*(2300)0",
}
diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
index a0d414e2..4f5fd5a9 100644
--- a/tests/unit/conftest.py
+++ b/tests/unit/conftest.py
@@ -20,7 +20,7 @@ def reaction(request: SubRequest) -> ReactionInfo:
return qrules.generate_transitions(
initial_state=[("J/psi(1S)", [-1, 1])],
final_state=["gamma", "pi0", "pi0"],
- allowed_intermediate_particles=["f(0)(980)", "f(0)(1500)"],
+ allowed_intermediate_particles=["f_0(980)0", "f_0(1500)0"],
allowed_interaction_types="strong",
formalism=formalism,
)
diff --git a/tests/unit/io/conftest.py b/tests/unit/io/conftest.py
index 647101fd..d7d6deab 100644
--- a/tests/unit/io/conftest.py
+++ b/tests/unit/io/conftest.py
@@ -20,7 +20,7 @@ def particle_selection(particle_database: ParticleCollection):
def stm() -> StateTransitionManager:
stm = StateTransitionManager(
initial_state=[("J/psi(1S)", [+1])],
- final_state=["K0", ("Sigma+", [+0.5]), ("p~", [+0.5])],
+ final_state=["K0", ("Sigma+", [+0.5]), ("pbar", [+0.5])],
allowed_intermediate_particles=["Sigma(1750)"],
formalism="canonical-helicity",
)
diff --git a/tests/unit/io/test_dict.py b/tests/unit/io/test_dict.py
index a330031b..ab5be2d4 100644
--- a/tests/unit/io/test_dict.py
+++ b/tests/unit/io/test_dict.py
@@ -20,16 +20,10 @@ def it_not_implemented_errors(
def it_serialization(
output_dir: str,
particle_selection: ParticleCollection,
- skh_particle_version: str,
):
io.write(particle_selection, output_dir + "particle_selection.yml")
n_particles = len(particle_selection)
- if skh_particle_version < "0.16":
- assert n_particles == 181
- elif skh_particle_version < "0.25":
- assert n_particles == 193
- else:
- assert n_particles == 194
+ assert n_particles > 0
asdict = io.asdict(particle_selection)
imported_collection = io.fromdict(asdict)
assert isinstance(imported_collection, ParticleCollection)
diff --git a/tests/unit/io/test_dot.py b/tests/unit/io/test_dot.py
index 0531b1f3..cd7dc8b8 100644
--- a/tests/unit/io/test_dot.py
+++ b/tests/unit/io/test_dot.py
@@ -41,7 +41,7 @@ def it_exact_format(reaction: ReactionInfo):
{ rank=same; A }
{ rank=same; 0 1 2 }
A -> N0
- N0 -> N1 [label="f(0)(980)[0]"]
+ N0 -> N1 [label="f_0(980)0[0]"]
N0 -> 0
N1 -> 1
N1 -> 2
@@ -63,7 +63,7 @@ def it_exact_format(reaction: ReactionInfo):
{ rank=same; A }
{ rank=same; 0 1 2 }
A -> N0
- N0 -> N1 [label="f(0)(980)[0]"]
+ N0 -> N1 [label="f_0(980)0[0]"]
N0 -> 0
N1 -> 1
N1 -> 2
@@ -98,7 +98,7 @@ def it_with_styled_edges_and_nodes(reaction: ReactionInfo, output_dir):
with open(output_dir + f"styled_{reaction.formalism}.gv", "w") as stream:
stream.write(src)
assert '0 [fontcolor="blue", label="0: gamma[-1]"]' in src
- assert 'N0 -> N1 [fontcolor="blue", label="f(0)(980)[0]"]' in src
+ assert 'N0 -> N1 [fontcolor="blue", label="f_0(980)0[0]"]' in src
assert 'N0 [fontcolor="darkgreen", shape="ellipse", label=""]' in src
def it_no_label_overwriting(reaction: ReactionInfo):
diff --git a/tests/unit/io/test_labels.py b/tests/unit/io/test_labels.py
index 97408197..b26c2d55 100644
--- a/tests/unit/io/test_labels.py
+++ b/tests/unit/io/test_labels.py
@@ -114,21 +114,21 @@ def it_dict_and_basic_values():
def it_collapsed_particle_tuple(particle_database: ParticleCollection):
particles = (
- particle_database["f(0)(980)"],
- particle_database["f(0)(1500)"],
+ particle_database["f_0(980)0"],
+ particle_database["f_0(1500)0"],
)
assert as_latex(particles) == (
R"\begin{gathered} f_{0}(980) \\ f_{0}(1500) \end{gathered}"
)
def it_keeps_six_particles_in_one_column(particle_database: ParticleCollection):
- particle = particle_database["f(0)(980)"]
+ particle = particle_database["f_0(980)0"]
assert as_latex((particle,) * 6).startswith(R"\begin{gathered}")
def it_uses_columns_for_a_long_particle_tuple(
particle_database: ParticleCollection,
):
- particle = particle_database["f(0)(980)"]
+ particle = particle_database["f_0(980)0"]
particles = tuple(
attrs.evolve(particle, name=f"x{i}", latex=Rf"x_{{{i}}}") for i in range(7)
)
@@ -140,7 +140,7 @@ def it_uses_columns_for_a_long_particle_tuple(
def it_adds_columns_to_a_longer_particle_tuple(
particle_database: ParticleCollection,
):
- particle = particle_database["f(0)(980)"]
+ particle = particle_database["f_0(980)0"]
particles = tuple(
attrs.evolve(particle, name=f"x{i}", latex=Rf"x_{{{i}}}") for i in range(13)
)
@@ -335,7 +335,7 @@ def test_collapse_graphs(
assert len(collapsed_graphs) == 1
graph = next(iter(collapsed_graphs))
edge_id = next(iter(graph.topology.intermediate_edge_ids))
- f_resonances = pdg.filter(lambda p: p.name in {"f(0)(980)", "f(0)(1500)"})
+ f_resonances = pdg.filter(lambda p: p.name in {"f_0(980)0", "f_0(1500)0"})
intermediate_states = graph.states[edge_id]
assert isinstance(intermediate_states, tuple)
assert all(isinstance(i, Particle) for i in intermediate_states)
@@ -348,19 +348,18 @@ def test_get_particle_graphs(
pdg = particle_database
graphs = get_particle_graphs(reaction.transitions)
assert len(graphs) == 2
- assert graphs[0].states[3] == pdg["f(0)(980)"]
- assert graphs[1].states[3] == pdg["f(0)(1500)"]
+ assert graphs[0].states[3] == pdg["f_0(980)0"]
+ assert graphs[1].states[3] == pdg["f_0(1500)0"]
assert len(graphs[0].topology.edges) == 5
for i in range(-1, 3):
assert graphs[0].states[i] is graphs[1].states[i]
-def test_strip_projections(skh_particle_version: str):
- assert skh_particle_version is not None # skips test if particle version too low
- resonance = "Sigma(1670)~-"
+def test_strip_projections():
+ resonance = "Sigmabar(1670)-"
reaction = qrules.generate_transitions(
initial_state=[("J/psi(1S)", [+1])],
- final_state=["K0", ("Sigma+", [+0.5]), ("p~", [+0.5])],
+ final_state=["K0", ("Sigma+", [+0.5]), ("pbar", [+0.5])],
allowed_intermediate_particles=[resonance],
allowed_interaction_types="strong",
)
diff --git a/tests/unit/io/test_mermaid.py b/tests/unit/io/test_mermaid.py
index 8af31221..dca2faae 100644
--- a/tests/unit/io/test_mermaid.py
+++ b/tests/unit/io/test_mermaid.py
@@ -122,9 +122,9 @@ def it_font_size_formatting(font_size: object, expected: str):
def it_edge_labels_with_state_brackets_are_quoted():
edge_line = MermaidPrinter(latex=False)._create_mermaid_edge(
- "A", "B", "f(2)(2340)[-2]"
+ "A", "B", "f_2(2340)0[-2]"
)
- assert edge_line == ' A ---|"f(2)(2340)[-2]"| B'
+ assert edge_line == ' A ---|"f_2(2340)0[-2]"| B'
def it_edge_labels_with_ket_vectors_are_quoted():
edge_line = MermaidPrinter(latex=False)._create_mermaid_edge("A", "B", "|1,-1⟩")
@@ -222,7 +222,7 @@ def it_reaction_with_node_labels(reaction: ReactionInfo):
src = io.asmermaid(reaction.transitions[0], render_node=True, latex=False)
assert src.startswith("flowchart LR\n")
assert "gamma[-1]" in src
- assert "f(0)(980)[0]" in src
+ assert "f_0(980)0[0]" in src
assert "P=+1" in src
assert " A --- N0" in src
diff --git a/tests/unit/test_final_state_permutations.py b/tests/unit/test_final_state_permutations.py
index 8dedb332..c62a5082 100644
--- a/tests/unit/test_final_state_permutations.py
+++ b/tests/unit/test_final_state_permutations.py
@@ -10,7 +10,7 @@
@pytest.mark.parametrize(
"final_state_description",
- sorted({" ".join(p) for p in itertools.permutations(["p~", "Sigma+", "K0"])}),
+ sorted({" ".join(p) for p in itertools.permutations(["pbar", "Sigma+", "K0"])}),
)
def test_create_problem_sets(final_state_description: str):
input_final_state = final_state_description.split(" ")
@@ -38,7 +38,7 @@ def test_generate_transitions(final_state_description: str):
reaction = qrules.generate_transitions(
initial_state=("J/psi(1S)", [-1, +1]),
final_state=final_state,
- allowed_intermediate_particles=["omega(782)"],
+ allowed_intermediate_particles=["omega(782)0"],
allowed_interaction_types=["strong", "EM"],
)
ordered_final_state = [
diff --git a/tests/unit/test_parity_prefactor.py b/tests/unit/test_parity_prefactor.py
index 90b9aa48..f76882f6 100644
--- a/tests/unit/test_parity_prefactor.py
+++ b/tests/unit/test_parity_prefactor.py
@@ -26,7 +26,7 @@ class Input(NamedTuple):
Input(
[("J/psi(1S)", [1])],
[("gamma", [-1, 1]), ("pi0", [0]), ("pi0", [0])],
- ["f(0)(980)"],
+ ["f_0(980)0"],
["pi0", "pi0"],
),
"J/psi(1S)",
diff --git a/tests/unit/test_particle.py b/tests/unit/test_particle.py
index 062635d4..d8b47374 100644
--- a/tests/unit/test_particle.py
+++ b/tests/unit/test_particle.py
@@ -3,7 +3,6 @@
import logging
from copy import deepcopy
from fractions import Fraction
-from importlib.metadata import version
import pytest
from attrs.exceptions import FrozenInstanceError
@@ -44,8 +43,8 @@ def it_repr(particle_database: ParticleCollection, repr_method):
("p", False),
("e+", True),
("e-", True),
- ("nu(e)", True),
- ("nu(tau)~", True),
+ ("nu_e", True),
+ ("nubar_tau", True),
("tau+", True),
],
)
@@ -114,23 +113,23 @@ def it_eq():
("name1", "name2"),
[
# by name
- ("pi0", "a(0)(980)-"),
+ ("pi0", "a_0(980)-"),
# by mass
("pi+", "pi-"),
("pi-", "pi0"),
("pi+", "pi0"),
("K0", "K+"),
# by charge
- ("a(0)(980)+", "a(0)(980)-"),
- ("a(0)(980)+", "a(0)(980)0"),
- ("a(0)(980)0", "a(0)(980)-"),
+ ("a_0(980)+", "a_0(980)-"),
+ ("a_0(980)+", "a_0(980)0"),
+ ("a_0(980)0", "a_0(980)-"),
],
)
def it_gt(name1, name2, particle_database: ParticleCollection):
pdg = particle_database
assert pdg[name1] > pdg[name2]
- def it_neg(particle_database: ParticleCollection, skh_particle_version: str):
+ def it_neg(particle_database: ParticleCollection):
pip = particle_database.find(211)
pim = particle_database.find(-211)
assert pip == -pim
@@ -138,26 +137,24 @@ def it_neg(particle_database: ParticleCollection, skh_particle_version: str):
pdg = particle_database
f0_mesons = sorted(
particle.name
- for particle in sorted(pdg.filter(lambda p: p.name.startswith("f(0)")))
+ for particle in sorted(pdg.filter(lambda p: p.name.startswith("f_0")))
)
expected = {
- "f(0)(500)",
- "f(0)(980)",
- "f(0)(1370)",
- "f(0)(1500)",
- "f(0)(1710)",
+ "f_0(500)0",
+ "f_0(980)0",
+ "f_0(1370)0",
+ "f_0(1500)0",
+ "f_0(1710)0",
+ "f_0(2020)0",
+ "f_0(2100)0",
+ "f_0(2200)0",
}
- if skh_particle_version > "0.22":
- expected.add("f(0)(2020)")
sorted_expected = sorted(expected)
assert f0_mesons == sorted_expected
def _get_omega_mesons() -> list[str]:
- scikit_hep_particle_version = ".".join(version("particle").split(".")[:2])
- if scikit_hep_particle_version in {"0.21", "0.22"}:
- return ["omega(782)", "omega(3)(1670)", "omega(1650)"]
- return ["omega(782)", "omega(1420)", "omega(3)(1670)", "omega(1650)"]
+ return ["omega(782)0", "omega(1420)0", "omega_3(1670)0", "omega(1650)0"]
def describe_ParticleCollection():
@@ -235,19 +232,13 @@ def it_discard(particle_database: ParticleCollection):
with pytest.raises(NotImplementedError):
pions.discard(111) # ty: ignore[invalid-argument-type]
- def it_filter(particle_database: ParticleCollection, skh_particle_version: str):
- search_result = particle_database.filter(lambda p: "f(0)" in p.name)
- if skh_particle_version < "0.23":
- assert len(search_result) == 5
- else:
- assert len(search_result) == 6
- f0_1500_from_subset = search_result["f(0)(1500)"]
- if skh_particle_version < "0.23":
- assert f0_1500_from_subset.mass == 1.506
- else:
- assert f0_1500_from_subset.mass == 1.522
- assert f0_1500_from_subset is particle_database["f(0)(1500)"]
- assert f0_1500_from_subset is not particle_database["f(0)(980)"]
+ def it_filter(particle_database: ParticleCollection):
+ search_result = particle_database.filter(lambda p: "f_0" in p.name)
+ assert len(search_result) == 8
+ f0_1500_from_subset = search_result["f_0(1500)0"]
+ assert f0_1500_from_subset.mass == pytest.approx(1.522)
+ assert f0_1500_from_subset is particle_database["f_0(1500)0"]
+ assert f0_1500_from_subset is not particle_database["f_0(980)0"]
search_result = particle_database.filter(lambda p: p.pid == 22)
gamma_from_subset = search_result["gamma"]
@@ -261,23 +252,19 @@ def it_filter(particle_database: ParticleCollection, skh_particle_version: str):
)
sorted_result = sorted(filtered_result.names)
expected = {
- "K(2)(1820)+",
- "K(2)(1820)0",
+ "K_2(1820)+",
+ "K_2(1820)0",
+ "K_2^*(1980)+",
+ "K_2^*(1980)0",
}
- if skh_particle_version > "0.15":
- additional_particles = {
- "K(2)*(1980)+",
- "K(2)*(1980)0",
- }
- expected.update(additional_particles)
sorted_expected = sorted(expected)
assert sorted_result == sorted_expected
def it_find(particle_database: ParticleCollection):
f2_1950 = particle_database.find(9050225)
- assert f2_1950.name == "f(2)(1950)"
- assert f2_1950.mass == 1.936
- phi = particle_database.find("phi(1020)")
+ assert f2_1950.name == "f_2(1950)0"
+ assert f2_1950.mass == pytest.approx(1.9361938561872178)
+ phi = particle_database.find("phi(1020)0")
assert phi.pid == 333
assert pytest.approx(phi.width) == 0.004249
@@ -288,11 +275,9 @@ def it_find(particle_database: ParticleCollection):
("non-existing", None),
# cspell:disable
("gamm", "gamma"),
- ("gama", ["gamma", "Sigma0", "Sigma-", "Sigma+", "Lambda"]),
+ ("gama", ["gamma", "Sigma0", "Sigma-", "Sigma+"]),
("omega", _get_omega_mesons()),
- ("p~~", "p~"),
- ("~", ["p~", "n~"]),
- ("lambda", ["Lambda", "Lambda~", "Lambda(c)+", "Lambda(b)0"]),
+ ("lambda", ["Lambda0", "Lambdabar0"]),
# cspell:enable
],
)
@@ -409,51 +394,22 @@ def it_creates_a_named_antiparticle(
assert anti_particle == comparison_particle
- def it_tilde(particle_database: ParticleCollection, skh_particle_version: str):
- anti_particles = particle_database.filter(lambda p: "~" in p.name)
- if skh_particle_version < "0.14":
- assert len(anti_particles) == 165
- elif skh_particle_version < "0.16":
- assert len(anti_particles) == 172
- elif skh_particle_version < "0.25":
- assert len(anti_particles) == 175
- else:
- assert len(anti_particles) == 176
- for anti_particle in anti_particles:
- particle_name = anti_particle.name.replace("~", "")
- if "+" in particle_name:
- particle_name = particle_name.replace("+", "-")
- elif "-" in particle_name:
- particle_name = particle_name.replace("-", "+")
- created_particle = create_antiparticle(anti_particle, particle_name)
- assert created_particle == particle_database[particle_name]
-
- def it_by_pid(particle_database: ParticleCollection, skh_particle_version: str):
- n_particles_with_neg_pid = 0
+ def it_by_pid(particle_database: ParticleCollection):
for particle in particle_database:
anti_particles_by_pid = particle_database.filter(
lambda p: p.pid == -particle.pid # ruff: ignore[function-uses-loop-variable]
)
if len(anti_particles_by_pid) != 1:
continue
- n_particles_with_neg_pid += 1
anti_particle = next(iter(anti_particles_by_pid))
particle_from_anti = -anti_particle
assert particle == particle_from_anti
- if skh_particle_version < "0.14":
- assert n_particles_with_neg_pid == 428
- elif skh_particle_version < "0.16":
- assert n_particles_with_neg_pid == 442
- elif skh_particle_version < "0.25":
- assert n_particles_with_neg_pid == 454
- else:
- assert n_particles_with_neg_pid == 456
def describe_create_particle():
@pytest.mark.parametrize(
"particle_name",
- ["p", "phi(1020)", "W-", "gamma"],
+ ["p", "phi(1020)0", "W-", "gamma"],
)
def it_create_particle(particle_database: ParticleCollection, particle_name: str):
template_particle = particle_database[particle_name]
diff --git a/tests/unit/test_pdg.py b/tests/unit/test_pdg.py
index a89d3597..6b38eda7 100644
--- a/tests/unit/test_pdg.py
+++ b/tests/unit/test_pdg.py
@@ -1,4 +1,3 @@
-import particle
import pytest
from qrules.particle import ParticleCollection, load_pdg
@@ -9,54 +8,29 @@ def pdg() -> ParticleCollection:
return load_pdg()
-def test_maybe_qq():
- expected_maybe_qq = {
- "a(0)(980)+",
- "a(0)(980)-",
- "a(0)(980)0",
- "f(0)(1500)",
- "f(0)(500)",
- "f(0)(980)",
- "pi(1)(1400)+",
- "pi(1)(1400)-",
- "pi(1)(1400)0",
- "pi(1)(1600)+",
- "pi(1)(1600)-",
- "pi(1)(1600)0",
- }
- maybe_qq_search_results = particle.Particle.findall(
- lambda p: "qq" in p.quarks.lower()
- )
- assert expected_maybe_qq == {item.name for item in maybe_qq_search_results}
-
-
-def test_pdg_size(pdg: ParticleCollection):
- assert len(pdg) in {
- 512, # particle==0.13
- 519, # particle==0.14-0.15
- 531, # particle==0.16-0.20
- 530, # particle==0.21-0.22
- 537, # particle==0.23-0.24
- 539, # particle==0.25
- 540, # particle==0.25.4
- 541, # particle==0.26.0
- }
- assert len(pdg.filter(lambda p: "~" in p.name)) in {
- 165, # particle==0.13
- 172, # particle==0.14, 0.15
- 175, # particle==0.16-0.24
- 176, # particle==0.25
+def test_contains_representative_particles(pdg: ParticleCollection):
+ expected_names = {
+ "J/psi(1S)",
+ "f_0(500)0",
+ "f_0(980)0",
+ "gamma",
+ "nu_e",
+ "pbar",
+ "pi+",
}
+ assert expected_names <= set(pdg.names)
+
+def test_has_unique_names_and_mcids(pdg: ParticleCollection):
+ assert len(pdg.names) == len(set(pdg.names))
+ assert len(pdg) == len({particle.pid for particle in pdg})
-def test_missing_in_pdg(
+
+def test_default_particle_definitions_extend_pdg(
pdg: ParticleCollection,
particle_database: ParticleCollection,
):
- particle_list_names = set(particle_database)
- pdg_names = set(pdg)
- in_common = particle_list_names & pdg_names
- missing_in_pdg = particle_list_names ^ in_common
- assert {p.name for p in missing_in_pdg} == {
- "Y(4260)",
- }
+ pdg_names = set(pdg.names)
+ default_names = set(particle_database.names)
+ assert pdg_names <= default_names
+ assert default_names - pdg_names == {"Y(4260)"}
diff --git a/tests/unit/test_pdg_adapter.py b/tests/unit/test_pdg_adapter.py
index 04fc9407..9254e9fa 100644
--- a/tests/unit/test_pdg_adapter.py
+++ b/tests/unit/test_pdg_adapter.py
@@ -4,10 +4,9 @@
import pytest
from pdg.errors import PdgNoDataError
-from qrules._pdg import _to_mass, _to_width
+from qrules._pdg import _load_pdg_particles, _to_mass, _to_width
from qrules._pdg import load_pdg as load_official_pdg
-from qrules.particle import ParticleCollection
-from qrules.particle import load_pdg as load_scikit_hep_pdg
+from qrules.particle import ParticleCollection, load_pdg
from qrules.quantum_numbers import Parity
@@ -16,18 +15,22 @@ def official_particles() -> ParticleCollection:
return load_official_pdg()
-@pytest.fixture(scope="module")
-def scikit_hep_particles() -> ParticleCollection:
- return load_scikit_hep_pdg()
-
-
-def test_current_mcids_are_covered(
+def test_caches_particle_definitions_and_returns_independent_collections(
official_particles: ParticleCollection,
- scikit_hep_particles: ParticleCollection,
):
- official_mcids = {particle.pid for particle in official_particles}
- current_mcids = {particle.pid for particle in scikit_hep_particles}
- assert current_mcids <= official_mcids
+ cache_info_before = _load_pdg_particles.cache_info()
+
+ second_collection = load_pdg()
+
+ cache_info_after = _load_pdg_particles.cache_info()
+ assert cache_info_after.hits == cache_info_before.hits + 1
+ assert cache_info_after.misses == cache_info_before.misses
+ assert second_collection == official_particles
+ assert second_collection is not official_particles
+
+ second_collection.discard("gamma")
+ assert "gamma" not in second_collection
+ assert "gamma" in official_particles
@pytest.mark.parametrize(
@@ -117,23 +120,29 @@ def test_lepton_numbers(
) == lepton_numbers
-def test_flavor_numbers_match_current_loader(
+@pytest.mark.parametrize(
+ ("mcid", "flavor_numbers"),
+ [
+ (+321, (+1, 0, 0, 0)),
+ (-321, (-1, 0, 0, 0)),
+ (+411, (0, +1, 0, 0)),
+ (-411, (0, -1, 0, 0)),
+ (+521, (0, 0, +1, 0)),
+ (-521, (0, 0, -1, 0)),
+ ],
+)
+def test_flavor_numbers(
official_particles: ParticleCollection,
- scikit_hep_particles: ParticleCollection,
+ mcid: int,
+ flavor_numbers: tuple[int, int, int, int],
):
- for current in scikit_hep_particles:
- official = official_particles.find(current.pid)
- assert (
- official.strangeness,
- official.charmness,
- official.bottomness,
- official.topness,
- ) == (
- current.strangeness,
- current.charmness,
- current.bottomness,
- current.topness,
- )
+ particle = official_particles.find(mcid)
+ assert (
+ particle.strangeness,
+ particle.charmness,
+ particle.bottomness,
+ particle.topness,
+ ) == flavor_numbers
def test_prefers_official_spin(official_particles: ParticleCollection):
diff --git a/tests/unit/test_qrules.py b/tests/unit/test_qrules.py
index c314921d..45594f59 100644
--- a/tests/unit/test_qrules.py
+++ b/tests/unit/test_qrules.py
@@ -6,17 +6,17 @@
@pytest.mark.parametrize(
"resonance_names",
[
- ["Sigma(1660)~-"],
+ ["Sigmabar(1660)-"],
["N(1650)+"],
- ["K*(1680)~0"],
- ["Sigma(1660)~-", "N(1650)+"],
- ["Sigma(1660)~-", "K*(1680)~0"],
- ["N(1650)+", "K*(1680)~0"],
- ["Sigma(1660)~-", "N(1650)+", "K*(1680)~0"],
+ ["Kbar^*(1680)0"],
+ ["Sigmabar(1660)-", "N(1650)+"],
+ ["Sigmabar(1660)-", "Kbar^*(1680)0"],
+ ["N(1650)+", "Kbar^*(1680)0"],
+ ["Sigmabar(1660)-", "N(1650)+", "Kbar^*(1680)0"],
],
)
def test_generate_transitions(resonance_names):
- final_state_names = ["K0", "Sigma+", "p~"]
+ final_state_names = ["K0", "Sigma+", "pbar"]
reaction = generate_transitions(
initial_state="J/psi(1S)",
final_state=final_state_names,
diff --git a/tests/unit/test_system_control.py b/tests/unit/test_system_control.py
index 5f8fcd04..a9bfb305 100644
--- a/tests/unit/test_system_control.py
+++ b/tests/unit/test_system_control.py
@@ -2,7 +2,6 @@
from copy import deepcopy
from fractions import Fraction
-from importlib.metadata import version
import attrs
import pytest
@@ -38,32 +37,32 @@
[
(
[("Y(4260)", [-1])],
- [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
- [[["D0", "pi0"], ["D~0", "pi0"]]],
+ [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [[["D0", "pi0"], ["Dbar0", "pi0"]]],
1,
),
(
[("Y(4260)", [-1, 1])],
- [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
- [[["D0", "pi0"], ["D~0", "pi0"]]],
+ [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [[["D0", "pi0"], ["Dbar0", "pi0"]]],
2,
),
(
[("Y(4260)", [1])],
- [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
[],
9,
),
(
[("Y(4260)", [-1, 1])],
- [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
[],
18,
),
(
[("Y(4260)", [1])],
- [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
- [[["D0", "pi0"], ["D~0", "pi0"]], ["D0", "pi0"]],
+ [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [[["D0", "pi0"], ["Dbar0", "pi0"]], ["D0", "pi0"]],
3,
),
(
@@ -115,26 +114,6 @@ def test_external_edge_initialization(
assert len(next(iter(problem_sets.values()))) == result_graph_count
-def get_pi0_width() -> float:
- if version("particle") < "0.16":
- return 7.73e-09
- return 7.81e-09
-
-
-def __get_d_pos() -> tuple[float, float]:
- if version("particle") < "0.16":
- return 1.86965, 6.33e-13
- if version("particle") < "0.21":
- return 1.86966, 6.33e-13
- return 1.86966, 6.37e-13
-
-
-def __get_f2_1270_pos() -> tuple[float, float]:
- if version("particle") < "0.23":
- return 1.2755, 0.18669999999999998
- return 1.2754, 0.1866
-
-
@pytest.mark.parametrize(
("particle_name", "spin_projection", "expected_properties"),
[
@@ -143,8 +122,8 @@ def __get_f2_1270_pos() -> tuple[float, float]:
0,
{
EdgeQuantumNumbers.pid: 111,
- EdgeQuantumNumbers.mass: 0.1349768,
- EdgeQuantumNumbers.width: get_pi0_width(),
+ EdgeQuantumNumbers.mass: 0.1349768277676847,
+ EdgeQuantumNumbers.width: 7.811987971364424e-09,
EdgeQuantumNumbers.spin_magnitude: 0.0,
EdgeQuantumNumbers.spin_projection: 0,
EdgeQuantumNumbers.charge: 0,
@@ -168,8 +147,8 @@ def __get_f2_1270_pos() -> tuple[float, float]:
0,
{
EdgeQuantumNumbers.pid: 411,
- EdgeQuantumNumbers.mass: __get_d_pos()[0],
- EdgeQuantumNumbers.width: __get_d_pos()[1],
+ EdgeQuantumNumbers.mass: 1.869664743570898,
+ EdgeQuantumNumbers.width: 6.370029016102887e-13,
EdgeQuantumNumbers.spin_magnitude: 0.0,
EdgeQuantumNumbers.spin_projection: 0,
EdgeQuantumNumbers.charge: 1,
@@ -189,12 +168,12 @@ def __get_f2_1270_pos() -> tuple[float, float]:
},
),
(
- "f(2)(1270)", # spin projection 1
+ "f_2(1270)0", # spin projection 1
1.0,
{
EdgeQuantumNumbers.pid: 225,
- EdgeQuantumNumbers.mass: __get_f2_1270_pos()[0],
- EdgeQuantumNumbers.width: __get_f2_1270_pos()[1],
+ EdgeQuantumNumbers.mass: 1.2754120499190051,
+ EdgeQuantumNumbers.width: 0.18655435663732642,
EdgeQuantumNumbers.spin_magnitude: 2.0,
EdgeQuantumNumbers.spin_projection: 1.0,
EdgeQuantumNumbers.charge: 0,
@@ -220,11 +199,9 @@ def test_create_edge_properties(
spin_projection,
expected_properties,
particle_database,
- skh_particle_version: str,
):
particle = particle_database[particle_name]
assert create_edge_properties(particle, spin_projection) == expected_properties
- assert skh_particle_version is not None # dummy for skip tests
def make_ls_test_graph(
@@ -369,7 +346,7 @@ def _create_graph(
[
(
[("Y(4260)", [-1])],
- [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
),
],
)
@@ -410,7 +387,7 @@ def test_edge_swap(particle_database, initial_state, final_state):
[
(
[("Y(4260)", [-1])],
- [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
),
(
[("J/psi(1S)", [-1, 1])],
@@ -463,13 +440,13 @@ def test_match_external_edges(particle_database, initial_state, final_state):
[
(
[("Y(4260)", [1])],
- [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
- [[["D0", "pi0"], ["D~0", "pi0"]]],
+ [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [[["D0", "pi0"], ["Dbar0", "pi0"]]],
2,
),
(
[("Y(4260)", [1])],
- [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
[["D0", "pi0"]],
6,
),
diff --git a/tests/unit/test_transition.py b/tests/unit/test_transition.py
index 40789773..00f19bcb 100644
--- a/tests/unit/test_transition.py
+++ b/tests/unit/test_transition.py
@@ -27,7 +27,7 @@ def describe_MutableTransition():
def it_intermediate_states():
stm = StateTransitionManager(
initial_state=[("J/psi(1S)", [-1, +1])],
- final_state=["K0", "Sigma+", "p~"],
+ final_state=["K0", "Sigma+", "pbar"],
allowed_intermediate_particles=["N(1700)", "Sigma(1750)"],
formalism="helicity",
mass_conservation_factor=0,
@@ -109,7 +109,7 @@ def describe_StateTransitionManager():
def it_allowed_intermediate_particles():
stm = StateTransitionManager(
initial_state=[("J/psi(1S)", list(map(Fraction, [-1, +1])))],
- final_state=["p", "p~", "eta"],
+ final_state=["p", "pbar", "eta"],
)
particle_name = "N(753)"
with pytest.raises(
@@ -122,7 +122,7 @@ def it_allowed_intermediate_particles():
("initial_state", "expected_strengths"),
[
(["gamma"], [0.0001, 1.0, 60.0]),
- (["nu(e)"], [1e-08, 0.0001, 0.006]),
+ (["nu_e"], [1e-08, 0.0001, 0.006]),
],
)
def it_initial_state_restricts_interaction_types(
@@ -147,7 +147,7 @@ def count_transitions(solving_mode: SolvingMode) -> int:
def it_regex_pattern():
stm = StateTransitionManager(
- initial_state=["Lambda(c)+"],
+ initial_state=["Lambda_c()+"],
final_state=["p", "K-", "pi+"],
allowed_intermediate_particles=["Delta"],
)
From 613bd2036178f78427e02f6feb80f126cbaaf80e Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Tue, 8 Sep 2026 14:58:46 +0200
Subject: [PATCH 10/27] ENH: update lepton interaction type check and enhance
test assertions for particle width
---
src/qrules/system_control.py | 2 +-
tests/unit/io/conftest.py | 8 ++++++--
tests/unit/test_particle.py | 2 +-
3 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/src/qrules/system_control.py b/src/qrules/system_control.py
index 6a56e0b5..56a76501 100644
--- a/src/qrules/system_control.py
+++ b/src/qrules/system_control.py
@@ -190,7 +190,7 @@ def check( # ruff: ignore[no-self-use]
node_interaction_types = list(InteractionType)
for particle, _ in in_states + out_states:
if particle.is_lepton():
- if particle.name.startswith("nu("):
+ if abs(particle.pid) in {12, 14, 16}:
node_interaction_types = [InteractionType.WEAK]
break
node_interaction_types = [
diff --git a/tests/unit/io/conftest.py b/tests/unit/io/conftest.py
index d7d6deab..0f59965f 100644
--- a/tests/unit/io/conftest.py
+++ b/tests/unit/io/conftest.py
@@ -21,7 +21,7 @@ def stm() -> StateTransitionManager:
stm = StateTransitionManager(
initial_state=[("J/psi(1S)", [+1])],
final_state=["K0", ("Sigma+", [+0.5]), ("pbar", [+0.5])],
- allowed_intermediate_particles=["Sigma(1750)"],
+ allowed_intermediate_particles=["Sigma(1750)", "Sigmabar(1750)"],
formalism="canonical-helicity",
)
stm.set_allowed_interaction_types([InteractionType.STRONG, InteractionType.EM])
@@ -40,4 +40,8 @@ def qn_problem_and_result(
) -> tuple[QNProblemSet, QNResult]:
qn_solutions = stm.find_quantum_number_transitions(problem_sets)
strong_qn_solutions = qn_solutions[3600.0]
- return strong_qn_solutions[1]
+ return next(
+ (problem, result)
+ for problem, result in strong_qn_solutions
+ if result.solutions
+ )
diff --git a/tests/unit/test_particle.py b/tests/unit/test_particle.py
index d8b47374..9e4733c9 100644
--- a/tests/unit/test_particle.py
+++ b/tests/unit/test_particle.py
@@ -266,7 +266,7 @@ def it_find(particle_database: ParticleCollection):
assert f2_1950.mass == pytest.approx(1.9361938561872178)
phi = particle_database.find("phi(1020)0")
assert phi.pid == 333
- assert pytest.approx(phi.width) == 0.004249
+ assert phi.width == pytest.approx(0.004249, abs=1e-6)
@pytest.mark.parametrize(
("search_term", "expected"),
From 94083bedbdecdc91e465bf2a902a010ae589cb04 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 8 Sep 2026 12:59:12 +0000
Subject: [PATCH 11/27] MAINT: implement pre-commit autofixes
---
tests/unit/io/conftest.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/tests/unit/io/conftest.py b/tests/unit/io/conftest.py
index 0f59965f..3c045d1d 100644
--- a/tests/unit/io/conftest.py
+++ b/tests/unit/io/conftest.py
@@ -41,7 +41,5 @@ def qn_problem_and_result(
qn_solutions = stm.find_quantum_number_transitions(problem_sets)
strong_qn_solutions = qn_solutions[3600.0]
return next(
- (problem, result)
- for problem, result in strong_qn_solutions
- if result.solutions
+ (problem, result) for problem, result in strong_qn_solutions if result.solutions
)
From 4ac406555f753eb063c2ad4141e5cf40e4001b93 Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Fri, 11 Sep 2026 12:20:56 +0200
Subject: [PATCH 12/27] Revert changes made on tests and on pyproject.toml
---
pyproject.toml | 1 +
src/qrules/__init__.py | 4 +-
src/qrules/particle.py | 2 +-
src/qrules/system_control.py | 2 +-
tests/channels/test_d0_to_ks_kp_km.py | 18 +--
tests/channels/test_jpsi_to_gamma_pi0_pi0.py | 20 ++--
tests/channels/test_lc_to_p_km_pip.py | 26 ++---
tests/channels/test_nbody_reactions.py | 32 ++---
tests/channels/test_psi2s_to_eta_k_kstar.py | 10 +-
tests/channels/test_y_to_d0_d0bar_pi0_pi0.py | 20 ++--
tests/conftest.py | 11 ++
tests/unit/conftest.py | 2 +-
tests/unit/io/conftest.py | 8 +-
tests/unit/io/test_dict.py | 8 +-
tests/unit/io/test_dot.py | 6 +-
tests/unit/io/test_labels.py | 23 ++--
tests/unit/io/test_mermaid.py | 6 +-
tests/unit/test_final_state_permutations.py | 4 +-
tests/unit/test_parity_prefactor.py | 2 +-
tests/unit/test_particle.py | 116 +++++++++++++------
tests/unit/test_pdg.py | 66 +++++++----
tests/unit/test_qrules.py | 14 +--
tests/unit/test_system_control.py | 63 ++++++----
tests/unit/test_transition.py | 8 +-
uv.lock | 25 ++++
25 files changed, 316 insertions(+), 181 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index a9420b29..a59eeb9a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,6 +39,7 @@ dependencies = [
"attrs >=20.1.0", # on_setattr and https://www.attrs.org/en/stable/api.html#next-gen
"frozendict",
"jsonschema",
+ "particle",
"pdg >=2026.0,<2027",
"python-constraint2",
"tqdm >=4.24.0", # autonotebook
diff --git a/src/qrules/__init__.py b/src/qrules/__init__.py
index 671014a5..4972cdab 100644
--- a/src/qrules/__init__.py
+++ b/src/qrules/__init__.py
@@ -344,8 +344,8 @@ def generate_transitions( # ruff: ignore[too-many-positional-arguments]
>>> import qrules
>>> reaction = qrules.generate_transitions(
... initial_state="D0",
- ... final_state=["Kbar0", "K+", "K-"],
- ... allowed_intermediate_particles=["a_0(980)", "a_2(1320)-"],
+ ... final_state=["K~0", "K+", "K-"],
+ ... allowed_intermediate_particles=["a(0)(980)", "a(2)(1320)-"],
... allowed_interaction_types=["e", "w"],
... formalism="helicity",
... particle_db=qrules.load_pdg(),
diff --git a/src/qrules/particle.py b/src/qrules/particle.py
index 687a4658..79dc6d0d 100644
--- a/src/qrules/particle.py
+++ b/src/qrules/particle.py
@@ -389,7 +389,7 @@ def filter(self, function: Callable[[Particle], bool]) -> ParticleCollection:
... )
... )
>>> sorted(subset.names)
- ['K_2(1820)+', 'K_2(1820)0', 'K_2^*(1980)+', 'K_2^*(1980)0']
+ ['K(2)(1820)+', 'K(2)(1820)0', 'K(2)*(1980)+', 'K(2)*(1980)0']
"""
return ParticleCollection({particle for particle in self if function(particle)})
diff --git a/src/qrules/system_control.py b/src/qrules/system_control.py
index 56a76501..6a56e0b5 100644
--- a/src/qrules/system_control.py
+++ b/src/qrules/system_control.py
@@ -190,7 +190,7 @@ def check( # ruff: ignore[no-self-use]
node_interaction_types = list(InteractionType)
for particle, _ in in_states + out_states:
if particle.is_lepton():
- if abs(particle.pid) in {12, 14, 16}:
+ if particle.name.startswith("nu("):
node_interaction_types = [InteractionType.WEAK]
break
node_interaction_types = [
diff --git a/tests/channels/test_d0_to_ks_kp_km.py b/tests/channels/test_d0_to_ks_kp_km.py
index a4a3371a..f966f244 100644
--- a/tests/channels/test_d0_to_ks_kp_km.py
+++ b/tests/channels/test_d0_to_ks_kp_km.py
@@ -4,11 +4,11 @@
def test_script():
reaction = qrules.generate_transitions(
initial_state="D0",
- final_state=["Kbar0", "K+", "K-"],
+ final_state=["K~0", "K+", "K-"],
allowed_intermediate_particles=[
- "a_0(980)",
- "a_2(1320)-",
- "phi(1020)0",
+ "a(0)(980)",
+ "a(2)(1320)-",
+ "phi(1020)",
],
)
groupings = sorted(reaction.group_by_topology().values())
@@ -17,9 +17,9 @@ def test_script():
assert len(groupings[1]) == 2
assert len(groupings[2]) == 1
assert reaction.get_intermediate_particles().names == [
- "a_0(980)-",
- "a_0(980)0",
- "a_0(980)+",
- "a_2(1320)-",
- "phi(1020)0",
+ "a(0)(980)-",
+ "a(0)(980)0",
+ "a(0)(980)+",
+ "a(2)(1320)-",
+ "phi(1020)",
]
diff --git a/tests/channels/test_jpsi_to_gamma_pi0_pi0.py b/tests/channels/test_jpsi_to_gamma_pi0_pi0.py
index 72f3b329..8125d2c9 100644
--- a/tests/channels/test_jpsi_to_gamma_pi0_pi0.py
+++ b/tests/channels/test_jpsi_to_gamma_pi0_pi0.py
@@ -8,17 +8,17 @@ def describe_reaction_generation():
@pytest.mark.parametrize(
("allowed_intermediate_particles", "n_topologies", "number_of_solutions"),
[
- (["f_0(1500)0"], 1, 4),
- (["f_0(980)0", "f_0(1500)0"], 1, 8),
- (["f_2(1270)0"], 1, 12),
- (["omega(782)0"], 1, 8),
+ (["f(0)(1500)"], 1, 4),
+ (["f(0)(980)", "f(0)(1500)"], 1, 8),
+ (["f(2)(1270)"], 1, 12),
+ (["omega(782)"], 1, 8),
(
[
- "f_0(980)0",
- "f_2(1270)0",
- "f_0(1500)0",
- "f_2(1950)0",
- "omega(782)0",
+ "f(0)(980)",
+ "f(2)(1270)",
+ "f(0)(1500)",
+ "f(2)(1950)",
+ "omega(782)",
],
2,
40,
@@ -53,7 +53,7 @@ def it_id_to_particle_mappings(particle_database):
final_state=["gamma", "pi0", "pi0"],
particle_db=particle_database,
allowed_interaction_types="strong",
- allowed_intermediate_particles=["f_0(980)0"],
+ allowed_intermediate_particles=["f(0)(980)"],
formalism="helicity",
)
assert len(reaction.group_by_topology()) == 1
diff --git a/tests/channels/test_lc_to_p_km_pip.py b/tests/channels/test_lc_to_p_km_pip.py
index 14f46abb..a54ae66e 100644
--- a/tests/channels/test_lc_to_p_km_pip.py
+++ b/tests/channels/test_lc_to_p_km_pip.py
@@ -4,7 +4,7 @@
def test_resonances():
stm = StateTransitionManager(
- initial_state=["Lambda_c()+"],
+ initial_state=["Lambda(c)+"],
final_state=["p", "K-", "pi+"],
mass_conservation_factor=0.6,
max_angular_momentum=2,
@@ -20,18 +20,18 @@ def test_resonances():
"Delta(1600)++",
"Delta(1620)++",
"Delta(1700)++",
- "Kbar_0^*(700)0",
- "Kbar^*(892)0",
- "Kbar^*(1410)0",
- "Kbar_0^*(1430)0",
- "Lambda(1405)0",
- "Lambda(1520)0",
- "Lambda(1600)0",
- "Lambda(1670)0",
- "Lambda(1690)0",
- "Lambda(1810)0",
- "Lambda(1800)0",
- "Lambda(1890)0",
+ "K(0)*(700)~0",
+ "K*(892)~0",
+ "K*(1410)~0",
+ "K(0)*(1430)~0",
+ "Lambda(1405)",
+ "Lambda(1520)",
+ "Lambda(1600)",
+ "Lambda(1670)",
+ "Lambda(1690)",
+ "Lambda(1810)",
+ "Lambda(1800)",
+ "Lambda(1890)",
}
sorted_expected = sorted(expected)
assert sorted_resonances == sorted_expected
diff --git a/tests/channels/test_nbody_reactions.py b/tests/channels/test_nbody_reactions.py
index c07cd14b..e854bfe9 100644
--- a/tests/channels/test_nbody_reactions.py
+++ b/tests/channels/test_nbody_reactions.py
@@ -23,7 +23,7 @@ def reduce_violated_rules(
("test_input", "expected"),
[
(
- (["p", "pbar"], ["pi+", "pi0"]),
+ (["p", "p~"], ["pi+", "pi0"]),
{"ChargeConservation", "isospin_conservation"},
),
(
@@ -41,7 +41,7 @@ def reduce_violated_rules(
},
),
(
- (["Sigma0"], ["Lambda0", "pi0"]),
+ (["Sigma0"], ["Lambda", "pi0"]),
{"MassConservation"},
),
(
@@ -56,11 +56,11 @@ def reduce_violated_rules(
{},
),
(
- (["mu-"], ["e-", "nubar_e"]),
+ (["mu-"], ["e-", "nu(e)~"]),
{"MuonLNConservation", "spin_magnitude_conservation"},
),
(
- (["mu-"], ["e-", "nu_e"]),
+ (["mu-"], ["e-", "nu(e)"]),
{
"ElectronLNConservation",
"MuonLNConservation",
@@ -72,15 +72,15 @@ def reduce_violated_rules(
{},
),
(
- (["nubar_e", "p"], ["n", "e+"]),
+ (["nu(e)~", "p"], ["n", "e+"]),
{},
),
(
- (["e-", "p"], ["nu_e", "pi0"]),
+ (["e-", "p"], ["nu(e)", "pi0"]),
{"BaryonNumberConservation", "spin_magnitude_conservation"},
),
(
- (["f_0(980)0"], ["pi+", "pi-"]),
+ (["f(0)(980)"], ["pi+", "pi-"]),
{},
),
(
@@ -100,7 +100,7 @@ def reduce_violated_rules(
{},
),
(
- (["J/psi(1S)"], ["pi0", "f_0(980)0"]),
+ (["J/psi(1S)"], ["pi0", "f(0)(980)"]),
{
"isospin_conservation",
"c_parity_conservation",
@@ -112,7 +112,7 @@ def reduce_violated_rules(
{},
),
(
- (["p", "p"], ["Sigma+", "n", "Kbar0", "pi+", "pi0"]),
+ (["p", "p"], ["Sigma+", "n", "K~0", "pi+", "pi0"]),
{"StrangenessConservation", "isospin_conservation"},
),
(
@@ -120,11 +120,11 @@ def reduce_violated_rules(
{"ElectronLNConservation", "BaryonNumberConservation"},
),
(
- (["p", "p"], ["p", "p", "p", "pbar"]),
+ (["p", "p"], ["p", "p", "p", "p~"]),
{},
),
(
- (["n", "nbar"], ["pi+", "pi-", "pi0"]),
+ (["n", "n~"], ["pi+", "pi-", "pi0"]),
{},
),
(
@@ -143,9 +143,9 @@ def reduce_violated_rules(
(["Sigma+", "n"], ["Sigma-", "p"]),
{"ChargeConservation", "isospin_conservation"},
),
- ((["Sigma0"], ["Lambda0", "gamma"]), []),
+ ((["Sigma0"], ["Lambda", "gamma"]), []),
(
- (["Xi-"], ["Lambda0", "pi-"]),
+ (["Xi-"], ["Lambda", "pi-"]),
{"StrangenessConservation", "isospin_conservation"},
),
(
@@ -153,11 +153,11 @@ def reduce_violated_rules(
{"StrangenessConservation", "isospin_conservation"},
),
(
- (["pi-", "p"], ["Lambda0", "K0"]),
+ (["pi-", "p"], ["Lambda", "K0"]),
{},
),
(
- (["Sigma-"], ["n", "e-", "nubar_e"]),
+ (["Sigma-"], ["n", "e-", "nu(e)~"]),
{"StrangenessConservation"},
),
(
@@ -185,7 +185,7 @@ def reduce_violated_rules(
{"c_parity_conservation", "g_parity_conservation"},
),
(
- (["Kbar0"], ["pi+", "pi-", "pi0"]),
+ (["K~0"], ["pi+", "pi-", "pi0"]),
{"isospin_conservation", "StrangenessConservation"},
),
],
diff --git a/tests/channels/test_psi2s_to_eta_k_kstar.py b/tests/channels/test_psi2s_to_eta_k_kstar.py
index 8c6029d8..fcc1133c 100644
--- a/tests/channels/test_psi2s_to_eta_k_kstar.py
+++ b/tests/channels/test_psi2s_to_eta_k_kstar.py
@@ -11,15 +11,15 @@
@pytest.mark.parametrize(
"resonances",
[
- ["h_1(1415)0"],
- ["omega(1650)0"],
- ["h_1(1415)0", "omega(1650)0"],
+ ["h(1)(1415)"],
+ ["omega(1650)"],
+ ["h(1)(1415)", "omega(1650)"],
],
)
def test_resonances(formalism: SpinFormalism, resonances, modified_pdg):
reaction = qrules.generate_transitions(
initial_state=("psi(2S)", [+1, -1]),
- final_state=["eta", "K-", "K^*(892)+"],
+ final_state=["eta", "K-", "K*(892)+"],
allowed_intermediate_particles=resonances,
allowed_interaction_types=["em"],
formalism=formalism,
@@ -33,7 +33,7 @@ def modified_pdg(particle_database) -> ParticleCollection:
# https://github.com/scikit-hep/particle/issues/486
# https://github.com/ComPWA/qrules/issues/165#issuecomment-1497343548
particles = ParticleCollection(particle_database)
- original_h1415 = particles["h_1(1415)0"]
+ original_h1415 = particles["h(1)(1415)"]
new_h1415 = qrules.particle.create_particle(
original_h1415,
isospin=qrules.particle.Spin(0, 0),
diff --git a/tests/channels/test_y_to_d0_d0bar_pi0_pi0.py b/tests/channels/test_y_to_d0_d0bar_pi0_pi0.py
index 6e48b62b..8b0049d2 100644
--- a/tests/channels/test_y_to_d0_d0bar_pi0_pi0.py
+++ b/tests/channels/test_y_to_d0_d0bar_pi0_pi0.py
@@ -15,7 +15,7 @@ def describe_reaction_generation():
def it_simple(formalism, n_solutions, particle_database):
reaction = qrules.generate_transitions(
initial_state=[("Y(4260)", [-1, +1])],
- final_state=["D^*(2007)0", "Dbar^*(2007)0"],
+ final_state=["D*(2007)0", "D*(2007)~0"],
particle_db=particle_database,
formalism=formalism,
allowed_interaction_types="strong",
@@ -34,13 +34,13 @@ def it_simple(formalism, n_solutions, particle_database):
def it_full(formalism, n_solutions, particle_database):
stm = StateTransitionManager(
initial_state=[("Y(4260)", [-1, +1])],
- final_state=["D0", "Dbar0", "pi0", "pi0"],
+ final_state=["D0", "D~0", "pi0", "pi0"],
particle_db=particle_database,
- allowed_intermediate_particles=["D^*", "Dbar^*"],
+ allowed_intermediate_particles=["D*"],
formalism=formalism,
)
stm.set_allowed_interaction_types([InteractionType.STRONG])
- stm.add_final_state_grouping([["D0", "pi0"], ["Dbar0", "pi0"]])
+ stm.add_final_state_grouping([["D0", "pi0"], ["D~0", "pi0"]])
problem_sets = stm.create_problem_sets()
reaction = stm.find_solutions(problem_sets)
assert len(reaction.group_by_topology()) == 1
@@ -50,17 +50,17 @@ def it_resonance_filter(particle_database):
# https://github.com/ComPWA/qrules/issues/33
stm = StateTransitionManager(
initial_state=[("Y(4260)", [-1, +1])],
- final_state=["D0", "Dbar0", "pi0", "pi0"],
+ final_state=["D0", "D~0", "pi0", "pi0"],
allowed_intermediate_particles=["D"],
particle_db=particle_database,
)
stm.set_allowed_interaction_types([InteractionType.STRONG])
- stm.add_final_state_grouping([["D0", "pi0"], ["Dbar0", "pi0"]])
+ stm.add_final_state_grouping([["D0", "pi0"], ["D~0", "pi0"]])
problem_sets = stm.create_problem_sets()
result = stm.find_solutions(problem_sets)
assert set(result.get_intermediate_particles().names) == {
- "D^*(2007)0",
- "Dbar^*(2007)0",
- "D_0^*(2300)0",
- "Dbar_0^*(2300)0",
+ "D*(2007)0",
+ "D*(2007)~0",
+ "D(0)*(2300)0",
+ "D(0)*(2300)~0",
}
diff --git a/tests/conftest.py b/tests/conftest.py
index a31d1bc7..897c5232 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,3 +1,5 @@
+from importlib.metadata import version
+
import pytest
from qrules import load_default_particles
@@ -17,3 +19,12 @@ def particle_database() -> ParticleCollection:
@pytest.fixture(scope="session")
def output_dir(pytestconfig) -> str:
return f"{pytestconfig.rootpath}/tests/output/"
+
+
+@pytest.fixture(scope="session")
+def skh_particle_version() -> str:
+ major, minor, *_ = (int(i) for i in version("particle").split("."))
+ particle_version = f"{major}.{minor}"
+ if (major, minor) < (0, 11):
+ pytest.skip(f"Version {particle_version} is not supported in the tests")
+ return particle_version
diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
index 4f5fd5a9..a0d414e2 100644
--- a/tests/unit/conftest.py
+++ b/tests/unit/conftest.py
@@ -20,7 +20,7 @@ def reaction(request: SubRequest) -> ReactionInfo:
return qrules.generate_transitions(
initial_state=[("J/psi(1S)", [-1, 1])],
final_state=["gamma", "pi0", "pi0"],
- allowed_intermediate_particles=["f_0(980)0", "f_0(1500)0"],
+ allowed_intermediate_particles=["f(0)(980)", "f(0)(1500)"],
allowed_interaction_types="strong",
formalism=formalism,
)
diff --git a/tests/unit/io/conftest.py b/tests/unit/io/conftest.py
index 3c045d1d..647101fd 100644
--- a/tests/unit/io/conftest.py
+++ b/tests/unit/io/conftest.py
@@ -20,8 +20,8 @@ def particle_selection(particle_database: ParticleCollection):
def stm() -> StateTransitionManager:
stm = StateTransitionManager(
initial_state=[("J/psi(1S)", [+1])],
- final_state=["K0", ("Sigma+", [+0.5]), ("pbar", [+0.5])],
- allowed_intermediate_particles=["Sigma(1750)", "Sigmabar(1750)"],
+ final_state=["K0", ("Sigma+", [+0.5]), ("p~", [+0.5])],
+ allowed_intermediate_particles=["Sigma(1750)"],
formalism="canonical-helicity",
)
stm.set_allowed_interaction_types([InteractionType.STRONG, InteractionType.EM])
@@ -40,6 +40,4 @@ def qn_problem_and_result(
) -> tuple[QNProblemSet, QNResult]:
qn_solutions = stm.find_quantum_number_transitions(problem_sets)
strong_qn_solutions = qn_solutions[3600.0]
- return next(
- (problem, result) for problem, result in strong_qn_solutions if result.solutions
- )
+ return strong_qn_solutions[1]
diff --git a/tests/unit/io/test_dict.py b/tests/unit/io/test_dict.py
index fb36c268..f44d8b55 100644
--- a/tests/unit/io/test_dict.py
+++ b/tests/unit/io/test_dict.py
@@ -29,10 +29,16 @@ def it_not_implemented_errors(
def it_serialization(
output_dir: str,
particle_selection: ParticleCollection,
+ skh_particle_version: str,
):
io.write(particle_selection, output_dir + "particle_selection.yml")
n_particles = len(particle_selection)
- assert n_particles > 0
+ if skh_particle_version < "0.16":
+ assert n_particles == 181
+ elif skh_particle_version < "0.25":
+ assert n_particles == 193
+ else:
+ assert n_particles == 194
asdict = io.asdict(particle_selection)
imported_collection = io.fromdict(asdict)
assert isinstance(imported_collection, ParticleCollection)
diff --git a/tests/unit/io/test_dot.py b/tests/unit/io/test_dot.py
index cd7dc8b8..0531b1f3 100644
--- a/tests/unit/io/test_dot.py
+++ b/tests/unit/io/test_dot.py
@@ -41,7 +41,7 @@ def it_exact_format(reaction: ReactionInfo):
{ rank=same; A }
{ rank=same; 0 1 2 }
A -> N0
- N0 -> N1 [label="f_0(980)0[0]"]
+ N0 -> N1 [label="f(0)(980)[0]"]
N0 -> 0
N1 -> 1
N1 -> 2
@@ -63,7 +63,7 @@ def it_exact_format(reaction: ReactionInfo):
{ rank=same; A }
{ rank=same; 0 1 2 }
A -> N0
- N0 -> N1 [label="f_0(980)0[0]"]
+ N0 -> N1 [label="f(0)(980)[0]"]
N0 -> 0
N1 -> 1
N1 -> 2
@@ -98,7 +98,7 @@ def it_with_styled_edges_and_nodes(reaction: ReactionInfo, output_dir):
with open(output_dir + f"styled_{reaction.formalism}.gv", "w") as stream:
stream.write(src)
assert '0 [fontcolor="blue", label="0: gamma[-1]"]' in src
- assert 'N0 -> N1 [fontcolor="blue", label="f_0(980)0[0]"]' in src
+ assert 'N0 -> N1 [fontcolor="blue", label="f(0)(980)[0]"]' in src
assert 'N0 [fontcolor="darkgreen", shape="ellipse", label=""]' in src
def it_no_label_overwriting(reaction: ReactionInfo):
diff --git a/tests/unit/io/test_labels.py b/tests/unit/io/test_labels.py
index b26c2d55..97408197 100644
--- a/tests/unit/io/test_labels.py
+++ b/tests/unit/io/test_labels.py
@@ -114,21 +114,21 @@ def it_dict_and_basic_values():
def it_collapsed_particle_tuple(particle_database: ParticleCollection):
particles = (
- particle_database["f_0(980)0"],
- particle_database["f_0(1500)0"],
+ particle_database["f(0)(980)"],
+ particle_database["f(0)(1500)"],
)
assert as_latex(particles) == (
R"\begin{gathered} f_{0}(980) \\ f_{0}(1500) \end{gathered}"
)
def it_keeps_six_particles_in_one_column(particle_database: ParticleCollection):
- particle = particle_database["f_0(980)0"]
+ particle = particle_database["f(0)(980)"]
assert as_latex((particle,) * 6).startswith(R"\begin{gathered}")
def it_uses_columns_for_a_long_particle_tuple(
particle_database: ParticleCollection,
):
- particle = particle_database["f_0(980)0"]
+ particle = particle_database["f(0)(980)"]
particles = tuple(
attrs.evolve(particle, name=f"x{i}", latex=Rf"x_{{{i}}}") for i in range(7)
)
@@ -140,7 +140,7 @@ def it_uses_columns_for_a_long_particle_tuple(
def it_adds_columns_to_a_longer_particle_tuple(
particle_database: ParticleCollection,
):
- particle = particle_database["f_0(980)0"]
+ particle = particle_database["f(0)(980)"]
particles = tuple(
attrs.evolve(particle, name=f"x{i}", latex=Rf"x_{{{i}}}") for i in range(13)
)
@@ -335,7 +335,7 @@ def test_collapse_graphs(
assert len(collapsed_graphs) == 1
graph = next(iter(collapsed_graphs))
edge_id = next(iter(graph.topology.intermediate_edge_ids))
- f_resonances = pdg.filter(lambda p: p.name in {"f_0(980)0", "f_0(1500)0"})
+ f_resonances = pdg.filter(lambda p: p.name in {"f(0)(980)", "f(0)(1500)"})
intermediate_states = graph.states[edge_id]
assert isinstance(intermediate_states, tuple)
assert all(isinstance(i, Particle) for i in intermediate_states)
@@ -348,18 +348,19 @@ def test_get_particle_graphs(
pdg = particle_database
graphs = get_particle_graphs(reaction.transitions)
assert len(graphs) == 2
- assert graphs[0].states[3] == pdg["f_0(980)0"]
- assert graphs[1].states[3] == pdg["f_0(1500)0"]
+ assert graphs[0].states[3] == pdg["f(0)(980)"]
+ assert graphs[1].states[3] == pdg["f(0)(1500)"]
assert len(graphs[0].topology.edges) == 5
for i in range(-1, 3):
assert graphs[0].states[i] is graphs[1].states[i]
-def test_strip_projections():
- resonance = "Sigmabar(1670)-"
+def test_strip_projections(skh_particle_version: str):
+ assert skh_particle_version is not None # skips test if particle version too low
+ resonance = "Sigma(1670)~-"
reaction = qrules.generate_transitions(
initial_state=[("J/psi(1S)", [+1])],
- final_state=["K0", ("Sigma+", [+0.5]), ("pbar", [+0.5])],
+ final_state=["K0", ("Sigma+", [+0.5]), ("p~", [+0.5])],
allowed_intermediate_particles=[resonance],
allowed_interaction_types="strong",
)
diff --git a/tests/unit/io/test_mermaid.py b/tests/unit/io/test_mermaid.py
index dca2faae..8af31221 100644
--- a/tests/unit/io/test_mermaid.py
+++ b/tests/unit/io/test_mermaid.py
@@ -122,9 +122,9 @@ def it_font_size_formatting(font_size: object, expected: str):
def it_edge_labels_with_state_brackets_are_quoted():
edge_line = MermaidPrinter(latex=False)._create_mermaid_edge(
- "A", "B", "f_2(2340)0[-2]"
+ "A", "B", "f(2)(2340)[-2]"
)
- assert edge_line == ' A ---|"f_2(2340)0[-2]"| B'
+ assert edge_line == ' A ---|"f(2)(2340)[-2]"| B'
def it_edge_labels_with_ket_vectors_are_quoted():
edge_line = MermaidPrinter(latex=False)._create_mermaid_edge("A", "B", "|1,-1⟩")
@@ -222,7 +222,7 @@ def it_reaction_with_node_labels(reaction: ReactionInfo):
src = io.asmermaid(reaction.transitions[0], render_node=True, latex=False)
assert src.startswith("flowchart LR\n")
assert "gamma[-1]" in src
- assert "f_0(980)0[0]" in src
+ assert "f(0)(980)[0]" in src
assert "P=+1" in src
assert " A --- N0" in src
diff --git a/tests/unit/test_final_state_permutations.py b/tests/unit/test_final_state_permutations.py
index c62a5082..8dedb332 100644
--- a/tests/unit/test_final_state_permutations.py
+++ b/tests/unit/test_final_state_permutations.py
@@ -10,7 +10,7 @@
@pytest.mark.parametrize(
"final_state_description",
- sorted({" ".join(p) for p in itertools.permutations(["pbar", "Sigma+", "K0"])}),
+ sorted({" ".join(p) for p in itertools.permutations(["p~", "Sigma+", "K0"])}),
)
def test_create_problem_sets(final_state_description: str):
input_final_state = final_state_description.split(" ")
@@ -38,7 +38,7 @@ def test_generate_transitions(final_state_description: str):
reaction = qrules.generate_transitions(
initial_state=("J/psi(1S)", [-1, +1]),
final_state=final_state,
- allowed_intermediate_particles=["omega(782)0"],
+ allowed_intermediate_particles=["omega(782)"],
allowed_interaction_types=["strong", "EM"],
)
ordered_final_state = [
diff --git a/tests/unit/test_parity_prefactor.py b/tests/unit/test_parity_prefactor.py
index f76882f6..90b9aa48 100644
--- a/tests/unit/test_parity_prefactor.py
+++ b/tests/unit/test_parity_prefactor.py
@@ -26,7 +26,7 @@ class Input(NamedTuple):
Input(
[("J/psi(1S)", [1])],
[("gamma", [-1, 1]), ("pi0", [0]), ("pi0", [0])],
- ["f_0(980)0"],
+ ["f(0)(980)"],
["pi0", "pi0"],
),
"J/psi(1S)",
diff --git a/tests/unit/test_particle.py b/tests/unit/test_particle.py
index 9e4733c9..062635d4 100644
--- a/tests/unit/test_particle.py
+++ b/tests/unit/test_particle.py
@@ -3,6 +3,7 @@
import logging
from copy import deepcopy
from fractions import Fraction
+from importlib.metadata import version
import pytest
from attrs.exceptions import FrozenInstanceError
@@ -43,8 +44,8 @@ def it_repr(particle_database: ParticleCollection, repr_method):
("p", False),
("e+", True),
("e-", True),
- ("nu_e", True),
- ("nubar_tau", True),
+ ("nu(e)", True),
+ ("nu(tau)~", True),
("tau+", True),
],
)
@@ -113,23 +114,23 @@ def it_eq():
("name1", "name2"),
[
# by name
- ("pi0", "a_0(980)-"),
+ ("pi0", "a(0)(980)-"),
# by mass
("pi+", "pi-"),
("pi-", "pi0"),
("pi+", "pi0"),
("K0", "K+"),
# by charge
- ("a_0(980)+", "a_0(980)-"),
- ("a_0(980)+", "a_0(980)0"),
- ("a_0(980)0", "a_0(980)-"),
+ ("a(0)(980)+", "a(0)(980)-"),
+ ("a(0)(980)+", "a(0)(980)0"),
+ ("a(0)(980)0", "a(0)(980)-"),
],
)
def it_gt(name1, name2, particle_database: ParticleCollection):
pdg = particle_database
assert pdg[name1] > pdg[name2]
- def it_neg(particle_database: ParticleCollection):
+ def it_neg(particle_database: ParticleCollection, skh_particle_version: str):
pip = particle_database.find(211)
pim = particle_database.find(-211)
assert pip == -pim
@@ -137,24 +138,26 @@ def it_neg(particle_database: ParticleCollection):
pdg = particle_database
f0_mesons = sorted(
particle.name
- for particle in sorted(pdg.filter(lambda p: p.name.startswith("f_0")))
+ for particle in sorted(pdg.filter(lambda p: p.name.startswith("f(0)")))
)
expected = {
- "f_0(500)0",
- "f_0(980)0",
- "f_0(1370)0",
- "f_0(1500)0",
- "f_0(1710)0",
- "f_0(2020)0",
- "f_0(2100)0",
- "f_0(2200)0",
+ "f(0)(500)",
+ "f(0)(980)",
+ "f(0)(1370)",
+ "f(0)(1500)",
+ "f(0)(1710)",
}
+ if skh_particle_version > "0.22":
+ expected.add("f(0)(2020)")
sorted_expected = sorted(expected)
assert f0_mesons == sorted_expected
def _get_omega_mesons() -> list[str]:
- return ["omega(782)0", "omega(1420)0", "omega_3(1670)0", "omega(1650)0"]
+ scikit_hep_particle_version = ".".join(version("particle").split(".")[:2])
+ if scikit_hep_particle_version in {"0.21", "0.22"}:
+ return ["omega(782)", "omega(3)(1670)", "omega(1650)"]
+ return ["omega(782)", "omega(1420)", "omega(3)(1670)", "omega(1650)"]
def describe_ParticleCollection():
@@ -232,13 +235,19 @@ def it_discard(particle_database: ParticleCollection):
with pytest.raises(NotImplementedError):
pions.discard(111) # ty: ignore[invalid-argument-type]
- def it_filter(particle_database: ParticleCollection):
- search_result = particle_database.filter(lambda p: "f_0" in p.name)
- assert len(search_result) == 8
- f0_1500_from_subset = search_result["f_0(1500)0"]
- assert f0_1500_from_subset.mass == pytest.approx(1.522)
- assert f0_1500_from_subset is particle_database["f_0(1500)0"]
- assert f0_1500_from_subset is not particle_database["f_0(980)0"]
+ def it_filter(particle_database: ParticleCollection, skh_particle_version: str):
+ search_result = particle_database.filter(lambda p: "f(0)" in p.name)
+ if skh_particle_version < "0.23":
+ assert len(search_result) == 5
+ else:
+ assert len(search_result) == 6
+ f0_1500_from_subset = search_result["f(0)(1500)"]
+ if skh_particle_version < "0.23":
+ assert f0_1500_from_subset.mass == 1.506
+ else:
+ assert f0_1500_from_subset.mass == 1.522
+ assert f0_1500_from_subset is particle_database["f(0)(1500)"]
+ assert f0_1500_from_subset is not particle_database["f(0)(980)"]
search_result = particle_database.filter(lambda p: p.pid == 22)
gamma_from_subset = search_result["gamma"]
@@ -252,21 +261,25 @@ def it_filter(particle_database: ParticleCollection):
)
sorted_result = sorted(filtered_result.names)
expected = {
- "K_2(1820)+",
- "K_2(1820)0",
- "K_2^*(1980)+",
- "K_2^*(1980)0",
+ "K(2)(1820)+",
+ "K(2)(1820)0",
}
+ if skh_particle_version > "0.15":
+ additional_particles = {
+ "K(2)*(1980)+",
+ "K(2)*(1980)0",
+ }
+ expected.update(additional_particles)
sorted_expected = sorted(expected)
assert sorted_result == sorted_expected
def it_find(particle_database: ParticleCollection):
f2_1950 = particle_database.find(9050225)
- assert f2_1950.name == "f_2(1950)0"
- assert f2_1950.mass == pytest.approx(1.9361938561872178)
- phi = particle_database.find("phi(1020)0")
+ assert f2_1950.name == "f(2)(1950)"
+ assert f2_1950.mass == 1.936
+ phi = particle_database.find("phi(1020)")
assert phi.pid == 333
- assert phi.width == pytest.approx(0.004249, abs=1e-6)
+ assert pytest.approx(phi.width) == 0.004249
@pytest.mark.parametrize(
("search_term", "expected"),
@@ -275,9 +288,11 @@ def it_find(particle_database: ParticleCollection):
("non-existing", None),
# cspell:disable
("gamm", "gamma"),
- ("gama", ["gamma", "Sigma0", "Sigma-", "Sigma+"]),
+ ("gama", ["gamma", "Sigma0", "Sigma-", "Sigma+", "Lambda"]),
("omega", _get_omega_mesons()),
- ("lambda", ["Lambda0", "Lambdabar0"]),
+ ("p~~", "p~"),
+ ("~", ["p~", "n~"]),
+ ("lambda", ["Lambda", "Lambda~", "Lambda(c)+", "Lambda(b)0"]),
# cspell:enable
],
)
@@ -394,22 +409,51 @@ def it_creates_a_named_antiparticle(
assert anti_particle == comparison_particle
- def it_by_pid(particle_database: ParticleCollection):
+ def it_tilde(particle_database: ParticleCollection, skh_particle_version: str):
+ anti_particles = particle_database.filter(lambda p: "~" in p.name)
+ if skh_particle_version < "0.14":
+ assert len(anti_particles) == 165
+ elif skh_particle_version < "0.16":
+ assert len(anti_particles) == 172
+ elif skh_particle_version < "0.25":
+ assert len(anti_particles) == 175
+ else:
+ assert len(anti_particles) == 176
+ for anti_particle in anti_particles:
+ particle_name = anti_particle.name.replace("~", "")
+ if "+" in particle_name:
+ particle_name = particle_name.replace("+", "-")
+ elif "-" in particle_name:
+ particle_name = particle_name.replace("-", "+")
+ created_particle = create_antiparticle(anti_particle, particle_name)
+ assert created_particle == particle_database[particle_name]
+
+ def it_by_pid(particle_database: ParticleCollection, skh_particle_version: str):
+ n_particles_with_neg_pid = 0
for particle in particle_database:
anti_particles_by_pid = particle_database.filter(
lambda p: p.pid == -particle.pid # ruff: ignore[function-uses-loop-variable]
)
if len(anti_particles_by_pid) != 1:
continue
+ n_particles_with_neg_pid += 1
anti_particle = next(iter(anti_particles_by_pid))
particle_from_anti = -anti_particle
assert particle == particle_from_anti
+ if skh_particle_version < "0.14":
+ assert n_particles_with_neg_pid == 428
+ elif skh_particle_version < "0.16":
+ assert n_particles_with_neg_pid == 442
+ elif skh_particle_version < "0.25":
+ assert n_particles_with_neg_pid == 454
+ else:
+ assert n_particles_with_neg_pid == 456
def describe_create_particle():
@pytest.mark.parametrize(
"particle_name",
- ["p", "phi(1020)0", "W-", "gamma"],
+ ["p", "phi(1020)", "W-", "gamma"],
)
def it_create_particle(particle_database: ParticleCollection, particle_name: str):
template_particle = particle_database[particle_name]
diff --git a/tests/unit/test_pdg.py b/tests/unit/test_pdg.py
index 6b38eda7..a89d3597 100644
--- a/tests/unit/test_pdg.py
+++ b/tests/unit/test_pdg.py
@@ -1,3 +1,4 @@
+import particle
import pytest
from qrules.particle import ParticleCollection, load_pdg
@@ -8,29 +9,54 @@ def pdg() -> ParticleCollection:
return load_pdg()
-def test_contains_representative_particles(pdg: ParticleCollection):
- expected_names = {
- "J/psi(1S)",
- "f_0(500)0",
- "f_0(980)0",
- "gamma",
- "nu_e",
- "pbar",
- "pi+",
+def test_maybe_qq():
+ expected_maybe_qq = {
+ "a(0)(980)+",
+ "a(0)(980)-",
+ "a(0)(980)0",
+ "f(0)(1500)",
+ "f(0)(500)",
+ "f(0)(980)",
+ "pi(1)(1400)+",
+ "pi(1)(1400)-",
+ "pi(1)(1400)0",
+ "pi(1)(1600)+",
+ "pi(1)(1600)-",
+ "pi(1)(1600)0",
+ }
+ maybe_qq_search_results = particle.Particle.findall(
+ lambda p: "qq" in p.quarks.lower()
+ )
+ assert expected_maybe_qq == {item.name for item in maybe_qq_search_results}
+
+
+def test_pdg_size(pdg: ParticleCollection):
+ assert len(pdg) in {
+ 512, # particle==0.13
+ 519, # particle==0.14-0.15
+ 531, # particle==0.16-0.20
+ 530, # particle==0.21-0.22
+ 537, # particle==0.23-0.24
+ 539, # particle==0.25
+ 540, # particle==0.25.4
+ 541, # particle==0.26.0
+ }
+ assert len(pdg.filter(lambda p: "~" in p.name)) in {
+ 165, # particle==0.13
+ 172, # particle==0.14, 0.15
+ 175, # particle==0.16-0.24
+ 176, # particle==0.25
}
- assert expected_names <= set(pdg.names)
-
-
-def test_has_unique_names_and_mcids(pdg: ParticleCollection):
- assert len(pdg.names) == len(set(pdg.names))
- assert len(pdg) == len({particle.pid for particle in pdg})
-def test_default_particle_definitions_extend_pdg(
+def test_missing_in_pdg(
pdg: ParticleCollection,
particle_database: ParticleCollection,
):
- pdg_names = set(pdg.names)
- default_names = set(particle_database.names)
- assert pdg_names <= default_names
- assert default_names - pdg_names == {"Y(4260)"}
+ particle_list_names = set(particle_database)
+ pdg_names = set(pdg)
+ in_common = particle_list_names & pdg_names
+ missing_in_pdg = particle_list_names ^ in_common
+ assert {p.name for p in missing_in_pdg} == {
+ "Y(4260)",
+ }
diff --git a/tests/unit/test_qrules.py b/tests/unit/test_qrules.py
index 45594f59..c314921d 100644
--- a/tests/unit/test_qrules.py
+++ b/tests/unit/test_qrules.py
@@ -6,17 +6,17 @@
@pytest.mark.parametrize(
"resonance_names",
[
- ["Sigmabar(1660)-"],
+ ["Sigma(1660)~-"],
["N(1650)+"],
- ["Kbar^*(1680)0"],
- ["Sigmabar(1660)-", "N(1650)+"],
- ["Sigmabar(1660)-", "Kbar^*(1680)0"],
- ["N(1650)+", "Kbar^*(1680)0"],
- ["Sigmabar(1660)-", "N(1650)+", "Kbar^*(1680)0"],
+ ["K*(1680)~0"],
+ ["Sigma(1660)~-", "N(1650)+"],
+ ["Sigma(1660)~-", "K*(1680)~0"],
+ ["N(1650)+", "K*(1680)~0"],
+ ["Sigma(1660)~-", "N(1650)+", "K*(1680)~0"],
],
)
def test_generate_transitions(resonance_names):
- final_state_names = ["K0", "Sigma+", "pbar"]
+ final_state_names = ["K0", "Sigma+", "p~"]
reaction = generate_transitions(
initial_state="J/psi(1S)",
final_state=final_state_names,
diff --git a/tests/unit/test_system_control.py b/tests/unit/test_system_control.py
index a9bfb305..5f8fcd04 100644
--- a/tests/unit/test_system_control.py
+++ b/tests/unit/test_system_control.py
@@ -2,6 +2,7 @@
from copy import deepcopy
from fractions import Fraction
+from importlib.metadata import version
import attrs
import pytest
@@ -37,32 +38,32 @@
[
(
[("Y(4260)", [-1])],
- [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
- [[["D0", "pi0"], ["Dbar0", "pi0"]]],
+ [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [[["D0", "pi0"], ["D~0", "pi0"]]],
1,
),
(
[("Y(4260)", [-1, 1])],
- [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
- [[["D0", "pi0"], ["Dbar0", "pi0"]]],
+ [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [[["D0", "pi0"], ["D~0", "pi0"]]],
2,
),
(
[("Y(4260)", [1])],
- [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
[],
9,
),
(
[("Y(4260)", [-1, 1])],
- [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
[],
18,
),
(
[("Y(4260)", [1])],
- [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
- [[["D0", "pi0"], ["Dbar0", "pi0"]], ["D0", "pi0"]],
+ [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [[["D0", "pi0"], ["D~0", "pi0"]], ["D0", "pi0"]],
3,
),
(
@@ -114,6 +115,26 @@ def test_external_edge_initialization(
assert len(next(iter(problem_sets.values()))) == result_graph_count
+def get_pi0_width() -> float:
+ if version("particle") < "0.16":
+ return 7.73e-09
+ return 7.81e-09
+
+
+def __get_d_pos() -> tuple[float, float]:
+ if version("particle") < "0.16":
+ return 1.86965, 6.33e-13
+ if version("particle") < "0.21":
+ return 1.86966, 6.33e-13
+ return 1.86966, 6.37e-13
+
+
+def __get_f2_1270_pos() -> tuple[float, float]:
+ if version("particle") < "0.23":
+ return 1.2755, 0.18669999999999998
+ return 1.2754, 0.1866
+
+
@pytest.mark.parametrize(
("particle_name", "spin_projection", "expected_properties"),
[
@@ -122,8 +143,8 @@ def test_external_edge_initialization(
0,
{
EdgeQuantumNumbers.pid: 111,
- EdgeQuantumNumbers.mass: 0.1349768277676847,
- EdgeQuantumNumbers.width: 7.811987971364424e-09,
+ EdgeQuantumNumbers.mass: 0.1349768,
+ EdgeQuantumNumbers.width: get_pi0_width(),
EdgeQuantumNumbers.spin_magnitude: 0.0,
EdgeQuantumNumbers.spin_projection: 0,
EdgeQuantumNumbers.charge: 0,
@@ -147,8 +168,8 @@ def test_external_edge_initialization(
0,
{
EdgeQuantumNumbers.pid: 411,
- EdgeQuantumNumbers.mass: 1.869664743570898,
- EdgeQuantumNumbers.width: 6.370029016102887e-13,
+ EdgeQuantumNumbers.mass: __get_d_pos()[0],
+ EdgeQuantumNumbers.width: __get_d_pos()[1],
EdgeQuantumNumbers.spin_magnitude: 0.0,
EdgeQuantumNumbers.spin_projection: 0,
EdgeQuantumNumbers.charge: 1,
@@ -168,12 +189,12 @@ def test_external_edge_initialization(
},
),
(
- "f_2(1270)0", # spin projection 1
+ "f(2)(1270)", # spin projection 1
1.0,
{
EdgeQuantumNumbers.pid: 225,
- EdgeQuantumNumbers.mass: 1.2754120499190051,
- EdgeQuantumNumbers.width: 0.18655435663732642,
+ EdgeQuantumNumbers.mass: __get_f2_1270_pos()[0],
+ EdgeQuantumNumbers.width: __get_f2_1270_pos()[1],
EdgeQuantumNumbers.spin_magnitude: 2.0,
EdgeQuantumNumbers.spin_projection: 1.0,
EdgeQuantumNumbers.charge: 0,
@@ -199,9 +220,11 @@ def test_create_edge_properties(
spin_projection,
expected_properties,
particle_database,
+ skh_particle_version: str,
):
particle = particle_database[particle_name]
assert create_edge_properties(particle, spin_projection) == expected_properties
+ assert skh_particle_version is not None # dummy for skip tests
def make_ls_test_graph(
@@ -346,7 +369,7 @@ def _create_graph(
[
(
[("Y(4260)", [-1])],
- [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
),
],
)
@@ -387,7 +410,7 @@ def test_edge_swap(particle_database, initial_state, final_state):
[
(
[("Y(4260)", [-1])],
- [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
),
(
[("J/psi(1S)", [-1, 1])],
@@ -440,13 +463,13 @@ def test_match_external_edges(particle_database, initial_state, final_state):
[
(
[("Y(4260)", [1])],
- [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
- [[["D0", "pi0"], ["Dbar0", "pi0"]]],
+ [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [[["D0", "pi0"], ["D~0", "pi0"]]],
2,
),
(
[("Y(4260)", [1])],
- [("D0", [0]), ("Dbar0", [0]), ("pi0", [0]), ("pi0", [0])],
+ [("D0", [0]), ("D~0", [0]), ("pi0", [0]), ("pi0", [0])],
[["D0", "pi0"]],
6,
),
diff --git a/tests/unit/test_transition.py b/tests/unit/test_transition.py
index 00f19bcb..40789773 100644
--- a/tests/unit/test_transition.py
+++ b/tests/unit/test_transition.py
@@ -27,7 +27,7 @@ def describe_MutableTransition():
def it_intermediate_states():
stm = StateTransitionManager(
initial_state=[("J/psi(1S)", [-1, +1])],
- final_state=["K0", "Sigma+", "pbar"],
+ final_state=["K0", "Sigma+", "p~"],
allowed_intermediate_particles=["N(1700)", "Sigma(1750)"],
formalism="helicity",
mass_conservation_factor=0,
@@ -109,7 +109,7 @@ def describe_StateTransitionManager():
def it_allowed_intermediate_particles():
stm = StateTransitionManager(
initial_state=[("J/psi(1S)", list(map(Fraction, [-1, +1])))],
- final_state=["p", "pbar", "eta"],
+ final_state=["p", "p~", "eta"],
)
particle_name = "N(753)"
with pytest.raises(
@@ -122,7 +122,7 @@ def it_allowed_intermediate_particles():
("initial_state", "expected_strengths"),
[
(["gamma"], [0.0001, 1.0, 60.0]),
- (["nu_e"], [1e-08, 0.0001, 0.006]),
+ (["nu(e)"], [1e-08, 0.0001, 0.006]),
],
)
def it_initial_state_restricts_interaction_types(
@@ -147,7 +147,7 @@ def count_transitions(solving_mode: SolvingMode) -> int:
def it_regex_pattern():
stm = StateTransitionManager(
- initial_state=["Lambda_c()+"],
+ initial_state=["Lambda(c)+"],
final_state=["p", "K-", "pi+"],
allowed_intermediate_particles=["Delta"],
)
diff --git a/uv.lock b/uv.lock
index 8a126271..3576e438 100644
--- a/uv.lock
+++ b/uv.lock
@@ -972,6 +972,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
+[[package]]
+name = "hepunits"
+version = "2.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/1f/c5f7525bf7e5d14d61750f91845f9f9350a8f33f5ac95672cec92f317038/hepunits-2.4.6.tar.gz", hash = "sha256:bca6ada937147166d66e9fa152566f2378868d798211cad6990080399560fa34", size = 17985, upload-time = "2026-06-16T09:23:36.685Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/10/7f9c58d1ec6a0b7f7783fe552f3593f39cda30c2e1d7a9d148ae711e748d/hepunits-2.4.6-py3-none-any.whl", hash = "sha256:089c52c3b84ef67a159b5e9ee9bdd50e1a442e3fd0c101303cc409c1e9011c4d", size = 17090, upload-time = "2026-06-16T09:23:35.35Z" },
+]
+
[[package]]
name = "httpcore"
version = "1.0.9"
@@ -1923,6 +1932,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" },
]
+[[package]]
+name = "particle"
+version = "1.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "hepunits" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c0/cf/c22d976f32b899e81da7e4e2a47b7d1c1a8f5a6ab38bc6e01d3f2b074714/particle-1.0.1.tar.gz", hash = "sha256:3f2ec4dbb8953c90ba83b891d6534cd82dc1aece4111b79998ff284ce377a922", size = 285779, upload-time = "2026-09-10T14:59:26.818Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/3d/c3a313039860766c56ec9d5d5e7d02e305c54d49960b4db69e3260253974/particle-1.0.1-py3-none-any.whl", hash = "sha256:9c63b270bd4d1e1ab0f7d7120d548b34ffd12ab7fcfed92dcd646ac4bfd5711c", size = 245606, upload-time = "2026-09-10T14:59:25.116Z" },
+]
+
[[package]]
name = "pathspec"
version = "1.1.1"
@@ -2574,6 +2597,7 @@ dependencies = [
{ name = "attrs" },
{ name = "frozendict" },
{ name = "jsonschema" },
+ { name = "particle" },
{ name = "pdg" },
{ name = "python-constraint2", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "python-constraint2", version = "2.7.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
@@ -2699,6 +2723,7 @@ requires-dist = [
{ name = "frozendict" },
{ name = "graphviz", marker = "extra == 'viz'" },
{ name = "jsonschema" },
+ { name = "particle" },
{ name = "pdg", specifier = ">=2026.0,<2027" },
{ name = "python-constraint2" },
{ name = "pyyaml" },
From 60f03f27352bc1786460d1657a26ae66de41aade Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Fri, 11 Sep 2026 12:33:24 +0200
Subject: [PATCH 13/27] REMOVE: delete obsolete ignoreWords from cspell
configuration
---
.cspell.json | 5 -----
1 file changed, 5 deletions(-)
diff --git a/.cspell.json b/.cspell.json
index 034794db..7a901fc6 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -38,9 +38,6 @@
"pyproject.toml"
],
"ignoreWords": [
- "Dbar",
- "Kbar",
- "Xibar",
"adrs",
"ampform",
"arange",
@@ -97,7 +94,6 @@
"mimetype",
"modindex",
"mystnb",
- "nbar",
"nbconvert",
"nbformat",
"nbody",
@@ -108,7 +104,6 @@
"noreply",
"nrows",
"nsimplify",
- "nubar",
"pandoc",
"pbar",
"permalinks",
From c6b3d011263255a03cc5405a0209219c61e1589e Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Fri, 11 Sep 2026 12:45:07 +0200
Subject: [PATCH 14/27] ADD: include "Xibar" and "nubar" in ignoreWords list
---
.cspell.json | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.cspell.json b/.cspell.json
index 7a901fc6..587c9e4d 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -38,6 +38,7 @@
"pyproject.toml"
],
"ignoreWords": [
+ "Xibar",
"adrs",
"ampform",
"arange",
@@ -104,6 +105,7 @@
"noreply",
"nrows",
"nsimplify",
+ "nubar",
"pandoc",
"pbar",
"permalinks",
From 3dabd345e247625c870f7e5b52c95ec818134fb9 Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Fri, 11 Sep 2026 12:46:13 +0200
Subject: [PATCH 15/27] FIX: Revert particle package to standard load_pdg
package and add flag for import from official pdg package
---
src/qrules/particle.py | 196 +++++++++++++++++++++++++++++++--
tests/unit/test_pdg_adapter.py | 14 ++-
2 files changed, 198 insertions(+), 12 deletions(-)
diff --git a/src/qrules/particle.py b/src/qrules/particle.py
index 79dc6d0d..c1953e84 100644
--- a/src/qrules/particle.py
+++ b/src/qrules/particle.py
@@ -18,6 +18,7 @@
from difflib import get_close_matches
from fractions import Fraction
from functools import total_ordering
+from math import copysign
from typing import TYPE_CHECKING, Any
import attrs
@@ -38,6 +39,8 @@
from attrs import Attribute
from IPython.lib.pretty import RepresentationPrinter
+ from particle import Particle as PdgDatabase
+ from particle.particle import enums
_LOGGER = logging.getLogger(__name__)
@@ -238,10 +241,9 @@ def _repr_pretty_(self, p: RepresentationPrinter, cycle: bool) -> None:
def _get_name_root(name: str) -> str:
"""Strip a string (particularly the `.Particle.name`) of specifications."""
- name_root = re.sub(r"\([^)]*\)", "", name)
- name_root = name_root.split("_", maxsplit=1)[0]
- name_root = re.sub(r"[\^\*\+\-~\d']", "", name_root)
- return name_root.removesuffix("bar")
+ name_root = name
+ name_root = re.sub(r"\(.+\)", "", name_root)
+ return re.sub(r"[\*\+\-~\d']", "", name_root)
ParticleWithSpin = tuple[Particle, Fraction]
@@ -492,12 +494,188 @@ def create_antiparticle(
)
-def load_pdg() -> ParticleCollection:
+def load_pdg(*, use_official_pdg: bool = False) -> ParticleCollection:
"""Create a `.ParticleCollection` with all entries from the PDG.
- PDG info is imported from the official `PDG Python API
- `_.
+ By default, particle definitions are imported from the `Scikit-HEP particle
+ `_ package. Set ``use_official_pdg`` to
+ ``True`` to import them from the official `PDG Python API
+ `_ instead.
"""
- from qrules._pdg import load_pdg as load_official_pdg # ruff: ignore[import-outside-top-level]
+ if use_official_pdg:
+ from qrules._pdg import load_pdg as load_official_pdg # ruff: ignore[import-outside-top-level]
- return load_official_pdg()
+ return load_official_pdg()
+
+ from particle import Particle as PdgDatabase # ruff: ignore[import-outside-top-level]
+
+ all_pdg_particles = PdgDatabase.findall(
+ lambda item: (
+ item.charge is not None
+ and float(item.charge).is_integer() # remove quarks
+ and item.J is not None # remove new physics and nuclei
+ and abs(item.pdgid) < 1e9 # p and n as nucleus
+ and item.name not in __skip_particles
+ and not (item.mass is None and not item.name.startswith("nu"))
+ )
+ )
+ particle_collection = ParticleCollection()
+ for pdg_particle in all_pdg_particles:
+ new_particle = __convert_pdg_instance(pdg_particle)
+ particle_collection.add(new_particle)
+ return particle_collection
+
+
+__skip_particles = {
+ "K(L)0", # no isospin projection
+ "K(S)0", # no isospin projection
+ "B(s2)*(5840)0", # isospin(0.5, 0.0) ?
+ "B(s2)*(5840)~0", # isospin(0.5, 0.0) ?
+}
+
+
+def __sign(value: float) -> int:
+ return int(copysign(1, value))
+
+
+# cspell:ignore pdgid
+def __convert_pdg_instance(pdg_particle: PdgDatabase) -> Particle:
+ def convert_mass_width(value: float | None) -> float:
+ if value is None:
+ return 0.0
+ return float(value) / 1e3 # https://github.com/ComPWA/qrules/issues/14
+
+ def convert_spin(value: Fraction | float | None) -> float:
+ if value is None:
+ msg = f"PDG instance has no spin:\n{pdg_particle}"
+ raise ValueError(msg)
+ return float(value)
+
+ if pdg_particle.charge is None:
+ msg = f"PDG instance has no charge:\n{pdg_particle}"
+ raise ValueError(msg)
+ quark_numbers = __compute_quark_numbers(pdg_particle)
+ lepton_numbers = __compute_lepton_numbers(pdg_particle)
+ if pdg_particle.pdgid.is_lepton: # convention: C(fermion)=+1
+ parity: Parity | None = Parity(__sign(pdg_particle.pdgid))
+ else:
+ parity = __create_parity(pdg_particle.P)
+ latex = None
+ if pdg_particle.latex_name != "Unknown":
+ latex = str(pdg_particle.latex_name)
+ return Particle(
+ name=str(pdg_particle.name),
+ latex=latex,
+ pid=int(pdg_particle.pdgid),
+ mass=convert_mass_width(pdg_particle.mass),
+ width=convert_mass_width(pdg_particle.width),
+ charge=int(pdg_particle.charge),
+ spin=convert_spin(pdg_particle.J),
+ strangeness=quark_numbers[0],
+ charmness=quark_numbers[1],
+ bottomness=quark_numbers[2],
+ topness=quark_numbers[3],
+ baryon_number=__compute_baryonnumber(pdg_particle),
+ electron_lepton_number=lepton_numbers[0],
+ muon_lepton_number=lepton_numbers[1],
+ tau_lepton_number=lepton_numbers[2],
+ isospin=__create_isospin(pdg_particle),
+ parity=parity,
+ c_parity=__create_parity(pdg_particle.C),
+ g_parity=__create_parity(pdg_particle.G),
+ )
+
+
+def __compute_quark_numbers(
+ pdg_particle: PdgDatabase,
+) -> tuple[int, int, int, int]:
+ strangeness = 0
+ charmness = 0
+ bottomness = 0
+ topness = 0
+ if pdg_particle.pdgid.is_hadron:
+ quark_content = __filter_quark_content(pdg_particle)
+ strangeness = quark_content.count("S") - quark_content.count("s")
+ charmness = quark_content.count("c") - quark_content.count("C")
+ bottomness = quark_content.count("B") - quark_content.count("b")
+ topness = quark_content.count("t") - quark_content.count("T")
+ return (
+ strangeness,
+ charmness,
+ bottomness,
+ topness,
+ )
+
+
+def __compute_lepton_numbers(
+ pdg_particle: PdgDatabase,
+) -> tuple[int, int, int]:
+ electron_lepton_number = 0
+ muon_lepton_number = 0
+ tau_lepton_number = 0
+ if pdg_particle.pdgid.is_lepton:
+ lepton_number = int(__sign(pdg_particle.pdgid))
+ if "e" in pdg_particle.name:
+ electron_lepton_number = lepton_number
+ elif "mu" in pdg_particle.name:
+ muon_lepton_number = lepton_number
+ elif "tau" in pdg_particle.name:
+ tau_lepton_number = lepton_number
+ return electron_lepton_number, muon_lepton_number, tau_lepton_number
+
+
+def __compute_baryonnumber(pdg_particle: PdgDatabase) -> int:
+ return int(__sign(pdg_particle.pdgid) * pdg_particle.pdgid.is_baryon)
+
+
+def __create_isospin(pdg_particle: PdgDatabase) -> Spin | None:
+ if pdg_particle.I is None:
+ return None
+ magnitude = Fraction(pdg_particle.I)
+ projection = __isospin_projection_from_pdg(pdg_particle)
+ return Spin(magnitude, projection)
+
+
+def __isospin_projection_from_pdg(pdg_particle: PdgDatabase, /) -> Fraction:
+ if pdg_particle.charge is None:
+ msg = f"PDG instance has no charge:\n{pdg_particle}"
+ raise ValueError(msg)
+ if "qq" in pdg_particle.quarks.lower():
+ strangeness, charmness, bottomness, topness = __compute_quark_numbers(
+ pdg_particle
+ )
+ baryon_number = __compute_baryonnumber(pdg_particle)
+ projection = pdg_particle.charge - 0.5 * (
+ baryon_number + strangeness + charmness + bottomness + topness
+ )
+ else:
+ projection = 0.0
+ if pdg_particle.pdgid.is_hadron:
+ quark_content = __filter_quark_content(pdg_particle)
+ projection += quark_content.count("u") + quark_content.count("D")
+ projection -= quark_content.count("U") + quark_content.count("d")
+ projection *= 0.5
+ if (
+ pdg_particle.I is not None
+ and not float(pdg_particle.I - projection).is_integer()
+ ):
+ msg = f"Cannot have isospin {pdg_particle.I, projection}"
+ raise ValueError(msg)
+ return Fraction(projection)
+
+
+def __filter_quark_content(pdg_particle: PdgDatabase) -> str:
+ matches = re.search(r"([dDuUsScCbBtT+-]{2,})", pdg_particle.quarks)
+ if matches is None:
+ return ""
+ return matches[1]
+
+
+def __create_parity(parity_enum: enums.Parity) -> Parity | None:
+ from particle.particle import enums # ruff: ignore[import-outside-top-level]
+
+ if parity_enum is None or parity_enum == enums.Parity.u:
+ return None
+ if parity_enum == getattr(parity_enum, "o", None): # particle < 0.14
+ return None
+ return Parity(int(parity_enum))
diff --git a/tests/unit/test_pdg_adapter.py b/tests/unit/test_pdg_adapter.py
index 9254e9fa..108544d0 100644
--- a/tests/unit/test_pdg_adapter.py
+++ b/tests/unit/test_pdg_adapter.py
@@ -5,14 +5,22 @@
from pdg.errors import PdgNoDataError
from qrules._pdg import _load_pdg_particles, _to_mass, _to_width
-from qrules._pdg import load_pdg as load_official_pdg
from qrules.particle import ParticleCollection, load_pdg
from qrules.quantum_numbers import Parity
@pytest.fixture(scope="module")
def official_particles() -> ParticleCollection:
- return load_official_pdg()
+ return load_pdg(use_official_pdg=True)
+
+
+def test_uses_scikit_hep_source_by_default(
+ official_particles: ParticleCollection,
+):
+ default_particles = load_pdg()
+
+ assert default_particles.find(-2212).name == "p~"
+ assert official_particles.find(-2212).name == "pbar"
def test_caches_particle_definitions_and_returns_independent_collections(
@@ -20,7 +28,7 @@ def test_caches_particle_definitions_and_returns_independent_collections(
):
cache_info_before = _load_pdg_particles.cache_info()
- second_collection = load_pdg()
+ second_collection = load_pdg(use_official_pdg=True)
cache_info_after = _load_pdg_particles.cache_info()
assert cache_info_after.hits == cache_info_before.hits + 1
From cc44b40d8423d5cceba5594411fe68fa1ce3564e Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Fri, 11 Sep 2026 12:47:29 +0200
Subject: [PATCH 16/27] FIX: Update pdg dependency to remove version
constraints
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index a59eeb9a..ae9c0ed4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -40,7 +40,7 @@ dependencies = [
"frozendict",
"jsonschema",
"particle",
- "pdg >=2026.0,<2027",
+ "pdg",
"python-constraint2",
"tqdm >=4.24.0", # autonotebook
]
From a160698f7a49cb1751b8230c9cf52352acaecfc3 Mon Sep 17 00:00:00 2001
From: GitHub
Date: Fri, 11 Sep 2026 10:51:04 +0000
Subject: [PATCH 17/27] MAINT: implement updates from formatters
---
uv.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/uv.lock b/uv.lock
index 3576e438..c14411b5 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2724,7 +2724,7 @@ requires-dist = [
{ name = "graphviz", marker = "extra == 'viz'" },
{ name = "jsonschema" },
{ name = "particle" },
- { name = "pdg", specifier = ">=2026.0,<2027" },
+ { name = "pdg" },
{ name = "python-constraint2" },
{ name = "pyyaml" },
{ name = "tqdm", specifier = ">=4.24.0" },
From 108e9f65bfac90f78c95f4eee01ef8e2cc11d0e3 Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Fri, 11 Sep 2026 13:08:33 +0200
Subject: [PATCH 18/27] Rename _pdg.py into _pdg_adapter.py to clarify its task
---
src/qrules/{_pdg.py => _pdg_adapter.py} | 0
src/qrules/particle.py | 2 +-
tests/unit/test_pdg_adapter.py | 2 +-
3 files changed, 2 insertions(+), 2 deletions(-)
rename src/qrules/{_pdg.py => _pdg_adapter.py} (100%)
diff --git a/src/qrules/_pdg.py b/src/qrules/_pdg_adapter.py
similarity index 100%
rename from src/qrules/_pdg.py
rename to src/qrules/_pdg_adapter.py
diff --git a/src/qrules/particle.py b/src/qrules/particle.py
index c1953e84..edc451e4 100644
--- a/src/qrules/particle.py
+++ b/src/qrules/particle.py
@@ -503,7 +503,7 @@ def load_pdg(*, use_official_pdg: bool = False) -> ParticleCollection:
`_ instead.
"""
if use_official_pdg:
- from qrules._pdg import load_pdg as load_official_pdg # ruff: ignore[import-outside-top-level]
+ from qrules._pdg_adapter import load_pdg as load_official_pdg # ruff: ignore[import-outside-top-level]
return load_official_pdg()
diff --git a/tests/unit/test_pdg_adapter.py b/tests/unit/test_pdg_adapter.py
index 108544d0..a0ea0ae3 100644
--- a/tests/unit/test_pdg_adapter.py
+++ b/tests/unit/test_pdg_adapter.py
@@ -4,7 +4,7 @@
import pytest
from pdg.errors import PdgNoDataError
-from qrules._pdg import _load_pdg_particles, _to_mass, _to_width
+from qrules._pdg_adapter import _load_pdg_particles, _to_mass, _to_width
from qrules.particle import ParticleCollection, load_pdg
from qrules.quantum_numbers import Parity
From d8c06d91e9c5344e5ca81d5526c238d969c935a7 Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Fri, 11 Sep 2026 14:26:52 +0200
Subject: [PATCH 19/27] REFAC: Replace _UnsupportedParticleError with
ValueError
---
src/qrules/_pdg_adapter.py | 16 ++++++----------
1 file changed, 6 insertions(+), 10 deletions(-)
diff --git a/src/qrules/_pdg_adapter.py b/src/qrules/_pdg_adapter.py
index 3e204869..2c273695 100644
--- a/src/qrules/_pdg_adapter.py
+++ b/src/qrules/_pdg_adapter.py
@@ -28,10 +28,6 @@
_SKIPPED_MC_IDS = {-535, 130, 310, 535}
-class _UnsupportedParticleError(ValueError):
- """Raised when a PDG entry cannot be represented by QRules."""
-
-
def load_pdg() -> ParticleCollection:
"""Load particle definitions from the official PDG database.
@@ -49,7 +45,7 @@ def _load_pdg_particles() -> tuple[Particle, ...]:
for source_particle in _iter_particles(api):
try:
particle = _convert_particle(source_particle)
- except _UnsupportedParticleError:
+ except ValueError:
continue
particles.append(particle)
return tuple(particles)
@@ -74,10 +70,10 @@ def _convert_particle(source: PdgParticle) -> Particle:
mcid = source.mcid
if mcid is None:
msg = f"Particle {source.name} has no Monte Carlo ID"
- raise _UnsupportedParticleError(msg)
+ raise ValueError(msg)
if mcid in _SKIPPED_MC_IDS or abs(mcid) >= 1_000_000_000:
msg = f"Particle {source.name} is not supported"
- raise _UnsupportedParticleError(msg)
+ raise ValueError(msg)
charge = _to_integer_charge(source.charge)
spin = _to_spin(
@@ -137,7 +133,7 @@ def _convert_particle(source: PdgParticle) -> Particle:
def _to_integer_charge(value: float) -> int:
if not float(value).is_integer():
msg = f"QRules does not support fractional charge {value}"
- raise _UnsupportedParticleError(msg)
+ raise ValueError(msg)
return int(value)
@@ -152,7 +148,7 @@ def _to_spin(value: str | None, mcid: int, *, is_hadron: bool) -> Fraction:
if is_hadron and spin_code > 0:
return Fraction(spin_code - 1, 2)
msg = f"Cannot determine spin for MC ID {mcid} from {value!r}"
- raise _UnsupportedParticleError(msg)
+ raise ValueError(msg)
def _to_mass(source: PdgParticle) -> float:
@@ -169,7 +165,7 @@ def _to_mass(source: PdgParticle) -> float:
if abs(source.mcid) in {12, 14, 16, 21, 22}:
return 0.0
msg = f"Particle {source.name} has no supported mass value"
- raise _UnsupportedParticleError(msg)
+ raise ValueError(msg)
def _to_width(source: PdgParticle) -> float:
From 53ddda11bb390007a50d6ec64aa92fa7ff444729 Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Fri, 11 Sep 2026 14:27:16 +0200
Subject: [PATCH 20/27] REFAC: Update load_pdg function to use source parameter
for particle source selection
---
src/qrules/particle.py | 17 +++++++++++------
tests/unit/test_pdg_adapter.py | 12 ++++++++++--
2 files changed, 21 insertions(+), 8 deletions(-)
diff --git a/src/qrules/particle.py b/src/qrules/particle.py
index edc451e4..20a23bc4 100644
--- a/src/qrules/particle.py
+++ b/src/qrules/particle.py
@@ -19,7 +19,7 @@
from fractions import Fraction
from functools import total_ordering
from math import copysign
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Literal
import attrs
from attrs import field, frozen
@@ -494,18 +494,23 @@ def create_antiparticle(
)
-def load_pdg(*, use_official_pdg: bool = False) -> ParticleCollection:
+def load_pdg(
+ *, source: Literal["scikit-hep", "pdg"] = "scikit-hep"
+) -> ParticleCollection:
"""Create a `.ParticleCollection` with all entries from the PDG.
By default, particle definitions are imported from the `Scikit-HEP particle
- `_ package. Set ``use_official_pdg`` to
- ``True`` to import them from the official `PDG Python API
- `_ instead.
+ `_ package. Set ``source="pdg"`` to
+ import them from the official `PDG Python API `_
+ instead.
"""
- if use_official_pdg:
+ if source == "pdg":
from qrules._pdg_adapter import load_pdg as load_official_pdg # ruff: ignore[import-outside-top-level]
return load_official_pdg()
+ if source != "scikit-hep":
+ msg = f"Unknown particle source: {source!r}"
+ raise ValueError(msg)
from particle import Particle as PdgDatabase # ruff: ignore[import-outside-top-level]
diff --git a/tests/unit/test_pdg_adapter.py b/tests/unit/test_pdg_adapter.py
index a0ea0ae3..b4de8bb1 100644
--- a/tests/unit/test_pdg_adapter.py
+++ b/tests/unit/test_pdg_adapter.py
@@ -1,4 +1,5 @@
from fractions import Fraction
+from typing import Any, cast
from unittest.mock import MagicMock, PropertyMock
import pytest
@@ -11,24 +12,31 @@
@pytest.fixture(scope="module")
def official_particles() -> ParticleCollection:
- return load_pdg(use_official_pdg=True)
+ return load_pdg(source="pdg")
def test_uses_scikit_hep_source_by_default(
official_particles: ParticleCollection,
):
default_particles = load_pdg()
+ scikit_hep_particles = load_pdg(source="scikit-hep")
+ assert default_particles == scikit_hep_particles
assert default_particles.find(-2212).name == "p~"
assert official_particles.find(-2212).name == "pbar"
+def test_rejects_unknown_source():
+ with pytest.raises(ValueError, match="Unknown particle source"):
+ load_pdg(source=cast("Any", "unknown"))
+
+
def test_caches_particle_definitions_and_returns_independent_collections(
official_particles: ParticleCollection,
):
cache_info_before = _load_pdg_particles.cache_info()
- second_collection = load_pdg(use_official_pdg=True)
+ second_collection = load_pdg(source="pdg")
cache_info_after = _load_pdg_particles.cache_info()
assert cache_info_after.hits == cache_info_before.hits + 1
From 507477254a3954e944c812226a7206f388598130 Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Fri, 11 Sep 2026 14:41:14 +0200
Subject: [PATCH 21/27] REFAC: Update load_pdg function to use PyPIPackage for
source parameter
---
src/qrules/particle.py | 11 ++++++-----
tests/unit/test_pdg_adapter.py | 2 +-
2 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/src/qrules/particle.py b/src/qrules/particle.py
index 20a23bc4..efb5141c 100644
--- a/src/qrules/particle.py
+++ b/src/qrules/particle.py
@@ -494,12 +494,13 @@ def create_antiparticle(
)
-def load_pdg(
- *, source: Literal["scikit-hep", "pdg"] = "scikit-hep"
-) -> ParticleCollection:
+PyPIPackage = Literal["particle", "pdg"]
+
+
+def load_pdg(*, source: PyPIPackage = "particle") -> ParticleCollection:
"""Create a `.ParticleCollection` with all entries from the PDG.
- By default, particle definitions are imported from the `Scikit-HEP particle
+ By default, particle definitions are imported from the `particle
`_ package. Set ``source="pdg"`` to
import them from the official `PDG Python API `_
instead.
@@ -508,7 +509,7 @@ def load_pdg(
from qrules._pdg_adapter import load_pdg as load_official_pdg # ruff: ignore[import-outside-top-level]
return load_official_pdg()
- if source != "scikit-hep":
+ if source != "particle":
msg = f"Unknown particle source: {source!r}"
raise ValueError(msg)
diff --git a/tests/unit/test_pdg_adapter.py b/tests/unit/test_pdg_adapter.py
index b4de8bb1..b52d0fb2 100644
--- a/tests/unit/test_pdg_adapter.py
+++ b/tests/unit/test_pdg_adapter.py
@@ -19,7 +19,7 @@ def test_uses_scikit_hep_source_by_default(
official_particles: ParticleCollection,
):
default_particles = load_pdg()
- scikit_hep_particles = load_pdg(source="scikit-hep")
+ scikit_hep_particles = load_pdg(source="particle")
assert default_particles == scikit_hep_particles
assert default_particles.find(-2212).name == "p~"
From 2226f09311fcc05d219c6a6508200355165f0aa9 Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Fri, 11 Sep 2026 15:12:44 +0200
Subject: [PATCH 22/27] REFAC: Remove LaTeX name generation functionality and
associated tests for pdg package
---
src/qrules/_pdg_adapter.py | 6 ---
src/qrules/_pdg_latex.py | 82 ----------------------------------
tests/unit/test_pdg_adapter.py | 23 ----------
tests/unit/test_pdg_latex.py | 46 -------------------
4 files changed, 157 deletions(-)
delete mode 100644 src/qrules/_pdg_latex.py
delete mode 100644 tests/unit/test_pdg_latex.py
diff --git a/src/qrules/_pdg_adapter.py b/src/qrules/_pdg_adapter.py
index 2c273695..71e96d08 100644
--- a/src/qrules/_pdg_adapter.py
+++ b/src/qrules/_pdg_adapter.py
@@ -11,7 +11,6 @@
from pdg.errors import PdgNoDataError
from pdg.units import convert
-from qrules._pdg_latex import create_latex_name
from qrules.particle import Particle, ParticleCollection, Spin
from qrules.quantum_numbers import Parity
@@ -105,11 +104,6 @@ def _convert_particle(source: PdgParticle) -> Particle:
return Particle(
name=source.name,
- latex=create_latex_name(
- source.name,
- isospin=source.quantum_I,
- self_conjugate=source.self_conjugate,
- ),
pid=mcid,
spin=spin,
mass=mass,
diff --git a/src/qrules/_pdg_latex.py b/src/qrules/_pdg_latex.py
deleted file mode 100644
index 4f94188c..00000000
--- a/src/qrules/_pdg_latex.py
+++ /dev/null
@@ -1,82 +0,0 @@
-"""Convert official PDG ASCII particle names to LaTeX."""
-
-from __future__ import annotations
-
-import re
-
-_PARTICLE_NAME_PATTERN = re.compile(
- r"^(?P[A-Za-z]+(?:/[A-Za-z]+)?)"
- r"(?P(?:[_^](?:[A-Za-z0-9]+|\*|'))*)"
- r"(?P\([^()]*\))?"
- r"(?P\+\+|--|\+|-|0)?$"
-)
-_SCRIPT_PATTERN = re.compile(r"(?P[_^])(?P[A-Za-z0-9]+|\*|')")
-
-_LATEX_SYMBOLS = {
- "Delta": R"\Delta",
- "Lambda": R"\Lambda",
- "Omega": R"\Omega",
- "Sigma": R"\Sigma",
- "Upsilon": R"\Upsilon",
- "Xi": R"\Xi",
- "chi": R"\chi",
- "eta": R"\eta",
- "gamma": R"\gamma",
- "mu": R"\mu",
- "nu": R"\nu",
- "omega": R"\omega",
- "phi": R"\phi",
- "pi": R"\pi",
- "psi": R"\psi",
- "rho": R"\rho",
- "tau": R"\tau",
-}
-
-
-def create_latex_name(
- name: str,
- *,
- isospin: str | None,
- self_conjugate: bool,
-) -> str | None:
- """Convert a canonical PDG name, returning `None` for unsupported syntax."""
- match = _PARTICLE_NAME_PATTERN.fullmatch(name)
- if match is None:
- return None
-
- latex = _render_base(match.group("base"))
-
- subscripts: list[str] = []
- superscripts: list[str] = []
- for script in _SCRIPT_PATTERN.finditer(match.group("scripts")):
- value = script.group("value")
- if script.group("kind") == "_":
- subscripts.append(_LATEX_SYMBOLS.get(value, value))
- else:
- superscripts.append(R"\prime" if value == "'" else value)
- latex += "".join(Rf"_{{{value}}}" for value in subscripts)
-
- charge = match.group("charge")
- if charge == "0" and self_conjugate and isospin == "0":
- charge = None
- qualifier = match.group("qualifier")
- if qualifier in {None, "()"} and charge is not None and superscripts:
- superscripts.append(charge)
- charge = None
- if superscripts:
- latex += Rf"^{{{''.join(superscripts)}}}"
- if qualifier != "()" and qualifier is not None:
- latex += qualifier
- if charge is not None:
- latex += Rf"^{{{charge}}}"
- return latex
-
-
-def _render_base(base: str) -> str:
- is_antiparticle = base.endswith("bar")
- if is_antiparticle:
- base = base.removesuffix("bar")
- latex = "/".join(_LATEX_SYMBOLS.get(part, part) for part in base.split("/"))
- if is_antiparticle:
- latex = Rf"\overline{{{latex}}}"
- return latex
diff --git a/tests/unit/test_pdg_adapter.py b/tests/unit/test_pdg_adapter.py
index b52d0fb2..35f7db2a 100644
--- a/tests/unit/test_pdg_adapter.py
+++ b/tests/unit/test_pdg_adapter.py
@@ -67,29 +67,6 @@ def test_uses_official_names(
assert official_particles.find(mcid).name == name
-def test_all_particles_have_latex(official_particles: ParticleCollection):
- assert all(particle.latex is not None for particle in official_particles)
-
-
-@pytest.mark.parametrize(
- ("mcid", "latex"),
- [
- (12, R"\nu_{e}"),
- (211, R"\pi^{+}"),
- (-2212, R"\overline{p}"),
- (443, R"J/\psi(1S)"),
- (9010221, R"f_{0}(980)"),
- (5122, R"\Lambda_{b}^{0}"),
- ],
-)
-def test_uses_generated_latex(
- official_particles: ParticleCollection,
- mcid: int,
- latex: str,
-):
- assert official_particles.find(mcid).latex == latex
-
-
def test_pion_quantum_numbers(official_particles: ParticleCollection):
pion = official_particles.find(211)
assert pion.spin == 0
diff --git a/tests/unit/test_pdg_latex.py b/tests/unit/test_pdg_latex.py
deleted file mode 100644
index 6d2ea2b0..00000000
--- a/tests/unit/test_pdg_latex.py
+++ /dev/null
@@ -1,46 +0,0 @@
-import pytest
-
-from qrules._pdg_latex import create_latex_name
-
-
-@pytest.mark.parametrize(
- ("name", "isospin", "self_conjugate", "latex"),
- [
- ("pi+", "1", False, R"\pi^{+}"),
- ("pi0", "1", True, R"\pi^{0}"),
- ("pbar", "1/2", False, R"\overline{p}"),
- ("nubar_mu", None, False, R"\overline{\nu}_{\mu}"),
- ("J/psi(1S)", "0", True, R"J/\psi(1S)"),
- ("f_0(980)0", "0", True, R"f_{0}(980)"),
- ("Lambda_b()0", "0", False, R"\Lambda_{b}^{0}"),
- ("D_0^*(2300)+", "1/2", False, R"D_{0}^{*}(2300)^{+}"),
- ("D_s^*()+", "0", False, R"D_{s}^{*+}"),
- ("Xibar_c^'()0", "1/2", False, R"\overline{\Xi}_{c}^{\prime0}"),
- ("eta^'(958)0", "0", True, R"\eta^{\prime}(958)"),
- ],
-)
-def test_create_latex_name(
- name: str,
- isospin: str | None,
- self_conjugate: bool,
- latex: str,
-):
- assert (
- create_latex_name(
- name,
- isospin=isospin,
- self_conjugate=self_conjugate,
- )
- == latex
- )
-
-
-def test_rejects_unsupported_name_syntax():
- assert (
- create_latex_name(
- "not a PDG name",
- isospin=None,
- self_conjugate=False,
- )
- is None
- )
From bf6fb96032102527b7d138581afffd6eeae1ad3b Mon Sep 17 00:00:00 2001
From: Remco de Boer <29308176+redeboer@users.noreply.github.com>
Date: Fri, 11 Sep 2026 15:24:31 +0200
Subject: [PATCH 23/27] MAINT: remove redundant cSpell words
---
.cspell.json | 3 ---
1 file changed, 3 deletions(-)
diff --git a/.cspell.json b/.cspell.json
index 587c9e4d..24fc3009 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -38,7 +38,6 @@
"pyproject.toml"
],
"ignoreWords": [
- "Xibar",
"adrs",
"ampform",
"arange",
@@ -90,7 +89,6 @@
"mathrm",
"maxdepth",
"mcid",
- "mcids",
"meijerg",
"mimetype",
"modindex",
@@ -105,7 +103,6 @@
"noreply",
"nrows",
"nsimplify",
- "nubar",
"pandoc",
"pbar",
"permalinks",
From b13c06c147d0741b1c1ee3a9dae74f54095f1937 Mon Sep 17 00:00:00 2001
From: Remco de Boer <29308176+redeboer@users.noreply.github.com>
Date: Fri, 11 Sep 2026 15:24:45 +0200
Subject: [PATCH 24/27] MAINT: minor formatting improvements
---
src/qrules/particle.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/qrules/particle.py b/src/qrules/particle.py
index efb5141c..46f4e8e2 100644
--- a/src/qrules/particle.py
+++ b/src/qrules/particle.py
@@ -501,9 +501,8 @@ def load_pdg(*, source: PyPIPackage = "particle") -> ParticleCollection:
"""Create a `.ParticleCollection` with all entries from the PDG.
By default, particle definitions are imported from the `particle
- `_ package. Set ``source="pdg"`` to
- import them from the official `PDG Python API `_
- instead.
+ `_ package. Set ``source="pdg"`` to import
+ them from the official `PDG Python API `_ instead.
"""
if source == "pdg":
from qrules._pdg_adapter import load_pdg as load_official_pdg # ruff: ignore[import-outside-top-level]
From 5e396a0ee1c4a35cce565af7503fb382570cca6a Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Mon, 14 Sep 2026 12:11:26 +0200
Subject: [PATCH 25/27] DOCS: Enhance particle loading documentation with
source parameter details
---
docs/usage/particle.ipynb | 33 +++++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/docs/usage/particle.ipynb b/docs/usage/particle.ipynb
index 20892d0d..819f2dfb 100644
--- a/docs/usage/particle.ipynb
+++ b/docs/usage/particle.ipynb
@@ -33,6 +33,13 @@
"Here, we call this method directly to illustrate what happens (we use {func}`.load_pdg`, which loads a subset):"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The function {func}`.load_pdg` uses a flag `source` to determine the source of the particle definitions. If `source` is set to `\"pdg\"`, it loads particle definitions from the [`pdg`](https://pdgapi.lbl.gov/doc/) package provided by PDG. If `source` is set to `\"particle\"`, it loads the particle definitions from the [`particle`](https://pypi.org/project/particle/) package, which is provided by scikit-hep."
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -45,6 +52,32 @@
"print(\"Number of loaded particles:\", len(particle_db))"
]
},
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "particle_db_pdg = load_pdg(source=\"pdg\")\n",
+ "print(\"Number of particles loaded from PDG:\", len(particle_db_pdg))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "One can see on one hand that the number of loaded {class}`.Particle` definitions from the `pdg` package is currently greater than the number of particles loaded from the `particle` package and on the other hand, that loading the {class}`.ParticleCollection` instance takes significantly more time due to it using SQLite to store the particle data."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ ":::{warning}\n",
+ "As of writing the `pdg` package does not support LaTeX names for particles.\n",
+ ":::"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
From 9f1c2435f7a8c7eb4b8aa3bbdf1dd41748ff7ecf Mon Sep 17 00:00:00 2001
From: Fabian Steube <206550910+Flomber@users.noreply.github.com>
Date: Mon, 14 Sep 2026 12:18:55 +0200
Subject: [PATCH 26/27] REFAC: Restructure tests in test_pdg_adapter.py for
improved readability and organization
---
tests/unit/test_pdg_adapter.py | 357 ++++++++++++++++-----------------
1 file changed, 178 insertions(+), 179 deletions(-)
diff --git a/tests/unit/test_pdg_adapter.py b/tests/unit/test_pdg_adapter.py
index 35f7db2a..6b410782 100644
--- a/tests/unit/test_pdg_adapter.py
+++ b/tests/unit/test_pdg_adapter.py
@@ -15,182 +15,181 @@ def official_particles() -> ParticleCollection:
return load_pdg(source="pdg")
-def test_uses_scikit_hep_source_by_default(
- official_particles: ParticleCollection,
-):
- default_particles = load_pdg()
- scikit_hep_particles = load_pdg(source="particle")
-
- assert default_particles == scikit_hep_particles
- assert default_particles.find(-2212).name == "p~"
- assert official_particles.find(-2212).name == "pbar"
-
-
-def test_rejects_unknown_source():
- with pytest.raises(ValueError, match="Unknown particle source"):
- load_pdg(source=cast("Any", "unknown"))
-
-
-def test_caches_particle_definitions_and_returns_independent_collections(
- official_particles: ParticleCollection,
-):
- cache_info_before = _load_pdg_particles.cache_info()
-
- second_collection = load_pdg(source="pdg")
-
- cache_info_after = _load_pdg_particles.cache_info()
- assert cache_info_after.hits == cache_info_before.hits + 1
- assert cache_info_after.misses == cache_info_before.misses
- assert second_collection == official_particles
- assert second_collection is not official_particles
-
- second_collection.discard("gamma")
- assert "gamma" not in second_collection
- assert "gamma" in official_particles
-
-
-@pytest.mark.parametrize(
- ("mcid", "name"),
- [
- (12, "nu_e"),
- (-2212, "pbar"),
- (443, "J/psi(1S)"),
- (9010221, "f_0(980)0"),
- (5122, "Lambda_b()0"),
- ],
-)
-def test_uses_official_names(
- official_particles: ParticleCollection,
- mcid: int,
- name: str,
-):
- assert official_particles.find(mcid).name == name
-
-
-def test_pion_quantum_numbers(official_particles: ParticleCollection):
- pion = official_particles.find(211)
- assert pion.spin == 0
- assert pion.charge == +1
- assert pion.isospin is not None
- assert pion.isospin.magnitude == 1
- assert pion.isospin.projection == +1
- assert pion.parity == Parity(-1)
- assert pion.c_parity is None
- assert pion.g_parity == Parity(-1)
-
-
-def test_antiproton_quantum_numbers(official_particles: ParticleCollection):
- antiproton = official_particles.find(-2212)
- assert antiproton.spin == Fraction(1, 2)
- assert antiproton.charge == -1
- assert antiproton.baryon_number == -1
- assert antiproton.isospin is not None
- assert antiproton.isospin.projection == Fraction(-1, 2)
- assert antiproton.parity == Parity(-1)
-
-
-@pytest.mark.parametrize(
- ("mcid", "lepton_numbers"),
- [
- (11, (+1, 0, 0)),
- (-12, (-1, 0, 0)),
- (13, (0, +1, 0)),
- (-14, (0, -1, 0)),
- (15, (0, 0, +1)),
- (-16, (0, 0, -1)),
- ],
-)
-def test_lepton_numbers(
- official_particles: ParticleCollection,
- mcid: int,
- lepton_numbers: tuple[int, int, int],
-):
- particle = official_particles.find(mcid)
- assert (
- particle.electron_lepton_number,
- particle.muon_lepton_number,
- particle.tau_lepton_number,
- ) == lepton_numbers
-
-
-@pytest.mark.parametrize(
- ("mcid", "flavor_numbers"),
- [
- (+321, (+1, 0, 0, 0)),
- (-321, (-1, 0, 0, 0)),
- (+411, (0, +1, 0, 0)),
- (-411, (0, -1, 0, 0)),
- (+521, (0, 0, +1, 0)),
- (-521, (0, 0, -1, 0)),
- ],
-)
-def test_flavor_numbers(
- official_particles: ParticleCollection,
- mcid: int,
- flavor_numbers: tuple[int, int, int, int],
-):
- particle = official_particles.find(mcid)
- assert (
- particle.strangeness,
- particle.charmness,
- particle.bottomness,
- particle.topness,
- ) == flavor_numbers
-
-
-def test_prefers_official_spin(official_particles: ParticleCollection):
- assert official_particles.find(104122).spin == Fraction(3, 2)
-
-
-def test_uses_measured_mass_and_width(official_particles: ParticleCollection):
- rho = official_particles.find(113)
- assert rho.mass == pytest.approx(0.7752611563582926)
- assert rho.width == pytest.approx(0.14739133387028722)
-
-
-def test_derives_width_from_lifetime(official_particles: ParticleCollection):
- muon = official_particles.find(13)
- assert muon.width == pytest.approx(2.9959292110062035e-19)
-
-
-def test_uses_zero_width_for_stable_particle(
- official_particles: ParticleCollection,
-):
- assert official_particles.find(22).width == 0.0
-
-
-def test_uses_antiparticle_mass_if_particle_has_no_mass():
- source = MagicMock(self_conjugate=False, mcid=1, name="particle")
- source.has_mass_entry = False
- type(source).mass = PropertyMock(side_effect=PdgNoDataError("no mass"))
- source.antiparticle.has_mass_entry = True
- source.antiparticle.mass = 0.5
-
- assert _to_mass(source) == 0.5
-
-
-def test_uses_antiparticle_width_if_particle_has_no_decay_data():
- source = MagicMock(self_conjugate=False)
- source.has_width_entry = False
- source.has_lifetime_entry = False
- source.width = 0.0
- source.antiparticle.has_width_entry = True
- source.antiparticle.has_lifetime_entry = False
- source.antiparticle.width = 0.25
-
- assert _to_width(source) == 0.25
-
-
-@pytest.mark.parametrize(
- ("mcid", "width"),
- [
- (9010221, 0.055), # 10 to 100 MeV
- (2224, 0.117), # 114 to 117 to 120 MeV
- ],
-)
-def test_uses_central_value_for_width_ranges(
- official_particles: ParticleCollection,
- mcid: int,
- width: float,
-):
- assert official_particles.find(mcid).width == pytest.approx(width)
+def describe_load_pdg():
+ def it_uses_scikit_hep_source_by_default(
+ official_particles: ParticleCollection,
+ ):
+ default_particles = load_pdg()
+ scikit_hep_particles = load_pdg(source="particle")
+
+ assert default_particles == scikit_hep_particles
+ assert default_particles.find(-2212).name == "p~"
+ assert official_particles.find(-2212).name == "pbar"
+
+ def it_rejects_unknown_source():
+ with pytest.raises(ValueError, match="Unknown particle source"):
+ load_pdg(source=cast("Any", "unknown"))
+
+ def it_caches_particle_definitions_and_returns_independent_collections(
+ official_particles: ParticleCollection,
+ ):
+ cache_info_before = _load_pdg_particles.cache_info()
+
+ second_collection = load_pdg(source="pdg")
+
+ cache_info_after = _load_pdg_particles.cache_info()
+ assert cache_info_after.hits == cache_info_before.hits + 1
+ assert cache_info_after.misses == cache_info_before.misses
+ assert second_collection == official_particles
+ assert second_collection is not official_particles
+
+ second_collection.discard("gamma")
+ assert "gamma" not in second_collection
+ assert "gamma" in official_particles
+
+ @pytest.mark.parametrize(
+ ("mcid", "name"),
+ [
+ (12, "nu_e"),
+ (-2212, "pbar"),
+ (443, "J/psi(1S)"),
+ (9010221, "f_0(980)0"),
+ (5122, "Lambda_b()0"),
+ ],
+ )
+ def it_uses_official_names(
+ official_particles: ParticleCollection,
+ mcid: int,
+ name: str,
+ ):
+ assert official_particles.find(mcid).name == name
+
+ def it_loads_pion_quantum_numbers(
+ official_particles: ParticleCollection,
+ ):
+ pion = official_particles.find(211)
+ assert pion.spin == 0
+ assert pion.charge == +1
+ assert pion.isospin is not None
+ assert pion.isospin.magnitude == 1
+ assert pion.isospin.projection == +1
+ assert pion.parity == Parity(-1)
+ assert pion.c_parity is None
+ assert pion.g_parity == Parity(-1)
+
+ def it_loads_antiproton_quantum_numbers(
+ official_particles: ParticleCollection,
+ ):
+ antiproton = official_particles.find(-2212)
+ assert antiproton.spin == Fraction(1, 2)
+ assert antiproton.charge == -1
+ assert antiproton.baryon_number == -1
+ assert antiproton.isospin is not None
+ assert antiproton.isospin.projection == Fraction(-1, 2)
+ assert antiproton.parity == Parity(-1)
+
+ @pytest.mark.parametrize(
+ ("mcid", "lepton_numbers"),
+ [
+ (11, (+1, 0, 0)),
+ (-12, (-1, 0, 0)),
+ (13, (0, +1, 0)),
+ (-14, (0, -1, 0)),
+ (15, (0, 0, +1)),
+ (-16, (0, 0, -1)),
+ ],
+ )
+ def it_loads_lepton_numbers(
+ official_particles: ParticleCollection,
+ mcid: int,
+ lepton_numbers: tuple[int, int, int],
+ ):
+ particle = official_particles.find(mcid)
+ assert (
+ particle.electron_lepton_number,
+ particle.muon_lepton_number,
+ particle.tau_lepton_number,
+ ) == lepton_numbers
+
+ @pytest.mark.parametrize(
+ ("mcid", "flavor_numbers"),
+ [
+ (+321, (+1, 0, 0, 0)),
+ (-321, (-1, 0, 0, 0)),
+ (+411, (0, +1, 0, 0)),
+ (-411, (0, -1, 0, 0)),
+ (+521, (0, 0, +1, 0)),
+ (-521, (0, 0, -1, 0)),
+ ],
+ )
+ def it_loads_flavor_numbers(
+ official_particles: ParticleCollection,
+ mcid: int,
+ flavor_numbers: tuple[int, int, int, int],
+ ):
+ particle = official_particles.find(mcid)
+ assert (
+ particle.strangeness,
+ particle.charmness,
+ particle.bottomness,
+ particle.topness,
+ ) == flavor_numbers
+
+ def it_prefers_official_spin(official_particles: ParticleCollection):
+ assert official_particles.find(104122).spin == Fraction(3, 2)
+
+ def it_uses_measured_mass_and_width(
+ official_particles: ParticleCollection,
+ ):
+ rho = official_particles.find(113)
+ assert rho.mass == pytest.approx(0.7752611563582926)
+ assert rho.width == pytest.approx(0.14739133387028722)
+
+ def it_derives_width_from_lifetime(
+ official_particles: ParticleCollection,
+ ):
+ muon = official_particles.find(13)
+ assert muon.width == pytest.approx(2.9959292110062035e-19)
+
+ def it_uses_zero_width_for_stable_particle(
+ official_particles: ParticleCollection,
+ ):
+ assert official_particles.find(22).width == 0.0
+
+ @pytest.mark.parametrize(
+ ("mcid", "width"),
+ [
+ (9010221, 0.055), # 10 to 100 MeV
+ (2224, 0.117), # 114 to 117 to 120 MeV
+ ],
+ )
+ def it_uses_central_value_for_width_ranges(
+ official_particles: ParticleCollection,
+ mcid: int,
+ width: float,
+ ):
+ assert official_particles.find(mcid).width == pytest.approx(width)
+
+
+def describe_to_mass():
+ def it_uses_antiparticle_mass_if_particle_has_no_mass():
+ source = MagicMock(self_conjugate=False, mcid=1, name="particle")
+ source.has_mass_entry = False
+ type(source).mass = PropertyMock(side_effect=PdgNoDataError("no mass"))
+ source.antiparticle.has_mass_entry = True
+ source.antiparticle.mass = 0.5
+
+ assert _to_mass(source) == 0.5
+
+
+def describe_to_width():
+ def it_uses_antiparticle_width_if_particle_has_no_decay_data():
+ source = MagicMock(self_conjugate=False)
+ source.has_width_entry = False
+ source.has_lifetime_entry = False
+ source.width = 0.0
+ source.antiparticle.has_width_entry = True
+ source.antiparticle.has_lifetime_entry = False
+ source.antiparticle.width = 0.25
+
+ assert _to_width(source) == 0.25
From ff78ae212a22fa04a224328c32fbc7ff488808de Mon Sep 17 00:00:00 2001
From: Remco de Boer <29308176+redeboer@users.noreply.github.com>
Date: Mon, 14 Sep 2026 13:20:33 +0200
Subject: [PATCH 27/27] DOC: add link to PDG issue
---
docs/usage/particle.ipynb | 2 +-
tests/unit/test_pdg_adapter.py | 2 --
2 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/docs/usage/particle.ipynb b/docs/usage/particle.ipynb
index 819f2dfb..3c0565f4 100644
--- a/docs/usage/particle.ipynb
+++ b/docs/usage/particle.ipynb
@@ -74,7 +74,7 @@
"metadata": {},
"source": [
":::{warning}\n",
- "As of writing the `pdg` package does not support LaTeX names for particles.\n",
+ "As of writing the `pdg` package does not support LaTeX names for particles ([particledatagroup/api#42](https://github.com/particledatagroup/api/issues/42)).\n",
":::"
]
},
diff --git a/tests/unit/test_pdg_adapter.py b/tests/unit/test_pdg_adapter.py
index 6b410782..b1f27a59 100644
--- a/tests/unit/test_pdg_adapter.py
+++ b/tests/unit/test_pdg_adapter.py
@@ -178,7 +178,6 @@ def it_uses_antiparticle_mass_if_particle_has_no_mass():
type(source).mass = PropertyMock(side_effect=PdgNoDataError("no mass"))
source.antiparticle.has_mass_entry = True
source.antiparticle.mass = 0.5
-
assert _to_mass(source) == 0.5
@@ -191,5 +190,4 @@ def it_uses_antiparticle_width_if_particle_has_no_decay_data():
source.antiparticle.has_width_entry = True
source.antiparticle.has_lifetime_entry = False
source.antiparticle.width = 0.25
-
assert _to_width(source) == 0.25