From 65d3bc41250348a9ef7cc74e96803d3c347bb1b8 Mon Sep 17 00:00:00 2001 From: lavanyagoyal12 Date: Fri, 16 Oct 2026 05:18:47 +0000 Subject: [PATCH 1/2] added SKILL.md file for particles --- agents/skills/particle/SKILL.md | 110 ++++++++++ .../particle/references/interoperability.md | 117 +++++++++++ .../references/particle-properties.md | 193 ++++++++++++++++++ agents/skills/particle/references/pdgid.md | 85 ++++++++ .../particle/references/quantum-numbers.md | 90 ++++++++ 5 files changed, 595 insertions(+) create mode 100644 agents/skills/particle/SKILL.md create mode 100644 agents/skills/particle/references/interoperability.md create mode 100644 agents/skills/particle/references/particle-properties.md create mode 100644 agents/skills/particle/references/pdgid.md create mode 100644 agents/skills/particle/references/quantum-numbers.md diff --git a/agents/skills/particle/SKILL.md b/agents/skills/particle/SKILL.md new file mode 100644 index 00000000..3ed95003 --- /dev/null +++ b/agents/skills/particle/SKILL.md @@ -0,0 +1,110 @@ +--- +name: particle-usage +description: Use the Scikit-HEP `particle` library from Python code — PDG particle data lookups and searches (`Particle`), PDG ID queries (`PDGID` and the `particle.pdgid` functions), particle literals, MC generator ID classes (`PythiaID`, `Geant3ID`, `Corsika7ID`), EvtGen/LHCb name conversion, nuclei, and width/lifetime helpers. Use when writing application code that needs particle masses, widths, quantum numbers, antiparticle relations, or MC-ID conversions. +--- + +# Using the `particle` library + +## Overview + +`particle` is a pure-Python Scikit-HEP package providing a pythonic interface to the +Particle Data Group (PDG) particle data tables and the standard PDG particle +identification (ID) numbering scheme. It has two central objects: + +- **`Particle`** — a full record for a particle (mass, width, lifetime, quantum + numbers, quark content, PDG/LaTeX/EvtGen/LHCb names, status), with powerful + search and look-up utilities. +- **`PDGID`** — an `int` subclass adding queries and classification on a PDG ID + (is it a meson/baryon/lepton/nucleus? which quarks? what charge and spin?), + mirrored by free-standing functions in `particle.pdgid`. + +It also provides classes for the identification codes used by MC generators +(`PythiaID`, `Geant3ID`, `Corsika7ID`), name conversion maps (EvtGen, LHCb), +and `Particle`/`PDGID` literals for common particles. + +All data tables ship inside the package and are loaded lazily: importing +`particle` does not read any data. The table is loaded on the first lookup +(`from_pdgid`, `findall`, `all`, …). + +## When to use it + +- You need PDG numbers — mass, width, lifetime, `c*tau` — in analysis/plotting code. +- You need to interpret or classify PDG IDs: meson/baryon/lepton/nucleus, quark + content, charge, spin. +- You need antiparticle relations or standardized names (PDG, LaTeX, HTML, + EvtGen, LHCb, programmatic). +- You need to convert between PDG IDs and MC generator IDs (Pythia, Geant3, + Corsika7). +- You want ready-made constants for common particles (literals). + +## Core concepts + +- **Two objects, one ID scheme.** `PDGID` wraps an integer; `Particle` wraps one + row of the PDG data tables. A negative PDG ID is the antiparticle; `-pid` + returns another `PDGID`. +- **The PDG ID encodes quantum numbers.** Classification, spin and quark content + are decoded from the ID digits; other properties (`charge`, `I`, `P`, `C`, + `G`, `rank`, `status`) come from the data tables. +- **Names.** `name` is display-ready (`'B+'`, `'B~0'`, `'K0'`), `pdg_name` is the + PDG table name *without* charge (`'B'`, `'K'`); LaTeX/HTML/EvtGen/programmatic + variants exist too. +- **Search semantics.** A bare string is a *fuzzy* substring search on `name`; + keyword arguments match *exactly*; values may be callables; + `particle=True/False` filters to particles/antiparticles. +- **Units.** Mass and width in MeV, `lifetime` in ns, `ctau` in mm. `charge`, + `J` and `I` are `Fraction`s, not floats. +- **Lazy table.** `Particle.all()`, `table_names()` and `table_loaded()` inspect + the loaded table; lookups trigger loading automatically. + +## Essential patterns + +```python +from particle import Particle, PDGID +from particle.pdgid import is_meson, has_charm + +p = Particle.from_pdgid(521) # look up by ID (see references/particle-properties.md) +Particle.findall("pi", charge=-1) # fuzzy name + exact charge +Particle.findall(lambda q: q.pdgid.has_bottom and q.charge == 0) +is_meson(211) # classify a bare number +has_charm(p.pdgid) # pass the PDGID, not the Particle +``` + +For literals (`Particle`/`PDGID` aliases) and MC-ID conversions, see the +reference files below. + +## Major gotchas + +- **`pdg_name` ≠ `name`** — `pdg_name` has no charge; `from_name` needs an + exact, unique match (`from_name("pi")` raises). +- **Exceptions** — `from_pdgid` raises `InvalidParticle` (malformed ID) or + `ParticleNotFound` (valid but not in the table); MC ID conversions raise + `MatchingIDNotFound` (a `ValueError`, imported from `particle.exceptions`). +- **`None` means unknown** — quarks/neutrinos have `mass=None`, quarks have + `width=None`; `lifetime`/`ctau` are `None` when the width is unknown, but + `inf` for stable particles (`width == 0`). +- **Fractions** — `charge`, `J`, `I` return `fractions.Fraction`; + `three_charge` is the int charge × 3. +- **`particle.pdgid` functions take numbers**, not `Particle` objects + (`is_meson(p.pdgid)`, not `is_meson(p)`). +- **Proton/neutron have two PDG IDs** (baryon form `2112`/`2212` and nucleus + form `1000000010`/`1000010010`); equality considers both, `from_name("p")` + returns the baryon form. +- **Converters** — the `particle.converters` BiMaps (`Pythia2PDGIDBiMap`, …) + are deprecated since 1.0.0; use `X.to_pdgid()`/`X.from_pdgid()` instead. + +## Reference files + +| File | Contents | +| --- | --- | +| [references/pdgid.md](references/pdgid.md) | `PDGID` queries, ID classification, validity, antiparticles, `particle.pdgid` functions | +| [references/particle-properties.md](references/particle-properties.md) | Particle lookups, properties, searches, literals, names, mass/width/lifetime, custom particles and data tables | +| [references/quantum-numbers.md](references/quantum-numbers.md) | Charge, spin, parity, isospin, flavour quantum numbers, and the related enums | +| [references/interoperability.md](references/interoperability.md) | MC generator IDs, EvtGen/LHCb names, `hepunits`, pandas/tabulate | + +## Documentation + +The repository's `README.md` is a full usage tutorial; `notebooks/ParticleDemo.ipynb` +contains worked examples. Docstrings on `Particle`, `PDGID`, `MCParticleID`, +`Corsika7ID` and the `particle.pdgid` functions carry precise semantics (units, +exceptions). `docs/CHANGELOG.md` lists user-visible changes and +`src/particle/data/README.rst` documents the bundled data files. diff --git a/agents/skills/particle/references/interoperability.md b/agents/skills/particle/references/interoperability.md new file mode 100644 index 00000000..09f9ca51 --- /dev/null +++ b/agents/skills/particle/references/interoperability.md @@ -0,0 +1,117 @@ +# Interoperability: MC IDs, names, hepunits, pandas + +## MC generator ID classes + +`PythiaID`, `Geant3ID` and `Corsika7ID` convert to and from `PDGID` +(`MatchingIDNotFound` is raised when no conversion exists; import it from +`particle.exceptions`). Geant4 uses the standard PDG IDs. + +```python +from particle import PDGID, PythiaID, Geant3ID, Corsika7ID, Particle + +pyid = PythiaID.from_pdgid(PDGID(9010221)) # +pyid.to_pdgid() # +pdg = Geant3ID(8).to_pdgid() # +Particle.from_pdgid(pdg) # pi+ +cid = Corsika7ID(5).to_pdgid() # +``` + +**Corsika7 specifics** (Corsika8 uses Geant3 IDs): + +```python +Corsika7ID.from_particle_description(6000) # (Corsika7ID(6), False) -> mu- +Corsika7ID.from_particle_description(-6000) # (Corsika7ID(6), True) — a (grand)mother particle +Corsika7ID(76).is_particle() # False — additional info, not a particle +Corsika7ID(76).name() # 'μ− add. info.' +``` + +Note: `Corsika7ID` arithmetic (e.g. `-Corsika7ID(5)`) returns a plain `int`, +unlike `-PDGID(...)`, which returns a `PDGID`. + +**Custom ID classes** are easy if you know the mapping: + +```python +from particle.mcid import MCParticleID + +class MyGeneratorID(MCParticleID): + _to_pdg_map = {1: 11, 2: -11, 3: 22} + +MyGeneratorID(3).to_pdgid() # +MyGeneratorID.from_pdgid(PDGID(-11)) # +``` + +The older converter objects in `particle.converters` (`Pythia2PDGIDBiMap`, +`Geant2PDGIDBiMap`, `Corsika72PDGIDBiMap`) are **deprecated since 1.0.0** — use +`to_pdgid()`/`from_pdgid()` instead. + +## EvtGen and LHCb names + +```python +Particle.from_evtgen_name("J/psi").pdgid # +Particle.from_pdgid(443).evtgen_name # 'J/psi' +from particle import lhcb +lhcb.to_lhcb_name(Particle.from_pdgid(-531)) # 'B_s~0' +lhcb.from_lhcb_name("B_s~0") # +``` + +The `particle.lhcb` module also exposes `LHCbName2PDGIDBiMap` for direct +bidirectional lookups. EvtGen names are the ones used in `.dec` decay files. + +## hepunits + +`particle` returns plain floats in HEP standard units (MeV, ns, mm). The +`hepunits` package (a strict dependency) provides unit-aware quantities that +compare naturally: + +```python +from particle import Particle +from hepunits import GeV, meter + +p = Particle.from_pdgid(521) +5.2 * GeV < p.mass < 5.3 * GeV # masses in MeV +p.ctau > 1 * meter # ctau in mm +``` + +## pandas and tabulate + +`Particle.to_list()` and `Particle.to_dict()` render a search as tabular data. +`to_list` returns rows with a header row first; `to_dict` is transposed (field +name → tuple of values). They accept the same filter/keyword arguments as +`findall`, plus `exclusive_fields`, `exclude_fields` and `n_rows`. + +```python +rows = Particle.to_list( + exclusive_fields=["pdgid", "name", "mass", "charge"], + filter_fn=lambda p: p.pdgid.is_lepton and p.charge != 0, + particle=True, +) +# rows[0] is the header; use with tabulate, or: +import pandas as pd +df = pd.DataFrame(rows[1:], columns=rows[0]) +``` + +## NumPy and awkward arrays + +`p.pdgid` is a `PDGID` (an `int` subclass). Convert with `int(p.pdgid)` when +you need a plain Python integer for NumPy/awkward arrays: + +```python +import numpy as np +from particle import Particle + +p = Particle.from_pdgid(521) +np.int64(int(p.pdgid)) # works fine +``` + +## Gotchas + +- **Deprecated BiMaps** — do not use `Pythia2PDGIDBiMap` etc. from + `particle.converters`; use the ID-class methods. +- **`MatchingIDNotFound`** import path is `particle.exceptions` (it subclasses + `ValueError`); it is not re-exported at the top level. +- **Corsika7ID is not an antiparticle-preserving int** — arithmetic returns + plain `int`s; negative Corsika IDs in files indicate mother particles, so use + `from_particle_description`. +- **`Particle` has no decay data** — for branching fractions and decay chains + use the separate `decaylanguage` package (see + [particle-properties.md](particle-properties.md)). diff --git a/agents/skills/particle/references/particle-properties.md b/agents/skills/particle/references/particle-properties.md new file mode 100644 index 00000000..30eeaa10 --- /dev/null +++ b/agents/skills/particle/references/particle-properties.md @@ -0,0 +1,193 @@ +# Particle: lookups, properties, searches and custom particles + +`Particle` is a full record for one row of the PDG particle data tables. Prefer +the class methods for lookups; never guess properties by hand. + +## Getting a particle + +```python +from particle import Particle + +p = Particle.from_pdgid(521) # B+ (raises InvalidParticle / ParticleNotFound) +p = Particle.from_name("J/psi(1S)") # exact, must match exactly one particle +p = Particle.from_evtgen_name("J/psi") +p = Particle.from_nucleus_info(z=2, a=4) # He4; anti=True for the antinucleus +``` + +- `from_pdgid` raises `InvalidParticle` for malformed codes and + `ParticleNotFound` for valid codes not present in the loaded tables. +- `from_name` requires an **exact, unique** match on `name`; ambiguous names + raise `ParticleNotFound` (`from_name("pi")` fails because many particles + start with `pi`). +- `from_nucleus_info(z, a, ...)` builds the nucleus ID from the atomic number + and mass number; `z > a` or out-of-range inputs raise `InvalidParticle`. + +## Useful properties + +```python +p.pdgid # the PDGID +p.name # 'B+', 'B~0', 'K0', ... (charge + anti-tilde included) +p.pdg_name # 'B', 'K' — the PDG table name, WITHOUT charge +p.evtgen_name # EvtGen name, e.g. 'B+' +p.latex_name # r'B^{+}' +p.html_name # HTML rendering of the LaTeX name +p.programmatic_name # 'B_plus', 'B_0' — safe as a variable name +p.mass, p.mass_upper, p.mass_lower # in MeV; None if unknown +p.width, p.width_upper, p.width_lower # in MeV; None if unknown +p.lifetime # in ns (hbar/Gamma); inf for stable particles +p.ctau # in mm +p.charge # Fraction; p.three_charge is the int 3*charge +p.quarks, p.rank, p.status, p.anti_flag +p.is_name_barred, p.is_self_conjugate +p.describe() # high-density human-readable summary +``` + +Quantum numbers (`I`, `J`, `L`, `S`, `P`, `C`, `G`, `spin_type`, flavour +numbers, …) are covered in [quantum-numbers.md](quantum-numbers.md). + +Antiparticles: `p.invert()` (also `-p` and `~p`). Sorting a list of particles +puts the lowest `abs(PDGID)` first. `str(p)`/`repr(p)` are display-ready. + +## Search and look-up + +`Particle.findall(...)` returns a list, `Particle.finditer(...)` an iterator. +The first positional argument is either a callable `lambda p: ...` or a string, +which is matched **fuzzily** (substring) against the particle `name`. Keyword +arguments match **exactly** against any attribute/property and may themselves +be callables. + +```python +from hepunits import meter + +Particle.findall("pi", charge=-1) # fuzzy name + exact charge +Particle.findall(pdg_name="B", particle=True) # exact match on PDG name +Particle.findall(pdg_name="B", charge=0, particle=True) +Particle.findall(lambda p: p.pdgid.has_bottom and p.charge == 0) +Particle.findall(lambda p: p.pdgid.is_meson and p.pdgid.has_strange + and p.ctau > 1 * meter, particle=True) +Particle.finditer(name="Omega", particle=False) # exact name search +``` + +`particle=True` keeps only particles, `particle=False` only antiparticles, and +`None` (default) both. Valid keyword terms include `pdg_name`, `name`, `mass`, +`mass_upper/lower`, `width`, `width_upper/lower`, `charge`, `three_charge`, +`anti_flag`, `rank`, `I`, `J`, `G`, `P`, `C`, `quarks`, `status`. A callable as +a value receives the property (e.g. `mass=lambda m: m is not None and m > 5.2*GeV`). + +### Fuzzy vs exact name search — a common trap + +A bare string is a fuzzy substring search on `name`; a keyword is exact: + +```python +Particle.finditer("Omega", particle=False) # fuzzy: several Omega states +Particle.finditer(name="Omega", particle=False) # exact: no match (name is 'Omega~+') +Particle.findall(pdg_name="Omega", particle=False) # exact PDG name: 1 match +``` + +The string match is a **literal substring** test, not a glob: `"K[+-]"` and +`"pi*"` match nothing. Use `fnmatch` (or a regex) inside a callable for +patterns: + +```python +import fnmatch +from particle import Particle + +Particle.findall(lambda p: fnmatch.fnmatch(p.name, "K[+-]")) # K+, K- +``` + +`finditer` is lazy — prefer it over `findall` for large scans. + +## Ready-made literals + +`particle.literals` holds `Particle` instances, `particle.pdgid.literals` +holds `PDGID` instances. Names use the programmatic form, e.g. `pi_plus`, +`D_0`, `Lambda_b_0` (check the module `__dir__`). Accessing a literal creates +the object on demand. + +```python +from particle import literals as lp +from particle.pdgid import literals as lid + +lp.pi_plus # +lid.pi_plus # +lp.Lambda_b_0.J # Fraction(1, 2) +``` + +## Width, lifetime and c*tau helpers + +Mass/width live in the table in MeV; lifetime and `ctau` are derived: + +```python +from particle import width_to_lifetime, lifetime_to_width + +width_to_lifetime(4.33e-10) # 0.00152 ns (input MeV, output ns) +lifetime_to_width(0.00152) # 4.33e-10 MeV +width_to_lifetime(0) # inf +# width_to_lifetime(-1) # -> ValueError +``` + +`lifetime` is `None` when the width is unknown, and `inf` when the width is 0 +(stable particles). `ctau` is in mm. + +There is **no `is_stable` property** — test the lifetime directly: + +```python +from particle import Particle + +def is_stable(p: Particle) -> bool: + return p.lifetime == float("inf") + +is_stable(Particle.from_pdgid(11)) # True (electron) +is_stable(Particle.from_pdgid(211)) # False (pion decays) +``` + +## Loading custom data tables + +The default table is `particle2026.csv` followed by `nuclei2026.csv`. Append or +replace via `Particle.load_table`; data files are reachable through +`particle.data.basepath` (works even when the package runs from a zipapp): + +```python +import particle.data as data + +Particle.load_table("my_particles.csv", append=True) # extend (file must exist; same CSV layout) +Particle.load_table("my_table.csv") # replace (default append=False) +Particle.load_table(data.basepath / "particle2026.csv") +Particle.table_names() # names of loaded tables (triggers loading if needed) +Particle.table_loaded() # check without triggering a load +``` + +## Creating user-defined particles + +`Particle(...)` takes `(pdgid, pdg_name)` positionally plus optional fields. +Use sparingly: a meaningful PDG ID encodes quantum numbers, so a "made-up" ID +gives undefined or wrong properties, and `mass`/`width` are `None` unless you +supply them. + +```python +p = Particle.empty() # +p = Particle(9912345, "MyPentaquark") +p = Particle(pdgid=9221132, pdg_name="Theta", three_charge=3, latex_name=r"\Theta^{+}") +``` + +Hand-built particles are **not** in the table (`findall` won't return them) +unless added via `load_table`. + +## Gotchas + +- **`pdg_name` ≠ `name`.** The PDG table name omits the charge; `name` adds it + (and a `~` for barred antiparticles, e.g. `pdg_name='B'` → `name='B+'`). + Use `name` for display and `pdg_name` for exact table-name matching. +- **`from_name` needs a unique exact match** — use `from_pdgid` or + `findall`/`finditer` for ambiguous cases. +- **`None` means unknown.** Quarks/neutrinos have `mass=None`; quarks have + `width=None`; `lifetime`/`ctau` are `None` when the width is unknown. Filter + lambdas that touch these may raise `TypeError` — `findall` skips such + particles, so write filters defensively. +- **Stable particles report infinite lifetime/ctau** (`width == 0` gives + `inf`), not `None`; there is no `is_stable` — test `p.lifetime == float("inf")`. +- **No decay modes.** `Particle` carries identity, mass, width and quantum + numbers — there is no `decay_modes`/branching-fraction attribute. For decay + chains and branching fractions use the separate `decaylanguage` package. +- **Hand-built particles** have `None` mass/width and properties derived from + the ID; they are not in the table unless added via `load_table`. diff --git a/agents/skills/particle/references/pdgid.md b/agents/skills/particle/references/pdgid.md new file mode 100644 index 00000000..e6e71a3a --- /dev/null +++ b/agents/skills/particle/references/pdgid.md @@ -0,0 +1,85 @@ +# PDGID: queries, classification and antiparticles + +`PDGID` is a subclass of `int`, so it can be used anywhere an `int` is +expected. A negative ID represents the antiparticle. + +```python +from particle import PDGID, Particle + +pid = PDGID(211) +pid.is_meson # True +pid.charge # Fraction(1, 1) (a Fraction, not a float) +pid.J # Fraction(0, 1) +pid.has_bottom # False +pid.is_valid # True +PDGID(99999999).is_valid # False +-PDGID(211) # +~PDGID(211) # also +PDGID(323).info() # one property per line, for inspection +``` + +## What a PDG ID encodes + +The PDG numbering scheme is a signed integer whose digits encode the particle's +quantum numbers (e.g. meson spin, orbital angular momentum, quark content, +nucleus composition). This means classification and spin are decoded *from the +ID*, and `PDGID` never needs the data tables. + +Properties and their meaning: + +- **Classification**: `is_meson`, `is_baryon`, `is_lepton`, `is_quark`, + `is_hadron`, `is_nucleus`, `is_pentaquark`, `is_diquark`, + `is_gauge_boson_or_higgs`, `is_sm_*` (Standard Model subset), `is_SUSY`, + `is_technicolor`, `is_special_particle`, `is_generator_specific`, + `is_Qball`, `is_Rhadron`, `is_excited_quark_or_lepton`, `is_dyon`. +- **Flavour content**: `has_up`, `has_down`, `has_strange`, `has_charm`, + `has_bottom`, `has_top`, `has_fundamental_anti`. +- **Charge and spin**: `charge` (Fraction), `three_charge` (int × 3), + `j_spin`, `l_spin`, `s_spin`, `J`, `L`, `S`, `A`, `Z`, `abspid`. + +## Validity + +`PDGID.is_valid` is `False` for codes outside the PDG scheme (e.g. +`PDGID(99999999)`); its repr shows `(is_valid==False)`. Raising operations such +as `Particle.from_pdgid` check validity and raise `InvalidParticle` for invalid +codes (see [particle-properties.md](particle-properties.md)). + +## Antiparticles + +- Negative IDs are antiparticles. `-PDGID(211)` gives ``. +- `~pid` is an alias for `-pid` and returns a `PDGID` too. +- `abspid` strips the sign (e.g. for keying or sorting). +- Note: `PDGID` operations preserve the class (`-pid` is a `PDGID`), unlike + `Corsika7ID` arithmetic (see [interoperability.md](interoperability.md)). + +## Free-standing `particle.pdgid` functions + +Every `PDGID` property exists as a standalone function in `particle.pdgid`, +operating on any int-like value (useful for composition): + +```python +from particle.pdgid import is_meson, has_charm, has_bottom +from particle import Particle + +is_meson(211) # True (plain int works) +is_meson(Particle.from_pdgid(211).pdgid) # pass the PDGID, not the Particle +has_charm(4122) # True +is_heavy = lambda x: has_charm(x) or has_bottom(x) +``` + +Important: these functions accept a `SupportsInt` — pass `p.pdgid`, **not** the +`Particle` object (passing a `Particle` raises `TypeError`). + +## Gotchas + +- **Two PDG IDs for proton and neutron.** The baryon forms `2112`/`2212` + and the nucleus forms `1000000010`/`1000010010` describe the same particles. + `Particle.__eq__` treats both as equal, but lookup/hash prefer the baryon + form; `Particle.from_name("p")` returns the baryon representation, while + `Particle.findall(pdg_name="p")` returns both. +- **A "random" ID gives wrong or `None` properties.** Quantum numbers are + decoded from the digits, so inventing an ID yields inconsistent results + (e.g. `Particle(9912345, "X").pdgid.is_pentaquark` is `False`). +- **`J`/`L`/`S` may be `None`** for non-mesons; `is_valid` is `False` for + out-of-scheme codes. Prefer looking particles up from the table rather than + constructing IDs by hand. diff --git a/agents/skills/particle/references/quantum-numbers.md b/agents/skills/particle/references/quantum-numbers.md new file mode 100644 index 00000000..8486d410 --- /dev/null +++ b/agents/skills/particle/references/quantum-numbers.md @@ -0,0 +1,90 @@ +# Quantum numbers and classification + +## Where they come from + +- **From the data tables**: charge (`three_charge`), isospin `I`, `P`, `C`, + `G`, `rank`, `status`. +- **Decoded from the PDG ID**: total spin `J`, orbital angular momentum `L`, + spin `S` (mesons), and the quark content used by `PDGID` predicates. +- **Derived**: `spin_type`, `baryon_number`, `lepton_number`, `r_parity`, + `strangeness`, `charmness`, `bottomness`, `topness`, `hypercharge`, + `is_self_conjugate`, `is_unflavoured_meson`. + +## Charge + +```python +from particle import Particle + +p = Particle.from_pdgid(521) +p.charge # Fraction(1, 1) — in units of the positron charge +p.three_charge # 3 — int, 3 × charge; None if unknown +``` + +- `charge` is a `fractions.Fraction` (since 1.0.0); compare with `== 0` or + convert with `float()`. +- `three_charge` is the int charge × 3 and matches the `Charge` enum + (`Charge.p` = 3, `Charge.o` = 0, …); it can be used as a search term: + `Particle.findall("pi", three_charge=Charge.p)`. + +## Spin, parity, isospin + +```python +from particle import Particle + +p = Particle.from_pdgid(443) # J/psi +p.J # Fraction(1, 1) +p.L, p.S # meson orbital/spin quantum numbers, or None +p.I # Fraction(1, 2) — isospin +p.P, p.C, p.G # Parity enums (Parity.p / Parity.m / Parity.u) +p.spin_type # SpinType.Vector +``` + +- `J`, `L`, `S` are read off the PDG ID; `L`/`S` are `None` for non-mesons, + and `J` is `None` when not meaningful. +- `P`, `C`, `G` are `Parity` enum values (`.p` = `+`, `.m` = `-`, `.u` = + unknown/not applicable); they compare equal to their int values. +- `spin_type` (`Scalar`, `PseudoScalar`, `Vector`, `Axial`, `Tensor`, + `PseudoTensor`) is relevant for bosons only; `SpinType.NonDefined` is + returned otherwise, `SpinType.Unknown` when `(J, P)` is not known. + +## Baryon, lepton and flavour numbers + +```python +from particle import Particle + +Particle.from_pdgid(2212).baryon_number # 1 (proton) +Particle.from_pdgid(11).lepton_number # 1 (electron); -1 for e+ +Particle.from_pdgid(521).bottomness # 1 (B+) +Particle.from_pdgid(321).strangeness # 1 (K+) +Particle.from_pdgid(443).charmness # 0 (J/psi is charmonium: net 0) +p = Particle.from_pdgid(443) +p.r_parity, p.hypercharge, p.topness, p.charmness # 1, 0, 0, 0 +p.is_self_conjugate # True for e.g. pi0, J/psi +p.is_unflavoured_meson # True for e.g. J/psi +``` + +These satisfy the usual relations — for example +`hypercharge = baryon_number + strangeness + charmness + bottomness + topness` +(as defined in the `hypercharge` docstring). + +## Related enums + +- **`Charge`** — charge × 3 (`Charge.pp`, `Charge.p`, `Charge.o`, …). +- **`Parity`** — `p` (+), `m` (−), `u` (unknown/relevant). +- **`SpinType`** — boson spin-parity families; `Unknown`, `NonDefined`. +- **`Inv`** — antiparticle behaviour: `Same` (self-conjugate), `Barred` + (bar, e.g. proton), `ChargeInv` (charge flip, e.g. pi+ ↔ pi−). +- **`Status`** — PDG status: `Common` (established), `Rare`, `Unsure`, + `Further`, `NotInPDT` (non-standard/exotic). + +## Gotchas + +- **Quantum numbers can disagree with the name/table for hand-built IDs** — + `J`/`L`/`S` and quark content come from the ID encoding, so a fabricated ID + yields inconsistent results. Use table lookups. +- **`charge`/`J`/`I` are `Fraction`s** — don't compare with `== 1.0` relying on + float exactness; use `== 0`/`== 1` or `float()`. +- **`P`, `C`, `G` may be `Parity.u`** ("unknown/not applicable") rather than + missing — check against the enum, not `None`. +- **`spin_type` is for bosons** — fermions and undefined spin states return + `SpinType.NonDefined`/`SpinType.Unknown`. From e5e1014de7936ad62d976dafe50f7a3dfc6699ee Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 05:52:36 +0000 Subject: [PATCH 2/2] style: pre-commit fixes --- agents/skills/particle/SKILL.md | 24 +++--- .../particle/references/interoperability.md | 42 +++++----- .../references/particle-properties.md | 76 ++++++++++--------- agents/skills/particle/references/pdgid.md | 28 +++---- .../particle/references/quantum-numbers.md | 32 ++++---- 5 files changed, 107 insertions(+), 95 deletions(-) diff --git a/agents/skills/particle/SKILL.md b/agents/skills/particle/SKILL.md index 3ed95003..0a9fa82f 100644 --- a/agents/skills/particle/SKILL.md +++ b/agents/skills/particle/SKILL.md @@ -46,10 +46,10 @@ All data tables ship inside the package and are loaded lazily: importing are decoded from the ID digits; other properties (`charge`, `I`, `P`, `C`, `G`, `rank`, `status`) come from the data tables. - **Names.** `name` is display-ready (`'B+'`, `'B~0'`, `'K0'`), `pdg_name` is the - PDG table name *without* charge (`'B'`, `'K'`); LaTeX/HTML/EvtGen/programmatic + PDG table name _without_ charge (`'B'`, `'K'`); LaTeX/HTML/EvtGen/programmatic variants exist too. -- **Search semantics.** A bare string is a *fuzzy* substring search on `name`; - keyword arguments match *exactly*; values may be callables; +- **Search semantics.** A bare string is a _fuzzy_ substring search on `name`; + keyword arguments match _exactly_; values may be callables; `particle=True/False` filters to particles/antiparticles. - **Units.** Mass and width in MeV, `lifetime` in ns, `ctau` in mm. `charge`, `J` and `I` are `Fraction`s, not floats. @@ -62,11 +62,11 @@ All data tables ship inside the package and are loaded lazily: importing from particle import Particle, PDGID from particle.pdgid import is_meson, has_charm -p = Particle.from_pdgid(521) # look up by ID (see references/particle-properties.md) -Particle.findall("pi", charge=-1) # fuzzy name + exact charge +p = Particle.from_pdgid(521) # look up by ID (see references/particle-properties.md) +Particle.findall("pi", charge=-1) # fuzzy name + exact charge Particle.findall(lambda q: q.pdgid.has_bottom and q.charge == 0) -is_meson(211) # classify a bare number -has_charm(p.pdgid) # pass the PDGID, not the Particle +is_meson(211) # classify a bare number +has_charm(p.pdgid) # pass the PDGID, not the Particle ``` For literals (`Particle`/`PDGID` aliases) and MC-ID conversions, see the @@ -94,12 +94,12 @@ reference files below. ## Reference files -| File | Contents | -| --- | --- | -| [references/pdgid.md](references/pdgid.md) | `PDGID` queries, ID classification, validity, antiparticles, `particle.pdgid` functions | +| File | Contents | +| ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| [references/pdgid.md](references/pdgid.md) | `PDGID` queries, ID classification, validity, antiparticles, `particle.pdgid` functions | | [references/particle-properties.md](references/particle-properties.md) | Particle lookups, properties, searches, literals, names, mass/width/lifetime, custom particles and data tables | -| [references/quantum-numbers.md](references/quantum-numbers.md) | Charge, spin, parity, isospin, flavour quantum numbers, and the related enums | -| [references/interoperability.md](references/interoperability.md) | MC generator IDs, EvtGen/LHCb names, `hepunits`, pandas/tabulate | +| [references/quantum-numbers.md](references/quantum-numbers.md) | Charge, spin, parity, isospin, flavour quantum numbers, and the related enums | +| [references/interoperability.md](references/interoperability.md) | MC generator IDs, EvtGen/LHCb names, `hepunits`, pandas/tabulate | ## Documentation diff --git a/agents/skills/particle/references/interoperability.md b/agents/skills/particle/references/interoperability.md index 09f9ca51..d79ebad9 100644 --- a/agents/skills/particle/references/interoperability.md +++ b/agents/skills/particle/references/interoperability.md @@ -9,20 +9,22 @@ ```python from particle import PDGID, PythiaID, Geant3ID, Corsika7ID, Particle -pyid = PythiaID.from_pdgid(PDGID(9010221)) # -pyid.to_pdgid() # -pdg = Geant3ID(8).to_pdgid() # -Particle.from_pdgid(pdg) # pi+ -cid = Corsika7ID(5).to_pdgid() # +pyid = PythiaID.from_pdgid(PDGID(9010221)) # +pyid.to_pdgid() # +pdg = Geant3ID(8).to_pdgid() # +Particle.from_pdgid(pdg) # pi+ +cid = Corsika7ID(5).to_pdgid() # ``` **Corsika7 specifics** (Corsika8 uses Geant3 IDs): ```python -Corsika7ID.from_particle_description(6000) # (Corsika7ID(6), False) -> mu- -Corsika7ID.from_particle_description(-6000) # (Corsika7ID(6), True) — a (grand)mother particle -Corsika7ID(76).is_particle() # False — additional info, not a particle -Corsika7ID(76).name() # 'μ− add. info.' +Corsika7ID.from_particle_description(6000) # (Corsika7ID(6), False) -> mu- +Corsika7ID.from_particle_description( + -6000 +) # (Corsika7ID(6), True) — a (grand)mother particle +Corsika7ID(76).is_particle() # False — additional info, not a particle +Corsika7ID(76).name() # 'μ− add. info.' ``` Note: `Corsika7ID` arithmetic (e.g. `-Corsika7ID(5)`) returns a plain `int`, @@ -33,11 +35,13 @@ unlike `-PDGID(...)`, which returns a `PDGID`. ```python from particle.mcid import MCParticleID + class MyGeneratorID(MCParticleID): _to_pdg_map = {1: 11, 2: -11, 3: 22} -MyGeneratorID(3).to_pdgid() # -MyGeneratorID.from_pdgid(PDGID(-11)) # + +MyGeneratorID(3).to_pdgid() # +MyGeneratorID.from_pdgid(PDGID(-11)) # ``` The older converter objects in `particle.converters` (`Pythia2PDGIDBiMap`, @@ -47,11 +51,12 @@ The older converter objects in `particle.converters` (`Pythia2PDGIDBiMap`, ## EvtGen and LHCb names ```python -Particle.from_evtgen_name("J/psi").pdgid # -Particle.from_pdgid(443).evtgen_name # 'J/psi' +Particle.from_evtgen_name("J/psi").pdgid # +Particle.from_pdgid(443).evtgen_name # 'J/psi' from particle import lhcb -lhcb.to_lhcb_name(Particle.from_pdgid(-531)) # 'B_s~0' -lhcb.from_lhcb_name("B_s~0") # + +lhcb.to_lhcb_name(Particle.from_pdgid(-531)) # 'B_s~0' +lhcb.from_lhcb_name("B_s~0") # ``` The `particle.lhcb` module also exposes `LHCbName2PDGIDBiMap` for direct @@ -68,8 +73,8 @@ from particle import Particle from hepunits import GeV, meter p = Particle.from_pdgid(521) -5.2 * GeV < p.mass < 5.3 * GeV # masses in MeV -p.ctau > 1 * meter # ctau in mm +5.2 * GeV < p.mass < 5.3 * GeV # masses in MeV +p.ctau > 1 * meter # ctau in mm ``` ## pandas and tabulate @@ -87,6 +92,7 @@ rows = Particle.to_list( ) # rows[0] is the header; use with tabulate, or: import pandas as pd + df = pd.DataFrame(rows[1:], columns=rows[0]) ``` @@ -100,7 +106,7 @@ import numpy as np from particle import Particle p = Particle.from_pdgid(521) -np.int64(int(p.pdgid)) # works fine +np.int64(int(p.pdgid)) # works fine ``` ## Gotchas diff --git a/agents/skills/particle/references/particle-properties.md b/agents/skills/particle/references/particle-properties.md index 30eeaa10..a3f5aa4e 100644 --- a/agents/skills/particle/references/particle-properties.md +++ b/agents/skills/particle/references/particle-properties.md @@ -8,10 +8,10 @@ the class methods for lookups; never guess properties by hand. ```python from particle import Particle -p = Particle.from_pdgid(521) # B+ (raises InvalidParticle / ParticleNotFound) -p = Particle.from_name("J/psi(1S)") # exact, must match exactly one particle +p = Particle.from_pdgid(521) # B+ (raises InvalidParticle / ParticleNotFound) +p = Particle.from_name("J/psi(1S)") # exact, must match exactly one particle p = Particle.from_evtgen_name("J/psi") -p = Particle.from_nucleus_info(z=2, a=4) # He4; anti=True for the antinucleus +p = Particle.from_nucleus_info(z=2, a=4) # He4; anti=True for the antinucleus ``` - `from_pdgid` raises `InvalidParticle` for malformed codes and @@ -25,21 +25,21 @@ p = Particle.from_nucleus_info(z=2, a=4) # He4; anti=True for the antinucle ## Useful properties ```python -p.pdgid # the PDGID -p.name # 'B+', 'B~0', 'K0', ... (charge + anti-tilde included) -p.pdg_name # 'B', 'K' — the PDG table name, WITHOUT charge -p.evtgen_name # EvtGen name, e.g. 'B+' -p.latex_name # r'B^{+}' -p.html_name # HTML rendering of the LaTeX name +p.pdgid # the PDGID +p.name # 'B+', 'B~0', 'K0', ... (charge + anti-tilde included) +p.pdg_name # 'B', 'K' — the PDG table name, WITHOUT charge +p.evtgen_name # EvtGen name, e.g. 'B+' +p.latex_name # r'B^{+}' +p.html_name # HTML rendering of the LaTeX name p.programmatic_name # 'B_plus', 'B_0' — safe as a variable name -p.mass, p.mass_upper, p.mass_lower # in MeV; None if unknown -p.width, p.width_upper, p.width_lower # in MeV; None if unknown -p.lifetime # in ns (hbar/Gamma); inf for stable particles -p.ctau # in mm -p.charge # Fraction; p.three_charge is the int 3*charge +p.mass, p.mass_upper, p.mass_lower # in MeV; None if unknown +p.width, p.width_upper, p.width_lower # in MeV; None if unknown +p.lifetime # in ns (hbar/Gamma); inf for stable particles +p.ctau # in mm +p.charge # Fraction; p.three_charge is the int 3*charge p.quarks, p.rank, p.status, p.anti_flag p.is_name_barred, p.is_self_conjugate -p.describe() # high-density human-readable summary +p.describe() # high-density human-readable summary ``` Quantum numbers (`I`, `J`, `L`, `S`, `P`, `C`, `G`, `spin_type`, flavour @@ -59,13 +59,15 @@ be callables. ```python from hepunits import meter -Particle.findall("pi", charge=-1) # fuzzy name + exact charge -Particle.findall(pdg_name="B", particle=True) # exact match on PDG name +Particle.findall("pi", charge=-1) # fuzzy name + exact charge +Particle.findall(pdg_name="B", particle=True) # exact match on PDG name Particle.findall(pdg_name="B", charge=0, particle=True) Particle.findall(lambda p: p.pdgid.has_bottom and p.charge == 0) -Particle.findall(lambda p: p.pdgid.is_meson and p.pdgid.has_strange - and p.ctau > 1 * meter, particle=True) -Particle.finditer(name="Omega", particle=False) # exact name search +Particle.findall( + lambda p: p.pdgid.is_meson and p.pdgid.has_strange and p.ctau > 1 * meter, + particle=True, +) +Particle.finditer(name="Omega", particle=False) # exact name search ``` `particle=True` keeps only particles, `particle=False` only antiparticles, and @@ -79,7 +81,7 @@ a value receives the property (e.g. `mass=lambda m: m is not None and m > 5.2*Ge A bare string is a fuzzy substring search on `name`; a keyword is exact: ```python -Particle.finditer("Omega", particle=False) # fuzzy: several Omega states +Particle.finditer("Omega", particle=False) # fuzzy: several Omega states Particle.finditer(name="Omega", particle=False) # exact: no match (name is 'Omega~+') Particle.findall(pdg_name="Omega", particle=False) # exact PDG name: 1 match ``` @@ -92,7 +94,7 @@ patterns: import fnmatch from particle import Particle -Particle.findall(lambda p: fnmatch.fnmatch(p.name, "K[+-]")) # K+, K- +Particle.findall(lambda p: fnmatch.fnmatch(p.name, "K[+-]")) # K+, K- ``` `finditer` is lazy — prefer it over `findall` for large scans. @@ -108,9 +110,9 @@ the object on demand. from particle import literals as lp from particle.pdgid import literals as lid -lp.pi_plus # -lid.pi_plus # -lp.Lambda_b_0.J # Fraction(1, 2) +lp.pi_plus # +lid.pi_plus # +lp.Lambda_b_0.J # Fraction(1, 2) ``` ## Width, lifetime and c*tau helpers @@ -120,9 +122,9 @@ Mass/width live in the table in MeV; lifetime and `ctau` are derived: ```python from particle import width_to_lifetime, lifetime_to_width -width_to_lifetime(4.33e-10) # 0.00152 ns (input MeV, output ns) -lifetime_to_width(0.00152) # 4.33e-10 MeV -width_to_lifetime(0) # inf +width_to_lifetime(4.33e-10) # 0.00152 ns (input MeV, output ns) +lifetime_to_width(0.00152) # 4.33e-10 MeV +width_to_lifetime(0) # inf # width_to_lifetime(-1) # -> ValueError ``` @@ -134,11 +136,13 @@ There is **no `is_stable` property** — test the lifetime directly: ```python from particle import Particle + def is_stable(p: Particle) -> bool: return p.lifetime == float("inf") -is_stable(Particle.from_pdgid(11)) # True (electron) -is_stable(Particle.from_pdgid(211)) # False (pion decays) + +is_stable(Particle.from_pdgid(11)) # True (electron) +is_stable(Particle.from_pdgid(211)) # False (pion decays) ``` ## Loading custom data tables @@ -150,11 +154,13 @@ replace via `Particle.load_table`; data files are reachable through ```python import particle.data as data -Particle.load_table("my_particles.csv", append=True) # extend (file must exist; same CSV layout) -Particle.load_table("my_table.csv") # replace (default append=False) +Particle.load_table( + "my_particles.csv", append=True +) # extend (file must exist; same CSV layout) +Particle.load_table("my_table.csv") # replace (default append=False) Particle.load_table(data.basepath / "particle2026.csv") -Particle.table_names() # names of loaded tables (triggers loading if needed) -Particle.table_loaded() # check without triggering a load +Particle.table_names() # names of loaded tables (triggers loading if needed) +Particle.table_loaded() # check without triggering a load ``` ## Creating user-defined particles @@ -165,7 +171,7 @@ gives undefined or wrong properties, and `mass`/`width` are `None` unless you supply them. ```python -p = Particle.empty() # +p = Particle.empty() # p = Particle(9912345, "MyPentaquark") p = Particle(pdgid=9221132, pdg_name="Theta", three_charge=3, latex_name=r"\Theta^{+}") ``` diff --git a/agents/skills/particle/references/pdgid.md b/agents/skills/particle/references/pdgid.md index e6e71a3a..47e02c63 100644 --- a/agents/skills/particle/references/pdgid.md +++ b/agents/skills/particle/references/pdgid.md @@ -7,23 +7,23 @@ expected. A negative ID represents the antiparticle. from particle import PDGID, Particle pid = PDGID(211) -pid.is_meson # True -pid.charge # Fraction(1, 1) (a Fraction, not a float) -pid.J # Fraction(0, 1) -pid.has_bottom # False -pid.is_valid # True -PDGID(99999999).is_valid # False --PDGID(211) # -~PDGID(211) # also -PDGID(323).info() # one property per line, for inspection +pid.is_meson # True +pid.charge # Fraction(1, 1) (a Fraction, not a float) +pid.J # Fraction(0, 1) +pid.has_bottom # False +pid.is_valid # True +PDGID(99999999).is_valid # False +-PDGID(211) # +~PDGID(211) # also +PDGID(323).info() # one property per line, for inspection ``` ## What a PDG ID encodes The PDG numbering scheme is a signed integer whose digits encode the particle's quantum numbers (e.g. meson spin, orbital angular momentum, quark content, -nucleus composition). This means classification and spin are decoded *from the -ID*, and `PDGID` never needs the data tables. +nucleus composition). This means classification and spin are decoded _from the +ID_, and `PDGID` never needs the data tables. Properties and their meaning: @@ -61,9 +61,9 @@ operating on any int-like value (useful for composition): from particle.pdgid import is_meson, has_charm, has_bottom from particle import Particle -is_meson(211) # True (plain int works) -is_meson(Particle.from_pdgid(211).pdgid) # pass the PDGID, not the Particle -has_charm(4122) # True +is_meson(211) # True (plain int works) +is_meson(Particle.from_pdgid(211).pdgid) # pass the PDGID, not the Particle +has_charm(4122) # True is_heavy = lambda x: has_charm(x) or has_bottom(x) ``` diff --git a/agents/skills/particle/references/quantum-numbers.md b/agents/skills/particle/references/quantum-numbers.md index 8486d410..ed0ad580 100644 --- a/agents/skills/particle/references/quantum-numbers.md +++ b/agents/skills/particle/references/quantum-numbers.md @@ -16,8 +16,8 @@ from particle import Particle p = Particle.from_pdgid(521) -p.charge # Fraction(1, 1) — in units of the positron charge -p.three_charge # 3 — int, 3 × charge; None if unknown +p.charge # Fraction(1, 1) — in units of the positron charge +p.three_charge # 3 — int, 3 × charge; None if unknown ``` - `charge` is a `fractions.Fraction` (since 1.0.0); compare with `== 0` or @@ -31,12 +31,12 @@ p.three_charge # 3 — int, 3 × charge; None if unknown ```python from particle import Particle -p = Particle.from_pdgid(443) # J/psi -p.J # Fraction(1, 1) -p.L, p.S # meson orbital/spin quantum numbers, or None -p.I # Fraction(1, 2) — isospin -p.P, p.C, p.G # Parity enums (Parity.p / Parity.m / Parity.u) -p.spin_type # SpinType.Vector +p = Particle.from_pdgid(443) # J/psi +p.J # Fraction(1, 1) +p.L, p.S # meson orbital/spin quantum numbers, or None +p.I # Fraction(1, 2) — isospin +p.P, p.C, p.G # Parity enums (Parity.p / Parity.m / Parity.u) +p.spin_type # SpinType.Vector ``` - `J`, `L`, `S` are read off the PDG ID; `L`/`S` are `None` for non-mesons, @@ -52,15 +52,15 @@ p.spin_type # SpinType.Vector ```python from particle import Particle -Particle.from_pdgid(2212).baryon_number # 1 (proton) -Particle.from_pdgid(11).lepton_number # 1 (electron); -1 for e+ -Particle.from_pdgid(521).bottomness # 1 (B+) -Particle.from_pdgid(321).strangeness # 1 (K+) -Particle.from_pdgid(443).charmness # 0 (J/psi is charmonium: net 0) +Particle.from_pdgid(2212).baryon_number # 1 (proton) +Particle.from_pdgid(11).lepton_number # 1 (electron); -1 for e+ +Particle.from_pdgid(521).bottomness # 1 (B+) +Particle.from_pdgid(321).strangeness # 1 (K+) +Particle.from_pdgid(443).charmness # 0 (J/psi is charmonium: net 0) p = Particle.from_pdgid(443) -p.r_parity, p.hypercharge, p.topness, p.charmness # 1, 0, 0, 0 -p.is_self_conjugate # True for e.g. pi0, J/psi -p.is_unflavoured_meson # True for e.g. J/psi +p.r_parity, p.hypercharge, p.topness, p.charmness # 1, 0, 0, 0 +p.is_self_conjugate # True for e.g. pi0, J/psi +p.is_unflavoured_meson # True for e.g. J/psi ``` These satisfy the usual relations — for example