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)
  • Built-in error handling
  • Compatibility with MeasurementConfigurator GUI elements
  • Compatibility with ActionQueue objects

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();
        measurement.start();            
    }

});

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.

For instance, 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.");
}

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.

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.");
}

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()) {

            measurement.newResults();

            try {
                measurement.start();
            } 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:

Clone this wiki locally