Skip to content

Commit 63e96c0

Browse files
authored
docs: Optimisation tutorial (#147)
# Summary Adds a tutorial demonstrating how to use the `Tuner`, both in code and via CLI. # Changes * Tutorial and associated code. * Updates to MkDocs config to publish documentation on the `plugboard.tune` submodule.
1 parent e71b82b commit 63e96c0

7 files changed

Lines changed: 253 additions & 0 deletions

File tree

docs/api/tune/tune.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
::: plugboard.tune
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
tags:
3+
- optimisation
4+
---
5+
Once you have built a model of your process, a common problem you might face is tuning its parameters. Plugboard includes a built-in optimisation utility based on [Ray Tune](https://docs.ray.io/en/latest/tune/index.html) and [Optuna](https://optuna.org/). Using this tool you can do things like:
6+
7+
* Calibrate the parameters of a process model to match observed results; and
8+
* Optimise a process model to maximise or minimise its output.
9+
10+
These capabilities are particularly useful when working with digital twins: for example given a model of a production line, you could use the tuner to work out how to maximise its output.
11+
12+
!!! tip
13+
By using Ray Tune, Plugboard allows you to run optimisations in parallel within a Ray cluster, allowing you to explore the parameter space quickly even when working with long simulations.
14+
15+
## Define a model to optimise
16+
17+
As a simple example, we'll create a simple 3-component model to calculate the maximum height of a [projectile](https://en.wikipedia.org/wiki/Projectile_motion#Displacement) launched at a given angle and velocity.
18+
```mermaid
19+
flowchart LR
20+
horizontal@{ shape: rounded, label: Iterator<br>**horizontal** } --> trajectory@{ shape: rounded, label: Trajectory<br>**trajectory** }
21+
trajectory@{ shape: rounded, label: Trajectory<br>**trajectory** } --> max-height@{ shape: rounded, label: MaxHeight<br>**max-height** }
22+
```
23+
24+
Running the model with different values of the angle and velocity parameters configured on the `Trajectory` component will result in different heights being found on the `MaxHeight` component at the end of the simulation. We will use the [`Tuner`][plugboard.tune.Tuner] class to explore this parameter space and maximise the projectile height.
25+
26+
### Setting up the components
27+
28+
We'll need the following components to implement the model above:
29+
```python
30+
--8<-- "examples/tutorials/006_optimisation/hello_tuner.py:components"
31+
```
32+
33+
Instead of building a [`Process`][plugboard.process.Process] as we would normally do to run the model directly, we'll instead define the [`ProcessSpec`][plugboard.schemas.ProcessSpec] for the model.
34+
```python
35+
--8<-- "examples/tutorials/006_optimisation/hello_tuner.py:define_process"
36+
```
37+
38+
## Setting up the Tuner
39+
40+
Next, we set up a [`Tuner`][plugboard.tune.Tuner] object by configuring the `angle` and `velocity` arguments as floating point parameters, along with constraints.
41+
42+
!!! info
43+
Plugboard supports floating point, integer and categorical variables as tunable model parameters. See the definition of [`ParameterSpec`][plugboard.schemas.ParameterSpec] for details.
44+
45+
When building the tuner, we also specify the number of optimisation samples and how many we will allow to run in parallel on Ray.
46+
```python
47+
--8<-- "examples/tutorials/006_optimisation/hello_tuner.py:run_tuner"
48+
```
49+
50+
1. Set the objective, i.e. what we want our optimisation to target. In this case it is a field on the `max-height` component. This can be a list of objectives if you need to do multi-objective optimisation.
51+
2. List the tunable parameters here. The `field_type` can be `"arg"` or `"initial_value"`. This is also where you can specify constraints on the parameters.
52+
3. Set the number of trials to run. More trials will take longer, but may get closer to finding the true optimum.
53+
4. The level of concurrency to use in Ray.
54+
5. Whether to minimise or maximise the objective. This must be set as a list for multi-objective optimisation.
55+
56+
Running this code will execute an optimisation job and print out information on each trial, along with the final optimisation result.
57+
58+
!!! tip
59+
Since [Optuna](https://optuna.org/) is used under the hood, you can configure the optional `algorithm` argument on the `Tuner` with additional configuration defined in [`OptunaSpec`][plugboard.schemas.OptunaSpec]. For example, the [`storage`](https://optuna.readthedocs.io/en/stable/reference/storages.html) argument allows you to save the optimisation results to a database or SQLite file. You can then use a tool like [Optuna Dashboard](https://optuna-dashboard.readthedocs.io/en/stable/getting-started.html) to study the optimisation output in more detail.
60+
61+
## Using YAML config
62+
63+
Plugboard's YAML config supports an optional `tune` section, allowing you to define optimisation jobs alongside your model configuration:
64+
```yaml
65+
--8<-- "examples/tutorials/006_optimisation/model-with-tuner.yaml"
66+
```
67+
68+
1. As usual, this section defines the [`Process`][plugboard.process.Process]. It can also be replaced by a path to another YAML file.
69+
2. This section is optional, and configures the [`Tuner`][plugboard.tune.Tuner].
70+
3. Parameters need to reference a type, so that Plugboard knows the type of parameter to build.
71+
72+
Now run `plugboard process tune model-with-tuner.yaml` to execute the optimisation job from the CLI.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""Optimisation demonstration."""
2+
3+
# fmt: off
4+
import typing as _t
5+
6+
from plugboard.component import Component, IOController as IO
7+
from plugboard.process import ProcessBuilder
8+
from plugboard.schemas import ComponentArgsDict, ProcessSpec, ProcessArgsSpec, ObjectiveSpec
9+
from plugboard.schemas.tune import FloatParameterSpec
10+
from plugboard.tune import Tuner
11+
import math
12+
13+
14+
# --8<-- [start:components]
15+
class Iterator(Component):
16+
"""Creates a sequence of x values."""
17+
18+
io = IO(outputs=["x"])
19+
20+
def __init__(self, iters: int, **kwargs: _t.Unpack[ComponentArgsDict]) -> None:
21+
super().__init__(**kwargs)
22+
self._iters = iters
23+
24+
async def init(self) -> None:
25+
self._seq = iter(range(self._iters))
26+
27+
async def step(self) -> None:
28+
try:
29+
self.x = next(self._seq)
30+
except StopIteration:
31+
await self.io.close()
32+
33+
34+
class Trajectory(Component):
35+
"""Computes the height of a projectile."""
36+
37+
io = IO(inputs=["x"], outputs=["y"])
38+
39+
def __init__(
40+
self, angle: float = 30, velocity: float = 20, **kwargs: _t.Unpack[ComponentArgsDict]
41+
) -> None:
42+
super().__init__(**kwargs)
43+
self._angle_radians = math.radians(angle)
44+
self._v0 = velocity
45+
46+
async def step(self) -> None:
47+
self._logger.info("Calculating trajectory", x=self.x)
48+
self.y = self.x * math.tan(self._angle_radians) - (9.81 * self.x**2) / (
49+
2 * self._v0**2 * math.cos(self._angle_radians) ** 2
50+
)
51+
52+
53+
class MaxHeight(Component):
54+
"""Record the maximum height achieved."""
55+
56+
io = IO(inputs=["y"], outputs=["max_y"])
57+
58+
def __init__(self, **kwargs: _t.Unpack[ComponentArgsDict]) -> None:
59+
super().__init__(**kwargs)
60+
self.max_y: float = 0
61+
62+
async def step(self) -> None:
63+
self.max_y = max(self.y, self.max_y)
64+
# --8<-- [end:components]
65+
66+
67+
if __name__ == "__main__":
68+
# --8<-- [start:define_process]
69+
process_spec = ProcessSpec(
70+
args=ProcessArgsSpec(
71+
components=[
72+
{"type": "hello_tuner.Iterator", "args": {"name": "horizontal", "iters": 100}},
73+
{
74+
"type": "hello_tuner.Trajectory",
75+
"args": {"name": "trajectory", "angle": 30, "velocity": 20},
76+
},
77+
{"type": "hello_tuner.MaxHeight", "args": {"name": "max-height"}},
78+
],
79+
connectors=[
80+
{"source": "horizontal.x", "target": "trajectory.x"},
81+
{"source": "trajectory.y", "target": "max-height.y"},
82+
],
83+
),
84+
type="plugboard.process.LocalProcess",
85+
)
86+
# Check that the process spec can be built
87+
_ = ProcessBuilder.build(spec=process_spec)
88+
# --8<-- [end:define_process]
89+
# --8<-- [start:run_tuner]
90+
tuner = Tuner(
91+
objective=ObjectiveSpec( # (1)!
92+
object_type="component",
93+
object_name="max-height",
94+
field_type="field",
95+
field_name="max_y",
96+
),
97+
parameters=[
98+
FloatParameterSpec( # (2)!
99+
object_type="component",
100+
object_name="trajectory",
101+
field_type="arg",
102+
field_name="angle",
103+
lower=0,
104+
upper=90,
105+
),
106+
FloatParameterSpec(
107+
object_type="component",
108+
object_name="trajectory",
109+
field_type="arg",
110+
field_name="velocity",
111+
lower=0,
112+
upper=100,
113+
),
114+
],
115+
num_samples=40, # (3)!
116+
max_concurrent=4, # (4)!
117+
mode="max", # (5)!
118+
)
119+
result = tuner.run(spec=process_spec)
120+
print(
121+
f"Best parameters: angle={result.config['trajectory.angle']}, velocity={result.config['trajectory.velocity']}"
122+
)
123+
print(f"Best max height: {result.metrics['max-height.max_y']}")
124+
# --8<-- [end:run_tuner]
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
plugboard:
2+
process: # (1)!
3+
args:
4+
components:
5+
- type: hello_tuner.Iterator
6+
args:
7+
name: horizontal
8+
iters: 100
9+
- type: hello_tuner.Trajectory
10+
args:
11+
name: trajectory
12+
angle: 25
13+
velocity: 20
14+
- type: hello_tuner.MaxHeight
15+
args:
16+
name: max-height
17+
connectors:
18+
- source: horizontal.x
19+
target: trajectory.x
20+
- source: trajectory.y
21+
target: max-height.y
22+
tune: # (2)!
23+
args:
24+
objective:
25+
object_name: max-height
26+
field_type: field
27+
field_name: max_y
28+
parameters:
29+
- type: ray.tune.uniform # (3)!
30+
object_type: component
31+
object_name: trajectory
32+
field_type: arg
33+
field_name: angle
34+
lower: 0
35+
upper: 90
36+
- type: ray.tune.uniform
37+
object_type: component
38+
object_name: trajectory
39+
field_type: arg
40+
field_name: velocity
41+
lower: 0
42+
upper: 100
43+
num_samples: 40
44+
mode: max
45+
max_concurrent: 4
46+

mkdocs.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ nav:
113113
- More components: examples/tutorials/more-components.md
114114
- Running in parallel: examples/tutorials/running-in-parallel.md
115115
- Event-driven models: examples/tutorials/event-driven-models.md
116+
- Tuning a process: examples/tutorials/tuning-a-process.md
116117
- Configuration: usage/configuration.md
117118
- Topics: usage/topics.md
118119
- Demos:
@@ -133,6 +134,7 @@ nav:
133134
- process: api/process/process.md
134135
- schemas: api/schemas/schemas.md
135136
- state: api/state/state.md
137+
- tune: api/tune/tune.md
136138
- utils:
137139
- api/utils/index.md
138140
- settings: api/utils/settings/settings.md

plugboard/schemas/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
from .process import ProcessArgsDict, ProcessArgsSpec, ProcessSpec
2222
from .state import StateBackendArgsDict, StateBackendArgsSpec, StateBackendSpec, Status
2323
from .tune import (
24+
CategoricalParameterSpec,
2425
Direction,
26+
FloatParameterSpec,
27+
IntParameterSpec,
2528
ObjectiveSpec,
2629
OptunaSpec,
2730
ParameterSpec,
@@ -32,6 +35,7 @@
3235

3336

3437
__all__ = [
38+
"CategoricalParameterSpec",
3539
"ComponentSpec",
3640
"ComponentArgsDict",
3741
"ComponentArgsSpec",
@@ -44,6 +48,8 @@
4448
"ConnectorSpec",
4549
"Direction",
4650
"Entity",
51+
"FloatParameterSpec",
52+
"IntParameterSpec",
4753
"IODirection",
4854
"ObjectiveSpec",
4955
"OptunaSpec",

plugboard/schemas/tune.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,10 @@ class CategoricalParameterSpec(BaseFieldSpec):
118118
IntParameterSpec,
119119
CategoricalParameterSpec,
120120
]
121+
"""A union type for all parameter specifications."""
121122

122123
Direction = _t.Literal["min", "max"]
124+
"""A type for the direction of optimisation."""
123125

124126

125127
class TuneArgsDict(_t.TypedDict):

0 commit comments

Comments
 (0)