Skip to content

Dipole kick held in KickAngle is not seen by the magnet accessors #471

Description

@thellert

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

  1. 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).
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions