Inspiration

Drug labels carry timing rules: take on an empty stomach, every 4 hours, at least 2 hours before or 6 hours after antacids. A patient on five medications can have a dozen of these interacting, and pharmacists work them out by hand. The published scheduling methods I found place one dose at a time and give up when they get stuck. When they give up on a regimen that could have been scheduled, a patient may be told to change a prescription they did not need to change.

What it does

You enter the medications and the patient's day (meals and sleep). LINNET gives one of three answers:

  • Tier 0: it fits the day as it is. You get a 24-hour schedule.
  • Tier 1: it fits with a small change. You get ranked options, such as moving lunch 30 minutes or dropping one as-needed dose.
  • Tier 2: the prescriptions cannot be scheduled at all, even under the loosest reading of every label. You get the smallest set of label sentences that clash, each linked to DailyMed.

A real tier 2 found in the labels: nimodipine every 4 hours, together with colestipol's "other drugs at least one hour before or four hours after". No 24-hour day fits both.

LINNET makes no drug recommendations. It shows that a conflict exists and which sentences cause it; what to change is the prescriber's decision.

How I built it

  • Rules come from public FDA labels. A language model (Claude Haiku 5.5) proposes rules and cites sentence ids. Code then checks each one: the sentence exists, every number in the rule appears in that sentence, and the rule fits the schema. The quote is copied by code, never by the model.
  • The scheduler is a Z3 encoding written in C++. It models one repeating 24-hour day in exact fractions, so 23:00 and 01:00 are 2 hours apart, not 22.
  • Everything shown is re-checked. An independent verifier re-checks every schedule in exact arithmetic, and every conflict is shrunk until each remaining rule is needed.
  • Patient data stays in memory. The model reads only public labels and never sees patient data. The solver never calls a model.
  • Python, FastAPI and a single-page UI. Solo build.

Results

  • On 500 random regimens, a greedy one-dose-at-a-time heuristic gave up on 33% of the regimens LINNET could schedule (64 of 194). On real NHANES daily routines it was 34% (67 of 199).
  • An exhaustive 15-minute grid search never disagreed with the solver (274 regimens searched).
  • Every conflict shown was minimal (78 of 78, and 93 of 93 on NHANES days).
  • A full check takes 35 ms at the median.
  • Of 362 NHANES adults taking a product the library covers, 41 (11%) cannot follow their labels on their recorded day. Most are levothyroxine users who ate breakfast less than 30 minutes after waking.

Challenges

  • Midnight. A naive encoding treats doses at 23:00 and 01:00 as 22 hours apart. Every gap is measured around the clock, with regression tests for it.
  • Solver state. Z3 keeps state across solves, and late in a long sweep one regimen went from 0.04 s to over 40 s. Each check now runs in a fresh process with a hard time limit, and a check that times out is reported as undetermined, never as an answer.
  • Getting rules out of labels reliably. For the last twelve labels, each label was read four separate times and a rule entered the library only when three or four readings produced the same rule. Five of the twelve labels failed that test and were left out.

What I learned

Most of the effort went into the rules, not the solver. A model reading a label will state a rule backwards with full confidence: two of four readings inverted risedronate's "30 minutes after" rule. The deterministic checks and the agreement test mattered more than the choice of model.

Limits

  • No pharmacist has reviewed the rules. The sign-off step is designed for a person; in this submission it was done by Claude (an AI) and every decision is recorded that way.
  • The library is small: 17 products.
  • The output is a draft for a pharmacist to check, not an instruction to a patient. Not for clinical use.

Built With

Share this project:

Updates

Submission history