xcoll 0.3.5__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xcoll/__init__.py +12 -4
- xcoll/beam_elements/__init__.py +7 -5
- xcoll/beam_elements/absorber.py +41 -7
- xcoll/beam_elements/base.py +1161 -244
- xcoll/beam_elements/collimators_src/black_absorber.h +118 -0
- xcoll/beam_elements/collimators_src/black_crystal.h +111 -0
- xcoll/beam_elements/collimators_src/everest_block.h +40 -28
- xcoll/beam_elements/collimators_src/everest_collimator.h +129 -50
- xcoll/beam_elements/collimators_src/everest_crystal.h +217 -73
- xcoll/beam_elements/everest.py +60 -113
- xcoll/colldb.py +250 -750
- xcoll/general.py +2 -2
- xcoll/headers/checks.h +1 -1
- xcoll/headers/particle_states.h +2 -2
- xcoll/initial_distribution.py +195 -0
- xcoll/install.py +177 -0
- xcoll/interaction_record/__init__.py +1 -0
- xcoll/interaction_record/interaction_record.py +252 -0
- xcoll/interaction_record/interaction_record_src/interaction_record.h +98 -0
- xcoll/{impacts → interaction_record}/interaction_types.py +11 -4
- xcoll/line_tools.py +83 -0
- xcoll/lossmap.py +209 -0
- xcoll/manager.py +2 -937
- xcoll/rf_sweep.py +1 -1
- xcoll/scattering_routines/everest/amorphous.h +239 -0
- xcoll/scattering_routines/everest/channeling.h +245 -0
- xcoll/scattering_routines/everest/crystal_parameters.h +137 -0
- xcoll/scattering_routines/everest/everest.h +8 -30
- xcoll/scattering_routines/everest/everest.py +13 -10
- xcoll/scattering_routines/everest/jaw.h +27 -197
- xcoll/scattering_routines/everest/materials.py +2 -0
- xcoll/scattering_routines/everest/multiple_coulomb_scattering.h +31 -10
- xcoll/scattering_routines/everest/nuclear_interaction.h +86 -0
- xcoll/scattering_routines/geometry/__init__.py +6 -0
- xcoll/scattering_routines/geometry/collimator_geometry.h +219 -0
- xcoll/scattering_routines/geometry/crystal_geometry.h +150 -0
- xcoll/scattering_routines/geometry/geometry.py +26 -0
- xcoll/scattering_routines/geometry/get_s.h +92 -0
- xcoll/scattering_routines/geometry/methods.h +111 -0
- xcoll/scattering_routines/geometry/objects.h +154 -0
- xcoll/scattering_routines/geometry/rotation.h +23 -0
- xcoll/scattering_routines/geometry/segments.h +226 -0
- xcoll/scattering_routines/geometry/sort.h +184 -0
- {xcoll-0.3.5.dist-info → xcoll-0.4.0.dist-info}/METADATA +1 -1
- {xcoll-0.3.5.dist-info → xcoll-0.4.0.dist-info}/RECORD +48 -33
- xcoll/beam_elements/collimators_src/absorber.h +0 -141
- xcoll/collimator_settings.py +0 -457
- xcoll/impacts/__init__.py +0 -1
- xcoll/impacts/impacts.py +0 -102
- xcoll/impacts/impacts_src/impacts.h +0 -99
- xcoll/scattering_routines/everest/crystal.h +0 -1302
- xcoll/scattering_routines/everest/scatter.h +0 -169
- xcoll/scattering_routines/everest/scatter_crystal.h +0 -260
- {xcoll-0.3.5.dist-info → xcoll-0.4.0.dist-info}/LICENSE +0 -0
- {xcoll-0.3.5.dist-info → xcoll-0.4.0.dist-info}/NOTICE +0 -0
- {xcoll-0.3.5.dist-info → xcoll-0.4.0.dist-info}/WHEEL +0 -0
xcoll/beam_elements/base.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# copyright ############################### #
|
|
2
2
|
# This file is part of the Xcoll Package. #
|
|
3
|
-
# Copyright (c) CERN,
|
|
3
|
+
# Copyright (c) CERN, 2024. #
|
|
4
4
|
# ######################################### #
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
@@ -8,9 +8,13 @@ import numpy as np
|
|
|
8
8
|
import xobjects as xo
|
|
9
9
|
import xtrack as xt
|
|
10
10
|
|
|
11
|
-
from ..
|
|
12
|
-
from ..impacts import CollimatorImpacts
|
|
11
|
+
from ..interaction_record import InteractionRecord
|
|
13
12
|
from ..general import _pkg_root
|
|
13
|
+
from ..scattering_routines.geometry import XcollGeometry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
OPEN_JAW = 3.
|
|
17
|
+
OPEN_GAP = 999.
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
class InvalidXcoll(xt.BeamElement):
|
|
@@ -22,7 +26,7 @@ class InvalidXcoll(xt.BeamElement):
|
|
|
22
26
|
behaves_like_drift = True
|
|
23
27
|
allow_track = False
|
|
24
28
|
skip_in_loss_location_refinement = True
|
|
25
|
-
|
|
29
|
+
allow_loss_refinement = True
|
|
26
30
|
|
|
27
31
|
# InvalidXcoll catches unallowed cases, like backtracking through a collimator
|
|
28
32
|
_extra_c_sources = [
|
|
@@ -42,7 +46,10 @@ class InvalidXcoll(xt.BeamElement):
|
|
|
42
46
|
|
|
43
47
|
class BaseBlock(xt.BeamElement):
|
|
44
48
|
_xofields = {
|
|
45
|
-
'length':
|
|
49
|
+
'length': xo.Float64,
|
|
50
|
+
'active': xo.Int8,
|
|
51
|
+
'record_touches': xo.Int8,
|
|
52
|
+
'record_scatterings': xo.Int8
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
isthick = True
|
|
@@ -51,7 +58,8 @@ class BaseBlock(xt.BeamElement):
|
|
|
51
58
|
skip_in_loss_location_refinement = True
|
|
52
59
|
|
|
53
60
|
_depends_on = [InvalidXcoll]
|
|
54
|
-
|
|
61
|
+
|
|
62
|
+
_internal_record_class = InteractionRecord
|
|
55
63
|
|
|
56
64
|
# This is an abstract class and cannot be instantiated
|
|
57
65
|
def __new__(cls, *args, **kwargs):
|
|
@@ -73,42 +81,51 @@ class BaseBlock(xt.BeamElement):
|
|
|
73
81
|
_context=_context, _buffer=_buffer, _offset=_offset)
|
|
74
82
|
|
|
75
83
|
|
|
76
|
-
class BaseCollimator(
|
|
77
|
-
_xofields = {
|
|
78
|
-
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
|
|
87
|
-
'
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
'
|
|
91
|
-
|
|
92
|
-
'
|
|
93
|
-
'
|
|
94
|
-
'
|
|
95
|
-
'
|
|
96
|
-
'
|
|
84
|
+
class BaseCollimator(BaseBlock):
|
|
85
|
+
_xofields = {**BaseBlock._xofields,
|
|
86
|
+
# Collimator angle
|
|
87
|
+
'_sin_zL': xo.Float64,
|
|
88
|
+
'_cos_zL': xo.Float64,
|
|
89
|
+
'_sin_zR': xo.Float64,
|
|
90
|
+
'_cos_zR': xo.Float64,
|
|
91
|
+
'_sin_zDiff': xo.Float64, # Angle of right jaw: difference with respect to angle of left jaw
|
|
92
|
+
'_cos_zDiff': xo.Float64,
|
|
93
|
+
'_jaws_parallel': xo.Int8,
|
|
94
|
+
# Jaw corners (this is the x-coordinate in the rotated frame)
|
|
95
|
+
'_jaw_LU': xo.Float64, # left upstream
|
|
96
|
+
'_jaw_RU': xo.Float64, # right upstream
|
|
97
|
+
'_jaw_LD': xo.Float64, # left downstream
|
|
98
|
+
'_jaw_RD': xo.Float64, # right downstream
|
|
99
|
+
# Tilts (superfluous but added to speed up calculations)
|
|
100
|
+
'_sin_yL': xo.Float64,
|
|
101
|
+
'_cos_yL': xo.Float64,
|
|
102
|
+
'_tan_yL': xo.Float64,
|
|
103
|
+
'_sin_yR': xo.Float64,
|
|
104
|
+
'_cos_yR': xo.Float64,
|
|
105
|
+
'_tan_yR': xo.Float64,
|
|
106
|
+
# Other
|
|
107
|
+
'_side': xo.Int8,
|
|
108
|
+
# These are not used in C, but need to be an xofield to get them in the to_dict:
|
|
109
|
+
'_align': xo.Int8,
|
|
110
|
+
'_gap_L': xo.Float64,
|
|
111
|
+
'_gap_R': xo.Float64,
|
|
112
|
+
'_nemitt_x': xo.Float64,
|
|
113
|
+
'_nemitt_y': xo.Float64
|
|
97
114
|
}
|
|
98
115
|
|
|
99
|
-
isthick =
|
|
100
|
-
allow_track =
|
|
101
|
-
behaves_like_drift =
|
|
102
|
-
skip_in_loss_location_refinement =
|
|
116
|
+
isthick = BaseBlock.isthick
|
|
117
|
+
allow_track = BaseBlock.allow_track
|
|
118
|
+
behaves_like_drift = BaseBlock.behaves_like_drift
|
|
119
|
+
skip_in_loss_location_refinement = BaseBlock.skip_in_loss_location_refinement
|
|
120
|
+
allow_double_sided = True
|
|
121
|
+
|
|
122
|
+
_skip_in_to_dict = [f for f in _xofields if f.startswith('_')]
|
|
123
|
+
_store_in_to_dict = ['angle', 'jaw', 'tilt', 'gap', 'side', 'align', 'emittance']
|
|
124
|
+
|
|
125
|
+
_depends_on = [BaseBlock]
|
|
103
126
|
|
|
104
|
-
|
|
105
|
-
'sin_yL', 'cos_yL', 'tan_yL', 'sin_yR', 'cos_yR', 'tan_yR',
|
|
106
|
-
'sin_zL', 'cos_zL', 'sin_zR', 'cos_zR', '_side']
|
|
107
|
-
_store_in_to_dict = ['angle', 'tilt', 'jaw', 'reference_center', 'side']
|
|
108
|
-
# Extra fields (only in Python): angle_L, angle_R, tilt_L, tilt_R, jaw_LU, jaw_LD, jaw_RU, jaw_RD
|
|
127
|
+
_internal_record_class = BaseBlock._internal_record_class
|
|
109
128
|
|
|
110
|
-
_depends_on = [InvalidXcoll, xt.Drift, xt.XYShift, xt.SRotation, xt.YRotation]
|
|
111
|
-
_internal_record_class = CollimatorImpacts
|
|
112
129
|
|
|
113
130
|
# This is an abstract class and cannot be instantiated
|
|
114
131
|
def __new__(cls, *args, **kwargs):
|
|
@@ -118,200 +135,688 @@ class BaseCollimator(xt.BeamElement):
|
|
|
118
135
|
return instance
|
|
119
136
|
|
|
120
137
|
def __init__(self, **kwargs):
|
|
138
|
+
to_assign = {}
|
|
121
139
|
if '_xobject' not in kwargs:
|
|
122
|
-
# Set jaw
|
|
123
|
-
if 'jaw' in kwargs:
|
|
124
|
-
if 'jaw_L' in kwargs or 'jaw_R' in kwargs:
|
|
125
|
-
raise ValuError("Cannot use both 'jaw' and 'jaw_L/R'!")
|
|
126
|
-
_set_LR(kwargs, 'jaw', kwargs.pop('jaw'), neg=True)
|
|
127
|
-
else:
|
|
128
|
-
kwargs.setdefault('jaw_L', 1)
|
|
129
|
-
kwargs.setdefault('jaw_R', -1)
|
|
130
|
-
|
|
131
|
-
# Set reference_center
|
|
132
|
-
if 'reference_center' in kwargs:
|
|
133
|
-
if 'ref_x' in kwargs or 'ref_y' in kwargs:
|
|
134
|
-
raise ValuError("Cannot use both 'reference_center' and 'ref_x/y'!")
|
|
135
|
-
_set_LR(kwargs, 'ref', kwargs.pop('reference_center'), name_L='_x', name_R='_y')
|
|
136
|
-
else:
|
|
137
|
-
kwargs.setdefault('ref_x', 0)
|
|
138
|
-
kwargs.setdefault('ref_y', 0)
|
|
139
|
-
|
|
140
140
|
# Set side
|
|
141
|
-
|
|
141
|
+
to_assign['side'] = kwargs.pop('side', 'both')
|
|
142
142
|
|
|
143
143
|
# Set angle
|
|
144
144
|
if 'angle' in kwargs:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
for key in ['angle_L', 'angle_R']:
|
|
146
|
+
if key in kwargs:
|
|
147
|
+
raise ValueError(f"Cannot use both `angle` and `{key}`!")
|
|
148
|
+
to_assign['angle'] = kwargs.pop('angle')
|
|
149
149
|
else:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
to_assign['angle_L'] = kwargs.pop('angle_L', 0)
|
|
151
|
+
to_assign['angle_R'] = kwargs.pop('angle_R', 0)
|
|
152
|
+
|
|
153
|
+
# Set jaw
|
|
154
|
+
if 'jaw' in kwargs:
|
|
155
|
+
for key in ['jaw_L', 'jaw_R', 'jaw_LU', 'jaw_LD', 'jaw_RU', 'jaw_RD', 'gap', 'gap_L', 'gap_R']:
|
|
156
|
+
if key in kwargs:
|
|
157
|
+
raise ValueError(f"Cannot use both `jaw` and `{key}`!")
|
|
158
|
+
to_assign['jaw'] = kwargs.pop('jaw')
|
|
159
|
+
elif 'jaw_L' in kwargs or 'jaw_R' in kwargs:
|
|
160
|
+
for key in ['jaw_LU', 'jaw_LD', 'jaw_RU', 'jaw_RD', 'gap', 'gap_L', 'gap_R']:
|
|
161
|
+
if key in kwargs:
|
|
162
|
+
raise ValueError(f"Cannot use `jaw_L` or `jaw_R` together with `{key}`!")
|
|
163
|
+
to_assign['jaw_L'] = kwargs.pop('jaw_L', None)
|
|
164
|
+
to_assign['jaw_R'] = kwargs.pop('jaw_R', None)
|
|
165
|
+
elif 'jaw_LU' in kwargs or 'jaw_LD' in kwargs or 'jaw_RU' in kwargs or 'jaw_RD' in kwargs:
|
|
166
|
+
for key in ['tilt', 'tilt_L', 'tilt_R', 'gap', 'gap_L', 'gap_R']:
|
|
167
|
+
if key in kwargs:
|
|
168
|
+
raise ValueError(f"Cannot use both `jaw_LU` etc with `{key}`!")
|
|
169
|
+
to_assign['jaw_LU'] = kwargs.pop('jaw_LU', None)
|
|
170
|
+
to_assign['jaw_RU'] = kwargs.pop('jaw_RU', None)
|
|
171
|
+
to_assign['jaw_LD'] = kwargs.pop('jaw_LD', None)
|
|
172
|
+
to_assign['jaw_RD'] = kwargs.pop('jaw_RD', None)
|
|
173
|
+
kwargs.setdefault('_jaw_LU', OPEN_JAW) # Important that these are initialised, in
|
|
174
|
+
kwargs.setdefault('_jaw_RU', -OPEN_JAW) # order to keep a tilt (given together with
|
|
175
|
+
kwargs.setdefault('_jaw_LD', OPEN_JAW) # a gap) when optics are not known yet.
|
|
176
|
+
kwargs.setdefault('_jaw_RD', -OPEN_JAW)
|
|
156
177
|
|
|
157
178
|
# Set tilt
|
|
158
179
|
if 'tilt' in kwargs:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
= _angle_setter(kwargs.pop('tilt', 0), rad=True)
|
|
180
|
+
for key in ['tilt_L', 'tilt_R']:
|
|
181
|
+
if key in kwargs:
|
|
182
|
+
raise ValueError(f"Cannot use both `tilt` and `{key}`!")
|
|
183
|
+
to_assign['tilt'] = kwargs.pop('tilt')
|
|
164
184
|
else:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
kwargs['inactive_front'] = kwargs.pop('length') - kwargs['inactive_back'] - kwargs['active_length']
|
|
187
|
-
else:
|
|
188
|
-
kwargs['inactive_front'] = (kwargs['length']- kwargs['active_length'])/2
|
|
189
|
-
kwargs['inactive_back'] = (kwargs.pop('length') - kwargs['active_length'])/2
|
|
190
|
-
elif 'inactive_front' in kwargs.keys():
|
|
191
|
-
if 'inactive_back' in kwargs.keys():
|
|
192
|
-
kwargs['active_length'] = kwargs.pop('length') - kwargs['inactive_back'] - kwargs['inactive_front']
|
|
193
|
-
else:
|
|
194
|
-
kwargs['active_length'] = kwargs.pop('length') - kwargs['inactive_front']
|
|
195
|
-
elif 'inactive_back' in kwargs.keys():
|
|
196
|
-
kwargs['active_length'] = kwargs.pop('length') - kwargs['inactive_back']
|
|
197
|
-
else:
|
|
198
|
-
kwargs['active_length'] = kwargs.pop('length')
|
|
199
|
-
kwargs.setdefault('inactive_front', 0)
|
|
200
|
-
kwargs.setdefault('inactive_back', 0)
|
|
201
|
-
kwargs.setdefault('active_length', 0)
|
|
185
|
+
to_assign['tilt_L'] = kwargs.pop('tilt_L', 0)
|
|
186
|
+
to_assign['tilt_R'] = kwargs.pop('tilt_R', 0)
|
|
187
|
+
|
|
188
|
+
# Set gap
|
|
189
|
+
if 'gap' in kwargs:
|
|
190
|
+
for key in ['jaw', 'jaw_L', 'jaw_R', 'jaw_LU', 'jaw_LD', 'jaw_RU', 'jaw_RD', 'gap_L', 'gap_R']:
|
|
191
|
+
if key in kwargs:
|
|
192
|
+
raise ValueError(f"Cannot use both `gap` and `{key}`!")
|
|
193
|
+
to_assign['gap'] = kwargs.pop('gap')
|
|
194
|
+
elif 'gap_L' in kwargs or 'gap_R' in kwargs:
|
|
195
|
+
for key in ['jaw', 'jaw_L', 'jaw_R', 'jaw_LU', 'jaw_LD', 'jaw_RU', 'jaw_RD', 'gap']:
|
|
196
|
+
if key in kwargs:
|
|
197
|
+
raise ValueError(f"Cannot use both `gap` and `{key}`!")
|
|
198
|
+
to_assign['gap_L'] = kwargs.pop('gap_L', None)
|
|
199
|
+
to_assign['gap_R'] = kwargs.pop('gap_R', None)
|
|
200
|
+
kwargs.setdefault('_gap_L', OPEN_GAP)
|
|
201
|
+
kwargs.setdefault('_gap_R', -OPEN_GAP)
|
|
202
|
+
|
|
203
|
+
# Set others
|
|
204
|
+
to_assign['align'] = kwargs.pop('align', 'upstream')
|
|
205
|
+
to_assign['emittance'] = kwargs.pop('emittance', None)
|
|
202
206
|
kwargs.setdefault('active', True)
|
|
207
|
+
kwargs.setdefault('record_touches', False)
|
|
208
|
+
kwargs.setdefault('record_scatterings', False)
|
|
203
209
|
|
|
204
210
|
super().__init__(**kwargs)
|
|
211
|
+
# Careful: non-xofields are not passed correctly between copy's / to_dict. This messes with flags etc..
|
|
212
|
+
# We also have to manually initialise them for xobject generation
|
|
213
|
+
if not hasattr(self, '_optics'):
|
|
214
|
+
self._optics = None
|
|
215
|
+
for key, val in to_assign.items():
|
|
216
|
+
setattr(self, key, val)
|
|
217
|
+
self._verify_consistency()
|
|
205
218
|
|
|
206
219
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
self._tracking = True
|
|
220
|
+
# Main collimator angle
|
|
221
|
+
# =====================
|
|
210
222
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
223
|
+
@property
|
|
224
|
+
def angle(self):
|
|
225
|
+
return self.angle_L if self.angle_L==self.angle_R else [self.angle_L, self.angle_R]
|
|
226
|
+
|
|
227
|
+
@angle.setter
|
|
228
|
+
def angle(self, val):
|
|
229
|
+
if not hasattr(val, '__iter__'):
|
|
230
|
+
self.angle_L = val
|
|
231
|
+
self.angle_R = val
|
|
232
|
+
elif len(val) == 1:
|
|
233
|
+
self.angle_L = val[0]
|
|
234
|
+
self.angle_R = val[0]
|
|
235
|
+
elif len(val) == 2:
|
|
236
|
+
self.angle_L = val[0]
|
|
237
|
+
self.angle_R = val[1]
|
|
238
|
+
else:
|
|
239
|
+
raise ValueError(f"The attribute `angle` should be of the form LR or [L, R] "
|
|
240
|
+
+ f"but got {val}.")
|
|
214
241
|
|
|
215
242
|
@property
|
|
216
|
-
def
|
|
217
|
-
return
|
|
243
|
+
def angle_L(self):
|
|
244
|
+
return round(np.rad2deg(np.arctan2(self._sin_zL, self._cos_zL)), 10)
|
|
218
245
|
|
|
219
|
-
@
|
|
246
|
+
@angle_L.setter
|
|
247
|
+
def angle_L(self, angle_L):
|
|
248
|
+
self._sin_zL = np.sin(np.deg2rad(angle_L))
|
|
249
|
+
self._cos_zL = np.cos(np.deg2rad(angle_L))
|
|
250
|
+
if np.isclose(self.angle_R, angle_L):
|
|
251
|
+
self._jaws_parallel = True
|
|
252
|
+
self._sin_zDiff = 0.
|
|
253
|
+
self._cos_zDiff = 1.
|
|
254
|
+
else:
|
|
255
|
+
self._jaws_parallel = False
|
|
256
|
+
self._sin_zDiff = np.sin(np.deg2rad(self.angle_R - angle_L))
|
|
257
|
+
self._cos_zDiff = np.cos(np.deg2rad(self.angle_R - angle_L))
|
|
258
|
+
self._apply_optics()
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def angle_R(self):
|
|
262
|
+
return round(np.rad2deg(np.arctan2(self._sin_zL, self._cos_zL)), 10)
|
|
263
|
+
|
|
264
|
+
@angle_R.setter
|
|
265
|
+
def angle_R(self, angle_R):
|
|
266
|
+
self._sin_zR = np.sin(np.deg2rad(angle_R))
|
|
267
|
+
self._cos_zR = np.cos(np.deg2rad(angle_R))
|
|
268
|
+
if np.isclose(self.angle_L, angle_R):
|
|
269
|
+
self._jaws_parallel = True
|
|
270
|
+
self._sin_zDiff = 0.
|
|
271
|
+
self._cos_zDiff = 1.
|
|
272
|
+
else:
|
|
273
|
+
self._jaws_parallel = False
|
|
274
|
+
self._sin_zDiff = np.sin(np.deg2rad(angle_R - self.angle_L))
|
|
275
|
+
self._cos_zDiff = np.cos(np.deg2rad(angle_R - self.angle_L))
|
|
276
|
+
self._apply_optics()
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# Jaw attributes
|
|
280
|
+
# ==============
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def jaw(self):
|
|
284
|
+
if self.jaw_L is None and self.jaw_R is None:
|
|
285
|
+
return None
|
|
286
|
+
elif (self.side == 'left' and self.tilt_L == 0) \
|
|
287
|
+
or (self.side == 'right' and self.tilt_R == 0) \
|
|
288
|
+
or (self.tilt_L == 0 and self.tilt_R == 0):
|
|
289
|
+
return [self.jaw_L, self.jaw_R]
|
|
290
|
+
else:
|
|
291
|
+
return [[self.jaw_LU, self.jaw_RU], [self.jaw_LD, self.jaw_RD]]
|
|
292
|
+
|
|
293
|
+
@jaw.setter # Keeps the tilts unless all 4 corners are specified
|
|
220
294
|
def jaw(self, val):
|
|
221
|
-
|
|
295
|
+
if not hasattr(val, '__iter__') or len(val) == 1:
|
|
296
|
+
val = val[0] if hasattr(val, '__iter__') else val
|
|
297
|
+
if self.side == 'left':
|
|
298
|
+
self.jaw_L = val
|
|
299
|
+
elif self.side == 'right':
|
|
300
|
+
# self.jaw_R = -val if val is not None else None
|
|
301
|
+
self.jaw_R = val
|
|
302
|
+
else:
|
|
303
|
+
self.jaw_L = val
|
|
304
|
+
self.jaw_R = -val if val is not None else None
|
|
305
|
+
return
|
|
306
|
+
elif len(val) == 2:
|
|
307
|
+
if hasattr(val[0], '__iter__'):
|
|
308
|
+
if hasattr(val[1], '__iter__') and len(val[0]) == 2 and len(val[1]) == 2:
|
|
309
|
+
self.jaw_LU = val[0][0]
|
|
310
|
+
self.jaw_RU = val[0][1]
|
|
311
|
+
self.jaw_LD = val[1][0]
|
|
312
|
+
self.jaw_RD = val[1][1]
|
|
313
|
+
return
|
|
314
|
+
else:
|
|
315
|
+
self.jaw_L = val[0]
|
|
316
|
+
self.jaw_R = val[1]
|
|
317
|
+
return
|
|
318
|
+
# If we got here, val is incompatible
|
|
319
|
+
raise ValueError(f"The attribute `jaw` should be of the form [L, R] or "
|
|
320
|
+
+ f"[[LU, RU], [LD, RD], but got {val}.")
|
|
222
321
|
|
|
223
322
|
@property
|
|
224
|
-
def
|
|
225
|
-
|
|
323
|
+
def jaw_L(self):
|
|
324
|
+
jaw_L = (self._jaw_LU + self._jaw_LD) / 2
|
|
325
|
+
if not np.isclose(jaw_L, OPEN_JAW, atol=1.e-10): # open position
|
|
326
|
+
return jaw_L
|
|
327
|
+
|
|
328
|
+
@jaw_L.setter # This moves both jaw_LU and jaw_LD in parallel
|
|
329
|
+
def jaw_L(self, val):
|
|
330
|
+
if self.side == 'right' and val is not None:
|
|
331
|
+
val = None
|
|
332
|
+
print("Warning: Ignored value for jaw_L (right-sided collimator).")
|
|
333
|
+
if val is None:
|
|
334
|
+
val = OPEN_JAW
|
|
335
|
+
self._gap_L = OPEN_GAP
|
|
336
|
+
diff = val - (self._jaw_LU + self._jaw_LD) / 2
|
|
337
|
+
self._jaw_LU += diff
|
|
338
|
+
self._jaw_LD += diff
|
|
339
|
+
self._update_gaps()
|
|
226
340
|
|
|
227
|
-
@
|
|
228
|
-
def
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
341
|
+
@property
|
|
342
|
+
def jaw_R(self):
|
|
343
|
+
jaw_R = (self._jaw_RU + self._jaw_RD) / 2
|
|
344
|
+
if not np.isclose(jaw_R, -OPEN_JAW, atol=1.e-10): # open position
|
|
345
|
+
return jaw_R
|
|
346
|
+
|
|
347
|
+
@jaw_R.setter # This moves both jaw_RU and jaw_RD in parallel
|
|
348
|
+
def jaw_R(self, val):
|
|
349
|
+
if self.side == 'left' and val is not None:
|
|
350
|
+
val = None
|
|
351
|
+
print("Warning: Ignored value for jaw_R (left-sided collimator).")
|
|
352
|
+
if val is None:
|
|
353
|
+
val = -OPEN_JAW
|
|
354
|
+
self._gap_R = OPEN_GAP
|
|
355
|
+
diff = val - (self._jaw_RU + self._jaw_RD) / 2
|
|
356
|
+
self._jaw_RU += diff
|
|
357
|
+
self._jaw_RD += diff
|
|
358
|
+
self._update_gaps()
|
|
234
359
|
|
|
235
360
|
@property
|
|
236
|
-
def
|
|
237
|
-
|
|
361
|
+
def jaw_LU(self):
|
|
362
|
+
if not self.jaw_L is None:
|
|
363
|
+
return self._jaw_LU
|
|
364
|
+
|
|
365
|
+
@jaw_LU.setter # This assumes jaw_LD remains fixed, hence both jaw_L and the tilt change
|
|
366
|
+
def jaw_LU(self, val):
|
|
367
|
+
if self.side == 'right':
|
|
368
|
+
if val is not None:
|
|
369
|
+
print("Warning: Ignored value for jaw_LU (right-sided collimator).")
|
|
370
|
+
return
|
|
371
|
+
if val is None:
|
|
372
|
+
raise ValueError("Cannot set corner to None! Use open_jaws() or set jaw_L to None.")
|
|
373
|
+
self._jaw_LU = val
|
|
374
|
+
self._update_tilts() # Extra, to update tilts which are also in C for efficiency
|
|
375
|
+
self._update_gaps()
|
|
238
376
|
|
|
239
|
-
@
|
|
240
|
-
def jaw_LD(self
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
377
|
+
@property
|
|
378
|
+
def jaw_LD(self):
|
|
379
|
+
if not self.jaw_L is None:
|
|
380
|
+
return self._jaw_LD
|
|
381
|
+
|
|
382
|
+
@jaw_LD.setter # This assumes jaw_LU remains fixed, hence both jaw_L and the tilt change
|
|
383
|
+
def jaw_LD(self, val):
|
|
384
|
+
if self.side == 'right':
|
|
385
|
+
if val is not None:
|
|
386
|
+
print("Warning: Ignored value for jaw_LD (right-sided collimator).")
|
|
387
|
+
return
|
|
388
|
+
if val is None:
|
|
389
|
+
raise ValueError("Cannot set corner to None! Use open_jaws() or set jaw_L to None.")
|
|
390
|
+
self._jaw_LD = val
|
|
391
|
+
self._update_tilts() # Extra, to update tilts which are also in C for efficiency
|
|
392
|
+
self._update_gaps()
|
|
246
393
|
|
|
247
394
|
@property
|
|
248
395
|
def jaw_RU(self):
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
self.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
396
|
+
if not self.jaw_R is None:
|
|
397
|
+
return self._jaw_RU
|
|
398
|
+
|
|
399
|
+
@jaw_RU.setter # This assumes jaw_RD remains fixed, hence both jaw_R and the tilt change
|
|
400
|
+
def jaw_RU(self, val):
|
|
401
|
+
if self.side == 'left':
|
|
402
|
+
if val is not None:
|
|
403
|
+
print("Warning: Ignored value for jaw_RU (left-sided collimator).")
|
|
404
|
+
return
|
|
405
|
+
if val is None:
|
|
406
|
+
raise ValueError("Cannot set corner to None! Use open_jaws() or set jaw_R to None.")
|
|
407
|
+
self._jaw_RU = val
|
|
408
|
+
self._update_tilts() # Extra, to update tilts which are also in C for efficiency
|
|
409
|
+
self._update_gaps()
|
|
258
410
|
|
|
259
411
|
@property
|
|
260
412
|
def jaw_RD(self):
|
|
261
|
-
|
|
413
|
+
if not self.jaw_R is None:
|
|
414
|
+
return self._jaw_RD
|
|
415
|
+
|
|
416
|
+
@jaw_RD.setter # This assumes jaw_RU remains fixed, hence both jaw_R and the tilt change
|
|
417
|
+
def jaw_RD(self, val):
|
|
418
|
+
if self.side == 'left':
|
|
419
|
+
if val is not None:
|
|
420
|
+
print("Warning: Ignored value for jaw_RD (left-sided collimator).")
|
|
421
|
+
return
|
|
422
|
+
if val is None:
|
|
423
|
+
raise ValueError("Cannot set corner to None! Use open_jaws() or set jaw_R to None.")
|
|
424
|
+
self._jaw_RD = val
|
|
425
|
+
self._update_tilts() # Extra, to update tilts which are also in C for efficiency
|
|
426
|
+
self._update_gaps()
|
|
427
|
+
|
|
428
|
+
def open_jaws(self, keep_tilts=False):
|
|
429
|
+
self.jaw_L = None
|
|
430
|
+
self.jaw_R = None
|
|
431
|
+
if not keep_tilts:
|
|
432
|
+
self.tilt = 0
|
|
433
|
+
|
|
434
|
+
def _update_tilts(self):
|
|
435
|
+
if self.side != 'right':
|
|
436
|
+
self._sin_yL = (self.jaw_LD - self.jaw_LU) / self.length
|
|
437
|
+
self._cos_yL = np.sqrt(1 - self._sin_yL**2)
|
|
438
|
+
self._tan_yL = self._sin_yL / self._cos_yL
|
|
439
|
+
if self.side != 'left':
|
|
440
|
+
self._sin_yR = (self.jaw_RD - self.jaw_RU) / self.length
|
|
441
|
+
self._cos_yR = np.sqrt(1 - self._sin_yR**2)
|
|
442
|
+
self._tan_yR = self._sin_yR / self._cos_yR
|
|
443
|
+
|
|
444
|
+
def _update_gaps(self):
|
|
445
|
+
# If we had set a value for the gap manually, this needs to be updated
|
|
446
|
+
# as well after setting the jaw
|
|
447
|
+
if self._gap_L_set_manually():
|
|
448
|
+
self._gap_L = self.gap_L
|
|
449
|
+
if self._gap_R_set_manually():
|
|
450
|
+
self._gap_R = self.gap_R
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# Tilt attributes
|
|
454
|
+
# ===============
|
|
262
455
|
|
|
263
|
-
|
|
264
|
-
def jaw_RD(self, jaw_RD):
|
|
265
|
-
jaw_RU = self.jaw_RU
|
|
266
|
-
self.jaw_R = (jaw_RU+jaw_RD)/2
|
|
267
|
-
self.sin_yR = (jaw_RD-jaw_RU)/self.active_length
|
|
268
|
-
self.cos_yR = np.sqrt(1-self.sin_yR**2)
|
|
269
|
-
self.tan_yR = self.sin_yR / self.cos_yR
|
|
456
|
+
# TODO: tilts are in rad! Do we want that? It's a bit inconsistent with angle which is in deg...
|
|
270
457
|
|
|
271
458
|
@property
|
|
272
|
-
def
|
|
273
|
-
|
|
459
|
+
def tilt(self):
|
|
460
|
+
if self.side == 'left':
|
|
461
|
+
return self.tilt_L
|
|
462
|
+
elif self.side == 'right':
|
|
463
|
+
return self.tilt_R
|
|
464
|
+
else:
|
|
465
|
+
return self.tilt_L if self.tilt_L==self.tilt_R else [self.tilt_L, self.tilt_R]
|
|
274
466
|
|
|
275
|
-
@
|
|
276
|
-
def
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
467
|
+
@tilt.setter
|
|
468
|
+
def tilt(self, val):
|
|
469
|
+
if not hasattr(val, '__iter__') or len(val) == 1:
|
|
470
|
+
val = val[0] if hasattr(val, '__iter__') else val
|
|
471
|
+
if self.side == 'left':
|
|
472
|
+
self.tilt_L = val
|
|
473
|
+
elif self.side == 'right':
|
|
474
|
+
self.tilt_R = val
|
|
475
|
+
else:
|
|
476
|
+
self.tilt_L = val
|
|
477
|
+
self.tilt_R = val
|
|
478
|
+
elif len(val) == 2:
|
|
479
|
+
self.tilt_L = val[0]
|
|
480
|
+
self.tilt_R = val[1]
|
|
481
|
+
else:
|
|
482
|
+
raise ValueError
|
|
280
483
|
|
|
281
484
|
@property
|
|
282
|
-
def
|
|
283
|
-
|
|
485
|
+
def tilt_L(self):
|
|
486
|
+
if self.side != 'right':
|
|
487
|
+
return round(np.arctan2(self._sin_yL, self._cos_yL), 10)
|
|
488
|
+
|
|
489
|
+
@tilt_L.setter # This assumes jaw_L remains fixed (hence jaw_LU and jaw_LD change)
|
|
490
|
+
def tilt_L(self, val):
|
|
491
|
+
if self.side == 'right' and val != 0:
|
|
492
|
+
val = 0
|
|
493
|
+
print("Warning: Ignored value for tilt_L (right-sided collimator).")
|
|
494
|
+
if val != 0:
|
|
495
|
+
print("Warning: Setting a tilt does not preserve the hierarchy, as there "
|
|
496
|
+
+ "will always be one corner that tightens (the tilt is applied at "
|
|
497
|
+
+ "the centre of the jaw).")
|
|
498
|
+
self._sin_yL = np.sin(val)
|
|
499
|
+
self._cos_yL = np.cos(val)
|
|
500
|
+
self._tan_yL = np.tan(val)
|
|
501
|
+
jaw_L = (self._jaw_LU + self._jaw_LD) / 2
|
|
502
|
+
self._jaw_LD = jaw_L + self._sin_yL * self.length / 2.
|
|
503
|
+
self._jaw_LU = jaw_L - self._sin_yL * self.length / 2.
|
|
284
504
|
|
|
285
|
-
@
|
|
286
|
-
def
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
505
|
+
@property
|
|
506
|
+
def tilt_R(self):
|
|
507
|
+
if self.side != 'left':
|
|
508
|
+
return round(np.arctan2(self._sin_yR, self._cos_yR), 10)
|
|
509
|
+
|
|
510
|
+
@tilt_R.setter # This assumes jaw_R remains fixed (hence jaw_RU and jaw_RD change)
|
|
511
|
+
def tilt_R(self, val):
|
|
512
|
+
if self.side == 'left' and val != 0:
|
|
513
|
+
val = 0
|
|
514
|
+
print("Warning: Ignored value for tilt_R (left-sided collimator).")
|
|
515
|
+
if val != 0:
|
|
516
|
+
print("Warning: Setting a tilt does not preserve the hierarchy, as there "
|
|
517
|
+
+ "will always be one corner that tightens (the tilt is applied at "
|
|
518
|
+
+ "the centre of the jaw).")
|
|
519
|
+
self._sin_yR = np.sin(val)
|
|
520
|
+
self._cos_yR = np.cos(val)
|
|
521
|
+
self._tan_yR = np.tan(val)
|
|
522
|
+
jaw_R = (self._jaw_RU + self._jaw_RD) / 2
|
|
523
|
+
self._jaw_RD = jaw_R + self._sin_yR * self.length / 2.
|
|
524
|
+
self._jaw_RU = jaw_R - self._sin_yR * self.length / 2.
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
# Optics
|
|
528
|
+
# ======
|
|
290
529
|
|
|
291
530
|
@property
|
|
292
|
-
def
|
|
293
|
-
return self.
|
|
531
|
+
def optics(self):
|
|
532
|
+
return self._optics
|
|
533
|
+
|
|
534
|
+
def optics_ready(self):
|
|
535
|
+
return self.emittance is not None and self.optics is not None
|
|
536
|
+
|
|
537
|
+
def assign_optics(self, *, nemitt_x=None, nemitt_y=None, beta_gamma_rel=None, name=None, twiss=None,
|
|
538
|
+
twiss_upstream=None, twiss_downstream=None):
|
|
539
|
+
from xcoll import element_classes
|
|
540
|
+
if not isinstance(self, element_classes):
|
|
541
|
+
raise ValueError("Please install collimator before assigning optics.")
|
|
542
|
+
if nemitt_x is None:
|
|
543
|
+
if self.nemitt_x is None:
|
|
544
|
+
raise ValueError("Need to provide `nemitt_x`.")
|
|
545
|
+
else:
|
|
546
|
+
self.nemitt_x = nemitt_x
|
|
547
|
+
if nemitt_y is None:
|
|
548
|
+
if self.nemitt_y is None:
|
|
549
|
+
raise ValueError("Need to provide `nemitt_y`.")
|
|
550
|
+
else:
|
|
551
|
+
self.nemitt_y = nemitt_y
|
|
552
|
+
if beta_gamma_rel is None:
|
|
553
|
+
raise ValueError("Need to provide `beta_gamma_rel`.")
|
|
554
|
+
if twiss is None:
|
|
555
|
+
if twiss_upstream is None or twiss_downstream is None:
|
|
556
|
+
raise ValueError("Use either `twiss` or `twiss_upstream` and `twiss_downstream`.")
|
|
557
|
+
if name is None:
|
|
558
|
+
if len(twiss_upstream.name) > 1 or len(twiss_downstream.name) > 1:
|
|
559
|
+
raise ValueError("Need to provide `name` or twisses that are a single row each.")
|
|
560
|
+
tw_up = twiss_upstream
|
|
561
|
+
tw_down = twiss_downstream
|
|
562
|
+
else:
|
|
563
|
+
tw_up = twiss_upstream.rows[name]
|
|
564
|
+
tw_down = twiss_downstream.rows[name]
|
|
565
|
+
elif twiss_downstream is not None or twiss_downstream is not None:
|
|
566
|
+
raise ValueError("Use either `twiss` or `twiss_upstream` and `twiss_downstream`.")
|
|
567
|
+
elif name is None:
|
|
568
|
+
raise ValueError("When using `twiss`, need to provide the name as well.")
|
|
569
|
+
else:
|
|
570
|
+
tw_up = twiss.rows[name]
|
|
571
|
+
tw_down = twiss.rows[twiss.mask[[name]]+1]
|
|
572
|
+
if not np.isclose(tw_up.s[0] + self.length, tw_down.s[0]):
|
|
573
|
+
raise ValueError(f"Downstream twiss not compatible with length {self.length}m.")
|
|
574
|
+
self._optics = {
|
|
575
|
+
'upstream': tw_up,
|
|
576
|
+
'downstream': tw_down,
|
|
577
|
+
'beta_gamma_rel': beta_gamma_rel
|
|
578
|
+
}
|
|
579
|
+
self._apply_optics()
|
|
294
580
|
|
|
295
|
-
@
|
|
296
|
-
def
|
|
297
|
-
|
|
581
|
+
@property
|
|
582
|
+
def nemitt_x(self):
|
|
583
|
+
if self._nemitt_x == 0:
|
|
584
|
+
return None
|
|
585
|
+
return self._nemitt_x
|
|
586
|
+
|
|
587
|
+
@nemitt_x.setter
|
|
588
|
+
def nemitt_x(self, val):
|
|
589
|
+
if val is None:
|
|
590
|
+
self._nemitt_x = 0
|
|
591
|
+
if val <= 0:
|
|
592
|
+
raise ValueError(f"The field `nemitt_x` should be positive, but got {val}.")
|
|
593
|
+
self._nemitt_x = val
|
|
594
|
+
self._apply_optics()
|
|
298
595
|
|
|
299
596
|
@property
|
|
300
|
-
def
|
|
301
|
-
|
|
597
|
+
def nemitt_y(self):
|
|
598
|
+
if self._nemitt_y == 0:
|
|
599
|
+
return None
|
|
600
|
+
return self._nemitt_y
|
|
601
|
+
|
|
602
|
+
@nemitt_y.setter
|
|
603
|
+
def nemitt_y(self, val):
|
|
604
|
+
if val is None:
|
|
605
|
+
self._nemitt_y = 0
|
|
606
|
+
else:
|
|
607
|
+
if val <= 0:
|
|
608
|
+
raise ValueError(f"The field `nemitt_y` should be positive, but got {val}.")
|
|
609
|
+
self._nemitt_y = val
|
|
610
|
+
self._apply_optics()
|
|
302
611
|
|
|
303
|
-
@
|
|
304
|
-
def
|
|
305
|
-
|
|
612
|
+
@property
|
|
613
|
+
def emittance(self):
|
|
614
|
+
if self.nemitt_x is not None and self.nemitt_y is not None:
|
|
615
|
+
if np.isclose(self.nemitt_x, self.nemitt_y):
|
|
616
|
+
return self.nemitt_x
|
|
617
|
+
else:
|
|
618
|
+
return [self.nemitt_x, self.nemitt_y]
|
|
619
|
+
|
|
620
|
+
@emittance.setter
|
|
621
|
+
def emittance(self, val):
|
|
622
|
+
if val is None:
|
|
623
|
+
self._nemitt_x = 0
|
|
624
|
+
self._nemitt_y = 0
|
|
625
|
+
else:
|
|
626
|
+
if not hasattr(val, '__iter__'):
|
|
627
|
+
val = [val]
|
|
628
|
+
if len(val) == 1:
|
|
629
|
+
val = [val[0], val[0]]
|
|
630
|
+
assert len(val) == 2
|
|
631
|
+
if val[0] <= 0 or val[1] <= 0:
|
|
632
|
+
raise ValueError(f"The field `emittance` should be positive, but got {val}.")
|
|
633
|
+
self._nemitt_x = val[0]
|
|
634
|
+
self._nemitt_y = val[1]
|
|
635
|
+
self._apply_optics()
|
|
306
636
|
|
|
307
637
|
@property
|
|
308
|
-
def
|
|
309
|
-
|
|
638
|
+
def sigma(self):
|
|
639
|
+
if self.optics_ready():
|
|
640
|
+
betx = self.optics[self.align]['betx'][0]
|
|
641
|
+
bety = self.optics[self.align]['bety'][0]
|
|
642
|
+
sigma_x = np.sqrt(betx*self.nemitt_x/self.optics['beta_gamma_rel'])
|
|
643
|
+
sigma_y = np.sqrt(bety*self.nemitt_y/self.optics['beta_gamma_rel'])
|
|
644
|
+
if hasattr(self, '_cos_zL'):
|
|
645
|
+
sigma_L = np.sqrt((sigma_x*self._cos_zL)**2 + (sigma_y*self._sin_zL)**2)
|
|
646
|
+
sigma_R = np.sqrt((sigma_x*self._cos_zR)**2 + (sigma_y*self._sin_zR)**2)
|
|
647
|
+
return [sigma_L, sigma_R], [sigma_x, sigma_y]
|
|
648
|
+
else:
|
|
649
|
+
sigma = np.sqrt((sigma_x*self._cos_z)**2 + (sigma_y*self._sin_z)**2)
|
|
650
|
+
return sigma, [sigma_x, sigma_y]
|
|
310
651
|
|
|
311
|
-
@
|
|
312
|
-
def
|
|
313
|
-
|
|
314
|
-
|
|
652
|
+
@property
|
|
653
|
+
def co(self):
|
|
654
|
+
if self.optics_ready():
|
|
655
|
+
x = self.optics[self.align]['x'][0]
|
|
656
|
+
y = self.optics[self.align]['y'][0]
|
|
657
|
+
if hasattr(self, '_cos_zL'):
|
|
658
|
+
co_L = x*self._cos_zL + y*self._sin_zL
|
|
659
|
+
co_R = x*self._cos_zR + y*self._sin_zR
|
|
660
|
+
return [co_L, co_R], [x, y]
|
|
661
|
+
else:
|
|
662
|
+
co = x*self._cos_z + y*self._sin_z
|
|
663
|
+
return co, [x, y]
|
|
664
|
+
|
|
665
|
+
@property
|
|
666
|
+
def divergence(self):
|
|
667
|
+
if self.optics_ready():
|
|
668
|
+
alfx = self.optics[self.align]['alfx'][0]
|
|
669
|
+
alfy = self.optics[self.align]['alfy'][0]
|
|
670
|
+
betx = self.optics[self.align]['betx'][0]
|
|
671
|
+
bety = self.optics[self.align]['bety'][0]
|
|
672
|
+
divx = -np.sqrt(self.nemitt_x/self.optics['beta_gamma_rel']/betx)*alfx
|
|
673
|
+
divy = -np.sqrt(self.nemitt_y/self.optics['beta_gamma_rel']/bety)*alfy
|
|
674
|
+
if hasattr(self, '_cos_zL'):
|
|
675
|
+
if self.side != 'right':
|
|
676
|
+
return divx if abs(self.angle_L) < 1e-6 else divy
|
|
677
|
+
else:
|
|
678
|
+
return divx if abs(self.angle_R) < 1e-6 else divy
|
|
679
|
+
else:
|
|
680
|
+
return divx if abs(self.angle) < 1e-6 else divy
|
|
681
|
+
|
|
682
|
+
@property
|
|
683
|
+
def align(self):
|
|
684
|
+
if self._align == 0:
|
|
685
|
+
return 'upstream'
|
|
686
|
+
elif self._align == 1:
|
|
687
|
+
return 'downstream'
|
|
688
|
+
else:
|
|
689
|
+
raise ValueError(f"The attribute `align` can only be 'upstream' or "
|
|
690
|
+
+f"'downstream', but stored as {self._align}.")
|
|
691
|
+
|
|
692
|
+
@align.setter
|
|
693
|
+
def align(self, val):
|
|
694
|
+
if val == 'upstream':
|
|
695
|
+
self._align = 0
|
|
696
|
+
elif val == 'downstream':
|
|
697
|
+
self._align = 1
|
|
698
|
+
else:
|
|
699
|
+
raise ValueError(f"The attribute `align` can only be 'upstream' or "
|
|
700
|
+
+f"'downstream', but got {val}.")
|
|
701
|
+
self._apply_optics()
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
# Gap attributes
|
|
705
|
+
# ==============
|
|
706
|
+
|
|
707
|
+
@property
|
|
708
|
+
def gap(self):
|
|
709
|
+
if self.gap_L is None and self.gap_R is None:
|
|
710
|
+
return None
|
|
711
|
+
elif self.gap_R is not None and self.gap_L == -self.gap_R:
|
|
712
|
+
return self.gap_L
|
|
713
|
+
else:
|
|
714
|
+
return [self.gap_L, self.gap_R]
|
|
715
|
+
|
|
716
|
+
@gap.setter
|
|
717
|
+
def gap(self, val):
|
|
718
|
+
if not hasattr(val, '__iter__') or len(val) == 1:
|
|
719
|
+
val = val[0] if hasattr(val, '__iter__') else val
|
|
720
|
+
if self.side == 'left':
|
|
721
|
+
self.gap_L = val
|
|
722
|
+
elif self.side == 'right':
|
|
723
|
+
# self.gap_R = -val if val is not None else None
|
|
724
|
+
self.gap_R = val
|
|
725
|
+
else:
|
|
726
|
+
self.gap_L = val
|
|
727
|
+
self.gap_R = -val if val is not None else None
|
|
728
|
+
return
|
|
729
|
+
elif len(val) == 2:
|
|
730
|
+
if not hasattr(val[0], '__iter__') \
|
|
731
|
+
and not hasattr(val[1], '__iter__'):
|
|
732
|
+
if val[0] is not None and val[1] is not None:
|
|
733
|
+
if val[0] <= val[1]:
|
|
734
|
+
raise ValueError(f"The attribute `gap_L` should be larger "
|
|
735
|
+
+ f"than `gap_R` but got {val}.")
|
|
736
|
+
self.gap_L = val[0]
|
|
737
|
+
self.gap_R = val[1]
|
|
738
|
+
return
|
|
739
|
+
# If we got here, val is incompatible
|
|
740
|
+
raise ValueError(f"The attribute `gap` should be of the form `gap` or "
|
|
741
|
+
+ f"`[gap_L, gap_R]`, but got {val}.")
|
|
742
|
+
|
|
743
|
+
@property
|
|
744
|
+
def gap_L(self):
|
|
745
|
+
if self.side != 'right':
|
|
746
|
+
if self.optics_ready() and self.jaw_L is not None:
|
|
747
|
+
return round((self.jaw_L - self.co[0][0])/self.sigma[0][0], 6)
|
|
748
|
+
elif not self._gap_L_set_manually():
|
|
749
|
+
return None
|
|
750
|
+
else:
|
|
751
|
+
return self._gap_L
|
|
752
|
+
|
|
753
|
+
@gap_L.setter
|
|
754
|
+
def gap_L(self, val):
|
|
755
|
+
if val is None:
|
|
756
|
+
val = OPEN_GAP
|
|
757
|
+
self.jaw_L = None
|
|
758
|
+
if val <= 0:
|
|
759
|
+
raise ValueError(f"The field `gap_L` should be positive, but got {val}.")
|
|
760
|
+
self._gap_L = val
|
|
761
|
+
self._apply_optics()
|
|
762
|
+
|
|
763
|
+
@property
|
|
764
|
+
def gap_R(self):
|
|
765
|
+
if self.side != 'left':
|
|
766
|
+
if self.optics_ready() and self.jaw_R is not None:
|
|
767
|
+
return round((self.jaw_R - self.co[0][1])/self.sigma[0][1], 6)
|
|
768
|
+
elif not self._gap_R_set_manually():
|
|
769
|
+
return None
|
|
770
|
+
else:
|
|
771
|
+
return self._gap_R
|
|
772
|
+
|
|
773
|
+
@gap_R.setter
|
|
774
|
+
def gap_R(self, val):
|
|
775
|
+
if val is None:
|
|
776
|
+
val = -OPEN_GAP
|
|
777
|
+
self.jaw_R = None
|
|
778
|
+
if val >= 0:
|
|
779
|
+
raise ValueError(f"The field `gap_R` should be negative, but got {val}.")
|
|
780
|
+
self._gap_R = val
|
|
781
|
+
self._apply_optics()
|
|
782
|
+
|
|
783
|
+
@property
|
|
784
|
+
def gap_LU(self):
|
|
785
|
+
if self.gap_L is not None and self.optics_ready():
|
|
786
|
+
return round(self._gap_L - self._sin_yL * self.length / 2. / self.sigma[0][0], 6)
|
|
787
|
+
|
|
788
|
+
@property
|
|
789
|
+
def gap_LD(self):
|
|
790
|
+
if self.gap_L is not None and self.optics_ready():
|
|
791
|
+
return round(self._gap_L + self._sin_yL * self.length / 2. / self.sigma[0][0], 6)
|
|
792
|
+
|
|
793
|
+
@property
|
|
794
|
+
def gap_RU(self):
|
|
795
|
+
if self.gap_R is not None and self.optics_ready():
|
|
796
|
+
return round(self._gap_R - self._sin_yR * self.length / 2. / self.sigma[0][1], 6)
|
|
797
|
+
|
|
798
|
+
@property
|
|
799
|
+
def gap_RD(self):
|
|
800
|
+
if self.gap_R is not None and self.optics_ready():
|
|
801
|
+
return round(self._gap_R + self._sin_yR * self.length / 2. / self.sigma[0][1], 6)
|
|
802
|
+
|
|
803
|
+
def _gap_L_set_manually(self):
|
|
804
|
+
return not np.isclose(self._gap_L, OPEN_GAP)
|
|
805
|
+
|
|
806
|
+
def _gap_R_set_manually(self):
|
|
807
|
+
return not np.isclose(self._gap_R, -OPEN_GAP)
|
|
808
|
+
|
|
809
|
+
def _apply_optics(self):
|
|
810
|
+
if self.optics_ready():
|
|
811
|
+
# Only if we have set a value for the gap manually, this needs to be updated
|
|
812
|
+
if self._gap_L_set_manually():
|
|
813
|
+
self.jaw_L = self._gap_L * self.sigma[0][0] + self.co[0][0]
|
|
814
|
+
if self._gap_R_set_manually():
|
|
815
|
+
self.jaw_R = self._gap_R * self.sigma[0][1] + self.co[0][1]
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
# Other attributes
|
|
819
|
+
# ================
|
|
315
820
|
|
|
316
821
|
@property
|
|
317
822
|
def side(self):
|
|
@@ -319,55 +824,106 @@ class BaseCollimator(xt.BeamElement):
|
|
|
319
824
|
return 'both'
|
|
320
825
|
elif self._side == 1:
|
|
321
826
|
return 'left'
|
|
322
|
-
elif self._side ==
|
|
827
|
+
elif self._side == -1:
|
|
323
828
|
return 'right'
|
|
324
829
|
|
|
325
830
|
@side.setter
|
|
326
|
-
def side(self,
|
|
327
|
-
|
|
831
|
+
def side(self, val):
|
|
832
|
+
if isinstance(val, str):
|
|
833
|
+
if val.lower() == 'both' or val == '+-' or val == '-+':
|
|
834
|
+
self._side = 0
|
|
835
|
+
return
|
|
836
|
+
elif val.lower() == 'left' or val.lower() == 'l' or val == '+':
|
|
837
|
+
self._side = 1
|
|
838
|
+
self.gap_R = None
|
|
839
|
+
return
|
|
840
|
+
elif val.lower() == 'right' or val.lower() == 'r' or val == '-':
|
|
841
|
+
self._side = -1
|
|
842
|
+
self.gap_L = None
|
|
843
|
+
return
|
|
844
|
+
raise ValueError(f"Unkown setting {val} for 'side'! Choose from "
|
|
845
|
+
+ f"('left', 'L', '+'), ('right', 'R', '-'), or ('both', '+-').")
|
|
328
846
|
|
|
329
|
-
# TODO: tilts are in rad! Do we want that? It's a bit inconsistent with angle which is in deg...
|
|
330
|
-
# ==============================================================================================
|
|
331
847
|
@property
|
|
332
|
-
def
|
|
333
|
-
|
|
334
|
-
return round(np.arctan2(self.sin_yL, self.cos_yL), 10)
|
|
848
|
+
def active_length(self):
|
|
849
|
+
raise ValueError("`active_length`is deprecated. Please use `length`.")
|
|
335
850
|
|
|
336
|
-
@
|
|
337
|
-
def
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
self.sin_yL = np.sin(anglerad_L)
|
|
341
|
-
self.cos_yL = np.cos(anglerad_L)
|
|
342
|
-
self.tan_yL = np.tan(anglerad_L)
|
|
851
|
+
@property
|
|
852
|
+
def inactive_front(self):
|
|
853
|
+
raise ValueError("`inactive_front`is deprecated. Collimators now only "
|
|
854
|
+
+ "contain their active length (implemented as `length`).")
|
|
343
855
|
|
|
344
856
|
@property
|
|
345
|
-
def
|
|
346
|
-
|
|
347
|
-
|
|
857
|
+
def inactive_back(self):
|
|
858
|
+
raise ValueError("`inactive_back`is deprecated. Collimators now only "
|
|
859
|
+
+ "contain their active length (implemented as `length`).")
|
|
348
860
|
|
|
349
|
-
@tilt_R.setter
|
|
350
|
-
def tilt_R(self, tilt_R):
|
|
351
|
-
# anglerad_R = tilt_R / 180. * np.pi
|
|
352
|
-
anglerad_R = tilt_R
|
|
353
|
-
self.sin_yR = np.sin(anglerad_R)
|
|
354
|
-
self.cos_yR = np.cos(anglerad_R)
|
|
355
|
-
self.tan_yR = np.tan(anglerad_R)
|
|
356
861
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return self.tilt_L if self.tilt_L==self.tilt_R else [self.tilt_L, self.tilt_R]
|
|
862
|
+
# Methods
|
|
863
|
+
# =======
|
|
360
864
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
865
|
+
def enable_scattering(self):
|
|
866
|
+
if hasattr(self, '_tracking'):
|
|
867
|
+
if self.optics is None:
|
|
868
|
+
raise ValueError("Optics not assigned! Cannot enable scattering.")
|
|
869
|
+
self._tracking = True
|
|
870
|
+
|
|
871
|
+
def disable_scattering(self):
|
|
872
|
+
if hasattr(self, '_tracking'):
|
|
873
|
+
self._tracking = False
|
|
874
|
+
|
|
875
|
+
def _verify_consistency(self):
|
|
876
|
+
# Verify angles
|
|
877
|
+
if abs(self.angle_L - self.angle_R) >= 90.:
|
|
878
|
+
raise ValueError("Angles of both jaws differ more than 90 degrees!")
|
|
879
|
+
ang = abs(np.arccos(self._cos_zL))
|
|
880
|
+
ang = np.pi - ang if ang > np.pi/2 else ang
|
|
881
|
+
assert np.isclose(ang, abs(np.arcsin(self._sin_zL)))
|
|
882
|
+
ang = abs(np.arccos(self._cos_zR))
|
|
883
|
+
ang = np.pi - ang if ang > np.pi/2 else ang
|
|
884
|
+
assert np.isclose(ang, abs(np.arcsin(self._sin_zR)))
|
|
885
|
+
if np.isclose(self.angle_L, self.angle_R):
|
|
886
|
+
assert self._jaws_parallel == True
|
|
887
|
+
assert np.isclose(self._sin_zL, self._sin_zR)
|
|
888
|
+
assert np.isclose(self._cos_zL, self._cos_zR)
|
|
889
|
+
assert np.isclose(self._sin_zDiff, 0.)
|
|
890
|
+
assert np.isclose(self._cos_zDiff, 1.)
|
|
891
|
+
else:
|
|
892
|
+
assert self._jaws_parallel == False
|
|
893
|
+
assert np.isclose(self._sin_zDiff, self._cos_zL*self._sin_zR - self._sin_zL*self._cos_zR)
|
|
894
|
+
assert np.isclose(self._cos_zDiff, self._cos_zL*self._cos_zR + self._sin_zL*self._sin_zR)
|
|
895
|
+
if self.side == 'both' and abs(self.tilt_L - self.tilt_R) >= 90.:
|
|
896
|
+
raise ValueError("Tilts of both jaws differ more than 90 degrees!")
|
|
897
|
+
if self.side != 'right':
|
|
898
|
+
ang = abs(np.arccos(self._cos_yL))
|
|
899
|
+
ang = np.pi - ang if ang > np.pi/2 else ang
|
|
900
|
+
assert np.isclose(ang, abs(np.arcsin(self._sin_yL)))
|
|
901
|
+
assert np.isclose(self._sin_yL/self._cos_yL, self._tan_yL)
|
|
902
|
+
if self.side != 'left':
|
|
903
|
+
ang = abs(np.arccos(self._cos_yR))
|
|
904
|
+
ang = np.pi - ang if ang > np.pi/2 else ang
|
|
905
|
+
assert np.isclose(ang, abs(np.arcsin(self._sin_yR)))
|
|
906
|
+
assert np.isclose(self._sin_yR/self._cos_yR, self._tan_yR)
|
|
907
|
+
|
|
908
|
+
# Verify bools
|
|
909
|
+
assert self._side in [-1, 1, 0]
|
|
910
|
+
assert isinstance(self._jaws_parallel, bool) or self._jaws_parallel in [0, 1]
|
|
911
|
+
assert isinstance(self.active, bool) or self.active in [0, 1]
|
|
912
|
+
assert isinstance(self.record_touches, bool) or self.record_touches in [0, 1]
|
|
913
|
+
assert isinstance(self.record_scatterings, bool) or self.record_scatterings in [0, 1]
|
|
364
914
|
|
|
365
915
|
def jaw_func(self, pos):
|
|
366
916
|
positions = ['LU', 'RU', 'LD', 'RD']
|
|
917
|
+
if pos[0] == 'L':
|
|
918
|
+
other_pos = 'R'
|
|
919
|
+
else:
|
|
920
|
+
other_pos = 'L'
|
|
921
|
+
point_x = ((getattr(self, 'jaw_' + pos[0]) * getattr(self, 'cos_z' + pos[0])
|
|
922
|
+
+ getattr(self, 'jaw_' + other_pos) * getattr(self, 'cos_z' + other_pos))/2)
|
|
923
|
+
point_y = ((getattr(self, 'jaw_' + pos[0]) * getattr(self, 'sin_z' + pos[0])
|
|
924
|
+
+ getattr(self, 'jaw_' + other_pos) * getattr(self, 'sin_z' + other_pos))/2)
|
|
367
925
|
if not pos in positions:
|
|
368
926
|
raise ValueError(f"Parameter {pos} needs to be one of {positions}!")
|
|
369
|
-
point_x = getattr(self, 'ref_x')
|
|
370
|
-
point_y = getattr(self, 'ref_y')
|
|
371
927
|
sinz = getattr(self, 'sin_z' + pos[0])
|
|
372
928
|
cosz = getattr(self, 'cos_z' + pos[0])
|
|
373
929
|
# Shift to the jaw, whose location is given as the shortest distance:
|
|
@@ -376,32 +932,393 @@ class BaseCollimator(xt.BeamElement):
|
|
|
376
932
|
return lambda t: (point_x - t*sinz, point_y + t*cosz)
|
|
377
933
|
|
|
378
934
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
935
|
+
class BaseCrystal(BaseBlock):
|
|
936
|
+
_xofields = {**BaseBlock._xofields,
|
|
937
|
+
# Collimator angle
|
|
938
|
+
'_sin_z': xo.Float64,
|
|
939
|
+
'_cos_z': xo.Float64,
|
|
940
|
+
# Jaw corners (this is the x-coordinate in the rotated frame)
|
|
941
|
+
'_jaw_U': xo.Float64,
|
|
942
|
+
# Tilts (not superfluous)
|
|
943
|
+
'_sin_y': xo.Float64,
|
|
944
|
+
'_cos_y': xo.Float64,
|
|
945
|
+
'_tan_y': xo.Float64,
|
|
946
|
+
# Other
|
|
947
|
+
'_side': xo.Int8,
|
|
948
|
+
# These are not used in C, but need to be an xofield to get them in the to_dict:
|
|
949
|
+
'_align': xo.Int8,
|
|
950
|
+
'_gap': xo.Float64,
|
|
951
|
+
'_nemitt_x': xo.Float64,
|
|
952
|
+
'_nemitt_y': xo.Float64,
|
|
953
|
+
# Crystal specific
|
|
954
|
+
'_bending_radius': xo.Float64,
|
|
955
|
+
'_bending_angle': xo.Float64,
|
|
956
|
+
'width': xo.Float64,
|
|
957
|
+
'height': xo.Float64
|
|
958
|
+
# 'thick': xo.Float64
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
isthick = BaseBlock.isthick
|
|
962
|
+
allow_track = BaseBlock.allow_track
|
|
963
|
+
behaves_like_drift = BaseBlock.behaves_like_drift
|
|
964
|
+
skip_in_loss_location_refinement = BaseBlock.skip_in_loss_location_refinement
|
|
965
|
+
allow_double_sided = False
|
|
966
|
+
|
|
967
|
+
_skip_in_to_dict = [f for f in _xofields if f.startswith('_')]
|
|
968
|
+
_store_in_to_dict = ['angle', 'jaw', 'tilt', 'gap', 'side', 'align', 'emittance',
|
|
969
|
+
'bending_radius', 'bending_angle']
|
|
970
|
+
|
|
971
|
+
_depends_on = [BaseCollimator]
|
|
972
|
+
|
|
973
|
+
_internal_record_class = BaseBlock._internal_record_class
|
|
974
|
+
|
|
975
|
+
# This is an abstract class and cannot be instantiated
|
|
976
|
+
def __new__(cls, *args, **kwargs):
|
|
977
|
+
if cls == BaseCrystal:
|
|
978
|
+
raise Exception("Abstract class `BaseCrystal` cannot be instantiated!")
|
|
979
|
+
instance = super().__new__(cls)
|
|
980
|
+
return instance
|
|
981
|
+
|
|
982
|
+
def __init__(self, **kwargs):
|
|
983
|
+
to_assign = {}
|
|
984
|
+
if '_xobject' not in kwargs:
|
|
985
|
+
# Set side
|
|
986
|
+
to_assign['side'] = kwargs.pop('side', 'left')
|
|
987
|
+
|
|
988
|
+
# Set angle
|
|
989
|
+
to_assign['angle'] = kwargs.pop('angle', 0)
|
|
990
|
+
|
|
991
|
+
# Set jaw
|
|
992
|
+
if 'jaw' in kwargs:
|
|
993
|
+
for key in ['jaw_U', 'jaw_D', 'gap']:
|
|
994
|
+
if key in kwargs:
|
|
995
|
+
raise ValueError(f"Cannot use both `jaw` and `{key}`!")
|
|
996
|
+
to_assign['jaw'] = kwargs.pop('jaw')
|
|
997
|
+
elif 'jaw_D' in kwargs:
|
|
998
|
+
for key in ['tilt', 'gap']:
|
|
999
|
+
if key in kwargs:
|
|
1000
|
+
raise ValueError(f"Cannot use both `jaw_D` with `{key}`!")
|
|
1001
|
+
if not 'jaw_U' in kwargs:
|
|
1002
|
+
raise ValueError("Need to provide `jaw_U` when setting `jaw_D`!")
|
|
1003
|
+
to_assign['jaw_U'] = kwargs.pop('jaw_U')
|
|
1004
|
+
to_assign['jaw_D'] = kwargs.pop('jaw_D')
|
|
1005
|
+
elif 'jaw_U' in kwargs:
|
|
1006
|
+
if 'gap' in kwargs:
|
|
1007
|
+
raise ValueError(f"Cannot use both `jaw_U` and `gap`!")
|
|
1008
|
+
to_assign['jaw_U'] = kwargs.pop('jaw_U')
|
|
1009
|
+
# TODO: correct sign if right-sided
|
|
1010
|
+
kwargs.setdefault('_jaw_U', OPEN_JAW)
|
|
1011
|
+
|
|
1012
|
+
# Set gap
|
|
1013
|
+
if 'gap' in kwargs:
|
|
1014
|
+
for key in ['jaw', 'jaw_U', 'jaw_D']:
|
|
1015
|
+
if key in kwargs:
|
|
1016
|
+
raise ValueError(f"Cannot use both `gap` and `{key}`!")
|
|
1017
|
+
to_assign['gap'] = kwargs.pop('gap')
|
|
1018
|
+
# TODO: correct sign if right-sided
|
|
1019
|
+
kwargs.setdefault('_gap', OPEN_GAP)
|
|
1020
|
+
|
|
1021
|
+
# Set tilt
|
|
1022
|
+
if 'jaw_D' not in kwargs:
|
|
1023
|
+
to_assign['tilt'] = kwargs.pop('tilt', 0)
|
|
1024
|
+
|
|
1025
|
+
# Set others
|
|
1026
|
+
to_assign['align'] = kwargs.pop('align', 'upstream')
|
|
1027
|
+
to_assign['emittance'] = kwargs.pop('emittance', None)
|
|
1028
|
+
kwargs.setdefault('active', True)
|
|
1029
|
+
kwargs.setdefault('record_touches', False)
|
|
1030
|
+
kwargs.setdefault('record_scatterings', False)
|
|
1031
|
+
|
|
1032
|
+
# Set crystal specific
|
|
1033
|
+
if 'bending_angle' in kwargs:
|
|
1034
|
+
if 'bending_radius' in kwargs:
|
|
1035
|
+
raise ValueError("Need to choose between 'bending_radius' and 'bending_angle'!")
|
|
1036
|
+
to_assign['bending_angle'] = kwargs.pop('bending_angle')
|
|
1037
|
+
else:
|
|
1038
|
+
to_assign['bending_radius'] = kwargs.pop('bending_radius', 1)
|
|
1039
|
+
kwargs.setdefault('width', 0)
|
|
1040
|
+
kwargs.setdefault('height', 0)
|
|
1041
|
+
|
|
1042
|
+
xt.BeamElement.__init__(self, **kwargs)
|
|
1043
|
+
# Careful: non-xofields are not passed correctly between copy's / to_dict. This messes with flags etc..
|
|
1044
|
+
# We also have to manually initialise them for xobject generation
|
|
1045
|
+
if not hasattr(self, '_optics'):
|
|
1046
|
+
self._optics = None
|
|
1047
|
+
for key, val in to_assign.items():
|
|
1048
|
+
setattr(self, key, val)
|
|
1049
|
+
if self.side == 'right':
|
|
1050
|
+
if np.isclose(self._jaw_U, OPEN_JAW):
|
|
1051
|
+
self._jaw_U *= -1
|
|
1052
|
+
if np.isclose(self._gap, OPEN_GAP):
|
|
1053
|
+
self._gap *= -1
|
|
1054
|
+
self._verify_consistency()
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
# Main crystal angle
|
|
1058
|
+
# ==================
|
|
1059
|
+
|
|
1060
|
+
@property
|
|
1061
|
+
def angle(self):
|
|
1062
|
+
return round(np.rad2deg(np.arctan2(self._sin_z, self._cos_z)), 10)
|
|
1063
|
+
|
|
1064
|
+
@angle.setter
|
|
1065
|
+
def angle(self, val):
|
|
1066
|
+
self._sin_z = np.sin(np.deg2rad(val))
|
|
1067
|
+
self._cos_z = np.cos(np.deg2rad(val))
|
|
1068
|
+
self._apply_optics()
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
# Jaw attributes
|
|
1072
|
+
# ==============
|
|
1073
|
+
|
|
1074
|
+
@property
|
|
1075
|
+
def jaw(self):
|
|
1076
|
+
return self.jaw_U
|
|
1077
|
+
|
|
1078
|
+
@jaw.setter
|
|
1079
|
+
def jaw(self, val):
|
|
1080
|
+
if val is None:
|
|
1081
|
+
val = self._side*OPEN_JAW
|
|
1082
|
+
self.jaw_U = val
|
|
1083
|
+
|
|
1084
|
+
@property
|
|
1085
|
+
def jaw_U(self):
|
|
1086
|
+
if not np.isclose(self._jaw_U, self._side*OPEN_JAW, atol=1.e-10): # open position
|
|
1087
|
+
return self._jaw_U
|
|
406
1088
|
|
|
1089
|
+
@jaw_U.setter # This moves both jaw_LU and jaw_LD in parallel
|
|
1090
|
+
def jaw_U(self, val):
|
|
1091
|
+
if val is None:
|
|
1092
|
+
raise ValueError("Cannot set corner to None! Use open_jaws() or set jaw to None.")
|
|
1093
|
+
self._jaw_U = val
|
|
1094
|
+
self._update_gaps()
|
|
1095
|
+
|
|
1096
|
+
@property
|
|
1097
|
+
def jaw_D(self):
|
|
1098
|
+
if not np.isclose(self._jaw_U, self._side*OPEN_JAW, atol=1.e-10): # open position
|
|
1099
|
+
length = self.length
|
|
1100
|
+
if self._side*self.bending_radius < 0:
|
|
1101
|
+
# Correction for inner corner point
|
|
1102
|
+
length -= self.width*np.sin(abs(self._bending_angle))
|
|
1103
|
+
shift = np.tan(self._bending_angle/2)*self._cos_y + self._sin_y
|
|
1104
|
+
return self._jaw_U + length*shift
|
|
1105
|
+
|
|
1106
|
+
@jaw_D.setter # This moves both jaw_LU and jaw_LD in parallel
|
|
1107
|
+
def jaw_D(self, val):
|
|
1108
|
+
if val is None:
|
|
1109
|
+
self.tilt = 0
|
|
1110
|
+
else:
|
|
1111
|
+
shift = (val - self._jaw_U )/self.length * np.cos(self._bending_angle/2)
|
|
1112
|
+
self._sin_y = shift*np.cos(self._bending_angle/2)
|
|
1113
|
+
self._sin_y -= np.sin(self._bending_angle/2)*np.sqrt(1 - shift**2)
|
|
1114
|
+
self._cos_y = np.sqrt(1 - self._sin_y**2)
|
|
1115
|
+
self._tan_y = self._sin_y / self._cos_y
|
|
1116
|
+
self._update_gaps()
|
|
1117
|
+
|
|
1118
|
+
def open_jaws(self, keep_tilts=False):
|
|
1119
|
+
self.jaw = None
|
|
1120
|
+
if not keep_tilts:
|
|
1121
|
+
self.tilt = 0
|
|
1122
|
+
|
|
1123
|
+
def _update_gaps(self):
|
|
1124
|
+
# If we had set a value for the gap manually, this needs to be updated
|
|
1125
|
+
# as well after setting the jaw
|
|
1126
|
+
if self._gap_set_manually():
|
|
1127
|
+
self._gap = self.gap
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
# Tilt attributes
|
|
1131
|
+
# ===============
|
|
1132
|
+
|
|
1133
|
+
# TODO: tilts are in rad! Do we want that? It's a bit inconsistent with angle which is in deg...
|
|
1134
|
+
|
|
1135
|
+
@property
|
|
1136
|
+
def tilt(self):
|
|
1137
|
+
return round(np.arctan2(self._sin_y, self._cos_y), 10)
|
|
1138
|
+
|
|
1139
|
+
@tilt.setter # This assumes jaw_U remains fixed (hence jaw_D changes)
|
|
1140
|
+
def tilt(self, val):
|
|
1141
|
+
if self.side == 'left':
|
|
1142
|
+
if val < min(0, self.bending_angle/2):
|
|
1143
|
+
print("Warning: Setting a negative tilt does not preserve the hierarchy, as the "
|
|
1144
|
+
+ "crystal tightens towards the beam.")
|
|
1145
|
+
elif self.side == 'right':
|
|
1146
|
+
if val > min(0, -self.bending_angle/2):
|
|
1147
|
+
print("Warning: Setting a positive tilt does not preserve the hierarchy, as the "
|
|
1148
|
+
+ "crystal tightens towards the beam.")
|
|
1149
|
+
self._sin_y = np.sin(val)
|
|
1150
|
+
self._cos_y = np.cos(val)
|
|
1151
|
+
self._tan_y = np.tan(val)
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
# Optics
|
|
1155
|
+
# ======
|
|
1156
|
+
|
|
1157
|
+
@property
|
|
1158
|
+
def optics(self):
|
|
1159
|
+
return self._optics
|
|
1160
|
+
|
|
1161
|
+
def optics_ready(self):
|
|
1162
|
+
return BaseCollimator.optics_ready(self)
|
|
1163
|
+
|
|
1164
|
+
def assign_optics(self, *, nemitt_x=None, nemitt_y=None, beta_gamma_rel=None, name=None, twiss=None,
|
|
1165
|
+
twiss_upstream=None, twiss_downstream=None):
|
|
1166
|
+
return BaseCollimator.assign_optics(self, nemitt_x=nemitt_x, nemitt_y=nemitt_y,
|
|
1167
|
+
beta_gamma_rel=beta_gamma_rel, name=name, twiss=twiss,
|
|
1168
|
+
twiss_upstream=twiss_upstream, twiss_downstream=twiss_downstream)
|
|
1169
|
+
|
|
1170
|
+
@property
|
|
1171
|
+
def nemitt_x(self):
|
|
1172
|
+
return BaseCollimator.nemitt_x.fget(self)
|
|
1173
|
+
|
|
1174
|
+
@nemitt_x.setter
|
|
1175
|
+
def nemitt_x(self, val):
|
|
1176
|
+
BaseCollimator.nemitt_x.fset(self, val)
|
|
1177
|
+
|
|
1178
|
+
@property
|
|
1179
|
+
def nemitt_y(self):
|
|
1180
|
+
return BaseCollimator.nemitt_y.fget(self)
|
|
1181
|
+
|
|
1182
|
+
@nemitt_y.setter
|
|
1183
|
+
def nemitt_y(self, val):
|
|
1184
|
+
BaseCollimator.nemitt_y.fset(self, val)
|
|
1185
|
+
|
|
1186
|
+
@property
|
|
1187
|
+
def emittance(self):
|
|
1188
|
+
return BaseCollimator.emittance.fget(self)
|
|
1189
|
+
|
|
1190
|
+
@emittance.setter
|
|
1191
|
+
def emittance(self, val):
|
|
1192
|
+
BaseCollimator.emittance.fset(self, val)
|
|
1193
|
+
|
|
1194
|
+
@property
|
|
1195
|
+
def sigma(self):
|
|
1196
|
+
return BaseCollimator.sigma.fget(self)
|
|
1197
|
+
|
|
1198
|
+
@property
|
|
1199
|
+
def co(self):
|
|
1200
|
+
return BaseCollimator.co.fget(self)
|
|
1201
|
+
|
|
1202
|
+
@property
|
|
1203
|
+
def divergence(self):
|
|
1204
|
+
return BaseCollimator.divergence.fget(self)
|
|
1205
|
+
|
|
1206
|
+
@property
|
|
1207
|
+
def align(self):
|
|
1208
|
+
return BaseCollimator.align.fget(self)
|
|
1209
|
+
|
|
1210
|
+
@align.setter
|
|
1211
|
+
def align(self, val):
|
|
1212
|
+
if val != 'upstream':
|
|
1213
|
+
raise NotImplementedError("Crystals cannot be aligned to the downstream optics!")
|
|
1214
|
+
BaseCollimator.align.fset(self, val)
|
|
1215
|
+
|
|
1216
|
+
def align_to_beam_divergence(self):
|
|
1217
|
+
if not self.optics_ready():
|
|
1218
|
+
raise ValueError("Optics not assigned! Cannot align to beam divergence.")
|
|
1219
|
+
if self.gap is None:
|
|
1220
|
+
raise ValueError("Need to set `gap` to align to beam divergence.")
|
|
1221
|
+
self.tilt = self.divergence * self.gap
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
# Gap attributes
|
|
1225
|
+
# ==============
|
|
1226
|
+
|
|
1227
|
+
@property
|
|
1228
|
+
def gap(self):
|
|
1229
|
+
if self.optics_ready() and self.jaw_U is not None:
|
|
1230
|
+
return round((self.jaw_U - self.co[0])/self.sigma[0], 6)
|
|
1231
|
+
elif not self._gap_set_manually():
|
|
1232
|
+
return None
|
|
1233
|
+
else:
|
|
1234
|
+
return self._gap
|
|
1235
|
+
|
|
1236
|
+
# Gap is always positive, irrespective of the side
|
|
1237
|
+
@gap.setter
|
|
1238
|
+
def gap(self, val):
|
|
1239
|
+
if val is None:
|
|
1240
|
+
val = OPEN_GAP
|
|
1241
|
+
self.jaw = None
|
|
1242
|
+
if hasattr(val, '__iter__'):
|
|
1243
|
+
raise ValueError("The attribute `gap` should be a single value, not a list.")
|
|
1244
|
+
if val <= 0:
|
|
1245
|
+
raise ValueError(f"The field `gap` should be positive, but got {val}.")
|
|
1246
|
+
self._gap = val
|
|
1247
|
+
self._apply_optics()
|
|
1248
|
+
|
|
1249
|
+
def _gap_set_manually(self):
|
|
1250
|
+
return not np.isclose(self._gap, OPEN_GAP)
|
|
1251
|
+
|
|
1252
|
+
def _apply_optics(self):
|
|
1253
|
+
if self.optics_ready():
|
|
1254
|
+
# Only if we have set a value for the gap manually, this needs to be updated
|
|
1255
|
+
if self._gap_set_manually():
|
|
1256
|
+
self.jaw_U = self._gap * self.sigma[0] + self.co[0]
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
# Other attributes
|
|
1260
|
+
# ================
|
|
1261
|
+
|
|
1262
|
+
@property
|
|
1263
|
+
def bending_radius(self):
|
|
1264
|
+
return self._bending_radius
|
|
1265
|
+
|
|
1266
|
+
@bending_radius.setter
|
|
1267
|
+
def bending_radius(self, bending_radius):
|
|
1268
|
+
bending_angle = np.arcsin(self.length/bending_radius)
|
|
1269
|
+
if abs(bending_angle) > np.pi/2:
|
|
1270
|
+
raise ValueError("Bending angle cannot be larger than 90 degrees!")
|
|
1271
|
+
self._bending_radius = bending_radius
|
|
1272
|
+
self._bending_angle = bending_angle
|
|
1273
|
+
|
|
1274
|
+
@property
|
|
1275
|
+
def bending_angle(self):
|
|
1276
|
+
return self._bending_angle
|
|
1277
|
+
|
|
1278
|
+
@bending_angle.setter
|
|
1279
|
+
def bending_angle(self, bending_angle):
|
|
1280
|
+
if abs(bending_angle) > np.pi/2:
|
|
1281
|
+
raise ValueError("Bending angle cannot be larger than 90 degrees!")
|
|
1282
|
+
self._bending_angle = bending_angle
|
|
1283
|
+
self._bending_radius = self.length / np.sin(bending_angle)
|
|
1284
|
+
|
|
1285
|
+
@property
|
|
1286
|
+
def side(self):
|
|
1287
|
+
return BaseCollimator.side.fget(self)
|
|
1288
|
+
|
|
1289
|
+
@side.setter
|
|
1290
|
+
def side(self, val):
|
|
1291
|
+
temp = self._side
|
|
1292
|
+
BaseCollimator.side.fset(self, val)
|
|
1293
|
+
if self._side == 0:
|
|
1294
|
+
self._side = temp
|
|
1295
|
+
raise ValueError("Crystal cannot be two-sided! Please set `side` "
|
|
1296
|
+
+ "to 'left' or 'right'.")
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
# Methods
|
|
1300
|
+
# =======
|
|
1301
|
+
|
|
1302
|
+
def enable_scattering(self):
|
|
1303
|
+
BaseCollimator.enable_scattering(self)
|
|
1304
|
+
|
|
1305
|
+
def disable_scattering(self):
|
|
1306
|
+
BaseCollimator.disable_scattering(self)
|
|
1307
|
+
|
|
1308
|
+
def _verify_consistency(self):
|
|
1309
|
+
# Verify angles
|
|
1310
|
+
ang = abs(np.arccos(self._cos_z))
|
|
1311
|
+
ang = np.pi - ang if ang > np.pi/2 else ang
|
|
1312
|
+
assert np.isclose(ang, abs(np.arcsin(self._sin_z)))
|
|
1313
|
+
ang = abs(np.arccos(self._cos_y))
|
|
1314
|
+
ang = np.pi - ang if ang > np.pi/2 else ang
|
|
1315
|
+
assert np.isclose(ang, abs(np.arcsin(self._sin_y)))
|
|
1316
|
+
assert np.isclose(self._sin_y/self._cos_y, self._tan_y)
|
|
1317
|
+
# Verify bools
|
|
1318
|
+
assert self._side in [-1, 1, 0]
|
|
1319
|
+
assert isinstance(self.active, bool) or self.active in [0, 1]
|
|
1320
|
+
assert isinstance(self.record_touches, bool) or self.record_touches in [0, 1]
|
|
1321
|
+
assert isinstance(self.record_scatterings, bool) or self.record_scatterings in [0, 1]
|
|
1322
|
+
# Crystal specific
|
|
1323
|
+
assert np.isclose(self._bending_angle, np.arcsin(self.length/self._bending_radius))
|
|
407
1324
|
|