Source code for laura.models.control
import builtins
from pydantic import (
BaseModel,
ValidationError,
field_validator,
model_serializer,
ConfigDict,
)
from typing import Dict, Type, Literal
import operator
OPS = {
"add": operator.add,
"sub": operator.sub,
"mul": operator.mul,
"truediv": operator.truediv,
"pow": operator.pow,
}
[docs]
def resolve_path(context, path: str):
head, *rest = path.split(".")
if head not in context:
raise KeyError(f"Unknown symbol '{head}' in expression")
obj = context[head]
for attr in rest:
obj = getattr(obj, attr)
return obj
[docs]
def eval_expr(expr, context):
if isinstance(expr, (int, float)):
return expr
if isinstance(expr, str):
return resolve_path(context, expr)
op = OPS[expr["op"]]
args = [eval_expr(a, context) for a in expr["args"]]
return op(*args)
[docs]
def set_attr_by_path(obj, path: str, value):
*parents, attr = path.split(".")
for part in parents:
obj = getattr(obj, part)
setattr(obj, attr, value)
[docs]
class ControlVariable(BaseModel):
"""
Model representing a control variable in a system.
Example on updating element attributes based on control variables:
```python
from laura.models.element import Element
from laura.models.control import ControlVariable, ControlsInformation
# Define control variables
cv1 = ControlVariable(
identifier="k1l_control",
dtype=float,
protocol="some_protocol",
units="1/m",
description="Control for k1l",
read_only=False,
value=0.1,
target="magnetic.k1l",
expression={"op": "mul", "args": ["k1l_control", "magnetic.length"]},
)
controls_info = ControlsInformation(variables={"k1l_control": cv1})
# Create an element with magnetic attributes
element = Element(
name="Quad1",
magnetic={"k1l": 0.0, "k2l": 0.0},
controls=controls_info,
)
# Apply control variables to the element
controls_info.apply(element)
print(element.magnetic.k1l) # Should reflect the updated value based on the control variable
```
"""
identifier: str
"""Unique identifier for the control variable."""
dtype: type = float
"""Data type of the control variable (e.g., int, float, str)."""
protocol: str
"""Protocol or method used to interact with the control variable."""
units: str = "Arb. Units"
"""Units of measurement for the control variable."""
description: str = "Default Description"
"""Description of the control variable."""
read_only: bool = True
"""Indicates if the variable is read-only."""
value: float | int | str | list | None = None
"""Current value of the control variable."""
target: str | None = None # "magnetic.k1l"
"""Target attribute path in the system to apply the control variable."""
expression: dict | None = None # expression graph
"""Expression defining how to compute the value to set at the target."""
type: Literal["scalar", "binary", "state", "string", "waveform", "statistical"] = (
"statistical"
)
"""Type of control variable."""
model_config = ConfigDict(
arbitrary_types_allowed=False,
extra="allow",
# frozen=True,
)
def __init__(self, **data):
super().__init__(**data)
@field_validator("dtype", mode="before")
def validate_dtype(cls, v) -> Type:
"""Convert from string to type if necessary."""
if isinstance(v, str):
try:
return getattr(builtins, v)
except AttributeError:
raise ValueError(f"Unknown dtype string: {v}")
if isinstance(v, type):
return v
raise TypeError(f"dtype must be a type or string, got {type(v)}")
@model_serializer
def serialize(self):
data = self.__dict__.copy()
if isinstance(self.dtype, type):
data["dtype"] = self.dtype.__name__
elif isinstance(self.dtype, str):
data["dtype"] = self.dtype
return data
[docs]
def apply(self, element, context):
if self.target is None or self.expression is None:
return
value = eval_expr(self.expression, context)
set_attr_by_path(element, self.target, value)
[docs]
class ControlsInformation(BaseModel):
"""
Model representing a collection of control variables.
"""
variables: Dict[str, ControlVariable]
"""Dictionary mapping variable names to `~laura.models.control.ControlVariable` instances."""
model_config = ConfigDict(
arbitrary_types_allowed=True,
extra="allow",
frozen=True,
)
@field_validator("variables", mode="before")
def validate_variables(cls, v) -> Dict[str, ControlVariable]:
"""Ensure all values are ControlVariable instances."""
if isinstance(v, dict):
validated_dict = {}
for key, value in v.items():
if isinstance(value, ControlVariable):
validated_dict[key] = value
elif isinstance(value, dict):
try:
validated_dict[key] = ControlVariable(**value)
except ValidationError as e:
raise ValueError(
(
"Invalid ControlVariable definition for key "
+ f"'{key}': "
+ f"{e}"
)
) from e
else:
raise TypeError(
(
f"Value for key '{key}'"
+ "must be a ControlVariable or dict, "
+ f"got {type(value)}"
)
)
return validated_dict
raise TypeError(f"variables must be a dict, got {type(v)}")
[docs]
@staticmethod
def build_context(element):
ctx = {k: v.value for k, v in element.controls.variables.items()}
for param in ["magnetic", "physical", "cavity", "simulation"]:
if hasattr(element, param):
ctx.update({param: getattr(element, param)})
return ctx
# @staticmethod
# def build_context(element):
# return {
# **{k: v.value for k, v in element.controls.variables.items()},
# "magnetic": element.magnetic,
# "physical": element.physical,
# }
[docs]
def apply(self, element):
ctx = self.build_context(element)
for cv in element.controls.variables.values():
cv.apply(element, ctx)
[docs]
class ScreenControlsInformation(ControlsInformation):
"""
Model representing a collection of control variables pertaining to screens.
"""
movement_type: str
"""Screen motion type"""
devices: dict
"""List of screen device enums"""
horizontal_devices: dict | None = None
"""List of horizontal screen device enums"""
vertical_devices: dict | None = None
"""List of vertical screen device enums"""
[docs]
class MirrorControlsInformation(ControlsInformation):
"""
Model representing a collection of control variables pertaining to mirrors.
"""
step_max: float = 0.05
"""Maximum step size for mirror movement"""
default_step: float = 0.005
"""Default step size for mirror movement"""
right_sense: int = -1
"""Right sense for mirror movement"""
up_sense: int = -1
"""Up sense for mirror movement"""
left_sense: int = 1
"""Left sense for mirror movement"""
down_sense: int = 1
"""Down sense for mirror movement"""
[docs]
class ShutterControlsInformation(ControlsInformation):
"""
Model representing a collection of control variables pertaining to shutters.
"""
shutter_type: str
"""Type of shutter, i.e. 'LASER', 'BEAM'"""