flow 0.4.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
exact_apertures_spheres.py
Go to the documentation of this file.
1#!/usr/bin/env python
2"""EXACT analytic face apertures for sphere packings -> peclet.flow set_openness_override.
3
4The in-solver aperture estimators (set_aperture_order 1/2) are limited to the trilinearly
5sampled SDF field; for ANALYTIC geometry this module computes machine-precision apertures
6(closed-form disk-rectangle overlap, validated to 4e-16) and returns the three staggered
7openness fields (ox[i] = -x face of cell i, x-fastest [x,y,z] arrays).
8
9Provenance: the aperture-bias investigation (flow doc/collocated_paper_plan.md row 51) -- the
10one-sample linear model carries a signed convexity bias (+0.59%/+0.27% bed permeability at
11R=8/12, decaying ~h^2); exact apertures remove it. Multi-sphere faces are handled by clamped
12subtraction (a slight over-closing where solid disks of different spheres overlap on one face
13-- rare, counted, and conservative). A sub-resolution floor (1e-6) is REQUIRED: alpha ~ 1e-12
14rows destroy the pressure-operator conditioning (measured: GPU watchdog-level solve stalls).
15For general (non-sphere) analytic SDFs the same interface should be fed by a high-order
16implicit quadrature (R. Saye, SIAM J. Sci. Comput. 37(2), 2015) -- not implemented here.
17
18Usage:
19 from exact_apertures_spheres import exact_openness
20 ox, oy, oz = exact_openness(N, centers_cells, radii_cells) # cubic N, periodic
21 s.set_openness_override(ox.ravel(order="F"), oy.ravel(order="F"), oz.ravel(order="F"))
22"""
23import numpy as np
24
25
26def Fquad(x, y, r):
27 """Vectorized area of {u<x, v<y, u^2+v^2<r^2}, disk at origin."""
28 x = np.clip(x, -r, r)
29 yc = np.clip(y, -r, r)
30
31 def G(t):
32 t = np.clip(t, -r, r)
33 return 0.5 * (t * np.sqrt(np.maximum(r * r - t * t, 0.0)) + r * r * np.arcsin(t / r))
34
35 us = np.sqrt(np.maximum(r * r - yc * yc, 0.0))
36 # middle band u in [max(-r,-us), min(x, us)]: contribute y + s(u)
37 a_m = np.maximum(-r, -us)
38 b_m = np.minimum(x, us)
39 mid = np.where(b_m > a_m, yc * (b_m - a_m) + (G(b_m) - G(a_m)), 0.0)
40 # outer bands (|u| > us): full 2s where y > 0, zero where y < 0
41 a1, b1 = -r + 0.0 * x, np.minimum(x, -us) # left band
42 left = np.where(b1 > a1, 2.0 * (G(b1) - G(a1)), 0.0)
43 a2, b2 = us, np.maximum(x, us) # right band (only if x > us)
44 right = np.where(b2 > a2, 2.0 * (G(b2) - G(a2)), 0.0)
45 outer = np.where(y >= 0, left + right, 0.0)
46 F = mid + outer
47 F = np.where(y >= r, 2.0 * (G(x) - G(-r)), F)
48 F = np.where(y <= -r, 0.0, F)
49 return F
50
51
52def disk_cell_areas(cy, cz, rho, j0, j1, k0, k1):
53 """Vectorized exact disk-cell overlap areas on cells [j,j+1]x[k,k+1], j in [j0,j1), k in [k0,k1)."""
54 j = np.arange(j0, j1)
55 k = np.arange(k0, k1)
56 X0 = j[:, None] - cy
57 X1 = X0 + 1.0
58 Y0 = k[None, :] - cz
59 Y1 = Y0 + 1.0
60 return (Fquad(X1, Y1, rho) - Fquad(X0, Y1, rho) - Fquad(X1, Y0, rho) + Fquad(X0, Y0, rho))
61
62
63def exact_openness(N, C, Rr):
64 """o[a][i,j,k]: openness of the -a face of cell (i,j,k); union over spheres by subtraction
65 with clamping (multi-sphere faces are rare; counted and reported)."""
66 o = [np.ones((N, N, N)) for _ in range(3)]
67 multi = 0
68 for sh in np.stack(np.meshgrid(*[[-1., 0., 1.]] * 3, indexing="ij"), -1).reshape(-1, 3):
69 cs = C + sh * N
70 keep = np.all((cs + (Rr + 2)[:, None] > 0) & (cs - (Rr + 2)[:, None] < N + 1), axis=1)
71 for (scx, scy, scz), rr in zip(cs[keep], Rr[keep]):
72 cen = (scx, scy, scz)
73 for a in range(3):
74 t1, t2 = (a + 1) % 3, (a + 2) % 3
75 p0 = max(int(np.ceil(cen[a] - rr)), 0)
76 p1 = min(int(np.floor(cen[a] + rr)), N - 1)
77 for p in range(p0, p1 + 1):
78 d = p - cen[a]
79 rho2 = rr * rr - d * d
80 if rho2 <= 0:
81 continue
82 rho = np.sqrt(rho2)
83 j0 = max(int(np.floor(cen[t1] - rho)), 0)
84 j1 = min(int(np.ceil(cen[t1] + rho)), N)
85 k0 = max(int(np.floor(cen[t2] - rho)), 0)
86 k1 = min(int(np.ceil(cen[t2] + rho)), N)
87 if j0 >= j1 or k0 >= k1:
88 continue
89 A = disk_cell_areas(cen[t1], cen[t2], rho, j0, j1, k0, k1)
90 idx = [slice(None)] * 3
91 idx[a] = p
92 idx[t1] = slice(j0, j1)
93 idx[t2] = slice(k0, k1)
94 view = o[a][tuple(idx)]
95 # view axes = the remaining array axes in ascending order; A is (t1, t2)
96 Ax = A.T if t1 > t2 else A
97 multi += int(((view < 1.0) & (Ax > 1e-12)).sum())
98 np.subtract(view, Ax, out=view)
99 np.clip(view, 0.0, 1.0, out=view)
100 print(f" exact_openness: multi-sphere faces (union approximated): {multi}", flush=True)
101 for a in range(3): # floor: sub-1e-6 apertures cannot carry resolved flux; keep rows sane
102 o[a][o[a] < 1e-6] = 0.0
103 return o
104
105
disk_cell_areas(cy, cz, rho, j0, j1, k0, k1)
static constexpr int G