Brakke’s samples: soap films and foams

Soap films spanning wires, a bubble, and foams in a periodic box, from the sample datafiles that come with Surface Evolver (pyse.examples). The helpers are in docs/brakke_helpers.py.

Each sample is run twice from its datafile: once with the recipe Brakke wrote into it (its gogo command, or the usage its comments describe), once with a default ev.relax() given as many refinements as the recipe does (levels). The table shows the energy each ends with, the residual (how far from equilibrium, see Relaxing a surface), the facet count, the time, and what relax() reports: converged, stable, and any health issues. A lower energy on the same mesh is not always the better answer: a coarse mesh can lower its energy in ways the smooth surface can’t, so where an exact value is known, the error against it is the measure. Times are from the documentation build and vary.

This gallery assumes Getting started and Relaxing a surface; its pages can be read in any order.

import numpy as np
import pyvista as pv
import pysurfaceevolver as pyse
import brakke_helpers as bh

pv.set_jupyter_backend("static")

cube.fe: a cube becoming a sphere

A unit cube of fixed volume 1, its surface area the energy: it relaxes toward a sphere (area \((36\pi)^{1/3} \approx 4.836\)). The recipe refines twice with gradient and Newton steps in between.

print(bh.recipe("cube.fe"))
runs = bh.compare("cube.fe")
bh.table(runs, reference=(36*np.pi)**(1/3))
bh.show(runs)
g 5; r; g 5; hessian; r; g 5; hessian;
energyresidualfacetssecondsconvergedstablenotes
recipe: g 5; r; g 5; hessian; r; g 5; hessian;4.8544598155.6e-043840.00
relax(levels=2)4.8523444162.8e-093840.02yesyes
energyerrorresidualfacetssecondsconvergedstablenotes
recipe: g 5; r; g 5; hessian; r; g 5; hessian;4.854459815+3.82e-035.6e-043840.00
relax(levels=2)4.852344416+3.38e-032.8e-093840.02yesyes
_images/74a223f0e3e421943bca23c30e99c49e34ef1a4476c38d2ec6956db14908389d.png

Both meshes are polyhedra inscribed in the sphere, with a little more area than it; relax() takes its residual to about 1e-9 where the recipe stops sooner, at nearly the same energy.

catman.fe: the catenoid of the manual’s tutorial

A film between two rings of radius 1 at height \(\pm 0.55\); the stable catenoid has area 6.511674. The recipe’s comments say what happens on this coarse mesh: after 50 steps “at this point have nearly a saddle point”, then “triangulation twists around to lower energy”. Twisting lowers the area of the coarse mesh, not its error.

print(bh.recipe("catman.fe"))
runs = bh.compare("catman.fe")
bh.table(runs, reference=6.511674)
bh.show(runs)
g; u; r; g 50; g 200;
energyresidualfacetssecondsconvergedstablenotes
recipe: g; u; r; g 50; g 200;6.4337031674.9e-02960.01
relax(levels=1)6.4586654382.0e-10960.01yesyes
energyerrorresidualfacetssecondsconvergedstablenotes
recipe: g; u; r; g 50; g 200;6.433703167-1.20e-024.9e-02960.01
relax(levels=1)6.458665438-8.14e-032.0e-10960.01yesyes
_images/54bba0d599af4d60ffb43d29451b4855d3352fa9780785f8380df152c804a530.png

Both are below the smooth catenoid (the rings are coarse polygons); the twisted mesh of the recipe is further below. relax() stops at the untwisted equilibrium, and leaves sliding along the wires (where the twist happens) to its gradient rounds.

cat.fe: a catenoid past its limit

Here the rings are as far apart as a catenoid can span (RMAX set at the fold). The neck collapses, and the recipe deletes the tiny edges there (t .05) and pops the pinched vertex (o): the film becomes two flat discs. relax() can’t converge the film, so as a last resort it takes gradient steps without tidying, lets the neck collapse, deletes the collapsed edges and pops (collapsed, popped).

print(bh.recipe("cat.fe"))
runs = bh.compare("cat.fe")
R = 1.5088795                       # RMAX: the rings' radius
discs = 2*3*R**2                    # two 12-gons inscribed in the rings (after one refinement)
bh.table(runs, reference=discs)
bh.show(runs)
r; u; g 120; t .05; o; g 5;
energyresidualfacetssecondsconvergedstablenotes
recipe: r; u; g 120; t .05; o; g 5;13.712879135.7e-01720.01
relax(levels=1), 1 popped, 9 collapsed edges13.660304070.0e+00780.05yesyes
energyerrorresidualfacetssecondsconvergedstablenotes
recipe: r; u; g 120; t .05; o; g 5;13.71287913+3.85e-035.7e-01720.01
relax(levels=1), 1 popped, 9 collapsed edges13.66030407-4.34e-100.0e+00780.05yesyes
_images/3ca5eb85202a7e9b77c64a95708dff5f6733eb12162244fbe014c5049e03a5fa.png

The recipe stops after g 5 on the discs; relax() finishes them: two regular 12-gons, the polygons the rings’ vertices make after one refinement.

catbody.fe: a catenoid enclosing a fixed volume

The same catenoid shape, now closed by plane ends and holding a fixed volume, so it has a pressure. The file adjusts that volume to find zero pressure (the unstable catenoid) in a separate script; here only its gogo.

print(bh.recipe("catbody.fe"))
runs = bh.compare("catbody.fe")
bh.show(runs)
u; r; g 5; r; g 5; r; g 5; hessian; hessian
energyresidualfacetssecondsconvergedstablenotes
recipe: u; r; g 5; r; g 5; r; g 5; hessian; hessian6.9919138755.8e-0415360.01
relax(levels=3)6.994408381.1e-1015360.27yesyes
_images/be35c04c5388234d5f4ac3a9e03b664956f50ca1d0afc80318b43547d647d2f8.png

octa.fe: a film in an octahedral frame

A symmetric film from the edges of an octahedral wire frame to its centre: six edges there carry four films each. Plateau’s laws (soap films meet three at a time along an edge, at 120 degrees, and such edges four at a time at a point) say that is unstable: splitting the junction lowers the area. The recipe pops junctions (o), deletes small edges (t .1) and iterates, several times; relax() pops after converging, relaxes again, and deletes the films that shrink away.

print(bh.recipe("octa.fe"))
runs = bh.compare("octa.fe")
bh.show(runs)
o; U; g 20; t .1; g 10; o; g 20; u; V; t .1; o; g 12; o; u; V; u; V; g 10; t .1; o; V; g 10; t .1; u; V; g 10; t .1; g 10;
energyresidualfacetssecondsconvergedstablenotes
recipe: o; U; g 20; t .1; g 10; o; g 20; u; V; t .1; o; g 12; o; u; V; u; V; g 10; t .1; o; V; g 10; t .1; u; V; g 10; t .1; g 10;22.6274171.5e-06900.00
relax(levels=0), 11 popped, 4 collapsed edges22.6274176.0e-14900.04yesyes
_images/b831ecd8c143ba3abd4537b5eb1e7ec260e44c440c5c5f9aefc91c391bfa8765.png

Both reach the same film, with a central tetrahedral point.

Kelvin against Weaire-Phelan: twointor.fe and phelanc.fe

Lord Kelvin asked in 1887 how to divide space into cells of equal volume with the least interface area, and proposed the cell of twointor.fe (two cells in a periodic box). In 1993 Weaire and Phelan found a structure with two kinds of cells (phelanc.fe, eight cells) with about 0.3% less area. Per cell, the area over the cell volume to the power 2/3 compares them independently of the box.

kelvin = bh.compare("twointor.fe")
wp = bh.compare("phelanc.fe")

def per_cell(name, levels):
    ev = pyse.Evolver(name)
    ev.relax(levels=levels)
    n = len(ev.bodies().volume)
    V = float(np.sum(ev.bodies().volume))
    return ev.total_energy/n/(V/n)**(2/3)

k, w = per_cell("twointor.fe", 2), per_cell("phelanc.fe", 2)
print(f"area per cell / volume^(2/3): Kelvin {k:.5f}, Weaire-Phelan {w:.5f}, "
      f"{1 - w/k:.2%} less")
energyresidualfacetssecondsconvergedstablenotes
recipe: g 5; r; g 5; hessian; r; g 5; hessian; hessian;3.3431530753.8e-1011520.03
relax(levels=2)3.3431520783.6e-1111520.13yesyes
energyresidualfacetssecondsconvergedstablenotes
recipe: g 5; r; g 5; hessian; r; g 5; hessian;21.153961871.8e-0544160.11
relax(levels=2)21.153921093.9e-1344160.58yesyes
area per cell / volume^(2/3): Kelvin 2.65346, Weaire-Phelan 2.64424, 0.35% less
bh.show(wp)
_images/401506dedceeddc474a47b14be8195b2e6125e38408011aaba0531fbefc1b79b.png

symtest.fe: the Kelvin foam in a quotient space

The same two Kelvin cells, but in Evolver’s quotient-space representation (a symmetry group instead of the periodic box). The recipe is twointor.fe’s.

runs = bh.compare("symtest.fe", bh.recipe("twointor.fe"))
energyresidualfacetssecondsconvergedstablenotes
recipe: g 5; r; g 5; hessian; r; g 5; hessian; hessian;3.3431530753.8e-1011520.01
relax(levels=2)3.3431520773.6e-1111520.09yesnounstable: 1 direction(s) lower the energy

The energy is that of twointor.fe, but relax() reports the surface unstable, with one negative Hessian eigenvalue (as Brakke’s Evolver 2.70a does), and twointor.fe stable. The instability is real: the volumes in this file’s bodies section are comments (// volume 0.500), so its two cells have no volume constraint, and one can grow at the expense of the other. Moving along that mode (Evolver’s saddle) lowers the energy, and relaxing from there shrinks one cell away.

ev = pyse.Evolver("symtest.fe")
print(ev.bodies().fixed)                    # no volume constraints
ev.relax(levels=1)
e0 = ev.total_energy
ev.saddle()                                 # a step along the negative mode
print(f"energy {e0:.6f} -> {ev.total_energy:.6f}")
[False False]
energy 3.344298 -> 3.330655
/opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/site-packages/pysurfaceevolver/_relax.py:235: UnstableEquilibriumWarning: relax() converged to an unstable equilibrium: 1 direction(s) lower the energy (lowest eigenvalue -2.65)
  return self._relax(tol=tol, max_iter=max_iter, levels=levels, newton=newton,

Triply periodic minimal surfaces: pcell.fe, dcell.fe, IWP.fe

From Brakke’s collection of periodic minimal surfaces online (included with pySE): Schwarz’s P and D surfaces and Schoen’s I-WP, each a small piece of the surface in a quotient space (a symmetry group generates the rest).

for name in ("pcell.fe", "dcell.fe", "IWP.fe"):
    runs = bh.compare(name)
energyresidualfacetssecondsconvergedstablenotes
recipe: g 5; r; g 5; r; g 5; r; g 10; u; refine edge where on_constraint 2; refine edge where on_constraint 4; hessian; hessian;0.19550958251.1e-062720.01
relax(levels=3)0.19551190361.4e-102560.03yesyes
energyresidualfacetssecondsconvergedstablenotes
recipe: g 5; r; g 5; r; g 5; r; g 10; r; g 10; hessian; hessian;0.31992602481.7e-102560.03
relax(levels=4)0.31992422457.4e-122560.06yesyes
energyresidualfacetssecondsconvergedstablenotes
recipe: r; u; g 5; refine edge where valence==1; u; V; g 5; r; g 12; u; V; convert_to_quantities; hessian; hessian; r; hessian; hessian;0.28894512283.0e-053840.01
relax(levels=3)0.28911081422.1e-112560.05yesyes

relax() agrees with the recipes of P and D to about 1e-5. On I-WP the recipe refines the edges along the boundary once more and stops unconverged, so the two end on different meshes.

Next

Drops against walls, gravity and solder: Brakke’s samples: drops and capillarity. The tutorial builds a double bubble from arrays instead of a datafile.