-
Notifications
You must be signed in to change notification settings - Fork 9
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
- Using a
Measurement
Object - Methods
- Interrupts, Sleeps and Checkpoints
- User-Configurable Parameters
- Completed Example
- Measurement Configurators
- Example in Kotlin
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();
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.
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";
}
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")
};
}
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()
);
}
}
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());
}
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.
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());
}
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.
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.
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()
);
}
}
...
}
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()
);
}
}
...
}
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.");
}
}
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:
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.")
}
}
- Getting Started
- Object Orientation
- Choosing a Language
- Using JISA in Java
- Using JISA in Python
- Using JISA in Kotlin
- Exceptions
- Functions as Objects
- Instrument Basics
- SMUs
- Thermometers (and old TCs)
- PID and Temperature Controllers
- Lock-Ins
- Power Supplies
- Pre-Amplifiers
- Writing New Drivers