Source code for simba.Modules.Fields.opal

import numpy as np
from .sdds import write_SDDS_field_file
from warnings import warn
from collections import Counter
from .FieldParameter import FieldParameter
from ..units import UnitValue
import re

d = ",!?/&-:;@'\n \t"


[docs] def write_opal_field_file( self, frequency: float = None, radius: float = None, fourier: int = 100, orientation: str = None, ): """ Generate the field data in a format that is suitable for OPAL, based on the :class:`~simba.Modules.Fields.field` object provided. See the `OPAL manual`_ for more details. This is then written to a text file. The `field_type` parameter determines the format of the file. A warning is raised if the field type is not supported (perhaps elevate to a `NotImplementedError`? .. _OPAL manual: https://amas.web.psi.ch/opal/Documentation/master/OPAL_Manual.html Parameters ---------- self: :class:`~simba.Modules.Fields.field` The field object frequency: float | None The frequency of the field radius: float | None The radius of the field, for 1D axially symmetric fields; defaults to 0.1 fourier: int = 100 Number of fourier coefficients orientation: str | None Orientation of the field (for 2D) Returns ------- str | None: The name of the field file. Will return None if required parameters for certain fields are not provided. Raises ------ Warning: if too many or not enough fourier components are provided; will default to 1/100 Warning: if the magnet radius is not provided; will default to 0.1 Warning: if the RF frequency, radius or orientation is not provided for `1DElectroDynamic` or `2DElectrodynamic`, return None Warning: if trying to use wakefields; these are tricky to get working with OPAL Warning: if a given `field_type` is not supported. """ length = self.length opal_file = self._output_filename(extension=".opal") data = None header = None fourier_ratio = fourier / length if isinstance(self.field_type, bytes): self.field_type = self.field_type.decode("utf-8") if self.field_type != "2DElectroDynamic": if fourier_ratio > 1.0: warn("Too many fourier components provided; defaulting to 1/100") fourier = int(length / 100) elif fourier_ratio < 0.01: warn("Not enough fourier components provided; defaulting to 1/100") fourier = int(length / 100) if self.field_type == "1DMagnetoStatic": zmin = min(self.z.value.val) * 1e2 zmax = max(self.z.value.val) * 1e2 data = self.Bz.value.val head = ["1DMagnetoStatic", str(fourier)] if radius is None: warn("Magnet radius not provided; defaulting to 10cm") radius = 0.1 rvals = [str(0), str(radius * 100), str(length), fourier] zvals = [str(zmin), str(zmax), str(length)] header = [head, zvals, rvals] elif self.field_type == "1DElectroDynamic": if frequency is None: warn("RF Frequency not provided to field class") return head = ["AstraDynamic", str(fourier)] freq = [str(frequency * 1e-6)] zdata = self.z.value.val ezdata = self.Ez.value.val data = np.transpose([zdata, ezdata]) header = [head, freq] elif self.field_type == "2DElectroDynamic": if frequency is None: warn("RF Frequency not provided to field class") return if radius is None: warn("Radius not provided to field class") return if orientation is None: warn("Orientation not provided to field class") return orient = self.orientation ezvals = self.Ez.value.val ervals = self.Er.value.val eabsvals = self.Ex.value.val brvals = self.Br.value.val shape = len(ezvals) count = Counter(ezvals) repeating_floats = {key: value for key, value in count.items() if value > 1} zlen = int(len(repeating_floats) - 1) rlen = int((shape / len(repeating_floats)) - 1) head = ["2DDynamic", orient] leng = ["0.0", str(self.length * 10), str(zlen)] freq = [str(frequency * 1e-6)] rad = ["0.0", str(self.radius * 10), str(rlen)] data = np.transpose([ezvals, ervals, eabsvals, brvals]) header = [head, leng, freq, rad] elif "wake" in self.field_type.lower(): warn(f"Field type {self.field_type} defaulting to SDDS type; use with caution") return write_SDDS_field_file(self) else: warn(f"Field type {self.field_type} not supported for OPAL") if data is not None: with open(f"{opal_file}", "w") as f: for h in header: f.write(" ".join([str(x) for x in h]) + "\n") for dat in data: if not type(dat) in [list, np.ndarray]: dat = [dat] f.write(" ".join([str(x) for x in dat]) + "\n") return opal_file
[docs] def read_opal_field_file( self, filename: str, field_type: str, cavity_type: str | None = None, frequency: float | None = None, ): """ Read an OPAL field file and convert it into a :class:`simba.Modules.Fields.field` object Parameters ---------- self: :class:`~simba.Modules.Fields.field` The field object to be updated. filename: str The path to the OPAL field file field_type: str The name of the field, see :attr:`~simba.Modules.Fields.allowed_fields` cavity_type: str, optional The type of RF cavity, see :attr:`~simba.Modules.Fields.allowed_cavities` frequency: float, optional The frequency of the RF cavity. Returns ------- None Raises ------ ValueError: if the cavity `field_type` contains the string `Electro` and `cavity_type` is not provided ValueError: if the cavity `field_type` contains the string `Electro` and `frequency` is not provided NotImplementedError: if a given `field_type` is not implemented """ self.reset_dicts() setattr(self, "field_type", field_type) if "Electro" in field_type: if cavity_type is None: raise ValueError(f"cavity_type must be provided for {field_type}") else: setattr(self, "cavity_type", cavity_type) if frequency is None: raise ValueError(f"frequency must be provided for {field_type}") else: setattr(self, "frequency", frequency) if field_type == "2DElectroDynamic": with open(filename) as f: rl = f.readlines() setattr(self, "length", float(rl[1].split(" ")[1]) * 1e-2) setattr(self, "frequency", float(rl[2]) * 1e6) setattr(self, "radius", float(rl[3].split(" ")[1]) * 1e-2) setattr(self, "orientation", re.split("[" + "\\".join(d) + "]", rl[0])) fdat = np.loadtxt(filename, skiprows=4) setattr( self, "Ez", FieldParameter(name="Ez", value=UnitValue(fdat[::, 0], units="V/m")), ) setattr( self, "Er", FieldParameter(name="Er", value=UnitValue(fdat[::, 1], units="V/m")), ) setattr( self, "Ex", FieldParameter(name="Ex", value=UnitValue(fdat[::, 2], units="V/m")), ) setattr( self, "Br", FieldParameter(name="Br", value=UnitValue(fdat[::, 3], units="T")), ) elif field_type == "1DMagnetoStatic": with open(filename) as f: rl = f.readlines() if rl[0].split(" ")[0] not in ["1DMagnetoStatic"]: raise NotImplementedError( f"{rl[0].split(' ')[0]} field type not implemented for OPAL fields" ) setattr(self, "fourier", int(rl[0].split(" ")[1])) setattr(self, "radius", float(rl[2].split(" ")[1]) * 1e-2) zstart = float(rl[1].split(" ")[0]) * 1e-2 zend = float(rl[1].split(" ")[1]) * 1e-2 fdat = np.loadtxt(filename, skiprows=3) zvals = np.linspace(zstart, zend, len(fdat)) setattr(self, "z", FieldParameter(name="z", value=UnitValue(zvals, units="m"))) setattr( self, "Bz", FieldParameter(name="Bz", value=UnitValue(fdat / max(fdat), units="T")), ) elif field_type == "1DElectroDynamic": with open(filename) as f: rl = f.readlines() if rl[0].split(" ")[0] not in ["AstraDynamic"]: raise NotImplementedError( f"{rl[0].split(' ')[0]} field type not implemented for OPAL fields" ) setattr(self, "fourier", int(rl[0].split(" ")[1])) setattr(self, "frequency", float(rl[1]) * 1e6) fdat = np.loadtxt(filename, skiprows=2) setattr( self, "z", FieldParameter(name="z", value=UnitValue(fdat[::, 0], units="m")) ) setattr( self, "Ez", FieldParameter( name="Ez", value=UnitValue(fdat[::, 1] / max(fdat[::, 1]), units="V/m") ), )