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:

  1. Define new devices in code

  2. Load onto server (object server)

For Jython:

  1. Define classes in Jython scripts that extend ScannableMotionBase

  2. Load them into the object server by importing the Jython module, and make instances of the Jython-defined devices

For Java:

  1. Write new devices in Java implementing different device interfaces. Here we illustrate by writing new Scannable devices

  2. 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’.

_images/Scannable_ClassDiagram.png

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