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 FitParameter
s 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)