ScanScripts¶

This GitHub repository provides a framework for running experiment scripts at various beamline at the Advanced Photon Source.
Content¶
Install¶
This section covers the basics of how to download and install ScanScripts.We recommend you to install the Anaconda Python distribution.
Contents:
Installing from source¶
Clone the ScanScripts from GitHub repository:
git clone https://github.com/tomography/scanscripts.git project
then:
cd project
python setup.py install
Beamline Configuration¶
The scanscripts library looks for a file in the top director (eg
~/TXM/scanscripts
) called beamline_config.conf
. This file
should contain configuration details for how the beamline is
setup. This allows easy configuration changes without having to modify
library code. See the documentation for each beamline for more details
on which options are supported:
Sector 32-ID Scripts¶
General Features¶
All the scan scripts below can be executed in one of three ways.
- Through the
tomography.sh
graphical user interface (GUI) - From the command line interface (CLI)
- Directly from a python interpreter
The mechanisms behind the GUI and command-line interfaces are identical. Every argument in the GUI parameter panel is also present as a long argument on the command-line:
$ energy-scan --Energy_End 8.5 --Energy_Start 8.3 --ExposureTime 1.5 --SampleXOut 0.1
The programatic python versions start with run_
. They often have
slightly differet parameters to the GUI/CLI implementation, allowing
for more precise control.
>>> import aps_32id
>>> import numpy as np
>>> aps_32id.run_energy(energies=np.linspace(8.3, 8.5, num=101))
Logging¶
These scripts (except for move_energy
) uses the standard library
logging
module to save logs with file names matching the
HDF5 data files. The default level is logging.INFO
, but this can
be changed by using the Log_Level
variable:
$ energy-scan --Log_Level 10
or the log_level
parameter:
>>> import numpy as np
>>> import logging
>>> import aps_32id
>>> aps_32id.run_energy_scan(energies=np.linspace(8.3, 8.5, 100), log_level=logging.DEBUG)
The log levels are the same as those defined in the logging module. They get set to the root logger, so logging.UNSET results in all messages being sent through. The special value -1 causes no changes to the logging configuration.
Level | Value |
---|---|
(no change) | -1 |
logging.UNSET | 0 |
logging.DEBUG | 10 |
logging.INFO | 20 |
logging.WARNING | 30 |
logging.ERROR | 40 |
logging.CRITICAL | 50 |
Move Energy¶
GUI: | run/move_energy.py |
Command-line: | $ move-energy |
Python: | >>> aps_32id.move_energy() |
The move_energy
script provides a way to change
the energy of the beamline. If the parameter constant_mag
is
truthy, the detector will move to maintain a constant level of
magnification. The equivalent function
move_energy()
can be used
programatically.
Energy Scan¶
GUI: | run/energy_scan.py |
Command-line: | $ energy-scan |
Python: | >>> aps_32id.run_energy_scan() |
The energy_scan
script collects 2D frames over a
range of energies, as well as the corresponding flat-field and
dark-field images. The equivalent function
run_energy_scan()
lets this script be
called programatically. The variable dictionary contains parameters
for Energy_Start
, Energy_End
and Energy_Step
. If more
control is needed (eg, non-evenly spaced energies), then the function
should be used with the energies
argument. The helper function
energy_range()
allows easy construction of a unique
list of energies.
from aps_32id import run_energy_scan
from scanlib import energy_range
import numpy as np
# Create a list of energies from energy ranges
energies = energy_range(
# (start, end, step)
(8250, 8290, 10),
(8290, 8300, 2),
(8300, 8380, 1),
(8380, 8500, 10),
)
# Describe position for sample and flat-field frames
# (x, y, z, θ°)
out_pos = (0.2, None, None, 0)
sample_pos = (0, None, None, 0)
# Execute the scan
run_energy_scan(energies=energies, out_pos=out_pos, sample_pos=sample_pos)
Tomography Step Scan¶
GUI: | run/tomo_step_scan.py |
Command-line: | $ tomo-step-scan |
Python: | >>> aps_32id.run_tomo_step_scan() |
The tomo_step_scan
script collects a tomogram as
well as flat-field and dark-field images. The variable dictionary
entries SampleStart_Rot
, SampleEnd_Rot
, Projections
control which angles get run. If more control is needed, the
run_tomo_step_scan()
function
with the angles
parameter can be used. It is not a requirement
that the angles be equally spaced.
import numpy as np
from aps_32id import run_tomo_step_scan
# Create the list of angles to scan
angles = np.linspace(0, 180, 361)
# Describe positions for sample and white-field position
# (x, y, z, θ°)
out_pos = (0.2, None, None, 0)
sample_pos = (0, None, None, 0)
# Execute the scan
run_tomo_step_scan(angles=angles, sample_pos=sample_pos, out_pos=out_pos)
Tomography Fly Scan¶
GUI: | run/tomo_fly_scan.py |
Command-line: | $ tomo-fly-scan |
Python: | >>> aps_32id.run_tomo_fly_scan() |
The tomo_fly_scan
script is similar to
tomo_step_scan
except it does not come to a
complete stop when collecting projection. The timing must be uniform,
so only equally spaced angles are allowed, even in the python function
form.
Mosaic Tomography Fly Scan¶
Warning
This function has not yet replaced the “old style” script at the beamline.
The mosaic_tomo_fly_scan
script and
mosaic_tomo_fly_scan()
are
similar to tomo_step_scan
except multiple fields
of view are collected.
Roll-Your-Own Scripts¶
Those with a sense of adventure can write their own scripts for Sector 32. It’s highly recommended to become familiar with the Sector 32-ID TXM and Examples pages.
Sector 32-ID TXM¶
Note
This code is under active development and may change at any time. If you encounter issues, or documentation bugs, please submit an issue.
This page describes the features of the
aps_32id.txm.NanoTXM
class, and a few supporting
classes. The NanoTXM
class is the primary
interface for controlling the Transmission X-ray Microscope (TXM) at
beamline 32-ID-C. There is also a complimentary
aps_32id.txm.MicroTXM
.
A core design goal is to keep as much of the complexity in the
NanoTXM
class, which leaves the scripts to
handle high-level details. It also allows for better unit and
integration testing. When creating new scripts, it is recommended to
put all interactions to process variables (PVs) in methods of the
NanoTXM
class. This may seem silly for
single PV situations, but will make the script more readable. A
hypothetical example:
# Not readable at all: what does that address even mean??
PV('32idcTXM:SG_RdCntr:reset.PROC').put(1, wait=True)
# Better, but still not great: what does 1 mean?
txm.Reset_Theta = 1
# Best, even though this method definition would only have one line
txm.reset_theta()
Sector 32-ID Configuration¶
The following configuration options can be set in the
beamline_config.conf
file under the [32-ID-C]
heading:
- has_permit (yes|no)
- If
has_permit
is “no”, then the script will not attempt to change the X-ray source, monochromator, shutters, etc. This allows testing of scripts while the B-hutch is operating without risking interferance. - stage (NanoTXM|MicroCT)
- Controls which stage/optics/shutters to use for manipulating the
sample.
MicroCT
uses the front stage andNanoTXM
uses the rear stage. - zone_plate_drn (int)
- The width, in nm, of the outermost zone of the zone-plate of the zone-plate (\(\Delta r_n\)).
- zone_plate_diameter (int)
- The total diameter, in µm, of the zone-plate.
- zone_plate_drift_x (float)
- Adjusts the zoneplate x position by this amount for every unit change of zoneplate z. When properly set, this will keep the sample centered when changing energy.
- zone_plate_drift_y (float)
- Adjusts the zoneplate y position by this amount for every unit change of zoneplate z. When properly set, this will keep the sample centered when changing energy.
[32-ID-C]
has_permit = True
# Either NanoTXM or MicroCT
stage = NanoTXM
# Correct for zoneplate drift when changing energies
zone_plate_drn = 50
zone_plate_diameter = 180
zone_plate_drift_x = 0.
zone_plate_drift_y = 0.
Internally, these options are parsed in
aps_32id.txm.txm_config()
using the standard library’s
configparser package. To make scripts easier to read, it is best to
read the configuration only inside methods of
NanoTXM
(or subclasses). The configuration
values can be read in the following manner:
cfg = txm_config()['32-ID-C']
zp_drn = cfg.getfloat('zone_plate_drn')
has_permit = cfg.getboolean('has_permit')
Stopping Scans Gracefully¶
When a scan script ends, we want the instrument to return to a
usable configuration even if an exception occurred. Using the
run_scan()
context manager, this
becomes easy. At the start of the context, this manager saves certain
configuration details about instrument; when exiting the context for
any reason the configuration is restored, the CCD is set to
“continuous mode”, and any extra logging is stopped:
import logging
import aps_32id
txm = aps_32id.NanoTXM()
with txm.run_scan():
# Setup the microscope as desired
txm.setup_hdf_writer()
txm.start_logging(logging.INFO)
txm.setup_detector()
# Now do experiment stuff
Process Variables¶
Process variables (PVs), though the pyepics
package are the way
python controls the actuators and sensors of the instrument. There are
two ways to interact with process variables:
- The
pv_put()
method on aNanoTXM
object. - A
TxmPV
descriptor on theNanoTXM
class (or subclass).
The second option handles more of the underlying complexity, but
understanding it requires a good grasp of the first option. The
NanoTXM.pv_put()
method is a
wrapper around pyepics.PV.put()
, and accepts similar
arguments:
# These two sets of statements have the same effect
# Using the epics PV class
epics.PV('my_great_pv').put(1, wait=True)
# Using the TXM method
my_txm = TXM()
my_txm.pv_put('my_great_pv', 1, wait=True)
Behind the scenes, there is some extra magic so the txm can coordinate PVs that work together.
Manually supplying the PV name and options each time is cumbersome, so
the TxmPV
descriptor can be used to
define PVs at import time. Set instances of the
TxmPV
class as attributes on a
NanoTXM
subclass, then assign and retrieve
values directly from the attribute:
from aps_32id import NanoTXM
from scanlib import TxmPV
class ExampleTXM(NanoTXM):
# Define a PV during import time
my_awesome_pv = TxmPV('cryptic:pv:string', dtype=float, wait=True)
# More PV definitions go here
# Now we can use the PV attribute of the txm class
my_txm = ExampleTXM()
# Retrieve the current value
# Equivalent to ``float(epics.PV('cryptic:pv:string').get())``
curr_value = my_txm.my_awesome_pv
# Set the value
# Equivalent of epics.PV('cryptic:pv:string').put(2.718, wait=True)
my_txm.my_awesome_pv = 2.718
The advantage here is that boilerplate, such as type-casting and
blocking, can be defined once then forgotten. This approach also lets
you define PVs that should not be changed when the B-hutch is being
operated, by passing permit_required=True
to the TxmPV
constructor. More on this below.
Waiting on Process Variables¶
Sometimes it is necessary to set one PV then wait on a different PV to
confirm the new value. The tomo.32id.txm.TXM.wait_pv()
method
will poll a specified PV until it reaches its target value. It accepts
the attribute name of a PV, not the actual PV name itself. It may be
necessary to use the wait=False
argument on the first PV to avoid
blocking forever:
class MyTXM(TXM):
motor_pv = TxmPV('txm:motorA', wait=False
sensor_pv = TxmPV('txm:sensorA')
txm = MyTXM()
# First set the actuator to the desired value
new_position = 3.
txm.motor_pv = new_position
# This will block until the sensor reaches the target value
tmx.wait_pv('sensor_pv', new_position)
Waiting on Multiple Process Variables¶
Warning
This feature should be considered experimental. It has been know to break during some operations, most notably setting the undulator gap.
By default, calling the pv_put()
method
will block execution until the put
call has completed. This means
that setting several PVs becomes a serial operation. This is the
safest approach but is unnecessary in many situations. For example,
setting the x, y and z stage positions can be done simultaneously. You
can always use wait=False
and handle the blocking yourself,
however this is not always straight-forward and may involve messy
callbacks. Using the wait_pvs()
context
manager takes care of this. Any PVs that are set inside the context
will move immediately; if block=True
(default) the manager will
wait for them to finish before leaving the context.
txm = TXM()
# These move one at a time
txm.Motor_SampleY = 5
txm.Motor_SampleZ = 3
# This waits while both motors move simultaneously
with txm.wait_pvs():
txm.Motor_SampleY = 8
txm.Motor_SampleZ = 9
# These move in the background without blocking
with txm.wait_pvs(block=False):
txm.Motor_SampleY = 3
txm.Motor_SampleZ = 12
This table describes whether if and when a process variable blocks the execution of python code and waits for the PV to achieve its target value:
Context manager | pv_put(wait=True) |
pv_put(wait=False) |
---|---|---|
No context | Blocks now | No blocking |
TXM().wait_pvs |
Blocks later | No blocking |
TXM().wait_pvs(block=False) |
No blocking | No blocking |
Locking Shutter Permits¶
Sometimes it’s desireable to test portions of the codebase during
downtime while the B-hutch is operating. In order to do this, however,
it’s important to ensure that the shutters, undulator and
monochromator are not changed. Using the
TxmPV
descriptors makes this easy: any
PV’s that should not be changed can be given the
permit_required=True
argument to their constructor:
class MyTXM(TXM):
SHUTTER_OPEN = 1
my_shutter = TxmPV('32idc:shutter', permit_required=True)
def open_shutter(self):
"""Opens the shutter so we can science!"""
self.my_shutter = self.SHUTTER_OPEN
# This will not do anything
my_txm = MyTXM()
my_txm.open_shutter()
# This will control the PV as expected
my_txm = MyTXM(has_permit=True)
my_txm.open_shutter()
Note
There is no check that the C-hutch actually has permission to
open the shutter, etc. It’s controlled only by the has_permit
argument given to the TXM
constructor. Please be considerate.
Fast Shutter¶
The instrument is equipped with a “fast shutter” than protects the
specimen from excessive X-ray exposure. Calling
enable_fast_shutter()
turns this feature
on. If using the run_scan()
context manager
(recommended), the fast shutter is automatically disabled, otherwise
the disable_fast_shutter()
method should be
called to return to normal behavior. The fast shutter respects
exposure_time()
attribute.
API reference¶
project Modules:
Examples¶
Sector 32-ID-C¶
An template TXM script is show below. It doesn’t actually collect any
data, but it does set up the TXM, open the shutters, close them again,
and tear down the TXM. The variableDict
describes the parameters
that are presented to the user in the GUI when running this script. In
the example below, Several actions take place within a
run_scan()
context manager. This
ensures that the current configuration is restored after the scan.
#!/bin/env python
"""An example script for controlling the sector 32 ID-C microscope."""
import logging
from scanlib import update_variable_dict
from aps_32id import NanoTXM
# Prepare for logging data to a file, or whatever
log = logging.getLogger(__name__)
# A dictionary with the options that can be used when invoking this script
variableDict = {
'Parameter A': 0.1,
'Parameter B': 505,
# Logging: -1=no change, 0=UNSET, 10=DEBUG, 20=INFO, 30=WARNING, 40=ERROR, 50=CRITICAL
'Log_Level': logging.INFO,
}
def getVariableDict():
return variableDict
def run_my_experiment(param_a, param_b, log_level=20, txm=None):
"""Separate out the work-horse code so that it can be executed
programatically. The ``txm`` parameter is intended for testing,
where an instance of :py:class:`tests.tools.TXMStub` is used.
Parameters
==========
param_a :
An experimental parameter.
param_b :
Another experimental parameter.
log_level : logging.INFO
How much detail to save to the logs.
txm : NanoTXM, optional
A NanoTXM object that represents the X-ray microscope. Useful
for testing.
"""
log.debug("Starting my experiment")
# Create a TXM object to control the instrument
if txm is None:
txm = new_txm()
# Run the experiment in this context manager so it stops properly
with txm.run_scan():
# Setup the microscope as desired
txm.setup_hdf_writer()
txm.start_logging(log_level)
txm.setup_detector()
txm.enable_fast_shutter() # Optional: reduces beam damage
txm.open_shutters()
# Now do some tomography or XANES or whatever
pass
# Close the shutters and shutdown
txm.close_shutters()
def main():
# The script was launched (not imported) so load the variable
# dictionary from CLI parameters
update_variable_dict(variableDict)
# Start the experiment
run_my_experiment(param_a=variableDict['Parameter A'],
param_b=variableDict['Parameter B'],
log_level=variableDict['Log_Level'])
if __name__ == '__main__':
main()