mirror of
https://github.com/slsdetectorgroup/aare.git
synced 2026-09-03 16:00:43 +02:00
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).
This commit is contained in:
@@ -62,6 +62,14 @@ void define_ClusterFinder(py::module &m, const std::string &typestr) {
|
||||
*arr = self.noise();
|
||||
return return_image_data(arr);
|
||||
})
|
||||
// AARE_BRANCH_TRACE (diagnostic, removable)
|
||||
.def_property_readonly(
|
||||
"branch_map",
|
||||
[](ClusterFinder<ClusterType, uint16_t, pd_type> &self) {
|
||||
auto arr = new NDArray<uint8_t, 2>{};
|
||||
*arr = self.branch_map();
|
||||
return return_image_data(arr);
|
||||
})
|
||||
.def(
|
||||
"steal_clusters",
|
||||
[](ClusterFinder<ClusterType, uint16_t, pd_type> &self,
|
||||
|
||||
@@ -58,6 +58,14 @@ void define_ClusterFinderFrozen(py::module &m, const std::string &typestr) {
|
||||
*arr = self.noise();
|
||||
return return_image_data(arr);
|
||||
})
|
||||
// AARE_BRANCH_TRACE (diagnostic, removable)
|
||||
.def_property_readonly(
|
||||
"branch_map",
|
||||
[](ClusterFinderFrozen<ClusterType, uint16_t, pd_type> &self) {
|
||||
auto arr = new NDArray<uint8_t, 2>{};
|
||||
*arr = self.branch_map();
|
||||
return return_image_data(arr);
|
||||
})
|
||||
.def(
|
||||
"steal_clusters",
|
||||
[](ClusterFinderFrozen<ClusterType, uint16_t, pd_type> &self,
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,133 @@
|
||||
"""Dump one Test3 divergence site in full, for the annex figure.
|
||||
|
||||
Re-runs both finders up to the target frame and, at that frame, records the
|
||||
3x3 neighbourhood as each model saw it: raw ADU, the pedestal mean each model
|
||||
was reading, the resulting pedestal-subtracted values, and both test statistics
|
||||
against their thresholds.
|
||||
|
||||
The site is the first frame at which a Test3 flip creates a cluster in the
|
||||
frozen finder that the serial finder does not produce.
|
||||
|
||||
Writes branch_site.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
|
||||
sys.path.append('/home/ferjao_k/aare/build')
|
||||
sys.path.append('/home/ferjao_k/aare/python/tests')
|
||||
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
from aare import File, ClusterFinder, ClusterFinderFrozen
|
||||
|
||||
OUT = Path(__file__).resolve().parent
|
||||
BASE = Path('/mnt/sls_det_storage/moench_data/2603_MaxIVBeamtime/2026032408/'
|
||||
'process/xrf/')
|
||||
|
||||
N_PED, N_SIGMA = 1000, 5
|
||||
CLUSTER, IMG, CAP = (3, 3), (400, 400), 50_000
|
||||
TARGET_FRAME, TY, TX = 203, 125, 245 # from branch_trace.json
|
||||
C3 = np.sqrt(9.0) # sqrt(ClusterSizeX * ClusterSizeY)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
f.seek(0)
|
||||
data = f.read_n(TARGET_FRAME + 1)
|
||||
|
||||
# state entering the target frame
|
||||
for fid in range(TARGET_FRAME):
|
||||
cf_cpu.find_clusters(data[fid]); cf_cpu.steal_clusters(realloc_same_capacity=True)
|
||||
cf_frz.find_clusters(data[fid]); cf_frz.steal_clusters(realloc_same_capacity=True)
|
||||
|
||||
ped_cpu_before = np.asarray(cf_cpu.pedestal).copy()
|
||||
ped_frz_before = np.asarray(cf_frz.pedestal).copy()
|
||||
rms_cpu_before = np.asarray(cf_cpu.noise).copy()
|
||||
rms_frz_before = np.asarray(cf_frz.noise).copy()
|
||||
|
||||
# the frame itself
|
||||
cf_cpu.find_clusters(data[TARGET_FRAME])
|
||||
cf_frz.find_clusters(data[TARGET_FRAME])
|
||||
b_cpu = np.asarray(cf_cpu.branch_map)
|
||||
b_frz = np.asarray(cf_frz.branch_map)
|
||||
|
||||
sl = (slice(TY - 1, TY + 2), slice(TX - 1, TX + 2))
|
||||
raw = data[TARGET_FRAME][sl].astype(float)
|
||||
|
||||
# The frozen model reads its frame-start snapshot for the whole frame, so
|
||||
# ped_frz_before IS what it used. The serial model's 3-above/1-left neighbours
|
||||
# may already carry this frame's sample by the time (TY,TX) is tested, so its
|
||||
# effective pedestal is read AFTER the frame -- but only for pixels it pushed.
|
||||
ped_cpu_after = np.asarray(cf_cpu.pedestal).copy()
|
||||
|
||||
# raster order: the four already-scanned neighbours of (TY,TX)
|
||||
scanned = np.zeros((3, 3), dtype=bool)
|
||||
scanned[0, :] = True # the row above: (-1,-1) (-1,0) (-1,+1)
|
||||
scanned[1, 0] = True # and the pixel to the left
|
||||
|
||||
ped_cpu_used = np.where(scanned, ped_cpu_after[sl], ped_cpu_before[sl])
|
||||
ped_frz_used = ped_frz_before[sl]
|
||||
|
||||
val_cpu = raw - ped_cpu_used
|
||||
val_frz = raw - ped_frz_used
|
||||
rms = float(rms_frz_before[TY, TX])
|
||||
|
||||
rec = dict(
|
||||
frame=TARGET_FRAME, iy=TY, ix=TX, n_sigma=N_SIGMA, c3=float(C3),
|
||||
raw=raw.tolist(),
|
||||
ped_cpu=ped_cpu_used.tolist(), ped_frz=ped_frz_used.tolist(),
|
||||
val_cpu=val_cpu.tolist(), val_frz=val_frz.tolist(),
|
||||
scanned=scanned.tolist(),
|
||||
rms_cpu=float(rms_cpu_before[TY, TX]), rms_frz=rms,
|
||||
total_cpu=float(val_cpu.sum()), total_frz=float(val_frz.sum()),
|
||||
max_cpu=float(val_cpu.max()), max_frz=float(val_frz.max()),
|
||||
value_cpu=float(val_cpu[1, 1]), value_frz=float(val_frz[1, 1]),
|
||||
thr_test1_cpu=float(N_SIGMA * rms_cpu_before[TY, TX]),
|
||||
thr_test1_frz=float(N_SIGMA * rms),
|
||||
thr_test3_cpu=float(C3 * N_SIGMA * rms_cpu_before[TY, TX]),
|
||||
thr_test3_frz=float(C3 * N_SIGMA * rms),
|
||||
branch_cpu=int(b_cpu[TY, TX]), branch_frz=int(b_frz[TY, TX]),
|
||||
)
|
||||
# A wider patch for the context panel: what the raster was doing around the
|
||||
# site. Branch codes come from the SERIAL finder, since the left panel's job is
|
||||
# to show the scan in progress.
|
||||
R = 10
|
||||
py0, px0 = TY - R, TX - R
|
||||
pat = (slice(py0, TY + R + 1), slice(px0, TX + R + 1))
|
||||
rec["patch"] = dict(
|
||||
r=R, y0=int(py0), x0=int(px0),
|
||||
val=(data[TARGET_FRAME][pat].astype(float) - ped_frz_before[pat]).tolist(),
|
||||
branch_cpu=b_cpu[pat].astype(int).tolist(),
|
||||
branch_frz=b_frz[pat].astype(int).tolist(),
|
||||
)
|
||||
|
||||
json.dump(rec, open(OUT / 'branch_site.json', 'w'), indent=1)
|
||||
|
||||
print(f"frame {TARGET_FRAME}, pixel ({TY},{TX}) branch cpu={rec['branch_cpu']} "
|
||||
f"frozen={rec['branch_frz']} (4=QUIET_UPDATE, 3=TEST3_STORE)")
|
||||
print(f" rms {rms:.3f} Test1 thr {rec['thr_test1_frz']:.2f} "
|
||||
f"Test3 thr {rec['thr_test3_frz']:.2f}")
|
||||
print(f" serial : max {rec['max_cpu']:8.3f} total {rec['total_cpu']:8.3f} "
|
||||
f"-> Test3 {'PASS' if rec['total_cpu'] > rec['thr_test3_cpu'] else 'fail'}")
|
||||
print(f" frozen : max {rec['max_frz']:8.3f} total {rec['total_frz']:8.3f} "
|
||||
f"-> Test3 {'PASS' if rec['total_frz'] > rec['thr_test3_frz'] else 'fail'}")
|
||||
print(f" the four already-scanned neighbours differ by "
|
||||
f"{np.abs(ped_cpu_used - ped_frz_used)[scanned].sum():.4f} ADU total")
|
||||
print(f"wrote {OUT / 'branch_site.json'}")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,148 @@
|
||||
"""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"}')
|
||||
Reference in New Issue
Block a user