Tutorial

Basic examples

First, we’ll just do a simple measurement on the main detector for 600 frames.

>>> from SansScripting import * 
>>> do_sans("Sample Name", frames=600)
Setup Larmor for event
Using the following Sample Parameters
Geometry=Flat Plate
Width=10
Height=10
Thick=1.0
Measuring Sample Name_SANS for 600 frames
>>> do_trans("Sample Name", frames=180)
Setup Larmor for transmission
Using the following Sample Parameters
Geometry=Flat Plate
Width=10
Height=10
Thick=1.0
Measuring Sample Name_TRANS for 180 frames

The ScanningInstrument.do_sans() and ScanningInstrument.do_trans() functions are both simple wrappers around ScanningInstrument.measure(). measure is the primary entry point for all types of SANS measurement. All of the parameters that will be covered for measure can also be applied to do_sans and do_trans. Below is an example of the extended information that can be passed to these functions.

>>> measure("Sample Name", "QT", aperature="Medium", blank=True, uamps=5)
Setup Larmor for event
Moving to sample changer position QT
Using the following Sample Parameters
Geometry=Flat Plate
Width=10
Height=10
Thick=1.0
Measuring Sample Name_SANS for 5 uamps

A couple of things changed with this new command.

  1. I’ve measured for 5 µamps instead of the 600 frames we did before. The measure command will take and of the time commands that genie_python’s waitfor command will accept, though uamps, frames, and seconds will almost always be the ones which are needed.
  2. We’ve passed sample position QT in as the position parameter and the instrument has dutifully moved into position QT before starting the measurement.
  3. We specified the beam size. The individual beamlines will have the opportunity to decide their own aperature settings, but they should hopefully reach a consensus on the names.
  4. The sample has been marked as a blank. The MEASUREMENT:TYPE block in the run’s journal entry will be set to “blank”, instead of “sans”. Had this been a transmission measurement, the block would have been set to “blank_transmission”
>>> measure("Sample Name", CoarseZ=25, uamps=5, thickness=2.0, trans=True, blank=True)
Setup Larmor for transmission
Moving CoarseZ to 25
Using the following Sample Parameters
Geometry=Flat Plate
Width=10
Height=10
Thick=2.0
Measuring Sample Name_TRANS for 5 uamps

Here we are directly setting the CoarseZ motor on the sample stack to our desired position, instead of just picking a position for the sample changer. We have also recorded that this run is on a 2 mm sample, unlike our previous 1 mm runs. Finally, the instrument has converted into transmission mode, setting the appropriate wiring tables and moving the M4 monitor into the beam.

>>> measure("Sample Name", "CT", SampleX=10, Julabo1_SP=35, uamps=5)
Setup Larmor for event
Moving to sample changer position CT
Moving Julabo1_SP to 35
Moving SampleX to 10
Using the following Sample Parameters
Geometry=Flat Plate
Width=10
Height=10
Thick=1.0
Measuring Sample Name_SANS for 5 uamps

We can combine a sample changer position with motor movements. This is useful for custom mounting that may not perfectly align with the sample changer positions. Alternately, since any block can be set within the measure command, it is also possible to set temperatures and other beam-line parameters for a measurement.

>>> def weird_place():
...   gen.cset(Translation=100)
...   gen.cset(CoarseZ=-75)
>>> measure("Sample Name", weird_place, Julabo1_SP=37, uamps=10)
Moving to position weird_place
Moving Julabo1_SP to 37
Using the following Sample Parameters
Geometry=Flat Plate
Width=10
Height=10
Thick=1.0
Measuring Sample Name_SANS for 10 uamps

Finally, if the experiment requires a large number of custom positions, they can be set independently in their own functions. Measure can then move to that position as though it were a standard sample changer position. It’s still possible to override or amend these custom positions with measurement specific values, as we have done above with the Julabo temperature again.

>>> measure("Sample Name", 7, Julabo1_SP=37, uamps=10)
Traceback (most recent call last):
...
TypeError: Cannot understand position 7

If the position is gibberish, the instrument will raise an error and not try to start a measurement in an unknown position.

>>> set_default_dae(setup_dae_bsalignment)
>>> measure("Beam stop", frames=300)
Setup Larmor for bsalignment
Using the following Sample Parameters
Geometry=Flat Plate
Width=10
Height=10
Thick=1.0
Measuring Beam stop_SANS for 300 frames

The default DAE mode for all SANS measurements is event mode. This can be overridden with the ScanningInstrument.set_default_dae() function, which will assign a new default SANS method. This new event mode will be used for all future SANS measurements. For brevity, the ScanningInstrument.set_default_dae() will also take a string argument. The first line can also be run as

>>> set_default_dae("bsalignment")

It’s similarly possible to set the default dae for transmission measurements.

>>> set_default_dae("bsalignment", trans=True)
>>> set_default_dae("transmission", trans=True)
>>> measure("Beam stop", dae="event", frames=300)
Setup Larmor for event
Using the following Sample Parameters
Geometry=Flat Plate
Width=10
Height=10
Thick=1.0
Measuring Beam stop_SANS for 300 frames

The ScanningInstrument.measure() function also has a dae keyword parameter that is automatically passed to setup_default_dae(). The above example puts the instrument back into event mode.

>>> enumerate_dae()
['4periods', 'bsalignment', 'diffraction', 'event', 'event_fastsave', 'histogram', 'monitorsonly', 'monotest', 'nr', 'nrscanning', 'polarised', 'resonantimaging', 'resonantimaging_choppers', 'scanning', 'sesans', 'transmission', 'tshift']

The ScanningInstrument.enumerate_dae() function will list all of the supported dae modes on the current beamline.

Automated script checking

This module includes a decorator user_script() that can be added to the front of any user function. This will allow the scripting system to scan the script for common problems before it is run, ensuring that problems are noticed immediately and not at one in the morning. All that’s required of the user is putting @user_script on the line before any functions that they define.

>>> @user_script
... def trial(time, trans):
...     measure("Test1", "BT", uamps=time)
...     measure("Test2", "VT", uamps=time)
...     measure("Test1", "BT", trans=True, uanps=trans)
...     measure("Test2", "VT", trans=True, uamps=trans)
>>> trial(30, trans=10)
Traceback (most recent call last):
...
RuntimeError: Position VT does not exist

What may not be immediately obvious from reading is that this error message occurs instantly, not forty five minutes into the run after the first measurement has already been performed. Fixing the “VT” positions to “CT” then gives:

>>> @user_script
... def trial():
...     measure("Test1", "BT", uamps=30)
...     measure("Test2", "CT", uamps=30)
...     measure("Test1", "BT", trans=True, uanps=10)
...     measure("Test2", "CT", trans=True, uamps=10)
>>> trial()
Traceback (most recent call last):
...
RuntimeError: Unknown Block uanps

Again, an easy typo to make at midnight that normally would not be found until two in the morning.

>>> @user_script
... def trial():
...     measure("Test1", "BT", uamps=30)
...     measure("Test2", "CT", uamps=30)
...     measure("Test1", "BT", trans=True, uamps=10)
...     measure("Test2", "CT", trans=True, uamps=10)
>>> trial() 
The script should finish in 2.0 hours
...
Measuring Test2_TRANS for 10 uamps

Once the script has been validated, which should happen nearly instantly, the program will print an estimate of the time needed for the script and the approximate time of completion (not shown). It will then run the script for real.

Large script handling

The ScanningInstrument.measure_file() function allows the user to define everything in a CSV file with excel and then run it through python.

test.csv
title uamps pos thickness trans CoarseZ
Sample1 10 AT 1    
Sample2 10 BT 1   30
Sample3 10 CT 2    
Sample4 10 DT 2    
Sample5 20 ET 1    
Sample1 10 AT 1 TRUE  
Sample2 10 BT 1 TRUE 30
Sample3 10 CT 2 TRUE  
Sample4 10 DT 2 TRUE  
Sample5 20 ET 1 TRUE  
>>> measure_file("tests/test.csv") 
The script should finish in 3.0 hours
...
Measuring Sample5_TRANS for 20 uamps

The particular keyword argument to the ScanningInstrument.measure() function is given in the header on the first line of the file. Each subsequent line represents a single run with the parameters given in the columns of that row. If an argument is left blank, then the keyword’s default value is used. The boolean values True and False are case insensitive, but all other strings retain their case.

bad_julabo.csv
title uamps pos thickness trans Julabo
Sample1 10 AT 1    
Sample2 10 BT 1 TRUE 7
>>> measure_file("tests/bad_julabo.csv") 
Traceback (most recent call last):
...
RuntimeError: Unknown Block Julabo

Each CSV file is run through the user_script() function defined above, so the script will be checked for errors before being run. In the example above, the user set the column header to “Julabo”, but the actual block name is “Julabo1_SP”.

If we fix the script file

good_julabo.csv
title uamps pos thickness trans frames
Sample1 10 AT 1    
Sample2 5 AT 1 TRUE  
Sample2 5 BT 1 TRUE  
Sample2 10 BT 1 FALSE  
Sample3   CT 2 TRUE 3000
Sample3   CT 2 FALSE 6000
>>> measure_file("tests/good_julabo.csv") 
The script should finish in 1.0 hours
...
Measuring Sample3_SANS for 6000 frames

The scan then runs as normal.

>>> measure_file("tests/good_julabo.csv", forever=True) 

If the users are leaving and you want to ensure that the script keeps taking data until they return, the forever flag causes the instrument to repeatedly cycle through the script until there is a manual intervention at the keyboard. The output is not shown above because there is infinite output.

>>> from __future__ import print_function
>>> convert_file("tests/good_julabo.csv")
>>> with open("tests/good_julabo.csv.py", "r") as infile:
...     for line in infile:
...         print line,
from SansScripting import *
@user_script
def good_julabo():
    do_sans("Sample1", "AT", uamps=10, thickness=1)
    do_trans("Sample2", "AT", uamps=5, thickness=1)
    do_trans("Sample2", "BT", uamps=5, thickness=1)
    do_sans("Sample2", "BT", uamps=10, thickness=1)
    do_trans("Sample3", "CT", thickness=2, frames=3000)
    do_sans("Sample3", "CT", thickness=2, frames=6000)

When the user is ready to take the next step into full python scripting, the CSV file can be turned into a python source file that performs identical work. This file can then be edited and customised to the user’s desires.

Detector Status

As an obvious sanity check, it is possible to check if the detector is on.

>>> detector_on()
True

We can also power cycle the detector.

>>> detector_on(False)
Waiting For Detector To Power Down (60s)
False

If we try to perform a measurement with the detector off, then the measurement will fail.

>>> measure("Sample", frames=100)
Traceback (most recent call last):
...
RuntimeError: The detector is off.  Either turn on the detector or use the detector_lock(True) to indicate that the detector is off intentionally

Performing transmission measurements does not require the detector

>>> detector_on(False)
Waiting For Detector To Power Down (60s)
False
>>> measure("Sample", trans=True, frames=100)
Setup Larmor for transmission
Using the following Sample Parameters
Geometry=Flat Plate
Width=10
Height=10
Thick=1.0
Measuring Sample_TRANS for 100 frames
>>> detector_on(True)
Waiting For Detector To Power Up (180s)
True

If the detector needs to run in a special configuration (e.g. due to electrical problems), the detector state can be locked. This will prevent attempts to turn the detector on and off and will bypass any checks for the detector state:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> detector_lock()
False
>>> detector_on(False)
Waiting For Detector To Power Down (60s)
False
>>> detector_lock(True)
True
>>> measure("Sample", frames=100)
Setup Larmor for event
Using the following Sample Parameters
Geometry=Flat Plate
Width=10
Height=10
Thick=1.0
Measuring Sample_SANS for 100 frames
>>> detector_on(True)
Traceback (most recent call last):
...
RuntimeError: The instrument scientist has locked the detector state
>>> detector_lock(False)
False
>>> detector_on(True)
Waiting For Detector To Power Up (180s)
True

Custom Running Modes

Some modes may be much more complicated than a simple sans measurement. For example, a SESANS measurement needs to setup the DAE for two periods, manage the flipper state, and switch between those periods. From the user’s perspective, this is all handled in the same manner as a normal measurement.

>>> set_default_dae(setup_dae_sesans)
>>> measure("SESANS Test", frames=6000)
Setup Larmor for sesans
Using the following Sample Parameters
Geometry=Flat Plate
Width=10
Height=10
Thick=1.0
Measuring SESANS Test_SESANS for 6000 frames
Flipper On
Flipper Off
Flipper On
Flipper Off
Flipper On
Flipper Off

In this example, the instrument scientist has written two functions Larmor._begin_sesans() and Larmor._waitfor_sesans() which handle the SESANS specific nature of the measurement.

>>> measure("SESANS Test", u=1500, d=1500, uamps=10)
Using the following Sample Parameters
Geometry=Flat Plate
Width=10
Height=10
Thick=1.0
Measuring SESANS Test_SESANS for 10 uamps
Flipper On
Flipper Off
Flipper On
Flipper Off
Flipper On
Flipper Off

These custom mode also allow more default parameters to be added onto ScanningInstrument.measure(). In this instance, the u and d parameters set the number of frames in the up and down states.

Reduction Script Generation

A small amount of metadata is attached to each run. It’s possible to generate a reduction script from this metadata.

>>> d = sesans_connection(0, 110, path="tests/sans.xml")

The variable d will hold every possible sesans measurement that could be collected from runs 29200 through 29309 in a nested dictionary. The orders of the keys will be the sample name, the blank name, and finally the magnet angle.

>>> d["example in pure h2o"]["h2o blank"]["20.0"]
{'Sample': [88, 98, 107], 'P0Trans': [89], 'P0': [90, 99, 108], 'Trans': [87]}

Once we’ve chose out instrument parameters, we get a labelled set of run numbers which describe the reduction that we want to perform.

>>> sesans_reduction("tests/sesans_out.py", d, {"example in pure h2o": "h2o blank"})

sesans_reduction() take a file name, the connected sesans data, and a dictionary where the keys are the sample names and the values are the appropriate blanks for those samples. A python script is written to the file which will perform the data reduction in Mantid for those given runs.

sesans_out.py
reduction({'Sample': [88, 98, 107], 'P0Trans': [89], 'P0': [90, 99, 108], 'Trans': [87]})
reduction({'Sample': [92, 101], 'P0Trans': [89], 'P0': [93, 102], 'Trans': [87]})
reduction({'Sample': [95, 104], 'P0Trans': [89], 'P0': [96, 105], 'Trans': [87]})

The above code can use the sesans reduction library to create .SES files for all of the desired runs.

For the majority of simple cases, we can use the identify_pairs() to save us on much of the boiler plate of reducing samples.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
>>> d = sans_connection(70, 110, path="tests/sesans.xml")
>>> pairs = identify_pairs(d, oracle=test_oracle)
What is the blank for the sample: example in pure h2o
1: air blank
2: example solvent 1mm cell
3: h2o blank
3
What is the blank for the sample: example solution 23 1mm cell
1: air blank
2: example solvent 1mm cell
3: h2o blank
2
What is the blank for the sample: polar bear p1 across hairs
1: air blank
2: example solvent 1mm cell
3: h2o blank
1
What is the blank for the sample: polar bear p1 along hairs
1: air blank
2: example solvent 1mm cell
3: h2o blank
1
What is the blank for the sample: polar bear p2 across hairs
1: air blank
2: example solvent 1mm cell
3: h2o blank
1
What is the blank for the sample: polar bear p2 along hairs
1: air blank
2: example solvent 1mm cell
3: h2o blank
1

In the above, identify pairs() asked the user to find the correct blank for each sample, which the user gave by submitting a number. This then creates the pairs dictionary, like the one manually created above, but with less effort and typing. This can then be used in the sans_reduction or sesans_reduction, as normal.

Note

The oracle parameter was only needed in this instance because we’re inside the test framework. Under normal conditions, that parameter can be ignored.

>>> sans_reduction("tests/sans_out.py", d, pairs, "Mask.txt", direct=85)

The sans_reduction() function takes the same parameters as sesans_reduction(), plus two more. The first is a mask file, as is used by all SANS reduction scripts. The second is the run number for the direct run.

sans_out.py
from ISISCommandInterface import MaskFile, AddRuns, AssignSample, AssignCan
from ISISCommandInterface import TransmissionSample, TransmissionCan
from ISISCommandInterface import WavRangeReduction
MaskFile('Mask.txt')
#  example in pure h2o
sample = AddRuns([88, 92, 95, 98, 101, 104, 107])
AssignSample(sample)
trans = AddRuns([87])
TransmissionSample(trans,85)
can = AddRuns([90, 93, 96, 99, 102, 105, 108])
AssignCan(can)
can_tr = AddRuns([89])
TransmissionCan(can_tr,85)
WavRangeReduction(3, 9)
#  polar bear p1 along hairs
#  Error: Missing transmission information
#  polar bear p2 along hairs
#  Error: Missing transmission information
#  polar bear p1 across hairs
sample = AddRuns([84])
AssignSample(sample)
trans = AddRuns([83])
TransmissionSample(trans,85)
can = AddRuns([80, 86])
AssignCan(can)
can_tr = AddRuns([85])
TransmissionCan(can_tr,85)
WavRangeReduction(3, 9)
#  polar bear p2 across hairs
sample = AddRuns([82])
AssignSample(sample)
trans = AddRuns([81])
TransmissionSample(trans,85)
can = AddRuns([80, 86])
AssignCan(can)
can_tr = AddRuns([85])
TransmissionCan(can_tr,85)
WavRangeReduction(3, 9)
#  example solution 23 1mm cell
#  Error: Missing transmission information

Under the hood

>>> gen.reset_mock()
>>> measure("Test", "BT", dae="event", aperature="Medium", uamps=15)
Setup Larmor for event
Moving to sample changer position BT
Using the following Sample Parameters
Geometry=Flat Plate
Width=10
Height=10
Thick=1.0
Measuring Test_SANS for 15 uamps

This command returns no result, but should cause a large number of actions to be run through genie-python. We can verify those actions through the mock genie object that’s created when the actual genie-python isn’t found.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
>>> print(gen.mock_calls)
[call.get_runstate(),
 call.get_pv('IN:LARMOR:CAEN:hv0:0:8:status'),
 call.get_pv('IN:LARMOR:CAEN:hv0:0:9:status'),
 call.get_pv('IN:LARMOR:CAEN:hv0:0:10:status'),
 call.get_pv('IN:LARMOR:CAEN:hv0:0:11:status'),
 call.set_pv('IN:LARMOR:PARS:SAMPLE:MEAS:TYPE', 'sesans'),
 call.change(nperiods=1),
 call.change_start(),
 call.change_tables(detector='C:\\Instrument\\Settings\\Tables\\detector.dat'),
 call.change_tables(spectra='C:\\Instrument\\Settings\\Tables\\spectra_1To1.dat'),
 call.change_tables(wiring='C:\\Instrument\\Settings\\Tables\\wiring_event.dat'),
 call.change_tcb(high=100000.0, log=0, low=5.0, step=100.0, trange=1),
 call.change_tcb(high=0.0, log=0, low=0.0, step=0.0, trange=2),
 call.change_tcb(high=100000.0, log=0, low=5.0, regime=2, step=2.0, trange=1),
 call.change_finish(),
 call.cset(T0Phase=0),
 call.cset(TargetDiskPhase=2750),
 call.cset(InstrumentDiskPhase=2450),
 call.cset(m4trans=200.0),
 call.set_pv('IN:LARMOR:PARS:SAMPLE:MEAS:LABEL', 'Test'),
 call.cset(a1hgap=20.0, a1vgap=20.0, s1hgap=14.0, s1vgap=14.0),
 call.cset(SamplePos='BT'),
 call.waitfor_move(),
 call.change_sample_par('Thick', 1.0),
 call.get_sample_pars(),
 call.change(title='Test_SANS'),
 call.begin(),
 call.waitfor(uamps=15),
 call.end()]

That’s quite a few commands, so it’s worth running through them.

2:Ensure that the instrument is ready to start a measurement
3-6:Check that the detector is on
7:Check that the detector is on
8-19:Put the instrument in event mode
20:Move the M4 transmission monitor out of the beam
21:Set the upstream slits
22:Move the sample into position
23:Let motors finish moving.
24:Set the sample thickness
25:Print and log the sample parameters
26:Set the sample title
27:Start the measurement.
28:Wait the requested time
29:Stop the measurement.