OPAL (Object Oriented Parallel Accelerator Library)  2024.1
OPAL
ffa_field_mapper.py
Go to the documentation of this file.
1 # Routines to generate field maps in Cartesian and cylindrical coordinate
2 # systems
3 #
4 # Copyright (c) 2023, Chris Rogers, STFC Rutherford Appleton Laboratory, Didcot, UK
5 #
6 # This file is part of OPAL.
7 #
8 # OPAL is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with OPAL. If not, see <https://www.gnu.org/licenses/>.
15 #
16 
17 """
18 Module to hold FFAFieldMapper
19 """
20 
21 
22 import os
23 import math
24 
25 import numpy
26 import matplotlib
27 import matplotlib.pyplot
28 
29 import pyopal.objects.field
30 
31 
33  """
34  Class to make field maps, intended for FFAs/ring geometries
35  """
36  def __init__(self):
37  # for cylindrical field map
38  self.r_points = []
39  self.phi_points = []
40 
41  # for cartesian field map
42  self.x_points = []
43  self.y_points = []
44 
45  self.verbose = 0
46  self.cmap = "PiYG"
47 
48  self.radial_contours = []
49  self.spiral_contours = []
50 
51  # for derivative calculations
52  self.delta_x = 0.001
53  self.delta_t = 0.001
54  self.bmax = 1.0
55 
56  self.plot_dir = os.getcwd()
57 
58  # if empty, accept all tracks; else only those in track_id_list
60  self.track_orbit_dict = {}
61 
62  @classmethod
63  def binner(cls, a_list):
64  """Build a set of bins based on a list of grid coordinates"""
65  return [a_list[0]+(a_list[1]-a_list[0])*(i-0.5) \
66  for i in range(len(a_list)+1)]
67 
68  def load_tracks(self, track_orbit):
69  """
70  Load tracks from track_orbit file
71  - track_orbit: string file name
72  """
73  self.track_orbit_dict = {}
74  with open(track_orbit, encoding="utf8") as fin:
75  for i, line in enumerate(fin.readlines()):
76  words = line.split()
77  if i < 2: # header lines
78  continue
79  track_id = words[0]
80  if track_id not in self.track_orbit_dict:
81  if self.valid_track_id_list and \
82  track_id not in self.valid_track_id_list:
83  continue
84  self.track_orbit_dict[track_id] = {
85  "x":[], "px":[], "y":[], "py":[], "r":[], "phi":[]
86  }
87  pos_r = (float(words[1])**2+float(words[3])**2)**0.5
88  phi = math.atan2(float(words[3]), float(words[1]))
89  self.track_orbit_dict[track_id]["x"].append(float(words[1]))
90  self.track_orbit_dict[track_id]["px"].append(float(words[2]))
91  self.track_orbit_dict[track_id]["y"].append(float(words[3]))
92  self.track_orbit_dict[track_id]["py"].append(float(words[4]))
93  self.track_orbit_dict[track_id]["r"].append(pos_r)
94  self.track_orbit_dict[track_id]["phi"].append(math.degrees(phi))
95 
96  def gen_cmap(self, bz_grid):
97  """
98  Generate the colour mapping for field bz
99  - bz_grid: set of bz values. Colour mapping will proceed symmetrically
100  0 in the positive and negative direction. Maximum value is
101  the maximum absolute value in the bz_grid, up to self.bmax.
102  If the grid contains a value greater than self.bmax, then
103  self.bmax becomes the maximum value. To disable the bmax
104  behaviour, set self.bmax to None
105  Returns a tuple of (min_bz in the grid,
106  max_bz in the grid,
107  max value of bz to be used in the colormap)
108  """
109  min_bz = min(bz_grid)
110  max_bz = max(bz_grid)
111  if self.bmax is None:
112  cmax = max(abs(min_bz), abs(max_bz))
113  else:
114  cmax = self.bmax
115  return min_bz, max_bz, cmax
116 
117  def field_map_cylindrical(self, axes = None):
118  """
119  Plot a field map in cylindrical coordinates.
120 
121  - axes: matplotlib Axes object or None. If defined, plot the field map
122  on axes as a hist2d; if None make a new figure/axes and plot it there.
123 
124  Returns the figure, either a new figure or the parent figure to which
125  axes belongs.
126  """
127  r_grid = []
128  phi_grid = []
129  bz_grid = []
130 
131  for radius in self.r_points:
132  for phi in self.phi_points:
133  r_grid.append(radius)
134  phi_grid.append(phi)
135  point = (radius*math.cos(math.radians(phi)),
136  radius*math.sin(math.radians(phi)),
137  0,
138  0)
139  value = pyopal.objects.field.get_field_value(*point)
140  bz_grid.append(value[3])
141  if self.verbose > 0:
142  print("Field value at r, phi", radius, round(phi, 2),
143  "point", point,
144  "is B:", value[1:4],
145  "E:", value[4:])
146  r_bins = self.binner(self.r_points)
147  phi_bins = self.binner(self.phi_points)
148  if not axes:
149  figure = matplotlib.pyplot.figure()
150  axes = figure.add_subplot(1, 1, 1)
151 
152  min_by, max_by, cmax = self.gen_cmap(bz_grid)
153  axes.hist2d(phi_grid, r_grid, bins=[phi_bins, r_bins], weights=bz_grid,
154  cmin=min_by, cmax=max_by, cmap=self.cmap, vmin=-cmax, vmax=cmax)
155  axes.set_xlabel("$\\phi$ [deg]")
156  axes.set_ylabel("r [m]")
157  axes.set_title("$B_z$ [T]")
158  for contour in self.radial_contours:
159  self.draw_cylindrical_radial_contour(axes, contour)
160  for contour in self.spiral_contours:
161  self.draw_cylindrical_spiral_contour(axes, contour)
162  fig_fname = os.path.join(self.plot_dir, "scaling_ffa_map_cyl.png")
163  figure.savefig(fig_fname)
164  print("Generated cylindrical field map in", fig_fname)
165  return figure
166 
167  @classmethod
168  def draw_cylindrical_radial_contour(cls, axes, contour):
169  """
170  Draw a purely radial contour on axes
171  - axes: matplotlib Axes object to draw on.
172  - contour: dictionary (see default_contour for definitions)
173  """
174  print("Plotting cylindrical radial contour", contour)
175  xlim = axes.get_xlim()
176  ylim = axes.get_ylim()
177  axes.plot(xlim,
178  [contour["radius"]]*2,
179  linestyle=contour["linestyle"],
180  color=contour["colour"])
181  axes.text(xlim[-1],
182  contour["radius"],
183  contour["label"],
184  horizontalalignment="right",
185  va="top",
186  color=contour["colour"])
187  axes.set_xlim(xlim)
188  axes.set_ylim(ylim)
189 
190  def draw_cylindrical_spiral_contour(self, axes, contour):
191  """
192  Draw a radially spiralling contour on axes
193  - axes: matplotlib Axes object to draw on.
194  - contour: dictionary (see default_contour for definitions)
195  """
196  xlim = axes.get_xlim()
197  ylim = axes.get_ylim()
198  tan_d = math.tan(math.radians(contour["spiral_angle"]))
199  phi_points = [math.radians(contour["phi0"]) + \
200  tan_d*math.log(r/contour["r0"]) for r in self.r_points]
201  phi_points = [math.degrees(phi) for phi in phi_points]
202  axes.plot(phi_points, self.r_points,
203  linestyle=contour["linestyle"],
204  color=contour["colour"])
205  axes.text(phi_points[-1],
206  self.r_points[-1],
207  contour["label"],
208  va="top",
209  rotation="vertical",
210  color=contour["colour"])
211  axes.set_xlim(xlim)
212  axes.set_ylim(ylim)
213 
214  def field_map_cartesian(self, axes = None):
215  """
216  Plot a field map in cartesian coordinates.
217 
218  - axes: matplotlib Axes object or None. If defined, plot the field map
219  on axes as a hist2d; if None make a new figure/axes and plot it there.
220 
221  Returns the figure, either a new figure or the parent figure to which
222  axes belongs.
223  """
224  x_grid = []
225  y_grid = []
226  bz_grid = []
227  for pos_x in self.x_points:
228  for pos_y in self.y_points:
229  x_grid.append(pos_x)
230  y_grid.append(pos_y)
231  point = (pos_x, pos_y, 0, 0)
232  value = pyopal.objects.field.get_field_value(*point)
233  bz_grid.append(value[3])
234  if self.verbose > 0:
235  print("Field value at point", point,
236  "is B:", value[1:4], "E:", value[4:])
237  x_bins = self.binner(self.x_points)
238  y_bins = self.binner(self.y_points)
239  if not axes:
240  figure = matplotlib.pyplot.figure()
241  axes = figure.add_subplot(1, 1, 1)
242  min_by, max_by, cmax = self.gen_cmap(bz_grid)
243  hist = axes.hist2d(x_grid, y_grid, bins=[x_bins, y_bins], weights=bz_grid,
244  cmin=min_by, cmax=max_by, cmap=self.cmap, vmin=-cmax, vmax=cmax)
245  axes.set_xlabel("x [m]")
246  axes.set_ylabel("y [m]")
247  axes.set_title("$B_{z}$ [T]")
248  figure.colorbar(hist[3])
249  fig_fname = os.path.join(self.plot_dir, "scaling_ffa_map_cart.png")
250  figure.savefig(fig_fname)
251  print("Generated cartesian field map in", fig_fname)
252  return figure
253 
254  def plot_tracks_cartesian(self, axes):
255  """
256  Plot tracks in cartesian coordinates
257  - axes: Axes object to draw on
258  """
259  for pid, track in self.track_orbit_dict.items():
260  pos_x = track["x"]
261  pos_y = track["y"]
262  axes.plot(pos_x, pos_y)
263 
264  def plot_tracks_cylindrical(self, axes):
265  """
266  Plot tracks in cylindrical coordinates
267  - axes: Axes object to draw on
268  If phi changes by more than 180 degrees in a single step, the track is
269  assumed to have gone through an entire turn (360 <-> 0) and a new plot
270  is started
271  """
272  for pid, track in self.track_orbit_dict.items():
273  # find the list of indices where the track loops
274  delta_list = [i+1 for i, phi1 in enumerate(track["phi"][1:]) \
275  if abs(track["phi"][i] - phi1) > 180]
276  phi_list_of_lists = numpy.split(track["phi"], delta_list)
277  r_list_of_lists = numpy.split(track["r"], delta_list)
278  for i in range(len(phi_list_of_lists)):
279  axes.plot(phi_list_of_lists[i], r_list_of_lists[i])
280 
281 
282  def oned_field_map(self, radius, axes = None):
283  """
284  Make a one dimensional field map along a line of constant radius
285  - radius: line to draw the fields on
286  - axes: Axes object to draw on
287  """
288  bz_points = []
289 
290  for phi in self.phi_points:
291  point = (radius*math.cos(math.radians(phi)),
292  radius*math.sin(math.radians(phi)),
293  0,
294  0)
295  value = pyopal.objects.field.get_field_value(*point)
296  bz_points.append(value[3])
297 
298  if not axes:
299  figure = matplotlib.pyplot.figure()
300  axes = figure.add_subplot(1, 1, 1)
301 
302  axes.plot(self.phi_points, bz_points)
303  #for contour in self.spiral_contours:
304  # self.draw_azimuthal_contour(radius, axes, contour)
305 
306  axes.set_xlabel("$\\phi$ [deg]")
307  axes.set_ylabel("$B_z$ [T]")
308  return axes.figure, bz_points
309 
310  @classmethod
311  def draw_azimuthal_contour(cls, radius, axes, contour):
312  """
313  Draw an azimuthal contour (including spiral angle if required)
314  - radius: r0 for spiral angle calculations
315  - axes: Axes object to draw on
316  - contour: contour object
317  """
318  xlim = axes.get_xlim()
319  ylim = axes.get_ylim()
320  tan_d = math.tan(contour["spiral_angle"])
321  phi = contour["phi0"] + tan_d*math.log(radius/contour["r0"])
322  phi = math.degrees(phi)
323  axes.plot([phi, phi], ylim,
324  linestyle=contour["linestyle"],
325  color=contour["colour"])
326  axes.text(phi, ylim[-1],
327  contour["label"], va="top",
328  rotation="vertical",
329  color=contour["colour"])
330  axes.set_xlim(xlim)
331  axes.set_ylim(ylim)
332 
333 
334  def get_derivative(self, var1, var2, pos_x, pos_y, pos_z, time):
335  """
336  Calculate a numerical derivative of the field in cartesian coordinates
337  - var1: variable in the numerator, should be one of self.position_variables
338  - var2: variable in the denominator (range 1 to 4)
339  """
340  pos_vec = [pos_x, pos_y, pos_z, time]
341  var2 = self.position_variables.index(var2)
342  pos_vec[var2] += self.delta_x
343  field_plus = pyopal.objects.field.get_field_value(*pos_vec)[var1]
344  pos_vec[var2] -= 2*self.delta_x
345  field_minus = pyopal.objects.field.get_field_value(*pos_vec)[var1]
346  derivative = (field_plus-field_minus)/2.0/self.delta_x
347  return derivative
348 
349  def get_derivative_int(self, var1, var2, pos_x, pos_y, pos_z, time):
350  """
351  Calculate a numerical derivative of the field in cartesian coordinates
352  - var1: should be an integer range 1 to 6 inclusive for bx, by, bz, ex, ey, ez
353  - var2: should be an integer range 0 to 3 inclusive for x, y, z, t
354  - x, y, z, t: position at which the derivative should be calculated
355  """
356  raise RuntimeError("Not implemented")
357 
358  def get_div_b(self, pos_x, pos_y, pos_z, time):
359  """
360  Calculate Div B
361  """
362  div_b = self.get_derivative("bx", "x", pos_x, pos_y, pos_z, time) + \
363  self.get_derivative("by", "y", pos_x, pos_y, pos_z, time) + \
364  self.get_derivative("bz", "z", pos_x, pos_y, pos_z, time)
365  return div_b
366 
367  def get_curl_b(self, pos_x, pos_y, pos_z, time):
368  """
369  Calculate Curl B
370  """
371  curl_b = [
372  self.get_derivative("by", "z", pos_x, pos_y, pos_z, time) - \
373  self.get_derivative("bz", "y", pos_x, pos_y, pos_z, time),
374  self.get_derivative("bx", "z", pos_x, pos_y, pos_z, time) - \
375  self.get_derivative("bz", "x", pos_x, pos_y, pos_z, time),
376  self.get_derivative("bx", "y", pos_x, pos_y, pos_z, time) - \
377  self.get_derivative("by", "x", pos_x, pos_y, pos_z, time)
378  ]
379  return curl_b
380 
381  default_radial_contour = {"radius":0.0,
382  "linestyle":"-",
383  "colour":"grey",
384  "oned_plot":False}
385  default_spiral_contour = {
386  "phi0":0.0, # the contour will pass through a point having cylindrical coordinates phi0, r0
387  "r0":0, # the contour will pass through a point having cylindrical coordinates phi0, r0
388  "spiral_angle":0, # spiral angle parameter in degrees
389  "linestyle":"-", # matplotlib line style
390  "colour":"grey", # matplotlib line colour
391  }
392  position_variables = ["x", "y", "z", "t"]
393  field_variables = ["out_of_bounds", "bx", "by", "bz", "ex", "ey", "ez"]
PETE_TUTree< FnAbs, typename T::PETE_Expr_t > abs(const PETE_Expr< T > &l)
T::PETE_Expr_t::PETE_Return_t min(const PETE_Expr< T > &expr, NDIndex< D > &loc)
Definition: ReductionLoc.h:76
T::PETE_Expr_t::PETE_Return_t max(const PETE_Expr< T > &expr, NDIndex< D > &loc)
Definition: ReductionLoc.h:84