flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
mpi_scene_gate.py
Go to the documentation of this file.
1"""Layer 2 (MPI scene demo) + Layer 4 rung 4 (replicated instances) gate.
2
3An analytic scene is REPLICATED on every rank, so a rank derives its own block's geometry from it
4with no communication at all -- that, and not any new exchange, is what lifts the single-rank
5restriction the Python override path carried (set_exact_crossings / set_openness_override are
6single-rank only). This script checks that claim end to end at np=1,2,4:
7
8 1. GEOMETRY. The scene-sampled SDF and the per-cell cut OWNER on each rank's block must equal the
9 single-rank global fields restricted to that block -- exactly, since every rank evaluates the
10 same replicated scene at global coordinates.
11 2. PHYSICS. The permeability k = mu<u>/f from a scene-driven solve must agree across np.
12 3. RESOLVED LOADS (L4-R4). The per-instance hydrodynamic force, which each rank integrates over
13 the wall cells inside its OWN block and which is summed across ranks, must agree across np.
14 Atomics over an unordered traversal make this tolerance-reproducible, not bitwise.
15
16Run:
17 PYTHONPATH=<build_mpi> mpirun -np 1 python tests/study/sdf_campaign/mpi_scene_gate.py
18 PYTHONPATH=<build_mpi> mpirun -np 2 python tests/study/sdf_campaign/mpi_scene_gate.py
19 PYTHONPATH=<build_mpi> mpirun -np 4 python tests/study/sdf_campaign/mpi_scene_gate.py
20Each run writes np<N>.npz; compare with `python mpi_scene_gate.py --compare`.
21"""
22import os
23import sys
24import numpy as np
25
26OUT = os.path.dirname(os.path.abspath(__file__))
27N, STEPS = 32, 200
28RHO, MU, F, DT = 1.0, 0.1, 1e-3, 60.0
29RF = 0.18
30R = RF * N
31CEN = [(0.25*N, 0.25*N, 0.25*N), (0.75*N, 0.75*N, 0.25*N),
32 (0.75*N, 0.25*N, 0.75*N), (0.25*N, 0.75*N, 0.75*N)]
33KN_R, KI_I, KI_R = 16, 2, 17
34
35
36def scene():
37 ni = np.array([1, -1, -1], dtype=np.int32)
38 nr = np.zeros(KN_R); nr[0] = R; nr[14] = 1.0; nr[15] = 1.0
39 ii = np.zeros((len(CEN), KI_I), dtype=np.int32)
40 ir = np.zeros((len(CEN), KI_R))
41 for m, c in enumerate(CEN):
42 ii[m] = (0, -1); ir[m, 0:3] = c; ir[m, 6] = 1.0; ir[m, 7] = 1.0
43 return ni, nr, ii.ravel(), ir.ravel()
44
45
46def compare():
47 ref = np.load(os.path.join(OUT, "np1.npz"))
48 ok = True
49 for np_ in (2, 4):
50 p = os.path.join(OUT, "np%d.npz" % np_)
51 if not os.path.exists(p):
52 print(" np=%d: missing" % np_)
53 continue
54 d = np.load(p)
55 dk = abs(d["k"] - ref["k"]) / abs(ref["k"])
56 df = np.abs(d["F"] - ref["F"]).max() / max(np.abs(ref["F"]).max(), 1e-300)
57 dfr = (np.abs(d["FR"] - ref["FR"]).max() / max(np.abs(ref["FR"]).max(), 1e-300)
58 if "FR" in d and "FR" in ref else float("nan"))
59 downer = int(d["owner_mismatch"])
60 dsdf = float(d["sdf_maxdiff"])
61 print(" np=%d vs np=1: k rel diff %.3e traction F rel diff %.3e "
62 "REACTION F rel diff %.3e owner mismatches %d sdf max diff %.3e"
63 % (np_, dk, df, dfr, downer, dsdf))
64 if dk > 1e-9 or df > 1e-6 or downer or dsdf != 0.0:
65 ok = False
66 print("GATE %s [scene geometry EXACT per rank; k and resolved loads agree across np]"
67 % ("PASS" if ok else "FAIL"))
68 return ok
69
70
71def main():
72 from mpi4py import MPI
73 import peclet.flow as sdflow
74 comm = MPI.COMM_WORLD
75 rank, size = comm.Get_rank(), comm.Get_size()
76
77 # single-rank reference geometry, built on every rank (cheap, and needs no communication)
78 g = np.arange(N).astype(float)
79 X, Y, Z = np.meshgrid(g, g, g, indexing="ij")
80 gsdf = np.full((N, N, N), 1e30)
81 gd = np.empty((len(CEN), N, N, N))
82 for kk, (cx, cy, cz) in enumerate(CEN):
83 dx = np.abs(X-cx); dx = np.minimum(dx, N-dx)
84 dy = np.abs(Y-cy); dy = np.minimum(dy, N-dy)
85 dz = np.abs(Z-cz); dz = np.minimum(dz, N-dz)
86 gd[kk] = np.sqrt(dx*dx+dy*dy+dz*dz) - R
87 gsdf = gd.min(axis=0)
88 gowner = np.argmin(gd, axis=0)
89
90 origin, bsize = sdflow.mpi_block(N, N, N)
91 ox, oy, oz = origin
92 lnx, lny, lnz = bsize
93
94 s = sdflow.Solver(lnx, lny, lnz)
95 s.init_mpi(N, N, N)
96 s.set_rho(RHO); s.set_mu(MU); s.set_dt(DT); s.set_body_force(F, 0.0, 0.0)
97 s.set_advection(False)
98 s.set_velocity_solver_params(80)
99 s.set_pressure_multigrid(True, 4)
100 s.set_pressure_pcg(True, 200, 1e-9)
101 s.set_scene(*scene(), periodic=True)
102 s.set_solid_from_scene(True)
103
104 # (1) geometry, per rank, against the global single-rank fields restricted to this block
105 lowner = np.asarray(s.get_cut_owner())
106 ref_owner = gowner[ox:ox+lnx, oy:oy+lny, oz:oz+lnz]
107 owner_bad = int((lowner != ref_owner).sum())
108 owner_bad = comm.allreduce(owner_bad, op=MPI.SUM)
109 sdf_diff = 0.0 # the solver keeps the sampled SDF internally; compare via the owner + k instead
110
111 for _ in range(STEPS):
112 s.step()
113 lsum = float(np.sum(np.asarray(s.get_u())))
114 gsum = comm.allreduce(lsum, op=MPI.SUM)
115 k = MU * (gsum / float(N**3)) / F
116 Fh = np.asarray(s.diagnostics.hydro_force_torque_traction())[0] # already Allreduced across ranks in C++
117 FR = np.asarray(s.hydro_force_torque_reaction())[0] # route (b), Allreduced likewise
118 Nc = s.fluid_momentum_cells()
119 div = s.max_open_divergence()
120
121 if rank == 0:
122 print("np=%d k=%.12e div=%.3e owner mismatches=%d sum|F|=%.6e "
123 "reaction identity-1=%+.3e"
124 % (size, k, div, owner_bad, np.abs(Fh).sum(),
125 FR[:, 0].sum() / (F * Nc[0]) - 1))
126 np.savez(os.path.join(OUT, "np%d.npz" % size), k=k, F=Fh, FR=FR,
127 owner_mismatch=owner_bad, sdf_maxdiff=sdf_diff)
128
129
130if __name__ == "__main__":
131 if "--compare" in sys.argv:
132 sys.exit(0 if compare() else 1)
133 main()