DEV Community

Rat Wolf (ratwolf)
Rat Wolf (ratwolf)

Posted on • Edited on

I Built a Finite-Element Solver With Nothing to Hide

A transparent, dependency-free capacitor simulator — and an honest account of exactly where a simple mesh lets it down.

Plate Capacitor

Every physics course teaches the parallel-plate capacitor formula in the first week:

Plate Capacitor

Clean. Memorable. Wrong the moment your capacitor isn't two infinite plates in a vacuum. Real capacitors have edges. Real dielectrics are layered. Real geometries are curved. The formula survives exactly as long as the idealization does.

So you reach for simulation instead — and run into two unsatisfying options.

Option one: a commercial FEM package. Powerful, polished, and a complete black box. You get a number. You don't get to see the mesh, the matrix, or the fifty implicit decisions that produced it.

Option two: a serious open-source stack — FEniCS, deal.II, scikit-fem with gmsh underneath. Genuinely open, genuinely capable, and also genuinely heavy: compiled mesh generators, several abstraction layers, a real learning curve before you can even see where the physics happens.

I wanted a third thing. So I set myself three rules:

  • Every formula in the code has to be traceable to something you could write on a whiteboard.
  • Every accuracy claim has to be measured, not assumed.
  • Every known limitation has to be printed by the program itself, not buried in a comment nobody reads.

Here's what building to those rules actually looked like.

The Physics, Compressed

A capacitor problem is one equation. Given some conductors at fixed voltages and a dielectric — possibly several, possibly varying in space — filling the gaps between them, the electric potential $V$ satisfies:

Potential

That's the whole physics. Everything else — field, displacement, stored energy, capacitance — falls out of V:

Physics Details

That last formula is worth a pause. The obvious way to get capacitance out of a simulation is to integrate surface charge on a conductor. This solver doesn't do that — it computes the total energy stored in the field instead and rearranges that same relationship to solve for C. Charge integration means differentiating a numerically noisy field exactly at a boundary, the single worst place to differentiate anything. Energy integration only needs a field the solver has already computed everywhere. Small choice, but the kind that separates code that runs from code that's numerically honest.

What "No Black Box" Actually Means

Every finite element method runs on the same trick: chop the domain into small pieces, assume the solution is simple on each piece, turn a differential equation into a large sparse system of linear equations.

For a triangular piece, the entire physics collapses into one formula for how strongly two corners of that triangle interact:

Grid

𝐴 is the triangle’s area. 𝑏𝑖 and 𝑐𝑖 aren’t mysterious — they’re just numbers describing the edge directly across from corner 𝑖: bigger for a longer opposite edge, and they flip sign depending on which way that edge points. Feed in three corner coordinates and a material, and out comes a small table of these numbers, one for every pair of corners. That’s the whole trick. There’s no hidden layer between this formula and the code that computes it — about twenty lines of NumPy, every one of them readable.

Assemble that over every triangle, apply boundary conditions — a conductor is just a region held at a fixed voltage — solve the sparse system, and you have 𝑉 everywhere. The same twenty lines handle a flat plate, a circular cable cross-section, or any shape you can build from simple ones combined with union, intersection, and difference. The geometry never touches the physics.

The Deliberate Trade-Off

Here's the part most "look what I built" posts skip: an honest accounting of what it costs.

Real finite element tools mesh the actual geometry — triangles that hug a curved boundary exactly, dense where the field is doing something interesting, sparse where it isn't. Building that kind of mesh is genuinely hard, and it's exactly what pulls in the heavy dependencies — a geometry kernel, mesh-quality algorithms, compiled binaries — that make FEM tooling opaque in the first place.

I made a different choice: a plain Cartesian grid, split into triangles. No mesh generator. No native dependencies — just numpy.linspace and index arithmetic. A circle on a grid like this isn't a circle. It's a staircase of grid cells that looks roughly circular from far enough away.

That's not an oversight. It's the trade, made on purpose, to keep the whole tool inside three lines of pip install. The question is whether the trade is honest about its cost — and that's where most projects stop.

Proving It's Not a Toy

Before trusting any result, I checked the solver against a case with a known exact answer: plates spanning the entire simulation width, so no fringing is even geometrically possible. Just,

Capacity

exactly, with nothing to approximate on the physics side.

The solver matched it to 0.0000% error, at every mesh resolution tested. That's the baseline: the finite element machinery has no bugs hiding in it. Whatever error shows up elsewhere comes from the mesh, not the math.

That check used a deliberately artificial setup — plates spanning the whole domain so fringing couldn’t happen — specifically so the formula would be exact and the test would be clean.
Real capacitors don’t look like that. So next I ran the two real examples, with real, finite edges — a coax cable filled with polyethylene, and a parallel plate with a glass slab across half the gap — through an actual convergence study instead of reporting one number and hoping.
The two reference values below aren’t the same kind of thing, on purpose: the coax formula is exact, so its gap is genuine mesh error. The parallel-plate formula, by contrast, is deliberately fringing-free — the classic no-fringing approximation, now extended to a glass-and-air sandwich instead of a single material — so its gap here isn’t a solver problem, it’s the real physics that formula was never built to capture.

Convergence

The coax case increases at every step, though not at a perfectly steady rate — there’s a plateau partway through, still climbing but less steeply.

The parallel-plate case looks different again — across the four resolutions actually tested, it runs 102.0 → 101.9 → 98.8 → 101.8 pF/m, dipping in the middle before recovering, rather than settling smoothly toward the table’s two endpoints.

Two things are tangled together in that dip.
One is mundane: a fixed grid can only land exactly on some target sizes, and this particular resolution happens to round the capacitor’s own gap and dielectric thickness to slightly different values than its neighbors do — a small, real change in what’s actually being simulated, not an error.

The other is more interesting: a conductor’s sharp corner carries a genuine field singularity ( (𝐸∼𝑟^−1/3, for anyone curious), and independent meshes built at different resolutions aren’t strict refinements of each other, so the usual “finer mesh, better answer” guarantee doesn’t hold between them.

I could have shown just the two endpoints and let it look like ordinary convergence. Instead the program prints the whole sweep, and flags which of the two effects is at play at each point, rather than leaving the reader to guess.

A tool that only shows you its clean case isn’t more trustworthy than one that shows you both. It’s just quieter about its limits.

What This Is — and What It Isn't

This is This is not
A from-scratch, mathematically correct FEM electrostatics solver A replacement for COMSOL, Ansys Maxwell, or a production EDA tool
Small enough to read in one sitting — about 1,000 lines, three dependencies An adaptive, conforming-mesh solver — curved boundaries are approximated, and quantifiably so
Validated against exact solutions, not just "it runs and the number looks plausible" A 3D solver, or one that ingests arbitrary CAD geometry
Honest, in its own printed output, about exactly where and why it's approximate Trying to be any of the above

If you need production-grade accuracy on complex geometry, this isn't your tool. If you want to actually see what a finite element solver is doing — every matrix entry, every approximation, every place it's wrong and by how much — this was built for exactly that.

Where It Goes From Here

The single biggest lever left on the table is the mesh itself. A few concrete next steps, in roughly the order they'd pay off:

  • Swap the Cartesian grid for a real unstructured, conforming mesh, fixing the staircased boundaries and under-resolved corners in one move — at the cost of the dependency-free simplicity that's the whole point of this version.
  • Short of that: grade the existing grid finer near sharp corners while staying dependency-free, which would tighten the parallel-plate numbers specifically without touching the coax case.
  • Add the classic extras once the mesh is solid: floating conductors, nonlinear and anisotropic dielectrics, 3D.

None of that happens silently. That's kind of the point.

The full code, the complete derivation, and both worked examples with figures are on: GitHub.

Top comments (0)