Coverage for dragonfly_sim/utils/gradient_condensation.py: 73%
96 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 03:37 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 03:37 +0000
1"""
2Newton problems whose residual reads a gradient field that is eliminated
3from the unknowns.
5At p = 0 the RANS model reconstructs a DG0 gradient G of the state by
6Green-Gauss (core/reconstruction.py),
8 M_G G = b(U), b(U; tau) = sum over facets of int u_f (x) n : tau ds,
10with M_G the diagonal (cell-volume) DG0 mass matrix. G is linear in the
11facet states, so it is eliminated exactly instead of being carried as an
12unknown:
14 R(U) = R_U(U, G(U)), dR/dU = A_UU + A_UG M_G^-1 B_GU,
16with A_UU = dR_U/dU at fixed G, A_UG = dR_U/dG and B_GU = db/dU. The product
17widens the Jacobian's stencil to the neighbours' neighbours, which is the
18stencil of the reconstructed scheme. Everything downstream (SNES, the
19positivity limiter, the time integrator) still sees a single-field problem
20in U.
22jacobian_mode selects what Newton's linear solve uses (configure_snes wires
23it into an existing SNES):
25 exact operator = preconditioning matrix = A_UU + A_UG M^-1 B_GU
26 compact_pc operator exact, preconditioning matrix A_UU: the compact,
27 nearest-neighbour part (gradients frozen), as finite-volume
28 codes precondition with their first-order Jacobian. The
29 default: on the coarse SA NACA0012 C-grid it takes the same
30 PTC path as MUMPS, 5x faster, and scales to the 181k-cell STW
31 wing where every exact factorization runs out of memory.
32 compact operator = preconditioning matrix = A_UU: approximate Newton
33 (defect correction); converges linearly
34"""
35import ufl
36from petsc4py import PETSc
37from dolfinx.fem.forms import form as _create_form
38from dolfinx.fem.petsc import assemble_matrix, create_matrix
40from dragonfly_sim.utils.Nonlinear_utils import NonlinearProblem_mod
43class CondensedJacobian:
44 """
45 A = A_UU + A_UG M^-1 B_GU for a residual form F(U, G), with `b_G` the UFL
46 Green-Gauss right-hand side (test function on G's space) and `inv_mass`
47 M_G^-1 as a Vec in G's layout. The matrices are plain aij: MatMatMult
48 does not take baij here. The product's sparsity is fixed by the
49 operands', so one symbolic product sizes A for good.
50 """
52 def __init__(self, F, u, G, b_G, inv_mass, jit_options={}, A_UU=None, a_UU=None):
53 self._inv_mass = inv_mass
54 self._a = a_UU if a_UU is not None else _create_form(ufl.derivative(F, u), jit_options=jit_options)
55 self._a_UG = _create_form(ufl.derivative(F, G, ufl.TrialFunction(G.function_space)),
56 jit_options=jit_options)
57 self._a_GU = _create_form(ufl.derivative(b_G, u, ufl.TrialFunction(u.function_space)),
58 jit_options=jit_options)
59 self.A_UU = A_UU if A_UU is not None else create_matrix(self._a)
60 self.A_UG = create_matrix(self._a_UG)
61 self.B_GU = create_matrix(self._a_GU)
62 self._assemble_blocks()
63 self._C = self.A_UG.matMult(self.B_GU)
64 A = self.A_UU.duplicate(copy=True)
65 A.axpy(1.0, self._C, structure=PETSc.Mat.Structure.DIFFERENT_NONZERO_PATTERN)
66 A.zeroEntries()
67 self.A = A
69 def _assemble_blocks(self):
70 for M, a in ((self.A_UU, self._a), (self.A_UG, self._a_UG), (self.B_GU, self._a_GU)):
71 M.zeroEntries()
72 assemble_matrix(M, a)
73 M.assemble()
74 self.B_GU.diagonalScale(L=self._inv_mass)
76 def assemble(self):
77 """Assemble A_UU, A_UG, M^-1 B_GU and A at the current state; G must
78 be up to date."""
79 self._assemble_blocks()
80 self.A_UG.matMult(self.B_GU, result=self._C)
81 self.A.zeroEntries()
82 self.A.axpy(1.0, self.A_UU, structure=PETSc.Mat.Structure.SUBSET_NONZERO_PATTERN)
83 self.A.axpy(1.0, self._C, structure=PETSc.Mat.Structure.SUBSET_NONZERO_PATTERN)
84 self.A.assemble()
85 return self.A
88class TransposedCondensedOperator:
89 """
90 Python-Mat context of the transposed condensed Jacobian without forming it:
92 A^T x = (A_UU + A_UG B)^T x = P x + B^T (A_UG^T x), P = A_UU^T
94 with B = M_G^-1 dG/dU (any Mat with mult / multTranspose, e.g. the
95 structured GreenGaussStateOperator). Neither A nor the product A_UG B
96 is ever assembled. It owns P (also the preconditioning matrix), A_UG and
97 B (released through `release_B`, a callable) and frees them in destroy().
98 """
100 def __init__(self, P, A_UG, B, release_B):
101 self.P, self.A_UG, self.B, self._release_B = P, A_UG, B, release_B
102 self._w = A_UG.createVecRight()
104 def mult(self, mat, x, y):
105 self.A_UG.multTranspose(x, self._w)
106 self.B.multTranspose(self._w, y)
107 self.P.multAdd(x, y, y)
109 def destroy(self, mat=None):
110 # called by PETSc when the python Mat is destroyed
111 if self.P is None:
112 return
113 self._release_B(self.B)
114 for obj in (self._w, self.A_UG, self.P):
115 obj.destroy()
116 self.P = self.A_UG = self.B = None
118 def create_mat(self, comm):
119 M = PETSc.Mat().createPython(self.P.getSizes(), comm=comm)
120 M.setPythonContext(self)
121 M.setUp()
122 return M
125class CondensedGradientProblem(NonlinearProblem_mod):
126 """
127 NonlinearProblem_mod for R_U(U, G(U)).
129 `G` is the gradient Function the residual reads, `b_G` the UFL form of
130 the Green-Gauss right-hand side, `update_G()` recomputes G from the
131 current u in place and `inv_mass` is M_G^-1 as a Vec in G's layout.
132 """
134 MODES = ("exact", "compact_pc", "compact")
136 def __init__(self, F, J, u, G, b_G, update_G, inv_mass, jit_options={}):
137 super().__init__(F, J, u, jit_options=jit_options, mat_type=None)
138 self.G = G
139 self.update_G = update_G
140 self.update_G()
141 self._condensed = CondensedJacobian(F, u, G, b_G, inv_mass, jit_options=jit_options,
142 A_UU=self._A, a_UU=self._a)
143 self._A_UU = self._condensed.A_UU
144 self._A = self._condensed.A
145 self.jacobian_mode = "exact"
147 def configure_snes(self, snes, mode):
148 """Re-wire `snes`'s Jacobian for jacobian_mode `mode` (see the module
149 docstring). Call after the solver is built."""
150 if mode not in self.MODES:
151 raise ValueError("jacobian_mode must be one of {}".format(self.MODES))
152 self.jacobian_mode = mode
153 if mode == "exact":
154 return
156 def sync(x):
157 x.copy(self.u.x.petsc_vec)
158 self.u.x.petsc_vec.ghostUpdate(addv=PETSc.InsertMode.INSERT,
159 mode=PETSc.ScatterMode.FORWARD)
161 if mode == "compact_pc":
162 def jac(snes_, x, A, P):
163 sync(x)
164 self.J(x, self._A) # assembles A_UU on the way
165 snes.setJacobian(jac, self._A, self._A_UU)
166 return
168 def compact(snes_, x, A, P):
169 sync(x)
170 self.update_G()
171 self._A_UU.zeroEntries()
172 assemble_matrix(self._A_UU, self._a)
173 self._A_UU.assemble()
174 snes.setJacobian(compact, self._A_UU, self._A_UU)
176 def F(self, x, b):
177 self.update_G()
178 super().F(x, b)
180 def J(self, x, A):
181 self.update_G()
182 self._condensed.assemble()
183 if A.handle != self._A.handle:
184 A.assemble()
185 self._A.copy(A)