Inspiration
Every CS student is told that a service falls over when utilisation gets too high. Almost nobody has visualised it happen.
That gap is the whole problem. Modelling a dynamic system today means either writing code, where every question costs a rerun and you never build intuition, or using a graphing tool, where you can feel the curve but can't express anything past a single equation. We wanted the responsiveness of dragging a slider with the expressiveness of writing real differential equations, and we wanted to get there by describing the system in a sentence.
What it does
Describe a system in plain English. Manifold writes the state variables, rate laws, parameters and plots, then solves them in your browser.
The spec stays on screen and stays editable. Drag a parameter and the trajectory re-integrates as you move. Edit a rate law by hand. Ask for a change in words and it applies as a patch to the model currently on screen, so the AI never overwrites your edits and you never lose its work.
It also computes diagnostics. For a request queue it derives ρ = λ/μ and labels the regime, so dragging the arrival rate past the service rate flips the model from "keeps up" to "backlog grows" while you watch. Sweeps re-run the whole model across a parameter range and plot the peak of whatever you asked to observe, which turns that threshold into a curve.
How we built it
Everything runs in the browser except the model call.
The spec is JSON, with ordinary infix math for the equations. We deliberately did not invent a language. The model has enormous exposure to JSON and to standard math notation, and none at all to a syntax we'd design on a Friday night.
We do not constrain decoding. The model writes a spec freely and everything downstream is responsible for catching it: a hand-written validator, and a repair loop that hands failures back with the specific message and lets it try again. That choice is what made the rest of the project possible. Once the validator is the authority rather than the grammar, any property we can express as a check becomes a property the model has to satisfy — including ones no schema can state.
Expressions go through a Pratt parser into an AST, then get lowered to closures with every variable reference resolved to an array index at compile time. Integration is fixed-step RK4 over a flat Float64Array with preallocated stage buffers. That is why slider dragging feels like direct manipulation rather than a request: a drag re-solves the entire system on every animation frame and never touches a string lookup.
Conversational edits come back as RFC 6902 JSON Patch against the spec on screen. Patches apply, validate, and only then commit.
Challenges we ran into
The hard bugs were not crashes. They were models that validated, ran, and lied.
Asked to draw a heart, the AI produced a spec with a parameter a and a slider for it. Dragging a did nothing. The flows were the Van der Pol oscillator, and a appeared in neither of them nor in any derived expression. Chat cheerfully announced the heart would widen at a = 2 while the re-solve returned a pixel-identical plot. It had also relabelled the damping coefficient as "heart height."
Then the request queue did the same thing one level up. It declared a state variable for requests in service, wrote the service term as proportional to that variable, and started it at zero. Nothing ever moved requests into service, so the term was zero forever, the queue grew as a perfectly straight line, and the sweep came out as a straight line too because peak queue was just arrival rate times duration.
Both bugs have the same shape. The spec's dependency graph was disconnected — in one case an isolated parameter, in the other an isolated state variable. Our validator only checked the forward direction, that every identifier used in an expression resolves to something declared. Nothing checked the reverse.
So we wrote the reverse checks. A declared parameter nothing reads. A derived quantity nothing consumes. A flow in which no state variable and no time appear, which makes the derivative a constant and the state a straight line by construction. And the one that finally caught the queue: a state whose flow is identically zero, found by probing the expanded right-hand side at random points, because service - mu * S where service = mu * S cancels in a way no walk over identifiers can see. All four also surface in the editor as non-blocking warnings, so a human reading the spec sees what the repair loop sees.
The queue had a second cause worth naming. Our expression language has arithmetic and a handful of builtins, including min and max, but no comparisons and no conditionals. A real queue drains at the service rate only while it is non-empty, and that is a branch. The model could not write the correct equations, so it wrote expressible and wrong ones instead. This is the failure mode nobody warns you about: when the right answer is outside the grammar, a well-behaved model produces a confident, valid, incorrect one.
We fixed it from both ends. The lexer no longer answers a < with "unexpected character" — it explains that there are no conditionals and names the arithmetic that replaces them. And the queue now drains through a saturating term rather than a switch, μQ/(K+Q), which is zero at empty so the queue can never go negative, approaches μ when the backlog is deep, integrates cleanly under a fixed-step solver, and puts a genuine threshold at λ = μ. It is the same form as Monod kinetics in a chemostat, which was a nice thing to notice.
What we learned
A valid spec is not a correct one, and that gap is where the real work lives. Structural validation catches malformed models. Semantic checks catch disconnected ones. Neither catches a system that is simply the wrong class of model for what was asked, and that one still has to be handled in the prompt.
The corollary is that error messages are part of the interface, not the plumbing. Every message the validator produces is read by a model that gets to try again, so a message that names the fix is worth more than one that names the fault.
We also learned to stop regenerating. Rewriting the full spec each turn destroyed hand edits silently, and users stop trusting an editor the first time that happens to them.
What's next
Open-source updates. Conditionals and piecewise forms, so genuinely switching systems can be stated honestly instead of approximated. Discrete and agent-based models alongside continuous ones. An adaptive solver so stiff systems bend instead of breaking. And export to runnable code, so a model you built by talking can leave the tool and go into real work.
Built With
- canvas
- codemirror
- deepseek
- featherless
- json-patch
- llm
- nextjs
- numerical-integration
- ordinary-differential-equations
- pratt-parser
- react
- rk4
- typescript
- yaml
Log in or sign up for Devpost to join the conversation.