Observables#
Definition#
Observables provide a unified way to access a large quantity of figures resulting from various computations on lattices. They may be used in parameter scans, matching, response matrices, plots…
Observables values are usually scalars or numpy arrays of any shape, but may have any type.
An Observable has a name, optional target, weight and bounds attributes for matching,
a label for plot legends. After evaluation, it has the following main properties:
weighted_value:value / weightdeviation:value - targetweighted_deviation:(value - target)/weightresidual:((value - target)/weight)**2
For evaluation, observables must be grouped in an ObservableList which combines the needs of all its members, avoiding redundant computations. ObservableList provides the evaluate() method, and the
values, deviations,
residuals and sum_residuals properties, among others.
Attention
Evaluation must only be done on ObservableLists, not directly on Observables. Evaluating directly an Observable will miss the data computed at the list level to avoid redundant computations.
Only instances of the base Observableclass may be directly evaluated since they don’t
require any data
Observable values may depend on parameters being varied for different evaluations: for instance, optics results depend on the off-momentum dp, trajectory coordinates depend on the input coordinates r_in. These are called “evaluation parameters”. They can be defined, in increasing priority
at Observable instantation, to provide default values,
in the containing ObservableList, to give common parameters to all its members,
as arguments of the
evaluate()method.
For optics specific Observables, the available evaluation parameters are documented with the Observable. For user-defined Observables, any evaluation parameter can be defined.
Attention
For optics-specific Observables, all optics computations are made at the ObservableList level for efficiency. So evaluation parameters defined at the Observable level will be ignored.
For instance the ring, dp parameters will be used in optics computations common to all the Observables. Such parameters defined in single Observables are ignored
AT provides a number of specific observables sharing a common interface, inherited from the
Observable base class. They are:
RingObservable: Any user-defined property depending upon ringElementObservable: Any user-defined property at a specific location along the latticeOrbitObservable: Closed orbit: \(x_{co}\)…,GlobalOpticsObservable: global output ofget_optics(): tunes, damping times…,LocalOpticsObservable: local output ofget_optics(): \(\beta\), \(\eta\)…,MatrixObservable: transfer matrix elements \(T_{ij}\)…,TrajectoryObservable: \(x, p_x\)…,EmittanceObservable: \(\epsilon_x\), \(\tau_x\)…,LatticeObservable: attributes of lattice elements,GeometryObservable: floor positions of elements,LocalEmittanceObservable: local output ofohmi_envelope()
Custom Observables may be created by providing the adequate evaluation function.
Observable gallery#
These examples show how to declare various Observables, how to evaluate them and how to extract and display their values.
Setup the environment#
import at
import numpy as np
from importlib.resources import files, as_file
import time
from at import (
Observable,
ObservableList,
RingObservable,
OrbitObservable,
GlobalOpticsObservable,
LocalOpticsObservable,
MatrixObservable,
TrajectoryObservable,
EmittanceObservable,
LatticeObservable,
GeometryObservable,
Need,
)
Load a test lattice#
fname = "hmba.mat"
with as_file(files("machine_data") / fname) as path:
hmba_lattice = at.load_lattice(path)
Create Observables#
Create an empty ObservableList:
obs1 = ObservableList()
Horizontal closed orbit on all Monitors:
obs1.append(OrbitObservable(at.Monitor, axis="x"))
Create a 2nd ObservableList:
obs2 = ObservableList()
Vertical \(\beta\) at all monitors, with a target and bounds.
The vertical \(\beta\) is constrained in the interval [target+low_bound target+up_bound], so here [-Infinity 7.0]
The residual will be zero within the interval.
obs2.append(
LocalOpticsObservable(
at.Monitor, "beta", plane=1, target=7.0, bounds=(-np.inf, 0.0)
)
)
check the concatenation of ObservableLists:
allobs = obs1 + obs2
Full transfer matrix to BPM02:
allobs.append(MatrixObservable("BPM_02"))
Maximum of vertical beta on monitors:
allobs.append(LocalOpticsObservable(at.Monitor, "beta", plane="v", statfun=np.amax))
First 4 coordinates of the closed orbit at Quadrupoles:
allobs.append(
LocalOpticsObservable(
at.Quadrupole, "closed_orbit", plane=slice(4), target=0.0, weight=1.0e-6
)
)
Position along the lattice of all quadrupoles:
allobs.append(LocalOpticsObservable(at.Quadrupole, "s_pos"))
Horizontal tune with the integer part:
allobs.append(GlobalOpticsObservable("tune", plane=0, use_integer=True))
Total phase advance at the end of the lattice (all planes):
allobs.append(LocalOpticsObservable(at.End, "mu"))
Horizontal W-function:
allobs.append(LocalOpticsObservable(at.Sextupole, "W", plane="x"))
Chromaticity in all planes:
allobs.append(GlobalOpticsObservable("chromaticity"))
Average of sextupole strengths:
allobs.append(LatticeObservable(at.Sextupole, "H", statfun=np.mean))
Strengths of all sextupoles:
allobs.append(LatticeObservable(at.Sextupole, "PolynomB", index=2))
Horizontal emittance:
allobs.append(EmittanceObservable("emittances", plane="x"))
px component of the trajectory on all monitors:
allobs.append(TrajectoryObservable(at.Monitor, axis="px"))
Floor position of all monitors:
allobs.append(GeometryObservable(at.Monitor, "x"))
Horizontal beam size {math}`\sigma_x} at monitor positions:
allobs.append(at.LocalEmittanceObservable(at.Monitor, "sigma6", axis=0))
Evaluation#
An input trajectory is required for the trajectory Observable
r_in = np.zeros(6)
r_in[0] = 0.001
r_in[2] = 0.001
values = allobs.evaluate(
ring=hmba_lattice.enable_6d(copy=True), r_in=r_in, dp=0.0, initial=True
)
Extract a single Observable value#
index 7: total phase advance
allobs[7].value
array([[1.49638018e+01, 5.36820522e+00, 6.85246956e-04]])
Get the list of all Observable values:#
allobs.values
(array([-3.02197151e-09, 4.50706098e-07, 4.08215750e-07, 2.37905632e-08,
-1.31787024e-08, 2.47236652e-08, -2.95318222e-08, -4.05608194e-07,
-4.47409214e-07, -2.24856454e-09]),
array([5.30279703, 7.17604152, 6.55087808, 2.31448878, 3.40498444,
3.405044 , 2.3146451 , 6.55106241, 7.17614175, 5.30283837]),
array([[[-1.08194106e+00, 3.18809568e+00, 0.00000000e+00,
0.00000000e+00, 8.22407787e-02, -1.72158067e-05],
[-6.80522735e-01, 1.08099571e+00, 0.00000000e+00,
0.00000000e+00, 4.90131193e-02, -1.02601216e-05],
[ 0.00000000e+00, 0.00000000e+00, 7.55929650e-01,
3.87059271e+00, 0.00000000e+00, 0.00000000e+00],
[ 0.00000000e+00, 0.00000000e+00, -6.79279293e-01,
-2.15524755e+00, 0.00000000e+00, 0.00000000e+00],
[-1.13313809e-08, -1.08618621e-07, 0.00000000e+00,
0.00000000e+00, 9.99995907e-01, -2.09333332e-04],
[ 2.93742345e-03, 6.73567965e-02, 0.00000000e+00,
0.00000000e+00, 2.83582574e-04, 9.99999941e-01]]]),
array(7.17614175),
array([[-3.02817763e-09, -1.45848773e-10, 0.00000000e+00,
0.00000000e+00],
[-1.78482703e-09, 2.17272049e-09, 0.00000000e+00,
0.00000000e+00],
[ 2.06043297e-07, 1.68974844e-07, 0.00000000e+00,
0.00000000e+00],
[ 4.63473229e-07, 2.65118677e-07, 0.00000000e+00,
0.00000000e+00],
[ 4.92858972e-07, -2.48835339e-09, 0.00000000e+00,
0.00000000e+00],
[ 2.39074029e-07, -2.69330093e-07, 0.00000000e+00,
0.00000000e+00],
[ 2.24952949e-08, -2.55618841e-08, 0.00000000e+00,
0.00000000e+00],
[-2.95810596e-08, -2.51349416e-08, 0.00000000e+00,
0.00000000e+00],
[ 3.86447211e-08, 4.66083371e-08, 0.00000000e+00,
0.00000000e+00],
[-1.14696047e-08, -5.89639317e-08, 0.00000000e+00,
0.00000000e+00],
[-1.92477070e-07, -1.36146236e-07, 0.00000000e+00,
0.00000000e+00],
[-4.58538822e-07, -2.67114035e-07, 0.00000000e+00,
0.00000000e+00],
[-4.90197682e-07, -2.06362952e-09, 0.00000000e+00,
0.00000000e+00],
[-2.42431087e-07, 2.63622246e-07, 0.00000000e+00,
0.00000000e+00],
[-8.04974088e-10, -1.24376477e-09, 0.00000000e+00,
0.00000000e+00],
[-1.92739960e-09, -1.83202800e-09, 0.00000000e+00,
0.00000000e+00]]),
array([ 2.693952 , 3.4295565 , 5.52309303, 6.52741246, 7.08941246,
8.14326589, 10.34278161, 11.93982486, 13.94182609, 15.63285034,
18.00333506, 19.05718849, 19.61918849, 20.67257592, 22.71704445,
23.36843995]),
array(2.38156302),
array([[1.49638018e+01, 5.36820522e+00, 6.85246956e-04]]),
array([18.37111583, 25.91715269, 12.96214618, 9.66599263, 13.81023313,
24.25197574]),
array([1.79196879e-01, 1.22425549e-01, 1.69974604e-04]),
array(-25.36921225),
array([-78.95535579, 77.03724443, -74.18952538, -74.18952538,
77.03724443, -78.95535579]),
array(1.32035953e-10),
array([ 0.00000000e+00, -6.94370475e-04, 6.07151650e-04, 2.38468289e-04,
-6.81824075e-04, -4.78921799e-04, 4.41491590e-04, 7.01582185e-04,
-6.05543948e-04, -9.78684826e-05]),
array([ 2.6514 , 6.4783308 , 7.51380991, 10.28830988, 12.71979153,
13.62739043, 16.04912109, 18.79203055, 19.81402164, 23.58054559]),
array([3.23752157e-05, 3.24423602e-05, 1.93341763e-05, 3.77495033e-05,
8.30721965e-05, 8.54454703e-05, 9.11034786e-05, 7.56573193e-05,
4.47778371e-05, 1.99563466e-05, 2.04779223e-05, 2.25196538e-05,
1.87401375e-05, 1.87404612e-05, 2.53949876e-05, 1.75326751e-05,
1.99555568e-05, 3.65786596e-05, 7.56601806e-05, 8.54311269e-05,
9.11142778e-05, 8.30753073e-05, 4.48522091e-05, 1.55259757e-05,
2.90185948e-05, 3.23758407e-05, 3.02266954e-05]))
Get a pretty output of all Observables.#
As no variation was made, Actual values are always equal to Initial values.
The deviation is zero for all Observables for which no target was specified
print(allobs)
location Initial Actual Low bound High bound deviation
orbit[x]
BPM_01 -3.02197e-09 -3.02197e-09 - - 0.0
BPM_02 4.50706e-07 4.50706e-07 - - 0.0
BPM_03 4.08216e-07 4.08216e-07 - - 0.0
BPM_04 2.37906e-08 2.37906e-08 - - 0.0
BPM_05 -1.31787e-08 -1.31787e-08 - - 0.0
BPM_06 2.47237e-08 2.47237e-08 - - 0.0
BPM_07 -2.95318e-08 -2.95318e-08 - - 0.0
BPM_08 -4.05608e-07 -4.05608e-07 - - 0.0
BPM_09 -4.47409e-07 -4.47409e-07 - - 0.0
BPM_10 -2.24856e-09 -2.24856e-09 - - 0.0
beta[y]
BPM_01 5.3028 5.3028 -inf 7.0 0.0
BPM_02 7.17604 7.17604 -inf 7.0 0.176042
BPM_03 6.55088 6.55088 -inf 7.0 0.0
BPM_04 2.31449 2.31449 -inf 7.0 0.0
BPM_05 3.40498 3.40498 -inf 7.0 0.0
BPM_06 3.40504 3.40504 -inf 7.0 0.0
BPM_07 2.31465 2.31465 -inf 7.0 0.0
BPM_08 6.55106 6.55106 -inf 7.0 0.0
BPM_09 7.17614 7.17614 -inf 7.0 0.176142
BPM_10 5.30284 5.30284 -inf 7.0 0.0
matrix
BPM_02 [-1.082 ...] [-1.082 ...] - - [ 0.0 ...]
amax(beta[y])
7.17614 7.17614 - - 0.0
closed_orbit[slice(None, 4, None)]
QF1A [-3.028e-09 ...] [-3.028e-09 ...] [ 0.0 ...] [ 0.0 ...] [-3.028e-09 ...]
QD2A [-1.785e-09 ...] [-1.785e-09 ...] [ 0.0 ...] [ 0.0 ...] [-1.785e-09 ...]
QD3A [ 2.06e-07 ...] [ 2.06e-07 ...] [ 0.0 ...] [ 0.0 ...] [ 2.06e-07 ...]
QF4A [ 4.635e-07 ...] [ 4.635e-07 ...] [ 0.0 ...] [ 0.0 ...] [ 4.635e-07 ...]
QF4B [ 4.929e-07 ...] [ 4.929e-07 ...] [ 0.0 ...] [ 0.0 ...] [ 4.929e-07 ...]
QD5B [ 2.391e-07 ...] [ 2.391e-07 ...] [ 0.0 ...] [ 0.0 ...] [ 2.391e-07 ...]
QF6B [ 2.25e-08 ...] [ 2.25e-08 ...] [ 0.0 ...] [ 0.0 ...] [ 2.25e-08 ...]
QF8B [-2.958e-08 ...] [-2.958e-08 ...] [ 0.0 ...] [ 0.0 ...] [-2.958e-08 ...]
QF8D [ 3.864e-08 ...] [ 3.864e-08 ...] [ 0.0 ...] [ 0.0 ...] [ 3.864e-08 ...]
QF6D [-1.147e-08 ...] [-1.147e-08 ...] [ 0.0 ...] [ 0.0 ...] [-1.147e-08 ...]
QD5D [-1.925e-07 ...] [-1.925e-07 ...] [ 0.0 ...] [ 0.0 ...] [-1.925e-07 ...]
QF4D [-4.585e-07 ...] [-4.585e-07 ...] [ 0.0 ...] [ 0.0 ...] [-4.585e-07 ...]
QF4E [-4.902e-07 ...] [-4.902e-07 ...] [ 0.0 ...] [ 0.0 ...] [-4.902e-07 ...]
QD3E [-2.424e-07 ...] [-2.424e-07 ...] [ 0.0 ...] [ 0.0 ...] [-2.424e-07 ...]
QD2E [-8.05e-10 ...] [-8.05e-10 ...] [ 0.0 ...] [ 0.0 ...] [-8.05e-10 ...]
QF1E [-1.927e-09 ...] [-1.927e-09 ...] [ 0.0 ...] [ 0.0 ...] [-1.927e-09 ...]
s_pos
QF1A 2.69395 2.69395 - - 0.0
QD2A 3.42956 3.42956 - - 0.0
QD3A 5.52309 5.52309 - - 0.0
QF4A 6.52741 6.52741 - - 0.0
QF4B 7.08941 7.08941 - - 0.0
QD5B 8.14327 8.14327 - - 0.0
QF6B 10.3428 10.3428 - - 0.0
QF8B 11.9398 11.9398 - - 0.0
QF8D 13.9418 13.9418 - - 0.0
QF6D 15.6329 15.6329 - - 0.0
QD5D 18.0033 18.0033 - - 0.0
QF4D 19.0572 19.0572 - - 0.0
QF4E 19.6192 19.6192 - - 0.0
QD3E 20.6726 20.6726 - - 0.0
QD2E 22.717 22.717 - - 0.0
QF1E 23.3684 23.3684 - - 0.0
tune[x]
2.38156 2.38156 - - 0.0
mu
End [ 14.96 ...] [ 14.96 ...] - - [ 0.0 ...]
W[x]
SD1A 18.3711 18.3711 - - 0.0
SF2A 25.9172 25.9172 - - 0.0
SD1B 12.9621 12.9621 - - 0.0
SD1D 9.66599 9.66599 - - 0.0
SF2E 13.8102 13.8102 - - 0.0
SD1E 24.252 24.252 - - 0.0
chromaticity
[ 0.1792 ...] [ 0.1792 ...] - - [ 0.0 ...]
mean(H)
-25.3692 -25.3692 - - 0.0
PolynomB[2]
SD1A -78.9554 -78.9554 - - 0.0
SF2A 77.0372 77.0372 - - 0.0
SD1B -74.1895 -74.1895 - - 0.0
SD1D -74.1895 -74.1895 - - 0.0
SF2E 77.0372 77.0372 - - 0.0
SD1E -78.9554 -78.9554 - - 0.0
emittances[x]
1.32036e-10 1.32036e-10 - - 0.0
trajectory[px]
BPM_01 0.0 0.0 - - 0.0
BPM_02 -0.00069437 -0.00069437 - - 0.0
BPM_03 0.000607152 0.000607152 - - 0.0
BPM_04 0.000238468 0.000238468 - - 0.0
BPM_05 -0.000681824 -0.000681824 - - 0.0
BPM_06 -0.000478922 -0.000478922 - - 0.0
BPM_07 0.000441492 0.000441492 - - 0.0
BPM_08 0.000701582 0.000701582 - - 0.0
BPM_09 -0.000605544 -0.000605544 - - 0.0
BPM_10 -9.78685e-05 -9.78685e-05 - - 0.0
geometry[x]
BPM_01 2.6514 2.6514 - - 0.0
BPM_02 6.47833 6.47833 - - 0.0
BPM_03 7.51381 7.51381 - - 0.0
BPM_04 10.2883 10.2883 - - 0.0
BPM_05 12.7198 12.7198 - - 0.0
BPM_06 13.6274 13.6274 - - 0.0
BPM_07 16.0491 16.0491 - - 0.0
BPM_08 18.792 18.792 - - 0.0
BPM_09 19.814 19.814 - - 0.0
BPM_10 23.5805 23.5805 - - 0.0
sigma6[x]
BPM_01 3.23752e-05 3.23752e-05 - - 0.0
BPM_02 3.24424e-05 3.24424e-05 - - 0.0
BPM_03 1.93342e-05 1.93342e-05 - - 0.0
BPM_04 3.77495e-05 3.77495e-05 - - 0.0
BPM_05 8.30722e-05 8.30722e-05 - - 0.0
BPM_06 8.54455e-05 8.54455e-05 - - 0.0
BPM_07 9.11035e-05 9.11035e-05 - - 0.0
BPM_08 7.56573e-05 7.56573e-05 - - 0.0
BPM_09 4.47778e-05 4.47778e-05 - - 0.0
BPM_10 1.99563e-05 1.99563e-05 - - 0.0
User-defined evaluation functions#
User-defined functions receive as keyword arguments all the evaluation parameters specified
on their Observable instantiation, on the containing ObservableList and as arguments of the
evaluate() method. They must therefore be ready to accept and ignore
all keywords targeting other Observables.
With the base Observable class#
When using the base Observable class, the evaluation function is entirely responsible to provide the return value. It is called as value = fun(*eval_args, **eval_kw). eval_args is a tuple of
positional evaluation arguments defined at the Observable instantiation, eval_kw is the dictionary of evaluation keywords.
The evaluation function returns the current date and time. We define the output format as an
evaluation parameter named format:
def now(format="%j", **_):
# ignore all other keywords
return time.strftime(format)
We can now create two observables using this function, the second forcing the date format to %c
allobs = ObservableList([Observable(now), Observable(now, format="%c")])
Evaluation returns both different results:
allobs.evaluate()
for obs in allobs:
print(f"{obs.name!r}: {obs.value}")
'now': 019
'now': Mon Jan 19 15:01:40 2026
But if we give a format to evaluate(), it will have priority in both cases:
allobs.evaluate(format="%A")
for obs in allobs:
print(f"{obs.name!r}: {obs.value}")
'now': Monday
'now': Monday
With RingObservable#
The evaluation function receives the lattice attached to the RingObservable.
It is called as value = fun(ring, **eval_kw). The lattice is given as a positional
argument. All the evaluation keywords are also provided and can be ignored if irrelevant.
By default the observable name is taken as the name of the evaluation function.
allobs = ObservableList()
Lattice circumference:
def circumference(ring, **_):
# ignore all other keywords
return ring.circumference
allobs.append(RingObservable(circumference))
Lattice momentum compaction:
As get_mcf() needs a 4D lattice, we need to make sure
that the given lattice is 4D, whatever the lattice given to evaluate():
def momentum_compaction(ring, dp=0.0, **_):
# ignore all other keywords
return ring.get_mcf(dp=dp)
allobs.append(RingObservable(momentum_compaction, needs=Need.NEED_4D))
Evaluation:
allobs.evaluate(ring=hmba_lattice.enable_6d(copy=True), dp=0.01)
for obs in allobs:
print(f"{obs.name!r}: {obs.value}")
'circumference': 843.9772144741422
'momentum_compaction': 8.820785280498264e-05
With LocalOpticsObservable#
The evaluation function receives the optical data computed at the specified locations.
It is called as value = fun(elemdata, **eval_kw) where elemdata is the output of get_optics() for all the locations specified in refpts.
The return value must have as many lines as observation points (the length of elemdata) and any number of columns. The column may be selected in the output with the plane keyword.
allobs = ObservableList()
Phase advance between 2 points:
We ask for the phase at 2 points and take the difference
def phase_advance(elemdata, **_):
mu = elemdata.mu
return mu[-1] - mu[0]
We need to set all_points to avoid jumps in the phase advance, and to set summary to tell that the evaluation returns a single output instead of one output per observation point. We look at positions 33 and 101 and select vertical plane:
allobs.append(
LocalOpticsObservable(
[33, 101], phase_advance, plane="y", all_points=True, summary=True
)
)
Beam size:
We can compute the beam envelope for arbitrary emittances \(\epsilon_{x|y}\) and momentum spread \(\sigma_\delta\) using \(\sigma_{x|y} = \sqrt{\beta_{x|y} \epsilon_{x|y} + (\eta_{x|y} \sigma_\delta)^2}\).
We will define the emittances as a emit evaluation keyword and the momentum spread as a sigma_e keyword:
def beam_size(elemdata, emit=None, sigma_e=None, **_):
return np.sqrt(
elemdata.beta * emit + (elemdata.dispersion[:, [0, 2]] * sigma_e) ** 2
)
We set default values for the emittances at instantiation of the observable and look at all the monitors:
allobs.append(
LocalOpticsObservable(
at.Monitor, beam_size, emit=[130.0e-12, 10.0e-12], sigma_e=0.9e-3
)
)
We can now evaluate the results for default options:
allobs.evaluate(ring=hmba_lattice)
for obs in allobs:
print(f"{obs.name!r}: {obs.value}")
'phase_advance[y]': 2.9974197440054082
'beam_size': [[3.21226173e-05 7.28205547e-06]
[8.04615146e-05 8.47119400e-06]
[7.32761035e-05 8.09381727e-06]
[1.95950639e-05 4.81099629e-06]
[1.82841176e-05 5.83525760e-06]
[1.82841255e-05 5.83526128e-06]
[1.95950796e-05 4.81100216e-06]
[7.32760882e-05 8.09380363e-06]
[8.04614926e-05 8.47117301e-06]
[3.21226186e-05 7.28203334e-06]]
But we can also evaluate with different parameters, for instance at 1% off-momentum and with different emittances:
allobs.evaluate(ring=hmba_lattice, dp=0.01, emit=[140.0e-12, 20.0e-12])
for obs in allobs:
print(f"{obs.name!r}: {obs.value}")
'phase_advance[y]': 2.944614942817739
'beam_size': [[3.56856110e-05 1.02701261e-05]
[7.99928036e-05 1.22514050e-05]
[7.27788329e-05 1.17851226e-05]
[1.89108125e-05 6.92431365e-06]
[1.93833205e-05 8.05116711e-06]
[1.93833291e-05 8.05117223e-06]
[1.89108266e-05 6.92432199e-06]
[7.27788189e-05 1.17851048e-05]
[7.99927834e-05 1.22513769e-05]
[3.56856115e-05 1.02700955e-05]]