Add jet, surface and fixed-patch tests and runnable examples
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
"""Low-Re startup flow in an x-periodic confined domain; nozzle remains coarse.
|
||||
|
||||
From this checkout, after checking the selected physical GPU is idle:
|
||||
PYTHONPATH="$PWD/src" CUDA_VISIBLE_DEVICES=0 python examples/fixed_patch_jet.py --output /tmp/patch-jet
|
||||
|
||||
The fixed-amplitude start includes acoustic transients, not a developed jet.
|
||||
Only native leaf cells are plotted. No interpolation, restart or flow oracle.
|
||||
Scale estimates use the continuous peak A=.02, not inlet mean speed: W/A=600,
|
||||
H/A=2250, W**2/nu=1440, H**2/nu=20250, where H=NY-3=45 between on-site planes.
|
||||
An acoustic crossing (~78 steps) does not imply that transients have disappeared.
|
||||
Mach is sampled over all leaves every 50 steps and is usually inlet dominated;
|
||||
the discrete inlet peak is slightly below continuous A. This is not an accuracy test.
|
||||
The 120s checks are cooperative, not an exact interruption deadline. main elapsed
|
||||
includes construction, advancement and plotting; it is not pure CFD timing.
|
||||
"""
|
||||
import argparse
|
||||
from dataclasses import replace
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
import CelerisLab
|
||||
from CelerisLab.config import load_lbm_config
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
from CelerisLab.lbm.equilibrium import compute_feq_d2q9, compute_macro_from_ddf
|
||||
from CelerisLab.lbm.refinement import ACTIVE
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
|
||||
BOUNDS, AMPLITUDE, WIDTH = (16, 4, 32, 28), .02, 12
|
||||
TIMES, MAX_MACH, MAX_SECONDS = (0, 300, 1200), .1, 120
|
||||
|
||||
|
||||
def measure(arrays, masks, centers, initial_mass):
|
||||
fields = []
|
||||
for array, mask in zip(arrays, masks):
|
||||
if not np.isfinite(array[:, mask]).all():
|
||||
raise RuntimeError("Nonfinite leaf populations")
|
||||
field = np.full((*mask.shape, 3), np.nan)
|
||||
field[mask] = [compute_macro_from_ddf(f) for f in array[:, mask].T]
|
||||
fields.append(field)
|
||||
leaves = np.concatenate([f[m] for f, m in zip(fields, masks)])
|
||||
speed = np.hypot(leaves[:, 1], leaves[:, 2])
|
||||
mass = sum(area*a[:, m].sum(dtype=np.float64)
|
||||
for area, a, m in zip((1, .25), arrays, masks))
|
||||
record = dict(finite=bool(np.isfinite(leaves).all()), rho_min=float(leaves[:, 0].min()),
|
||||
rho_max=float(leaves[:, 0].max()), speed_max=float(speed.max()),
|
||||
sampled_max_Mach=float(speed.max()*np.sqrt(3)),
|
||||
density_deviation=float(np.abs(leaves[:, 0]-1).max()),
|
||||
discrete_leaf_mass=float(mass), mass_change=float(mass-initial_mass))
|
||||
if not record['finite'] or record['rho_min'] <= 0 or record['sampled_max_Mach'] > MAX_MACH:
|
||||
raise RuntimeError(f"Finite/positive-density/Ma<={MAX_MACH} guard failed: {record}")
|
||||
walls = fields[0][[1, -2]]
|
||||
record['macro_sum_rho_v'] = dict(zip(('Q_in', 'Q_out'),
|
||||
(walls[:, :, 0]*walls[:, :, 2]).sum(axis=1).tolist()))
|
||||
record['centerline'] = []
|
||||
for level, y in ((1, 16.25), (0, 32.5)):
|
||||
xs, ys = centers[level]
|
||||
columns = np.argsort(np.abs(xs-24))[:2]
|
||||
row = int(np.flatnonzero(ys == y)[0])
|
||||
values = fields[level][row, columns].mean(axis=0)
|
||||
record['centerline'].append(dict(level=level, x=24, y=y,
|
||||
sampled_x=sorted(xs[columns].tolist()), rule='arithmetic mean of two neighboring native columns',
|
||||
rho_ux_uy=values.tolist()))
|
||||
return fields, record
|
||||
|
||||
|
||||
def draw(out, snapshots, masks, centers):
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.collections import PolyCollection
|
||||
|
||||
def decorate(ax):
|
||||
ax.set(xlim=(0, 48), ylim=(1.5, 46.5), aspect='equal', xlabel='X', ylabel='Y')
|
||||
ax.plot([16, 32, 32, 16, 16], [4, 4, 28, 28, 4], color='tab:red', lw=1)
|
||||
ax.axhline(1.5, color='black', lw=1)
|
||||
ax.axhline(46.5, color='black', lw=1, ls='--')
|
||||
ax.plot([18, 30], [1.5, 1.5], color='tab:orange', lw=4, clip_on=False)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(6, 6), constrained_layout=True)
|
||||
for mask, (xs, ys), dx in zip(masks, centers, (1, .5)):
|
||||
yy, xx = np.meshgrid(ys, xs, indexing='ij')
|
||||
cells = np.stack((xx[mask], yy[mask]), axis=-1)
|
||||
vertices = cells[:, None, :] + dx*np.array([[-.5, -.5], [.5, -.5], [.5, .5], [-.5, .5]])
|
||||
ax.add_collection(PolyCollection(vertices, facecolors='none', edgecolors='0.55', linewidths=.3))
|
||||
decorate(ax)
|
||||
ax.set_title('Native leaf grid: coarse dx=1, fine dx=0.5\n'
|
||||
'Red: internal patch; orange: coarse slot; dashed: reservoir\n'
|
||||
'Flow may cross patch sides; patch does not contain all startup flow', fontsize=10)
|
||||
fig.savefig(out/'layout.png', dpi=150, bbox_inches='tight', pad_inches=.15)
|
||||
plt.close(fig)
|
||||
vmax = max(1., *(np.nanmax(np.hypot(f[..., 1], f[..., 2]))/AMPLITUDE
|
||||
for fields in snapshots for f in fields))
|
||||
fig, axes = plt.subplots(1, 3, figsize=(12, 4.8), constrained_layout=True)
|
||||
for ax, step, fields in zip(axes, TIMES, snapshots):
|
||||
for field, mask, (xs, ys), dx in zip(fields, masks, centers, (1, .5)):
|
||||
speed = np.ma.masked_where(~mask, np.hypot(field[..., 1], field[..., 2])/AMPLITUDE)
|
||||
image = ax.pcolormesh(np.r_[xs-dx/2, xs[-1]+dx/2],
|
||||
np.r_[ys-dx/2, ys[-1]+dx/2], speed,
|
||||
shading='flat', cmap='viridis', vmin=0, vmax=vmax, rasterized=True)
|
||||
stride = int(4/dx)
|
||||
yy, xx = np.meshgrid(ys, xs, indexing='ij')
|
||||
selection = np.zeros_like(mask)
|
||||
selection[::stride, ::stride] = mask[::stride, ::stride]
|
||||
ax.quiver(xx[selection], yy[selection], field[..., 1][selection], field[..., 2][selection],
|
||||
angles='xy', scale_units='xy', scale=AMPLITUDE/3, width=.005, color='white')
|
||||
decorate(ax)
|
||||
ax.set_title(f'coarse t={step}, fine steps={2*step}')
|
||||
fig.colorbar(image, ax=axes, label='speed / imposed amplitude')
|
||||
fig.suptitle('Low-Re startup flow in an x-periodic confined domain\n'
|
||||
'Fixed-amplitude start includes acoustic transients; not steady/developed\n'
|
||||
'Nozzle remains coarse; flow may cross patch sides (not fully enclosed)', fontsize=11)
|
||||
fig.savefig(out/'velocity.png', dpi=150)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--output', type=Path, required=True)
|
||||
out = parser.parse_args().output.resolve()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
if Path(CelerisLab.__file__).resolve() != root/'src/CelerisLab/__init__.py':
|
||||
raise RuntimeError(f'Set PYTHONPATH to {root / "src"}')
|
||||
out.mkdir(parents=True, exist_ok=False)
|
||||
cfg = replace(load_lbm_config(str(root/'src/CelerisLab/configs/config_lbm.json')),
|
||||
nx=48, ny=48, viscosity=.1, velocity=0., rho=1., wall_slot_width=WIDTH,
|
||||
wall_normal_velocity=AMPLITUDE, y_wall_bc='on_site_velocity', top_wall_bc='reservoir',
|
||||
lattice_model='D2Q9', collision='SRT', streaming='double_buffer',
|
||||
store_precision='FP32', ddf_shifting=False, les_enabled=False)
|
||||
report = dict(config=dict(nx=48, ny=48, bounds=BOUNDS, W=WIDTH, A=AMPLITUDE, nu=.1,
|
||||
rho=1, tau_coarse=.8, tau_fine=1.1, coarse_boundary='slot_reservoir'),
|
||||
guards=dict(check_every=50, positive_density=True, finite=True,
|
||||
max_Mach=MAX_MACH, max_seconds=MAX_SECONDS,
|
||||
timing='cooperative checks, not an exact interruption deadline'),
|
||||
scale_estimates=dict(velocity_basis='continuous peak A, not inlet mean', H=cfg.ny-3,
|
||||
W_over_A=WIDTH/AMPLITUDE, H_over_A=(cfg.ny-3)/AMPLITUDE,
|
||||
W_squared_over_nu=WIDTH**2/cfg.viscosity,
|
||||
H_squared_over_nu=(cfg.ny-3)**2/cfg.viscosity,
|
||||
acoustic_crossing=(cfg.ny-3)*np.sqrt(3)),
|
||||
notes='Node mass uses areas 1 and 1/4, not an exact physical volume integral. '
|
||||
'Q_in/Q_out are bottom/top macro sum(rho*vy), not a closed population budget. '
|
||||
'Mach maxima are sampled every 50 steps, not every-step peaks. '
|
||||
'One acoustic crossing does not imply transient decay; no steady-state claim.',
|
||||
samples=[], status='running', last_synchronized_coarse_step=0)
|
||||
start, run = time.monotonic(), None
|
||||
try:
|
||||
with CudaContext(0) as context, FixedPatchStepper(cfg, BOUNDS, context=context,
|
||||
coarse_boundary='slot_reservoir') as run:
|
||||
p = run.patch
|
||||
masks = [np.ones(p.coarse_shape, bool), p.fine_kind == ACTIVE]
|
||||
masks[0][[0, -1]] = False
|
||||
masks[0][4:28, 16:32] = False
|
||||
centers = [(origin[0]+dx*(np.arange(shape[1])+.5),
|
||||
origin[1]+dx*(np.arange(shape[0])+.5))
|
||||
for shape, origin, dx in ((p.coarse_shape, (0, 0), 1),
|
||||
(p.fine_shape, p.fine_origin, .5))]
|
||||
initial = [np.broadcast_to(compute_feq_d2q9(1., 0., 0.)[:, None, None], (9, *m.shape)).copy()
|
||||
for m in masks]
|
||||
for x in range(cfg.nx):
|
||||
v = AMPLITUDE*max(0., 1-((2*x-(cfg.nx-1))/WIDTH)**2)**2
|
||||
initial[0][:, 1, x] = compute_feq_d2q9(1., 0., v)
|
||||
run.initialize(*initial)
|
||||
mass0 = sum(area*a[:, m].sum(dtype=np.float64)
|
||||
for area, a, m in zip((1, .25), initial, masks))
|
||||
snapshots = []
|
||||
for step in range(0, TIMES[-1]+1, 50):
|
||||
if time.monotonic()-start > MAX_SECONDS:
|
||||
raise TimeoutError('120 second demonstration limit reached')
|
||||
if step:
|
||||
run.step(50)
|
||||
report['last_synchronized_coarse_step'] = run.step_count
|
||||
if time.monotonic()-start > MAX_SECONDS:
|
||||
raise TimeoutError('120 second demonstration limit reached after chunk')
|
||||
fields, sample = measure(run.download(), masks, centers, mass0)
|
||||
sample.update(t=step, coarse_steps=step, fine_steps=2*step)
|
||||
report['samples'].append(sample)
|
||||
print(json.dumps(sample), flush=True)
|
||||
if step in TIMES:
|
||||
snapshots.append(fields)
|
||||
with tempfile.TemporaryDirectory(prefix='.plot-', dir=out) as cache:
|
||||
os.environ['MPLCONFIGDIR'] = cache
|
||||
draw(out, snapshots, masks, centers)
|
||||
if time.monotonic()-start > MAX_SECONDS:
|
||||
raise TimeoutError('120 second demonstration limit reached after plotting')
|
||||
report['status'] = 'complete'
|
||||
except BaseException as exc:
|
||||
report.update(status='failed', error=f'{type(exc).__name__}: {exc}',
|
||||
last_synchronized_coarse_step=run.step_count if run is not None else 0)
|
||||
raise
|
||||
finally:
|
||||
report['elapsed_seconds'] = time.monotonic()-start
|
||||
report['elapsed_scope'] = 'main construction, advancement and plotting; not pure CFD time'
|
||||
(out/'summary.json').write_text(json.dumps(report, indent=2)+'\n')
|
||||
print(f'{report["status"]}: last synchronized coarse step '
|
||||
f'{report["last_synchronized_coarse_step"]}; output={out}', flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Public API demo: small periodic shear, one fixed 2:1 patch, eight coarse steps.
|
||||
|
||||
From this checkout, after checking the selected physical GPU is idle:
|
||||
PYTHONPATH="$PWD/src" CUDA_VISIBLE_DEVICES=0 python examples/fixed_periodic_patch.py
|
||||
|
||||
This demonstrates initialization/stepping/leaf output, not interface accuracy.
|
||||
There are no walls, objects, forces, checkpoint files or plots.
|
||||
"""
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
import CelerisLab
|
||||
from CelerisLab.config import load_lbm_config
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
from CelerisLab.lbm.equilibrium import compute_feq_d2q9, compute_macro_from_ddf
|
||||
from CelerisLab.lbm.refinement import ACTIVE
|
||||
from CelerisLab.lbm.refinement_stepper import PeriodicPatchStepper
|
||||
|
||||
|
||||
def shear_equilibrium(shape, origin, dx, period):
|
||||
"""Sample ux(y)=1e-3*sin(2*pi*y/period) at physical cell centers.
|
||||
|
||||
Centers are origin + dx*(index+.5); this shear is independent of x.
|
||||
Fine origin includes patch offset AND the four-cell halo, with dx=.5.
|
||||
Equilibrium is already postcollision G, so no initial collision is needed.
|
||||
"""
|
||||
y = origin[1] + dx * (np.arange(shape[0]) + .5)
|
||||
ux = 1e-3 * np.sin(2*np.pi*y/period)
|
||||
rows = np.stack([compute_feq_d2q9(1.0, float(u), 0.0) for u in ux], axis=1)
|
||||
return np.repeat(rows[:, :, None], shape[1], axis=2)
|
||||
|
||||
|
||||
def leaf_summary(coarse, fine, coarse_leaf, fine_leaf):
|
||||
"""Use the same leaf masks/areas for initial and final mass."""
|
||||
c, f = coarse[:, coarse_leaf], fine[:, fine_leaf]
|
||||
mass = c.sum(dtype=np.float64) + .25*f.sum(dtype=np.float64)
|
||||
macros = np.array([compute_macro_from_ddf(v) for v in np.concatenate((c, f), axis=1).T])
|
||||
return mass, macros.min(axis=0), macros.max(axis=0)
|
||||
|
||||
|
||||
def main():
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
if Path(CelerisLab.__file__).resolve() != root / "src/CelerisLab/__init__.py":
|
||||
raise RuntimeError(f"Set PYTHONPATH to {root / 'src'}")
|
||||
cfg = replace(load_lbm_config(str(root / "src/CelerisLab/configs/config_lbm.json")),
|
||||
nx=16, ny=16, viscosity=.1, velocity=1e-3, rho=1.0,
|
||||
lattice_model="D2Q9", collision="SRT", streaming="double_buffer",
|
||||
store_precision="FP32", ddf_shifting=False, les_enabled=False)
|
||||
with CudaContext(0) as context, PeriodicPatchStepper(cfg, (4, 4, 12, 12), context=context) as run:
|
||||
p = run.patch
|
||||
coarse = shear_equilibrium(p.coarse_shape, (0, 0), 1.0, cfg.ny)
|
||||
fine = shear_equilibrium(p.fine_shape, p.fine_origin, .5, cfg.ny)
|
||||
coarse_leaf = np.ones(p.coarse_shape, dtype=bool)
|
||||
x0, y0, x1, y1 = p.bounds
|
||||
coarse_leaf[y0:y1, x0:x1] = False
|
||||
fine_leaf = p.fine_kind == ACTIVE
|
||||
coarse[:, ~coarse_leaf] = np.nan
|
||||
fine[:, ~fine_leaf] = np.nan
|
||||
run.initialize(coarse, fine)
|
||||
initial_mass, _, _ = leaf_summary(*run.download(), coarse_leaf, fine_leaf)
|
||||
run.step(8)
|
||||
coarse, fine = run.download() # Both levels synchronized, stored postcollision G.
|
||||
assert np.isnan(coarse[:, ~coarse_leaf]).all() and np.isnan(fine[:, ~fine_leaf]).all()
|
||||
assert np.isfinite(coarse[:, coarse_leaf]).all() and np.isfinite(fine[:, fine_leaf]).all()
|
||||
mass, low, high = leaf_summary(coarse, fine, coarse_leaf, fine_leaf)
|
||||
print(f"periodic patch API demo: synchronized coarse_steps={run.step_count}, fine_steps={2*run.step_count}")
|
||||
print(f"leaf mass: initial={initial_mass:.12g} final={mass:.12g} drift={mass-initial_mass:+.6e}")
|
||||
for name, lo, hi in zip(("rho", "ux", "uy"), low, high):
|
||||
print(f"leaf {name}: [{lo:.9g}, {hi:.9g}]")
|
||||
print("covered coarse / fine halo: NaN; functional demonstration only")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user