-
Notifications
You must be signed in to change notification settings - Fork 1
Creating Sweeps
Sweeps in FetCh are defined similarly to how Measurements are. That is, you
define each type by creating a new class that extends the FetChSweep
class.
This page serves to provide an overview of how this is done.
Before diving into the code, we first need to understand the logic of a sweep. In general, sweeps work like so:
- The user is asked for the sweep parameters and any instruments needed
- The user is asked what should happen at each "sweep point"
- The values of the sweep are generated by using the parameters the user provided in step 1
- For each value generated in step 3, a list of actions is generated based on what the user provided in step 2
- The overall list of all generated actions is added to the queue contained in a "sweep" action
For instance, let's take a look at the Temperature Sweep as an example:
- The user is asked to input the temperature values to sweep over, as well as stability criteria and which temperature controller to use
- The user is asked what actions/measurements should be performed at each temperature
- The sweep knows the temperature values to sweep over because the user entered them directly in step 1
- For each temperature, the sweep adds a "Change Temperature" action which changes to that temperature and waits until the stability cirteria are met, followed by all the actions the user specified in step 2
- All the actions are added to the queue contained in a "Temperature Sweep" action.
To facilitate this, there's multiple parts of a FetchSweep
to implement:
- Defining user-input parameters
- Defining instrument configurations
- Generating/retrieving a list of values to sweep over
- Generating a list of actions to perform for each sweep value
- Defining how to format the sweep values when displayed as text
To define a new sweep, in the org.oefet.fetch.sweep
package create a new class
that extends FetChSweep
. For instance, let's say we have a voltage source and
we want to run various measurements with this voltage source set to different
voltage values (i.e. a "Voltage Sweep"). We will therefore start by defining a VoltageSweep
class like so:
class VoltageSweep : FetChSweep<Double>("Voltage Sweep", "V") {
}
There's a few things to note here. The first is that FetChSweep
is a "generic"
class, meaning we can specify different variable "types" in-between the <...>
depending on what sort of values this sweep will be sweeping over. In our case,
we're just sweeping over voltages, which are just numbers, so we say <Double>
to indicate that the sweep values will just be floating-point numbers. The
constructor arguments for FetChSweep
are: the human-readable name of the sweep
(i.e. "Voltage Sweep"), followed by the symbol used to represent the swept
quantity (in this case voltage, hence "V").
Now the basic structure is defined, we can move onto implementing the various required methods and inputs. This is covered in the subsequent sections.
Now that we have our class, we can start adding things to it. The first thing to think about and add is what parameters we want it to tell FetCh to ask the user for. For instance, if we continue with our two-wire conductivity example, we would want FetCh to ask the user for what values of current to sweep over.
This done by using a feature of Kotlin called "variable delegation". Essentially
what we will do is declare a variable for each parameter but then instead of
assigning it a value, we "delegate" it to FetCh to fill-in for us when it asks
the user what they want it to be. We do this by using the by
keyword and
calling userInput(...)
like so:
class VoltageSweep : FetChSweep<Double>("Voltage Sweep", "V") {
val parameter by userInput("Section", "Name", defaultValue)
}
FetCh will figure out what type of input you're after based on what you provide
as the defaultValue
. In our case, for a voltage sweep, we want a list of
voltages and the amount of time to wait after setting each voltage before
starting measurements/actions for that sweep value. We would therefore write:
class VoltageSweep : FetChSweep<Double>("Voltage Sweep", "V") {
val voltages by userInput("Voltages", "Values [V]", Range.linear(0, 10))
val delay by userInput("Timing", "Delay [ms]", 500)
}
Note that we have asked for the delay time in integer milliseconds. This is
because the various sleep()
methods used to cause the routine to pause require
the time to be provided as such. However, this isn't very user friendly. People
tend to prefer working in seconds, therefore we can use a mapping to convert an
arbitrary amount of seconds into integer milliseconds automatically like so:
val delay by userInput("Timing", "Delay [s]", 0.5) map { (it * 1e3).toInt() }
By writing this, we're essentially telling FetCh that any time we try to read
the value from delay
it should instead tell us the value multiplied by 1000
and rounded to an integer. Therefore, our class now overall looks like:
class VoltageSweep : FetChSweep<Double>("Voltage Sweep", "V") {
val voltages by userInput("Voltages", "Values [V]", Range.linear(0, 10))
val delay by userInput("Timing", "Delay [s]", 0.5) map { (it * 1e3).toInt() }
}
Similar to how FetCh asks the user to input required parameters for the
sweep, it can also ask the user to select which instruments/channels they
would like to be used for specific purposes - allowing for said instruments to
be configured in the process. Much like with parameters, this is done by
delegation. However, this time there's two options: requiredInstrument
and
optionalInstrument
:
val requiredInstrument by requiredInstrument("Name 1", Type::class)
val optionalInstrument by optionalInstrument("Name 2", Type::class)
The difference between the two is that an instrument delegated by use of
requiredInstrument
requires the user to select a connected device to be that
instrument - causing the measurement to give an error if not - whereas the
optionalInstrument
can be left as "None" or disconnected. As an example, you may
want the user to be able to optionally configure a thermometer to record
temperature throughout a measurement or to just be ignored if they don't.
This is all explained in detail on the Measurements wiki page.
class VoltageSweep : FetChSweep<Double>("Voltage Sweep", "V") {
val voltages by userInput("Voltages", "Values [V]", Range.linear(0, 10))
val delay by userInput("Timing", "Delay [s]", 0.5) map { (it * 1e3).toInt() }
val vSource by requiredInstrument("Voltage Source", VSource::class)
}
The next thing to define is how the sweep values should be generated. This is done by defining a getValues()
method. In many cases this is quite simple - for instance, in our example of a Voltage Sweep, we just need to return what the user has entered as a list of Double
objects like so:
class VoltageSweep : FetChSweep<Double>("Voltage Sweep", "V") {
val voltages by userInput("Voltages", "Values [V]", Range.linear(0, 10))
val delay by userInput("Timing", "Delay [s]", 0.5) map { (it * 1e3).toInt() }
val vSource by requiredInstrument("Voltage Source", VSource::class)
override fun getValues(): List<Double> {
return voltages.array().toList()
}
}
If the way you get the user to define a sweep range is less direct, you may need to include some logic. For instance, if the user was only asked to provide "start", "stop" and "no. steps" values, this is where you would generate the list of values based off these values:
class VoltageSweep : FetChSweep<Double>("Voltage Sweep", "V") {
val start by userInput("Voltages", "Start [V]", 0.0)
val stop by userInput("Voltages", "Stop [V]", 10.0)
val steps by userInput("Voltages", "No. Steps", 11)
val delay by userInput("Timing", "Delay [s]", 0.5) map { (it * 1e3).toInt() }
val vSource by requiredInstrument("Voltage Source", VSource::class)
override fun getValues(): List<Double> {
return Range.linear(start, stop, steps).array().toList()
}
}
As another example, let's say your sweep is over a 2-dimensional plane and the
user is supposed to enter some initial co-ordinates, the spacing in x and y and
the total number of "rows" and "columns". Your sweep would be generating a
custom class of object that can hold an x
and a y
value like so:
class Position(val x: Double, val y: Double)
class PositionSweep : FetChSweep<Position>("Position Sweep", "r") {
val startX by userInput("Start", "X [m]", 0.0)
val startY by userInput("Start", "Y [m]", 0.0)
val spaceX by userInput("Spacing", "X [m]", 1e-2)
val spaceY by userInput("Spacing", "Y [m]", 1e-2)
val countX by userInput("Count", "X", 10)
val countY by userInput("Count", "Y", 5)
override fun getValues(): List<Position> {
val list = ArrayList<Position>()
for (i in 0 until countX) {
for (j in 0 until countY) {
val x = startX + i * spaceX
val y = startY + j * spaceY
list += Position(x, y)
}
}
return list
}
}
Now that the sweep can generate the values it needs to sweep over, we need to
tell it what to do at each value. This is done by using the
generateForValue(...)
method:
override fun generateForValue(value, actions): List<Action>
This methods takes two arguments. The first is the sweep value we are at and the second is a list of (copies of) the actions that the user has specified they want to happen at each sweep value. Generally, what you want to do is define a new list and sandwich the user-provided actions between whatever actions you need to do to perform the sweep. In our case, we just need to add a "Voltage Change" action which will set the voltage on the voltage source followed by all the actions the user has provided.
JISA provides a simple way to add a basic action to an ActionQueue
by means of
creating a SimpleAction
object. To do this, you just need to specify a name
for it along with code to execute when it is run. In our case this is just
setting the voltage on the VSource
instrument and then wait for the delay
time.
Therefore, in our example we would write:
class VoltageSweep : FetChSweep<Double>("Voltage Sweep", "V") {
val voltages by userInput("Voltages", "Values [V]", Range.linear(0, 10))
val delay by userInput("Timing", "Delay [s]", 0.5) map { (it * 1e3).toInt() }
val vSource by requiredInstrument("Voltage Source", VSource::class)
override fun generateForValue(value: Double, actions: List<Action<*>>): List<Action<*>> {
// Create new list
val list = ArrayList<Action<*>>()
// Add action to change voltage and wait
list += SimpleAction("Change Voltage to $value V") {
vSource.voltage = value
sleep(delay)
}
// Add all the other actions the user wanted afterwards
list += actions
return list
}
}
The final thing left to do is to tell FetCh
how it should format your sweep
value when displaying it as text to the user. This is done by defining the
formatValue(...)
method which takes a sweep value and returns a String
representation of it. In our voltage sweep example this is quite simple, we just
need to stick the units of voltage (i.e. a "V") after the value:
override fun formatValue(value: Double): String {
return "$value V"
}
In our previous, more complicated, example of a positional sweep, we could write something like:
override fun formatValue(value: Position): String {
return "(x = ${value.x}, y = ${value.y})"
}
If you want a sweep to always perform certain actions when it ends, whether it ended successfully, was interrupted, or there was an error, then you can define these as "final" actions. To do this, make your sweep class override the generateFinalActions()
method like so:
override fun generateFinalActions(): List<Action<*>> {
return listOf(action1, action2, etc...)
}
Any actions returned in the list from this method will be displayed in a separated "End-of-Sweep Actions" list when displayed in the queue.
Our sweep is now completely defined and is ready to use.
class VoltageSweep : FetChSweep<Double>("Voltage Sweep", "V") {
val voltages by userInput("Voltages", "Values [V]", Range.linear(0, 10))
val delay by userInput("Timing", "Delay [s]", 0.5) map { (it * 1e3).toInt() }
val vSource by requiredInstrument("Voltage Source", VSource::class)
override fun getValues(): List<Double> {
return voltages.array().toList()
}
override fun generateForValue(value: Double, actions: List<Action<*>>): List<Action<*>> {
// Create new list
val list = ArrayList<Action<*>>()
// Add action to change voltage and wait
list += SimpleAction("Change Voltage to $value V") {
vSource.voltage = value
sleep(delay)
}
// Add all the other actions the user wanted afterwards
list += actions
return list
}
override fun formatValue(value: Double): String {
return "$value V"
}
}