<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://allamaprabhuani.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://allamaprabhuani.github.io/" rel="alternate" type="text/html" /><updated>2026-05-17T01:44:38+00:00</updated><id>https://allamaprabhuani.github.io/feed.xml</id><title type="html">Allamaprabhu Ani</title><subtitle>Doctoral researcher in computational mechanics and deep learning at City, St George&apos;s, University of London. Building SynaCAD.</subtitle><author><name>Allamaprabhu S Ani</name><email>allamaprabhuani@gmail.com</email></author><entry><title type="html">The Annotated Phase-Field Solver</title><link href="https://allamaprabhuani.github.io/blog/2026/05/17/annotated-phase-field-solver/" rel="alternate" type="text/html" title="The Annotated Phase-Field Solver" /><published>2026-05-17T00:00:00+00:00</published><updated>2026-05-17T00:00:00+00:00</updated><id>https://allamaprabhuani.github.io/blog/2026/05/17/annotated-phase-field-solver</id><content type="html" xml:base="https://allamaprabhuani.github.io/blog/2026/05/17/annotated-phase-field-solver/"><![CDATA[<p>A phase-field fracture solver is a strange object. It is a finite-element
PDE solver, an optimisation routine, a non-smooth variational problem,
and — when you write it in PyTorch — an end-to-end differentiable
function of every input. Each of those framings is on its own a small
PhD’s worth of material.</p>

<p>This post is the version of the explainer I wish I had three years ago.
Each piece of math gets one figure, one code block, and one paragraph of
why-it-is-the-way-it-is. By the end you should understand why a phase-
field solver looks the way it does, what’s hard about turning one into a
PyTorch module, and why differentiability changes what you can do with
one.</p>

<p>The annotated walk-through format is borrowed from Sasha Rush’s
<a href="https://nlp.seas.harvard.edu/2018/04/03/attention.html"><em>Annotated Transformer</em></a>;
if you’ve never read it, go read it first — it’s the template.</p>

<h2 id="1-the-thing-we-are-modelling">1. The thing we are modelling</h2>

<p>A crack is a discontinuity in a continuum. That is the whole problem.</p>

<p>Classical fracture mechanics — Griffith, 1921 — frames crack growth as
an energy bookkeeping exercise: a crack advances when the elastic
strain energy released by its growth equals or exceeds the energy
required to create new surface. The bookkeeping is exact, but it
requires you to track a moving discontinuity. Numerically, that’s
horrible. You need remeshing, level sets, branch detection, criteria
for nucleation versus propagation, special elements at the tip — every
one of which is its own decade of research.</p>

<p>Phase-field methods sidestep the whole apparatus by <strong>replacing the
discontinuity with a smooth scalar damage field</strong> <code class="language-plaintext highlighter-rouge">d(x, t) ∈ [0, 1]</code>.
0 is intact material; 1 is fully cracked. Wherever <code class="language-plaintext highlighter-rouge">d</code> is high, the
material has effectively dissolved — its stiffness is multiplied by a
degradation function <code class="language-plaintext highlighter-rouge">g(d) = (1 - d)²</code>.</p>

<figure class="tutorial-fig">
  <img src="/assets/blog/annotated-phase-field/fig1-sharp-vs-diffuse.png" alt="Left: a sharp crack drawn as a red line through a rectangular domain. Right: the same crack represented as a smoothly-varying red damage field with width governed by a small length parameter" />
  <figcaption>(a) The sharp-crack ideal — a 1D discontinuity inside a 2D continuum. (b) The phase-field substitute — a diffuse damage band of width ~ℓ. The regularisation length ℓ is small but finite; convergence to the sharp ideal as ℓ → 0 is the central theoretical guarantee.</figcaption>
</figure>

<p>The regularisation length ℓ controls how diffuse the crack is. Smaller
ℓ → sharper representation → finer mesh required. There is a Γ-convergence
result (Ambrosio-Tortorelli, 1990; lifted to fracture by Bourdin,
Francfort and Marigo, 2000) that says the phase-field functional
converges <em>as a variational problem</em> to Griffith’s surface-energy
functional as ℓ → 0. That theorem is the reason this whole field exists.</p>

<h2 id="2-the-energy-functional-annotated">2. The energy functional, annotated</h2>

<p>The variational form is</p>

\[\mathcal{E}(u, d) = \underbrace{\int_\Omega g(d)\,\psi(\varepsilon(u))\,\mathrm{d}V}_{\text{degraded elastic energy}} + \underbrace{\frac{G_c}{c_w} \int_\Omega \left( \frac{w(d)}{\ell} + \ell\,|\nabla d|^2 \right) \mathrm{d}V}_{\text{surface (fracture) energy}}.\]

<p>Three blocks worth chewing on:</p>

<ul>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">g(d) ψ(ε)</code></strong> is the elastic strain energy, degraded where there is
damage. <code class="language-plaintext highlighter-rouge">ψ</code> is the usual quadratic strain energy density from linear
elasticity. <code class="language-plaintext highlighter-rouge">g(d) = (1 - d)²</code> is the standard choice; it has the
right <code class="language-plaintext highlighter-rouge">g(0) = 1</code>, <code class="language-plaintext highlighter-rouge">g(1) = 0</code>, and <code class="language-plaintext highlighter-rouge">g'(1) = 0</code> to avoid stress
concentration at fully-damaged points.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">w(d)/ℓ</code></strong> is the dissipation density per unit damage. The choice
<code class="language-plaintext highlighter-rouge">w(d) = d</code> gives the <strong>AT1</strong> model (compact support, an actual elastic
region before damage onset). <code class="language-plaintext highlighter-rouge">w(d) = d²</code> gives <strong>AT2</strong> (smooth,
exponential profile, no elastic limit — damage initiates the moment
load is applied). <code class="language-plaintext highlighter-rouge">c_w</code> is a normalising constant chosen so the total
surface energy of a fully-formed crack equals <code class="language-plaintext highlighter-rouge">G_c × (crack length)</code>.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">ℓ |∇d|²</code></strong> is the gradient term that <em>makes the regularisation
work</em>. Without it, <code class="language-plaintext highlighter-rouge">d</code> would degenerate to a delta function. With it,
the optimal damage profile around a crack is a smooth band of width
proportional to ℓ.</p>
  </li>
</ul>

<p>The two analytical profiles fall out of a 1D Euler-Lagrange calculation:</p>

<figure class="tutorial-fig">
  <img src="/assets/blog/annotated-phase-field/fig2-at1-at2-profiles.png" alt="Damage profiles for AT1 (compact, parabolic-like) and AT2 (exponential decay) at three values of ℓ" />
  <figcaption>Optimal 1D damage profiles around a crack. AT1: <code>d*(x) = (1 - |x|/2ℓ)²</code> for <code>|x| &lt; 2ℓ</code>, zero outside. AT2: <code>d*(x) = exp(-|x|/ℓ)</code>. Note the AT1 profile is compactly supported; outside the damaged band the material is truly intact. AT2 leaks damage everywhere.</figcaption>
</figure>

<p>Drag the slider below to feel how ℓ controls width:</p>

<div class="tutorial-interactive">
  <iframe src="/assets/blog/annotated-phase-field/profile-interactive.html" loading="lazy" title="ℓ slider for AT1 vs AT2"></iframe>
</div>

<h2 id="3-the-discrete-problem">3. The discrete problem</h2>

<p>The continuous energy is unhelpful for a computer. We discretise:</p>

<ul>
  <li>Mesh the domain. Use linear triangles or bilinear quads; nothing
fancier helps for phase-field.</li>
  <li><code class="language-plaintext highlighter-rouge">u</code> lives on the nodes as a vector field (dim × n_nodes degrees of
freedom).</li>
  <li><code class="language-plaintext highlighter-rouge">d</code> lives on the nodes as a scalar field (n_nodes DOFs).</li>
  <li>The energy becomes a function of two big vectors: <code class="language-plaintext highlighter-rouge">E(U, D)</code>.</li>
</ul>

<p>The variational principle says the solution <code class="language-plaintext highlighter-rouge">(U, D)</code> is a <em>joint
minimiser</em> of <code class="language-plaintext highlighter-rouge">E</code>. We almost never minimise jointly — the problem is
non-convex in <code class="language-plaintext highlighter-rouge">(U, D)</code> together but separately convex in each. So
practitioners use <strong>alternating minimisation</strong>:</p>

<figure class="tutorial-fig">
  <img src="/assets/blog/annotated-phase-field/fig4-alt-min-flow.png" alt="Flow diagram: initial state, solve u with d fixed, solve d with u fixed (irreversible), check convergence, advance load" />
  <figcaption>The canonical phase-field timestep. The inner loop ("solve u → solve d → check") repeats until <code>(u, d)</code> has converged for the current load. Then advance load and start over.</figcaption>
</figure>

<p>The two key constraints on the <strong>d-step</strong>:</p>

<ol>
  <li><strong>Box constraint</strong>: <code class="language-plaintext highlighter-rouge">d ∈ [0, 1]</code> pointwise.</li>
  <li><strong>Irreversibility</strong>: <code class="language-plaintext highlighter-rouge">d(t + Δt) ≥ d(t)</code> pointwise. Damage cannot heal.</li>
</ol>

<p>The second one matters enormously. The first time you skip it because
“it’s just a small Δt” your solver will silently produce thermodynamic
nonsense.</p>

<h2 id="4-the-80-line-pytorch-sketch">4. The 80-line PyTorch sketch</h2>

<p>The shortest convincing phase-field solver you can write in PyTorch is
something like the following. This is a 1D bar in tension — the dumbest
possible PF example, but everything you’d add for 2D/3D is just more
indexing.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">torch</span>

<span class="c1"># Mesh, parameters
</span><span class="n">N</span><span class="p">,</span> <span class="n">L</span> <span class="o">=</span> <span class="mi">121</span><span class="p">,</span> <span class="mf">1.0</span>
<span class="n">xs</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">linspace</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">L</span><span class="p">,</span> <span class="n">N</span><span class="p">)</span>
<span class="n">dx</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">xs</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">-</span> <span class="n">xs</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
<span class="n">E0</span><span class="p">,</span> <span class="n">Gc</span><span class="p">,</span> <span class="n">ell</span><span class="p">,</span> <span class="n">cw</span> <span class="o">=</span> <span class="mf">100.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.04</span><span class="p">,</span> <span class="mi">8</span><span class="o">/</span><span class="mi">3</span>  <span class="c1"># AT1 normaliser
</span>
<span class="c1"># State (trainable wrt energy)
</span><span class="n">u</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">zeros</span><span class="p">(</span><span class="n">N</span><span class="p">,</span> <span class="n">requires_grad</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">zeros</span><span class="p">(</span><span class="n">N</span><span class="p">,</span> <span class="n">requires_grad</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">energy</span><span class="p">(</span><span class="n">u</span><span class="p">,</span> <span class="n">d</span><span class="p">,</span> <span class="n">u_app</span><span class="p">):</span>
    <span class="c1"># Element strain (constant per cell)
</span>    <span class="n">eps</span> <span class="o">=</span> <span class="p">(</span><span class="n">u</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span> <span class="o">-</span> <span class="n">u</span><span class="p">[:</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span> <span class="o">/</span> <span class="n">dx</span>
    <span class="n">d_e</span> <span class="o">=</span> <span class="mf">0.5</span> <span class="o">*</span> <span class="p">(</span><span class="n">d</span><span class="p">[:</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="n">d</span><span class="p">[</span><span class="mi">1</span><span class="p">:])</span>         <span class="c1"># element damage
</span>    <span class="n">g</span> <span class="o">=</span> <span class="p">(</span><span class="mf">1.0</span> <span class="o">-</span> <span class="n">d_e</span><span class="p">)</span> <span class="o">**</span> <span class="mi">2</span>                  <span class="c1"># degradation
</span>    <span class="n">psi</span> <span class="o">=</span> <span class="mf">0.5</span> <span class="o">*</span> <span class="n">E0</span> <span class="o">*</span> <span class="n">eps</span> <span class="o">**</span> <span class="mi">2</span>             <span class="c1"># elastic density
</span>    <span class="n">elastic</span> <span class="o">=</span> <span class="p">(</span><span class="n">g</span> <span class="o">*</span> <span class="n">psi</span> <span class="o">*</span> <span class="n">dx</span><span class="p">).</span><span class="nb">sum</span><span class="p">()</span>

    <span class="n">grad_d</span> <span class="o">=</span> <span class="p">(</span><span class="n">d</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span> <span class="o">-</span> <span class="n">d</span><span class="p">[:</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span> <span class="o">/</span> <span class="n">dx</span>
    <span class="n">surface</span> <span class="o">=</span> <span class="p">(</span><span class="n">Gc</span> <span class="o">/</span> <span class="n">cw</span><span class="p">)</span> <span class="o">*</span> <span class="p">((</span><span class="n">d_e</span> <span class="o">/</span> <span class="n">ell</span> <span class="o">+</span> <span class="n">ell</span> <span class="o">*</span> <span class="n">grad_d</span> <span class="o">**</span> <span class="mi">2</span><span class="p">)</span> <span class="o">*</span> <span class="n">dx</span><span class="p">).</span><span class="nb">sum</span><span class="p">()</span>

    <span class="c1"># Dirichlet BC penalty (illustrative; in a real solver these
</span>    <span class="c1"># are enforced by removing rows/cols from the linear system)
</span>    <span class="n">bc</span> <span class="o">=</span> <span class="mf">1e4</span> <span class="o">*</span> <span class="p">(</span><span class="n">u</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">**</span> <span class="mi">2</span> <span class="o">+</span> <span class="p">(</span><span class="n">u</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">-</span> <span class="n">u_app</span><span class="p">)</span> <span class="o">**</span> <span class="mi">2</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">elastic</span> <span class="o">+</span> <span class="n">surface</span> <span class="o">+</span> <span class="n">bc</span>

<span class="c1"># Alternating-minimisation outer loop
</span><span class="n">d_prev</span> <span class="o">=</span> <span class="n">d</span><span class="p">.</span><span class="n">detach</span><span class="p">().</span><span class="n">clone</span><span class="p">()</span>
<span class="k">for</span> <span class="n">u_app</span> <span class="ow">in</span> <span class="n">torch</span><span class="p">.</span><span class="n">linspace</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mf">0.04</span><span class="p">,</span> <span class="mi">50</span><span class="p">):</span>
    <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">40</span><span class="p">):</span>                  <span class="c1"># u-step (fixed d)
</span>        <span class="k">if</span> <span class="n">u</span><span class="p">.</span><span class="n">grad</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span> <span class="n">u</span><span class="p">.</span><span class="n">grad</span><span class="p">.</span><span class="n">zero_</span><span class="p">()</span>
        <span class="n">E</span> <span class="o">=</span> <span class="n">energy</span><span class="p">(</span><span class="n">u</span><span class="p">,</span> <span class="n">d</span><span class="p">.</span><span class="n">detach</span><span class="p">(),</span> <span class="n">u_app</span><span class="p">)</span>
        <span class="n">E</span><span class="p">.</span><span class="n">backward</span><span class="p">()</span>
        <span class="k">with</span> <span class="n">torch</span><span class="p">.</span><span class="n">no_grad</span><span class="p">():</span> <span class="n">u</span> <span class="o">-=</span> <span class="mf">1e-4</span> <span class="o">*</span> <span class="n">u</span><span class="p">.</span><span class="n">grad</span>

    <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">40</span><span class="p">):</span>                  <span class="c1"># d-step (fixed u, irreversibility)
</span>        <span class="k">if</span> <span class="n">d</span><span class="p">.</span><span class="n">grad</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span> <span class="n">d</span><span class="p">.</span><span class="n">grad</span><span class="p">.</span><span class="n">zero_</span><span class="p">()</span>
        <span class="n">E</span> <span class="o">=</span> <span class="n">energy</span><span class="p">(</span><span class="n">u</span><span class="p">.</span><span class="n">detach</span><span class="p">(),</span> <span class="n">d</span><span class="p">,</span> <span class="n">u_app</span><span class="p">)</span>
        <span class="n">E</span><span class="p">.</span><span class="n">backward</span><span class="p">()</span>
        <span class="k">with</span> <span class="n">torch</span><span class="p">.</span><span class="n">no_grad</span><span class="p">():</span>
            <span class="n">d_new</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">clamp</span><span class="p">(</span><span class="n">d</span> <span class="o">-</span> <span class="mf">0.05</span> <span class="o">*</span> <span class="n">d</span><span class="p">.</span><span class="n">grad</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">)</span>
            <span class="n">d_new</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">maximum</span><span class="p">(</span><span class="n">d_new</span><span class="p">,</span> <span class="n">d_prev</span><span class="p">)</span>  <span class="c1"># irreversibility
</span>            <span class="n">d</span><span class="p">.</span><span class="n">copy_</span><span class="p">(</span><span class="n">d_new</span><span class="p">)</span>
    <span class="n">d_prev</span> <span class="o">=</span> <span class="n">d</span><span class="p">.</span><span class="n">detach</span><span class="p">().</span><span class="n">clone</span><span class="p">()</span>
</code></pre></div></div>

<p>That’s it. Two coupled minimisations, an irreversibility projection, a
load loop. Real production solvers — Akantu’s phase-field module,
PhaFiDyn, Bourdin’s original FreeFEM++ codes, PRACE-scale MPI codes —
are doing exactly this with better linear solvers, smarter convergence
criteria, and adaptive meshing. The math is unchanged.</p>

<p>The toy version above produces this:</p>

<figure class="tutorial-fig">
  <img src="/assets/blog/annotated-phase-field/fig3-1d-tension.png" alt="Left: force-displacement curve showing the expected brittle softening. Right: damage localising into a band of width ℓ as load increases" />
  <figcaption>1D bar pulled in tension. Left: the nominal reaction force vs applied displacement — the textbook softening past the peak as damage localises. Right: damage profile at four load steps; it sharpens into a band roughly 2ℓ wide, which is the AT1 prediction.</figcaption>
</figure>

<h2 id="5-whats-hard-about-putting-this-in-pytorch">5. What’s hard about putting this in PyTorch</h2>

<p>This is where most people get hurt. A short, opinionated list.</p>

<p><strong>Higher-order derivatives matter.</strong> The d-step gradient passes through
<code class="language-plaintext highlighter-rouge">(1 - d)² ψ(u)</code>, and the u-step gradient passes through
<code class="language-plaintext highlighter-rouge">(1 - d)² ψ(u)</code> again. Both are smooth. But if you ever introduce a
<strong>spectral split</strong> (Miehe et al., 2010) to prevent crack-closure in
compression, you have <code class="language-plaintext highlighter-rouge">max(0, ε⁺) · max(0, ε⁻)</code> terms whose derivatives
are well-defined almost everywhere but pathological at the eigenvalue
crossings. Naïve autograd produces gradients that look fine numerically
but make Newton iterations stall. This is the single biggest pitfall.</p>

<p><strong>Irreversibility breaks the variational structure.</strong> The constraint
<code class="language-plaintext highlighter-rouge">d(t+Δt) ≥ d(t)</code> is a hard inequality, not part of the energy. You
enforce it either via projection (what the 80-line sketch above does)
or via a history variable (Miehe, 2010, which converts the inequality
into an extra term in the energy). The projection approach is simpler
but biases the gradient — the cells where the projection is active
have zero descent direction for further damage growth, so your line
search sees a kink. Production codes use projected-gradient or
active-set methods.</p>

<p><strong>Operator-split instability under explicit dynamics.</strong> For dynamic
fracture you advance <code class="language-plaintext highlighter-rouge">(u, d)</code> via a Verlet-style explicit scheme. The
CFL condition is <em>not</em> just the elastic CFL — it tightens dramatically
as damage approaches 1. If you ignore this (because your elastic CFL
felt safe) your damage field gets gradient blow-up of order 10^96 and
the solver eats memory until OOM. Ask me how I know.</p>

<p><strong><code class="language-plaintext highlighter-rouge">requires_grad</code> semantics inside a load loop.</strong> PyTorch’s tape grows
unboundedly if you keep reusing the same <code class="language-plaintext highlighter-rouge">Parameter</code>s without detaching
between load steps. For a 10⁵-node mesh and 10³ load steps, you’ll
exhaust GPU memory before you reach crack initiation. The fix is to
<code class="language-plaintext highlighter-rouge">.detach()</code> between load steps and reattach inside each minimisation,
which obviously breaks differentiability across load steps — which is
<em>exactly the thing</em> you wanted for the inverse-problem story below.
Resolving that tension is the subject of a whole sub-literature
(<a href="https://arxiv.org/abs/2504.02260">Akhare et al., 2025</a> is the
state-of-the-art for stable implicit fixed-point regimes).</p>

<h2 id="6-what-differentiability-buys-you">6. What differentiability buys you</h2>

<p>The PyTorch sketch in §4 has a property the legacy solvers don’t:
<strong>every output is a differentiable function of every input.</strong> That
includes the irreversibility projection, the load loop, and the
alternating min — autograd will carry the gradient back through all of
it, as long as you avoid the four pitfalls in §5.</p>

<p>That single property is the door. Once it’s open, the material
parameters in the equation (<code class="language-plaintext highlighter-rouge">G_c</code>, <code class="language-plaintext highlighter-rouge">E</code>, ℓ) stop being constants and
become <em>trainable</em>. Pick any experimental measurement of a real
cracking specimen — a load-displacement curve from an Instron, a
digital-image-correlation displacement field, a crack trajectory from a
high-speed camera — and you can recover the material parameters that
<em>would have produced</em> that measurement:</p>

<figure class="tutorial-fig">
  <img src="/assets/blog/annotated-phase-field/fig5-gc-recovery.png" alt="Recovered Gc estimate converging to the true value across outer iterations" />
  <figcaption>Outer-loop convergence of <code>G_c</code> recovered by backpropagating a load-curve mismatch through a phase-field simulation. The forward simulation is differentiable; the gradient comes back through every alternating-min iteration and tells the outer optimiser which direction to nudge <code>G_c</code>.</figcaption>
</figure>

<p>The same simulation read two ways: a fracture mechanician calls it a
<em>forward solver</em> (geometry + material → load curve); an applied-ML
researcher calls it an <em>inverse problem</em> (load curve → material).
Differentiability is what lets one piece of code be both.</p>

<p>The same property unlocks three other things people are actually
shipping today:</p>

<ul>
  <li><strong>Gradient-based topology optimisation</strong> of toughened structures —
design a part to maximise fracture resistance, not just stiffness.</li>
  <li><strong>Hybrid neural acceleration</strong> — train a small network to predict
the next damage increment, with the physics solver as a verifier
(rejection sampling on residual norm).</li>
  <li><strong>Differentiable digital twins</strong> — assimilate live sensor data into
a phase-field simulation by gradient descent on initial conditions.</li>
</ul>

<p>None of these are speculative. People are shipping all three.</p>

<h2 id="7-whats-actually-hard-and-where-the-field-is-going">7. What’s actually hard (and where the field is going)</h2>

<p>A short editorial.</p>

<ul>
  <li>
    <p><strong>Benchmarks.</strong> PINN / neural-operator literature has Burgers’,
Navier-Stokes, Darcy. <em>Fracture has no canonical benchmark suite of
the same rigor.</em> The closest things are the Borden 2012 dynamic
branching cases and the Kalthoff impact test, both of which require
significant interpretation to compare against. Building a
GPQA-equivalent for fracture is genuinely open.</p>
  </li>
  <li>
    <p><strong>Neural operators on PF data.</strong> Once you have a differentiable
solver, generating large datasets is cheap. The hard part is the
representation: damage fields are sparse and localised, which kills
the spectral inductive bias of an FNO. Equivariant operators or
graph-based architectures look more promising. See Mishra’s CIRM
<a href="https://www.youtube.com/watch?v=5CnctvgyssU">neural-operator lecture series</a>
for the theory.</p>
  </li>
  <li>
    <p><strong>Implicit-step differentiability.</strong> The state of the art for
differentiable <em>implicit</em> PDE solvers is implicit-function-theorem
adjoints
(<a href="https://arxiv.org/abs/2504.02260">Akhare 2025, Im-PiNDiff</a>). For
<em>explicit</em> time-stepping with damage coupling — the dominant regime
in dynamic fracture — gradient stability is still an open problem,
and is the main thrust of my own thesis work.</p>
  </li>
  <li>
    <p><strong>Hybrid causation vs correlation.</strong> Neural surrogates that learn
the next-step damage map are seductive but trivially overfit to a
single load history. Until benchmarks include held-out <em>out-of-
distribution</em> loadings, the literature will continue to claim 100×
speedups that don’t generalise.</p>
  </li>
</ul>

<h2 id="8-where-to-go-from-here">8. Where to go from here</h2>

<p>If you want to build one of these yourself:</p>

<ul>
  <li><strong>Read the canon.</strong> Bourdin, Francfort &amp; Marigo (2000), “Numerical
experiments in revisited brittle fracture”,
<a href="https://doi.org/10.1016/S0022-5096(99)00028-9">J. Mech. Phys. Solids 48(4)</a>.
Miehe, Welschinger &amp; Hofacker (2010),
<a href="https://doi.org/10.1002/nme.2861">Int. J. Numer. Meth. Engng 83(10)</a>.
These are the two papers that make every subsequent paper
intelligible.</li>
  <li><strong>Reference codes.</strong> Bourdin’s
<a href="https://bitbucket.org/bourdin/mef90">FreeFEM++ phase-field repo</a>
is the canonical implementation; Akantu has a maintained C++ phase-field
module; <a href="https://github.com/Vinh-Tran/PhaFiDyn">PhaFiDyn</a> is a
validated explicit-dynamics FEniCS code worth reading end-to-end.</li>
  <li><strong>My own work</strong> — <a href="https://github.com/allamaprabhuani/torch_pf_solver"><code class="language-plaintext highlighter-rouge">torch_pf_solver</code></a>
is a GPU-native, matrix-free phase-field solver in pure PyTorch with
full adjoint + autograd + checkpoint differentiability. Currently
private (it’s tied to in-flight thesis chapters); I’m carving out a
public adjoint-demo slice — the inverse-problem cartoon above will
be a runnable notebook in that repo.</li>
</ul>

<p>If you want to use one without building it: pick Akantu (C++, fast,
limited Python interface), or write a thin wrapper around the
80-line sketch above and add your physics there. The barrier to entry
is much lower than the literature suggests.</p>

<p>If you read this far and have feedback or want to discuss differentiable
solvers more generally, my email is in the
<a href="/#elsewhere">site footer</a>. I am especially interested in hearing about
benchmark cases that should exist but don’t.</p>

<hr />

<h2 id="references">References</h2>

<ol>
  <li>Griffith, A. A. (1921). <strong>The phenomena of rupture and flow in solids.</strong> <em>Philosophical Transactions of the Royal Society A</em> 221(582–593), 163–198. <a href="https://doi.org/10.1098/rsta.1921.0006">doi:10.1098/rsta.1921.0006</a></li>
  <li>Francfort, G. A., &amp; Marigo, J.-J. (1998). <strong>Revisiting brittle fracture as an energy minimization problem.</strong> <em>Journal of the Mechanics and Physics of Solids</em> 46(8), 1319–1342. <a href="https://doi.org/10.1016/S0022-5096\(98\)00034-9">doi:10.1016/S0022-5096(98)00034-9</a></li>
  <li>Bourdin, B., Francfort, G. A., &amp; Marigo, J.-J. (2000). <strong>Numerical experiments in revisited brittle fracture.</strong> <em>Journal of the Mechanics and Physics of Solids</em> 48(4), 797–826. <a href="https://doi.org/10.1016/S0022-5096\(99\)00028-9">doi:10.1016/S0022-5096(99)00028-9</a> — the original phase-field-for-fracture paper.</li>
  <li>Ambrosio, L., &amp; Tortorelli, V. M. (1990). <strong>Approximation of functionals depending on jumps by elliptic functionals via Γ-convergence.</strong> <em>Communications on Pure and Applied Mathematics</em> 43(8), 999–1036. <a href="https://doi.org/10.1002/cpa.3160430805">doi:10.1002/cpa.3160430805</a> — the Γ-convergence guarantee that makes the whole field rigorous.</li>
  <li>Miehe, C., Welschinger, F., &amp; Hofacker, M. (2010). <strong>Thermodynamically consistent phase-field models of fracture: Variational principles and multi-field FE implementations.</strong> <em>International Journal for Numerical Methods in Engineering</em> 83(10), 1273–1311. <a href="https://doi.org/10.1002/nme.2861">doi:10.1002/nme.2861</a> — the staggered scheme + history-variable formulation cited in §5.</li>
  <li>Borden, M. J., Verhoosel, C. V., Scott, M. A., Hughes, T. J. R., &amp; Landis, C. M. (2012). <strong>A phase-field description of dynamic brittle fracture.</strong> <em>Computer Methods in Applied Mechanics and Engineering</em> 217–220, 77–95. <a href="https://doi.org/10.1016/j.cma.2012.01.008">doi:10.1016/j.cma.2012.01.008</a> — the dynamic-branching benchmark cases.</li>
  <li>Pham, K., Amor, H., Marigo, J.-J., &amp; Maurini, C. (2011). <strong>Gradient damage models and their use to approximate brittle fracture.</strong> <em>International Journal of Damage Mechanics</em> 20(4), 618–652. <a href="https://doi.org/10.1177/1056789510386852">doi:10.1177/1056789510386852</a> — the AT1 vs AT2 distinction.</li>
  <li>Akhare, D., Luo, T., &amp; Wang, J.-X. (2025). <strong>Im-PiNDiff: Implicit physics-informed neural differentiable solver for stiff temporal systems.</strong> <a href="https://arxiv.org/abs/2504.02260">arXiv:2504.02260</a> — the implicit-step differentiability state-of-the-art cited in §7.</li>
  <li>Lu, L., Jin, P., Pang, G., Zhang, Z., &amp; Karniadakis, G. E. (2021). <strong>Learning nonlinear operators via DeepONet.</strong> <em>Nature Machine Intelligence</em> 3, 218–229. <a href="https://arxiv.org/abs/1910.03193">arXiv:1910.03193</a></li>
  <li>Li, Z., Kovachki, N., Azizzadenesheli, K., et al. (2021). <strong>Fourier neural operator for parametric partial differential equations.</strong> <em>ICLR 2021</em>. <a href="https://arxiv.org/abs/2010.08895">arXiv:2010.08895</a></li>
  <li>Mishra, S. (2024). <strong>Learning operators — Lecture 1, CIRM Marseille.</strong> <a href="https://www.youtube.com/watch?v=5CnctvgyssU">YouTube</a></li>
  <li>Rush, A. M. (2018). <strong>The Annotated Transformer.</strong> Harvard NLP. <a href="https://nlp.seas.harvard.edu/2018/04/03/attention.html">nlp.seas.harvard.edu</a> — the explainer format borrowed for this post.</li>
</ol>

<hr />

<p><em>Related: <a href="/tutorials/05-pinns/">PINNs tutorial</a> — the data-vs-physics
warm-up that this post extends. Cross-posted from the
<a href="/tutorials/">Tutorials series</a>.</em></p>]]></content><author><name>Allamaprabhu S Ani</name><email>allamaprabhuani@gmail.com</email></author><category term="phase-field" /><category term="fracture" /><category term="scientific-ml" /><category term="autograd" /><category term="pytorch" /><summary type="html"><![CDATA[A phase-field fracture solver is a strange object. It is a finite-element PDE solver, an optimisation routine, a non-smooth variational problem, and — when you write it in PyTorch — an end-to-end differentiable function of every input. Each of those framings is on its own a small PhD’s worth of material.]]></summary></entry><entry><title type="html">hello, world</title><link href="https://allamaprabhuani.github.io/blog/2026/04/30/hello-world/" rel="alternate" type="text/html" title="hello, world" /><published>2026-04-30T00:00:00+00:00</published><updated>2026-04-30T00:00:00+00:00</updated><id>https://allamaprabhuani.github.io/blog/2026/04/30/hello-world</id><content type="html" xml:base="https://allamaprabhuani.github.io/blog/2026/04/30/hello-world/"><![CDATA[<p>This is the first post on the blog.</p>

<p>The homepage is the <strong>scrapbook</strong> — a snapshot of who I am and what I’m building right now.
This is the <strong>work-log</strong> — longer-form notes, the why behind specific decisions, working code,
half-finished thoughts I want to come back to.</p>

<p>A few topics I expect to write about:</p>

<ul>
  <li>the actual benchmark methodology behind the 8× phase-field speedup</li>
  <li>what worked and what didn’t when wiring <a href="https://allamaprabhuani.github.io/synacad/">SynaCAD</a>’s solver layer to the LLM</li>
  <li>short Bruhn / Niu / ESDU walk-throughs as I revisit them</li>
  <li>occasional notes on Hindustani classical music, when a raga and a problem rhyme</li>
</ul>

<p>Posts go in <code class="language-plaintext highlighter-rouge">_posts/YYYY-MM-DD-slug.md</code>. Markdown, Jekyll handles the build.</p>]]></content><author><name>Allamaprabhu S Ani</name><email>allamaprabhuani@gmail.com</email></author><category term="meta" /><summary type="html"><![CDATA[This is the first post on the blog.]]></summary></entry><entry><title type="html">SynaCAD: the synapse, and what it’s for</title><link href="https://allamaprabhuani.github.io/blog/2026/04/30/synapse-cad-vision/" rel="alternate" type="text/html" title="SynaCAD: the synapse, and what it’s for" /><published>2026-04-30T00:00:00+00:00</published><updated>2026-04-30T00:00:00+00:00</updated><id>https://allamaprabhuani.github.io/blog/2026/04/30/synapse-cad-vision</id><content type="html" xml:base="https://allamaprabhuani.github.io/blog/2026/04/30/synapse-cad-vision/"><![CDATA[<div class="word-reel" role="img" aria-label="A CAD that has cognition, intelligence, logic, memory, intent, structure, judgement, and taste.">
  <span class="reel-label">a CAD that has</span>
  <span class="reel-track">
    <span class="reel-word">cognition</span>
    <span class="reel-word">intelligence</span>
    <span class="reel-word">logic</span>
    <span class="reel-word">memory</span>
    <span class="reel-word">intent</span>
    <span class="reel-word">structure</span>
    <span class="reel-word">judgement</span>
    <span class="reel-word">taste</span>
    <span class="reel-word">cognition</span>
  </span>
</div>

<h2 id="why-synapse">Why “Synapse”?</h2>

<p>A synapse is the place where a signal becomes a decision. A neuron alone doesn’t think; the <em>connection</em>
does. SynaCAD (“Synapse CAD”) is the connection between two things that have rarely talked to each other:
the <strong>classical mechanics canon</strong> (Bruhn, Niu, ESDU, Lekhnitskii — a hundred years of structural reasoning)
and <strong>modern generative models</strong> (LLMs that can read your sketch, your photo, your sentence).</p>

<p>It is not “ChatGPT for CAD.” It is a working engineer with a textbook open on its lap.</p>

<h2 id="the-vision">The vision</h2>

<blockquote>
  <p>Anyone, anywhere, designing real engineered components from a sentence — and getting back a part that
a real machine shop can hold tolerances on, with citations from real handbooks for every number.</p>
</blockquote>

<h2 id="what-it-actually-does">What it actually does</h2>

<div class="flow-diagram" aria-hidden="true">
  <div class="flow-step">
    <div class="flow-icon flow-in">✎</div>
    <p class="flow-label">describe</p>
    <p class="flow-sub">words · sketch · photo · CAD</p>
  </div>
  <div class="flow-arrow">→</div>
  <div class="flow-step">
    <div class="flow-icon flow-mid"><span class="flow-pulse"></span>∫</div>
    <p class="flow-label">solve</p>
    <p class="flow-sub">validated solvers, citations, mechanics</p>
  </div>
  <div class="flow-arrow">→</div>
  <div class="flow-step">
    <div class="flow-icon flow-out">⎙</div>
    <p class="flow-label">deliver</p>
    <p class="flow-sub">drawing · GD&amp;T · g-code · report</p>
  </div>
</div>

<h2 id="the-non-negotiable">The non-negotiable</h2>

<p><strong>Every number SynaCAD outputs is computed by a validated solver.</strong> Not invented by the language model,
not interpolated from training data, not vibes. The LLM understands your <em>intent</em>, picks the <em>right
classical method</em>, and writes the report. The numbers come from a function that, given the same input,
returns the same output — and whose source you can read.</p>

<p>This is the line between “interesting demo” and “thing you can put your name on a drawing for.”</p>

<h2 id="design-decisions-worth-naming">Design decisions worth naming</h2>

<ol>
  <li><strong>Solver-first, LLM-second.</strong> The model decides which calculation to run; the calculation runs in
plain Python. No tool call, no hallucinated stress.</li>
  <li><strong>Citations for every number.</strong> Every result links back to a page in Bruhn / Niu / ESDU or to a
peer-reviewed paper. If we can’t cite it, we don’t ship it.</li>
  <li><strong>Open by default.</strong> The bolted-joint solver is already MIT-licensed. The phase-field solver goes
public the moment the paper hits arXiv. SynaCAD’s classical-tools layer follows.</li>
  <li><strong>Manufacturing-aware from day one.</strong> Drawings come out with GD&amp;T that’s actually holdable on a
real shop floor. We are not trying to impress a CAD reviewer; we are trying to make a real part.</li>
  <li><strong>No mystery in the loop.</strong> A junior engineer should be able to read SynaCAD’s report and learn
<em>why</em> the part is sized the way it is, not just trust the answer.</li>
</ol>

<h2 id="where-were-at">Where we’re at</h2>

<p>In active development. Currently pitching for the
<a href="https://allamaprabhuani.github.io/synacad/">O’Shaughnessy Fellowship 2026</a>. Public alpha planned for 2026.</p>

<p>If this resonates — collaborators, manufacturers, advisors, students who want to test it on their own
parts — <a href="/#elsewhere">get in touch</a>.</p>

<hr />

<p class="muted">More about SynaCAD &rarr; <a href="https://allamaprabhuani.github.io/synacad/">the project site</a>.</p>]]></content><author><name>Allamaprabhu S Ani</name><email>allamaprabhuani@gmail.com</email></author><category term="synacad" /><category term="vision" /><category term="mechanics" /><category term="ai" /><summary type="html"><![CDATA[a CAD that has cognition intelligence logic memory intent structure judgement taste cognition]]></summary></entry></feed>