Skip to content

ENH: use Fraction for spin values #288

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 51 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
ec1c54e
changed Spin to have Fraction-fields
grayson-helmholz Oct 4, 2024
c4c2504
floats in Spin substituted with Fractions, regex-tests fail
grayson-helmholz Oct 7, 2024
3dc40d0
fixed regex-patterns in test_particle -> tests pass
grayson-helmholz Oct 8, 2024
3a54c4e
substituted global namespace variable with function
grayson-helmholz Oct 8, 2024
80807e9
fixed typo
grayson-helmholz Oct 8, 2024
afc647b
Merge remote-tracking branch 'origin/main' into spin_as_fraction
grayson-helmholz Oct 8, 2024
a1d48fa
added rendering for fractions
grayson-helmholz Oct 8, 2024
1957f5b
suppressed warnings in io & tests
grayson-helmholz Oct 9, 2024
64d009c
Union-type for _Spin
grayson-helmholz Oct 16, 2024
1eb80b5
Merge branch 'main' of github.com:CompWA/qrules into spin_as_fraction
grayson-helmholz Nov 5, 2024
6bbf7c0
changed conservation_rules and `arange` to use `Fraction` only
grayson-helmholz Nov 8, 2024
e8a184a
`Particle.spin` is now of type `Fraction`
grayson-helmholz Nov 8, 2024
13d11b9
changed aux-types and literals to `Fraction`
grayson-helmholz Nov 11, 2024
2da9bd9
`settings.py` only with `Fraction`
grayson-helmholz Nov 11, 2024
4ffd1e3
added `Fraction` to `Scalar`-type-union
grayson-helmholz Nov 11, 2024
a4bcc95
`InteractionProperties` with `Fraction` and new converter
grayson-helmholz Nov 11, 2024
a6ab1f8
coercion to `Fraction` in `create_edge_properties`
grayson-helmholz Nov 11, 2024
a365f9b
`Fraction`-literal in `StateTransitionManager`-constructor
grayson-helmholz Nov 11, 2024
5312754
`Fraction`-literals in `__init__.py`
grayson-helmholz Nov 11, 2024
eefb777
coercion instead of forcing `Fraction`-type in `__init__.py`
grayson-helmholz Nov 11, 2024
6bf5c76
coercion instead of forcing `Fraction`-type in `StateTransitionManager`
grayson-helmholz Nov 11, 2024
31099ad
float allowed again in `create_edge_properties`
grayson-helmholz Nov 11, 2024
28e5ca2
float allowed again in `create_interaction_settings`
grayson-helmholz Nov 11, 2024
274284b
map `Fraction` to input-list
grayson-helmholz Nov 11, 2024
881689f
now preserves stm-API, explicit coercion in `test_settings`-arguments
grayson-helmholz Nov 11, 2024
2bfb50f
reworked rendering fractions
grayson-helmholz Nov 18, 2024
372a11a
changed `parity_prefactor` to float
grayson-helmholz Nov 18, 2024
f78b232
introduced `StateDefinitionInput` and converter to `StateDefinition`
grayson-helmholz Nov 18, 2024
9489372
retyped `generate_transitions` and STM-`__init__`
grayson-helmholz Nov 18, 2024
36729d0
renders parity as `int`
grayson-helmholz Nov 18, 2024
315bda6
input-conversion to `Fraction` and new rendering in tests
grayson-helmholz Nov 18, 2024
b911392
docstring for `StateDefinitionInput`
grayson-helmholz Nov 18, 2024
9ac8b3a
using `Sequence` in `permutate_topology_kinematically`
grayson-helmholz Nov 18, 2024
24578d1
ignoring `Fraction` in API-Docs
grayson-helmholz Nov 18, 2024
7aca88c
Merge branch 'main' into spin_as_fraction
grayson-helmholz Nov 18, 2024
3e5ed8c
FIX: relink to `fractions.Fraction`
redeboer Nov 18, 2024
c9ad862
MAINT: simplify `Fraction` construction and notation
redeboer Nov 18, 2024
7b4732d
refactored `_render_fraction`
grayson-helmholz Nov 25, 2024
b46d0d9
fixed type in `conf.py`
grayson-helmholz Nov 25, 2024
5536aa8
removed `Fraction` from user-facing functions/classes
grayson-helmholz Nov 25, 2024
321f9f3
format in regex-pattern
grayson-helmholz Nov 25, 2024
e94531e
`isospin` can now be given as `float`, uses converter
grayson-helmholz Nov 25, 2024
d691695
`test_settings` uses `float` again as input
grayson-helmholz Nov 25, 2024
e8dc2a2
destructuring in `as_state_definition`
grayson-helmholz Nov 25, 2024
90537d9
fused `_int_as_signed_str` and `_float_as_signed_str`
grayson-helmholz Nov 25, 2024
c061be1
removed redundancies in `__render_as_fraction`
grayson-helmholz Nov 25, 2024
3e2ba1a
removed `__render_as_fraction` altogether
grayson-helmholz Nov 25, 2024
1d1e7e0
Merge branch 'spin_as_fraction' of github.com:CompWA/qrules into spin…
grayson-helmholz Nov 25, 2024
6333441
rendering `Fraction`s now uses simpler implementation from `particle.py`
grayson-helmholz Nov 25, 2024
33e9009
MAINT: use `attrs` instead of `attr`
redeboer Dec 6, 2024
d0c10b9
MAINT: simplify implementation
redeboer Dec 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/qrules/conservation_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import operator
import sys
from copy import deepcopy
from fractions import Fraction
from functools import reduce
from textwrap import dedent
from typing import Any, Callable, List, Optional, Set, Tuple, Type, Union
Expand All @@ -65,7 +66,7 @@
from typing_extensions import Protocol


def _is_boson(spin_magnitude: float) -> bool:
def _is_boson(spin_magnitude: Fraction) -> bool:
return abs(spin_magnitude % 1) < 0.01


Expand Down Expand Up @@ -240,6 +241,7 @@ class CParityEdgeInput:

@frozen
class CParityNodeInput:
# These converters currently do not do anything, as "NewType"s do not have constructors
l_magnitude: NodeQN.l_magnitude = field(converter=NodeQN.l_magnitude)
s_magnitude: NodeQN.s_magnitude = field(converter=NodeQN.s_magnitude)

Expand Down Expand Up @@ -269,8 +271,8 @@ def _get_c_parity_multiparticle(
# if boson
if _is_boson(part_qns[0].spin_magnitude):
return (-1) ** int(ang_mom)
coupled_spin = interaction_qns.s_magnitude
if isinstance(coupled_spin, int) or coupled_spin.is_integer():
coupled_spin = Fraction(interaction_qns.s_magnitude)
if isinstance(coupled_spin, int) or coupled_spin.denominator == 1:
return (-1) ** int(ang_mom + coupled_spin)
return None

Expand Down Expand Up @@ -319,12 +321,12 @@ def check_multistate_g_parity(
double_state_qns[0].pid, double_state_qns[1].pid
):
ang_mom = interaction_qns.l_magnitude
if isinstance(isospin, int) or isospin.is_integer():
if isinstance(isospin, int) or isospin.denominator == 1:
# if boson
if _is_boson(double_state_qns[0].spin_magnitude):
return (-1) ** int(ang_mom + isospin)
coupled_spin = interaction_qns.s_magnitude
if isinstance(coupled_spin, int) or coupled_spin.is_integer():
if isinstance(coupled_spin, int) or coupled_spin.denominator == 1:
return (-1) ** int(ang_mom + coupled_spin + isospin)
return None

Expand Down
3 changes: 3 additions & 0 deletions src/qrules/io/_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import json
from collections import abc
from fractions import Fraction
from os.path import dirname, realpath
from typing import Any

Expand Down Expand Up @@ -43,6 +44,8 @@ def _value_serializer(inst: type, field: attrs.Attribute, value: Any) -> Any: #
"magnitude": value.magnitude,
"projection": value.projection,
}
if isinstance(value, Fraction):
return float(value)
return value


Expand Down
27 changes: 19 additions & 8 deletions src/qrules/io/_dot.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import re
import string
from collections import abc
from fractions import Fraction
from functools import singledispatch
from inspect import isfunction
from numbers import Number
Expand All @@ -18,8 +19,14 @@
from attrs import Attribute, define, field
from attrs.converters import default_if_none

from qrules.particle import Particle, ParticleWithSpin, Spin
from qrules.quantum_numbers import InteractionProperties, _to_fraction
from qrules.particle import (
Particle,
ParticleWithSpin,
Spin,
_float_as_signed_fraction_str,
_to_signed_fraction,
)
from qrules.quantum_numbers import InteractionProperties
from qrules.solving import EdgeSettings, NodeSettings, QNProblemSet, QNResult
from qrules.topology import FrozenTransition, MutableTransition, Topology, Transition
from qrules.transition import ProblemSet, ReactionInfo, State
Expand Down Expand Up @@ -339,18 +346,18 @@ def _(obj: InteractionProperties) -> str:
lines = []
if obj.l_magnitude is not None:
if obj.l_projection is None:
l_label = _to_fraction(obj.l_magnitude)
l_label = _to_signed_fraction(Fraction(obj.l_magnitude))
else:
l_label = _spin_to_str(Spin(obj.l_magnitude, obj.l_projection))
lines.append(f"L={l_label}")
if obj.s_magnitude is not None:
if obj.s_projection is None:
s_label = _to_fraction(obj.s_magnitude)
s_label = _to_signed_fraction(Fraction(obj.s_magnitude))
else:
s_label = _spin_to_str(Spin(obj.s_magnitude, obj.s_projection))
lines.append(f"S={s_label}")
if obj.parity_prefactor is not None:
label = _to_fraction(obj.parity_prefactor, render_plus=True)
label = _to_signed_fraction(Fraction(obj.parity_prefactor), render_plus=True)
lines.append(f"P={label}")
return "\n".join(lines)

Expand Down Expand Up @@ -408,15 +415,19 @@ def _(particle: Particle) -> str:

@as_string.register(Spin)
def _spin_to_str(spin: Spin) -> str:
spin_magnitude = _to_fraction(spin.magnitude)
spin_projection = _to_fraction(spin.projection, render_plus=True)
spin_magnitude = _float_as_signed_fraction_str(float(spin.magnitude))
spin_projection = _float_as_signed_fraction_str(
float(spin.projection), render_plus=True
)
return f"|{spin_magnitude},{spin_projection}⟩"


@as_string.register(State)
def _state_to_str(state: State) -> str:
particle = state.particle.name
spin_projection = _to_fraction(state.spin_projection, render_plus=True)
spin_projection = _float_as_signed_fraction_str(
state.spin_projection, render_plus=True
)
return f"{particle}[{spin_projection}]"


Expand Down
97 changes: 64 additions & 33 deletions src/qrules/particle.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import sys
from collections import abc
from difflib import get_close_matches
from fractions import Fraction
from functools import total_ordering
from math import copysign
from typing import (
Expand All @@ -34,54 +35,66 @@
from attrs.validators import instance_of

from qrules.conservation_rules import GellMannNishijimaInput, gellmann_nishijima
from qrules.quantum_numbers import Parity, _to_fraction
from qrules.quantum_numbers import Parity

if sys.version_info < (3, 11):
from typing_extensions import Self
else:
from typing import Self
if TYPE_CHECKING:
from attr import Attribute
from IPython.lib.pretty import PrettyPrinter
from particle import Particle as PdgDatabase
from particle.particle import enums

_LOGGER = logging.getLogger(__name__)


def _to_float(value: SupportsFloat) -> float:
def _to_fraction(value: SupportsFloat) -> Fraction:
float_value = float(value)
if float_value == -0.0:
float_value = 0.0
return float_value
return Fraction(float_value)


def _validate_fraction_for_spin(
instance: Spin,
attribute: Attribute, # noqa: ARG001
value: Fraction, # noqa: ARG001
) -> Any:
if instance.magnitude % Fraction(1, 2) != Fraction(0, 1):
msg = f"Spin magnitude {instance.magnitude} has to be a multitude of 0.5"
raise ValueError(msg)
if abs(instance.projection) > instance.magnitude:
if instance.magnitude < Fraction(0, 1):
msg = f"Spin magnitude has to be positive, but is {instance.magnitude}"
raise ValueError(msg)
msg = (
"Absolute value of spin projection cannot be larger than its"
f" magnitude:\n abs({instance.projection}) > {instance.magnitude}"
)
raise ValueError(msg)
if (instance.projection - instance.magnitude).denominator != 1:
msg = (
f"{type(instance).__name__}{(instance.magnitude, instance.projection)}: (projection -"
" magnitude) should be integer"
)
raise ValueError(msg)


@total_ordering
@frozen(eq=False, hash=True, order=False)
class Spin: # noqa: PLW1641
"""Safe, immutable data container for spin **with projection**."""

magnitude: float = field(converter=_to_float)
projection: float = field(converter=_to_float)

def __attrs_post_init__(self) -> None:
if self.magnitude % 0.5 != 0.0:
msg = f"Spin magnitude {self.magnitude} has to be a multitude of 0.5"
raise ValueError(msg)
if abs(self.projection) > self.magnitude:
if self.magnitude < 0.0:
msg = f"Spin magnitude has to be positive, but is {self.magnitude}"
raise ValueError(msg)
msg = (
"Absolute value of spin projection cannot be larger than its"
f" magnitude:\n abs({self.projection}) > {self.magnitude}"
)
raise ValueError(msg)
if not (self.projection - self.magnitude).is_integer():
msg = (
f"{type(self).__name__}{self.magnitude, self.projection}: (projection -"
" magnitude) should be integer"
)
raise ValueError(msg)
magnitude: Fraction = field(
converter=_to_fraction,
validator=_validate_fraction_for_spin,
)
projection: Fraction = field(
converter=_to_fraction,
validator=_validate_fraction_for_spin,
)

def __eq__(self, other: object) -> bool:
if isinstance(other, Spin):
Expand All @@ -92,7 +105,7 @@ def __eq__(self, other: object) -> bool:
return self.magnitude == other

def __float__(self) -> float:
return self.magnitude
return float(self.magnitude)

def __gt__(self, other: Any) -> bool:
if isinstance(other, Spin):
Expand All @@ -107,16 +120,23 @@ def __repr__(self) -> str:

def _repr_pretty_(self, p: PrettyPrinter, _: bool) -> None:
class_name = type(self).__name__
magnitude = _to_fraction(self.magnitude)
projection = _to_fraction(self.projection, render_plus=True)
magnitude = _to_signed_fraction(self.magnitude)
projection = _to_signed_fraction(self.projection, render_plus=True)
p.text(f"{class_name}({magnitude}, {projection})")


def _to_signed_fraction(fraction: Fraction, render_plus: bool = False) -> str:
string_representation = str(fraction)
if render_plus and fraction.numerator > 0:
return f"+{string_representation}"
return string_representation


def _to_parity(value: Parity | int) -> Parity:
return Parity(int(value))


def _to_spin(value: Spin | tuple[float, float]) -> Spin:
def _to_spin(value: Spin | tuple[Fraction, Fraction]) -> Spin:
if isinstance(value, tuple):
return Spin(*value)
return value
Expand Down Expand Up @@ -227,14 +247,25 @@ def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None:
p.breakable()
p.text(f"{attribute.name}=")
if isinstance(value, Parity):
p.text(_to_fraction(int(value), render_plus=True))
p.text(
_float_as_signed_fraction_str(
int(value), render_plus=True
)
)
else:
p.pretty(value) # type: ignore[attr-defined]
p.text(",")
p.breakable()
p.text(")")


def _float_as_signed_fraction_str(value: float, render_plus: bool = False) -> str:
label = str(Fraction(value))
if render_plus and value > 0:
return f"+{label}"
return label


def _get_name_root(name: str) -> str:
"""Strip a string (particularly the `.Particle.name`) of specifications."""
name_root = name
Expand Down Expand Up @@ -610,12 +641,12 @@ def __compute_baryonnumber(pdg_particle: PdgDatabase) -> int:
def __create_isospin(pdg_particle: PdgDatabase) -> Spin | None:
if pdg_particle.I is None:
return None
magnitude = pdg_particle.I
magnitude = Fraction(pdg_particle.I)
projection = __isospin_projection_from_pdg(pdg_particle)
return Spin(magnitude, projection)


def __isospin_projection_from_pdg(pdg_particle: PdgDatabase) -> float:
def __isospin_projection_from_pdg(pdg_particle: PdgDatabase) -> Fraction:
if pdg_particle.charge is None:
msg = f"PDG instance has no charge:\n{pdg_particle}"
raise ValueError(msg)
Expand All @@ -637,7 +668,7 @@ def __isospin_projection_from_pdg(pdg_particle: PdgDatabase) -> float:
if pdg_particle.I is not None and not (pdg_particle.I - projection).is_integer():
msg = f"Cannot have isospin {pdg_particle.I, projection}"
raise ValueError(msg)
return projection
return Fraction(projection)


def __filter_quark_content(pdg_particle: PdgDatabase) -> str:
Expand Down
Loading
Loading