Skip to main content

ThresholdFinder

The distribution ships two packages:

  • threshold_finder (CLIs threshold-finder, qn-options) finds hadronic thresholds; see below.
  • gimodel (CLI gi-spectrum) computes Godfrey–Isgur quark-model meson spectra; see Quark-model spectrum.

Finds n-body hadronic thresholds compatible with given J^P quantum numbers. Given a mass range and a target J^P, it scans all combinations of PDG hadrons whose combined mass falls in that range and checks whether they can couple — via some total orbital angular momentum L — to produce the desired quantum numbers. The default is two-body; use --n-body (or n_body= in the API) to search three-body or higher final states.

Requirements

  • Python >= 3.11
  • particle >= 0.24
  • numpy and scipy (for the quark model, gimodel)
  • pdg >= 2026.0, the official PDG Python API (for gi-spectrum --match)
pip install thresholds

Usage

Command line

threshold-finder mass_min mass_max [J P] [options]

Positional arguments:

Argument Description
mass_min Lower bound of the threshold search range (MeV)
mass_max Upper bound of the threshold search range (MeV)
J Target total angular momentum (integer or half-integer, e.g. 1, 0.5). Optional if --particles is given.
P Target parity: +1 or -1. Optional if --particles is given.

J and P must either both be given or both be omitted. When omitted, --particles is required and the 3 lowest J^P combinations the pair can produce are used automatically.

Optional arguments:

Flag Default Description
--max-L L auto Maximum orbital angular momentum to consider. Without this flag, L is capped automatically at J + J₁ + J₂ + 4 for each pair.
--charge CHARGE 0 Required total electric charge of the two-particle system.
--status S [S ...] 0 PDG status codes to include: 0 = well-established, 1 = evidence but unconfirmed, 2 = omitted from summary tables.
--unique-pairs off Show each particle combination only once (keeping the lowest L), instead of one entry per valid L.
--n-body N 2 Number of particles in the final state (must be >= 2). Default is 2 (two-body). Use 3 for three-body, etc.

Flavor conservation flags (all optional, independent):

Flag Description
--u N Required net u-quark number of the pair (#u − #ū)
--d N Required net d-quark number of the pair (#d − #d̄)
--s N Required net s-quark number of the pair (#s − #s̄)
--c N Required net charm of the pair (#c − #c̄)
--b N Required net bottomness of the pair (#b − #b̄)
--particles P1 P2 Derive all flavor numbers automatically from two PDG particle names

Only the flags you provide are enforced. Omit a flag to leave that flavor unconstrained. Pairs involving particles with undefined quark content (e.g. η, ω — mixed states like uū+dd̄) are excluded when any flavor flag is set.

--particles does two things:

  1. Derives flavor conservation automatically — no need to compute net quark numbers by hand. Explicit --u/--d/... flags override the derived values.
  2. Checks feasibility — if J and P are given, the tool first verifies that the specified pair can actually produce that J^P at some L. If not, a warning is printed and no results are shown for that J^P.
  3. Auto-detects J^P — if J and P are omitted, the 3 lowest J^P combinations the pair can produce (ordered by minimum L) are determined and each is searched separately.

Examples

Find all 1⁻ channels with threshold between 250 and 300 MeV (the ρ region):

$ python3 -m threshold_finder.cli 250 300 1 -1

Thresholds for J^P = 1^-  in [250.0, 300.0] MeV  (max L = ∞)
Found 1 combination(s):
  pi+ + pi-  threshold=279.1 MeV  L=1  J^P=1^-

Find 1⁻ channels near 1 GeV with full flavor conservation (all net quark numbers = 0):

$ python3 -m threshold_finder.cli 900 1100 1 -1 --u 0 --d 0 --s 0 --c 0 --b 0 --unique-pairs

Thresholds for J^P = 1^-  in [900.0, 1100.0] MeV  (max L = ∞)  flavor: u=+0, d=+0, s=+0, c=+0, b=+0
Found 4 combination(s):
  pi+ + rho(770)-  threshold=914.7 MeV  L=1  J^P=1^-
  pi- + rho(770)+  threshold=914.7 MeV  L=1  J^P=1^-
  K+ + K-  threshold=987.4 MeV  L=1  J^P=1^-
  K0 + K~0  threshold=995.2 MeV  L=1  J^P=1^-

Constrain only strangeness (leave u/d free) to find kaonic channels:

$ python3 -m threshold_finder.cli 600 700 0 -1 --s -1 --unique-pairs

Thresholds for J^P = 0^-  in [600.0, 700.0] MeV  (max L = ∞)  flavor: s=-1
Found 4 combination(s):
  pi0 + K(L)0  threshold=632.6 MeV  L=0  J^P=0^-
  ...

Find open-charm 1⁻ thresholds near ψ(3770) with net zero flavor:

$ python3 -m threshold_finder.cli 3700 3900 1 -1 --u 0 --d 0 --s 0 --c 0 --b 0 --unique-pairs

...
  D0 + D~0  threshold=3729.7 MeV  L=1  J^P=1^-
  D+ + D-   threshold=3739.3 MeV  L=1  J^P=1^-
  ...
  D0 + D*(2007)~0  threshold=3871.7 MeV  L=1  J^P=1^-
  ...

Omit J and P to auto-detect the 3 lowest J^P combinations D0 + Lambda can produce:

$ threshold-finder 2800 3000 --particles 'D0' 'Lambda' --unique-pairs

Flavor conservation derived from 'D0' + 'Lambda':
  u = +0
  d = +1
  s = +1
  c = +1

No J^P given — using the 3 lowest combinations 'D0' + 'Lambda' can produce:
  J^P = 1/2^-  (lowest at L=0)
  J^P = 1/2^+  (lowest at L=1)
  J^P = 3/2^+  (lowest at L=1)

Thresholds for J^P = 1/2^-  in [2800.0, 3000.0] MeV  ...
...
Thresholds for J^P = 1/2^+  in [2800.0, 3000.0] MeV  ...
...
Thresholds for J^P = 3/2^+  in [2800.0, 3000.0] MeV  ...
...

If the requested J^P cannot be produced by the given pair at any L, a warning is shown and that J^P is skipped:

$ threshold-finder 2800 3000 0.5 +1 --particles 'pi+' 'pi-'

WARNING: 'pi+' + 'pi-' cannot produce J^P = 1/2^+ at any L

Derive flavor numbers from reference particles (D0 + Lambda defines the channel):

$ threshold-finder 2800 3000 0.5 -1 --particles 'D0' 'Lambda' --unique-pairs

Flavor conservation derived from 'D0' + 'Lambda':
  u = +0
  d = +1
  s = +1
  c = +1

Thresholds for J^P = 1/2^-  in [2800.0, 3000.0] MeV  (max L = ∞)  flavor: u=+0, d=+1, s=+1, c=+1
Found 5 combination(s):
  pi- + Xi(c)(2790)+  threshold=2931.5 MeV  L=1  J^P=1/2^-
  K- + Sigma(c)(2455)+  threshold=2946.3 MeV  L=0  J^P=1/2^-
  ...

If the reference threshold is outside the search range, a warning is printed but the search still runs:

$ threshold-finder 2500 2800 0.5 -1 --particles 'D0' 'Lambda' --unique-pairs

WARNING: threshold of D0 + Lambda = 2980.5 MeV is above mass_max = 2800.0 MeV
Flavor conservation derived from 'D0' + 'Lambda':
  ...

If a particle has ambiguous quark content (mixed state), the tool reports what could be determined and prints a ready-to-edit command with ??? placeholders for the unknown flavors:

$ threshold-finder 2800 3000 0.5 -1 --particles 'eta' 'Lambda'

ERROR: Quark content is ambiguous (mixed/superposition state) for:
  eta  (PDG quarks string: 'x(uU+dD)+y(sS)')
Determined from ['Lambda']: d=+1, s=+1, u=+1

Set the remaining flavor flags manually. Example command:
  threshold-finder 2800.0 3000.0 0.5 -1 --u 1 --d 1 --s 1 --c ??? --b ???

If a particle name is not found, 5 suggestions are printed as ready-to-run commands:

$ threshold-finder 2800 3000 0.5 -1 --particles 'D0' 'Lmabda'

ERROR: Unknown particle 'Lmabda'
Did you mean one of these?
  threshold-finder 2800.0 3000.0 0.5 -1 --particles 'D0' 'Lambda'
  threshold-finder 2800.0 3000.0 0.5 -1 --particles 'D0' 'Lambda~'
  threshold-finder 2800.0 3000.0 0.5 -1 --particles 'D0' 'Lambda(c)+'
  threshold-finder 2800.0 3000.0 0.5 -1 --particles 'D0' 'Lambda(b)0'
  threshold-finder 2800.0 3000.0 0.5 -1 --particles 'D0' 'Lambda(1520)'

Find three-body 0⁻ thresholds near 420 MeV (the 3π region):

$ threshold-finder 400 450 0 -1 --n-body 3 --max-L 0 --unique-pairs

Thresholds for J^P = 0^-  in [400.0, 450.0] MeV  (max L = 0)  (3-body)
Found 2 combination(s):
  pi- + pi0 + pi+  threshold=414.1 MeV  L=0  J^P=0^-
  pi0 + pi0 + pi0  threshold=404.9 MeV  L=0  J^P=0^-

Find 2⁺ channels with threshold 500–700 MeV, restricting to L ≤ 2:

$ python3 -m threshold_finder.cli 500 700 2 +1 --max-L 2 --unique-pairs

Thresholds for J^P = 2^+  in [500.0, 700.0] MeV  (max L = 2)
Found 7 combination(s):
  pi0 + K(L)0  threshold=632.6 MeV  L=2  J^P=2^+
  ...

Python API

from threshold_finder import ThresholdFinder, FlavorFilter

finder = ThresholdFinder(
    mass_min=900,
    mass_max=1100,
    J_target=1,
    P_target=-1,
    n_body=2,                                # 2 = two-body (default); set 3 for three-body, etc.
    max_L=None,                              # None = automatic
    total_charge=0.0,
    flavor_filter=FlavorFilter(u=0, d=0, s=0, c=0, b=0),  # all net quark numbers = 0
    status_filter=(0,),                      # established particles only
)
result = finder.run()

print(result)  # formatted summary

for c in result.combinations:
    print(" + ".join(c.particles), "  L =", c.L, "  threshold =", c.threshold, "MeV")

Three-body search:

finder = ThresholdFinder(
    mass_min=400,
    mass_max=450,
    J_target=0,
    P_target=-1,
    n_body=3,
    max_L=0,
)
result = finder.run()
for c in result.combinations:
    print(" + ".join(c.particles), "  threshold =", c.threshold, "MeV")

Constrain only specific flavors by omitting the rest:

# Only require net charm = 0; u, d, s, b are unconstrained
flavor_filter=FlavorFilter(c=0)

# Only require net strangeness = -1
flavor_filter=FlavorFilter(s=-1)

ThresholdResult has the fields J_target, P_target, mass_min, mass_max, max_L, n_body, flavor_filter, and combinations (a list of CombinationResult).

Each CombinationResult contains:

Field Type Description
particles tuple[str, ...] PDG names of all particles in the final state
masses tuple[float, ...] Masses of all particles (MeV)
charges tuple[float, ...] Charges of all particles
spins tuple[float, ...] J values of all particles
parities tuple[int, ...] Parities of all particles
threshold float Sum of all particle masses (MeV)
L int Total orbital angular momentum
J_total float Total angular momentum (= J_target)
P_total int Total parity (= P_target)

For two-body results the legacy per-particle properties (particle1, particle2, mass1, mass2, charge1, charge2, J1, J2, P1, P2, identical) are still available as convenience aliases.

Physics

The tool checks whether a set of n particles (each with Jᵢ^Pᵢ) in a state of total orbital angular momentum L can produce the target J^P.

Parity:

P_total = P₁ · P₂ · … · Pₙ · (-1)^L

Angular momentum: J_total must be reachable by sequentially coupling all particle spins J₁ ⊗ J₂ ⊗ … ⊗ Jₙ via the triangle rule, then adding L.

Identical bosons (two-body only): For two identical bosons (e.g. π⁰π⁰), the spatial wave function must be symmetric under exchange, which requires L to be even. For n > 2 this constraint is not enforced (exact symmetrisation of n-body wave functions depends on the full Bose/Fermi statistics of all permutations).

Flavor conservation: Net quark numbers are computed as #quark − #antiquark for each flavor (u, d, s, c, b). They are additive over all n particles in the final state. Setting a flavor to 0 requires the combination to have no net quark content in that flavor. Particles with mixed or superposition quark content (η, ω, φ, π⁰, …) have undefined quark numbers and are excluded from any result when a flavor constraint is active.

Particle data (masses, J, P, charge, quark content) are read from the PDG via the particle package. Only hadrons with known mass, J, and P are considered.

Quark-model spectrum (gimodel, gi-spectrum)

A Python port of the core of GIModel.jl by M. Mikhasenko: the Godfrey–Isgur relativized quark model for meson spectra (S. Godfrey and N. Isgur, Phys. Rev. D 32, 189 (1985)).

The port covers the spectrum core: the Table II / Appendix A parameters (bundled TOML), the running coupling, the closed-form smeared potentials, the finite-difference relativized Hamiltonian, the contact hyperfine, spin-orbit (vector and Thomas) and tensor operators, fixed-(L,S,J) channel solves, intra-meson mixing (antisymmetric spin-orbit and tensor), and radial-wave utilities. It uses the same numerics, conventions and defaults as the Julia package. Both radial solvers are ported: the default finite-difference grid (FiniteDifferenceSolver) and the paper's harmonic-oscillator basis (OscillatorSolver). Masses agree with GIModel.jl to about 1e-12 GeV on the grid and about 1e-14 GeV in the oscillator basis.

It does not include flavour annihilation mixing or the transition operators.

Python API

from gimodel import (Meson, compute_spectrum, load_parameters_and_quark_masses,
                     physical_components, radial_wave, spectrum_levels, spectrum_state,
                     wave_mean_squares)

params, mq = load_parameters_and_quark_masses()          # bundled GI parameter set
spec = compute_spectrum(params, Meson.from_table(mq, "c", "c"), levels=spectrum_levels(2))
print(spec)                                              # table of contributions (GeV)

chi_c1 = spectrum_state(spec, "1^3P_1")
chi_c1.mass_GeV, chi_c1.central_GeV, chi_c1.spin_orbit_shift_GeV, chi_c1.tensor_shift_GeV

[(c.basis.label, c.coefficient) for c in physical_components(spec, "1^3S_1")]  # J/psi
wave_mean_squares(radial_wave(spec, "1^1S_0"), 0)       # <r^2> [GeV^-2], <p^2> [GeV^2]

To vary parameters, use dataclasses.replace, for example replace(params, potential=replace(params.potential, b=0.19)). To change the grid, use FiniteDifferenceSolver(ngrid=900, rmax=24.0). To switch terms off, use SpinTerms(tensor=False).

Units are GeV and GeV⁻¹ throughout.

Command line: gi-spectrum

gi-spectrum lists the model's meson states in a mass window. It follows the conventions of threshold-finder from the same repository: masses are in MeV, J P are optional positional filters, and it has the same flavour flags, --particles handling and unknown-name suggestions.

gi-spectrum mass_min mass_max [J P] (--quarks Q QBAR | --u/--d/--s/--c/--b N | --particles P [P ...]) [options]

Flavour content. This is required, and you give it in exactly one way:

Input Meaning
--quarks Q QBAR Quark and antiquark, from u d q s c b, e.g. --quarks c u (c ū) or --quarks c c (charmonium). This is the only way to request hidden flavour.
--u/--d/--s/--c/--b N Net quark numbers, as in threshold-finder. They must add up to one quark (+1) and one antiquark (−1) of different flavours, e.g. --c 1 --s -1. A net-zero input is rejected with a --quarks suggestion.
--particles P [P ...] The summed PDG quark content: D0 pi+ gives c d̄ and D(s)+ gives c s̄. A single self-conjugate particle uses its own content: J/psi(1S) gives c c̄ and Upsilon(1S) gives b b̄. Light superpositions (pi0, rho(770)0) give the light q q̄. Ambiguous content (eta, eta'(958), phi(1020), K(S)0), net-zero sums such as D0 D~0, and anything that isn't one quark plus one antiquark are errors that suggest --quarks. Flavour-neutral superpositions in a sum carry no net flavour. As in threshold-finder, explicit --u/--d/... flags override the derived values.

u and d both map to the model's isospin-averaged light quark q. The quark is flavour 1 and the antiquark flavour 2. This order only affects the sign of mixing angles. For light hidden flavour (q q̄, s s̄) the output notes that isoscalar annihilation mixing (η/η′, ω/φ) is not included.

Options.

Flag Default Description
J P – Filter by J^P. J is an integer (half-integer J is rejected), P is +1 or -1. They must be given together.
--C {+1,-1} – Filter by C-parity. Only valid for self-conjugate systems.
--solver {fd,ho} fd Finite-difference or harmonic-oscillator solver. If the solver is unavailable, the CLI prints a clean error.
--nmax N 3 Radial levels per (L, S, J) sector
--max-L L 3 Highest orbital angular momentum (S P D F; at most 4)
--params PATH bundled Parameter TOML file
--details off Mass contributions (central, contact, spin-orbit, tensor, mixing) and mixing components
--json off Machine-readable output
--match off Pair each predicted state with PDG mesons (see PDG matching)
--match-mass-tol MEV 50 Mass tolerance in MeV
--match-width-frac F 0.5 Widen the tolerance to F·Γ_PDG for broad states
--match-status S [S ...] 0 PDG status codes to include, with the same codes and default as threshold-finder's --status. See PDG matching for how they map onto the PDG API.
--match-include-uncertain off Also use J/P values the PDG gives in parentheses or as alternatives, and flag those candidates

P = (−1)^(L+1). C = (−1)^(L+S) is shown only for equal-flavour systems. Unequal-flavour ¹L_L/³L_L states are labelled by their dominant component and marked as mixtures. If the highest computed level of a relevant sector is still below mass_max, a warning on stderr suggests a larger --nmax. Errors go to stderr with a non-zero exit status.

Examples

$ gi-spectrum 2900 3200 --quarks c c
GI spectrum of c c̄  in [2900.0, 3200.0] MeV  (solver = fd, nmax = 3, L = SPDF)
Parameters: .../gimodel/data/parameters.provisional.toml
Found 2 state(s):
  1^1S_0  J^PC=0^-+  mass=2966.7 MeV
  1^3S_1  J^PC=1^--  mass=3091.0 MeV
$ gi-spectrum 2400 2600 1 +1 --particles 'D(s)+' --details
Reference particles: 'D(s)+'  ->  c s̄
GI spectrum of c s̄ for J^P = 1^+  in [2400.0, 2600.0] MeV  (solver = fd, nmax = 3, L = SPDF)
Parameters: .../gimodel/data/parameters.provisional.toml
Found 2 state(s):
  1^1P_1  J^P=1^+  mass=2547.1 MeV  (mixture: 59% 1^1P_1 + 41% 1^3P_1)
      contributions [MeV]: central=2565.0  contact=-14.7  spin-orbit=+0.0  tensor=+0.0  mixing=-3.2
      components: +0.7708 1^1P_1, -0.6366 1^3P_1, -0.0200 2^3P_1, +0.0166 2^1P_1
  1^3P_1  J^P=1^+  mass=2554.1 MeV  (mixture: 59% 1^3P_1 + 41% 1^1P_1)
      contributions [MeV]: central=2565.5  contact=+5.0  spin-orbit=-30.7  tensor=+11.7  mixing=+2.5
      components: +0.7708 1^3P_1, +0.6366 1^1P_1, -0.0197 2^1P_1, -0.0162 2^3P_1, +0.0015 3^1P_1, +0.0011 3^3P_1
$ gi-spectrum 3000 3700 --particles 'J/psi(1S)' --C -1
Reference particles: 'J/psi(1S)'  ->  c c̄
GI spectrum of c c̄ with C = -  in [3000.0, 3700.0] MeV  (solver = fd, nmax = 3, L = SPDF)
Parameters: .../gimodel/data/parameters.provisional.toml
Found 3 state(s):
  1^3S_1  J^PC=1^--  mass=3091.0 MeV
  1^1P_1  J^PC=1^+-  mass=3515.1 MeV
  2^3S_1  J^PC=1^--  mass=3678.8 MeV
$ gi-spectrum 1800 2100 --particles D0 pi+ --max-L 1
Reference particles: 'D0' + 'pi+'  ->  c d̄
GI spectrum of c d̄  in [1800.0, 2100.0] MeV  (solver = fd, nmax = 3, L = SP)
Parameters: .../gimodel/data/parameters.provisional.toml
Note: u and d map to the isospin-averaged light quark: model system c q̄
Found 2 state(s):
  1^1S_0  J^P=0^-  mass=1873.4 MeV
  1^3S_1  J^P=1^-  mass=2038.0 MeV
$ gi-spectrum 2900 3200 --c 0
ERROR: Net flavour from the flavour flags is zero (hidden flavour), which the --u/--d/--s/--c/--b numbers cannot specify.
Give the quark and antiquark explicitly with --quarks, e.g.:
  gi-spectrum 2900.0 3200.0 --quarks c c

--json prints the system, solver, parameter file, filters, notes, warnings and, for each state, its label, n, L, S, J, P, C, mass in MeV, mixing components and contributions.

PDG matching (--match)

--match pairs every predicted state with existing PDG mesons. The data comes from the official PDG Python API (pdg package). It ships the full PDG database as SQLite and works offline. Unlike the particle package, it includes states without Monte-Carlo IDs, such as D_s1*(2700), D_s1*(2860), D_s3*(2860), χc1(3872), ψ(4230) and the X states. The header names the package version and the edition, e.g. PDG data: PDG Python API (pdg 2026.0), 2026 edition, and JSON gives them under match_settings.source. threshold-finder and --particles keep using particle. The --match-* options require --match. Without --match the output is unchanged.

Data used. The matcher reads every meson listing from api.get_particles() (listings whose data_flags contain M) and every charge state in it (PdgParticle). For each state it takes:

  • the best mass from best_summary(). A mass given as a range, e.g. f₀(500) at "400 to 800", uses the midpoint and is flagged. Limits count as unknown.
  • the best width, if the listing has a width entry. Upper limits, e.g. D*(2007)⁰ at "< 2.1", count as unknown. The API's width property returns 0 for listings without a width entry, so the matcher doesn't use it.
  • the strings quantum_J, quantum_P, quantum_C and quantum_I.

Antiparticles (negative MC IDs or charges, and Dbar…/Kbar…/Bbar… names) are skipped.

Status. The PDG API has no per-listing status. It only marks values that appear in the Summary Tables (in_summary_table). --match-status uses threshold-finder's codes as follows:

Code threshold-finder (particle) PDG API
0 established the listing's best mass is in the Summary Tables
1 evidence, not confirmed no counterpart: selects nothing
2 omitted from the summary tables the listing's best mass appears only in the Listings

In the 2026 edition every meson listing with a usable mass has status 0, so the default already includes D_s1*(2700) and ψ(4230). The light-meson "Further States" (listing M300, e.g. X(1575), ρ(2000)) exist only as text descriptions without mass, J or P data, so they can't be matched.

Criterion. A PDG state is a candidate for a prediction if all of these hold:

  1. Mass. |m_pred − m_PDG| ≤ max(--match-mass-tol, --match-width-frac · Γ_PDG). The model predicts no widths, so the PDG width only widens the window for broad states. If Γ_PDG is unknown, the mass tolerance is used alone. The output shows the effective tolerance and whether it came from the width (tol = 0.5·Γ = 74.1).

  2. J and P equal. C must also be equal when both are defined, i.e. the system is self-conjugate and the PDG state has C. States whose J or P is ? are never matched, e.g. X(3940), K(1630) and D*(2640). Values in parentheses, and alternatives such as f_J(2220)'s J = 2++ or 4, count as unknown by default. With --match-include-uncertain they are used, with one candidate per alternative, and flagged uncertain J^P in PDG. J and P are never inferred from MC-ID digits, because those are a numbering convention, not measured quantum numbers.

  3. Flavour compatible. The PDG API has no quark content, so the flavour family comes from the PDG meson naming scheme:

    PDG name Family
    pi rho omega phi eta f a b h (eta^', f_2^', …) light unflavoured
    K… s q̄
    D… / D_s… c q̄ / c s̄
    B… / B_s… / B_c… b q̄ / b s̄ / b c̄
    eta_c J/psi psi chi_c h_c c c̄
    eta_b Upsilon chi_b h_b b b̄
    X(…) the PDG section it is listed in (the nearest preceding listing), flagged
    T… (four-quark states, e.g. T_cc̄1(3900)⁺ = Z_c) never a q q̄ partner

    This is matched against the system, where u and d are the model's averaged light quark q:

    System Matches
    open flavour, e.g. c s̄, c ū the same pair, up to charge conjugation and isospin partners: c q̄ matches D⁰, D⁺, D⁰ and D⁺
    c c̄, b b̄ the c c̄ / b b̄ family, including χc1(3872) and ψ(4230)
    q q̄, u ū, d d̄ light unflavoured I = 1 and I = 0 states
    u d̄, d ū light unflavoured I = 1 only
    s s̄ light unflavoured I = 0 states

    Light isoscalars (I = 0, or I unknown) fit both q q̄ and s s̄. They are flagged flavour-ambiguous because isoscalar annihilation mixing isn't modelled.

Isospin and charge multiplets. Most charge states share one PDG listing, e.g. ρ(770)⁰/⁺, D_0*(2300)⁰/⁺ and K*(892)⁰/⁺. Some don't: D⁰/D⁺, D*(2007)⁰/D*(2010)⁺, K⁺/K⁰ and B⁰/B⁺ are separate listings. These are merged when their MC IDs agree up to the u/d quark digits (423/413). Members must also share isospin, J and P, which keeps π⁰ (I = 1) and η (I = 0) apart. The representative is the neutral member, or the lowest MC ID if there is no neutral member. For a multiplet it is shown with the charge suffix dropped (D, D^*(2007), rho(770)), and singletons keep their full PDG API name (omega(782)0, D_s1^*(2700)+). The mass is the average over the charge states, matching the model's isospin-averaged light quark, so D is 1867.3 MeV from D⁰ and D⁺. The width is the average of the known widths. C comes from the neutral member. --details and JSON list the members.

Assignment. Every candidate within tolerance is listed for each prediction, ranked by |Δm| (Δ = m_pred − m_PDG). Each J^P(C) group also gets a one-to-one assignment from scipy.optimize.linear_sum_assignment that minimises the total |Δm|. Pairs outside tolerance get a large cost and are dropped afterwards, so the assignment first maximises the number of matched pairs. The assignment only uses the predictions shown in the window. The table shows the assigned partner (→ chi_c1(1P) 3510.7 MeV (Δ = -2.5)) or → no match. --details lists all candidates with Δm, effective tolerance, assignment and flags.

Unassigned PDG states. After the table, the section "PDG states in window without assigned prediction" lists the compatible states that no prediction took. These states pass the flavour rules and the J P / --C filter, and their mass lies in [mass_min, mass_max] widened by their own tolerance. This helps to spot missing levels and exotics. A J^PC that no q q̄ state can have (0⁻⁻, 0⁺⁻, 1⁻⁺, 2⁺⁻, …) is flagged exotic J^PC, e.g. π₁(1600).

JSON. Each state gains a match object with assigned (or null) and candidates. Each entry has the name, members, pdg_listing (e.g. M182), status, mass, width, J/P/C, isospin, delta_MeV, tolerance_MeV, tolerance_source (mass_tol or width), flags and assigned_to. The top level gains match_settings, which holds the tolerances, status, include_uncertain and the data source, and unassigned_pdg. The existing keys are unchanged.

Data quirks (2026 edition): φ(2170) lists a charged row with I = 0, which is merged into the single φ(2170) entry. Some narrow states have only an upper limit on the width (D_s*, D_s0*(2317)) or no width entry (B*), so they use the mass tolerance alone.

$ gi-spectrum 2900 3600 --quarks c c --nmax 2 --max-L 2 --match
GI spectrum of c c̄  in [2900.0, 3600.0] MeV  (solver = fd, nmax = 2, L = SPD)
Parameters: .../gimodel/data/parameters.provisional.toml
PDG match: |m_pred - m_PDG| <= max(50 MeV, 0.5·Γ_PDG), PDG status 0; Δ = m_pred - m_PDG; one-to-one per J^P(C), minimal total |Δ|
PDG data: PDG Python API (pdg 2026.0), 2026 edition; only listings with known mass, J and P are matched (uncertain J/P count as unknown)
Found 6 state(s):
  1^1S_0  J^PC=0^-+  mass=2966.7 MeV  → eta_c(1S) 2984.1 MeV (Δ = -17.4)
  1^3S_1  J^PC=1^--  mass=3091.0 MeV  → J/psi(1S) 3096.9 MeV (Δ = -5.9)
  1^3P_0  J^PC=0^++  mass=3442.8 MeV  → chi_c0(1P) 3415.5 MeV (Δ = +27.3)
  1^3P_1  J^PC=1^++  mass=3508.2 MeV  → chi_c1(1P) 3510.7 MeV (Δ = -2.5)
  1^1P_1  J^PC=1^+-  mass=3515.1 MeV  → h_c(1P) 3525.4 MeV (Δ = -10.3)
  1^3P_2  J^PC=2^++  mass=3548.1 MeV  → chi_c2(1P) 3556.2 MeV (Δ = -8.0)

PDG states in window without assigned prediction ([2900.0, 3600.0] MeV widened by each state's tolerance):
  eta_c(2S)  J^PC=0^-+  mass=3637.8 MeV  Γ=11.6 MeV  tol = 50.0

η_c(2S) is listed because its prediction (2^1S_0 at 3625.4 MeV) lies above the window. With 2900 3700 it is assigned, and so is ψ(2S).

$ gi-spectrum 2650 3000 --particles 'D~0' 'K-' --match --match-mass-tol 80
Reference particles: 'D~0' + 'K-'  ->  s c̄
GI spectrum of s c̄  in [2650.0, 3000.0] MeV  (solver = fd, nmax = 3, L = SPDF)
Parameters: .../gimodel/data/parameters.provisional.toml
PDG match: |m_pred - m_PDG| <= max(80 MeV, 0.5·Γ_PDG), PDG status 0; Δ = m_pred - m_PDG; one-to-one per J^P(C), minimal total |Δ|
PDG data: PDG Python API (pdg 2026.0), 2026 edition; only listings with known mass, J and P are matched (uncertain J/P count as unknown)
Found 6 state(s):
  2^1S_0  J^P=0^-  mass=2674.6 MeV                                      → no match
  2^3S_1  J^P=1^-  mass=2734.3 MeV                                      → D_s1^*(2700)+ 2714.0 MeV (Δ = +20.3)
  1^3D_1  J^P=1^-  mass=2898.5 MeV                                      → D_s1^*(2860)+ 2859.0 MeV (Δ = +39.5)
  1^1D_2  J^P=2^-  mass=2899.9 MeV  (mixture: 61% 1^1D_2 + 39% 1^3D_2)  → no match
  1^3D_3  J^P=3^-  mass=2916.1 MeV                                      → D_s3^*(2860)+ 2860.5 MeV (Δ = +55.6)
  1^3D_2  J^P=2^-  mass=2925.4 MeV  (mixture: 61% 1^3D_2 + 39% 1^1D_2)  → no match

PDG states in window without assigned prediction ([2650.0, 3000.0] MeV widened by each state's tolerance):
  D_s0(2590)+  J^P=0^-  mass=2591.0 MeV  Γ=89.0 MeV  tol = 80.0
$ gi-spectrum 700 850 1 -1 --quarks u u --nmax 1 --max-L 1 --match --details
...
Found 1 state(s):
  1^3S_1  J^PC=1^--  mass=771.2 MeV  → rho(770) 775.2 MeV (Δ = -4.0, tol = 0.5·Γ = 74.1)
      contributions [MeV]: central=667.1  contact=+104.1  spin-orbit=+0.0  tensor=+0.0  mixing=+0.0
      components: +1.0000 1^3S_1
      PDG candidates (by |Δm|):
        rho(770)     mass=775.2 MeV  Γ=148.2 MeV  Δ=-4.0  tol = 0.5·Γ = 74.1  assigned  (members: rho(770)0, rho(770)+)
        omega(782)0  mass=782.7 MeV  Γ=8.7 MeV  Δ=-11.5  tol = 50.0  unassigned  [flavour-ambiguous]

PDG states in window without assigned prediction ([700.0, 850.0] MeV widened by each state's tolerance):
  omega(782)0  J^PC=1^--  mass=782.7 MeV  Γ=8.7 MeV  tol = 50.0  [flavour-ambiguous]

With --quarks u d (I = 1) ω(782) is not a candidate.

Tests and reference data

tests/gimodel/ compares the port layer by layer against JSON reference data generated from GIModel.jl (tests/gimodel/reference/). tools/julia_reference/ holds the Julia generator and explains how to regenerate the data.

python -m pytest                 # everything
python -m pytest -m "not slow"   # skip the long Julia cross-checks

Metadata

Release files for thresholds 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for thresholds 0.5.0
File Size Uploaded
thresholds-0.5.0.tar.gz 121.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for thresholds 0.5.0
File Interpreter ABI Platform
thresholds-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 228.0 kB

Release files / thresholds-0.5.0.tar.gz

Download URL thresholds-0.5.0.tar.gz
Size 121.7 kB
Tags Source
SHA-256 checksum
How to use checksums
0b992c947e06c53f09705a1b0e7ac83a8458d484da2f48831cc4c99ada076064
BLAKE2b-256 checksum
How to use checksums
95d1132c9067d279d57a3d28d5f719e0feef56e30f32859f13dc6a4b302a0b4d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / thresholds-0.5.0-py3-none-any.whl

Download URL thresholds-0.5.0-py3-none-any.whl
Size 106.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cd4a367ddabafad000736a196995bb5abab0b7354d7ba6d2dbd36c6c090fb32d
BLAKE2b-256 checksum
How to use checksums
7c0c0ee7863e1fa0c4c00447e1eb2e69b9a594509613d68e996fc5900261dbef
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page