Toolkits
Information about toolkits provided by LabBench.
Scripting in LabBench allows you to add executable logic to otherwise declarative XML protocols.
Throughout this documentation, you will have seen this notation type = Calculated(context, x) or type = Calculated(context) as the type for attributes. This notation means that the attribute is evaluated as either a single-line Python expression or a function call in a Python script. These attributes are referred to as calculated attributes.
The ability to evaluate expressions or call Python functions from an Experiment Definition File makes it possible to automate protocols, and this is where the LabBench language derives its power and versatility.
Unless any keywords are provided for a calculated attribute, it will be evaluated as a single-line Python expression.
General form:
<attribute>="expression"
Examples:
Istart="SR.PDT"
Ts="min(2, SD.Chronaxie)"
Imin="0.1 * Stimulator.Max"
An expression must:
Single-line expressions are evaluated in a constructed execution scope. This scope defines exactly which variables, results, functions, and constants are available when the expression is executed. The scope is not a full Python environment—it is explicitly built by LabBench before evaluation.
When an expression is evaluated, LabBench creates a dictionary of variables and injects:
x) if applicableThis combined scope is then used to execute the expression.
All results are injected in two ways:
Test01.PDT
Results.Test01.PDT
| Name | Description |
|---|---|
Results |
Collection of all results |
[ResultID] |
Direct access to a result |
Current |
The result of the currently selected procedure |
All variables are available directly:
MyVariable
Participant
| Name | Description |
|---|---|
[DefineName] |
Value defined in the protocol or runtime |
All instruments are injected directly into scope:
Stimulator.Max
ImageDisplay.Show(...)
| Name | Description |
|---|---|
[InstrumentName] |
Instrument instance |
Procedure-specific parameters are also available:
NumberOfStimuli
Assets are available both as a collection and directly:
Assets.Images.Cue
Images.Cue
| Name | Description |
|---|---|
Assets |
Asset manager |
[AssetID] |
Direct access to asset |
The following functions are available in single-line expressions. All functions operate on double values unless otherwise noted.
| Signature | Return type | Description | Edge conditions |
|---|---|---|---|
exp(x) |
double |
Returns (e^x). | Large positive x → +∞; large negative x → 0; NaN propagates. |
round(x) |
double |
Rounds to nearest integer using banker’s rounding. | Midpoints round to nearest even; NaN propagates. |
ceiling(x) |
double |
Smallest integer ≥ x. |
±∞ and NaN returned unchanged. |
floor(x) |
double |
Largest integer ≤ x. |
±∞ and NaN returned unchanged. |
truncate(x) |
double |
Removes fractional part (toward zero). | ±∞ and NaN returned unchanged. |
log(x) |
double |
Natural logarithm. | x > 0 valid; x = 0 → -∞; x < 0 → NaN. |
log10(x) |
double |
Base-10 logarithm. | Same domain as log. |
sin(x) |
double |
Sine of x (radians). |
±∞ → NaN; NaN propagates. |
sinh(x) |
double |
Hyperbolic sine. | Large |
asin(x) |
double |
Arcsine (radians). | Domain: -1 ≤ x ≤ 1; outside → NaN. |
cos(x) |
double |
Cosine of x (radians). |
±∞ → NaN; NaN propagates. |
cosh(x) |
double |
Hyperbolic cosine. | Large |
acos(x) |
double |
Arccosine (radians). | Domain: -1 ≤ x ≤ 1; outside → NaN. |
tan(x) |
double |
Tangent of x (radians). |
Undefined at odd multiples of π/2; returns large finite values; ±∞ → NaN. |
tanh(x) |
double |
Hyperbolic tangent. | Approaches ±1 as x → ±∞; NaN propagates. |
abs(x) |
double |
Absolute value. | ±∞ → +∞; NaN propagates. |
sqrt(x) |
double |
Square root. | x ≥ 0 valid; x < 0 → NaN; +∞ → +∞. |
max(x, y) |
double |
Returns larger of two values. | If either input is NaN → NaN. |
min(x, y) |
double |
Returns smaller of two values. | If either input is NaN → NaN. |
pow(x, y) |
double |
Returns (x^y). | Negative x with non-integer y → NaN; 0^0 = 1; overflow → ±∞. |
step(x) |
double |
Step function: returns 1 if x ≥ 0, else 0. |
NaN → 0 (comparison false). |
pulse(x, d) |
double |
Rectangular pulse: 1 if 0 ≤ x < d, else 0. |
NaN in either argument → 0. |
linspace(x0, x1, n) |
double[] |
Linearly spaced array from x0 to x1 (inclusive). |
n < 2 may cause division issues; NaN/∞ propagate to output. |
geomspace(x0, x1, n) |
double[] |
Geometric progression from x0 to x1. |
x0 ≤ 0 or x1 ≤ 0 may produce invalid values; n < 2 invalid. |
logspace(x0, x1, base, n) |
double[] |
Values defined as (base^{linspace(x0,x1)}). | Large exponents may overflow; invalid base or NaN inputs propagate. |
| Name | Value |
|---|---|
PI |
π |
E |
e |
Variables are added to the scope in sequence. If a name already exists:
This means:
XML uses a small set of reserved characters to define its structure. These characters cannot be used directly inside attribute values or element content without being escaped, as they would otherwise be interpreted as part of the XML syntax.
This becomes particularly important in LabBench, where calculated attributes often contain single-line Python expressions. Many valid Python expressions include characters that must be escaped in XML, which can lead to subtle errors if not handled correctly.
The following characters must be escaped when used inside XML:
| Character | Meaning in XML | Escape sequence |
|---|---|---|
< |
Start of a tag | < |
> |
End of a tag | > |
& |
Start of an entity | & |
" |
Attribute delimiter (double quote) | " |
Calculated attributes are typically written like this:
<arbitrary Ts="50" expression="x < 5" />
This is invalid XML, because < is interpreted as the start of a new tag.
Correct version:
<arbitrary Ts="50" expression="x < 5" />
which is hard to read and understand. If escaped characters are needed it is often better to call a Python function in a script.
Practical guidance:
<, >, and & in expressionsGotchas:
<, >, etc.) can make expressions difficult to read and debug.&lt; instead of < will produce incorrect results.When writing calculated attributes:
Understanding this dual constraint avoids a large class of subtle and frustrating errors.
When the required functionality cannot be implemented with single-line expressions, you can call Python functions defined in script files. Calling functions enables the full power of the Python scripting engine, including defining and instantiating objects, using the Python standard library, and accessing the functionality provided by LabBench through the context object and Toolkits.
Use the func: keyword to call a Python function:
stimulate="func: Script.Stimulate(context, x)
In this example, the calculated attribute expects the function to accept two parameters (context, x). The function must be defined as def [Function Name ](context, x):
def Stimulate(context, x):
context.Instruments.Stimulator.Generate("port2", context.Stimulus)
context.Instruments.ImageDisplay.Display(context.Assets.Images.Cue, 250, True)
return True
Script files must be included explicitly in a protocol by a
<assets>
<file-asset file="Script.py" />
</assets>
When Python scripts are used in LabBench (via func: Script.Function(...)), the script file is compiled once and loaded into a persistent execution environment. The functions defined in that file are then called repeatedly during the procedure.
This means that standard Python scope rules apply, with a few important implications for how scripts behave over time.
All code defined at the top level of a script file lives in module scope and is persistent across function calls.
counter = 0
def Increment(context):
global counter
counter += 1
return counter
counter is created onceIncrement will reuse and modify the same variableThis allows scripts to maintain state across trials or stimulations
Variables defined inside a function are local to that function call
def Compute(context):
value = 10
return value
value is recreated on each callTo modify module-level variables inside a function, you must use global:
value = 0
def Update(context):
global value
value += 1
return value
Without global, a new local variable is created instead.
All functions in the same script file share the same module scope:
state = 0
def A(context):
global state
state += 1
def B(context):
return state
A modifies stateB will see the updated valueUnlike typical scripting environments, the script is not reloaded for each call
This means:
To avoid unintended state persistence, use an explicit initialization function:
state = None
def Initialize(context):
global state
state = 0
def Step(context):
global state
state += 1
return state
This makes lifecycle control explicit and predictable.
global creates bugs: Forgetting global results in a local variable instead of updating shared state.Think of a script file as:
A loaded Python module that stays alive for the entire experimental session
Once you treat it like a long-lived Python module, the behavior becomes predictable.
Text is used extensively in experiments (instructions, labels, prompts). In many cases, text must be dynamic—depending on language, experimental state, or recorded results. Using full Python expressions for all text would require quoting text strings inside Python syntax, which is error-prone. To address this, LabBench introduces dynamic text attributes.
Dynamic text attributes are either a literal string or a calculated attribute that returns a string.
Without the dynamic: keyword, a dynamic text attribute is just a string literal, like for example title=”Age”. With the dynamic: keyword, it is a calculated text attribute:
title=”dynamic: TextDatabase.Age”
This calculated text attribute will follow all the rules for calculated attributes. Consequently, it can be either a single-line expression or a function call. To call a function, use the func: keyword after the dynamic: keyword as you would for conventional calculated attributes:
title=”dynamic: func: Script.GetTitle(context)”
f-strings (formatted string literals) are a concise way to embed expressions inside strings in Python. They are prefixed with f and evaluate expressions directly inside {}.
<variable name="value" value="3.14159">
text = f'Value is {value}'
Expressions inside {} are evaluated at runtime:
f'{2 + 2}' # "4"
f'{value * 2}' # "6.28318"
You can control formatting using a format specifier after a colon : inside the braces:
f'{value:.2f}' # "3.14"
General syntax:
{expression:format_spec}
Common number formatting:
| Format | Description | Example | Result |
|---|---|---|---|
{x} |
Default | f'{3.14}' |
3.14 |
{x:.2f} |
Fixed-point (2 decimals) | f'{3.14159:.2f}' |
3.14 |
{x:.0f} |
No decimals (rounded) | f'{3.6:.0f}' |
4 |
{x:6.2f} |
Width + decimals | f'{3.14:6.2f}' |
" 3.14" |
{x:06.2f} |
Zero-padded | f'{3.14:06.2f}' |
"003.14" |
{x:+.2f} |
Always show sign | f'{3.14:+.2f}' |
+3.14 |
{x:.2e} |
Scientific notation | f'{1234:.2e}' |
1.23e+03 |
{x:.1%} |
Percentage | f'{0.256:.1%}' |
25.6% |
{x:b} |
Binary | f'{5:b}' |
101 |
{x:x} |
Hex (lowercase) | f'{255:x}' |
ff |
{x:X} |
Hex (uppercase) | f'{255:X}' |
FF |
Alignment and width:
| Format | Description | Example | Result |
|---|---|---|---|
{x:>6} |
Right aligned | f'{42:>6}' |
' 42' |
{x:<6} |
Left aligned | f'{42:<6}' |
'42 ' |
{x:^6} |
Center aligned | f'{42:^6}' |
' 42 ' |
context objectWhen a Python function is called from a calculated attribute, it receives a context object (named context) that provides access to the full state of the experiment at runtime.
This object is an instance of ProcedureBlackboard and acts as the central interface between your Python code and LabBench.
The context exposes several categories of data and functionality:
The context object exposes both automatically injected variables and public properties that describe the current execution environment. These can be accessed directly from Python functions.
| Name | Type | Description |
|---|---|---|
Language |
string |
Active language code for the session. |
Participant |
string |
Name or identifier of the participant. |
ParticipantNumber |
int |
Participant number within the study. Can be used for example for generation of latin-squares. |
StartTime |
DateTime |
Timestamp when the session was initialized. |
ExperimentalSetup |
string |
Identifier of the experimental setup. |
ActiveSession |
string |
Identifier of the currently active session. |
Current |
Result |
The currently executing procedure result. |
Results |
ItemCollection<Result> |
Collection of all procedure results. Please note they are also exposed as context.[Result ID]. |
Variables |
ItemCollection<object> |
Collection of all defined variables (defines + runtime variables). Please note they are also exposed as context.[Variable Name]. |
Parameters |
ItemCollection<object> |
Collection of procedure-specific parameters. Please note they are also exposed as context.[Parameter Name]. |
Instruments |
ItemCollection<IInstrument> |
Collection of available instruments. |
Assets |
AssetManager |
Access to protocol assets (images, scripts, etc.). |
These values can be accessed directly:
context.Participant
context.ParticipantNumber
context.Language
context.Current
Automatically injected variables behave like global values in expressions, while properties provide structured access to collections and services exposed by the runtime.
Access results from procedures:
context.Results
context.Test01
context.Test01.PDT
context.Current refers to the currently executing resultAll variables and runtime variables are available directly:
context.MyVariable
context.Participant
context.ParticipantNumber
context.Language
These include:
Access instruments declared in the procedure:
context.Instruments.Stimulator
context.Instruments.ImageDisplay
These are the same instruments declared via <instrument> elements.
The context provides a set of toolkits for common operations:
| Toolkit | Description |
|---|---|
context.Stimuli |
Create stimuli |
context.Triggers |
Generate trigger signals |
context.Image |
Display and generate images |
context.Waveforms |
Create waveform data |
context.Psychophysics |
Psychophysical utilities |
context.Scheduler |
Schedule tasks |
context.Keyboard |
Keyboard input |
The context.Log property is implemented with Serilog, a structured logging framework used throughout LabBench. Logging is useful for debugging, tracing execution, and recording runtime information during experiments.
LabBench supports multiple log levels, which indicate the severity or importance of a message:
| Level | Description |
|---|---|
Debug |
Diagnostic information useful during protocol development. |
Information |
General runtime information (normal operation). |
Error |
An error occurred that affects part of the execution. |
Example:
context.Log.Information("Stimulus started")
context.Log.Warning("Intensity is near maximum")
context.Log.Error("Failed to generate stimulus")
Serilog uses structured (semantic) logging, where values are passed separately from the message template.
Instead of concatenating strings:
# Avoid this
context.Log.Information("Intensity: " + str(x))
Use placeholders:
context.Log.Information("Intensity: {Intensity}", x)
Multiple values:
context.Log.Information("Stimulus {ID} at intensity {Intensity}", stimulus_id, x)
Named placeholders improve clarity:
context.Log.Information(
"Stimulus {StimulusID} delivered at {Intensity}",
stimulus_id,
x
)
Serilog uses semantic logging, meaning:
This allows logs to be:
Intensity > 5)Compared to plain text logging, this makes logs significantly more useful for diagnostics and data analysis.
Logging a value:
context.Log.Debug("Current intensity: {Intensity}", x)
Logging multiple values:
context.Log.Information(
"Trial {Trial} completed in {Time}s",
trial_number,
duration
)
Logging errors:
try:
DoSomething()
except Exception as e:
context.Log.Error("Error during stimulation: {Error}", str(e))
{Intensity} instead of {x}).Example using results:
def NextIntensity(context):
return context.Test01.PDT * 1.2
Example using assets and instruments:
def Stimulate(context, x):
context.Instruments.ImageDisplay.Display(context.Assets.Images.Cue, 200, True)
context.Instruments.Stimulator.Generate("port1", context.Stimulus)
return True
None: This can lead to silent failures if not checked.context.Current is only set during execution of a procedure.LabBench uses IronPython as its scripting engine. IronPython is not a full modern CPython implementation, and understanding its feature set is important for writing reliable scripts.
The IronPython used in LabBench is based on Python 3.4, but includes selected features from newer versions.
Most notably:
This means you are working in a hybrid environment:
Python 3.4 core + selected modern features (not a full Python 3.x runtime)
These features are safe and recommended:
f"Intensity: {x:.2f}"
.format() also works and is fully supportedIronPython differs from the newest version of Python, not fully supported or missing:
dataclassesasync / awaitExternal packages:
Runtime differences:
| Pattern | Example | Why |
|---|---|---|
| Use f-strings | f"{value:.2f}" |
Supported and clean |
| Use built-in types | list, dict |
Fully supported |
| Keep dependencies minimal | — | Avoid compatibility issues |
| Use LabBench context objects | context.Instruments |
Native integration |
| Feature | Issue |
|---|---|
| Advanced Python 3 features | May not exist or behave differently |
Typing (typing module) |
Only partially supported |
| Generators with advanced patterns | Sometimes inconsistent |
| Reflection / dynamic tricks | May behave differently on .NET |
| Pattern | Why |
|---|---|
| NumPy / SciPy | Requires CPython extensions |
| Async frameworks | Not reliably supported |
| Complex metaprogramming | Unpredictable in DLR |
| Relying on latest Python features | Not implemented |
The safest way to think about IronPython in LabBench is:
“Python 3.4 + a few modern conveniences (like f-strings), running on .NET”
If you stay within that mental model, your scripts will be predictable, portable, and robust.
IronPython provides access to a subset of the Python standard library, along with deep integration into the .NET ecosystem. Many commonly used modules work as expected, but support is not complete—especially for modules that rely on CPython internals or native extensions.
As a rule of thumb:
Pure Python modules → usually supported
C-extension / CPython-dependent modules → not supported
| Module | Description | IronPython support |
|---|---|---|
math |
Mathematical functions (sin, log, sqrt, etc.) | ✅ Full |
random |
Random number generation | ✅ Full |
datetime |
Dates and times | ✅ Full |
itertools |
Efficient iteration utilities | ✅ Full |
operator |
Functional operators (add, mul, etc.) | ✅ Full |
re |
Regular expressions | ✅ Full |
string |
String constants and helpers | ✅ Full |
json |
JSON encoding/decoding | ✅ Full |
csv |
CSV file handling | ✅ Full |
time |
Time-related functions | ✅ Mostly |
functools |
Functional programming tools | ✅ Mostly |
collections |
Specialized containers (deque, namedtuple, etc.) |
✅ Mostly |
sys |
Interpreter interaction | ⚠️ Partial |
os |
Operating system interface | ⚠️ Partial |
pathlib |
Filesystem paths | ⚠️ Partial |
io |
Stream handling | ⚠️ Partial |
typing |
Type hints | ⚠️ Partial |
inspect |
Introspection utilities | ⚠️ Partial |
importlib |
Import system utilities | ⚠️ Partial |
Documentation: Python 3.4 Standard Library Documentation
The following modules are available in IronPython but only partially supported. In most cases, core functionality works, while features relying on CPython internals, OS-specific behavior, or advanced runtime features may be missing or behave differently.
sys — Interpreter interactionProvides access to interpreter-level information and runtime configuration.
Supported
sys.version, sys.platform sys.argv sys.path (module search path) Basic stdout/stderr access (sys.stdout, sys.stderr)
Limitations
No CPython-specific internals (e.g., reference counting details) Limited support for low-level interpreter hooks sys.getsizeof() may not behave as in CPython
Recommendation Use for basic environment inspection and path management only.
os — Operating system interfaceProvides functions for interacting with the file system and environment.
Supported
os.listdir, os.mkdir, os.remove)os.path)os.environ)Limitations
os.fork not available)Recommendation Safe for basic file and directory operations; avoid advanced OS/process control.
pathlib — Filesystem pathsObject-oriented filesystem path handling.
Supported
Path("file.txt"))exists())read_text(), write_text())Limitations
Recommendation
Use for simple path manipulation; fall back to os.path if needed.
io — Stream handlingProvides tools for working with streams and file-like objects.
Supported
open, text/binary modes)StringIO for in-memory streamsLimitations
Recommendation Use for standard file operations; avoid complex stream manipulation.
typing — Type hintsProvides support for type annotations.
Supported
List, Dict, Optional)Limitations
Protocol, TypedDict) missing or incompleteRecommendation Use for readability/documentation only—not for enforcement.
inspect — Introspection utilitiesProvides tools for inspecting objects, functions, and call stacks.
Supported
inspect.getmembers, inspect.isfunction)Limitations
getsource) may failRecommendation Use cautiously; avoid relying on deep introspection.
importlib — Import system utilitiesProvides programmatic access to the import system.
Supported
import_module)Limitations
Recommendation Use only for simple dynamic imports.
Calculated attributes are validated before execution:
At runtime:
(context, x) signature) raises explicit errors(context) vs (context, x) must match exactly, or execution fails at runtime.linspace, geomspace, and logspace can fail if inputs are invalid (e.g. negative values in logspace).Information about toolkits provided by LabBench.
Information on how to use instruments from python scripts.