Skip to content

Commit 01351a3

Browse files
authored
Merge pull request #106 from underworldcode/feature/nitsche-bc
Add Nitsche free-slip boundary condition for Stokes solver
2 parents 72ece1f + babaecf commit 01351a3

3 files changed

Lines changed: 623 additions & 71 deletions

File tree

docs/advanced/curved-boundary-conditions.md

Lines changed: 100 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,47 @@ This can cause significant errors in free-slip boundary conditions.
3333

3434
---
3535

36-
## Three Approaches
36+
## Four Approaches
3737

38-
### 1. Raw `mesh.Gamma` (Simplest)
38+
### 1. Nitsche Free-Slip (Recommended)
3939

40-
Use the mesh-derived normals directly:
40+
Nitsche's method provides a variationally consistent alternative to penalty
41+
that is insensitive to the penalty magnitude and gives optimal convergence:
42+
43+
```python
44+
stokes.add_nitsche_bc("Upper", gamma=10)
45+
```
46+
47+
The method automatically constructs penalty, consistency (stress flux),
48+
symmetry, and pressure coupling terms. The `gamma` parameter is dimensionless
49+
and mesh-independent — `gamma=10` works for P2 elements regardless of
50+
resolution or viscosity.
51+
52+
**Prescribed normal velocity:**
53+
```python
54+
stokes.add_nitsche_bc("Inlet", g=1.0, gamma=10)
55+
```
56+
57+
**Custom constraint direction** (e.g., fault normal different from surface normal):
58+
```python
59+
fault_normal = sympy.Matrix([0.6, 0.8])
60+
stokes.add_nitsche_bc("Fault", direction=fault_normal, gamma=10)
61+
```
62+
63+
**When to use:**
64+
- Free-slip on any geometry (boxes, annuli, spherical shells)
65+
- Spherical shell models where penalty is fragile
66+
- When you don't want to tune a penalty parameter
67+
- Basal shear constraints with custom direction
68+
69+
**Accuracy:** Optimal convergence rate. On a Cartesian box test, Nitsche at
70+
`gamma=10` gives 0.08% velocity error vs the essential BC solution (penalty
71+
at 1e4 gives 0.15%).
72+
73+
74+
### 2. Penalty Free-Slip (Simple but Fragile)
75+
76+
Use the mesh-derived normals directly with a penalty parameter:
4177

4278
```python
4379
Gamma = mesh.Gamma
@@ -46,14 +82,16 @@ stokes.add_natural_bc(penalty * Gamma.dot(v.sym) * Gamma, "Boundary")
4682
```
4783

4884
**When to use:**
49-
- Straight-edged boundaries (boxes, channels)
50-
- Circular boundaries (normals are radial, so facet direction is correct)
5185
- Quick prototyping where high accuracy isn't critical
86+
- When Nitsche is not yet available for your solver type
5287

53-
**Accuracy:** ~25-30% error on elliptical boundaries
88+
**Limitations:**
89+
- Penalty must be tuned: too small → loose constraint, too large → ill-conditioning
90+
- On spherical shells, penalty can become unstable at moderate resolution
91+
- ~25-30% error on elliptical boundaries when using raw facet normals
5492

5593

56-
### 2. Projected Normals (Recommended for Curved Boundaries)
94+
### 3. Projected Normals (For Curved Boundaries with Penalty)
5795

5896
Project `mesh.Gamma` onto a continuous mesh variable, which interpolates and smooths the normals:
5997

@@ -96,7 +134,7 @@ stokes.add_natural_bc(penalty * n_proj.sym.dot(v.sym) * n_proj.sym, "Boundary")
96134
**Why it works:** The projection solves a weak-form problem that naturally smooths the discontinuous facet normals into a continuous field. The finite element basis functions interpolate between facets, approximating the true surface direction.
97135

98136

99-
### 3. Analytical Normals (Most Accurate)
137+
### 4. Analytical Normals (Most Accurate)
100138

101139
Derive the surface normal from the mathematical definition of the boundary:
102140

@@ -179,15 +217,18 @@ unit_normal = normal / sympy.sqrt(normal.dot(normal))
179217

180218
## Experimental Comparison
181219

182-
We tested the three approaches on an elliptical annulus (ellipticity = 1.5) with a free-slip boundary condition:
220+
We tested the approaches on an elliptical annulus (ellipticity = 1.5) with a free-slip boundary condition:
183221

184-
| Approach | Error vs Analytical |
185-
|----------|---------------------|
186-
| Raw `mesh.Gamma` | 26.88% |
187-
| Projected normals | 0.06% |
188-
| Analytical | 0% (reference) |
222+
| Approach | Error vs Analytical | Notes |
223+
|----------|---------------------|-------|
224+
| Penalty + raw `mesh.Gamma` | 26.88% | Penalty-sensitive |
225+
| Penalty + projected normals | 0.06% | Requires projection solve |
226+
| Penalty + analytical normals | 0% (reference) | Requires surface formula |
227+
| Nitsche (default) | ~0.1% | No penalty tuning needed |
189228

190-
The projected normals provide **99.8% improvement** over raw `mesh.Gamma` for curved boundaries.
229+
Nitsche is recommended for most use cases — it matches the accuracy of
230+
projected normals without requiring a separate projection solve or penalty
231+
tuning.
191232

192233
---
193234

@@ -222,21 +263,59 @@ For complex geometries where the orientation varies spatially, the projection ap
222263

223264
---
224265

266+
## Internal Boundaries
267+
268+
```{warning}
269+
Nitsche BCs are designed for **exterior** boundaries only. Do not use
270+
`add_nitsche_bc` on internal boundary labels (e.g., `"Internal"` from
271+
`BoxInternalBoundary` or `AnnulusInternalBoundary`).
272+
```
273+
274+
On an internal surface, PETSc evaluates boundary residuals from **both**
275+
adjacent cells with opposite normals. The Nitsche consistency and pressure
276+
terms flip sign between the two sides and partially cancel, injecting
277+
a spurious residual that degrades the solution. Testing on an annulus
278+
with an internal boundary showed that Nitsche produced *worse* constraint
279+
enforcement than no constraint at all.
280+
281+
**For internal boundary constraints, continue using the penalty approach:**
282+
283+
```python
284+
Gamma = mesh.Gamma
285+
stokes.add_natural_bc(penalty * Gamma.dot(v.sym) * Gamma, "Internal")
286+
```
287+
288+
The penalty term is quadratic in the normal (`n × n`), so both sides
289+
reinforce correctly regardless of normal orientation.
290+
291+
A proper Nitsche formulation for internal surfaces would require an
292+
interior penalty (IP/SIP) method with explicit jump and average operators
293+
across the interface — accessing the solution from both adjacent cells.
294+
PETSc's current pointwise callbacks do not provide cross-cell access,
295+
so this would require a different assembly strategy. This is an area
296+
for future development, particularly for embedded impermeable surfaces
297+
in 3D spherical models where penalty sensitivity becomes problematic.
298+
299+
---
300+
225301
## Tips for Success
226302

227-
1. **Always normalize**: Analytical formulas need explicit normalization
228-
2. **Check orientation**: Ensure normals point outward (use `sign(r.dot(normal))`)
229-
3. **Verify visually**: Plot the normal field to catch errors
230-
4. **Start with projection**: It's robust and doesn't require deriving formulas
231-
5. **Use analytical for validation**: Compare projected normals against analytical when possible
303+
1. **Start with Nitsche**: `stokes.add_nitsche_bc("Upper", gamma=10)` — no penalty tuning needed
304+
2. **For penalty BCs, always normalize**: Analytical formulas need explicit normalization
305+
3. **Check orientation**: Ensure normals point outward (use `sign(r.dot(normal))`)
306+
4. **Verify visually**: Plot the normal field to catch errors
307+
5. **Use analytical normals for validation**: Compare against exact surface geometry when possible
308+
6. **Custom constraint direction**: Use `direction=` parameter when the constraint
309+
direction differs from the surface normal (faults, basal shear)
232310

233311
---
234312

235313
## Further Reading
236314

237315
- [Custom Mesh Creation](custom-meshes.md) — Creating elliptical and complex meshes
238316
- [Stokes Ellipse Example](../examples/fluid_mechanics/intermediate/Ex_Stokes_Ellipse_Cartesian.py) — Complete worked example
317+
- Sime & Wilson (2020), [arXiv:2001.10639](https://arxiv.org/abs/2001.10639) — Nitsche free-slip for geodynamics
239318

240319
---
241320

242-
*Last updated: January 2026*
321+
*Last updated: April 2026*

0 commit comments

Comments
 (0)