Source code for piol.gicosyelements

from .transfermatrix import TransferMatrix
from .elements import Collimator
from .elements import TMElement
from .settings import PLOTBACKEND

import sys
import os
import copy
import subprocess 
import contextlib
import logging
import numpy as np
import scipy.constants as const
from scipy.spatial.transform import Rotation
import matplotlib.patches as patches
import matplotlib.lines as lines
logger = logging.getLogger(__name__)

@contextlib.contextmanager
def cd(newdir):
    prevdir = os.getcwd()
    os.chdir(os.path.expanduser(newdir))
    try:
        yield
    finally:
        os.chdir(prevdir)

__all__ = ['GICOSYElement', 'GICOSYDipole', 'GICOSYDrift', 'GICOSYQuadrupole', 'setGICOSYPath', 'writeFringeFields', 'setFringeField']

_gicosy_path = None

[docs]def setGICOSYPath(path): """Define the path to the GICOSY installation directory. The GICOSYElement and the derived classes require access to GICOSY program for producing the elements matrices. This function is used to set the path to the GICOSY program manually. The access to GICOSY can be defined with this function or by default the PIOL searches for the presence of either a GICOSYPATH environment variable or a folder with 'gicosy' in the PATH. If neither can be found, an exception will be raised. Parameters ---------- path : str Path to GICOSY installation directory. """ global _gicosy_path _gicosy_path = path
def readGICOSYPath(): """Reads the environment variables GICOSYPATH and PATH and defines GICOSY installation directory. The GICOSYElement and the derived classes require access to GICOSY program for producing the elements matrices. This function searches for the presence of either a GICOSYPATH environment variable or a folder with 'gicosy' in the PATH. If neither can be found, an exception will be raised. This function is automatically called by the GICOSYElement as needed. """ global _gicosy_path if _gicosy_path is None: if 'GICOSYPATH' in os.environ: _gicosy_path = os.environ['GICOSYPATH'] logger.info('Found environment variable GICOSYPATH: {:s}'.format(_gicosy_path)) else: path = os.environ['PATH'].split(';') for p in path: if 'gicosy' in p.lower(): _gicosy_path = p logger.info('Found gicosy location in PATH: {}'.format(_gicosy_path)) break else: logger.info('\n'.join(path)) raise Exception('No GICOSYPATH environment variable set, nor location found in PATH')
[docs]class GICOSYElement(TMElement): """Base class for all transfer matrix elements using gicosy. """ def __init__(self, refple, name='', description=''): super().__init__(refple, name, description) self.collimator = None self.parts = 1 self.tm_ffentrance = None self.tm_part = None self.tm_ffexit = None self.tm_full = None self.part_ds = 0.0 self.part_dr = np.zeros(3) self.part_dx = np.zeros(3) self.part_dy = np.zeros(3)
[docs] def extractMatrices(self, energy, mass, charge, commands, order=1): """Runs external GICOSY program and extracts matrices. Overwrites the fringe field data file GICOSYFF.DAT if marked with tag 'PIOL', otherwise rises an exception. Parameters ---------- energy : float energy of a reference particle in eV mass : float mass of a reference particle in atomic mass units charge : float charge of a reference particle in units of elementary charge commands : list of str list of lines to be inserted into GICOSY.DAT file without a semicolon order : int order of transfer matrices Returns ------- list of TransferMatrix objects. """ logger.info('Generating matrices for {:s}'.format(self.name)) readGICOSYPath() writeFringeFields() # Writing the header to the GICOSY input file global _gicosy_path with cd(_gicosy_path): with open('GICOSYIN.DAT','w') as gin: gin.write("SYSTEM NAME PIOL ;\n" ) gin.write("REFERENCE PARTICLE {:g} {:g} {:g} ;\n".format(1e-6*energy,mass,charge) ) gin.write("CALCULATION MODE 2 ;\n" ) gin.write("CALCULATION ORDER {:d} ;\n".format(order) ) gin.write("OUTPUT COORDINATES NONSYMPLECTIC ENERGY TIME NONSCALED ;\n" ) gin.write("PRINT ALL ;\n" ) gin.write("START SYSTEM ;\n" ) for c in commands: gin.write( "PRINT FILE \'PIOL: {:s}\' ;\n".format(c)) gin.write( "{:s} ;\n".format(c) ) gin.write( "END ;\n" ) logger.info('Calling GICOSY.') try: subprocess.check_output(['./gicosy.x']) except FileNotFoundError: subprocess.check_output(['gicosy.exe']) logger.info('GICOSY finished.') matrices = [] with open('GICOSYOUT.DAT') as gout: inMatrix = False position = 0.0 linenum = 0 matrix = None command = None for line in gout: if not inMatrix: if line.find('PIOL: ')==1: # Reading a command from GICOSYOUT.DAT file # and save it to transfer matrix for user # information. command = line[7:].strip() if line.find('NON SYMPL. ELEMENT TRANSFER MATRIX AT PATH-LENGTH L=') >= 0: inMatrix = True matrix = TransferMatrix() outCoords = ['x','a','y','b','t'] # Adding coefficient which accumulates the time # x a y b t matrix.addCoefficient('t',1.0,'t') linenum = 0 else: if len(line) <= 1: inMatrix = False # matrix.info['GICOSY command'] = command command = None matrices.append(matrix) else: linenum += 1 columns = line.split() if linenum == 2: outcoords = line.split() if outcoords != ['X', 'A', 'Y', 'B', 'T']: raise Exception('Output coordinates are invalid. Should be X,A,Y,B,T.') elif len(columns) > 1 and columns[0]=='0': # Adding zeroth order coefficients. values = [float(tmp) for tmp in columns[1:] ] for i,v in enumerate(values): if v != 0: matrix.addCoefficient( outCoords[i], values[i], '' ); elif linenum > 5 and (len(columns) > 1) and (int(columns[0])==linenum-5): values = [float(tmp) for tmp in columns[2:] ] for i,v in enumerate(values): if v != 0: matrix.addCoefficient( outCoords[i], values[i], columns[1].lower() ); return matrices
[docs] def transfer(self, driver): """ Transfers the particles saved in the driver through the element. Parameters ---------- driver : Driver Driver of the system """ driver.onElementEnter(self) # Getting particles as a copy. pleset = driver.getParticlesAsCopy() # Creating a new dictionary but just copying the values as references. coords = copy.copy(pleset) # Building gicosy coordinates G and D and adding to the newly created dict. refple = self.parameters["refple"] coords['g'] = pleset['m']/pleset['q']*(refple.charge/refple.mass) - 1.0 coords['d'] = pleset['K']/pleset['q']*(refple.charge/refple.energy) - 1.0 # Entrance fringe field if needed if self.parameters['ff_entrance']: pleset = driver.getParticlesAsCopy() pleset['g'] = pleset['m']/pleset['q']*(refple.charge/refple.mass) - 1.0 pleset['d'] = pleset['K']/pleset['q']*(refple.charge/refple.energy) - 1.0 self.tm_ffentrance.apply(pleset) del pleset['g'] del pleset['d'] driver.registerTransfer(pleset, 0.0) # Running collimator before actual element transfer matrices # and then after each transfer matrix. if self.collimator: self.collimator.transfer(driver) # Looping over parts for i in range(self.parts): pleset = driver.getParticlesAsCopy() pleset['g'] = pleset['m']/pleset['q']*(refple.charge/refple.mass) - 1.0 pleset['d'] = pleset['K']/pleset['q']*(refple.charge/refple.energy) - 1.0 self.tm_part.apply(pleset) del pleset['g'] del pleset['d'] driver.registerTransfer(pleset, self.part_ds, self.part_dr, self.part_dx, self.part_dy) # Collimating after each part if self.collimator: self.collimator.transfer(driver) # Exit fringe field if needed if self.parameters['ff_exit']: pleset = driver.getParticlesAsCopy() pleset['g'] = pleset['m']/pleset['q']*(refple.charge/refple.mass) - 1.0 pleset['d'] = pleset['K']/pleset['q']*(refple.charge/refple.energy) - 1.0 self.tm_ffexit.apply(pleset) del pleset['g'] del pleset['d'] driver.registerTransfer(pleset, 0.0) # Running collimator after the fringe field. if self.collimator: self.collimator.transfer(driver) driver.onElementExit(self)
[docs] def getTransferMatrix(self): """ Returns full transfer matrix of the system. If full transfer matrix is found already then returning it. Otherwise calculating it. """ #if self.tm_full != None: # return self.tm_full matrices = [] matrices.append(self.tm_ffentrance) matrices.extend([self.tm_part] * self.parts) matrices.append(self.tm_ffexit) matrices = list(reversed([matrix for matrix in matrices if matrix is not None])) total = matrices[0] for matrix in matrices[1:]: total = total.multiply( matrix, order=max(total.order,matrix.order)) return total
[docs]class GICOSYDipole(GICOSYElement): """Dipole element Interact with a magnetic or electric dipole to bend a beam with the given radius and deflection angle. Parameters ---------- refple : ReferenceParticle Reference particle for the element creation. ftype : {'electric', 'magnetic'} String representing the type of dipole. rho0 : float Radius of curvature in m. phi0 : float Angle of deflection in degrees. G0 : float Half gap of dipole in m. directionAngle : float Defines the direction of B field in degrees in the dipole in the optical axis xyz-coordinate system at the entrance. 0 deg corresponds the case that B field is towards positive y-direction and positive particles are to be bend towards negative x-axis (by Lorentz force), 90 deg <-> B field towards positive x-axis and positive particles are to be bend upwords, 180 deg <-> B-field to neg. y-axis and positive particles are to be bend towards positive x-axis. nvalues : list of floats, optional Field indices of the multipole expansion. eps_entrance : float, optional Entrance angle of the field boundary in degree (default zero). curv_entrance : float, optional Entrance field curvature relative to rho0 (default zero). eps_exit : float, optional Exit angle of the field boundary in degree (default zero). curv_exit : float, optional Exit field curvature relative to rho0 (default zero). ff_entrance : int, optional Fringe fields table number to use for entrance. Use zero for no fringe field. ff_exit : int, optional Fringe fields table number to use for exit. Use zero for no fringe field. order : int, optional Order of the transfermatrix. Defaults to 1. maxSAdvance : float, optional Maximal advance along the s axis in m. Defaults to 0.2 collimator : float or tuple, optional Collimator parameters if one is used. name : string, optional Name given to the element. description : string, optional Description of the element, currently not used. Attributes ---------- collimator : Collimator or None Collimator used in the element. parts : int Number of times the transfer matrix is to be used. Notes ----- The most parameters are saved in the parameters dictionary. A warning will be issued in the logger if the collimator is larger than it should be. """ def __init__(self, refple, ftype, rho0, phi0, G0, directionAngle=0, nvalues=None, eps_entrance=0.0, curv_entrance=0.0, eps_exit=0.0, curv_exit=0.0, ff_entrance=1, ff_exit=1, order=1, maxSAdvance=0.2, collimator=None, name='Dipole', description=''): super().__init__(refple, name=name, description=description) if ftype not in ['electric','magnetic']: raise ValueError('ftype must be \'electric\' or \'magnetic\'') # Storing all parameters needed for calculation of matrices self.parameters['ftype'] = ftype self.parameters['rho0'] = rho0 self.parameters['phi0'] = phi0 self.parameters['G0'] = G0 self.parameters['directionAngle'] = directionAngle self.parameters['nvalues'] = nvalues self.parameters['eps_entrance'] = eps_entrance self.parameters['curv_entrance'] = curv_entrance self.parameters['eps_exit'] = eps_exit self.parameters['curv_exit'] = curv_exit self.parameters['order'] = order self.parameters['ff_entrance'] = ff_entrance self.parameters['ff_exit'] = ff_exit self.parameters['maxSAdvance'] = maxSAdvance self.parameters['displayfield'] = True self.collimator = collimator if collimator is None else Collimator(collimator) self.parameterModified = True self.rebuild() def getCalibration(self, massrange=[1, 300], energyrange=[1000, 60000], energysteps=10): mass = np.arange(min(massrange), max(massrange)+1, 1.0) energy = np.linspace(min(energyrange), max(energyrange), energysteps) mass, energy = np.meshgrid(mass, energy) refple = self.parameters['refple'] kind = self.parameters['ftype'] if kind == 'magnetic': field = np.sqrt(2*energy*mass*const.u/const.e)/ \ (refple.charge*self.parameters['rho0']) else: V0 = energy r0 = self.parameters['rho0'] r1 = r0 - self.parameters['G0'] r2 = r0 + self.parameters['G0'] field = V0*np.log(r2/r1) return mass, energy, field
[docs] def field(self, kind): if kind == self.parameters['ftype']: refple = self.parameters['refple'] if kind == 'magnetic': field = np.sqrt(2*refple.energy*refple.mass*const.u/const.e)/ \ (refple.charge*self.parameters['rho0']) else: V0 = refple.energy r0 = self.parameters['rho0'] r1 = r0 - self.parameters['G0'] r2 = r0 + self.parameters['G0'] field = V0*np.log(r2/r1) return field else: return 0.0
def setVoltage(self, field): if self.parameters['ftype'] != 'electric': raise TypeError r0 = self.parameters['rho0'] r1 = r0 - self.parameters['G0'] r2 = r0 + self.parameters['G0'] V0 = field / np.log(r2/r1) self.parameters['refple'].energy = V0 def length(self): return self.parameters['rho0'] * np.deg2rad(self.parameters['phi0'])
[docs] def height(self): return self.parameters['G0'] * 2
[docs] def opticalAxisExit(self): """Returns the exiting optical axis. Returns the coordinate transformation from the entrance plane to the exit plane. The returned value is a tuple (r,x,y), where r (vector) is a translation from entrance optical axis origin to exit optical axis origin; x and y are unit vectors defining the exit plane coordinate system. The z-axis is the cross product of x and y, and points towards positive optical axis. """ phi0rad = np.deg2rad(self.parameters['phi0']) dirRad = np.deg2rad(self.parameters['directionAngle']) # Optical axis direction at entrance (this is definition, not # in absolute coordinates). # Rotation around entrance y-axis phirot = Rotation.from_rotvec([0.0,-phi0rad,0.0]) # Rotation around entrance z-axis dirrot = Rotation.from_rotvec([0.0,0.0,dirRad]) # Vector from entrance to exit dR = dirrot.apply( self.parameters['rho0']*np.array( [-1+np.cos(phi0rad), 0, np.sin(phi0rad) ] ) ) x0 = np.array([1.0,0.0,0.0]) print("### x0 = ", x0 ) y0 = np.array([0.0,1.0,0.0]) z0 = np.array([0.0,0.0,1.0]) z1 = dirrot.apply( phirot.apply( z0 ) ) print("### z1 = ", z1 ) dirrot2 = Rotation.from_rotvec( -dirRad*z1 ) u0 = phirot.apply( x0 ) uu0 = dirrot.apply( u0 ) x1 = dirrot2.apply( uu0 ) v0 = phirot.apply( y0 ) vv0 = dirrot.apply( v0 ) y1 = dirrot2.apply( vv0 ) return np.round(dR,decimals=9), np.round(x1-x0,decimals=9), np.round(y1-y0,decimals=9)
[docs] def plot(self, ax, view, s): length = self.length() try: if 'y' in view: height = self.collimator.height('y') else: height = self.collimator.height('x') except: height = max(self.collimator.height()) if not np.isfinite(height): height = self.height() try: thickness = self.parameters['thickness'] except: thickness = height * 0.1 fill = 'palegoldenrod' hatch_fill = 'royalblue' color = 'slategray' if PLOTBACKEND == 'matplotlib': from matplotlib import patches patch = patches.Rectangle((s, -height/2), length, height, fill=True, fc=fill, ec=color, zorder=0) draw_patches = [patch] hatched_1 = patches.Rectangle((s, height/2), length, thickness, fill=True, hatch='////', fc=hatch_fill, ec=color, zorder=0) hatched_2 = patches.Rectangle((s, -height/2-thickness), length, thickness, fill=True, hatch='////', fc=hatch_fill, ec=color, zorder=0) if ('x' in view and self.parameters['ftype'] == 'electric') or ('y' in view and self.parameters['ftype'] == 'magnetic'): draw_patches.extend([hatched_1, hatched_2]) try: displaytext = self.parameters['display'] except: displaytext = False for patch in draw_patches: ax.add_patch(patch) if displaytext: for t in [((s+length/2, height/2+thickness+0.001, self.name), {'ha': 'center'})]: ax.text(*t[0], **t[1])
[docs] def rebuild(self): if not self.collimator: if self.parameters['ftype'] == 'magnetic': self.collimator = Collimator((self.parameters['G0'], self.parameters['G0'])) else: self.collimator = Collimator((self.parameters['G0'], self.parameters['G0'])) else: if self.parameters['ftype'] == 'magnetic': try: aperture = self.collimator.parameters['size'][1] except: aperture = self.collimator.parameters['size'] if aperture > self.parameters['G0']: logger.warning('Magnetic dipole aperture is larger in the y direction than the pole gap! Results can be unphysical!') else: try: aperture = self.collimator.parameters['size'][0] except: aperture = self.collimator.parameters['size'] if aperture > self.parameters['G0']: logger.warning('Electric dipole aperture is larger in the x direction than the pole gap! Results can be unphysical!') rho = np.abs(self.parameters['rho0']) self.totalDeltaS = rho*np.deg2rad(self.parameters['phi0']) self.parts = int(self.totalDeltaS / self.parameters['maxSAdvance'] + 1) commands = [] # Need to have a drift length in order to generate the # fringe field. This matrix is not used nor passed for a user. commands.append('DRIFT LENGTH 0.1') commands.append('FRINGING FIELD {} {:g} {:g}'.format(self.parameters['ff_entrance'], self.parameters['eps_entrance'], self.parameters['curv_entrance'])) nvaluesstr = '' if self.parameters['nvalues'] is not None: nvaluesstr = ' '.join([str(n) for n in self.parameters['nvalues']]) if self.parameters['ftype']=='magnetic': commands.append('M S {:g} {:g} {:g} {:s}' .format(self.parameters['rho0'], self.parameters['phi0']/self.parts, self.parameters['G0'], nvaluesstr) ) else: commands.append('E S {:g} {:g} {:g} {:s}' .format(self.parameters['rho0'],self.parameters['phi0']/self.parts, self.parameters['G0'], nvaluesstr) ) commands.append('FRINGING FIELD {} {:g} {:g}'.format(self.parameters['ff_exit'], self.parameters['eps_exit'], self.parameters['curv_exit'])) # Need to add drift length in order to generate the fringe field. commands.append('DRIFT LENGTH 0.1') refple = self.parameters['refple'] matrices = self.extractMatrices(refple.energy, refple.mass, refple.charge, commands, self.parameters['order'] ) # Storing the transfer matrices self.tm_ffentrance, self.tm_part, self.tm_ffexit = (matrices[1], matrices[2], matrices[3]) self.tm_ffentrance.name = 'TransferMatrix for fringe field entrance of dipole' self.tm_ffexit.name = 'TransferMatrix for fringe field exit of dipole' self.tm_part.name = 'TransferMatrix for dipole' self.tm_full = None; self.tm_full = self.getTransferMatrix() self.parameterModified = False self.part_ds = rho*np.deg2rad(self.parameters['phi0'])/self.parts #### Calculating the change in profile planes for one part phirad = np.deg2rad(self.parameters['phi0']/self.parts) phirot = Rotation.from_rotvec([0.0,-phirad,0.0]) # Vector from entrance to exit self.part_dr = rho*np.array( [-1+np.cos(phirad), 0, np.sin(phirad) ] ) x0 = np.array([1.0,0.0,0.0]) y0 = np.array([0.0,1.0,0.0]) x1 = phirot.apply( x0 ) y1 = phirot.apply( y0 ) self.part_dx = x1-x0 self.part_dy = y1-y1
[docs] def length(self): return self.totalDeltaS
[docs]class GICOSYDrift(GICOSYElement): """Drift element Transports the beam through a length without electromagnetic interactions. Parameters ---------- refple : ReferenceParticle Reference particle for the element creation. dl : float Drift length of the element in m. order : int Order of the transfermatrix. maxSAdvance : float Maximal advance along the s axis in m. collimator : float or tuple, optional Collimator parameters if one is used. name : string, optional Name given to the element. description : string, optional Description of the element, currently not used. Attributes ---------- collimator : Collimator or None Collimator used in the element. parts : int Number of times the transfer matrix is to be used. Notes ----- The reference particle, the order and the driftlength are saved in the parameters. """ def __init__(self, refple, dl, order=1, maxSAdvance=0.2, collimator=None, name='Drift length', description='' ): super().__init__(refple, name=name, description=description) if collimator is not None and not isinstance(collimator, Collimator): self.collimator = Collimator(collimator) else: self.collimator = collimator self.parameters['order'] = order self.parameters['dl'] = dl self.parameters['ff_entrance'] = False self.parameters['ff_exit'] = False self.parameters['maxSAdvance'] = maxSAdvance self.parameters['displayfield'] = False self.parameterModified = True self.rebuild()
[docs] def length(self): return self.parameters['dl']
[docs] def height(self): try: return self.collimator.height() except: return 0.0
[docs] def plot(self, ax, view, s): if self.collimator is None: return ([], [], []) length = self.length() try: if 'x' in view: height = self.collimator.height('x') elif 'y' in view: height = self.collimator.height('y') else: raise ValueError except: height = self.collimator.height() try: thickness = self.parameters['thickness'] except: thickness = height * 0.1 fill = 'palegoldenrod' hatch_fill = 'royalblue' color = 'slategray' if PLOTBACKEND == 'matplotlib': from matplotlib import patches patch = patches.Rectangle((s, -height/2-thickness/2), length, height+thickness, fill=True, fc=fill, ec=color, zorder=0) hatched_1 = patches.Rectangle((s, height/2), length, thickness, fill=True, hatch='////', fc=hatch_fill, ec=color, zorder=0) hatched_2 = patches.Rectangle((s, -height/2-thickness), length, thickness, fill=True, hatch='////', fc=hatch_fill, ec=color, zorder=0) draw_patches = [patch, hatched_1, hatched_2]
[docs] def rebuild(self): if self.parameterModified == False: return self.parts = int(self.parameters['dl'] / self.parameters['maxSAdvance'] + 1) commands = ['DRIFT LENGTH {:g}'.format(self.parameters['dl']/self.parts)] refple = self.parameters['refple'] matrices = self.extractMatrices(refple.energy, refple.mass, refple.charge, commands, self.parameters['order']) self.tm_part = matrices[0] self.tm_full = None; self.tm_full = self.getTransferMatrix() self.part_ds = self.parameters['dl']/self.parts self.part_dr = np.array([0.0,0.0,self.part_ds])
[docs]class GICOSYQuadrupole(GICOSYElement): """Quadrupole element Interact with a magnetic or electric quadrupole with the given field over the given length. Parameters ---------- refple : ReferenceParticle Reference particle for the element creation. ftype : {'electric', 'magnetic'} String representing the type of quadrupole. dl : float Drift length of the element in m. field : float Voltage (V) or magnetic (T) field to be used in the quadrupole. G0 : float Aperture radius of the quad in m. ff_entrance : int, optional Fringe fields table number to use for entrance. Use zero for no fringe field. ff_exit : int, optional Fringe fields table number to use for exit. Use zero for no fringe field. order : int Order of the transfermatrix. maxSAdvance : float Maximal advance along the s axis in m. collimator : float or tuple, optional Collimator parameters if one is used. name : string, optional Name given to the element. description : string, optional Description of the element, currently not used. Attributes ---------- collimator : Collimator or None Collimator used in the element. parts : int Number of times the transfer matrix is to be used. Notes ----- The reference particle, the order and the driftlength are saved in the parameters. A warning will be sent into the logger if the aperture is larger that it should be. """ def __init__(self, refple, ftype, dl, field, G0, ff_entrance=1, ff_exit=1, order=1, maxSAdvance=0.2, collimator=None, name='Quadrupole', description=''): super().__init__(refple, name=name, description=description) self.collimator = collimator if collimator is None else Collimator(collimator) self.parameters['ftype'] = ftype self.parameters['dl'] = dl self.parameters['G0'] = G0 self.parameters['field'] = field self.parameters['refple'] = refple self.parameters['order'] = order self.parameters['ff_entrance'] = ff_entrance self.parameters['ff_exit'] = ff_exit self.parameters['maxSAdvance'] = maxSAdvance self.parameters['displayfield'] = True self.parameterModified = True self.rebuild()
[docs] def field(self, kind): if kind == self.parameters['ftype']: return self.parameters['field'] else: return 0.0
[docs] def length(self): return self.parameters['dl']
[docs] def height(self): return self.parameters['G0'] * 2
[docs] def plot(self, ax, view, s): length = self.length() height = self.height() try: thickness = self.parameters['thickness'] except: thickness = self.parameters['G0']*1.148 fill = 'palegoldenrod' hatch_fill = 'royalblue' color = 'slategray' if PLOTBACKEND == 'matplotlib': from matplotlib import patches patch = patches.Rectangle((s, -height/2-thickness/2), length, height+thickness, fill=True, fc=fill, ec=color, zorder=0) if self.parameters['ftype'] == 'electric': hatched_1 = patches.Rectangle((s, height/2), length, thickness, fill=True, hatch='////', fc=hatch_fill, ec=color, zorder=0) hatched_2 = patches.Rectangle((s, -height/2-thickness), length, thickness, fill=True, hatch='////', fc=hatch_fill, ec=color, zorder=0) # draw_patches.extend([hatched_1, hatched_2]) ax.add_patch(patch) if self.parameters['ftype'] == 'electric': ax.add_patch(hatched_1) ax.add_patch(hatched_2)
[docs] def rebuild(self): if self.parameterModified == False: return #print( "rebuilding drift with {:f}".format( self.parameters['dl']) ) self.parts = int(self.parameters['dl'] / self.parameters['maxSAdvance'] + 1) if not self.collimator: self.collimator = Collimator(self.parameters['G0']) else: try: if self.collimator.parameters['size'][0] > self.parameters['G0']: logger.warning('Quadrupole aperture is larger in the x direction than the pole gap! Results can be unphysical!') elif self.collimator.parameters['size'][1] > self.parameters['G0']: logger.warning('Quadrupole aperture is larger in the y direction than the pole gap! Results can be unphysical!') else: pass except: if self.collimator.parameters['size'] > self.parameters['G0']: logger.warning('Quadrupole aperture is larger than the pole gap! Results can be unphysical!') else: pass commands = [] # Need to have a drift length in order to generate the # fringe field. This matrix is not used nor passed for a user. commands.append('DRIFT LENGTH 0.1') commands.append('FRINGING FIELD {}'.format(self.parameters['ff_entrance'])) s = self.parameters['dl']/self.parts F = self.parameters['field'] G0 = self.parameters['G0'] refple = self.parameters['refple'] if self.parameters['ftype'] == 'magnetic': commands.append('M Q {:g} {:g} {:g}'.format(s, F, G0)) else: commands.append('E Q {:g} {:g} {:g}'.format(s, 1e-3*F, G0)) commands.append('FRINGING FIELD {}'.format(self.parameters['ff_exit'])) # Need to add drift length in order to generate the fringe field. commands.append('DRIFT LENGTH 0.1') matrices = self.extractMatrices(refple.energy, refple.mass, refple.charge, commands, self.parameters['order']) self.tm_ffentrance = matrices[1] self.tm_part = matrices[2] self.tm_ffexit = matrices[3] self.tm_full = None self.tm_full = self.getTransferMatrix() self.part_ds = self.parameters['dl']/self.parts self.part_dr = np.array([0.0,0.0,self.part_ds])
ff_format_str = """*----------------------------------- {} | {} | | {} {} {} {} | | | {} {} {} {} | | | {} {} {} """ MSparams = { 1: (0.205133, 0.840972, -0.141308, 0.050050, 0.000076, 0.005197, 0, 0, 0, 0, 0), 2: (2, 0.20513185638, 0.8409723305, -0.141308269, 5.00500213E-02, 7.5965396644E-05, 5.1969989319E-03, 0, 0, 0, 0, 0), 3: (0.38855444, 2.40798992, -0.834568931, 0.19822262, 2.444162E-03, -3.187845E-04, 0, 0, 0, 0, 0), 4: (0.061479475, 1.771726174, -0.158179632, 0.056507703, 0.033236894, 0.005135692, 0, 0, 0, 0, 0), 5: (0.056658, 1.008271, -0.016882, -0.003549, 0.000139, 0.000013, 0, 0, 0, 0, 0), 6: (0.478959, 0.955644, -0.296488, -0.067666, 0.009941, 0.176659, 0, 0, 0, 0, 0), 7: (0.183486031, 2.573644333, -0.207994544, -0.091821834, -0.002378879, 0.003844973, 0, 0, 0, 0, 0), 8: (0.285850000, 1.235990000, -0.291610000, 0.443700000, -0.205160000, 0.055770000, 0, 0, 0, 0, 0), } ESparams = { 1: (0.2049, 0.84105, -0.14135, 0.05005, 6.875E-5, 5.1968E-3, 0, 0, 0, 0, 0), 2: (0.75, 0.09, 90, 0, 0, 0, 0, 0, 0, 0, 0), 3: (0.38855444, 2.40798992, -0.834568931, 0.19822262, 2.444162E-03, -3.187845E-04, 0, 0, 0, 0, 0), 4: (0.061479475, 1.771726174, -0.158179632, 0.056507703, 0.033236894, 0.005135692, 0, 0, 0, 0, 0), 7: (0.183486031, 2.573644333, -0.207994544, -0.091821834, -0.002378879, 0.003844973, 0, 0, 0, 0, 0), } MQparams = { 1: (0, 3.59463, 0, -0, 0, 0, 0, 0, 0, 0, 0), 2: (0, 3.59463, 0, -0, 0, 0, 0, 0, 0, 0, 0), 3: (0.10391, 6.27098, -1.51225, 3.59925, -2.13239, 1.72300, 0, 0, 0, 0, 0), 4: (0.388554, 2.407990, -0.834569, 0.198223, 0.002444, -0.000319, 0, 0, 0, 0, 0), 5: (0.3091603650, 2.3792544734, -0.4781358927, 3.56933296E-02, 3.980007740E-04, 6.649291231E-04, 0, 0, 0, 0, 0), 6: (0.2964295664, 2.266630258, -0.56775282622, 0.1335785413, -2.274501006530167E-03, 6.95656239929E-04, 0, 0, 0, 0, 0), 9: (0.271647162, 2.83514462, -0.602080979, 2.604809986E-02, 7.275585244E-03, 1.550781439E-03, 0, 0, 0, 0, 0), } EQparams = { 1: (0, 3.59276, 0, -0.081520, 0, -0.00177, 0, 0, 0, 0, 0), 2: (-0.77363101, 8.26159995, 0, 0, 0, 0, 0, 0, 0, 0, 0), 3: (0.10391, 6.27098, -1.51225, 3.59925, -2.13239, 1.72300, 0, 0, 0, 0, 0), 4: (0.388554, 2.407990, -0.834569, 0.198223, 0.002444, -0.000319, 0, 0, 0, 0, 0), 5: (0, 2.407990, 0, 0, 0, 0, 0, 0, 0, 0, 0), 6: (0.2964295664, 2.266630258, -0.56775282622, 0.1335785413, -2.274501006530167E-03, 6.95656239929E-04, 0, 0, 0, 0, 0), 7: (0.176659, 3.576539, -0.778279, 0.430539, -0.123546, 0.016877, 0, 0, 0, 0, 0), 9: (0.271647162, 2.83514462, -0.602080979, 2.604809986E-02, 7.275585244E-03, 1.550781439E-03, 0, 0, 0, 0, 0), }
[docs]def setFringeField(element, tableno, params, force=False): """Set fringe field for an element type and table number tableno. By default PIOL uses a built-in standard table for fringe fields. Currently the fringe field data contains mostly nonsense. Parameters ---------- element : str The element type. One of {'MS', 'ES', 'MQ', 'EQ'}. tableno : int The table number to set. Valid table numbers are from 1 to 9. params : array-like An array of polynomial coefficients describing the fringe field with the Enge model. Lowest order first. Maximum of 11th order polynomial is supported. force : boolean Forces the overwriting of the GICOSYFF.DAT file. Defaults to 'False'. """ params = np.array(params) if len(params) > 11: raise ValueError('Enge model supports a maximum of 11th order polynomial, {:d} terms was given!'.format(len(params))) if params[params.nonzero()][-1] < 0: raise ValueError('Highest order term is zero in Enge model - not physical!') mapping = {'MS': MSparams, 'ES': ESparams, 'MQ': MQparams, 'EQ': EQparams} if element not in mapping.keys(): raise ValueError('Unknown element type {} for fringe field!'.format(element)) tableno = int(tableno) if (tableno > 9 ) or (tableno < 1): raise ValueError('Invalid table number {}, use numbers 1-9!'.format(tableno)) params.resize(11) mapping[element][tableno] = params writeFringeFields(force=force)
[docs]def writeFringeFields(force=False): """Writes the GICOSYFF.DAT fringe field data file. Overwrites the fringe field data file GICOSYFF.DAT if marked with tag '* PIOL', otherwise rises an exception. Overwriting the fringe field data file can be forced by setting force=True. """ readGICOSYPath() global _gicosy_path mapping = {'MS': MSparams, 'ES': ESparams, 'MQ': MQparams, 'EQ': EQparams} # Read the GICOSYFF.DAT and check the tag if not force: with cd(_gicosy_path): with open('GICOSYFF.DAT', 'r') as gff: s = gff.readline() if s[:6] != '* PIOL': raise Exception('GICOSYFF.DAT not written by PIOL. Avoiding overwriting.\nBackup if necessary and use writeFringeFields(force=True) to proceed.') # Writing the GICOSYFF.DAT fringe field data file s = '* PIOL has written this file\n' for key in mapping.keys(): if key.lower() == 'custom': continue for k in mapping[key].keys(): s += ff_format_str.format(key, k, *mapping[key][k]) with cd(_gicosy_path): with open('GICOSYFF.DAT', 'w') as gff: gff.write(s)
def Enge(z, a, D): """"The Enge fringe field shape evaluator. Returns the relative field strength as a function of position along the beam line according to the Enge model, i.e. E = 1 / (1+exp( a[0] + a[1]*z/D + a[2]*(z/D)^2 + a[3]*(z/D)^3 + ... )) Parameters: ----------- z : float Coordinate along the beam line relative to the physical edge of the element. The Enge fringe field model describes the field going into the element, i.e. negative coordinates are outside the element (where this function returns 0) and positive coordinates are inside (where this function returns 1). a : ndarray An array of polynomial coefficients describing the fringe field with the Enge model. Lowest order first. D : The physical scaling factor for the element (usually half the element gap size). """ z = -z/D v = np.polyval(a[::-1], z) E = 1/(1+np.exp(v)) return E def calculateEnge(element, tableno, face='entrance'): """Calculate the Enge fringe field for element Returns the field shape as a function of z-coordinate near the element edge. The returned (z,E) contains value pairs, which describe the field shape from field value of 0.0001 to 0.9999. Parameters: ----------- element : Element The element object tableno : int The table number to use. Valid table numbers are from 1 to 9. face : str Either 'entrance' or 'exit' Returns: -------- tuple (z,E), where z is the z-coordinates and E is the scaled fringe field. """ D = element.height()/2 if type(element) == GICOSYDipole: map = {'electric': ESparams, 'magnetic': MSparams} elif type(element) == GICOSYQuadrupole: map = {'electric': EQparams, 'magnetic': MQparams} else: raise Exception('Unknown element type') params = map[element.parameters['ftype']][tableno] try: from scipy.optimize import root_scalar function = lambda z: Enge(z, params, D) - 0.9999 result_max = root_scalar(function, x0=0, method='brentq', bracket=[0, 10*D], maxiter=100) function = lambda z: Enge(z, params, D) - 0.0001 result_min = root_scalar(function, x0=0, method='brentq', bracket=[-10*D, 0], maxiter=100) z = np.linspace(result_min.root, result_max.root, 100) except: logger.warning('Could not find fringe field limits') z = np.linspace(-D, D, 100) y = Enge(z, params, D) if type(element) == GICOSYDipole: # Adjust scale along optical axis due to dipole face angle if face == 'entrance': z = z/np.cos(np.radians(element.parameters['eps_entrance'])) else: z = z/np.cos(np.radians(element.parameters['eps_exit'])) return z, y