diff --git a/ophyd_devices/epics/DeviceFactory.py b/ophyd_devices/epics/DeviceFactory.py index ad25d94..88db585 100644 --- a/ophyd_devices/epics/DeviceFactory.py +++ b/ophyd_devices/epics/DeviceFactory.py @@ -25,47 +25,36 @@ fp = open(f"{path}/db/test_database.yml", "r") lut_db = yaml.load(fp, Loader=yaml.Loader) # Load SLS common database (already in DB) -#fp = open(f"{path}/db/machine_database.yml", "r") -#lut_db = yaml.load(fp, Loader=yaml.Loader) +# fp = open(f"{path}/db/machine_database.yml", "r") +# lut_db = yaml.load(fp, Loader=yaml.Loader) # Load beamline specific database -bl = os.getenv('BEAMLINE_XNAME', "X12SA") +bl = os.getenv("BEAMLINE_XNAME", "X12SA") fp = open(f"{path}/db/{bl.lower()}_database.yml", "r") lut_db.update(yaml.load(fp, Loader=yaml.Loader)) def createProxy(name: str, connect=True) -> OphydObject: - """ Factory routine to create an ophyd device with a pre-defined schema. - Does nothing if the device is already an OphydObject! - """ + """Factory routine to create an ophyd device with a pre-defined schema. + Does nothing if the device is already an OphydObject! + """ if issubclass(type(name), OphydObject): return name - - entry = lut_db[name] + + entry = lut_db[name] cls_candidate = globals()[entry["type"]] print(f"Device candidate: {cls_candidate}") - try: - if issubclass(cls_candidate, OphydObject): - ret = cls_candidate(**entry["config"]) - if connect: - ret.wait_for_connection(timeout=5) - return ret - else: - raise RuntimeError(f"Unsupported return class: {schema}") - except TypeError: - # Simulated devices - if issubclass(type(cls_candidate), OphydObject): - return cls_candidate - else: - raise RuntimeError(f"Unsupported return class: {schema}") + if issubclass(cls_candidate, OphydObject): + ret = cls_candidate(**entry["config"]) + if connect: + ret.wait_for_connection(timeout=5) + return ret + else: + raise RuntimeError(f"Unsupported return class: {entry["type"]}") if __name__ == "__main__": for key in lut_db: print(key) dut = createProxy(str(key)) - - - - diff --git a/ophyd_devices/epics/proxies/DelayGeneratorDG645.py b/ophyd_devices/epics/proxies/DelayGeneratorDG645.py index f3f6ed6..b4cc615 100644 --- a/ophyd_devices/epics/proxies/DelayGeneratorDG645.py +++ b/ophyd_devices/epics/proxies/DelayGeneratorDG645.py @@ -11,79 +11,78 @@ from ophyd.pseudopos import pseudo_position_argument, real_position_argument, Ps class DelayStatic(Device): - """ Static axis for the T0 output channel - It allows setting the logic levels, but the timing is fixed. - The signal is high after receiving the trigger until the end - of the holdoff period. + """Static axis for the T0 output channel + It allows setting the logic levels, but the timing is fixed. + The signal is high after receiving the trigger until the end + of the holdoff period. """ # Other channel stuff ttl_mode = Component(EpicsSignal, "OutputModeTtlSS.PROC", kind=Kind.config) nim_mode = Component(EpicsSignal, "OutputModeNimSS.PROC", kind=Kind.config) polarity = Component(EpicsSignal, "OutputPolarityBI", write_pv="OutputPolarityBO", name='polarity', kind=Kind.config) amplitude = Component(EpicsSignal, "OutputAmpAI", write_pv="OutputAmpAO", name='amplitude', kind=Kind.config) - polarity = Component(EpicsSignal, "OutputOffsetAI", write_pv="OutputOffsetAO", name='offset', kind=Kind.config) + polarity = Component(EpicsSignal, "OutputOffsetAI", write_pv="OutputOffsetAO", name='offset', kind=Kind.config) + - class DummyPositioner(Device, PositionerBase): setpoint = Component(EpicsSignal, "DelayAO", kind=Kind.config) readback = Component(EpicsSignalRO, "DelayAI", kind=Kind.config) - + class DelayPair(PseudoPositioner): - """ Delay pair interface for DG645 - - Virtual motor interface to a pair of signals (on the frontpanel). - It offers a simple delay and pulse width interface for scanning. + """Delay pair interface for DG645 + + Virtual motor interface to a pair of signals (on the frontpanel). + It offers a simple delay and pulse width interface for scanning. """ # The pseudo positioner axes delay = Component(PseudoSingle, limits=(0, 2000.0), name='delay') - width = Component(PseudoSingle, limits=(0, 2000.0), name='pulsewidth') + width = Component(PseudoSingle, limits=(0, 2000.0), name='pulsewidth') # The real delay axes ch1 = Component(DummyPositioner, name='ch1') - ch2 = Component(DummyPositioner, name='ch2') - + ch2 = Component(DummyPositioner, name='ch2') + def __init__(self, *args, **kwargs): # Change suffix names before connecting (a bit of dynamic connections) self.__class__.__dict__['ch1'].suffix = kwargs['channel'][0] self.__class__.__dict__['ch2'].suffix = kwargs['channel'][1] - del kwargs['channel'] + del kwargs['channel'] # Call parent to start the connections super().__init__(*args, **kwargs) - + @pseudo_position_argument def forward(self, pseudo_pos): - '''Run a forward (pseudo -> real) calculation''' + """Run a forward (pseudo -> real) calculation""" return self.RealPosition(ch1=pseudo_pos.delay, ch2=pseudo_pos.delay+pseudo_pos.width) @real_position_argument def inverse(self, real_pos): - '''Run an inverse (real -> pseudo) calculation''' - return self.PseudoPosition(delay=real_pos.ch1, width=real_pos.ch2 - real_pos.ch1) - - + """Run an inverse (real -> pseudo) calculation""" + return self.PseudoPosition(delay=real_pos.ch1, width=real_pos.ch2 - real_pos.ch1) + + class DelayGeneratorDG645(Device): - """ DG645 delay generator - - This class implements a thin Ophyd wrapper around the Stanford Research DG645 - digital delay generator. - - Internally, the DG645 generates 8+1 signals: A, B, C, D, E, F, G, H and T0 - Front panel outputs T0, AB, CD, EF and GH are a combination of these signals. - Back panel outputs are directly routed signals. So signals are NOT INDEPENDENT. - - - Front panel signals: - All signals go high after their defined delays and go low after the trigger - holdoff period, i.e. this is the trigger window. Front panel outputs provide - a combination of these events. - Option 1 back panel 5V signals: - All signals go high after their defined delays and go low after the trigger - holdoff period, i.e. this is the trigger window. The signals will stay high - until the end of the window. - Option 2 back panel 30V signals: - All signals go high after their defined delays for ~100ns. This is fixed by - electronics (30V needs quite some power). This is not implemented in the - current device + """DG645 delay generator + + This class implements a thin Ophyd wrapper around the Stanford Research DG645 + digital delay generator. + + Internally, the DG645 generates 8+1 signals: A, B, C, D, E, F, G, H and T0 + Front panel outputs T0, AB, CD, EF and GH are a combination of these signals. + Back panel outputs are directly routed signals. So signals are NOT INDEPENDENT. + + Front panel signals: + All signals go high after their defined delays and go low after the trigger + holdoff period, i.e. this is the trigger window. Front panel outputs provide + a combination of these events. + Option 1 back panel 5V signals: + All signals go high after their defined delays and go low after the trigger + holdoff period, i.e. this is the trigger window. The signals will stay high + until the end of the window. + Option 2 back panel 30V signals: + All signals go high after their defined delays for ~100ns. This is fixed by + electronics (30V needs quite some power). This is not implemented in the + current device """ state = Component(EpicsSignalRO, "EventStatusLI", name='status_register') status = Component(EpicsSignalRO, "StatusSI", name='status') @@ -94,7 +93,7 @@ class DelayGeneratorDG645(Device): channelCD = Component(DelayPair, "", name='CD', channel="CD") channelEF = Component(DelayPair, "", name='EF', channel="EF") channelGH = Component(DelayPair, "", name='GH', channel="GH") - + # Minimum time between triggers holdoff = Component(EpicsSignal, "TriggerHoldoffAI", write_pv="TriggerHoldoffAO", name='trigger_holdoff', kind=Kind.config) inhibit = Component(EpicsSignal, "TriggerInhibitMI", write_pv="TriggerInhibitMO", name='trigger_inhibit', kind=Kind.config) @@ -112,15 +111,14 @@ class DelayGeneratorDG645(Device): burstDelay = Component(EpicsSignal, "BurstDelayAI", write_pv="BurstDelayAO", name='burstdelay', kind=Kind.config) burstPeriod = Component(EpicsSignal, "BurstPeriodAI", write_pv="BurstPeriodAO", name='burstperiod', kind=Kind.config) - def stage(self): """Trigger the generator by arming to accept triggers""" self.arm.write(1).wait() - + def unstage(self): """Stop the trigger generator from accepting triggers""" self.arm.write(0).wait() - + def burstEnable(self, count, delay, period, config="all"): """Enable the burst mode""" # Validate inputs @@ -139,16 +137,12 @@ class DelayGeneratorDG645(Device): self.burstConfig.set(0).wait() elif config=="first": self.burstConfig.set(1).wait() - + def busrtDisable(self): """Disable the burst mode""" self.burstMode.set(0).wait() - - - -# pair = DelayPair("DGEN01:", name="delayer", channel="CD") -dgen = DelayGeneratorDG645("X01DA-PC-DGEN:", name="delayer") - - - - + + +# Automatically connect to test environmenr if directly invoked +if __name__ == "__main__": + dgen = DelayGeneratorDG645("X01DA-PC-DGEN:", name="delayer") diff --git a/ophyd_devices/epics/proxies/InsertionDevice.py b/ophyd_devices/epics/proxies/InsertionDevice.py index 53cc976..ef0bb99 100644 --- a/ophyd_devices/epics/proxies/InsertionDevice.py +++ b/ophyd_devices/epics/proxies/InsertionDevice.py @@ -2,13 +2,13 @@ from ophyd import PVPositioner, Component, EpicsSignal, EpicsSignalRO, Kind class InsertionDevice(PVPositioner): - """ Python wrapper for the CSAXS insertion device control - - This wrapper provides a positioner interface for the ID control. - is completely custom XBPM with templates directly in the - VME repo. Thus it needs a custom ophyd template as well... + """Python wrapper for the CSAXS insertion device control - WARN: The x and y are not updated by the IOC + This wrapper provides a positioner interface for the ID control. + is completely custom XBPM with templates directly in the + VME repo. Thus it needs a custom ophyd template as well... + + WARN: The x and y are not updated by the IOC """ status = Component(EpicsSignalRO, "-USER:STATUS", auto_monitor=True) errorSource = Component(EpicsSignalRO, "-USER:ERROR-SOURCE", auto_monitor=True) @@ -24,20 +24,3 @@ class InsertionDevice(PVPositioner): # (NA for important devices) if __name__ == "__main__": pass - - - - - - - - - - - - - - - - - diff --git a/ophyd_devices/epics/proxies/SpmBase.py b/ophyd_devices/epics/proxies/SpmBase.py index b080ac2..88ca98b 100644 --- a/ophyd_devices/epics/proxies/SpmBase.py +++ b/ophyd_devices/epics/proxies/SpmBase.py @@ -2,15 +2,16 @@ import numpy as np from ophyd import Device, Component, EpicsSignal, EpicsSignalRO import matplotlib.pyplot as plt + class SpmBase(Device): - """ Python wrapper for the Staggered Blade Pair Monitors - - SPM's consist of a set of four horizontal tungsten blades and are - used to monitor the beam height (only Y) for the bending magnet - beamlines of SLS. - - Note: EPICS provided signals are read only, but the user can - change the beam position offset. + """Python wrapper for the Staggered Blade Pair Monitors + + SPM's consist of a set of four horizontal tungsten blades and are + used to monitor the beam height (only Y) for the bending magnet + beamlines of SLS. + + Note: EPICS provided signals are read only, but the user can + change the beam position offset. """ # Motor interface s1 = Component(EpicsSignalRO, "Current1", auto_monitor=True) @@ -21,17 +22,17 @@ class SpmBase(Device): y = Component(EpicsSignalRO, "Y", auto_monitor=True) scale = Component(EpicsSignal, "PositionScaleY", auto_monitor=True) offset = Component(EpicsSignal, "PositionOffsetY", auto_monitor=True) - + class SpmSim(SpmBase): - """ Python wrapper for simulated Staggered Blade Pair Monitors - - SPM's consist of a set of four horizontal tungsten blades and are - used to monitor the beam height (only Y) for the bending magnet - beamlines of SLS. + """Python wrapper for simulated Staggered Blade Pair Monitors - This simulation device extends the basic proxy with a script that - fills signals with quasi-randomized values. + SPM's consist of a set of four horizontal tungsten blades and are + used to monitor the beam height (only Y) for the bending magnet + beamlines of SLS. + + This simulation device extends the basic proxy with a script that + fills signals with quasi-randomized values. """ # Motor interface s1w = Component(EpicsSignal, "Current1:RAW.VAL", auto_monitor=False) @@ -42,25 +43,25 @@ class SpmSim(SpmBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - + self._MX = 0 self._MY = 0 self._I0 = 255.0 self._x = np.linspace(-5, 5, 64) self._y = np.linspace(-5, 5, 64) self._x, self._y = np.meshgrid(self._x, self._y) - + def _simFrame(self): """Generator to simulate a jumping gaussian""" # define normalized 2D gaussian def gaus2d(x=0, y=0, mx=0, my=0, sx=1, sy=1): return np.exp(-((x - mx)**2. / (2. * sx**2.) + (y - my)**2. / (2. * sy**2.))) - + #Generator for dynamic values self._MX = 0.75 * self._MX + 0.25 * (10.0 * np.random.random()-5.0) self._MY = 0.75 * self._MY + 0.25 * (10.0 * np.random.random()-5.0) self._I0 = 0.75 * self._I0 + 0.25 * (255.0 * np.random.random()) - + arr = self._I0 * gaus2d(self._x, self._y, self._MX, self._MY) return arr @@ -84,7 +85,7 @@ class SpmSim(SpmBase): #plt.imshow(beam) #plt.show(block=False) plt.pause(0.5) - + # Automatically start simulation if directly invoked if __name__ == "__main__": @@ -101,4 +102,3 @@ if __name__ == "__main__": print("---") spm1.sim() spm2.sim() - diff --git a/ophyd_devices/epics/proxies/XbpmBase.py b/ophyd_devices/epics/proxies/XbpmBase.py index bcd30bd..5f6d13b 100644 --- a/ophyd_devices/epics/proxies/XbpmBase.py +++ b/ophyd_devices/epics/proxies/XbpmBase.py @@ -4,12 +4,12 @@ import matplotlib.pyplot as plt class XbpmCsaxsOp(Device): - """ Python wrapper for custom XBPMs in the cSAXS optics hutch - - This is completely custom XBPM with templates directly in the - VME repo. Thus it needs a custom ophyd template as well... + """Python wrapper for custom XBPMs in the cSAXS optics hutch - WARN: The x and y are not updated by the IOC + This is completely custom XBPM with templates directly in the + VME repo. Thus it needs a custom ophyd template as well... + + WARN: The x and y are not updated by the IOC """ sum = Component(EpicsSignalRO, "SUM", auto_monitor=True) x = Component(EpicsSignalRO, "POSH", auto_monitor=True) @@ -21,16 +21,16 @@ class XbpmCsaxsOp(Device): class XbpmBase(Device): - """ Python wrapper for X-ray Beam Position Monitors - - XBPM's consist of a metal-coated diamond window that ejects - photoelectrons from the incoming X-ray beam. These electons - are collected and their current is measured. Effectively - they act as four quadrant photodiodes and are used as BPMs - at the undulator beamlines of SLS. - - Note: EPICS provided signals are read only, but the user can - change the beam position offset. + """Python wrapper for X-ray Beam Position Monitors + + XBPM's consist of a metal-coated diamond window that ejects + photoelectrons from the incoming X-ray beam. These electons + are collected and their current is measured. Effectively + they act as four quadrant photodiodes and are used as BPMs + at the undulator beamlines of SLS. + + Note: EPICS provided signals are read only, but the user can + change the beam position offset. """ # Motor interface s1 = Component(EpicsSignalRO, "Current1", auto_monitor=True) @@ -48,23 +48,20 @@ class XbpmBase(Device): offsetV = Component(EpicsSignal, "PositionOffsetY", auto_monitor=False) - - - class XbpmSim(XbpmBase): - """ Python wrapper for simulated X-ray Beam Position Monitors - - XBPM's consist of a metal-coated diamond window that ejects - photoelectrons from the incoming X-ray beam. These electons - are collected and their current is measured. Effectively - they act as four quadrant photodiodes and are used as BPMs - at the undulator beamlines of SLS. - - Note: EPICS provided signals are read only, but the user can - change the beam position offset. + """Python wrapper for simulated X-ray Beam Position Monitors - This simulation device extends the basic proxy with a script that - fills signals with quasi-randomized values. + XBPM's consist of a metal-coated diamond window that ejects + photoelectrons from the incoming X-ray beam. These electons + are collected and their current is measured. Effectively + they act as four quadrant photodiodes and are used as BPMs + at the undulator beamlines of SLS. + + Note: EPICS provided signals are read only, but the user can + change the beam position offset. + + This simulation device extends the basic proxy with a script that + fills signals with quasi-randomized values. """ # Motor interface s1w = Component(EpicsSignal, "Current1:RAW.VAL", auto_monitor=False) @@ -75,25 +72,25 @@ class XbpmSim(XbpmBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - + self._MX = 0 self._MY = 0 - self._I0 = 255.0 + self._I0 = 255.0 self._x = np.linspace(-5, 5, 64) self._y = np.linspace(-5, 5, 64) - self._x, self._y = np.meshgrid(self._x, self._y) - + self._x, self._y = np.meshgrid(self._x, self._y) + def _simFrame(self): """Generator to simulate a jumping gaussian""" # define normalized 2D gaussian def gaus2d(x=0, y=0, mx=0, my=0, sx=1, sy=1): return np.exp(-((x - mx)**2. / (2. * sx**2.) + (y - my)**2. / (2. * sy**2.))) - + #Generator for dynamic values self._MX = 0.75 * self._MX + 0.25 * (10.0 * np.random.random()-5.0) self._MY = 0.75 * self._MY + 0.25 * (10.0 * np.random.random()-5.0) self._I0 = 0.75 * self._I0 + 0.25 * (255.0 * np.random.random()) - + arr = self._I0 * gaus2d(self._x, self._y, self._MX, self._MY) return arr @@ -117,7 +114,7 @@ class XbpmSim(XbpmBase): #plt.imshow(beam) #plt.show(block=False) plt.pause(0.5) - + # Automatically start simulation if directly invoked if __name__ == "__main__": @@ -134,20 +131,3 @@ if __name__ == "__main__": print("---") xbpm1.sim() xbpm2.sim() - - - - - - - - - - - - - - - - - diff --git a/ophyd_devices/epics/proxies/mono_dccm.py b/ophyd_devices/epics/proxies/mono_dccm.py index 25f2414..4adeded 100644 --- a/ophyd_devices/epics/proxies/mono_dccm.py +++ b/ophyd_devices/epics/proxies/mono_dccm.py @@ -3,11 +3,10 @@ Created on Wed Oct 13 18:06:15 2021 @author: mohacsi_i + +IMPORTANT: Virtual monochromator axes should be implemented already in EPICS!!! """ - - - import numpy as np from math import isclose from ophyd import EpicsSignal, EpicsSignalRO, EpicsMotor, PseudoPositioner, PseudoSingle, Device, Component, Kind @@ -16,9 +15,10 @@ from ophyd.sim import SynAxis, Syn2DGauss LN_CORR = 2e-4 + def a2e(angle, hkl=[1,1,1], lnc=False, bent=False, deg=False): - """ Convert between angle and energy for Si monchromators - ATTENTION: 'angle' must be in radians, not degrees! + """Convert between angle and energy for Si monchromators + ATTENTION: 'angle' must be in radians, not degrees! """ lncorr = LN_CORR if lnc else 0.0 angle = angle*np.pi/180 if deg else angle @@ -30,28 +30,28 @@ def a2e(angle, hkl=[1,1,1], lnc=False, bent=False, deg=False): def e2w(energy): - """ Convert between energy and wavelength + """Convert between energy and wavelength """ return 0.1 * 12398.42 / energy def w2e(wwl): - """ Convert between wavelength and energy + """Convert between wavelength and energy """ return 12398.42 * 0.1 / wwl def e2a(energy, hkl=[1,1,1], lnc=False, bent=False): - """ Convert between energy and angle for Si monchromators - ATTENTION: 'angle' must be in radians, not degrees! + """Convert between energy and angle for Si monchromators + ATTENTION: 'angle' must be in radians, not degrees! """ lncorr = LN_CORR if lnc else 0.0 - # Lattice constant along direction + # Lattice constant along direction d0 = 2*5.43102 * (1.0-lncorr) / np.linalg.norm(hkl) angle = np.arcsin(12.39842/d0/energy) - # Rfine for bent mirror + # Rfine for bent mirror if bent: rho = 2 * 19.65 * 8.35 / 28 * np.sin(angle) dt = 0.2e-3 / rho * 0.279 @@ -61,37 +61,35 @@ def e2a(energy, hkl=[1,1,1], lnc=False, bent=False): return angle - - class MonoMotor(PseudoPositioner): - """ Monochromator axis - - Small wrapper to combine a real angular axis with the corresponding energy. - ATTENTION: 'angle' is in degrees, at least for PXIII + """Monochromator axis + + Small wrapper to combine a real angular axis with the corresponding energy. + ATTENTION: 'angle' is in degrees, at least for PXIII """ # Real axis (in degrees) angle = Component(EpicsMotor, "", name='angle') # Virtual axis energy = Component(PseudoSingle, name='energy') - + _real = ['angle'] @pseudo_position_argument def forward(self, pseudo_pos): return self.RealPosition(angle=180.0*e2a(pseudo_pos.energy)/3.141592) - + @real_position_argument def inverse(self, real_pos): - return self.PseudoPosition(energy=a2e(3.141592*real_pos.angle/180.0)) - - + return self.PseudoPosition(energy=a2e(3.141592*real_pos.angle/180.0)) + + class MonoDccm(PseudoPositioner): - """ Combined DCCM monochromator - - The first crystal selects the energy, the second one is only following. - DCCMs are quite simple in terms that they can't crash and we don't - have a beam offset. - ATTENTION: 'angle' is in degrees, at least for PXIII + """Combined DCCM monochromator + + The first crystal selects the energy, the second one is only following. + DCCMs are quite simple in terms that they can't crash and we don't + have a beam offset. + ATTENTION: 'angle' is in degrees, at least for PXIII """ # Real axis (in degrees) @@ -110,21 +108,16 @@ class MonoDccm(PseudoPositioner): @pseudo_position_argument def forward(self, pseudo_pos): - """ - WARNING: We have an overdefined system! Not sure if common crystal movement is reliable without retuning - - """ + """WARNING: We have an overdefined system! Not sure if common crystal movement is reliable without retuning""" if abs(pseudo_pos.energy-self.energy.position) > 0.0001 and abs(pseudo_pos.en1-self.en1.position) < 0.0001 and abs(pseudo_pos.en2-self.en2.position) < 0.0001: # Probably the common energy was changed return self.RealPosition(th1=-180.0*e2a(pseudo_pos.energy)/3.141592, th2=180.0*e2a(pseudo_pos.energy)/3.141592) else: # Probably the individual axes was changes return self.RealPosition(th1=-180.0*e2a(pseudo_pos.en1)/3.141592, th2=180.0*e2a(pseudo_pos.en2)/3.141592) - + @real_position_argument def inverse(self, real_pos): return self.PseudoPosition(en1=-a2e(3.141592*real_pos.th1/180.0), en2=a2e(3.141592*real_pos.th2/180.0), - energy=-a2e(3.141592*real_pos.th1/180.0)) - - + energy=-a2e(3.141592*real_pos.th1/180.0)) diff --git a/ophyd_devices/epics/proxies/quadem.py b/ophyd_devices/epics/proxies/quadem.py deleted file mode 100644 index 25f2414..0000000 --- a/ophyd_devices/epics/proxies/quadem.py +++ /dev/null @@ -1,130 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Oct 13 18:06:15 2021 - -@author: mohacsi_i -""" - - - - -import numpy as np -from math import isclose -from ophyd import EpicsSignal, EpicsSignalRO, EpicsMotor, PseudoPositioner, PseudoSingle, Device, Component, Kind -from ophyd.pseudopos import pseudo_position_argument, real_position_argument -from ophyd.sim import SynAxis, Syn2DGauss - -LN_CORR = 2e-4 - -def a2e(angle, hkl=[1,1,1], lnc=False, bent=False, deg=False): - """ Convert between angle and energy for Si monchromators - ATTENTION: 'angle' must be in radians, not degrees! - """ - lncorr = LN_CORR if lnc else 0.0 - angle = angle*np.pi/180 if deg else angle - - # Lattice constant along direction - d0 = 5.43102 * (1.0-lncorr) / np.linalg.norm(hkl) - energy = 12.39842 / (2.0 * d0 * np.sin(angle)) - return energy - - -def e2w(energy): - """ Convert between energy and wavelength - """ - return 0.1 * 12398.42 / energy - - -def w2e(wwl): - """ Convert between wavelength and energy - """ - return 12398.42 * 0.1 / wwl - - -def e2a(energy, hkl=[1,1,1], lnc=False, bent=False): - """ Convert between energy and angle for Si monchromators - ATTENTION: 'angle' must be in radians, not degrees! - """ - lncorr = LN_CORR if lnc else 0.0 - - # Lattice constant along direction - d0 = 2*5.43102 * (1.0-lncorr) / np.linalg.norm(hkl) - angle = np.arcsin(12.39842/d0/energy) - - # Rfine for bent mirror - if bent: - rho = 2 * 19.65 * 8.35 / 28 * np.sin(angle) - dt = 0.2e-3 / rho * 0.279 - d0 = 2 * 5.43102 * (1.0+dt) / np.linalg.norm(hkl) - angle = np.arcsin(12.39842/d0/energy) - - return angle - - - - -class MonoMotor(PseudoPositioner): - """ Monochromator axis - - Small wrapper to combine a real angular axis with the corresponding energy. - ATTENTION: 'angle' is in degrees, at least for PXIII - """ - # Real axis (in degrees) - angle = Component(EpicsMotor, "", name='angle') - # Virtual axis - energy = Component(PseudoSingle, name='energy') - - _real = ['angle'] - - @pseudo_position_argument - def forward(self, pseudo_pos): - return self.RealPosition(angle=180.0*e2a(pseudo_pos.energy)/3.141592) - - @real_position_argument - def inverse(self, real_pos): - return self.PseudoPosition(energy=a2e(3.141592*real_pos.angle/180.0)) - - -class MonoDccm(PseudoPositioner): - """ Combined DCCM monochromator - - The first crystal selects the energy, the second one is only following. - DCCMs are quite simple in terms that they can't crash and we don't - have a beam offset. - ATTENTION: 'angle' is in degrees, at least for PXIII - """ - - # Real axis (in degrees) - th1 = Component(EpicsMotor, "ROX1", name='theta1') - th2 = Component(EpicsMotor, "ROX2", name='theta2') - - # Virtual axes - en1 = Component(PseudoSingle, name='en1') - en2 = Component(PseudoSingle, name='en2') - energy = Component(PseudoSingle, name='energy', kind=Kind.hinted) - - # Other parameters - #feedback = Component(EpicsSignal, "MONOBEAM", name="feedback") - #enc1 = Component(EpicsSignalRO, "1:EXC1", name="enc1") - #enc2 = Component(EpicsSignalRO, "1:EXC2", name="enc2") - - @pseudo_position_argument - def forward(self, pseudo_pos): - """ - WARNING: We have an overdefined system! Not sure if common crystal movement is reliable without retuning - - """ - if abs(pseudo_pos.energy-self.energy.position) > 0.0001 and abs(pseudo_pos.en1-self.en1.position) < 0.0001 and abs(pseudo_pos.en2-self.en2.position) < 0.0001: - # Probably the common energy was changed - return self.RealPosition(th1=-180.0*e2a(pseudo_pos.energy)/3.141592, th2=180.0*e2a(pseudo_pos.energy)/3.141592) - else: - # Probably the individual axes was changes - return self.RealPosition(th1=-180.0*e2a(pseudo_pos.en1)/3.141592, th2=180.0*e2a(pseudo_pos.en2)/3.141592) - - @real_position_argument - def inverse(self, real_pos): - return self.PseudoPosition(en1=-a2e(3.141592*real_pos.th1/180.0), - en2=a2e(3.141592*real_pos.th2/180.0), - energy=-a2e(3.141592*real_pos.th1/180.0)) - - diff --git a/ophyd_devices/epics/proxies/slits.py b/ophyd_devices/epics/proxies/slits.py index 20337ff..0e8a14d 100644 --- a/ophyd_devices/epics/proxies/slits.py +++ b/ophyd_devices/epics/proxies/slits.py @@ -2,20 +2,14 @@ from ophyd import Device, Component, EpicsMotor, PseudoPositioner, PseudoSingle from ophyd.pseudopos import pseudo_position_argument,real_position_argument - - - - - - class SlitH(PseudoPositioner): - """ Python wrapper for virtual slits - - These devices should be implemented as an EPICS SoftMotor IOC, - but thats not the case for all slits. So here is a pure ophyd - implementation. Uses standard naming convention! - - NOTE: The real and virtual axes are wrapped together. + """Python wrapper for virtual slits + + These devices should be implemented as an EPICS SoftMotor IOC, + but thats not the case for all slits. So here is a pure ophyd + implementation. Uses standard naming convention! + + NOTE: The real and virtual axes are wrapped together. """ # Motor interface x1 = Component(EpicsMotor, "TRX1") @@ -37,16 +31,15 @@ class SlitH(PseudoPositioner): gapx=real_pos.x2-real_pos.x1) - class SlitV(PseudoPositioner): - """ Python wrapper for virtual slits - - These devices should be implemented as an EPICS SoftMotor IOC, - but thats not the case for all slits. So here is a pure ophyd - implementation. Uses standard naming convention! - - NOTE: The real and virtual axes are wrapped together. - """ + """Python wrapper for virtual slits + + These devices should be implemented as an EPICS SoftMotor IOC, + but thats not the case for all slits. So here is a pure ophyd + implementation. Uses standard naming convention! + + NOTE: The real and virtual axes are wrapped together. + """ # Motor interface y1 = Component(EpicsMotor, "TRY1") y2 = Component(EpicsMotor, "TRY2") @@ -56,13 +49,12 @@ class SlitV(PseudoPositioner): @pseudo_position_argument def forward(self, pseudo_pos): - '''Run a forward (pseudo -> real) calculation''' + """Run a forward (pseudo -> real) calculation""" return self.RealPosition(y1=pseudo_pos.ceny-pseudo_pos.gapy/2, y2=pseudo_pos.ceny+pseudo_pos.gapy/2) @real_position_argument def inverse(self, real_pos): - '''Run an inverse (real -> pseudo) calculation''' + """Run an inverse (real -> pseudo) calculation""" return self.PseudoPosition(ceny=(real_pos.y1+real_pos.y2)/2, gapy=real_pos.y2-real_pos.y1) -