What happens
AT keeps the dipole kick of an element in two places: the polynom (PolynomB[0] / PolynomA[0]) and KickAngle. The magnet accessors in pyaml/lattice/abstract_impl.py read and write the polynom only, so a kick that lives in KickAngle is not seen:
| element in the lattice |
before any write |
after strength.set(1e-4) |
at.Corrector (CorrectorPass), any length |
– |
get returns 1e-4, closed orbit stays 0 |
at.Corrector defined without PolynomA/B |
AttributeError when the accessor is built |
– |
ThinMultipole with KickAngle=[1e-4, 0] |
beam is kicked, get returns 0 |
orbit doubles, get returns 1e-4 |
Multipole (0.2 m) with KickAngle=[1e-4, 0] |
beam is kicked, get returns 0 |
orbit doubles, get returns 1e-4 |
ThinMultipole without KickAngle |
orbit 0, get returns 0 |
correct |
The reason is in AT: CorrectorPass integrates KickAngle and ignores the polynoms; the multipole pass methods (ThinMPolePass, StrMPole*, BndMPole*, ExactMultipole*) integrate the polynom and add KickAngle to it.
#435 fixed the length bookkeeping for zero-length elements; this is a separate point. The thin-corrector test added there uses at.Corrector, so it checks that set and get agree but not that the orbit moves. I saw that #467 moved the TL2 model from CorrectorPass to ThinMPolePass, which avoids the first row for that lattice.
How to reproduce
import at, numpy as np
from pyaml.lattice.abstract_impl import RWStrengthScalar
from pyaml.magnet.hcorrector import HCorrector
from pyaml.magnet.identity_model import IdentityMagnetModel
def ring(cor):
qf, qd = at.Quadrupole("QF", 0.5, 1.2), at.Quadrupole("QD", 0.5, -1.2)
return at.Lattice([cor] + [qf, at.Drift("D", 1.0), at.Monitor("BPM"), qd, at.Drift("D", 1.0)] * 8,
energy=2e9, periodicity=1)
def orbit(cor):
return np.abs(at.find_orbit4(ring(cor), refpts=at.Monitor)[1][:, 0]).max()
model = IdentityMagnetModel(physics="COR", unit="rad")
for cor in (at.Corrector("C", 0.0, [0.0, 0.0], PolynomA=[0.0], PolynomB=[0.0]),
at.ThinMultipole("C", [0.0], [0.0], KickAngle=[1e-4, 0.0]),
at.ThinMultipole("C", [0.0], [0.0])):
acc = RWStrengthScalar([cor], HCorrector.polynom, model)
before = (acc.get(), orbit(cor))
acc.set(1e-4)
print(f"{cor.PassMethod}: before get={before[0]:g} orbit={before[1]:.3e} | after get={acc.get():g} orbit={orbit(cor):.3e}")
Output on main (AT 0.8.0); 1.518e-3 m is the orbit of a 1e-4 rad kick in this ring:
CorrectorPass: before get=0 orbit=0.000e+00 | after get=0.0001 orbit=0.000e+00
ThinMPolePass: before get=0 orbit=1.518e-03 | after get=0.0001 orbit=3.036e-03
ThinMPolePass: before get=0 orbit=0.000e+00 | after get=0.0001 orbit=1.518e-03
Possible directions
- Support it: the accessors address the dipole component through
KickAngle where the pass method uses it (write KickAngle for CorrectorPass; read polynom + KickAngle for the multipole passes and reset the KickAngle component on write).
- Refuse it: reject such elements when the simulator is loaded, with a message that says to model correctors as (thin) multipoles without a
KickAngle.
Either way, an orbit-based test would keep it from coming back.
I opened #470 with an implementation of direction 1 before reading the contribution guide — apologies for the order. I am happy to change it to direction 2, or to whatever you prefer after discussion here.
What happens
AT keeps the dipole kick of an element in two places: the polynom (
PolynomB[0]/PolynomA[0]) andKickAngle. The magnet accessors inpyaml/lattice/abstract_impl.pyread and write the polynom only, so a kick that lives inKickAngleis not seen:strength.set(1e-4)at.Corrector(CorrectorPass), any lengthgetreturns 1e-4, closed orbit stays 0at.Correctordefined withoutPolynomA/BAttributeErrorwhen the accessor is builtThinMultipolewithKickAngle=[1e-4, 0]getreturns 0getreturns 1e-4Multipole(0.2 m) withKickAngle=[1e-4, 0]getreturns 0getreturns 1e-4ThinMultipolewithoutKickAnglegetreturns 0The reason is in AT:
CorrectorPassintegratesKickAngleand ignores the polynoms; the multipole pass methods (ThinMPolePass,StrMPole*,BndMPole*,ExactMultipole*) integrate the polynom and addKickAngleto it.#435 fixed the length bookkeeping for zero-length elements; this is a separate point. The thin-corrector test added there uses
at.Corrector, so it checks thatsetandgetagree but not that the orbit moves. I saw that #467 moved the TL2 model fromCorrectorPasstoThinMPolePass, which avoids the first row for that lattice.How to reproduce
Output on
main(AT 0.8.0); 1.518e-3 m is the orbit of a 1e-4 rad kick in this ring:Possible directions
KickAnglewhere the pass method uses it (writeKickAngleforCorrectorPass; read polynom +KickAnglefor the multipole passes and reset theKickAnglecomponent on write).KickAngle.Either way, an orbit-based test would keep it from coming back.
I opened #470 with an implementation of direction 1 before reading the contribution guide — apologies for the order. I am happy to change it to direction 2, or to whatever you prefer after discussion here.