7. Writing new Device classes in Jython and Java¶
7.1. Introduction¶
New devices can be written using core classes in GDA. These can be written in either Jython or Java.
For both Jython and Java:
Define new devices in code
Load onto server (object server)
For Jython:
Define classes in Jython scripts that extend ScannableMotionBase
Load them into the object server by importing the Jython module, and make instances of the Jython-defined devices
For Java:
Write new devices in Java implementing different device interfaces. Here we illustrate by writing new Scannable devices
Import instances of the classes defined in Spring beans configuration files
To illustrate the process of developing new devices in Java, and incorporating them into GDA, we describe the process of developing several new devices that implement the Scannable interface. These devices are then included in the system by editing configuration files which are read by the server at startup. The devices can then be scanned and manipulated in GDA from the Jython terminal.
Developing software for new devices for GDA is a likely requirement at each site using GDA, to accommodate specific beamline components into the GDA software framework.
Users should first read “Chapter 5: Scanning” in the GDA Users manual for an introduction to the basic data acquisition techniques used in GDA. Below, we describe developing new classes which implement the Scannable interface. This will likely be required development at each site using GDA in order to accommodate specific beamline components into the GDA software framework.
7.2. The Scannable interface and ScannableBase classes¶
All Scannable classes implement the Scannable interface. A core base class implementing the Scannable interface is available in GDA as the class gda.devive.scannable.ScannableBase. New user-defined Scannable implementations should extend ScannableBase. Instances of these will then be visible in the GDA terminal after issuing the command ‘ls Scannable’.
The most important methods for a Scannable to implement are:
getPosition()
asynchronousMoveTo()
isBusy()
Other fields in the Scannable that must be defined are:
name
initial position
inputNames
extraNames
outputFormats
units
A full description of the parameters available in a Scannable implementation is available in ‘Chapter 5: Scanning’ of the GDA Users Manual.
A test class that has static methods for constructing instances of several different types of ‘dummy’ or testable software Scannables is available in the uk.ac.gda.example.server src directory within the gda.org.myls.scannable package. It has methods:
genarateScannableGaussian()
generateScannableGaussian(Gaussian)
generateScannableSine()
generateScannableSine(SineWave)
This generator constructs instances of the two Scannable classes ScannableGaussian, and ScannableSine. These scannables classes differ in the value returned by getPosition(). For ScannableGaussian, the method returns the value of a Gaussian of the specified position, width and height 1, with additional noise if defined, at the specified x value:
@Override
public Object getPosition() throws DeviceException {
// we assume the position is a double - it is only for testing
double x = (Double) super.getPosition();
double x2 = x - centre;
double sigma = 0.425 * width // FWHM -> sd
double noiseVal = height * (Math.random() * noise;
double y = Math.exp(-(x2 * x2) / (sigma * sigma)) + noiseVal;
return new Double[] { x, y };
}
7.3. Description of the Scannable properties and relations between them¶
(This material is derived from ‘Chapter 5: Scanning’ in the GDA Users’ manual; it is repeated here for convenience)
It is obligatory to set the values of several fields in the constructor of all Scannables. These obligatory fields are:
name
inputNames
extraNames
outputFormat
currentPosition
The fields ‘inputNames’, ‘outputNames’, and ‘outputFormat’ together define what numbers this Scannable represents, what they are called, and the format for printing their values out to file or console.
The ‘inputNames’ array defines the size of the array that this Scannable’s rawAsynchronousMoveTo expects. Each element of the inputNames array is a label for that element which is used in file headers etc. Note that this array can be empty (size 0) if required.
The ‘extraNames’ array is used in a similar manner to the inputNames array, but lists additional elements in the array returned by the Scannable’s rawGetPosition() method, i.e. the array returned by getRawPosition() may be larger than the array required by rawAsynchronousMoveTo(). This allows for the possibility that a Scannable may hold and return more information than it needs in order to move pr perform whatever operation it does inside its rawAsynchronousMoveTo() method. This array is normally empty (size 0).
The ‘outputFormat’ array lists the formatting strings for the elements of both the inputNames and extraNames arrays. It is used when printing the output from the rawGetPosition() method to the console and logfiles.
NOTE:
It is an absolute requirement that the length of the outputFormat array is the sum of the lengths of the inputNames and outputNames arrays for the Scannable to work properly.
7.4. Ensuring Correct Scan Behavior from Scannables¶
Scannables inject custom functionality into scans. They should conform to certain rules so that the scan can handle them properly, especially in unexpected events such as a scan abort.
7.4.1. Stopping¶
Any Scannable that takes a finite amount of time to complete a move should implement the stop()
method, giving the scan the option to halt it if needed.
For example, if the Scannable wraps a motor then stop()
should send a request to the motor to stop moving immediately.
7.4.2. Handling Interrupts¶
Scans are Jython commands. When a scan abort is requested. A scan is interrupted by a Jython thread interrupt, therefore handling it inside a Scannable can have odd consequences for the scan’s behavior. Example code:
try {
// Code that throws an InterruptedException, e.g. Thread.sleep()
} catch (InterruptedException e) {
// ...
}
// Or
try {
// Code that throws an InterruptedException, e.g. Thread.sleep()
} catch (Exception e) {
// ...
}
// Or
if (Thread.interrupted()) {
// Any code
}
Avoiding all of the above cases is recommended, if they are unavoidable then it would be best practice to re-interrupt the thread after handling the exception:
// Inside the catch block or if statement
Thread.currentThread().interrupt();
For managing this problem in a Jython context, see Ensuring Correct Scan Behavior from Scannables in the User Guide.
7.5. Add a new device to the server¶
The new device is added to the server by defining it as a bean in a Spring beans configuration file. In the distribution, this file is ‘server.xml’ in the ‘xml’ directory. This file can be consulted for the syntax used to define new object instances as beans in the Spring beans configuration file. The beans defined in this file are loaded into the object server at server startup, and can be accessed and manipulated by the GDA client.
Both getter and constructor dependency injection can be used. Each object on the server must have a ‘name’ property, which is its unique identifier in the server object namespace. As an example, we define several instances of the ScannableGaussian class in demonstration.xml as part of example-config (servers/main/dummy/) contained in the gda-core repository. This demonstrates the use of different bean definitions :
scannableGaussian0 – all properties set in the bean definition
scannableGaussian1 – only the properties of the Gaussian are set in the bean. Other properties such as input and extra names, and output formats are set to defaults in the Java constructor
Similar examples are provided by several instances of the scannableSine class in the Spring configuration file:
scannableSine0 – the name and properties of the sine are set in the bean definition. Default values for other properties, such as input and extra names, and output formats, are defined in the Java class.
scannableSine1 – the properties of the sine are assigned to the object by a test sine bean defined in the bean configuration file (‘testSineWave’ bean)
7.5.1. Example: ScannableGaussian with setter injection¶
Fields of the ScannableGaussian are set as properties in the Spring beans configuration file, and default values defined. Atomic fields are defined with ‘name’ and ‘value’ attributes fields; array fields are defined using the ‘list’ tag:
<bean id='scannableGaussian0' class='org.myls.gda.device.scannable.ScannableGaussian'>
<property name='name' valuew='simpleScannable1'/>
<property name='position' value='0.0'/>
<property name='inputNames'>
<list>
<value>x</value>
</list>
</property>
<property name='extraNames'>
<list>
<value>y</value>
</list>
</property>
<property name='level' value='3'/>
<property name='outputFormat'>
<list>
<value>%5.5G</value>
<value>%5.5G</value>
</list>
</property>
<property name='units'>
<list>
<value>mm</value>
<value>counts</value>
</list>
</property>
</bean>
Now instantiate a ScannableGaussian using a predefined Gaussian Spring bean. Spring beans definition of a test Gaussian object:
<bean id='testGaussian' class='org.myls.gda.device.scannable.Gaussian'>
<property name='testGaussian' value='testGaussian'/>
<property name='centre' value='0.0'/>
<property name='width' value='1.0'/>
<property name='height' value='1.0'/>
<property name='noise' value='0.1'/>
</bean>
This test Gaussian bean can be used to create an instance of a ScannableGaussian using constructor injection with the test Gaussian as a constructor argument:
<bean id='scannableGaussian2'>
<property name='name' value='scannableGassian2'/>
<constructor-arg ref='testGaussian'/>
</bean>
7.5.2. Exercise¶
Start with an empty server_beans.xml file, add Scannable components one by one, and test them in the GDA Jython console (requires server restart to incorporate the new components).
7.6. Examples of other Scannable classes and tests in GDA¶
DummyMotor: from core: gda.device.motor.DummyMotor
ScannableMotorTest: from core/test: gda.device.scannable.ScannableMotorTest
TotalDummyMotor from core (used by test): gda.device.motor.TotalDummyMotor
7.6.1. Example: ScannableGroup¶
Groups of Scannables underwent a rewrite in November 2019, moving differing functionality into ScannableGroups, ScannableGroupNameds and AssemblyBases.
A group of Scannables, (ScannableGroup, ScannableGroupNamed or AssemblyBase), maintains information about a logical grouping of scannables, allowing multiple scannables to be moved at the same time, or allowing further validation for movement, e.g. MotomanRobotScannableGroup’s validation on simultaneous KTheta, KPhi movement. inputNames, extraNames and outputFormat are taken from the constituent Scannables, not maintained as a field of the Group.
A ScannableGroup is a logical group of Scannables that can be created through Spring instantiation or by adding Scannables from the Jython console. Configuring a ScannableGroup configures all of its component Scannables and adds itself as an IObserver, and the default behaviour of adding a Scannable to an already configured ScannableGroup is to configure the Scannable (although it can instead un-configure the group, allowing it to be configured again). ScannableGroups can add, remove or set Scannables by using the Scannables, and additionally can remove Scannables by their index.
A ScannableGroupNames is the above, with the additional ability to manage (add, remove, set) Scannables with their name, through the finder. It does not maintain a list of names, simply taking them from its Scannables. It should be used over a ScannableGroup where the name of scannables will be relevant to their management, e.g. DetectorArm’s getXMotor and equivalent functions, or where managing Scannables through their Findable name is more sensible than through passing the Scannable instance.
An Assembly instead has two sets of behaviours depending on whether it is Configured. If it is not configured, it maintains a list of Scannables and a list of Names of Scannables. Scannables can be added as though a ScannableGroup, but adding a Scannable with its name instead adds the name of the Scannable to this list. Upon configuring, the Assembly adds all of the Scannables for which it has names to its list of Scannables and behaves like a ScannableGroupNamed until it is unconfigured again. It is considered more “light weight” than a ScannableGroupNamed, and could additionally be used where a Scannable might not be initially available so that it cannot be injected as a Spring bean.
Example Spring bean definition of a Scannable and a ScannableGroup or ScannableGroupNamed that contains it:
<bean id="m3c_yaw" class="gda.device.scannable.ScannableMotor">
<property name="motor" ref="m3c_yaw_motor" />
</bean>
<bean id="m3c" class="gda.device.scannable.scannablegroup.ScannableGroup">
<property name="groupMembers">
<list>
<ref bean="m3c_yaw" />
</list>
</property>
</bean>
Example Spring bean definition of an AssemblyBase
<bean id="pgonio_assembly" class="uk.ac.gda.component.model.core.AssemblyBase" >
<property name="name" value="pgonio" />
<property name="groupMemberNames">
<list>
<value>pinx</value>
</list>
</property>
</bean>
7.7. Demonstrate use of Scannable in terminal¶
The new components are now available to be controlled from the GDA client.
7.7.1. Scan 1D¶
The example scanabbles can be scanned and manipulated from the Jython terminal in the GDA GUI.
Scan the example scannable scannableGaussian0 from -2 to 2 in steps of 0.01:
>>> scan scannableGaussian0 -2.0 2.0 0.1
Change the width of scannableGaussian0 from 1 to 2, and rescan:
>>> scannableGaussian0.setWidth(2)
>>> scan scannableGaussian0 -2.0 2.0 0.1
Change the centre of scannableGaussian0 to -1.0 and rescan:
>>> scannableGaussian0.setCentre(-1)
>>> scan scannableGaussian0 -2.0 2.0 0.1
7.7.2. Nested scan¶
Import the demo scannable classes defined in the user demonstration module scannableClasses.py (located in ‘documentation/users/scripts’, and viewable from the JythonEditor view):
>>> import scannableClasses
>>> from scannableClasses import *
>>> sgw = ScannableGaussianWidth('sgw', scannableGaussian0)
>>> scan sgw 0.2 2.0 0.2 scannableGaussian0 -1.0 1.0 0.02
This nested scan has an outer scan which sets the width of the contained scannable Gaussian to different values from 0.2 to 2.0 in steps of 0.2. The inner scannable is then plotted for each width from -1.0 to 1.0 in steps of 0.02