From ec1c54e1200270f49adadf167b5b3f7d94df34db Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Fri, 4 Oct 2024 17:10:06 +0200 Subject: [PATCH 01/45] changed Spin to have Fraction-fields --- src/qrules/particle.py | 74 ++++++++++++++++++++++++++++------- src/qrules/quantum_numbers.py | 20 +++++----- 2 files changed, 70 insertions(+), 24 deletions(-) diff --git a/src/qrules/particle.py b/src/qrules/particle.py index 899b5449..a10aa8d9 100644 --- a/src/qrules/particle.py +++ b/src/qrules/particle.py @@ -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 ( @@ -29,12 +30,13 @@ ) import attrs +from attr import Attribute from attrs import field, frozen from attrs.converters import optional 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 @@ -48,11 +50,27 @@ _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, value: Fraction +) -> Any: + if value.denominator != 1 or value.denominator != 2: + msg = f"Spin magnitude/projection {value} has to have a denominator of 1 or 2" + raise ValueError(msg) + + +def _validate_spin_magnitude_projection_integrity( + instance: Spin, attribute: Attribute, value: Fraction +) -> None: + if abs(instance.projection) > instance.magnitude: + msg = f"Spin magnitude {instance.magnitude} has to be greater than abs(projection) = {abs(instance.projection)}" + raise ValueError(msg) @total_ordering @@ -60,8 +78,20 @@ def _to_float(value: SupportsFloat) -> float: class Spin: # noqa: PLW1641 """Safe, immutable data container for spin **with projection**.""" - magnitude: float = field(converter=_to_float) - projection: float = field(converter=_to_float) + magnitude: Fraction = field( + converter=_to_fraction, + validator=[ + _validate_fraction_for_spin, + _validate_spin_magnitude_projection_integrity, + ], + ) + projection: Fraction = field( + converter=_to_fraction, + validator=[ + _validate_fraction_for_spin, + _validate_spin_magnitude_projection_integrity, + ], + ) def __attrs_post_init__(self) -> None: if self.magnitude % 0.5 != 0.0: @@ -76,7 +106,7 @@ def __attrs_post_init__(self) -> None: f" magnitude:\n abs({self.projection}) > {self.magnitude}" ) raise ValueError(msg) - if not (self.projection - self.magnitude).is_integer(): + if (self.projection - self.magnitude).denominator != 1: msg = ( f"{type(self).__name__}{self.magnitude, self.projection}: (projection -" " magnitude) should be integer" @@ -92,7 +122,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): @@ -107,16 +137,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 @@ -227,7 +264,9 @@ 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( + _as_signed_fraction_str(int(value), render_plus=True) + ) else: p.pretty(value) # type: ignore[attr-defined] p.text(",") @@ -235,6 +274,13 @@ def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: p.text(")") +def _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 @@ -610,12 +656,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) @@ -637,7 +683,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: diff --git a/src/qrules/quantum_numbers.py b/src/qrules/quantum_numbers.py index a1a4f63f..8fa6948c 100644 --- a/src/qrules/quantum_numbers.py +++ b/src/qrules/quantum_numbers.py @@ -58,14 +58,14 @@ def __neg__(self) -> Parity: return Parity(-self.value) def __repr__(self) -> str: - return f"{type(self).__name__}({_to_fraction(self.value)})" + return f"{type(self).__name__}({_float_as_signed_str(self.value)})" -def _to_fraction(value: float, render_plus: bool = False) -> str: - label = str(Fraction(value)) - if render_plus and value > 0: - return f"+{label}" - return label +def _float_as_signed_str(value: float) -> str: + string_representation = str(value) + if value > 0: + return f"+{string_representation}" + return string_representation @frozen(init=False) @@ -83,11 +83,11 @@ class EdgeQuantumNumbers: pid = NewType("pid", int) mass = NewType("mass", float) width = NewType("width", float) - spin_magnitude = NewType("spin_magnitude", float) - spin_projection = NewType("spin_projection", float) + spin_magnitude = NewType("spin_magnitude", Fraction) + spin_projection = NewType("spin_projection", Fraction) charge = NewType("charge", int) - isospin_magnitude = NewType("isospin_magnitude", float) - isospin_projection = NewType("isospin_projection", float) + isospin_magnitude = NewType("isospin_magnitude", Fraction) + isospin_projection = NewType("isospin_projection", Fraction) strangeness = NewType("strangeness", int) charmness = NewType("charmness", int) bottomness = NewType("bottomness", int) From c4c2504c6442a6f23b84c115dcb9b126944c3dcb Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 7 Oct 2024 16:15:16 +0200 Subject: [PATCH 02/45] floats in Spin substituted with Fractions, regex-tests fail --- src/qrules/conservation_rules.py | 12 ++++++---- src/qrules/io/_dict.py | 3 +++ src/qrules/io/_dot.py | 27 +++++++++++++++------- src/qrules/particle.py | 8 ++++--- src/qrules/quantum_numbers.py | 22 ++++++++++-------- src/qrules/settings.py | 3 ++- src/qrules/transition.py | 4 ++-- tests/unit/conservation_rules/test_spin.py | 23 ++++++++++++++++-- tests/unit/test_particle.py | 16 +++++++------ tests/unit/test_quantum_numbers.py | 5 ++-- 10 files changed, 83 insertions(+), 40 deletions(-) diff --git a/src/qrules/conservation_rules.py b/src/qrules/conservation_rules.py index 1863a331..5421421c 100644 --- a/src/qrules/conservation_rules.py +++ b/src/qrules/conservation_rules.py @@ -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 @@ -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 @@ -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) @@ -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 @@ -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 diff --git a/src/qrules/io/_dict.py b/src/qrules/io/_dict.py index 3d52249f..6e801566 100644 --- a/src/qrules/io/_dict.py +++ b/src/qrules/io/_dict.py @@ -4,6 +4,7 @@ import json from collections import abc +from fractions import Fraction from os.path import dirname, realpath from typing import Any @@ -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 diff --git a/src/qrules/io/_dot.py b/src/qrules/io/_dot.py index e9ffd723..c79d74b8 100644 --- a/src/qrules/io/_dot.py +++ b/src/qrules/io/_dot.py @@ -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 @@ -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 @@ -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) @@ -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}]" diff --git a/src/qrules/particle.py b/src/qrules/particle.py index a10aa8d9..04f5574f 100644 --- a/src/qrules/particle.py +++ b/src/qrules/particle.py @@ -60,7 +60,7 @@ def _to_fraction(value: SupportsFloat) -> Fraction: def _validate_fraction_for_spin( instance: Spin, attribute: Attribute, value: Fraction ) -> Any: - if value.denominator != 1 or value.denominator != 2: + if value % Fraction(1, 2) != 0: msg = f"Spin magnitude/projection {value} has to have a denominator of 1 or 2" raise ValueError(msg) @@ -265,7 +265,9 @@ def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: p.text(f"{attribute.name}=") if isinstance(value, Parity): p.text( - _as_signed_fraction_str(int(value), render_plus=True) + _float_as_signed_fraction_str( + int(value), render_plus=True + ) ) else: p.pretty(value) # type: ignore[attr-defined] @@ -274,7 +276,7 @@ def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: p.text(")") -def _as_signed_fraction_str(value: float, render_plus: bool = False) -> str: +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}" diff --git a/src/qrules/quantum_numbers.py b/src/qrules/quantum_numbers.py index 8fa6948c..76cb41e6 100644 --- a/src/qrules/quantum_numbers.py +++ b/src/qrules/quantum_numbers.py @@ -9,7 +9,6 @@ from __future__ import annotations import sys -from decimal import Decimal from fractions import Fraction from functools import total_ordering from typing import Any, Generator, NewType, Union @@ -135,11 +134,11 @@ class EdgeQuantumNumbers: 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) + l_magnitude = NewType("l_magnitude", Fraction) + l_projection = NewType("l_projection", Fraction) + s_magnitude = NewType("s_magnitude", Fraction) + s_projection = NewType("s_projection", Fraction) + parity_prefactor = NewType("parity_prefactor", Fraction) for node_qn_name, node_qn_type in NodeQuantumNumbers.__dict__.items(): @@ -198,8 +197,11 @@ class InteractionProperties: parity_prefactor: float | None = field(default=None, converter=_to_optional_float) -def arange(x_1: float, x_2: float, delta: float = 1.0) -> Generator[float, None, None]: - current = Decimal(x_1) +def arange( + x_1: float, x_2: float, delta: float = 1.0 +) -> Generator[Fraction, None, None]: + current = Fraction(x_1) + delta = Fraction(delta) while current < x_2: - yield float(current) - current += Decimal(delta) + yield current + current += delta diff --git a/src/qrules/settings.py b/src/qrules/settings.py index 1bf920d1..a47027aa 100644 --- a/src/qrules/settings.py +++ b/src/qrules/settings.py @@ -319,7 +319,8 @@ def _halves_domain(start: float, stop: float) -> list[float]: msg = f"Stop value {stop} needs to be multiple of 0.5" raise ValueError(msg) return [ - int(v) if v.is_integer() else v for v in arange(start, stop + 0.25, delta=0.5) + int(v) if v.denominator == 1 else v + for v in arange(start, stop + 0.25, delta=0.5) ] diff --git a/src/qrules/transition.py b/src/qrules/transition.py index 1b2939c6..a39cf30a 100644 --- a/src/qrules/transition.py +++ b/src/qrules/transition.py @@ -30,7 +30,7 @@ Particle, ParticleCollection, ParticleWithSpin, - _to_float, + _to_fraction, load_pdg, ) from qrules.quantum_numbers import ( @@ -733,7 +733,7 @@ def _strip_spin(state_definition: Sequence[StateDefinition]) -> list[str]: @frozen(order=True) class State: particle: Particle = field(validator=instance_of(Particle)) - spin_projection: float = field(converter=_to_float) + spin_projection: float = field(converter=_to_fraction) StateTransition = FrozenTransition[State, InteractionProperties] diff --git a/tests/unit/conservation_rules/test_spin.py b/tests/unit/conservation_rules/test_spin.py index 921f1a56..a3a5abc5 100644 --- a/tests/unit/conservation_rules/test_spin.py +++ b/tests/unit/conservation_rules/test_spin.py @@ -1,5 +1,6 @@ from __future__ import annotations +from fractions import Fraction from typing import List, Tuple import pytest @@ -129,7 +130,16 @@ def test_spin_all_defined(rule_input: _SpinRuleInputType, expected: bool) -> Non ("rule_input", "expected"), [ ( - ([1], [spin2_mag, 1], SpinNodeInput(ang_mom_mag, 0, coupled_spin_mag, -1)), + ( + [1], + [spin2_mag, 1], + SpinNodeInput( + Fraction(ang_mom_mag), + Fraction(0), + Fraction(coupled_spin_mag), + Fraction(-1), + ), + ), True, ) for spin2_mag, ang_mom_mag, coupled_spin_mag in zip( @@ -138,7 +148,16 @@ def test_spin_all_defined(rule_input: _SpinRuleInputType, expected: bool) -> Non ] + [ ( - ([1], [spin2_mag, 1], SpinNodeInput(ang_mom_mag, 0, coupled_spin_mag, 0)), + ( + [1], + [spin2_mag, 1], + SpinNodeInput( + Fraction(ang_mom_mag), + Fraction(0), + Fraction(coupled_spin_mag), + Fraction(0), + ), + ), False, ) for spin2_mag, ang_mom_mag, coupled_spin_mag in zip( diff --git a/tests/unit/test_particle.py b/tests/unit/test_particle.py index 4d68c984..5e276a70 100644 --- a/tests/unit/test_particle.py +++ b/tests/unit/test_particle.py @@ -3,6 +3,7 @@ import logging import sys from copy import deepcopy +from fractions import Fraction import pytest from attrs.exceptions import FrozenInstanceError @@ -31,8 +32,10 @@ class TestParticle: @pytest.mark.parametrize("repr_method", [repr, pretty]) def test_repr(self, particle_database: ParticleCollection, repr_method): + local_namespace = locals() + local_namespace["Fraction"] = Fraction for instance in particle_database: - from_repr = eval(repr_method(instance)) + from_repr = eval(repr_method(instance), None, local_namespace) assert from_repr == instance @pytest.mark.parametrize( @@ -340,8 +343,8 @@ def test_init_and_eq(self): assert isospin.magnitude == 1.5 assert isospin.projection == -0.5 isospin = Spin(1, -0.0) - assert isinstance(isospin.magnitude, float) - assert isinstance(isospin.projection, float) + assert isinstance(isospin.magnitude, Fraction) + assert isinstance(isospin.projection, Fraction) assert isospin.magnitude == 1.0 assert isospin.projection == 0.0 @@ -383,10 +386,9 @@ def test_repr(self, instance: Spin, repr_method): [(0.3, 0.3), (1.0, 0.5), (0.5, 0.0), (-0.5, 0.5)], ) def test_exceptions(self, magnitude, projection): - regex_pattern = "|".join([ # noqa: FLY002 - r"Spin magnitude \d\.\d has to be a multitude of \d\.[05]", - r"\(projection - magnitude\) should be integer", - r"Spin magnitude has to be positive", + regex_pattern = "|".join([ + r"Spin magnitude \d/\d has to be", + r"greater than abs\(projection\) = \d/\d", ]) regex_pattern = f"({regex_pattern})" with pytest.raises(ValueError, match=regex_pattern): diff --git a/tests/unit/test_quantum_numbers.py b/tests/unit/test_quantum_numbers.py index 38ac3e95..7be677d4 100644 --- a/tests/unit/test_quantum_numbers.py +++ b/tests/unit/test_quantum_numbers.py @@ -3,7 +3,8 @@ import pytest -from qrules.quantum_numbers import Parity, _to_fraction +from qrules.particle import _float_as_signed_fraction_str +from qrules.quantum_numbers import Parity class TestParity: @@ -68,4 +69,4 @@ def test_exceptions(self): ], ) def test_to_fraction(value, render_plus: bool, expected: str): - assert _to_fraction(value, render_plus) == expected + assert _float_as_signed_fraction_str(value, render_plus) == expected From 3dc40d0045a4bb8339a4f0d4dfc4748a87c35a49 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Tue, 8 Oct 2024 12:06:20 +0200 Subject: [PATCH 03/45] fixed regex-patterns in test_particle -> tests pass --- src/qrules/particle.py | 59 +++++++++++++---------------------- tests/unit/test_particle.py | 29 ++++++++++------- tests/unit/test_transition.py | 6 +++- 3 files changed, 44 insertions(+), 50 deletions(-) diff --git a/src/qrules/particle.py b/src/qrules/particle.py index 04f5574f..4fbcc47d 100644 --- a/src/qrules/particle.py +++ b/src/qrules/particle.py @@ -30,7 +30,6 @@ ) import attrs -from attr import Attribute from attrs import field, frozen from attrs.converters import optional from attrs.validators import instance_of @@ -43,6 +42,7 @@ 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 @@ -58,18 +58,27 @@ def _to_fraction(value: SupportsFloat) -> Fraction: def _validate_fraction_for_spin( - instance: Spin, attribute: Attribute, value: Fraction + instance: Spin, + attribute: Attribute, # noqa: ARG001 + value: Fraction, # noqa: ARG001 ) -> Any: - if value % Fraction(1, 2) != 0: - msg = f"Spin magnitude/projection {value} has to have a denominator of 1 or 2" + 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) - - -def _validate_spin_magnitude_projection_integrity( - instance: Spin, attribute: Attribute, value: Fraction -) -> None: if abs(instance.projection) > instance.magnitude: - msg = f"Spin magnitude {instance.magnitude} has to be greater than abs(projection) = {abs(instance.projection)}" + 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) @@ -80,39 +89,13 @@ class Spin: # noqa: PLW1641 magnitude: Fraction = field( converter=_to_fraction, - validator=[ - _validate_fraction_for_spin, - _validate_spin_magnitude_projection_integrity, - ], + validator=_validate_fraction_for_spin, ) projection: Fraction = field( converter=_to_fraction, - validator=[ - _validate_fraction_for_spin, - _validate_spin_magnitude_projection_integrity, - ], + validator=_validate_fraction_for_spin, ) - 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 (self.projection - self.magnitude).denominator != 1: - msg = ( - f"{type(self).__name__}{self.magnitude, self.projection}: (projection -" - " magnitude) should be integer" - ) - raise ValueError(msg) - def __eq__(self, other: object) -> bool: if isinstance(other, Spin): return ( diff --git a/tests/unit/test_particle.py b/tests/unit/test_particle.py index 5e276a70..c61edbde 100644 --- a/tests/unit/test_particle.py +++ b/tests/unit/test_particle.py @@ -29,13 +29,17 @@ from importlib.metadata import version +NAMESPACE_WITH_FRACTIONS = globals() +NAMESPACE_WITH_FRACTIONS["Fraction"] = Fraction + + class TestParticle: @pytest.mark.parametrize("repr_method", [repr, pretty]) def test_repr(self, particle_database: ParticleCollection, repr_method): local_namespace = locals() local_namespace["Fraction"] = Fraction for instance in particle_database: - from_repr = eval(repr_method(instance), None, local_namespace) + from_repr = eval(repr_method(instance), NAMESPACE_WITH_FRACTIONS) assert from_repr == instance @pytest.mark.parametrize( @@ -61,7 +65,7 @@ def test_exceptions(self): width=0.1, spin=1, charge=0, - isospin=(1, 0), + isospin=(Fraction(1), Fraction(0)), ) with pytest.raises(FrozenInstanceError): test_state.charge = 1 # type: ignore[misc] @@ -78,7 +82,7 @@ def test_exceptions(self): parity=-1, c_parity=-1, g_parity=-1, - isospin=(0, 0), + isospin=(Fraction(0), Fraction(0)), charmness=1, ) @@ -89,7 +93,7 @@ def test_eq(self): mass=1.2, spin=1, charge=0, - isospin=(1, 0), + isospin=(Fraction(1), Fraction(0)), ) assert particle != Particle( name="MyParticle", pid=123, mass=1.5, width=0.2, spin=1 @@ -104,7 +108,7 @@ def test_eq(self): mass=1.2, spin=1, charge=0, - isospin=(1, 0), + isospin=(Fraction(1), Fraction(0)), ) assert particle == different_labels assert hash(particle) == hash(different_labels) @@ -179,7 +183,9 @@ def test_equality(self, particle_database: ParticleCollection): @pytest.mark.parametrize("repr_method", [repr, pretty]) def test_repr(self, particle_database: ParticleCollection, repr_method): instance = particle_database - from_repr = eval(repr_method(instance)) + local_namespace = locals() + local_namespace["Fraction"] = Fraction + from_repr = eval(repr_method(instance), NAMESPACE_WITH_FRACTIONS) assert from_repr == instance def test_add(self, particle_database: ParticleCollection): @@ -313,7 +319,7 @@ def test_find_fail( list_str = message.strip("?") *_, list_str = list_str.split("Did you mean ") *_, list_str = list_str.split("one of these? ") - found_particles = eval(list_str) + found_particles = eval(list_str, NAMESPACE_WITH_FRACTIONS) assert found_particles == expected def test_exceptions(self, particle_database: ParticleCollection): @@ -378,7 +384,7 @@ def test_neg(self): "instance", [Spin(2.5, -0.5), Spin(1, 0), Spin(3, -1), Spin(0, 0)] ) def test_repr(self, instance: Spin, repr_method): - from_repr = eval(repr_method(instance)) + from_repr = eval(repr_method(instance), NAMESPACE_WITH_FRACTIONS) assert from_repr == instance @pytest.mark.parametrize( @@ -387,10 +393,11 @@ def test_repr(self, instance: Spin, repr_method): ) def test_exceptions(self, magnitude, projection): regex_pattern = "|".join([ - r"Spin magnitude \d/\d has to be", - r"greater than abs\(projection\) = \d/\d", + r"Spin magnitude \d*/\d* has to be a multitude of \d\.[05]", + r"\(projection - magnitude\) should be integer", + r"Spin magnitude has to be positive", + r"Absolute value of spin projection cannot be larger than its", ]) - regex_pattern = f"({regex_pattern})" with pytest.raises(ValueError, match=regex_pattern): print(Spin(magnitude, projection)) diff --git a/tests/unit/test_transition.py b/tests/unit/test_transition.py index f1588ec9..3fe3ff7b 100644 --- a/tests/unit/test_transition.py +++ b/tests/unit/test_transition.py @@ -1,5 +1,6 @@ # pyright: reportUnusedImport=false from copy import deepcopy +from fractions import Fraction import pytest from IPython.lib.pretty import pretty @@ -16,6 +17,9 @@ ) from qrules.transition import ReactionInfo, State, StateTransitionManager +NAMESPACE_WITH_FRACTIONS = globals() +NAMESPACE_WITH_FRACTIONS["Fraction"] = Fraction + class TestReactionInfo: def test_properties(self, reaction: ReactionInfo): @@ -34,7 +38,7 @@ def test_properties(self, reaction: ReactionInfo): @pytest.mark.parametrize("repr_method", [repr, pretty]) def test_repr(self, repr_method, reaction: ReactionInfo): instance = reaction - from_repr = eval(repr_method(instance)) + from_repr = eval(repr_method(instance), NAMESPACE_WITH_FRACTIONS) assert from_repr == instance def test_hash(self, reaction: ReactionInfo): From 3a54c4e5de553462f24de05c8600acc59ca383cd Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Tue, 8 Oct 2024 13:32:07 +0200 Subject: [PATCH 04/45] substituted global namespace variable with function --- tests/unit/test_particle.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_particle.py b/tests/unit/test_particle.py index c61edbde..639fe099 100644 --- a/tests/unit/test_particle.py +++ b/tests/unit/test_particle.py @@ -29,8 +29,10 @@ from importlib.metadata import version -NAMESPACE_WITH_FRACTIONS = globals() -NAMESPACE_WITH_FRACTIONS["Fraction"] = Fraction +def gen_namespace_with_fraction(): + namespace = globals() + namespace["Fraction"] = Fraction + return namespace class TestParticle: @@ -39,7 +41,7 @@ def test_repr(self, particle_database: ParticleCollection, repr_method): local_namespace = locals() local_namespace["Fraction"] = Fraction for instance in particle_database: - from_repr = eval(repr_method(instance), NAMESPACE_WITH_FRACTIONS) + from_repr = eval(repr_method(instance), None, gen_namespace_with_fraction()) assert from_repr == instance @pytest.mark.parametrize( @@ -185,7 +187,7 @@ def test_repr(self, particle_database: ParticleCollection, repr_method): instance = particle_database local_namespace = locals() local_namespace["Fraction"] = Fraction - from_repr = eval(repr_method(instance), NAMESPACE_WITH_FRACTIONS) + from_repr = eval(repr_method(instance), None, gen_namespace_with_fraction()) assert from_repr == instance def test_add(self, particle_database: ParticleCollection): @@ -319,7 +321,7 @@ def test_find_fail( list_str = message.strip("?") *_, list_str = list_str.split("Did you mean ") *_, list_str = list_str.split("one of these? ") - found_particles = eval(list_str, NAMESPACE_WITH_FRACTIONS) + found_particles = eval(list_str, None), gen_namespace_with_fraction() assert found_particles == expected def test_exceptions(self, particle_database: ParticleCollection): @@ -384,7 +386,7 @@ def test_neg(self): "instance", [Spin(2.5, -0.5), Spin(1, 0), Spin(3, -1), Spin(0, 0)] ) def test_repr(self, instance: Spin, repr_method): - from_repr = eval(repr_method(instance), NAMESPACE_WITH_FRACTIONS) + from_repr = eval(repr_method(instance), None), gen_namespace_with_fraction() assert from_repr == instance @pytest.mark.parametrize( From 80807e93955639508a048c489ad884a989f846a9 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Tue, 8 Oct 2024 13:35:38 +0200 Subject: [PATCH 05/45] fixed typo --- tests/unit/test_particle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_particle.py b/tests/unit/test_particle.py index 639fe099..029974a1 100644 --- a/tests/unit/test_particle.py +++ b/tests/unit/test_particle.py @@ -321,7 +321,7 @@ def test_find_fail( list_str = message.strip("?") *_, list_str = list_str.split("Did you mean ") *_, list_str = list_str.split("one of these? ") - found_particles = eval(list_str, None), gen_namespace_with_fraction() + found_particles = eval(list_str, None, gen_namespace_with_fraction()) assert found_particles == expected def test_exceptions(self, particle_database: ParticleCollection): @@ -386,7 +386,7 @@ def test_neg(self): "instance", [Spin(2.5, -0.5), Spin(1, 0), Spin(3, -1), Spin(0, 0)] ) def test_repr(self, instance: Spin, repr_method): - from_repr = eval(repr_method(instance), None), gen_namespace_with_fraction() + from_repr = eval(repr_method(instance), None, gen_namespace_with_fraction()) assert from_repr == instance @pytest.mark.parametrize( From a1d48fa2b35eb228f1670042de2eb09525ecf5cd Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Tue, 8 Oct 2024 17:03:14 +0200 Subject: [PATCH 06/45] added rendering for fractions --- src/qrules/io/_dot.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/qrules/io/_dot.py b/src/qrules/io/_dot.py index c79d74b8..61340f43 100644 --- a/src/qrules/io/_dot.py +++ b/src/qrules/io/_dot.py @@ -321,12 +321,12 @@ def _(obj: dict) -> str: key_repr = key if value != 0 or any(s in key_repr for s in ["magnitude", "projection"]): pm = not any(s in key_repr for s in ["pid", "mass", "width", "magnitude"]) - value_repr = __render_fraction(value, pm) + value_repr = __render_as_fraction(value, pm) lines.append(f"{key_repr} = {value_repr}") return "\n".join(lines) -def __render_fraction(value: Any, plusminus: bool) -> str: +def __render_as_fraction(value: Any, plusminus: bool) -> str: plusminus &= isinstance(value, Number) and bool(value) if isinstance(value, float): if value.is_integer(): @@ -336,11 +336,21 @@ def __render_fraction(value: Any, plusminus: bool) -> str: if plusminus: return f"{nom:+}/{denom}" return f"{nom}/{denom}" + if isinstance(value, Fraction): + return _render_fraction(value, plusminus) if plusminus: return f"{value:+}" return str(value) +def _render_fraction(fraction: Fraction, plusminus: bool) -> str: + if fraction.denominator == 1: + return str(int(fraction)) + if plusminus: + return f"{fraction.numerator:+}/{fraction.denominator}" + return f"{fraction.numerator}/{fraction.denominator}" + + @as_string.register(InteractionProperties) def _(obj: InteractionProperties) -> str: lines = [] From 1957f5bf366f4e9d0f266f738f8a34335751932c Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Wed, 9 Oct 2024 14:12:41 +0200 Subject: [PATCH 07/45] suppressed warnings in io & tests --- src/qrules/io/_dict.py | 2 +- tests/unit/test_particle.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qrules/io/_dict.py b/src/qrules/io/_dict.py index 6e801566..f8c8017d 100644 --- a/src/qrules/io/_dict.py +++ b/src/qrules/io/_dict.py @@ -29,7 +29,7 @@ def from_attrs_decorated(inst: Any) -> dict: ) -def _value_serializer(inst: type, field: attrs.Attribute, value: Any) -> Any: # noqa: ARG001 +def _value_serializer(inst: type, field: attrs.Attribute, value: Any) -> Any: # noqa: ARG001, PLR0911 if isinstance(value, abc.Mapping): if all(isinstance(p, Particle) for p in value.values()): return {k: v.name for k, v in value.items()} diff --git a/tests/unit/test_particle.py b/tests/unit/test_particle.py index e20af490..635a7399 100644 --- a/tests/unit/test_particle.py +++ b/tests/unit/test_particle.py @@ -394,7 +394,7 @@ def test_repr(self, instance: Spin, repr_method): [(0.3, 0.3), (1.0, 0.5), (0.5, 0.0), (-0.5, 0.5)], ) def test_exceptions(self, magnitude, projection): - regex_pattern = "|".join([ + regex_pattern = "|".join([ # noqa: FLY002 r"Spin magnitude \d*/\d* has to be a multitude of \d\.[05]", r"\(projection - magnitude\) should be integer", r"Spin magnitude has to be positive", From 64d009cc61b53c1371061d6ad58803a19230e5f3 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Wed, 16 Oct 2024 10:57:44 +0200 Subject: [PATCH 08/45] Union-type for _Spin --- src/qrules/conservation_rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qrules/conservation_rules.py b/src/qrules/conservation_rules.py index 5421421c..05b42f2f 100644 --- a/src/qrules/conservation_rules.py +++ b/src/qrules/conservation_rules.py @@ -436,8 +436,8 @@ def _check_particles_identical( @frozen class _Spin: - magnitude: float - projection: float + magnitude: Union[NodeQN.s_magnitude, NodeQN.l_magnitude] + projection: Union[NodeQN.s_projection, NodeQN.l_projection] def _is_clebsch_gordan_coefficient_zero( From 6bbf7c02ff63dfddf24ee5c9abffdcbc7cd86df6 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Fri, 8 Nov 2024 17:07:40 +0100 Subject: [PATCH 09/45] changed conservation_rules and `arange` to use `Fraction` only --- src/qrules/conservation_rules.py | 51 +++++++++++++++----------------- src/qrules/quantum_numbers.py | 2 +- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/qrules/conservation_rules.py b/src/qrules/conservation_rules.py index 2211d132..57d17c73 100644 --- a/src/qrules/conservation_rules.py +++ b/src/qrules/conservation_rules.py @@ -431,8 +431,8 @@ def _check_particles_identical( @frozen class _Spin: - magnitude: Union[NodeQN.s_magnitude, NodeQN.l_magnitude] - projection: Union[NodeQN.s_projection, NodeQN.l_projection] + magnitude: Union[Fraction, NodeQN.s_magnitude, NodeQN.l_magnitude] + projection: Union[Fraction, NodeQN.s_projection, NodeQN.l_projection] def _is_clebsch_gordan_coefficient_zero( @@ -470,26 +470,25 @@ class SpinMagnitudeNodeInput: def ls_spin_validity(spin_input: SpinNodeInput) -> bool: r"""Check for valid isospin magnitude and projection.""" return _check_spin_valid( - float(spin_input.l_magnitude), float(spin_input.l_projection) - ) and _check_spin_valid( - float(spin_input.s_magnitude), float(spin_input.s_projection) - ) + spin_input.l_magnitude, spin_input.l_projection + ) and _check_spin_valid(spin_input.s_magnitude, spin_input.s_projection) def _check_magnitude( - in_part: list[float], - out_part: list[float], + in_part: list[Fraction], + out_part: list[Fraction], interaction_qns: Optional[Union[SpinMagnitudeNodeInput, SpinNodeInput]], ) -> bool: - def couple_mags(j_1: float, j_2: float) -> list[float]: + def couple_mags(j_1: Fraction, j_2: Fraction) -> list[Fraction]: return [ - x / 2.0 for x in range(int(2 * abs(j_1 - j_2)), int(2 * (j_1 + j_2 + 1)), 2) + Fraction(x, 2) + for x in range(int(2 * abs(j_1 - j_2)), int(2 * (j_1 + j_2 + 1)), 2) ] def couple_magnitudes( - magnitudes: list[float], + magnitudes: list[Fraction], interaction_qns: Optional[Union[SpinMagnitudeNodeInput, SpinNodeInput]], - ) -> set[float]: + ) -> set[Fraction]: if len(magnitudes) == 1: return set(magnitudes) @@ -572,13 +571,13 @@ def __spin_couplings(spin1: _Spin, spin2: _Spin) -> set[_Spin]: :math:`|S_1 - S_2| \leq S \leq |S_1 + S_2|` and :math:`M_1 + M_2 = M` """ - s_1 = spin1.magnitude - s_2 = spin2.magnitude - sum_proj = spin1.projection + spin2.projection return { - _Spin(x, sum_proj) - for x in arange(abs(s_1 - s_2), s_1 + s_2 + 1, 1.0) + _Spin(Fraction(x), Fraction(sum_proj)) + for x in arange( + abs(spin1.magnitude - spin2.magnitude), + spin1.magnitude + spin2.magnitude + 1, + ) if x >= abs(sum_proj) and not _is_clebsch_gordan_coefficient_zero(spin1, spin2, _Spin(x, sum_proj)) } @@ -594,19 +593,17 @@ class IsoSpinEdgeInput: ) -def _check_spin_valid(magnitude: float, projection: float) -> bool: - if magnitude % 0.5 != 0.0: +def _check_spin_valid(magnitude: Fraction, projection: Fraction) -> bool: + if magnitude.denominator not in {1, 2}: return False if abs(projection) > magnitude: return False - return float(projection - magnitude).is_integer() + return (projection - magnitude).denominator == 1 def isospin_validity(isospin: IsoSpinEdgeInput) -> bool: r"""Check for valid isospin magnitude and projection.""" - return _check_spin_valid( - float(isospin.isospin_magnitude), float(isospin.isospin_projection) - ) + return _check_spin_valid(isospin.isospin_magnitude, isospin.isospin_projection) def isospin_conservation( @@ -644,7 +641,7 @@ class SpinEdgeInput: def spin_validity(spin: SpinEdgeInput) -> bool: r"""Check for valid spin magnitude and projection.""" - return _check_spin_valid(float(spin.spin_magnitude), float(spin.spin_projection)) + return _check_spin_valid(spin.spin_magnitude, spin.spin_projection) def spin_conservation( @@ -712,8 +709,8 @@ def spin_magnitude_conservation( len(ingoing_spin_magnitudes) == 2 and len(outgoing_spin_magnitudes) == 1 ): return _check_magnitude( - [float(x) for x in ingoing_spin_magnitudes], - [float(x) for x in outgoing_spin_magnitudes], + [Fraction(x) for x in ingoing_spin_magnitudes], + [Fraction(x) for x in outgoing_spin_magnitudes], interaction_qns, ) @@ -858,7 +855,7 @@ def calculate_hypercharge( or edge_qns.tau_lepton_number ): return True - isospin_3 = 0.0 + isospin_3 = Fraction(0, 1) if edge_qns.isospin_projection: isospin_3 = edge_qns.isospin_projection return float(edge_qns.charge) == isospin_3 + 0.5 * calculate_hypercharge(edge_qns) diff --git a/src/qrules/quantum_numbers.py b/src/qrules/quantum_numbers.py index fad380aa..8e386553 100644 --- a/src/qrules/quantum_numbers.py +++ b/src/qrules/quantum_numbers.py @@ -227,7 +227,7 @@ class InteractionProperties: def arange( - x_1: float, x_2: float, delta: float = 1.0 + x_1: Fraction, x_2: Fraction, delta: Fraction = Fraction(1, 1) ) -> Generator[Fraction, None, None]: current = Fraction(x_1) delta = Fraction(delta) From e8a184ab007468a68e5d1709e50049cce9a6cb4e Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Fri, 8 Nov 2024 17:15:14 +0100 Subject: [PATCH 10/45] `Particle.spin` is now of type `Fraction` --- src/qrules/particle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qrules/particle.py b/src/qrules/particle.py index d27660aa..7a5fd07d 100644 --- a/src/qrules/particle.py +++ b/src/qrules/particle.py @@ -161,7 +161,7 @@ class Particle: pid: int = field(eq=False) latex: str | None = field(eq=False, default=None) # Unique properties - spin: float = field(converter=float) + spin: Fraction = field(converter=Fraction) mass: float = field(converter=float) width: float = field(converter=float, default=0.0) charge: int = field(default=0) @@ -486,7 +486,7 @@ def create_antiparticle( isospin = -template_particle.isospin parity: Parity | None = None if template_particle.parity is not None: - if template_particle.spin.is_integer(): + if template_particle.spin.denominator == 1: parity = template_particle.parity else: parity = -template_particle.parity From 13d11b95ad68f98f1c93122b012edf5ffc27236e Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 11 Nov 2024 10:01:01 +0100 Subject: [PATCH 11/45] changed aux-types and literals to `Fraction` --- src/qrules/combinatorics.py | 9 +++++---- src/qrules/particle.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/qrules/combinatorics.py b/src/qrules/combinatorics.py index f87cf23f..16181d7d 100644 --- a/src/qrules/combinatorics.py +++ b/src/qrules/combinatorics.py @@ -11,6 +11,7 @@ from collections import OrderedDict from collections.abc import Iterable, Mapping, Sequence from copy import deepcopy +from fractions import Fraction from typing import TYPE_CHECKING, Any, Callable, Union from qrules.particle import ParticleWithSpin @@ -21,7 +22,7 @@ from qrules.particle import ParticleCollection -StateWithSpins = tuple[str, Sequence[float]] +StateWithSpins = tuple[str, Sequence[Fraction]] StateDefinition = Union[str, StateWithSpins] """Particle name, optionally with a list of spin projections.""" InitialFacts = MutableTransition[ParticleWithSpin, InteractionProperties] @@ -215,9 +216,9 @@ def fill_spin_projections(state: StateDefinition) -> StateWithSpins: if isinstance(state, str): particle_name = state particle = particle_db[particle_name] - spin_projections = set(arange(-particle.spin, particle.spin + 1, 1.0)) - if particle.mass == 0.0 and 0.0 in spin_projections: - spin_projections.remove(0.0) + spin_projections = set(arange(-particle.spin, particle.spin + 1)) + if particle.mass == 0.0 and Fraction(0, 1) in spin_projections: + spin_projections.remove(Fraction(0, 1)) return particle_name, sorted(spin_projections) return state diff --git a/src/qrules/particle.py b/src/qrules/particle.py index 7a5fd07d..86b5e1fb 100644 --- a/src/qrules/particle.py +++ b/src/qrules/particle.py @@ -267,7 +267,7 @@ def _get_name_root(name: str) -> str: return re.sub(r"[\*\+\-~\d']", "", name_root) -ParticleWithSpin = tuple[Particle, float] +ParticleWithSpin = tuple[Particle, Fraction] """A particle and its spin projection.""" From 2da9bd9615b3ff01f6da80d80eea74df0c957dc2 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 11 Nov 2024 10:13:37 +0100 Subject: [PATCH 12/45] `settings.py` only with `Fraction` --- src/qrules/settings.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/qrules/settings.py b/src/qrules/settings.py index f9310ad9..2d39abf0 100644 --- a/src/qrules/settings.py +++ b/src/qrules/settings.py @@ -12,6 +12,7 @@ import multiprocessing from copy import deepcopy from enum import Enum, auto +from fractions import Fraction from os.path import dirname, join, realpath from typing import TYPE_CHECKING, Any, Callable @@ -126,7 +127,7 @@ def create_interaction_settings( # noqa: PLR0917 nbody_topology: bool = False, mass_conservation_factor: float | None = 3.0, max_angular_momentum: int = 2, - max_spin_magnitude: float = 2.0, + max_spin_magnitude: Fraction = Fraction(2, 1), ) -> dict[InteractionType, tuple[EdgeSettings, NodeSettings]]: """Create a container that holds the settings for `.InteractionType`.""" formalism_edge_settings = EdgeSettings( @@ -233,16 +234,18 @@ def create_interaction_settings( # noqa: PLR0917 return interaction_type_settings -def __get_ang_mom_magnitudes(is_nbody: bool, max_angular_momentum: int) -> list[float]: +def __get_ang_mom_magnitudes(is_nbody: bool, max_angular_momentum: int) -> list[int]: if is_nbody: return [0] return _int_domain(0, max_angular_momentum) # type: ignore[return-value] -def __get_spin_magnitudes(is_nbody: bool, max_spin_magnitude: float) -> list[float]: +def __get_spin_magnitudes( + is_nbody: bool, max_spin_magnitude: Fraction +) -> list[Fraction]: if is_nbody: - return [0] - return _halves_domain(0, max_spin_magnitude) + return [Fraction(0, 1)] + return _halves_domain(Fraction(0, 1), max_spin_magnitude) def _create_domains(particle_db: ParticleCollection) -> dict[Any, list]: @@ -301,9 +304,9 @@ def set(cls, n_cores: int | None) -> None: def __positive_halves_domain( particle_db: ParticleCollection, attr_getter: Callable[[Particle], Any] -) -> list[float]: +) -> list[Fraction]: values = set(map(attr_getter, particle_db)) - return _halves_domain(0, max(values)) + return _halves_domain(Fraction(0, 1), max(values)) def __positive_int_domain( @@ -313,17 +316,14 @@ def __positive_int_domain( return _int_domain(0, max(values)) -def _halves_domain(start: float, stop: float) -> list[float]: - if start % 0.5 != 0.0: +def _halves_domain(start: Fraction, stop: Fraction) -> list[Fraction]: + if start.denominator not in {1, 2}: msg = f"Start value {start} needs to be multiple of 0.5" raise ValueError(msg) - if stop % 0.5 != 0.0: + if stop.denominator not in {1, 2}: msg = f"Stop value {stop} needs to be multiple of 0.5" raise ValueError(msg) - return [ - int(v) if v.denominator == 1 else v - for v in arange(start, stop + 0.25, delta=0.5) - ] + return list(arange(start, stop + Fraction(1, 4), delta=Fraction(1, 2))) def _int_domain(start: int, stop: int) -> list[int]: @@ -331,6 +331,6 @@ def _int_domain(start: int, stop: int) -> list[int]: def __extend_negative( - magnitudes: Iterable[int | float], -) -> list[int | float]: + magnitudes: Iterable[int | Fraction], +) -> list[int | Fraction]: return sorted(list(magnitudes) + [-x for x in magnitudes if x > 0]) From 4ffd1e385dd1623a0527ea7eaa576f0f6aaada2e Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 11 Nov 2024 11:27:41 +0100 Subject: [PATCH 13/45] added `Fraction` to `Scalar`-type-union --- src/qrules/argument_handling.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/qrules/argument_handling.py b/src/qrules/argument_handling.py index 95a19176..dd670031 100644 --- a/src/qrules/argument_handling.py +++ b/src/qrules/argument_handling.py @@ -8,6 +8,7 @@ from __future__ import annotations import inspect +from fractions import Fraction from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, Union import attrs @@ -22,7 +23,7 @@ if TYPE_CHECKING: from collections.abc import Sequence -Scalar = Union[int, float] +Scalar = Union[int, float, Fraction] Rule = Union[GraphElementRule, EdgeQNConservationRule, ConservationRule] From a4bcc95314251a383d49e157c74f660eb02598b9 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 11 Nov 2024 11:29:19 +0100 Subject: [PATCH 14/45] `InteractionProperties` with `Fraction` and new converter --- src/qrules/quantum_numbers.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/qrules/quantum_numbers.py b/src/qrules/quantum_numbers.py index 8e386553..49571fe1 100644 --- a/src/qrules/quantum_numbers.py +++ b/src/qrules/quantum_numbers.py @@ -193,6 +193,12 @@ def _to_optional_float(optional_float: float | None) -> float | None: return float(optional_float) +def _to_optional_fraction(optional_fraction: Fraction | None) -> Fraction | None: + if optional_fraction is None: + return None + return Fraction(optional_fraction) + + def _to_optional_int(optional_int: int | None) -> int | None: if optional_int is None: return None @@ -221,8 +227,8 @@ class InteractionProperties: default=None, converter=_to_optional_int ) l_projection: int | None = field(default=None, converter=_to_optional_int) - s_magnitude: float | None = field(default=None, converter=_to_optional_float) - s_projection: float | None = field(default=None, converter=_to_optional_float) + s_magnitude: Fraction | None = field(default=None, converter=_to_optional_fraction) + s_projection: Fraction | None = field(default=None, converter=_to_optional_fraction) parity_prefactor: float | None = field(default=None, converter=_to_optional_float) From a6ab1f85dbe2eec43d653d375222d5f8953c962c Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 11 Nov 2024 11:30:37 +0100 Subject: [PATCH 15/45] coercion to `Fraction` in `create_edge_properties` --- src/qrules/system_control.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/qrules/system_control.py b/src/qrules/system_control.py index 00f43cc9..31173d8f 100644 --- a/src/qrules/system_control.py +++ b/src/qrules/system_control.py @@ -5,6 +5,7 @@ import logging import operator from abc import ABC, abstractmethod +from fractions import Fraction from typing import TYPE_CHECKING, Callable import attrs @@ -35,7 +36,7 @@ def create_edge_properties( particle: Particle, - spin_projection: float | None = None, + spin_projection: Fraction | None = None, ) -> GraphEdgePropertyMap: edge_qn_mapping: dict[str, type[EdgeQuantumNumber]] = { qn_name: qn_type @@ -108,7 +109,7 @@ def find_particle( # noqa: D417 if spin_projection is None: msg = f"{GraphEdgePropertyMap.__name__} does not contain a spin projection" raise ValueError(msg) - return particle, spin_projection + return particle, Fraction(spin_projection) def create_interaction_properties( From a365f9bdb148b92f1561a5d1f33733abd476a4be Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 11 Nov 2024 11:32:01 +0100 Subject: [PATCH 16/45] `Fraction`-literal in `StateTransitionManager`-constructor --- src/qrules/transition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/qrules/transition.py b/src/qrules/transition.py index 52504ef1..d8f71dfc 100644 --- a/src/qrules/transition.py +++ b/src/qrules/transition.py @@ -8,6 +8,7 @@ from collections import defaultdict from copy import copy, deepcopy from enum import Enum, auto +from fractions import Fraction from multiprocessing import Pool from typing import TYPE_CHECKING, Literal, overload @@ -242,7 +243,7 @@ def __init__( # noqa: C901, PLR0912, PLR0917 reload_pdg: bool = False, mass_conservation_factor: float | None = 3.0, max_angular_momentum: int = 1, - max_spin_magnitude: float = 2.0, + max_spin_magnitude: Fraction = Fraction(2, 1), number_of_threads: int | None = None, ) -> None: if number_of_threads is not None: From 53127549e8e94b2035da388ec6a59fd203f4b3aa Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 11 Nov 2024 11:33:19 +0100 Subject: [PATCH 17/45] `Fraction`-literals in `__init__.py` --- src/qrules/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/qrules/__init__.py b/src/qrules/__init__.py index 9abd1717..cad9070c 100644 --- a/src/qrules/__init__.py +++ b/src/qrules/__init__.py @@ -17,6 +17,7 @@ from __future__ import annotations +from fractions import Fraction from itertools import product from typing import TYPE_CHECKING @@ -73,7 +74,7 @@ def check_reaction_violations( # noqa: C901, PLR0917 mass_conservation_factor: float | None = 3.0, particle_db: ParticleCollection | None = None, max_angular_momentum: int = 1, - max_spin_magnitude: float = 2.0, + max_spin_magnitude: Fraction = Fraction(2, 1), ) -> set[frozenset[str]]: """Determine violated interaction rules for a given particle reaction. @@ -207,7 +208,7 @@ def check_edge_qn_conservation() -> set[frozenset[str]]: InteractionProperties(l_magnitude=l_magnitude, s_magnitude=s_magnitude) for l_magnitude, s_magnitude in product( _int_domain(0, max_angular_momentum), - _halves_domain(0, max_spin_magnitude), + _halves_domain(Fraction(0, 1), max_spin_magnitude), ) ] @@ -272,7 +273,7 @@ def generate_transitions( # noqa: PLR0917 particle_db: ParticleCollection | None = None, mass_conservation_factor: float | None = 3.0, max_angular_momentum: int = 2, - max_spin_magnitude: float = 2.0, + max_spin_magnitude: Fraction = Fraction(2, 1), topology_building: str = "isobar", number_of_threads: int | None = None, ) -> ReactionInfo: From eefb7776daef68ab7e38c030a9ede28145c0a880 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 11 Nov 2024 12:04:04 +0100 Subject: [PATCH 18/45] coercion instead of forcing `Fraction`-type in `__init__.py` --- src/qrules/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/qrules/__init__.py b/src/qrules/__init__.py index cad9070c..b915f0a6 100644 --- a/src/qrules/__init__.py +++ b/src/qrules/__init__.py @@ -74,7 +74,7 @@ def check_reaction_violations( # noqa: C901, PLR0917 mass_conservation_factor: float | None = 3.0, particle_db: ParticleCollection | None = None, max_angular_momentum: int = 1, - max_spin_magnitude: Fraction = Fraction(2, 1), + max_spin_magnitude: float | Fraction = Fraction(2, 1), ) -> set[frozenset[str]]: """Determine violated interaction rules for a given particle reaction. @@ -208,7 +208,7 @@ def check_edge_qn_conservation() -> set[frozenset[str]]: InteractionProperties(l_magnitude=l_magnitude, s_magnitude=s_magnitude) for l_magnitude, s_magnitude in product( _int_domain(0, max_angular_momentum), - _halves_domain(Fraction(0, 1), max_spin_magnitude), + _halves_domain(Fraction(0, 1), Fraction(max_spin_magnitude)), ) ] @@ -273,7 +273,7 @@ def generate_transitions( # noqa: PLR0917 particle_db: ParticleCollection | None = None, mass_conservation_factor: float | None = 3.0, max_angular_momentum: int = 2, - max_spin_magnitude: Fraction = Fraction(2, 1), + max_spin_magnitude: float | Fraction = Fraction(2, 1), topology_building: str = "isobar", number_of_threads: int | None = None, ) -> ReactionInfo: @@ -361,7 +361,7 @@ def generate_transitions( # noqa: PLR0917 formalism=formalism, mass_conservation_factor=mass_conservation_factor, max_angular_momentum=max_angular_momentum, - max_spin_magnitude=max_spin_magnitude, + max_spin_magnitude=Fraction(max_spin_magnitude), topology_building=topology_building, number_of_threads=number_of_threads, ) From 6bf5c768e22a9a5a30aa0062f77bc6da0c8f5881 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 11 Nov 2024 12:06:29 +0100 Subject: [PATCH 19/45] coercion instead of forcing `Fraction`-type in `StateTransitionManager` --- src/qrules/transition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qrules/transition.py b/src/qrules/transition.py index d8f71dfc..38a4e8ad 100644 --- a/src/qrules/transition.py +++ b/src/qrules/transition.py @@ -243,7 +243,7 @@ def __init__( # noqa: C901, PLR0912, PLR0917 reload_pdg: bool = False, mass_conservation_factor: float | None = 3.0, max_angular_momentum: int = 1, - max_spin_magnitude: Fraction = Fraction(2, 1), + max_spin_magnitude: float | Fraction = Fraction(2, 1), number_of_threads: int | None = None, ) -> None: if number_of_threads is not None: @@ -317,7 +317,7 @@ def __init__( # noqa: C901, PLR0912, PLR0917 nbody_topology=use_nbody_topology, mass_conservation_factor=mass_conservation_factor, max_angular_momentum=max_angular_momentum, - max_spin_magnitude=max_spin_magnitude, + max_spin_magnitude=Fraction(max_spin_magnitude), ) self.__intermediate_particle_filters = allowed_intermediate_particles From 31099ad18dcf14cf84bd749bf7aaeab143670e8c Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 11 Nov 2024 12:10:32 +0100 Subject: [PATCH 20/45] float allowed again in `create_edge_properties` --- src/qrules/system_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qrules/system_control.py b/src/qrules/system_control.py index 31173d8f..4c6cd15c 100644 --- a/src/qrules/system_control.py +++ b/src/qrules/system_control.py @@ -36,7 +36,7 @@ def create_edge_properties( particle: Particle, - spin_projection: Fraction | None = None, + spin_projection: float | Fraction | None = None, ) -> GraphEdgePropertyMap: edge_qn_mapping: dict[str, type[EdgeQuantumNumber]] = { qn_name: qn_type From 28e5ca2ccdb53b501278707438c1459de282fe2e Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 11 Nov 2024 12:13:41 +0100 Subject: [PATCH 21/45] float allowed again in `create_interaction_settings` --- src/qrules/settings.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/qrules/settings.py b/src/qrules/settings.py index 2d39abf0..5920ce21 100644 --- a/src/qrules/settings.py +++ b/src/qrules/settings.py @@ -127,7 +127,7 @@ def create_interaction_settings( # noqa: PLR0917 nbody_topology: bool = False, mass_conservation_factor: float | None = 3.0, max_angular_momentum: int = 2, - max_spin_magnitude: Fraction = Fraction(2, 1), + max_spin_magnitude: float | Fraction = Fraction(2, 1), ) -> dict[InteractionType, tuple[EdgeSettings, NodeSettings]]: """Create a container that holds the settings for `.InteractionType`.""" formalism_edge_settings = EdgeSettings( @@ -144,7 +144,9 @@ def create_interaction_settings( # noqa: PLR0917 angular_momentum_domain = __get_ang_mom_magnitudes( nbody_topology, max_angular_momentum ) - spin_magnitude_domain = __get_spin_magnitudes(nbody_topology, max_spin_magnitude) + spin_magnitude_domain = __get_spin_magnitudes( + nbody_topology, Fraction(max_spin_magnitude) + ) if "helicity" in formalism: formalism_node_settings.conservation_rules = { spin_magnitude_conservation, From 274284b85ddf40f1e48fe91bfa0fd243ec0c105b Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 11 Nov 2024 13:56:53 +0100 Subject: [PATCH 22/45] map `Fraction` to input-list --- tests/unit/test_transition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_transition.py b/tests/unit/test_transition.py index 3fe3ff7b..dc9b19ad 100644 --- a/tests/unit/test_transition.py +++ b/tests/unit/test_transition.py @@ -70,7 +70,7 @@ def create_state(state_def) -> State: class TestStateTransitionManager: def test_allowed_intermediate_particles(self): stm = StateTransitionManager( - initial_state=[("J/psi(1S)", [-1, +1])], + initial_state=[("J/psi(1S)", list(map(Fraction, [-1, +1])))], final_state=["p", "p~", "eta"], ) particle_name = "N(753)" From 881689f073215a7f060d4be2f5a5b0e589acfbe8 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 11 Nov 2024 16:35:58 +0100 Subject: [PATCH 23/45] now preserves stm-API, explicit coercion in `test_settings`-arguments --- src/qrules/transition.py | 17 ++++++++++++++--- tests/unit/test_settings.py | 29 ++++++++++++++++++----------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/qrules/transition.py b/src/qrules/transition.py index 38a4e8ad..7a72bc99 100644 --- a/src/qrules/transition.py +++ b/src/qrules/transition.py @@ -221,6 +221,17 @@ def calculate_strength(node_interaction_settings: dict[int, NodeSettings]) -> fl return strength_sorted_problem_sets +def _fractionalize_statedefinitions(definition: StateDefinition) -> StateDefinition: + if type(definition) is str: + return definition + if type(definition) is tuple: + name = definition[0] + state = definition[1] + return name, list(map(Fraction, state)) + msg = f"value has to be of type {StateDefinition}, got {type(definition)}" + raise ValueError(msg) + + class StateTransitionManager: """Main handler for decay topologies. @@ -263,8 +274,8 @@ def __init__( # noqa: C901, PLR0912, PLR0917 if particle_db is not None: self.__particles = particle_db self.reaction_mode = str(solving_mode) - self.initial_state = list(initial_state) - self.final_state = list(final_state) + self.initial_state = list(map(_fractionalize_statedefinitions, initial_state)) + self.final_state = list(map(_fractionalize_statedefinitions, final_state)) self.interaction_type_settings = interaction_type_settings self.interaction_determinators: list[InteractionDeterminator] = [ @@ -732,7 +743,7 @@ def _strip_spin(state_definition: Sequence[StateDefinition]) -> list[str]: @frozen(order=True) class State: particle: Particle = field(validator=instance_of(Particle)) - spin_projection: float = field(converter=_to_fraction) + spin_projection: Fraction = field(converter=_to_fraction) StateTransition = FrozenTransition[State, InteractionProperties] diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index 10eb633d..73aae958 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -1,6 +1,10 @@ +from __future__ import annotations + +from fractions import Fraction +from typing import TYPE_CHECKING + import pytest -from qrules.particle import ParticleCollection from qrules.quantum_numbers import EdgeQuantumNumbers as EdgeQN from qrules.settings import ( InteractionType, @@ -9,7 +13,10 @@ _int_domain, create_interaction_settings, ) -from qrules.transition import SpinFormalism + +if TYPE_CHECKING: + from qrules.particle import ParticleCollection + from qrules.transition import SpinFormalism class TestInteractionType: @@ -82,11 +89,11 @@ def test_create_interaction_settings( "parity": [-1, +1], "c_parity": [-1, +1, None], "g_parity": [-1, +1, None], - "spin_magnitude": _halves_domain(0, 4), - "spin_projection": _halves_domain(-4, +4), + "spin_magnitude": _halves_domain(*tuple(map(Fraction, (0, 4)))), + "spin_projection": _halves_domain(*tuple(map(Fraction, (-4, +4)))), "charge": _int_domain(-2, 2), - "isospin_magnitude": _halves_domain(0, 1.5), - "isospin_projection": _halves_domain(-1.5, +1.5), + "isospin_magnitude": _halves_domain(*tuple(map(Fraction, (0, 1.5)))), + "isospin_projection": _halves_domain(*tuple(map(Fraction, (-1.5, +1.5)))), "strangeness": _int_domain(-3, +3), "charmness": _int_domain(-1, 1), "bottomness": _int_domain(-1, 1), @@ -94,11 +101,11 @@ def test_create_interaction_settings( expected = { "l_magnitude": _int_domain(0, 2), - "s_magnitude": _halves_domain(0, 2), + "s_magnitude": _halves_domain(*tuple(map(Fraction, (0, 2)))), } if "canonical" in formalism: expected["l_projection"] = [-2, -1, 0, 1, 2] - expected["s_projection"] = _halves_domain(-2, 2) + expected["s_projection"] = _halves_domain(*tuple(map(Fraction, (-2, 2)))) if formalism == "canonical-helicity": expected["l_projection"] = [0] if "helicity" in formalism and interaction_type != InteractionType.WEAK: @@ -124,9 +131,9 @@ def test_create_interaction_settings( (-1, +1, [-1, -0.5, 0, 0.5, +1]), ], ) -def test_halves_range(start: float, stop: float, expected: list): +def test_halves_range(start: float, stop: float, expected: list | None): if expected is None: with pytest.raises(ValueError, match=r"needs to be multiple of 0.5"): - _halves_domain(start, stop) + _halves_domain(Fraction(start), Fraction(stop)) else: - assert _halves_domain(start, stop) == expected + assert _halves_domain(Fraction(start), Fraction(stop)) == expected From 2bfb50f296793093efef4e4ea2daabe03813d7ba Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 18 Nov 2024 11:56:11 +0100 Subject: [PATCH 24/45] reworked rendering fractions --- src/qrules/io/_dot.py | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/qrules/io/_dot.py b/src/qrules/io/_dot.py index e88424df..398c1e31 100644 --- a/src/qrules/io/_dot.py +++ b/src/qrules/io/_dot.py @@ -19,13 +19,7 @@ from attrs import Attribute, define, field from attrs.converters import default_if_none -from qrules.particle import ( - Particle, - ParticleWithSpin, - Spin, - _float_as_signed_fraction_str, - _to_signed_fraction, -) +from qrules.particle import Particle, ParticleWithSpin, Spin from qrules.quantum_numbers import InteractionProperties from qrules.solving import EdgeSettings, NodeSettings, QNProblemSet, QNResult from qrules.topology import FrozenTransition, MutableTransition, Topology, Transition @@ -345,9 +339,11 @@ def __render_as_fraction(value: Any, plusminus: bool) -> str: return str(value) -def _render_fraction(fraction: Fraction, plusminus: bool) -> str: +def _render_fraction(fraction: Fraction, plusminus: bool = False) -> str: if fraction.denominator == 1: - return str(int(fraction)) + if plusminus and fraction.numerator > 0: + return f"{fraction.numerator:+}" + return str(fraction.numerator) if plusminus: return f"{fraction.numerator:+}/{fraction.denominator}" return f"{fraction.numerator}/{fraction.denominator}" @@ -358,18 +354,18 @@ def _(obj: InteractionProperties) -> str: lines = [] if obj.l_magnitude is not None: if obj.l_projection is None: - l_label = _to_signed_fraction(Fraction(obj.l_magnitude)) + l_label = _render_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_signed_fraction(Fraction(obj.s_magnitude)) + s_label = _render_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_signed_fraction(Fraction(obj.parity_prefactor), render_plus=True) + label = _render_fraction(Fraction(obj.parity_prefactor), plusminus=True) lines.append(f"P={label}") return "\n".join(lines) @@ -427,19 +423,15 @@ def _(particle: Particle) -> str: @as_string.register(Spin) def _spin_to_str(spin: Spin) -> str: - spin_magnitude = _float_as_signed_fraction_str(float(spin.magnitude)) - spin_projection = _float_as_signed_fraction_str( - float(spin.projection), render_plus=True - ) + spin_magnitude = _render_fraction(spin.magnitude) + spin_projection = _render_fraction(spin.projection, plusminus=True) return f"|{spin_magnitude},{spin_projection}⟩" @as_string.register(State) def _state_to_str(state: State) -> str: particle = state.particle.name - spin_projection = _float_as_signed_fraction_str( - state.spin_projection, render_plus=True - ) + spin_projection = _render_fraction(state.spin_projection, plusminus=True) return f"{particle}[{spin_projection}]" From 372a11a7a228b84aba9f0f41092804dc58a33a16 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 18 Nov 2024 11:58:00 +0100 Subject: [PATCH 25/45] changed `parity_prefactor` to float --- src/qrules/quantum_numbers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qrules/quantum_numbers.py b/src/qrules/quantum_numbers.py index 49571fe1..8d44c059 100644 --- a/src/qrules/quantum_numbers.py +++ b/src/qrules/quantum_numbers.py @@ -158,7 +158,7 @@ class NodeQuantumNumbers: l_projection = NewType("l_projection", Fraction) s_magnitude = NewType("s_magnitude", Fraction) s_projection = NewType("s_projection", Fraction) - parity_prefactor = NewType("parity_prefactor", Fraction) + parity_prefactor = NewType("parity_prefactor", float) for node_qn_name, node_qn_type in NodeQuantumNumbers.__dict__.items(): From f78b2321d6a0287c73aa3c4328aa7e75953807bf Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 18 Nov 2024 11:59:18 +0100 Subject: [PATCH 26/45] introduced `StateDefinitionInput` and converter to `StateDefinition` --- src/qrules/combinatorics.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/qrules/combinatorics.py b/src/qrules/combinatorics.py index 16181d7d..96e55923 100644 --- a/src/qrules/combinatorics.py +++ b/src/qrules/combinatorics.py @@ -14,6 +14,7 @@ from fractions import Fraction from typing import TYPE_CHECKING, Any, Callable, Union +from qrules.argument_handling import Scalar from qrules.particle import ParticleWithSpin from qrules.quantum_numbers import InteractionProperties, arange from qrules.topology import MutableTransition, Topology, get_originating_node_list @@ -25,10 +26,24 @@ StateWithSpins = tuple[str, Sequence[Fraction]] StateDefinition = Union[str, StateWithSpins] """Particle name, optionally with a list of spin projections.""" +StateDefinitionInput = Union[str, tuple[str, Sequence[Scalar]]] InitialFacts = MutableTransition[ParticleWithSpin, InteractionProperties] """A `.Transition` with only initial and final state information.""" +def as_state_definition( + definition: StateDefinitionInput, +) -> StateDefinition: + if type(definition) is str: + return definition + if type(definition) is tuple: + name = definition[0] + state = definition[1] + return name, list(map(Fraction, state)) # type: ignore # noqa: PGH003 + msg = f"value has to be of type {StateDefinitionInput}, got {type(definition)}" + raise ValueError(msg) + + class _KinematicRepresentation: # noqa: PLW1641 def __init__( self, @@ -183,13 +198,14 @@ def fill_groupings( def create_initial_facts( topology: Topology, - initial_state: Sequence[StateDefinition], - final_state: Sequence[StateDefinition], + initial_state: Sequence[StateDefinitionInput], + final_state: Sequence[StateDefinitionInput], particle_db: ParticleCollection, ) -> list[InitialFacts]: states = __create_states_with_spin_projections( list(topology.incoming_edge_ids) + list(topology.outgoing_edge_ids), - list(initial_state) + list(final_state), + list(map(as_state_definition, initial_state)) + + list(map(as_state_definition, final_state)), particle_db, ) spin_states = __generate_spin_combinations(states, particle_db) @@ -257,14 +273,14 @@ def populate_edge_with_spin_projections( def permutate_topology_kinematically( topology: Topology, - initial_state: list[StateDefinition], - final_state: list[StateDefinition], + initial_state: list[StateDefinitionInput] | list[StateDefinition], + final_state: list[StateDefinitionInput] | list[StateDefinition], final_state_groupings: list[list[list[str]]] | list[list[str]] | list[str] | None = None, ) -> list[Topology]: - def strip_spin(state: StateDefinition) -> str: + def strip_spin(state: StateDefinitionInput) -> str: if isinstance(state, tuple): return state[0] return state From 94893727dfca4398b34c744585a8e43e8827931f Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 18 Nov 2024 12:01:06 +0100 Subject: [PATCH 27/45] retyped `generate_transitions` and STM-`__init__` --- src/qrules/__init__.py | 11 ++++++++--- src/qrules/transition.py | 21 ++++++--------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/qrules/__init__.py b/src/qrules/__init__.py index b915f0a6..8c37f008 100644 --- a/src/qrules/__init__.py +++ b/src/qrules/__init__.py @@ -24,7 +24,12 @@ import attrs from qrules import io -from qrules.combinatorics import InitialFacts, StateDefinition, create_initial_facts +from qrules.combinatorics import ( + InitialFacts, + StateDefinition, + StateDefinitionInput, + create_initial_facts, +) from qrules.conservation_rules import ( BaryonNumberConservation, BottomnessConservation, @@ -265,8 +270,8 @@ def check_edge_qn_conservation() -> set[frozenset[str]]: def generate_transitions( # noqa: PLR0917 - initial_state: StateDefinition | Sequence[StateDefinition], - final_state: Sequence[StateDefinition], + initial_state: StateDefinitionInput | Sequence[StateDefinitionInput], + final_state: Sequence[StateDefinitionInput], allowed_intermediate_particles: list[str] | None = None, allowed_interaction_types: str | Iterable[str] | None = None, formalism: SpinFormalism = "canonical-helicity", diff --git a/src/qrules/transition.py b/src/qrules/transition.py index 7a72bc99..0c5240fc 100644 --- a/src/qrules/transition.py +++ b/src/qrules/transition.py @@ -21,6 +21,8 @@ from qrules.combinatorics import ( InitialFacts, StateDefinition, + StateDefinitionInput, + as_state_definition, create_initial_facts, ensure_nested_list, match_external_edges, @@ -221,17 +223,6 @@ def calculate_strength(node_interaction_settings: dict[int, NodeSettings]) -> fl return strength_sorted_problem_sets -def _fractionalize_statedefinitions(definition: StateDefinition) -> StateDefinition: - if type(definition) is str: - return definition - if type(definition) is tuple: - name = definition[0] - state = definition[1] - return name, list(map(Fraction, state)) - msg = f"value has to be of type {StateDefinition}, got {type(definition)}" - raise ValueError(msg) - - class StateTransitionManager: """Main handler for decay topologies. @@ -240,8 +231,8 @@ class StateTransitionManager: def __init__( # noqa: C901, PLR0912, PLR0917 self, - initial_state: Sequence[StateDefinition], - final_state: Sequence[StateDefinition], + initial_state: Sequence[StateDefinitionInput], + final_state: Sequence[StateDefinitionInput], particle_db: ParticleCollection | None = None, allowed_intermediate_particles: list[str] | None = None, interaction_type_settings: dict[ @@ -274,8 +265,8 @@ def __init__( # noqa: C901, PLR0912, PLR0917 if particle_db is not None: self.__particles = particle_db self.reaction_mode = str(solving_mode) - self.initial_state = list(map(_fractionalize_statedefinitions, initial_state)) - self.final_state = list(map(_fractionalize_statedefinitions, final_state)) + self.initial_state = list(map(as_state_definition, initial_state)) + self.final_state = list(map(as_state_definition, final_state)) self.interaction_type_settings = interaction_type_settings self.interaction_determinators: list[InteractionDeterminator] = [ From 36729d09ee1feda3bf1093ae739da944feda2c8f Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 18 Nov 2024 12:06:56 +0100 Subject: [PATCH 28/45] renders parity as `int` --- src/qrules/particle.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/qrules/particle.py b/src/qrules/particle.py index 86b5e1fb..43a72d4a 100644 --- a/src/qrules/particle.py +++ b/src/qrules/particle.py @@ -241,11 +241,7 @@ def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: p.breakable() p.text(f"{attribute.name}=") if isinstance(value, Parity): - p.text( - _float_as_signed_fraction_str( - int(value), render_plus=True - ) - ) + p.text(_int_as_signed_str(int(value), render_plus=True)) else: p.pretty(value) # type: ignore[attr-defined] p.text(",") @@ -253,8 +249,8 @@ def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: p.text(")") -def _float_as_signed_fraction_str(value: float, render_plus: bool = False) -> str: - label = str(Fraction(value)) +def _int_as_signed_str(value: int, render_plus: bool = False) -> str: + label = str(value) if render_plus and value > 0: return f"+{label}" return label From 315bda636f9d5810166137e29797bc9086cf7b92 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 18 Nov 2024 12:10:35 +0100 Subject: [PATCH 29/45] input-conversion to `Fraction` and new rendering in tests --- tests/unit/conservation_rules/test_c_parity.py | 9 +++++++-- tests/unit/conservation_rules/test_clebsch_gordan.py | 8 +++++--- tests/unit/conservation_rules/test_g_parity.py | 7 +++++-- .../conservation_rules/test_gellmann_nishijima.py | 7 +++++-- .../conservation_rules/test_parity_conservation.py | 3 ++- tests/unit/test_quantum_numbers.py | 5 +++-- tests/unit/test_system_control.py | 11 +++++++---- 7 files changed, 34 insertions(+), 16 deletions(-) diff --git a/tests/unit/conservation_rules/test_c_parity.py b/tests/unit/conservation_rules/test_c_parity.py index df261a77..2d4a40a9 100644 --- a/tests/unit/conservation_rules/test_c_parity.py +++ b/tests/unit/conservation_rules/test_c_parity.py @@ -1,3 +1,4 @@ +from fractions import Fraction from itertools import product import pytest @@ -55,7 +56,9 @@ def test_c_parity_all_defined(rule_input, expected): CParityEdgeInput(spin_magnitude=0, pid=100), CParityEdgeInput(spin_magnitude=0, pid=-100), ], - CParityNodeInput(l_magnitude=l_magnitude, s_magnitude=0), + CParityNodeInput( + l_magnitude=Fraction(l_magnitude), s_magnitude=Fraction(0) + ), ), (-1) ** l_magnitude == c_parity, ) @@ -80,7 +83,9 @@ def test_c_parity_multiparticle_boson(rule_input, expected): CParityEdgeInput(spin_magnitude=0.5, pid=100), CParityEdgeInput(spin_magnitude=0.5, pid=-100), ], - CParityNodeInput(l_magnitude=l_magnitude, s_magnitude=s_magnitude), + CParityNodeInput( + l_magnitude=Fraction(l_magnitude), s_magnitude=Fraction(s_magnitude) + ), ), (s_magnitude + l_magnitude) % 2 == abs(c_parity - 1) / 2, ) diff --git a/tests/unit/conservation_rules/test_clebsch_gordan.py b/tests/unit/conservation_rules/test_clebsch_gordan.py index 9594e46b..4eec0746 100644 --- a/tests/unit/conservation_rules/test_clebsch_gordan.py +++ b/tests/unit/conservation_rules/test_clebsch_gordan.py @@ -1,3 +1,5 @@ +from fractions import Fraction + import pytest from qrules.conservation_rules import ( @@ -213,10 +215,10 @@ def test_isospin_clebsch_gordan_zeros( ) -> None: assert ( isospin_conservation( - [IsoSpinEdgeInput(coupled_isospin_mag, 0)], + [IsoSpinEdgeInput(Fraction(coupled_isospin_mag), Fraction(0))], [ - IsoSpinEdgeInput(isospin_mag1, 0), - IsoSpinEdgeInput(isospin_mag2, 0), + IsoSpinEdgeInput(Fraction(isospin_mag1), Fraction(0)), + IsoSpinEdgeInput(Fraction(isospin_mag2), Fraction(0)), ], ) is expected diff --git a/tests/unit/conservation_rules/test_g_parity.py b/tests/unit/conservation_rules/test_g_parity.py index ef6af7ad..7ada9449 100644 --- a/tests/unit/conservation_rules/test_g_parity.py +++ b/tests/unit/conservation_rules/test_g_parity.py @@ -1,3 +1,4 @@ +from fractions import Fraction from itertools import product import pytest @@ -37,7 +38,7 @@ g_parity=Parity(g_parity_out[0][1]), ), ], - GParityNodeInput(l_magnitude=0, s_magnitude=0), + GParityNodeInput(l_magnitude=Fraction(0), s_magnitude=Fraction(0)), ), g_parity_in[1] is g_parity_out[1], ) @@ -84,7 +85,9 @@ def test_g_parity_all_defined(rule_input, expected): pid=-100, ), ], - GParityNodeInput(l_magnitude=l_magnitude, s_magnitude=0), + GParityNodeInput( + l_magnitude=Fraction(l_magnitude), s_magnitude=Fraction(0) + ), ), (-1) ** (l_magnitude + isospin) == g_parity, ) diff --git a/tests/unit/conservation_rules/test_gellmann_nishijima.py b/tests/unit/conservation_rules/test_gellmann_nishijima.py index 9b718e77..491f4b02 100644 --- a/tests/unit/conservation_rules/test_gellmann_nishijima.py +++ b/tests/unit/conservation_rules/test_gellmann_nishijima.py @@ -1,3 +1,4 @@ +from fractions import Fraction from itertools import product import pytest @@ -16,7 +17,9 @@ ), charge == isospin_z + 0.5, ) - for charge, isospin_z in product(range(-1, 1), [-1, 0.5, 0, 0.5, 1]) + for charge, isospin_z in product( + range(-1, 1), list(map(Fraction, [-1, 0.5, 0, 0.5, 1])) + ) ] + [ ( @@ -29,7 +32,7 @@ charge == isospin_z + 0.5 * (1 + strangeness), ) for charge, isospin_z, strangeness in product( - range(-1, 1), [-1, 0.5, 0, 0.5, 1], [-1, 0, 1] + range(-1, 1), list(map(Fraction, [-1, 0.5, 0, 0.5, 1])), [-1, 0, 1] ) ], ) diff --git a/tests/unit/conservation_rules/test_parity_conservation.py b/tests/unit/conservation_rules/test_parity_conservation.py index 1463c9b8..b61ac814 100644 --- a/tests/unit/conservation_rules/test_parity_conservation.py +++ b/tests/unit/conservation_rules/test_parity_conservation.py @@ -1,3 +1,4 @@ +from fractions import Fraction from itertools import product import pytest @@ -21,7 +22,7 @@ Parity(parity_out1), Parity(1), ], - NodeQuantumNumbers.l_magnitude(l_magnitude), + NodeQuantumNumbers.l_magnitude(Fraction(l_magnitude)), parity_in == parity_out1 * (-1) ** (l_magnitude), ) for parity_in, parity_out1, l_magnitude in product([-1, 1], [-1, 1], range(5)) diff --git a/tests/unit/test_quantum_numbers.py b/tests/unit/test_quantum_numbers.py index 7be677d4..6bd7587d 100644 --- a/tests/unit/test_quantum_numbers.py +++ b/tests/unit/test_quantum_numbers.py @@ -1,9 +1,10 @@ import typing from copy import deepcopy +from fractions import Fraction import pytest -from qrules.particle import _float_as_signed_fraction_str +from qrules.io._dot import _render_fraction from qrules.quantum_numbers import Parity @@ -69,4 +70,4 @@ def test_exceptions(self): ], ) def test_to_fraction(value, render_plus: bool, expected: str): - assert _float_as_signed_fraction_str(value, render_plus) == expected + assert _render_fraction(Fraction(value), render_plus) == expected diff --git a/tests/unit/test_system_control.py b/tests/unit/test_system_control.py index 7ad803f8..a5baf190 100644 --- a/tests/unit/test_system_control.py +++ b/tests/unit/test_system_control.py @@ -1,6 +1,7 @@ from __future__ import annotations from copy import deepcopy +from fractions import Fraction from importlib.metadata import version import attrs @@ -226,7 +227,9 @@ def test_create_edge_properties( assert skh_particle_version is not None # dummy for skip tests -def make_ls_test_graph(angular_momentum_magnitude, coupled_spin_magnitude, particle): +def make_ls_test_graph( + angular_momentum_magnitude, coupled_spin_magnitude, particle: Particle +): topology = Topology( nodes={0}, edges={-1: Edge(None, 0)}, @@ -237,12 +240,12 @@ def make_ls_test_graph(angular_momentum_magnitude, coupled_spin_magnitude, parti l_magnitude=angular_momentum_magnitude, ) } - states: dict[int, ParticleWithSpin] = {-1: (particle, 0)} + states: dict[int, ParticleWithSpin] = {-1: (particle, Fraction(0))} return MutableTransition(topology, states, interactions) # type: ignore[arg-type,var-annotated] def make_ls_test_graph_scrambled( - angular_momentum_magnitude, coupled_spin_magnitude, particle + angular_momentum_magnitude, coupled_spin_magnitude, particle: Particle ): topology = Topology( nodes={0}, @@ -254,7 +257,7 @@ def make_ls_test_graph_scrambled( s_magnitude=coupled_spin_magnitude, ) } - states: dict[int, ParticleWithSpin] = {-1: (particle, 0)} + states: dict[int, ParticleWithSpin] = {-1: (particle, Fraction(0))} return MutableTransition(topology, states, interactions) # type: ignore[arg-type,var-annotated] From b911392d238be2ffa694727d7e90c242ecbd9994 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 18 Nov 2024 13:42:30 +0100 Subject: [PATCH 30/45] docstring for `StateDefinitionInput` --- src/qrules/combinatorics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qrules/combinatorics.py b/src/qrules/combinatorics.py index 96e55923..f7953012 100644 --- a/src/qrules/combinatorics.py +++ b/src/qrules/combinatorics.py @@ -27,6 +27,7 @@ StateDefinition = Union[str, StateWithSpins] """Particle name, optionally with a list of spin projections.""" StateDefinitionInput = Union[str, tuple[str, Sequence[Scalar]]] +"""Input type for `StateDefinition` permitting also `int` and `float`""" InitialFacts = MutableTransition[ParticleWithSpin, InteractionProperties] """A `.Transition` with only initial and final state information.""" From 9ac8b3aa7bf17991d948fe147f99463f720d8a7a Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 18 Nov 2024 14:04:26 +0100 Subject: [PATCH 31/45] using `Sequence` in `permutate_topology_kinematically` --- src/qrules/combinatorics.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qrules/combinatorics.py b/src/qrules/combinatorics.py index f7953012..1e2f9dec 100644 --- a/src/qrules/combinatorics.py +++ b/src/qrules/combinatorics.py @@ -274,8 +274,8 @@ def populate_edge_with_spin_projections( def permutate_topology_kinematically( topology: Topology, - initial_state: list[StateDefinitionInput] | list[StateDefinition], - final_state: list[StateDefinitionInput] | list[StateDefinition], + initial_state: Sequence[StateDefinitionInput] | Sequence[StateDefinition], + final_state: Sequence[StateDefinitionInput] | Sequence[StateDefinition], final_state_groupings: list[list[list[str]]] | list[list[str]] | list[str] @@ -287,7 +287,7 @@ def strip_spin(state: StateDefinitionInput) -> str: return state edge_ids = sorted(topology.incoming_edge_ids) + sorted(topology.outgoing_edge_ids) - states = initial_state + final_state + states = list(initial_state) + list(final_state) return _generate_kinematic_permutations( topology, particle_names={i: strip_spin(s) for i, s in zip(edge_ids, states)}, From 24578d1821aa602888f799db81a5d99d5699d038 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 18 Nov 2024 14:21:29 +0100 Subject: [PATCH 32/45] ignoring `Fraction` in API-Docs --- docs/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 13ac2193..9e22caaa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,6 +48,7 @@ def pick_newtype_attrs(some_type: type) -> list: api_github_repo = f"{ORGANIZATION}/{REPO_NAME}" api_target_substitutions: dict[str, str | tuple[str, str]] = { "EdgeType": "typing.TypeVar", + "Fraction": "fraction.Fraction", "GraphEdgePropertyMap": ("obj", "qrules.argument_handling.GraphEdgePropertyMap"), "GraphElementProperties": ("obj", "qrules.solving.GraphElementProperties"), "GraphNodePropertyMap": ("obj", "qrules.argument_handling.GraphNodePropertyMap"), @@ -63,6 +64,7 @@ def pick_newtype_attrs(some_type: type) -> list: "qrules.topology.NodeType": "typing.TypeVar", "SpinFormalism": ("obj", "qrules.transition.SpinFormalism"), "StateDefinition": ("obj", "qrules.combinatorics.StateDefinition"), + "StateDefinitionInput": ("obj", "qrules.combinatorics.StateDefinition"), "StateTransition": ("obj", "qrules.transition.StateTransition"), "typing.Literal[-1, 1]": "typing.Literal", } @@ -291,6 +293,7 @@ def pick_newtype_attrs(some_type: type) -> list: (r"py:(class|obj)", r"qrules\.topology\.NewNodeType"), (r"py:(class|obj)", r"qrules\.topology\.NodeType"), (r"py:(class|obj)", r"qrules\.topology\.VT"), + (r"py:(class|obj)", r"fraction\.Fraction"), *nitpick_temp_patterns, ] nitpicky = True From 3e5ed8c39d17c18e95a64aef8787e48a354c38d6 Mon Sep 17 00:00:00 2001 From: Remco de Boer <29308176+redeboer@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:38:24 +0100 Subject: [PATCH 33/45] FIX: relink to `fractions.Fraction` --- docs/conf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index f97aefa7..68f5f03f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -49,7 +49,7 @@ def pick_newtype_attrs(some_type: type) -> list: api_target_substitutions: dict[str, str | tuple[str, str]] = { "EdgeQuantumNumberTypes": ("obj", "qrules.quantum_numbers.EdgeQuantumNumberTypes"), "EdgeType": "typing.TypeVar", - "Fraction": "fraction.Fraction", + "Fraction": ("obj", "fractions.Fraction"), "GraphEdgePropertyMap": ("obj", "qrules.argument_handling.GraphEdgePropertyMap"), "GraphElementProperties": ("obj", "qrules.solving.GraphElementProperties"), "GraphNodePropertyMap": ("obj", "qrules.argument_handling.GraphNodePropertyMap"), @@ -296,7 +296,6 @@ def pick_newtype_attrs(some_type: type) -> list: (r"py:(class|obj)", r"qrules\.topology\.NewNodeType"), (r"py:(class|obj)", r"qrules\.topology\.NodeType"), (r"py:(class|obj)", r"qrules\.topology\.VT"), - (r"py:(class|obj)", r"fraction\.Fraction"), *nitpick_temp_patterns, ] nitpicky = True From c9ad862fd20745b2800e2c99362d5782cd49b118 Mon Sep 17 00:00:00 2001 From: Remco de Boer <29308176+redeboer@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:45:14 +0100 Subject: [PATCH 34/45] MAINT: simplify `Fraction` construction and notation --- src/qrules/__init__.py | 8 ++++---- src/qrules/combinatorics.py | 4 ++-- src/qrules/conservation_rules.py | 2 +- src/qrules/particle.py | 4 ++-- src/qrules/quantum_numbers.py | 2 +- src/qrules/settings.py | 8 ++++---- src/qrules/transition.py | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/qrules/__init__.py b/src/qrules/__init__.py index 8c37f008..d472dfaa 100644 --- a/src/qrules/__init__.py +++ b/src/qrules/__init__.py @@ -79,7 +79,7 @@ def check_reaction_violations( # noqa: C901, PLR0917 mass_conservation_factor: float | None = 3.0, particle_db: ParticleCollection | None = None, max_angular_momentum: int = 1, - max_spin_magnitude: float | Fraction = Fraction(2, 1), + max_spin_magnitude: float | Fraction = 2, ) -> set[frozenset[str]]: """Determine violated interaction rules for a given particle reaction. @@ -213,7 +213,7 @@ def check_edge_qn_conservation() -> set[frozenset[str]]: InteractionProperties(l_magnitude=l_magnitude, s_magnitude=s_magnitude) for l_magnitude, s_magnitude in product( _int_domain(0, max_angular_momentum), - _halves_domain(Fraction(0, 1), Fraction(max_spin_magnitude)), + _halves_domain(Fraction(0), Fraction(max_spin_magnitude)), ) ] @@ -278,7 +278,7 @@ def generate_transitions( # noqa: PLR0917 particle_db: ParticleCollection | None = None, mass_conservation_factor: float | None = 3.0, max_angular_momentum: int = 2, - max_spin_magnitude: float | Fraction = Fraction(2, 1), + max_spin_magnitude: float | Fraction = 2, topology_building: str = "isobar", number_of_threads: int | None = None, ) -> ReactionInfo: @@ -366,7 +366,7 @@ def generate_transitions( # noqa: PLR0917 formalism=formalism, mass_conservation_factor=mass_conservation_factor, max_angular_momentum=max_angular_momentum, - max_spin_magnitude=Fraction(max_spin_magnitude), + max_spin_magnitude=max_spin_magnitude, topology_building=topology_building, number_of_threads=number_of_threads, ) diff --git a/src/qrules/combinatorics.py b/src/qrules/combinatorics.py index 1e2f9dec..62165870 100644 --- a/src/qrules/combinatorics.py +++ b/src/qrules/combinatorics.py @@ -234,8 +234,8 @@ def fill_spin_projections(state: StateDefinition) -> StateWithSpins: particle_name = state particle = particle_db[particle_name] spin_projections = set(arange(-particle.spin, particle.spin + 1)) - if particle.mass == 0.0 and Fraction(0, 1) in spin_projections: - spin_projections.remove(Fraction(0, 1)) + if particle.mass == 0.0 and Fraction(0) in spin_projections: + spin_projections.remove(Fraction(0)) return particle_name, sorted(spin_projections) return state diff --git a/src/qrules/conservation_rules.py b/src/qrules/conservation_rules.py index 57d17c73..457f582c 100644 --- a/src/qrules/conservation_rules.py +++ b/src/qrules/conservation_rules.py @@ -855,7 +855,7 @@ def calculate_hypercharge( or edge_qns.tau_lepton_number ): return True - isospin_3 = Fraction(0, 1) + isospin_3 = Fraction(0) if edge_qns.isospin_projection: isospin_3 = edge_qns.isospin_projection return float(edge_qns.charge) == isospin_3 + 0.5 * calculate_hypercharge(edge_qns) diff --git a/src/qrules/particle.py b/src/qrules/particle.py index 43a72d4a..9f5efdee 100644 --- a/src/qrules/particle.py +++ b/src/qrules/particle.py @@ -56,11 +56,11 @@ def _validate_fraction_for_spin( attribute: Attribute, # noqa: ARG001 value: Fraction, # noqa: ARG001 ) -> Any: - if instance.magnitude % Fraction(1, 2) != Fraction(0, 1): + if instance.magnitude % Fraction(1, 2) != Fraction(0): 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): + if instance.magnitude < Fraction(0): msg = f"Spin magnitude has to be positive, but is {instance.magnitude}" raise ValueError(msg) msg = ( diff --git a/src/qrules/quantum_numbers.py b/src/qrules/quantum_numbers.py index 294d7ce6..3ce2dbac 100644 --- a/src/qrules/quantum_numbers.py +++ b/src/qrules/quantum_numbers.py @@ -234,7 +234,7 @@ class InteractionProperties: def arange( - x_1: Fraction, x_2: Fraction, delta: Fraction = Fraction(1, 1) + x_1: Fraction, x_2: Fraction, delta: Fraction = Fraction(1) ) -> Generator[Fraction, None, None]: current = Fraction(x_1) delta = Fraction(delta) diff --git a/src/qrules/settings.py b/src/qrules/settings.py index 5920ce21..812b3d68 100644 --- a/src/qrules/settings.py +++ b/src/qrules/settings.py @@ -127,7 +127,7 @@ def create_interaction_settings( # noqa: PLR0917 nbody_topology: bool = False, mass_conservation_factor: float | None = 3.0, max_angular_momentum: int = 2, - max_spin_magnitude: float | Fraction = Fraction(2, 1), + max_spin_magnitude: float | Fraction = 2, ) -> dict[InteractionType, tuple[EdgeSettings, NodeSettings]]: """Create a container that holds the settings for `.InteractionType`.""" formalism_edge_settings = EdgeSettings( @@ -246,8 +246,8 @@ def __get_spin_magnitudes( is_nbody: bool, max_spin_magnitude: Fraction ) -> list[Fraction]: if is_nbody: - return [Fraction(0, 1)] - return _halves_domain(Fraction(0, 1), max_spin_magnitude) + return [Fraction(0)] + return _halves_domain(Fraction(0), max_spin_magnitude) def _create_domains(particle_db: ParticleCollection) -> dict[Any, list]: @@ -308,7 +308,7 @@ def __positive_halves_domain( particle_db: ParticleCollection, attr_getter: Callable[[Particle], Any] ) -> list[Fraction]: values = set(map(attr_getter, particle_db)) - return _halves_domain(Fraction(0, 1), max(values)) + return _halves_domain(Fraction(0), max(values)) def __positive_int_domain( diff --git a/src/qrules/transition.py b/src/qrules/transition.py index 0c5240fc..35ada83c 100644 --- a/src/qrules/transition.py +++ b/src/qrules/transition.py @@ -245,7 +245,7 @@ def __init__( # noqa: C901, PLR0912, PLR0917 reload_pdg: bool = False, mass_conservation_factor: float | None = 3.0, max_angular_momentum: int = 1, - max_spin_magnitude: float | Fraction = Fraction(2, 1), + max_spin_magnitude: float | Fraction = 2, number_of_threads: int | None = None, ) -> None: if number_of_threads is not None: From 7b4732d4b33949c331a35a85ecb6cd85e5df7868 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 25 Nov 2024 12:04:28 +0100 Subject: [PATCH 35/45] refactored `_render_fraction` --- src/qrules/io/_dot.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/qrules/io/_dot.py b/src/qrules/io/_dot.py index 398c1e31..0ca8455c 100644 --- a/src/qrules/io/_dot.py +++ b/src/qrules/io/_dot.py @@ -340,13 +340,13 @@ def __render_as_fraction(value: Any, plusminus: bool) -> str: def _render_fraction(fraction: Fraction, plusminus: bool = False) -> str: - if fraction.denominator == 1: - if plusminus and fraction.numerator > 0: - return f"{fraction.numerator:+}" - return str(fraction.numerator) if plusminus: + if fraction.denominator == 1: + if fraction.numerator > 0: + return f"{fraction.numerator:+}" + return str(fraction) return f"{fraction.numerator:+}/{fraction.denominator}" - return f"{fraction.numerator}/{fraction.denominator}" + return str(fraction) @as_string.register(InteractionProperties) From b46d0d9aefe0abb58722ba2b3f201bdbddc7d03f Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 25 Nov 2024 12:36:11 +0100 Subject: [PATCH 36/45] fixed type in `conf.py` --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index f97aefa7..25fffee7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -67,7 +67,7 @@ def pick_newtype_attrs(some_type: type) -> list: "Rule": ("obj", "qrules.argument_handling.Rule"), "SpinFormalism": ("obj", "qrules.transition.SpinFormalism"), "StateDefinition": ("obj", "qrules.combinatorics.StateDefinition"), - "StateDefinitionInput": ("obj", "qrules.combinatorics.StateDefinition"), + "StateDefinitionInput": ("obj", "qrules.combinatorics.StateDefinitionInput"), "StateTransition": ("obj", "qrules.transition.StateTransition"), "typing.Literal[-1, 1]": "typing.Literal", } From 5536aa8e9a771f50b588eef16d27f2610326455c Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 25 Nov 2024 13:20:32 +0100 Subject: [PATCH 37/45] removed `Fraction` from user-facing functions/classes --- src/qrules/__init__.py | 9 ++++----- src/qrules/settings.py | 26 ++++++++++++-------------- src/qrules/transition.py | 6 +++--- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/qrules/__init__.py b/src/qrules/__init__.py index 8c37f008..ce37be05 100644 --- a/src/qrules/__init__.py +++ b/src/qrules/__init__.py @@ -17,7 +17,6 @@ from __future__ import annotations -from fractions import Fraction from itertools import product from typing import TYPE_CHECKING @@ -79,7 +78,7 @@ def check_reaction_violations( # noqa: C901, PLR0917 mass_conservation_factor: float | None = 3.0, particle_db: ParticleCollection | None = None, max_angular_momentum: int = 1, - max_spin_magnitude: float | Fraction = Fraction(2, 1), + max_spin_magnitude: float = 2, ) -> set[frozenset[str]]: """Determine violated interaction rules for a given particle reaction. @@ -213,7 +212,7 @@ def check_edge_qn_conservation() -> set[frozenset[str]]: InteractionProperties(l_magnitude=l_magnitude, s_magnitude=s_magnitude) for l_magnitude, s_magnitude in product( _int_domain(0, max_angular_momentum), - _halves_domain(Fraction(0, 1), Fraction(max_spin_magnitude)), + _halves_domain(0, max_spin_magnitude), ) ] @@ -278,7 +277,7 @@ def generate_transitions( # noqa: PLR0917 particle_db: ParticleCollection | None = None, mass_conservation_factor: float | None = 3.0, max_angular_momentum: int = 2, - max_spin_magnitude: float | Fraction = Fraction(2, 1), + max_spin_magnitude: float = 2, topology_building: str = "isobar", number_of_threads: int | None = None, ) -> ReactionInfo: @@ -366,7 +365,7 @@ def generate_transitions( # noqa: PLR0917 formalism=formalism, mass_conservation_factor=mass_conservation_factor, max_angular_momentum=max_angular_momentum, - max_spin_magnitude=Fraction(max_spin_magnitude), + max_spin_magnitude=max_spin_magnitude, topology_building=topology_building, number_of_threads=number_of_threads, ) diff --git a/src/qrules/settings.py b/src/qrules/settings.py index 5920ce21..105ac373 100644 --- a/src/qrules/settings.py +++ b/src/qrules/settings.py @@ -127,7 +127,7 @@ def create_interaction_settings( # noqa: PLR0917 nbody_topology: bool = False, mass_conservation_factor: float | None = 3.0, max_angular_momentum: int = 2, - max_spin_magnitude: float | Fraction = Fraction(2, 1), + max_spin_magnitude: float = 2, ) -> dict[InteractionType, tuple[EdgeSettings, NodeSettings]]: """Create a container that holds the settings for `.InteractionType`.""" formalism_edge_settings = EdgeSettings( @@ -144,9 +144,7 @@ def create_interaction_settings( # noqa: PLR0917 angular_momentum_domain = __get_ang_mom_magnitudes( nbody_topology, max_angular_momentum ) - spin_magnitude_domain = __get_spin_magnitudes( - nbody_topology, Fraction(max_spin_magnitude) - ) + spin_magnitude_domain = __get_spin_magnitudes(nbody_topology, max_spin_magnitude) if "helicity" in formalism: formalism_node_settings.conservation_rules = { spin_magnitude_conservation, @@ -242,12 +240,10 @@ def __get_ang_mom_magnitudes(is_nbody: bool, max_angular_momentum: int) -> list[ return _int_domain(0, max_angular_momentum) # type: ignore[return-value] -def __get_spin_magnitudes( - is_nbody: bool, max_spin_magnitude: Fraction -) -> list[Fraction]: +def __get_spin_magnitudes(is_nbody: bool, max_spin_magnitude: float) -> list[Fraction]: if is_nbody: - return [Fraction(0, 1)] - return _halves_domain(Fraction(0, 1), max_spin_magnitude) + return [Fraction(0)] + return _halves_domain(0, max_spin_magnitude) def _create_domains(particle_db: ParticleCollection) -> dict[Any, list]: @@ -308,7 +304,7 @@ def __positive_halves_domain( particle_db: ParticleCollection, attr_getter: Callable[[Particle], Any] ) -> list[Fraction]: values = set(map(attr_getter, particle_db)) - return _halves_domain(Fraction(0, 1), max(values)) + return _halves_domain(0, max(values)) def __positive_int_domain( @@ -318,14 +314,16 @@ def __positive_int_domain( return _int_domain(0, max(values)) -def _halves_domain(start: Fraction, stop: Fraction) -> list[Fraction]: - if start.denominator not in {1, 2}: +def _halves_domain(start: float, stop: float) -> list[Fraction]: + start_frac = Fraction(start) + stop_frac = Fraction(stop) + if start_frac.denominator not in {1, 2}: msg = f"Start value {start} needs to be multiple of 0.5" raise ValueError(msg) - if stop.denominator not in {1, 2}: + if stop_frac.denominator not in {1, 2}: msg = f"Stop value {stop} needs to be multiple of 0.5" raise ValueError(msg) - return list(arange(start, stop + Fraction(1, 4), delta=Fraction(1, 2))) + return list(arange(start_frac, stop_frac + Fraction(1, 4), delta=Fraction(1, 2))) def _int_domain(start: int, stop: int) -> list[int]: diff --git a/src/qrules/transition.py b/src/qrules/transition.py index 0c5240fc..78147b83 100644 --- a/src/qrules/transition.py +++ b/src/qrules/transition.py @@ -8,7 +8,6 @@ from collections import defaultdict from copy import copy, deepcopy from enum import Enum, auto -from fractions import Fraction from multiprocessing import Pool from typing import TYPE_CHECKING, Literal, overload @@ -79,6 +78,7 @@ if TYPE_CHECKING: from collections.abc import Iterable, Sequence + from fractions import Fraction _LOGGER = logging.getLogger(__name__) @@ -245,7 +245,7 @@ def __init__( # noqa: C901, PLR0912, PLR0917 reload_pdg: bool = False, mass_conservation_factor: float | None = 3.0, max_angular_momentum: int = 1, - max_spin_magnitude: float | Fraction = Fraction(2, 1), + max_spin_magnitude: float = 2, number_of_threads: int | None = None, ) -> None: if number_of_threads is not None: @@ -319,7 +319,7 @@ def __init__( # noqa: C901, PLR0912, PLR0917 nbody_topology=use_nbody_topology, mass_conservation_factor=mass_conservation_factor, max_angular_momentum=max_angular_momentum, - max_spin_magnitude=Fraction(max_spin_magnitude), + max_spin_magnitude=max_spin_magnitude, ) self.__intermediate_particle_filters = allowed_intermediate_particles From 321f9f3add7a38fef0a4bd17ffff1f4fabb01106 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 25 Nov 2024 13:37:40 +0100 Subject: [PATCH 38/45] format in regex-pattern --- tests/unit/test_particle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_particle.py b/tests/unit/test_particle.py index 904e2187..d9d4cabe 100644 --- a/tests/unit/test_particle.py +++ b/tests/unit/test_particle.py @@ -390,10 +390,10 @@ def test_repr(self, instance: Spin, repr_method): ) def test_exceptions(self, magnitude, projection): regex_pattern = "|".join([ # noqa: FLY002 - r"Spin magnitude \d*/\d* has to be a multitude of \d\.[05]", + r"Spin magnitude \d+/\d+ has to be a multitude of \d\.[05]", r"\(projection - magnitude\) should be integer", r"Spin magnitude has to be positive", - r"Absolute value of spin projection cannot be larger than its", + r"Absolute value of spin projection cannot be larger than the magnitude", ]) with pytest.raises(ValueError, match=regex_pattern): print(Spin(magnitude, projection)) From e94531e23aba3e281efeaad699a8abfa4c98b07f Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 25 Nov 2024 13:49:57 +0100 Subject: [PATCH 39/45] `isospin` can now be given as `float`, uses converter --- src/qrules/particle.py | 2 +- tests/unit/test_particle.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qrules/particle.py b/src/qrules/particle.py index 43a72d4a..bed8f5a8 100644 --- a/src/qrules/particle.py +++ b/src/qrules/particle.py @@ -130,7 +130,7 @@ def _to_parity(value: Parity | int) -> Parity: return Parity(int(value)) -def _to_spin(value: Spin | tuple[Fraction, Fraction]) -> Spin: +def _to_spin(value: Spin | tuple[Fraction, Fraction] | tuple[float, float]) -> Spin: if isinstance(value, tuple): return Spin(*value) return value diff --git a/tests/unit/test_particle.py b/tests/unit/test_particle.py index d9d4cabe..b2dec582 100644 --- a/tests/unit/test_particle.py +++ b/tests/unit/test_particle.py @@ -79,7 +79,7 @@ def test_exceptions(self): parity=-1, c_parity=-1, g_parity=-1, - isospin=(Fraction(0), Fraction(0)), + isospin=(0, 0), charmness=1, ) From d691695f0a1642bc69869e39f373de18e7ae4d45 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 25 Nov 2024 13:52:09 +0100 Subject: [PATCH 40/45] `test_settings` uses `float` again as input --- tests/unit/test_settings.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index 73aae958..33ab1ad4 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -1,6 +1,5 @@ from __future__ import annotations -from fractions import Fraction from typing import TYPE_CHECKING import pytest @@ -89,11 +88,11 @@ def test_create_interaction_settings( "parity": [-1, +1], "c_parity": [-1, +1, None], "g_parity": [-1, +1, None], - "spin_magnitude": _halves_domain(*tuple(map(Fraction, (0, 4)))), - "spin_projection": _halves_domain(*tuple(map(Fraction, (-4, +4)))), + "spin_magnitude": _halves_domain(0, 4), + "spin_projection": _halves_domain(-4, 4), "charge": _int_domain(-2, 2), - "isospin_magnitude": _halves_domain(*tuple(map(Fraction, (0, 1.5)))), - "isospin_projection": _halves_domain(*tuple(map(Fraction, (-1.5, +1.5)))), + "isospin_magnitude": _halves_domain(0, 1.5), + "isospin_projection": _halves_domain(-1.5, +1.5), "strangeness": _int_domain(-3, +3), "charmness": _int_domain(-1, 1), "bottomness": _int_domain(-1, 1), @@ -101,11 +100,11 @@ def test_create_interaction_settings( expected = { "l_magnitude": _int_domain(0, 2), - "s_magnitude": _halves_domain(*tuple(map(Fraction, (0, 2)))), + "s_magnitude": _halves_domain(0, 2), } if "canonical" in formalism: expected["l_projection"] = [-2, -1, 0, 1, 2] - expected["s_projection"] = _halves_domain(*tuple(map(Fraction, (-2, 2)))) + expected["s_projection"] = _halves_domain(-2, 2) if formalism == "canonical-helicity": expected["l_projection"] = [0] if "helicity" in formalism and interaction_type != InteractionType.WEAK: @@ -134,6 +133,6 @@ def test_create_interaction_settings( def test_halves_range(start: float, stop: float, expected: list | None): if expected is None: with pytest.raises(ValueError, match=r"needs to be multiple of 0.5"): - _halves_domain(Fraction(start), Fraction(stop)) + _halves_domain(start, stop) else: - assert _halves_domain(Fraction(start), Fraction(stop)) == expected + assert _halves_domain(start, stop) == expected From e8dc2a21dcc6c105b64a4773c1982eb4b578b6a4 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 25 Nov 2024 13:57:16 +0100 Subject: [PATCH 41/45] destructuring in `as_state_definition` --- src/qrules/combinatorics.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/qrules/combinatorics.py b/src/qrules/combinatorics.py index 1e2f9dec..e8603981 100644 --- a/src/qrules/combinatorics.py +++ b/src/qrules/combinatorics.py @@ -38,8 +38,7 @@ def as_state_definition( if type(definition) is str: return definition if type(definition) is tuple: - name = definition[0] - state = definition[1] + name, state = definition return name, list(map(Fraction, state)) # type: ignore # noqa: PGH003 msg = f"value has to be of type {StateDefinitionInput}, got {type(definition)}" raise ValueError(msg) From 90537d99d92b97c6fb3da634da4d7f7a628fb221 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 25 Nov 2024 14:53:35 +0100 Subject: [PATCH 42/45] fused `_int_as_signed_str` and `_float_as_signed_str` --- src/qrules/particle.py | 11 ++--------- src/qrules/quantum_numbers.py | 4 ++-- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/qrules/particle.py b/src/qrules/particle.py index bed8f5a8..8bea7e4e 100644 --- a/src/qrules/particle.py +++ b/src/qrules/particle.py @@ -27,7 +27,7 @@ from attrs.validators import instance_of from qrules.conservation_rules import GellMannNishijimaInput, gellmann_nishijima -from qrules.quantum_numbers import Parity +from qrules.quantum_numbers import Parity, _float_as_signed_str if sys.version_info < (3, 11): from typing_extensions import Self @@ -241,7 +241,7 @@ def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: p.breakable() p.text(f"{attribute.name}=") if isinstance(value, Parity): - p.text(_int_as_signed_str(int(value), render_plus=True)) + p.text(_float_as_signed_str(int(value), render_plus=True)) else: p.pretty(value) # type: ignore[attr-defined] p.text(",") @@ -249,13 +249,6 @@ def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: p.text(")") -def _int_as_signed_str(value: int, render_plus: bool = False) -> str: - label = str(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 diff --git a/src/qrules/quantum_numbers.py b/src/qrules/quantum_numbers.py index 294d7ce6..9e6d3163 100644 --- a/src/qrules/quantum_numbers.py +++ b/src/qrules/quantum_numbers.py @@ -57,9 +57,9 @@ def __repr__(self) -> str: return f"{type(self).__name__}({_float_as_signed_str(self.value)})" -def _float_as_signed_str(value: float) -> str: +def _float_as_signed_str(value: float, render_plus: bool = False) -> str: string_representation = str(value) - if value > 0: + if value > 0 or render_plus: return f"+{string_representation}" return string_representation From c061be1244c48e4df3d588dddc213205c19da4a4 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 25 Nov 2024 15:14:01 +0100 Subject: [PATCH 43/45] removed redundancies in `__render_as_fraction` --- src/qrules/io/_dot.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/qrules/io/_dot.py b/src/qrules/io/_dot.py index 0ca8455c..2b39a40e 100644 --- a/src/qrules/io/_dot.py +++ b/src/qrules/io/_dot.py @@ -325,13 +325,7 @@ def _(obj: dict) -> str: def __render_as_fraction(value: Any, plusminus: bool) -> str: plusminus &= isinstance(value, Number) and bool(value) if isinstance(value, float): - if value.is_integer(): - return str(int(value)) - nom, denom = value.as_integer_ratio() - if denom == 2: - if plusminus: - return f"{nom:+}/{denom}" - return f"{nom}/{denom}" + return _render_fraction(Fraction(value), plusminus) if isinstance(value, Fraction): return _render_fraction(value, plusminus) if plusminus: From 3e2ba1a1aa6c8a8c2e2102b798ed8e35b74ec3ac Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 25 Nov 2024 15:19:54 +0100 Subject: [PATCH 44/45] removed `__render_as_fraction` altogether --- src/qrules/io/_dot.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/qrules/io/_dot.py b/src/qrules/io/_dot.py index 2b39a40e..455e7d50 100644 --- a/src/qrules/io/_dot.py +++ b/src/qrules/io/_dot.py @@ -12,7 +12,6 @@ from fractions import Fraction from functools import singledispatch from inspect import isfunction -from numbers import Number from typing import TYPE_CHECKING, Any, cast import attrs @@ -317,22 +316,11 @@ def _(obj: dict) -> str: key_repr = key if value != 0 or any(s in key_repr for s in ["magnitude", "projection"]): pm = not any(s in key_repr for s in ["pid", "mass", "width", "magnitude"]) - value_repr = __render_as_fraction(value, pm) + value_repr = _render_fraction(value, pm) lines.append(f"{key_repr} = {value_repr}") return "\n".join(lines) -def __render_as_fraction(value: Any, plusminus: bool) -> str: - plusminus &= isinstance(value, Number) and bool(value) - if isinstance(value, float): - return _render_fraction(Fraction(value), plusminus) - if isinstance(value, Fraction): - return _render_fraction(value, plusminus) - if plusminus: - return f"{value:+}" - return str(value) - - def _render_fraction(fraction: Fraction, plusminus: bool = False) -> str: if plusminus: if fraction.denominator == 1: From 6333441b4d437e3b34bccdf8c8bd1384dae98454 Mon Sep 17 00:00:00 2001 From: grayson-helmholz Date: Mon, 25 Nov 2024 15:49:39 +0100 Subject: [PATCH 45/45] rendering `Fraction`s now uses simpler implementation from `particle.py` --- src/qrules/io/_dot.py | 12 +----------- src/qrules/particle.py | 8 ++++---- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/qrules/io/_dot.py b/src/qrules/io/_dot.py index 455e7d50..2b6a9b20 100644 --- a/src/qrules/io/_dot.py +++ b/src/qrules/io/_dot.py @@ -18,7 +18,7 @@ from attrs import Attribute, define, field from attrs.converters import default_if_none -from qrules.particle import Particle, ParticleWithSpin, Spin +from qrules.particle import Particle, ParticleWithSpin, Spin, _render_fraction from qrules.quantum_numbers import InteractionProperties from qrules.solving import EdgeSettings, NodeSettings, QNProblemSet, QNResult from qrules.topology import FrozenTransition, MutableTransition, Topology, Transition @@ -321,16 +321,6 @@ def _(obj: dict) -> str: return "\n".join(lines) -def _render_fraction(fraction: Fraction, plusminus: bool = False) -> str: - if plusminus: - if fraction.denominator == 1: - if fraction.numerator > 0: - return f"{fraction.numerator:+}" - return str(fraction) - return f"{fraction.numerator:+}/{fraction.denominator}" - return str(fraction) - - @as_string.register(InteractionProperties) def _(obj: InteractionProperties) -> str: lines = [] diff --git a/src/qrules/particle.py b/src/qrules/particle.py index 8ac2b853..1d4dbce2 100644 --- a/src/qrules/particle.py +++ b/src/qrules/particle.py @@ -114,14 +114,14 @@ def __repr__(self) -> str: def _repr_pretty_(self, p: PrettyPrinter, _: bool) -> None: class_name = type(self).__name__ - magnitude = _to_signed_fraction(self.magnitude) - projection = _to_signed_fraction(self.projection, render_plus=True) + magnitude = _render_fraction(self.magnitude) + projection = _render_fraction(self.projection, plusminus=True) p.text(f"{class_name}({magnitude}, {projection})") -def _to_signed_fraction(fraction: Fraction, render_plus: bool = False) -> str: +def _render_fraction(fraction: Fraction, plusminus: bool = False) -> str: string_representation = str(fraction) - if render_plus and fraction.numerator > 0: + if plusminus and fraction.numerator > 0: return f"+{string_representation}" return string_representation