Coverage for dragonfly_sim/core/RANS_model.py: 89%
161 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"""
2Compressible RANS (and laminar Navier-Stokes) at p = 0, built on the Euler
3model by composition.
5CompressibleRANSModel owns a CompressibleEulerModel (`self.flow`) whose state
6carries the turbulence model's transported scalars after the mean flow
7(rho*nu_tilde for Spalart-Allmaras), and plugs the viscous physics into it.
8Everything else -- spaces, boundary conditions, the Newton solver and
9positivity limiter, pseudo-transient continuation, time stepping, adjoint
10and shape-derivative assembly -- is the Euler model's own code. Attributes
11and methods the RANS model does not define itself are forwarded to
12`self.flow` (reads and writes), so DG_windtunnel_model and DG_postprocessor
13use it exactly like the Euler model.
15Discretization (cell-centred finite volume, as DG0)
16---------------------------------------------------
17Inviscid: NS_utils.extended_hll_flux (HLL on the mean flow, the same fan
18upwinding the scalars), with MUSCL-reconstructed interior-facet traces.
19Without MUSCL the first-order HLL flux puts acoustic-speed dissipation on
20the tangential momentum at wall-parallel facets, a numerical viscosity that
21exceeds the turbulent one through the boundary layer (SA NACA0012 at M 0.7:
22friction drag ~10x too high).
24Viscous: through each facet, F_v(U_f, grad U_f) . n with the reconstructed
25facet gradient of core/reconstruction.py; no penalty term. Turbulence
26sources use the Green-Gauss gradient G. G is condensed out of Newton
27exactly, and is a derived field for adjoints and shape derivatives, as are
28the cell centres and the wall distance.
30Nondimensionalization: rho_inf = p_inf = 1, chord 1, U_inf = M sqrt(gamma),
31mu_inf = rho_inf U_inf / Re (Re held fixed under set_mach).
33Boundary conditions (kind: extra-variable values)
34-------------------------------------------------
35farfield the characteristic state; scalars from the free stream where it
36 flows in, from the interior where it flows out
37wall adiabatic no-slip (define_wall_bc): u = 0, interior density and
38 internal energy, the turbulence model's wall values; zero heat flux
39slip (symmetry) the Euler slip state, interior scalars, no viscous flux
41Solver defaults
42---------------
43Pseudo-transient continuation from the free stream (use_ptc, CFL0 5), one
44Newton step per pseudo step; FGMRES on the exact condensed Jacobian,
45preconditioned with ASM(1)/ILU(2) of the compact gradient-frozen part
46(jacobian_mode "compact_pc", utils/gradient_condensation.py). Set
47direct_linear_solver for MUMPS on the exact Jacobian instead.
48"""
49import ufl
50import dolfinx
52from dragonfly_sim.core.Euler_model import CompressibleEulerModel
53from dragonfly_sim.core.reconstruction import GreenGaussReconstruction
54from dragonfly_sim.core.turbulence_models import SpalartAllmarasNeg
55from dragonfly_sim.core.wall_distance import WallDistance
56from dragonfly_sim.utils.Euler_utils import hll_flux, pressure
57from dragonfly_sim.utils.NS_utils import (mean_flow, extra_components, concat_state, viscous_flux,
58 viscous_stress, primitive_gradients, sutherland_viscosity)
61class CompressibleRANSModel:
63 # Attributes that live on the RANS object itself; every other attribute
64 # read or written goes to self.flow.
65 _OWN = frozenset((
66 "flow", "turbulence_model", "Re", "Pr", "Pr_t", "viscosity_law", "sutherland_S_ratio",
67 "muscl", "limiter", "limiter_eps", "jacobian_mode", "reconstruction", "wall_distance",
68 "wall_tags", "mu_inf_const"))
70 def __init__(self, mesh, Re, turbulence_model=None, gamma=1.4, Pr=0.72, Pr_t=0.9,
71 viscosity_law="sutherland", T_inf_dim=300.0, sutherland_S_dim=110.4,
72 muscl=True, limiter=None, limiter_eps=1e-2, jacobian_mode="compact_pc"):
73 tm = SpalartAllmarasNeg() if turbulence_model is None else turbulence_model
74 if viscosity_law not in ("sutherland", "constant"):
75 raise ValueError("viscosity_law must be 'sutherland' or 'constant'")
76 self.turbulence_model = tm
77 self.Re = float(Re)
78 self.Pr = float(Pr)
79 self.Pr_t = float(Pr_t)
80 self.viscosity_law = viscosity_law
81 self.sutherland_S_ratio = float(sutherland_S_dim) / float(T_inf_dim)
82 self.muscl = muscl
83 self.limiter = limiter
84 self.limiter_eps = limiter_eps
85 self.jacobian_mode = jacobian_mode
86 self.reconstruction = None
87 self.wall_distance = None
88 self.wall_tags = []
89 self.mu_inf_const = None
91 self.flow = flow = CompressibleEulerModel(mesh, poly_order=0, gamma=gamma,
92 flux_function=hll_flux, extra_variables=tm.names)
93 # solver defaults (see the module docstring)
94 flow.use_ptc = True
95 flow.ptc_settings = {"cfl0": 5.0}
96 flow.fom_newton_inner_max_it = 1
97 flow.krylov_type = "fgmres"
98 flow.asm_overlap = 1
99 flow.ilu_levels = 2
100 # physics plug-ins
101 flow.freestream_scalars = self._freestream_extras
102 flow.boundary_scalars = self._boundary_scalars
103 flow.residual_terms.append(self._interior_terms)
104 flow.boundary_terms.append(self._viscous_boundary_terms)
105 flow.wall_bc = self.define_noslip_wall_bc
106 flow.wall_traction = self._wall_traction
107 flow.time_scale_terms.append(self._viscous_time_scale)
108 flow.extra_scales = self._extra_scales
110 # ------------------------------------------------------------------
111 # Forwarding to the Euler host
112 # ------------------------------------------------------------------
113 def __getattr__(self, name):
114 # only called for attributes not found on the RANS object
115 if name.startswith("__") or name in CompressibleRANSModel._OWN:
116 raise AttributeError(name)
117 return getattr(self.__dict__["flow"], name)
119 def __setattr__(self, name, value):
120 if name in CompressibleRANSModel._OWN:
121 object.__setattr__(self, name, value)
122 else:
123 setattr(self.flow, name, value)
125 # ------------------------------------------------------------------
126 # Set-up (overrides of the host's steps)
127 # ------------------------------------------------------------------
128 def define_elements_functionspaces(self):
129 self.flow.define_elements_functionspaces()
130 if self.reconstruction is None:
131 self.reconstruction = GreenGaussReconstruction(
132 self.flow, muscl=self.muscl, limiter=self.limiter, limiter_eps=self.limiter_eps,
133 limiter_scales=self._limiter_scales)
134 self.reconstruction.install()
136 def define_farfield_conditions(self, bc_dict):
137 self.flow.define_farfield_conditions(bc_dict)
138 ff = self.flow.farfield_conditions
139 self.mu_inf_const = dolfinx.fem.Constant(
140 self.flow.mesh_obj.mesh, ff['rho'] * ff['M'] * ff['c'] / self.Re)
142 def set_mach(self, M):
143 # Re is held fixed, so mu_inf follows U_inf
144 self.flow.set_mach(M)
145 ff = self.flow.farfield_conditions
146 self.mu_inf_const.value = ff['rho'] * ff['M'] * ff['c'] / self.Re
148 def set_up_solver(self):
149 flow = self.flow
150 was_set_up = flow.solver_is_set_up
151 flow.set_up_solver()
152 if was_set_up:
153 return
154 mode = "exact" if flow.direct_linear_solver else self.jacobian_mode
155 if mode != "exact":
156 flow.problem.configure_snes(flow.solver.snes, mode)
157 flow.solver.krylov_solver.setFromOptions()
159 # ------------------------------------------------------------------
160 # Material laws and free stream
161 # ------------------------------------------------------------------
162 def laminar_viscosity(self, U):
163 if self.viscosity_law == "constant":
164 return self.mu_inf_const
165 ff = self.flow.farfield_conditions
166 return sutherland_viscosity(U, self.flow.gamma, self.flow.n_mean, self.mu_inf_const,
167 ff['p'] / ff['rho'], self.sutherland_S_ratio)
169 def eddy_viscosity(self, U):
170 return self.turbulence_model.eddy_viscosity(U, self.laminar_viscosity(U), self.flow.n_mean)
172 def viscous_flux(self, U, G, heat_flux=True):
173 flow = self.flow
174 mu = self.laminar_viscosity(U)
175 tm = self.turbulence_model
176 return viscous_flux(U, G, flow.gamma, flow.n_mean, mu,
177 tm.eddy_viscosity(U, mu, flow.n_mean), self.Pr, self.Pr_t,
178 extra_fluxes=tm.diffusive_fluxes(U, mu, flow.n_mean),
179 heat_flux=heat_flux)
181 def freestream(self):
182 """The free-stream dict TurbulenceModel methods take."""
183 ff = self.flow.farfield_conditions
184 return {"rho": ff['rho'], "p": ff['p'], "c": ff['c'], "M": ff['M'],
185 "speed": ff['M'] * ff['c'], "mu": self.mu_inf_const}
187 def _freestream_extras(self):
188 return self.turbulence_model.freestream_values(self.freestream())
190 def _extra_scales(self):
191 return list(self.turbulence_model.limiter_scales(self.freestream()))
193 def _limiter_scales(self):
194 return self.flow.state_scale_expressions()
196 def _boundary_scalars(self, kind, U, inflow):
197 nm = self.flow.n_mean
198 if kind == "slip":
199 return extra_components(U, nm)
200 if kind == "farfield":
201 return [ufl.conditional(inflow, s_inf, s)
202 for s_inf, s in zip(self._freestream_extras(), extra_components(U, nm))]
203 if kind == "wall":
204 return self.turbulence_model.wall_values(U, nm)
205 raise ValueError("unknown boundary kind {!r}".format(kind))
207 # ------------------------------------------------------------------
208 # Residual terms (plugged into the host)
209 # ------------------------------------------------------------------
210 def _interior_terms(self):
211 flow, rec = self.flow, self.reconstruction
212 U, v, n = flow.u_vec, flow.v_vec, flow.mesh_obj.n
213 nm = flow.n_mean
214 jump_v = ufl.outer(v('+') - v('-'), n('+'))
215 F = - ufl.inner(self.viscous_flux(0.5 * (U('+') + U('-')), rec.face_gradient_interior()),
216 jump_v) * flow.forms.dS
217 if flow.n_extra:
218 # the WallDistance field, or a UFL expression standing in for it
219 # (manufactured-solution tests)
220 d = self.wall_distance
221 d = 1.0 if d is None else getattr(d, "function", d)
222 mu = self.laminar_viscosity(U)
223 S = self.turbulence_model.sources(U, rec.G, mu, d, nm, flow.gamma)
224 F -= sum(S[k] * v[nm + k] for k in range(flow.n_extra)) * flow.forms.dx
225 return F
227 def _viscous_boundary_terms(self, U_b, meshtag, kind):
228 if kind == "slip":
229 return None # symmetry: zero shear stress and heat flux
230 flow = self.flow
231 Fv = self.viscous_flux(U_b, self.reconstruction.face_gradient_boundary(U_b),
232 heat_flux=(kind != "wall"))
233 return - ufl.inner(ufl.dot(Fv, flow.mesh_obj.n), flow.v_vec) * flow.forms.ds(meshtag)
235 def _viscous_time_scale(self, U):
236 rho = ufl.max_value(U[0], 1e-8)
237 nu_eff = (self.flow.gamma / self.Pr) * (self.laminar_viscosity(U)
238 + ufl.max_value(self.eddy_viscosity(U), 0.0)) / rho
239 h = self.flow.mesh_obj.h_min
240 return 2.0 * nu_eff / h**2
242 # ------------------------------------------------------------------
243 # No-slip wall
244 # ------------------------------------------------------------------
245 def noslip_wall_state(self, U):
246 d = self.flow.dimensions
247 rho = U[0]
248 rho_e = U[1 + d] - 0.5 * sum(U[1 + i]**2 for i in range(d)) / rho
249 return concat_state(ufl.as_vector([rho] + [0.0] * d + [rho_e]),
250 self._boundary_scalars("wall", U, None))
252 def define_noslip_wall_bc(self, meshtag):
253 """
254 Adiabatic no-slip wall. The inviscid flux is the exact flux of the
255 wall state, (0, p n, 0, 0): no mass, energy or scalar flux through
256 the wall, the interior pressure on it. Conduction is dropped from the
257 viscous flux, which is the q.n = 0 condition.
258 """
259 flow = self.flow
260 flow.add_boundary_condition(meshtag, self.noslip_wall_state(flow.u_vec), "wall",
261 inviscid_flux="exact")
262 self.wall_tags.append(meshtag)
263 if self.turbulence_model.needs_wall_distance:
264 if self.wall_distance is None:
265 self.wall_distance = WallDistance(flow.mesh_obj, self.wall_tags)
266 flow.derived_fields.append(self.wall_distance)
267 else:
268 self.wall_distance.wall_tags = list(self.wall_tags)
269 self.wall_distance.update_geometry()
271 def define_wall_bc(self, meshtag):
272 self.define_noslip_wall_bc(meshtag)
274 def _wall_stress_arguments(self, U):
275 """(state, gradient) the wall shear is evaluated with: the wall state
276 and the reconstructed facet gradient, exactly what the residual's wall
277 flux uses."""
278 U_w = self.noslip_wall_state(U)
279 return U_w, self.reconstruction.face_gradient_boundary(U_w, U)
281 def _wall_traction(self, U, n):
282 U_s, G_s = self._wall_stress_arguments(U)
283 grad_u, _, _, _ = primitive_gradients(U_s, G_s, self.flow.gamma, self.flow.n_mean)
284 tau = viscous_stress(grad_u, self.laminar_viscosity(U_s) + self.eddy_viscosity(U_s))
285 return - ufl.dot(tau, n)
287 def skin_friction_vector(self, U, n, q_inf):
288 """tau_w / q_inf: the wall shear (tangential traction) coefficient
289 vector. Call update_derived_fields() before assembling it."""
290 U_s, G_s = self._wall_stress_arguments(U)
291 grad_u, _, _, _ = primitive_gradients(U_s, G_s, self.flow.gamma, self.flow.n_mean)
292 t = ufl.dot(viscous_stress(grad_u, self.laminar_viscosity(U_s)), n)
293 return (t - ufl.dot(t, n) * n) / q_inf
295 def eddy_viscosity_ratio(self):
296 """mu_t / mu of the current state, as a UFL expression."""
297 U = self.flow.u_vec
298 return self.eddy_viscosity(U) / self.laminar_viscosity(U)