"""Definitions used internally for type hints and signatures.
`qrules` is strictly typed (enforced through :doc:`mypy <mypy:index>`). This module
bundles structures and definitions that don't serve as data containers but only as type
hints. `.EdgeQuantumNumbers` and `.NodeQuantumNumbers` are the main structures and serve
as a bridge between the :mod:`.particle` and the :mod:`.conservation_rules` module.
"""
from decimal import Decimal
from fractions import Fraction
from functools import total_ordering
from typing import Any, Generator, NewType, Optional, Union
import attrs
from attrs import field, frozen
from attrs.validators import instance_of
from qrules._implementers import implement_pretty_repr
def _check_plus_minus(_: Any, __: attrs.Attribute, value: Any) -> None:
if not isinstance(value, int):
raise TypeError(
f"Input for {Parity.__name__} has to be of type {int.__name__},"
f" not {type(value).__name__}"
)
if value not in [-1, +1]:
raise ValueError(f"Parity can only be +1 or -1, not {value}")
[docs]@total_ordering
@frozen(eq=False, hash=True, order=False, repr=False)
class Parity:
value: int = field(validator=[instance_of(int), _check_plus_minus])
def __eq__(self, other: object) -> bool:
if isinstance(other, Parity):
return self.value == other.value
return self.value == other
def __gt__(self, other: Any) -> bool:
if other is None:
return True
return self.value > int(other)
def __int__(self) -> int:
return self.value
def __neg__(self) -> "Parity":
return Parity(-self.value)
def __repr__(self) -> str:
return f"{type(self).__name__}({_to_fraction(self.value)})"
def _to_fraction(value: Union[float, int], render_plus: bool = False) -> str:
label = str(Fraction(value))
if render_plus and value > 0:
return f"+{label}"
return label
[docs]@frozen(init=False)
class EdgeQuantumNumbers: # pylint: disable=too-many-instance-attributes
"""Definition of quantum numbers for edges.
This class defines the types that are used in the :mod:`.conservation_rules`, for
instance in `.additive_quantum_number_rule`. You can also create data classes (see
:func:`attrs.define`) with data members that are typed as the data members of
`.EdgeQuantumNumbers` (see for example `.HelicityParityEdgeInput`) and use them in
conservation rules that satisfy the appropriate rule protocol (see
`.ConservationRule`, `.EdgeQNConservationRule`).
"""
pid = NewType("pid", int)
mass = NewType("mass", float)
width = NewType("width", float)
spin_magnitude = NewType("spin_magnitude", float)
spin_projection = NewType("spin_projection", float)
charge = NewType("charge", int)
isospin_magnitude = NewType("isospin_magnitude", float)
isospin_projection = NewType("isospin_projection", float)
strangeness = NewType("strangeness", int)
charmness = NewType("charmness", int)
bottomness = NewType("bottomness", int)
topness = NewType("topness", int)
baryon_number = NewType("baryon_number", int)
electron_lepton_number = NewType("electron_lepton_number", int)
muon_lepton_number = NewType("muon_lepton_number", int)
tau_lepton_number = NewType("tau_lepton_number", int)
parity = NewType("parity", Parity)
c_parity = NewType("c_parity", Parity)
g_parity = NewType("g_parity", Parity)
for edge_qn_name, edge_qn_type in EdgeQuantumNumbers.__dict__.items():
if not edge_qn_name.startswith("__"):
edge_qn_type.__qualname__ = f"EdgeQuantumNumbers.{edge_qn_name}"
edge_qn_type.__module__ = __name__
# for static typing
EdgeQuantumNumber = Union[
EdgeQuantumNumbers.pid,
EdgeQuantumNumbers.mass,
EdgeQuantumNumbers.width,
EdgeQuantumNumbers.spin_magnitude,
EdgeQuantumNumbers.spin_projection,
EdgeQuantumNumbers.charge,
EdgeQuantumNumbers.isospin_magnitude,
EdgeQuantumNumbers.isospin_projection,
EdgeQuantumNumbers.strangeness,
EdgeQuantumNumbers.charmness,
EdgeQuantumNumbers.bottomness,
EdgeQuantumNumbers.topness,
EdgeQuantumNumbers.baryon_number,
EdgeQuantumNumbers.electron_lepton_number,
EdgeQuantumNumbers.muon_lepton_number,
EdgeQuantumNumbers.tau_lepton_number,
EdgeQuantumNumbers.parity,
EdgeQuantumNumbers.c_parity,
EdgeQuantumNumbers.g_parity,
]
[docs]@frozen(init=False)
class NodeQuantumNumbers:
"""Definition of quantum numbers for interaction nodes."""
l_magnitude = NewType("l_magnitude", float)
l_projection = NewType("l_projection", float)
s_magnitude = NewType("s_magnitude", float)
s_projection = NewType("s_projection", float)
parity_prefactor = NewType("parity_prefactor", float)
for node_qn_name, node_qn_type in NodeQuantumNumbers.__dict__.items():
if not node_qn_name.startswith("__"):
node_qn_type.__qualname__ = f"NodeQuantumNumbers.{node_qn_name}"
node_qn_type.__module__ = __name__
# for static typing
NodeQuantumNumber = Union[
NodeQuantumNumbers.l_magnitude,
NodeQuantumNumbers.l_projection,
NodeQuantumNumbers.s_magnitude,
NodeQuantumNumbers.s_projection,
NodeQuantumNumbers.parity_prefactor,
]
def _to_optional_float(optional_float: Optional[float]) -> Optional[float]:
if optional_float is None:
return None
return float(optional_float)
def _to_optional_int(optional_int: Optional[int]) -> Optional[int]:
if optional_int is None:
return None
return int(optional_int)
[docs]@implement_pretty_repr
@frozen(order=True)
class InteractionProperties:
"""Immutable data structure containing interaction properties.
Interactions are represented by a node on a `.MutableTransition`. This class
represents the properties that are carried collectively by the edges that this node
connects.
Interaction properties are in particular important in the canonical basis of the
helicity formalism. There, the *coupled spin* and angular momentum of each
interaction are used for the Clebsch-Gordan coefficients for each term in a
sequential amplitude.
.. note:: As opposed to `NodeQuantumNumbers`, the `InteractionProperties` class
serves as an interface to the user.
"""
l_magnitude: Optional[int] = field( # L cannot be half integer
default=None, converter=_to_optional_int
)
l_projection: Optional[int] = field(default=None, converter=_to_optional_int)
s_magnitude: Optional[float] = field(default=None, converter=_to_optional_float)
s_projection: Optional[float] = field(default=None, converter=_to_optional_float)
parity_prefactor: Optional[float] = field(
default=None, converter=_to_optional_float
)
[docs]def arange(x_1: float, x_2: float, delta: float = 1.0) -> Generator[float, None, None]:
current = Decimal(x_1)
while current < x_2:
yield float(current)
current += Decimal(delta)