"""Isolated, pre-registered 24-interval validation; never imported by pytest.""" import csv import hashlib import importlib.util import json import struct from pathlib import Path from unittest.mock import patch import numpy as np ROOT = Path('/home/frank14f/CelerisLab') OUT = Path(__file__).resolve().parent spec = importlib.util.spec_from_file_location('accepted_hooks', ROOT/'tests/validation/multigrid/test_fine_surface_gpu.py') h = importlib.util.module_from_spec(spec) spec.loader.exec_module(h) EPS = np.finfo(np.float32).eps M = h.MOMENTS def save(name, obj): text = json.dumps(obj, indent=2, allow_nan=False)+'\n' assert len(text) <= 40000, (name, len(text)) (OUT/name).write_text(text) def table(name, rows): with (OUT/name).open('w') as file: writer = csv.DictWriter(file, fieldnames=list(rows[0])) writer.writeheader() writer.writerows(rows) assert (OUT/name).stat().st_size <= 40000 def vec(row, prefix, values): row.update({prefix+'_'+axis: float(value) for axis, value in zip(('m', 'px', 'py'), values)}) def equilibrium(values): a = values.astype(float) rho = a.sum(axis=0) u = M[1:]@a/rho cu = h.CX[:, None]*u[0]+h.CY[:, None]*u[1] eq = h.W[:, None]*rho*(1+3*cu+4.5*cu*cu-1.5*(u*u).sum(axis=0)) return eq, u, rho def pull(a): return np.stack([np.roll(a[q], (h.CY[q], h.CX[q]), (0, 1)) for q in range(9)]) def collision(incoming, output, omega, area): """Local observed drift; no access to global initial/final states or ledger.""" a, b = incoming.astype(float), output.astype(float) eq, u, rho = equilibrium(a) ideal = (1-omega)*a+omega*eq tolerance = 8*EPS*abs(a) error = abs(b-ideal) # Independent local FP64 oracle plus a measured (not inferred) drift. return dict(drift=area*(M@(b-a).sum(axis=1)), ideal_defect=area*(M@(ideal-a).sum(axis=1)), measured_rounding=area*(M@(b-ideal).sum(axis=1)), max_error=float(error.max()), max_ratio=float((error/tolerance).max()), regime=bool(np.isfinite(b).all() and np.all(a>0) and np.all(b>0) and np.all((rho>.9)&(rho<1.1)) and np.max(np.linalg.norm(u, axis=0))<.05)) def coarse_arrivals(before, fine, active, p): """Independent midpoint map; FP32 addition order checked against actual map.""" got = pull(before) ideal = got.astype(float) targets, sources = [], [] for y, x in np.argwhere(active): for q in range(1, 9): if 4 <= x+.5-h.CX[q]/2 <= 20 and 4 <= y+.5-h.CY[q]/2 <= 20: a, b = 4+2*(x-4), 4+2*(y-4) children = fine[q, b:b+2, a:a+2].ravel() assert len(children) == 4 and np.isfinite(children).all() got[q, y, x] = np.float32(.25)*(((children[0]+children[1])+children[2])+children[3]) ideal[q, y, x] = children.astype(float).sum()/4 targets.append(q*576+y*24+x) sources.append([q*1600+(b+dy)*40+a+dx for dy in (0, 1) for dx in (0, 1)]) order = np.argsort(targets) porder = np.argsort(p.coalesce_coarse) np.testing.assert_array_equal(np.array(targets)[order], p.coalesce_coarse[porder]) np.testing.assert_array_equal(np.array(sources)[order], p.coalesce_fine[porder]) assert np.isfinite(got[:, active]).all() flux = (got[:, active].astype(float)-before[:, active]).sum(axis=1) rounding = (got[:, active].astype(float)-ideal[:, active]).sum(axis=1) children = fine.ravel()[p.coalesce_fine] bound = 3*(EPS/2)/(1-3*(EPS/2))*np.abs(children.astype(float)).sum(axis=1)/4 differences = abs(got.ravel()[p.coalesce_coarse]-ideal.ravel()[p.coalesce_coarse]) return got, M@flux, M@rounding, float(np.max(differences/bound)) def observation(values, mask): eq, u, _ = equilibrium(values[:, mask]) return u, values[:, mask].astype(float)-eq def run(speed, label): cfg = h.LBMConfig(nx=24, ny=24, viscosity=.05, velocity=0, rho=1) cmask = np.ones((24, 24), bool) cmask[4:20, 4:20] = False source = h.object_spec(speed) state = vars(source.state).copy() ledger_rows, collision_rows, boundary_rows = [], [], [] trace = [] with h.CudaContext(0) as context: with h.implementation.FixedPatchStepper(cfg, h.BOUNDS, context=context, fine_objects=[source]) as backend, context.activate(): c, f = backend._levels cl, p = backend._fine_curved, backend.patch geometry = {name: getattr(cl, name).copy() for name in h.GEOMETRY} flags = backend.fine_boundary.flags.copy().reshape(40, 40) solid = (flags & h.SOLID) != 0 active = backend._fine_leaf.copy() fine_mesh = p.fine_kind == 0 band = active & np.logical_or.reduce([~np.roll(fine_mesh, (cy, cx), (0, 1)) for cx, cy in zip(h.CX[1:], h.CY[1:])]) interface = np.zeros(576, bool) interface[p.coalesce_coarse % 576] = True interface = interface.reshape(24, 24) bulk = cmask & ~interface if speed == 0: def coords(mask, dx, origin): yx = np.argwhere(mask) return ((yx[:, ::-1]+.5)*dx+np.array(origin)).tolist() save('regions.json', dict(coarse_interface=coords(interface, 1, (0, 0)), coarse_bulk=coords(bulk, 1, (0, 0)), fine_band=coords(band, .5, (2, 2)), transfer_slots=[dict(q=int(slot//576), x=float(slot%24+.5), y=float((slot%576)//24+.5)) for slot in p.coalesce_coarse])) backend.initialize(h.rest((24, 24)), h.rest((40, 40))) initial = backend.download() initial_m = M@(initial[0][:, cmask].sum(axis=1, dtype=float) +.25*initial[1][:, active].sum(axis=1, dtype=float)) previous = initial_m.copy() totals = {name: np.zeros(3) for name in ('boundary', 'fine_transfer', 'coarse_transfer', 'rounding', 'average')} interval = None orig_b, orig_f, orig_c = backend._curved, f.kernel, c.kernel omegas = [] for level, tau in ((c, p.tau_coarse), (f, p.tau_fine)): pointer, size = level.module.get_global('d_params') assert size == 20 omega, fx, fy, fz, count = struct.unpack('=ffffI', h.read(pointer, (size,), np.uint8).tobytes()) assert omega == float(np.float32(1/tau)) and (fx, fy, fz) == (0., 0., 0.) omegas.append(omega) omega_c, omega_f = omegas def curved(*args, **kwargs): backend._stream.synchronize() before = h.read(args[0], f.shape) expected = h.reflection_oracle(before, cl, speed) passive = h.reflection_oracle(before, cl, None, verify_projection=False) orig_b(*args, **kwargs) backend._stream.synchronize() staged = h.read(args[0], f.shape) d, k = cl.dir.astype(int), cl.fluid_idx.astype(int) values = staged.reshape(9, -1)[h.OPP[d], k+h.CX[d]+40*h.CY[d]] np.testing.assert_array_equal(staged[:, active], before[:, active]) b, tr, _ = h.fine_flux(before, staged, active, solid) exchange = h.read(cl.link_exchange_gpu, (cl.count, 3)).astype(float).sum(axis=0) row = dict(step=interval['step'], substep=len(interval['boundary'])+1, time=interval['step']-1+.5*(len(interval['boundary'])+1), bfl_error=float(abs(values-expected).max()), drive_mass=.25*float((values-passive).sum()), latest_body_impulse_x=float(.25*exchange[0]), latest_body_impulse_y=float(.25*exchange[1]), latest_body_angular_impulse=float(.125*exchange[2])) vec(row, 'boundary', .25*(M@b)) vec(row, 'fine_transfer', .25*(M@tr)) interval['boundary'].append(.25*(M@b)) interval['fine_transfer'].append(.25*(M@tr)) boundary_rows.append(row) def fine(*args, **kwargs): backend._stream.synchronize() staged = h.read(args[0], f.shape) incoming = pull(staged)[:, active] orig_f(*args, **kwargs) backend._stream.synchronize() after = h.read(args[1], f.shape) assert np.isfinite(after[:, active]).all() and np.isnan(after[:, solid]).all() outer = np.ones((40, 40), bool) outer[2:-2, 2:-2] = False assert np.isnan(after[:, outer]).all() # Inner ghost is stream-only: compare including legitimate NaNs. ghost = ~fine_mesh & ~outer np.testing.assert_array_equal(after[:, ghost], pull(staged)[:, ghost]) stat = collision(incoming, after[:, active], omega_f, .25) interval['collision'].append(stat) row = dict(step=interval['step'], level='fine', substep=len(interval['collision']), max_error=stat['max_error'], max_ratio=stat['max_ratio'], regime=stat['regime']) for key in ('drift', 'ideal_defect', 'measured_rounding'): vec(row, key, stat[key]) collision_rows.append(row) def coarse(*args, **kwargs): backend._stream.synchronize() before, fine = h.read(args[0], c.shape), h.read(args[2], f.shape) incoming, flux, avg, ratio = coarse_arrivals(before, fine, cmask, p) interval['coarse_transfer'], interval['average'] = flux, avg interval['average_ratio'] = ratio eq, _, _ = equilibrium(incoming[:, cmask]) neq = np.full(c.shape, np.nan) neq[:, cmask] = incoming[:, cmask].astype(float)-eq interval['transfer_neq'] = neq.ravel()[p.coalesce_coarse].copy() orig_c(*args, **kwargs) backend._stream.synchronize() after = h.read(args[1], c.shape) assert np.isfinite(after[:, cmask]).all() and np.isnan(after[:, ~cmask]).all() stat = collision(incoming[:, cmask], after[:, cmask], omega_c, 1.) interval['collision'].append(stat) row = dict(step=interval['step'], level='coarse', substep=1, max_error=stat['max_error'], max_ratio=stat['max_ratio'], regime=stat['regime']) for key in ('drift', 'ideal_defect', 'measured_rounding'): vec(row, key, stat[key]) collision_rows.append(row) with patch.object(backend, '_curved', curved), patch.object(f, 'kernel', fine), patch.object(c, 'kernel', coarse): for n in range(1, 25): interval = dict(step=n, boundary=[], fine_transfer=[], collision=[]) backend.step(1) coarse_g, fine_g = backend.download() assert np.isfinite(coarse_g[:, cmask]).all() and np.isnan(coarse_g[:, ~cmask]).all() assert np.isfinite(fine_g[:, active]).all() and np.isnan(fine_g[:, ~active]).all() assert len(interval['boundary']) == 2 and len(interval['collision']) == 3 current = M@(coarse_g[:, cmask].sum(axis=1, dtype=float) +.25*fine_g[:, active].sum(axis=1, dtype=float)) b = sum(interval['boundary']) ft = sum(interval['fine_transfer']) ct = interval['coarse_transfer'] drift = sum(s['drift'] for s in interval['collision']) raw = current-previous-b-ft-ct row = dict(step=n, time=n, average_bound_ratio=interval['average_ratio']) for key, value in (('leaf', current), ('delta', current-previous), ('boundary', b), ('fine_transfer', ft), ('coarse_transfer', ct), ('raw', raw), ('collision_drift', drift), ('average_rounding', interval['average']), ('closure', raw-drift)): vec(row, key, value) for key, val in (('boundary', b), ('fine_transfer', ft), ('coarse_transfer', ct), ('rounding', drift), ('average', interval['average'])): totals[key] += val cumulative = current-initial_m-totals['boundary']-totals['fine_transfer']-totals['coarse_transfer'] vec(row, 'cumulative_raw', cumulative) vec(row, 'cumulative_closure', cumulative-totals['rounding']) ledger_rows.append(row) iu, ine = observation(coarse_g, interface) bu, _ = observation(coarse_g, bulk) fu, fne = observation(fine_g, band) trace.append(dict(coarse_interface_u=iu, coarse_bulk_u=bu, fine_band_neq=fne, transfer_neq=interval['transfer_neq'])) previous = current print(label, n, 'raw', raw.tolist(), 'cumulative_raw', cumulative.tolist(), flush=True) assert backend.step_count == 24 and len(boundary_rows) == 48 and len(collision_rows) == 72 for name, val in geometry.items(): np.testing.assert_array_equal(getattr(cl, name), val) np.testing.assert_array_equal(flags.ravel(), backend.fine_boundary.flags) assert vars(source.state) == state and source.geometry.center == source.center table(label+'_ledger.csv', ledger_rows) table(label+'_collisions.csv', collision_rows) table(label+'_boundary.csv', boundary_rows) # Small compressed diagnostic arrays, not full-field population snapshots. np.savez_compressed(OUT/(label+'_signals.npz'), **{key: np.stack([t[key] for t in trace]) for key in trace[0]}) final = ledger_rows[-1] checks = dict(validity=True, geometry_unchanged=True, boundary_count=len(boundary_rows), collision_count=len(collision_rows), local_collision=max(r['max_ratio'] for r in collision_rows)<=1, local_regime=all(r['regime'] for r in collision_rows), bfl=max(r['bfl_error'] for r in boundary_rows)<=1e-7, transfer_rounding=max(r['average_bound_ratio'] for r in ledger_rows)<=1, interval_closure=max(abs(r['closure_'+k]) for r in ledger_rows for k in ('m','px','py'))<=1e-10, cumulative_closure=max(abs(r['cumulative_closure_'+k]) for r in ledger_rows for k in ('m','px','py'))<=1e-9) return trace, dict(speed=speed, initial_leaf=initial_m.tolist(), final_leaf=previous.tolist(), raw_residual=[final['cumulative_raw_'+k] for k in ('m','px','py')], exact_average_residual=(np.array([final['cumulative_raw_'+k] for k in ('m','px','py')])+totals['average']).tolist(), cumulative={k: v.tolist() for k,v in totals.items()}, checks=checks, max_collision_ratio=max(r['max_ratio'] for r in collision_rows), max_collision_error=max(r['max_error'] for r in collision_rows), max_bfl_error=max(r['bfl_error'] for r in boundary_rows), max_interval_closure=max(abs(r['closure_'+k]) for r in ledger_rows for k in ('m','px','py')), drive_mass=sum(r['drive_mass'] for r in boundary_rows)) def compare(driven, zero, label): regions = json.loads((OUT/'regions.json').read_text()) summaries, rows = {}, [] for key in driven[0]: velocity = key.endswith('_u') floor = 1e-6 if velocity else 1e-7 first, peak = None, None for n, (d, z) in enumerate(zip(driven, zero), 1): difference = d[key]-z[key] if velocity: signal = np.linalg.norm(difference, axis=0) base = np.linalg.norm(z[key], axis=0) absolute = np.linalg.norm(d[key], axis=0) elif difference.ndim == 2: signal, base, absolute = abs(difference).max(axis=0), abs(z[key]).max(axis=0), abs(d[key]).max(axis=0) else: signal, base, absolute = abs(difference), abs(z[key]), abs(d[key]) eligible = (signal>floor)&(signal>10*base) idx = int(np.argmax(signal)) def location(i): region = key.removesuffix('_u').removesuffix('_neq') if key == 'transfer_neq': return regions['transfer_slots'][i] return dict(zip(('x','y'), regions[region][i])) record = dict(metric=key, time=n, signal=float(signal[idx]), baseline=float(base[idx]), absolute=float(absolute[idx]), any_crossed=bool(eligible.any()), peak_crossed=bool(eligible[idx]), **location(idx)) rows.append(record) if peak is None or record['signal']>peak['signal']: peak = record.copy() if first is None and eligible.any(): i = int(np.argmax(np.where(eligible, signal, -1))) first = dict(time=n, signal=float(signal[i]), baseline=float(base[i]), absolute=float(absolute[i]), **location(i)) summaries[key] = dict(threshold=floor, baseline_factor=10, first_arrival=first, peak=peak) # q is present only for transfer populations; normalize CSV columns. for row in rows: row.setdefault('q', '') table(label+'_propagation.csv', rows) return summaries def main(): assert Path(h.CelerisLab.__file__).resolve() == ROOT/'src/CelerisLab/__init__.py' assert Path(h.compiler._KERNEL_DIR).resolve() == ROOT/'src/CelerisLab/lbm/kernels' frozen = json.loads((OUT/'frozen.json').read_text()) for path, digest in frozen['hashes'].items(): assert hashlib.sha256((ROOT/path).read_bytes()).hexdigest() == digest, path print('SOURCE', h.CelerisLab.__file__, 'CRITERIA', frozen['frozen_utc'], flush=True) traces, summaries = {}, {} for label, speed in (('zero', 0.), ('blow', .002), ('suction', -.002)): traces[label], summaries[label] = run(speed, label) save(label+'_summary.json', summaries[label]) for label in ('blow','suction'): summaries[label]['propagation'] = compare(traces[label], traces['zero'], label) summaries[label]['raw_minus_zero'] = (np.array(summaries[label]['raw_residual'])-summaries['zero']['raw_residual']).tolist() summaries[label]['checks']['propagation'] = all(s['first_arrival'] is not None for s in summaries[label]['propagation'].values()) summaries[label]['checks']['drive_sign'] = summaries[label]['speed']*summaries[label]['drive_mass']>0 save(label+'_summary.json', summaries[label]) passed = all(all(v for k,v in s['checks'].items() if k not in ('boundary_count','collision_count')) for s in summaries.values()) save('result.json', dict(passed=passed, runs=summaries)) print('RESULT', json.dumps(dict(passed=passed, summaries=summaries)), flush=True) raise SystemExit(0 if passed else 1) if __name__ == '__main__': main()