Files
aare/python/tests/branch_trace.py
T
kferjaoui a9ee1fc074
Build on RHEL8 / build (push) Successful in 3m19s
Build on RHEL9 / build (push) Successful in 3m38s
Run tests using data on local RHEL8 / build (push) Successful in 4m16s
deck: identify Test3 as the CPU/CPU divergence channel
Slide 30 reported an 8/11 cluster mismatch between ClusterFinder and
ClusterFinderFrozen without naming its cause. Two experiments now do.
Instrumented: 974 divergent pixels partition onto the three decisions with
no remainder -- Test1 619 flips / 0 clusters, Test3 347 / 11, local-max gate
8 / 8. Ablated: the 11 go to exactly zero. Corrects a claim the data refuted
-- Test1 initiates independently, ~7x slower, and never creates a cluster.

AARE_BRANCH_TRACE defaults to 0 and folds away at compile time, so the CPU
baseline the deck quotes is unaffected. AARE_TEST3_ENABLED defaults to 1.

New annex A7 (2 slides): part 1 restores the third test slide 4 omits and
derives c3 from variance addition; part 2 carries the measurement.

fig_test3 painted a completed branch map with a "scan is here" cursor over
it -- now the finished frame, arrows meaning raster order, shadow given its
own fill so a photon stops looking 3x4 wide, frozen in red so amber keeps
one meaning. fig_overlap_9x9: row spacing now clears the tag, not the lane.

New docs/deck/QA.md: the code and algorithm questions this raised, with
where each is settled. Also retired 3 figures placed on no slide; annex
count in report and README was stale (6/53, now 7/55).
2026-08-26 17:14:31 +02:00

149 lines
6.1 KiB
Python

"""Which test is responsible for the serial-vs-frozen disagreement?
ClusterFinder and ClusterFinderFrozen differ in exactly one thing: WHEN the
pedestal is pushed. `push_fast` touches only the pixel's own accumulators and
never reads the stencil, so the update itself is order-independent: given the
same set of updated pixels, both models end a frame with a bit-identical
pedestal. The only channel for divergence is therefore a differing DECISION.
There are three places a decision can differ, and they are not equally exposed:
Test1 max > nSigma*rms reads the stencil MAX.
local-max gate v == m compares two stencil values.
Test3 total > c3*nSigma*rms reads the stencil SUM, so it collects the
shift of every already-scanned neighbour --
three above and one left -- where Test1 feels
at most the one that happens to be the argmax.
WHAT THIS ACTUALLY FOUND (the prior stated here before running it was only half
right, so it is recorded as measured rather than as predicted):
* Every cluster frozen finds and serial does not -- 11 of them -- comes from
Test3. Compiling Test3 out sends that count to exactly zero.
* Every cluster serial finds and frozen does not -- 8 -- comes from the
local-max gate, downstream of accumulated pedestal drift.
* Test1's threshold flips MOST often, 619 of 974 divergent pixels, and yields
ZERO clusters: it only moves a pixel between QUIET_UPDATE and SHADOW, and
neither of those stores.
* Test1 is not merely downstream of Test3. With Test3 ablated the first
divergence is still a Test1 flip, on a frame where the two pedestals were
provably identical. It initiates on its own, just ~7x slower (frame 30
against frame 4), and cannot create a cluster by itself.
This does not ablate anything -- both finders run their shipped logic. Each
records a per-pixel branch code (see branch_map in either header) and we diff
the two maps frame by frame, which names the guilty test on the real algorithm
and localises it for the annex figure.
Writes branch_trace.json next to itself.
REQUIRES a build with branch tracing on, which is OFF by default so the
shipped path carries no per-pixel store:
sed -i 's/#define AARE_BRANCH_TRACE 0/#define AARE_BRANCH_TRACE 1/' \
include/aare/ClusterFinder.hpp include/aare/ClusterFinderFrozen.hpp
cmake --build build -j8
Set it back to 0 afterwards. Without it branch_map is all-5 and this
script reports no divergence at all.
"""
import sys, json, time
sys.path.append('/home/ferjao_k/aare/build')
sys.path.append('/home/ferjao_k/aare/python/tests')
from pathlib import Path
from collections import Counter
import numpy as np
from aare import File, ClusterFinder, ClusterFinderFrozen
from helper import centers, only_sets
OUT = Path(__file__).resolve().parent
BASE = Path('/mnt/sls_det_storage/moench_data/2603_MaxIVBeamtime/2026032408/'
'process/xrf/')
N_PED, N, N_SIGMA = 1000, 10000, 5
CLUSTER = (3, 3)
IMG = (400, 400)
CAP = 50_000
CODE = {0: "NEG", 1: "SHADOW", 2: "TEST1_STORE", 3: "TEST3_STORE",
6: "TEST3_SKIP", 4: "QUIET_UPDATE", 5: "UNTOUCHED"}
f = File(BASE / 'Cu_factor_10_data_master_0.json')
pd = File(BASE / 'Cu_factor_10_pedestal_master_0.json')
cf_cpu = ClusterFinder(IMG, CLUSTER, n_sigma=N_SIGMA, capacity=CAP)
cf_frz = ClusterFinderFrozen(IMG, CLUSTER, n_sigma=N_SIGMA, capacity=CAP)
t0 = time.perf_counter()
pd.seek(0)
for _ in range(N_PED):
img = pd.read_frame().copy()
cf_cpu.push_pedestal_frame(img)
cf_frz.push_pedestal_frame(img)
print(f'pedestal train: {time.perf_counter()-t0:.1f}s', flush=True)
f.seek(0)
data = f.read_n(N)
print('data:', data.shape, data.dtype, flush=True)
# (cpu_code, frozen_code) -> count, over every pixel where the two maps differ
transitions = Counter()
# the first frame at which the branch maps diverge at all
first_div = None
# per-frame centre-set difference, to reproduce the 8/11 headline
cpu_only_tot = frz_only_tot = 0
n_cpu = n_frz = 0
# every divergent pixel, kept for the figure (there should be very few)
sites = []
t0 = time.perf_counter()
for fid in range(N):
cf_cpu.find_clusters(data[fid])
cf_frz.find_clusters(data[fid])
b_cpu = np.asarray(cf_cpu.branch_map)
b_frz = np.asarray(cf_frz.branch_map)
cv_cpu = cf_cpu.steal_clusters(realloc_same_capacity=True)
cv_frz = cf_frz.steal_clusters(realloc_same_capacity=True)
c_cpu, c_frz = centers(cv_cpu), centers(cv_frz)
n_cpu += len(c_cpu); n_frz += len(c_frz)
a_only, b_only = only_sets(c_cpu, c_frz, tol=0)
cpu_only_tot += len(a_only); frz_only_tot += len(b_only)
diff = np.argwhere(b_cpu != b_frz)
if diff.size:
if first_div is None:
first_div = int(fid)
for iy, ix in diff:
pair = (int(b_cpu[iy, ix]), int(b_frz[iy, ix]))
transitions[pair] += 1
if len(sites) < 400:
sites.append(dict(frame=int(fid), iy=int(iy), ix=int(ix),
cpu=CODE[pair[0]], frozen=CODE[pair[1]]))
if (fid + 1) % 2000 == 0:
print(f' {fid+1}/{N} divergent pixels so far: '
f'{sum(transitions.values())}', flush=True)
dt = time.perf_counter() - t0
print(f'\nscan: {dt:.1f}s')
print(f'clusters: cpu {n_cpu} frozen {n_frz}')
print(f'centre-set difference (the 8/11 headline): '
f'cpu-only {cpu_only_tot} frozen-only {frz_only_tot}')
print(f'first frame with any branch divergence: {first_div}')
print(f'\ndivergent pixels, by (cpu branch -> frozen branch):')
for (a, b), n in transitions.most_common():
print(f' {CODE[a]:>12} -> {CODE[b]:<12} {n:>8}')
json.dump(dict(n_frames=N, n_ped=N_PED, n_sigma=N_SIGMA,
clusters=dict(cpu=n_cpu, frozen=n_frz),
centre_diff=dict(cpu_only=cpu_only_tot,
frozen_only=frz_only_tot),
first_divergent_frame=first_div,
transitions={f'{CODE[a]}->{CODE[b]}': n
for (a, b), n in transitions.items()},
sites=sites),
open(OUT / 'branch_trace.json', 'w'), indent=1)
print(f'\nwrote {OUT / "branch_trace.json"}')