Technical guide The maths, worked through Live results Pitch deck GitHub

Sub-page · Health-a-thon 2026

The maths, with numbers you can change

Five ideas carry the whole system. Each one is explained with a tiny example first, then a demo you can play with. No formula appears without a worked number beside it. If you remember one sentence per section, you can answer the follow-up questions.

Warm-up

0 · Percentiles: what “P90” means

Sort the waits from shortest to longest. The P90 is the value that 90% of waits are below.

Ten patients wait (minutes): 3, 8, 12, 15, 20, 25, 31, 44, 70, 125

  • Average = (3+8+12+15+20+25+31+44+70+125) ÷ 10 = 35.3. One long wait pulls it up.
  • Median (P50) = halfway = between 20 and 25 → 22.5. Half wait less.
  • P90 ≈ the 9th value → 70 (about). Nine in ten wait less than this.
Say it simplyThe median tells you what a typical patient feels. The P90 tells you how bad it gets for the unlucky one in ten. Our headline is the P90 because that is where the pain is.

Concept 1 · the duration model

1 · Quantile loss: teaching a model to give P50 and P80

A model learns by minimising a “loss”: a penalty for each mistake. Choose the penalty well and the model's best guess becomes any percentile you like.

The idea in two lines

loss(guess c, real y) = q × (y − c) if the real time is longer (we under-guessed) (1 − q) × (c − y) if the real time is shorter (we over-guessed)
  • q = 0.5: both mistakes cost the same → the best guess is the median.
  • q = 0.8: under-guessing costs 0.8 per minute, over-guessing only 0.2 → the best guess moves up until 80% of real sessions are below it. That is P80.

Example with q = 0.8, guess 10 min. Real session 14 min: under-guess by 4 → 0.8 × 4 = 3.2. Real session 7 min: over-guess by 3 → 0.2 × 3 = 0.6.

Try it: ten recorded sessions of one technique

Sessions (min): 5, 6, 6, 7, 7, 8, 9, 11, 14, 22. Average 9.5, median 7.5. Drag the guess and watch the total penalty; the curve's lowest point is the best guess.

If they ask“Why not just predict the average?” The average is the best guess if all mistakes cost the same, but a plan has to be robust to long sessions. P80 says “8 times in 10 the session is no longer than this”. We reserve the expected minutes so the day's total is right, and use P80 to judge risk. The model is trained for all three (expected, P50, P80).

Concept 2 · the duration model

2 · Gradient boosting: many small corrections

Start with a rough guess for everyone. Look at where it is wrong. Add a tiny rule that corrects some of that error. Repeat a few hundred times.

  1. Start: guess the average for everyone (12.3 min in the demo below).
  2. Measure mistakes: real minus guess for every session (the residual). Easy sessions are negative, hard ones positive.
  3. Fit a tiny tree (here a single yes/no split such as “difficulty ≤ 6?”) that predicts those residuals.
  4. Add a fraction of it to the guess. That fraction is the learning rate. A small step avoids overreacting to noise.
  5. Repeat. Each new tree fixes what the previous ones missed. Our model uses 300 rounds, learning rate 0.08, and trees with up to 31 leaves over ten input facts.

Try it: one fact (set-up difficulty 1–10) and the session minutes

If they ask“Why not a neural network?” With a few thousand sessions and a handful of tabular facts, boosted trees are usually as accurate or better, train in seconds, and need no special hardware. They also give the quantiles directly and are easier to explain.

Concept 3 · the optimiser

3 · The scheduling puzzle: yes/no variables, one score

We turn “find a good timetable” into: pick yes/no values for a long list of switches so that the rules hold and the total penalty is as low as possible.

The model, written out

x[p, s] = 1 if patient p starts at slot s, else 0 ← the switches Each patient exactly once: sum over s of x[p, s] = 1 for every p One patient per slot: sum over p of x[p, s] ≤ 1 for every s Hard rule (e.g. not before 10): x[p, s] = 0 for every forbidden s Minimise: sum of cost[p, s] × x[p, s]

The real model has the same shape, only bigger: patient × machine × 2-minute slot, with extra rules for accessories, senior staff and breaks. For 87 patients that is about 4,700 switches and 1,660 rules.

How does the solver find the best one without trying everything?

Trying every combination is impossible: even 20 patients can be ordered in more ways than there are grains of sand on Earth. The solver uses branch and bound: it first solves an easier version where switches may be fractions (0.3 on, 0.7 off), which gives a floor that no real plan can beat. Then it fixes one switch at a time, discarding whole families of plans whose floor is already worse than the best plan found. When the best plan found is within 0.03% of the floor, it stops. That gap is the “proven within 0.03% of best possible” in our results.

Try it: four patients, four 10-minute slots, every permutation checked

A pays and wants 09:00 (weight ×4). B is ordinary (×1) and wants 09:00; D is an older patient (×1.5) who wants 09:10. C is an inpatient who can wait (×0.2). Cost of a placement = weight × minutes away from the wish ÷ 10. The solver would search; here your browser simply tries all 24 orders.

What to take from itHard rules remove options (the grey rows). Soft wishes price the options that remain. Raise the weight on a patient and the solver quietly moves them closer to what they asked for. The department turns these weights; nobody edits code.

Concept 4 · the optimiser's costs

4 · The cost numbers, calculated by hand

These are the real weights from the configuration file. Each is “points per 10 minutes”. Compare the scales: it explains almost every choice the solver makes.

Planning a day: stay near the wished time

cost = weight × max(0, |start − wish| − tolerance) ÷ 10
PatientWeightFree (min)Off byCost
Ordinary1.030501.0 × 20 ÷ 10 = 2.0
Paying4.015404.0 × 25 ÷ 10 = 10.0
Flexible (ward, dorm.)0.230500.2 × 20 ÷ 10 = 0.4
Finishes 80 min into overtime6.00806.0 × 80 ÷ 10 = 48

Reading it: a paying patient 40 minutes off costs five times an ordinary patient 50 minutes off, and overtime is very expensive, so the solver protects the day's end first.

Re-planning: moving versus making someone wait

move (not here yet): (3 × shift + 3 × max(0, shift − 60)) ÷ 10 wait (already here): (20 × late + 50 × max(0, late − 60)) ÷ 10
MinutesTold a new timeWaiting in room
257.550
8030260

So 25 minutes of waiting is as costly as moving someone about 170 minutes. That is why, when the machine breaks, the solver shifts people still at home before it leaves anyone in the waiting room.

Not placing someone costs 1,000

Every patient must start once or pay 1,000 points. The worst ordinary placement costs a few tens, so “unplanned” appears only if the hard rules truly leave no room. A human then decides; the system never quietly drops anyone.

Concept 5 · the digital twin

5 · Monte Carlo and “common random numbers”

Run the day many times with dice rolls for the uncertain parts, then read the spread of outcomes. Use the same dice for both plans you are comparing.

The recipe

  1. Draw every uncertain thing: each session's real length, each patient's arrival, no-shows, urgent cases.
  2. Play the day through (who is ready, which patient the machine takes next) and record waits.
  3. Repeat 300 times with fresh dice and report the median and the P90 across the days, with confidence ranges.

Why share the dice? Suppose you compare plan A and plan B. If each gets its own random day, a lucky day for A can hide a real advantage for B. If both get the same random day, the luck cancels in the difference. Try it:

Try it: 12 patients, one machine, sessions with a median of 8 minutes

Plan A books every 10 minutes; Plan B every 9 (tighter). We want to know how much longer the average wait is under B. Each “day” is random. We repeat 300 days and look at the difference in average wait, with and without sharing the dice.

Press the button.
Say it simplyBoth answers agree on the size of the effect. The shared-dice answer is less noisy, so with the same 300 days we can tell the plans apart with confidence. That is how we show RAD-SMART's gain is not luck.

Concept 6 · the forecast

6 · Forecast: adding up minutes that are already promised

A patient with 12 sessions left will use the machine on each of the next 12 working days. Add those up and the future load is mostly known.

load on day k = sum over patients with sessions left on day k of their predicted minutes capacity = machine minutes × 92% − urgent holds − TBI minutes (180 on a TBI day) share used = load ÷ capacity

Worked example. One machine has 843 usable minutes on a normal day. 40 patients still have sessions tomorrow, averaging 17 minutes each: load = 40 × 17 = 680 minutes → 680 ÷ 843 = 81%. A new start with 25 sessions adds about 25 minutes on day 1 and about 9 a day after: that is 3.0% of capacity on day 1, then 1.1% a day.

The bars below use the prototype's committed load for the Versa HD over 15 days. The new-start size is illustrative.

Try it: start new patients on day 3

If they ask“Why minutes and not head-counts?” Ten SBRT patients take far more machine time than ten palliative ones. In the prototype, splitting new starts equally by head-count pushed the Versa HD to 154% of capacity; balancing by predicted minutes kept it at 98%.

Back to the big picture

Modules, architecture, safety and the question list.

← Technical guide