From 4d76f8f57c924cfe041f337f7eba2a5d1e37fc83 Mon Sep 17 00:00:00 2001 From: andytorrestb Date: Sat, 18 Jul 2026 21:26:21 -0500 Subject: [PATCH 01/11] Phase 1: Cai 2016 inclined-plate impingement reference + figure scripts Add pyrpod/plume/CaiImpingement2016.py, a plain-function verification reference (not a plume model) for Cai 2016 Section 4: exact diffuse-plate surface coefficients Cp,d/Cf1,d/Cf2,d/Cq,d (Eqs. 9-13), specular Cp,s (Eq. 14), and Eq. 15 plate-averaged properties, via tensor-product Gauss-Legendre quadrature over the exit disk with the overflow-safe exp(-S0^2) scaling style of RarefiedPlumeGasKinetics. The diffuse-wall re-emission density n_w (not printed for 3D in the paper) is derived from non-penetration as the A1-analog exit-disk flux integral; the interpretation is documented in the module docstring and validated against Figs. 17-18 magnitudes (diffuse peak 0.268 above the 0.2 innermost contour, specular peak 0.368 above 0.3 and higher at the impingement center, heat-flux peak 0.080 beneath the plate center, Cf1 stagnation line at tau ~ -2.1 -- all matching the paper). Built-in sanity checks (s-parity, Cf2 antisymmetry, Cp,s = 2x jet-only Cp, dblquad flux balance for n_w, quadrature-order convergence) pass at machine precision. Figure scripts plume_verification_test_28..32 reproduce Figs. 17-21 following the established manual-run conventions (design decision D5), with digitized-overlay slots fig17_*.csv..fig21_*.csv. Each figure also overlays the current PyRPOD approximation -- SimplifiedGasKinetics local field values fed through the Shen/Maxwellian wall formulas at the TRUE incidence angle (radial flow direction vs inclined plate normal), i.e. exactly what the Phase-2-fixed strike pipeline computes per face (sigma=0 for the specular figure). plume_impingement_error_summary.py extends the error-summary pattern with max/mean relative-error rows restricted to |ref| >= 5% of peak: pressure agrees to ~4% mean / 13% max, shear ~8% mean, heat flux ~18% mean -- the measured gap of the Maxwellian engineering chain vs the exact solution. Co-Authored-By: Claude Fable 5 --- pyrpod/plume/CaiImpingement2016.py | 409 ++++++++++++++++++ tests/plume/data/digitized/README.md | 7 + .../plume/plume_impingement_error_summary.py | 108 +++++ tests/plume/plume_impingement_utils.py | 240 ++++++++++ tests/plume/plume_verification_test_28.py | 32 ++ tests/plume/plume_verification_test_29.py | 32 ++ tests/plume/plume_verification_test_30.py | 33 ++ tests/plume/plume_verification_test_31.py | 32 ++ tests/plume/plume_verification_test_32.py | 30 ++ 9 files changed, 923 insertions(+) create mode 100644 pyrpod/plume/CaiImpingement2016.py create mode 100644 tests/plume/plume_impingement_error_summary.py create mode 100644 tests/plume/plume_impingement_utils.py create mode 100644 tests/plume/plume_verification_test_28.py create mode 100644 tests/plume/plume_verification_test_29.py create mode 100644 tests/plume/plume_verification_test_30.py create mode 100644 tests/plume/plume_verification_test_31.py create mode 100644 tests/plume/plume_verification_test_32.py diff --git a/pyrpod/plume/CaiImpingement2016.py b/pyrpod/plume/CaiImpingement2016.py new file mode 100644 index 0000000..5315365 --- /dev/null +++ b/pyrpod/plume/CaiImpingement2016.py @@ -0,0 +1,409 @@ +""" +Verification reference for Cai 2016, Section 4 (3D inclined-plate impingement). + +Cai, C., "Gaskinetic Modeling on Dilute Gaseous Plume Impingement Flows," +Aerospace 2016, 3(4), 43, doi:10.3390/aerospace3040043. + +This module is a PLAIN-FUNCTION reference implementation of the paper's exact +surface-property solutions (Eqs. 9-15 with the Appendix A integrals) for a +collisionless round jet impinging on an inclined, rectangular, flat plate. +It exists to verify PyRPOD's plume modeling against the paper -- it is NOT a +new plume model class and must not be wired into the strike pipeline; the +Cai & Wang 2012 field models in RarefiedPlumeGasKinetics.py stay the plume +models of record. + +Geometry and symbol conventions (paper Fig. 11) +----------------------------------------------- +* Nozzle exit: disk of radius R_0 centered at the origin in the Y-Z plane; + bulk flow along +X with speed ratio S_0 = U_0 / sqrt(2*R*T_0). +* Plate: center at (L, 0, 0), inclination angle alpha_0. Local plate + coordinates (s, tau) map to global coordinates as + X = L + tau*cos(alpha_0), Y = s, Z = tau*sin(alpha_0), + i.e. tau runs along the inclined direction in the X-Z plane + (|tau| <= H_0) and s along the horizontal Y direction (|s| <= W_0). + The plate unit normal facing the nozzle is (-sin(alpha_0), 0, cos(alpha_0)); + alpha_0 = 90 deg is a plate normal to the jet axis. +* Exit-disk integration point: E = (0, r*cos(theta), r*sin(theta)), so that + Eq. 10 reads Q^2 = |P - E|^2 / X^2 >= 1 (NOTE: unlike the 2012 paper's + special factor Q_2012 = X^2/|P-E|^2 <= 1, the 2016 paper's Q is the + reciprocal square root: a = S_0/Q = S_0*sqrt(Q_2012), the speed ratio + projected on the ray from E to the plate point). +* B1 = (Z - r*sin(theta))/X, B2 = (Y - r*cos(theta))/X, and + mu = sin(alpha_0) - B1*cos(alpha_0) = -(ray direction . outward normal)*Q, + so mu > 0 exactly when the ray from E strikes the FRONT face of the plate. + +Overflow safety: every Appendix-A factor A_k(a) carries a e^(a^2) term while +the solutions carry e^(-S_0^2); since a = S_0/Q <= S_0, the two are combined +into e^(a^2 - S_0^2) <= 1 (the same scaling style used by get_K_factor in +RarefiedPlumeGasKinetics.py), so no term can overflow for large S_0. + +Diffuse-wall re-emission density n_w (interpretation note) +---------------------------------------------------------- +The paper states n_w(s, tau) is set by non-penetration at the plate but does +not print the 3D expression (its refs. [35, 37] give the 2D principle). The +3D analog is derived here from first principles: the incoming number flux +from the jet at plate point P, + + Phi_in = n_0 * e^(-S_0^2) / (pi^(3/2) * beta_0 * X^2) + * Int_disk (A_1/Q^4) * mu * r dr dtheta, + +must equal the effusion flux of the wall Maxwellian at T_w, +Phi_out = n_w * sqrt(R*T_w/(2*pi)) = n_w / (2*sqrt(pi)*beta_w), giving + + n_w/n_0 = (2/sqrt(eps)) * e^(-S_0^2)/(pi * X^2) + * Int_disk (A_1/Q^4) * mu * r dr dtheta, eps = T_w/T_0. + +This is exactly "the A_1-analog exit-disk integral": the same reduction that +produces Eq. 9 (pressure ~ A_2/Q^5 * mu^2) and Eq. 13 (energy flux ~ +A_3/Q^4 * mu) yields number flux ~ A_1/Q^4 * mu. Validation against the +paper's figures: with this n_w the center-region magnitudes reproduce +Figs. 17-18 (diffuse Cp peaks just above the 0.2 innermost contour and +specular Cp above 0.3, specular slightly higher at the impingement center, +as the paper notes), and the wall energy-emission term of Eq. 13, +-eps^(3/2) * (n_w/n_0) / (sqrt(pi)*S_0^3), is the effusion energy flux +(2*k*T_w per emitted molecule) of the SAME n_w -- an independent +consistency check between Eqs. 9 and 13. + +Front-face visibility clamp +--------------------------- +The printed Eqs. 9-13 integrate over the whole exit disk. For the paper's +validation geometry (alpha_0 = 60 deg, L = 4D) every disk point sees the +front face (mu > 0 everywhere), so clamping is a no-op there. For grazing +geometries (small alpha_0 and/or small L, reached in the sweep study) part +of the disk falls behind the plate plane; those rays cannot deposit flux on +the front face, so integrand contributions with mu <= 0 are dropped. This +extends the formulas continuously to the grazing limit instead of letting +unphysical negative-flux contributions enter. + +All returned coefficients use the paper's normalization: pressure and shear +by n_0*m*U_0^2/2, heat flux by n_0*m*U_0^3/2 (m = molecular mass); they are +dimensionless and independent of n_0. +""" + +import numpy as np +from scipy.special import erf + +#: Gauss-Legendre order per axis for the exit-disk integrals. The integrand +#: is analytic on the compact disk (denominators bounded below by X^2 > 0), +#: so convergence is geometric; 48 nodes reach ~1e-12 for every geometry in +#: the study (verified by the order-doubling test in run_sanity_checks). +DEFAULT_ORDER = 48 + + +def _scaled_A_factors(a, S_0): + ''' + Appendix-A factors A_1, A_2, A_3 evaluated at a and pre-multiplied + by the solutions' e^(-S_0^2) prefactor, combined overflow-safely + into e^(a^2 - S_0^2) <= 1 (a = S_0/Q <= S_0). + + Parameters + ---------- + a : ndarray + projected speed ratio S_0/Q along each disk-to-plate ray + S_0 : float + molecular speed ratio at the nozzle exit + + Returns + ------- + tuple of ndarray + (e^(-S_0^2)*A_1(a), e^(-S_0^2)*A_2(a), e^(-S_0^2)*A_3(a)) + ''' + erf_term = 0.25 * np.sqrt(np.pi) * (1 + erf(a)) * np.exp(a ** 2 - S_0 ** 2) + exp_term = np.exp(-S_0 ** 2) + A1 = erf_term * (3 * a + 2 * a ** 3) + exp_term * (0.5 + 0.5 * a ** 2) + A2 = (erf_term * (1.5 + 6 * a ** 2 + 2 * a ** 4) + + exp_term * (1.25 * a + 0.5 * a ** 3)) + A3 = (erf_term * (2 * a ** 5 + 10 * a ** 3 + 7.5 * a) + + exp_term * (0.5 * a ** 4 + 2.25 * a ** 2 + 1.0)) + return A1, A2, A3 + + +def plate_point_coords(s, tau, alpha_0, L): + ''' + Global coordinates of plate points from local plate coordinates + (see module docstring for the convention). + + Parameters + ---------- + s, tau : float or ndarray + local plate coordinates (m); broadcast together + alpha_0 : float + plate inclination angle (rad) + L : float + center-to-center nozzle-to-plate distance (m) + + Returns + ------- + tuple of ndarray + (X, Y, Z) global coordinates (m) + ''' + s = np.asarray(s, dtype=float) + tau = np.asarray(tau, dtype=float) + X = L + tau * np.cos(alpha_0) + Y = np.broadcast_to(s, np.broadcast(s, tau).shape).copy() + Z = tau * np.sin(alpha_0) + return X, Y, Z + + +def surface_coefficients(X, Y, Z, S_0, alpha_0, eps, R_0, + order=DEFAULT_ORDER, chunk=256): + ''' + Exact surface coefficients of Cai 2016 Eqs. 9-14 at global plate + points (X, Y, Z), vectorized with tensor-product Gauss-Legendre + quadrature over the exit disk (r in [0, R_0], theta in [0, 2*pi]). + + Points with X <= 0 are behind the exit plane: the collisionless jet + carries no molecules there, so every coefficient is 0. + + Parameters + ---------- + X, Y, Z : ndarray + global plate-point coordinates (m); flattened internally, + outputs keep the input shape + S_0 : float + exit speed ratio + alpha_0 : float + plate inclination angle (rad) + eps : float + wall-to-exit temperature ratio T_w/T_0 + R_0 : float + nozzle exit radius (m) + order : int + Gauss-Legendre nodes per axis (see DEFAULT_ORDER) + chunk : int + plate points evaluated per vectorized block (memory control) + + Returns + ------- + dict of ndarray + 'Cp_d' : diffuse-plate pressure coefficient [Eq. 9] + 'Cf1_d' : diffuse shear along the inclined direction [Eq. 11] + 'Cf2_d' : diffuse shear along the horizontal (Y) [Eq. 12] + 'Cq_d' : diffuse heat-flux coefficient [Eq. 13] + 'Cp_s' : specular-plate pressure coefficient [Eq. 14] + 'nw' : wall re-emission density ratio n_w/n_0 (see docstring) + ''' + X = np.asarray(X, dtype=float) + shape = X.shape + Xf = X.ravel() + Yf = np.asarray(Y, dtype=float).ravel() + Zf = np.asarray(Z, dtype=float).ravel() + + nodes, weights = np.polynomial.legendre.leggauss(order) + r = 0.5 * R_0 * (nodes + 1) + w_r = 0.5 * R_0 * weights + th = np.pi * (nodes + 1) + w_th = np.pi * weights + Rn, Tn = np.meshgrid(r, th, indexing='ij') + WR = np.outer(w_r, w_th) * Rn # quadrature weight * r (Jacobian) + Ey = Rn * np.cos(Tn) # exit-disk point (0, Ey, Ez) + Ez = Rn * np.sin(Tn) + + sin_a, cos_a = np.sin(alpha_0), np.cos(alpha_0) + sin_2a, cos_2a = np.sin(2 * alpha_0), np.cos(2 * alpha_0) + + n_pts = Xf.size + I_p = np.zeros(n_pts) + I_f1 = np.zeros(n_pts) + I_f2 = np.zeros(n_pts) + I_q = np.zeros(n_pts) + I_n = np.zeros(n_pts) + ahead = Xf > 0.0 + + idx_all = np.nonzero(ahead)[0] + for start in range(0, idx_all.size, chunk): + idx = idx_all[start:start + chunk] + Xc = Xf[idx][:, None, None] + Yc = Yf[idx][:, None, None] + Zc = Zf[idx][:, None, None] + + dy = Yc - Ey + dz = Zc - Ez + Q2 = (Xc ** 2 + dy ** 2 + dz ** 2) / Xc ** 2 # Eq. 10 + Q = np.sqrt(Q2) + a = S_0 / Q + B1 = dz / Xc + B2 = dy / Xc + mu = sin_a - B1 * cos_a + front = mu > 0.0 # front-face visibility clamp + mu_p = np.where(front, mu, 0.0) + + A1, A2, A3 = _scaled_A_factors(a, S_0) + inv_Q4 = 1.0 / (Q2 * Q2) + inv_Q5 = inv_Q4 / Q + + I_p[idx] = np.sum(WR * A2 * inv_Q5 * mu_p ** 2, axis=(1, 2)) + I_f1[idx] = np.sum( + WR * A2 * inv_Q5 + * (0.5 * (1 - B1 ** 2) * sin_2a - B1 * cos_2a) + * front, axis=(1, 2)) + I_f2[idx] = np.sum(WR * A2 * B2 * inv_Q5 * mu_p, axis=(1, 2)) + I_q[idx] = np.sum(WR * A3 * inv_Q4 * mu_p, axis=(1, 2)) + I_n[idx] = np.sum(WR * A1 * inv_Q4 * mu_p, axis=(1, 2)) + + with np.errstate(divide='ignore', invalid='ignore'): + pref = np.where(ahead, 1.0 / (np.pi ** 1.5 * Xf ** 2), 0.0) + pref_pi = np.where(ahead, 1.0 / (np.pi * Xf ** 2), 0.0) + + Cp_jet = (2.0 / S_0 ** 2) * pref * I_p + Cf1 = (2.0 / S_0 ** 2) * pref * I_f1 + Cf2 = (2.0 / S_0 ** 2) * pref * I_f2 + nw = (2.0 / np.sqrt(eps)) * pref_pi * I_n + Cq = (pref_pi * I_q - eps ** 1.5 * nw) / (np.sqrt(np.pi) * S_0 ** 3) + Cp_d = Cp_jet + eps / (2.0 * S_0 ** 2) * nw + Cp_s = 2.0 * Cp_jet + + return {'Cp_d': Cp_d.reshape(shape), 'Cf1_d': Cf1.reshape(shape), + 'Cf2_d': Cf2.reshape(shape), 'Cq_d': Cq.reshape(shape), + 'Cp_s': Cp_s.reshape(shape), 'nw': nw.reshape(shape)} + + +def surface_coefficients_plate(s, tau, S_0, alpha_0, eps, R_0, L, + order=DEFAULT_ORDER): + ''' + Convenience wrapper of surface_coefficients over local plate + coordinates (s, tau); see plate_point_coords for the mapping. + ''' + s = np.asarray(s, dtype=float) + tau = np.asarray(tau, dtype=float) + shape = np.broadcast(s, tau).shape + sB = np.broadcast_to(s, shape) + tB = np.broadcast_to(tau, shape) + X, Y, Z = plate_point_coords(sB, tB, alpha_0, L) + return surface_coefficients(X, Y, Z, S_0, alpha_0, eps, R_0, order=order) + + +def averaged_coefficients(S_0, alpha_0, eps, R_0, L, W_0, H_0, + n_gl=64, order=DEFAULT_ORDER): + ''' + Plate-averaged properties of Eq. 15 by Gauss-Legendre quadrature + over the plate: CP, CF1, CF2, CQ, the moment coefficient + CM = 1/(2*H_0*S) * Int tau*Cp,d ds dtau, and s_cc = CM/CP. + + Parameters + ---------- + S_0, alpha_0, eps, R_0, L : as in surface_coefficients + W_0 : float + plate semi-width along s (m) + H_0 : float + plate semi-length along the inclined direction tau (m) + n_gl : int + Gauss-Legendre nodes per plate axis + order : int + exit-disk quadrature order per axis + + Returns + ------- + dict of float + keys 'CP', 'CF1', 'CF2', 'CQ', 'CM', 's_cc' + ''' + nodes, weights = np.polynomial.legendre.leggauss(n_gl) + s = W_0 * nodes + w_s = W_0 * weights + tau = H_0 * nodes + w_tau = H_0 * weights + Sg, Tg = np.meshgrid(s, tau, indexing='ij') + W2D = np.outer(w_s, w_tau) + area = 4.0 * W_0 * H_0 + + c = surface_coefficients_plate(Sg, Tg, S_0, alpha_0, eps, R_0, L, + order=order) + CP = np.sum(W2D * c['Cp_d']) / area + CF1 = np.sum(W2D * c['Cf1_d']) / area + CF2 = np.sum(W2D * c['Cf2_d']) / area + CQ = np.sum(W2D * c['Cq_d']) / area + CM = np.sum(W2D * Tg * c['Cp_d']) / (2.0 * H_0 * area) + s_cc = CM / CP if CP != 0.0 else np.nan + return {'CP': CP, 'CF1': CF1, 'CF2': CF2, 'CQ': CQ, 'CM': CM, + 's_cc': s_cc} + + +def run_sanity_checks(S_0=2.0, alpha_0=np.deg2rad(60.0), eps=1.5, + R_0=0.5, L=4.0, verbose=True): + ''' + Built-in verification of the implementation: + + 1. s-symmetry: Cp_d, Cf1_d, Cq_d even in s; Cf2_d odd in s + (Figs. 17-21 are left-right symmetric / antisymmetric). + 2. Cp_s = 2 x jet-only part of Cp_d (Eq. 14 vs Eq. 9), i.e. + Cp_s = 2*(Cp_d - eps/(2*S_0^2)*n_w/n_0). + 3. Wall-term flux balance: the incoming number-flux integral + recomputed independently with scipy.integrate.dblquad matches + the Gauss-Legendre n_w to ~1e-10 (validates both the quadrature + and the n_w reduction). + 4. Quadrature convergence: order 48 vs 96 agree to ~1e-12. + + Raises AssertionError on failure; returns a dict of the measured + deviations. + ''' + from scipy import integrate + + s = np.linspace(-3.5, 3.5, 15) + tau = np.linspace(-3.5, 3.5, 15) + Sg, Tg = np.meshgrid(s, tau, indexing='ij') + c = surface_coefficients_plate(Sg, Tg, S_0, alpha_0, eps, R_0, L) + c_mirror = surface_coefficients_plate(-Sg, Tg, S_0, alpha_0, eps, R_0, L) + + results = {} + for key, parity in [('Cp_d', 1), ('Cf1_d', 1), ('Cq_d', 1), + ('Cf2_d', -1)]: + dev = np.max(np.abs(c[key] - parity * c_mirror[key])) + scale = np.max(np.abs(c[key])) + results[f'symmetry_{key}'] = dev / scale + assert dev <= 1e-12 * scale, f'{key} s-parity violated: {dev}' + + dev = np.max(np.abs( + c['Cp_s'] - 2.0 * (c['Cp_d'] - eps / (2 * S_0 ** 2) * c['nw']))) + results['specular_vs_jet'] = dev + assert dev <= 1e-14, f'Cp_s != 2 x jet-only Cp_d: {dev}' + + # independent dblquad check of the n_w flux integral at spot points + sin_a, cos_a = np.sin(alpha_0), np.cos(alpha_0) + for s0, t0 in [(0.0, 0.0), (1.5, -2.0), (-2.5, 3.0)]: + X, Y, Z = plate_point_coords(s0, t0, alpha_0, L) + X, Y, Z = float(X), float(Y), float(Z) + + def flux_integrand(r, th): + dy = Y - r * np.cos(th) + dz = Z - r * np.sin(th) + Q = np.sqrt((X ** 2 + dy ** 2 + dz ** 2) / X ** 2) + a = S_0 / Q + A1, _, _ = _scaled_A_factors(np.asarray(a), S_0) + mu = sin_a - (dz / X) * cos_a + return float(A1) / Q ** 4 * max(mu, 0.0) * r + + I_n_ref, _ = integrate.dblquad( + flux_integrand, 0.0, 2 * np.pi, 0.0, R_0, + epsabs=1e-13, epsrel=1e-12) + nw_ref = (2.0 / np.sqrt(eps)) * I_n_ref / (np.pi * X ** 2) + nw_gl = float(surface_coefficients( + np.array([X]), np.array([Y]), np.array([Z]), + S_0, alpha_0, eps, R_0)['nw'][0]) + dev = abs(nw_gl - nw_ref) / nw_ref + results[f'nw_dblquad_s{s0}_tau{t0}'] = dev + assert dev <= 1e-9, f'n_w flux balance failed at ({s0},{t0}): {dev}' + + c_fine = surface_coefficients_plate(Sg, Tg, S_0, alpha_0, eps, R_0, L, + order=2 * DEFAULT_ORDER) + for key in ('Cp_d', 'Cf1_d', 'Cf2_d', 'Cq_d'): + scale = np.max(np.abs(c_fine[key])) + dev = np.max(np.abs(c[key] - c_fine[key])) / scale + results[f'convergence_{key}'] = dev + assert dev <= 1e-10, f'quadrature not converged for {key}: {dev}' + + if verbose: + for name, value in results.items(): + print(f' {name}: {value:.3e}') + return results + + +if __name__ == '__main__': + print('Cai 2016 reference implementation sanity checks ' + '(S0=2, alpha0=60 deg, eps=1.5, L=4D):') + run_sanity_checks() + print('all checks passed') + avg = averaged_coefficients(2.0, np.deg2rad(60.0), 1.5, 0.5, 4.0, + 4.0, 4.0) + print('Eq. 15 averaged coefficients at the paper conditions:') + for key, value in avg.items(): + print(f' {key} = {value:.6g}') diff --git a/tests/plume/data/digitized/README.md b/tests/plume/data/digitized/README.md index 9f43dd1..0efda09 100644 --- a/tests/plume/data/digitized/README.md +++ b/tests/plume/data/digitized/README.md @@ -5,6 +5,13 @@ Drop digitized curves from Cai & Wang 2012 (JSR 49(1), DOI (`tests/plume/plume_verification_test_04` ... `_27`) will overlay them automatically on their next run. No code changes are needed. +The same convention serves the Cai 2016 impingement figures +(Aerospace 3(4):43, DOI 10.3390/aerospace3040043) reproduced by +`plume_verification_test_28` ... `_32`: slots `fig17_*.csv` ... +`fig21_*.csv`, one CSV per digitized DSMC/analytic contour polyline in +plate coordinates (x = s, y = tau, both normalized by the nozzle +diameter), e.g. `fig17_dsmc_0p2.csv` for the Cp = 0.2 line of Fig. 17. + ## File convention - Name: `_.csv`, where `` is the paper figure diff --git a/tests/plume/plume_impingement_error_summary.py b/tests/plume/plume_impingement_error_summary.py new file mode 100644 index 0000000..9a49c9c --- /dev/null +++ b/tests/plume/plume_impingement_error_summary.py @@ -0,0 +1,108 @@ +# ======================== +# PyRPOD: tests/plume/plume_impingement_error_summary.py +# ======================== +# Extends the model-vs-model error-summary pattern of +# plume_verification_error_summary.py to the Cai 2016 inclined-plate +# impingement study (Figs. 17-21 conditions): maximum and mean relative +# differences between the exact reference surface coefficients +# (pyrpod/plume/CaiImpingement2016.py, Eqs. 9-14) and the current +# PyRPOD approximation (SimplifiedGasKinetics field + Shen/Maxwellian +# wall formulas at the true incidence angle -- the Phase-2-fixed strike +# pipeline chain), over the plate contour grid. +# +# As in the 2012 summary, each row restricts the comparison to the +# region where the reference magnitude is at least 5% of its peak: the +# largest relative differences otherwise occur near the plate corners +# and sign-change lines where the reference value is near zero. +# DSMC reference columns are pending digitized data +# (tests/plume/data/digitized/fig17_*.csv ... fig21_*.csv). +# +# The filename intentionally avoids pytest's collection patterns +# (test_*.py / *_test_*.py): this is a manual-run generator -- +# python tests/plume/plume_impingement_error_summary.py +# It writes tests/plume/output/cai2016_error_summary.csv and .md. + +import numpy as np + +import plume_impingement_utils as u +from pyrpod.plume import CaiImpingement2016 as cai + + +def build_rows(): + # module self-verification first: quadrature convergence, symmetry, + # specular identity, and the n_w flux balance (raises on failure) + cai.run_sanity_checks(u.S_0, u.ALPHA_0, u.EPS, u.R_0, u.L_PLATE, + verbose=False) + + ref = u.reference_grid() + chain_d = u.chain_grid() # diffuse chain (sigma = 1) + chain_s = u.chain_grid(sigma=0.0) # specular chain for Cp,s + + ref_shear = np.hypot(ref['Cf1_d'], ref['Cf2_d']) + comparisons = [ + ('Cp,d (Fig. 17)', chain_d['Cp'], ref['Cp_d']), + ('Cp,s (Fig. 18)', chain_s['Cp'], ref['Cp_s']), + ('Cf1,d (Fig. 19)', chain_d['Cf1'], ref['Cf1_d']), + ('Cf2,d (Fig. 20)', chain_d['Cf2'], ref['Cf2_d']), + ('|Cf,d| (shear magnitude)', chain_d['Cshear'], ref_shear), + ('Cq,d (Fig. 21)', chain_d['Cq'], ref['Cq_d']), + ] + + rows = [] + for quantity, chain, reference in comparisons: + mask = u.significant_mask(reference) + rows.append({ + 'quantity': quantity, + 'comparison': 'PyRPOD chain vs Cai 2016 exact', + 'max_rel_diff': u.base.max_rel_diff(chain[mask], + reference[mask]), + 'mean_rel_diff': u.mean_rel_diff(chain, reference, mask), + 'restriction': '|ref| >= 5% of peak', + 'vs_DSMC': 'pending digitized data', + }) + return rows + + +def write_summary(): + rows = build_rows() + u.base.OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + columns = ['quantity', 'comparison', 'max_rel_diff', 'mean_rel_diff', + 'restriction', 'vs_DSMC'] + + csv_path = u.base.OUTPUT_DIR / 'cai2016_error_summary.csv' + with open(csv_path, 'w', encoding='utf-8', newline='') as fh: + fh.write(','.join(columns) + '\n') + for row in rows: + fh.write(','.join( + f'{row[c]:.4g}' if isinstance(row[c], float) else str(row[c]) + for c in columns) + '\n') + + md_path = u.base.OUTPUT_DIR / 'cai2016_error_summary.md' + with open(md_path, 'w', encoding='utf-8', newline='') as fh: + fh.write('# Cai 2016 impingement error summary ' + '(reference vs PyRPOD chain)\n\n') + fh.write('Section-4 conditions: argon, D = 1 m, S0 = 2.0, ' + 'L = 4D, alpha0 = 60 deg, Tw/T0 = 1.5, 8 m x 8 m ' + 'plate, contour grid ' + f'{u.GRID_N}x{u.GRID_N}. DSMC reference columns are ' + 'pending digitized data.\n\n') + fh.write('| ' + ' | '.join(columns) + ' |\n') + fh.write('|' + '---|' * len(columns) + '\n') + for row in rows: + fh.write('| ' + ' | '.join( + f'{row[c]:.4g}' if isinstance(row[c], float) else str(row[c]) + for c in columns) + ' |\n') + fh.write('\nNotes: the PyRPOD chain is the strike-pipeline ' + 'computation (SimplifiedGasKinetics local field + ' + 'Shen/Maxwellian wall model at the true incidence ' + 'angle); Cp,s uses sigma = 0. Rows are restricted to ' + '|ref| >= 5% of the peak magnitude -- unrestricted ' + 'maxima occur where the reference is near zero (plate ' + 'corners, stagnation/sign-change lines), the same ' + 'caveat as the 2012 summary.\n') + return csv_path, md_path + + +if __name__ == '__main__': + for path in write_summary(): + print(f'wrote {path}') diff --git a/tests/plume/plume_impingement_utils.py b/tests/plume/plume_impingement_utils.py new file mode 100644 index 0000000..203467d --- /dev/null +++ b/tests/plume/plume_impingement_utils.py @@ -0,0 +1,240 @@ +# ======================== +# PyRPOD: tests/plume/plume_impingement_utils.py +# ======================== +# Shared helpers for the manual-run verification figure scripts +# plume_verification_test_28 ... _32, which reproduce Figs. 17-21 of +# Cai, C., "Gaskinetic Modeling on Dilute Gaseous Plume Impingement +# Flows," Aerospace 2016, 3(4), 43, doi:10.3390/aerospace3040043 +# (Section 4: round jet impinging on an inclined rectangular plate). +# +# Design notes +# ------------ +# * Paper validation conditions (Sec. 4): argon, D = 1.0 m, L = 4D, +# plate 8 m x 8 m (W0 = H0 = L), T0 = 200 K, S0 = 2.0, Tw = 300 K +# (eps = 1.5), alpha0 = 60 deg, fully diffuse plate. All plotted +# quantities are the paper's dimensionless coefficients, so n_0 is +# inert; 1e20 m^-3 matches plume_figure_utils. +# * The exact reference curves delegate to +# pyrpod/plume/CaiImpingement2016.py (Eqs. 9-14); that module's +# docstring records the n_w interpretation and its validation. +# * Every figure also overlays the CURRENT PYRPOD APPROXIMATION: +# SimplifiedGasKinetics local field values (evaluated at the plume +# coordinates distance / off-axis angle of each plate point) fed +# through the Shen/Maxwellian wall formulas at the TRUE incidence +# angle between the local flow direction (radial from the exit) and +# the inclined plate normal. This replicates exactly what the +# Phase-2-fixed strike pipeline computes per struck face, so the +# pipeline comparison in tests/rpod is apples-to-apples with these +# figures. The Shen shear magnitude is decomposed onto the plate's +# (s, tau) axes along the tangential projection of the flow +# direction to compare with the paper's Cf1/Cf2 components. +# * Specular figure (Fig. 18): the comparable Maxwellian setting is +# sigma = 0 (fully specular reflection) -- Shen's formulas then drop +# the wall-temperature term, matching the paper's remark that the +# plate temperature has no effect on Cp,s. +# * Digitized-overlay slots follow tests/plume/data/digitized/README.md: +# fig17_*.csv ... fig21_*.csv (absent files silently skipped). +# * Generic figure plumbing (save_figure, run_script, overlay_digitized, +# annotate_error, max_rel_diff) is reused from plume_figure_utils; +# only the paper-specific constants live here. + +import sys +from pathlib import Path + +import numpy as np + +_THIS_DIR = Path(__file__).resolve().parent +_REPO_ROOT = _THIS_DIR.parents[1] +if str(_REPO_ROOT) not in sys.path: # direct `python tests/plume/...py` runs + sys.path.insert(0, str(_REPO_ROOT)) + +import plume_figure_utils as base # noqa: E402 +from plume_figure_utils import plt # noqa: E402 +from pyrpod.plume import CaiImpingement2016 as cai # noqa: E402 +from pyrpod.plume.RarefiedPlumeGasKinetics import ( # noqa: E402 + AVOGADROS_NUMBER, + GAS_CONSTANT, + SimplifiedGasKinetics, + get_maxwellian_heat_transfer, + get_maxwellian_pressure, + get_maxwellian_shear_pressure, +) + +# Cai 2016 Section-4 validation conditions (argon) +R_SPECIFIC = 208.13 # J / (kg K) +GAMMA = 5.0 / 3.0 +T_0 = 200.0 # K +N_0 = 1.0e20 # m^-3 (inert for the coefficients) +D_NOZZLE = 1.0 # m +R_0 = D_NOZZLE / 2.0 # m +S_0 = 2.0 +U_0 = S_0 * np.sqrt(2.0 * R_SPECIFIC * T_0) # ~577.07 m/s +T_W = 300.0 # K +EPS = T_W / T_0 # 1.5 +ALPHA_0 = np.deg2rad(60.0) +L_PLATE = 4.0 * D_NOZZLE # m +W_0 = H_0 = L_PLATE # plate semi-width/semi-length (8 m x 8 m) +SIGMA = 1.0 + +M_PARTICLE = (GAS_CONSTANT / R_SPECIFIC) / AVOGADROS_NUMBER # kg +Q_DYN = 0.5 * N_0 * M_PARTICLE * U_0 ** 2 # n0*m*U0^2/2 (Pa) +Q_DYN_HEAT = 0.5 * N_0 * M_PARTICLE * U_0 ** 3 # n0*m*U0^3/2 (W/m^2) + +GRID_N = 81 # nodes per plate axis for contour grids + +THRUSTER_CHARACTERISTICS = {'d': D_NOZZLE, 've': U_0, 'R': R_SPECIFIC, + 'gamma': GAMMA, 'Te': T_0, 'n': N_0} + +_SIN_A, _COS_A = np.sin(ALPHA_0), np.cos(ALPHA_0) +#: plate inward normal (flow side -> plate) and in-plane axes in global coords +NORMAL_IN = np.array([_SIN_A, 0.0, -_COS_A]) +TANGENT_TAU = np.array([_COS_A, 0.0, _SIN_A]) +TANGENT_S = np.array([0.0, 1.0, 0.0]) + + +def plate_axes(n=GRID_N): + """(s, tau) axes in meters (= diameters, D = 1 m) spanning the plate.""" + return np.linspace(-W_0, W_0, n), np.linspace(-H_0, H_0, n) + + +def reference_grid(n=GRID_N): + """Exact Cai 2016 coefficients on the (s, tau) tensor grid; returns + dict of arrays shaped (len(tau), len(s)) for plt.contour.""" + s, tau = plate_axes(n) + Sg, Tg = np.meshgrid(s, tau) + return cai.surface_coefficients_plate(Sg, Tg, S_0, ALPHA_0, EPS, R_0, + L_PLATE) + + +def chain_point_loads(X, Y, Z, sigma=SIGMA): + """Current-PyRPOD approximation at one global plate point (X, Y, Z): + SimplifiedGasKinetics field state at the point's plume coordinates, + Shen/Maxwellian wall loads at the true incidence angle. + + Returns (pressure, shear_magnitude, heat_flux) in SI units. This is + the per-face computation of the Phase-2-fixed strike pipeline + (PlumeStrikeCalculator), kept in one helper so the figures and the + pipeline comparison test share a single definition. + """ + dist = float(np.sqrt(X * X + Y * Y + Z * Z)) + theta_pos = float(np.arctan2(np.hypot(Y, Z), X)) + plume = SimplifiedGasKinetics(dist, theta_pos, THRUSTER_CHARACTERISTICS, + T_W, sigma) + if theta_pos != 0.0: + n_ratio = plume.get_num_density_ratio() + T = T_0 * plume.get_temp_ratio() + u = plume.get_U_normalized() / plume.beta_0 + w = plume.get_W_normalized() / plume.beta_0 + U = float(np.hypot(u, w)) + else: + n_ratio = plume.get_num_density_centerline() + T = T_0 * plume.get_temp_centerline() + U = plume.get_velocity_centerline() / plume.beta_0 + rho_inf = N_0 * n_ratio * M_PARTICLE + S = U * plume.get_beta(T) + + cos_inc = (X * _SIN_A - Z * _COS_A) / dist # flow dir . inward normal + incidence = float(np.arccos(np.clip(cos_inc, -1.0, 1.0))) + + pressure = get_maxwellian_pressure(rho_inf, U, S, sigma, incidence, + T, T_W) + shear = abs(get_maxwellian_shear_pressure(rho_inf, U, S, sigma, + incidence)) + heat = get_maxwellian_heat_transfer(rho_inf, S, sigma, incidence, + T, T_W, R_SPECIFIC, GAMMA) + return pressure, shear, heat + + +def chain_grid(n=GRID_N, sigma=SIGMA): + """Current-PyRPOD approximation coefficients on the (s, tau) grid. + + Returns dict with 'Cp', 'Cf1', 'Cf2', 'Cshear', 'Cq' shaped like + reference_grid arrays. The shear magnitude from Shen's formula acts + along the tangential projection of the (radial) flow direction; its + components on the plate axes give Cf1 (inclined direction) and Cf2 + (horizontal), signed like the paper's Figs. 19-20. + """ + s, tau = plate_axes(n) + Sg, Tg = np.meshgrid(s, tau) + X, Y, Z = cai.plate_point_coords(Sg, Tg, ALPHA_0, L_PLATE) + + Cp = np.empty_like(X) + Cf1 = np.empty_like(X) + Cf2 = np.empty_like(X) + Csh = np.empty_like(X) + Cq = np.empty_like(X) + it = np.nditer(X, flags=['multi_index']) + for _ in it: + i = it.multi_index + x, y, z = float(X[i]), float(Y[i]), float(Z[i]) + p, sh, q = chain_point_loads(x, y, z, sigma=sigma) + dist = np.sqrt(x * x + y * y + z * z) + u_hat = np.array([x, y, z]) / dist + cos_inc = float(u_hat @ NORMAL_IN) + t_vec = u_hat - cos_inc * NORMAL_IN + t_norm = np.linalg.norm(t_vec) + t_hat = t_vec / t_norm if t_norm > 1e-12 else np.zeros(3) + Cp[i] = p / Q_DYN + Csh[i] = sh / Q_DYN + Cf1[i] = sh * float(t_hat @ TANGENT_TAU) / Q_DYN + Cf2[i] = sh * float(t_hat @ TANGENT_S) / Q_DYN + Cq[i] = q / Q_DYN_HEAT + return {'Cp': Cp, 'Cf1': Cf1, 'Cf2': Cf2, 'Cshear': Csh, 'Cq': Cq} + + +def mean_rel_diff(candidate, reference, mask): + """mean |candidate - reference| / |reference| over the masked entries.""" + candidate = np.asarray(candidate, dtype=float) + reference = np.asarray(reference, dtype=float) + m = mask & np.isfinite(candidate) & np.isfinite(reference) + return float(np.mean(np.abs(candidate[m] - reference[m]) + / np.abs(reference[m]))) + + +def significant_mask(reference, fraction=0.05): + """Entries where |reference| >= fraction * max|reference| -- the + region where relative errors are meaningful (the largest relative + differences otherwise sit in the near-zero plate corners, the same + caveat the 2012 error summary records).""" + reference = np.asarray(reference, dtype=float) + return np.abs(reference) >= fraction * np.max(np.abs(reference)) + + +def impingement_contour_figure(fig_stem, ref_field, chain_field, levels, + title, file_name, chain_label='PyRPOD chain', + fmt='%g'): + """Shared Figs. 17-21 builder: reference contours (solid black), + current-PyRPOD-approximation contours (dashed red, same levels), + digitized slots, and a max/mean relative-error annotation over the + significant region.""" + s, tau = plate_axes() + + fig, ax = plt.subplots(figsize=(6, 6)) + cs = ax.contour(s, tau, ref_field, levels=levels, colors='k', + linewidths=1.2) + ax.clabel(cs, fmt=fmt, fontsize=7) + ax.contour(s, tau, chain_field, levels=levels, colors='r', + linewidths=0.9, linestyles='dashed') + n_dig = base.overlay_digitized(ax, fig_stem, style='line') + + ax.set_xlim(-W_0, W_0) + ax.set_ylim(-H_0, H_0) + ax.set_xlabel('s') + ax.set_ylabel(r'$\tau$') + ax.set_title(title) + handles = [plt.Line2D([], [], color='k', lw=1.2, + label='Cai 2016 (exact)'), + plt.Line2D([], [], color='r', lw=0.9, ls='--', + label=chain_label)] + if n_dig: + handles.append(plt.Line2D([], [], color='k', lw=1.0, + label='DSMC (digitized)')) + ax.legend(handles=handles, fontsize=7, loc='upper right') + + mask = significant_mask(ref_field) + base.annotate_error( + ax, + f'max rel diff = {base.max_rel_diff(chain_field[mask], ref_field[mask]):.2g}\n' + f'mean rel diff = {mean_rel_diff(chain_field, ref_field, mask):.2g}\n' + r'(where $|C_{ref}| \geq$ 5% of peak)') + return base.save_figure(fig, file_name) diff --git a/tests/plume/plume_verification_test_28.py b/tests/plume/plume_verification_test_28.py new file mode 100644 index 0000000..9970ffc --- /dev/null +++ b/tests/plume/plume_verification_test_28.py @@ -0,0 +1,32 @@ +# ======================== +# PyRPOD: tests/plume/plume_verification_test_28.py +# ======================== +# Reproduces Cai 2016 Fig. 17: diffuse-plate surface pressure contours +# Cp,d(s, tau) for a round argon jet (D = 1 m, S0 = 2.0, T0 = 200 K) on +# the 8 m x 8 m plate at L = 4D, alpha0 = 60 deg, Tw/T0 = 1.5 (Eq. 9, +# solid black). Overlaid dashed red: the current PyRPOD approximation +# (SimplifiedGasKinetics field + Shen/Maxwellian wall pressure at the +# true incidence angle, sigma = 1) -- exactly what the fixed strike +# pipeline computes per struck face. +# +# Manual-run verification script (design decision D5): no pytest tests; +# run directly -- python tests/plume/plume_verification_test_28.py +# Digitized overlays looked for: fig17_*.csv (e.g. fig17_dsmc_0p2.csv, +# one CSV per digitized contour polyline). + +import plume_impingement_utils as u + + +def generate_figure(): + ref = u.reference_grid() + chain = u.chain_grid() + levels = [0.005, 0.01, 0.05, 0.1, 0.2] + return u.impingement_contour_figure( + 'fig17', ref['Cp_d'], chain['Cp'], levels, + 'Diffuse plate $C_{p,d}(s,\\tau)$, $S_0$=2.0, ' + '$T_w/T_0$=1.5, $\\alpha_0$=60$^\\circ$ (Fig. 17)', + 'fig17_diffuse_pressure') + + +if __name__ == '__main__': + u.base.run_script(generate_figure) diff --git a/tests/plume/plume_verification_test_29.py b/tests/plume/plume_verification_test_29.py new file mode 100644 index 0000000..f2260c1 --- /dev/null +++ b/tests/plume/plume_verification_test_29.py @@ -0,0 +1,32 @@ +# ======================== +# PyRPOD: tests/plume/plume_verification_test_29.py +# ======================== +# Reproduces Cai 2016 Fig. 18: specular-plate surface pressure contours +# Cp,s(s, tau) (Eq. 14, solid black) at the Section-4 conditions +# (argon, D = 1 m, S0 = 2.0, L = 4D, alpha0 = 60 deg). Overlaid dashed +# red: the current PyRPOD approximation with sigma = 0 -- Shen's +# Maxwellian pressure reduces to the fully specular reflection, and, +# consistent with the paper, the wall temperature drops out. +# +# Manual-run verification script (design decision D5): no pytest tests; +# run directly -- python tests/plume/plume_verification_test_29.py +# Digitized overlays looked for: fig18_*.csv (one CSV per digitized +# contour polyline). + +import plume_impingement_utils as u + + +def generate_figure(): + ref = u.reference_grid() + chain = u.chain_grid(sigma=0.0) + levels = [0.005, 0.01, 0.05, 0.1, 0.2, 0.3] + return u.impingement_contour_figure( + 'fig18', ref['Cp_s'], chain['Cp'], levels, + 'Specular plate $C_{p,s}(s,\\tau)$, $S_0$=2.0, ' + '$\\alpha_0$=60$^\\circ$ (Fig. 18)', + 'fig18_specular_pressure', + chain_label='PyRPOD chain ($\\sigma$=0)') + + +if __name__ == '__main__': + u.base.run_script(generate_figure) diff --git a/tests/plume/plume_verification_test_30.py b/tests/plume/plume_verification_test_30.py new file mode 100644 index 0000000..c401773 --- /dev/null +++ b/tests/plume/plume_verification_test_30.py @@ -0,0 +1,33 @@ +# ======================== +# PyRPOD: tests/plume/plume_verification_test_30.py +# ======================== +# Reproduces Cai 2016 Fig. 19: diffuse-plate friction coefficient +# Cf1,d(s, tau) along the inclined direction (Eq. 11, solid black) at +# the Section-4 conditions. The zero contour is the stagnation line +# (tau ~ -2): gas flows up-plate above it (positive Cf1) and down-plate +# below. Overlaid dashed red: the current PyRPOD approximation -- the +# Shen/Maxwellian shear magnitude at the true incidence angle, +# decomposed onto the inclined plate axis along the tangential +# projection of the radial flow direction. +# +# Manual-run verification script (design decision D5): no pytest tests; +# run directly -- python tests/plume/plume_verification_test_30.py +# Digitized overlays looked for: fig19_*.csv (one CSV per digitized +# contour polyline). + +import plume_impingement_utils as u + + +def generate_figure(): + ref = u.reference_grid() + chain = u.chain_grid() + levels = [-0.001, 0.0, 0.001, 0.01, 0.05] + return u.impingement_contour_figure( + 'fig19', ref['Cf1_d'], chain['Cf1'], levels, + 'Diffuse plate $C_{f_1,d}(s,\\tau)$, $S_0$=2.0, ' + '$T_w/T_0$=1.5, $\\alpha_0$=60$^\\circ$ (Fig. 19)', + 'fig19_diffuse_friction_tau') + + +if __name__ == '__main__': + u.base.run_script(generate_figure) diff --git a/tests/plume/plume_verification_test_31.py b/tests/plume/plume_verification_test_31.py new file mode 100644 index 0000000..9553910 --- /dev/null +++ b/tests/plume/plume_verification_test_31.py @@ -0,0 +1,32 @@ +# ======================== +# PyRPOD: tests/plume/plume_verification_test_31.py +# ======================== +# Reproduces Cai 2016 Fig. 20: diffuse-plate friction coefficient +# Cf2,d(s, tau) along the horizontal direction (Eq. 12, solid black) at +# the Section-4 conditions -- antisymmetric in s (gas flows outward to +# both sides of the vertical symmetry plane). Overlaid dashed red: the +# current PyRPOD approximation, decomposed onto the horizontal plate +# axis (see plume_verification_test_30 header). +# +# Manual-run verification script (design decision D5): no pytest tests; +# run directly -- python tests/plume/plume_verification_test_31.py +# Digitized overlays looked for: fig20_*.csv (one CSV per digitized +# contour polyline). + +import plume_impingement_utils as u + + +def generate_figure(): + ref = u.reference_grid() + chain = u.chain_grid() + levels = [-0.02, -0.01, -0.005, -0.001, + 0.001, 0.005, 0.01, 0.02] + return u.impingement_contour_figure( + 'fig20', ref['Cf2_d'], chain['Cf2'], levels, + 'Diffuse plate $C_{f_2,d}(s,\\tau)$, $S_0$=2.0, ' + '$T_w/T_0$=1.5, $\\alpha_0$=60$^\\circ$ (Fig. 20)', + 'fig20_diffuse_friction_s') + + +if __name__ == '__main__': + u.base.run_script(generate_figure) diff --git a/tests/plume/plume_verification_test_32.py b/tests/plume/plume_verification_test_32.py new file mode 100644 index 0000000..d533a64 --- /dev/null +++ b/tests/plume/plume_verification_test_32.py @@ -0,0 +1,30 @@ +# ======================== +# PyRPOD: tests/plume/plume_verification_test_32.py +# ======================== +# Reproduces Cai 2016 Fig. 21: diffuse-plate heat-flux coefficient +# Cq,d(s, tau) (Eq. 13, solid black) at the Section-4 conditions -- +# peak just beneath the plate center, as the paper notes. Overlaid +# dashed red: the current PyRPOD approximation (SimplifiedGasKinetics +# field + Shen/Maxwellian heat transfer at the true incidence angle). +# +# Manual-run verification script (design decision D5): no pytest tests; +# run directly -- python tests/plume/plume_verification_test_32.py +# Digitized overlays looked for: fig21_*.csv (one CSV per digitized +# contour polyline). + +import plume_impingement_utils as u + + +def generate_figure(): + ref = u.reference_grid() + chain = u.chain_grid() + levels = [0.001, 0.01, 0.05] + return u.impingement_contour_figure( + 'fig21', ref['Cq_d'], chain['Cq'], levels, + 'Diffuse plate $C_{q,d}(s,\\tau)$, $S_0$=2.0, ' + '$T_w/T_0$=1.5, $\\alpha_0$=60$^\\circ$ (Fig. 21)', + 'fig21_diffuse_heat_flux') + + +if __name__ == '__main__': + u.base.run_script(generate_figure) From b2632eb2f8982e8489ca987febab45803de9375e Mon Sep 17 00:00:00 2001 From: andytorrestb Date: Sat, 18 Jul 2026 23:03:57 -0500 Subject: [PATCH 02/11] Phase 2: true-incidence surface loads + Cai 2016 inclined-plate case Fix the orientation-blind load path in PlumeStrikeCalculator: the Shen/Maxwellian wall formulas now receive the TRUE incidence angle between the local flow direction (face centroid minus thruster exit -- the collisionless flow is radial) and the face unit normal, via the shared _surface_loads_with_incidence() helper used identically by the vectorized core and the scalar reference path. The positional off-axis theta keeps its two legitimate roles unchanged: the legacy 3.14-based wedge hit test (bit-for-bit untouched) and the SimplifiedGasKinetics field evaluation (where it is a plume-field coordinate). Previously the positional theta was also fed to the wall formulas as the incidence angle, so plate orientation never affected load magnitudes. Regression: all 96 tests/rpod + tests/plume tests pass unchanged. The expected-strike logs of rpod_integration_test_01..04 assert strike counts and struck-face IDs only, both of which are produced by the untouched hit test, so no expected logs needed updating. Load values do change on non-normal faces by design: e.g. faces seen at grazing positional angles but struck head-on now take full normal pressure. The scalar-vs-vectorized (plume_unit_test_04) and serial-vs-parallel (plume_integration_test_02) equivalences hold with the shared helper. New case case/plume/plume_inclined_plate mirrors plume_sq_plate: a generated 10368-face 8 m x 8 m plate rotated 60 deg about its center at (4, 0, 0) m (parametrized transform_inclined_plate.py for the Phase-3 sweep), single argon thruster at the paper conditions (S0 = 2.0, T0 = 200 K, D = 1 m, Tw = 300 K, sigma = 1), script-generated single-firing JFH, and wedge_theta = 1.55 so the gating wedge clears the whole plate (the 0.436 default would clip it). rpod_integration_test_07 runs the case through PlumeStrikeEstimationStudy, asserts all faces struck once, both compute paths identical, and pipeline-vs-analytic-chain consistency <2%, then documents the pipeline-vs-exact-reference gap quantitatively (Cp mean 3.8% / max 12%, shear mean 7.4%, heat flux mean 17.2% over significant faces) in tests/plume/output/cai2016_pipeline_error_summary.md -- acceptance is the measured gap, not a hard tolerance gate. Co-Authored-By: Claude Fable 5 --- case/plume/plume_inclined_plate/config.ini | 68 ++++++ .../jfh/generate_jfh_inclined_plate.py | 37 ++++ .../jfh/jfh_plume_inclined_plate.A | 3 + .../stl/inclined_plate_transformed.stl | Bin 0 -> 518484 bytes .../stl/transform_inclined_plate.py | 101 +++++++++ .../plume_inclined_plate/tcd/tcf_1_argon.txt | 6 + case/plume/plume_inclined_plate/tcd/tdf.csv | 2 + pyrpod/plume/PlumeStrikeCalculator.py | 90 +++++++- tests/rpod/rpod_integration_test_07.py | 204 ++++++++++++++++++ 9 files changed, 500 insertions(+), 11 deletions(-) create mode 100644 case/plume/plume_inclined_plate/config.ini create mode 100644 case/plume/plume_inclined_plate/jfh/generate_jfh_inclined_plate.py create mode 100644 case/plume/plume_inclined_plate/jfh/jfh_plume_inclined_plate.A create mode 100644 case/plume/plume_inclined_plate/stl/inclined_plate_transformed.stl create mode 100644 case/plume/plume_inclined_plate/stl/transform_inclined_plate.py create mode 100644 case/plume/plume_inclined_plate/tcd/tcf_1_argon.txt create mode 100644 case/plume/plume_inclined_plate/tcd/tdf.csv create mode 100644 tests/rpod/rpod_integration_test_07.py diff --git a/case/plume/plume_inclined_plate/config.ini b/case/plume/plume_inclined_plate/config.ini new file mode 100644 index 0000000..e8192e1 --- /dev/null +++ b/case/plume/plume_inclined_plate/config.ini @@ -0,0 +1,68 @@ +# Cai 2016 inclined-plate impingement verification case (Aerospace 3(4):43, +# Section 4): argon round jet, D = 1 m, S0 = 2.0 (ve = S0*sqrt(2*R*T0)), +# T0 = 200 K, plate 8 m x 8 m at L = 4D inclined alpha0 = 60 deg, Tw = 300 K, +# fully diffuse (sigma = 1). Geometry assets are generated by the scripts in +# stl/ and jfh/ (see their docstrings). + +# Visiting Vehicle for RPOD analysis +[vv] +stl_lm = cylinder_transformed.stl +stl_thruster = mold_funnel_transformed.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = inclined_plate_transformed.stl + +# surface wall temperature (Kelvin): Tw/T0 = 1.5 (paper Figs. 17-21) +surface_temp = 300 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# check plume constraints? 0 or 1 +check_constraints = 0 + +# max heat flux integral (J/m^2) +heat_flux_load = inf +heat_flux_window_size = 1 + +# max heat flux rate (W/m^2) +heat_flux = inf + +# max pressure load +normal_pressure_load = inf +normal_pressure_window_size = inf + +# max normal pressure (N/m^2) +normal_pressure = inf + +# max shear pressure (N/m^2) +shear_pressure = inf + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = Simplified + +# Gas-surface interaction model +surface_interaction = Maxwellian + +# Jet Firing History +[jfh] +jfh = jfh_plume_inclined_plate.A + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - single head-on thruster, no cant. +tcf = tcf_1_argon.txt + +# Thruster Definition File - argon at the paper conditions. +tdf = tdf.csv + +# Parameters for scaling plume geometry. +[plume] +radius = 25 +# ~89 deg: the default 0.436 (25 deg) would clip the plate -- its lower +# corners sit ~70 deg off-axis at the paper distance. 1.55 rad keeps the +# gating wedge clear of the whole plate at every sweep pose. +wedge_theta = 1.55 diff --git a/case/plume/plume_inclined_plate/jfh/generate_jfh_inclined_plate.py b/case/plume/plume_inclined_plate/jfh/generate_jfh_inclined_plate.py new file mode 100644 index 0000000..f228dab --- /dev/null +++ b/case/plume/plume_inclined_plate/jfh/generate_jfh_inclined_plate.py @@ -0,0 +1,37 @@ +"""Generate the single-firing JFH for the Cai 2016 inclined-plate case. + +One firing at the paper geometry (Aerospace 2016, 3(4):43, Section 4): the +visiting vehicle sits at the origin with an identity DCM, so its single +thruster (exit at the VV origin, no cant) fires along +X toward the plate +centered at (4, 0, 0) m. Firing time 1.0 s makes heat_flux_load equal the +heat-flux rate. + +JFH files are always generated by script, never hand-edited. Run from this +directory: python generate_jfh_inclined_plate.py +""" + +from pathlib import Path + +OUT_NAME = 'jfh_plume_inclined_plate.A' + +DCM_IDENTITY = ('1.000000e+00 0.000000e+00 0.000000e+00 ' + '0.000000e+00 1.000000e+00 0.000000e+00 ' + '0.000000e+00 0.000000e+00 1.000000e+00') + + +def write_jfh(path, firings): + """firings: list of (dt, t, dcm_str, xyz, thrusters).""" + lines = [f'offseted {len(firings)} 0', ' 0.000 0.000 0.000'] + for n, (dt, t, dcm, xyz, thrusters) in enumerate(firings, start=1): + xyz_str = ' '.join(f'{v:g}' for v in xyz) + thr_str = ' '.join(str(i) for i in thrusters) + lines.append(f' {n} {dt:g} {t:g} 0 {dcm} {xyz_str} 1 ' + f'{len(thrusters)} {thr_str}') + with open(path, 'w', newline='\n') as fh: + fh.write('\n'.join(lines) + '\n') + + +if __name__ == '__main__': + out_path = Path(__file__).resolve().parent / OUT_NAME + write_jfh(out_path, [(0.0, 1.0, DCM_IDENTITY, (0.0, 0.0, 0.0), [1])]) + print(f'saved {out_path}') diff --git a/case/plume/plume_inclined_plate/jfh/jfh_plume_inclined_plate.A b/case/plume/plume_inclined_plate/jfh/jfh_plume_inclined_plate.A new file mode 100644 index 0000000..610bf30 --- /dev/null +++ b/case/plume/plume_inclined_plate/jfh/jfh_plume_inclined_plate.A @@ -0,0 +1,3 @@ +offseted 1 0 + 0.000 0.000 0.000 + 1 0 1 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0 0 0 1 1 1 diff --git a/case/plume/plume_inclined_plate/stl/inclined_plate_transformed.stl b/case/plume/plume_inclined_plate/stl/inclined_plate_transformed.stl new file mode 100644 index 0000000000000000000000000000000000000000..6debcc468155f7d8a33810ce6275f86d5ea18932 GIT binary patch literal 518484 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 literal 0 HcmV?d00001 diff --git a/case/plume/plume_inclined_plate/stl/transform_inclined_plate.py b/case/plume/plume_inclined_plate/stl/transform_inclined_plate.py new file mode 100644 index 0000000..dc6d9be --- /dev/null +++ b/case/plume/plume_inclined_plate/stl/transform_inclined_plate.py @@ -0,0 +1,101 @@ +"""Generate the inclined-plate target mesh for the Cai 2016 verification case. + +Builds a single-sided, uniformly triangulated rectangular plate matching the +paper's Section-4 geometry (Aerospace 2016, 3(4):43): an 8 m x 8 m plate whose +center sits at (L, 0, 0) = (4, 0, 0) m from the nozzle exit, inclined by +alpha0 = 60 deg. The thruster fires along +X from a head-on JFH pose (VV at +the origin, identity DCM), so the global frame coincides with the paper's +nozzle frame and with pyrpod/plume/CaiImpingement2016.py: + + plate point(s, tau) = center + s * (0, 1, 0) + tau * (cos a0, 0, sin a0) + +Every face normal points toward the thruster, (-sin a0, 0, cos a0), which the +strike pipeline's facing test (surface_dot_plume < 0) requires; the script +asserts this before saving. + +Angle, distance, plate size, and mesh resolution are parametrized for the +Phase-3 sweep reuse. Defaults reproduce the paper case with 2 * 72^2 = 10368 +faces (~0.11 m elements): contour-quality resolution at a per-face kinetics +cost that keeps a single firing in the tens of seconds. + +Run from this directory: python transform_inclined_plate.py +""" + +import argparse +from pathlib import Path + +import numpy as np +from stl import mesh + + +def build_plate_mesh(alpha0_deg=60.0, center=(4.0, 0.0, 0.0), + half_width=4.0, half_height=4.0, n_div=72): + """Return a numpy-stl Mesh of the inclined plate. + + Parameters + ---------- + alpha0_deg : float + plate inclination angle alpha0 (deg); 90 = normal to the jet axis + center : sequence of float + plate center in the global/nozzle frame (m) + half_width : float + semi-width W0 along the horizontal s direction (m) + half_height : float + semi-length H0 along the inclined tau direction (m) + n_div : int + quad divisions per axis (faces = 2 * n_div^2) + """ + a0 = np.deg2rad(alpha0_deg) + center = np.asarray(center, dtype=float) + t_s = np.array([0.0, 1.0, 0.0]) + t_tau = np.array([np.cos(a0), 0.0, np.sin(a0)]) + normal = np.array([-np.sin(a0), 0.0, np.cos(a0)]) + + s_edges = np.linspace(-half_width, half_width, n_div + 1) + tau_edges = np.linspace(-half_height, half_height, n_div + 1) + + def point(s, tau): + return center + s * t_s + tau * t_tau + + data = np.zeros(2 * n_div * n_div, dtype=mesh.Mesh.dtype) + k = 0 + for i in range(n_div): + for j in range(n_div): + p00 = point(s_edges[i], tau_edges[j]) + p10 = point(s_edges[i + 1], tau_edges[j]) + p01 = point(s_edges[i], tau_edges[j + 1]) + p11 = point(s_edges[i + 1], tau_edges[j + 1]) + # wound so cross(v1-v0, v2-v0) points along `normal` + data['vectors'][k] = np.array([p00, p01, p11]) + data['vectors'][k + 1] = np.array([p00, p11, p10]) + k += 2 + + plate = mesh.Mesh(data) + plate.update_normals() + unit_normals = plate.get_unit_normals() + assert np.allclose(unit_normals, normal, atol=1e-9), ( + 'face normals do not all point toward the thruster') + return plate + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument('--alpha0-deg', type=float, default=60.0) + parser.add_argument('--distance', type=float, default=4.0, + help='nozzle-to-plate-center distance L (m)') + parser.add_argument('--half-width', type=float, default=4.0) + parser.add_argument('--half-height', type=float, default=4.0) + parser.add_argument('--n-div', type=int, default=72) + parser.add_argument('--out', type=str, + default='inclined_plate_transformed.stl') + args = parser.parse_args() + + plate = build_plate_mesh(alpha0_deg=args.alpha0_deg, + center=(args.distance, 0.0, 0.0), + half_width=args.half_width, + half_height=args.half_height, + n_div=args.n_div) + out_path = Path(__file__).resolve().parent / args.out + plate.save(str(out_path)) + print(f'saved {out_path} ({len(plate.vectors)} faces, ' + f'alpha0 = {args.alpha0_deg} deg, L = {args.distance} m)') diff --git a/case/plume/plume_inclined_plate/tcd/tcf_1_argon.txt b/case/plume/plume_inclined_plate/tcd/tcf_1_argon.txt new file mode 100644 index 0000000..53e6e0e --- /dev/null +++ b/case/plume/plume_inclined_plate/tcd/tcf_1_argon.txt @@ -0,0 +1,6 @@ +1 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +T1 ARG 0 0 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +0 diff --git a/case/plume/plume_inclined_plate/tcd/tdf.csv b/case/plume/plume_inclined_plate/tcd/tdf.csv new file mode 100644 index 0000000..dc39035 --- /dev/null +++ b/case/plume/plume_inclined_plate/tcd/tdf.csv @@ -0,0 +1,2 @@ +#,name,prop,F,isp,MIB,m,mdot,ve,d,R,gamma,Te,rhoe,n +ARG,CAI2016,argon,1,1,1,1,0.001,577.0684534784414,1.0,208.13,1.6666666666666667,200,6.6329E-06,1.0E+20 diff --git a/pyrpod/plume/PlumeStrikeCalculator.py b/pyrpod/plume/PlumeStrikeCalculator.py index 755f8c9..4ac9463 100644 --- a/pyrpod/plume/PlumeStrikeCalculator.py +++ b/pyrpod/plume/PlumeStrikeCalculator.py @@ -11,9 +11,19 @@ Implementation notes: - compute_plume_strikes() runs a NumPy-vectorized strike-detection path by default. _compute_plume_strikes_scalar() preserves the original per-face - loop verbatim as a reference implementation for tests and benchmarking. + loop as a reference implementation for tests and benchmarking; the two + must produce identical strike arrays, struck-face IDs, and load values. - The vectorized core operates on plain serializable inputs (arrays, dicts, floats) so it can also run inside process-based workers. +- Surface loads use the TRUE incidence angle: the positional off-axis angle + theta locates a face in the plume field (SimplifiedGasKinetics evaluates + n, U, T there, and the legacy 3.14-based theta still gates the wedge hit + test, bit-for-bit unchanged), but the Shen/Maxwellian wall formulas + receive the angle between the local flow direction (face centroid minus + thruster exit -- the collisionless flow is radial) and the face unit + normal. Previously the positional theta was fed to the wall formulas as + the incidence angle, so plate orientation never affected load magnitudes; + _surface_loads_with_incidence() is the shared fix for both paths. Future work (no new dependencies planned): - Vectorize the SimplifiedGasKinetics evaluations for struck faces. @@ -26,7 +36,53 @@ from typing import Any, Dict, List, Optional, Sequence import numpy as np -from pyrpod.plume.RarefiedPlumeGasKinetics import SimplifiedGasKinetics +from pyrpod.plume.RarefiedPlumeGasKinetics import ( + AVOGADROS_NUMBER, + SimplifiedGasKinetics, + get_maxwellian_heat_transfer, + get_maxwellian_pressure, + get_maxwellian_shear_pressure, +) + + +def _surface_loads_with_incidence(simple_plume: SimplifiedGasKinetics, + incidence: float): + """Maxwellian surface loads for a struck face at the true incidence angle. + + simple_plume carries the plume-field state at the face's position (its + constructor theta is the positional off-axis angle -- a plume-field + coordinate); this helper extracts the same local field values the class's + own get_pressure/get_shear_pressure/get_heat_flux use (including their + exact-centerline branch at theta == 0) and feeds them to the Shen wall + formulas with `incidence`, the angle between the local flow direction + (radial from the thruster exit) and the face unit normal. + + Returns (pressure, shear, heat_flux) in SI units; shear is signed as + returned by get_maxwellian_shear_pressure (callers take abs, matching + the legacy accumulation). + """ + if simple_plume.theta != 0: # not on plume centerline + n_inf = simple_plume.n_0 * simple_plume.get_num_density_ratio() + T = simple_plume.T_0 * simple_plume.get_temp_ratio() + u = simple_plume.get_U_normalized() / simple_plume.beta_0 + w = simple_plume.get_W_normalized() / simple_plume.beta_0 + U = np.sqrt(u ** 2 + w ** 2) + else: # on plume centerline: exact closed forms + n_inf = simple_plume.n_0 * simple_plume.get_num_density_centerline() + T = simple_plume.T_0 * simple_plume.get_temp_centerline() + U = simple_plume.get_velocity_centerline() / simple_plume.beta_0 + rho_inf = n_inf * simple_plume.molar_mass / AVOGADROS_NUMBER + S = U * simple_plume.get_beta(T) + + sigma = simple_plume.sigma + T_w = simple_plume.T_w + pressure = get_maxwellian_pressure(rho_inf, U, S, sigma, incidence, + T, T_w) + shear = get_maxwellian_shear_pressure(rho_inf, U, S, sigma, incidence) + heat_flux = get_maxwellian_heat_transfer(rho_inf, S, sigma, incidence, + T, T_w, simple_plume.R, + simple_plume.gamma) + return pressure, shear, heat_flux def compute_face_centroids(vectors: np.ndarray) -> np.ndarray: @@ -159,14 +215,19 @@ def _compute_plume_strikes_core( sigma = plume_params['sigma'] t_type = thruster_data[thruster_id]['type'][0] metrics = thruster_metrics[t_type] + # True incidence angle between the local (radial) flow direction + # -unit_distance and the face unit normal; fed to the wall + # formulas in place of the positional theta (see module header). + incidence = np.arccos(np.clip( + (unit_distance * normals).sum(axis=1), -1.0, 1.0)) for idx in np.nonzero(hit)[0]: simple_plume = SimplifiedGasKinetics( norm_distance[idx], theta[idx], metrics, T_w, sigma ) - pressures[idx] += simple_plume.get_pressure() - shear = simple_plume.get_shear_pressure() + p, shear, hf = _surface_loads_with_incidence( + simple_plume, incidence[idx]) + pressures[idx] += p shear_stresses[idx] += abs(shear) - hf = simple_plume.get_heat_flux() heat_flux[idx] += hf heat_flux_load[idx] += hf * firing_time @@ -229,9 +290,12 @@ def _compute_plume_strikes_scalar( ) -> Dict[str, np.ndarray]: """Scalar reference implementation of compute_plume_strikes(). - Preserved verbatim from the original per-face loop. Kept for regression - tests, debugging, and benchmarking against the vectorized path; the two - must produce identical strike arrays and struck-face IDs. + Preserves the original per-face loop structure (the hit test is + bit-for-bit the legacy computation). Kept for regression tests, + debugging, and benchmarking against the vectorized path; the two must + produce identical strike arrays, struck-face IDs, and load values. + Surface loads use the true incidence angle via the shared + _surface_loads_with_incidence(), exactly as the vectorized core does. """ num_faces = len(target_mesh.vectors) strikes = np.zeros(num_faces) @@ -298,10 +362,14 @@ def _compute_plume_strikes_scalar( t_type = vv.thruster_data[thruster_id]['type'][0] thruster_metrics = vv.thruster_metrics[t_type] simple_plume = SimplifiedGasKinetics(norm_distance, theta, thruster_metrics, T_w, sigma) - pressures[idx] += simple_plume.get_pressure() - shear = simple_plume.get_shear_pressure() + # True incidence angle (see module header): local flow + # direction -unit_distance vs the face unit normal. + incidence = np.arccos(np.clip( + np.dot(np.squeeze(unit_distance), n), -1.0, 1.0)) + p, shear, hf = _surface_loads_with_incidence( + simple_plume, incidence) + pressures[idx] += p shear_stresses[idx] += abs(shear) - hf = simple_plume.get_heat_flux() heat_flux[idx] += hf heat_flux_load[idx] += hf * firing_time diff --git a/tests/rpod/rpod_integration_test_07.py b/tests/rpod/rpod_integration_test_07.py new file mode 100644 index 0000000..8bc1782 --- /dev/null +++ b/tests/rpod/rpod_integration_test_07.py @@ -0,0 +1,204 @@ +# ======================== +# PyRPOD: tests/rpod/rpod_integration_test_07.py +# ======================== +# Cai 2016 inclined-plate verification: runs case/plume/plume_inclined_plate +# (argon round jet, D = 1 m, S0 = 2.0, 8 m x 8 m plate at L = 4D inclined +# 60 deg, Tw/T0 = 1.5, sigma = 1) through PlumeStrikeEstimationStudy, +# converts the per-face dimensional loads to the paper's coefficient +# normalization (pressure/shear by n0*m*U0^2/2, heat flux by n0*m*U0^3/2), +# and compares face-by-face against the exact reference functions +# (pyrpod/plume/CaiImpingement2016.py, Eqs. 9-13) evaluated at the face +# centroids. +# +# Hard assertions cover what must be exact: +# - geometry: every plate face is struck exactly once (the whole plate +# sits inside the gating wedge/radius at this pose); +# - the vectorized and scalar strike paths agree (strike arrays exactly, +# kinetics arrays to 1e-12, per the plume_unit_test_04 convention); +# - internal consistency: the pipeline loads match the analytic +# PyRPOD-approximation chain of tests/plume/plume_impingement_utils +# (same SimplifiedGasKinetics field + Shen/Maxwellian wall model at the +# true incidence angle) to <2% -- the only difference is the hit test's +# legacy 3.14-based positional theta feeding the field evaluation. +# +# The pipeline-vs-reference gap itself is QUANTITATIVE DOCUMENTATION, not a +# hard tolerance gate: the Maxwellian chain is an engineering approximation +# of Cai's exact solution, and the deliverable is the measured gap. Max and +# mean relative errors (restricted to faces where the reference magnitude +# is >= 5% of its peak, as in the error summaries) are printed and appended +# to tests/plume/output/cai2016_pipeline_error_summary.md. +# +# Run: python -m pytest rpod/rpod_integration_test_07.py -s (from tests/) + +import sys +import time +import unittest +from pathlib import Path + +import numpy as np + +_TESTS_DIR = Path(__file__).resolve().parents[1] +if str(_TESTS_DIR / 'plume') not in sys.path: + sys.path.insert(0, str(_TESTS_DIR / 'plume')) +if str(_TESTS_DIR.parent) not in sys.path: # repo root for direct runs + sys.path.insert(0, str(_TESTS_DIR.parent)) + +import plume_impingement_utils as piu # noqa: E402 +from pyrpod.mission import MissionEnvironment # noqa: E402 +from pyrpod.plume import CaiImpingement2016 as cai # noqa: E402 +from pyrpod.plume.PlumeStrikeCalculator import ( # noqa: E402 + _compute_plume_strikes_scalar, + compute_face_centroids, + compute_plume_strikes, +) +from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy # noqa: E402 +from pyrpod.vehicle import TargetVehicle, VisitingVehicle # noqa: E402 + +CASE_DIR = '../case/plume/plume_inclined_plate/' +SUMMARY_PATH = _TESTS_DIR / 'plume' / 'output' / \ + 'cai2016_pipeline_error_summary.md' + + +def face_areas(vectors): + v0, v1, v2 = vectors[:, 0], vectors[:, 1], vectors[:, 2] + return 0.5 * np.linalg.norm(np.cross(v1 - v0, v2 - v0), axis=1) + + +class InclinedPlateVerificationChecks(unittest.TestCase): + + def test_pipeline_vs_cai2016_reference(self): + # 1. Set up (driver pattern of rpod_integration_test_05) + jfh = JetFiringHistory.JetFiringHistory(CASE_DIR) + jfh.read_jfh() + + tv = TargetVehicle.TargetVehicle(CASE_DIR) + tv.set_stl() + + vv = VisitingVehicle.VisitingVehicle(CASE_DIR) + vv.set_thruster_config() + vv.set_thruster_metrics() + + me = MissionEnvironment.MissionEnvironment(CASE_DIR) + + study = PlumeStrikeEstimationStudy.PlumeStrikeEstimationStudy(me) + study.study_init(jfh, tv, vv) + + # 2. Execute the full pipeline (writes VTK to the case results dir) + t0 = time.perf_counter() + firing_data = study.jfh_plume_strikes() + t_pipeline = time.perf_counter() - t0 + + self.assertEqual(list(firing_data.keys()), ['1']) + result = firing_data['1'] + + target = tv.mesh + n_faces = len(target.vectors) + centroids = compute_face_centroids(target.vectors) + normals = target.get_unit_normals() + + # 3. Geometry: the whole plate lies inside the gating wedge/radius + # at the paper pose, so every face must be struck exactly once. + np.testing.assert_array_equal(result['strikes'], np.ones(n_faces)) + + # 4. Vectorized and scalar paths must agree (shared incidence fix). + step = {'thrusters': jfh.JFH[0]['thrusters'], + 'xyz': np.array(jfh.JFH[0]['xyz']), + 'dcm': np.array(jfh.JFH[0]['dcm']), + 't': float(jfh.JFH[0]['t'])} + vec = compute_plume_strikes(target, normals, vv, step, me, + face_centroids=centroids) + scal = _compute_plume_strikes_scalar(target, normals, vv, step, me) + np.testing.assert_array_equal(vec['strikes'], scal['strikes']) + for key in ('pressures', 'shear_stress', 'heat_flux_rate'): + self.assertTrue(np.allclose(vec[key], scal[key], + rtol=1e-12, atol=1e-12), + msg=f'{key}: scalar vs vectorized differ') + + # 5. Convert to the paper's coefficients. Thruster at the origin + # firing +X, so centroid coordinates are the paper's (X, Y, Z). + Cp_pipe = result['pressures'] / piu.Q_DYN + Csh_pipe = result['shear_stress'] / piu.Q_DYN + Cq_pipe = result['heat_flux_rate'] / piu.Q_DYN_HEAT + # firing time is 1 s, so the load equals the rate + self.assertTrue(np.allclose(result['heat_flux_load'], + result['heat_flux_rate'])) + + # 6. Exact reference at the face centroids. + ref = cai.surface_coefficients( + centroids[:, 0], centroids[:, 1], centroids[:, 2], + piu.S_0, piu.ALPHA_0, piu.EPS, piu.R_0) + ref_shear = np.hypot(ref['Cf1_d'], ref['Cf2_d']) + + # 7. Internal consistency vs the Phase-1 analytic chain on a face + # subsample (every 97th face covers the plate quasi-uniformly). + # Denominators are floored at 5% of the subsample peak: the + # Maxwellian heat flux changes sign on the plate (adiabatic-wall + # crossover), so a purely relative test would divide by ~0 there + # while the absolute agreement stays tight. + sub = np.arange(0, n_faces, 97) + chain = np.array([piu.chain_point_loads(*centroids[i]) for i in sub]) + for pipe_vals, chain_vals, name in [ + (result['pressures'][sub], chain[:, 0], 'pressure'), + (result['shear_stress'][sub], chain[:, 1], 'shear'), + (result['heat_flux_rate'][sub], chain[:, 2], 'heat flux')]: + scale = np.maximum(np.abs(chain_vals), + 0.05 * np.max(np.abs(chain_vals))) + rel = np.abs(pipe_vals - chain_vals) / scale + self.assertLess(np.max(rel), 2e-2, + msg=f'pipeline diverged from analytic chain ' + f'({name}): max rel {np.max(rel):.3g}') + + # 8. Quantitative documentation of the pipeline-vs-reference gap. + areas = face_areas(target.vectors) + rows = [] + for name, pipe, reference in [ + ('Cp,d', Cp_pipe, ref['Cp_d']), + ('|Cf,d|', Csh_pipe, ref_shear), + ('Cq,d', Cq_pipe, ref['Cq_d'])]: + mask = piu.significant_mask(reference) + rel = np.abs(pipe[mask] - reference[mask]) / np.abs(reference[mask]) + rows.append((name, float(np.max(rel)), float(np.mean(rel)), + float(np.sum(pipe * areas) / np.sum(areas)), + float(np.sum(reference * areas) / np.sum(areas)))) + print(f'[cai2016] {name}: max rel err {np.max(rel):.3f}, ' + f'mean rel err {np.mean(rel):.3f} ' + f'(over {mask.sum()}/{n_faces} significant faces)') + print(f'[cai2016] pipeline wall time: {t_pipeline:.1f} s ' + f'({n_faces} faces, 1 firing)') + + SUMMARY_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(SUMMARY_PATH, 'w', encoding='utf-8', newline='') as fh: + fh.write('# Cai 2016 pipeline error summary ' + '(strike pipeline vs exact reference)\n\n') + fh.write('Written by tests/rpod/rpod_integration_test_07.py: ' + 'face-by-face comparison of the ' + 'PlumeStrikeEstimationStudy loads (Simplified ' + 'kinetics + Maxwellian wall model at the true ' + 'incidence angle) against ' + 'pyrpod/plume/CaiImpingement2016.py at the paper ' + f'conditions; {n_faces} faces, pipeline wall time ' + f'{t_pipeline:.1f} s.\n\n') + fh.write('| quantity | max_rel_err | mean_rel_err | ' + 'plate avg (pipeline) | plate avg (reference) |\n') + fh.write('|---|---|---|---|---|\n') + for name, mx, mn, avg_p, avg_r in rows: + fh.write(f'| {name} | {mx:.4g} | {mn:.4g} | {avg_p:.4g} | ' + f'{avg_r:.4g} |\n') + fh.write('\nRelative errors restricted to faces with ' + '|reference| >= 5% of its peak (the same near-zero ' + 'caveat as the other summaries). The gap is the ' + 'documented accuracy of the Maxwellian engineering ' + 'chain vs the exact collisionless solution -- not a ' + 'regression gate.\n') + print(f'[cai2016] wrote {SUMMARY_PATH}') + + # Loose envelope so a future regression that breaks the chain + # entirely (e.g. reverting to positional theta) fails loudly: + # with the orientation-blind bug, mean Cp error was ~3x larger. + self.assertLess(rows[0][2], 0.15, + msg='mean Cp error vs reference far above the ' + 'documented Maxwellian-chain gap') + + +if __name__ == '__main__': + unittest.main() From ac327948b2faf5574e77c2e3324c2dc63bfa2196 Mon Sep 17 00:00:00 2001 From: andytorrestb Date: Sat, 18 Jul 2026 23:24:39 -0500 Subject: [PATCH 03/11] Phase 3: angle x distance sweep of the inclined-plate impingement study generate_sweep_jfh.py emits ONE 95-firing JFH: the plate stays stationary while the visiting vehicle is swept over arcs of radius L about the plate center with the thruster aimed at the center -- alpha from -90 to +90 deg in 10 deg steps (0 = head-on = paper alpha0 = 90 deg, alpha_paper = 90 - |alpha|) x L/D in {2, 4, 6, 8, 10}. The JFH DCM first column carries the thruster axis per the pipeline plume-normal convention; xyz precision raised to 9 significant digits. scripts/inclined_plate_sweep_study.py (standalone; pyrpod/mdao untouched, no optimization) runs each firing through compute_plume_strikes -- the per-firing pipeline core -- keeping per-firing results separated from any cumulative accumulation, and writes to case/plume/plume_inclined_plate/results/sweep/ (gitignored): Eq.-15 plate-averaged CP/CF1/CF2/CQ/CM/s_cc per pose from the pipeline AND the exact Cai 2016 reference on the same axes, peak per-face Cp/|Cf|/Cq curves, and a full CSV. Pipeline |shear| is decomposed onto the plate axes along the tangential projection of the radial flow direction; alpha > 0 poses are mirror images of the paper geometry, so CF1/CM are sign-flipped into the paper convention. Built-in sanity checks: mirror symmetry in +/-alpha holds to 2.15e-07 (the JFH file's 6-digit DCM precision); CF2 vanishes to the mesh's O(h^2) triangulation asymmetry; the +/-90 deg edge-on poses average to ~zero load as expected (CP a few percent of head-on). Documented degeneracy: at exactly edge-on the facing test dot product is 0, so float32 mesh normals make strike membership epsilon-arbitrary and a few near-nozzle faces carry large grazing values at L/D = 2; those rows stay in the CSV/report but are excluded from the plots and symmetry assertions. Measured agreement: pipeline plate-averaged CP tracks the exact reference within ~1-3% over the sweep and CF1 within ~5%. Timings on the 10368-face mesh: 15.6 s for all 95 firings (164 ms/firing) plus 86 s for the reference quadrature. Co-Authored-By: Claude Fable 5 --- .../jfh/generate_jfh_inclined_plate.py | 2 +- .../jfh/generate_sweep_jfh.py | 77 ++++ .../jfh/jfh_inclined_plate_sweep.A | 97 +++++ scripts/inclined_plate_sweep_study.py | 333 ++++++++++++++++++ 4 files changed, 508 insertions(+), 1 deletion(-) create mode 100644 case/plume/plume_inclined_plate/jfh/generate_sweep_jfh.py create mode 100644 case/plume/plume_inclined_plate/jfh/jfh_inclined_plate_sweep.A create mode 100644 scripts/inclined_plate_sweep_study.py diff --git a/case/plume/plume_inclined_plate/jfh/generate_jfh_inclined_plate.py b/case/plume/plume_inclined_plate/jfh/generate_jfh_inclined_plate.py index f228dab..249f71d 100644 --- a/case/plume/plume_inclined_plate/jfh/generate_jfh_inclined_plate.py +++ b/case/plume/plume_inclined_plate/jfh/generate_jfh_inclined_plate.py @@ -23,7 +23,7 @@ def write_jfh(path, firings): """firings: list of (dt, t, dcm_str, xyz, thrusters).""" lines = [f'offseted {len(firings)} 0', ' 0.000 0.000 0.000'] for n, (dt, t, dcm, xyz, thrusters) in enumerate(firings, start=1): - xyz_str = ' '.join(f'{v:g}' for v in xyz) + xyz_str = ' '.join(f'{v:.9g}' for v in xyz) thr_str = ' '.join(str(i) for i in thrusters) lines.append(f' {n} {dt:g} {t:g} 0 {dcm} {xyz_str} 1 ' f'{len(thrusters)} {thr_str}') diff --git a/case/plume/plume_inclined_plate/jfh/generate_sweep_jfh.py b/case/plume/plume_inclined_plate/jfh/generate_sweep_jfh.py new file mode 100644 index 0000000..d559b7b --- /dev/null +++ b/case/plume/plume_inclined_plate/jfh/generate_sweep_jfh.py @@ -0,0 +1,77 @@ +"""Generate the ONE sweep JFH for the Phase-3 angle x distance study. + +The plate is STATIONARY (target vehicles do not move): the visiting vehicle +is swept instead. For each firing the VV (single thruster at the VV origin, +no cant) is placed on the arc of radius L about the plate center +C = (4, 0, 0), with the thruster axis aimed at C and the JFH DCM set so the +pipeline's plume normal (first column of the DCM, with the identity TCF) +points along that axis. + +Pose parametrization, all in the plate's tilt plane (global X-Z): + + d_hat(alpha) = cos(alpha) * n_hat + sin(alpha) * t_hat + VV position = C + L * d_hat(alpha), thruster axis = -d_hat(alpha) + +where n_hat = (-sin 60, 0, cos 60) is the plate normal facing the nozzle +side and t_hat = (cos 60, 0, sin 60) the plate's inclined tangent. +alpha = 0 is HEAD-ON (thruster axis along the plate normal; the paper's +alpha0 = 90 deg); the sign is the tilt direction, mapping +alpha_paper = 90 deg - |alpha|. Results must be mirror-symmetric in ++/-alpha (the plate is square); +/-90 deg is edge-on/degenerate and is +kept to confirm ~zero struck faces. + +Sweep: alpha from -90 to +90 deg in 10 deg steps (19 angles) x +L/D in {2, 4, 6, 8, 10} (D = 1 m), enumerated distance-major: +firing index = i_L * 19 + i_alpha + 1. 95 firings total, firing time +1.0 s each. + +JFH files are always generated by script, never hand-edited. Run from this +directory: python generate_sweep_jfh.py +""" + +from pathlib import Path + +import numpy as np + +from generate_jfh_inclined_plate import write_jfh + +OUT_NAME = 'jfh_inclined_plate_sweep.A' + +PLATE_CENTER = np.array([4.0, 0.0, 0.0]) +ALPHA0_DEG = 60.0 +ALPHAS_DEG = np.arange(-90.0, 90.0 + 1e-9, 10.0) +L_OVER_D = [2.0, 4.0, 6.0, 8.0, 10.0] + + +def pose_for(alpha_deg, L): + """(vv_position, dcm) for one sweep firing; dcm's first column is the + thruster axis (see the pipeline's plume-normal convention).""" + a0 = np.deg2rad(ALPHA0_DEG) + n_hat = np.array([-np.sin(a0), 0.0, np.cos(a0)]) + t_hat = np.array([np.cos(a0), 0.0, np.sin(a0)]) + alpha = np.deg2rad(alpha_deg) + d_hat = np.cos(alpha) * n_hat + np.sin(alpha) * t_hat + position = PLATE_CENTER + L * d_hat + axis = -d_hat # aimed at the plate center + # right-handed triad; the axis always lies in the X-Z plane, so the + # global Y axis is a valid second column + c1 = np.array([0.0, 1.0, 0.0]) + c2 = np.cross(axis, c1) + dcm = np.column_stack([axis, c1, c2]) + return position, dcm + + +def dcm_string(dcm): + return ' '.join(f'{v:.6e}' for v in np.asarray(dcm).ravel()) + + +if __name__ == '__main__': + firings = [] + for L in L_OVER_D: + for alpha_deg in ALPHAS_DEG: + position, dcm = pose_for(alpha_deg, L) + firings.append((0.0, 1.0, dcm_string(dcm), tuple(position), [1])) + out_path = Path(__file__).resolve().parent / OUT_NAME + write_jfh(out_path, firings) + print(f'saved {out_path} ({len(firings)} firings: ' + f'{len(ALPHAS_DEG)} angles x {len(L_OVER_D)} distances)') diff --git a/case/plume/plume_inclined_plate/jfh/jfh_inclined_plate_sweep.A b/case/plume/plume_inclined_plate/jfh/jfh_inclined_plate_sweep.A new file mode 100644 index 0000000..3889cef --- /dev/null +++ b/case/plume/plume_inclined_plate/jfh/jfh_inclined_plate_sweep.A @@ -0,0 +1,97 @@ +offseted 95 0 + 0.000 0.000 0.000 + 1 0 1 0 5.000000e-01 0.000000e+00 -8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 8.660254e-01 0.000000e+00 5.000000e-01 3 0 -1.73205081 1 1 1 + 2 0 1 0 6.427876e-01 0.000000e+00 -7.660444e-01 -0.000000e+00 1.000000e+00 0.000000e+00 7.660444e-01 0.000000e+00 6.427876e-01 2.71442478 0 -1.53208889 1 1 1 + 3 0 1 0 7.660444e-01 0.000000e+00 -6.427876e-01 -0.000000e+00 1.000000e+00 0.000000e+00 6.427876e-01 0.000000e+00 7.660444e-01 2.46791111 0 -1.28557522 1 1 1 + 4 0 1 0 8.660254e-01 0.000000e+00 -5.000000e-01 -0.000000e+00 1.000000e+00 0.000000e+00 5.000000e-01 0.000000e+00 8.660254e-01 2.26794919 0 -1 1 1 1 + 5 0 1 0 9.396926e-01 0.000000e+00 -3.420201e-01 -0.000000e+00 1.000000e+00 0.000000e+00 3.420201e-01 0.000000e+00 9.396926e-01 2.12061476 0 -0.684040287 1 1 1 + 6 0 1 0 9.848078e-01 0.000000e+00 -1.736482e-01 -0.000000e+00 1.000000e+00 0.000000e+00 1.736482e-01 0.000000e+00 9.848078e-01 2.03038449 0 -0.347296355 1 1 1 + 7 0 1 0 1.000000e+00 0.000000e+00 2.220446e-16 -0.000000e+00 1.000000e+00 -0.000000e+00 -2.220446e-16 0.000000e+00 1.000000e+00 2 0 4.4408921e-16 1 1 1 + 8 0 1 0 9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -1.736482e-01 0.000000e+00 9.848078e-01 2.03038449 0 0.347296355 1 1 1 + 9 0 1 0 9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -3.420201e-01 0.000000e+00 9.396926e-01 2.12061476 0 0.684040287 1 1 1 + 10 0 1 0 8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -5.000000e-01 0.000000e+00 8.660254e-01 2.26794919 0 1 1 1 1 + 11 0 1 0 7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.427876e-01 0.000000e+00 7.660444e-01 2.46791111 0 1.28557522 1 1 1 + 12 0 1 0 6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -7.660444e-01 0.000000e+00 6.427876e-01 2.71442478 0 1.53208889 1 1 1 + 13 0 1 0 5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -8.660254e-01 0.000000e+00 5.000000e-01 3 0 1.73205081 1 1 1 + 14 0 1 0 3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.396926e-01 0.000000e+00 3.420201e-01 3.31595971 0 1.87938524 1 1 1 + 15 0 1 0 1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.848078e-01 0.000000e+00 1.736482e-01 3.65270364 0 1.96961551 1 1 1 + 16 0 1 0 -0.000000e+00 0.000000e+00 1.000000e+00 -0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 4 0 2 1 1 1 + 17 0 1 0 -1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.848078e-01 0.000000e+00 -1.736482e-01 4.34729636 0 1.96961551 1 1 1 + 18 0 1 0 -3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.396926e-01 0.000000e+00 -3.420201e-01 4.68404029 0 1.87938524 1 1 1 + 19 0 1 0 -5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -8.660254e-01 0.000000e+00 -5.000000e-01 5 0 1.73205081 1 1 1 + 20 0 1 0 5.000000e-01 0.000000e+00 -8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 8.660254e-01 0.000000e+00 5.000000e-01 2 0 -3.46410162 1 1 1 + 21 0 1 0 6.427876e-01 0.000000e+00 -7.660444e-01 -0.000000e+00 1.000000e+00 0.000000e+00 7.660444e-01 0.000000e+00 6.427876e-01 1.42884956 0 -3.06417777 1 1 1 + 22 0 1 0 7.660444e-01 0.000000e+00 -6.427876e-01 -0.000000e+00 1.000000e+00 0.000000e+00 6.427876e-01 0.000000e+00 7.660444e-01 0.935822228 0 -2.57115044 1 1 1 + 23 0 1 0 8.660254e-01 0.000000e+00 -5.000000e-01 -0.000000e+00 1.000000e+00 0.000000e+00 5.000000e-01 0.000000e+00 8.660254e-01 0.535898385 0 -2 1 1 1 + 24 0 1 0 9.396926e-01 0.000000e+00 -3.420201e-01 -0.000000e+00 1.000000e+00 0.000000e+00 3.420201e-01 0.000000e+00 9.396926e-01 0.241229517 0 -1.36808057 1 1 1 + 25 0 1 0 9.848078e-01 0.000000e+00 -1.736482e-01 -0.000000e+00 1.000000e+00 0.000000e+00 1.736482e-01 0.000000e+00 9.848078e-01 0.060768988 0 -0.694592711 1 1 1 + 26 0 1 0 1.000000e+00 0.000000e+00 2.220446e-16 -0.000000e+00 1.000000e+00 -0.000000e+00 -2.220446e-16 0.000000e+00 1.000000e+00 0 0 8.8817842e-16 1 1 1 + 27 0 1 0 9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -1.736482e-01 0.000000e+00 9.848078e-01 0.060768988 0 0.694592711 1 1 1 + 28 0 1 0 9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -3.420201e-01 0.000000e+00 9.396926e-01 0.241229517 0 1.36808057 1 1 1 + 29 0 1 0 8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -5.000000e-01 0.000000e+00 8.660254e-01 0.535898385 0 2 1 1 1 + 30 0 1 0 7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.427876e-01 0.000000e+00 7.660444e-01 0.935822228 0 2.57115044 1 1 1 + 31 0 1 0 6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -7.660444e-01 0.000000e+00 6.427876e-01 1.42884956 0 3.06417777 1 1 1 + 32 0 1 0 5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -8.660254e-01 0.000000e+00 5.000000e-01 2 0 3.46410162 1 1 1 + 33 0 1 0 3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.396926e-01 0.000000e+00 3.420201e-01 2.63191943 0 3.75877048 1 1 1 + 34 0 1 0 1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.848078e-01 0.000000e+00 1.736482e-01 3.30540729 0 3.93923101 1 1 1 + 35 0 1 0 -0.000000e+00 0.000000e+00 1.000000e+00 -0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 4 0 4 1 1 1 + 36 0 1 0 -1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.848078e-01 0.000000e+00 -1.736482e-01 4.69459271 0 3.93923101 1 1 1 + 37 0 1 0 -3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.396926e-01 0.000000e+00 -3.420201e-01 5.36808057 0 3.75877048 1 1 1 + 38 0 1 0 -5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -8.660254e-01 0.000000e+00 -5.000000e-01 6 0 3.46410162 1 1 1 + 39 0 1 0 5.000000e-01 0.000000e+00 -8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 8.660254e-01 0.000000e+00 5.000000e-01 1 0 -5.19615242 1 1 1 + 40 0 1 0 6.427876e-01 0.000000e+00 -7.660444e-01 -0.000000e+00 1.000000e+00 0.000000e+00 7.660444e-01 0.000000e+00 6.427876e-01 0.143274342 0 -4.59626666 1 1 1 + 41 0 1 0 7.660444e-01 0.000000e+00 -6.427876e-01 -0.000000e+00 1.000000e+00 0.000000e+00 6.427876e-01 0.000000e+00 7.660444e-01 -0.596266659 0 -3.85672566 1 1 1 + 42 0 1 0 8.660254e-01 0.000000e+00 -5.000000e-01 -0.000000e+00 1.000000e+00 0.000000e+00 5.000000e-01 0.000000e+00 8.660254e-01 -1.19615242 0 -3 1 1 1 + 43 0 1 0 9.396926e-01 0.000000e+00 -3.420201e-01 -0.000000e+00 1.000000e+00 0.000000e+00 3.420201e-01 0.000000e+00 9.396926e-01 -1.63815572 0 -2.05212086 1 1 1 + 44 0 1 0 9.848078e-01 0.000000e+00 -1.736482e-01 -0.000000e+00 1.000000e+00 0.000000e+00 1.736482e-01 0.000000e+00 9.848078e-01 -1.90884652 0 -1.04188907 1 1 1 + 45 0 1 0 1.000000e+00 0.000000e+00 2.220446e-16 -0.000000e+00 1.000000e+00 -0.000000e+00 -2.220446e-16 0.000000e+00 1.000000e+00 -2 0 1.33226763e-15 1 1 1 + 46 0 1 0 9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -1.736482e-01 0.000000e+00 9.848078e-01 -1.90884652 0 1.04188907 1 1 1 + 47 0 1 0 9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -3.420201e-01 0.000000e+00 9.396926e-01 -1.63815572 0 2.05212086 1 1 1 + 48 0 1 0 8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -5.000000e-01 0.000000e+00 8.660254e-01 -1.19615242 0 3 1 1 1 + 49 0 1 0 7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.427876e-01 0.000000e+00 7.660444e-01 -0.596266659 0 3.85672566 1 1 1 + 50 0 1 0 6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -7.660444e-01 0.000000e+00 6.427876e-01 0.143274342 0 4.59626666 1 1 1 + 51 0 1 0 5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -8.660254e-01 0.000000e+00 5.000000e-01 1 0 5.19615242 1 1 1 + 52 0 1 0 3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.396926e-01 0.000000e+00 3.420201e-01 1.94787914 0 5.63815572 1 1 1 + 53 0 1 0 1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.848078e-01 0.000000e+00 1.736482e-01 2.95811093 0 5.90884652 1 1 1 + 54 0 1 0 -0.000000e+00 0.000000e+00 1.000000e+00 -0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 4 0 6 1 1 1 + 55 0 1 0 -1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.848078e-01 0.000000e+00 -1.736482e-01 5.04188907 0 5.90884652 1 1 1 + 56 0 1 0 -3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.396926e-01 0.000000e+00 -3.420201e-01 6.05212086 0 5.63815572 1 1 1 + 57 0 1 0 -5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -8.660254e-01 0.000000e+00 -5.000000e-01 7 0 5.19615242 1 1 1 + 58 0 1 0 5.000000e-01 0.000000e+00 -8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 8.660254e-01 0.000000e+00 5.000000e-01 -8.8817842e-16 0 -6.92820323 1 1 1 + 59 0 1 0 6.427876e-01 0.000000e+00 -7.660444e-01 -0.000000e+00 1.000000e+00 0.000000e+00 7.660444e-01 0.000000e+00 6.427876e-01 -1.14230088 0 -6.12835554 1 1 1 + 60 0 1 0 7.660444e-01 0.000000e+00 -6.427876e-01 -0.000000e+00 1.000000e+00 0.000000e+00 6.427876e-01 0.000000e+00 7.660444e-01 -2.12835554 0 -5.14230088 1 1 1 + 61 0 1 0 8.660254e-01 0.000000e+00 -5.000000e-01 -0.000000e+00 1.000000e+00 0.000000e+00 5.000000e-01 0.000000e+00 8.660254e-01 -2.92820323 0 -4 1 1 1 + 62 0 1 0 9.396926e-01 0.000000e+00 -3.420201e-01 -0.000000e+00 1.000000e+00 0.000000e+00 3.420201e-01 0.000000e+00 9.396926e-01 -3.51754097 0 -2.73616115 1 1 1 + 63 0 1 0 9.848078e-01 0.000000e+00 -1.736482e-01 -0.000000e+00 1.000000e+00 0.000000e+00 1.736482e-01 0.000000e+00 9.848078e-01 -3.87846202 0 -1.38918542 1 1 1 + 64 0 1 0 1.000000e+00 0.000000e+00 2.220446e-16 -0.000000e+00 1.000000e+00 -0.000000e+00 -2.220446e-16 0.000000e+00 1.000000e+00 -4 0 1.77635684e-15 1 1 1 + 65 0 1 0 9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -1.736482e-01 0.000000e+00 9.848078e-01 -3.87846202 0 1.38918542 1 1 1 + 66 0 1 0 9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -3.420201e-01 0.000000e+00 9.396926e-01 -3.51754097 0 2.73616115 1 1 1 + 67 0 1 0 8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -5.000000e-01 0.000000e+00 8.660254e-01 -2.92820323 0 4 1 1 1 + 68 0 1 0 7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.427876e-01 0.000000e+00 7.660444e-01 -2.12835554 0 5.14230088 1 1 1 + 69 0 1 0 6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -7.660444e-01 0.000000e+00 6.427876e-01 -1.14230088 0 6.12835554 1 1 1 + 70 0 1 0 5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -8.660254e-01 0.000000e+00 5.000000e-01 0 0 6.92820323 1 1 1 + 71 0 1 0 3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.396926e-01 0.000000e+00 3.420201e-01 1.26383885 0 7.51754097 1 1 1 + 72 0 1 0 1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.848078e-01 0.000000e+00 1.736482e-01 2.61081458 0 7.87846202 1 1 1 + 73 0 1 0 -0.000000e+00 0.000000e+00 1.000000e+00 -0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 4 0 8 1 1 1 + 74 0 1 0 -1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.848078e-01 0.000000e+00 -1.736482e-01 5.38918542 0 7.87846202 1 1 1 + 75 0 1 0 -3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.396926e-01 0.000000e+00 -3.420201e-01 6.73616115 0 7.51754097 1 1 1 + 76 0 1 0 -5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -8.660254e-01 0.000000e+00 -5.000000e-01 8 0 6.92820323 1 1 1 + 77 0 1 0 5.000000e-01 0.000000e+00 -8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 8.660254e-01 0.000000e+00 5.000000e-01 -1 0 -8.66025404 1 1 1 + 78 0 1 0 6.427876e-01 0.000000e+00 -7.660444e-01 -0.000000e+00 1.000000e+00 0.000000e+00 7.660444e-01 0.000000e+00 6.427876e-01 -2.4278761 0 -7.66044443 1 1 1 + 79 0 1 0 7.660444e-01 0.000000e+00 -6.427876e-01 -0.000000e+00 1.000000e+00 0.000000e+00 6.427876e-01 0.000000e+00 7.660444e-01 -3.66044443 0 -6.4278761 1 1 1 + 80 0 1 0 8.660254e-01 0.000000e+00 -5.000000e-01 -0.000000e+00 1.000000e+00 0.000000e+00 5.000000e-01 0.000000e+00 8.660254e-01 -4.66025404 0 -5 1 1 1 + 81 0 1 0 9.396926e-01 0.000000e+00 -3.420201e-01 -0.000000e+00 1.000000e+00 0.000000e+00 3.420201e-01 0.000000e+00 9.396926e-01 -5.39692621 0 -3.42020143 1 1 1 + 82 0 1 0 9.848078e-01 0.000000e+00 -1.736482e-01 -0.000000e+00 1.000000e+00 0.000000e+00 1.736482e-01 0.000000e+00 9.848078e-01 -5.84807753 0 -1.73648178 1 1 1 + 83 0 1 0 1.000000e+00 0.000000e+00 2.220446e-16 -0.000000e+00 1.000000e+00 -0.000000e+00 -2.220446e-16 0.000000e+00 1.000000e+00 -6 0 2.22044605e-15 1 1 1 + 84 0 1 0 9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -1.736482e-01 0.000000e+00 9.848078e-01 -5.84807753 0 1.73648178 1 1 1 + 85 0 1 0 9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -3.420201e-01 0.000000e+00 9.396926e-01 -5.39692621 0 3.42020143 1 1 1 + 86 0 1 0 8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -5.000000e-01 0.000000e+00 8.660254e-01 -4.66025404 0 5 1 1 1 + 87 0 1 0 7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.427876e-01 0.000000e+00 7.660444e-01 -3.66044443 0 6.4278761 1 1 1 + 88 0 1 0 6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -7.660444e-01 0.000000e+00 6.427876e-01 -2.4278761 0 7.66044443 1 1 1 + 89 0 1 0 5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -8.660254e-01 0.000000e+00 5.000000e-01 -1 0 8.66025404 1 1 1 + 90 0 1 0 3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.396926e-01 0.000000e+00 3.420201e-01 0.579798567 0 9.39692621 1 1 1 + 91 0 1 0 1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.848078e-01 0.000000e+00 1.736482e-01 2.26351822 0 9.84807753 1 1 1 + 92 0 1 0 -0.000000e+00 0.000000e+00 1.000000e+00 -0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 4 0 10 1 1 1 + 93 0 1 0 -1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.848078e-01 0.000000e+00 -1.736482e-01 5.73648178 0 9.84807753 1 1 1 + 94 0 1 0 -3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.396926e-01 0.000000e+00 -3.420201e-01 7.42020143 0 9.39692621 1 1 1 + 95 0 1 0 -5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -8.660254e-01 0.000000e+00 -5.000000e-01 9 0 8.66025404 1 1 1 diff --git a/scripts/inclined_plate_sweep_study.py b/scripts/inclined_plate_sweep_study.py new file mode 100644 index 0000000..d890464 --- /dev/null +++ b/scripts/inclined_plate_sweep_study.py @@ -0,0 +1,333 @@ +"""Phase-3 driver: angle x distance sweep of the Cai 2016 inclined plate. + +Runs the 95-firing sweep JFH of case/plume/plume_inclined_plate (generated by +jfh/generate_sweep_jfh.py: VV on arcs of radius L about the stationary plate +center, thruster aimed at the center; alpha = 0 is head-on = paper +alpha0 = 90 deg, alpha_paper = 90 - |alpha| deg; L/D in {2, 4, 6, 8, 10}) +through the strike pipeline's per-firing core, compute_plume_strikes -- the +same function PlumeStrikeEstimationStudy calls per firing -- keeping each +firing's per-face arrays separate so the pipeline's cumulative accumulation +never pollutes the per-pose analysis. This is a standalone study script: +nothing in pyrpod/mdao is used and nothing is optimized. + +Outputs, to case/plume/plume_inclined_plate/results/sweep/ (gitignored): + - sweep_coefficients.csv: per firing, the Eq.-15 plate-averaged + coefficients CP/CF1/CF2/CQ/CM/s_cc from the pipeline AND from the exact + reference (pyrpod/plume/CaiImpingement2016.py), plus the peak per-face + Cp / |Cf| / Cq, struck-face counts, and per-firing wall time; + - coeff_.png: each averaged coefficient vs alpha, one curve per + L/D, pipeline (solid + markers) and reference (dashed) on the same axes; + - peaks.png: peak per-face pressure/shear/heat-flux coefficients vs alpha + per L/D. + +Conventions and sanity checks: + - Averaged pipeline coefficients follow Eq. 15: area-weighted sums over + the whole plate area S (unstruck faces carry zero load), with + CM = sum(tau * Cp * A) / (2 * H0 * S) and s_cc = CM / CP. + - The pipeline's per-face |shear| is decomposed onto the plate's + (tau, s) axes along the tangential projection of the radial flow + direction (the same decomposition as the Phase-1 figure overlays) to + obtain signed Cf1/Cf2 comparable to the paper's components. + - Poses with alpha < 0 map directly onto the paper geometry + (tau_paper = tau_case); alpha > 0 is its mirror image, so CF1 and CM + are sign-flipped into the paper convention. Mirror symmetry of the + results in +/-alpha is then asserted (the plate is square). + - +/-90 deg is edge-on/degenerate: the firings are kept (CSV rows and a + printed report) but excluded from the plots and the mirror check. At + exactly edge-on the facing test's surface_dot_plume is 0 in exact + arithmetic, so the mesh's float32 normals make strike membership + epsilon-arbitrary (a third to two thirds of the faces pass, not + mirror-symmetrically). The plate-AVERAGED load there is ~zero as + expected (a few percent of head-on), but individual epsilon-passing + faces next to the in-plane nozzle carry large grazing near-field + values at close range (peak Cp ~ 4 at L/D = 2), which would dominate + the peak plots meaninglessly. (The reference at alpha_paper = 0 + likewise keeps only the small plume-spread load.) + +Run from the repo root: python scripts/inclined_plate_sweep_study.py +""" + +import csv +import sys +import time +from pathlib import Path + +import numpy as np + +_REPO_ROOT = Path(__file__).resolve().parents[1] +for extra in (_REPO_ROOT, _REPO_ROOT / 'tests' / 'plume'): + if str(extra) not in sys.path: + sys.path.insert(0, str(extra)) + +import matplotlib # noqa: E402 +matplotlib.use('Agg') +import matplotlib.pyplot as plt # noqa: E402 + +import plume_impingement_utils as piu # noqa: E402 (paper constants) +from pyrpod.mission import MissionEnvironment # noqa: E402 +from pyrpod.plume import CaiImpingement2016 as cai # noqa: E402 +from pyrpod.plume.PlumeStrikeCalculator import ( # noqa: E402 + compute_face_centroids, + compute_plume_strikes, +) +from pyrpod.rpod import JetFiringHistory # noqa: E402 +from pyrpod.vehicle import TargetVehicle, VisitingVehicle # noqa: E402 + +CASE_DIR = str(_REPO_ROOT / 'case' / 'plume' / 'plume_inclined_plate') + '/' +SWEEP_JFH = 'jfh_inclined_plate_sweep.A' +RESULTS_DIR = Path(CASE_DIR) / 'results' / 'sweep' + +PLATE_CENTER = np.array([4.0, 0.0, 0.0]) +ALPHA0_DEG = 60.0 # the mesh's fixed global tilt +H_0 = W_0 = 4.0 # plate semi-lengths (m) +ALPHAS_DEG = np.arange(-90.0, 90.0 + 1e-9, 10.0) +L_OVER_D = [2.0, 4.0, 6.0, 8.0, 10.0] + +# dataviz categorical palette, fixed slot order (validated CVD-safe) +PALETTE = ['#2a78d6', '#008300', '#e87ba4', '#eda100', '#1baf7a'] + +_A0 = np.deg2rad(ALPHA0_DEG) +NORMAL_OUT = np.array([-np.sin(_A0), 0.0, np.cos(_A0)]) # toward the VV side +TANGENT_TAU = np.array([np.cos(_A0), 0.0, np.sin(_A0)]) +TANGENT_S = np.array([0.0, 1.0, 0.0]) + +COEFF_NAMES = ['CP', 'CF1', 'CF2', 'CQ', 'CM', 's_cc'] + + +def load_case(): + jfh = JetFiringHistory.JetFiringHistory(CASE_DIR) + jfh.config.set('jfh', 'jfh', SWEEP_JFH) # sweep JFH, same case assets + jfh.read_jfh() + + tv = TargetVehicle.TargetVehicle(CASE_DIR) + tv.set_stl() + + vv = VisitingVehicle.VisitingVehicle(CASE_DIR) + vv.set_thruster_config() + vv.set_thruster_metrics() + + me = MissionEnvironment.MissionEnvironment(CASE_DIR) + return jfh, tv, vv, me + + +def face_areas(vectors): + v0, v1, v2 = vectors[:, 0], vectors[:, 1], vectors[:, 2] + return 0.5 * np.linalg.norm(np.cross(v1 - v0, v2 - v0), axis=1) + + +def pipeline_row(result, centroids, normals, areas, tau_face, thruster_pos, + alpha_deg): + """Eq.-15 averaged coefficients + peaks from one firing's face arrays.""" + S_tot = float(np.sum(areas)) + Cp = result['pressures'] / piu.Q_DYN + Csh = result['shear_stress'] / piu.Q_DYN + Cq = result['heat_flux_rate'] / piu.Q_DYN_HEAT + + # signed shear components along the plate axes (tangential projection + # of the radial flow direction, as in the Phase-1 overlays) + rel = centroids - thruster_pos + dist = np.linalg.norm(rel, axis=1) + u_hat = rel / dist[:, None] + n_in = -NORMAL_OUT + cos_inc = u_hat @ n_in + t_vec = u_hat - cos_inc[:, None] * n_in + t_norm = np.linalg.norm(t_vec, axis=1) + safe = t_norm > 1e-12 + t_hat = np.zeros_like(t_vec) + t_hat[safe] = t_vec[safe] / t_norm[safe, None] + Cf1 = Csh * (t_hat @ TANGENT_TAU) + Cf2 = Csh * (t_hat @ TANGENT_S) + + flip = -1.0 if alpha_deg > 0 else 1.0 # into the paper convention + CP = float(np.sum(Cp * areas) / S_tot) + CF1 = flip * float(np.sum(Cf1 * areas) / S_tot) + CF2 = float(np.sum(Cf2 * areas) / S_tot) + CQ = float(np.sum(Cq * areas) / S_tot) + CM = flip * float(np.sum(tau_face * Cp * areas) / (2.0 * H_0 * S_tot)) + s_cc = CM / CP if CP != 0.0 else float('nan') + return ({'CP': CP, 'CF1': CF1, 'CF2': CF2, 'CQ': CQ, 'CM': CM, + 's_cc': s_cc}, + {'peak_Cp': float(np.max(Cp)), 'peak_Cshear': float(np.max(Csh)), + 'peak_Cq': float(np.max(Cq)), + 'n_struck': int(np.count_nonzero(result['strikes']))}) + + +def reference_rows(): + """Exact Eq.-15 coefficients per (|alpha|, L), cached (mirror-invariant).""" + cache = {} + for L in L_OVER_D: + for abs_alpha in sorted({abs(a) for a in ALPHAS_DEG}): + alpha_paper = np.deg2rad(90.0 - abs_alpha) + cache[(abs_alpha, L)] = cai.averaged_coefficients( + piu.S_0, alpha_paper, piu.EPS, piu.R_0, L, W_0, H_0) + return cache + + +def check_mirror_symmetry(rows): + """Results must be mirror-symmetric in +/-alpha (paper convention). + + The +/-90 deg poses are excluded: their strike membership comes from + float32-epsilon signs in the facing test, which are not mirrored. + CF2 is checked separately -- it vanishes identically by symmetry, so + normalizing its asymmetry by its own (noise) scale is meaningless. + """ + worst = 0.0 + for L in L_OVER_D: + by_alpha = {r['alpha_deg']: r for r in rows if r['L_over_D'] == L + and abs(r['alpha_deg']) < 90.0} + cp_scale = max(abs(r['CP_pipe']) for r in by_alpha.values()) + # tolerance covers the triangulation's O(h^2) s-asymmetry (the + # cell-diagonal split is not mirror-invariant) + assert max(abs(r['CF2_pipe']) for r in by_alpha.values()) \ + < 1e-4 * cp_scale, f'CF2 not ~0 at L/D={L}' + for a in ALPHAS_DEG[(ALPHAS_DEG > 0) & (ALPHAS_DEG < 90.0)]: + for name in ('CP', 'CF1', 'CQ', 'CM'): + lo, hi = by_alpha[-a][f'{name}_pipe'], by_alpha[a][f'{name}_pipe'] + scale = max(abs(v) for r in by_alpha.values() + for v in [r[f'{name}_pipe']]) + scale = max(scale, 1e-6 * cp_scale) + worst = max(worst, abs(hi - lo) / scale) + # tolerance: the JFH file stores DCMs to 6 significant digits (and + # positions to 9), so mirrored poses reproduce to ~1e-6 relative; + # a convention error would show up as O(1). + assert worst < 1e-5, f'mirror symmetry violated: {worst:.3g}' + return worst + + +def plot_coefficients(rows, out_dir): + style_note = 'solid + markers: pipeline dashed: Cai 2016 reference' + labels = {'CP': r'$C_P$', 'CF1': r'$C_{F1}$', 'CF2': r'$C_{F2}$', + 'CQ': r'$C_Q$', 'CM': r'$C_M$', 's_cc': r'$s_{cc}$'} + for name in COEFF_NAMES: + fig, ax = plt.subplots(figsize=(7, 4.5)) + for color, L in zip(PALETTE, L_OVER_D): + sub = sorted((r for r in rows if r['L_over_D'] == L + and abs(r['alpha_deg']) < 90.0), + key=lambda r: r['alpha_deg']) + a = [r['alpha_deg'] for r in sub] + ax.plot(a, [r[f'{name}_pipe'] for r in sub], '-o', color=color, + lw=2, ms=4, label=f'L/D = {L:g}') + ax.plot(a, [r[f'{name}_ref'] for r in sub], '--', color=color, + lw=2) + ax.annotate(f'{L:g}', (a[-1], sub[-1][f'{name}_pipe']), + textcoords='offset points', xytext=(6, 0), + color=color, fontsize=8, va='center') + ax.axhline(0.0, color='0.85', lw=0.8, zorder=0) + ax.set_xlabel(r'$\alpha$ (deg), 0 = head-on') + ax.set_ylabel(labels[name]) + ax.set_title(f'Plate-averaged {labels[name]} vs approach angle\n' + f'{style_note}', fontsize=10) + ax.set_xticks(np.arange(-90, 91, 30)) + ax.legend(fontsize=8, title='distance', title_fontsize=8) + fig.savefig(out_dir / f'coeff_{name}.png', dpi=200, + bbox_inches='tight') + plt.close(fig) + + +def plot_peaks(rows, out_dir): + quantities = [('peak_Cp', r'peak $C_p$'), + ('peak_Cshear', r'peak $|C_f|$'), + ('peak_Cq', r'peak $C_q$')] + fig, axes = plt.subplots(3, 1, figsize=(7, 9), sharex=True) + for ax, (key, label) in zip(axes, quantities): + for color, L in zip(PALETTE, L_OVER_D): + sub = sorted((r for r in rows if r['L_over_D'] == L + and abs(r['alpha_deg']) < 90.0), + key=lambda r: r['alpha_deg']) + ax.plot([r['alpha_deg'] for r in sub], [r[key] for r in sub], + '-o', color=color, lw=2, ms=4, label=f'L/D = {L:g}') + ax.set_ylabel(label) + ax.set_yscale('log') + axes[0].legend(fontsize=8, title='distance', title_fontsize=8) + axes[0].set_title('Peak per-face coefficients vs approach angle ' + '(pipeline)', fontsize=10) + axes[-1].set_xlabel(r'$\alpha$ (deg), 0 = head-on') + axes[-1].set_xticks(np.arange(-90, 91, 30)) + fig.savefig(out_dir / 'peaks.png', dpi=200, bbox_inches='tight') + plt.close(fig) + + +def main(): + t_start = time.perf_counter() + jfh, tv, vv, me = load_case() + n_firings = len(jfh.JFH) + assert n_firings == len(ALPHAS_DEG) * len(L_OVER_D), ( + f'unexpected sweep JFH length {n_firings}; regenerate with ' + 'case/plume/plume_inclined_plate/jfh/generate_sweep_jfh.py') + + target = tv.mesh + normals = target.get_unit_normals() + centroids = compute_face_centroids(target.vectors) + areas = face_areas(target.vectors) + tau_face = (centroids - PLATE_CENTER) @ TANGENT_TAU + + print(f'reference curves: {len(set(abs(ALPHAS_DEG)))} angles x ' + f'{len(L_OVER_D)} distances (Eq. 15 quadrature) ...') + t0 = time.perf_counter() + ref_cache = reference_rows() + t_ref = time.perf_counter() - t0 + print(f' done in {t_ref:.1f} s') + + rows = [] + t0 = time.perf_counter() + for i in range(n_firings): + L = L_OVER_D[i // len(ALPHAS_DEG)] + alpha_deg = float(ALPHAS_DEG[i % len(ALPHAS_DEG)]) + step = {'thrusters': jfh.JFH[i]['thrusters'], + 'xyz': np.array(jfh.JFH[i]['xyz']), + 'dcm': np.array(jfh.JFH[i]['dcm']), + 't': float(jfh.JFH[i]['t'])} + t_f = time.perf_counter() + result = compute_plume_strikes(target, normals, vv, step, me, + face_centroids=centroids) + dt = time.perf_counter() - t_f + + coeffs, peaks = pipeline_row(result, centroids, normals, areas, + tau_face, step['xyz'], alpha_deg) + ref = ref_cache[(abs(alpha_deg), L)] + row = {'alpha_deg': alpha_deg, 'L_over_D': L, + 'alpha_paper_deg': 90.0 - abs(alpha_deg), + 'wall_time_s': dt, **peaks} + for name in COEFF_NAMES: + row[f'{name}_pipe'] = coeffs[name] + row[f'{name}_ref'] = float(ref[name]) + rows.append(row) + t_sweep = time.perf_counter() - t0 + + # sanity: report the edge-on poses (strike membership is + # epsilon-arbitrary there, see the module docstring); mirror symmetry + head_on = {r['L_over_D']: r['CP_pipe'] for r in rows + if r['alpha_deg'] == 0.0} + for r in rows: + if abs(r['alpha_deg']) == 90.0: + print(f"edge-on alpha={r['alpha_deg']:+.0f}, " + f"L/D={r['L_over_D']:g}: {r['n_struck']} faces pass the " + f"epsilon-degenerate facing test; CP=" + f"{r['CP_pipe']:.3e} (head-on " + f"{head_on[r['L_over_D']]:.3e}), peak Cp=" + f"{r['peak_Cp']:.3e}, peak |Cf|={r['peak_Cshear']:.3e}") + worst = check_mirror_symmetry(rows) + print(f'mirror-symmetry check: worst normalized asymmetry {worst:.2e}') + + RESULTS_DIR.mkdir(parents=True, exist_ok=True) + columns = list(rows[0].keys()) + csv_path = RESULTS_DIR / 'sweep_coefficients.csv' + with open(csv_path, 'w', encoding='utf-8', newline='') as fh: + writer = csv.DictWriter(fh, fieldnames=columns) + writer.writeheader() + writer.writerows(rows) + print(f'wrote {csv_path}') + + plot_coefficients(rows, RESULTS_DIR) + plot_peaks(rows, RESULTS_DIR) + print(f'wrote coefficient and peak plots to {RESULTS_DIR}') + + n_faces = len(target.vectors) + print(f'timings: {n_firings} firings x {n_faces} faces -- sweep ' + f'{t_sweep:.1f} s ({t_sweep / n_firings * 1e3:.0f} ms/firing), ' + f'reference {t_ref:.1f} s, total ' + f'{time.perf_counter() - t_start:.1f} s') + + +if __name__ == '__main__': + main() From 5de6c023d1f81d9fe0c96c4f631c3750433de71e Mon Sep 17 00:00:00 2001 From: andytorrestb Date: Sat, 18 Jul 2026 23:25:08 -0500 Subject: [PATCH 04/11] Update tests dashboard with the Cai 2016 impingement verification suite Co-Authored-By: Claude Fable 5 --- tests/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/README.md b/tests/README.md index 7d007bc..d107c74 100644 --- a/tests/README.md +++ b/tests/README.md @@ -41,6 +41,12 @@ This dashboard provides an overview of all tests in the PyRPOD project, categori | `plume_integration_test_01.py` | Integration | Tests plume modeling in integrated systems. | ❌ | | `plume_unit_test_01.py` | Unit | Verifies individual plume calculation methods.| ❌ | | `plume_verification_test_01.py` | Verification | Validates plume outputs against benchmarks. | ❌ | +| `plume_verification_test_28.py` | Verification | Cai 2016 Fig. 17: diffuse-plate Cp contours (manual-run figure). | ✅ | +| `plume_verification_test_29.py` | Verification | Cai 2016 Fig. 18: specular-plate Cp contours (manual-run figure). | ✅ | +| `plume_verification_test_30.py` | Verification | Cai 2016 Fig. 19: diffuse-plate Cf1 contours (manual-run figure). | ✅ | +| `plume_verification_test_31.py` | Verification | Cai 2016 Fig. 20: diffuse-plate Cf2 contours (manual-run figure). | ✅ | +| `plume_verification_test_32.py` | Verification | Cai 2016 Fig. 21: diffuse-plate Cq contours (manual-run figure). | ✅ | +| `plume_impingement_error_summary.py` | Verification | Cai 2016 reference vs PyRPOD-chain max/mean error table (manual-run generator). | ✅ | --- @@ -51,6 +57,7 @@ This dashboard provides an overview of all tests in the PyRPOD project, categori | `rpod_integration_test_02.py` | Integration | Asserts plume strikes for notional 1D approach. | ✅ | | `rpod_integration_test_03.py` | Integration | Asserts plume strikes using KOZ geometry. | ✅ | | `rpod_integration_test_04.py` | Integration | Asserts plume strikes using hollow cube geometry. | ✅ | +| `rpod_integration_test_07.py` | Integration | Cai 2016 inclined-plate case: pipeline loads vs exact reference. | ✅ | | `rpod_unit_test_01.py` | Unit | Verifies STL to VTK data conversion. | ✅ | | `rpod_unit_test_02.py` | Unit | Verifiy behavior of JFH reader. | ✅ | | `rpod_unit_test_03.py` | Unit | Produces JFH data according to produced equation. | ⏳ | From 06bf9660bf17f2789d88dfa4925e45c6df53eaca Mon Sep 17 00:00:00 2001 From: andytorrestb Date: Sat, 18 Jul 2026 23:45:12 -0500 Subject: [PATCH 05/11] Save Cai 2016 verification figures to tests/plume/output/Cai2016 Route the Figs. 17-21 reproduction scripts (plume_verification_test_28 ..32) through a study-local save_figure that writes to the Cai2016 subfolder of the figure output directory, keeping the 2016 impingement set separate from the 2012 figure outputs. The output/.gitignore (ignore-all) already covers the subfolder. Co-Authored-By: Claude Fable 5 --- tests/plume/plume_impingement_utils.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/plume/plume_impingement_utils.py b/tests/plume/plume_impingement_utils.py index 203467d..c0eaec0 100644 --- a/tests/plume/plume_impingement_utils.py +++ b/tests/plume/plume_impingement_utils.py @@ -34,9 +34,11 @@ # plate temperature has no effect on Cp,s. # * Digitized-overlay slots follow tests/plume/data/digitized/README.md: # fig17_*.csv ... fig21_*.csv (absent files silently skipped). -# * Generic figure plumbing (save_figure, run_script, overlay_digitized, +# * Generic figure plumbing (run_script, overlay_digitized, # annotate_error, max_rel_diff) is reused from plume_figure_utils; -# only the paper-specific constants live here. +# only the paper-specific constants live here. Figures are saved to +# the study's own subfolder, tests/plume/output/Cai2016, to keep the +# Cai 2016 verification set separate from the 2012 figure outputs. import sys from pathlib import Path @@ -82,6 +84,8 @@ GRID_N = 81 # nodes per plate axis for contour grids +OUTPUT_DIR = base.OUTPUT_DIR / 'Cai2016' # figure output subfolder + THRUSTER_CHARACTERISTICS = {'d': D_NOZZLE, 've': U_0, 'R': R_SPECIFIC, 'gamma': GAMMA, 'Te': T_0, 'n': N_0} @@ -182,6 +186,15 @@ def chain_grid(n=GRID_N, sigma=SIGMA): return {'Cp': Cp, 'Cf1': Cf1, 'Cf2': Cf2, 'Cshear': Csh, 'Cq': Cq} +def save_figure(fig, name): + """Save PNG to tests/plume/output/Cai2016 and return the path.""" + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + path = OUTPUT_DIR / f'{name}.png' + fig.savefig(path, dpi=200, bbox_inches='tight') + plt.close(fig) + return path + + def mean_rel_diff(candidate, reference, mask): """mean |candidate - reference| / |reference| over the masked entries.""" candidate = np.asarray(candidate, dtype=float) @@ -237,4 +250,4 @@ def impingement_contour_figure(fig_stem, ref_field, chain_field, levels, f'max rel diff = {base.max_rel_diff(chain_field[mask], ref_field[mask]):.2g}\n' f'mean rel diff = {mean_rel_diff(chain_field, ref_field, mask):.2g}\n' r'(where $|C_{ref}| \geq$ 5% of peak)') - return base.save_figure(fig, file_name) + return save_figure(fig, file_name) From 374e62c5336cdd82fc02fbb35a8036b77ffbfbd5 Mon Sep 17 00:00:00 2001 From: andytorrestb Date: Sun, 19 Jul 2026 08:43:21 -0500 Subject: [PATCH 06/11] Reproduce Cai 2016 Figs. 5-10 and 15-16 from the reference module Extend pyrpod/plume/CaiImpingement2016.py -- the single source of truth for every Cai-2016 verification figure -- with the remaining paper solutions: * Section 3, 2D slot jet on an inclined planar plate: exact surface coefficients Cp,d/Cf,d/Cq,d/Cp,s (Eqs. 2-4, 8) with the non-penetration wall density derived as the A0 number-flux integral (2D analog of the 3D A1 integral), and the combined jet + plate flowfield moments for the temperature contours -- diffuse wall emission integrated over arrival directions with n_w(s) at the ray-plate intersection (finite plate edges handled naturally), and the specular virtual nozzle built by literal mirroring of the exit (reproduces Eq. 7). The Eq.-4 energy integrand was re-derived and verified to equal 2 e^(a^2)(I4 + I2/2); the virtual DRIFT is the mirror image U0(cos 2a0, sin 2a0) -- the papers printed negative signs are the velocity-space domain vertex of Fig. 14, documented like the 2012 Eq. 7 typo note. Section-3 geometry (L = 4*(2H), W = 5*(2H)) is read off the figures and documented. * Section 4 flowfield pressure in the Y = 0 plane (local-T static pressure, as the 2012 paper requires): free-jet raw moments via the K/M/N factors imported from RarefiedPlumeGasKinetics (never re-derived), plus either the Eq.-9 wall-emission solid-angle integral over the plate or the 3D virtual nozzle. New sanity checks pass at machine precision: 2D parity at alpha0 = 90, 2D n_w quad flux balance, wall-emission half-space closure (3e-4 at h = 1e-3), specular mirror symmetry of the 2D and 3D fields (~1e-15, the defining property of Figs. 6/16), a closed-form solid-angle sum rule for the 3D emission kernel, and GL-vs-dblquad of the assembled emission integral. Paper anchors reproduced: 2D peak Cp,d 0.919 (~0.93), Cp,s 1.233 (~1.23), Cf,d 0.325 (~0.33) with zero crossing at s = -2.12 (~-2), Cq,d 0.268 (~0.27); flowfield peaks T/T0 2.44 / 3.60 (Figs. 5/6 top contours 2.4/3.5) and impingement-region p/p0 0.58 / 0.50 (Figs. 15/16 innermost contours 0.5/0.4). Figure scripts plume_verification_test_33..40 reproduce Figs. 5-10 and 15-16 (manual-run, digitized slots registered); they carry no PyRPOD-chain overlay because SimplifiedGasKinetics is a round-nozzle model (no 2D slot analog) and the pipeline computes no flowfields-with-plates. All Cai-2016 digitized slots move to a cai16_ stem prefix (cai16_fig05 ... cai16_fig21), fixing a latent collision: the 2016 figure numbers 17-21 overlapped the Cai & Wang 2012 fig17..21 slots already registered by tests 04-27. Co-Authored-By: Claude Fable 5 --- pyrpod/plume/CaiImpingement2016.py | 675 +++++++++++++++++++++- tests/README.md | 8 + tests/plume/data/digitized/README.md | 13 +- tests/plume/plume_impingement_utils.py | 125 +++- tests/plume/plume_verification_test_28.py | 8 +- tests/plume/plume_verification_test_29.py | 6 +- tests/plume/plume_verification_test_30.py | 6 +- tests/plume/plume_verification_test_31.py | 6 +- tests/plume/plume_verification_test_32.py | 6 +- tests/plume/plume_verification_test_33.py | 30 + tests/plume/plume_verification_test_34.py | 31 + tests/plume/plume_verification_test_35.py | 31 + tests/plume/plume_verification_test_36.py | 30 + tests/plume/plume_verification_test_37.py | 31 + tests/plume/plume_verification_test_38.py | 31 + tests/plume/plume_verification_test_39.py | 33 ++ tests/plume/plume_verification_test_40.py | 33 ++ 17 files changed, 1075 insertions(+), 28 deletions(-) create mode 100644 tests/plume/plume_verification_test_33.py create mode 100644 tests/plume/plume_verification_test_34.py create mode 100644 tests/plume/plume_verification_test_35.py create mode 100644 tests/plume/plume_verification_test_36.py create mode 100644 tests/plume/plume_verification_test_37.py create mode 100644 tests/plume/plume_verification_test_38.py create mode 100644 tests/plume/plume_verification_test_39.py create mode 100644 tests/plume/plume_verification_test_40.py diff --git a/pyrpod/plume/CaiImpingement2016.py b/pyrpod/plume/CaiImpingement2016.py index 5315365..3a6a6dc 100644 --- a/pyrpod/plume/CaiImpingement2016.py +++ b/pyrpod/plume/CaiImpingement2016.py @@ -1,16 +1,47 @@ """ -Verification reference for Cai 2016, Section 4 (3D inclined-plate impingement). +Verification reference for Cai 2016 (inclined-plate plume impingement). Cai, C., "Gaskinetic Modeling on Dilute Gaseous Plume Impingement Flows," Aerospace 2016, 3(4), 43, doi:10.3390/aerospace3040043. This module is a PLAIN-FUNCTION reference implementation of the paper's exact -surface-property solutions (Eqs. 9-15 with the Appendix A integrals) for a -collisionless round jet impinging on an inclined, rectangular, flat plate. -It exists to verify PyRPOD's plume modeling against the paper -- it is NOT a -new plume model class and must not be wired into the strike pipeline; the -Cai & Wang 2012 field models in RarefiedPlumeGasKinetics.py stay the plume -models of record. +solutions -- the single source of truth for every Cai-2016 verification +figure in tests/plume. It covers: + +* Section 4 surface properties: diffuse-plate Cp/Cf1/Cf2/Cq (Eqs. 9-13), + specular Cp (Eq. 14), Eq. 15 plate averages (Figs. 17-21); +* Section 3, the 2D slot jet on an inclined planar plate: surface + coefficients Cp,d/Cf,d/Cq,d and Cp,s (Eqs. 2-4, 8; Figs. 7-10) and the + combined jet + plate flowfield temperature (Figs. 5-6); +* Section 4 flowfield pressure in the vertical Y = 0 plane with the plate + contribution, diffuse and specular (Figs. 15-16). + +The free-jet field factors of the 3D solutions are imported from +pyrpod/plume/RarefiedPlumeGasKinetics.py (the Cai & Wang 2012 plume model +of record) rather than re-derived, so the two implementations can never +drift apart. This module exists to verify PyRPOD's plume modeling against +the paper -- it is NOT a new plume model class and must not be wired into +the strike pipeline. + +Section-3 validation geometry (interpretation note): this paper does not +print the 2D case's numbers (they come from its ref. [39], Cai & He 2016); +they are read off Figs. 1 and 5-10: slot height 2H, center-to-center +distance L = 4*(2H), plate semi-width W = 5*(2H), Tw/T0 = 1.5, with +(S0, alpha0) per figure legend. The plotted axes are X/(2H) and s/(2H). + +Flowfield figures (5-6, 15-16) evaluate the analytic solutions on the +whole X > 0 half-plane with no plate shadowing, exactly as the paper's +contours do (Fig. 6's specular temperature field is symmetric about the +plate line only under this convention); the diffuse plate emits from its +front face only, so behind-plate points carry the jet-only continuation. +Specular-plate effects use the paper's virtual-nozzle construction; the +virtual exit is built by literal mirror reflection of the real exit about +the plate plane, which reproduces Eq. 7 (2D) and the printed 3D virtual +position (L(1 - cos 2a0), 0, -L sin 2a0). The virtual DRIFT is the mirror +image U0*(cos 2a0, 0, sin 2a0); the paper's prose sign +(-U0 cos 2a0, ..., -U0 sin 2a0) is the velocity-space DOMAIN VERTEX -U0' +of Fig. 14, not the drift (same resolution style as the 2012 Eq. 7 typo +note in RarefiedPlumeGasKinetics). Geometry and symbol conventions (paper Fig. 11) ----------------------------------------------- @@ -83,6 +114,13 @@ import numpy as np from scipy.special import erf +from pyrpod.plume.RarefiedPlumeGasKinetics import ( + get_K_factor, + get_M_factor, + get_N_factor, + get_Q_full, +) + #: Gauss-Legendre order per axis for the exit-disk integrals. The integrand #: is analytic on the compact disk (denominators bounded below by X^2 > 0), #: so convergence is geometric; 48 nodes reach ~1e-12 for every geometry in @@ -318,6 +356,419 @@ def averaged_coefficients(S_0, alpha_0, eps, R_0, L, W_0, H_0, 's_cc': s_cc} +# --------------------------------------------------------------------------- +# Section 3: 2D slot jet impinging on an inclined planar plate (Eqs. 1-8) +# --------------------------------------------------------------------------- + +def _scaled_planar_factors(a, S): + ''' + e^(-S^2)-scaled polar-velocity wedge moments e^(a^2) * I_k(a) of a + drifting Maxwellian in 2D, k = 1, 2, 3, where + I_k(a) = Int_0^inf t^k e^(-(t-a)^2) dt; k = 2 and 3 reproduce the + Appendix A_0 and A_1. Here a = S*cos(theta - drift angle) may be + NEGATIVE (rays opposed to the drift); a^2 <= S^2 keeps every + exponential bounded and (1 + erf(a)) -> 0 kills opposed rays. + + Returns (E1, A0, A1), each pre-multiplied by e^(-S^2). + ''' + erf_term = (1 + erf(a)) * np.exp(a ** 2 - S ** 2) + exp_term = np.exp(-S ** 2) + sq = np.sqrt(np.pi) + E1 = 0.5 * exp_term + 0.5 * sq * a * erf_term + A0 = 0.25 * sq * (1 + 2 * a ** 2) * erf_term + 0.5 * a * exp_term + A1 = (0.25 * sq * (3 * a + 2 * a ** 3) * erf_term + + (0.5 + 0.5 * a ** 2) * exp_term) + return E1, A0, A1 + + +def _scaled_G_factor(a, S): + ''' + e^(-S^2)-scaled energy-flux integrand of Eq. 4, + G(a) = (sqrt(pi)/2)(2 + 7a^2 + 2a^4) e^(a^2) [1 + erf(a)] + 3a + a^3. + Verified to equal 2 e^(a^2) (I_4 + I_2/2): the planar c^3 moment + plus the out-of-plane w^2 energy carried by the number flux. + ''' + erf_term = (1 + erf(a)) * np.exp(a ** 2 - S ** 2) + return (0.5 * np.sqrt(np.pi) * (2 + 7 * a ** 2 + 2 * a ** 4) * erf_term + + (3 * a + a ** 3) * np.exp(-S ** 2)) + + +def planar_plate_point_coords(s, alpha_0, L): + '''2D plate point (X, Y) = (L + s cos(alpha_0), s sin(alpha_0)).''' + s = np.asarray(s, dtype=float) + return L + s * np.cos(alpha_0), s * np.sin(alpha_0) + + +def planar_surface_coefficients(s, S_0, alpha_0, eps, H, L, + order=DEFAULT_ORDER): + ''' + Exact 2D surface coefficients of Cai 2016 Eqs. 2-4 and 8 at plate + positions s (distance from the plate center along the plate). + + The wedge subtended by the slot exit (x = 0, y in [-H, H]) at the + plate point (X, Y) spans theta in [atan2(Y - H, X), atan2(Y + H, X)] + (Eq. 1); a = S_0 cos(theta). The diffuse-wall density n_w(s) follows + from non-penetration with the A_0 number-flux integral, + n_w/n_0 = 2/sqrt(pi*eps) * e^(-S_0^2) * + Int A_0(a) sin(alpha_0 - theta) dtheta, + the 2D analog of the 3D A_1-integral (module docstring). Front-face + visibility clamps sin(alpha_0 - theta) <= 0 contributions, and + plate points behind the exit plane (X <= 0) carry zero load. + + Returns dict with 'Cp_d', 'Cf_d', 'Cq_d', 'Cp_s', 'nw'. + ''' + s = np.asarray(s, dtype=float) + shape = s.shape + X, Y = planar_plate_point_coords(s.ravel(), alpha_0, L) + ahead = X > 0.0 + + nodes, wts = np.polynomial.legendre.leggauss(order) + Xa, Ya = X[ahead][:, None], Y[ahead][:, None] + th1 = np.arctan2(Ya - H, Xa) + th2 = np.arctan2(Ya + H, Xa) + mid, half = 0.5 * (th1 + th2), 0.5 * (th2 - th1) + theta = mid + half * nodes[None, :] + w = half * wts[None, :] + + a = S_0 * np.cos(theta) + _, A0s, A1s = _scaled_planar_factors(a, S_0) + Gs = _scaled_G_factor(a, S_0) + mu = np.sin(alpha_0 - theta) + front = mu > 0.0 + mu_p = np.where(front, mu, 0.0) + + I_p = np.sum(w * A1s * mu_p ** 2, axis=1) + I_f = np.sum(w * A1s * np.sin(2 * alpha_0 - 2 * theta) * front, axis=1) + I_q = np.sum(w * Gs * mu_p, axis=1) + I_n = np.sum(w * A0s * mu_p, axis=1) + + def expand(vals): + full = np.zeros(X.size) + full[ahead] = vals + return full.reshape(shape) + + nw = expand(2.0 / np.sqrt(np.pi * eps) * I_n) + Cp_jet = expand(2.0 / (np.pi * S_0 ** 2) * I_p) + Cf_d = expand(1.0 / (np.pi * S_0 ** 2) * I_f) + Cq_d = (expand(I_q / (2.0 * np.pi * S_0 ** 3)) + - eps ** 1.5 * nw / (np.sqrt(np.pi) * S_0 ** 3)) + return {'Cp_d': Cp_jet + eps / (2.0 * S_0 ** 2) * nw, + 'Cf_d': Cf_d, 'Cq_d': Cq_d, 'Cp_s': 2.0 * Cp_jet, 'nw': nw} + + +def _mirror_about_plate_2d(p, alpha_0, L): + '''Mirror 2D point(s) about the plate line through (L, 0) at alpha_0.''' + n_hat = np.array([-np.sin(alpha_0), np.cos(alpha_0)]) + p = np.asarray(p, dtype=float) + d = (p[..., 0] - L) * n_hat[0] + p[..., 1] * n_hat[1] + return p - 2.0 * d[..., None] * n_hat + + +def planar_flowfield(X, Y, S_0, alpha_0, eps, H, L, W, plate='diffuse', + order=DEFAULT_ORDER, nw_grid=1024): + ''' + Combined 2D flowfield moments at points (X, Y) for the Section-3 + problem: the free slot jet plus either the diffuse-plate wall + emission (Fig. 5) or the specular virtual nozzle (Fig. 6); + plate=None gives the free jet alone (used by the sanity checks). + + Populations are combined by raw moments (number, momentum, energy + including the out-of-plane thermal energy at each population's own + temperature); T/T0 = (2/3) beta_0^2 (M2/n - V^2) + 0 (the 1/3 + out-of-plane share is inside M2). Diffuse emission integrates over + arrival directions theta with the wall density n_w(s(theta)) at the + ray-plate intersection, interpolated from a dense s grid, so finite + plate edges are handled naturally (rays missing the plate carry + nothing) and only front-face emission counts. The specular virtual + exit is the literal mirror of the real exit with mirrored drift + (module docstring). No shadowing is modeled (matching the paper's + contours); points with X <= 0 return NaN. + + Returns dict 'n' (n/n_0), 'Ux', 'Uy' (times sqrt(beta_0)), + 'T' (T/T_0). + ''' + X = np.asarray(X, dtype=float) + shape = X.shape + Xf = X.ravel() + Yf = np.asarray(Y, dtype=float).ravel() + valid = Xf > 0.0 + Xa, Ya = Xf[valid][:, None], Yf[valid][:, None] + + nodes, wts = np.polynomial.legendre.leggauss(order) + N = np.zeros(Xa.size) + MX = np.zeros(Xa.size) + MY = np.zeros(Xa.size) + M2 = np.zeros(Xa.size) + + def add_population(th_lo, span, drift_angle, S, beta_ratio, + n_ref=None): + '''Wedge population moments; n_ref = None means constant n_0.''' + nonlocal N, MX, MY, M2 + theta = th_lo + span * 0.5 * (nodes[None, :] + 1.0) + w = span * 0.5 * wts[None, :] + a = S * np.cos(theta - drift_angle) + E1, A0s, _A1s = _scaled_planar_factors(a, S) + dens = 1.0 if n_ref is None else n_ref + dN = np.sum(w * dens * E1, axis=1) / np.pi + N += dN + MX += np.sum(w * dens * A0s * np.cos(theta), axis=1) \ + / np.pi * beta_ratio + MY += np.sum(w * dens * A0s * np.sin(theta), axis=1) \ + / np.pi * beta_ratio + M2 += np.sum(w * dens * _A1s, axis=1) / np.pi * beta_ratio ** 2 \ + + dN * beta_ratio ** 2 / 2.0 + + # free jet: wedge subtended by the slot exit, drift along +x + th1 = np.arctan2(Ya - H, Xa) + th2 = np.arctan2(Ya + H, Xa) + add_population(th1, th2 - th1, 0.0, S_0, 1.0) + + if plate == 'diffuse': + s_grid = np.linspace(-W, W, nw_grid) + nw_vals = planar_surface_coefficients(s_grid, S_0, alpha_0, eps, + H, L, order=order)['nw'] + t_hat = np.array([np.cos(alpha_0), np.sin(alpha_0)]) + n_hat = np.array([-np.sin(alpha_0), np.cos(alpha_0)]) + ends = np.array([planar_plate_point_coords(sgn * W, alpha_0, L) + for sgn in (-1.0, 1.0)]) + ang = np.arctan2(Ya - ends[:, 1], Xa - ends[:, 0]) + span = np.mod(ang[:, 1:2] - ang[:, 0:1] + np.pi, 2 * np.pi) - np.pi + lo = np.where(span >= 0, ang[:, 0:1], ang[:, 1:2]) + span = np.abs(span) + theta = lo + span * 0.5 * (nodes[None, :] + 1.0) + w = span * 0.5 * wts[None, :] + ct, st = np.cos(theta), np.sin(theta) + # ray-plate intersection: P = plate(s) + u * theta_hat, u > 0 + denom = t_hat[0] * st - t_hat[1] * ct + rx, ry = Xa - L, Ya + with np.errstate(divide='ignore', invalid='ignore'): + s_hit = (rx * st - ry * ct) / denom + u_hit = (t_hat[0] * ry - t_hat[1] * rx) / denom + emitting = (u_hit > 0.0) & (ct * n_hat[0] + st * n_hat[1] > 0.0) + nw_at = np.interp(s_hit, s_grid, nw_vals, left=0.0, right=0.0) + nw_at = np.where(emitting & np.isfinite(s_hit), nw_at, 0.0) + # resting wall Maxwellian: E1 = 1/2, A0 = sqrt(pi)/4, A1 = 1/2 + sqe = np.sqrt(eps) + dN = np.sum(w * nw_at, axis=1) * 0.5 / np.pi + N += dN + MX += np.sum(w * nw_at * ct, axis=1) \ + * (np.sqrt(np.pi) / 4.0) / np.pi * sqe + MY += np.sum(w * nw_at * st, axis=1) \ + * (np.sqrt(np.pi) / 4.0) / np.pi * sqe + M2 += np.sum(w * nw_at, axis=1) * 0.5 / np.pi * eps \ + + dN * eps / 2.0 + elif plate == 'specular': + exit_pts = np.array([[0.0, -H], [0.0, H]]) + mirrored = _mirror_about_plate_2d(exit_pts, alpha_0, L) + ang = np.arctan2(Ya - mirrored[:, 1], Xa - mirrored[:, 0]) + span = np.mod(ang[:, 1:2] - ang[:, 0:1] + np.pi, 2 * np.pi) - np.pi + lo = np.where(span >= 0, ang[:, 0:1], ang[:, 1:2]) + add_population(lo, np.abs(span), 2.0 * alpha_0, S_0, 1.0) + elif plate is not None: + raise ValueError(f'unknown plate treatment {plate!r}') + + with np.errstate(divide='ignore', invalid='ignore'): + Ux = MX / N + Uy = MY / N + T = (2.0 / 3.0) * (M2 / N - Ux ** 2 - Uy ** 2) + + def expand(vals): + full = np.full(Xf.size, np.nan) + full[valid] = vals + return full.reshape(shape) + + return {'n': expand(N), 'Ux': expand(Ux), 'Uy': expand(Uy), + 'T': expand(T)} + + +# --------------------------------------------------------------------------- +# Section 4 flowfield pressure in the Y = 0 plane (Figs. 15-16) +# --------------------------------------------------------------------------- + +def _plate_emission_moments(Xa, Za, Px, Py, Pz, nw, wA, alpha_0, eps, + chunk=128): + ''' + Raw-moment contributions of the diffuse-wall emission at field + points (Xa, 0, Za): solid-angle integrals over plate nodes + (Px, Py, Pz) carrying weights wA and wall densities nw, emitting + half-space Maxwellians at T_w from the front face only + (cos(xi) > 0). Per solid angle: dn = n_w/(4 pi) domega, + d(nV) = n_w sqrt(eps)/(2 pi^(3/2)) domega along the ray, and + dM2 = 3 n_w eps/(8 pi) domega (the resting-Maxwellian I_2, I_3, + I_4 moments; full-sphere limits n_w and 3 n_w R T_w check out). + + Returns (dN, dMx, dMy, dMz, dM2) normalized like the jet moments. + ''' + n_hat = np.array([-np.sin(alpha_0), 0.0, np.cos(alpha_0)]) + npts = Xa.size + dN = np.zeros(npts) + dMx = np.zeros(npts) + dMy = np.zeros(npts) + dMz = np.zeros(npts) + dM2 = np.zeros(npts) + mom = np.sqrt(eps) / (2.0 * np.pi ** 1.5) + for start in range(0, npts, chunk): + sl = slice(start, min(start + chunk, npts)) + dx = Xa[sl][:, None] - Px[None, :] + dy = -Py[None, :] * np.ones((Xa[sl].size, 1)) + dz = Za[sl][:, None] - Pz[None, :] + d2 = dx ** 2 + dy ** 2 + dz ** 2 + d = np.sqrt(d2) + cos_xi = (dx * n_hat[0] + dy * n_hat[1] + dz * n_hat[2]) / d + cos_xi = np.clip(cos_xi, 0.0, None) # front-face emission only + dodd = nw[None, :] * cos_xi / d2 * wA[None, :] + dN[sl] = np.sum(dodd, axis=1) / (4.0 * np.pi) + dMx[sl] = mom * np.sum(dodd * dx / d, axis=1) + dMy[sl] = mom * np.sum(dodd * dy / d, axis=1) + dMz[sl] = mom * np.sum(dodd * dz / d, axis=1) + dM2[sl] = 3.0 * eps / (8.0 * np.pi) * np.sum(dodd, axis=1) + return dN, dMx, dMy, dMz, dM2 + + +def _jet_moments_3d(x_loc, rho_loc, S_0, R_0, order=DEFAULT_ORDER, + chunk=256): + ''' + Raw moments (n/n_0, U*sqrt(beta_0), W*sqrt(beta_0), + M2 = n beta_0^2 / n_0) of the free round jet at local + coordinates (x_loc downstream of the exit, rho_loc off-axis), + evaluated with the Cai & Wang 2012 exit-disk integrals via the + K/M/N factors imported from RarefiedPlumeGasKinetics (single + source of truth). Zero for x_loc <= 0 (no backflow in the model). + ''' + x_loc = np.asarray(x_loc, dtype=float).ravel() + rho_loc = np.asarray(rho_loc, dtype=float).ravel() + + nodes, weights = np.polynomial.legendre.leggauss(order) + r = 0.5 * R_0 * (nodes + 1) + w_r = 0.5 * R_0 * weights + epsn = 0.5 * np.pi * nodes + w_eps = 0.5 * np.pi * weights + Rn, En = np.meshgrid(r, epsn, indexing='ij') + W2D = np.outer(w_r, w_eps) + sinE = np.sin(En) + + n = np.zeros(x_loc.size) + U = np.zeros(x_loc.size) + W = np.zeros(x_loc.size) + M2 = np.zeros(x_loc.size) + idx_all = np.nonzero(x_loc > 0.0)[0] + for start in range(0, idx_all.size, chunk): + idx = idx_all[start:start + chunk] + Xc = x_loc[idx][:, None, None] + Zc = rho_loc[idx][:, None, None] + Q = get_Q_full(Rn, En, Xc, Zc) + Kf = get_K_factor(Q, S_0) + Mf = get_M_factor(Q, S_0) + Nf = get_N_factor(Q, S_0) + I_K = np.sum(W2D * Rn * Kf, axis=(1, 2)) + I_M = np.sum(W2D * Rn * Mf, axis=(1, 2)) + I_W = np.sum(W2D * (Zc - Rn * sinE) * Rn * Mf, axis=(1, 2)) + I_N = np.sum(W2D * Rn * Nf, axis=(1, 2)) + n[idx] = I_K / (np.pi ** 1.5 * x_loc[idx] ** 2) + U[idx] = I_M / I_K + W[idx] = I_W / (x_loc[idx] * I_K) + T = (-(2.0 / 3.0) * (U[idx] ** 2 + W[idx] ** 2) + + (4.0 / 3.0) * I_N / I_K) + M2[idx] = n[idx] * (1.5 * T + U[idx] ** 2 + W[idx] ** 2) + return n, U, W, M2 + + +def flowfield_pressure_plane(X, Z, S_0, alpha_0, eps, R_0, L, W_0, H_0, + plate='diffuse', order=DEFAULT_ORDER, + plate_order=48, chunk=128): + ''' + Static-pressure field p/p_0 = (n/n_0)(T/T_0) in the vertical Y = 0 + plane for the Section-4 3D impingement problem (Figs. 15-16), + normalized by the exit static pressure p_0 = n_0 k T_0. The local + temperature enters explicitly (the 2012 paper warns n*k*T_0 is + invalid), assembled from the raw moments of the populations: + + * the free round jet (2012 exit-disk integrals, see + _jet_moments_3d); + * plate='diffuse': the wall re-emission -- a solid-angle integral + over the plate of half-space Maxwellians with the Eq.-9 n_w(s, + tau) at the plate's own Gauss-Legendre nodes (front-face + emission only, cos(xi) > 0); + * plate='specular': the virtual nozzle mirrored about the plate + plane with mirrored drift (module docstring), evaluated with the + same jet integrals in the virtual frame; + * plate=None: free jet alone (sanity checks). + + No shadowing is modeled, matching the paper's contour convention; + X <= 0 returns NaN. + + Returns dict 'n', 'T', 'p' (all normalized), shaped like X. + ''' + X = np.asarray(X, dtype=float) + shape = X.shape + Xf = X.ravel() + Zf = np.asarray(Z, dtype=float).ravel() + valid = Xf > 0.0 + Xa, Za = Xf[valid], Zf[valid] + npts = Xa.size + + # free jet in the global frame (axis +x, exit at the origin) + n1, U1, W1, M21 = _jet_moments_3d(Xa, np.abs(Za), S_0, R_0, order=order) + N = n1.copy() + MXv = n1 * U1 + MYv = np.zeros(npts) + MZv = n1 * W1 * np.sign(Za) + M2 = M21.copy() + + if plate == 'diffuse': + nodes, wts = np.polynomial.legendre.leggauss(plate_order) + s_n = W_0 * nodes + w_s = W_0 * wts + t_n = H_0 * nodes + w_t = H_0 * wts + Sg, Tg = np.meshgrid(s_n, t_n, indexing='ij') + wA = np.outer(w_s, w_t).ravel() + Px, Py, Pz = plate_point_coords(Sg.ravel(), Tg.ravel(), alpha_0, L) + nw = surface_coefficients(Px, Py, Pz, S_0, alpha_0, eps, R_0, + order=order)['nw'] + dN, dMx, dMy, dMz, dM2 = _plate_emission_moments( + Xa, Za, Px, Py, Pz, nw, wA, alpha_0, eps, chunk=chunk) + N += dN + MXv += dMx + MYv += dMy + MZv += dMz + M2 += dM2 + elif plate == 'specular': + origin_v = np.array([L * (1.0 - np.cos(2 * alpha_0)), 0.0, + -L * np.sin(2 * alpha_0)]) + axis_v = np.array([np.cos(2 * alpha_0), 0.0, np.sin(2 * alpha_0)]) + rx = Xa - origin_v[0] + rz = Za - origin_v[2] + x_loc = rx * axis_v[0] + rz * axis_v[2] + px = rx - x_loc * axis_v[0] + pz = rz - x_loc * axis_v[2] + rho = np.hypot(px, pz) + n2, U2, W2, M22 = _jet_moments_3d(x_loc, rho, S_0, R_0, order=order) + safe = rho > 1e-12 + rhx = np.where(safe, px / np.where(safe, rho, 1.0), 0.0) + rhz = np.where(safe, pz / np.where(safe, rho, 1.0), 0.0) + N += n2 + MXv += n2 * (U2 * axis_v[0] + W2 * rhx) + MZv += n2 * (U2 * axis_v[2] + W2 * rhz) + M2 += M22 + elif plate is not None: + raise ValueError(f'unknown plate treatment {plate!r}') + + with np.errstate(divide='ignore', invalid='ignore'): + Vx = MXv / N + Vy = MYv / N + Vz = MZv / N + T = (2.0 / 3.0) * (M2 / N - Vx ** 2 - Vy ** 2 - Vz ** 2) + p = N * T + + def expand(vals): + full = np.full(Xf.size, np.nan) + full[valid] = vals + return full.reshape(shape) + + return {'n': expand(N), 'T': expand(T), 'p': expand(p)} + + def run_sanity_checks(S_0=2.0, alpha_0=np.deg2rad(60.0), eps=1.5, R_0=0.5, L=4.0, verbose=True): ''' @@ -397,13 +848,223 @@ def flux_integrand(r, th): return results +def run_planar_sanity_checks(S_0=2.0, alpha_0=np.deg2rad(60.0), eps=1.5, + H=0.5, L=4.0, W=5.0, verbose=True): + ''' + Verification of the Section-3 (2D planar) implementation: + + 1. alpha_0 = 90 deg parity: Cp,d/Cq,d/n_w even and Cf,d odd in s + (the normal-plate configuration is mirror-symmetric). + 2. Cp,s = 2 x jet-only part of Cp,d (Eq. 8 vs Eq. 2). + 3. n_w flux balance against an independent scipy.integrate.quad + evaluation of the A_0 number-flux integral. + 4. Diffuse half-space closure: just in front of the plate center + the wall-emission population fills a half-space, so its density + tends to n_w(0)/2 (checked via flowfield(diffuse) - flowfield + (jet only)). + 5. Specular mirror symmetry: the virtual-nozzle field satisfies + T(P) = T(P') and n(P) = n(P') for P' mirrored about the plate + line -- the defining symmetry of Fig. 6. + + Raises AssertionError on failure; returns measured deviations. + ''' + from scipy import integrate + + results = {} + s = np.linspace(-4.0, 4.0, 17) + c90 = planar_surface_coefficients(s, S_0, np.pi / 2, eps, H, L) + for key, parity in [('Cp_d', 1), ('Cq_d', 1), ('nw', 1), ('Cf_d', -1)]: + dev = np.max(np.abs(c90[key] - parity * c90[key][::-1])) + scale = np.max(np.abs(c90[key])) + results[f'planar_parity_{key}'] = dev / scale + assert dev <= 1e-12 * scale, f'2D {key} parity violated: {dev}' + + c = planar_surface_coefficients(s, S_0, alpha_0, eps, H, L) + dev = np.max(np.abs( + c['Cp_s'] - 2.0 * (c['Cp_d'] - eps / (2 * S_0 ** 2) * c['nw']))) + results['planar_specular_vs_jet'] = dev + assert dev <= 1e-14, f'2D Cp_s != 2 x jet-only Cp_d: {dev}' + + for s0 in (0.0, -2.0, 3.0): + X, Y = planar_plate_point_coords(s0, alpha_0, L) + + def flux_integrand(theta): + a = S_0 * np.cos(theta) + _, A0s, _ = _scaled_planar_factors(np.asarray(a), S_0) + return float(A0s) * max(np.sin(alpha_0 - theta), 0.0) + + I_ref, _ = integrate.quad(flux_integrand, + np.arctan2(Y - H, X), + np.arctan2(Y + H, X), + epsabs=1e-13, epsrel=1e-12) + nw_ref = 2.0 / np.sqrt(np.pi * eps) * I_ref + nw_gl = float(planar_surface_coefficients( + np.array([s0]), S_0, alpha_0, eps, H, L)['nw'][0]) + dev = abs(nw_gl - nw_ref) / nw_ref + results[f'planar_nw_quad_s{s0}'] = dev + assert dev <= 1e-9, f'2D n_w flux balance failed at s={s0}: {dev}' + + h = 1e-3 + n_hat = np.array([-np.sin(alpha_0), np.cos(alpha_0)]) + P = np.array([L, 0.0]) + h * n_hat + both = planar_flowfield(P[0:1], P[1:2], S_0, alpha_0, eps, H, L, W, + plate='diffuse') + jet = planar_flowfield(P[0:1], P[1:2], S_0, alpha_0, eps, H, L, W, + plate=None) + nw0 = float(planar_surface_coefficients( + np.array([0.0]), S_0, alpha_0, eps, H, L)['nw'][0]) + emission = float(both['n'][0] - jet['n'][0]) + dev = abs(emission - nw0 / 2.0) / (nw0 / 2.0) + results['planar_halfspace_closure'] = dev + assert dev <= 1e-2, f'2D wall-emission half-space closure: {dev}' + + pts = np.array([[1.0, 0.5], [2.5, -1.0], [3.0, 2.0], [1.5, -2.5]]) + mirrored = _mirror_about_plate_2d(pts, alpha_0, L) + f1 = planar_flowfield(pts[:, 0], pts[:, 1], S_0, alpha_0, eps, H, L, + W, plate='specular') + f2 = planar_flowfield(mirrored[:, 0], mirrored[:, 1], S_0, alpha_0, + eps, H, L, W, plate='specular') + for key in ('n', 'T'): + dev = np.max(np.abs(f1[key] - f2[key]) / np.abs(f1[key])) + results[f'planar_specular_mirror_{key}'] = dev + assert dev <= 1e-9, f'2D specular mirror symmetry ({key}): {dev}' + + if verbose: + for name, value in results.items(): + print(f' {name}: {value:.3e}') + return results + + +def run_flowfield3d_sanity_checks(S_0=2.0, alpha_0=np.deg2rad(60.0), + eps=1.5, R_0=0.5, L=4.0, W_0=4.0, + H_0=4.0, verbose=True): + ''' + Verification of the Section-4 flowfield-pressure implementation + (Figs. 15-16): + + 1. Solid-angle sum rule for the wall-emission kernel: a uniformly + emitting square plate (half-side a, on-axis height h) subtends + the closed-form solid angle 4*atan(a^2/(h*sqrt(2a^2 + h^2))), + so the emission density must equal n_w * Omega/(4 pi) exactly + (-> n_w/2 in the half-space limit). Validates the kernel + constant and geometry independent of n_w. + 2. GL-vs-dblquad of the real n_w-weighted emission-density + integral at a field point (validates the assembly with the + Eq.-9 wall density). + 3. Specular mirror symmetry in the Y = 0 plane: n, T, p match at + points mirrored about the plate line (the virtual-nozzle + construction's defining property). + + Raises AssertionError on failure; returns measured deviations. + ''' + from scipy import integrate + + results = {} + kw = dict(S_0=S_0, alpha_0=alpha_0, eps=eps, R_0=R_0, L=L, + W_0=W_0, H_0=H_0) + + # 1. uniform-emitter sum rule (synthetic plate, alpha_0 = 90 deg so + # the plate normal is -x and "on-axis height" is along x) + a_test, h_test, n_gl = 40.0, 5.0, 96 + nodes, wts = np.polynomial.legendre.leggauss(n_gl) + Sg, Tg = np.meshgrid(a_test * nodes, a_test * nodes, indexing='ij') + wA = np.outer(a_test * wts, a_test * wts).ravel() + Px, Py, Pz = plate_point_coords(Sg.ravel(), Tg.ravel(), np.pi / 2, 0.0) + dN, _, _, _, _ = _plate_emission_moments( + np.array([-h_test]), np.array([0.0]), Px, Py, Pz, + np.ones(Px.size), wA, np.pi / 2, eps) + omega = 4.0 * np.arctan(a_test ** 2 / ( + h_test * np.sqrt(2 * a_test ** 2 + h_test ** 2))) + dev = abs(float(dN[0]) - omega / (4 * np.pi)) / (omega / (4 * np.pi)) + results['3d_solid_angle_sum_rule'] = dev + assert dev <= 1e-6, f'3D emission solid-angle sum rule: {dev}' + + # 2. GL vs dblquad with the real Eq.-9 n_w at a resolvable standoff + n_hat2 = np.array([-np.sin(alpha_0), np.cos(alpha_0)]) + P = np.array([L, 0.0]) + 1.0 * n_hat2 + np.array([0.3, 0.0]) + both = flowfield_pressure_plane(P[0:1], P[1:2], plate='diffuse', **kw) + jet = flowfield_pressure_plane(P[0:1], P[1:2], plate=None, **kw) + emission_gl = float(both['n'][0] - jet['n'][0]) + n_hat3 = np.array([-np.sin(alpha_0), 0.0, np.cos(alpha_0)]) + + def integrand(tau, s): + px, py, pz = plate_point_coords(s, tau, alpha_0, L) + nw = float(surface_coefficients( + np.array([px]), np.array([py]), np.array([pz]), + S_0, alpha_0, eps, R_0)['nw'][0]) + dvec = np.array([P[0] - px, -py, P[1] - pz]) + d2 = float(dvec @ dvec) + cos_xi = max(float(dvec @ n_hat3) / np.sqrt(d2), 0.0) + return nw * cos_xi / d2 / (4.0 * np.pi) + + emission_ref, _ = integrate.dblquad(integrand, -W_0, W_0, -H_0, H_0, + epsabs=1e-10, epsrel=1e-8) + dev = abs(emission_gl - emission_ref) / emission_ref + results['3d_emission_gl_vs_dblquad'] = dev + assert dev <= 1e-6, f'3D emission GL vs dblquad: {dev}' + + pts = np.array([[1.0, 0.5], [2.5, -1.0], [3.0, 2.0], [1.5, -2.5]]) + mirrored = _mirror_about_plate_2d(pts, alpha_0, L) + f1 = flowfield_pressure_plane(pts[:, 0], pts[:, 1], + plate='specular', **kw) + f2 = flowfield_pressure_plane(mirrored[:, 0], mirrored[:, 1], + plate='specular', **kw) + for key in ('n', 'T', 'p'): + dev = np.max(np.abs(f1[key] - f2[key]) / np.abs(f1[key])) + results[f'3d_specular_mirror_{key}'] = dev + assert dev <= 1e-9, f'3D specular mirror symmetry ({key}): {dev}' + + if verbose: + for name, value in results.items(): + print(f' {name}: {value:.3e}') + return results + + if __name__ == '__main__': print('Cai 2016 reference implementation sanity checks ' '(S0=2, alpha0=60 deg, eps=1.5, L=4D):') run_sanity_checks() + print('Section-3 (2D planar) checks:') + run_planar_sanity_checks() + print('Section-4 flowfield-pressure checks:') + run_flowfield3d_sanity_checks() print('all checks passed') avg = averaged_coefficients(2.0, np.deg2rad(60.0), 1.5, 0.5, 4.0, 4.0, 4.0) print('Eq. 15 averaged coefficients at the paper conditions:') for key, value in avg.items(): print(f' {key} = {value:.6g}') + + # paper-figure anchors (visual validation targets) + s = np.linspace(-5.0, 5.0, 401) + c2d = planar_surface_coefficients(s, 2.0, np.deg2rad(60.0), 1.5, + 0.5, 4.0) + print('2D anchors (S0=2, 60 deg): ' + f"peak Cp_d {c2d['Cp_d'].max():.3f} (Fig. 7 ~0.93), " + f"peak Cp_s {c2d['Cp_s'].max():.3f} (Fig. 8 ~1.23), " + f"peak Cf_d {c2d['Cf_d'].max():.3f} (Fig. 9 ~0.33), " + f"peak Cq_d {c2d['Cq_d'].max():.3f} (Fig. 10 ~0.27)") + sign_change = s[np.nonzero(np.diff(np.sign(c2d['Cf_d'])))[0]] + print(f' Cf_d zero crossings at s/(2H) = {np.round(sign_change, 2)} ' + '(Fig. 9 separation point ~ -2)') + x = np.linspace(0.2, 7.0, 60) + y = np.linspace(-3.5, 3.5, 50) + Xg, Yg = np.meshgrid(x, y) + Td = planar_flowfield(Xg, Yg, 2.0, np.deg2rad(60.0), 1.5, 0.5, 4.0, + 5.0, plate='diffuse')['T'] + Ts = planar_flowfield(Xg, Yg, 2.0, np.deg2rad(60.0), 1.5, 0.5, 4.0, + 5.0, plate='specular')['T'] + print(f'2D flowfield peaks: diffuse T/T0 {np.nanmax(Td):.2f} ' + f'(Fig. 5 top contour 2.4), specular {np.nanmax(Ts):.2f} ' + '(Fig. 6 top contour 3.5)') + pd = flowfield_pressure_plane(Xg, Yg, 2.0, np.deg2rad(60.0), 1.5, + 0.5, 4.0, 4.0, 4.0, plate='diffuse')['p'] + ps = flowfield_pressure_plane(Xg, Yg, 2.0, np.deg2rad(60.0), 1.5, + 0.5, 4.0, 4.0, 4.0, + plate='specular')['p'] + # near the exit p/p0 -> 1 for any plate; the figure anchor is the + # impingement-center accumulation region (X > 2.5) + imp = Xg > 2.5 + print('3D flowfield impingement-region peaks: diffuse p/p0 ' + f'{np.nanmax(pd[imp]):.2f} (Fig. 15 top contour 0.5), specular ' + f'{np.nanmax(ps[imp]):.2f} (Fig. 16 top contour 0.4)') diff --git a/tests/README.md b/tests/README.md index d107c74..ffcecf1 100644 --- a/tests/README.md +++ b/tests/README.md @@ -46,6 +46,14 @@ This dashboard provides an overview of all tests in the PyRPOD project, categori | `plume_verification_test_30.py` | Verification | Cai 2016 Fig. 19: diffuse-plate Cf1 contours (manual-run figure). | ✅ | | `plume_verification_test_31.py` | Verification | Cai 2016 Fig. 20: diffuse-plate Cf2 contours (manual-run figure). | ✅ | | `plume_verification_test_32.py` | Verification | Cai 2016 Fig. 21: diffuse-plate Cq contours (manual-run figure). | ✅ | +| `plume_verification_test_33.py` | Verification | Cai 2016 Fig. 5: 2D diffuse-plate flowfield T contours (manual-run figure). | ✅ | +| `plume_verification_test_34.py` | Verification | Cai 2016 Fig. 6: 2D specular-plate flowfield T contours (manual-run figure). | ✅ | +| `plume_verification_test_35.py` | Verification | Cai 2016 Fig. 7: 2D diffuse-plate Cp profiles (manual-run figure). | ✅ | +| `plume_verification_test_36.py` | Verification | Cai 2016 Fig. 8: 2D specular-plate Cp profiles (manual-run figure). | ✅ | +| `plume_verification_test_37.py` | Verification | Cai 2016 Fig. 9: 2D diffuse-plate Cf profiles (manual-run figure). | ✅ | +| `plume_verification_test_38.py` | Verification | Cai 2016 Fig. 10: 2D diffuse-plate Cq profiles (manual-run figure). | ✅ | +| `plume_verification_test_39.py` | Verification | Cai 2016 Fig. 15: 3D diffuse-plate flowfield p contours (manual-run figure). | ✅ | +| `plume_verification_test_40.py` | Verification | Cai 2016 Fig. 16: 3D specular-plate flowfield p contours (manual-run figure). | ✅ | | `plume_impingement_error_summary.py` | Verification | Cai 2016 reference vs PyRPOD-chain max/mean error table (manual-run generator). | ✅ | --- diff --git a/tests/plume/data/digitized/README.md b/tests/plume/data/digitized/README.md index 0efda09..29c2d9a 100644 --- a/tests/plume/data/digitized/README.md +++ b/tests/plume/data/digitized/README.md @@ -7,10 +7,15 @@ automatically on their next run. No code changes are needed. The same convention serves the Cai 2016 impingement figures (Aerospace 3(4):43, DOI 10.3390/aerospace3040043) reproduced by -`plume_verification_test_28` ... `_32`: slots `fig17_*.csv` ... -`fig21_*.csv`, one CSV per digitized DSMC/analytic contour polyline in -plate coordinates (x = s, y = tau, both normalized by the nozzle -diameter), e.g. `fig17_dsmc_0p2.csv` for the Cp = 0.2 line of Fig. 17. +`plume_verification_test_28` ... `_40`, with a `cai16_` stem prefix so +the 2016 paper's figure numbers never collide with the 2012 slots +above: `cai16_fig05_*.csv` ... `cai16_fig10_*.csv` (Section 3, 2D +planar plate), `cai16_fig15_*.csv`, `cai16_fig16_*.csv` (flowfield +pressure contours), and `cai16_fig17_*.csv` ... `cai16_fig21_*.csv` +(3D plate surface coefficients). One CSV per digitized DSMC/analytic +curve or contour polyline in the figure's plotted units, e.g. +`cai16_fig17_dsmc_0p2.csv` for the Cp = 0.2 line of the 2016 Fig. 17, +or `cai16_fig07_dsmc.csv` for the DSMC Cp profile of the 2016 Fig. 7. ## File convention diff --git a/tests/plume/plume_impingement_utils.py b/tests/plume/plume_impingement_utils.py index c0eaec0..37cf6be 100644 --- a/tests/plume/plume_impingement_utils.py +++ b/tests/plume/plume_impingement_utils.py @@ -32,8 +32,22 @@ # sigma = 0 (fully specular reflection) -- Shen's formulas then drop # the wall-temperature term, matching the paper's remark that the # plate temperature has no effect on Cp,s. -# * Digitized-overlay slots follow tests/plume/data/digitized/README.md: -# fig17_*.csv ... fig21_*.csv (absent files silently skipped). +# * Scripts _33.._40 reproduce the paper's remaining figures from the +# same single source of truth (pyrpod/plume/CaiImpingement2016.py): +# Figs. 5-6 (Section-3 2D flowfield temperature with a diffuse / +# specular plate), Figs. 7-10 (2D plate surface Cp/Cf/Cq profiles), +# and Figs. 15-16 (Section-4 flowfield pressure in the Y = 0 plane). +# The Section-3 validation geometry (slot 2H, L = 4*(2H), plate +# semi-width W = 5*(2H)) is read off the paper's figures -- see the +# reference module docstring. These figures have no PyRPOD-chain +# overlay: SimplifiedGasKinetics is a round-nozzle model (not +# comparable to the 2D slot jet) and the strike pipeline computes no +# flowfields-with-plates, so they are reference + digitized-DSMC +# reproductions only. +# * Digitized-overlay slots follow tests/plume/data/digitized/README.md +# with the cai16_ stem prefix (the 2016 paper's figure numbers would +# otherwise collide with the 2012 slots): cai16_fig05_*.csv ... +# cai16_fig21_*.csv (absent files silently skipped). # * Generic figure plumbing (run_script, overlay_digitized, # annotate_error, max_rel_diff) is reused from plume_figure_utils; # only the paper-specific constants live here. Figures are saved to @@ -86,6 +100,18 @@ OUTPUT_DIR = base.OUTPUT_DIR / 'Cai2016' # figure output subfolder +# Section-3 (2D slot jet) validation geometry in units of the slot +# height D2 = 2H (read off the paper's figures; see the reference +# module docstring): L = 4*(2H), plate semi-width W = 5*(2H). +D2_SLOT = 1.0 +H_SLOT = D2_SLOT / 2.0 +L_2D = 4.0 * D2_SLOT +W_2D = 5.0 * D2_SLOT +ALPHA_2D = np.deg2rad(60.0) # Figs. 5-6 inclination + +#: paper-legend line styles for the four-parameter profile figures +PROFILE_STYLES = ['-', '--', (0, (6, 2)), '-.'] + THRUSTER_CHARACTERISTICS = {'d': D_NOZZLE, 've': U_0, 'R': R_SPECIFIC, 'gamma': GAMMA, 'Te': T_0, 'n': N_0} @@ -251,3 +277,98 @@ def impingement_contour_figure(fig_stem, ref_field, chain_field, levels, f'mean rel diff = {mean_rel_diff(chain_field, ref_field, mask):.2g}\n' r'(where $|C_{ref}| \geq$ 5% of peak)') return save_figure(fig, file_name) + + +def _draw_plate_trace(ax, alpha_0, L, semi_length): + """Plate trace in the plotted plane (2D plate line or the 3D plate's + Y = 0 section), drawn like the paper's contour figures.""" + tau = np.array([-semi_length, semi_length]) + ax.plot(L + tau * np.cos(alpha_0), tau * np.sin(alpha_0), 'k-', lw=1.6) + + +def planar_temperature_contour_figure(plate, levels, fig_stem, file_name, + title, S_0=2.0, grid_n=161): + """Figs. 5-6 builder: 2D flowfield temperature contours T/T0 for the + slot jet with a diffuse or specular plate (delegates to + cai.planar_flowfield), solid black with labels, plate line drawn, + digitized slots overlaid.""" + x = np.linspace(0.05, 8.0, grid_n) + y = np.linspace(-4.0, 4.0, grid_n) + Xg, Yg = np.meshgrid(x * D2_SLOT, y * D2_SLOT) + T = cai.planar_flowfield(Xg, Yg, S_0, ALPHA_2D, EPS, H_SLOT, L_2D, + W_2D, plate=plate)['T'] + + fig, ax = plt.subplots(figsize=(6, 6)) + cs = ax.contour(x, y, T, levels=levels, colors='k', linewidths=1.1) + ax.clabel(cs, fmt='%g', fontsize=7) + _draw_plate_trace(ax, ALPHA_2D, L_2D, W_2D) + n_dig = base.overlay_digitized(ax, fig_stem, style='line') + + ax.set_xlim(0, 8) + ax.set_ylim(-4, 4) + ax.set_xlabel('X/(2H)') + ax.set_ylabel('Y/(2H)') + ax.set_title(title) + handles = [plt.Line2D([], [], color='k', lw=1.1, label='Analytical')] + if n_dig: + handles.append(plt.Line2D([], [], color='k', lw=1.0, + label='DSMC (digitized)')) + ax.legend(handles=handles, fontsize=7, loc='lower right') + return save_figure(fig, file_name) + + +def planar_profile_figure(quantity, params, fig_stem, file_name, title, + ylabel, ylim, n_s=401): + """Figs. 7-10 builder: 2D plate surface coefficient profiles vs + s/(2H) for (S_0, alpha_0 deg) parameter combinations (delegates to + cai.planar_surface_coefficients), paper-style black line styles, + digitized slots overlaid.""" + s = np.linspace(-5.0, 5.0, n_s) * D2_SLOT + + fig, ax = plt.subplots(figsize=(6, 4.5)) + for (S_0, alpha_deg), ls in zip(params, PROFILE_STYLES): + coeffs = cai.planar_surface_coefficients( + s, S_0, np.deg2rad(alpha_deg), EPS, H_SLOT, L_2D) + ax.plot(s / D2_SLOT, coeffs[quantity], color='k', ls=ls, lw=1.1, + label=f'$S_0$={S_0:g}, {alpha_deg:g}$^\\circ$') + base.overlay_digitized(ax, fig_stem) + + ax.set_xlim(-5, 6) + ax.set_ylim(*ylim) + ax.set_xlabel('s/(2H)') + ax.set_ylabel(ylabel) + ax.set_title(title) + ax.legend(fontsize=8) + return save_figure(fig, file_name) + + +def pressure_contour_figure_3d(plate, levels, fig_stem, file_name, title, + S_0=2.0, grid_n=141): + """Figs. 15-16 builder: Section-4 flowfield static-pressure contours + p/p0 in the vertical Y = 0 plane with a diffuse or specular plate + (delegates to cai.flowfield_pressure_plane), plate section drawn, + digitized slots overlaid.""" + x = np.linspace(0.05, 8.0, grid_n) + z = np.linspace(-4.0, 4.0, grid_n) + Xg, Zg = np.meshgrid(x * D_NOZZLE, z * D_NOZZLE) + p = cai.flowfield_pressure_plane( + Xg, Zg, S_0, ALPHA_0, EPS, R_0, L_PLATE, W_0, H_0, + plate=plate)['p'] + + fig, ax = plt.subplots(figsize=(6, 6)) + cs = ax.contour(x, z, p, levels=levels, colors='k', linewidths=1.1) + ax.clabel(cs, fmt='%g', fontsize=7) + _draw_plate_trace(ax, ALPHA_0, L_PLATE, H_0) + n_dig = base.overlay_digitized(ax, fig_stem, style='line') + + ax.set_xlim(0, 8) + ax.set_ylim(-4, 4) + ax.set_xlabel('$X/(2R_0)$') + ax.set_ylabel('$W/(2R_0)$') + ax.set_title(title) + handles = [plt.Line2D([], [], color='k', lw=1.1, label='Analytical')] + if n_dig: + handles.append(plt.Line2D([], [], color='k', lw=1.0, + label='DSMC (digitized)')) + ax.legend(handles=handles, fontsize=7, loc='lower right') + return save_figure(fig, file_name) diff --git a/tests/plume/plume_verification_test_28.py b/tests/plume/plume_verification_test_28.py index 9970ffc..7e3a818 100644 --- a/tests/plume/plume_verification_test_28.py +++ b/tests/plume/plume_verification_test_28.py @@ -11,8 +11,10 @@ # # Manual-run verification script (design decision D5): no pytest tests; # run directly -- python tests/plume/plume_verification_test_28.py -# Digitized overlays looked for: fig17_*.csv (e.g. fig17_dsmc_0p2.csv, -# one CSV per digitized contour polyline). +# Digitized overlays looked for: cai16_fig17_*.csv (e.g. +# cai16_fig17_dsmc_0p2.csv, one CSV per digitized contour polyline; the +# cai16_ prefix keeps the 2016 paper's slots distinct from the Cai & +# Wang 2012 fig17 slot). import plume_impingement_utils as u @@ -22,7 +24,7 @@ def generate_figure(): chain = u.chain_grid() levels = [0.005, 0.01, 0.05, 0.1, 0.2] return u.impingement_contour_figure( - 'fig17', ref['Cp_d'], chain['Cp'], levels, + 'cai16_fig17', ref['Cp_d'], chain['Cp'], levels, 'Diffuse plate $C_{p,d}(s,\\tau)$, $S_0$=2.0, ' '$T_w/T_0$=1.5, $\\alpha_0$=60$^\\circ$ (Fig. 17)', 'fig17_diffuse_pressure') diff --git a/tests/plume/plume_verification_test_29.py b/tests/plume/plume_verification_test_29.py index f2260c1..858221e 100644 --- a/tests/plume/plume_verification_test_29.py +++ b/tests/plume/plume_verification_test_29.py @@ -10,8 +10,8 @@ # # Manual-run verification script (design decision D5): no pytest tests; # run directly -- python tests/plume/plume_verification_test_29.py -# Digitized overlays looked for: fig18_*.csv (one CSV per digitized -# contour polyline). +# Digitized overlays looked for: cai16_fig18_*.csv (one CSV per +# digitized contour polyline; cai16_ prefix avoids the 2012 fig18 slot). import plume_impingement_utils as u @@ -21,7 +21,7 @@ def generate_figure(): chain = u.chain_grid(sigma=0.0) levels = [0.005, 0.01, 0.05, 0.1, 0.2, 0.3] return u.impingement_contour_figure( - 'fig18', ref['Cp_s'], chain['Cp'], levels, + 'cai16_fig18', ref['Cp_s'], chain['Cp'], levels, 'Specular plate $C_{p,s}(s,\\tau)$, $S_0$=2.0, ' '$\\alpha_0$=60$^\\circ$ (Fig. 18)', 'fig18_specular_pressure', diff --git a/tests/plume/plume_verification_test_30.py b/tests/plume/plume_verification_test_30.py index c401773..964be20 100644 --- a/tests/plume/plume_verification_test_30.py +++ b/tests/plume/plume_verification_test_30.py @@ -12,8 +12,8 @@ # # Manual-run verification script (design decision D5): no pytest tests; # run directly -- python tests/plume/plume_verification_test_30.py -# Digitized overlays looked for: fig19_*.csv (one CSV per digitized -# contour polyline). +# Digitized overlays looked for: cai16_fig19_*.csv (one CSV per +# digitized contour polyline; cai16_ prefix avoids the 2012 fig19 slot). import plume_impingement_utils as u @@ -23,7 +23,7 @@ def generate_figure(): chain = u.chain_grid() levels = [-0.001, 0.0, 0.001, 0.01, 0.05] return u.impingement_contour_figure( - 'fig19', ref['Cf1_d'], chain['Cf1'], levels, + 'cai16_fig19', ref['Cf1_d'], chain['Cf1'], levels, 'Diffuse plate $C_{f_1,d}(s,\\tau)$, $S_0$=2.0, ' '$T_w/T_0$=1.5, $\\alpha_0$=60$^\\circ$ (Fig. 19)', 'fig19_diffuse_friction_tau') diff --git a/tests/plume/plume_verification_test_31.py b/tests/plume/plume_verification_test_31.py index 9553910..2801257 100644 --- a/tests/plume/plume_verification_test_31.py +++ b/tests/plume/plume_verification_test_31.py @@ -10,8 +10,8 @@ # # Manual-run verification script (design decision D5): no pytest tests; # run directly -- python tests/plume/plume_verification_test_31.py -# Digitized overlays looked for: fig20_*.csv (one CSV per digitized -# contour polyline). +# Digitized overlays looked for: cai16_fig20_*.csv (one CSV per +# digitized contour polyline; cai16_ prefix avoids the 2012 fig20 slot). import plume_impingement_utils as u @@ -22,7 +22,7 @@ def generate_figure(): levels = [-0.02, -0.01, -0.005, -0.001, 0.001, 0.005, 0.01, 0.02] return u.impingement_contour_figure( - 'fig20', ref['Cf2_d'], chain['Cf2'], levels, + 'cai16_fig20', ref['Cf2_d'], chain['Cf2'], levels, 'Diffuse plate $C_{f_2,d}(s,\\tau)$, $S_0$=2.0, ' '$T_w/T_0$=1.5, $\\alpha_0$=60$^\\circ$ (Fig. 20)', 'fig20_diffuse_friction_s') diff --git a/tests/plume/plume_verification_test_32.py b/tests/plume/plume_verification_test_32.py index d533a64..d4115af 100644 --- a/tests/plume/plume_verification_test_32.py +++ b/tests/plume/plume_verification_test_32.py @@ -9,8 +9,8 @@ # # Manual-run verification script (design decision D5): no pytest tests; # run directly -- python tests/plume/plume_verification_test_32.py -# Digitized overlays looked for: fig21_*.csv (one CSV per digitized -# contour polyline). +# Digitized overlays looked for: cai16_fig21_*.csv (one CSV per +# digitized contour polyline; cai16_ prefix avoids the 2012 fig21 slot). import plume_impingement_utils as u @@ -20,7 +20,7 @@ def generate_figure(): chain = u.chain_grid() levels = [0.001, 0.01, 0.05] return u.impingement_contour_figure( - 'fig21', ref['Cq_d'], chain['Cq'], levels, + 'cai16_fig21', ref['Cq_d'], chain['Cq'], levels, 'Diffuse plate $C_{q,d}(s,\\tau)$, $S_0$=2.0, ' '$T_w/T_0$=1.5, $\\alpha_0$=60$^\\circ$ (Fig. 21)', 'fig21_diffuse_heat_flux') diff --git a/tests/plume/plume_verification_test_33.py b/tests/plume/plume_verification_test_33.py new file mode 100644 index 0000000..acebd38 --- /dev/null +++ b/tests/plume/plume_verification_test_33.py @@ -0,0 +1,30 @@ +# ======================== +# PyRPOD: tests/plume/plume_verification_test_33.py +# ======================== +# Reproduces Cai 2016 Fig. 5: temperature contours T/T0 for the 2D slot +# jet (S0 = 2.0) impinging on the inclined DIFFUSE planar plate +# (alpha0 = 60 deg, Tw/T0 = 1.5, L = 4*(2H), W = 5*(2H)) -- the combined +# free-jet + wall-emission field of Section 3, with the stagnation +# heating peaking at T/T0 ~ 2.4 in front of the plate. Delegates to +# pyrpod/plume/CaiImpingement2016.planar_flowfield (single source of +# truth); the analytic field is evaluated on the whole X > 0 plane with +# no shadowing, exactly as the paper's contours are drawn. +# +# Manual-run verification script (design decision D5): no pytest tests; +# run directly -- python tests/plume/plume_verification_test_33.py +# Digitized overlays looked for: cai16_fig05_*.csv (one CSV per +# digitized contour polyline). + +import plume_impingement_utils as u + + +def generate_figure(): + return u.planar_temperature_contour_figure( + 'diffuse', [1.0, 1.4, 1.8, 2.0, 2.2, 2.4], 'cai16_fig05', + 'cai16_fig05_planar_diffuse_temperature', + '2D jet on diffuse plate, $T/T_0$, $S_0$=2.0, ' + '$\\alpha_0$=60$^\\circ$, $T_w/T_0$=1.5 (Fig. 5)') + + +if __name__ == '__main__': + u.base.run_script(generate_figure) diff --git a/tests/plume/plume_verification_test_34.py b/tests/plume/plume_verification_test_34.py new file mode 100644 index 0000000..787d1da --- /dev/null +++ b/tests/plume/plume_verification_test_34.py @@ -0,0 +1,31 @@ +# ======================== +# PyRPOD: tests/plume/plume_verification_test_34.py +# ======================== +# Reproduces Cai 2016 Fig. 6: temperature contours T/T0 for the 2D slot +# jet (S0 = 2.0) impinging on the inclined SPECULAR planar plate +# (alpha0 = 60 deg, L = 4*(2H)) -- the free jet plus the paper's +# virtual-nozzle construction (mirrored exit and drift, Eq. 7), whose +# defining property is the contour symmetry about the plate line that +# the module's sanity checks assert to machine precision; counterflow +# stagnation heating peaks at T/T0 ~ 3.5. Delegates to +# pyrpod/plume/CaiImpingement2016.planar_flowfield (single source of +# truth). +# +# Manual-run verification script (design decision D5): no pytest tests; +# run directly -- python tests/plume/plume_verification_test_34.py +# Digitized overlays looked for: cai16_fig06_*.csv (one CSV per +# digitized contour polyline). + +import plume_impingement_utils as u + + +def generate_figure(): + return u.planar_temperature_contour_figure( + 'specular', [1.0, 1.5, 2.0, 2.5, 3.0, 3.5], 'cai16_fig06', + 'cai16_fig06_planar_specular_temperature', + '2D jet on specular plate, $T/T_0$, $S_0$=2.0, ' + '$\\alpha_0$=60$^\\circ$ (Fig. 6)') + + +if __name__ == '__main__': + u.base.run_script(generate_figure) diff --git a/tests/plume/plume_verification_test_35.py b/tests/plume/plume_verification_test_35.py new file mode 100644 index 0000000..79dff66 --- /dev/null +++ b/tests/plume/plume_verification_test_35.py @@ -0,0 +1,31 @@ +# ======================== +# PyRPOD: tests/plume/plume_verification_test_35.py +# ======================== +# Reproduces Cai 2016 Fig. 7: 2D diffuse-plate surface pressure +# profiles Cp,d(s) (Eq. 2, with the A0-integral non-penetration wall +# density) for (S0, alpha0) = (2.0, 60), (2.0, 30), (1.5, 60), +# (1.5, 30) deg at Tw/T0 = 1.5 -- the profiles skew with smaller +# inclination angle, peaking at ~0.93 for the DSMC-validated +# (2.0, 60 deg) case. Delegates to +# pyrpod/plume/CaiImpingement2016.planar_surface_coefficients (single +# source of truth). +# +# Manual-run verification script (design decision D5): no pytest tests; +# run directly -- python tests/plume/plume_verification_test_35.py +# Digitized overlays looked for: cai16_fig07_*.csv (e.g. +# cai16_fig07_dsmc.csv, the paper's S0 = 2.0 / 60 deg DSMC symbols). + +import plume_impingement_utils as u + +PARAMS = [(2.0, 60.0), (2.0, 30.0), (1.5, 60.0), (1.5, 30.0)] + + +def generate_figure(): + return u.planar_profile_figure( + 'Cp_d', PARAMS, 'cai16_fig07', 'cai16_fig07_planar_cp_diffuse', + 'Diffuse planar plate $C_{p,d}(s)$, $T_w/T_0$=1.5 (Fig. 7)', + '$C_p$', (0.0, 1.0)) + + +if __name__ == '__main__': + u.base.run_script(generate_figure) diff --git a/tests/plume/plume_verification_test_36.py b/tests/plume/plume_verification_test_36.py new file mode 100644 index 0000000..79cca26 --- /dev/null +++ b/tests/plume/plume_verification_test_36.py @@ -0,0 +1,30 @@ +# ======================== +# PyRPOD: tests/plume/plume_verification_test_36.py +# ======================== +# Reproduces Cai 2016 Fig. 8: 2D specular-plate surface pressure +# profiles Cp,s(s) (Eq. 8 -- twice the jet-only normal momentum flux, +# wall temperature drops out) for (S0, alpha0) = (2.0, 60), (2.0, 30), +# (1.5, 60), (1.5, 30) deg, peaking at ~1.23 for the DSMC-validated +# (2.0, 60 deg) case. Delegates to +# pyrpod/plume/CaiImpingement2016.planar_surface_coefficients (single +# source of truth). +# +# Manual-run verification script (design decision D5): no pytest tests; +# run directly -- python tests/plume/plume_verification_test_36.py +# Digitized overlays looked for: cai16_fig08_*.csv (e.g. +# cai16_fig08_dsmc.csv). + +import plume_impingement_utils as u + +PARAMS = [(2.0, 60.0), (2.0, 30.0), (1.5, 60.0), (1.5, 30.0)] + + +def generate_figure(): + return u.planar_profile_figure( + 'Cp_s', PARAMS, 'cai16_fig08', 'cai16_fig08_planar_cp_specular', + 'Specular planar plate $C_{p,s}(s)$ (Fig. 8)', + '$C_p$', (0.0, 1.25)) + + +if __name__ == '__main__': + u.base.run_script(generate_figure) diff --git a/tests/plume/plume_verification_test_37.py b/tests/plume/plume_verification_test_37.py new file mode 100644 index 0000000..634e2bf --- /dev/null +++ b/tests/plume/plume_verification_test_37.py @@ -0,0 +1,31 @@ +# ======================== +# PyRPOD: tests/plume/plume_verification_test_37.py +# ======================== +# Reproduces Cai 2016 Fig. 9: 2D diffuse-plate surface friction +# profiles Cf,d(s) (Eq. 3) for (S0, alpha0) = (2.0, 60), (2.0, 30), +# (1.5, 60), (1.5, 30) deg at Tw/T0 = 1.5. Smaller inclination angles +# create larger friction; the (2.0, 60 deg) profile crosses zero near +# s/(2H) ~ -2 (the paper's possible flow-separation spot; the module +# anchor reproduces it at -2.12). Delegates to +# pyrpod/plume/CaiImpingement2016.planar_surface_coefficients (single +# source of truth). +# +# Manual-run verification script (design decision D5): no pytest tests; +# run directly -- python tests/plume/plume_verification_test_37.py +# Digitized overlays looked for: cai16_fig09_*.csv (e.g. +# cai16_fig09_dsmc.csv). + +import plume_impingement_utils as u + +PARAMS = [(2.0, 60.0), (2.0, 30.0), (1.5, 60.0), (1.5, 30.0)] + + +def generate_figure(): + return u.planar_profile_figure( + 'Cf_d', PARAMS, 'cai16_fig09', 'cai16_fig09_planar_cf_diffuse', + 'Diffuse planar plate $C_{f,d}(s)$, $T_w/T_0$=1.5 (Fig. 9)', + '$C_f$', (-0.1, 0.4)) + + +if __name__ == '__main__': + u.base.run_script(generate_figure) diff --git a/tests/plume/plume_verification_test_38.py b/tests/plume/plume_verification_test_38.py new file mode 100644 index 0000000..8952487 --- /dev/null +++ b/tests/plume/plume_verification_test_38.py @@ -0,0 +1,31 @@ +# ======================== +# PyRPOD: tests/plume/plume_verification_test_38.py +# ======================== +# Reproduces Cai 2016 Fig. 10: 2D diffuse-plate surface heat-flux +# profiles Cq,d(s) (Eq. 4, incoming energy flux minus the wall-Maxwellian +# effusion at Tw) for (S0, alpha0) = (2.0, 60), (2.0, 90), (1.5, 60), +# (1.5, 90) deg at Tw/T0 = 1.5 -- the larger S0, the larger the heat +# flux, peaking at ~0.27 for the DSMC-validated (2.0, 60 deg) case. +# Delegates to +# pyrpod/plume/CaiImpingement2016.planar_surface_coefficients (single +# source of truth). +# +# Manual-run verification script (design decision D5): no pytest tests; +# run directly -- python tests/plume/plume_verification_test_38.py +# Digitized overlays looked for: cai16_fig10_*.csv (e.g. +# cai16_fig10_dsmc.csv). + +import plume_impingement_utils as u + +PARAMS = [(2.0, 60.0), (2.0, 90.0), (1.5, 60.0), (1.5, 90.0)] + + +def generate_figure(): + return u.planar_profile_figure( + 'Cq_d', PARAMS, 'cai16_fig10', 'cai16_fig10_planar_cq_diffuse', + 'Diffuse planar plate $C_{q,d}(s)$, $T_w/T_0$=1.5 (Fig. 10)', + '$C_q$', (0.0, 0.3)) + + +if __name__ == '__main__': + u.base.run_script(generate_figure) diff --git a/tests/plume/plume_verification_test_39.py b/tests/plume/plume_verification_test_39.py new file mode 100644 index 0000000..72871f7 --- /dev/null +++ b/tests/plume/plume_verification_test_39.py @@ -0,0 +1,33 @@ +# ======================== +# PyRPOD: tests/plume/plume_verification_test_39.py +# ======================== +# Reproduces Cai 2016 Fig. 15: static-pressure contours p/p0 in the +# vertical Y = 0 plane for the Section-4 round jet (argon, D = 1 m, +# S0 = 2.0) impinging on the inclined DIFFUSE rectangular plate +# (alpha0 = 60 deg, L = 4D, Tw/T0 = 1.5) -- the free-jet field (2012 +# exit-disk integrals, imported from RarefiedPlumeGasKinetics) combined +# with the Eq.-9 wall-emission population, showing the paper's gas +# accumulation in the impingement center region (peak p/p0 ~ 0.58 just +# below the plate center vs the paper's 0.5 innermost contour). +# Delegates to +# pyrpod/plume/CaiImpingement2016.flowfield_pressure_plane (single +# source of truth). +# +# Manual-run verification script (design decision D5): no pytest tests; +# run directly -- python tests/plume/plume_verification_test_39.py +# Digitized overlays looked for: cai16_fig15_*.csv (one CSV per +# digitized contour polyline). + +import plume_impingement_utils as u + + +def generate_figure(): + return u.pressure_contour_figure_3d( + 'diffuse', [0.05, 0.1, 0.2, 0.4, 0.5], 'cai16_fig15', + 'cai16_fig15_pressure_diffuse', + 'Round jet on diffuse plate, $p/p_0$ in $Y$=0 plane, ' + '$S_0$=2.0, $\\alpha_0$=60$^\\circ$ (Fig. 15)') + + +if __name__ == '__main__': + u.base.run_script(generate_figure) diff --git a/tests/plume/plume_verification_test_40.py b/tests/plume/plume_verification_test_40.py new file mode 100644 index 0000000..cf9ad18 --- /dev/null +++ b/tests/plume/plume_verification_test_40.py @@ -0,0 +1,33 @@ +# ======================== +# PyRPOD: tests/plume/plume_verification_test_40.py +# ======================== +# Reproduces Cai 2016 Fig. 16: static-pressure contours p/p0 in the +# vertical Y = 0 plane for the Section-4 round jet (argon, D = 1 m, +# S0 = 2.0) impinging on the inclined SPECULAR rectangular plate +# (alpha0 = 60 deg, L = 4D) -- the free-jet field plus the paper's 3D +# virtual nozzle at (L(1 - cos 2a0), 0, -L sin 2a0) with mirrored drift +# (see the reference module docstring for the drift-sign resolution), +# whose mirror symmetry about the plate plane the module's sanity +# checks assert to machine precision; peak p/p0 ~ 0.50 vs the paper's +# 0.4 innermost contour. Delegates to +# pyrpod/plume/CaiImpingement2016.flowfield_pressure_plane (single +# source of truth). +# +# Manual-run verification script (design decision D5): no pytest tests; +# run directly -- python tests/plume/plume_verification_test_40.py +# Digitized overlays looked for: cai16_fig16_*.csv (one CSV per +# digitized contour polyline). + +import plume_impingement_utils as u + + +def generate_figure(): + return u.pressure_contour_figure_3d( + 'specular', [0.01, 0.05, 0.1, 0.2, 0.3, 0.4], 'cai16_fig16', + 'cai16_fig16_pressure_specular', + 'Round jet on specular plate, $p/p_0$ in $Y$=0 plane, ' + '$S_0$=2.0, $\\alpha_0$=60$^\\circ$ (Fig. 16)') + + +if __name__ == '__main__': + u.base.run_script(generate_figure) From f7feeec164c14267cdef28f6c238cae7f0d8005d Mon Sep 17 00:00:00 2001 From: andytorrestb Date: Mon, 20 Jul 2026 22:27:09 -0500 Subject: [PATCH 07/11] Store Cai & Wang 2012 verification figures in output/Cai2012 The 2012 figure scripts (plume_verification_test_04..27) and the 2012 error summary wrote fig02..fig25 and model_error_summary.* loose in tests/plume/output; move them into an output/Cai2012 subfolder, parallel to the Cai 2016 impingement set already in output/Cai2016. save_figure now targets a new CAI2012_DIR = OUTPUT_DIR/Cai2012 and the 2012 error summary writes there too. OUTPUT_DIR stays the shared output root, so plume_impingement_utils (OUTPUT_DIR = base.OUTPUT_DIR/Cai2016) and the loose cai2016_* summaries are unaffected -- verified by re-running a Cai2016 script (still lands in Cai2016) and the plume test suite (81 passed). Co-Authored-By: Claude Fable 5 --- tests/plume/plume_figure_utils.py | 11 ++++++++--- tests/plume/plume_verification_error_summary.py | 8 ++++---- tests/plume/plume_verification_test_04.py | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/plume/plume_figure_utils.py b/tests/plume/plume_figure_utils.py index 194772e..0ac8a75 100644 --- a/tests/plume/plume_figure_utils.py +++ b/tests/plume/plume_figure_utils.py @@ -77,6 +77,11 @@ BOLTZMANN = 1.380649e-23 # J / K OUTPUT_DIR = _THIS_DIR / 'output' +# Cai & Wang 2012 figures live in their own output subfolder, parallel to +# the Cai 2016 impingement set in output/Cai2016 (see +# plume_impingement_utils.OUTPUT_DIR). OUTPUT_DIR itself stays the shared +# output root so that derived path is unaffected. +CAI2012_DIR = OUTPUT_DIR / 'Cai2012' DIGITIZED_DIR = _THIS_DIR / 'data' / 'digitized' X_MIN_OVER_D = 0.05 # models require X > 0 @@ -381,9 +386,9 @@ def annotate_error(ax, text, loc='lower left'): def save_figure(fig, name): - """Save PNG to tests/plume/output and return the path.""" - OUTPUT_DIR.mkdir(parents=True, exist_ok=True) - path = OUTPUT_DIR / f'{name}.png' + """Save PNG to tests/plume/output/Cai2012 and return the path.""" + CAI2012_DIR.mkdir(parents=True, exist_ok=True) + path = CAI2012_DIR / f'{name}.png' fig.savefig(path, dpi=200, bbox_inches='tight') plt.close(fig) return path diff --git a/tests/plume/plume_verification_error_summary.py b/tests/plume/plume_verification_error_summary.py index b99b6d7..a3963e1 100644 --- a/tests/plume/plume_verification_error_summary.py +++ b/tests/plume/plume_verification_error_summary.py @@ -16,7 +16,7 @@ # The filename intentionally avoids pytest's collection patterns # (test_*.py / *_test_*.py): this is a manual-run generator -- # python tests/plume/plume_verification_error_summary.py -# It writes tests/plume/output/model_error_summary.csv and .md. +# It writes tests/plume/output/Cai2012/model_error_summary.csv and .md. import numpy as np @@ -77,11 +77,11 @@ def build_rows(): def write_summary(): rows = build_rows() - u.OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + u.CAI2012_DIR.mkdir(parents=True, exist_ok=True) columns = ['curve', 'quantity', 'comparison', 'max_rel_diff', 'max_rel_diff_restricted', 'restriction', 'vs_DSMC'] - csv_path = u.OUTPUT_DIR / 'model_error_summary.csv' + csv_path = u.CAI2012_DIR / 'model_error_summary.csv' with open(csv_path, 'w', encoding='utf-8', newline='') as fh: fh.write(','.join(columns) + '\n') for row in rows: @@ -89,7 +89,7 @@ def write_summary(): f'{row[c]:.4g}' if isinstance(row[c], float) else str(row[c]) for c in columns) + '\n') - md_path = u.OUTPUT_DIR / 'model_error_summary.md' + md_path = u.CAI2012_DIR / 'model_error_summary.md' with open(md_path, 'w', encoding='utf-8', newline='') as fh: fh.write('# Model-vs-model error summary (Table 1 analog)\n\n') fh.write(f'Cai & Wang 2012 conditions, S0 = {S_0}. DSMC reference ' diff --git a/tests/plume/plume_verification_test_04.py b/tests/plume/plume_verification_test_04.py index 5978a31..0429d24 100644 --- a/tests/plume/plume_verification_test_04.py +++ b/tests/plume/plume_verification_test_04.py @@ -8,7 +8,7 @@ # Manual-run verification script (design decision D5): it defines no # pytest tests and is executed directly -- # python tests/plume/plume_verification_test_04.py -# The figure is saved to tests/plume/output/. +# The figure is saved to tests/plume/output/Cai2012/. # Digitized overlays looked for: fig02_*.csv (none published -- Fig. 2 # has no DSMC data in the paper). From ed8a4427562cfd99cca452b14fdeeebf63a0a17f Mon Sep 17 00:00:00 2001 From: andytorrestb Date: Mon, 20 Jul 2026 22:30:37 -0500 Subject: [PATCH 08/11] Write cai2016_* error summaries into the Cai2016 output folder The 2016 reference-vs-chain error tables (cai2016_error_summary.csv/.md, from plume_impingement_error_summary.py) and the pipeline-vs-reference summary (cai2016_pipeline_error_summary.md, from rpod_integration_test_07) landed loose in tests/plume/output; point them at output/Cai2016 alongside that study's figures. The former switch to u.OUTPUT_DIR (already output/Cai2016 in plume_impingement_utils); the latter adds the Cai2016 segment to SUMMARY_PATH (its parent.mkdir already creates it). output/ now holds only Cai2012/ and Cai2016/. rpod_integration_test_07 passes. Co-Authored-By: Claude Fable 5 --- tests/plume/plume_impingement_error_summary.py | 8 ++++---- tests/rpod/rpod_integration_test_07.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/plume/plume_impingement_error_summary.py b/tests/plume/plume_impingement_error_summary.py index 9a49c9c..04fbb40 100644 --- a/tests/plume/plume_impingement_error_summary.py +++ b/tests/plume/plume_impingement_error_summary.py @@ -20,7 +20,7 @@ # The filename intentionally avoids pytest's collection patterns # (test_*.py / *_test_*.py): this is a manual-run generator -- # python tests/plume/plume_impingement_error_summary.py -# It writes tests/plume/output/cai2016_error_summary.csv and .md. +# It writes tests/plume/output/Cai2016/cai2016_error_summary.csv and .md. import numpy as np @@ -65,11 +65,11 @@ def build_rows(): def write_summary(): rows = build_rows() - u.base.OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + u.OUTPUT_DIR.mkdir(parents=True, exist_ok=True) columns = ['quantity', 'comparison', 'max_rel_diff', 'mean_rel_diff', 'restriction', 'vs_DSMC'] - csv_path = u.base.OUTPUT_DIR / 'cai2016_error_summary.csv' + csv_path = u.OUTPUT_DIR / 'cai2016_error_summary.csv' with open(csv_path, 'w', encoding='utf-8', newline='') as fh: fh.write(','.join(columns) + '\n') for row in rows: @@ -77,7 +77,7 @@ def write_summary(): f'{row[c]:.4g}' if isinstance(row[c], float) else str(row[c]) for c in columns) + '\n') - md_path = u.base.OUTPUT_DIR / 'cai2016_error_summary.md' + md_path = u.OUTPUT_DIR / 'cai2016_error_summary.md' with open(md_path, 'w', encoding='utf-8', newline='') as fh: fh.write('# Cai 2016 impingement error summary ' '(reference vs PyRPOD chain)\n\n') diff --git a/tests/rpod/rpod_integration_test_07.py b/tests/rpod/rpod_integration_test_07.py index 8bc1782..d47cc6d 100644 --- a/tests/rpod/rpod_integration_test_07.py +++ b/tests/rpod/rpod_integration_test_07.py @@ -55,7 +55,7 @@ from pyrpod.vehicle import TargetVehicle, VisitingVehicle # noqa: E402 CASE_DIR = '../case/plume/plume_inclined_plate/' -SUMMARY_PATH = _TESTS_DIR / 'plume' / 'output' / \ +SUMMARY_PATH = _TESTS_DIR / 'plume' / 'output' / 'Cai2016' / \ 'cai2016_pipeline_error_summary.md' From 5636a446c01da0687e8fb993e154024071e8b8b5 Mon Sep 17 00:00:00 2001 From: andytorrestb Date: Mon, 20 Jul 2026 22:56:43 -0500 Subject: [PATCH 09/11] Add per-pose VTK export to the inclined-plate sweep study The sweep driver called the array-only pipeline core (compute_plume_ strikes) and wrote only the CSV + coefficient/peak plots, so -- unlike jfh_plume_strikes for the other RPOD cases -- it produced no VTK to visualize. Add a per-firing VTK export via the shared convert_stl_to_vtk writer: one results/sweep/vtk/firing-_a_LoD.vtu per pose carrying that firing's own per-face strikes, dimensional loads (pressure_Pa, shear_Pa, heat_flux_W_m2), and coefficients (Cp, Cshear, Cf1_case, Cf2_case, Cq). The Cf components are in the mesh CASE frame, un-flipped, so shear vectors point as they physically do on each pose's mesh (the paper-convention aggregate CF1/CM in the CSV are unchanged). Because each file is a single independent pose, there is no cumulative accumulation across firings (the concern that kept the sweep on the array core), so per-pose VTK is safe. Five sweep_LoD.pvd ParaView collections group the fixed-distance poses with alpha as the time coordinate, so an angle sweep is scrubbable in ParaView. Default on; --no-vtk skips it. Verified: full run writes 95 .vtu (152 MB, gitignored) + 5 .pvd cleanly; VTK export adds ~7 s to the sweep. Co-Authored-By: Claude Fable 5 --- scripts/inclined_plate_sweep_study.py | 92 +++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 7 deletions(-) diff --git a/scripts/inclined_plate_sweep_study.py b/scripts/inclined_plate_sweep_study.py index d890464..9be3c71 100644 --- a/scripts/inclined_plate_sweep_study.py +++ b/scripts/inclined_plate_sweep_study.py @@ -18,7 +18,20 @@ - coeff_.png: each averaged coefficient vs alpha, one curve per L/D, pipeline (solid + markers) and reference (dashed) on the same axes; - peaks.png: peak per-face pressure/shear/heat-flux coefficients vs alpha - per L/D. + per L/D; + - vtk/firing-_a_LoD.vtu: one VTK unstructured grid per + firing (same writer as the other RPOD cases) carrying that pose's own + per-face strikes, dimensional loads (pressure_Pa, shear_Pa, + heat_flux_W_m2), and coefficients (Cp, Cshear, Cf1_case, Cf2_case, Cq). + The Cf components here are in the mesh's CASE frame, un-flipped -- so + the shear vectors point the way they physically do on that pose's + mesh, unlike the paper-convention aggregate CF1/CM in the CSV. Written + per firing (independent poses; no cumulative accumulation, unlike + PlumeStrikeEstimationStudy.jfh_plume_strikes), so each file is exactly + one pose. sweep_LoD.pvd ParaView collections group the + fixed-distance poses with alpha as the time coordinate, so an angle + sweep can be scrubbed in ParaView. Disable the VTK export with + --no-vtk (CSV + plots only). Conventions and sanity checks: - Averaged pipeline coefficients follow Eq. 15: area-weighted sums over @@ -45,8 +58,10 @@ likewise keeps only the small plume-spread load.) Run from the repo root: python scripts/inclined_plate_sweep_study.py +(add --no-vtk to skip the per-pose VTK export). """ +import argparse import csv import sys import time @@ -71,11 +86,13 @@ compute_plume_strikes, ) from pyrpod.rpod import JetFiringHistory # noqa: E402 +from pyrpod.util.stl.stl import convert_stl_to_vtk # noqa: E402 from pyrpod.vehicle import TargetVehicle, VisitingVehicle # noqa: E402 CASE_DIR = str(_REPO_ROOT / 'case' / 'plume' / 'plume_inclined_plate') + '/' SWEEP_JFH = 'jfh_inclined_plate_sweep.A' RESULTS_DIR = Path(CASE_DIR) / 'results' / 'sweep' +VTK_DIR = RESULTS_DIR / 'vtk' PLATE_CENTER = np.array([4.0, 0.0, 0.0]) ALPHA0_DEG = 60.0 # the mesh's fixed global tilt @@ -117,7 +134,10 @@ def face_areas(vectors): def pipeline_row(result, centroids, normals, areas, tau_face, thruster_pos, alpha_deg): - """Eq.-15 averaged coefficients + peaks from one firing's face arrays.""" + """Eq.-15 averaged coefficients, peaks, and per-face field arrays from + one firing. Returns (coeffs, peaks, face_fields); face_fields holds + the case-frame per-face arrays for VTK (Cf1_case/Cf2_case un-flipped, + matching the actual mesh, unlike the paper-convention aggregates).""" S_tot = float(np.sum(areas)) Cp = result['pressures'] / piu.Q_DYN Csh = result['shear_stress'] / piu.Q_DYN @@ -145,11 +165,45 @@ def pipeline_row(result, centroids, normals, areas, tau_face, thruster_pos, CQ = float(np.sum(Cq * areas) / S_tot) CM = flip * float(np.sum(tau_face * Cp * areas) / (2.0 * H_0 * S_tot)) s_cc = CM / CP if CP != 0.0 else float('nan') + + face_fields = { + 'strikes': result['strikes'], + 'pressure_Pa': result['pressures'], + 'shear_Pa': result['shear_stress'], + 'heat_flux_W_m2': result['heat_flux_rate'], + 'Cp': Cp, + 'Cshear': Csh, + 'Cf1_case': Cf1, + 'Cf2_case': Cf2, + 'Cq': Cq, + } return ({'CP': CP, 'CF1': CF1, 'CF2': CF2, 'CQ': CQ, 'CM': CM, 's_cc': s_cc}, {'peak_Cp': float(np.max(Cp)), 'peak_Cshear': float(np.max(Csh)), 'peak_Cq': float(np.max(Cq)), - 'n_struck': int(np.count_nonzero(result['strikes']))}) + 'n_struck': int(np.count_nonzero(result['strikes']))}, + face_fields) + + +def write_pose_vtk(target, face_fields, base_name): + """Write one pose's per-face fields to VTK_DIR/.vtu via the + shared convert_stl_to_vtk writer (pyevtk needs C-contiguous float64).""" + cell_data = {k: np.ascontiguousarray(v, dtype=np.float64) + for k, v in face_fields.items()} + convert_stl_to_vtk(target, VTK_DIR, filename=base_name, + cellData=cell_data) + + +def write_pvd(path, entries): + """ParaView collection grouping (timestep, relative_vtu_path) entries.""" + lines = ['', + '', ' '] + for timestep, rel_file in sorted(entries): + lines.append(f' ') + lines += [' ', ''] + path.write_text('\n'.join(lines) + '\n', encoding='utf-8') def reference_rows(): @@ -247,7 +301,7 @@ def plot_peaks(rows, out_dir): plt.close(fig) -def main(): +def main(write_vtk=True): t_start = time.perf_counter() jfh, tv, vv, me = load_case() n_firings = len(jfh.JFH) @@ -261,6 +315,10 @@ def main(): areas = face_areas(target.vectors) tau_face = (centroids - PLATE_CENTER) @ TANGENT_TAU + if write_vtk: + VTK_DIR.mkdir(parents=True, exist_ok=True) + pvd_entries = {} # L/D -> [(alpha_deg, relative vtu path)] + print(f'reference curves: {len(set(abs(ALPHAS_DEG)))} angles x ' f'{len(L_OVER_D)} distances (Eq. 15 quadrature) ...') t0 = time.perf_counter() @@ -282,8 +340,9 @@ def main(): face_centroids=centroids) dt = time.perf_counter() - t_f - coeffs, peaks = pipeline_row(result, centroids, normals, areas, - tau_face, step['xyz'], alpha_deg) + coeffs, peaks, face_fields = pipeline_row( + result, centroids, normals, areas, tau_face, step['xyz'], + alpha_deg) ref = ref_cache[(abs(alpha_deg), L)] row = {'alpha_deg': alpha_deg, 'L_over_D': L, 'alpha_paper_deg': 90.0 - abs(alpha_deg), @@ -292,6 +351,13 @@ def main(): row[f'{name}_pipe'] = coeffs[name] row[f'{name}_ref'] = float(ref[name]) rows.append(row) + + if write_vtk: + base = (f'firing-{i:02d}_a{int(alpha_deg):+03d}' + f'_LoD{int(L):02d}') + write_pose_vtk(target, face_fields, base) + pvd_entries.setdefault(L, []).append( + (alpha_deg, f'vtk/{base}.vtu')) t_sweep = time.perf_counter() - t0 # sanity: report the edge-on poses (strike membership is @@ -322,6 +388,13 @@ def main(): plot_peaks(rows, RESULTS_DIR) print(f'wrote coefficient and peak plots to {RESULTS_DIR}') + if write_vtk: + for L, entries in pvd_entries.items(): + write_pvd(RESULTS_DIR / f'sweep_LoD{int(L):02d}.pvd', entries) + print(f'wrote {n_firings} per-pose VTK files to {VTK_DIR} ' + f'(+ {len(pvd_entries)} sweep_LoD*.pvd collections in ' + f'{RESULTS_DIR})') + n_faces = len(target.vectors) print(f'timings: {n_firings} firings x {n_faces} faces -- sweep ' f'{t_sweep:.1f} s ({t_sweep / n_firings * 1e3:.0f} ms/firing), ' @@ -330,4 +403,9 @@ def main(): if __name__ == '__main__': - main() + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument('--no-vtk', action='store_true', + help='skip the per-pose VTK export (CSV + plots ' + 'only)') + args = parser.parse_args() + main(write_vtk=not args.no_vtk) From 099c7acfbf743fc769dfcd95fed38485d3385aa3 Mon Sep 17 00:00:00 2001 From: andytorrestb Date: Wed, 22 Jul 2026 00:26:42 -0500 Subject: [PATCH 10/11] Migrate inclined-plate sweep study to a pytest and split sweep cases Promote the assertion-relevant core of scripts/inclined_plate_sweep_study.py into tests/rpod/rpod_verification_test_06.py (the swept analog of rpod_integration_test_07): runs the 95-firing angle x distance sweep through compute_plume_strikes and gates on JFH length, mirror symmetry (<1e-5), CF2~0, and a loose plate-averaged pipeline-vs-Cai2016 envelope, writing a compact error summary to tests/plume/output/Cai2016. Opt-in per-pose strike VTK via PYRPOD_SWEEP_VTK=1. Split the sweeps into dedicated, self-contained case folders so each writes strikes to its own results/strikes/firing-.vtu (standard RPOD convention, 0-based JFH index, + sweep_LoD*.pvd ParaView collections): - plume_inclined_plate/ paper single pose only (rpod_integration_test_07) - plume_inclined_plate_sweep/ 60 deg sweep (study script; CASE_DIR repointed) - plume_flat_plate_sweep/ flat alpha0=0 reframe (rpod_verification_test_06) The flat reframe is physics-invariant (identical coefficients; mirror worst 9e-11 vs 2.15e-7) but reads cleanly in ParaView. Kept separate because the paper STL is shared with test_07 + the script, so flattening in place would break them. generate_sweep_jfh.py is now argparse-parameterized (--alpha0-deg/--distance/--out; defaults reproduce the 60 deg paper values). Verified: rpod_verification_test_06 and rpod_integration_test_07 pass; the study script runs against plume_inclined_plate_sweep and both sweeps emit firing-0.vtu .. firing-94.vtu. Co-Authored-By: Claude Opus 4.8 --- case/plume/plume_flat_plate_sweep/config.ini | 73 ++++ .../jfh/generate_jfh_inclined_plate.py | 37 ++ .../jfh/generate_sweep_jfh.py | 38 +- .../jfh/jfh_flat_plate_sweep.A | 97 +++++ .../stl/flat_plate_transformed.stl | Bin 0 -> 518484 bytes .../stl/transform_inclined_plate.py | 101 +++++ .../tcd/tcf_1_argon.txt | 6 + case/plume/plume_flat_plate_sweep/tcd/tdf.csv | 2 + .../plume_inclined_plate_sweep/config.ini | 72 ++++ .../jfh/generate_jfh_inclined_plate.py | 37 ++ .../jfh/generate_sweep_jfh.py | 101 +++++ .../jfh/jfh_inclined_plate_sweep.A | 0 .../stl/inclined_plate_transformed.stl | Bin 0 -> 518484 bytes .../stl/transform_inclined_plate.py | 101 +++++ .../tcd/tcf_1_argon.txt | 6 + .../plume_inclined_plate_sweep/tcd/tdf.csv | 2 + scripts/inclined_plate_sweep_study.py | 63 +-- tests/rpod/rpod_verification_test_06.py | 398 ++++++++++++++++++ 18 files changed, 1097 insertions(+), 37 deletions(-) create mode 100644 case/plume/plume_flat_plate_sweep/config.ini create mode 100644 case/plume/plume_flat_plate_sweep/jfh/generate_jfh_inclined_plate.py rename case/plume/{plume_inclined_plate => plume_flat_plate_sweep}/jfh/generate_sweep_jfh.py (61%) create mode 100644 case/plume/plume_flat_plate_sweep/jfh/jfh_flat_plate_sweep.A create mode 100644 case/plume/plume_flat_plate_sweep/stl/flat_plate_transformed.stl create mode 100644 case/plume/plume_flat_plate_sweep/stl/transform_inclined_plate.py create mode 100644 case/plume/plume_flat_plate_sweep/tcd/tcf_1_argon.txt create mode 100644 case/plume/plume_flat_plate_sweep/tcd/tdf.csv create mode 100644 case/plume/plume_inclined_plate_sweep/config.ini create mode 100644 case/plume/plume_inclined_plate_sweep/jfh/generate_jfh_inclined_plate.py create mode 100644 case/plume/plume_inclined_plate_sweep/jfh/generate_sweep_jfh.py rename case/plume/{plume_inclined_plate => plume_inclined_plate_sweep}/jfh/jfh_inclined_plate_sweep.A (100%) create mode 100644 case/plume/plume_inclined_plate_sweep/stl/inclined_plate_transformed.stl create mode 100644 case/plume/plume_inclined_plate_sweep/stl/transform_inclined_plate.py create mode 100644 case/plume/plume_inclined_plate_sweep/tcd/tcf_1_argon.txt create mode 100644 case/plume/plume_inclined_plate_sweep/tcd/tdf.csv create mode 100644 tests/rpod/rpod_verification_test_06.py diff --git a/case/plume/plume_flat_plate_sweep/config.ini b/case/plume/plume_flat_plate_sweep/config.ini new file mode 100644 index 0000000..60238f8 --- /dev/null +++ b/case/plume/plume_flat_plate_sweep/config.ini @@ -0,0 +1,73 @@ +# Cai 2016 FLAT-plate SWEEP case (Aerospace 3(4):43, Section 4): a pure +# global-frame reframing of the paper geometry to alpha0 = 0 (flat 8 m x 8 m +# plate in the X-Y plane, center at the origin, argon round jet D = 1 m, +# S0 = 2.0, T0 = 200 K, Tw = 300 K, fully diffuse), swept over 19 approach +# angles x 5 stand-off distances (95 firings; jfh_flat_plate_sweep.A). The +# reframing is physics-invariant, so this reproduces the same coefficients as +# the sibling plume_inclined_plate_sweep but with strikes that read cleanly in +# ParaView (results/strikes/firing-.vtu). Driven by +# tests/rpod/rpod_verification_test_06.py. Geometry assets are generated by +# the scripts in stl/ and jfh/ (--alpha0-deg 0 --distance 0; see docstrings). + +# Visiting Vehicle for RPOD analysis +[vv] +stl_lm = cylinder_transformed.stl +stl_thruster = mold_funnel_transformed.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = flat_plate_transformed.stl + +# surface wall temperature (Kelvin): Tw/T0 = 1.5 (paper Figs. 17-21) +surface_temp = 300 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# check plume constraints? 0 or 1 +check_constraints = 0 + +# max heat flux integral (J/m^2) +heat_flux_load = inf +heat_flux_window_size = 1 + +# max heat flux rate (W/m^2) +heat_flux = inf + +# max pressure load +normal_pressure_load = inf +normal_pressure_window_size = inf + +# max normal pressure (N/m^2) +normal_pressure = inf + +# max shear pressure (N/m^2) +shear_pressure = inf + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = Simplified + +# Gas-surface interaction model +surface_interaction = Maxwellian + +# Jet Firing History +[jfh] +jfh = jfh_flat_plate_sweep.A + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - single head-on thruster, no cant. +tcf = tcf_1_argon.txt + +# Thruster Definition File - argon at the paper conditions. +tdf = tdf.csv + +# Parameters for scaling plume geometry. +[plume] +radius = 25 +# ~89 deg: the default 0.436 (25 deg) would clip the plate -- its lower +# corners sit ~70 deg off-axis at the paper distance. 1.55 rad keeps the +# gating wedge clear of the whole plate at every sweep pose. +wedge_theta = 1.55 diff --git a/case/plume/plume_flat_plate_sweep/jfh/generate_jfh_inclined_plate.py b/case/plume/plume_flat_plate_sweep/jfh/generate_jfh_inclined_plate.py new file mode 100644 index 0000000..249f71d --- /dev/null +++ b/case/plume/plume_flat_plate_sweep/jfh/generate_jfh_inclined_plate.py @@ -0,0 +1,37 @@ +"""Generate the single-firing JFH for the Cai 2016 inclined-plate case. + +One firing at the paper geometry (Aerospace 2016, 3(4):43, Section 4): the +visiting vehicle sits at the origin with an identity DCM, so its single +thruster (exit at the VV origin, no cant) fires along +X toward the plate +centered at (4, 0, 0) m. Firing time 1.0 s makes heat_flux_load equal the +heat-flux rate. + +JFH files are always generated by script, never hand-edited. Run from this +directory: python generate_jfh_inclined_plate.py +""" + +from pathlib import Path + +OUT_NAME = 'jfh_plume_inclined_plate.A' + +DCM_IDENTITY = ('1.000000e+00 0.000000e+00 0.000000e+00 ' + '0.000000e+00 1.000000e+00 0.000000e+00 ' + '0.000000e+00 0.000000e+00 1.000000e+00') + + +def write_jfh(path, firings): + """firings: list of (dt, t, dcm_str, xyz, thrusters).""" + lines = [f'offseted {len(firings)} 0', ' 0.000 0.000 0.000'] + for n, (dt, t, dcm, xyz, thrusters) in enumerate(firings, start=1): + xyz_str = ' '.join(f'{v:.9g}' for v in xyz) + thr_str = ' '.join(str(i) for i in thrusters) + lines.append(f' {n} {dt:g} {t:g} 0 {dcm} {xyz_str} 1 ' + f'{len(thrusters)} {thr_str}') + with open(path, 'w', newline='\n') as fh: + fh.write('\n'.join(lines) + '\n') + + +if __name__ == '__main__': + out_path = Path(__file__).resolve().parent / OUT_NAME + write_jfh(out_path, [(0.0, 1.0, DCM_IDENTITY, (0.0, 0.0, 0.0), [1])]) + print(f'saved {out_path}') diff --git a/case/plume/plume_inclined_plate/jfh/generate_sweep_jfh.py b/case/plume/plume_flat_plate_sweep/jfh/generate_sweep_jfh.py similarity index 61% rename from case/plume/plume_inclined_plate/jfh/generate_sweep_jfh.py rename to case/plume/plume_flat_plate_sweep/jfh/generate_sweep_jfh.py index d559b7b..a8e574f 100644 --- a/case/plume/plume_inclined_plate/jfh/generate_sweep_jfh.py +++ b/case/plume/plume_flat_plate_sweep/jfh/generate_sweep_jfh.py @@ -43,15 +43,23 @@ L_OVER_D = [2.0, 4.0, 6.0, 8.0, 10.0] -def pose_for(alpha_deg, L): +def pose_for(alpha_deg, L, plate_center=PLATE_CENTER, alpha0_deg=ALPHA0_DEG): """(vv_position, dcm) for one sweep firing; dcm's first column is the - thruster axis (see the pipeline's plume-normal convention).""" - a0 = np.deg2rad(ALPHA0_DEG) + thruster axis (see the pipeline's plume-normal convention). + + plate_center / alpha0_deg place the whole {plate + swept-arc} rig in the + global frame. They are physics-invariant (every relative incidence, and + thus every coefficient, is preserved under this rigid rotation + + translation) -- a flat alpha0 = 0 variant is a pure visualization + reframing of the same case. They must match the target STL's own + center/tilt (stl/transform_inclined_plate.py) and the analysis constants + (tests/rpod/rpod_verification_test_06.py).""" + a0 = np.deg2rad(alpha0_deg) n_hat = np.array([-np.sin(a0), 0.0, np.cos(a0)]) t_hat = np.array([np.cos(a0), 0.0, np.sin(a0)]) alpha = np.deg2rad(alpha_deg) d_hat = np.cos(alpha) * n_hat + np.sin(alpha) * t_hat - position = PLATE_CENTER + L * d_hat + position = np.asarray(plate_center, dtype=float) + L * d_hat axis = -d_hat # aimed at the plate center # right-handed triad; the axis always lies in the X-Z plane, so the # global Y axis is a valid second column @@ -66,12 +74,28 @@ def dcm_string(dcm): if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument('--alpha0-deg', type=float, default=ALPHA0_DEG, + help='global-frame plate tilt (deg); physics-' + 'invariant, must match the target STL') + parser.add_argument('--distance', type=float, default=PLATE_CENTER[0], + help='plate-center X in the global frame (m); the ' + 'center is (distance, 0, 0)') + parser.add_argument('--out', type=str, default=OUT_NAME, + help='output JFH filename in this jfh/ folder') + args = parser.parse_args() + + plate_center = np.array([args.distance, 0.0, 0.0]) firings = [] for L in L_OVER_D: for alpha_deg in ALPHAS_DEG: - position, dcm = pose_for(alpha_deg, L) + position, dcm = pose_for(alpha_deg, L, plate_center=plate_center, + alpha0_deg=args.alpha0_deg) firings.append((0.0, 1.0, dcm_string(dcm), tuple(position), [1])) - out_path = Path(__file__).resolve().parent / OUT_NAME + out_path = Path(__file__).resolve().parent / args.out write_jfh(out_path, firings) print(f'saved {out_path} ({len(firings)} firings: ' - f'{len(ALPHAS_DEG)} angles x {len(L_OVER_D)} distances)') + f'{len(ALPHAS_DEG)} angles x {len(L_OVER_D)} distances, ' + f'alpha0 = {args.alpha0_deg} deg, center X = {args.distance} m)') diff --git a/case/plume/plume_flat_plate_sweep/jfh/jfh_flat_plate_sweep.A b/case/plume/plume_flat_plate_sweep/jfh/jfh_flat_plate_sweep.A new file mode 100644 index 0000000..b32d99d --- /dev/null +++ b/case/plume/plume_flat_plate_sweep/jfh/jfh_flat_plate_sweep.A @@ -0,0 +1,97 @@ +offseted 95 0 + 0.000 0.000 0.000 + 1 0 1 0 1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.123234e-17 0.000000e+00 1.000000e+00 -2 0 1.2246468e-16 1 1 1 + 2 0 1 0 9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -1.736482e-01 0.000000e+00 9.848078e-01 -1.96961551 0 0.347296355 1 1 1 + 3 0 1 0 9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -3.420201e-01 0.000000e+00 9.396926e-01 -1.87938524 0 0.684040287 1 1 1 + 4 0 1 0 8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -5.000000e-01 0.000000e+00 8.660254e-01 -1.73205081 0 1 1 1 1 + 5 0 1 0 7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.427876e-01 0.000000e+00 7.660444e-01 -1.53208889 0 1.28557522 1 1 1 + 6 0 1 0 6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -7.660444e-01 0.000000e+00 6.427876e-01 -1.28557522 0 1.53208889 1 1 1 + 7 0 1 0 5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -8.660254e-01 0.000000e+00 5.000000e-01 -1 0 1.73205081 1 1 1 + 8 0 1 0 3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.396926e-01 0.000000e+00 3.420201e-01 -0.684040287 0 1.87938524 1 1 1 + 9 0 1 0 1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.848078e-01 0.000000e+00 1.736482e-01 -0.347296355 0 1.96961551 1 1 1 + 10 0 1 0 -0.000000e+00 0.000000e+00 1.000000e+00 -0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0 0 2 1 1 1 + 11 0 1 0 -1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.848078e-01 0.000000e+00 -1.736482e-01 0.347296355 0 1.96961551 1 1 1 + 12 0 1 0 -3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.396926e-01 0.000000e+00 -3.420201e-01 0.684040287 0 1.87938524 1 1 1 + 13 0 1 0 -5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -8.660254e-01 0.000000e+00 -5.000000e-01 1 0 1.73205081 1 1 1 + 14 0 1 0 -6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -7.660444e-01 0.000000e+00 -6.427876e-01 1.28557522 0 1.53208889 1 1 1 + 15 0 1 0 -7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -6.427876e-01 0.000000e+00 -7.660444e-01 1.53208889 0 1.28557522 1 1 1 + 16 0 1 0 -8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -5.000000e-01 0.000000e+00 -8.660254e-01 1.73205081 0 1 1 1 1 + 17 0 1 0 -9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -3.420201e-01 0.000000e+00 -9.396926e-01 1.87938524 0 0.684040287 1 1 1 + 18 0 1 0 -9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -1.736482e-01 0.000000e+00 -9.848078e-01 1.96961551 0 0.347296355 1 1 1 + 19 0 1 0 -1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 0.000000e+00 -6.123234e-17 0.000000e+00 -1.000000e+00 2 0 1.2246468e-16 1 1 1 + 20 0 1 0 1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.123234e-17 0.000000e+00 1.000000e+00 -4 0 2.4492936e-16 1 1 1 + 21 0 1 0 9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -1.736482e-01 0.000000e+00 9.848078e-01 -3.93923101 0 0.694592711 1 1 1 + 22 0 1 0 9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -3.420201e-01 0.000000e+00 9.396926e-01 -3.75877048 0 1.36808057 1 1 1 + 23 0 1 0 8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -5.000000e-01 0.000000e+00 8.660254e-01 -3.46410162 0 2 1 1 1 + 24 0 1 0 7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.427876e-01 0.000000e+00 7.660444e-01 -3.06417777 0 2.57115044 1 1 1 + 25 0 1 0 6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -7.660444e-01 0.000000e+00 6.427876e-01 -2.57115044 0 3.06417777 1 1 1 + 26 0 1 0 5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -8.660254e-01 0.000000e+00 5.000000e-01 -2 0 3.46410162 1 1 1 + 27 0 1 0 3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.396926e-01 0.000000e+00 3.420201e-01 -1.36808057 0 3.75877048 1 1 1 + 28 0 1 0 1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.848078e-01 0.000000e+00 1.736482e-01 -0.694592711 0 3.93923101 1 1 1 + 29 0 1 0 -0.000000e+00 0.000000e+00 1.000000e+00 -0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0 0 4 1 1 1 + 30 0 1 0 -1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.848078e-01 0.000000e+00 -1.736482e-01 0.694592711 0 3.93923101 1 1 1 + 31 0 1 0 -3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.396926e-01 0.000000e+00 -3.420201e-01 1.36808057 0 3.75877048 1 1 1 + 32 0 1 0 -5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -8.660254e-01 0.000000e+00 -5.000000e-01 2 0 3.46410162 1 1 1 + 33 0 1 0 -6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -7.660444e-01 0.000000e+00 -6.427876e-01 2.57115044 0 3.06417777 1 1 1 + 34 0 1 0 -7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -6.427876e-01 0.000000e+00 -7.660444e-01 3.06417777 0 2.57115044 1 1 1 + 35 0 1 0 -8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -5.000000e-01 0.000000e+00 -8.660254e-01 3.46410162 0 2 1 1 1 + 36 0 1 0 -9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -3.420201e-01 0.000000e+00 -9.396926e-01 3.75877048 0 1.36808057 1 1 1 + 37 0 1 0 -9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -1.736482e-01 0.000000e+00 -9.848078e-01 3.93923101 0 0.694592711 1 1 1 + 38 0 1 0 -1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 0.000000e+00 -6.123234e-17 0.000000e+00 -1.000000e+00 4 0 2.4492936e-16 1 1 1 + 39 0 1 0 1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.123234e-17 0.000000e+00 1.000000e+00 -6 0 3.6739404e-16 1 1 1 + 40 0 1 0 9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -1.736482e-01 0.000000e+00 9.848078e-01 -5.90884652 0 1.04188907 1 1 1 + 41 0 1 0 9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -3.420201e-01 0.000000e+00 9.396926e-01 -5.63815572 0 2.05212086 1 1 1 + 42 0 1 0 8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -5.000000e-01 0.000000e+00 8.660254e-01 -5.19615242 0 3 1 1 1 + 43 0 1 0 7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.427876e-01 0.000000e+00 7.660444e-01 -4.59626666 0 3.85672566 1 1 1 + 44 0 1 0 6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -7.660444e-01 0.000000e+00 6.427876e-01 -3.85672566 0 4.59626666 1 1 1 + 45 0 1 0 5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -8.660254e-01 0.000000e+00 5.000000e-01 -3 0 5.19615242 1 1 1 + 46 0 1 0 3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.396926e-01 0.000000e+00 3.420201e-01 -2.05212086 0 5.63815572 1 1 1 + 47 0 1 0 1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.848078e-01 0.000000e+00 1.736482e-01 -1.04188907 0 5.90884652 1 1 1 + 48 0 1 0 -0.000000e+00 0.000000e+00 1.000000e+00 -0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0 0 6 1 1 1 + 49 0 1 0 -1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.848078e-01 0.000000e+00 -1.736482e-01 1.04188907 0 5.90884652 1 1 1 + 50 0 1 0 -3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.396926e-01 0.000000e+00 -3.420201e-01 2.05212086 0 5.63815572 1 1 1 + 51 0 1 0 -5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -8.660254e-01 0.000000e+00 -5.000000e-01 3 0 5.19615242 1 1 1 + 52 0 1 0 -6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -7.660444e-01 0.000000e+00 -6.427876e-01 3.85672566 0 4.59626666 1 1 1 + 53 0 1 0 -7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -6.427876e-01 0.000000e+00 -7.660444e-01 4.59626666 0 3.85672566 1 1 1 + 54 0 1 0 -8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -5.000000e-01 0.000000e+00 -8.660254e-01 5.19615242 0 3 1 1 1 + 55 0 1 0 -9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -3.420201e-01 0.000000e+00 -9.396926e-01 5.63815572 0 2.05212086 1 1 1 + 56 0 1 0 -9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -1.736482e-01 0.000000e+00 -9.848078e-01 5.90884652 0 1.04188907 1 1 1 + 57 0 1 0 -1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 0.000000e+00 -6.123234e-17 0.000000e+00 -1.000000e+00 6 0 3.6739404e-16 1 1 1 + 58 0 1 0 1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.123234e-17 0.000000e+00 1.000000e+00 -8 0 4.8985872e-16 1 1 1 + 59 0 1 0 9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -1.736482e-01 0.000000e+00 9.848078e-01 -7.87846202 0 1.38918542 1 1 1 + 60 0 1 0 9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -3.420201e-01 0.000000e+00 9.396926e-01 -7.51754097 0 2.73616115 1 1 1 + 61 0 1 0 8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -5.000000e-01 0.000000e+00 8.660254e-01 -6.92820323 0 4 1 1 1 + 62 0 1 0 7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.427876e-01 0.000000e+00 7.660444e-01 -6.12835554 0 5.14230088 1 1 1 + 63 0 1 0 6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -7.660444e-01 0.000000e+00 6.427876e-01 -5.14230088 0 6.12835554 1 1 1 + 64 0 1 0 5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -8.660254e-01 0.000000e+00 5.000000e-01 -4 0 6.92820323 1 1 1 + 65 0 1 0 3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.396926e-01 0.000000e+00 3.420201e-01 -2.73616115 0 7.51754097 1 1 1 + 66 0 1 0 1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.848078e-01 0.000000e+00 1.736482e-01 -1.38918542 0 7.87846202 1 1 1 + 67 0 1 0 -0.000000e+00 0.000000e+00 1.000000e+00 -0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0 0 8 1 1 1 + 68 0 1 0 -1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.848078e-01 0.000000e+00 -1.736482e-01 1.38918542 0 7.87846202 1 1 1 + 69 0 1 0 -3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.396926e-01 0.000000e+00 -3.420201e-01 2.73616115 0 7.51754097 1 1 1 + 70 0 1 0 -5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -8.660254e-01 0.000000e+00 -5.000000e-01 4 0 6.92820323 1 1 1 + 71 0 1 0 -6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -7.660444e-01 0.000000e+00 -6.427876e-01 5.14230088 0 6.12835554 1 1 1 + 72 0 1 0 -7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -6.427876e-01 0.000000e+00 -7.660444e-01 6.12835554 0 5.14230088 1 1 1 + 73 0 1 0 -8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -5.000000e-01 0.000000e+00 -8.660254e-01 6.92820323 0 4 1 1 1 + 74 0 1 0 -9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -3.420201e-01 0.000000e+00 -9.396926e-01 7.51754097 0 2.73616115 1 1 1 + 75 0 1 0 -9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -1.736482e-01 0.000000e+00 -9.848078e-01 7.87846202 0 1.38918542 1 1 1 + 76 0 1 0 -1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 0.000000e+00 -6.123234e-17 0.000000e+00 -1.000000e+00 8 0 4.8985872e-16 1 1 1 + 77 0 1 0 1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.123234e-17 0.000000e+00 1.000000e+00 -10 0 6.123234e-16 1 1 1 + 78 0 1 0 9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -1.736482e-01 0.000000e+00 9.848078e-01 -9.84807753 0 1.73648178 1 1 1 + 79 0 1 0 9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -3.420201e-01 0.000000e+00 9.396926e-01 -9.39692621 0 3.42020143 1 1 1 + 80 0 1 0 8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -5.000000e-01 0.000000e+00 8.660254e-01 -8.66025404 0 5 1 1 1 + 81 0 1 0 7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.427876e-01 0.000000e+00 7.660444e-01 -7.66044443 0 6.4278761 1 1 1 + 82 0 1 0 6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -7.660444e-01 0.000000e+00 6.427876e-01 -6.4278761 0 7.66044443 1 1 1 + 83 0 1 0 5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -8.660254e-01 0.000000e+00 5.000000e-01 -5 0 8.66025404 1 1 1 + 84 0 1 0 3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.396926e-01 0.000000e+00 3.420201e-01 -3.42020143 0 9.39692621 1 1 1 + 85 0 1 0 1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.848078e-01 0.000000e+00 1.736482e-01 -1.73648178 0 9.84807753 1 1 1 + 86 0 1 0 -0.000000e+00 0.000000e+00 1.000000e+00 -0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0 0 10 1 1 1 + 87 0 1 0 -1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.848078e-01 0.000000e+00 -1.736482e-01 1.73648178 0 9.84807753 1 1 1 + 88 0 1 0 -3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.396926e-01 0.000000e+00 -3.420201e-01 3.42020143 0 9.39692621 1 1 1 + 89 0 1 0 -5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -8.660254e-01 0.000000e+00 -5.000000e-01 5 0 8.66025404 1 1 1 + 90 0 1 0 -6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -7.660444e-01 0.000000e+00 -6.427876e-01 6.4278761 0 7.66044443 1 1 1 + 91 0 1 0 -7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -6.427876e-01 0.000000e+00 -7.660444e-01 7.66044443 0 6.4278761 1 1 1 + 92 0 1 0 -8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -5.000000e-01 0.000000e+00 -8.660254e-01 8.66025404 0 5 1 1 1 + 93 0 1 0 -9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -3.420201e-01 0.000000e+00 -9.396926e-01 9.39692621 0 3.42020143 1 1 1 + 94 0 1 0 -9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -1.736482e-01 0.000000e+00 -9.848078e-01 9.84807753 0 1.73648178 1 1 1 + 95 0 1 0 -1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 0.000000e+00 -6.123234e-17 0.000000e+00 -1.000000e+00 10 0 6.123234e-16 1 1 1 diff --git a/case/plume/plume_flat_plate_sweep/stl/flat_plate_transformed.stl b/case/plume/plume_flat_plate_sweep/stl/flat_plate_transformed.stl new file mode 100644 index 0000000000000000000000000000000000000000..fe62d0f46aea559240e52978682ceb9f188c90a0 GIT binary patch literal 518484 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%0AfK+bT4fl&W=slL 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.vtu. Geometry assets are generated by the +# scripts in stl/ and jfh/ (see their docstrings). The single paper pose +# lives in the sibling case plume_inclined_plate (rpod_integration_test_07); +# the flat-plate reframing in plume_flat_plate_sweep (rpod_verification_test_06). + +# Visiting Vehicle for RPOD analysis +[vv] +stl_lm = cylinder_transformed.stl +stl_thruster = mold_funnel_transformed.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = inclined_plate_transformed.stl + +# surface wall temperature (Kelvin): Tw/T0 = 1.5 (paper Figs. 17-21) +surface_temp = 300 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# check plume constraints? 0 or 1 +check_constraints = 0 + +# max heat flux integral (J/m^2) +heat_flux_load = inf +heat_flux_window_size = 1 + +# max heat flux rate (W/m^2) +heat_flux = inf + +# max pressure load +normal_pressure_load = inf +normal_pressure_window_size = inf + +# max normal pressure (N/m^2) +normal_pressure = inf + +# max shear pressure (N/m^2) +shear_pressure = inf + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = Simplified + +# Gas-surface interaction model +surface_interaction = Maxwellian + +# Jet Firing History +[jfh] +jfh = jfh_inclined_plate_sweep.A + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - single head-on thruster, no cant. +tcf = tcf_1_argon.txt + +# Thruster Definition File - argon at the paper conditions. +tdf = tdf.csv + +# Parameters for scaling plume geometry. +[plume] +radius = 25 +# ~89 deg: the default 0.436 (25 deg) would clip the plate -- its lower +# corners sit ~70 deg off-axis at the paper distance. 1.55 rad keeps the +# gating wedge clear of the whole plate at every sweep pose. +wedge_theta = 1.55 diff --git a/case/plume/plume_inclined_plate_sweep/jfh/generate_jfh_inclined_plate.py b/case/plume/plume_inclined_plate_sweep/jfh/generate_jfh_inclined_plate.py new file mode 100644 index 0000000..249f71d --- /dev/null +++ b/case/plume/plume_inclined_plate_sweep/jfh/generate_jfh_inclined_plate.py @@ -0,0 +1,37 @@ +"""Generate the single-firing JFH for the Cai 2016 inclined-plate case. + +One firing at the paper geometry (Aerospace 2016, 3(4):43, Section 4): the +visiting vehicle sits at the origin with an identity DCM, so its single +thruster (exit at the VV origin, no cant) fires along +X toward the plate +centered at (4, 0, 0) m. Firing time 1.0 s makes heat_flux_load equal the +heat-flux rate. + +JFH files are always generated by script, never hand-edited. Run from this +directory: python generate_jfh_inclined_plate.py +""" + +from pathlib import Path + +OUT_NAME = 'jfh_plume_inclined_plate.A' + +DCM_IDENTITY = ('1.000000e+00 0.000000e+00 0.000000e+00 ' + '0.000000e+00 1.000000e+00 0.000000e+00 ' + '0.000000e+00 0.000000e+00 1.000000e+00') + + +def write_jfh(path, firings): + """firings: list of (dt, t, dcm_str, xyz, thrusters).""" + lines = [f'offseted {len(firings)} 0', ' 0.000 0.000 0.000'] + for n, (dt, t, dcm, xyz, thrusters) in enumerate(firings, start=1): + xyz_str = ' '.join(f'{v:.9g}' for v in xyz) + thr_str = ' '.join(str(i) for i in thrusters) + lines.append(f' {n} {dt:g} {t:g} 0 {dcm} {xyz_str} 1 ' + f'{len(thrusters)} {thr_str}') + with open(path, 'w', newline='\n') as fh: + fh.write('\n'.join(lines) + '\n') + + +if __name__ == '__main__': + out_path = Path(__file__).resolve().parent / OUT_NAME + write_jfh(out_path, [(0.0, 1.0, DCM_IDENTITY, (0.0, 0.0, 0.0), [1])]) + print(f'saved {out_path}') diff --git a/case/plume/plume_inclined_plate_sweep/jfh/generate_sweep_jfh.py b/case/plume/plume_inclined_plate_sweep/jfh/generate_sweep_jfh.py new file mode 100644 index 0000000..a8e574f --- /dev/null +++ b/case/plume/plume_inclined_plate_sweep/jfh/generate_sweep_jfh.py @@ -0,0 +1,101 @@ +"""Generate the ONE sweep JFH for the Phase-3 angle x distance study. + +The plate is STATIONARY (target vehicles do not move): the visiting vehicle +is swept instead. For each firing the VV (single thruster at the VV origin, +no cant) is placed on the arc of radius L about the plate center +C = (4, 0, 0), with the thruster axis aimed at C and the JFH DCM set so the +pipeline's plume normal (first column of the DCM, with the identity TCF) +points along that axis. + +Pose parametrization, all in the plate's tilt plane (global X-Z): + + d_hat(alpha) = cos(alpha) * n_hat + sin(alpha) * t_hat + VV position = C + L * d_hat(alpha), thruster axis = -d_hat(alpha) + +where n_hat = (-sin 60, 0, cos 60) is the plate normal facing the nozzle +side and t_hat = (cos 60, 0, sin 60) the plate's inclined tangent. +alpha = 0 is HEAD-ON (thruster axis along the plate normal; the paper's +alpha0 = 90 deg); the sign is the tilt direction, mapping +alpha_paper = 90 deg - |alpha|. Results must be mirror-symmetric in ++/-alpha (the plate is square); +/-90 deg is edge-on/degenerate and is +kept to confirm ~zero struck faces. + +Sweep: alpha from -90 to +90 deg in 10 deg steps (19 angles) x +L/D in {2, 4, 6, 8, 10} (D = 1 m), enumerated distance-major: +firing index = i_L * 19 + i_alpha + 1. 95 firings total, firing time +1.0 s each. + +JFH files are always generated by script, never hand-edited. Run from this +directory: python generate_sweep_jfh.py +""" + +from pathlib import Path + +import numpy as np + +from generate_jfh_inclined_plate import write_jfh + +OUT_NAME = 'jfh_inclined_plate_sweep.A' + +PLATE_CENTER = np.array([4.0, 0.0, 0.0]) +ALPHA0_DEG = 60.0 +ALPHAS_DEG = np.arange(-90.0, 90.0 + 1e-9, 10.0) +L_OVER_D = [2.0, 4.0, 6.0, 8.0, 10.0] + + +def pose_for(alpha_deg, L, plate_center=PLATE_CENTER, alpha0_deg=ALPHA0_DEG): + """(vv_position, dcm) for one sweep firing; dcm's first column is the + thruster axis (see the pipeline's plume-normal convention). + + plate_center / alpha0_deg place the whole {plate + swept-arc} rig in the + global frame. They are physics-invariant (every relative incidence, and + thus every coefficient, is preserved under this rigid rotation + + translation) -- a flat alpha0 = 0 variant is a pure visualization + reframing of the same case. They must match the target STL's own + center/tilt (stl/transform_inclined_plate.py) and the analysis constants + (tests/rpod/rpod_verification_test_06.py).""" + a0 = np.deg2rad(alpha0_deg) + n_hat = np.array([-np.sin(a0), 0.0, np.cos(a0)]) + t_hat = np.array([np.cos(a0), 0.0, np.sin(a0)]) + alpha = np.deg2rad(alpha_deg) + d_hat = np.cos(alpha) * n_hat + np.sin(alpha) * t_hat + position = np.asarray(plate_center, dtype=float) + L * d_hat + axis = -d_hat # aimed at the plate center + # right-handed triad; the axis always lies in the X-Z plane, so the + # global Y axis is a valid second column + c1 = np.array([0.0, 1.0, 0.0]) + c2 = np.cross(axis, c1) + dcm = np.column_stack([axis, c1, c2]) + return position, dcm + + +def dcm_string(dcm): + return ' '.join(f'{v:.6e}' for v in np.asarray(dcm).ravel()) + + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument('--alpha0-deg', type=float, default=ALPHA0_DEG, + help='global-frame plate tilt (deg); physics-' + 'invariant, must match the target STL') + parser.add_argument('--distance', type=float, default=PLATE_CENTER[0], + help='plate-center X in the global frame (m); the ' + 'center is (distance, 0, 0)') + parser.add_argument('--out', type=str, default=OUT_NAME, + help='output JFH filename in this jfh/ folder') + args = parser.parse_args() + + plate_center = np.array([args.distance, 0.0, 0.0]) + firings = [] + for L in L_OVER_D: + for alpha_deg in ALPHAS_DEG: + position, dcm = pose_for(alpha_deg, L, plate_center=plate_center, + alpha0_deg=args.alpha0_deg) + firings.append((0.0, 1.0, dcm_string(dcm), tuple(position), [1])) + out_path = Path(__file__).resolve().parent / args.out + write_jfh(out_path, firings) + print(f'saved {out_path} ({len(firings)} firings: ' + f'{len(ALPHAS_DEG)} angles x {len(L_OVER_D)} distances, ' + f'alpha0 = {args.alpha0_deg} deg, center X = {args.distance} m)') diff --git a/case/plume/plume_inclined_plate/jfh/jfh_inclined_plate_sweep.A b/case/plume/plume_inclined_plate_sweep/jfh/jfh_inclined_plate_sweep.A similarity index 100% rename from case/plume/plume_inclined_plate/jfh/jfh_inclined_plate_sweep.A rename to case/plume/plume_inclined_plate_sweep/jfh/jfh_inclined_plate_sweep.A diff --git a/case/plume/plume_inclined_plate_sweep/stl/inclined_plate_transformed.stl b/case/plume/plume_inclined_plate_sweep/stl/inclined_plate_transformed.stl new file mode 100644 index 0000000000000000000000000000000000000000..6debcc468155f7d8a33810ce6275f86d5ea18932 GIT binary patch literal 518484 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 literal 0 HcmV?d00001 diff --git a/case/plume/plume_inclined_plate_sweep/stl/transform_inclined_plate.py b/case/plume/plume_inclined_plate_sweep/stl/transform_inclined_plate.py new file mode 100644 index 0000000..dc6d9be --- /dev/null +++ b/case/plume/plume_inclined_plate_sweep/stl/transform_inclined_plate.py @@ -0,0 +1,101 @@ +"""Generate the inclined-plate target mesh for the Cai 2016 verification case. + +Builds a single-sided, uniformly triangulated rectangular plate matching the +paper's Section-4 geometry (Aerospace 2016, 3(4):43): an 8 m x 8 m plate whose +center sits at (L, 0, 0) = (4, 0, 0) m from the nozzle exit, inclined by +alpha0 = 60 deg. The thruster fires along +X from a head-on JFH pose (VV at +the origin, identity DCM), so the global frame coincides with the paper's +nozzle frame and with pyrpod/plume/CaiImpingement2016.py: + + plate point(s, tau) = center + s * (0, 1, 0) + tau * (cos a0, 0, sin a0) + +Every face normal points toward the thruster, (-sin a0, 0, cos a0), which the +strike pipeline's facing test (surface_dot_plume < 0) requires; the script +asserts this before saving. + +Angle, distance, plate size, and mesh resolution are parametrized for the +Phase-3 sweep reuse. Defaults reproduce the paper case with 2 * 72^2 = 10368 +faces (~0.11 m elements): contour-quality resolution at a per-face kinetics +cost that keeps a single firing in the tens of seconds. + +Run from this directory: python transform_inclined_plate.py +""" + +import argparse +from pathlib import Path + +import numpy as np +from stl import mesh + + +def build_plate_mesh(alpha0_deg=60.0, center=(4.0, 0.0, 0.0), + half_width=4.0, half_height=4.0, n_div=72): + """Return a numpy-stl Mesh of the inclined plate. + + Parameters + ---------- + alpha0_deg : float + plate inclination angle alpha0 (deg); 90 = normal to the jet axis + center : sequence of float + plate center in the global/nozzle frame (m) + half_width : float + semi-width W0 along the horizontal s direction (m) + half_height : float + semi-length H0 along the inclined tau direction (m) + n_div : int + quad divisions per axis (faces = 2 * n_div^2) + """ + a0 = np.deg2rad(alpha0_deg) + center = np.asarray(center, dtype=float) + t_s = np.array([0.0, 1.0, 0.0]) + t_tau = np.array([np.cos(a0), 0.0, np.sin(a0)]) + normal = np.array([-np.sin(a0), 0.0, np.cos(a0)]) + + s_edges = np.linspace(-half_width, half_width, n_div + 1) + tau_edges = np.linspace(-half_height, half_height, n_div + 1) + + def point(s, tau): + return center + s * t_s + tau * t_tau + + data = np.zeros(2 * n_div * n_div, dtype=mesh.Mesh.dtype) + k = 0 + for i in range(n_div): + for j in range(n_div): + p00 = point(s_edges[i], tau_edges[j]) + p10 = point(s_edges[i + 1], tau_edges[j]) + p01 = point(s_edges[i], tau_edges[j + 1]) + p11 = point(s_edges[i + 1], tau_edges[j + 1]) + # wound so cross(v1-v0, v2-v0) points along `normal` + data['vectors'][k] = np.array([p00, p01, p11]) + data['vectors'][k + 1] = np.array([p00, p11, p10]) + k += 2 + + plate = mesh.Mesh(data) + plate.update_normals() + unit_normals = plate.get_unit_normals() + assert np.allclose(unit_normals, normal, atol=1e-9), ( + 'face normals do not all point toward the thruster') + return plate + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument('--alpha0-deg', type=float, default=60.0) + parser.add_argument('--distance', type=float, default=4.0, + help='nozzle-to-plate-center distance L (m)') + parser.add_argument('--half-width', type=float, default=4.0) + parser.add_argument('--half-height', type=float, default=4.0) + parser.add_argument('--n-div', type=int, default=72) + parser.add_argument('--out', type=str, + default='inclined_plate_transformed.stl') + args = parser.parse_args() + + plate = build_plate_mesh(alpha0_deg=args.alpha0_deg, + center=(args.distance, 0.0, 0.0), + half_width=args.half_width, + half_height=args.half_height, + n_div=args.n_div) + out_path = Path(__file__).resolve().parent / args.out + plate.save(str(out_path)) + print(f'saved {out_path} ({len(plate.vectors)} faces, ' + f'alpha0 = {args.alpha0_deg} deg, L = {args.distance} m)') diff --git a/case/plume/plume_inclined_plate_sweep/tcd/tcf_1_argon.txt b/case/plume/plume_inclined_plate_sweep/tcd/tcf_1_argon.txt new file mode 100644 index 0000000..53e6e0e --- /dev/null +++ b/case/plume/plume_inclined_plate_sweep/tcd/tcf_1_argon.txt @@ -0,0 +1,6 @@ +1 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +T1 ARG 0 0 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +0 diff --git a/case/plume/plume_inclined_plate_sweep/tcd/tdf.csv b/case/plume/plume_inclined_plate_sweep/tcd/tdf.csv new file mode 100644 index 0000000..dc39035 --- /dev/null +++ b/case/plume/plume_inclined_plate_sweep/tcd/tdf.csv @@ -0,0 +1,2 @@ +#,name,prop,F,isp,MIB,m,mdot,ve,d,R,gamma,Te,rhoe,n +ARG,CAI2016,argon,1,1,1,1,0.001,577.0684534784414,1.0,208.13,1.6666666666666667,200,6.6329E-06,1.0E+20 diff --git a/scripts/inclined_plate_sweep_study.py b/scripts/inclined_plate_sweep_study.py index 9be3c71..f27a9f4 100644 --- a/scripts/inclined_plate_sweep_study.py +++ b/scripts/inclined_plate_sweep_study.py @@ -1,8 +1,9 @@ """Phase-3 driver: angle x distance sweep of the Cai 2016 inclined plate. -Runs the 95-firing sweep JFH of case/plume/plume_inclined_plate (generated by -jfh/generate_sweep_jfh.py: VV on arcs of radius L about the stationary plate -center, thruster aimed at the center; alpha = 0 is head-on = paper +Runs the 95-firing sweep JFH of case/plume/plume_inclined_plate_sweep +(generated by that case's jfh/generate_sweep_jfh.py: VV on arcs of radius L +about the stationary plate center, thruster aimed at the center; alpha = 0 is +head-on = paper alpha0 = 90 deg, alpha_paper = 90 - |alpha| deg; L/D in {2, 4, 6, 8, 10}) through the strike pipeline's per-firing core, compute_plume_strikes -- the same function PlumeStrikeEstimationStudy calls per firing -- keeping each @@ -10,25 +11,26 @@ never pollutes the per-pose analysis. This is a standalone study script: nothing in pyrpod/mdao is used and nothing is optimized. -Outputs, to case/plume/plume_inclined_plate/results/sweep/ (gitignored): - - sweep_coefficients.csv: per firing, the Eq.-15 plate-averaged +Outputs, under case/plume/plume_inclined_plate_sweep/results/ (gitignored): + - sweep/sweep_coefficients.csv: per firing, the Eq.-15 plate-averaged coefficients CP/CF1/CF2/CQ/CM/s_cc from the pipeline AND from the exact reference (pyrpod/plume/CaiImpingement2016.py), plus the peak per-face Cp / |Cf| / Cq, struck-face counts, and per-firing wall time; - - coeff_.png: each averaged coefficient vs alpha, one curve per - L/D, pipeline (solid + markers) and reference (dashed) on the same axes; - - peaks.png: peak per-face pressure/shear/heat-flux coefficients vs alpha - per L/D; - - vtk/firing-_a_LoD.vtu: one VTK unstructured grid per - firing (same writer as the other RPOD cases) carrying that pose's own - per-face strikes, dimensional loads (pressure_Pa, shear_Pa, - heat_flux_W_m2), and coefficients (Cp, Cshear, Cf1_case, Cf2_case, Cq). - The Cf components here are in the mesh's CASE frame, un-flipped -- so - the shear vectors point the way they physically do on that pose's - mesh, unlike the paper-convention aggregate CF1/CM in the CSV. Written - per firing (independent poses; no cumulative accumulation, unlike + - sweep/coeff_.png: each averaged coefficient vs alpha, one curve + per L/D, pipeline (solid + markers) and reference (dashed) together; + - sweep/peaks.png: peak per-face pressure/shear/heat-flux coefficients vs + alpha per L/D; + - strikes/firing-.vtu: one VTK unstructured grid per firing, numbered + strictly by JFH index (the standard RPOD strike convention; same writer + as the other RPOD cases) carrying that pose's own per-face strikes, + dimensional loads (pressure_Pa, shear_Pa, heat_flux_W_m2), and + coefficients (Cp, Cshear, Cf1_case, Cf2_case, Cq). The Cf components + here are in the mesh's CASE frame, un-flipped -- so the shear vectors + point the way they physically do on that pose's mesh, unlike the + paper-convention aggregate CF1/CM in the CSV. Written per firing + (independent poses; no cumulative accumulation, unlike PlumeStrikeEstimationStudy.jfh_plume_strikes), so each file is exactly - one pose. sweep_LoD.pvd ParaView collections group the + one pose. strikes/sweep_LoD.pvd ParaView collections group the fixed-distance poses with alpha as the time coordinate, so an angle sweep can be scrubbed in ParaView. Disable the VTK export with --no-vtk (CSV + plots only). @@ -89,10 +91,13 @@ from pyrpod.util.stl.stl import convert_stl_to_vtk # noqa: E402 from pyrpod.vehicle import TargetVehicle, VisitingVehicle # noqa: E402 -CASE_DIR = str(_REPO_ROOT / 'case' / 'plume' / 'plume_inclined_plate') + '/' +CASE_DIR = str(_REPO_ROOT / 'case' / 'plume' / 'plume_inclined_plate_sweep') + '/' SWEEP_JFH = 'jfh_inclined_plate_sweep.A' RESULTS_DIR = Path(CASE_DIR) / 'results' / 'sweep' -VTK_DIR = RESULTS_DIR / 'vtk' +# Per-pose strikes follow the standard RPOD convention: one .vtu per firing, +# numbered by JFH index in results/strikes/ (firing-.vtu). The CSV and the +# coefficient/peak plots stay in results/sweep/. +STRIKES_DIR = Path(CASE_DIR) / 'results' / 'strikes' PLATE_CENTER = np.array([4.0, 0.0, 0.0]) ALPHA0_DEG = 60.0 # the mesh's fixed global tilt @@ -186,11 +191,11 @@ def pipeline_row(result, centroids, normals, areas, tau_face, thruster_pos, def write_pose_vtk(target, face_fields, base_name): - """Write one pose's per-face fields to VTK_DIR/.vtu via the + """Write one pose's per-face fields to STRIKES_DIR/.vtu via the shared convert_stl_to_vtk writer (pyevtk needs C-contiguous float64).""" cell_data = {k: np.ascontiguousarray(v, dtype=np.float64) for k, v in face_fields.items()} - convert_stl_to_vtk(target, VTK_DIR, filename=base_name, + convert_stl_to_vtk(target, STRIKES_DIR, filename=base_name, cellData=cell_data) @@ -316,7 +321,7 @@ def main(write_vtk=True): tau_face = (centroids - PLATE_CENTER) @ TANGENT_TAU if write_vtk: - VTK_DIR.mkdir(parents=True, exist_ok=True) + STRIKES_DIR.mkdir(parents=True, exist_ok=True) pvd_entries = {} # L/D -> [(alpha_deg, relative vtu path)] print(f'reference curves: {len(set(abs(ALPHAS_DEG)))} angles x ' @@ -353,11 +358,10 @@ def main(write_vtk=True): rows.append(row) if write_vtk: - base = (f'firing-{i:02d}_a{int(alpha_deg):+03d}' - f'_LoD{int(L):02d}') + base = f'firing-{i}' # strict JFH-index numbering write_pose_vtk(target, face_fields, base) pvd_entries.setdefault(L, []).append( - (alpha_deg, f'vtk/{base}.vtu')) + (alpha_deg, f'{base}.vtu')) t_sweep = time.perf_counter() - t0 # sanity: report the edge-on poses (strike membership is @@ -390,10 +394,9 @@ def main(write_vtk=True): if write_vtk: for L, entries in pvd_entries.items(): - write_pvd(RESULTS_DIR / f'sweep_LoD{int(L):02d}.pvd', entries) - print(f'wrote {n_firings} per-pose VTK files to {VTK_DIR} ' - f'(+ {len(pvd_entries)} sweep_LoD*.pvd collections in ' - f'{RESULTS_DIR})') + write_pvd(STRIKES_DIR / f'sweep_LoD{int(L):02d}.pvd', entries) + print(f'wrote {n_firings} per-pose strike VTK files to {STRIKES_DIR} ' + f'(+ {len(pvd_entries)} sweep_LoD*.pvd collections)') n_faces = len(target.vectors) print(f'timings: {n_firings} firings x {n_faces} faces -- sweep ' diff --git a/tests/rpod/rpod_verification_test_06.py b/tests/rpod/rpod_verification_test_06.py new file mode 100644 index 0000000..9693006 --- /dev/null +++ b/tests/rpod/rpod_verification_test_06.py @@ -0,0 +1,398 @@ +# ======================== +# PyRPOD: tests/rpod/rpod_verification_test_06.py +# ======================== +# Cai 2016 flat-plate SWEEP verification: the swept analog of +# rpod_integration_test_07. Runs the 95-firing sweep JFH of +# case/plume/plume_flat_plate_sweep (a flat alpha0 = 0 reframing of the +# paper geometry; VV on arcs of radius L about the stationary plate, +# thruster aimed at the plate center; 19 approach angles alpha = -90..90 deg +# x 5 stand-off distances L/D in {2,4,6,8,10}) through the strike pipeline's +# per-firing core (compute_plume_strikes -- the same function +# PlumeStrikeEstimationStudy calls per firing), reduces each pose to the +# Eq.-15 plate-averaged coefficients, and compares them against the exact +# reference (pyrpod/plume/CaiImpingement2016.py). The 60 deg inclined sweep +# lives in the sibling case plume_inclined_plate_sweep (driven by the study +# script); the reframing is physics-invariant, so both give identical +# coefficients -- flat just reads more cleanly in ParaView. +# +# This is the pytest promotion of the assertion-relevant core of +# scripts/inclined_plate_sweep_study.py; that script is kept intact as the +# full-artifact generator (CSV, coefficient/peak plots, per-pose VTK + +# ParaView collections). Only the invariants live here -- no plots/CSV. +# Per-pose strike VTK (one .vtu per pose + ParaView .pvd collections, for +# visual inspection) is OPT-IN: set the env var PYRPOD_SWEEP_VTK=1 to write +# them to the case's results/sweep/ (gitignored); the default run is lean. +# +# Hard gates (a paper-frame convention error breaks these as O(1)): +# - the sweep JFH has exactly len(ALPHAS) x len(L/D) = 95 firings; +# - mirror symmetry: results in +/-alpha match to <1e-5 relative (the +# JFH stores DCMs to 6 significant digits, so mirrored poses reproduce +# to ~1e-6); the edge-on +/-90 deg poses are excluded (their strike +# membership is float32-epsilon-arbitrary, see the script docstring); +# - CF2 vanishes identically by symmetry (~0 to 1e-4 of the CP scale). +# +# The pipeline-vs-reference gap is QUANTITATIVE DOCUMENTATION with a loose +# regression gate (as in rpod_integration_test_07): the Maxwellian chain +# is an engineering approximation of Cai's exact solution. Plate-averaged +# CP/CF1/CQ max/mean relative errors (non-edge-on poses) are printed and +# written to tests/plume/output/Cai2016/cai2016_sweep_error_summary.md. +# +# Run: python -m pytest rpod/rpod_verification_test_06.py -s (from tests/) +# Inspect: PYRPOD_SWEEP_VTK=1 python -m pytest rpod/rpod_verification_test_06.py -s +# (writes the per-pose strike .vtu files + sweep_LoD*.pvd) + +import os +import sys +import time +import unittest +from pathlib import Path + +import numpy as np + +_TESTS_DIR = Path(__file__).resolve().parents[1] +if str(_TESTS_DIR / 'plume') not in sys.path: + sys.path.insert(0, str(_TESTS_DIR / 'plume')) +if str(_TESTS_DIR.parent) not in sys.path: # repo root for direct runs + sys.path.insert(0, str(_TESTS_DIR.parent)) + +import plume_impingement_utils as piu # noqa: E402 +from pyrpod.mission import MissionEnvironment # noqa: E402 +from pyrpod.plume import CaiImpingement2016 as cai # noqa: E402 +from pyrpod.plume.PlumeStrikeCalculator import ( # noqa: E402 + compute_face_centroids, + compute_plume_strikes, +) +from pyrpod.rpod import JetFiringHistory # noqa: E402 +from pyrpod.util.stl.stl import convert_stl_to_vtk # noqa: E402 +from pyrpod.vehicle import TargetVehicle, VisitingVehicle # noqa: E402 + +# Dedicated flat-plate sweep case (alpha0 = 0, plate center at the origin): +# a pure global-frame reframing of the paper geometry so the swept strikes +# read cleanly in ParaView (flat plate in the X-Y plane, VV swept above it). +# Its config.ini selects the flat STL (flat_plate_transformed.stl) and the +# flat sweep JFH (jfh_flat_plate_sweep.A), so no runtime override is needed; +# the ALPHA0_DEG/PLATE_CENTER constants below match those assets (generated +# by stl/transform_inclined_plate.py --alpha0-deg 0 --distance 0 and +# jfh/generate_sweep_jfh.py --alpha0-deg 0 --distance 0). The paper single +# pose lives in plume_inclined_plate (rpod_integration_test_07); the 60 deg +# sweep in plume_inclined_plate_sweep (the study script). +CASE_DIR = '../case/plume/plume_flat_plate_sweep/' +SUMMARY_PATH = _TESTS_DIR / 'plume' / 'output' / 'Cai2016' / \ + 'cai2016_sweep_error_summary.md' + +# Per-pose strike VTK export (opt-in for inspection in ParaView; off by +# default so the normal pytest run stays lean). Enable by setting the +# environment variable PYRPOD_SWEEP_VTK=1. Strikes follow the standard RPOD +# pipeline convention -- one .vtu per firing, numbered by JFH index: +# results/strikes/firing-.vtu (i = 0 .. 94, one pose each) +# results/strikes/sweep_LoD.pvd (ParaView collections; alpha as time) +WRITE_VTK = bool(os.environ.get('PYRPOD_SWEEP_VTK')) +_CASE_ROOT = _TESTS_DIR.parent / 'case' / 'plume' / 'plume_flat_plate_sweep' +STRIKES_DIR = _CASE_ROOT / 'results' / 'strikes' + +# Sweep definition (mirrors scripts/inclined_plate_sweep_study.py). +PLATE_CENTER = np.array([0.0, 0.0, 0.0]) +ALPHA0_DEG = 0.0 # the mesh's fixed global tilt +ALPHAS_DEG = np.arange(-90.0, 90.0 + 1e-9, 10.0) +L_OVER_D = [2.0, 4.0, 6.0, 8.0, 10.0] +COEFF_NAMES = ['CP', 'CF1', 'CF2', 'CQ', 'CM', 's_cc'] + +_A0 = np.deg2rad(ALPHA0_DEG) +NORMAL_OUT = np.array([-np.sin(_A0), 0.0, np.cos(_A0)]) # toward the VV side +TANGENT_TAU = np.array([np.cos(_A0), 0.0, np.sin(_A0)]) +TANGENT_S = np.array([0.0, 1.0, 0.0]) + + +def face_areas(vectors): + v0, v1, v2 = vectors[:, 0], vectors[:, 1], vectors[:, 2] + return 0.5 * np.linalg.norm(np.cross(v1 - v0, v2 - v0), axis=1) + + +def pipeline_coeffs(result, centroids, areas, tau_face, thruster_pos, + alpha_deg): + """Eq.-15 averaged coefficients, per-face peaks, and the case-frame + per-face field arrays (for VTK) for one firing. + + Faithful to scripts/inclined_plate_sweep_study.py's pipeline_row. + Signed shear components are decomposed onto the plate (tau, s) axes + along the tangential projection of the radial flow direction; alpha > 0 + poses are sign-flipped (CF1/CM) into the paper convention. The returned + face_fields carry the un-flipped case-frame components, matching the + actual mesh (as the script's VTK export does). + """ + S_tot = float(np.sum(areas)) + Cp = result['pressures'] / piu.Q_DYN + Csh = result['shear_stress'] / piu.Q_DYN + Cq = result['heat_flux_rate'] / piu.Q_DYN_HEAT + + rel = centroids - thruster_pos + dist = np.linalg.norm(rel, axis=1) + u_hat = rel / dist[:, None] + n_in = -NORMAL_OUT + cos_inc = u_hat @ n_in + t_vec = u_hat - cos_inc[:, None] * n_in + t_norm = np.linalg.norm(t_vec, axis=1) + safe = t_norm > 1e-12 + t_hat = np.zeros_like(t_vec) + t_hat[safe] = t_vec[safe] / t_norm[safe, None] + Cf1 = Csh * (t_hat @ TANGENT_TAU) + Cf2 = Csh * (t_hat @ TANGENT_S) + + flip = -1.0 if alpha_deg > 0 else 1.0 # into the paper convention + CP = float(np.sum(Cp * areas) / S_tot) + CF1 = flip * float(np.sum(Cf1 * areas) / S_tot) + CF2 = float(np.sum(Cf2 * areas) / S_tot) + CQ = float(np.sum(Cq * areas) / S_tot) + CM = flip * float(np.sum(tau_face * Cp * areas) / (2.0 * piu.H_0 * S_tot)) + s_cc = CM / CP if CP != 0.0 else float('nan') + + face_fields = { + 'strikes': result['strikes'], + 'pressure_Pa': result['pressures'], + 'shear_Pa': result['shear_stress'], + 'heat_flux_W_m2': result['heat_flux_rate'], + 'Cp': Cp, + 'Cshear': Csh, + 'Cf1_case': Cf1, + 'Cf2_case': Cf2, + 'Cq': Cq, + } + return ({'CP': CP, 'CF1': CF1, 'CF2': CF2, 'CQ': CQ, 'CM': CM, + 's_cc': s_cc}, + {'peak_Cp': float(np.max(Cp)), 'peak_Cshear': float(np.max(Csh)), + 'peak_Cq': float(np.max(Cq)), + 'n_struck': int(np.count_nonzero(result['strikes']))}, + face_fields) + + +def write_pose_vtk(target, face_fields, vtk_dir, base_name): + """Write one pose's per-face fields to vtk_dir/.vtu via the + shared convert_stl_to_vtk writer (pyevtk needs C-contiguous float64).""" + cell_data = {k: np.ascontiguousarray(v, dtype=np.float64) + for k, v in face_fields.items()} + convert_stl_to_vtk(target, vtk_dir, filename=base_name, + cellData=cell_data) + + +def write_pvd(path, entries): + """ParaView collection grouping (timestep, relative_vtu_path) entries.""" + lines = ['', + '', ' '] + for timestep, rel_file in sorted(entries): + lines.append(f' ') + lines += [' ', ''] + path.write_text('\n'.join(lines) + '\n', encoding='utf-8') + + +def reference_rows(): + """Exact Eq.-15 coefficients per (|alpha|, L), cached (mirror-invariant).""" + cache = {} + for L in L_OVER_D: + for abs_alpha in sorted({abs(a) for a in ALPHAS_DEG}): + alpha_paper = np.deg2rad(90.0 - abs_alpha) + cache[(abs_alpha, L)] = cai.averaged_coefficients( + piu.S_0, alpha_paper, piu.EPS, piu.R_0, L, piu.W_0, piu.H_0) + return cache + + +def check_mirror_symmetry(rows): + """Results must be mirror-symmetric in +/-alpha (paper convention). + + The +/-90 deg poses are excluded: their strike membership comes from + float32-epsilon signs in the facing test, which are not mirrored. + CF2 is checked separately -- it vanishes identically by symmetry, so + normalizing its asymmetry by its own (noise) scale is meaningless. + """ + worst = 0.0 + for L in L_OVER_D: + by_alpha = {r['alpha_deg']: r for r in rows if r['L_over_D'] == L + and abs(r['alpha_deg']) < 90.0} + cp_scale = max(abs(r['CP_pipe']) for r in by_alpha.values()) + # tolerance covers the triangulation's O(h^2) s-asymmetry (the + # cell-diagonal split is not mirror-invariant) + assert max(abs(r['CF2_pipe']) for r in by_alpha.values()) \ + < 1e-4 * cp_scale, f'CF2 not ~0 at L/D={L}' + for a in ALPHAS_DEG[(ALPHAS_DEG > 0) & (ALPHAS_DEG < 90.0)]: + for name in ('CP', 'CF1', 'CQ', 'CM'): + lo, hi = by_alpha[-a][f'{name}_pipe'], by_alpha[a][f'{name}_pipe'] + scale = max(abs(v) for r in by_alpha.values() + for v in [r[f'{name}_pipe']]) + scale = max(scale, 1e-6 * cp_scale) + worst = max(worst, abs(hi - lo) / scale) + # tolerance: the JFH file stores DCMs to 6 significant digits (and + # positions to 9), so mirrored poses reproduce to ~1e-6 relative; + # a convention error would show up as O(1). + assert worst < 1e-5, f'mirror symmetry violated: {worst:.3g}' + return worst + + +def reference_envelope(rows): + """Plate-averaged pipeline-vs-reference relative error over the + non-edge-on poses (|alpha| < 90). Denominators are floored at 5% of + each coefficient's own reference peak so the near-zero angles do not + divide by ~0 (the significant_mask convention). Returns + {name: (max_rel, mean_rel, n_poses)} for CP/CF1/CQ.""" + sub = [r for r in rows if abs(r['alpha_deg']) < 90.0] + out = {} + for name in ('CP', 'CF1', 'CQ'): + pipe = np.array([r[f'{name}_pipe'] for r in sub]) + ref = np.array([r[f'{name}_ref'] for r in sub]) + scale = np.maximum(np.abs(ref), 0.05 * np.max(np.abs(ref))) + rel = np.abs(pipe - ref) / scale + out[name] = (float(np.max(rel)), float(np.mean(rel)), len(sub)) + return out + + +class InclinedPlateSweepVerification(unittest.TestCase): + + def test_sweep_vs_cai2016_reference(self): + # 1. Set up the flat-sweep case; its config.ini already selects the + # flat STL + 95-firing angle x distance JFH, so no override is needed. + jfh = JetFiringHistory.JetFiringHistory(CASE_DIR) + jfh.read_jfh() + + tv = TargetVehicle.TargetVehicle(CASE_DIR) + tv.set_stl() + + vv = VisitingVehicle.VisitingVehicle(CASE_DIR) + vv.set_thruster_config() + vv.set_thruster_metrics() + + me = MissionEnvironment.MissionEnvironment(CASE_DIR) + + n_firings = len(jfh.JFH) + self.assertEqual( + n_firings, len(ALPHAS_DEG) * len(L_OVER_D), + msg=f'unexpected sweep JFH length {n_firings}; regenerate with ' + 'case/plume/plume_flat_plate_sweep/jfh/generate_sweep_jfh.py ' + '--alpha0-deg 0 --distance 0 --out jfh_flat_plate_sweep.A') + + # 2. Precompute the plate geometry once (poses share one mesh). + target = tv.mesh + n_faces = len(target.vectors) + normals = target.get_unit_normals() + centroids = compute_face_centroids(target.vectors) + areas = face_areas(target.vectors) + tau_face = (centroids - PLATE_CENTER) @ TANGENT_TAU + + # 3. Exact reference coefficients, cached per (|alpha|, L). + t0 = time.perf_counter() + ref_cache = reference_rows() + t_ref = time.perf_counter() - t0 + + # 4. Run every firing through the per-firing pipeline core. When + # PYRPOD_SWEEP_VTK is set, also write each pose's per-face strikes + # and loads to results/strikes/firing-.vtu (one file per firing, + # numbered by JFH index -- the standard RPOD strike convention; + # independent poses, no cumulative accumulation). + if WRITE_VTK: + STRIKES_DIR.mkdir(parents=True, exist_ok=True) + pvd_entries = {} # L/D -> [(alpha_deg, relative vtu path)] + + rows = [] + t0 = time.perf_counter() + for i in range(n_firings): + L = L_OVER_D[i // len(ALPHAS_DEG)] + alpha_deg = float(ALPHAS_DEG[i % len(ALPHAS_DEG)]) + step = {'thrusters': jfh.JFH[i]['thrusters'], + 'xyz': np.array(jfh.JFH[i]['xyz']), + 'dcm': np.array(jfh.JFH[i]['dcm']), + 't': float(jfh.JFH[i]['t'])} + result = compute_plume_strikes(target, normals, vv, step, me, + face_centroids=centroids) + coeffs, peaks, face_fields = pipeline_coeffs( + result, centroids, areas, tau_face, step['xyz'], alpha_deg) + ref = ref_cache[(abs(alpha_deg), L)] + row = {'alpha_deg': alpha_deg, 'L_over_D': L, **peaks} + for name in COEFF_NAMES: + row[f'{name}_pipe'] = coeffs[name] + row[f'{name}_ref'] = float(ref[name]) + rows.append(row) + + if WRITE_VTK: + base = f'firing-{i}' # strict JFH-index numbering + write_pose_vtk(target, face_fields, STRIKES_DIR, base) + pvd_entries.setdefault(L, []).append( + (alpha_deg, f'{base}.vtu')) + t_sweep = time.perf_counter() - t0 + + if WRITE_VTK: + for L, entries in pvd_entries.items(): + write_pvd(STRIKES_DIR / f'sweep_LoD{int(L):02d}.pvd', entries) + print(f'[cai2016-sweep] wrote {n_firings} per-pose strike VTK ' + f'files to {STRIKES_DIR} (+ {len(pvd_entries)} ' + f'sweep_LoD*.pvd collections)') + + # 5. Report the edge-on poses (excluded from the gates: strike + # membership is epsilon-degenerate there, see the script docstring). + head_on = {r['L_over_D']: r['CP_pipe'] for r in rows + if r['alpha_deg'] == 0.0} + for r in rows: + if abs(r['alpha_deg']) == 90.0: + print(f"[cai2016-sweep] edge-on alpha={r['alpha_deg']:+.0f}, " + f"L/D={r['L_over_D']:g}: {r['n_struck']} faces pass the " + f"epsilon-degenerate facing test; CP={r['CP_pipe']:.3e} " + f"(head-on {head_on[r['L_over_D']]:.3e})") + + # 6. Hard convention gates. + worst = check_mirror_symmetry(rows) + self.assertLess(worst, 1e-5, + msg=f'mirror symmetry violated: {worst:.3g}') + print(f'[cai2016-sweep] mirror-symmetry worst normalized asymmetry ' + f'{worst:.2e}') + + # 7. Quantitative documentation of the pipeline-vs-reference gap. + env = reference_envelope(rows) + for name in ('CP', 'CF1', 'CQ'): + mx, mn, n = env[name] + print(f'[cai2016-sweep] {name}: max rel err {mx:.3f}, ' + f'mean rel err {mn:.3f} (over {n} non-edge-on poses)') + print(f'[cai2016-sweep] timings: sweep {t_sweep:.1f} s ' + f'({t_sweep / n_firings * 1e3:.0f} ms/firing, {n_faces} faces), ' + f'reference {t_ref:.1f} s') + + SUMMARY_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(SUMMARY_PATH, 'w', encoding='utf-8', newline='') as fh: + fh.write('# Cai 2016 inclined-plate sweep error summary ' + '(strike pipeline vs exact reference)\n\n') + fh.write('Written by tests/rpod/rpod_verification_test_06.py: ' + 'plate-averaged Eq.-15 coefficients from the ' + 'PlumeStrikeEstimationStudy per-firing core ' + '(compute_plume_strikes; Simplified kinetics + ' + 'Maxwellian wall model at the true incidence angle) ' + 'over the 95-firing angle x distance sweep, compared ' + 'against pyrpod/plume/CaiImpingement2016.py ' + f'(Eq. 15 quadrature). {len(ALPHAS_DEG)} approach ' + f'angles x {len(L_OVER_D)} distances, {n_faces} faces, ' + f'sweep wall time {t_sweep:.1f} s.\n\n') + fh.write(f'Mirror-symmetry worst normalized asymmetry: ' + f'{worst:.2e} (gate < 1e-5).\n\n') + fh.write('| quantity | max_rel_err | mean_rel_err | poses |\n') + fh.write('|---|---|---|---|\n') + for name in ('CP', 'CF1', 'CQ'): + mx, mn, n = env[name] + fh.write(f'| {name} | {mx:.4g} | {mn:.4g} | {n} |\n') + fh.write('\nPlate-averaged relative errors over the non-edge-on ' + 'poses (|alpha| < 90 deg), denominators floored at 5% ' + "of each coefficient's reference peak. The gap is the " + 'documented accuracy of the Maxwellian engineering ' + 'chain vs the exact collisionless solution -- not a ' + 'regression gate.\n') + print(f'[cai2016-sweep] wrote {SUMMARY_PATH}') + + # 8. Loose envelope so a convention regression (e.g. a dropped + # paper-frame sign flip) fails loudly while the normal + # Maxwellian-chain gap passes. Calibrated from the measured run + # (CP tracks reference ~1-3%, CF1 ~5%). + self.assertLess(env['CP'][1], 0.08, + msg='mean plate-averaged CP error vs reference far ' + 'above the documented Maxwellian-chain gap') + self.assertLess(env['CF1'][1], 0.15, + msg='mean plate-averaged CF1 error vs reference far ' + 'above the documented Maxwellian-chain gap') + + +if __name__ == '__main__': + unittest.main() From ef5ccb09b3b80809a9977a0e6b8e606276030c71 Mon Sep 17 00:00:00 2001 From: andytorrestb Date: Wed, 22 Jul 2026 01:27:42 -0500 Subject: [PATCH 11/11] Add cylinder-target sweep smoke case and verification test New case case/plume/plume_cylinder_sweep/ sweeps the VV over 19 angles x 5 orbit radii (95 firings; jfh_cylinder_sweep.A, generated by generate_cylinder_sweep_jfh.py) about the centroid of the shared high-res cylinder mesh data/stl/cylinder.stl (14036 faces, radius 2 m, axis along X in [-7, 0]), aiming the thruster inward. The target STL is referenced from the shared data/ dir per the repo de-dup convention (not copied in). tests/rpod/rpod_verification_test_07.py drives the full PlumeStrikeEstimationStudy pipeline (jfh_plume_strikes) end-to-end on the cylinder and gates only on it completing with well-formed per-firing strikes and the sweep illuminating the target -- a smoke test that the pipeline runs on a curved/closed mesh, not a validated impingement scenario. Passes in ~20 s (95 firings x 14036 faces; peak 7026 faces struck in a single firing). Co-Authored-By: Claude Opus 4.8 --- case/plume/plume_cylinder_sweep/config.ini | 76 ++++++++++++ .../jfh/generate_cylinder_sweep_jfh.py | 75 ++++++++++++ .../jfh/generate_jfh_inclined_plate.py | 37 ++++++ .../jfh/jfh_cylinder_sweep.A | 97 +++++++++++++++ .../plume_cylinder_sweep/tcd/tcf_1_argon.txt | 6 + case/plume/plume_cylinder_sweep/tcd/tdf.csv | 2 + tests/rpod/rpod_verification_test_07.py | 112 ++++++++++++++++++ 7 files changed, 405 insertions(+) create mode 100644 case/plume/plume_cylinder_sweep/config.ini create mode 100644 case/plume/plume_cylinder_sweep/jfh/generate_cylinder_sweep_jfh.py create mode 100644 case/plume/plume_cylinder_sweep/jfh/generate_jfh_inclined_plate.py create mode 100644 case/plume/plume_cylinder_sweep/jfh/jfh_cylinder_sweep.A create mode 100644 case/plume/plume_cylinder_sweep/tcd/tcf_1_argon.txt create mode 100644 case/plume/plume_cylinder_sweep/tcd/tdf.csv create mode 100644 tests/rpod/rpod_verification_test_07.py diff --git a/case/plume/plume_cylinder_sweep/config.ini b/case/plume/plume_cylinder_sweep/config.ini new file mode 100644 index 0000000..49b20bc --- /dev/null +++ b/case/plume/plume_cylinder_sweep/config.ini @@ -0,0 +1,76 @@ +# Cylinder-target SWEEP smoke case: the plate-sweep setup re-aimed at a +# CYLINDER target (data/stl/cylinder_transformed.stl -- radius 2 m, axis along +# X in [-7, 0], centroid (-3.5, 0, 0)). The VV (single argon thruster, D = 1 m, +# S0 = 2.0, Simplified kinetics + Maxwellian wall) is swept over 19 approach +# angles x 5 orbit radii (95 firings; jfh_cylinder_sweep.A) about the cylinder +# centroid, writing per-firing strikes to results/strikes/firing-.vtu. This +# case exists to confirm the strike pipeline RUNS on a curved/closed target; +# the strike results are not physically meaningful. Geometry: the target is +# the shared high-res mesh data/stl/cylinder.stl (14036 faces; resolved from +# the shared data/ dir, not copied into the case per the repo's de-dup +# convention); the sweep JFH is generated by +# jfh/generate_cylinder_sweep_jfh.py (its centroid/radii are unchanged -- +# cylinder.stl and cylinder_transformed.stl share the same geometry). + +# Visiting Vehicle for RPOD analysis +[vv] +stl_lm = cylinder.stl +stl_thruster = mold_funnel_transformed.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = cylinder.stl + +# surface wall temperature (Kelvin): Tw/T0 = 1.5 (paper Figs. 17-21) +surface_temp = 300 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# check plume constraints? 0 or 1 +check_constraints = 0 + +# max heat flux integral (J/m^2) +heat_flux_load = inf +heat_flux_window_size = 1 + +# max heat flux rate (W/m^2) +heat_flux = inf + +# max pressure load +normal_pressure_load = inf +normal_pressure_window_size = inf + +# max normal pressure (N/m^2) +normal_pressure = inf + +# max shear pressure (N/m^2) +shear_pressure = inf + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = Simplified + +# Gas-surface interaction model +surface_interaction = Maxwellian + +# Jet Firing History +[jfh] +jfh = jfh_cylinder_sweep.A + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - single head-on thruster, no cant. +tcf = tcf_1_argon.txt + +# Thruster Definition File - argon at the paper conditions. +tdf = tdf.csv + +# Parameters for scaling plume geometry. +[plume] +radius = 25 +# ~89 deg: the default 0.436 (25 deg) would clip the plate -- its lower +# corners sit ~70 deg off-axis at the paper distance. 1.55 rad keeps the +# gating wedge clear of the whole plate at every sweep pose. +wedge_theta = 1.55 diff --git a/case/plume/plume_cylinder_sweep/jfh/generate_cylinder_sweep_jfh.py b/case/plume/plume_cylinder_sweep/jfh/generate_cylinder_sweep_jfh.py new file mode 100644 index 0000000..28bef6f --- /dev/null +++ b/case/plume/plume_cylinder_sweep/jfh/generate_cylinder_sweep_jfh.py @@ -0,0 +1,75 @@ +"""Generate the sweep JFH for the cylinder-target smoke case. + +Same angle x distance sweep idea as the plate cases, but aimed at a CYLINDER +target (data/stl/cylinder_transformed.stl: radius 2 m, axis along X spanning +x in [-7, 0], centroid C = (-3.5, 0, 0), bounding-sphere radius ~4.03 m). + +The plate is replaced by the cylinder; the visiting vehicle (single thruster +at the VV origin, no cant) is still swept. For each firing the VV is placed on +the arc of radius L about the cylinder centroid C, with the thruster axis aimed +at C and the JFH DCM set so the pipeline's plume normal (first column of the +DCM, with the identity TCF) points along that axis: + + d_hat(alpha) = cos(alpha) * n_hat + sin(alpha) * t_hat + VV position = C + L * d_hat(alpha), thruster axis = -d_hat(alpha) + +with n_hat = (0, 0, 1) and t_hat = (1, 0, 0), so the sweep orbits the cylinder +in the global X-Z plane (alpha = 0 fires straight down -Z onto the curved +surface; alpha = +/-90 fires along -/+X at the end caps). + +The orbit radii all exceed the cylinder's bounding sphere so the VV never sits +inside the target (this case is a pipeline smoke test -- the strike results are +not physically meaningful). Sweep: alpha from -90 to +90 deg in 10 deg steps +(19 angles) x L in {6, 8, 10, 12, 14} m, enumerated distance-major: +firing index = i_L * 19 + i_alpha + 1. 95 firings total, 1.0 s each. + +JFH files are always generated by script, never hand-edited. Run from this +directory: python generate_cylinder_sweep_jfh.py +""" + +from pathlib import Path + +import numpy as np + +from generate_jfh_inclined_plate import write_jfh + +OUT_NAME = 'jfh_cylinder_sweep.A' + +CYLINDER_CENTER = np.array([-3.5, 0.0, 0.0]) +ALPHA0_DEG = 0.0 # global-frame orbit-plane tilt +ALPHAS_DEG = np.arange(-90.0, 90.0 + 1e-9, 10.0) +RADII = [6.0, 8.0, 10.0, 12.0, 14.0] # arc radii (m); all > ~4.03 bounding sphere + + +def pose_for(alpha_deg, L): + """(vv_position, dcm) for one sweep firing; dcm's first column is the + thruster axis (see the pipeline's plume-normal convention).""" + a0 = np.deg2rad(ALPHA0_DEG) + n_hat = np.array([-np.sin(a0), 0.0, np.cos(a0)]) + t_hat = np.array([np.cos(a0), 0.0, np.sin(a0)]) + alpha = np.deg2rad(alpha_deg) + d_hat = np.cos(alpha) * n_hat + np.sin(alpha) * t_hat + position = CYLINDER_CENTER + L * d_hat + axis = -d_hat # aimed at the cylinder centroid + # right-handed triad; the axis always lies in the X-Z plane, so the + # global Y axis is a valid second column + c1 = np.array([0.0, 1.0, 0.0]) + c2 = np.cross(axis, c1) + dcm = np.column_stack([axis, c1, c2]) + return position, dcm + + +def dcm_string(dcm): + return ' '.join(f'{v:.6e}' for v in np.asarray(dcm).ravel()) + + +if __name__ == '__main__': + firings = [] + for L in RADII: + for alpha_deg in ALPHAS_DEG: + position, dcm = pose_for(alpha_deg, L) + firings.append((0.0, 1.0, dcm_string(dcm), tuple(position), [1])) + out_path = Path(__file__).resolve().parent / OUT_NAME + write_jfh(out_path, firings) + print(f'saved {out_path} ({len(firings)} firings: ' + f'{len(ALPHAS_DEG)} angles x {len(RADII)} radii)') diff --git a/case/plume/plume_cylinder_sweep/jfh/generate_jfh_inclined_plate.py b/case/plume/plume_cylinder_sweep/jfh/generate_jfh_inclined_plate.py new file mode 100644 index 0000000..249f71d --- /dev/null +++ b/case/plume/plume_cylinder_sweep/jfh/generate_jfh_inclined_plate.py @@ -0,0 +1,37 @@ +"""Generate the single-firing JFH for the Cai 2016 inclined-plate case. + +One firing at the paper geometry (Aerospace 2016, 3(4):43, Section 4): the +visiting vehicle sits at the origin with an identity DCM, so its single +thruster (exit at the VV origin, no cant) fires along +X toward the plate +centered at (4, 0, 0) m. Firing time 1.0 s makes heat_flux_load equal the +heat-flux rate. + +JFH files are always generated by script, never hand-edited. Run from this +directory: python generate_jfh_inclined_plate.py +""" + +from pathlib import Path + +OUT_NAME = 'jfh_plume_inclined_plate.A' + +DCM_IDENTITY = ('1.000000e+00 0.000000e+00 0.000000e+00 ' + '0.000000e+00 1.000000e+00 0.000000e+00 ' + '0.000000e+00 0.000000e+00 1.000000e+00') + + +def write_jfh(path, firings): + """firings: list of (dt, t, dcm_str, xyz, thrusters).""" + lines = [f'offseted {len(firings)} 0', ' 0.000 0.000 0.000'] + for n, (dt, t, dcm, xyz, thrusters) in enumerate(firings, start=1): + xyz_str = ' '.join(f'{v:.9g}' for v in xyz) + thr_str = ' '.join(str(i) for i in thrusters) + lines.append(f' {n} {dt:g} {t:g} 0 {dcm} {xyz_str} 1 ' + f'{len(thrusters)} {thr_str}') + with open(path, 'w', newline='\n') as fh: + fh.write('\n'.join(lines) + '\n') + + +if __name__ == '__main__': + out_path = Path(__file__).resolve().parent / OUT_NAME + write_jfh(out_path, [(0.0, 1.0, DCM_IDENTITY, (0.0, 0.0, 0.0), [1])]) + print(f'saved {out_path}') diff --git a/case/plume/plume_cylinder_sweep/jfh/jfh_cylinder_sweep.A b/case/plume/plume_cylinder_sweep/jfh/jfh_cylinder_sweep.A new file mode 100644 index 0000000..f60a586 --- /dev/null +++ b/case/plume/plume_cylinder_sweep/jfh/jfh_cylinder_sweep.A @@ -0,0 +1,97 @@ +offseted 95 0 + 0.000 0.000 0.000 + 1 0 1 0 1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.123234e-17 0.000000e+00 1.000000e+00 -9.5 0 3.6739404e-16 1 1 1 + 2 0 1 0 9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -1.736482e-01 0.000000e+00 9.848078e-01 -9.40884652 0 1.04188907 1 1 1 + 3 0 1 0 9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -3.420201e-01 0.000000e+00 9.396926e-01 -9.13815572 0 2.05212086 1 1 1 + 4 0 1 0 8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -5.000000e-01 0.000000e+00 8.660254e-01 -8.69615242 0 3 1 1 1 + 5 0 1 0 7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.427876e-01 0.000000e+00 7.660444e-01 -8.09626666 0 3.85672566 1 1 1 + 6 0 1 0 6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -7.660444e-01 0.000000e+00 6.427876e-01 -7.35672566 0 4.59626666 1 1 1 + 7 0 1 0 5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -8.660254e-01 0.000000e+00 5.000000e-01 -6.5 0 5.19615242 1 1 1 + 8 0 1 0 3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.396926e-01 0.000000e+00 3.420201e-01 -5.55212086 0 5.63815572 1 1 1 + 9 0 1 0 1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.848078e-01 0.000000e+00 1.736482e-01 -4.54188907 0 5.90884652 1 1 1 + 10 0 1 0 -0.000000e+00 0.000000e+00 1.000000e+00 -0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 -3.5 0 6 1 1 1 + 11 0 1 0 -1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.848078e-01 0.000000e+00 -1.736482e-01 -2.45811093 0 5.90884652 1 1 1 + 12 0 1 0 -3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.396926e-01 0.000000e+00 -3.420201e-01 -1.44787914 0 5.63815572 1 1 1 + 13 0 1 0 -5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -8.660254e-01 0.000000e+00 -5.000000e-01 -0.5 0 5.19615242 1 1 1 + 14 0 1 0 -6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -7.660444e-01 0.000000e+00 -6.427876e-01 0.356725658 0 4.59626666 1 1 1 + 15 0 1 0 -7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -6.427876e-01 0.000000e+00 -7.660444e-01 1.09626666 0 3.85672566 1 1 1 + 16 0 1 0 -8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -5.000000e-01 0.000000e+00 -8.660254e-01 1.69615242 0 3 1 1 1 + 17 0 1 0 -9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -3.420201e-01 0.000000e+00 -9.396926e-01 2.13815572 0 2.05212086 1 1 1 + 18 0 1 0 -9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -1.736482e-01 0.000000e+00 -9.848078e-01 2.40884652 0 1.04188907 1 1 1 + 19 0 1 0 -1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 0.000000e+00 -6.123234e-17 0.000000e+00 -1.000000e+00 2.5 0 3.6739404e-16 1 1 1 + 20 0 1 0 1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.123234e-17 0.000000e+00 1.000000e+00 -11.5 0 4.8985872e-16 1 1 1 + 21 0 1 0 9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -1.736482e-01 0.000000e+00 9.848078e-01 -11.378462 0 1.38918542 1 1 1 + 22 0 1 0 9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -3.420201e-01 0.000000e+00 9.396926e-01 -11.017541 0 2.73616115 1 1 1 + 23 0 1 0 8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -5.000000e-01 0.000000e+00 8.660254e-01 -10.4282032 0 4 1 1 1 + 24 0 1 0 7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.427876e-01 0.000000e+00 7.660444e-01 -9.62835554 0 5.14230088 1 1 1 + 25 0 1 0 6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -7.660444e-01 0.000000e+00 6.427876e-01 -8.64230088 0 6.12835554 1 1 1 + 26 0 1 0 5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -8.660254e-01 0.000000e+00 5.000000e-01 -7.5 0 6.92820323 1 1 1 + 27 0 1 0 3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.396926e-01 0.000000e+00 3.420201e-01 -6.23616115 0 7.51754097 1 1 1 + 28 0 1 0 1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.848078e-01 0.000000e+00 1.736482e-01 -4.88918542 0 7.87846202 1 1 1 + 29 0 1 0 -0.000000e+00 0.000000e+00 1.000000e+00 -0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 -3.5 0 8 1 1 1 + 30 0 1 0 -1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.848078e-01 0.000000e+00 -1.736482e-01 -2.11081458 0 7.87846202 1 1 1 + 31 0 1 0 -3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.396926e-01 0.000000e+00 -3.420201e-01 -0.763838853 0 7.51754097 1 1 1 + 32 0 1 0 -5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -8.660254e-01 0.000000e+00 -5.000000e-01 0.5 0 6.92820323 1 1 1 + 33 0 1 0 -6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -7.660444e-01 0.000000e+00 -6.427876e-01 1.64230088 0 6.12835554 1 1 1 + 34 0 1 0 -7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -6.427876e-01 0.000000e+00 -7.660444e-01 2.62835554 0 5.14230088 1 1 1 + 35 0 1 0 -8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -5.000000e-01 0.000000e+00 -8.660254e-01 3.42820323 0 4 1 1 1 + 36 0 1 0 -9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -3.420201e-01 0.000000e+00 -9.396926e-01 4.01754097 0 2.73616115 1 1 1 + 37 0 1 0 -9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -1.736482e-01 0.000000e+00 -9.848078e-01 4.37846202 0 1.38918542 1 1 1 + 38 0 1 0 -1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 0.000000e+00 -6.123234e-17 0.000000e+00 -1.000000e+00 4.5 0 4.8985872e-16 1 1 1 + 39 0 1 0 1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.123234e-17 0.000000e+00 1.000000e+00 -13.5 0 6.123234e-16 1 1 1 + 40 0 1 0 9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -1.736482e-01 0.000000e+00 9.848078e-01 -13.3480775 0 1.73648178 1 1 1 + 41 0 1 0 9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -3.420201e-01 0.000000e+00 9.396926e-01 -12.8969262 0 3.42020143 1 1 1 + 42 0 1 0 8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -5.000000e-01 0.000000e+00 8.660254e-01 -12.160254 0 5 1 1 1 + 43 0 1 0 7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.427876e-01 0.000000e+00 7.660444e-01 -11.1604444 0 6.4278761 1 1 1 + 44 0 1 0 6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -7.660444e-01 0.000000e+00 6.427876e-01 -9.9278761 0 7.66044443 1 1 1 + 45 0 1 0 5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -8.660254e-01 0.000000e+00 5.000000e-01 -8.5 0 8.66025404 1 1 1 + 46 0 1 0 3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.396926e-01 0.000000e+00 3.420201e-01 -6.92020143 0 9.39692621 1 1 1 + 47 0 1 0 1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.848078e-01 0.000000e+00 1.736482e-01 -5.23648178 0 9.84807753 1 1 1 + 48 0 1 0 -0.000000e+00 0.000000e+00 1.000000e+00 -0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 -3.5 0 10 1 1 1 + 49 0 1 0 -1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.848078e-01 0.000000e+00 -1.736482e-01 -1.76351822 0 9.84807753 1 1 1 + 50 0 1 0 -3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.396926e-01 0.000000e+00 -3.420201e-01 -0.0797985667 0 9.39692621 1 1 1 + 51 0 1 0 -5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -8.660254e-01 0.000000e+00 -5.000000e-01 1.5 0 8.66025404 1 1 1 + 52 0 1 0 -6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -7.660444e-01 0.000000e+00 -6.427876e-01 2.9278761 0 7.66044443 1 1 1 + 53 0 1 0 -7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -6.427876e-01 0.000000e+00 -7.660444e-01 4.16044443 0 6.4278761 1 1 1 + 54 0 1 0 -8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -5.000000e-01 0.000000e+00 -8.660254e-01 5.16025404 0 5 1 1 1 + 55 0 1 0 -9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -3.420201e-01 0.000000e+00 -9.396926e-01 5.89692621 0 3.42020143 1 1 1 + 56 0 1 0 -9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -1.736482e-01 0.000000e+00 -9.848078e-01 6.34807753 0 1.73648178 1 1 1 + 57 0 1 0 -1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 0.000000e+00 -6.123234e-17 0.000000e+00 -1.000000e+00 6.5 0 6.123234e-16 1 1 1 + 58 0 1 0 1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.123234e-17 0.000000e+00 1.000000e+00 -15.5 0 7.34788079e-16 1 1 1 + 59 0 1 0 9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -1.736482e-01 0.000000e+00 9.848078e-01 -15.317693 0 2.08377813 1 1 1 + 60 0 1 0 9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -3.420201e-01 0.000000e+00 9.396926e-01 -14.7763114 0 4.10424172 1 1 1 + 61 0 1 0 8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -5.000000e-01 0.000000e+00 8.660254e-01 -13.8923048 0 6 1 1 1 + 62 0 1 0 7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.427876e-01 0.000000e+00 7.660444e-01 -12.6925333 0 7.71345132 1 1 1 + 63 0 1 0 6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -7.660444e-01 0.000000e+00 6.427876e-01 -11.2134513 0 9.19253332 1 1 1 + 64 0 1 0 5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -8.660254e-01 0.000000e+00 5.000000e-01 -9.5 0 10.3923048 1 1 1 + 65 0 1 0 3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.396926e-01 0.000000e+00 3.420201e-01 -7.60424172 0 11.2763114 1 1 1 + 66 0 1 0 1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.848078e-01 0.000000e+00 1.736482e-01 -5.58377813 0 11.817693 1 1 1 + 67 0 1 0 -0.000000e+00 0.000000e+00 1.000000e+00 -0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 -3.5 0 12 1 1 1 + 68 0 1 0 -1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.848078e-01 0.000000e+00 -1.736482e-01 -1.41622187 0 11.817693 1 1 1 + 69 0 1 0 -3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.396926e-01 0.000000e+00 -3.420201e-01 0.60424172 0 11.2763114 1 1 1 + 70 0 1 0 -5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -8.660254e-01 0.000000e+00 -5.000000e-01 2.5 0 10.3923048 1 1 1 + 71 0 1 0 -6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -7.660444e-01 0.000000e+00 -6.427876e-01 4.21345132 0 9.19253332 1 1 1 + 72 0 1 0 -7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -6.427876e-01 0.000000e+00 -7.660444e-01 5.69253332 0 7.71345132 1 1 1 + 73 0 1 0 -8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -5.000000e-01 0.000000e+00 -8.660254e-01 6.89230485 0 6 1 1 1 + 74 0 1 0 -9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -3.420201e-01 0.000000e+00 -9.396926e-01 7.77631145 0 4.10424172 1 1 1 + 75 0 1 0 -9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -1.736482e-01 0.000000e+00 -9.848078e-01 8.31769304 0 2.08377813 1 1 1 + 76 0 1 0 -1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 0.000000e+00 -6.123234e-17 0.000000e+00 -1.000000e+00 8.5 0 7.34788079e-16 1 1 1 + 77 0 1 0 1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.123234e-17 0.000000e+00 1.000000e+00 -17.5 0 8.57252759e-16 1 1 1 + 78 0 1 0 9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -1.736482e-01 0.000000e+00 9.848078e-01 -17.2873085 0 2.43107449 1 1 1 + 79 0 1 0 9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -3.420201e-01 0.000000e+00 9.396926e-01 -16.6556967 0 4.78828201 1 1 1 + 80 0 1 0 8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -5.000000e-01 0.000000e+00 8.660254e-01 -15.6243557 0 7 1 1 1 + 81 0 1 0 7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -6.427876e-01 0.000000e+00 7.660444e-01 -14.2246222 0 8.99902654 1 1 1 + 82 0 1 0 6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -7.660444e-01 0.000000e+00 6.427876e-01 -12.4990265 0 10.7246222 1 1 1 + 83 0 1 0 5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -8.660254e-01 0.000000e+00 5.000000e-01 -10.5 0 12.1243557 1 1 1 + 84 0 1 0 3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.396926e-01 0.000000e+00 3.420201e-01 -8.28828201 0 13.1556967 1 1 1 + 85 0 1 0 1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 -0.000000e+00 -9.848078e-01 0.000000e+00 1.736482e-01 -5.93107449 0 13.7873085 1 1 1 + 86 0 1 0 -0.000000e+00 0.000000e+00 1.000000e+00 -0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 -3.5 0 14 1 1 1 + 87 0 1 0 -1.736482e-01 0.000000e+00 9.848078e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.848078e-01 0.000000e+00 -1.736482e-01 -1.06892551 0 13.7873085 1 1 1 + 88 0 1 0 -3.420201e-01 0.000000e+00 9.396926e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -9.396926e-01 0.000000e+00 -3.420201e-01 1.28828201 0 13.1556967 1 1 1 + 89 0 1 0 -5.000000e-01 0.000000e+00 8.660254e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -8.660254e-01 0.000000e+00 -5.000000e-01 3.5 0 12.1243557 1 1 1 + 90 0 1 0 -6.427876e-01 0.000000e+00 7.660444e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -7.660444e-01 0.000000e+00 -6.427876e-01 5.49902654 0 10.7246222 1 1 1 + 91 0 1 0 -7.660444e-01 0.000000e+00 6.427876e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -6.427876e-01 0.000000e+00 -7.660444e-01 7.2246222 0 8.99902654 1 1 1 + 92 0 1 0 -8.660254e-01 0.000000e+00 5.000000e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -5.000000e-01 0.000000e+00 -8.660254e-01 8.62435565 0 7 1 1 1 + 93 0 1 0 -9.396926e-01 0.000000e+00 3.420201e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -3.420201e-01 0.000000e+00 -9.396926e-01 9.65569669 0 4.78828201 1 1 1 + 94 0 1 0 -9.848078e-01 0.000000e+00 1.736482e-01 -0.000000e+00 1.000000e+00 0.000000e+00 -1.736482e-01 0.000000e+00 -9.848078e-01 10.2873085 0 2.43107449 1 1 1 + 95 0 1 0 -1.000000e+00 0.000000e+00 6.123234e-17 -0.000000e+00 1.000000e+00 0.000000e+00 -6.123234e-17 0.000000e+00 -1.000000e+00 10.5 0 8.57252759e-16 1 1 1 diff --git a/case/plume/plume_cylinder_sweep/tcd/tcf_1_argon.txt b/case/plume/plume_cylinder_sweep/tcd/tcf_1_argon.txt new file mode 100644 index 0000000..53e6e0e --- /dev/null +++ b/case/plume/plume_cylinder_sweep/tcd/tcf_1_argon.txt @@ -0,0 +1,6 @@ +1 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +T1 ARG 0 0 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +0 diff --git a/case/plume/plume_cylinder_sweep/tcd/tdf.csv b/case/plume/plume_cylinder_sweep/tcd/tdf.csv new file mode 100644 index 0000000..dc39035 --- /dev/null +++ b/case/plume/plume_cylinder_sweep/tcd/tdf.csv @@ -0,0 +1,2 @@ +#,name,prop,F,isp,MIB,m,mdot,ve,d,R,gamma,Te,rhoe,n +ARG,CAI2016,argon,1,1,1,1,0.001,577.0684534784414,1.0,208.13,1.6666666666666667,200,6.6329E-06,1.0E+20 diff --git a/tests/rpod/rpod_verification_test_07.py b/tests/rpod/rpod_verification_test_07.py new file mode 100644 index 0000000..57942ea --- /dev/null +++ b/tests/rpod/rpod_verification_test_07.py @@ -0,0 +1,112 @@ +# ======================== +# PyRPOD: tests/rpod/rpod_verification_test_07.py +# ======================== +# Cylinder-target sweep SMOKE test: exercises the strike pipeline on a +# CURVED, closed target (case/plume/plume_cylinder_sweep -- the shared +# high-res cylinder mesh data/stl/cylinder.stl: 14036 faces, radius 2 m, +# axis along X in [-7, 0], centroid (-3.5, 0, 0)) instead of a flat plate. The +# visiting vehicle (single argon thruster) is swept over 19 approach +# angles x 5 orbit radii (95 firings; jfh_cylinder_sweep.A) about the +# cylinder centroid, and the full PlumeStrikeEstimationStudy pipeline +# (jfh_plume_strikes) is run end-to-end. +# +# This confirms the case RUNS -- the pipeline loads the cylinder, sweeps +# every pose, and produces per-firing strikes -- NOT that the loads are +# physically meaningful (the orbit is a geometric smoke sweep, not a +# validated impingement scenario). Assertions therefore gate only on the +# pipeline completing with well-formed per-firing results and the sweep +# actually illuminating the cylinder. +# +# Run: python -m pytest rpod/rpod_verification_test_07.py -s (from tests/) + +import sys +import time +import unittest +from pathlib import Path + +import numpy as np + +_TESTS_DIR = Path(__file__).resolve().parents[1] +if str(_TESTS_DIR.parent) not in sys.path: # repo root for direct runs + sys.path.insert(0, str(_TESTS_DIR.parent)) + +from pyrpod.mission import MissionEnvironment # noqa: E402 +from pyrpod.rpod import ( # noqa: E402 + JetFiringHistory, + PlumeStrikeEstimationStudy, +) +from pyrpod.vehicle import TargetVehicle, VisitingVehicle # noqa: E402 + +CASE_DIR = '../case/plume/plume_cylinder_sweep/' +N_ANGLES = 19 +N_RADII = 5 +N_FACES = 14036 # data/stl/cylinder.stl (high-res target mesh) + + +class CylinderSweepSmoke(unittest.TestCase): + + def test_cylinder_sweep_runs(self): + # 1. Set up the case (minimal driver, as in rpod_integration_test_07); + # the case config already selects the cylinder STL + sweep JFH. + jfh = JetFiringHistory.JetFiringHistory(CASE_DIR) + jfh.read_jfh() + + tv = TargetVehicle.TargetVehicle(CASE_DIR) + tv.set_stl() + + vv = VisitingVehicle.VisitingVehicle(CASE_DIR) + vv.set_thruster_config() + vv.set_thruster_metrics() + + me = MissionEnvironment.MissionEnvironment(CASE_DIR) + + study = PlumeStrikeEstimationStudy.PlumeStrikeEstimationStudy(me) + study.study_init(jfh, tv, vv) + + # 2. Geometry sanity: the 95-firing sweep on the cylinder mesh. + n_firings = len(jfh.JFH) + self.assertEqual(n_firings, N_ANGLES * N_RADII, + msg=f'unexpected sweep length {n_firings}; regenerate ' + 'with case/plume/plume_cylinder_sweep/jfh/' + 'generate_cylinder_sweep_jfh.py') + n_faces = len(tv.mesh.vectors) + self.assertEqual(n_faces, N_FACES, + msg=f'unexpected target mesh ({n_faces} faces); ' + 'expected the cylinder_transformed.stl target') + + # 3. Execute the full pipeline end-to-end. + t0 = time.perf_counter() + firing_data = study.jfh_plume_strikes() + dt = time.perf_counter() - t0 + + # 4. Every firing produced well-formed per-face results. + self.assertEqual(len(firing_data), n_firings) + total_strikes = 0 + for key, cell in firing_data.items(): + strikes = np.asarray(cell['strikes']) + self.assertEqual(strikes.shape, (n_faces,), + msg=f'firing {key}: bad strikes shape') + # strikes are a 0/1 membership mask + self.assertTrue(np.all((strikes == 0) | (strikes == 1)), + msg=f'firing {key}: strikes not binary') + # cumulative strikes never fall below this firing's own strikes + cum = np.asarray(cell['cum_strikes']) + self.assertTrue(np.all(cum >= strikes), + msg=f'firing {key}: cum_strikes < strikes') + total_strikes += int(strikes.sum()) + + # 5. The sweep must actually illuminate the cylinder somewhere + # (a geometry/aiming regression would strike zero faces everywhere). + self.assertGreater(total_strikes, 0, + msg='no faces struck across the whole sweep -- ' + 'the orbit never illuminates the cylinder') + + peak = max(int(np.asarray(c['strikes']).sum()) + for c in firing_data.values()) + print(f'[cylinder-sweep] {n_firings} firings x {n_faces} faces ran in ' + f'{dt:.2f} s; {total_strikes} total face-strikes, peak ' + f'{peak}/{n_faces} struck in a single firing') + + +if __name__ == '__main__': + unittest.main()