Skip to content

Commit 1b5b079

Browse files
authored
feat: Process parameters (#174)
# Summary Closes #97 by implementing process parameters, which are shared via the `StateBackend` with the components. This allows users to reuse configuration across many components without duplication in the config. # Changes * Process parameters; * Updates to component to retrieve parameters once connected to the backend; * Update to `Tuner` to allow optimisation over process parameters; * Updated example in documentation.
1 parent 2e02288 commit 1b5b079

18 files changed

Lines changed: 315 additions & 53 deletions

File tree

docs/examples/tutorials/more-complex-process.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,24 @@ We'll provide an initial input value of `a = 0` to the `Scale` component, allowi
8383

8484
Setting `initial_values = {"a": [0]}` means that we want the `a` input to be set to `0` on the first step and then revert to reading its input as usual.
8585

86+
## Using parameters
87+
88+
Sometimes you might need to use a parameter value repeatedly across many [`Component`][plugboard.component.Component] objects. The [`Process`][plugboard.process.Process] object allows you to supply a single dictionary of parameter values that are then supplied to all the components before they start running.
89+
For example, we can rewrite the `Scale` component like this:
90+
91+
```python
92+
--8<-- "examples/tutorials/002_complex_processes/components.py:parameters"
93+
```
94+
95+
Now we can supply the `scale` value when we create the [`Process`][plugboard.process.Process], and use it many places within the model.
96+
97+
```python
98+
--8<-- "examples/tutorials/002_complex_processes/parameters.py:main"
99+
```
100+
101+
1. Supply a dictionary of common parameter values to the `Process` here. They are accessible from within all components.
102+
2. If you need to override a parameter on a specific component, you can supply it via the component-level `parameters` argument.
103+
86104
## Next steps
87105

88106
You've now learned how to build up complex model layouts in Plugboard. In the next tutorial we'll show how powerful a Plugboard model can be as we start to include different types of [`Component`][plugboard.component.Component].

docs/examples/tutorials/tuning-a-process.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ Running this code will execute an optimisation job and print out information on
6161
!!! tip
6262
You can impose arbitary constraints on variables within a `Process`. In your `step` method you can raise a [`ConstraintError`][plugboard.exceptions.ConstraintError] to indicate to the `Tuner` that a constraint has been breached. This will cause the trial to be stopped, and the optimisation will continue trying to find parameters that don't cause the constraint violation.
6363

64+
!!! tip
65+
You can optimise over process parameters if you have them in your model. Set `object_type="process"` and `field_type="parameter"` when specifying your tunable parameter.
66+
6467
## Using YAML config
6568

6669
Plugboard's YAML config supports an optional `tune` section, allowing you to define optimisation jobs alongside your model configuration:

examples/demos/fundamentals/002_production_line_optimisation/production-line.ipynb

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -347,11 +347,10 @@
347347
"\n",
348348
"```\n",
349349
"Best parameters found:\n",
350-
"Config: {'controller1.threshold': 10, 'controller2.threshold': 38} - Metrics: {'cost_per_unit.cost_per_unit': 22.491974317817014, 'timestamp': 1755022664, \n",
351-
"'checkpoint_dir_name': None, 'done': True, 'training_iteration': 1, 'trial_id': '287aff0b', 'date': '2025-08-12_19-17-44', 'time_this_iter_s': 2.224583864212036, \n",
352-
"'time_total_s': 2.224583864212036, 'pid': 94765, 'hostname': 'hostname.local', 'node_ip': '127.0.0.1', 'config': {'controller1.threshold': 10, \n",
353-
"'controller2.threshold': 38}, 'time_since_restore': 2.224583864212036, 'iterations_since_restore': 1, 'experiment_tag': \n",
354-
"'14_controller1_threshold=10,controller2_threshold=38'}\n",
350+
"Config: {'component.controller1.arg.threshold': 10, 'component.controller2.arg.threshold': 38} - Metrics: {'component.cost_per_unit.field.cost_per_unit': 22.491974317817014, 'timestamp': 1763499370, \n",
351+
"'checkpoint_dir_name': None, 'done': True, 'training_iteration': 1, 'trial_id': '480ac050', 'date': '2025-11-18_20-56-10', 'time_this_iter_s': 2.0268948078155518, 'time_total_s': 2.0268948078155518, 'pid': 35181,\n",
352+
"'hostname': 'Tobys-MacBook-Pro.local', 'node_ip': '127.0.0.1', 'config': {'component.controller1.arg.threshold': 10, 'component.controller2.arg.threshold': 38}, 'time_since_restore': 2.0268948078155518, \n",
353+
"'iterations_since_restore': 1, 'experiment_tag': '23_component_controller1_arg_threshold=10,component_controller2_arg_threshold=38'}\n",
355354
"```\n"
356355
]
357356
}

examples/tutorials/002_complex_processes/branching.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313

1414
async def main() -> None:
15-
# --8<-- [start:main]
1615
connect = lambda in_, out_: AsyncioConnector( # (1)!
1716
spec=ConnectorSpec(source=in_, target=out_)
1817
)

examples/tutorials/002_complex_processes/components.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,12 @@ async def step(self) -> None:
7676
async def destroy(self) -> None:
7777
self._f.close()
7878
# --8<-- [end:components]
79+
80+
# ---8<-- [start:parameters]
81+
class ScaleFromParameter(Component):
82+
"""Implements `x = a * scale` using parameters."""
83+
io = IO(inputs=["a"], outputs=["x"])
84+
85+
async def step(self) -> None:
86+
self.x = self.a * self.parameters.get("scale", 1)
87+
# ---8<-- [end:parameters]

examples/tutorials/002_complex_processes/loop.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313

1414
async def main() -> None:
15-
# --8<-- [start:main]
1615
connect = lambda in_, out_: AsyncioConnector(
1716
spec=ConnectorSpec(source=in_, target=out_)
1817
)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""An example using process parameters."""
2+
3+
# fmt: off
4+
import asyncio
5+
6+
from plugboard.connector import AsyncioConnector
7+
from plugboard.process import LocalProcess
8+
from plugboard.schemas import ConnectorSpec
9+
10+
from components import Random, Save, ScaleFromParameter, Sum
11+
12+
13+
async def main() -> None:
14+
# --8<-- [start:main]
15+
connect = lambda in_, out_: AsyncioConnector(
16+
spec=ConnectorSpec(source=in_, target=out_)
17+
)
18+
process = LocalProcess(
19+
components=[
20+
Random(name="random", iters=5, low=0, high=10),
21+
ScaleFromParameter(name="scale_a"),
22+
ScaleFromParameter(name="scale_b", parameters={"scale": 2.0}), # (2)!
23+
Sum(name="sum"),
24+
Save(name="save-input", path="input.txt"),
25+
Save(name="save-output", path="output.txt"),
26+
],
27+
connectors=[
28+
connect("random.x", "save-input.value_to_save"),
29+
connect("random.x", "scale_a.a"),
30+
connect("random.x", "scale_b.a"),
31+
connect("scale_a.x", "sum.a"),
32+
connect("scale_b.x", "sum.b"),
33+
connect("sum.x", "save-output.value_to_save"),
34+
],
35+
parameters={"scale": 0.5}, # (1)!
36+
)
37+
async with process:
38+
await process.run()
39+
# --8<-- [end:main]
40+
41+
if __name__ == "__main__":
42+
asyncio.run(main())
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
plugboard:
2+
process:
3+
args:
4+
components:
5+
- type: components.Random
6+
args:
7+
name: random
8+
iters: 5
9+
low: 0
10+
high: 10
11+
- type: components.ScaleFromParameter
12+
args:
13+
name: scale_a
14+
- type: components.ScaleFromParameter
15+
args:
16+
name: scale_b
17+
parameters:
18+
scale: 2.0
19+
- type: components.Sum
20+
args:
21+
name: sum
22+
- type: components.Save
23+
args:
24+
name: save-input
25+
path: input.txt
26+
- type: components.Save
27+
args:
28+
name: save-output
29+
path: output.txt
30+
connectors:
31+
- source:
32+
entity: random
33+
descriptor: x
34+
target:
35+
entity: save-input
36+
descriptor: value_to_save
37+
mode: pipeline
38+
- source:
39+
entity: random
40+
descriptor: x
41+
target:
42+
entity: scale_a
43+
descriptor: a
44+
mode: pipeline
45+
- source:
46+
entity: random
47+
descriptor: x
48+
target:
49+
entity: scale_b
50+
descriptor: a
51+
mode: pipeline
52+
- source:
53+
entity: scale_a
54+
descriptor: x
55+
target:
56+
entity: sum
57+
descriptor: a
58+
mode: pipeline
59+
- source:
60+
entity: scale_b
61+
descriptor: x
62+
target:
63+
entity: sum
64+
descriptor: b
65+
mode: pipeline
66+
- source:
67+
entity: sum
68+
descriptor: x
69+
target:
70+
entity: save-output
71+
descriptor: value_to_save
72+
mode: pipeline
73+
parameters:
74+
scale: 0.5
75+
type: plugboard.process.LocalProcess

examples/tutorials/006_optimisation/hello_tuner.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ def custom_space(trial: Trial) -> dict[str, _t.Any] | None:
127127
)
128128
result = tuner.run(spec=process_spec)
129129
print(
130-
f"Best parameters: angle={result.config['trajectory.angle']}, velocity={result.config['trajectory.velocity']}"
130+
"Best parameters: "
131+
f"angle={result.config['component.trajectory.arg.angle']}, "
132+
f"velocity={result.config['component.trajectory.arg.velocity']}"
131133
)
132-
print(f"Best max height: {result.metrics['max-height.max_y']}")
134+
print(f"Best max height: {result.metrics['component.max-height.field.max_y']}")
133135
# --8<-- [end:run_tuner]

plugboard/component/component.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def __init__(
5353
*,
5454
name: str,
5555
initial_values: _t.Optional[dict[str, _t.Iterable]] = None,
56-
parameters: _t.Optional[dict] = None,
56+
parameters: _t.Optional[dict[str, _t.Any]] = None,
5757
state: _t.Optional[StateBackend] = None,
5858
constraints: _t.Optional[dict] = None,
5959
) -> None:
@@ -111,6 +111,11 @@ def status(self) -> Status:
111111
"""Gets the status of the component."""
112112
return self._status
113113

114+
@property
115+
def parameters(self) -> dict[str, _t.Any]:
116+
"""Gets the parameters of the component."""
117+
return self._parameters
118+
114119
@classmethod
115120
def _configure_io(cls) -> None:
116121
# Get all parent classes that are Component subclasses
@@ -221,6 +226,9 @@ async def connect_state(self, state: _t.Optional[StateBackend] = None) -> None:
221226
with self._job_id_ctx():
222227
await self._state.upsert_component(self)
223228
self._state_is_connected = True
229+
# Merge this component's parameter values from those in the process
230+
process = await self._state.get_process_for_component(self.id)
231+
self._parameters = {**process["parameters"], **self._parameters}
224232

225233
async def init(self) -> None:
226234
"""Performs component initialisation actions."""

0 commit comments

Comments
 (0)