Rumoca starts with a simple promise: write the physics once, preserve the semantics, and let different tools consume the representation they actually need. For flight software that matters because the model, the analysis, the controller, the generated code, and the verification artifacts should not drift apart.
The first important point is not the CFD. It is the coupling. The airfoil does not just sit there. A first-order motor model moves the commanded angle of attack toward the physical angle of attack, and first-order shape states move commanded camber, camber position, and thickness toward the actual airfoil geometry. Those actuator and shape states feed directly into the PDE model of the surrounding flow.
In plain language: Rumoca can keep the wing, the actuator, the shape change, and the fluid response in the same mathematical object. Automatic differentiation through PDE solvers is already an active area, but that is not the whole point here. The more interesting move is cross-domain optimization: forward- or reverse-mode AD can see aerodynamic states, actuator lag, and geometry parameters together. If I change camber here, what happens to drag? If I change angle of attack through this motor, what happens to lift later? The optimizer can ask those questions across the coupled system, not across a hand-maintained chain of separate tools.
The long-term direction is aircraft design as an optimization loop instead of a pile of disconnected tools. Today this example lets you steer a NACA airfoil in the browser. Tomorrow the same kind of model could help choose an airfoil, actuator schedule, or morphing shape for maximum endurance, maximum range, stall margin, thermal limits, or whatever the mission requires.
The NACA airfoil PDE example in the Rumoca user guide is a useful stress test for that promise. It uses ordinary Modelica array variables and nested for-equations to express a method-of-lines discretization of a 2-D flow problem. Then the browser version compiles that model through Rumoca and runs the heavy path on the graphics card through WebGPU.
The core idea: Rumoca does not force a coupled physical model to become anonymous scalar soup too early. It keeps enough source-proven array, loop, actuator, and shape structure alive that downstream targets can emit real kernels: WebGPU/WGSL today, CUDA and JAX-style differentiable backends as the pipeline matures.
The PDE Model
Modelica is not a PDE language in the specialized CFD sense. It has no magic solveNavierStokes() primitive. What it does have is arrays, parameters, equations, and structural loops. That is enough to write method-of-lines models directly: discretize space into cells, assign states to each cell, and let time integration handle the resulting ODE or DAE system.
The airfoil example models 2-D incompressible flow around a NACA 2412 profile on a Cartesian grid. It keeps the model intentionally compact and browser-friendly by using two classical approximations.
- Artificial compressibility gives pressure a fast dynamic equation instead of solving a pressure-Poisson algebraic constraint at every step.
- Brinkman penalization avoids body-fitted meshing. The NACA shape becomes a smooth solid mask, and a drag term damps velocity inside the body.
That second point is where the NACA part becomes more than geometry. The solid mask is not a hard on/off indicator. It is a smooth tanh transition around the airfoil surface. At low resolution this makes the visualization easier to read, but the deeper point is differentiability: a smooth mask gives nonzero sensitivities with respect to camber, camber position, thickness, and angle of attack near the boundary.
The live version also includes simple dynamics for the things a designer would actually command. The angle of attack is filtered by a first-order motor state, while camber, camber position, and thickness are filtered by first-order shape states. Those states are ordinary Modelica variables, so the flow solver sees a time-varying airfoil instead of a disconnected slider. That is what makes the example a small cross-domain system rather than a static visualization.
parameter Integer NX = 30;
parameter Integer NY = 18;
Real u[NX, NY] "x velocity";
Real v[NX, NY] "y velocity";
Real q[NX, NY] "pressure / rho";
Real sig[NX, NY] "smooth solid mask";
for i in 2:NX - 1 loop
for j in 2:NY - 1 loop
der(u[i,j]) = advection_x + pressure_x + diffusion_x - sig[i,j] * u[i,j] / tau;
der(v[i,j]) = advection_y + pressure_y + diffusion_y - sig[i,j] * v[i,j] / tau;
der(q[i,j]) = -cs^2 * div_v + pressure_diffusion;
end for;
end for;
The snippet above is deliberately schematic. The important feature is the shape of the program: regular arrays and regular loop domains. That is what a compiler can preserve. If those loops are flattened into unrelated scalar expressions before the backend sees them, the GPU target has to rediscover the stencil pattern after the fact. Rumoca's Solve IR instead carries tensor program nodes, including affine stencil structure, so repeated grid work can become repeated GPU work naturally.
The Pipeline Is the Product
A lot of compilers ask users to hand over a source file and wait for an executable. Rumoca's design is different. The compiler pipeline exposes useful stopping points, and each point has a real audience.
The source-level syntax tree. It is the right layer for formatters, diagnostics, documentation tools, and any workflow that cares about the text users wrote.
Source code viewA single flattened Modelica model with resolved names, expanded connections, symbolic arrays, and Modelica operators still intact.
Modelica semantics viewA concise mathematical system description. This is the stable symbolic contract for analysis, export, templates, and many algebraic backends.
Math system viewAn implementable execution form: scalar register programs plus tensor nodes that can lower to interpreters, C, Rust, CUDA, WGSL, or JAX-like kernels.
Backend viewThat "jump off anywhere" property is not a convenience feature. It is the architecture. If you want a source formatter, consume AST. If you want a flat-Modelica export, consume Flat. If you want symbolic optimization, consume DAE. If you want a shader, CUDA kernel, embedded C function, or differentiable numerical kernel, consume Solve.
rumoca compile AirfoilFlow.mo --emit ast-json
rumoca compile AirfoilFlow.mo --emit flat-mo
rumoca compile AirfoilFlow.mo --emit dae-json
rumoca compile AirfoilFlow.mo --emit solve-json
rumoca compile AirfoilFlow.mo --target wgsl-solve --output gen/airfoil_gpu
Templates can attach at the level that matches the job. A shader generator should not be scraping Modelica source text. A documentation tool should not need BLT ordering. A JAX backend should not need to reverse engineer loop nests from generated C. The point of the pipeline is to keep those concerns separated without hiding the model.
Why Array-Native Matters
The usual beginner explanation is "arrays are shorter to write." That is true, but it is not the real reason. The real reason is that arrays carry structure: rank, shape, layout, iteration domains, and neighbor access patterns. In a PDE discretization, that structure is the problem.
For the NACA example, each interior cell reads neighboring velocity and pressure values, computes finite-difference terms, applies the smooth body mask, and writes derivatives. A scalar backend can expand that faithfully. A GPU backend can instead treat it as a row-parallel stencil over a 2-D field. Both should come from the same model.
This is what "array native all the way through" means in practical terms:
- Users write
u[NX, NY], not a handmade state vector index map. - Flattening resolves the model without erasing every useful array boundary.
- DAE lowering produces a lean symbolic system instead of stuffing solver artifacts into the math contract.
- Solve lowering keeps tensor and stencil structure available for targets that can use it.
- WebGPU can execute kernels on the graphics card while scalar fallbacks remain possible for CPU or embedded targets.
The Browser Path
Rumoca's live guide uses the same compiler in the browser through WebAssembly. The published npm package is @cognipilot/rumoca; as of this post, the current registry version is 0.9.9. The hand-written browser runtime lives in the repository's packages/rumoca-web package and is staged into the published package as subpath exports such as @cognipilot/rumoca/gpu and @cognipilot/rumoca/interactive.
import init, { prepare_gpu_simulation } from "@cognipilot/rumoca";
import { probeGpu, runGpuSimulation } from "@cognipilot/rumoca/gpu";
await init();
const gpu = await probeGpu();
if (!gpu.ok) throw new Error(gpu.reason);
const prep = JSON.parse(prepare_gpu_simulation(modelicaSource, "AirfoilFlow"));
const result = await runGpuSimulation(gpu.adapter, prep, {
tStart: 0,
tEnd: 30,
outputDt: 0.1,
internalDt: 0.005,
});
The exact guide runner does more than this: editor setup, diagnostics, parameter controls, live input wiring, plotting, and visualization callbacks. But the core path is the one above: compile the Modelica model to a GPU-ready Solve representation, build WebGPU compute pipelines, then run an RK-style integrator in page.
For blog posts, the important operational detail is versioning. This page pins the exact @cognipilot/rumoca@0.9.9 package in a small manifest and loads that immutable npm version through npm-backed CDN URLs. That means the post does not fetch latest, does not carry old runtime builds in this repository, and does not depend on the current user guide staying API-compatible forever.
There are caveats, and they are healthy. The current WebGPU path is experimental, uses f32, and intentionally freezes algebraics/events in the basic batch mode. The NACA grid is small and the Reynolds number is low. This is not a production CFD solver. It is a demonstration that a declarative physical model can retain enough structure to become a graphics-card computation without being rewritten in a shader language by hand.
Why This Matters for CogniPilot
CogniPilot is about safe autonomy, not just fast simulation. The flight stack needs models that can be inspected, transformed, proven against, compiled, and deployed. The NACA demo matters because it exercises a hard version of that problem: many states, structured arrays, nonlinear dynamics, and an execution target that wants parallel kernels.
If Rumoca can keep a PDE-like airfoil model coherent from source to GPU, the same machinery applies to less visual but more operationally important systems: reachable-set propagation, rotor and actuator models, sensor dynamics, estimator residuals, controller sensitivity analysis, and simulation-in-the-loop testing.
The strategic direction is straightforward: PDE today, neural ODEs tomorrow. Once a compiler can expose clean DAE and Solve forms, it becomes natural to connect physical models to differentiable programming workflows. A future neural ODE workflow should not require throwing away the Modelica model and rebuilding the system in a machine-learning framework. The physical model should remain the source of truth, while JAX, PyTorch, CasADi, CUDA, WGSL, or embedded targets receive representations designed for their job.
See the Run First
The live widget below is the real path, but first load has to fetch the pinned Rumoca package, initialize WebAssembly, compile the Modelica source, and set up the WebGPU kernels. On a typical laptop that can take around 10 seconds. The recording below shows the expected NACA airfoil result before you spend that time.