-
Notifications
You must be signed in to change notification settings - Fork 9
Configurators
In JISA
there are a set of GUI elements known as Configurator
elements. Each type is for a different instrument type and allow the user to select which out of a selection of connected instruments to use for a given purpose as well as letting them define standard configuration parameters (like range settings etc).
Using these makes your program more flexible by allowing your user choose which instruments (and which channels/sensors/output on that instrument) are used to fulfil specific roles without having to change your code.
To create a configurator, instantiate a Configurator
object. Which type depends on what type of instrument it is meant to configure:
Java
Configurator<SMU> smuConfig = new Configurator.SMU(...);
Configurator<VMeter> vmeterConfig = new Configurator.VMeter(...);
Configurator<TC> tcConfig = new Configurator.TC(...);
Configurator<TMeter> tmeterConfig = new Configurator.TMeter(...);
Kotlin
val smuConfig = Configurator.SMU(...)
val vmeterConfig = Configurator.VMeter(...)
val tcConfig = Configurator.TC(...)
val tmeterConfig = Configurator.TMeter(...)
Python
smuConfig = Configurator.SMU(...)
vmeterConfig = Configurator.VMeter(...)
tcConfig = Configurator.TC(...)
tmeterConfig = Configurator.TMeter(...)
When doing this you can either give it each individual Connection
object you wish the user to be able to select from or you can give it an entire ConnectionGrid
object and let it extract all the relevant connections automatically:
new Configurator.SMU(name, connection1, connection2, connection3, ...);
new Configurator.SMU(name, connectionGrid);
For example, to create an SMU
based Configurator
that takes a ConnectionGrid
with multiple SMU
connections in it to select and configure which SMU channel to use as source-drain:
ConnectionGrid connections = new ConnectionGrid("Connections");
connections.addSMU("SMU 1");
connections.addSMU("SMU 2");
connections.addSMU("SMU 3");
Configurator<SMU> sourceDrain = new Configurator.SMU("Source-Drain", connections);
connections.show();
sourceDrain.show();
Which would result in two windows, including the ConnectionGrid
:
and the SMU Configurator
:
When you have a Configurator
you can extract an Instrument
object from it, at any time, represented the selected channel/output on the selected instrument configured with the parameters specified in the Configurator
fields.
For instance, taking our previous example of an SMU
based Configurator
, we can extract an SMU
object from our Configurator
using get()
like so:
SMU sdSMU = sourceDrain.get();
When we call this, the Configurator
takes the SMU connected via whichever connection the user has chosen, extracts the channel (specified by the user) as its own SMU
object then applies all the settings to that extracted object before returning it. If the SMU selected is not connected this will return null
instead.
For example if we chose "SMU 1" and "Channel 1", we could represent this process using a diagram like so:
Therefore, this structure is useful for both allowing the user to configure standard instrument parameters but also being able to let them define whether a role is to be fulfilled by, for example, a single-channel SMU or one channel of a multi-channel SMU allowing you to generalise your code to work with any SMU configuration.
For example, if we wanted to write a program that performs FET output-curve measurements then we could write something like so:
val connections = ConnectionGrid("Connections");
connections.addSMU("SMU 1");
connections.addSMU("SMU 2");
val sdConfig = Configurator.SMU("Source-Drain", connections)
val sgConfig = Configurator.SMU("Source-Gate", connections)
which would let us either connect two single-channel SMUs using the ConnectionGrid
and then select "SMU 1" as source-drain and "SMU 2" as source-gate, or we could connect one multi-channel SMU to "SMU 1" and select "SMU 1" for both source-drain and source-gate but using channel 0 for one and channel 1 for the other like so:
This then means the our measurement code would look something like this:
val sourceDrain = sdConfig.get()
val sourceGate = sgConfig.get()
...
for (vSG in Util.makeLinearArray(0,60,7)) {
sourceGate.setVoltage(vSG)
for (vSD in Util.makeLinearArray(0, 60, 61)) {
sourceDrain.setVoltage(vSD)
...
}
}
and would work fine no-matter which configuration of SMU channels we use!
This example is in Kotlin. Note that the way it is set-out (ie all in one function) is not necessarily the best way of organising your code in such a case. However, it is the most straight-forward way and is designed to show you how Configurator
objects work rather than being a good example of code.
import jisa.Util
import jisa.control.ConfigStore
import jisa.experiment.ResultList
import jisa.gui.*
fun main() {
val config = ConfigStore("t-dep-output")
val connections = ConnectionGrid("Connections", config)
connections.addSMU("SMU 1")
connections.addSMU("SMU 2")
connections.addTC("Temperature Controller")
val sdConfig = Configurator.SMU("Source-Drain", "sdsmu", config, connections)
val sgConfig = Configurator.SMU("Source-Gate", "sgsmu", config, connections)
val tcConfig = Configurator.TC("T-Controller", "tc", config, connections)
val configs = Grid("Configurations", sdConfig, sgConfig, tcConfig)
val tParams = Fields("Temperatures")
val minT = tParams.addDoubleField("Min T [K]", 100.0)
val maxT = tParams.addDoubleField("Max T [K]", 300.0)
val numT = tParams.addIntegerField("No. Steps", 3)
val sParams = Fields("Source-Drain Voltages")
val minSDV = sParams.addDoubleField("Start [V]", 0.0)
val maxSDV = sParams.addDoubleField("Stop [V]", 60.0)
val numSDV = sParams.addIntegerField("No. Steps", 61)
val gParams = Fields("Source-Gate Voltages")
val minSGV = gParams.addDoubleField("Start [V]", 0.0)
val maxSGV = gParams.addDoubleField("Stop [V]", 60.0)
val numSGV = gParams.addIntegerField("No. Steps", 7)
val measurement = Grid("Measurement", tParams, sParams, gParams)
measurement.addToolbarButton("Start") {
val results = ResultList("Temperature", "Voltage", "Current", "Gate")
val controller = tcConfig.get()
val sourceDrain = sdConfig.get()
val sourceGate = sgConfig.get()
sourceDrain.setVoltage(0.0)
sourceGate.setVoltage(0.0)
sourceDrain.turnOn()
sourceGate.turnOn()
val tValues = Util.makeLinearArray(minT.get(), maxT.get(), numT.get())
val sdValues = Util.makeLinearArray(minSDV.get(), maxSDV.get(), numSDV.get())
val sgValues = Util.makeLinearArray(minSGV.get(), maxSGV.get(), numSGV.get())
for (T in tValues) {
controller.setTargetTemperature(T)
controller.useAutoHeater()
controller.waitForStableTemperature(T, 1.0, 60000)
val plot = Plot("Output Curve $T K", "Source-Drain Voltage [V]", "Drain Current [A]")
plot.createSeries()
.watch(results, 1, 2)
.filter {r -> r.get(0) == T}
.split(3, "Gate: %s V")
.showMarkers(false)
plot.setYAxisType(Plot.AxisType.LOGARITHMIC)
plot.show()
for (sgV in sgValues) {
sourceGate.setVoltage(sgV)
for (sdV in sdValues) {
sourceDrain.setVoltage(sdV)
Util.sleep(500)
results.addData(T, sourceDrain.getVoltage(), sourceDrain.getCurrent(), sgV)
}
}
}
sourceDrain.turnOff()
sourceGate.turnOff()
}
val mainWindow = Tabs("T-Dep Output", connections, configs, measurement)
mainWindow.setExitOnClose(true)
mainWindow.setMaximised(true)
mainWindow.show()
}
Resulting in:
And when "Start" is pressed we get a new plot window for each temperature:
- 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