Skip to content

Commit 8e965a3

Browse files
committed
Apply reviewer feedback and update Navier–Stokes CR–P0 solver
1 parent 44d23f3 commit 8e965a3

File tree

1 file changed

+146
-89
lines changed

1 file changed

+146
-89
lines changed

chapter2/monolithic_navierstokes_crp0.py

Lines changed: 146 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,47 @@
1010

1111

1212
def parse_args():
13-
p = argparse.ArgumentParser(description="CR–P0 steady Navier–Stokes with Nitsche BCs (robust)")
13+
p = argparse.ArgumentParser(
14+
description="CR–P0 steady Navier–Stokes with Nitsche BCs (monolithic)"
15+
)
1416
p.add_argument("--nx", type=int, default=64)
1517
p.add_argument("--ny", type=int, default=32)
1618
p.add_argument("--Lx", type=float, default=2.0)
1719
p.add_argument("--Ly", type=float, default=1.0)
1820
p.add_argument("--nu", type=float, default=1e-2)
19-
p.add_argument("--uin", type=float, default=1.0)
21+
p.add_argument("--uin", type=float, default=1.0)
2022
p.add_argument("--picard-it", type=int, default=20)
2123
p.add_argument("--picard-tol", type=float, default=1e-8)
2224
p.add_argument("--theta", type=float, default=0.5)
23-
p.add_argument("--eps-p", type=float, default=1e-12) # tiny pressure mass to remove nullspace
25+
p.add_argument(
26+
"--eps-p",
27+
type=float,
28+
default=1e-12,
29+
help="Tiny pressure mass to remove nullspace",
30+
)
2431
p.add_argument("--stokes-only", action="store_true")
2532
p.add_argument("--no-output", action="store_true")
26-
p.add_argument("--alpha", type=float, default=400.0, help="Nitsche penalty (100–1000 recommended)")
27-
p.add_argument("--outfile", type=str, default="chapter2/open_cavity_crp0.xdmf")
33+
p.add_argument(
34+
"--alpha",
35+
type=float,
36+
default=400.0,
37+
help="Nitsche penalty (100–1000 recommended)",
38+
)
39+
# Outfile stem; we append _u.bp / _p.bp
40+
p.add_argument(
41+
"--outfile",
42+
type=str,
43+
default="chapter2/open_cavity_crp0",
44+
help="Output file stem (without extension)",
45+
)
2846
return p.parse_args()
2947

3048

3149
args = parse_args()
3250
comm = MPI.COMM_WORLD
3351
rank = comm.rank
3452

35-
# --- mesh ---
53+
# --- Mesh --------------------------------------------------------------
3654
domain = mesh.create_rectangle(
3755
comm,
3856
[np.array([0.0, 0.0]), np.array([args.Lx, args.Ly])],
@@ -44,57 +62,79 @@ def parse_args():
4462
gdim = domain.geometry.dim
4563
cell = domain.basix_cell()
4664

47-
# Ensure needed connectivities
4865
domain.topology.create_connectivity(fdim, tdim)
4966
domain.topology.create_connectivity(tdim, tdim)
5067

51-
# --- boundary facet tags (needed for ds with Nitsche) ---
52-
left_facets = mesh.locate_entities_boundary(domain, fdim, lambda x: np.isclose(x[0], 0.0))
53-
right_facets = mesh.locate_entities_boundary(domain, fdim, lambda x: np.isclose(x[0], args.Lx))
54-
bottom_facets = mesh.locate_entities_boundary(domain, fdim, lambda x: np.isclose(x[1], 0.0))
55-
top_facets = mesh.locate_entities_boundary(domain, fdim, lambda x: np.isclose(x[1], args.Ly))
68+
# --- Boundary facet tags (for ds & Nitsche) ---------------------------
69+
left_facets = mesh.locate_entities_boundary(
70+
domain, fdim, lambda x: np.isclose(x[0], 0.0)
71+
)
72+
right_facets = mesh.locate_entities_boundary(
73+
domain, fdim, lambda x: np.isclose(x[0], args.Lx)
74+
)
75+
bottom_facets = mesh.locate_entities_boundary(
76+
domain, fdim, lambda x: np.isclose(x[1], 0.0)
77+
)
78+
top_facets = mesh.locate_entities_boundary(
79+
domain, fdim, lambda x: np.isclose(x[1], args.Ly)
80+
)
5681

5782
# Tag: 1=left, 2=right, 3=bottom, 4=top
58-
num_facets_local = (
59-
domain.topology.index_map(fdim).size_local
60-
+ domain.topology.index_map(fdim).num_ghosts
83+
facet_indices = np.concatenate(
84+
[left_facets, right_facets, bottom_facets, top_facets]
85+
).astype(np.int32)
86+
facet_tags = np.concatenate(
87+
[
88+
np.full_like(left_facets, 1, dtype=np.int32),
89+
np.full_like(right_facets, 2, dtype=np.int32),
90+
np.full_like(bottom_facets, 3, dtype=np.int32),
91+
np.full_like(top_facets, 4, dtype=np.int32),
92+
]
6193
)
62-
facet_markers = np.full(num_facets_local, 0, dtype=np.int32)
63-
facet_markers[left_facets] = 1
64-
facet_markers[right_facets] = 2
65-
facet_markers[bottom_facets] = 3
66-
facet_markers[top_facets] = 4
67-
facet_indices = np.flatnonzero(facet_markers)
68-
69-
ft = mesh.meshtags(domain, fdim, facet_indices, facet_markers[facet_indices])
7094

95+
if facet_indices.size == 0:
96+
ft = mesh.meshtags(
97+
domain,
98+
fdim,
99+
np.array([], dtype=np.int32),
100+
np.array([], dtype=np.int32),
101+
)
102+
else:
103+
order = np.argsort(facet_indices)
104+
ft = mesh.meshtags(domain, fdim, facet_indices[order], facet_tags[order])
71105

72106
dx = ufl.Measure("dx", domain=domain)
73107
ds = ufl.Measure("ds", domain=domain, subdomain_data=ft)
74108

75-
# --- spaces: CR–P0 (mixed) ---
109+
# --- Spaces: CR–P0 (mixed) --------------------------------------------
76110
V_el = element("CR", cell, 1, shape=(gdim,))
77111
Q_el = element("DG", cell, 0)
78112
W = fem.functionspace(domain, mixed_element([V_el, Q_el]))
79113
(u, p) = ufl.TrialFunctions(W)
80114
(v, q) = ufl.TestFunctions(W)
81115

82-
# --- parameters / RHS ---
83-
nu = fem.Constant(domain, dolfinx.default_scalar_type(args.nu))
116+
# --- Parameters / RHS -------------------------------------------------
117+
nu = fem.Constant(domain, PETSc.ScalarType(args.nu))
84118
eps_p = fem.Constant(domain, PETSc.ScalarType(args.eps_p))
85-
f_vec = fem.Constant(domain, np.zeros(mesh.geometry.dim, dtype=dolfinx.default_scalar_type))
119+
zero = fem.Constant(domain, PETSc.ScalarType(0.0))
120+
f_vec = ufl.as_vector((zero, zero)) # zero body force
86121

87-
# --- Picard state (for convection) ---
88-
w = fem.Function(W) # holds (u_k, p_k)
122+
# --- Picard state (for convection) -----------------------------------
123+
w = fem.Function(W) # holds (u_k, p_k)
89124
u_k, p_k = w.split()
90125

126+
91127
def build_forms():
92128
n = ufl.FacetNormal(domain)
93129
h = ufl.CellDiameter(domain)
94130
alpha = PETSc.ScalarType(args.alpha)
95131

96-
u_in = ufl.as_vector((PETSc.ScalarType(args.uin), PETSc.ScalarType(0.0)))
97-
zero_vec = ufl.as_vector((PETSc.ScalarType(0.0), PETSc.ScalarType(0.0)))
132+
u_in = ufl.as_vector(
133+
(PETSc.ScalarType(args.uin), PETSc.ScalarType(0.0))
134+
)
135+
zero_vec = ufl.as_vector(
136+
(PETSc.ScalarType(0.0), PETSc.ScalarType(0.0))
137+
)
98138

99139
# Core Stokes + tiny pressure mass
100140
F = (
@@ -105,69 +145,60 @@ def build_forms():
105145
- ufl.inner(f_vec, v) * dx
106146
)
107147

108-
# Add convection if not stokes-only
148+
# Convection (Picard) if not Stokes-only
109149
if not args.stokes_only:
110150
F += ufl.inner(ufl.dot(u_k, ufl.nabla_grad(u)), v) * dx
111151

112-
# -------- Nitsche + boundary consistency terms on Dirichlet parts --------
113-
# Left inlet (tag=1): u = u_in
152+
# --- Nitsche / consistency terms on Dirichlet boundaries ---------
153+
# Tag 1: left inlet, u = u_in
114154
F += (
115-
- nu * ufl.inner(ufl.grad(u) * n, v) # consistency
116-
- nu * ufl.inner(ufl.grad(v) * n, (u - u_in)) # symmetry
117-
+ alpha * nu / h * ufl.inner(u - u_in, v) # penalty
118-
- ufl.inner(p * n, v) # momentum BC consistency
119-
+ q * ufl.inner(u - u_in, n) # continuity BC consistency: q * ((u-u_in)·n)
155+
-nu * ufl.inner(ufl.grad(u) * n, v)
156+
-nu * ufl.inner(ufl.grad(v) * n, (u - u_in))
157+
+ alpha * nu / h * ufl.inner(u - u_in, v)
158+
- ufl.inner(p * n, v)
159+
+ q * ufl.dot(u - u_in, n)
120160
) * ds(1)
121161

122-
# Bottom (3) and Top (4): no-slip u = 0
123-
F += (
124-
- nu * ufl.inner(ufl.grad(u) * n, v)
125-
- nu * ufl.inner(ufl.grad(v) * n, (u - zero_vec))
126-
+ alpha * nu / h * ufl.inner(u - zero_vec, v)
127-
- ufl.inner(p * n, v)
128-
+ q * ufl.inner(u - zero_vec, n)
129-
) * ds((3,4))
162+
# Tags 3 and 4: bottom + top, u = 0
163+
for tag in (3, 4):
164+
F += (
165+
-nu * ufl.inner(ufl.grad(u) * n, v)
166+
-nu * ufl.inner(ufl.grad(v) * n, (u - zero_vec))
167+
+ alpha * nu / h * ufl.inner(u - zero_vec, v)
168+
- ufl.inner(p * n, v)
169+
+ q * ufl.dot(u - zero_vec, n)
170+
) * ds(tag)
130171

131-
# Right (2) left as natural outlet (do-nothing)
132-
return ufl.system(F)
172+
# Tag 2 (right) kept as natural outlet (do-nothing)
173+
a = ufl.lhs(F)
174+
L = ufl.rhs(F)
175+
return a, L
133176

134177

135178
def solve_once():
136179
a, L = build_forms()
137180
# No strong BCs with CR → Nitsche handles boundaries
138-
A = petsc.assemble_matrix(fem.form(a), bcs=[])
139-
A.assemble()
140-
b = A.createVecRight(); b.set(0.0)
141-
petsc.assemble_vector(b, fem.form(L))
142-
# (No lifting/BC since bcs=[])
143-
b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
144-
145-
if rank == 0:
146-
PETSc.Sys.Print(f" ||b||_2 = {b.norm():.3e}")
147-
148-
# Robust Krylov (no factorization)
149-
ksp = PETSc.KSP().create(comm)
150-
ksp.setOperators(A)
151-
ksp.setType("gmres")
152-
ksp.setTolerances(rtol=1e-8, max_it=1000)
153-
ksp.getPC().setType("jacobi")
154-
ksp.setErrorIfNotConverged(True)
155-
156-
# wrap dolfinx vector, solve, copy back
157-
x = PETSc.Vec().createWithArray(w.x.array, comm=comm)
158-
ksp.solve(b, x)
159-
w.x.array[:] = x.getArray()
160-
w.x.scatter_forward()
161-
if rank == 0:
162-
PETSc.Sys.Print(f" ||x||_2 = {x.norm():.3e}")
181+
problem = petsc.LinearProblem(
182+
a,
183+
L,
184+
u=w,
185+
bcs=[],
186+
petsc_options={
187+
"ksp_type": "gmres",
188+
"pc_type": "jacobi",
189+
"ksp_rtol": 1.0e-8,
190+
"ksp_max_it": 1000,
191+
},
192+
petsc_options_prefix="ns_",
193+
)
194+
problem.solve()
163195

164196

165-
# --- Picard loop ---
197+
# --- Picard loop ------------------------------------------------------
166198
theta = float(args.theta)
167199
tol = float(args.picard_tol)
168200
max_it = int(args.picard_it)
169201

170-
# for residual check (velocity only)
171202
V_sub = W.sub(0)
172203
V0, _ = V_sub.collapse()
173204
u_prev_fun = fem.Function(V0)
@@ -176,40 +207,66 @@ def solve_once():
176207
for it in range(1, max_it + 1):
177208
solve_once()
178209

179-
# velocity from mixed solution
210+
# Velocity from mixed solution
180211
u_view = w.sub(0).collapse()
181212
u_curr = u_view[0] if isinstance(u_view, tuple) else u_view
182213
u_curr_arr = u_curr.x.array.copy()
183214

184215
diff = u_curr_arr - u_prev_arr
185-
err = float(np.linalg.norm(diff)) if np.all(np.isfinite(diff)) else float("inf")
216+
err = float(np.linalg.norm(diff)) if np.all(
217+
np.isfinite(diff)
218+
) else float("inf")
186219

187220
if rank == 0:
188221
PETSc.Sys.Print(f"Picard {it:02d}: ||u - u_prev|| = {err:.3e}")
189-
PETSc.Sys.Print(f" |u| range: [{np.abs(u_curr_arr).min():.3e}, {np.abs(u_curr_arr).max():.3e}]")
222+
PETSc.Sys.Print(
223+
f" |u| range: [{np.abs(u_curr_arr).min():.3e}, "
224+
f"{np.abs(u_curr_arr).max():.3e}]"
225+
)
190226

191227
if not np.isfinite(err) or err < tol or args.stokes_only:
192228
break
193229

194-
# under-relax linearization state u_k := θ u_curr + (1-θ) u_prev
230+
# Under-relax: u_k := θ u_curr + (1-θ) u_prev
195231
relaxed = theta * u_curr_arr + (1.0 - theta) * u_prev_arr
196-
u_k.x.array[:len(relaxed)] = relaxed
232+
u_k.x.array[: len(relaxed)] = relaxed
197233
u_prev_arr = u_curr_arr.copy()
198234

199-
# --- output ---
235+
# --- Output (BP4 / VTKWriter) ----------------------------------------
200236
if not args.no_output:
237+
# Extract velocity and pressure from mixed solution
201238
u_view = w.sub(0).collapse()
202239
p_view = w.sub(1).collapse()
203240
u_fun = u_view[0] if isinstance(u_view, tuple) else u_view
204241
p_fun = p_view[0] if isinstance(p_view, tuple) else p_view
205242

206-
# interpolate to CG1 for cleaner XDMF viewing
207-
Vout = fem.functionspace(domain, element("DG", cell, 1, shape=(gdim,)))
208-
Qout = fem.functionspace(domain, element("Lagrange", cell, 1))
209-
u_out = fem.Function(Vout, name="u")
243+
# Interpolate to CG1 for smoother visualization
244+
Vout_u = fem.functionspace(
245+
domain, element("Lagrange", cell, 1, shape=(gdim,))
246+
)
247+
Vout_p = fem.functionspace(domain, element("Lagrange", cell, 1))
248+
249+
u_out = fem.Function(Vout_u, name="u")
210250
u_out.interpolate(u_fun)
211251

212-
with io.VTXWriter(domain.comm, args.outfile, [u_out, p_fun]) as writer:
213-
writer.write(0.0)
252+
p_out = fem.Function(Vout_p, name="p")
253+
p_out.interpolate(p_fun)
254+
255+
outfile = args.outfile
256+
257+
# Write velocity
258+
with io.VTXWriter(
259+
domain.comm, outfile + "_u.bp", u_out, engine="BP4"
260+
) as vtx_u:
261+
vtx_u.write(0.0)
262+
263+
# Write pressure
264+
with io.VTXWriter(
265+
domain.comm, outfile + "_p.bp", p_out, engine="BP4"
266+
) as vtx_p:
267+
vtx_p.write(0.0)
268+
214269
if rank == 0:
215-
PETSc.Sys.Print(f"Wrote {args.outfile}")
270+
PETSc.Sys.Print(
271+
f"Wrote {outfile}_u.bp and {outfile}_p.bp"
272+
)

0 commit comments

Comments
 (0)