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.
Files changed (56) hide show
  1. xcoll/__init__.py +12 -4
  2. xcoll/beam_elements/__init__.py +7 -5
  3. xcoll/beam_elements/absorber.py +41 -7
  4. xcoll/beam_elements/base.py +1161 -244
  5. xcoll/beam_elements/collimators_src/black_absorber.h +118 -0
  6. xcoll/beam_elements/collimators_src/black_crystal.h +111 -0
  7. xcoll/beam_elements/collimators_src/everest_block.h +40 -28
  8. xcoll/beam_elements/collimators_src/everest_collimator.h +129 -50
  9. xcoll/beam_elements/collimators_src/everest_crystal.h +217 -73
  10. xcoll/beam_elements/everest.py +60 -113
  11. xcoll/colldb.py +250 -750
  12. xcoll/general.py +2 -2
  13. xcoll/headers/checks.h +1 -1
  14. xcoll/headers/particle_states.h +2 -2
  15. xcoll/initial_distribution.py +195 -0
  16. xcoll/install.py +177 -0
  17. xcoll/interaction_record/__init__.py +1 -0
  18. xcoll/interaction_record/interaction_record.py +252 -0
  19. xcoll/interaction_record/interaction_record_src/interaction_record.h +98 -0
  20. xcoll/{impacts → interaction_record}/interaction_types.py +11 -4
  21. xcoll/line_tools.py +83 -0
  22. xcoll/lossmap.py +209 -0
  23. xcoll/manager.py +2 -937
  24. xcoll/rf_sweep.py +1 -1
  25. xcoll/scattering_routines/everest/amorphous.h +239 -0
  26. xcoll/scattering_routines/everest/channeling.h +245 -0
  27. xcoll/scattering_routines/everest/crystal_parameters.h +137 -0
  28. xcoll/scattering_routines/everest/everest.h +8 -30
  29. xcoll/scattering_routines/everest/everest.py +13 -10
  30. xcoll/scattering_routines/everest/jaw.h +27 -197
  31. xcoll/scattering_routines/everest/materials.py +2 -0
  32. xcoll/scattering_routines/everest/multiple_coulomb_scattering.h +31 -10
  33. xcoll/scattering_routines/everest/nuclear_interaction.h +86 -0
  34. xcoll/scattering_routines/geometry/__init__.py +6 -0
  35. xcoll/scattering_routines/geometry/collimator_geometry.h +219 -0
  36. xcoll/scattering_routines/geometry/crystal_geometry.h +150 -0
  37. xcoll/scattering_routines/geometry/geometry.py +26 -0
  38. xcoll/scattering_routines/geometry/get_s.h +92 -0
  39. xcoll/scattering_routines/geometry/methods.h +111 -0
  40. xcoll/scattering_routines/geometry/objects.h +154 -0
  41. xcoll/scattering_routines/geometry/rotation.h +23 -0
  42. xcoll/scattering_routines/geometry/segments.h +226 -0
  43. xcoll/scattering_routines/geometry/sort.h +184 -0
  44. {xcoll-0.3.5.dist-info → xcoll-0.4.0.dist-info}/METADATA +1 -1
  45. {xcoll-0.3.5.dist-info → xcoll-0.4.0.dist-info}/RECORD +48 -33
  46. xcoll/beam_elements/collimators_src/absorber.h +0 -141
  47. xcoll/collimator_settings.py +0 -457
  48. xcoll/impacts/__init__.py +0 -1
  49. xcoll/impacts/impacts.py +0 -102
  50. xcoll/impacts/impacts_src/impacts.h +0 -99
  51. xcoll/scattering_routines/everest/crystal.h +0 -1302
  52. xcoll/scattering_routines/everest/scatter.h +0 -169
  53. xcoll/scattering_routines/everest/scatter_crystal.h +0 -260
  54. {xcoll-0.3.5.dist-info → xcoll-0.4.0.dist-info}/LICENSE +0 -0
  55. {xcoll-0.3.5.dist-info → xcoll-0.4.0.dist-info}/NOTICE +0 -0
  56. {xcoll-0.3.5.dist-info → xcoll-0.4.0.dist-info}/WHEEL +0 -0
@@ -1,6 +1,6 @@
1
1
  # copyright ############################### #
2
2
  # This file is part of the Xcoll Package. #
3
- # Copyright (c) CERN, 2023. #
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 ..collimator_settings import _get_LR, _set_LR, _get_LRUD, _set_LRUD
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
- allow_backtrack = True
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': xo.Float64
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
- _internal_record_class = CollimatorImpacts
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(xt.BeamElement):
77
- _xofields = {
78
- 'inactive_front': xo.Float64, # Drift before jaws
79
- 'active_length': xo.Float64, # Length of jaws
80
- 'inactive_back': xo.Float64, # Drift after jaws
81
- 'jaw_L': xo.Float64, # left jaw (distance to ref)
82
- 'jaw_R': xo.Float64, # right jaw
83
- 'ref_x': xo.Float64, # center of collimator reference frame
84
- 'ref_y': xo.Float64,
85
- 'sin_zL': xo.Float64, # angle of left jaw
86
- 'cos_zL': xo.Float64,
87
- 'sin_zR': xo.Float64, # angle of right jaw
88
- 'cos_zR': xo.Float64,
89
- 'sin_yL': xo.Float64, # tilt of left jaw (around jaw midpoint)
90
- 'cos_yL': xo.Float64,
91
- 'tan_yL': xo.Float64,
92
- 'sin_yR': xo.Float64, # tilt of right jaw (around jaw midpoint)
93
- 'cos_yR': xo.Float64,
94
- 'tan_yR': xo.Float64,
95
- '_side': xo.Int8, # is it a onesided collimator?
96
- 'active': xo.Int8
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 = True
100
- allow_track = False
101
- behaves_like_drift = True
102
- skip_in_loss_location_refinement = True
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
- _skip_in_to_dict = ['jaw_L', 'jaw_R', 'ref_x', 'ref_y',
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
- kwargs['_side'] = _side_setter(kwargs.pop('side', 'both'))
141
+ to_assign['side'] = kwargs.pop('side', 'both')
142
142
 
143
143
  # Set angle
144
144
  if 'angle' in kwargs:
145
- if 'angle_L' in kwargs or 'angle_R' in kwargs:
146
- raise ValuError("Cannot use both 'angle' and 'angle_L/R'!")
147
- kwargs['sin_zL'], kwargs['cos_zL'], _, kwargs['sin_zR'], kwargs['cos_zR'], _ \
148
- = _angle_setter(kwargs.pop('angle', 0))
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
- anglerad_L = kwargs.pop('angle_L', 0) / 180. * np.pi
151
- kwargs['sin_zL'] = np.sin(anglerad_L)
152
- kwargs['cos_zL'] = np.cos(anglerad_L)
153
- anglerad_R = kwargs.pop('angle_R', 0) / 180. * np.pi
154
- kwargs['sin_zR'] = np.sin(anglerad_R)
155
- kwargs['cos_zR'] = np.cos(anglerad_R)
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
- if 'tilt_L' in kwargs or 'tilt_R' in kwargs:
160
- raise ValuError("Cannot use both 'tilt' and 'tilt_L/R'!")
161
- kwargs['sin_yL'], kwargs['cos_yL'], kwargs['tan_yL'], \
162
- kwargs['sin_yR'], kwargs['cos_yR'], kwargs['tan_yR'] \
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
- # anglerad_L = kwargs.pop('tilt_L', 0) / 180. * np.pi
166
- anglerad_L = kwargs.pop('tilt_L', 0)
167
- kwargs['sin_yL'] = np.sin(anglerad_L)
168
- kwargs['cos_yL'] = np.cos(anglerad_L)
169
- kwargs['tan_yL'] = np.cos(anglerad_L)
170
- # anglerad_R = kwargs.pop('tilt_R', 0) / 180. * np.pi
171
- anglerad_R = kwargs.pop('tilt_R', 0)
172
- kwargs['sin_yR'] = np.sin(anglerad_R)
173
- kwargs['cos_yR'] = np.cos(anglerad_R)
174
- kwargs['tan_yR'] = np.cos(anglerad_R)
175
-
176
- # Set lengths
177
- if 'length' in kwargs.keys():
178
- if 'active_length' in kwargs.keys():
179
- if 'inactive_front' in kwargs.keys():
180
- if 'inactive_back' in kwargs.keys():
181
- raise ValueError("Too many length variables used at initialisation!")
182
- kwargs['inactive_back'] = kwargs.pop('length') - kwargs['inactive_front'] - kwargs['active_length']
183
- elif 'inactive_back' in kwargs.keys():
184
- if 'inactive_front' in kwargs.keys():
185
- raise ValueError("Too many length variables used at initialisation!")
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
- def enable_scattering(self):
208
- if hasattr(self, '_tracking'):
209
- self._tracking = True
220
+ # Main collimator angle
221
+ # =====================
210
222
 
211
- def disable_scattering(self):
212
- if hasattr(self, '_tracking'):
213
- self._tracking = False
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 jaw(self):
217
- return _get_LR(self, 'jaw', neg=True)
243
+ def angle_L(self):
244
+ return round(np.rad2deg(np.arctan2(self._sin_zL, self._cos_zL)), 10)
218
245
 
219
- @jaw.setter
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
- _set_LR(self, 'jaw', val, neg=True)
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 jaw_LU(self):
225
- return self.jaw_L - self.sin_yL*self.active_length/2
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
- @jaw_LU.setter # This assumes you keep jaw_LD fixed, hence both jaw_L and the tilt change
228
- def jaw_LU(self, jaw_LU):
229
- jaw_LD = self.jaw_LD
230
- self.jaw_L = (jaw_LU+jaw_LD)/2
231
- self.sin_yL = (jaw_LD-jaw_LU)/self.active_length
232
- self.cos_yL = np.sqrt(1-self.sin_yL**2)
233
- self.tan_yL = self.sin_yL / self.cos_yL
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 jaw_LD(self):
237
- return self.jaw_L + self.sin_yL*self.active_length/2
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
- @jaw_LD.setter # This assumes you keep jaw_LU fixed, hence both jaw_L and the tilt change
240
- def jaw_LD(self, jaw_LD):
241
- jaw_LU = self.jaw_LU
242
- self.jaw_L = (jaw_LU+jaw_LD)/2
243
- self.sin_yL = (jaw_LD-jaw_LU)/self.active_length
244
- self.cos_yL = np.sqrt(1-self.sin_yL**2)
245
- self.tan_yL = self.sin_yL / self.cos_yL
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
- return self.jaw_R - self.sin_yR*self.active_length/2
250
-
251
- @jaw_RU.setter # This assumes you keep jaw_RD fixed, hence both jaw_R and the tilt change
252
- def jaw_RU(self, jaw_RU):
253
- jaw_RD = self.jaw_RD
254
- self.jaw_R = (jaw_RU+jaw_RD)/2
255
- self.sin_yR = (jaw_RD-jaw_RU)/self.active_length
256
- self.cos_yR = np.sqrt(1-self.sin_yR**2)
257
- self.tan_yR = self.sin_yR / self.cos_yR
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
- return self.jaw_R + self.sin_yR*self.active_length/2
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
- @jaw_RD.setter # This assumes you keep jaw_RU fixed, hence both jaw_R and the tilt change
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 angle_L(self):
273
- return round(np.arctan2(self.sin_zL, self.cos_zL) * (180.0 / np.pi), 10)
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
- @angle_L.setter
276
- def angle_L(self, angle_L):
277
- anglerad_L = angle_L / 180. * np.pi
278
- self.sin_zL = np.sin(anglerad_L)
279
- self.cos_zL = np.cos(anglerad_L)
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 angle_R(self):
283
- return round(np.arctan2(self.sin_zR, self.cos_zR) * (180.0 / np.pi), 10)
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
- @angle_R.setter
286
- def angle_R(self, angle_R):
287
- anglerad_R = angle_R / 180. * np.pi
288
- self.sin_zR = np.sin(anglerad_R)
289
- self.cos_zR = np.cos(anglerad_R)
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 angle(self):
293
- return self.angle_L if self.angle_L==self.angle_R else [self.angle_L, self.angle_R]
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
- @angle.setter
296
- def angle(self, angle):
297
- self.sin_zL, self.cos_zL, _, self.sin_zR, self.cos_zR, _ = _angle_setter(angle)
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 reference_center(self):
301
- return [self.ref_x, self.ref_y]
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
- @reference_center.setter
304
- def reference_center(self, ref):
305
- _set_LR(self, 'ref', ref, name_L='_x', name_R='_y')
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 length(self):
309
- return (self.inactive_front + self.active_length + self.inactive_back)
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
- @length.setter
312
- def length(self, val):
313
- raise ValueError("The parameter 'length' can only be set at initialisation. "
314
- + "Use 'active_length', 'inactive_front', and/or 'inactive_back'.")
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 == 2:
827
+ elif self._side == -1:
323
828
  return 'right'
324
829
 
325
830
  @side.setter
326
- def side(self, side):
327
- self._side = _side_setter(side)
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 tilt_L(self):
333
- # return round(np.arctan2(self.sin_yL, self.cos_yL) * (180.0 / np.pi), 10)
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
- @tilt_L.setter
337
- def tilt_L(self, tilt_L):
338
- # anglerad_L = tilt_L / 180. * np.pi
339
- anglerad_L = tilt_L
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 tilt_R(self):
346
- # return round(np.arctan2(self.sin_yR, self.cos_yR) * (180.0 / np.pi), 10)
347
- return round(np.arctan2(self.sin_yR, self.cos_yR), 10)
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
- @property
358
- def tilt(self):
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
- @tilt.setter
362
- def tilt(self, tilt):
363
- self.sin_yL, self.cos_yL, self.tan_yL, self.sin_yR, self.cos_yR, self.tan_yR = _angle_setter(tilt, rad=True)
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
- def _side_setter(val):
380
- if isinstance(val, str):
381
- if val.lower() == 'both' or val == '+-' or val == '-+':
382
- return 0
383
- elif val.lower() == 'left' or val.lower() == 'l' or val == '+':
384
- return 1
385
- elif val.lower() == 'right' or val.lower() == 'r' or val == '-':
386
- return 2
387
- raise ValueError(f"Unkown setting {val} for 'side'! Choose from "
388
- + f"('left', 'L', '+'), ('right', 'R', '-'), or ('both', '+-').")
389
-
390
- def _angle_setter(val, rad=False):
391
- if not hasattr(val, '__iter__'):
392
- val = [val]
393
- conversion = 1 if rad else np.pi / 180.
394
- if isinstance(val, str):
395
- raise ValueError(f"Error in setting angle: not a number!")
396
- elif len(val) == 2:
397
- anglerad_L = val[0] * conversion
398
- anglerad_R = val[1] * conversion
399
- elif len(val) == 1:
400
- anglerad_L = val[0] * conversion
401
- anglerad_R = val[0] * conversion
402
- else:
403
- raise ValueError(f"Error in setting angle: must have one or two (L, R) values!")
404
- return np.sin(anglerad_L), np.cos(anglerad_L), np.tan(anglerad_L), \
405
- np.sin(anglerad_R), np.cos(anglerad_R), np.tan(anglerad_R)
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