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/4] 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/4] 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/4] 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/4] 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