-
Notifications
You must be signed in to change notification settings - Fork 78
WIP( Work in progress): add monolithic CR–P0 Navier–Stokes solver #291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
akarve1507
wants to merge
12
commits into
jorgensd:main
Choose a base branch
from
akarve1507:monolithic-ns-crp0
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
fb40d69
WIP: add monolithic CR–P0 Navier–Stokes solver
akarve1507 2dddd17
Add CR–P0 Navier–Stokes tutorial markdown file
akarve1507 526a555
Update tutorial .md with corrected image path and equations
akarve1507 9adcad8
Tidy CR–P0 Navier–Stokes tutorial text
akarve1507 aa123d3
Update chapter2/monolithic_navierstokes_crp0.py
akarve1507 72ccb23
Update chapter2/monolithic_navierstokes_crp0.py
akarve1507 71ff533
Update chapter2/monolithic_navierstokes_crp0.py
akarve1507 7a18b68
Update chapter2/monolithic_navierstokes_crp0.py
akarve1507 5341048
Update chapter2/monolithic_navierstokes_crp0.py
akarve1507 97c45f6
Update chapter2/monolithic_navierstokes_crp0.py
akarve1507 44d23f3
Update chapter2/monolithic_navierstokes_crp0.py
akarve1507 8e965a3
Apply reviewer feedback and update Navier–Stokes CR–P0 solver
akarve1507 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| # Monolithic Incompressible Navier–Stokes with CR–P0 (Steady Picard) | ||
|
|
||
| We solve the steady incompressible Navier–Stokes equations in a 2D channel. | ||
|
|
||
| We use a **monolithic** formulation with | ||
| [Crouzeix–Raviart (CR)](https://defelement.org/elements/crouzeix-raviart.html) | ||
| for the velocity and **DG–0** for the pressure. | ||
|
|
||
| Dirichlet conditions for the velocity on the **top**, **bottom**, and **left** boundaries | ||
| are imposed **weakly using Nitsche’s method** | ||
| (see the tutorial on | ||
| [Weak imposition of Dirichlet conditions for the Poisson equation](../chapter1/nitsche) | ||
| for details). | ||
|
|
||
| ## Key Parameters | ||
|
|
||
| - **Velocity space:** $V_h = [\mathrm{CR}_1]^2$ (nonconforming, facet-based) | ||
| - **Pressure space:** $Q_h = \mathrm{DG}_0$ | ||
| - **Linearization:** Picard (frozen convection); start from Stokes baseline | ||
| - **Stabilization:** small pressure mass $\varepsilon_p \int_\Omega p\,q\,\mathrm{d}x$ removes the constant-pressure nullspace | ||
| - **Solver:** PETSc GMRES + Jacobi | ||
| - **Output:** {py:class}`VTKWriter<dolfinx.io.VTXWriter>` for ParaView | ||
|
|
||
| ## Governing Equations | ||
|
|
||
| \begin{align} | ||
| -\nu \Delta \mathbf{u} + (\mathbf{u}\cdot\nabla)\mathbf{u} + \nabla p &= \mathbf{f} && \text{in } \Omega,\\ | ||
| \nabla\cdot\mathbf{u} &= 0 && \text{in } \Omega. | ||
| \end{align} | ||
|
|
||
| ### Boundary Conditions | ||
|
|
||
| - Inlet (left): $\mathbf{u} = \mathbf{u}_{\text{in}}$ | ||
| - Walls (top/bottom): $\mathbf{u} = \mathbf{0}$ | ||
| - Outlet (right): natural (traction-free) | ||
|
|
||
| ## Weak Formulation | ||
|
|
||
| For test functions $\mathbf{v}, q$, find $(\mathbf{u},p)$ such that | ||
|
|
||
| \begin{align} | ||
| a_{\text{core}}((\mathbf{u},p);(\mathbf{v},q)) | ||
| &= \nu (\nabla\mathbf{u}, \nabla\mathbf{v})_\Omega | ||
| - (p, \nabla\cdot\mathbf{v})_\Omega | ||
| + (q, \nabla\cdot\mathbf{u})_\Omega,\\ | ||
| a_{\text{conv}}(\mathbf{u};\mathbf{v}\mid\mathbf{u}_k) | ||
| &= ((\mathbf{u}_k\cdot\nabla)\mathbf{u},\mathbf{v})_\Omega,\\ | ||
| a_{\text{pm}}(p;q) | ||
| &= \varepsilon_p (p,q)_\Omega. | ||
| \end{align} | ||
|
|
||
| Right-hand side: | ||
| \[ | ||
| \ell(\mathbf{v}) = (\mathbf{f},\mathbf{v})_\Omega. | ||
| \] | ||
|
|
||
| > **Reference** | ||
| > The CR–P0 mixed finite element formulation follows the framework in | ||
| > *F. Brezzi and M. Fortin, Mixed and Hybrid Finite Element Methods, Springer, 1991.* | ||
| > The weak enforcement of velocity Dirichlet conditions is based on the symmetric | ||
| > Nitsche method (*J. Nitsche, 1971*). | ||
|
|
||
| ### Nitsche Boundary Terms | ||
|
|
||
| \[ | ||
| \begin{aligned} | ||
| & -\nu\int_{\Gamma_D} (\nabla\mathbf{u}\,\mathbf{n})\cdot\mathbf{v}\,\mathrm{d}s | ||
| -\nu\int_{\Gamma_D} (\nabla\mathbf{v}\,\mathbf{n})\cdot(\mathbf{u}-\mathbf{u}_D)\,\mathrm{d}s | ||
| + \alpha\frac{\nu}{h}\int_{\Gamma_D} (\mathbf{u}-\mathbf{u}_D)\cdot\mathbf{v}\,\mathrm{d}s \\ | ||
| &\quad - \int_{\Gamma_D} p\,\mathbf{n}\cdot\mathbf{v}\,\mathrm{d}s | ||
| + \int_{\Gamma_D} q\,\mathbf{n}\cdot(\mathbf{u}-\mathbf{u}_D)\,\mathrm{d}s. | ||
| \end{aligned} | ||
| \] | ||
|
|
||
| ### Under-relaxed Picard Update | ||
|
|
||
| \[ | ||
| \mathbf{u}_{k+1}^{(\text{lin})} | ||
| = \theta\,\mathbf{u}_{k}^{(\text{new})} | ||
| + (1-\theta)\,\mathbf{u}_{k}^{(\text{lin})}, \quad 0<\theta\le1. | ||
| \] | ||
|
|
||
| ## Example Output (ParaView) | ||
|
|
||
|  | ||
|
|
||
| **Figure:** Velocity magnitude field with CR–P0 (Stokes baseline). | ||
| Color shows $|\mathbf{u}|$ and arrows indicate flow direction and speed. | ||
| Inlet on the left, natural outlet on the right. | ||
|
|
||
| ## Run Instructions (Docker) | ||
|
|
||
| ```bash | ||
| docker run --rm -it -v "$PWD":"$PWD" -w "$PWD" \ | ||
| ghcr.io/fenics/dolfinx/dolfinx:stable \ | ||
| python3 chapter2/monolithic_navierstokes_crp0.py \ | ||
| --uin 0.5 --nu 1e-2 --theta 0.5 --alpha 300 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,272 @@ | ||
| from __future__ import annotations | ||
| import argparse | ||
| import numpy as np | ||
| from mpi4py import MPI | ||
| from dolfinx import fem, io, mesh | ||
| from dolfinx.fem import petsc | ||
| from petsc4py import PETSc | ||
| import ufl | ||
| from basix.ufl import element, mixed_element | ||
|
|
||
|
|
||
| def parse_args(): | ||
| p = argparse.ArgumentParser( | ||
| description="CR–P0 steady Navier–Stokes with Nitsche BCs (monolithic)" | ||
| ) | ||
| p.add_argument("--nx", type=int, default=64) | ||
| p.add_argument("--ny", type=int, default=32) | ||
| p.add_argument("--Lx", type=float, default=2.0) | ||
| p.add_argument("--Ly", type=float, default=1.0) | ||
| p.add_argument("--nu", type=float, default=1e-2) | ||
| p.add_argument("--uin", type=float, default=1.0) | ||
| p.add_argument("--picard-it", type=int, default=20) | ||
| p.add_argument("--picard-tol", type=float, default=1e-8) | ||
| p.add_argument("--theta", type=float, default=0.5) | ||
| p.add_argument( | ||
| "--eps-p", | ||
| type=float, | ||
| default=1e-12, | ||
| help="Tiny pressure mass to remove nullspace", | ||
| ) | ||
| p.add_argument("--stokes-only", action="store_true") | ||
| p.add_argument("--no-output", action="store_true") | ||
| p.add_argument( | ||
| "--alpha", | ||
| type=float, | ||
| default=400.0, | ||
| help="Nitsche penalty (100–1000 recommended)", | ||
| ) | ||
| # Outfile stem; we append _u.bp / _p.bp | ||
| p.add_argument( | ||
| "--outfile", | ||
| type=str, | ||
| default="chapter2/open_cavity_crp0", | ||
| help="Output file stem (without extension)", | ||
| ) | ||
| return p.parse_args() | ||
|
|
||
|
|
||
| args = parse_args() | ||
| comm = MPI.COMM_WORLD | ||
| rank = comm.rank | ||
|
|
||
| # --- Mesh -------------------------------------------------------------- | ||
| domain = mesh.create_rectangle( | ||
| comm, | ||
| [np.array([0.0, 0.0]), np.array([args.Lx, args.Ly])], | ||
| [args.nx, args.ny], | ||
| cell_type=mesh.CellType.triangle, | ||
| ) | ||
| tdim = domain.topology.dim | ||
| fdim = tdim - 1 | ||
| gdim = domain.geometry.dim | ||
| cell = domain.basix_cell() | ||
|
|
||
| domain.topology.create_connectivity(fdim, tdim) | ||
| domain.topology.create_connectivity(tdim, tdim) | ||
|
|
||
| # --- Boundary facet tags (for ds & Nitsche) --------------------------- | ||
| left_facets = mesh.locate_entities_boundary( | ||
| domain, fdim, lambda x: np.isclose(x[0], 0.0) | ||
| ) | ||
| right_facets = mesh.locate_entities_boundary( | ||
| domain, fdim, lambda x: np.isclose(x[0], args.Lx) | ||
| ) | ||
| bottom_facets = mesh.locate_entities_boundary( | ||
| domain, fdim, lambda x: np.isclose(x[1], 0.0) | ||
| ) | ||
| top_facets = mesh.locate_entities_boundary( | ||
| domain, fdim, lambda x: np.isclose(x[1], args.Ly) | ||
| ) | ||
|
|
||
| # Tag: 1=left, 2=right, 3=bottom, 4=top | ||
| facet_indices = np.concatenate( | ||
| [left_facets, right_facets, bottom_facets, top_facets] | ||
| ).astype(np.int32) | ||
| facet_tags = np.concatenate( | ||
| [ | ||
| np.full_like(left_facets, 1, dtype=np.int32), | ||
| np.full_like(right_facets, 2, dtype=np.int32), | ||
| np.full_like(bottom_facets, 3, dtype=np.int32), | ||
| np.full_like(top_facets, 4, dtype=np.int32), | ||
| ] | ||
| ) | ||
|
|
||
| if facet_indices.size == 0: | ||
| ft = mesh.meshtags( | ||
| domain, | ||
| fdim, | ||
| np.array([], dtype=np.int32), | ||
| np.array([], dtype=np.int32), | ||
| ) | ||
| else: | ||
| order = np.argsort(facet_indices) | ||
| ft = mesh.meshtags(domain, fdim, facet_indices[order], facet_tags[order]) | ||
|
|
||
| dx = ufl.Measure("dx", domain=domain) | ||
| ds = ufl.Measure("ds", domain=domain, subdomain_data=ft) | ||
|
|
||
| # --- Spaces: CR–P0 (mixed) -------------------------------------------- | ||
| V_el = element("CR", cell, 1, shape=(gdim,)) | ||
| Q_el = element("DG", cell, 0) | ||
| W = fem.functionspace(domain, mixed_element([V_el, Q_el])) | ||
| (u, p) = ufl.TrialFunctions(W) | ||
| (v, q) = ufl.TestFunctions(W) | ||
|
|
||
| # --- Parameters / RHS ------------------------------------------------- | ||
| nu = fem.Constant(domain, PETSc.ScalarType(args.nu)) | ||
| eps_p = fem.Constant(domain, PETSc.ScalarType(args.eps_p)) | ||
| zero = fem.Constant(domain, PETSc.ScalarType(0.0)) | ||
| f_vec = ufl.as_vector((zero, zero)) # zero body force | ||
|
|
||
| # --- Picard state (for convection) ----------------------------------- | ||
| w = fem.Function(W) # holds (u_k, p_k) | ||
| u_k, p_k = w.split() | ||
|
|
||
|
|
||
| def build_forms(): | ||
| n = ufl.FacetNormal(domain) | ||
| h = ufl.CellDiameter(domain) | ||
| alpha = PETSc.ScalarType(args.alpha) | ||
|
|
||
| u_in = ufl.as_vector( | ||
| (PETSc.ScalarType(args.uin), PETSc.ScalarType(0.0)) | ||
| ) | ||
| zero_vec = ufl.as_vector( | ||
| (PETSc.ScalarType(0.0), PETSc.ScalarType(0.0)) | ||
| ) | ||
|
|
||
| # Core Stokes + tiny pressure mass | ||
| F = ( | ||
| nu * ufl.inner(ufl.grad(u), ufl.grad(v)) * dx | ||
| - ufl.div(v) * p * dx | ||
| + q * ufl.div(u) * dx | ||
| + eps_p * p * q * dx | ||
| - ufl.inner(f_vec, v) * dx | ||
| ) | ||
|
|
||
| # Convection (Picard) if not Stokes-only | ||
| if not args.stokes_only: | ||
| F += ufl.inner(ufl.dot(u_k, ufl.nabla_grad(u)), v) * dx | ||
|
|
||
| # --- Nitsche / consistency terms on Dirichlet boundaries --------- | ||
| # Tag 1: left inlet, u = u_in | ||
| F += ( | ||
| -nu * ufl.inner(ufl.grad(u) * n, v) | ||
| -nu * ufl.inner(ufl.grad(v) * n, (u - u_in)) | ||
| + alpha * nu / h * ufl.inner(u - u_in, v) | ||
| - ufl.inner(p * n, v) | ||
| + q * ufl.dot(u - u_in, n) | ||
| ) * ds(1) | ||
|
|
||
| # Tags 3 and 4: bottom + top, u = 0 | ||
| for tag in (3, 4): | ||
| F += ( | ||
| -nu * ufl.inner(ufl.grad(u) * n, v) | ||
| -nu * ufl.inner(ufl.grad(v) * n, (u - zero_vec)) | ||
| + alpha * nu / h * ufl.inner(u - zero_vec, v) | ||
| - ufl.inner(p * n, v) | ||
| + q * ufl.dot(u - zero_vec, n) | ||
| ) * ds(tag) | ||
|
|
||
| # Tag 2 (right) kept as natural outlet (do-nothing) | ||
| a = ufl.lhs(F) | ||
| L = ufl.rhs(F) | ||
| return a, L | ||
|
|
||
|
|
||
| def solve_once(): | ||
akarve1507 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| a, L = build_forms() | ||
| # No strong BCs with CR → Nitsche handles boundaries | ||
| problem = petsc.LinearProblem( | ||
| a, | ||
| L, | ||
| u=w, | ||
| bcs=[], | ||
| petsc_options={ | ||
| "ksp_type": "gmres", | ||
| "pc_type": "jacobi", | ||
| "ksp_rtol": 1.0e-8, | ||
| "ksp_max_it": 1000, | ||
| }, | ||
| petsc_options_prefix="ns_", | ||
| ) | ||
| problem.solve() | ||
|
|
||
|
|
||
| # --- Picard loop ------------------------------------------------------ | ||
| theta = float(args.theta) | ||
| tol = float(args.picard_tol) | ||
| max_it = int(args.picard_it) | ||
|
|
||
| V_sub = W.sub(0) | ||
| V0, _ = V_sub.collapse() | ||
| u_prev_fun = fem.Function(V0) | ||
| u_prev_arr = np.zeros_like(u_prev_fun.x.array) | ||
|
|
||
| for it in range(1, max_it + 1): | ||
| solve_once() | ||
|
|
||
| # Velocity from mixed solution | ||
| u_view = w.sub(0).collapse() | ||
| u_curr = u_view[0] if isinstance(u_view, tuple) else u_view | ||
| u_curr_arr = u_curr.x.array.copy() | ||
|
|
||
| diff = u_curr_arr - u_prev_arr | ||
| err = float(np.linalg.norm(diff)) if np.all( | ||
| np.isfinite(diff) | ||
| ) else float("inf") | ||
|
|
||
| if rank == 0: | ||
| PETSc.Sys.Print(f"Picard {it:02d}: ||u - u_prev|| = {err:.3e}") | ||
| PETSc.Sys.Print( | ||
| f" |u| range: [{np.abs(u_curr_arr).min():.3e}, " | ||
| f"{np.abs(u_curr_arr).max():.3e}]" | ||
| ) | ||
|
|
||
| if not np.isfinite(err) or err < tol or args.stokes_only: | ||
| break | ||
|
|
||
| # Under-relax: u_k := θ u_curr + (1-θ) u_prev | ||
| relaxed = theta * u_curr_arr + (1.0 - theta) * u_prev_arr | ||
| u_k.x.array[: len(relaxed)] = relaxed | ||
| u_prev_arr = u_curr_arr.copy() | ||
|
|
||
| # --- Output (BP4 / VTKWriter) ---------------------------------------- | ||
| if not args.no_output: | ||
| # Extract velocity and pressure from mixed solution | ||
| u_view = w.sub(0).collapse() | ||
| p_view = w.sub(1).collapse() | ||
| u_fun = u_view[0] if isinstance(u_view, tuple) else u_view | ||
| p_fun = p_view[0] if isinstance(p_view, tuple) else p_view | ||
|
|
||
| # Interpolate to CG1 for smoother visualization | ||
| Vout_u = fem.functionspace( | ||
| domain, element("Lagrange", cell, 1, shape=(gdim,)) | ||
| ) | ||
| Vout_p = fem.functionspace(domain, element("Lagrange", cell, 1)) | ||
|
|
||
| u_out = fem.Function(Vout_u, name="u") | ||
| u_out.interpolate(u_fun) | ||
|
|
||
| p_out = fem.Function(Vout_p, name="p") | ||
| p_out.interpolate(p_fun) | ||
|
|
||
| outfile = args.outfile | ||
|
|
||
| # Write velocity | ||
| with io.VTXWriter( | ||
| domain.comm, outfile + "_u.bp", u_out, engine="BP4" | ||
| ) as vtx_u: | ||
| vtx_u.write(0.0) | ||
|
|
||
| # Write pressure | ||
| with io.VTXWriter( | ||
| domain.comm, outfile + "_p.bp", p_out, engine="BP4" | ||
| ) as vtx_p: | ||
| vtx_p.write(0.0) | ||
|
|
||
| if rank == 0: | ||
| PETSc.Sys.Print( | ||
| f"Wrote {outfile}_u.bp and {outfile}_p.bp" | ||
| ) | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.