Table Scan

Creates a scan based on a table.

Basic Example

Each column of a table specifies a device name (Process Variable). Cells in each row provide the desired values.

temperature

position

50

1

100

2

The table above creates the following scan commands:

Comment('# Line 1'),
Set('temperature', 50),
Set('position', 1),
Comment('# Line 2'),
Set('temperature', 100),
Set('position', 2),
Comment('# End'),

Cells can remain empty if a device should not be changed in that row.

temperature

position

50

1

2

3

100

1

2

3

Results in:

Set('temperature', 50),
Set('position', 1),
Set('position', 2),
Set('position', 3),
Set('temperature', 100),
Set('position', 1),
Set('position', 2),
Set('position', 3),

Loops

Cells can contain loop(start, end, step) to set a device in a loop:

position

loop(2, 5, 1)

Results in this loop to set the ‘position’ to 2, 3, 4 and then 5:

Loop('position', 2, 5, 1)

When using loop(start, end) without specifying step, the step defaults to 1.

Colummns following a loop are handled inside the loop:

position

camera

loop(2, 5, 1)

snap

Results in a loop for the position, triggering a camera at each position:

Loop('position', 0, 3, 0.5,
     [ Set('camera', 'snap') ]
    )

Loops can be nested, again placing columns following a loop inside that loop:

X

Y

camera

loop(1, 10)

loop(2, 5)

snap

Results in an outer loop for the X position with an inner loop for the Y position, triggering a camera at each position:

Loop('X', 1, 10, 1,
     [ Loop('Y', 2, 5, 1,
            [ Set('Camera', 'Snap') ]
           )
     ]
    )

When using nested loops, note details regarding Loop Direction which can be used for reversing the direction of inner loops to more efficiently scan a surface.

Lists, Ranges

Cells that contain a list of values or a range() command will be expanded as if you had entered separate rows for each value:

position

[1, -3, 7, 2]

is equivalent to

position

1

-3

7

2

Compared to a loop which always generates equidistant values, the list can contain any sequence of values.

The range behaves similar to the Python range(start, stop, step) command, generating values from start up to but excluding stop.

position

range(1,4,1)

is equivalent to

position

1

2

3

loop(start, end, step) should be preferred over range(start, stop, step) because of the inclusive and thus more obvious handling of end vs. stop.

In addition, each loop translates into a single Loop command:

position

loop(1,100,0.1)

becomes:

Loop('position', 1, 100, 0.1)

This range-based table will set position to the same values:

position

range(1,100.1,0.1)

The resulting scan, however, becomes a sequence of 1000 Set commands:

Set('position', 1)
Set('position', 1.1)
Set('position', 1.2)
...
Set('position', 99.9)
Set('position', 100)

For larger loops respectively ranges, the loop will be more efficient.

Within a row, multiple ranges or lists are recursively expanded from right to left, similar to the nesting of loops:

temperature

position

[50,100]

[1, 2, 3]

is expanded into

temperature

position

50

1

50

2

50

3

100

1

100

2

100

3

Scan Settings

When for example accessing a ‘position’ device associtated with an EPICS motor, the Set command should await completion, then compare the ‘position.RBV’ against the desired position:

Set('position', 2, completion=True, readback='position.RBV', tolerance=0.1)

The tolerance for this comparison as well as a timeout will depend on the actual motor.

You may have a known list of device names and how they need to be accessed, or you may be able to derive this information based on a naming standard for devices at your site.

The scan.util.scan_settings module is used to configure how the TableScan accesses devices.

Code Example

from scan_settings1 import MyScanSettings, setScanSettings
from scan.table import TableScan

# Custom settings configure the TableScan to
# check 'temperature' for completion,
# and to treat 'position' as a motor with readback check via *.RBV 
setScanSettings(MyScanSettings())

# Table scan with these settings,
# definition of column headers,
# and rows
table = TableScan(
      ['temperature', 'position'],
    [ [      50,           1],
      [      '',           2],
      [      '',           3],
      [     100,           1],
      [      '',           2],
      [      '',           3],
    ])

# Create scan, print each command
scan = table.createScan()
for cmd in scan:
    print cmd
"""
Result:
Set('temperature', 50.0, completion=True, timeout=300)
Set('position', 1.0, completion=True, readback='position.RBV', timeout=100)
Set('position', 2.0, completion=True, readback='position.RBV', timeout=100)
Set('position', 3.0, completion=True, readback='position.RBV', timeout=100)
Set('temperature', 100.0, completion=True, timeout=300)
Set('position', 1.0, completion=True, readback='position.RBV', timeout=100)
Set('position', 2.0, completion=True, readback='position.RBV', timeout=100)
Set('position', 3.0, completion=True, readback='position.RBV', timeout=100)
"""

‘Wait For’, ‘Value’, ‘Or Time’ Columns

These two columns create commands that wait for a condition, and then log all devices which have been used within the scan up to that point.

If the cell contains a device name, a Wait command is created for the device to reach the given value.

position

Wait For

Value

2

counter

10000

The table above will create the following scan:

Set('position', 2.0, completion=true, readback='position.RBV', timeout=100)
Wait('counter', 10000.0, comparison='>=')
Log('position', 'counter')

The ScanSettings determine the detailed options of the Set and Wait commands. By default, the generated Wait command will wait forever, unless the ScanSettings provide a timeout. In the example above, we assume that the ScanSetting cause every access to the ‘position’ device to use completion and readback.

A prefix -c can disable the completion check:

-c position

Wait For

Value

2

counter

10000

Resulting scan without completion for the ‘position’:

Set('position', 2.0, readback='position.RBV', timeout=100)
Wait('counter', 10000.0, comparison='>=')
Log('position', 'counter')

For more prefix options see ScanSettings.

Waiting for seconds or time results in a simple Delay.

position

Wait For

Value

2

seconds

20

4

time

20

The table above will create the following scan:

Set('position', 2.0, completion=true, readback='position.RBV', timeout=100)
Delay(20)
Log('position')
Set('position', 4.0, completion=true, readback='position.RBV', timeout=100)
Delay(20)
Log('position')

The delay is specified in seconds, a “HH:MM:SS” notation for hours, minutes, seconds, or “MM:SS” for minutes, seconds. When using “HH:MM:SS” or “MM:SS”, time may be a more appropriate ‘Wait For’ condition, but either seconds or time are permitted. The following rows are all equivalent:

position

Wait For

Value

1

seconds

120

1

time

01:00

1

seconds

00:01:00

1

time

00:01:00

A column ‘Or Time’ can be added to allow the scan to continue even if the condition is not met:

position

Wait For

Value

Or Time

[2,4,7]

counter

10000

01:00:00

The scan generated by this table will set the ‘position’ to three different values. At each step, it will wait until either the ‘counter’ reaches a value of 10000, or one hour passes.

Parallel Commands

Device columns are typically processed sequentially from left to right, but sometimes it can be useful to for example command two motors or temperature controllers in parallel, waiting for both to proceed at the same time, waiting until both of them reach their respective desired value.

Device columns with a +p prefix are accessed in parallel. In this table, devices A and B will be commanded to some value, waiting for both to get there in parallel. Next, device C is set to some value. Once it reaches its value, devices D and E are again commanded in parallel, and finally device F is set to a value.

+p A

+p B

C

+p D

+p E

F

1

2

3

4

5

6

Result:

Parallel(Set('A', 1.0), Set('B', 2.0))
Set('C', 3.0)
Parallel(Set('D', 4.0), Set('E', 5.0))
Set('F', 6.0)

Columns that set devices in parallel need to be adjacent. Whenever the next column does not use +p, the previously accumulated parallel commands are executed.

If the next column is ‘Wait For’, the behavior depends on the ‘Wait For’ condition. If the special condition completion is used, the Wait For will perform the accumulated parallel commands. For other conditions, the accumulated commands are executed and then the Wait For awaits the requested condition as usual. In this example we also assume that Wait For uses start/stop commands as explained below.

+p A

+p B

C

+p D

+p E

Wait For

Value

1

2

3

4

5

completion

6

7

8

9

10

Seconds

10

Result:

Parallel(Set('A', 1.0), Set('B', 2.0))
Set('C', 3.0)
Comment('Start Run')
Parallel(Set('D', 4.0), Set('E', 5.0))
Log('A', 'B', 'C', 'D', 'E')
Comment('Stop Run')
Parallel(Set('A', 6.0), Set('B', 7.0))
Set('C', 8.0)
Parallel(Set('D', 9.0), Set('E', 10.0))
Comment('Start Run')
Delay(10)
Log('A', 'B', 'C', 'D', 'E')
Comment('Stop Run')

Note that the ‘Wait For’ with completion can be useful even for a single parallel command. In this example, A is written by itself, not in parallel with other operations, but the parallel mechanism allows you to wait for the completion, i.e. a run is started and then stopped while A is set:

+p A

Wait For

Value

1

completion

Result:

Comment('Start Run')
Parallel(Set('A', 1.0))
Log('A')
Comment('Stop Run')

Another example, which again includes start/stop commands as well as a special Delay column as described below:

+p A

+p B

Delay

Wait For

Value

1

2

00:05:00

counts

10

Result:

Parallel(Set('A', 1.0), Set('B', 2.0))
Delay(300)
Comment('Start Run')
Wait('counts', 10.0, comparison='>=', tolerance=0.1)
Log('A', 'B', 'counts')
Comment('Stop Run')

Log Additional Devices

The example just shown ends in:

Log('x', 'y', 'counter')

because all devices used in a row are logged upon completing the ‘Wait For’ condition. These devices could have been affected by a column that set them to a value, or the ‘Wait For’ condition depended on the device.

To log additional devices, even though they are otherwise not mentioned in the table, pass the log_always parameter to the table:

table = TableScan(headers, rows, log_always=[ 'neutrons' ])

will include the device neutrons whenever it logs values at the end of a row.

Pre, Post, Start and Stop Commands

Both the devices listed in the table column headers and the values used in the cells of the rows of the table are typically edited for different runs of an experiment.

There are, however, often the same actions that need to happen before and after the complete table is executed, as well as at each step of a table.

You can provide a list of commands for the following steps:

pre:

Before executing the table rows. Example: Open a beam line shutter.

post:

After executing all table rows. Example: Close a beam line shutter.

start:

Before each ‘Wait For’. Example: Zero counter PVs, in fact counters which the ‘Wait For’ command will then check to reach a certain value, and start data acquisition.

stop:

After each ‘Wait For’ completes. Example: Stop data acquisition.

Special Column handling

Going back to the most basic behavior of the table scan, given a column name and a cell value, each non-empty cell results in a command:

Set(column_name, cell_value)

For example, a column named “RunControl” with cell values “start” and “stop”:

RunControl

Start

Stop

would result in these commands:

Set("RunControl", "Start")
Set("RunControl", "Stop")

Assuming that the process variable “RunControl” is an enumerated type with valid states “start” and “stop”, maybe connected to an IOC sequence to start and stop a data acquisition run, this will work just fine.

Extending the example, assume that you would rather use “Run Control” as a column name to distinguish it from an ordinary process variable, and the cell values should result in Include commands.

Another example would be a “Delay” column that should turn into a plain delay.

To handle such special cases, the TableScan API allows you to provide a dictionary with special column handler functions. Each function is called with the value of the cell, and it must return a scan command.

Example:

from scan.util.seconds import parseSeconds
special_handlers = { 'Run Control': lambda cell : Include(cell + ".scn"),
                     'Delay':       lambda cell : Delay(parseSeconds(cell)),
                   }
table_scan = TableScan(
  (   "Run Control", "X",  "Delay",    "Wait For", "Value", ),
  [
    [ "Start",       "10", "",         "Neutrons", "10" ],
    [ "",            "20", "00:01:00", "Neutrons", "10" ],
    [ "Stop",        "",   "",         "",         "" ],
  ],
  special = special
)

When handling cells for the “Run Control” column, the special handler function will now be invoked with the cell values, i.e. “Start” and “Stop”:

lambda cell : Include(cell + ".scn")

resulting in these commands:

Include("Start.scn")
Include("Stop.scn")

The “Delay” column will invoke this handler:

lambda cell : Delay(parseSeconds(cell))

resulting in this for the above example:

Delay(60)

The special handler function can wrap multiple commands as a Sequence command.

API

class scan.table.table_scan.TableScan(headers, rows, pre=None, post=None, start=None, stop=None, log_always=None, special={})

Create Table scan

Parameters
  • headers[] – Column headers of the table

  • rows[][] – Rows of the scan. Each row must have len(headers) columns.

  • pre – Command or list of commands executed at the start of the table.

  • post – Command or list of commands executed at the end of the table.

  • start – Command or list of commands executed to start each ‘Wait For’.

  • stop – Command or list of commands executed at the end of each ‘Wait For’.

  • log_always – Optional list of device names that should be logged in addition to those that are affected by columns that set them or ‘Wait For’ conditions.

  • special – Dictionary with special column handlers

createScan(lineinfo=True)

Create scan.

Parameters

lineinfo – By default Comment commands are added for line info. If scan settings include a “table_scan_row”, that PV will also be set.

Returns

List of commands.

save(filename)

Save table to file

Writes table as CSV file.

Parameters

filename – File path, must end in “.csv” or “.tab”

scan.table.table_scan.is_list(x)

Check if argument is a list of some form

scan.table.table_scan.loadTableScan(filename, pre=None, post=None, start=None, stop=None)

Load table from spreadsheet file

Parameters
  • filename – File name, either ‘.cvs’, ‘.tab’, ‘.xls’ or ‘.gnumeric’

  • pre – Command or list of commands executed at the start of the table.

  • post – Command or list of commands executed at the end of the table.

  • start – Command or list of commands executed to start each ‘Wait For’.

  • stop – Command or list of commands executed at the end of each ‘Wait For’.