OpenFOAM Studio

A desktop app for running CFD simulations. Describe the flow in plain English, and it writes the case, runs the real OpenFOAM solver and checks the result.

Role
Design, frontend and backend, solo
Type
Personal project, open source (MIT)
Platforms
macOS and Windows desktop app
Built
April to July 2026

windAroundBuildingsfoamRunk-epsilon185,237 cellsOpenFOAM 13

One sentence in

In the app, a case starts as plain English. The assistant turns it into the files OpenFOAM reads.

You

Wind around a block of buildings at 10 m/s, 3D, steady, k-epsilon turbulence

Case files
  • 0/U
  • 0/p
  • 0/k
  • 0/epsilon
  • 0/nut
  • constant/physicalProperties
  • constant/momentumTransport
  • constant/geometry/buildings.obj
  • system/blockMeshDict
  • system/snappyHexMeshDict
  • system/controlDict
  • system/fvSchemes
  • system/fvSolution

A box to start from

The mesh starts as one block, 350 × 280 × 140 m, split into 25 × 20 × 10 cells.

system/blockMeshDict

  1. backgroundMesh
  2. {
  3. xMin -20; xMax 330;
  4. yMin -50; yMax 230;
  5. zMin 0; zMax 140;
  6. xCells 25; yCells 20; zCells 10;
  7. }
  8. vertices
  9. (
  10. ($xMin $yMin $zMin) ($xMax $yMin $zMin)
  11. ($xMax $yMax $zMin) ($xMin $yMax $zMin)
  12. // … and the same four at zMax
  13. );
  14. blocks
  15. (
  16. hex (0 1 2 3 4 5 6 7)
  17. ($xCells $yCells $zCells)
  18. );
  19. boundary
  20. (
  21. inlet { type patch; }
  22. outlet { type patch; }
  23. ground { type wall; }
  24. frontAndBack { type symmetry; }
  25. );

The buildings cut in

snappyHexMesh removes the buildings from the box and splits the cells around them into smaller ones.

Cells
185,237
Points
218,801
Faces
589,240
Refinement
level 3 on the buildings, level 2 in a box round them

The physics

Each patch gets a boundary condition, and the air gets a viscosity and a turbulence model.

inlet
U fixedValue (10 0 0) m/s
outlet
p totalPressure 0
buildings
U noSlip, wall functions
ground
U noSlip, wall functions
top, sides
symmetry
nu
1.5e-05 m²/s, air
Turbulence
RAS kEpsilon

Solving

The steady solver ran 500 iterations and saved the fields every 50. The slice is the wind speed 5 m above the ground at each save.

Upkchange per iteration
Iteration
50
U
–
p
–
k
–

Streamlines

Wind speeds up over the roofs and round the corners, then slows and circles in the streets and behind the tallest blocks.

0|U| m/s16

Loading the case…

OpenFOAM’s windAroundBuildings tutorial, meshed and solved on my machine with OpenFOAM 13. Every view here is read from that case’s files: the mesh, the boundary conditions, the saved fields and the velocity the streamlines follow.

“Whatever job you end up doing, find a way to contribute to the field you graduated in.”

Prof. Gautam Barua, former Director, IIT Guwahati, at our graduation tea party

Problem

OpenFOAM is one of the most capable CFD solvers there is, and one of the hardest to use. An engineer can describe a case in one sentence. OpenFOAM needs it as a folder of plain text files with no schema, and the files have to agree with each other.

What you mean

“Lid-driven cavity, Re=100, 2D, incompressible, run for 10 seconds”

Files
8
Lines
349
Checked before a run
0

Loading the case…

What OpenFOAM needs for that sentence: the eight files of the cavity case on this page, one bar per line. The failures are real runs from building the app.

I first hit this during my bachelor’s thesis, then saw students in a CFD course get stuck the same way. I work in product design now, and this project is my way back into that field.

The hard part is the tooling, not the physics.

Who it’s for

Engineers who understand fluid dynamics but don’t want to become OpenFOAM configuration specialists.

Primary

Mechanical and aerospace engineers

They know the physics. They want a case running in minutes, with files they can still check.

Secondary

Researchers

Quick sweeps across Reynolds number, geometry or model, without boilerplate case files for each run.

Not for

Meshing and CAD

GUI mesh generation and CAD import are a different problem, and out of scope.

Research

Before building the agent, I read four papers on LLMs for CFD: MetaOpenFOAM, Foam-Agent 2.0, ChatCFD and CFDLLMBench. All four landed on the same loop: plan, generate, run, review. The review step mattered most.

MetaOpenFOAM pass@1

Full system85%
Without reviewer27.5%
Without retrieval0%

Foam-Agent 2.0 success rate

Reviewer + file order88.2%
Reviewer only86.4%
File order only56.4%
Neither48.2%

MetaOpenFOAM (arXiv 2407.21320), Foam-Agent 2.0 (arXiv 2509.18178), ChatCFD (arXiv 2506.02019), CFDLLMBench (arXiv 2509.20374).

57.5

Points lost in MetaOpenFOAM when the reviewer is removed

6–7

Files in a typical case, each depending on others

300–600

Lines across those files

The model

Every simulation goes through five steps. If a run fails, review sends it back with a fix.

PlanClassify the case GenerateFiles, in order RunIn Docker ReviewRead the log ValidateMesh and benchmark Fix and re-run, at most 3 times

Principles

Inspectable

The AI writes dictionaries, not results. Every file is open in the editor before it runs.

Real solver

No surrogate model and no predicted flow. blockMesh, foamRun and foamToVTK run in a container.

Correct, not just running

A case that converges to the wrong answer is worse than one that fails loudly.

Local

Everything runs on your machine, and the agent never gets a raw shell.

Describe

Type a case in plain English. The agent writes the case files in dependency order, so the patch names in the mesh match every boundary condition. Follow-up messages change only the files that need it.

Hover a highlighted phrase to see where it ends up.

Lid-driven cavity, Re=100, 2D, incompressible, run for 10 seconds

1

0/U

movingWall
{
    type   fixedValue;
    value  uniform (1 0 0);
}

The lid moves at 1 m/s.

2

constant/physicalProperties

viscosityModel  constant;
nu  0.001 [m^2/s];

ν = U·L / Re = 1 × 0.1 / 100.

3

system/blockMeshDict

frontAndBack
{
    type  empty;
    ...
}

One cell deep, front and back ignored.

4

system/controlDict

endTime   10;
deltaT    0.005;

2,000 time steps.

Lid-driven cavity, Re=100, 2D, incompressible, run for 10 secondsfoamRunGenerate▶ Run
controlDict

system/controlDict

  1. application foamRun;
  2. solver incompressibleFluid;
  3. startFrom startTime;
  4. startTime 0;
  5. stopAt endTime;
  6. endTime 5;was 10
  7. deltaT 0.005;
  8. writeControl timeStep;
  9. writeInterval 200;
A follow-up changes one line in one file, and the editor shows which.

Inspect

The layout borrows from code editors: file tree on the left, editor in the middle, chat on the right. Files open as the agent writes them, and a Parameters tab turns the main values into fields you can edit without touching syntax.

U0/U
  1. dimensions [0 1 -1 0 0 0 0];
  2. internalField uniform (0 0 0);
  3. boundaryField
  4. {
  5. movingWall
  6. {
  7. type fixedValue;
  8. value uniform (1 0 0);
  9. }
  10. fixedWalls
  11. {
  12. type noSlip;
  13. }
  14. frontAndBack
  15. {
  16. type empty;
  17. }
  18. }
Every generated file, open in a real code editor.
ParametersReload

system/controlDictSave

constant/physicalPropertiesSave

constant/momentumTransportSave

The same values as form fields, written back to the files on save.

Run

Runs happen inside an OpenFOAM 13 container, with the log and a residual chart streaming live. Each run is saved to a history you can reopen.

Lid · 1 m/s →
Prompt
“Lid-driven cavity, Re=100, 2D, incompressible, run for 10 seconds”
Files written
8
Mesh
20 × 20 cells
Solver
foamRun · incompressibleFluid
Time steps
2,000 in 17 s
Final residual
Ux 5.0e-9
Exit
0

A real run: the velocity field of the cavity case, read from its output files. Particles follow the solved flow. Move the cursor through it to stir the flow and read the velocity at any point.

Lid-driven cavity, Re=100, 2D, incompressible, run for 10 secondsfoamRunGenerate▶ Run
LogsResults

DoneblockMesh=0 · foamToVTK=0 · foamRun=0

  1. > blockMesh -case /cavity
  2. [blockMesh exited with code 0]
  3. > foamRun -case /cavity
  4. Courant Number mean: 0.222277 max: 0.836407
  5. Time = 10s
  6. smoothSolver: Solving for Ux, Initial residual = 4.98332e-09
  7. smoothSolver: Solving for Uy, Initial residual = 7.54293e-09
  8. DICPCG: Solving for p, Initial residual = 1.68815e-07
  9. ExecutionTime = 0.956191 s ClockTime = 1 s
  10. End
  11. [foamRun exited with code 0]
The last lines of the same run. Every command, its output and its exit code stream in as they happen.
UxUyp
1e-101e-81e-61e-41e-21e00500100015002000time step
Initial residuals from the cavity run, read from its log. All three fall below 1e-6.
2,000

Time steps

17 s

From blockMesh to exit 0, on a laptop

3

Commands per run: blockMesh, foamToVTK, foamRun

Recover

When a run fails, the app reads the log, explains the likely cause and proposes a fix as a diff. You approve it before anything re-runs.

Solver log

Create time

--> FOAM FATAL ERROR:
solver not specified in the controlDict
or on the command-line

From function foamRun
in file foamRun.C at line 95.

FOAM exiting
[foamRun exited with code 1]

Diagnosis

Missing solver in controlDict

foamRun needs a solver entry to know which solver module to load. This case has none.

system/controlDict
- solidLibs ...
+ solver  incompressibleFluid;
Apply and re-runDismiss

Results

Solved cases open in a 3D viewer inside the app, modelled on ParaView and ANSYS CFD-Post. A result is a pipeline: the mesh coloured by any field, then slices, clips, iso-surfaces, streamlines and vector glyphs, each with its own settings, played over every saved time.

create a flow around cylinderincompressibleFluidLogsResults
t = 10.0 s 21 saved times
The same pipeline in 2D, on three runs from the app, read from each run’s saved fields. Slices and clips need a third dimension, so they’re left out here. Bad Apple!! is a scripted stress test: 24 video frames seeded one after another into a water and air solve.
Velocity magnitude in a backward-facing step: a fast core leaving the step and slow regions at the walls.
“Pitz daily 2D, re 10” Velocity magnitude. Height ×3.
Temperature in a channel with a heated bottom wall: a warm layer grows along the floor.
“Channel flow with a heated bottom wall” Temperature, 300 to 319 K. Height ×4.

Setup

Docker and a model are the two things that stop people on day one. A health check runs on first launch, checks Docker, the OpenFOAM image and the model login, and repairs what it can. Bring your own key or point it at a local model through Ollama.

Environment setup
  • DockerRunning
  • OpenFOAM imagemicrofluidica/openfoam:13
  • Claude CLINot installed
    $ npm install -g @anthropic-ai/claude-code
    Fix it for meI’ve done it
  • Model loginWaiting
Each missing piece comes with its exact command and a button that runs it.
AI provider

Stored locally in your user config directory. Never leaves this machine except when calling the selected provider.

A local model through Ollama needs no key at all.

System design

Most of the work is here: what runs where, what the model can and can’t touch, and how a failed run gets back on track.

Architecture

Electron starts a local Node server, and the React interface talks to it over REST, with live updates as server-sent events. All the real work lives in core/, plain Node with no Electron in it, so the whole pipeline runs and is tested without a window.

RendererReact · Vite · Zustand FilesEditorAssistantLogs · RunsParametersResults Local serverNode · port 3456 /generate/run/apply-fix/postprocess/vtk/manifest/health core/Plain Node, no Electron agentdockerrecoverypostprocessdocs indexsetup · health ContainerOpenFOAM 13 blockMeshcheckMeshfoamRunfoamToVTK RESTSSE calls dockerode Electron shellStarts the server, waits for it, opens the window LLM providersAnthropic · OpenAI · GoogleOllama · Claude CLI Project foldercase/ · runs · logs bind mount
Three layers in one app. The interface also runs in a plain browser against a mocked backend.

Headless core

No Electron import is allowed in core/. Every module can be run and tested from a terminal.

Thin shell

Electron only starts the server, waits up to 30 seconds for it to answer, then loads the interface.

One port

The server serves the built interface and the API from localhost:3456, so the browser and the app see the same thing.

Agent loop

Generation is a tool-calling loop, not one big prompt. The model gets eight tools and works until it calls finish or reaches 16 steps. Its instructions tell it to save the last steps for blockMesh, checkMesh and a short solver run, so it tests a case before calling it done.

ModelYour choice of provider ToolsFiles · docs · run tool call result finishSummary of the case step 1 … 16

A cavity case, step by step, in the order the agent is told to work.

  1. 01search_docs"lid-driven cavity incompressibleFluid"3 hits
  2. 02read_docwiki/cases/lid-driven-cavity.mdread
  3. 03list_case_filesempty case
  4. 04write_case_file0/Uwritten
  5. 05write_case_file0/pwritten
  6. 06write_case_fileconstant/physicalPropertieswritten
  7. 07write_case_fileconstant/momentumTransportwritten
  8. 08write_case_filesystem/controlDictwritten
  9. 09write_case_filesystem/fvSchemeswritten
  10. 10write_case_filesystem/fvSolutionwritten
  11. 11write_case_filesystem/blockMeshDictwritten
  12. 12run_commandblockMeshexit 0
  13. 13run_commandcheckMeshMesh OK
  14. 14run_commandfoamRun --steps 10exit 0
  15. 15finish8 files, validated

15 of 16 steps used

4

Case files

Every write stays inside the case folder and uses LF line endings.

  • list_case_files
  • read_case_file
  • write_case_file
  • edit_case_file
2

Docs

Search the OpenFOAM manual and the wiki, then read a full page.

  • search_docs
  • read_doc
1

Run

One command in the container, from the allowlist only.

  • run_command
1

Finish

Refused while the case folder is empty.

  • finish

Two engines

API keys go through the Vercel AI SDK. The Claude CLI option uses your own Claude Code login, so it works without a key.

Local models

With OpenAI-compatible models such as Ollama, files are written one at a time, which suits smaller models.

Every step streamed

Each tool call, result and file write goes to the interface as it happens.

Knowledge

The agent looks things up instead of relying on memory. A keyword index covers 48 pages of the OpenFOAM 13 manual and 18 wiki pages, split at headings. Search returns the source path, so the agent can read the whole page and cite it.

Search the agent’s sources, split at the same headings: the wiki’s OpenFOAM pages and the manual’s section titles. Try another word.
48

Manual pages, scraped from the OpenFOAM 13 user guide

18

Wiki pages on cases, solvers and common failures

0

Vector databases. The corpus is small, and exact keywords suit OpenFOAM’s vocabulary.

Sandbox

Every solver command runs in a fresh container with only the case folder mounted, and the container is removed when the command ends. Commands are checked against an allowlist in code, not in the prompt, and every argument is shell-escaped.

AllowlistIn code CreateMount case/ ExecEscaped args StreamLine by line ExitCode read RemoveAlways rm -rf /Rejected before Docker Cancel kills the container
  • blockMesh
  • checkMesh
  • setFields
  • foamRun
  • foamToVTK
  • icoFoam
  • simpleFoam
  • pimpleFoam

The whole allowlist. Anything else is refused before a container exists.

$

Fresh each time

No container outlives its command, so no state leaks between runs.

Windows paths

The usual src:dst bind broke on the colon in C:\, so mounts use an explicit mount object.

Honest exit codes

The stream can end before Docker records the exit code, so the runner polls until it’s final instead of guessing.

Recovery

Recovery has two tiers. Known failures are matched by pattern and fixed by set rules, with no model call. Anything else goes to an LLM reviewer, which returns a corrected file or says it can’t identify the problem instead of guessing.

Failed logexit ≠ 0 Pattern match5 known errors Rule fixSame fix every time LLM reviewerFix, or “can’t tell” Diff + approvalThen re-run known unknown At most 3 retries per run
Log saysFileFix
keyword UFinal is undefinedfvSolutionAdd a UFinal solver block
cannot find file …/phasePropertiesphasePropertiesCreate it for water and air
Unknown patchField type "…"0/UReplace the type with noSlip
No reference cell foundfvSolutionAdd pRefCell and pRefValue
Courant Number … max: > 1controlDictHalve deltaT
anything elseanyLLM reviewer proposes a corrected file

The five known errors, and everything else. Hover a row to see which path it takes.

Storage and streams

There’s no database. Each project is a folder, with append-only logs for runs and commands. Generation, runs and repairs all reach the interface as server-sent events.

Project folder

projects/82a55a15/
├─ meta.json        name, status, chat history
├─ case/            the OpenFOAM case
│  ├─ 0/  constant/  system/
│  ├─ 0.05/ … 10/   time steps
│  └─ VTK/          for the viewer
├─ runs.jsonl       one line per run
├─ commands.jsonl   every container command
└─ runs/
   └─ 342d3523.log  full solver log

POST /run · text/event-stream

run-started {"runId":"342d3523"}
line        > blockMesh -case /cavity
exit        {"cmd":"blockMesh","code":0}
line        Time = 0.005s
residual    {"Ux":1,"Uy":0.99,"p":1}
line        Time = 0.01s
…
exit        {"cmd":"foamRun","code":0}
done        {"status":"success"}

Event payloads are simplified here.

Plain files

An OpenFOAM case is already plain files, so the same folder opens in ParaView or a terminal. Nothing is locked inside the app.

Append-only

Runs and commands are added as new lines and never rewritten, so a crash can’t corrupt the history.

One stream per job

Closing the request cancels the job, and on a run that kills the container.

Results pipeline

After a run, foamToVTK converts every time step inside the container. The server builds a list of steps and patches, and the interface fetches each step when it’s needed. vtk.js has no reader for this format, so I wrote one.

case/ foamToVTKIn Docker VTK/*.vtkEvery step ManifestSteps + patches Viewervtk.js, WebGL parsersurfacecache In the browser, per step, on demand

Own parser

vtk.js has no reader for legacy unstructured VTK files, so the interface parses them itself.

Cached steps

Parsed steps are kept in memory, so scrubbing through time never reads from disk twice.

One engine

All vtk.js code sits behind one class. When WebGL drops its context, the viewer recovers instead of going blank.

Stack and release

  • InterfaceElectron, Vite, React, TypeScript, Zustand, Monaco, vtk.js
  • ServerNode, REST and server-sent events, dockerode
  • AIVercel AI SDK for Anthropic, OpenAI, Google and OpenAI-compatible models; the Claude CLI as a fallback; FlexSearch for docs
  • SolverOpenFOAM 13 (Foundation) in microfluidica/openfoam:13
  • TestsVitest, 25 test files: unit tests plus stages 0 to 5
  • ReleaseGitHub Actions builds a macOS DMG and a Windows installer and publishes them to GitHub Releases

Validation

Physics is in the test suite. One stage runs the lid-driven cavity and compares the velocity along its centreline with Ghia et al. (1982), and a physics regression fails the build.

OpenFOAM Studio runGhia et al. (1982)
-0.4-0.200.20.40.60.8100.250.50.751u / U lidy / L
Horizontal velocity on the vertical centreline of the cavity run, against the published values. On this 20 × 20 mesh the largest gap is 5.4% of the lid speed.
3.2%

Largest gap to Ghia in the stage 4 test

15%

Gap that fails the build

1.8e-6

Final residual in that test

How I worked

I built it with AI coding agents, in stages. No layer started until the layer below it passed its tests. A markdown wiki of OpenFOAM knowledge and an agent change log kept every session grounded.

0Docker5 tests 1Files4 tests 2Mesh3 tests 3Solver3 tests 4PhysicsGhia 5Chat andrecovery
Stages 0 to 4 all passed on 11 April 2026, before the interface was built.
  1. Apr 11Stages 0 to 4 pass: Docker, files, mesh, solver, physics
  2. Apr 16Chat follow-ups, residual chart, error recovery
  3. Apr 19Bring your own key, Windows build
  4. Apr 27New interface, built in seven phases
  5. May 2Local models, health check on launch
  6. Jun 12Results viewer
  7. Jul 3macOS releases from CI
  8. Jul 6Agent validation loop
Milestones, April to July 2026.

Outcome

Version 1.0.2 is out on GitHub for macOS and Windows, under the MIT licence. It’s validated on the lid-driven cavity and works best on incompressible laminar cases for now. Next, I’m sharing it with a professor for feedback.

OpenFOAM Studio v1.0.2Latest14 June 2026 · MIT licence · OpenFOAM 13 macOS installer.dmg Windows installer, x64.exe Windows installer, arm64.exe Source code.zip

What I learned

End to end

Every project before this was product design only. This is the first one I took from problem to shipped software: design, code, tests, packaging and release.

Systems, not screens

Most decisions were about where safety lives: in code, not the prompt; in rules for known errors, with the model only for the rest.

Correct over working

Using a benchmark as the pass gate changed what “done” meant.

Next projectAir AtlasWhat is in the air, printed as halftone ink.Read the case study →