Last release we introduced if
and “expressions” in experiment profiles. To quickly recap: if
is a directive that can be attached to actions to conditionally execute the action or not. Expressions can be used in if
statements and options to dynamically pull live data. For example: ${{ worker1:stirring:target_rpm + 10 }}
gets the current RPM in worker1, and adds 10 to it.
Here’s an update for the next release:
Using expressions and if
in the common
block
Previously we only allowed if
and expressions in the pioreactors
block. This was mostly due to us needing to determine how an expression should be evaluated for all workers in a Pioreactor cluster. The obvious choice is that an expression should executed for each worker. This causes some technical limitations, which we’ve solved, but also needs syntax. How do you specify that you want to run a dynamic expression for each worker? We decided to use the syntax ::<job>:<setting>
to tell the backend “this should be evaluated for all workers”. For example:
common:
jobs:
stirring:
actions:
- type: start
hours_elapsed: 0.0
- type: update
hours_elapsed: 1.0
if: ${{ ::stirring:$state == ready }} # checks each worker if worker's stirring is "ready"
options:
target_rpm: ${{ ::stirring:target_rpm + 100 }} # if, so updates the worker's RPM by 100
A looping directive: repeat
When we cleaned up the profile code a few weeks back, we fortunately picked a good abstraction where we can add the most useful feature to profiles: looping. Looping enables you to monitor state continuously and act on it, or vary a parameter until a target is hit, or run actions over and over again for ever, all via profiles.
We’ve introduced a new action, repeat
, to control looping. Here’s the high-level syntax for the action:
- type: repeat
hours_elapsed: <float>
if: <optional expression>
repeat_every_hours: <optional float>
while: <optional expression>
max_hours: <optional float>
actions: <list of actions>
The fields type
, hours_elapsed
and if
are familiar. Let’s go through the rest:
repeat_every_hours
: this is how long, in hours, the loop will take. For example, if you are starting a pump for ten seconds every hour, you will setrepeat_every_hours
to1
.while
: this is an expression that controls when to stop a loop, conditionally. It will run before the first loop executes, and check again before each additional execution of the loop. This field is optional, and defaults to True if not specified.max_hours
: this optional field controls how long your loops will execute for. If you only want to run the loop every hour for 10 hours, thenmax_hours
is10
. If not specified, then it will run forever, or untilwhile
is false (ifwhile
is even specified).actions
: this is a list of actions (likestart
,update
, etc.) that determine the behaviour of the loop. These actions usehours_elapsed
differently: in these actions,hours_elapsed
refers to the start of the loop.
Examples! In the below profile, we start stirring with target RPM 400. After 1 hour, we enter the repeat
directive. The while
loop checks each Pioreactor to see if their RPM is less than 1000, and if so, execute the list of actions. The list of actions says to increase RPM by 100, and then 15m later reduce RPM by 50. The the loop repeats after another 15 minutes.
experiment_profile_name: demo_stirring_repeat
common:
jobs:
stirring:
actions:
- type: start
hours_elapsed: 0.0
options:
target_rpm: 400.0
- type: repeat
hours_elapsed: 1
while: ::stirring:target_rpm <= 1000
repeat_every_hours: 0.5
actions:
- type: update
hours_elapsed: 0.0
options:
target_rpm: ${{::stirring:target_rpm + 100}}
- type: update
hours_elapsed: 0.25
options:
target_rpm: ${{::stirring:target_rpm - 50}}
(below screenshot is sped up so I’m not waiting for hours)
Here’s another of profile of scaling RPM in proportion to nOD:
experiment_profile_name: demo_stirring_repeat
metadata:
author: Cam Davidson-Pilon
description: A simple profile that increases RPM with increasing nOD
pioreactors:
testing_unit:
jobs:
stirring:
actions: # after 1 hour, every 1 hour, we increase the RPM in proportion to increases in nOD
- type: repeat
hours_elapsed: 1
interval: 1
actions:
- type: update
hours_elapsed: 0
options:
target_rpm: ${{testing_unit:stirring:target_rpm + 10 * (testing_unit:growth_rate_calculating:od_filtered.od_filtered - 1) }}
Conclusion
With these changes, we feel confident saying experiment profiles are nearly finished. Bugs will be cleaned up, some small features, and a better UI to edit and create profiles is needed, but we like the power level and flexibility of experiment profiles now.
Users can accomplish many things with profiles, without needing to dip into Python. As a bonus: since the YAML structure is so simple, we can use ChatGPT-like models to help users quickly write a profile with little chance for error.