Skip to content

Configurators

William Wood edited this page Jul 23, 2019 · 7 revisions

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.

Creating a Configurator

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:

Using a 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!

Example: Temperature-Depedent Output Curve

Kotlin

fun main() {

  val config      = ConfigStore("t-dep-output")
  val connections = ConnectionGrid("Connections")

  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(true)
      controller.waitForStableTemperature(T, 1.0, 60000)

      val plot = Plot("Output Curve $T K", "Source-Drain Voltage [V]", "Drain Current [A]")

      plot.createSeries()
          .watch(results)
          .filter({it.get(0) == T})
          .split(3, "Gate: %s V")

      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.show()

}

Resulting in:

And when "Start" is pressed we get a new plot window for each temperature:

Clone this wiki locally