Python operator library#
See Python’s built-in operator library
What we have now#
Build test model
import expertsystem as es
result = es.generate_transitions(
initial_state=[("J/psi(1S)", [-1, 1])],
final_state=["p", "p~", "eta"],
allowed_intermediate_particles=["N(1440)"],
allowed_interaction_types="strong",
)
model = es.generate_amplitudes(result)
for particle in result.get_intermediate_particles():
model.dynamics.set_breit_wigner(particle.name)
es.io.write(model, "recipe.yml")
Visualize the decay:
import graphviz
graphs = result.collapse_graphs()
dot = es.io.convert_to_dot(graphs)
graphviz.Source(dot)
model.parameters
FitParameters([
FitParameter(name='Magnitude_J/psi(1S)_to_N(1440)+_0.5+p~_-0.5;N(1440)+_to_eta_0+p_0.5;', value=1.0, fix=False),
FitParameter(name='Magnitude_J/psi(1S)_to_N(1440)+_0.5+p~_0.5;N(1440)+_to_eta_0+p_0.5;', value=1.0, fix=False),
FitParameter(name='Magnitude_J/psi(1S)_to_N(1440)~-_0.5+p_-0.5;N(1440)~-_to_eta_0+p~_0.5;', value=1.0, fix=False),
FitParameter(name='Magnitude_J/psi(1S)_to_N(1440)~-_0.5+p_0.5;N(1440)~-_to_eta_0+p~_0.5;', value=1.0, fix=False),
FitParameter(name='MesonRadius_J/psi(1S)', value=1.0, fix=True),
FitParameter(name='MesonRadius_N(1440)+', value=1.0, fix=True),
FitParameter(name='MesonRadius_N(1440)~-', value=1.0, fix=True),
FitParameter(name='Phase_J/psi(1S)_to_N(1440)+_0.5+p~_-0.5;N(1440)+_to_eta_0+p_0.5;', value=0.0, fix=False),
FitParameter(name='Phase_J/psi(1S)_to_N(1440)+_0.5+p~_0.5;N(1440)+_to_eta_0+p_0.5;', value=0.0, fix=False),
FitParameter(name='Phase_J/psi(1S)_to_N(1440)~-_0.5+p_-0.5;N(1440)~-_to_eta_0+p~_0.5;', value=0.0, fix=False),
FitParameter(name='Phase_J/psi(1S)_to_N(1440)~-_0.5+p_0.5;N(1440)~-_to_eta_0+p~_0.5;', value=0.0, fix=False),
FitParameter(name='Position_N(1440)+', value=1.44, fix=False),
FitParameter(name='Position_N(1440)~-', value=1.44, fix=False),
FitParameter(name='Width_N(1440)+', value=0.35, fix=False),
FitParameter(name='Width_N(1440)~-', value=0.35, fix=False),
])
Implementation with operators#
See this answer on Stack Overflow:
import operator
MAKE_BINARY = lambda opfn: lambda self, other: BinaryOp( # noqa: E731
self, asMagicNumber(other), opfn
)
MAKE_RBINARY = lambda opfn: lambda self, other: BinaryOp( # noqa: E731
asMagicNumber(other), self, opfn
)
class MagicNumber:
__add__ = MAKE_BINARY(operator.add)
__sub__ = MAKE_BINARY(operator.sub)
__mul__ = MAKE_BINARY(operator.mul)
__radd__ = MAKE_RBINARY(operator.add)
__rsub__ = MAKE_RBINARY(operator.sub)
__rmul__ = MAKE_RBINARY(operator.mul)
__truediv__ = MAKE_BINARY(operator.truediv)
__rtruediv__ = MAKE_RBINARY(operator.truediv)
__floordiv__ = MAKE_BINARY(operator.floordiv)
__rfloordiv__ = MAKE_RBINARY(operator.floordiv)
def __neg__(self):
return UnaryOp(self, operator.neg)
@property
def value(self):
return self.eval()
class Constant(MagicNumber):
def __init__(self, value):
self.value_ = value
def eval(self):
return self.value_
class Parameter(Constant):
def __init__(self):
super().__init__(0.0)
def setValue(self, v):
self.value_ = v
value = property(fset=setValue, fget=lambda self: self.value_)
class BinaryOp(MagicNumber):
def __init__(self, op1, op2, operation):
self.op1 = op1
self.op2 = op2
self.opn = operation
def eval(self):
return self.opn(self.op1.eval(), self.op2.eval())
class UnaryOp(MagicNumber):
def __init__(self, op1, operation):
self.op1 = op1
self.operation = operation
def eval(self):
return self.opn(self.op1.eval())
asMagicNumber = lambda x: ( # noqa: E731
x if isinstance(x, MagicNumber) else Constant(x)
)
asMagicNumber(2).eval()
2
Other ideas#
Option 1: parameter container
Remove name from the FitParameter class and give the FitParameters collection class the responsibility to keep track of ‘names’ of the FitParameters as keys in a dict. In the AmplitudeModel, locations where a FitParameter should be inserted are indicated by an immutable (!) str that should exist as a key in the FitParameters.
Such a setup best reflects the structure of the AmplitudeModel that we have now (best illustrated by expected_recipe, note in particular YAML anchors like &par1/*par1). It also allows one to couple FitParameters. See following snippet:
from attrs import define, frozen
# the new FitParameter class would have this structure
@define
class Parameter:
value: float
fix: bool = False
# the new FitParameters collection would have such a structure
mapping = {
"par1": Parameter(1.0),
"par2": Parameter(2.0, fix=False),
}
# intensity nodes and dynamics classes contain immutable strings
class Dynamics:
pass
@frozen
class CustomDynamics(Dynamics):
par: str
dyn1 = CustomDynamics(par="par1")
dyn2 = CustomDynamics(par="par2")
# Parameters would be coupled like this
mapping["par1"] = mapping["par2"]
assert mapping["par2"] is mapping["par1"]
assert mapping["par1"] == {
"par1": Parameter(1.0),
"par2": Parameter(1.0),
}
Option 2: read-only parameter manager
Remove the FitParameters collection class altogether and use something like immutable InitialParameter instances in the dynamics and intensity section of the AmplitudeModel. The AmplitudeModel then starts to serve as a read-only’ template. A fitter package like tensorwaves can then loop over the AmplitudeModel structure to extract the InitialParameter instances and convert them to something like an FitParameter.
Here’s a rough sketch with tensorwaves in mind.
from typing import Generator
import attrs
from attrs import define, field
from expertsystem.amplitude.model import (
AmplitudeModel,
Dynamics,
Node,
ParticleDynamics,
)
from expertsystem.reaction.particle import Particle
@define
class InitialParameter:
name: str = field()
value: float = field()
# fix: bool = field(default=False)
@define
class FitParameter:
name: str = field(on_setattr=attrs.setters.frozen)
value: float = field()
fix: bool = field(default=False)
class FitParameterManager:
"""Manages all fit parameters of the model"""
def __init__(self, model: AmplitudeModel) -> None:
self.__model: AmplitudeModel
self.__parameter_couplings: dict[str, str]
@property
def parameters(self) -> list[FitParameter]:
initial_parameters = list(__yield_parameter(self.__model))
self.__apply_couplings()
return self.__convert(initial_parameters)
def couple_parameters(self, parameter1: str, parameter2: str) -> None:
pass
def __convert(self, params: list[InitialParameter]) -> list[FitParameter]:
pass
@define
class CustomDynamics(Dynamics):
parameter: InitialParameter = field(kw_only=True)
@staticmethod
def from_particle(particle: Particle):
pass
def __yield_parameter(
instance: object,
) -> Generator[InitialParameter, None, None]:
if isinstance(instance, InitialParameter):
yield instance
elif isinstance(instance, (ParticleDynamics, Node)):
for item in instance.values():
yield from __yield_parameter(item)
elif isinstance(instance, (list,)):
for item in instance:
yield from __yield_parameter(item)
elif attrs.has(instance.__class__):
for field in attrs.fields(instance.__class__):
field_value = getattr(instance, field.name)
yield from __yield_parameter(field_value)
# usage in tensorwaves
amp_model = AmplitudeModel()
kinematics: HelicityKinematics = ...
builder = IntensityBuilder(kinematics)
intensity = builder.create(amp_model) # this would call amp_model.parameters
parameters: dict[str, float] = intensity.parameters
# PROBLEM?: fix status is lost at this point
data_sample = generate_data(...)
dataset = kinematics.convert(data_sample)
parameters["Width_f(0)(980)"] = 0.2 # name is immutable at this point
# name of a parameter can be changed in the AmplitudeModel though
# and then call builder again
intensity(dataset, parameters)