---
title: 'Two Graphs Are Simpler Than One: A Cooling System Simulator'
description: Live cooling-system sim for a PLC cybersecurity event. Splitting flow and heat into two graph passes kept it cheap and the behaviour believable.
date: 2026-05-04
period: 'October-November 2018'
thumbnail:
  src: ./_assets/process-simulator.jpg
  alt: Cooling system simulator interface with pipes, pumps, and temperature values.
article:
  featuredOrder: 5
  tags: ['simulation', 'systems', 'tools']
  role: Simulation and UI author
  stack: ['Python', 'Flask', 'NumPy', 'HTML canvas', 'JavaFX']
  scale: One remote sim server, many monitoring clients, separate JavaFX graph editor
  outcome: A believable PLC simulation usable by non-specialists during a live cybersecurity challenge
  audience: recruiter-relevant
  media:
    - type: image
      src: ./_assets/process-simulator.jpg
      alt: Screenshot of the cooling system simulator with pipes, pumps, coolers, and temperature values.
      caption: Flow ran first as a graph traversal, then heat solved as a matrix equation.
    - type: image
      src: ./_assets/process-simulator-input.jpg
      alt: Screenshot of the JavaFX graph editor used to define simulator input.
      caption: The JavaFX editor produced JSON that the simulator ate as input.
project:
  title: Cooling System Simulation
  description: 'A live cooling-plant simulator for a PLC cybersecurity event. Flow as graph traversal and heat as a matrix solve: two passes instead of one PDE.'
  selected: true
  thumbnail:
    src: ./_assets/nuclear-simulation.jpg
---

Solving flow and heat as one coupled system would have been a genuine CFD problem, and I had two weeks. A cybersecurity event in late 2018 needed a cooling-plant simulator that contestants could poke at through PLCs over a weekend, and that deadline made every decision for me: cheap to compute, plausible to a non-specialist, able to run unattended on one server from Friday night to Sunday. The design move I'd keep from it is modelling flow and heat as two separate graph passes instead of one PDE.

## What the event needed

The challenge was about PLCs. Contestants changed setpoints, valves, and pump speeds, and they needed to see whether their action kept the plant stable, wasted coolant, or melted something. Concretely that meant a single simulation server feeding many monitoring clients in near real time, a plant configurable enough that the organisers could hand me a new layout on Friday night and have it running Saturday morning, and behaviour legible enough to teach. A simulator nobody understands isn't a teaching tool; it's noise with units on it.

## The split that made it cheap

Each tick runs two passes:

1. **Flow first, as a graph traversal.** Walk the pipe network outward from the pumps, accumulate pressures, distribute water across the nodes.
2. **Heat second, as a linear system.** Use the flow result to build an adjacency matrix, add the boundary conditions (heaters, exchangers, ambient temperatures), and let NumPy solve for the node temperatures.

As physics, this is wrong; flow and heat are coupled, and a real plant knows it. As a model, it's right, because nothing a contestant could do made the coupling perceptible on a dashboard, and the decoupled version cost a tiny fraction of the real solve. It bought one more thing I didn't anticipate valuing so much: a clean seam for debugging. When the temperatures looked weird, I knew which of the two passes to suspect before I'd opened the editor.

## The editor mattered more than the solver

The most-used interface in the whole system was the input editor, a separate JavaFX tool where organisers laid out the plant, parameterised each element, and exported the JSON the sim consumed. It earned [its own write-up](/articles/graph-editor-javafx-simulation-input/), because in hindsight it was its own project: the simulator decided what was possible, but the editor decided who could use it.

## What I'd change

- **State what the model claims.** A convincing simulator owes its audience an honest README about what it does and doesn't capture. Mine didn't have one, and anyone who took the numbers seriously could have walked away believing more than the model deserved.
- **Recorded scenarios as regression tests.** Simulations drift in ways that still look plausible on screen. A few stored "this input over 60 seconds produces these outputs" runs would have caught me when I broke the temperature solver, which I did on the Saturday morning of the event, naturally.
- **Skip JavaFX.** Cross-platform packaging was a fight, and a web editor living next to the monitoring clients would have meant one fewer install for everyone.
