232 lines
10 KiB
Python
232 lines
10 KiB
Python
"""One-interval, collision-free population-basis analysis; NOT an LBM solver.
|
|
|
|
No production imports/maps, CUDA, collision, force, or time-stepping benchmark.
|
|
The hypotheses below extend the current geometric transfer to a wall crossing;
|
|
they are not claims about the shipped backend or any external implementation.
|
|
Run: python research/wall_seam_pulse_20261014/analyze.py
|
|
"""
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
import csv
|
|
import hashlib
|
|
import json
|
|
import resource
|
|
|
|
resource.setrlimit(resource.RLIMIT_FSIZE, (2*1024**2, 2*1024**2))
|
|
OUT = Path(__file__).resolve().parent
|
|
ROOT = OUT.parents[1]
|
|
N, NF = 16, 24
|
|
LO, HI, HALO = 4, 12, 4
|
|
CX = (0, 1, -1, 0, 0, 1, -1, 1, -1)
|
|
CY = (0, 0, 0, 1, -1, 1, -1, -1, 1)
|
|
OPP = (0, 2, 1, 4, 3, 6, 5, 8, 7)
|
|
|
|
|
|
def covered(x, y):
|
|
return LO <= x < HI and LO <= y < HI
|
|
|
|
|
|
def coarse_fluid(x, y):
|
|
return 0 <= x < N and 8 <= y < 15
|
|
|
|
|
|
def fine_fluid(x, y):
|
|
return 0 <= x < NF and 0 <= y < NF and 8 < 2+(y+.5)/2 < 15
|
|
|
|
|
|
def active(x, y):
|
|
return HALO <= x < NF-HALO and HALO <= y < NF-HALO and fine_fluid(x, y)
|
|
|
|
|
|
def parent(x, y):
|
|
return LO+(x-HALO)//2, LO+(y-HALO)//2
|
|
|
|
|
|
def fine_targets():
|
|
return [(x, y) for y in range(2, NF-2) for x in range(2, NF-2) if fine_fluid(x, y)]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Packet:
|
|
phase: str
|
|
slot: tuple # (q, x, y)
|
|
value: float
|
|
operation: str
|
|
parents: tuple = () # (Packet, coefficient); includes zero-valued inputs
|
|
|
|
|
|
def derive(phase, slot, operation, parents):
|
|
return Packet(phase, slot, sum(p.value*w for p, w in parents), operation, tuple(parents))
|
|
|
|
|
|
def initialize(pulse):
|
|
level, q0, x0, y0 = pulse
|
|
coarse = {(q, x, y): Packet('Gc0', (q, x, y), float(('coarse', q, x, y) == pulse), 'input')
|
|
for y in range(N) for x in range(N) if coarse_fluid(x, y) and not covered(x, y)
|
|
for q in range(9)}
|
|
fine = {}
|
|
for y in range(NF):
|
|
for x in range(NF):
|
|
# Full fine footprint: a shared parent and its four children must
|
|
# agree, including outer halo and solid cells (which hold no data).
|
|
px, py = parent(x, y)
|
|
if fine_fluid(x, y) != coarse_fluid(px, py):
|
|
raise AssertionError('parent/child physical classifications disagree')
|
|
if not fine_fluid(x, y):
|
|
continue
|
|
for q in range(9):
|
|
key = q, x, y
|
|
if active(x, y):
|
|
fine[key] = Packet('Gf0', key, float(('fine', q, x, y) == pulse), 'input')
|
|
else:
|
|
fine[key] = derive('Gf0', key, 'explode', [(coarse[q, px, py], 1)])
|
|
if level == 'coarse':
|
|
assert sum(p.value for p in coarse.values()) == 1
|
|
else:
|
|
assert active(x0, y0) and sum(p.value for p in fine.values() if p.operation == 'input') == 1
|
|
return coarse, fine
|
|
|
|
|
|
def fine_hop(inp, hop, prestream=False):
|
|
phase = ('pre_' if prestream else '')+f'F{hop}'
|
|
staged = dict(inp)
|
|
if prestream:
|
|
written = set()
|
|
# Independent prestream form: write the missing solid-source slots.
|
|
# Each (q,source) has exactly one fluid destination in a pull scheme.
|
|
for x, y in fine_targets():
|
|
for q in range(9):
|
|
sx, sy = x-CX[q], y-CY[q]
|
|
if not fine_fluid(sx, sy) and (OPP[q], x, y) in inp:
|
|
key = q, sx, sy
|
|
assert key not in written
|
|
written.add(key)
|
|
staged[key] = derive(phase+'_BCscratch', key, 'prestream_BB', [(inp[OPP[q], x, y], 1)])
|
|
out = {}
|
|
for x, y in fine_targets():
|
|
for q in range(9):
|
|
key, source = (q, x, y), (q, x-CX[q], y-CY[q])
|
|
if prestream or fine_fluid(source[1], source[2]):
|
|
donor, op = staged.get(source), 'pull'
|
|
else:
|
|
donor, op = inp.get((OPP[q], x, y)), 'local_BB'
|
|
if donor is not None:
|
|
assert key not in out
|
|
out[key] = derive(phase, key, op, [(donor, 1)])
|
|
# Missing slots are explicitly invalid, not zero; outer halo is not output.
|
|
return out
|
|
|
|
|
|
def links():
|
|
result = {}
|
|
for y in range(N):
|
|
for x in range(N):
|
|
if not coarse_fluid(x, y) or covered(x, y):
|
|
continue
|
|
for q in range(1, 9):
|
|
if LO <= x+.5-CX[q]/2 <= HI and LO <= y+.5-CY[q]/2 <= HI:
|
|
a, b = HALO+2*(x-LO), HALO+2*(y-LO)
|
|
result[q, x, y] = [(q, a+dx, b+dy) for dy in (0, 1) for dx in (0, 1)]
|
|
return result
|
|
|
|
|
|
LINKS = links()
|
|
HYPOTHESES = ('coarse_BB_then_coalesce', 'coalesce_then_coarse_BB', 'omit_solid_source_links')
|
|
|
|
|
|
def coarse_arrivals(gc, f2, hypothesis):
|
|
out = {}
|
|
for q, x, y in gc:
|
|
key = q, x, y
|
|
sx, sy = (x-CX[q]) % N, (y-CY[q]) % N
|
|
wall = not coarse_fluid(sx, sy)
|
|
transfer = key in LINKS
|
|
if hypothesis == 'omit_solid_source_links' and wall:
|
|
transfer = False
|
|
# Functional final assignment models the specified ordered overrides.
|
|
# Every destination has one final writer; this alone does not prove
|
|
# that one initial packet has only one physical owner globally.
|
|
if transfer and (not wall or hypothesis == 'coarse_BB_then_coalesce'):
|
|
parents = [(f2[child], .25) for child in LINKS[key]]
|
|
node = derive('Fc1', key, 'coalescence', parents)
|
|
elif wall:
|
|
node = derive('Fc1', key, 'coarse_BB', [(gc[OPP[q], x, y], 1)])
|
|
else:
|
|
node = derive('Fc1', key, 'coarse_pull', [(gc[q, sx, sy], 1)])
|
|
assert key not in out
|
|
out[key] = node
|
|
return out
|
|
|
|
|
|
def trace(p, contribution=1.):
|
|
return dict(phase=p.phase, slot=list(p.slot), operation=p.operation,
|
|
value=p.value, downstream_weight=contribution,
|
|
parents=[trace(parent, contribution*w) for parent, w in p.parents if parent.value])
|
|
|
|
|
|
def main():
|
|
cases = [('outer_coarse_q7', ('coarse', 7, 2, 8)),
|
|
('adjacent_coarse_q7', ('coarse', 7, 3, 8)),
|
|
('active_fine_q7', ('fine', 7, 4, 12))]
|
|
rows, results = [], []
|
|
for name, pulse in cases:
|
|
gc, gf = initialize(pulse)
|
|
f1 = fine_hop(gf, 1)
|
|
f2 = fine_hop(f1, 2)
|
|
pre1 = fine_hop(gf, 1, prestream=True)
|
|
pre2 = fine_hop(pre1, 2, prestream=True)
|
|
assert {k: p.value for k, p in f1.items()} == {k: p.value for k, p in pre1.items()}
|
|
assert {k: p.value for k, p in f2.items()} == {k: p.value for k, p in pre2.items()}
|
|
assert all(child in f2 for children in LINKS.values() for child in children)
|
|
initial = 1. if pulse[0] == 'coarse' else .25
|
|
fine_mass = .25*sum(p.value for (q, x, y), p in f2.items() if active(x, y))
|
|
for phase in (gc, gf, f1, f2):
|
|
for node in phase.values():
|
|
if node.value:
|
|
rows.append((name, 'shared', node.phase, *node.slot, node.operation, node.value,
|
|
';'.join(f'{p.phase}:{p.slot}*{w}' for p, w in node.parents)))
|
|
for hypothesis in HYPOTHESES:
|
|
out = coarse_arrivals(gc, f2, hypothesis)
|
|
coarse_mass = sum(p.value for p in out.values())
|
|
selected = out[8, 3, 8]
|
|
item = dict(case=name, pulse=pulse, hypothesis=hypothesis,
|
|
initial_leaf_weight=initial, coarse_leaf_weight=coarse_mass,
|
|
fine_leaf_weight=fine_mass, total_leaf_weight=coarse_mass+fine_mass,
|
|
residual=coarse_mass+fine_mass-initial,
|
|
inspected_slot=[8, 3, 8], selected_writer=selected.operation,
|
|
inspected_value=selected.value,
|
|
final_nonzero_traces=[trace(p) for p in out.values() if p.value])
|
|
results.append(item)
|
|
for node in out.values():
|
|
if node.value:
|
|
rows.append((name, hypothesis, node.phase, *node.slot, node.operation, node.value,
|
|
';'.join(f'{p.phase}:{p.slot}*{w}' for p, w in node.parents)))
|
|
print(f'{name:20s} {hypothesis:27s} initial={initial:g} coarse={coarse_mass:g} '
|
|
f'fine={fine_mass:g} residual={item["residual"]:+g}; '
|
|
f'K(3,8),q8={selected.value:g} ({selected.operation})')
|
|
sources = ['src/CelerisLab/lbm/refinement.py', 'src/CelerisLab/lbm/refinement_stepper.py',
|
|
'src/CelerisLab/lbm/kernels/refinement.cu',
|
|
'src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh']
|
|
report = dict(scope='Independent single-interval linear packet/basis analysis; no collision or solver execution',
|
|
geometry=dict(coarse=[16, 16], patch=[4, 4, 12, 12], fine=[24, 24],
|
|
fine_origin=[2, 2], coarse_dx_dt=[1, 1], fine_dx_dt=[.5, .5], walls_Y=[8, 15]),
|
|
assumptions=['fluid active populations pass unchanged between fine hops (identity transfer probe)',
|
|
'fine fluid inner ghosts reflect, never collide; outer halo output is invalid',
|
|
'solid destinations omitted; no filtering by coarse source in geometric LINKS',
|
|
'coalescence is equal-weight average of four children of each coarse target',
|
|
'results test these proposed extensions, not current production support or external libraries'],
|
|
prestream_vs_postpull='Exact values and validity match on both hops for all three probes',
|
|
source_files_read_only={p: hashlib.sha256((ROOT/p).read_bytes()).hexdigest() for p in sources},
|
|
script_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), results=results)
|
|
(OUT/'results.json').write_text(json.dumps(report, indent=2)+'\n')
|
|
with (OUT/'sources.csv').open('w', newline='') as stream:
|
|
writer = csv.writer(stream)
|
|
writer.writerow(('case', 'hypothesis', 'phase', 'q', 'x', 'y', 'operation', 'value', 'immediate_sources'))
|
|
writer.writerows(rows)
|
|
print('Prestream/postpull equivalence, valid coalescence reads, and unique destination writes: PASS')
|
|
print(f'peak_rss_MiB={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024:.3f}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|