Workflow and Task Options¶
The LabOne Q Workflows keyword arguments in addition to the input arguments that are explicitly defined. We call these keyword arguments "options".
The options can be specified either as dictionaries of parameters-value pairs, or as "options" classes. There are two categories of options provided in the library:
TaskOptions:
These options are used to set parameters for tasks in the workflow. An example of
TaskOptionsclass would be one which contains options for setting how experiments should run. For example, the number of experiment shotscounts, the averaging modeaveraging_mode, the acquisition typeacquisition_typeand so on.WorkflowOptions:
These options are used to set parameters for the workflow. These include the settings for operating the workflow, such as
logstorewhich specifies the protocol for storing a collection of records of workflow execution. In addition,WorkflowOptionscontain options for the constituent tasks and sub-workflow. Users typically do not have to manually set these options as they are automatically generated by the workflow.
Using options classes has several advantages:
autocompletion of the options fields inside the class;
the options fields have docstrings;
the
Workflowautomatically builds the tree of options for itself and its constituent tasks;the
Workflowautomatically distributes the options passed in by the user to its constituent tasks.
This tutorial will focus on explaining how the options classes work in more detail.
from __future__ import annotations
from laboneq import workflow
Creating a new options class¶
To create a new options class, use the @workflow.task_options decorator for task options and @workflow.workflow_options for workflow options respectively.
You can define fields using the dataclass syntax like x: int = 1, as shown in the below cell.
For more tailored field definitions such as adding descriptions (docstrings), you can use workflow.option_field provided by our library.
workflow.option_field provides basic type validations for the following types:
- Non-generic types: int, str, float, etc.
- Union and Optional.
- Generic types: List, Dict, Tuple, Set, Callable, etc. Only the type of the origin is checked; the type of elements is not checked.
- User-defined classes.
If you need more complex type validation, you can override the basic validator with your own validators via validator argument of workflow.option_field.
Check out the API reference of workflow.option_field for more details.
Let's define a simple TaskOptions class. WorkflowOptions work the same; just replace @workflow.task_options with @workflow.workflow_options.
@workflow.task_options
class NewExperimentOptions:
x: int = 1
y: int = workflow.option_field(2, description="This is the y field")
opt = NewExperimentOptions()
Options classes have some useful methods, such as nice printing and serialization/deserialization to dictionaries.
opt
opt.to_dict()
You can query the value of a field by calling it:
opt.y
And you can set a new value to a field as follow:
opt.y = 5
opt
The attributes (fields) of the options class must have default values. This allows the workflow to operate with those default settings when specific options are not specified by the user.
If the options classes are defined without default values, an error will be raised when the options instance is created:
@workflow.task_options
class InvalidExperimentOptions:
no_default: int
opt = InvalidExperimentOptions(no_default=1)
Options classes in Tasks and Workflows¶
As mentioned at the beginning of this tutorial, two advantages of using the options classes in Workflows are that the Workflow automatically builds the tree of options for itself and its constituent tasks, and that the Workflow automatically distributes the options passed in by the user to its constituent tasks.
At the moment, these features are enabled in a Workflow when the arguments include options and the expected type of this options argument is one of the following:
WorkflowOptionsA | None = NoneUnion[WorkflowOptionsA, None] = NoneOptional[WorkflowOptionsA]
where WorkflowOptionsA is a subclass of WorkflowOptions.
Note:
- From Python 3.10 onward, it is recommended to use
WorkflowOptionsA | Noneto conform with the standard practice. - On Python 3.9,
from _future_ import annotationsmust be imported to useWorkflowOptionsA | None.
Let's illustrate how the features above work via an example.
We first define two options classes, a child of WorkflowOptions called WorkflowOptionsA, and a child of TaskOptions called TaskOptionsB. Notice that both classes contain the filed count.
@workflow.workflow_options
class WorkflowOptionsA:
count: int = workflow.option_field(1024, description="The number of repetitions")
@workflow.task_options
class TaskOptionsB:
count: int = workflow.option_field(1024, description="The number of repetitions")
some_task_option: bool = workflow.option_field(
False, description="Some task option."
)
Next, we write two Tasks called task1 and task2, both of which use the options class TaskOptionsB. Both tasks simply return the value of the options field count. To learn more about Tasks in LabOne Q, check out our tutorial on Tasks.
@workflow.task
def task1(options: TaskOptionsB | None = None):
options = options or TaskOptionsB()
return options.count
@workflow.task
def task2(options: TaskOptionsB | None = None):
options = options or TaskOptionsB()
return options.count
Calling Tasks with options¶
Before we move on to define our Workflows, let's first call these tasks with our options class and see how it works.
options = TaskOptionsB()
options.count = 10
task1(options)
Workflows with options¶
Now let's define two Workflows.
The first one, called inner_workflow, simply calls task1 and task2 one after the other.
The second Workflow called my_workflow is a nested Workflow. It calls the two tasks and then calls inner_workflow.
@workflow.workflow
def inner_workflow(options: workflow.WorkflowOptions | None = None):
a = task1()
b = task2()
workflow.return_(task1_count=a, task2_count=b)
@workflow.workflow
def myworkflow(options: WorkflowOptionsA | None = None):
task1()
task2()
inner_workflow()
Important
Notice that we did not explicitly pass the options to the tasks of the workflows. This is not neeced, as the workflow takes care to automatically distribute the options to the correct tasks based on type matching.
Unless you have a good reason for manually passing the options to a task, you should not do this as it will probably lead to unintended behaviour.
Creating the workflow options¶
As we've seen in the previous section, we have to instantiate the options class before passing it to a Task.
Workflows works a bit differently. We create the options for a specific workflow directly by calling the options() method on the workflow. This returns an OptionBuilder instance, a wrapper of the WorkflowOptions instance that contains the options tree for this specific workflow. OptionBuilder helps to set the options fields more easily.
workflow_opt = myworkflow.options()
The options tree contained in the workflow could be displayed by printing the option builder:
workflow_opt
The fields of the workflow options together with the options of nested tasks and nested workflows are visible as attributes of the OptionBuilder instance.
The above lets you inspect the options of the workflow, grouped per task. However, it hard to get an overview of what are all the options that the workflow accepts. To see that more easily, you can use the .show_fields() method of the workflow, which displays all the available option fields together with their docstrings:
workflow.show_fields(workflow_opt)
Querying and setting values of workflow options¶
To query the value of an options field, simply access it as an attribute of the OptionBuilder instance, as shown below for the case of field count:
workflow_opt.count
Here, we see that there are five count fields shown together with its values: one at the top-level of the options (base,1024), and others at the sub-task and sub-workflow levels.
When working with the options classes themselves, we saw above that we set the value of an options field via standard assignment.
For the OptionsBuilder created by the Workflow, assignment works a bit differently in order to support more sophisticated assignment rules. Let's illustrate with examples.
To change the value of the field count everywhere it appears in the Workflow, use:
workflow_opt.count(2048)
workflow_opt.count
To change the value of count everywhere except at the top level workflow, use standard array slicing:
workflow_opt.count[1:](1024)
workflow_opt.count
For workflow options with a more complicated structure, option fields can also be set by specifying the task or workflow names for which you want to change those fields.
Let's start with setting count to 0 in the top-level Workflow and its tasks, but excluding the inner_workflow:
workflow_opt.count(0, ".")
workflow_opt.count
Similarly, we can set all count that appear as top-level fields of inner_workflow:
workflow_opt.count(1, "inner_workflow")
workflow_opt.count
To set a specific option field, we need to specify its full path. Let's set count to 2 for task1 of the inner_workflow and task1 of the top-level workflow:
workflow_opt.count(2, "task1")
workflow_opt.count(2, "inner_workflow.task1")
workflow_opt.count
Running a Workflow with options¶
Now to run the workflow with the modified options, simply pass the options to the workflow initialization
res = myworkflow(options=workflow_opt).run()
for task in res.tasks:
print("-----------------------")
print(f"{'Task name:':<15} {task.name}")
print(f"{'Task output:':<15} {task.output}")
# print("Task name: ", task.name)
# print("Task output: ", task.output)
Notice that the options have been automatically distributed to the correct tasks!
Disallowed types for options¶
When the type provided for the options of a Workflow includes a subclass of WorkflowOptions, we assume that users are attempting to use this automatic options creation and distribution mechanism offered by the Workflow machinery.
Hence, if the specified type does not follow the form of the above-mentioned types, an error will be raised to inform the user about this, as shown below:
# an error will be raised
@workflow.workflow
def invalid_workflow(options: WorkflowOptionsA | str):
task1()
task2()
Standard options classes¶
In the Applications Library package, we provide a few standard options classes for quantum experiments and analyses, such as BaseExperimentOptions, TuneupExperimentOptions and TuneUpWorkflowOptions, TuneupAnalysisOptions and TuneUpAnalysisWorkflowOptions, etc.
These implementations can be used in your workflows and tasks, or as a starting point for creating new options classes to serve your needs.
Check out the Applications Library section of this manual to learn more!