Coverage for dragonfly_sim/utils/NS_utils.py: 99%

78 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-17 03:37 +0000

1""" 

2Navier-Stokes / RANS building blocks on top of Euler_utils (used by 

3core/RANS_model.py and by CompressibleEulerModel whenever the state carries 

4extra transported scalars). 

5 

6The state is the conservative mean flow U_m = (rho, rho*u, rho*E), n_mean = 

72 + dim components, optionally followed by conservative transported scalars 

8(rho*phi_k, e.g. rho*nu_tilde for Spalart-Allmaras). Euler_utils only ever 

9sees the mean-flow slice; the scalars are convected passively here, with the 

10mean flow's wave speeds. 

11 

12Every flux is written against an explicit n_mean, and reduces to the 

13corresponding Euler_utils expression when there are no extra components. 

14 

15Nondimensionalization is the solver's: rho_inf = p_inf = 1, reference length 

161, so T is only ever needed as the ratio (p/rho)/(p_inf/rho_inf). The heat 

17flux is written in terms of p/rho, which removes the gas constant: 

18 

19 q = -kappa grad T = -(gamma/(gamma-1)) (mu/Pr + mu_t/Pr_t) grad(p/rho) 

20""" 

21import ufl 

22 

23from dragonfly_sim.utils.Euler_utils import (primitives, flux, 

24 signed_wave_speeds, llf_penalty_term) 

25 

26 

27def mean_flow(U, n_mean): 

28 """The mean-flow slice of the state expression U.""" 

29 if U.ufl_shape[0] == n_mean: 

30 return U 

31 return ufl.as_vector([U[i] for i in range(n_mean)]) 

32 

33 

34def extra_components(U, n_mean): 

35 """The transported scalars rho*phi_k of U, as a list.""" 

36 return [U[i] for i in range(n_mean, U.ufl_shape[0])] 

37 

38 

39def concat_state(U_mean, extras): 

40 """A full state from a mean-flow vector and a list of scalars.""" 

41 if len(extras) == 0: 

42 return U_mean 

43 return ufl.as_vector([U_mean[i] for i in range(U_mean.ufl_shape[0])] + list(extras)) 

44 

45 

46# ---------------------------------------------------------------------- 

47# Inviscid fluxes 

48# ---------------------------------------------------------------------- 

49def extended_flux(U, gamma, n_mean): 

50 """The inviscid flux tensor, (n_state, dim): Euler rows, then rho*phi*u.""" 

51 if U.ufl_shape[0] == n_mean: 

52 return flux(U, gamma) 

53 Um = mean_flow(U, n_mean) 

54 _, u, _ = primitives(Um) 

55 Fm = flux(Um, gamma) 

56 rows = [Fm[i, :] for i in range(n_mean)] 

57 rows += [s * u for s in extra_components(U, n_mean)] 

58 return ufl.as_tensor(rows) 

59 

60 

61def extended_normal_flux(U, n, gamma, n_mean): 

62 return ufl.dot(extended_flux(U, gamma, n_mean), n) 

63 

64 

65def extended_hll_flux(U_p, U_m, n, gamma, n_mean): 

66 """ 

67 hll_flux on the full state. The wave speeds come from the mean flow 

68 alone, and the same single-intermediate-state formula is applied to 

69 every component, so the mean-flow rows are exactly hll_flux and each 

70 scalar is upwinded with the fan of the flow carrying it. 

71 """ 

72 sl_p, sr_p = signed_wave_speeds(mean_flow(U_p, n_mean), n, gamma) 

73 sl_m, sr_m = signed_wave_speeds(mean_flow(U_m, n_mean), n, gamma) 

74 

75 SL_minus = ufl.min_value(ufl.min_value(sl_p, sl_m), 0.0) 

76 SR_plus = ufl.max_value(ufl.max_value(sr_p, sr_m), 0.0) 

77 denom = ufl.max_value(SR_plus - SL_minus, 1e-8) 

78 

79 return ( 

80 SR_plus * extended_normal_flux(U_p, n, gamma, n_mean) 

81 - SL_minus * extended_normal_flux(U_m, n, gamma, n_mean) 

82 + SL_minus * SR_plus * (U_m - U_p) 

83 ) / denom 

84 

85 

86def extended_boundary_flux(U, U_ext, n, gamma, n_mean): 

87 """Euler_utils.boundary_flux on the full state.""" 

88 alpha = llf_penalty_term(mean_flow(U, n_mean), mean_flow(U_ext, n_mean), n, gamma) 

89 return extended_normal_flux(U, n, gamma, n_mean) + alpha * (U - U_ext) 

90 

91 

92# ---------------------------------------------------------------------- 

93# Gradients of the primitive quantities 

94# ---------------------------------------------------------------------- 

95def primitive_gradients(U, G, gamma, n_mean): 

96 """ 

97 grad(u), grad(p/rho) and grad(phi_k) from the state U and a tensor G 

98 standing in for grad(U), shape (n_state, dim). 

99 

100 G is an argument rather than ufl.grad(U) because the interior-penalty 

101 terms evaluate the viscous flux with the jump tensor [U] (x) n in 

102 place of the gradient. Everything returned is linear in G. 

103 """ 

104 d = n_mean - 2 

105 rho = U[0] 

106 u = ufl.as_vector([U[1 + i] / rho for i in range(d)]) 

107 rhoE = U[1 + d] 

108 p = (gamma - 1.0) * (rhoE - 0.5 * rho * ufl.dot(u, u)) 

109 

110 grad_rho = ufl.as_vector([G[0, j] for j in range(d)]) 

111 grad_m = [ufl.as_vector([G[1 + i, j] for j in range(d)]) for i in range(d)] 

112 grad_rhoE = ufl.as_vector([G[1 + d, j] for j in range(d)]) 

113 

114 grad_u = ufl.as_tensor([(grad_m[i] - u[i] * grad_rho) / rho for i in range(d)]) 

115 grad_p = (gamma - 1.0) * (grad_rhoE - sum(u[i] * grad_m[i] for i in range(d)) 

116 + 0.5 * ufl.dot(u, u) * grad_rho) 

117 grad_p_over_rho = (grad_p - (p / rho) * grad_rho) / rho 

118 

119 grad_phi = [] 

120 for k in range(n_mean, U.ufl_shape[0]): 

121 phi = U[k] / rho 

122 grad_s = ufl.as_vector([G[k, j] for j in range(d)]) 

123 grad_phi.append((grad_s - phi * grad_rho) / rho) 

124 

125 return grad_u, grad_p_over_rho, grad_phi, grad_rho 

126 

127 

128def vorticity_magnitude(grad_u, eps=1e-16): 

129 """ 

130 sqrt(2 W:W), W the antisymmetric part of grad(u). Regularized by eps 

131 inside the root so its derivative stays finite where the flow is 

132 irrotational (the whole free stream). 

133 """ 

134 W = 0.5 * (grad_u - grad_u.T) 

135 return ufl.sqrt(2.0 * ufl.inner(W, W) + eps) 

136 

137 

138# ---------------------------------------------------------------------- 

139# Viscous flux 

140# ---------------------------------------------------------------------- 

141def viscous_stress(grad_u, mu_eff): 

142 d = grad_u.ufl_shape[0] 

143 return mu_eff * (grad_u + grad_u.T - (2.0 / 3.0) * ufl.tr(grad_u) * ufl.Identity(d)) 

144 

145 

146def viscous_flux(U, G, gamma, n_mean, mu, mu_t, Pr, Pr_t, 

147 extra_fluxes=None, heat_flux=True): 

148 """ 

149 The viscous flux tensor F_v(U, G), shape (n_state, dim), linear in G: 

150 

151 mass 0 

152 momentum tau = (mu + mu_t)(grad u + grad u^T - 2/3 div u I) 

153 energy tau u - q 

154 scalars extra_fluxes(grad_phi, grad_rho), one row per scalar 

155 

156 `mu` and `mu_t` are expressions of the state (not of G). heat_flux=False 

157 drops the conduction term, which is how the adiabatic wall imposes 

158 q.n = 0. 

159 """ 

160 grad_u, grad_T, grad_phi, grad_rho = primitive_gradients(U, G, gamma, n_mean) 

161 d = n_mean - 2 

162 rho = U[0] 

163 u = ufl.as_vector([U[1 + i] / rho for i in range(d)]) 

164 

165 tau = viscous_stress(grad_u, mu + mu_t) 

166 energy = ufl.dot(tau, u) 

167 if heat_flux: 

168 energy = energy + gamma / (gamma - 1.0) * (mu / Pr + mu_t / Pr_t) * grad_T 

169 

170 rows = [ufl.as_vector([0.0] * d)] 

171 rows += [ufl.as_vector([tau[i, j] for j in range(d)]) for i in range(d)] 

172 rows += [energy] 

173 if U.ufl_shape[0] > n_mean: 

174 rows += list(extra_fluxes(grad_phi, grad_rho)) 

175 return ufl.as_tensor(rows) 

176 

177 

178def sutherland_viscosity(U, gamma, n_mean, mu_inf, T_ratio_inf, S_ratio): 

179 """ 

180 Sutherland's law in the solver's units: 

181 

182 mu/mu_inf = theta^(3/2) (1 + S) / (theta + S), 

183 theta = T/T_inf = (p/rho) / (p_inf/rho_inf), S = S_dim / T_inf_dim 

184 

185 `T_ratio_inf` is p_inf/rho_inf. theta is floored so a transiently 

186 negative pressure cannot hand theta^(3/2) a negative base. 

187 """ 

188 Um = mean_flow(U, n_mean) 

189 rho, u, E = primitives(Um) 

190 p = (gamma - 1.0) * (E - 0.5 * rho * ufl.dot(u, u)) 

191 theta = ufl.max_value((p / rho) / T_ratio_inf, 1e-8) 

192 return mu_inf * theta ** 1.5 * (1.0 + S_ratio) / (theta + S_ratio)