This commit is contained in:
mohacsi_i 2022-11-14 17:37:03 +01:00
parent 19f5f728cc
commit c7867a910f
8 changed files with 174 additions and 373 deletions

View File

@ -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))

View File

@ -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")

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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))

View File

@ -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))

View File

@ -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)