"""A collection of data containers."""
__all__ = [ # fix order in API
"ParticleCollection",
"Particle",
"ComplexEnergyState",
"QuantumState",
"Parity",
"Spin",
"ComplexEnergy",
"create_antiparticle",
"create_particle",
"GellmannNishijima",
]
import logging
from collections import abc
from dataclasses import dataclass
from typing import (
Dict,
Generic,
ItemsView,
Iterator,
KeysView,
Optional,
TypeVar,
Union,
ValuesView,
)
[docs]class Parity:
"""Safe, immutable data container for parity."""
def __init__(self, value: Union[float, int, str]) -> None:
value = float(value)
if value not in [-1.0, +1.0]:
raise ValueError(f"Parity can only be +1 or -1, not {value}")
self.__value: int = int(value)
[docs] def __eq__(self, other: object) -> bool:
if isinstance(other, Parity):
return self.__value == other.value
return self.__value == other
def __int__(self) -> int:
return self.value
def __neg__(self) -> "Parity":
return Parity(-self.value)
def __repr__(self) -> str:
return (
f'{self.__class__.__name__}({"+1" if self.__value > 0 else "-1"})'
)
@property
def value(self) -> int:
return self.__value
[docs]class Spin(abc.Hashable):
"""Safe, immutable data container for spin **with projection**."""
def __init__(self, magnitude: float, projection: float) -> None:
magnitude = float(magnitude)
projection = float(projection)
if magnitude % 0.5 != 0.0:
raise ValueError(
f"Spin magnitude {magnitude} has to be a multitude of 0.5"
)
if abs(projection) > magnitude:
raise ValueError(
"Spin projection cannot be larger than its magnitude:\n"
f" {projection} > {magnitude}"
)
if not (projection - magnitude).is_integer():
raise ValueError(
f"{self.__class__.__name__}{magnitude, projection}: "
"(projection - magnitude) should be integer! "
)
if projection == -0.0:
projection = 0.0
self.__magnitude = magnitude
self.__projection = projection
[docs] def __eq__(self, other: object) -> bool:
if isinstance(other, Spin):
return (
self.__magnitude == other.magnitude
and self.__projection == other.projection
)
return self.__magnitude == other
def __float__(self) -> float:
return self.__magnitude
def __neg__(self) -> "Spin":
return Spin(self.magnitude, -self.projection)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}{self.__magnitude, self.__projection}"
)
@property
def magnitude(self) -> float:
return self.__magnitude
@property
def projection(self) -> float:
return self.__projection
def __hash__(self) -> int:
return hash(repr(self))
_T = TypeVar("_T", float, Spin)
[docs]@dataclass(frozen=True)
class QuantumState(
Generic[_T]
): # pylint: disable=too-many-instance-attributes
"""Set of quantum numbers with a **generic type spin**.
This is to make spin projection required in `.QuantumState` and unavailable
in `.Particle`.
"""
spin: _T
charge: int = 0
isospin: Optional[Spin] = None
strangeness: int = 0
charmness: int = 0
bottomness: int = 0
topness: int = 0
baryon_number: int = 0
electron_lepton_number: int = 0
muon_lepton_number: int = 0
tau_lepton_number: int = 0
parity: Optional[Parity] = None
c_parity: Optional[Parity] = None
g_parity: Optional[Parity] = None
[docs]class ComplexEnergy:
"""Defines a complex valued energy.
Resembles a position (pole) in the complex energy plane.
"""
def __init__(self, energy: complex):
self.__energy = complex(energy)
@property
def complex_energy(self) -> complex:
return self.__energy
@property
def mass(self) -> float:
return self.__energy.real
@property
def width(self) -> float:
return self.__energy.imag
[docs] def __eq__(self, other: object) -> bool:
if isinstance(other, ComplexEnergy):
return self.complex_energy == other.complex_energy
raise NotImplementedError
def __repr__(self) -> str:
return f"{self.__class__.__name__}{self.complex_energy}"
[docs]class ComplexEnergyState(ComplexEnergy):
"""Pole in the complex energy plane, with quantum numbers."""
def __init__(self, energy: complex, state: QuantumState[Spin]):
super().__init__(energy)
self.state: QuantumState[Spin] = state
def __repr__(self) -> str:
return f"{self.__class__.__name__}{self.complex_energy, self.state}"
[docs]class Particle(ComplexEnergy):
"""Immutable container of data defining a physical particle.
Can **only** contain info that the `PDG <http://pdg.lbl.gov/>`_ would list.
"""
def __init__( # pylint: disable=too-many-arguments
self,
name: str,
pid: int,
state: QuantumState[float],
mass: float,
width: float = 0.0,
):
if (
state.isospin is not None
and GellmannNishijima.compute_charge(state) != state.charge
):
raise ValueError(
f"Cannot construct particle {name} because its quantum numbers"
" don't agree with the Gell-Mann–Nishijima formula:\n"
f" Q[{state.charge}] != "
f"Iz[{state.isospin.projection}] + 1/2 "
f"(B[{state.baryon_number}] + "
f" S[{state.strangeness}] + "
f" C[{state.charmness}] +"
f" B'[{state.bottomness}] +"
f" T[{state.strangeness}]"
")"
)
super().__init__(complex(mass, width))
self.__name: str = name
self.__pid: int = pid
self.state: QuantumState[float] = state
@property
def name(self) -> str:
return self.__name
@property
def pid(self) -> int:
return self.__pid
[docs] def __eq__(self, other: object) -> bool:
if isinstance(other, Particle):
return (
self.name == other.name
and self.pid == other.pid
and super().__eq__(other)
and self.state == other.state
)
raise NotImplementedError
def __repr__(self) -> str:
return f"{self.__class__.__name__}{self.name, self.pid, self.state, self.mass, self.width}"
[docs]class GellmannNishijima:
r"""Collection of conversion methods using Gell-Mann–Nishijima.
The methods in this class use the `Gell-Mann–Nishijima formula
<https://en.wikipedia.org/wiki/Gell-Mann%E2%80%93Nishijima_formula>`_:
.. math::
Q = I_3 + \frac{1}{2}(B+S+C+B'+T)
where
:math:`Q` is charge (computed),
:math:`I_3` is `.Spin.projection`,
:math:`B` is `~.QuantumState.baryon_number`,
:math:`S` is `~.QuantumState.strangeness`,
:math:`C` is `~.QuantumState.charmness`,
:math:`B'` is `~.QuantumState.bottomness`, and
:math:`T` is `~.QuantumState.topness`.
"""
[docs] @staticmethod
def compute_charge(state: QuantumState) -> Optional[float]:
"""Compute charge using the Gell-Mann–Nishijima formula.
If isospin is not `None`, returns the value :math:`Q`: computed with
the `Gell-Mann–Nishijima formula <.GellmannNishijima>`.
"""
if state.isospin is None:
return None
computed_charge = state.isospin.projection + 0.5 * (
state.baryon_number
+ state.strangeness
+ state.charmness
+ state.bottomness
+ state.topness
)
return computed_charge
[docs] @staticmethod
def compute_isospin_projection( # pylint: disable=too-many-arguments
charge: float,
baryon_number: float,
strangeness: float,
charmness: float,
bottomness: float,
topness: float,
) -> float:
"""Compute isospin projection using the Gell-Mann–Nishijima formula.
See `~.GellmannNishijima.compute_charge`, but then computed for
:math:`I_3`.
"""
return charge - 0.5 * (
baryon_number + strangeness + charmness + bottomness + topness
)
[docs]class ParticleCollection(abc.Mapping):
"""Safe, `dict`-like collection of `.Particle` instances."""
def __init__(
self, particles: Optional[Dict[str, Particle]] = None
) -> None:
self.__particles: Dict[str, Particle] = dict()
if particles is not None:
if isinstance(particles, dict):
self.__particles.update(particles)
def __getitem__(self, particle_name: str) -> Particle:
return self.__particles[particle_name]
def __contains__(self, particle_name: object) -> bool:
return particle_name in self.__particles
def __iter__(self) -> Iterator[str]:
return self.__particles.__iter__()
def __len__(self) -> int:
return len(self.__particles)
def __iadd__(
self, other: Union[Particle, "ParticleCollection"]
) -> "ParticleCollection":
if isinstance(other, Particle):
self.add(other)
elif isinstance(other, ParticleCollection):
self.merge(other)
else:
raise NotImplementedError
return self
def __repr__(self) -> str:
return str(self.__particles)
[docs] def add(self, particle: Particle) -> None:
if particle.name in self.__particles:
logging.warning(
f"{self.__class__.__name__}: Overwriting particle {particle.name}"
)
self.__particles[particle.name] = particle
[docs] def find(self, search_term: Union[int, str]) -> Particle:
"""Search for a particle by either name (`str`) or PID (`int`)."""
if isinstance(search_term, str):
particle_name = search_term
return self.__particles[particle_name]
if isinstance(search_term, int):
pid = search_term
search_results = [
particle for particle in self.values() if particle.pid == pid
]
if len(search_results) == 0:
raise LookupError(f"Could not find particle with PID {pid}")
if len(search_results) > 1:
error_message = f"Found multiple results for PID {pid}!:"
for particle in search_results:
error_message += f"\n - {particle.name}"
raise LookupError(error_message)
return search_results[0]
raise NotImplementedError(
f"Cannot search for a search term of type {type(search_term)}"
)
[docs] def find_subset(
self, search_term: Union[int, str]
) -> "ParticleCollection":
"""Perform a 'fuzzy' search for a particle by name or PID.
Like `~.ParticleCollection.find`, but returns several results in the
form of a new `.ParticleCollection`.
"""
if isinstance(search_term, str):
search_results = {
particle.name: particle
for particle in self.values()
if search_term in particle.name
}
return ParticleCollection(search_results)
if isinstance(search_term, int):
pid = search_term
output = ParticleCollection()
particle = self.find(pid)
output.add(particle)
return output
raise NotImplementedError(
f"Cannot search for a search term of type {type(search_term)}"
)
[docs] def items(self) -> ItemsView[str, Particle]:
return self.__particles.items()
[docs] def keys(self) -> KeysView[str]:
return self.__particles.keys()
[docs] def values(self) -> ValuesView[Particle]:
return self.__particles.values()
[docs] def merge(self, other: "ParticleCollection") -> None:
for particle in other.values():
self.add(particle)
[docs]def create_particle( # pylint: disable=too-many-arguments,too-many-locals
template_particle: Particle,
name: Optional[str] = None,
pid: Optional[int] = None,
mass: Optional[float] = None,
width: Optional[float] = None,
charge: Optional[int] = None,
spin: Optional[float] = None,
isospin: Optional[Spin] = None,
strangeness: Optional[int] = None,
charmness: Optional[int] = None,
bottomness: Optional[int] = None,
topness: Optional[int] = None,
baryon_number: Optional[int] = None,
electron_lepton_number: Optional[int] = None,
muon_lepton_number: Optional[int] = None,
tau_lepton_number: Optional[int] = None,
parity: Optional[int] = None,
c_parity: Optional[int] = None,
g_parity: Optional[int] = None,
state: Optional[QuantumState[float]] = None,
) -> Particle:
if state is not None:
new_state = state
else:
new_state = QuantumState[float](
spin=spin if spin else template_particle.state.spin,
charge=charge if charge else template_particle.state.charge,
strangeness=strangeness
if strangeness
else template_particle.state.strangeness,
charmness=charmness
if charmness
else template_particle.state.charmness,
bottomness=bottomness
if bottomness
else template_particle.state.bottomness,
topness=topness if topness else template_particle.state.topness,
baryon_number=baryon_number
if baryon_number
else template_particle.state.baryon_number,
electron_lepton_number=electron_lepton_number
if electron_lepton_number
else template_particle.state.electron_lepton_number,
muon_lepton_number=muon_lepton_number
if muon_lepton_number
else template_particle.state.muon_lepton_number,
tau_lepton_number=tau_lepton_number
if tau_lepton_number
else template_particle.state.tau_lepton_number,
isospin=template_particle.state.isospin
if isospin is None
else template_particle.state.isospin,
parity=template_particle.state.parity
if parity is None
else Parity(parity),
c_parity=template_particle.state.c_parity
if c_parity is None
else Parity(c_parity),
g_parity=template_particle.state.g_parity
if g_parity is None
else Parity(g_parity),
)
new_particle = Particle(
name=name if name else template_particle.name,
pid=pid if pid else template_particle.pid,
mass=mass if mass else template_particle.mass,
width=width if width else template_particle.width,
state=new_state,
)
return new_particle
[docs]def create_antiparticle(
template_particle: Particle, new_name: str = None
) -> Particle:
isospin: Optional[Spin] = None
if template_particle.state.isospin:
isospin = -template_particle.state.isospin
parity: Optional[Parity] = None
if template_particle.state.parity is not None:
if template_particle.state.spin.is_integer():
parity = template_particle.state.parity
else:
parity = -template_particle.state.parity
return Particle(
name=new_name if new_name else "anti-" + template_particle.name,
pid=-template_particle.pid,
mass=template_particle.mass,
width=template_particle.width,
state=QuantumState[float](
charge=-template_particle.state.charge,
spin=template_particle.state.spin,
isospin=isospin,
strangeness=-template_particle.state.strangeness,
charmness=-template_particle.state.charmness,
bottomness=-template_particle.state.bottomness,
topness=-template_particle.state.topness,
baryon_number=-template_particle.state.baryon_number,
electron_lepton_number=-template_particle.state.electron_lepton_number,
muon_lepton_number=-template_particle.state.muon_lepton_number,
tau_lepton_number=-template_particle.state.tau_lepton_number,
parity=parity,
c_parity=template_particle.state.c_parity,
g_parity=template_particle.state.g_parity,
),
)