Quickstart#
This quickstart shows the current CutFEMx workflow on a scalar Poisson problem posed on an implicit disk. The physical domain is
Continuous problem
We solve a Dirichlet problem on the unfitted domain:
For the manufactured solution
the source term and boundary data are
The important API pattern is:
Interpolate a level-set function into a DOLFINx space.
Build one reusable
CutDataobject withcutfemx.cut(phi).Use the
CutDataobject to locate entities, create runtime quadrature, and create a visualization mesh.Compile UFL forms with
cutfemx.fem.form(...)before assembly.
Imports#
from mpi4py import MPI
import cutfemx
import numpy as np
import ufl
from dolfinx import fem, mesh
Background Mesh And Level Set#
The level set is negative inside the disk and positive outside.
For this example,
Hence
msh = mesh.create_rectangle(
MPI.COMM_WORLD,
points=((-1.0, -1.0), (1.0, 1.0)),
n=(21, 21),
cell_type=mesh.CellType.triangle,
)
V_phi = fem.functionspace(msh, ("Lagrange", 2))
phi = fem.Function(V_phi, name="phi")
phi.interpolate(lambda x: np.sqrt(x[0] ** 2 + x[1] ** 2) - 0.5)
Cut The Mesh#
cutfemx.cut(phi) returns a CutData object. All later classification and
quadrature calls use this object, not the raw level-set function.
cut_data = cutfemx.cut(phi)
inside_cells = cutfemx.locate_entities(cut_data, "phi<0")
interface_cells = cutfemx.locate_entities(cut_data, "phi=0")
active_cells = cutfemx.locate_entities(cut_data, "phi<=0")
Selectors use the CutCells expression grammar. The default level-set name is
phi; with multiple level sets the names are phi, phi1, phi2, and so on.
Active cells and cut cells
The background mesh is not fitted to \(\Gamma\). The active cell set is
The interface cell set is
In the code, "phi<=0" selects the active region and "phi=0" selects the
cells intersected by the embedded boundary.
Runtime Quadrature And Cut Mesh#
Cut cells need quadrature rules that are only known after cutting. CutFEMx
returns runintgen-compatible providers:
Runtime quadrature
On a cut mesh, the cell integrals are evaluated as
where the quadrature points \(x_{Kq}\) and weights \(w_{Kq}\) are generated from the local geometry \(K \cap \Omega\).
Boundary integrals use a separate set of rules:
These rules are attached to UFL measures through subdomain_data.
order = 4
dx_omega_rules = cutfemx.runtime_quadrature(cut_data, "phi<0", order)
dx_gamma_rules = cutfemx.runtime_quadrature(cut_data, "phi=0", order)
For visualization, create a cut mesh and transfer functions from the background mesh:
cut_mesh = cutfemx.create_cut_mesh(cut_data, "phi<0", mode="full")
phi_cut = cutfemx.fem.cut_function(phi, cut_mesh)
cut_mesh.mesh is a DOLFINx mesh. cut_mesh.parent_index maps cells in the
visualization mesh back to parent background cells.
Variational Form#
The unknown still lives on the background mesh. Runtime measures restrict the integrals to the selected cut domain and embedded boundary.
Symmetric Nitsche formulation
The homogeneous fitted weak form
is modified because \(\Gamma\) cuts through mesh cells. The symmetric Nitsche form for imposing \(u=g\) on \(\Gamma\) is: find \(u_h \in V_h\) such that
with
and
The normal is computed from the level set,
and evaluated at the runtime boundary quadrature points.
V = fem.functionspace(msh, ("Lagrange", 1))
u = ufl.TrialFunction(V)
v = ufl.TestFunction(V)
x = ufl.SpatialCoordinate(msh)
h = ufl.CellDiameter(msh)
u_exact = ufl.sin(np.pi * x[0]) * ufl.sin(np.pi * x[1])
f = 2.0 * np.pi**2 * u_exact
n_gamma = cutfemx.normal(phi)
dx_omega = ufl.Measure(
"dx",
domain=msh,
subdomain_id=0,
subdomain_data=[inside_cells, dx_omega_rules],
)
dx_gamma = ufl.Measure(
"dx",
domain=msh,
subdomain_id=1,
subdomain_data=dx_gamma_rules,
)
gamma = 40.0
a = ufl.inner(ufl.grad(u), ufl.grad(v)) * dx_omega
a += (
-ufl.dot(ufl.grad(u), n_gamma) * v
- ufl.dot(ufl.grad(v), n_gamma) * u
+ gamma / h * u * v
) * dx_gamma
L = f * v * dx_omega
L += (
-ufl.dot(ufl.grad(v), n_gamma) * u_exact
+ gamma / h * u_exact * v
) * dx_gamma
Stabilization#
Small cut cells can degrade the conditioning of the discrete system. A standard CutFEM remedy is to add a ghost-penalty term on interior facets near the interface:
Ghost penalty
For continuous first-order elements, a common stabilization is
where \({\mathcal F}_h^\Gamma\) is a band of interior facets adjacent to cut cells and
The complete stabilized form is
The minimal code pattern is:
ghost_facets = cutfemx.ghost_penalty_facets(cut_data, "phi<0")
dS_ghost = ufl.Measure(
"dS",
domain=msh,
subdomain_id=2,
subdomain_data=ghost_facets,
)
n = ufl.FacetNormal(msh)
h_avg = ufl.avg(h)
gamma_g = 0.1
a += (
gamma_g
* h_avg
* ufl.inner(
ufl.jump(ufl.grad(u), n),
ufl.jump(ufl.grad(v), n),
)
* dS_ghost
)
Assembly#
Compile with cutfemx.fem.form(...), then assemble through CutFEMx. Inactive
background degrees of freedom can be deactivated using the active-domain
information derived from the form.
Inactive degrees of freedom
The finite element space is defined on the background mesh, but only basis functions with support on the active domain are part of the physical problem:
Degrees of freedom outside \({\mathcal I}_\Omega\) are constrained by replacing their rows with identity equations,
a_form = cutfemx.fem.form(a)
L_form = cutfemx.fem.form(L)
b = cutfemx.fem.assemble_vector(L_form)
A = cutfemx.fem.assemble_matrix(a_form)
active_domain = cutfemx.fem.active_domain(a_form)
cutfemx.fem.deactivate_outside(A, b, active_domain)
The full runnable version, including linear solvers, error computation, ghost
penalty stabilization, and plotting, is in python/demo/demo_poisson.py.