class HardEnergy(ScannableMotionBase): """ Create beam energy scannable that encapsulates and fan-outs control to ID gap and DCM energy. This pseudo device requies a lookup table object to provide ID parameters for calculation of ID gap from beam energy required and harmonic order. The lookup table object must be created before the instance creation of this class. The child scannables or pseudo devices must exist in jython's global namespace prior to any method call of this class instance. The lookup Table object is described by gda.function.LookupTable class. """ def __init__(self, name, idgap, dcmenergy, lut, gap_offset=None, feedbackPVs=None): """ Constructor - Only succeeds if it finds the lookup table, otherwise raises exception. """ self.lut = readLookupTable( LocalProperties.get("gda.config") + "/lookupTables/" + lut) self.gap = idgap self.mono_energy = dcmenergy self.lambdau = 27 # undulator period self.scannables = ScannableGroup(name, [dcmenergy, idgap]) self.detune = gap_offset self.feedbackPVs = feedbackPVs self._busy = 0 self.setName(name) self.setLevel(3) self.setOutputFormat(["%10.6f"]) self.inputNames = [name] self.order = 3 self.SCANNING = False self.logger = logger.getChild(self.__class__.__name__) def harmonicEnergyRanges(self): """ Prints out a table of harmonics with corresponding minimum and maximum energies """ print("%s\t%s\t%s" % ("Harmonic", "Min Energy", "Max Energy")) keys = [int(key) for key in self.lut.keys()] for key in sorted(keys): print("%8.0d\t%10.2f\t%10.2f" % (key, self.lut[key][2], self.lut[key][3])) def energyRangeForOrder(self, order): """Returns a tuple with min and max energies for a harmonic order Args: order (int): The order of the harmonic Returns: (min_energy, max_energy) (tuple) """ return (self.lut[order][2], self.lut[order][3]) def setOrder(self, n): """Method to set the harmonic order""" self.order = n def getOrder(self): """Method to retrieve the harmonic order""" return self.order def idgap(self, Ep, n): """ Function to calculate the insertion device gap Arguments: Ep -- Energy n -- order """ lambda_u = self.lambdau M = 4 h = 16 me = 0.510999 gamma = 1000 * self.lut[n][0] / me k_squared = (4.959368e-6 * (n * gamma * gamma / (lambda_u * Ep)) - 2) if k_squared < 0: raise ValueError("k_squared must be positive!") K = math.sqrt(k_squared) A = ((2 * 0.0934 * lambda_u * self.lut[n][1] * M / math.pi) * math.sin(math.pi / M) * (1 - math.exp(-2 * math.pi * h / lambda_u))) gap = (lambda_u / math.pi) * math.log(A / K) + self.lut[n][6] if self.detune: gap = gap + float(self.detune.getPosition()) self.logger.debug("Required gap calculated to be {}".format(gap)) return gap def rawGetPosition(self): """Returns the current position of the beam energy.""" return self.mono_energy.getPosition() def calc(self, energy, order): return self.idgap(energy, order) def moveDevices(self, energy, gap): for scannable in self.scannables.getGroupMembers(): if scannable.getName() == self.gap.getName(): try: scannable.asynchronousMoveTo(gap) except: print("cannot set %s to %f " % (scannable.getName(), gap)) raise elif scannable.getName() == self.mono_energy.getName(): try: scannable.asynchronousMoveTo(energy) sleep(0.1) # Allow time for s to become busy except: print("cannot set %s to %f" % (scannable.getName(), energy)) raise def rawAsynchronousMoveTo(self, new_position): """ move beam energy to specified value. In the background this moves both ID gap and Mono Bragg to the values corresponding to this energy. If a child scannable can not be reached for whatever reason, it just prints out a message, then continue to next. """ min_energy, max_energy = self.energyRangeForOrder(self.order) energy = float(new_position) self.logger.debug( ("rawAsynchronousMoveTo called for energy {}. " "min_energy for order is: {}, max_energy is: {}").format( energy, min_energy, max_energy)) gap = self.idgap(energy, self.order) if not min_energy < energy < max_energy: raise ValueError(("Requested photon energy {} is out of range for " "harmonic {}: min: {}, max: {}").format( energy, self.order, min_energy, max_energy)) if self.feedbackPVs is not None and not self.SCANNING: caput(self.feedbackPVs[0], 1) caput(self.feedbackPVs[1], 1) self.moveDevices(energy, gap) self.waitWhileBusy() caput(self.feedbackPVs[0], 0) caput(self.feedbackPVs[1], 0) else: self.moveDevices(energy, gap) def isBusy(self): """ Checks the busy status of all child scannable. If and only if all child scannable are done this will be set to False. """ self._busy = 0 for scannable in self.scannables.getGroupMembers(): try: self._busy += scannable.isBusy() except: self.logger.error(scannable.getName() + "isBusy() method threw exception:", exc_info=True) raise if self._busy == 0: return 0 else: return 1 def toString(self): """formats what to print to the terminal console.""" return self.name + " : " + str(self.rawGetPosition()) def atScanStart(self): self.SCANNING = True def atScanEnd(self): self.SCANNING = False
class BeamEnergyPolarisationClass(ScannableMotionBase): '''Coupled beam energy and polarisation scannable that encapsulates and fan-outs control to ID gap, row phase, and PGM energy. This pseudo device requires a lookupTable table object to provide ID parameters for calculation of ID idgap from beam energy required and harmonic order. The lookupTable table object must be created before the instance creation of this class. The child scannables or pseudo devices must exist in jython's global namespace prior to any method call of this class instance. ''' harmonicOrder = 1 def __init__(self, name, idctrl, pgmenergy, lut="JIDEnergy2GapCalibrations.csv", energyConstant=False, polarisationConstant=False, gap_offset=None, feedbackPV=None): '''Constructor - Only succeed if it find the lookupTable table, otherwise raise exception.''' self.lut,self.header = load_lookup_table(LocalProperties.get("gda.config")+"/lookupTables/"+lut) self.idscannable = idctrl self.mono_energy = pgmenergy self.scannables = ScannableGroup(name, [pgmenergy, idctrl]) self.detune = gap_offset self.feedbackPV = feedbackPV self._busy = 0 self.setName(name) self.setLevel(3) self.setOutputFormat(["%10.6f"]) self.setInputNames([name]) self.setExtraNames([]) self.polarisation = 'LH' self.gap = 50 self.phase = 0 self.energyConstant = energyConstant self.polarisationConstant = polarisationConstant self.SCANNING = False if self.energyConstant and self.polarisationConstant: raise RuntimeError("Cannot create an instance with both energy and polarisation being constant.") self.isConfigured = False self.inputSignSwitched = False self.beamlinename = LocalProperties.get(LocalProperties.GDA_BEAMLINE_NAME) self.logger = logger.getChild(self.__class__.__name__) def configure(self): if self.idscannable is not None: self.maxGap=self.idscannable.getController().getMaxGapPos() self.minGap=self.idscannable.getController().getMinGapPos() self.maxPhase=self.idscannable.getController().getMaxPhaseMotorPos() self.isConfigured=True def getIDPositions(self): '''get gap and phase from ID hardware controller, and set polarisation mode in GDA 'idscannable' instance This method sync current object states with ID state in EPICS IOC. ''' result = list(self.idscannable.getPosition()) gap = float(result[0]) polarisation = str(result[1]) if BeamEnergyPolarisationClass.harmonicOrder > 1: # support other harmonic polarisation = str(polarisation)+str(BeamEnergyPolarisationClass.harmonicOrder) phase = float(result[2]) return (gap, polarisation, phase) def showFittingCoefficentsLookupTable(self): formatstring="%4s\t%11s\t%11s\t%11s\t%11s\t%11s\t%11s\t%11s\t%11s\t%11s\t%11s\t%11s\t%11s" print (formatstring % tuple([x for x in self.header])) for key, value in sorted(self.lut.iteritems()): print (formatstring % tuple([x for x in key] + [x for x in value])) def setOrder(self,n): BeamEnergyPolarisationClass.harmonicOrder = n def getOrder(self): return BeamEnergyPolarisationClass.harmonicOrder def idgap(self, energy): '''return gap for the given energy for current polarisation. used in cvscan where polarisation doesn't change during continuous energy moving. ''' gap, polarisation, phase = self.getIDPositions() # @UnusedVariable gap, phase = self.idgapphase(Ep=energy, mode=polarisation) # @UnusedVariable return gap def idgapphase(self, Ep=None, mode='LH'): '''coverts energy and polarisation to gap and phase. It supports polarisation modes: LH, LV, CR, CL, and LH3 ''' phase = 0 #phase value for LH and LV is ignored by self.idscannable if mode in ["LH", "LV", "CR", "CL", "LH3"]: #polarisation is constant in these modes coef = get_fitting_coefficents(mode, Ep, self.lut) gap = coef[0] + coef[1]*Ep + coef[2]*Ep**2 +coef[3]*Ep**3 + coef[4]*Ep**4 + coef[5]*Ep**5 + coef[6]*Ep**6 + coef[7]*Ep**7 + coef[8]*Ep**8 + coef[9]*Ep**9 #adjust gap if self.detune: gap = gap + float(self.detune.getPosition()) if (gap<self.minGap or gap>self.maxGap): #IDGroup Excel table only cover this range raise ValueError("Required Soft X-Ray ID gap is %s out side allowable bound (%s, %s)!" % (gap, self.minGap, self.maxGap)) if mode == "LH3": BeamEnergyPolarisationClass.harmonicOrder = 3 if mode == "LH": BeamEnergyPolarisationClass.harmonicOrder = 1 if mode == "LV": BeamEnergyPolarisationClass.harmonicOrder = 1 phase = self.maxPhase if mode in ["CR", "CL"]: BeamEnergyPolarisationClass.harmonicOrder = 1 phase=15.0 else: raise ValueError("Unsupported polarisation mode, only LH, LV, CR, CL, and LH3 are supported.") if phase < 0 or phase > self.maxPhase: #Physical limits of ID Row Phase raise ValueError("Required Soft X-Ray ID phase is %s out side allowable bound (%s, %s)!" % (phase, 0, self.maxPhase)) return (gap, phase) def calc(self, energy, order=1): message = "'order' input is no longer required. this is now merged into polarisation mode in the calibration lookup table!" print(message) self.logger.warn(message) return self.idgap(energy) def rawGetPosition(self): '''returns the current beam energy, or polarisation, or both.''' gap, polarisation, phase = self.getIDPositions() # @UnusedVariable energy=float(self.mono_energy.getPosition()/1000.0) #energy unit is in keV if polarisation in ["LH","LV","CR","CL","LH3"]: if self.polarisationConstant: return energy elif self.energyConstant: self.setOutputFormat(["%s"]) self.polarisation = polarisation return polarisation else: self.setOutputFormat(["%10.6f","%s"]) self.polarisation = polarisation return energy, polarisation def moveDevices(self, gap, new_polarisation, phase, energy): for s in self.scannables.getGroupMembers(): if str(s.getName()) == str(self.idscannable.getName()): try: if new_polarisation == "LH3" : new_polarisation = "LH" s.asynchronousMoveTo([gap, new_polarisation, phase]) except: print("cannot set %s to [%f, %s, %f]" % (s.getName(), gap, new_polarisation, phase)) raise elif not self.energyConstant: try: s.asynchronousMoveTo(energy * 1000) except: print("cannot set %s to %f." % (s.getName(), energy)) raise def rawAsynchronousMoveTo(self, new_position): '''move beam energy, polarisation, or both to specified values. At the background this moves both ID gap, phase, and PGM energy to the values corresponding to this energy, polarisation or both. If a child scannable can not be reached for whatever reason, it just prints out a message, then continue to next.''' gap = 20 new_polarisation = None phase = 0 try: if not self.SCANNING: #ensure ID hardware in sync in 'pos' command self.rawGetPosition() #parse arguments as it could be 1 or 2 inputs, string or number type, depending on polarisation mode and instance attribute value if not isinstance(new_position, list): # single argument self.logger.debug("Single argument: {} given".format(type(new_position))) if isinstance(new_position, basestring): #polarisation change requested energy=float(self.mono_energy.getPosition())/1000.0 #get existing energy if self.polarisationConstant: #input must be for energy raise ValueError("Input value must be a number.") new_polarisation=str(new_position) if not new_polarisation in ["LH", "LV","CR", "CL", "LH3"]: raise ValueError('Input value must be one of valid polarisation mode: "LH", "LV","CR", "CL", "LH3"') elif isinstance(new_position, numbers.Number): # energy change requested if self.polarisationConstant: #input must be for energy energy=float(new_position) gap, new_polarisation, phase = self.getIDPositions() #get existing polarisation else: raise ValueError("Polarisation is not constant, but a number: {} was given".format(new_position)) else: raise ValueError("Input value must be a string or number.") else: #2 arguments args = list(new_position) if len(args) != 2: raise ValueError("Expect 2 arguments but got %s" % len(args)) if isinstance(args[0], numbers.Number): self.logger.debug("Two arguments given and first argument {} is a number".format(args[0])) energy = float(args[0]) #range validation is done later else: raise ValueError("1st input for energy must be a number") if isinstance(args[1], basestring): new_polarisation = args[1] else: raise ValueError("2nd input for polarisation must be a string") gap, phase=self.idgapphase(Ep=energy, mode=new_polarisation) except: raise #re-raise any exception from above try block if self.feedbackPV is not None and not self.SCANNING: #stop feedback from gdascripts.utils import caput caput(self.feedbackPV, 1) self.moveDevices(gap, new_polarisation, phase, energy) self.waitWhileBusy() caput(self.feedbackPV, 0) else: self.moveDevices(gap, new_polarisation, phase, energy) def isBusy(self): '''checks the busy status of all child scannables. If and only if all child scannables are done this will be set to False. ''' if self.getName() == "dummyenergy" or self.getName()=="dummypolarisation": sleep(0.1) return False else: #real hardware self._busy=0 for s in self.scannables.getGroupMembers(): try: self._busy += s.isBusy() except: print (s.getName() + " isBusy() throws exception ", sys.exc_info()) raise if self._busy == 0: return 0 else: return 1 def stop(self): self.mono_energy.stop() if installation.isLive(): print("ID motion stop is not supported according to ID-Group instruction. Please wait for the Gap motion to complete!") else: self.idscannable.stop() def atScanStart(self): self.rawGetPosition() #ensure ID hardware in sync at start of scan self.SCANNING=True def atScanEnd(self): self.SCANNING=False
class SoftEnergy(ScannableMotionBase): """ Create beam energy scannable that encapsulates and fan-outs control to ID gap and DCM energy. This pseudo device requires a lookup table object to provide allowed energy ranges. However The lookup table object must be created before the instance creation of this class. Equations to calculate insertion device gaps are hard-coded into this class. The child scannables or pseudo devices must exist in Jython's global namespace prior to any method call of this class instance. The lookup Table object is described by gda.function.LookupTable class. """ def __init__(self, name, idgap, pgmenergy, lut, gap_offset=None, feedbackPV=None): """ Constructor - Only succeeds if it finds the lookup table, otherwise raises exception. """ self.lut = readLookupTable(LocalProperties.get("gda.config") + "/lookupTables/" + lut) self.gap = idgap self.mono_energy = pgmenergy self.scannables = ScannableGroup(name, [pgmenergy, idgap]) self.detune=gap_offset self.feedbackPV=feedbackPV self._busy = 0 self.setName(name) self.setLevel(3) self.setOutputFormat(["%10.6f"]) self.inputNames = [name] self.SCANNING=False self.order = 1 self.polarisation = 'LH' self.jidphase = Finder.find("jidphase") self.logger = logger.getChild(self.__class__.__name__) def setPolarisation(self, value): """Sets the polarisation.""" if value == "LH" or value == "LH3": self.jidphase.hortizontal() self.polarisation=value elif value == "LV": self.jidphase.vertical() self.polarisation = value elif value == "CL": self.jidphase.circular_left() self.polarisation = value elif value == "CR": self.jidphase.circular_right() self.polarisation = value else: raise ValueError("Input " + str(value) + " invalid. Valid values are 'LH', 'LV', 'CL' and 'CR'.") # Move back to the current position i.e. the correct gap for the new polarisation # Note this also causes the ID to actually move, if the gap demand is exactly the same it will never! self.asynchronousMoveTo(self.getPosition()) while (self.isBusy()) : sleep(0.5) def getPolarisation(self): """Returns the current polarisation (cached in object not directly from EPICS)""" return self.polarisation def harmonicEnergyRanges(self): """Prints out a table of harmonics with corresponding min and max energies""" print ("%s\t%s\t%s" % ("Harmonic", "Min Energy", "Max Energy")) keys = [int(key) for key in self.lut.keys()] for key in sorted(keys): print ("%8.0d\t%10.2f\t%10.2f" % (key, self.lut[key][2], self.lut[key][3])) def energyRangeForOrder(self, order): """Returns a tuple with min and max energies for a harmonic order Args: order (int): The order of the harmonic Returns: (min_energy, max_energy) (tuple) """ return (self.lut[order][2], self.lut[order][3]) def idgap(self, Ep, n): """ Function to calculate the insertion device gap Arguments: Ep -- Energy n -- order """ gap = 20.0 self.logger.debug("'idgap' function called with energy {} and order {}" .format(Ep, n)) self.logger.debug("Current cached polarisation is {}" .format(self.polarisation)) # Soft ID J branch # Linear Horizontal if self.getPolarisation() == "LH": if (Ep < 0.104 or Ep > 1.2): raise ValueError("Polarisation = LH but the demanding energy is outside the valid range between 0.104 and 1.2 keV!") # gap=3.06965 +177.99974*Ep -596.79184*Ep**2 +1406.28911*Ep**3 -2046.90669*Ep**4 +1780.26621*Ep**5 -844.81785*Ep**6 +168.99039*Ep**7 # gap=2.75529 + 184.24255*Ep - 639.07279*Ep**2 +1556.23192*Ep**3 -2340.01233*Ep**4 +2100.81252*Ep**5 -1027.88771*Ep**6 +211.47063*Ep**7 gap=0.52071 + 238.56372*Ep - 1169.06966*Ep**2 +4273.03275*Ep**3 -10497.36261*Ep**4 +17156.91928*Ep**5 -18309.05195*Ep**6 +12222.50318*Ep**7 -4623.70738*Ep**8 +755.90853*Ep**9 if (gap < 16 or gap > 60): raise ValueError("Required Soft X-Ray ID gap is out side allowable bound (16, 60)!") # Linear Horizontal 3rd Harmonic for 400 line/mm grating elif (self.getPolarisation()=="LH3"): if (Ep<0.7 or Ep > 1.95): raise ValueError("Polarisation = LH3 but the demanding energy is outside the valid range between 0.7 and 1.9 keV!") gap=10.98969 + 25.8301*Ep - 9.36535*Ep**2 + 1.74461*Ep**3 if (gap < 16 or gap > 60): raise ValueError("Required Soft X-Ray ID gap is out side allowable bound (16, 60)!") # Linear Vertical elif self.getPolarisation() == "LV": if (Ep < 0.22 or Ep > 1.0): raise ValueError("Demanding energy must lie between 0.22 and 1.0 eV!") gap = (5.33595 + 72.53678 * Ep - 133.96826 * Ep ** 2 + 179.99229 * Ep ** 3 - 128.83048 * Ep ** 4 + 39.34346 * Ep ** 5) if (gap < 16.01 or gap > 60): raise ValueError("Required Soft X-Ray ID gap is out side allowable bound (16, 60)!") # Circular left elif self.getPolarisation() == "CL": if (Ep < 0.145 or Ep > 1.2): raise ValueError("Demanding energy must lie between 0.146 and 1.2 eV!") # Circular left gap polymonimal gap = (5.32869 + 101.28316 * Ep - 192.74788 * Ep ** 2 + 249.91788 * Ep ** 3 - 167.93323 * Ep ** 4 + 47.22008 * Ep ** 5 - 0.054 * Ep - .0723) # Check the gap is possible if (gap < 16.01 or gap > 60): raise ValueError("Required Soft X-Ray ID gap is out side allowable bound (16, 60)!") # Circular right elif self.getName() == "jenergy" and self.getPolarisation() == "CR": if (Ep < 0.145 or Ep > 1.2): raise ValueError("Demanding energy must lie between 0.1 and 1.2 eV!") # Circular right gap polymonimal gap = (5.32869 + 101.28316 * Ep - 192.74788 * Ep ** 2 + 249.91788 * Ep ** 3 - 167.93323 * Ep ** 4 + 47.22008 * Ep ** 5) # Check the gap is possible if (gap < 16.01 or gap > 60): raise ValueError("Required Soft X-Ray ID gap is out side allowable bound (16, 60)!") # Unsupported else: raise ValueError("Unsupported scannable or polarisation mode") return gap def rawGetPosition(self): """returns the current position of the beam energy.""" return self.mono_energy.getPosition()/1000.0 def calc(self, energy, order): return self.idgap(energy, order) def moveDevices(self, energy, gap): for s in self.scannables.getGroupMembers(): if s.getName() == self.gap.getName(): try: if self.detune: gap = gap + float(self.detune.getPosition()) self.logger.debug("Calling asynchronousMoveTo() on {} with gap {}".format(s.getName(), gap)) s.asynchronousMoveTo(gap) except: self.logger.error("cannot set " + s.getName() + " to " + str(gap), exc_info=True) raise else: try: self.logger.debug("Calling asynchronousMoveTo() on {} with energy {}".format(s.getName(), energy * 1000)) s.asynchronousMoveTo(energy * 1000) # Allow time for s to become busy sleep(0.1) except: self.logger.error("Can not set " + s.getName() + " to " + str(energy), exc_info=True) raise def rawAsynchronousMoveTo(self, new_position): """ move beam energy to specified value. At the background this moves both ID gap and Mono Bragg to the values corresponding to this energy. If a child scannable can not be reached for whatever reason, it just prints out a message, then continue to next. """ energy = float(new_position) gap = self.idgap(energy, self.order) if self.feedbackPV is not None and not self.SCANNING: caput(self.feedbackPV, 1) self.moveDevices(energy, gap) self.waitWhileBusy() caput(self.feedbackPV, 0) else: self.moveDevices(energy, gap) def isBusy(self): """ checks the busy status of all child scannable. If and only if all child scannable are done this will be set to False. """ self._busy = 0 for s in self.scannables.getGroupMembers(): try: self._busy += s.isBusy() except: print ("%s isBusy() throws exception" % (s.getName()), sys.exc_info()) raise if self._busy == 0: return 0 else: return 1 def toString(self): """formats what to print to the terminal console.""" return self.name + " : " + str(self.rawGetPosition()) def atScanStart(self): self.SCANNING=True def atScanEnd(self): self.SCANNING=False
class BeamEnergy(ScannableMotionBase): '''Create beam energy scannable that encapsulates and fan-outs control to ID gap and DCM energy. This pseudo device requies a lookup table object to provide ID parameters for calculation of ID gap from beam energy required and harmonic order. The lookup table object must be created before the instance creation of this class. The child scannables or pseudo devices must exist in jython's global namespace prior to any method call of this class instance. The lookup Table object is described by gda.function.LookupTable class.''' def __init__(self, name, gap="jgap", dcm="pgmenergy", undulatorperiod=27, lut="JIDCalibrationTable.txt"): '''Constructor - Only succeed if it find the lookup table, otherwise raise exception.''' self.lut=readLookupTable(LocalProperties.get("gda.config")+"/lookupTables/"+lut) self.gap=gap self.dcm=dcm self.lambdau=undulatorperiod if dcm is None: self.scannableNames=[gap] else: self.scannableNames=[dcm,gap] self.scannables=ScannableGroup(name, [Finder.find(x) for x in self.scannableNames]) self._busy=0 self.setName(name) self.setLevel(3) self.setOutputFormat(["%10.6f"]) self.inputNames=[name] if self.dcm == "dcmenergy": self.order=3 else: self.order=1 self.energy=self.scannables.getGroupMember(self.scannableNames[0]).getPosition() self.polarisation='H' def setPolarisation(self, value): if self.getName()=="jenergy": if value == "H" or value == "V": self.polarisation=value else: raise ValueError("Input "+str(value)+" invalid. Valid values are 'H' or 'V'.") else: print "No polaristion parameter for Hard X-ray ID" def getPolarisation(self): if self.getName()=="jenergy": return self.polarisation else: return "No polaristion parameter for Hard X-ray ID" def HarmonicEnergyRanges(self): print ("%s\t%s\t%s" % ("Harmonic", "Min Energy", "Max Energy")) keys=[int(key) for key in self.lut.keys()] for key in sorted(keys): print ("%8.0d\t%10.2f\t%10.2f" % (key,self.lut[key][2],self.lut[key][3])) def eneryRangeForOrder(self, order): return [self.lut[order][2],self.lut[order][3]] def setOrder(self,n): self.order=n def getOrder(self): return self.order def idgap(self, Ep, n): gap=20.0 if self.getName() == "ienergy": lambdaU=self.lambdau M=4 h=16 me=0.510999 gamma=1000*self.lut[n][0]/me Ksquared=(4.959368e-6*(n*gamma*gamma/(lambdaU*Ep))-2) if Ksquared < 0: raise ValueError("Ksquared must be positive!") K=math.sqrt(Ksquared) A=(2*0.0934*lambdaU*self.lut[n][1]*M/math.pi)*math.sin(math.pi/M)*(1-math.exp(-2*math.pi*h/lambdaU)) gap=(lambdaU/math.pi) * math.log(A/K)+self.lut[n][6] # if self.gap=="igap" and (gap<5.1 or gap>9.1): # raise ValueError("Required Hard X-Ray ID gap is out side allowable bound (5.1, 9.1)!") if self.gap=="jgap" and gap<16: raise ValueError("Required Soft X-Ray ID gap is out side allowable bound (>=16)!") elif (self.getName() == "jenergy" and self.getPolarisation()=="H"): if (Ep<0.11 or Ep > 1.2): raise ValueError("Demanding energy must lie between 0.11 and 1.2 keV!") Epgap = Ep*1000 # gap=3.46389+0.17197*Epgap + -5.84455e-4*Epgap**2 + 1.43759e-6*Epgap**3 + -2.2321e-9*Epgap**4 + 2.09444e-12*Epgap**5 + -1.07453e-15*Epgap**6 + 2.3039e-19*Epgap**7 gap= 0.70492 + 232.97156*Ep - 1100.88615*Ep**2 + 3841.94972*Ep**3 - 8947.83296*Ep**4 + 13823.07663*Ep**5 - 13942.57738*Ep**6 + 8816.18277*Ep**7 - 3170.55571*Ep**8 + 495.16057*Ep**9 if self.gap=="jgap" and (gap<16 or gap>200): raise ValueError("Required Soft X-Ray ID gap is below the lower bound 0f 16 mm!") elif self.getName() == "jenergy" and self.getPolarisation()=="V": if (Ep<0.21 or Ep > 1.2): raise ValueError("Demanding energy must lie between 0.21 and 1.2 keV!") gap = 4.02266 + 89.86963*Ep - 220.65942*Ep**2 + 365.46127*Ep**3 - 168.84016*Ep**4 - 560.87782*Ep**5 + 1255.06201*Ep**6 - 1164.15704*Ep**7 + 531.63871*Ep**8 - 97.25326*Ep**9 if self.gap=="jgap" and (gap<16.05 or gap>40.24): raise ValueError("Required Soft X-Ray ID gap is out side allowable bound (16.05, 40.24)!") else: raise ValueError("Unsupported scannable or polarisation mode") return gap def rawGetPosition(self): '''returns the current position of the beam energy.''' self.energy=self.scannables.getGroupMember(self.scannableNames[0]).getPosition() return self.energy; def calc(self, energy, order): return self.idgap(energy, order) def rawAsynchronousMoveTo(self, new_position): '''move beam energy to specified value. At the background this moves both ID gap and Mono Bragg to the values corresponding to this energy. If a child scannable can not be reached for whatever reason, it just prints out a message, then continue to next.''' self.energy = float(new_position) gap = 7 try: if self.getName() == "dummyenergy": gap=self.energy else: gap=self.idgap(self.energy, self.order) except: raise if self.getName() == "ienergy": if self.energy<self.eneryRangeForOrder(self.order)[0] or self.energy>self.eneryRangeForOrder(self.order)[1]: raise ValueError("Requested photon energy is out of range for this harmonic!") for s in self.scannables.getGroupMembers(): if s.getName() == self.gap: try: s.asynchronousMoveTo(gap) except: print "cannot set " + s.getName() + " to " + str(gap) raise else: try: if s.getName() == "pgmenergy": s.asynchronousMoveTo(self.energy*1000) # caput("ELECTRON-ANALYSER-01:TEST:EXCITATION_ENERGY", self.energy*1000) else: s.asynchronousMoveTo(self.energy) # caput("ELECTRON-ANALYSER-01:TEST:EXCITATION_ENERGY", self.energy*1000) except: print "cannot set " + s.getName() + " to " + str(self.energy) raise def isBusy(self): '''checks the busy status of all child scannable. If and only if all child scannable are done this will be set to False.''' self._busy=0 for s in self.scannables.getGroupMembers(): try: self._busy += s.isBusy() except: print s.getName() + " isBusy() throws exception ", sys.exc_info() raise if self._busy == 0: return 0 else: return 1 def toString(self): '''formats what to print to the terminal console.''' return self.name + " : " + str(self.rawGetPosition())
class BeamEnergyPolarisationClass(ScannableMotionBase): '''Coupled beam energy and polarisation scannable that encapsulates and fan-outs control to ID gap, row phase, and PGM energy. This pseudo device requires a lookupTable table object to provide ID parameters for calculation of ID idgap from beam energy required and harmonic order. The lookupTable table object must be created before the instance creation of this class. The child scannables or pseudo devices must exist in jython's global namespace prior to any method call of this class instance. ''' def __init__(self, name, idctrl, pgmenergy, lut="JIDEnergy2GapCalibrations.txt", energyConstant=False, polarisationConstant=False): '''Constructor - Only succeed if it find the lookupTable table, otherwise raise exception.''' self.lut=loadLookupTable(LocalProperties.get("gda.config")+"/lookupTables/"+lut) self.idscannable=idctrl self.pgmenergy=pgmenergy self.scannables=ScannableGroup(name, [pgmenergy, idctrl]) self._busy=0 self.setName(name) self.setLevel(3) self.setOutputFormat(["%10.6f"]) self.setInputNames([name]) self.setExtraNames([]) self.order=1 self.polarisation=0.0 self.gap=50 self.polarisationMode='UNKNOWN' self.phase=0 self.energyConstant=energyConstant self.polarisationConstant=polarisationConstant self.SCANNING=False if self.energyConstant and self.polarisationConstant: raise Exception("Cannot create an instance with both energy and polarisation being constant.") self.isConfigured=False self.inputSignSwitched=False self.beamlinename=LocalProperties.get(LocalProperties.GDA_BEAMLINE_NAME) self.logger = logger.getChild(self.__class__.__name__) def configure(self): if self.idscannable is not None: self.maxGap=self.idscannable.getController().getMaxGapPos() self.minGap=self.idscannable.getController().getMinGapPos() self.maxPhase=self.idscannable.getController().getMaxPhaseMotorPos() self.isConfigured=True def getIDPositions(self): '''get gap and phase from ID hardware controller, and set polarisation mode in GDA 'idscannable' instance This method sync current object states with ID state in EPICS IOC. ''' result=list(self.idscannable.getPosition()) gap=float(result[0]) polarisationMode=str(result[1]) phase=float(result[2]) return (gap, polarisationMode, phase) def showFittingCoefficentsLookupTable(self): formatstring="%12s\t%12s\t%12s\t%12s\t%12s\t%12s\t%12s\t%12s" print (formatstring % ("Mode", "Min Energy", "Max Energy", "Coefficent0", "Coefficent1", "Coefficent2", "Coefficent3", "Coefficent4")) for key, value in sorted(self.lut.iteritems()): print (formatstring % (key[0],key[1],key[2],value[0],value[1],value[2],value[3],value[4])) def setHarmonic(self,n): self.order=n def getHarmonic(self): return self.order def idgapphase(self, Ep=None, mode='LH',n=1): '''coverts energy and polarisation to gap and phase. It supports polarisation modes: LH, LV, CR, CL, LAP, LAN. Harmonic order is not yet implemented. ''' gap=20.0 phase=0 #phase value for LH and LV is ignored by self.idscannable if mode in ["LH", "LV", "CR", "CL"]: Ep=Ep/n #polarisation is constant in these modes coef=getFittingCoefficents(mode, Ep, self.lut) gap = coef[0] + coef[1]*Ep + coef[2]*Ep**2 +coef[3]*Ep**3 + coef[4]*Ep**4 + coef[5]*Ep**5 + coef[6]*Ep**6 + coef[7]*Ep**7 if (gap<self.minGap or gap>self.maxGap): #IDGroup Excel table only cover this range raise ValueError("Required Soft X-Ray ID gap is %s out side allowable bound (%s, %s)!" % (gap, self.minGap, self.maxGap)) if mode == "LV": phase=self.maxPhase if mode in ["CR", "CL"]: if (self.beamlinename=="i09" or self.beamlinename=="i09-2"): phase=15.0 else: phase = 12.92907548 + 0.37353288*gap + -0.00614332*gap**2 + 5.3209E-06*gap**3 + 2.00631E-06*gap**4 + -3.9185E-08*gap**5 + 3.17986E-10*gap**6 + -9.93646E-13*gap**7 else: raise ValueError("Unsupported polarisationMode mode, only LH, LV, CR and CL are supported.") if phase<0 or phase>self.maxPhase: #Physical limits of ID Row Phase raise ValueError("Required Soft X-Ray ID phase is %s out side allowable bound (%s, %s)!" % (phase, 0, self.maxPhase)) return (gap, phase) def calc(self, energy, mode): return self.idgapphase(energy, mode, 1) def rawGetPosition(self): '''returns the current beam energy, or polarisation, or both.''' gap, polarisationMode, phase = self.getIDPositions() if (self.beamlinename=="i09" or self.beamlinename=="i09-2"): energy=self.pgmenergy.getPosition()/1000.0 else: energy=self.pgmenergy.getPosition() polarisation=polarisationMode if polarisationMode in ["LH","LV","CR","CL"]: if self.polarisationConstant: return energy elif self.energyConstant: self.setOutputFormat(["%s"]) self.polarisation = polarisation return polarisation else: self.setOutputFormat(["%10.6f","%s"]) self.polarisation = polarisation return energy, polarisation def rawAsynchronousMoveTo(self, new_position): '''move beam energy, polarisation, or both to specified value. At the background this moves both ID gap, phase, and PGM energy to the values corresponding to this energy, polarisation or both. If a child scannable can not be reached for whatever reason, it just prints out a message, then continue to next.''' gap=20 newPolarisationMode=None phase=0 try: if not self.SCANNING: #ensure ID hardware in sync in 'pos' command self.rawGetPosition() #parse arguments as it could be 1 or 2 inputs, string or number type, depending on polarisation mode and instance attribute value if not isinstance(new_position, list): # single argument self.logger.debug("Single argument: {} given".format(type(new_position))) if isinstance(new_position, basestring): energy=float(self.pgmenergy.getPosition()) if (self.beamlinename=="i09" or self.beamlinename=="i09-2"): energy = energy/1000 if self.polarisationConstant: #input must be for energy raise ValueError("Input value must be a number.") newPolarisationMode=str(new_position) if not newPolarisationMode in ["LH", "LV","CR", "CL"]: raise ValueError('Input value must be one of valid polarisation mode: "LH", "LV","CR", "CL"') elif isinstance(new_position, numbers.Number): if self.polarisationConstant: #input must be for energy energy=float(new_position) #energy validation is done in getFittingCoefficent() method gap, polarisationMode, phase = self.getIDPositions() newPolarisationMode=polarisationMode #using existing polarisation mode else: self.logger.error("Polarisation is not constant, but a number: {} was given".format(new_position)) raise ValueError("If using energypolarisation, input value must be a list in the form [energy, polarisation].") else: raise ValueError("Input value must be a string or number.") else: #2 arguments args = list(new_position) if len(args) != 2: raise ValueError("Expect 2 arguments but got %s" % len(args)) if isinstance(args[0], numbers.Number): self.logger.debug("Two arguments given and first argument {} is a number".format(args[0])) energy=float(args[0]) #range validation is done later else: self.logger.error("Two arguments given and first argument {} is not a number".format(args[0])) raise ValueError("1st input for energy must be a number") if isinstance(args[1], basestring): newPolarisationMode=args[1] else: self.logger.error("Two arguments given and second argument {} is not a string".format(args[1])) raise ValueError("2nd input for polarisation must be a string") if self.polarisationConstant: self.logger.debug("Polarisation is constant, getting gap and phase with energy {} and mode {}".format(energy, newPolarisationMode)) gap, phase = self.idgapphase(Ep=energy, mode=newPolarisationMode, n=self.order) elif self.energyConstant: self.logger.debug("Energy is constant, getting gap and phase with energy {} and mode {}".format(energy, newPolarisationMode)) gap, phase = self.idgapphase(Ep=energy, mode=newPolarisationMode, n=self.order) else: gap, phase=self.idgapphase(Ep=energy, mode=newPolarisationMode, n=self.order) except: raise #re-raise any exception from above try block for s in self.scannables.getGroupMembers(): #print s.getName(), self.idscannable.getName() if str(s.getName()) == str(self.idscannable.getName()): try: s.asynchronousMoveTo([gap, newPolarisationMode, phase]) #print "moving %s to [%f, %s,%f]" % (s.getName(), gap, newPolarisationMode, phase) except: print "cannot set %s to [%f, %s, %f]" % (s.getName(), gap, newPolarisationMode, phase) raise else: if self.energyConstant: #polarisation change only continue #do not need to move PGM energy else: try: if (self.beamlinename=="i09" or self.beamlinename=="i09-2"): s.asynchronousMoveTo(energy*1000) else: s.asynchronousMoveTo(energy) except: print "cannot set %s to %f." % (s.getName(), energy) raise def isBusy(self): '''checks the busy status of all child scannables. If and only if all child scannables are done this will be set to False. ''' if self.getName() == "dummyenergy" or self.getName()=="dummypolarisation": sleep(0.1) return False else: #real hardware self._busy=0 for s in self.scannables.getGroupMembers(): try: self._busy += s.isBusy() except: print s.getName() + " isBusy() throws exception ", sys.exc_info() raise if self._busy == 0: return 0 else: return 1 def atScanStart(self): self.rawGetPosition() #ensure ID hardware in sync at start of scan self.SCANNING=True def atScanEnd(self): self.SCANNING=False