Skip to content

Measurements

William Wood edited this page Mar 25, 2021 · 12 revisions

Measurements

JISA provides a means of neatly packaging any measurement routines you may want to run into what it calls Measurement objects. These provide you with several advantages including:

  • Automatic threading and interrupts for measurements (lets you have a "stop" button)
  • Simple error handling
  • Compatibility with MeasurementConfigurator GUI elements
  • Compatibility with ActionQueue objects

Contents

  1. Using a Measurement Object
  2. Methods
    1. The getName() Method
    2. The getColumns() Method
    3. The run() Method
    4. The onFinish() Method
    5. The onInterrupt() Method
    6. The onError() Method
  3. Interrupts, Sleeps and Checkpoints
  4. User-Configurable Parameters
    1. Parameters
    2. Instruments
  5. Completed Example
  6. Measurement Configurators
  7. Example in Kotlin

Using a Measurement Object

If you have a Measurement object, then you will want to use it like so:

Measurement myMeasurement = ...;

// Tells the measurement to generate a new ResultTable
myMeasurement.newResults();

// Tells the measurement to generate a new ResultTable
// that will stream results directly to a file
myMeasurement.newResults("/path/to/file.csv");

// Starts the measurement
myMeasurement.start();

// Stops the measurement if it's running
myMeasurement.stop();

// Returns whether the measurement is currently running or not
boolean running = myMeasurement.isRunning();

For instance, you may wish to add a "Start" and "Stop" button to a toolbar to control the measurement like so:

Measurement measurement = ...;
Grid        window      = new Grid("My Window");

window.addToolbarButton("Start", () -> {

    if (measurement.isRunning()) {
        GUI.errorAlert("Measurement is already running!");
    } else {

        measurement.newResults();

        try {
            measurement.start();
        } catch (InterruptedException e) {
            // Measurement was stopped early
        } catch (Exception e) {
            // Measurement encountered an error
        }

    }

});

window.addToolbarButton("Stop", () -> {
    
    if (measurement.isRunning()) {
        measurement.stop();
    } else {
        GUI.errorAlert("No measurement is currently running!");
    }

});

window.show();

Methods

The Measurement class is what's known as an "abstract" class in Java. This means that while it has some functionality built-in, some of its methods have no implementation (i.e. they are defined but have no code in them). Therefore, when creating a class that extends Measurement you will be required to implement a number of methods. This is how the Measurement class enables you to standardise and package your measurements. Effectively, it's a template.

Let's say we want to create a Measurement class for measuring the conductivity of something using a current source (ISource) and a voltmeter (VMeter). To do this, let's define a class called ConductivityMeasurement that extends Measurement:

public class ConductivityMeasurement extends Measurement {}

If you are using something like IntelliJ IDEA from JetBrains, then it will want to automatically implement the required methods for you. These are as follows:

public class ConductivityMeasurement extends Measurement {
    
    public String getName() {

    }

    public Col[] getColumns() {

    }
    
    protected void run(ResultTable results) throws Exception {

    }
    
    protected void onInterrupt() throws Exception {

    }
    
    protected void onError() throws Exception {

    }
    
    protected void onFinish() throws Exception {

    }

}

Let's take a look at each of these one-by-one.

The getName() Method

This method is quite simple, it should just return a human-readble name for this measurement. In our case that would be "Conductivity Measurement". Thus we would write:

public String getName() {
    return "Conductivity Measurement";
}

The getColumns() Method

This method is how we define what the structure of this measurement's ResultTable should look like. We do this by returning an array of Col objects to define the columns of the ResultTable.

In our case, we will want: "Injected Current" and "Measured Voltage". Therefore we would write:

public Col[] getColumns() {

    return new Col[]{
        new Col("Injected Current", "A"),
        new Col("Measured Voltage", "V")
    };

}

The run() Method

This is where the measurement code goes. The ResultTable to be used for recording results is passed to this method as the results argument. If we assume that our ISource is called currentSource and our VMeter is called voltMeter then we would write something like this:

public void run(ResultTable results) throws Exception {

    currentSource.setCurrent(0.0);

    currentSource.turnOn();
    voltMeter.turnOn();

    for (double current : Range.linear(0, 10e-6, 11)) {

        currentSource.setCurrent(current);
        sleep(500);

        results.addData(
            currentSource.getCurrent(),
            voltMeter.getVoltage()
        );

    }

}

The onFinish() Method

This method is always when the measurement has finished - regardless of whether it finished successfully, was interrupted or threw an exception. Therefore, this is where we want to put all and any code needed to return instruments to a safe state.

public void onFinish() throws Exception {
    currentSource.turnOff();
    voltMeter.turnOff();
}

It is a good idea to run each individual instrument command inside a Util.runRegardless(...) so that one command throwing an exception does not prevent the others from running:

public void onFinish() throws Exception {
    Util.runRegardless(() -> currentSource.turnOff());
    Util.runRegardless(() -> voltMeter.turnOff());
}

The onInterrupt() Method

This method is called if the measurement code is interrupted due to measurement.stop() being called. This is often left blank but could be useful if you wanted to log a measurement being interrupted etc. This will be called before onFinish(), so don't put anything in here that will block the thread. After onInterrupt() and onFinish() have finished executing, the measurement will throw an InterruptedException up to whichever thread called the start() method on the Measurement.

If we wanted our conductivity measurement to print to the stderr output when it's interrupted, then we could write:

public void onInterrupt() throws Exception {
    System.err.println("Conductivity Measurement Interrupted.");
}

Then if we started the measurement like so:

try {
    measurement.start();
} catch (InterruptedException e) {
    GUI.warningAlert("Measurement interrupted.");
}

Then interrupting the measurement will cause the interrupt to be logged in stderr, all relevant instruemnts to be returned to a safe state before throwing an InterruptedException causing the warning alert is displayed to the user.

The onError() Method

This method is called if the measurement code throws an exception. This will be called before the onFinish() method, so don't put anything in here that will block the thread. After onError() and onFinish() have completed, the exception that caused onError() to be called in the first place will be thrown up to whichever thread is running the measurement. Much like onInterrupt() this is often left empty. Similarly, when the onError() and onFinish() methods have returned, the Measurement will throw the exception it encountered up to where the start() method was called.

As an example, let's say we wanted our conductivity measurement to write to the stderr output when it encounters an error, we could do so like this:

public void onError() throws Exception {
    System.err.println("Error encountered during conductivity measurement.");
}

And the structure of our try-catch block:

try {
    measurement.start();
} catch (InterruptedException e) {
    GUI.warningAlert("Measurement interrupted.");
} catch (Execption e) {
    GUI.errorAlert(e.getMessage());
}

Interrupts, Sleeps and Checkpoints

To facilitate the ability for the measurement to be interrupted Measurement provides two methods: sleep(...) and checkPoint(). Basically, wherever you use one of these methods will become a point in your measurement code at which it can be interrupted by a measurement.stop() call.

Most measurement will require there to be some element of timing involved (i.e. some amount of "delay" time between setting a voltage/current and measuring). Therefore, the sleep(...) method will become a natural break-point for your code. For instance, in our conductivity example we use sleep() to cause a 500 millisecond pause between setting the injected current and measuring the resulting voltage:

for (double current : currentValues) {

    currentSource.setCurrent(current);
    sleep(500);

    results.addData(...);

}

Therefore, everytime it gets to the sleep(500) call if the stop button has been pressed, it will interrupt the measurement instead of waiting the 500 ms (or however long is left when the button is pressed).

In the case that your measurement code doesn't need any sleep(...) calls, just call checkPoint() periodically in your code at places that you consider to be safe for it to break-out. For instance, if we were to write

for (double current : currentValues) {

    currentSource.setCurrent(current);
    results.addData(currentSource.getCurrent(), voltMeter.getVoltage());

    checkPoint();

}

then the measurement will stop whenever the checkPoint() method is reached after measurement.stop() has been called.

User-Configurable Parameters

Using Measurement objects allows you to interface with the MeasurementConfigurator GUI element to provide a graphical means for users to configure and run a measurement. For this to work, the configurable parameters and instruments your measurement needs will have to be configured in a way that a MeasurementConfigurator can understand.

Parameters

To add configurable parameters to your measurement, you wil need to create corresponding Parameter objects in your measurement - storing them as class variables/properties. There are multiple types of Parameter object depending on what sort of parameter you want the user to input. In each you need to specify:

  • The "section name" of the parameter - this determines in which section or "panel" in the GUI the parameter will be shown in.
  • The name of the parameter
  • Any units the parameter has - if it has none then set this as null
  • The default value of the parameter

Here are examples of different types listed below:

// For text input
new StringParameter("Basic Info", "Name", null, "Conductivity");

// For numerical input
new DoubleParameter("Source-Gate", "Voltage", "V", 0.0);

// For integer numerical input
new IntegerParameter("Repeat", "Repeat Count", null, 5);

// For check-box yes/no true/false input
new BooleanParameter("Voltages", "Sweep Both Ways?", null, false);

// For user-defined ranges of values (default here: 0 to 10e-6 in 11 steps)
new RangeParameter("Source-Drain", "Current", "A", 0.0, 10e-6, 11);

For our example of conductivity we will want the user to specify a range of current values and a delay time (in integer milliseconds) between applying current and measuring voltage. Therefore, we would write:

public class ConductivityMeasurement extends Measurement {

    RangeParameter   currents = new RangeParameter("Source-Drain", "Current", "A", 0, 10e-6, 11);
    IntegerParameter delay    = new IntegerParameter("Basic", "Delay", "ms", 500);
    ...

}

We can then access the values the user has input into these parameters in our run() method by calling getValue() on them like so:

public class ConductivityMeasurement extends Measurement {

    RangeParameter   currents = new RangeParameter("Source-Drain", "Current", "A", 0, 10e-6, 11);
    IntegerParameter delay    = new IntegerParameter("Basic", "Delay", "ms", 500);
    
    ...

    public void run(ResultTable results) throws Exception {

        int           delayTime     = delay.getValue();
        Range<Double> currentValues = currents.getValue();

        currentSource.setCurrent(0.0);

        currentSource.turnOn();
        voltMeter.turnOn();

        for (double current : currentValues) {
            
            currentSource.setCurrent(current);
            sleep(delayTime);

            results.addData(
                currentSource.getCurrent(), 
                voltMeter.getVoltage()
            );

        }

    }

    ...

}

Instruments

User-configurable instruments can be added by using the addInstrument(...) method. This returns a Configuration object representing the instrument configuration and should be stored, like the Parameter objects before, as class variables/properties. The syntax should look like:

Configuration<Type> instrument = addInstrument("Name", Type.class);

For instance, to add an SMU:

Configuration<SMU> smu = addInstrument("SMU", SMU.class);

In our example of conductivity, we want a current source (ISource) and a voltmeter (VMeter), so we would write:

public class ConductivityMeasurement extends Measurement {

    Configuration<ISource> iSource = addInstrument("Current Source", ISource.class);
    Configuration<VMeter>  vMeter  = addInstrument("Voltmeter", VMeter.class);

    ...

}

These can then be used to retrieve the configured instrument object in the run() method by use of getInstrument(). If the user has failed to configure an instrument for a given Configuration then it will return null. You should check for this if your measurement requires the instrument to be present. In our example of conductivity this would look like:

public class ConductivityMeasurement extends Measurement {

    RangeParameter   currents = new RangeParameter("Source-Drain", "Current", "A", 0, 10e-6, 11);
    IntegerParameter delay    = new IntegerParameter("Basic", "Delay", "ms", 500);

    Configuration<ISource> iSource = addInstrument("Current Source", ISource.class);
    Configuration<VMeter>  vMeter  = addInstrument("Voltmeter", VMeter.class);
    
    ...

    public void run(ResultTable results) throws Exception {

        int           delayTime     = delay.getValue();
        Range<Double> currentValues = currents.getValue();
        ISource       currentSource = iSource.getInstrument();
        VMeter        voltMeter     = vMeter.getInstrument();

        if (currentSource == null) {
            throw new Exception("No current source configured!");
        }

        if (voltMeter == null) {
            throw new Exception("No voltmeter configured!");
        }

        currentSource.setCurrent(0.0);

        currentSource.turnOn();
        voltMeter.turnOn();

        for (double current : currentValues) {
            
            currentSource.setCurrent(current);
            sleep(delayTime);

            results.addData(
                currentSource.getCurrent(), 
                voltMeter.getVoltage()
            );

        }

    }

    ...

}

Completed Example

Our conductivity example is complete, combining everything we've talked about together we arrive at:

public class ConductivityMeasurement extends Measurement {

    RangeParameter   currents = new RangeParameter("Source-Drain", "Current", "A", 0, 10e-6, 11);
    IntegerParameter delay    = new IntegerParameter("Basic", "Delay", "ms", 500);

    Configuration<ISource> iSource = addInstrument("Current Source", ISource.class);
    Configuration<VMeter>  vMeter  = addInstrument("Voltmeter", VMeter.class);

    public String getName() {
        return "Conductivity Measurement";
    }

    public Col[] getColumns() {

        return new Col[]{
            new Col("Injected Current", "A"),
            new Col("Measured Voltage", "V")
        };

    }

    public void run(ResultTable results) throws Exception {

        int           delayTime     = delay.getValue();
        Range<Double> currentValues = currents.getValue();
        ISource       currentSource = iSource.getInstrument();
        VMeter        voltMeter     = vMeter.getInstrument();

        if (currentSource == null) {
            throw new Exception("No current source configured!");
        }

        if (voltMeter == null) {
            throw new Exception("No voltmeter configured!");
        }

        currentSource.setCurrent(0.0);

        currentSource.turnOn();
        voltMeter.turnOn();

        for (double current : currentValues) {
            
            currentSource.setCurrent(current);
            sleep(delayTime);

            results.addData(
                currentSource.getCurrent(), 
                voltMeter.getVoltage()
            );

        }

    }

    public void onFinish() throws Exception {

        Util.runRegardless(() -> iSource.getInstrument().turnOff());
        Util.runRegardless(() -> vMeter.getInstrument().turnOff());

    }

    public void onInterrupt() throws Exception {
        System.err.println("Conductivity measurement interrupted.");
    }

    public void onError() throws Exception {
        System.err.println("Conductivity measurement encountered an error.");
    }

}

Measurement Configurators

Now, for the reason we've been writing our measurement in this specific way. If we now create an instance of ConductivityMeasurement like so:

ConductivityMeasurement measurement = new ConductivityMeasurement()

We can pass it onto a MeasurementConfigurator object like so:

MeasurementConfigurator configurator = new MeasurementConfigurator(measurement);

We can then instruct configurator to display itself to the user as a window by use of showInput(). This will draw all the input fields needed to configure the measurement as defined by the Parameter and Configuration objects we defined in the class.

For our example, the resulting window will look like:

and on the "Instrument" tab:

The showInput() call will return true if the window closes by the user pressing "OK" or false otherwise:

if (configurator.showInput()) {
    // User pressed OK
} else {
    // User pressed Cancel or closed the window
}

Combining this with the example at the beginning of this page:

Measurement             measurement = new ConductivityMeasurement();
MeasurementConfigurator config      = new MeasurementConfigurator(measurement);
Table                   table       = new Table("Table of Results");
Plot                    plot        = new Plot("Plot of Results", "Current [A]", "Voltage [V]");
Grid                    window      = new Grid("Measurement Window", table, plot);

window.addToolbarButton("Start", () -> {

    if (measurement.isRunning()) {
        GUI.errorAlert("A measurement is already running.");
    } else {

        if (config.showInput()) {

            table.clear();
            plot.clear();

            ResultTable results = measurement.newResults();

            table.watch(results);
            plot.createSeries().watch(results);

            try {
                measurement.start();
            } catch (InterruptedException e) {
                GUI.warningAlert("Measurement Interrupted.");
            } catch (Exception e) {
                GUI.errorAlert(e.getMessage());
            } finally {
                GUI.infoAlert("Measurement Ended.");
            }

        }

    }
});

window.addToolbarButton("Stop", () -> {

    if (measurement.isRunning()) {
        measurement.stop();
    } else {
        GUI.errorAlert("No measurement is currently running.");
    }
    
});

window.show();

Resulting in the following window:

which after pressing "Start" will present us with the measurement configurator like so:

and after pressing "OK" on that will run the measurement as configured by the user like so:

Example in Kotlin

Kotlin is similar to Java in many ways but also different. Namely, it is much more concise which ends up lending itself very well to writing measurement code in Measurement classes. Here's what the same ConductivityMeasurement class would look like written in Kotlin - with some annotation:

class ConductivityMeasurement : Measurement() {

    val currents = RangeParameter("Source-Drain", "Current", "A", 0.0, 10e-6, 11)
    val delay    = IntegerParameter("Basic", "Delay", "ms", 500)

    val iSource  = addInstrument("Current Source", ISource::class)
    val vMeter   = addInstrument("Voltmeter", VMeter::class)

    // Kotlin "expression" syntax - for methods that just return a value
    override fun getName(): String = "Conductivity Measurement"

    override fun getColumns(): Array<Col> = arrayOf(
        Col("Injected Current", "A"),
        Col("Measured Voltage", "V")
    )

    override fun run(results: ResultTable) {
        
        // Property access syntax: equivalent to writing delay.getValue()
        val delayTime     = delay.value
        val currentValues = currents.value

        // Elvis operator "?:": defines what to do instead if null
        val currentSource = iSource.instrument 
            ?: throw Exception("No Current Source Configured.")

        val voltMeter = vMeter.instrument
            ?: throw Exception("No Voltmeter Configured.")

        // Same as writing currentSource.setCurrent(0.0)
        currentSource.current = 0.0

        currentSource.turnOn()
        voltMeter.turnOn()

        for (current in currentValues) {

            currentSource.current = current
            sleep(delayTime)

            results.addData(currentSource.current, voltMeter.voltage)

        }

    }

    override fun onFinish() {

        // Lambdas don't need to be inside the () of a method call in Kotlin
        // The "?" means only do this if it's not null (returns null otherwise)
        Util.runRegardless { iSource.instrument?.turnOff() }
        Util.runRegardless { vMeter.instrument?.turnOff() }

    }

    override fun onInterrupt() {
        println("Conductivity measurement interrupted.")
    }

    override fun onError() {
        println("Conductivity measurement encountered an error.")
    }

}
Clone this wiki locally