diff --git a/tomcat_bec/devices/aerotech/AerotechSimpleSequenceTemplate.ascript b/tomcat_bec/devices/aerotech/AerotechSimpleSequenceTemplate.ascript index 94d4caa..8bb6516 100644 --- a/tomcat_bec/devices/aerotech/AerotechSimpleSequenceTemplate.ascript +++ b/tomcat_bec/devices/aerotech/AerotechSimpleSequenceTemplate.ascript @@ -5,10 +5,10 @@ // enum ScanType - Pos = 0 - Neg = 1 - PosNeg = 2 - NegPos = 3 + POS = 0 + NEG = 1 + POSNEG = 2 + NEGPOS = 3 end @@ -18,15 +18,14 @@ program var $fStartPosition as real = {{ scan.startpos }} var $fScanRange as real = $fStartPosition + {{ scan.scanrange }} var $iNumRepeat as integer = {{ scan.nrepeat }} - var $eScanType as ScanType = ScanType.{{ scan.scandir or 'Pos' }} + var $eScanType as ScanType = ScanType.{{ scan.scandir or 'POS' }} var $iNumDdcRead as integer = {{ scan.npoints }} - var $fVelJog as real = {{ scan.jogvel or 200 }} var $fVelScan as real = {{ scan.scanvel }} - var $fAcceleration = {{ scan.scanacc or 500 }} - var $fSafeDist = 10.0 - + var $fAcceleration = {{ scan.scanacc }} + var $fAccDistance as real = {{ scan.accdist }} + ////////////////////////////////////////////////////////////////////////// // Internal parameters - dont use var $axis as axis = ROTY @@ -36,21 +35,20 @@ program // Set acceleration SetupAxisRampType($axis, RampType.Linear) SetupAxisRampValue($axis,0,$fAcceleration) - var $fAccDistance as real = 0.5 * $fVelScan * $fVelScan / $fAcceleration + $fSafeDist // set the actual scan range var $fPosStart as real var $fPosEnd as real - if $eScanType == ScanType.Pos + if $eScanType == ScanType.POS $fPosStart = $fStartPosition - $fAccDistance $fPosEnd = $fStartPosition + $fScanRange + $fAccDistance - elseif $eScanType == ScanType.Neg + elseif $eScanType == ScanType.NEG $fPosStart = $fStartPosition + $fAccDistance $fPosEnd = $fStartPosition - $fScanRange - $fAccDistance - elseif $eScanType == ScanType.PosNeg + elseif $eScanType == ScanType.POSNEG $fPosStart = $fStartPosition - $fAccDistance $fPosEnd = $fStartPosition + $fScanRange + $fAccDistance - elseif $eScanType == ScanType.NegPos + elseif $eScanType == ScanType.NEGPOS $fPosStart = $fStartPosition + $fAccDistance $fPosEnd = $fStartPosition - $fScanRange - $fAccDistance end @@ -67,9 +65,8 @@ program PsoOutputConfigureSource($axis, PsoOutputSource.Waveform) var $iPsoArrayAddr as integer = 0 // Simple PSO trigger pattern - var $iPsoArrayPos[] as real = [{% for psoDist in scan.psoBoundsPos[:-1] %} UnitsToCounts($axis, {{ psoDist }}), {% endfor %} UnitsToCounts($axis, {{ scan.psoBoundsPos[-1] }})] - // var $iPsoArrayPos[] as real = [UnitsToCounts($axis, $fAccDistance), {% for psoDist in scan.psoBoundsPos[:-1] %} UnitsToCounts($axis, psoDist), {% endfor %}] - // var $iPsoArrayNeg[] as real = [UnitsToCounts($axis, $fAccDistance), {% for psoDist in scan.psoBoundsNeg[:-1] %} UnitsToCounts($axis, psoDist), {% endfor %}] + var $iPsoArrayPos[] as real = [{% for psoDist in scan.psoBoundsPos[:-1] %} UnitsToCounts($axis, psoDist), {% endfor %}] + var $iPsoArrayNeg[] as real = [{% for psoDist in scan.psoBoundsNeg[:-1] %} UnitsToCounts($axis, psoDist), {% endfor %}] DriveArrayWrite($axis, $iPsoArrayPos, $iPsoArrayAddr, length($iPsoArrayPos), DriveArrayType.PsoDistanceEventDistances) PsoDistanceConfigureArrayDistances($axis, $iPsoArrayAddr, length($iPsoArrayPos), 0) @@ -102,17 +99,17 @@ program for $ii = 0 to ($iNumRepeat-1) PsoDistanceConfigureArrayDistances($axis, $iPsoArrayAddr, length($iPsoArrayPos), 0) - if $eScanType == ScanType.Pos + if $eScanType == ScanType.POS MoveAbsolute($axis, $fPosEnd, $fVelScan) WaitForInPosition($axis) MoveAbsolute($axis, $fPosStart, $fVelScan) WaitForInPosition($axis) - elseif $eScanType == ScanType.Neg + elseif $eScanType == ScanType.NEG MoveAbsolute($axis, $fPosEnd, $fVelScan) WaitForInPosition($axis) MoveAbsolute($axis, $fPosStart, $fVelScan) WaitForInPosition($axis) - elseif $eScanType == ScanType.PosNeg + elseif $eScanType == ScanType.POSNEG if ($ii % 2) == 0 MoveAbsolute($axis, $fPosEnd, $fVelScan) WaitForInPosition($axis) @@ -120,7 +117,7 @@ program MoveAbsolute($axis, $fPosStart, $fVelScan) WaitForInPosition($axis) end - elseif $eScanType == ScanType.NegPos + elseif $eScanType == ScanType.NEGPOS if ($ii % 2) == 0 MoveAbsolute($axis, $fPosEnd, $fVelScan) WaitForInPosition($axis) diff --git a/tomcat_bec/scans/__init__.py b/tomcat_bec/scans/__init__.py index c46698a..db28dae 100644 --- a/tomcat_bec/scans/__init__.py +++ b/tomcat_bec/scans/__init__.py @@ -1,2 +1,2 @@ from .gigafrost_test import GigaFrostStepScan -from .tomcat_scanbase import SnapAndStepScanBase, GigaStepScanBase +from .tomcat_scanbase import SnapAndStepScanBase, GigaStepScanBase, SequenceScanBase, SimpleFlyer diff --git a/tomcat_bec/scans/tomcat_scanbase.py b/tomcat_bec/scans/tomcat_scanbase.py index bc7b18e..1e26ce0 100644 --- a/tomcat_bec/scans/tomcat_scanbase.py +++ b/tomcat_bec/scans/tomcat_scanbase.py @@ -62,8 +62,6 @@ class TemplatedScanBase(AsyncFlyScanBase): drvdaq="es1_ddaq", daqcfg=None, daqmode="collect", preview="daq_stream1", **kwargs): - """Executes an AeroScript template as a flyer - """ super().__init__(**kwargs) self.axis = [] self.num_pos = 0 @@ -171,14 +169,62 @@ class TemplatedScanBase(AsyncFlyScanBase): +class SimpleFlyer(AsyncFlyScanBase): + """ Simple fly scan base class + + Very simple flyer class (something like the bluesky flyer). + It expects every device to be configured before the scan. + + Example + ------- + >>> scans.simpleflyer(sync_devices=['daq_stream1'], flyer_devices=['es1_tasks', 'es1_psod', 'es1_ddaq']) + """ + scan_name = "simpleflyer" + scan_report_hint = "table" + required_kwargs = ["sync_devices", "flyer_devices"] + + def __init__(self, sync_devices=[], async_devices=[], **kwargs): + # Auto-setup configuration parameters from input + self.sync_devices = sync_devices + self.async_devices = async_devices + + # Call super() + super().__init__(**kwargs) + + def stage(self): + + for dev in self.sync_devices: + yield from self.stubs.send_rpc_and_wait(dev, "stage") + for dev in self.async_devices: + yield from self.stubs.send_rpc_and_wait(dev, "stage") + + yield from super().stage() + + def scan_core(self): + + for dev in self.async_devices: + yield from self.stubs.send_rpc_and_wait(dev, "kickoff") + + for dev in self.async_devices: + yield from self.stubs.send_rpc_and_wait(dev, "complete") + + + def unstage(self): + """ Wait for DAQ before unstaging""" + time.sleep(5) + yield from super().unstage() + + + class GigaStepScanBase(ScanBase): - """ Gigafrost Step scan base class + """ Gigafrost software step scan base class - Example base class for AeroScript-based step scans using the low-level API. - Until the final cabling is done, the scan is in soft-trigger mode. + Example class for simple BEC-based step scans using the low-level API. + Until the final cabling is done, the scan can be tested in soft-trigger + mode. Example ------- @@ -254,7 +300,7 @@ class GigaStepScanBase(ScanBase): yield from self.stubs.send_rpc_and_wait("es1_psod", "prepare") # TODO: Explicitly arm detector - # Explicitly arm trigger + # Stage everything... yield from self.stubs.send_rpc_and_wait("gfclient", "stage") yield from self.stubs.send_rpc_and_wait("es1_ddaq", "stage") yield from self.stubs.send_rpc_and_wait("es1_psod", "stage") @@ -270,14 +316,6 @@ class GigaStepScanBase(ScanBase): - - - - - - - - class SnapAndStepScanBase(TemplatedScanBase): """ Snap'n Step scan base class @@ -295,25 +333,15 @@ class SnapAndStepScanBase(TemplatedScanBase): """ scan_name = "snapnstep" scan_report_hint = "table" - required_kwargs = ["range","steps"] - - required_kwargs = ["scan_start", "scan_end","steps"] + required_kwargs = ["scan_start", "scan_end", "steps"] gui_config = { - "Movement parameters": ["steps"], - "Acquisition parameters" : ["exp_time", "burst_at_each_point", "roix", "roiy"] + "Movement parameters": ["scan_start", "scan_end", "steps"], + "Acquisition parameters" : ["exp_time", "burst_at_each_point", "roix", "roiy", "sync"] } def __init__(self, scan_start, scan_end, steps, exp_time=0.005, burst_at_each_point=1, - roix=2016, roiy=2016, **kwargs): - """Example step scan - - Perform a simple step scan with a motor while software triggering the - gigafrost burst sequence at each point and recording it to the StdDAQ. - Actually only the configuration is gigafrost specific, everything else - is just using standard Bluesky event model. - - """ + roix=2016, roiy=2016, sync="event", **kwargs): # Auto-setup configuration parameters from input self.scan_start = scan_start self.scan_end = scan_end @@ -334,8 +362,14 @@ class SnapAndStepScanBase(TemplatedScanBase): } # Aerotech DDC settings: Internal event trigger: PsoEvent = 1 - daqname = "es1_ddaq" - daqcfg = {'ntotal': self.scan_ntotal, 'trigger': 1} + if isinstance(sync, str): + t_modes = {'event': 1, 'pso': 2, 'inp0': 3, 'inp1': 1} + daq_trigger = t_modes[sync] + else: + daq_trigger = int(sync) + + daqname = "es1_ddaq" + daqcfg = {'ntotal': self.scan_ntotal, 'trigger': daq_trigger} # Gigafrost config camname = "gfclient" camcfg = { @@ -360,6 +394,146 @@ class SnapAndStepScanBase(TemplatedScanBase): preview="daq_stream1", **kwargs) - # def scan_core(self): - # """ Don't do the scan during testing""" - # # raise RuntimeError("You shall NOT PASSS!!!") + def unstage(self): + """ Wait for DAQ before unstaging""" + time.sleep(5) + yield from super().unstage() + + + + +class SequenceScanBase(TemplatedScanBase): + """ Sequence scan base class + + Example base class for AeroScript-based sequence scans. This function + loads a pre-written AeroScript file on the Automation1 controller for + low latency gated scans. + + NOTE: As a temporary setup until the final cabling is done, the scan + is in a soft-trigger mode. The Gigafrost is just software triggered, + and the position readback is wired to the internal trigger. + + Example + ------- + >>> scans.sequencescan(33, 180, 180, exp_time=0.005, exp_frames=1800, repeats=10) + """ + scan_name = "sequencescan" + scan_report_hint = "table" + required_kwargs = ["scan_start", "gate_high", "gate_low"] + gui_config = { + "Movement parameters": ["scan_start", "repeats", "repmode"], + "Acquisition parameters" : ["gate_high", "gate_low", "exp_time", "exp_frames", "roix", "roiy", "sync"] + } + + def __init__(self, scan_start, gate_high, gate_low, repeats=1, repmode="PosNeg", + exp_time=0.005, exp_frames=180, + roix=2016, roiy=2016, sync="pso", **kwargs): + # Auto-setup configuration parameters from input + self.scan_start = scan_start + self.gate_high = gate_high + self.gate_low = gate_low + self.scan_repnum = repeats + self.scan_repmode = repmode + self.exp_time = exp_time + self.exp_frames = exp_frames + self.acc_dist = 42 + + # Synthetic values + self.scan_velocity = gate_high / (exp_time * exp_frames) + self.scan_acceleration = 500 + self.scan_safedistance = 10 + self.scan_accdistance = self.scan_safedistance + 0.5* self.scan_velocity * self.scan_velocity / self.scan_acceleration + self.scan_ntotal = exp_frames * repeats + if repmode.lower() in ("pos", "neg"): + self.scan_range = gate_high + gate_low + elif repmode.lower() in ("posneg", "negpos"): + self.scan_range = repeats *(gate_high + gate_low) + + filename = "AerotechSimpleSequenceTemplate.ascript" + filesubs = { + 'startpos': self.scan_start, + 'scanrange': self.scan_range, + 'scanacc': self.scan_acceleration, + 'accdist': self.scan_accdistance, + 'npoints' : self.scan_ntotal, + 'scandir' : self.scan_repmode.upper() + } + + # Aerotech DDC settings: Internal event trigger: PsoEvent = 1 + if isinstance(sync, str): + t_modes = {'event': 1, 'pso': 2, 'inp0': 3, 'inp1': 1} + daq_trigger = t_modes[sync] + else: + daq_trigger = int(sync) + + daqname = "es1_ddaq" + daqcfg = {'ntotal': self.scan_ntotal, 'trigger': daq_trigger} + # Gigafrost config + camname = "gfclient" + camcfg = { + "ntotal": self.scan_ntotal, + "nimages": self.exp_frames, + "exposure": 1000 * exp_time, + "period": 2000*exp_time, + "pixel_width": roix, + "pixel_height": roiy + } + + # Parameter validation before scan + if self.exp_frames <=0: + raise RuntimeError(f"Requires at least one frame per cycle, got {self.exp_frames}") + + # Call super() + super().__init__( + filename=filename, filesubs=filesubs, + controller="es1_tasks", taskindex=4, + camera=camname, camcfg=camcfg, + drvdaq=daqname, daqcfg=daqcfg, daqmode="collect", + preview="daq_stream1", + **kwargs) + + def prepare_positions(self): + # Fill PSO vectors according to scan direction + self.psoBoundsPos = [] + self.psoBoundsNeg = [] + if self.scan_repmode in ("pos", "Pos"): + self.psoBoundsPos.append(self.acc_dist) + for ii in range(self.scan_repnum): + self.psoBoundsPos += [self.gate_high, self.gate_low] + self.psoBoundsPos[-1] = 10* (self.acc_dist+self.gate_low) + if self.scan_repmode in ("neg", "Neg"): + self.psoBoundsNeg.append(self.acc_dist) + for ii in range(self.scan_repnum): + self.psoBoundsNeg += [self.gate_high, self.gate_low] + self.psoBoundsNeg[-1] = 10* (self.acc_dist+self.gate_low) + if self.scan_repmode in ("posneg", "PosNeg"): + self.psoBoundsPos.append(self.acc_dist) + self.psoBoundsNeg.append(self.acc_dist + self.gate_low) + for ii in range(self.scan_repnum // 2): + self.psoBoundsPos += [self.gate_high, self.gate_low] + self.psoBoundsNeg += [self.gate_high, self.gate_low] + self.psoBoundsPos[-1] = 10* (self.acc_dist+self.gate_low) + self.psoBoundsNeg[-1] = 10* (self.acc_dist+self.gate_low) + if self.scan_repmode in ("negpos", "NegPos"): + self.psoBoundsPos.append(self.acc_dist + self.gate_low) + self.psoBoundsNeg.append(self.acc_dist) + for ii in range(self.scan_repnum // 2): + self.psoBoundsPos += [self.gate_high, self.gate_low] + self.psoBoundsNeg += [self.gate_high, self.gate_low] + self.psoBoundsPos[-1] = 10* (self.acc_dist+self.gate_low) + self.psoBoundsNeg[-1] = 10* (self.acc_dist+self.gate_low) + + self.filesubs['psoBoundsPos'] = self.psoBoundsPos + self.filesubs['psoBoundsNeg'] = self.psoBoundsNeg + # Call super() to do the substitutions + yield from super().prepare_positions() + + def unstage(self): + """ Wait for DAQ before unstaging""" + time.sleep(5) + yield from super().unstage() + + + + + diff --git a/tomcat_bec/scripts/demoscans.py b/tomcat_bec/scripts/demoscans.py index cfe1207..b9e3ed6 100644 --- a/tomcat_bec/scripts/demoscans.py +++ b/tomcat_bec/scripts/demoscans.py @@ -1,22 +1,31 @@ """ Demo scans for Tomcat at the microXAS test bench """ +def bl_check_beam(): + """ Checks beamline status""" + return True -def demostepscan(scan_start, scan_end, steps, exp_time=0.005, burst_at_each_point=1, settling_time=0): + + +def demostepscan(scan_start, scan_end, steps, exp_time=0.005, burst_at_each_point=1, settling_time=0, sync='event'): """ Demo step scan with GigaFrost - This is a small user-space demo step scan at the microXAS testbench using - the gigafrost in software triggering mode. It tries to be as standard as - possible, while still setting up the environment. + This is a small BEC user-space demo step scan at the microXAS testbench + using the gigafrost in software triggering mode. It tries to be a + standard BEC scan, while still setting up the environment. Example: -------- demostepscan(scan_start=-32, scan_end=148, steps=180, exp_time=0.005, burst_at_each_point=5) """ + if not bl_check_beam(): + raise RuntimeError("Beamline is not in ready state") + scan_ntotal = burst_at_each_point * (steps + 1) # Move to start position - cfg = {'ntotal': scan_ntotal, 'trigger': 1} + t_modes = {'event': 1, 'pso': 2, 'inp0': 3, 'inp1': 1} + cfg = {'ntotal': scan_ntotal, 'trigger': t_modes[sync]} dev.es1_ddaq.configure(d=cfg) # Configure gigafrost cfg = { @@ -40,3 +49,63 @@ def demostepscan(scan_start, scan_end, steps, exp_time=0.005, burst_at_each_poin dev.es1_psod.software_trigger = False + + + +def demosequencescan(scan_start, gate_high, gate_low, repeats=1, repmode="PosNeg", + exp_time=0.005, exp_frames=180, sync='pso'): + """ Demo sequence scan with GigaFrost + + This is a small BEC user-space sequence scan at the microXAS testbench + triggering the gigafrost in gate-enable mode. It tries to do everything + through BEC, so while still setting up the environment. + + NOTE: It's not exactlythe same as the AeroScript template version. + + Example: + -------- + demostepscan(scan_start=-32, scan_end=148, steps=180, exp_time=0.005, burst_at_each_point=5) + """ + if not bl_check_beam(): + raise RuntimeError("Beamline is not in ready state") + + # Synthetic values + scan_velocity = gate_high / (exp_time * exp_frames) + scan_acceleration = 500 + scan_safedistance = 10 + scan_accdistance = scan_safedistance + 0.5* scan_velocity * scan_velocity / scan_acceleration + scan_ntotal = exp_frames * repeats + + # Move to start position + t_modes = {'event': 1, 'pso': 2, 'inp0': 3, 'inp1': 1} + cfg = {'ntotal': scan_ntotal, 'trigger': t_modes[sync]} + dev.es1_ddaq.configure(d=cfg) + # Configure gigafrost + cfg = { + "ntotal": scan_ntotal, "nimages": exp_frames, + "exposure": 1000*exp_time, "period": 2000*exp_time} + dev.gfclient.configure(d=cfg) + + # Fill PSO vectors according to scan direction + psoBounds = [] + if repmode in ("pos", "Pos"): + psoBounds.append(scan_accdistance) + for ii in range(repeats): + psoBounds += [gate_high, gate_low] + psoBounds[-1] = 10* (scan_accdistance+gate_low) + + dev.es1_psod.configure(d={'distance': psoBounds, 'wmode': 'toggle'}) + + # Explicitly arm trigger (and detector in future) + dev.es1_psod.prepare() + + print("Handing over to 'scans.line_scan'") + wait_time = 0.1 + 2*exp_time * burst_at_each_point + scans.line_scan( + dev.es1_roty, scan_start, scan_end, + steps=steps, exp_time=wait_time, relative=False, settling_time=settling_time) + + # Cleanup: disable SW trigger + dev.es1_psod.software_trigger = False + +