From 1c6d2ed08c90dd54a0a28a02853ed51ffffc289c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:30:59 +0000 Subject: [PATCH 1/7] Initial plan From 94336eedba58bd9bcb26930763356db2c3055c86 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:38:58 +0000 Subject: [PATCH 2/7] Add factory simulation demo with 5 machines and maintenance optimisation Co-authored-by: toby-coleman <13170610+toby-coleman@users.noreply.github.com> --- .../003_factory_simulation/.meta.yml | 3 + .../factory-simulation.yaml | 112 ++++++++ .../factory_simulation.py | 255 ++++++++++++++++++ 3 files changed, 370 insertions(+) create mode 100644 examples/demos/fundamentals/003_factory_simulation/.meta.yml create mode 100644 examples/demos/fundamentals/003_factory_simulation/factory-simulation.yaml create mode 100644 examples/demos/fundamentals/003_factory_simulation/factory_simulation.py diff --git a/examples/demos/fundamentals/003_factory_simulation/.meta.yml b/examples/demos/fundamentals/003_factory_simulation/.meta.yml new file mode 100644 index 00000000..dc32dbd0 --- /dev/null +++ b/examples/demos/fundamentals/003_factory_simulation/.meta.yml @@ -0,0 +1,3 @@ +tags: + - optimisation + - simulation diff --git a/examples/demos/fundamentals/003_factory_simulation/factory-simulation.yaml b/examples/demos/fundamentals/003_factory_simulation/factory-simulation.yaml new file mode 100644 index 00000000..e73298c0 --- /dev/null +++ b/examples/demos/fundamentals/003_factory_simulation/factory-simulation.yaml @@ -0,0 +1,112 @@ +plugboard: + process: + args: + components: + - type: factory_simulation.Iterator + args: + name: clock + num_days: 365 + - type: factory_simulation.Machine + args: + name: machine_1 + maintenance_interval: 30 + sigmoid_steepness: 0.10 + sigmoid_midpoint: 50.0 + seed: 101 + - type: factory_simulation.Machine + args: + name: machine_2 + maintenance_interval: 30 + sigmoid_steepness: 0.12 + sigmoid_midpoint: 45.0 + seed: 202 + - type: factory_simulation.Machine + args: + name: machine_3 + maintenance_interval: 30 + sigmoid_steepness: 0.15 + sigmoid_midpoint: 40.0 + seed: 303 + - type: factory_simulation.Machine + args: + name: machine_4 + maintenance_interval: 30 + sigmoid_steepness: 0.18 + sigmoid_midpoint: 35.0 + seed: 404 + - type: factory_simulation.Machine + args: + name: machine_5 + maintenance_interval: 30 + sigmoid_steepness: 0.20 + sigmoid_midpoint: 30.0 + seed: 505 + - type: factory_simulation.Factory + args: + name: factory + connectors: + - source: clock.day + target: machine_1.day + - source: clock.day + target: machine_2.day + - source: clock.day + target: machine_3.day + - source: clock.day + target: machine_4.day + - source: clock.day + target: machine_5.day + - source: machine_1.daily_value + target: factory.value_1 + - source: machine_2.daily_value + target: factory.value_2 + - source: machine_3.daily_value + target: factory.value_3 + - source: machine_4.daily_value + target: factory.value_4 + - source: machine_5.daily_value + target: factory.value_5 + tune: + args: + objective: + object_name: factory + field_type: field + field_name: total_value + parameters: + - type: ray.tune.randint + object_type: component + object_name: machine_1 + field_type: arg + field_name: maintenance_interval + lower: 5 + upper: 60 + - type: ray.tune.randint + object_type: component + object_name: machine_2 + field_type: arg + field_name: maintenance_interval + lower: 5 + upper: 60 + - type: ray.tune.randint + object_type: component + object_name: machine_3 + field_type: arg + field_name: maintenance_interval + lower: 5 + upper: 60 + - type: ray.tune.randint + object_type: component + object_name: machine_4 + field_type: arg + field_name: maintenance_interval + lower: 5 + upper: 60 + - type: ray.tune.randint + object_type: component + object_name: machine_5 + field_type: arg + field_name: maintenance_interval + lower: 5 + upper: 60 + num_samples: 40 + mode: max + max_concurrent: 4 diff --git a/examples/demos/fundamentals/003_factory_simulation/factory_simulation.py b/examples/demos/fundamentals/003_factory_simulation/factory_simulation.py new file mode 100644 index 00000000..bcdc1c22 --- /dev/null +++ b/examples/demos/fundamentals/003_factory_simulation/factory_simulation.py @@ -0,0 +1,255 @@ +"""Factory simulation demo. + +Simulates a factory with 5 machines over 1 year (365 days). Each machine: +- Produces $10,000 of value per day when running. +- Has a probability of random breakdown that increases with days since last maintenance + (modelled as a sigmoid function with different parameters per machine). +- Stops for 5 days when it breaks down. +- Has a proactive maintenance schedule: stops for 1 day at regular intervals. + +The simulation optimises the maintenance interval for each machine to maximise total output. +""" + +import math +import random +import typing as _t + +from plugboard.component import Component, IOController as IO +from plugboard.schemas import ComponentArgsDict + + +class Iterator(Component): + """Drives the simulation by emitting a sequence of day numbers.""" + + io = IO(outputs=["day"]) + + def __init__(self, num_days: int = 365, **kwargs: _t.Unpack[ComponentArgsDict]) -> None: + super().__init__(**kwargs) + self._num_days = num_days + + async def init(self) -> None: + self._seq = iter(range(self._num_days)) + + async def step(self) -> None: + try: + self.day = next(self._seq) + except StopIteration: + await self.io.close() + + +class Machine(Component): + """Simulates a single factory machine. + + The machine can be in one of three states: running, broken, or under maintenance. + When running, it produces a fixed daily output. Breakdown probability is modelled + as a sigmoid function of days since last maintenance, with configurable steepness + and midpoint parameters. Proactive maintenance occurs at regular intervals. + + Args: + maintenance_interval: Number of days between proactive maintenance stops. + breakdown_days: Number of days the machine is offline after a breakdown. + maintenance_days: Number of days the machine is offline for proactive maintenance. + daily_output: Value produced per day when running. + sigmoid_steepness: Controls how quickly breakdown probability rises. + sigmoid_midpoint: Days since maintenance at which breakdown probability is 50%. + seed: Random seed for reproducible results. + """ + + io = IO(inputs=["day"], outputs=["daily_value"]) + + def __init__( + self, + maintenance_interval: int = 30, + breakdown_days: int = 5, + maintenance_days: int = 1, + daily_output: float = 10_000.0, + sigmoid_steepness: float = 0.15, + sigmoid_midpoint: float = 40.0, + seed: int = 42, + **kwargs: _t.Unpack[ComponentArgsDict], + ) -> None: + super().__init__(**kwargs) + self._maintenance_interval = maintenance_interval + self._breakdown_days = breakdown_days + self._maintenance_days = maintenance_days + self._daily_output = daily_output + self._sigmoid_steepness = sigmoid_steepness + self._sigmoid_midpoint = sigmoid_midpoint + self._seed = seed + + async def init(self) -> None: + self._rng = random.Random(self._seed) + self._days_since_maintenance = 0 + self._downtime_remaining = 0 + + def _breakdown_probability(self) -> float: + """Sigmoid function: probability near 0 soon after maintenance, rising to ~1.""" + return 1.0 / ( + 1.0 + + math.exp( + -self._sigmoid_steepness * (self._days_since_maintenance - self._sigmoid_midpoint) + ) + ) + + async def step(self) -> None: + # If machine is down (breakdown or maintenance), count down + if self._downtime_remaining > 0: + self._downtime_remaining -= 1 + self.daily_value = 0.0 + if self._downtime_remaining == 0: + self._days_since_maintenance = 0 + return + + # Check for proactive maintenance + if ( + self._maintenance_interval > 0 + and self._days_since_maintenance > 0 + and self._days_since_maintenance % self._maintenance_interval == 0 + ): + self._downtime_remaining = self._maintenance_days + self.daily_value = 0.0 + return + + # Check for random breakdown + if self._rng.random() < self._breakdown_probability(): + self._downtime_remaining = self._breakdown_days + self.daily_value = 0.0 + return + + # Machine is running normally + self.daily_value = self._daily_output + self._days_since_maintenance += 1 + + +class Factory(Component): + """Aggregates daily output from all 5 machines and tracks total value.""" + + io = IO( + inputs=["value_1", "value_2", "value_3", "value_4", "value_5"], + outputs=["total_value"], + ) + + def __init__(self, **kwargs: _t.Unpack[ComponentArgsDict]) -> None: + super().__init__(**kwargs) + + async def init(self) -> None: + self._total = 0.0 + + async def step(self) -> None: + daily = self.value_1 + self.value_2 + self.value_3 + self.value_4 + self.value_5 + self._total += daily + self.total_value = self._total + + +# Machine configurations: each has different sigmoid parameters +MACHINE_CONFIGS: list[dict[str, _t.Any]] = [ + {"sigmoid_steepness": 0.10, "sigmoid_midpoint": 50.0, "seed": 101}, # Reliable + {"sigmoid_steepness": 0.12, "sigmoid_midpoint": 45.0, "seed": 202}, # Average + {"sigmoid_steepness": 0.15, "sigmoid_midpoint": 40.0, "seed": 303}, # Average + {"sigmoid_steepness": 0.18, "sigmoid_midpoint": 35.0, "seed": 404}, # Fragile + {"sigmoid_steepness": 0.20, "sigmoid_midpoint": 30.0, "seed": 505}, # Very fragile +] + + +def build_process_spec(num_days: int = 365) -> "ProcessSpec": + """Build the process specification for the factory simulation.""" + from plugboard.schemas import ProcessArgsSpec, ProcessSpec + + components: list[dict[str, _t.Any]] = [ + { + "type": "factory_simulation.Iterator", + "args": {"name": "clock", "num_days": num_days}, + }, + ] + + connectors: list[dict[str, str]] = [] + + for i, cfg in enumerate(MACHINE_CONFIGS, start=1): + components.append( + { + "type": "factory_simulation.Machine", + "args": { + "name": f"machine_{i}", + "maintenance_interval": 30, + **cfg, + }, + } + ) + connectors.append({"source": "clock.day", "target": f"machine_{i}.day"}) + connectors.append({"source": f"machine_{i}.daily_value", "target": f"factory.value_{i}"}) + + components.append( + { + "type": "factory_simulation.Factory", + "args": {"name": "factory"}, + } + ) + + return ProcessSpec( + args=ProcessArgsSpec(components=components, connectors=connectors), + type="plugboard.process.LocalProcess", + ) + + +if __name__ == "__main__": + import asyncio + + from plugboard.process import LocalProcess, ProcessBuilder + + async def run_single() -> None: + """Run a single simulation and print results.""" + spec = build_process_spec() + process = ProcessBuilder.build(spec=spec) + assert isinstance(process, LocalProcess) + async with process: + await process.run() + # Retrieve final total value from the factory component + factory = next(c for c in process.components.values() if c.name == "factory") + print(f"Total factory output over 365 days: ${factory._total:,.0f}") + + async def run_optimisation() -> None: + """Optimise maintenance intervals for all machines.""" + from plugboard.schemas import IntParameterSpec, ObjectiveSpec + from plugboard.tune import Tuner + + spec = build_process_spec() + + tuner = Tuner( + objective=ObjectiveSpec( + object_type="component", + object_name="factory", + field_type="field", + field_name="total_value", + ), + parameters=[ + IntParameterSpec( + object_type="component", + object_name=f"machine_{i}", + field_type="arg", + field_name="maintenance_interval", + lower=5, + upper=60, + ) + for i in range(1, 6) + ], + num_samples=40, + max_concurrent=4, + mode="max", + ) + + result = tuner.run(spec=spec) + print("\n=== Optimisation Results ===") + for i in range(1, 6): + key = f"component.machine_{i}.arg.maintenance_interval" + print(f" Machine {i} optimal maintenance interval: {result.config[key]} days") + print( + f" Maximum total value: ${result.metrics['component.factory.field.total_value']:,.0f}" + ) + + # Run single simulation + print("Running single simulation with default maintenance intervals (30 days)...") + asyncio.run(run_single()) + + # Uncomment to run optimisation (requires Ray): + # print("\nRunning optimisation...") + # run_optimisation() From 9f754a81ee8f6bc19746706953744bd617ef6bd6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:40:26 +0000 Subject: [PATCH 3/7] Make run_optimisation a sync function since Tuner.run() is synchronous Co-authored-by: toby-coleman <13170610+toby-coleman@users.noreply.github.com> --- .../fundamentals/003_factory_simulation/factory_simulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/demos/fundamentals/003_factory_simulation/factory_simulation.py b/examples/demos/fundamentals/003_factory_simulation/factory_simulation.py index bcdc1c22..706142f2 100644 --- a/examples/demos/fundamentals/003_factory_simulation/factory_simulation.py +++ b/examples/demos/fundamentals/003_factory_simulation/factory_simulation.py @@ -207,7 +207,7 @@ async def run_single() -> None: factory = next(c for c in process.components.values() if c.name == "factory") print(f"Total factory output over 365 days: ${factory._total:,.0f}") - async def run_optimisation() -> None: + def run_optimisation() -> None: """Optimise maintenance intervals for all machines.""" from plugboard.schemas import IntParameterSpec, ObjectiveSpec from plugboard.tune import Tuner From 52ac46a60462dd554ba45dfd31bc4320d4969e46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 21:17:34 +0000 Subject: [PATCH 4/7] Add Jupyter notebook with mermaid diagram and mkdocs navigation entry Co-authored-by: toby-coleman <13170610+toby-coleman@users.noreply.github.com> --- .../factory-simulation.ipynb | 216 ++++++++++++++++++ mkdocs.yaml | 1 + 2 files changed, 217 insertions(+) create mode 100644 examples/demos/fundamentals/003_factory_simulation/factory-simulation.ipynb diff --git a/examples/demos/fundamentals/003_factory_simulation/factory-simulation.ipynb b/examples/demos/fundamentals/003_factory_simulation/factory-simulation.ipynb new file mode 100644 index 00000000..5363f9cb --- /dev/null +++ b/examples/demos/fundamentals/003_factory_simulation/factory-simulation.ipynb @@ -0,0 +1,216 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Factory simulation with maintenance optimisation\n", + "\n", + "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/plugboard-dev/plugboard/blob/main/examples/demos/fundamentals/003_factory_simulation/factory-simulation.ipynb)\n", + "\n", + "[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/plugboard-dev/plugboard)\n", + "\n", + "This demo simulates a factory containing 5 machines over 1 year (365 days). The simulation runs in steps of 1 day, with each machine exhibiting realistic behaviour:\n", + "\n", + "### Components:\n", + "\n", + "1. **Iterator** (`clock`): Drives the simulation by emitting day numbers from 0 to 364\n", + "2. **Machine** (5 instances, each with different reliability characteristics):\n", + " - Produces $10,000 of value per day when running\n", + " - Has a probability of random breakdown modelled as a sigmoid function of days since last maintenance — near zero soon after maintenance, rising to near 1 over time\n", + " - Stops for 5 days when it breaks down\n", + " - Has a proactive maintenance schedule: stops for 1 day at regular intervals\n", + "3. **Factory**: Aggregates daily output from all machines and tracks the cumulative total value\n", + "\n", + "### Machine reliability profiles:\n", + "\n", + "| Machine | Sigmoid steepness | Sigmoid midpoint | Profile |\n", + "|---------|------------------|-----------------|----------|\n", + "| 1 | 0.10 | 50 days | Reliable |\n", + "| 2 | 0.12 | 45 days | Average |\n", + "| 3 | 0.15 | 40 days | Average |\n", + "| 4 | 0.18 | 35 days | Fragile |\n", + "| 5 | 0.20 | 30 days | Very fragile |\n", + "\n", + "We can then use the model to find optimal proactive maintenance intervals for each machine to maximise total output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "# Install plugboard and dependencies for Google Colab\n", + "!pip install -q plugboard[ray]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from plugboard.connector import AsyncioConnector\n", + "from plugboard.process import LocalProcess\n", + "from plugboard.schemas import ConnectorSpec\n", + "\n", + "from factory_simulation import Iterator, Machine, Factory, MACHINE_CONFIGS" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "The components are defined in `factory_simulation.py`:\n", + "\n", + "- **`Iterator`** emits a sequence of day numbers and closes once the configured number of days is reached.\n", + "- **`Machine`** tracks its own state (running, broken, or under maintenance). At each step it checks whether it is due for proactive maintenance, whether a random breakdown occurs (using a sigmoid probability curve), and outputs its daily production value.\n", + "- **`Factory`** sums daily values from all machines and maintains a running total.\n", + "\n", + "Each machine has different sigmoid parameters that control its breakdown probability curve, making some machines more reliable than others." + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "Now assemble the components into a `Process` and connect them together. Each machine receives the day count from the clock, and sends its daily output to the factory aggregator." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "connect = lambda s, t: AsyncioConnector(spec=ConnectorSpec(source=s, target=t))\n", + "\n", + "components = [Iterator(name=\"clock\", num_days=365)]\n", + "connectors = []\n", + "\n", + "for i, cfg in enumerate(MACHINE_CONFIGS, start=1):\n", + " components.append(Machine(name=f\"machine_{i}\", maintenance_interval=30, **cfg))\n", + " connectors.append(connect(\"clock.day\", f\"machine_{i}.day\"))\n", + " connectors.append(connect(f\"machine_{i}.daily_value\", f\"factory.value_{i}\"))\n", + "\n", + "components.append(Factory(name=\"factory\"))\n", + "\n", + "process = LocalProcess(components=components, connectors=connectors)\n", + "\n", + "print(f\"Process has {len(process.components)} components and {len(process.connectors)} connectors\")" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "We can create a diagram of the process to make a visual check.\n", + "\n", + "![Process Diagram](https://mermaid.ink/img/pako:eNrNlLEOgjAURX_lpSOBQYGFGOJkYqKLs4kp7SMQW2pqiTGEf1dAHZowUBzY7z3vnuU1hCmOJCG5UA9WUG3gcDpXAEwodt02cC_oDRPQqq44ch8EzVAksDeoqVF6k-nU8_qw50ELQZCCpKwoK7ysxuvHITK0f_mO0N2eBegn5JS9xz3H-7shMPQ_6e_5P6ivJy5f2-pOgEWohxOXh7a6E2AR6tHE5ZGt7gRYhHo8cXlsqzsB5qkTn0jUkpacJA0xBcruEXLMaS0MadsXZtO5rA==)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "# Visualize the process flow\n", + "from plugboard.diagram import MermaidDiagram\n", + "\n", + "diagram_url = MermaidDiagram.from_process(process).url\n", + "print(diagram_url)" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "Run the simulation for 365 days with a default maintenance interval of 30 days for all machines." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "# Run the simulation\n", + "async with process:\n", + " await process.run()" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "At the end of the simulation, we can read the cumulative total value from the `Factory` component." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "total_value = process.components[\"factory\"].total_value\n", + "max_possible = 365 * 5 * 10_000\n", + "print(f\"Total factory output over 365 days: ${total_value:,.0f}\")\n", + "print(f\"Maximum possible output: ${max_possible:,.0f}\")\n", + "print(f\"Efficiency: {total_value / max_possible:.1%}\")" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "Now suppose we want to find the optimal proactive maintenance interval for each machine. Maintaining too frequently wastes productive days, but maintaining too rarely leads to expensive breakdowns (5 days of downtime vs 1 day for scheduled maintenance).\n", + "\n", + "We can set up an optimisation to maximise `total_value` by varying the `maintenance_interval` argument on each machine. The YAML config in `factory-simulation.yaml` defines both the process and a tuner configuration. The easiest way to launch an optimisation job is via the CLI by running:\n", + "\n", + "```sh\n", + "plugboard process tune factory-simulation.yaml\n", + "```\n", + "\n", + "This will use Optuna to explore maintenance intervals between 5 and 60 days for each machine and report the combination that maximises total factory output.\n", + "\n", + "Since each machine has different reliability characteristics, the optimal maintenance schedule will differ for each — more fragile machines benefit from more frequent maintenance, while reliable machines can run longer between stops." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbformat_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/mkdocs.yaml b/mkdocs.yaml index 8cefcca5..199c291e 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -134,6 +134,7 @@ nav: - Fundamentals: - Simple model: examples/demos/fundamentals/001_simple_model/simple-model.ipynb - Production line: examples/demos/fundamentals/002_production_line_optimisation/production-line.ipynb + - Factory simulation: examples/demos/fundamentals/003_factory_simulation/factory-simulation.ipynb - LLMs: - Data filtering: examples/demos/llm/001_data_filter/llm-filtering.ipynb - Websocket streaming: examples/demos/llm/002_bluesky_websocket/bluesky-websocket.ipynb From 6f5bbe3631cccffd021f4a03d13cdc45d84cf10a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 21:17:41 +0000 Subject: [PATCH 5/7] Move optimisation code from factory_simulation.py to notebook Co-authored-by: toby-coleman <13170610+toby-coleman@users.noreply.github.com> --- .../factory-simulation.ipynb | 97 ++++++++++++++++- .../factory_simulation.py | 103 ------------------ 2 files changed, 91 insertions(+), 109 deletions(-) diff --git a/examples/demos/fundamentals/003_factory_simulation/factory-simulation.ipynb b/examples/demos/fundamentals/003_factory_simulation/factory-simulation.ipynb index 5363f9cb..da714c55 100644 --- a/examples/demos/fundamentals/003_factory_simulation/factory-simulation.ipynb +++ b/examples/demos/fundamentals/003_factory_simulation/factory-simulation.ipynb @@ -180,15 +180,100 @@ "source": [ "Now suppose we want to find the optimal proactive maintenance interval for each machine. Maintaining too frequently wastes productive days, but maintaining too rarely leads to expensive breakdowns (5 days of downtime vs 1 day for scheduled maintenance).\n", "\n", - "We can set up an optimisation to maximise `total_value` by varying the `maintenance_interval` argument on each machine. The YAML config in `factory-simulation.yaml` defines both the process and a tuner configuration. The easiest way to launch an optimisation job is via the CLI by running:\n", + "We can set up an optimisation to maximise `total_value` by varying the `maintenance_interval` argument on each machine. First, we build a `ProcessSpec` that describes the simulation programmatically." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "from plugboard.schemas import ProcessArgsSpec, ProcessSpec\n", "\n", - "```sh\n", - "plugboard process tune factory-simulation.yaml\n", - "```\n", + "spec_components = [\n", + " {\"type\": \"factory_simulation.Iterator\", \"args\": {\"name\": \"clock\", \"num_days\": 365}},\n", + "]\n", + "spec_connectors = []\n", "\n", - "This will use Optuna to explore maintenance intervals between 5 and 60 days for each machine and report the combination that maximises total factory output.\n", + "for i, cfg in enumerate(MACHINE_CONFIGS, start=1):\n", + " spec_components.append(\n", + " {\"type\": \"factory_simulation.Machine\", \"args\": {\"name\": f\"machine_{i}\", \"maintenance_interval\": 30, **cfg}}\n", + " )\n", + " spec_connectors.append({\"source\": \"clock.day\", \"target\": f\"machine_{i}.day\"})\n", + " spec_connectors.append({\"source\": f\"machine_{i}.daily_value\", \"target\": f\"factory.value_{i}\"})\n", + "\n", + "spec_components.append({\"type\": \"factory_simulation.Factory\", \"args\": {\"name\": \"factory\"}})\n", "\n", - "Since each machine has different reliability characteristics, the optimal maintenance schedule will differ for each — more fragile machines benefit from more frequent maintenance, while reliable machines can run longer between stops." + "spec = ProcessSpec(\n", + " args=ProcessArgsSpec(components=spec_components, connectors=spec_connectors),\n", + " type=\"plugboard.process.LocalProcess\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "Now configure the `Tuner` to search for the best `maintenance_interval` for each machine (between 5 and 60 days), maximising the factory's `total_value`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "from plugboard.schemas import IntParameterSpec, ObjectiveSpec\n", + "from plugboard.tune import Tuner\n", + "\n", + "tuner = Tuner(\n", + " objective=ObjectiveSpec(\n", + " object_type=\"component\",\n", + " object_name=\"factory\",\n", + " field_type=\"field\",\n", + " field_name=\"total_value\",\n", + " ),\n", + " parameters=[\n", + " IntParameterSpec(\n", + " object_type=\"component\",\n", + " object_name=f\"machine_{i}\",\n", + " field_type=\"arg\",\n", + " field_name=\"maintenance_interval\",\n", + " lower=5,\n", + " upper=60,\n", + " )\n", + " for i in range(1, 6)\n", + " ],\n", + " num_samples=40,\n", + " max_concurrent=4,\n", + " mode=\"max\",\n", + ")\n", + "\n", + "result = tuner.run(spec=spec)\n", + "\n", + "print(\"=== Optimisation Results ===\")\n", + "for i in range(1, 6):\n", + " key = f\"component.machine_{i}.arg.maintenance_interval\"\n", + " print(f\" Machine {i} optimal maintenance interval: {result.config[key]} days\")\n", + "print(f\" Maximum total value: ${result.metrics['component.factory.field.total_value']:,.0f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "Since each machine has different reliability characteristics, the optimal maintenance schedule will differ — more fragile machines benefit from more frequent maintenance, while reliable machines can run longer between stops.\n", + "\n", + "Alternatively, the same optimisation can be launched via the CLI using the YAML config in `factory-simulation.yaml`:\n", + "\n", + "```sh\n", + "plugboard process tune factory-simulation.yaml\n", + "```" ] } ], diff --git a/examples/demos/fundamentals/003_factory_simulation/factory_simulation.py b/examples/demos/fundamentals/003_factory_simulation/factory_simulation.py index 706142f2..6df1db5b 100644 --- a/examples/demos/fundamentals/003_factory_simulation/factory_simulation.py +++ b/examples/demos/fundamentals/003_factory_simulation/factory_simulation.py @@ -150,106 +150,3 @@ async def step(self) -> None: {"sigmoid_steepness": 0.20, "sigmoid_midpoint": 30.0, "seed": 505}, # Very fragile ] - -def build_process_spec(num_days: int = 365) -> "ProcessSpec": - """Build the process specification for the factory simulation.""" - from plugboard.schemas import ProcessArgsSpec, ProcessSpec - - components: list[dict[str, _t.Any]] = [ - { - "type": "factory_simulation.Iterator", - "args": {"name": "clock", "num_days": num_days}, - }, - ] - - connectors: list[dict[str, str]] = [] - - for i, cfg in enumerate(MACHINE_CONFIGS, start=1): - components.append( - { - "type": "factory_simulation.Machine", - "args": { - "name": f"machine_{i}", - "maintenance_interval": 30, - **cfg, - }, - } - ) - connectors.append({"source": "clock.day", "target": f"machine_{i}.day"}) - connectors.append({"source": f"machine_{i}.daily_value", "target": f"factory.value_{i}"}) - - components.append( - { - "type": "factory_simulation.Factory", - "args": {"name": "factory"}, - } - ) - - return ProcessSpec( - args=ProcessArgsSpec(components=components, connectors=connectors), - type="plugboard.process.LocalProcess", - ) - - -if __name__ == "__main__": - import asyncio - - from plugboard.process import LocalProcess, ProcessBuilder - - async def run_single() -> None: - """Run a single simulation and print results.""" - spec = build_process_spec() - process = ProcessBuilder.build(spec=spec) - assert isinstance(process, LocalProcess) - async with process: - await process.run() - # Retrieve final total value from the factory component - factory = next(c for c in process.components.values() if c.name == "factory") - print(f"Total factory output over 365 days: ${factory._total:,.0f}") - - def run_optimisation() -> None: - """Optimise maintenance intervals for all machines.""" - from plugboard.schemas import IntParameterSpec, ObjectiveSpec - from plugboard.tune import Tuner - - spec = build_process_spec() - - tuner = Tuner( - objective=ObjectiveSpec( - object_type="component", - object_name="factory", - field_type="field", - field_name="total_value", - ), - parameters=[ - IntParameterSpec( - object_type="component", - object_name=f"machine_{i}", - field_type="arg", - field_name="maintenance_interval", - lower=5, - upper=60, - ) - for i in range(1, 6) - ], - num_samples=40, - max_concurrent=4, - mode="max", - ) - - result = tuner.run(spec=spec) - print("\n=== Optimisation Results ===") - for i in range(1, 6): - key = f"component.machine_{i}.arg.maintenance_interval" - print(f" Machine {i} optimal maintenance interval: {result.config[key]} days") - print( - f" Maximum total value: ${result.metrics['component.factory.field.total_value']:,.0f}" - ) - - # Run single simulation - print("Running single simulation with default maintenance intervals (30 days)...") - asyncio.run(run_single()) - - # Uncomment to run optimisation (requires Ray): - # print("\nRunning optimisation...") - # run_optimisation() From ab18685687f3488337ef43a33e9eb1649baf59e7 Mon Sep 17 00:00:00 2001 From: Toby Coleman Date: Tue, 3 Mar 2026 21:29:34 +0000 Subject: [PATCH 6/7] Update links in CONTRIBUTING.md --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c18c82d9..bab13f86 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,8 +44,8 @@ uv run mkdocs serve This repo includes custom AI agent prompts to assist with development: -- [AGENTS.md](AGENTS.md) - General guidelines for working with the Plugboard codebase. -- [examples/AGENTS.md](examples/AGENTS.md) - Specific guidance for building example models and demos. -- Copilot-specific agents `docs`, `lint` and `test` which you can @-mention in a pull request. +- [AGENTS.md](https://github.com/plugboard-dev/plugboard/blob/main/AGENTS.md) - General guidelines for working with the Plugboard codebase. +- [examples/AGENTS.md](https://github.com/plugboard-dev/plugboard/blob/main/examples/AGENTS.md) - Specific guidance for building example models and demos. +- Copilot-specific agents `docs`, `lint` and `test`. If you use GitHub Copilot or other AI coding assistants that support the AGENTS.md convention, these prompts can help you build Plugboard models from a description of the process and/or the components that you would like to implement. We recommend using Copilot in agent mode and allowing it to implement the boilerplate code from your input prompt. From 2f3aba97ce4bc5ff2d9d0ccb3df18cafc9689a77 Mon Sep 17 00:00:00 2001 From: Toby Coleman Date: Tue, 3 Mar 2026 21:33:18 +0000 Subject: [PATCH 7/7] Linting --- .../003_factory_simulation/factory-simulation.ipynb | 5 ++++- .../003_factory_simulation/factory_simulation.py | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/demos/fundamentals/003_factory_simulation/factory-simulation.ipynb b/examples/demos/fundamentals/003_factory_simulation/factory-simulation.ipynb index da714c55..4282e1ae 100644 --- a/examples/demos/fundamentals/003_factory_simulation/factory-simulation.ipynb +++ b/examples/demos/fundamentals/003_factory_simulation/factory-simulation.ipynb @@ -199,7 +199,10 @@ "\n", "for i, cfg in enumerate(MACHINE_CONFIGS, start=1):\n", " spec_components.append(\n", - " {\"type\": \"factory_simulation.Machine\", \"args\": {\"name\": f\"machine_{i}\", \"maintenance_interval\": 30, **cfg}}\n", + " {\n", + " \"type\": \"factory_simulation.Machine\",\n", + " \"args\": {\"name\": f\"machine_{i}\", \"maintenance_interval\": 30, **cfg},\n", + " }\n", " )\n", " spec_connectors.append({\"source\": \"clock.day\", \"target\": f\"machine_{i}.day\"})\n", " spec_connectors.append({\"source\": f\"machine_{i}.daily_value\", \"target\": f\"factory.value_{i}\"})\n", diff --git a/examples/demos/fundamentals/003_factory_simulation/factory_simulation.py b/examples/demos/fundamentals/003_factory_simulation/factory_simulation.py index 6df1db5b..dd43c545 100644 --- a/examples/demos/fundamentals/003_factory_simulation/factory_simulation.py +++ b/examples/demos/fundamentals/003_factory_simulation/factory_simulation.py @@ -149,4 +149,3 @@ async def step(self) -> None: {"sigmoid_steepness": 0.18, "sigmoid_midpoint": 35.0, "seed": 404}, # Fragile {"sigmoid_steepness": 0.20, "sigmoid_midpoint": 30.0, "seed": 505}, # Very fragile ] -