Skip to content

Commit 4ab545d

Browse files
authored
Merge pull request #40 from roman-martin/feature_arex
Add arex/arey to available alignment errors
2 parents 714ca8b + 79d9a41 commit 4ab545d

File tree

3 files changed

+229
-10
lines changed

3 files changed

+229
-10
lines changed

pysixtrack/line.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# missing access to particles._m:
1111
deg2rad = np.pi / 180.
1212

13+
1314
class Line(Element):
1415
_description = [
1516
("elements", "", "List of elements", ()),
@@ -357,22 +358,37 @@ def find_element_ids(self, element_name):
357358
"""
358359
# will raise error if element not present:
359360
idx_el = self.element_names.index(element_name)
360-
idx_after_el = idx_el + 1
361-
if self.element_names[idx_after_el] == element_name + "_aperture":
362-
idx_after_el += 1
361+
try:
362+
# if aperture marker is present
363+
idx_after_el = self.element_names.index(element_name + "_aperture") + 1
364+
except ValueError:
365+
# if aperture marker is not present
366+
idx_after_el = idx_el + 1
363367
return idx_el, idx_after_el
364368

365369
def add_offset_error_to(self, element_name, dx=0, dy=0):
366370
idx_el, idx_after_el = self.find_element_ids(element_name)
367-
if not dx and not dy:
368-
return
369371
xyshift = elements.XYShift(dx=dx, dy=dy)
370372
inv_xyshift = elements.XYShift(dx=-dx, dy=-dy)
371373
self.insert_element(idx_el, xyshift, element_name + "_offset_in")
372374
self.insert_element(
373375
idx_after_el + 1, inv_xyshift, element_name + "_offset_out"
374376
)
375377

378+
def add_aperture_offset_error_to(self, element_name, arex=0, arey=0):
379+
idx_el, idx_after_el = self.find_element_ids(element_name)
380+
idx_el_aper = idx_after_el - 1
381+
if not self.element_names[idx_el_aper] == element_name + "_aperture":
382+
# it is allowed to provide arex/arey without providing an aperture
383+
print('Info: Element', element_name, ': arex/y provided without aperture -> arex/y ignored')
384+
return
385+
xyshift = elements.XYShift(dx=arex, dy=arey)
386+
inv_xyshift = elements.XYShift(dx=-arex, dy=-arey)
387+
self.insert_element(idx_el_aper, xyshift, element_name + "_aperture_offset_in")
388+
self.insert_element(
389+
idx_after_el + 1, inv_xyshift, element_name + "_aperture_offset_out"
390+
)
391+
376392
def add_tilt_error_to(self, element_name, angle):
377393
'''Alignment error of transverse rotation around s-axis.
378394
The element corresponding to the given `element_name`
@@ -384,8 +400,6 @@ def add_tilt_error_to(self, element_name, angle):
384400
by `angle` as well.
385401
'''
386402
idx_el, idx_after_el = self.find_element_ids(element_name)
387-
if not angle:
388-
return
389403
element = self.elements[self.element_names.index(element_name)]
390404
if isinstance(element, elements.Multipole) and (
391405
element.hxl or element.hyl):
@@ -450,7 +464,7 @@ def apply_madx_errors(self, error_table):
450464
if error_type == "name":
451465
continue
452466
if any(error_table[error_type]):
453-
if error_type in ["dx", "dy", "dpsi"]:
467+
if error_type in ["dx", "dy", "dpsi", "arex", "arey"]:
454468
# available alignment error
455469
continue
456470
elif error_type[:1] == "k" and error_type[-1:] == "l":
@@ -478,14 +492,28 @@ def apply_madx_errors(self, error_table):
478492
dy = error_table["dy"][i_line]
479493
except KeyError:
480494
dy = 0
481-
self.add_offset_error_to(element_name, dx, dy)
495+
if dx or dy:
496+
self.add_offset_error_to(element_name, dx, dy)
482497

483498
# add tilt
484499
try:
485500
dpsi = error_table["dpsi"][i_line]
501+
except KeyError:
502+
dpsi = 0
503+
if dpsi:
486504
self.add_tilt_error_to(element_name, angle=dpsi / deg2rad)
505+
506+
# add aperture-only offset
507+
try:
508+
arex = error_table["arex"][i_line]
509+
except KeyError:
510+
arex = 0
511+
try:
512+
arey = error_table["arey"][i_line]
487513
except KeyError:
488-
pass
514+
arey = 0
515+
if arex or arey:
516+
self.add_aperture_offset_error_to(element_name, arex, arey)
489517

490518
# add multipole error
491519
knl = [

tests/test_line.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,15 @@ def test_line():
3535
assert len(line) == n_elements
3636

3737
line.add_offset_error_to(multipole_name, dx=0, dy=0)
38+
n_elements += 2
3839
assert len(line) == n_elements
3940

4041
line.add_offset_error_to(multipole_name, dx=0.2, dy=-0.003)
4142
n_elements += 2
4243
assert len(line) == n_elements
4344

4445
line.add_tilt_error_to(multipole_name, angle=0)
46+
n_elements += 2
4547
assert len(line) == n_elements
4648

4749
line.add_tilt_error_to(multipole_name, angle=0.1)

tests/test_madx_import.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,192 @@ def test_madx_import():
107107
)
108108
else:
109109
raise ValueError("mode not understood")
110+
111+
112+
def test_error_import():
113+
cpymad_spec = util.find_spec("cpymad")
114+
if cpymad_spec is None:
115+
print("cpymad is not available - abort test")
116+
sys.exit(0)
117+
118+
from cpymad.madx import Madx
119+
120+
madx = Madx()
121+
122+
madx.input('''
123+
MQ1: Quadrupole, K1:=KQ1, L=1.0, apertype=CIRCLE, aperture={0.04};
124+
MQ2: Quadrupole, K1:=KQ2, L=1.0, apertype=CIRCLE, aperture={0.04};
125+
MQ3: Quadrupole, K1:=0.0, L=1.0, apertype=CIRCLE, aperture={0.04};
126+
127+
KQ1 = 0.02;
128+
KQ2 = -0.02;
129+
130+
testseq: SEQUENCE, l = 20.0;
131+
MQ1, at = 5;
132+
MQ2, at = 12;
133+
MQ3, at = 18;
134+
ENDSEQUENCE;
135+
136+
!---the usual stuff
137+
BEAM, PARTICLE=PROTON, ENERGY=7000.0, EXN=2.2e-6, EYN=2.2e-6;
138+
USE, SEQUENCE=testseq;
139+
140+
141+
Select, flag=makethin, pattern="MQ1", slice=2;
142+
makethin, sequence=testseq;
143+
144+
use, sequence=testseq;
145+
146+
!---assign misalignments and field errors
147+
select, flag = error, clear;
148+
select, flag = error, pattern = "MQ1";
149+
ealign, dx = 0.01, dy = 0.01, arex = 0.02, arey = 0.02;
150+
select, flag = error, clear;
151+
select, flag = error, pattern = "MQ2";
152+
ealign, dx = 0.04, dy = 0.04, dpsi = 0.1;
153+
select, flag = error, clear;
154+
select, flag = error, pattern = "MQ3";
155+
ealign, dx = 0.00, dy = 0.00, arex = 0.00, arey = 0.00, dpsi = 0.00;
156+
efcomp, DKN = {0.0, 0.0, 0.001, 0.002}, DKS = {0.0, 0.0, 0.003, 0.004};
157+
select, flag = error, full;
158+
''')
159+
seq = madx.sequence.testseq
160+
# store already applied errors:
161+
madx.command.esave(file='lattice_errors.err')
162+
madx.command.readtable(file='lattice_errors.err', table="errors")
163+
os.remove('lattice_errors.err')
164+
errors = madx.table.errors
165+
166+
pysixtrack_line = pysixtrack.Line.from_madx_sequence(seq, install_apertures=True)
167+
pysixtrack_line.apply_madx_errors(errors)
168+
madx.input('stop;')
169+
170+
expected_element_num = (
171+
2 # start and end marker
172+
+ 6 # drifts (including drift between MQ1 slices)
173+
+ 3 + 2 # quadrupoles + MQ1 slices
174+
+ 3 + 2 # corresponding aperture elements
175+
+ 2*(3+1) # dx/y in/out for MQ1 slices and MQ2
176+
+ 2 # tilt in/out for MQ2
177+
+ 2*3 # arex/y in/out for MQ1 slices
178+
)
179+
assert len(pysixtrack_line) == expected_element_num
180+
181+
expected_element_order = [
182+
pysixtrack.elements.Drift, # start marker
183+
pysixtrack.elements.Drift,
184+
pysixtrack.elements.XYShift, # dx/y in of MQ1 1st slice
185+
pysixtrack.elements.Multipole, # MQ1 1st slice
186+
pysixtrack.elements.XYShift, # arex/y in for MQ1 1st slice
187+
pysixtrack.elements.LimitEllipse, # MQ1 1st slice aperture
188+
pysixtrack.elements.XYShift, # arex/y out for MQ1 1st slice
189+
pysixtrack.elements.XYShift, # dx/y out for MQ1 1st slice
190+
pysixtrack.elements.Drift,
191+
pysixtrack.elements.XYShift, # dx/y in for MQ1 marker
192+
pysixtrack.elements.Drift, # MQ1 marker
193+
pysixtrack.elements.XYShift, # arex/y in for MQ1 marker
194+
pysixtrack.elements.LimitEllipse, # MQ1 marker aperture
195+
pysixtrack.elements.XYShift, # arex/y out for MQ1 marker
196+
pysixtrack.elements.XYShift, # dx/y out for MQ1 marker
197+
pysixtrack.elements.Drift,
198+
pysixtrack.elements.XYShift, # dx/y in for MQ1 2nd slice
199+
pysixtrack.elements.Multipole, # MQ1 2nd slice
200+
pysixtrack.elements.XYShift, # arex/y in for MQ1 2nd slice
201+
pysixtrack.elements.LimitEllipse, # MQ1 2nd slice aperture
202+
pysixtrack.elements.XYShift, # arex/y out for MQ1 2nd slice
203+
pysixtrack.elements.XYShift, # dx/y out for MQ1 2nd slice
204+
pysixtrack.elements.Drift,
205+
pysixtrack.elements.XYShift, # dx/y in for MQ2
206+
pysixtrack.elements.SRotation, # tilt in for MQ2
207+
pysixtrack.elements.Multipole, # MQ2
208+
pysixtrack.elements.LimitEllipse, # MQ2 aperture
209+
pysixtrack.elements.SRotation, # tilt out for MQ2
210+
pysixtrack.elements.XYShift, # dx/y out for MQ2
211+
pysixtrack.elements.Drift,
212+
pysixtrack.elements.Multipole, # MQ3
213+
pysixtrack.elements.LimitEllipse, # MQ3 aperture
214+
pysixtrack.elements.Drift,
215+
pysixtrack.elements.Drift # end marker
216+
]
217+
for element, expected_element in zip(pysixtrack_line.elements,
218+
expected_element_order):
219+
assert isinstance(element, expected_element)
220+
221+
idx_MQ3 = pysixtrack_line.element_names.index('mq3')
222+
MQ3 = pysixtrack_line.elements[idx_MQ3]
223+
assert abs(MQ3.knl[2] - 0.001) < 1e-14
224+
assert abs(MQ3.knl[3] - 0.002) < 1e-14
225+
assert abs(MQ3.ksl[2] - 0.003) < 1e-14
226+
assert abs(MQ3.ksl[3] - 0.004) < 1e-14
227+
228+
229+
def test_neutral_errors():
230+
# make sure that some misaligned drifts do not influence particle
231+
cpymad_spec = util.find_spec("cpymad")
232+
if cpymad_spec is None:
233+
print("cpymad is not available - abort test")
234+
sys.exit(0)
235+
236+
from cpymad.madx import Madx
237+
238+
madx = Madx()
239+
240+
madx.input('''
241+
T1: Collimator, L=1.0, apertype=CIRCLE, aperture={0.5};
242+
T2: Collimator, L=1.0, apertype=CIRCLE, aperture={0.5};
243+
T3: Collimator, L=1.0, apertype=CIRCLE, aperture={0.5};
244+
245+
KQ1 = 0.02;
246+
KQ2 = -0.02;
247+
248+
testseq: SEQUENCE, l = 20.0;
249+
T1, at = 5;
250+
T2, at = 12;
251+
T3, at = 18;
252+
ENDSEQUENCE;
253+
254+
!---the usual stuff
255+
BEAM, PARTICLE=PROTON, ENERGY=7000.0, EXN=2.2e-6, EYN=2.2e-6;
256+
USE, SEQUENCE=testseq;
257+
258+
259+
Select, flag=makethin, pattern="MQ1", slice=2;
260+
makethin, sequence=testseq;
261+
262+
use, sequence=testseq;
263+
264+
!---misalign collimators
265+
select, flag = error, clear;
266+
select, flag = error, pattern = "T1";
267+
ealign, dx = 0.01, dy = 0.01, arex = 0.02, arey = 0.02;
268+
select, flag = error, clear;
269+
select, flag = error, pattern = "T2";
270+
ealign, dx = 0.04, dy = 0.04, dpsi = 0.1;
271+
select, flag = error, clear;
272+
select, flag = error, pattern = "T3";
273+
ealign, dx = 0.02, dy = 0.01, arex = 0.03, arey = 0.02, dpsi = 0.1;
274+
select, flag = error, full;
275+
''')
276+
seq = madx.sequence.testseq
277+
# store already applied errors:
278+
madx.command.esave(file='lattice_errors.err')
279+
madx.command.readtable(file='lattice_errors.err', table="errors")
280+
os.remove('lattice_errors.err')
281+
errors = madx.table.errors
282+
283+
pysixtrack_line = pysixtrack.Line.from_madx_sequence(seq, install_apertures=True)
284+
pysixtrack_line.apply_madx_errors(errors)
285+
madx.input('stop;')
286+
287+
initial_x = 0.025
288+
initial_y = -0.015
289+
290+
particle = pysixtrack.Particles()
291+
particle.x = initial_x
292+
particle.y = initial_y
293+
particle.state = 1
294+
295+
pysixtrack_line.track(particle)
296+
297+
assert abs(particle.x-initial_x) < 1e-14
298+
assert abs(particle.y-initial_y) < 1e-14

0 commit comments

Comments
 (0)