|
| 1 | +""" |
| 2 | +Testing Sod tube with GSPH |
| 3 | +========================== |
| 4 | +
|
| 5 | +CI test for Sod tube with GSPH using M4 kernel and HLLC Riemann solver. |
| 6 | +Uses piecewise constant reconstruction (first-order, stable). |
| 7 | +Computes L2 error against analytical solution and checks for regression. |
| 8 | +""" |
| 9 | + |
| 10 | +import numpy as np |
| 11 | + |
| 12 | +import shamrock |
| 13 | + |
| 14 | +gamma = 1.4 |
| 15 | +rho_L, rho_R = 1.0, 0.125 |
| 16 | +P_L, P_R = 1.0, 0.1 |
| 17 | +fact = (rho_L / rho_R) ** (1.0 / 3.0) |
| 18 | +u_L = P_L / ((gamma - 1) * rho_L) |
| 19 | +u_R = P_R / ((gamma - 1) * rho_R) |
| 20 | +resol = 128 |
| 21 | + |
| 22 | +ctx = shamrock.Context() |
| 23 | +ctx.pdata_layout_new() |
| 24 | + |
| 25 | +model = shamrock.get_Model_GSPH(context=ctx, vector_type="f64_3", sph_kernel="M4") |
| 26 | +cfg = model.gen_default_config() |
| 27 | +cfg.set_riemann_hllc() |
| 28 | +cfg.set_reconstruct_piecewise_constant() |
| 29 | +cfg.set_boundary_periodic() |
| 30 | +cfg.set_eos_adiabatic(gamma) |
| 31 | +cfg.print_status() |
| 32 | +model.set_solver_config(cfg) |
| 33 | +model.init_scheduler(int(1e8), 1) |
| 34 | + |
| 35 | +(xs, ys, zs) = model.get_box_dim_fcc_3d(1, resol, 24, 24) |
| 36 | +dr = 1 / xs |
| 37 | +(xs, ys, zs) = model.get_box_dim_fcc_3d(dr, resol, 24, 24) |
| 38 | +model.resize_simulation_box((-xs, -ys / 2, -zs / 2), (xs, ys / 2, zs / 2)) |
| 39 | + |
| 40 | +model.add_cube_hcp_3d(dr, (-xs, -ys / 2, -zs / 2), (0, ys / 2, zs / 2)) |
| 41 | +model.add_cube_hcp_3d(dr * fact, (0, -ys / 2, -zs / 2), (xs, ys / 2, zs / 2)) |
| 42 | +model.set_field_in_box("uint", "f64", u_L, (-xs, -ys / 2, -zs / 2), (0, ys / 2, zs / 2)) |
| 43 | +model.set_field_in_box("uint", "f64", u_R, (0, -ys / 2, -zs / 2), (xs, ys / 2, zs / 2)) |
| 44 | + |
| 45 | +vol_b = xs * ys * zs |
| 46 | +totmass = (rho_R * vol_b) + (rho_L * vol_b) |
| 47 | +pmass = model.total_mass_to_part_mass(totmass) |
| 48 | +model.set_particle_mass(pmass) |
| 49 | +hfact = model.get_hfact() |
| 50 | + |
| 51 | +model.set_cfl_cour(0.1) |
| 52 | +model.set_cfl_force(0.1) |
| 53 | + |
| 54 | +t_target = 0.245 |
| 55 | +print(f"GSPH Sod Shock Tube Test (M4, HLLC, t={t_target})") |
| 56 | +model.evolve_until(t_target) |
| 57 | + |
| 58 | +sod = shamrock.phys.SodTube(gamma=gamma, rho_1=rho_L, P_1=P_L, rho_5=rho_R, P_5=P_R) |
| 59 | + |
| 60 | + |
| 61 | +def compute_L2_errors(ctx, sod, t, x_min, x_max): |
| 62 | + """Compute L2 errors using ctx.collect_data() (no pyvista dependency).""" |
| 63 | + data = ctx.collect_data() |
| 64 | + points = np.array(data["xyz"]) |
| 65 | + velocities = np.array(data["vxyz"]) |
| 66 | + hpart = np.array(data["hpart"]) |
| 67 | + uint = np.array(data["uint"]) |
| 68 | + |
| 69 | + rho_sim = pmass * (hfact / hpart) ** 3 |
| 70 | + P_sim = (gamma - 1) * rho_sim * uint |
| 71 | + |
| 72 | + x, vx, vy, vz = points[:, 0], velocities[:, 0], velocities[:, 1], velocities[:, 2] |
| 73 | + mask = (x >= x_min) & (x <= x_max) |
| 74 | + x_f, rho_f, vx_f, vy_f, vz_f, P_f = ( |
| 75 | + x[mask], |
| 76 | + rho_sim[mask], |
| 77 | + vx[mask], |
| 78 | + vy[mask], |
| 79 | + vz[mask], |
| 80 | + P_sim[mask], |
| 81 | + ) |
| 82 | + |
| 83 | + if len(x_f) == 0: |
| 84 | + raise RuntimeError("No particles in analysis region") |
| 85 | + |
| 86 | + rho_ana, vx_ana, P_ana = np.zeros(len(x_f)), np.zeros(len(x_f)), np.zeros(len(x_f)) |
| 87 | + for i, xi in enumerate(x_f): |
| 88 | + rho_ana[i], vx_ana[i], P_ana[i] = sod.get_value(t, xi) |
| 89 | + |
| 90 | + err_rho = np.sqrt(np.mean((rho_f - rho_ana) ** 2)) / np.mean(rho_ana) |
| 91 | + err_vx = np.sqrt(np.mean((vx_f - vx_ana) ** 2)) / (np.mean(np.abs(vx_ana)) + 0.1) |
| 92 | + err_vy = np.sqrt(np.mean(vy_f**2)) |
| 93 | + err_vz = np.sqrt(np.mean(vz_f**2)) |
| 94 | + err_P = np.sqrt(np.mean((P_f - P_ana) ** 2)) / np.mean(P_ana) |
| 95 | + return err_rho, (err_vx, err_vy, err_vz), err_P |
| 96 | + |
| 97 | + |
| 98 | +rho, v, P = compute_L2_errors(ctx, sod, t_target, -0.5, 0.5) |
| 99 | +vx, vy, vz = v |
| 100 | + |
| 101 | +print("current errors :") |
| 102 | +print(f"err_rho = {rho}") |
| 103 | +print(f"err_vx = {vx}") |
| 104 | +print(f"err_vy = {vy}") |
| 105 | +print(f"err_vz = {vz}") |
| 106 | +print(f"err_P = {P}") |
| 107 | + |
| 108 | +# Expected L2 error values (calibrated from CI run with M4 kernel) |
| 109 | +# Tolerance set very strict for regression testing (like sod_tube_sph.py) |
| 110 | +expect_rho = 0.03053641590859224 |
| 111 | +expect_vx = 0.10520433643658435 |
| 112 | +expect_vy = 0.0002224532508989796 |
| 113 | +expect_vz = 5.029288149671629e-05 |
| 114 | +expect_P = 0.037878823126488034 |
| 115 | + |
| 116 | +tol = 1e-8 |
| 117 | + |
| 118 | +test_pass = True |
| 119 | +err_log = "" |
| 120 | + |
| 121 | +error_checks = { |
| 122 | + "rho": (rho, expect_rho), |
| 123 | + "vx": (vx, expect_vx), |
| 124 | + "vy": (vy, expect_vy), |
| 125 | + "vz": (vz, expect_vz), |
| 126 | + "P": (P, expect_P), |
| 127 | +} |
| 128 | + |
| 129 | +for name, (value, expected) in error_checks.items(): |
| 130 | + if abs(value - expected) > tol * expected: |
| 131 | + err_log += f"error on {name} is outside of tolerances:\n" |
| 132 | + err_log += f" expected error = {expected} +- {tol*expected}\n" |
| 133 | + err_log += f" obtained error = {value} (relative error = {(value - expected)/expected})\n" |
| 134 | + test_pass = False |
| 135 | + |
| 136 | +if test_pass: |
| 137 | + print("\n" + "=" * 50) |
| 138 | + print("GSPH Sod Shock Tube Test: PASSED") |
| 139 | + print("=" * 50) |
| 140 | +else: |
| 141 | + exit("Test did not pass L2 margins : \n" + err_log) |
0 commit comments