mirror of
https://github.com/slsdetectorgroup/aare.git
synced 2026-09-03 17:20:43 +02:00
ClusterFinderFrozen: CPU finder for CUDA correctness study
- Diagnostic twin of ClusterFinder: identical decisions, but the pedestal is frozen per frame (snapshot at frame start, updates deferred to frame end), matching the CUDA kernel's update model. - Isolates pedestal-update timing as the sole remaining CPU/GPU mismatch. - Adds class + bindings + factory, validation notebook, and helper utilities.
This commit is contained in:
@@ -39,7 +39,21 @@ def ClusterFinder(image_size, cluster_size=(3,3), n_sigma=5, dtype = np.int32, c
|
||||
|
||||
|
||||
|
||||
def ClusterFinderMT(image_size, cluster_size = (3,3), dtype=np.int32, n_sigma=5, capacity = 1024, n_threads = 3):
|
||||
def ClusterFinderFrozen(image_size, cluster_size=(3,3), n_sigma=5, dtype=np.int32, capacity=1024):
|
||||
"""
|
||||
Factory function to create a ClusterFinderFrozen object.
|
||||
|
||||
Diagnostic twin of ClusterFinder: identical decision logic, but the pedestal
|
||||
is frozen per frame (every decision reads a start-of-frame snapshot, and all
|
||||
pedestal updates are deferred to the end of the frame). This mirrors the CUDA
|
||||
kernel's per-frame update model, so running it against ClusterFinderCUDA
|
||||
isolates pedestal-update timing as the sole variable.
|
||||
"""
|
||||
cls = _get_class("ClusterFinderFrozen", cluster_size, dtype)
|
||||
return cls(image_size, n_sigma=n_sigma, capacity=capacity)
|
||||
|
||||
|
||||
def ClusterFinderMT(image_size, cluster_size = (3,3), dtype=np.int32, n_sigma=5, capacity = 1024, n_threads = 3):
|
||||
"""
|
||||
Factory function to create a ClusterFinderMT object. Provides a cleaner syntax for
|
||||
the templated ClusterFinderMT in C++.
|
||||
|
||||
@@ -31,7 +31,7 @@ from ._aare import corner
|
||||
# from ._aare import ClusterFinderMT, ClusterCollector, ClusterFileSink, ClusterVector_i
|
||||
|
||||
from ._version import __version__
|
||||
from .ClusterFinder import ClusterFinder, ClusterCollector, ClusterFinderMT, ClusterFileSink, ClusterFile
|
||||
from .ClusterFinder import ClusterFinder, ClusterFinderFrozen, ClusterCollector, ClusterFinderMT, ClusterFileSink, ClusterFile
|
||||
from .ClusterFinder import ClusterFinderCUDA, ClusterFinderCUDAGraph, _cuda_available
|
||||
from .ClusterVector import ClusterVector
|
||||
from .Cluster import Cluster
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#include "aare/ClusterFinderFrozen.hpp"
|
||||
#include "aare/ClusterVector.hpp"
|
||||
#include "aare/NDView.hpp"
|
||||
#include "aare/Pedestal.hpp"
|
||||
#include "np_helper.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
#include <pybind11/stl_bind.h>
|
||||
|
||||
namespace py = pybind11;
|
||||
using pd_type = double;
|
||||
|
||||
using namespace aare;
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wunused-parameter"
|
||||
|
||||
template <typename T, uint8_t ClusterSizeX, uint8_t ClusterSizeY,
|
||||
typename CoordType = uint16_t>
|
||||
void define_ClusterFinderFrozen(py::module &m, const std::string &typestr) {
|
||||
auto class_name = fmt::format("ClusterFinderFrozen_{}", typestr);
|
||||
|
||||
using ClusterType = Cluster<T, ClusterSizeX, ClusterSizeY, CoordType>;
|
||||
|
||||
py::class_<ClusterFinderFrozen<ClusterType, uint16_t, pd_type>>(
|
||||
m, class_name.c_str())
|
||||
.def(py::init<Shape<2>, pd_type, size_t>(), py::arg("image_size"),
|
||||
py::arg("n_sigma") = 5.0, py::arg("capacity") = 1'000'000)
|
||||
|
||||
.def_property(
|
||||
"nSigma",
|
||||
&ClusterFinderFrozen<ClusterType, uint16_t, pd_type>::get_nSigma,
|
||||
&ClusterFinderFrozen<ClusterType, uint16_t, pd_type>::set_nSigma,
|
||||
R"(number of sigma above the pedestal to consider a photon during cluster finding.)")
|
||||
|
||||
.def("push_pedestal_frame",
|
||||
[](ClusterFinderFrozen<ClusterType, uint16_t, pd_type> &self,
|
||||
py::array_t<uint16_t> frame) {
|
||||
auto view = make_view_2d(frame);
|
||||
self.push_pedestal_frame(view);
|
||||
})
|
||||
.def("clear_pedestal", &ClusterFinderFrozen<ClusterType, uint16_t,
|
||||
pd_type>::clear_pedestal)
|
||||
.def_property_readonly(
|
||||
"pedestal",
|
||||
[](ClusterFinderFrozen<ClusterType, uint16_t, pd_type> &self) {
|
||||
auto pd = new NDArray<pd_type, 2>{};
|
||||
*pd = self.pedestal();
|
||||
return return_image_data(pd);
|
||||
})
|
||||
.def_property_readonly(
|
||||
"noise",
|
||||
[](ClusterFinderFrozen<ClusterType, uint16_t, pd_type> &self) {
|
||||
auto arr = new NDArray<pd_type, 2>{};
|
||||
*arr = self.noise();
|
||||
return return_image_data(arr);
|
||||
})
|
||||
.def(
|
||||
"steal_clusters",
|
||||
[](ClusterFinderFrozen<ClusterType, uint16_t, pd_type> &self,
|
||||
bool realloc_same_capacity) {
|
||||
ClusterVector<ClusterType> clusters =
|
||||
self.steal_clusters(realloc_same_capacity);
|
||||
return clusters;
|
||||
},
|
||||
py::arg("realloc_same_capacity") = false)
|
||||
.def(
|
||||
"find_clusters",
|
||||
[](ClusterFinderFrozen<ClusterType, uint16_t, pd_type> &self,
|
||||
py::array_t<uint16_t> frame, uint64_t frame_number) {
|
||||
auto view = make_view_2d(frame);
|
||||
self.find_clusters(view, frame_number);
|
||||
return;
|
||||
},
|
||||
py::arg(), py::arg("frame_number") = 0);
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "bind_ClusterFile.hpp"
|
||||
#include "bind_ClusterFileSink.hpp"
|
||||
#include "bind_ClusterFinder.hpp"
|
||||
#include "bind_ClusterFinderFrozen.hpp"
|
||||
#include "bind_ClusterFinderMT.hpp"
|
||||
#include "bind_ClusterVector.hpp"
|
||||
#include "bind_Defs.hpp"
|
||||
@@ -54,6 +55,7 @@ double, 'f' for float)
|
||||
|
||||
#define DEFINE_BINDINGS_CLUSTERFINDER(T, N, M, U, TYPE_CODE) \
|
||||
define_ClusterFinder<T, N, M, U>(m, "Cluster" #N "x" #M #TYPE_CODE); \
|
||||
define_ClusterFinderFrozen<T, N, M, U>(m, "Cluster" #N "x" #M #TYPE_CODE); \
|
||||
define_ClusterFinderMT<T, N, M, U>(m, "Cluster" #N "x" #M #TYPE_CODE); \
|
||||
define_ClusterFileSink<T, N, M, U>(m, "Cluster" #N "x" #M #TYPE_CODE); \
|
||||
define_ClusterCollector<T, N, M, U>(m, "Cluster" #N "x" #M #TYPE_CODE);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,404 @@
|
||||
"""Helpers for the CPU-vs-CUDA ClusterFinder mismatch analysis.
|
||||
|
||||
Pure, parameterised versions of the functions that used to live inline in the
|
||||
notebook's forensic-view cell, plus the reusable scan + plot routines so the
|
||||
notebook cells stay thin. Import in the notebook with::
|
||||
|
||||
from helper import (centers, only_sets, footprint_mask, shift_dist,
|
||||
train_pedestal, scan_mismatches, plot_masked_mismatch)
|
||||
|
||||
None of these rely on notebook globals — cluster geometry (rx, ry) and the
|
||||
frame shape (rows, cols) are passed in explicitly.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import boost_histogram as bh
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Pure helpers
|
||||
# --------------------------------------------------------------------------- #
|
||||
def print_pinning_budget(rows, cols, dtype=np.uint16, headroom_gb=4.0):
|
||||
"""
|
||||
Print system RAM stats and estimate the maximum number of frames
|
||||
that can be safely registered with cudaHostRegister / register_input_buffer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
rows, cols : int Detector frame dimensions.
|
||||
dtype : np.dtype Frame element type (default uint16 = 2 bytes/pixel).
|
||||
headroom_gb: float RAM to keep free for OS + CUDA context (default 4 GB).
|
||||
"""
|
||||
# /proc/meminfo is always available on Linux — no extra dependency needed
|
||||
meminfo = {}
|
||||
with open('/proc/meminfo') as f:
|
||||
for line in f:
|
||||
key, val = line.split(':')
|
||||
meminfo[key.strip()] = int(val.split()[0]) * 1024 # kB → bytes
|
||||
|
||||
total_bytes = meminfo['MemTotal']
|
||||
available_bytes = meminfo['MemAvailable'] # free + reclaimable cache
|
||||
safe_bytes = max(0, available_bytes - int(headroom_gb * 1024**3))
|
||||
|
||||
frame_bytes = rows * cols * np.dtype(dtype).itemsize
|
||||
max_frames = safe_bytes // frame_bytes
|
||||
|
||||
GiB = 1024**3
|
||||
print("── System RAM ──────────────────────────────────────────")
|
||||
print(f" Total RAM : {total_bytes/GiB:.1f} GiB")
|
||||
print(f" Currently available : {available_bytes/GiB:.1f} GiB "
|
||||
f"(free + reclaimable cache)")
|
||||
print(f" Reserved headroom : {headroom_gb:.1f} GiB "
|
||||
f"(OS + CUDA context)")
|
||||
print(f" Safe pinning budget : {safe_bytes/GiB:.1f} GiB")
|
||||
print()
|
||||
print("── Frame layout ────────────────────────────────────────")
|
||||
print(f" Frame size : {rows} × {cols} × "
|
||||
f"{np.dtype(dtype).itemsize} B = {frame_bytes/1024:.1f} kB")
|
||||
print()
|
||||
print("── Pinning estimate ────────────────────────────────────")
|
||||
print(f" Max frames pinnable : {max_frames:,} "
|
||||
f"({max_frames * frame_bytes / GiB:.1f} GiB)")
|
||||
print()
|
||||
print(" Note: no swap on this machine — exceeding available RAM")
|
||||
print(" will trigger the OOM killer. Stay within the budget.")
|
||||
|
||||
def centers(cv):
|
||||
"""Set of ``(x, y)`` integer cluster centres from a ClusterVector."""
|
||||
if cv.size == 0:
|
||||
return set()
|
||||
a = np.asarray(cv)
|
||||
return {(int(x), int(y)) for x, y in zip(a["x"], a["y"])}
|
||||
|
||||
|
||||
def only_sets(cpu_c, cu_c, tol=1):
|
||||
"""CPU-only / CUDA-only centres, ignoring ``<=tol`` px 'shifted' matches.
|
||||
|
||||
``tol=0`` returns the exact set difference (shifted twins stay counted as
|
||||
mismatches); ``tol=1`` drops any mismatch that has a counterpart in the
|
||||
other finder's 8-neighbourhood.
|
||||
"""
|
||||
def near(p, other):
|
||||
x, y = p
|
||||
return any((x + dx, y + dy) in other
|
||||
for dx in range(-tol, tol + 1)
|
||||
for dy in range(-tol, tol + 1))
|
||||
|
||||
cpu_only = {p for p in cpu_c - cu_c if not near(p, cu_c)}
|
||||
cu_only = {p for p in cu_c - cpu_c if not near(p, cpu_c)}
|
||||
return cpu_only, cu_only
|
||||
|
||||
|
||||
def footprint_mask(cs, shape, rx, ry):
|
||||
"""Boolean map: True where a pixel lies in some cluster's footprint.
|
||||
|
||||
Each centre paints a ``(2*ry+1) x (2*rx+1)`` box, clipped at the borders.
|
||||
"""
|
||||
m = np.zeros(shape, bool)
|
||||
for (cx, cy) in cs:
|
||||
m[max(0, cy - ry):cy + ry + 1, max(0, cx - rx):cx + rx + 1] = True
|
||||
return m
|
||||
|
||||
|
||||
def shift_dist(p, other, R=4):
|
||||
"""Chebyshev distance from ``p`` to the nearest member of ``other``.
|
||||
|
||||
Rings are searched inner-to-outer so the first hit is the nearest.
|
||||
Returns ``-1`` if nothing lies within ``R`` px.
|
||||
"""
|
||||
x, y = p
|
||||
for r in range(1, R + 1):
|
||||
for dx in range(-r, r + 1):
|
||||
for dy in range(-r, r + 1):
|
||||
if max(abs(dx), abs(dy)) == r and (x + dx, y + dy) in other:
|
||||
return r
|
||||
return -1
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Reusable scan + plot
|
||||
# --------------------------------------------------------------------------- #
|
||||
def train_pedestal(finders, f, n_frames, seek=0):
|
||||
"""Push the first ``n_frames`` frames of ``f`` into every finder given."""
|
||||
f.seek(seek)
|
||||
for _ in range(n_frames):
|
||||
img = f.read_frame().copy()
|
||||
for cf in finders:
|
||||
cf.push_pedestal_frame(img)
|
||||
|
||||
|
||||
def scan_mismatches(cf_cpu, cf_cuda, data, rx, ry,
|
||||
scan_count=1000, n_show=8, tol=0):
|
||||
"""Run both finders over ``scan_count`` frames sampled across ``data``.
|
||||
|
||||
Before every frame it snapshots the pedestal each finder will DECIDE with
|
||||
(CPU: host mean/rms; CUDA: device mean/rms on stream 0), so a later
|
||||
recompute uses the exact decision-time baseline.
|
||||
|
||||
``tol`` is forwarded to :func:`only_sets` when scoring a frame; ``tol=0``
|
||||
keeps shifted twins as mismatches. Returns ``(show, totals)`` where
|
||||
``show`` is the ``n_show`` frames with the most mismatches, each a dict
|
||||
with ``fid``, ``score``, ``cpu_c``, ``cu_c``, ``mism`` and the four
|
||||
pedestal arrays ``ped_cpu/noise_cpu/ped_cu/noise_cu``.
|
||||
"""
|
||||
show = []
|
||||
tot_cpu_only = tot_cu_only = 0
|
||||
for fid in np.linspace(0, len(data) - 1, scan_count, dtype=int):
|
||||
snap = dict(ped_cpu=np.asarray(cf_cpu.pedestal).copy(),
|
||||
noise_cpu=np.asarray(cf_cpu.noise).copy(),
|
||||
ped_cu=np.asarray(cf_cuda.device_pedestal(0)).copy(),
|
||||
noise_cu=np.asarray(cf_cuda.device_noise(0)).copy())
|
||||
cf_cpu.find_clusters(data[fid])
|
||||
cpu_c = centers(cf_cpu.steal_clusters(realloc_same_capacity=True))
|
||||
cf_cuda.find_clusters(data[fid])
|
||||
cu_c = centers(cf_cuda.steal_clusters(realloc_same_capacity=True))
|
||||
|
||||
cpu_only, cu_only = only_sets(cpu_c, cu_c, tol=tol)
|
||||
tot_cpu_only += len(cpu_only)
|
||||
tot_cu_only += len(cu_only)
|
||||
mism = cpu_only | cu_only
|
||||
if not mism:
|
||||
continue
|
||||
if len(show) < n_show or len(mism) > show[-1]["score"]:
|
||||
show.append(dict(score=len(mism), fid=int(fid),
|
||||
cpu_c=cpu_c, cu_c=cu_c, mism=mism, **snap))
|
||||
show.sort(key=lambda e: -e["score"])
|
||||
del show[n_show:]
|
||||
return show, dict(cpu_only=tot_cpu_only, cu_only=tot_cu_only)
|
||||
|
||||
|
||||
def compare_finders(finders, data, scan_count=1000, n_bins=200, e_range=(-2, 4000)):
|
||||
"""Run several finders over the same frames and score pairwise agreement.
|
||||
|
||||
``finders`` is a dict ``{name: finder}``; every finder must already be
|
||||
trained on the SAME pedestal frames. Each is run over the same
|
||||
``scan_count`` frames sampled across ``data`` (identical ``find_clusters``/
|
||||
``steal_clusters`` API for CPU, frozen-CPU and CUDA). In the same pass it
|
||||
accumulates a per-finder cluster-energy histogram (from ``cv.sum()``), so no
|
||||
extra scan is needed to draw the spectra.
|
||||
|
||||
Returns ``(totals, pairs, frames_scanned, hists)``:
|
||||
* ``totals[name]`` total clusters found by that finder
|
||||
* ``pairs[(a, b)]`` dict with ``a_only``/``b_only``/``mismatch``
|
||||
summed over frames (exact, tol=0)
|
||||
* ``hists[name]`` boost ``Histogram`` of cluster energies
|
||||
Companions :func:`print_comparison` and :func:`plot_spectra` render these.
|
||||
"""
|
||||
names = list(finders)
|
||||
totals = {n: 0 for n in names}
|
||||
hists = {n: bh.Histogram(bh.axis.Regular(n_bins, *e_range)) for n in names}
|
||||
pairs = {}
|
||||
for i, a in enumerate(names):
|
||||
for b in names[i + 1:]:
|
||||
pairs[(a, b)] = dict(a_only=0, b_only=0, mismatch=0)
|
||||
|
||||
fids = np.linspace(0, len(data) - 1, scan_count, dtype=int)
|
||||
for fid in fids:
|
||||
cs = {}
|
||||
for n, cf in finders.items():
|
||||
cf.find_clusters(data[fid])
|
||||
cv = cf.steal_clusters(realloc_same_capacity=True)
|
||||
cs[n] = centers(cv)
|
||||
totals[n] += len(cs[n])
|
||||
if cv.size:
|
||||
hists[n].fill(np.asarray(cv.sum()).ravel())
|
||||
for (a, b), acc in pairs.items():
|
||||
a_only, b_only = only_sets(cs[a], cs[b], tol=0)
|
||||
acc["a_only"] += len(a_only)
|
||||
acc["b_only"] += len(b_only)
|
||||
acc["mismatch"] += len(a_only) + len(b_only)
|
||||
return totals, pairs, len(fids), hists
|
||||
|
||||
|
||||
def plot_spectra(hists, totals=None, title="Cluster energy spectrum"):
|
||||
"""Overlay per-finder cluster-energy spectra with a ratio panel.
|
||||
|
||||
``hists`` is the ``{name: Histogram}`` returned by :func:`compare_finders`;
|
||||
the first finder is the reference for the ratio panel. Returns the Figure.
|
||||
"""
|
||||
names = list(hists)
|
||||
ref = names[0]
|
||||
edges = hists[ref].axes[0].edges
|
||||
vals = {n: hists[n].values() for n in names}
|
||||
|
||||
fig, (ax_spec, ax_ratio) = plt.subplots(
|
||||
2, 1, figsize=(8, 6), sharex=True,
|
||||
gridspec_kw={"height_ratios": [3, 1]})
|
||||
|
||||
styles = ["-", "--", "-.", ":"]
|
||||
for i, n in enumerate(names):
|
||||
lbl = n if totals is None else f"{n} ({totals[n]:,} clusters)"
|
||||
ax_spec.stairs(vals[n], edges, label=lbl, linestyle=styles[i % len(styles)])
|
||||
ax_spec.set_ylabel("Counts")
|
||||
ax_spec.set_title(title)
|
||||
ax_spec.legend()
|
||||
ax_spec.grid(alpha=0.2)
|
||||
|
||||
with np.errstate(divide="ignore", invalid="ignore"):
|
||||
for i, n in enumerate(names[1:], start=1):
|
||||
ratio = np.where(vals[ref] > 0, vals[n] / vals[ref], np.nan)
|
||||
ax_ratio.stairs(ratio, edges, label=f"{n} / {ref}",
|
||||
color=f"C{i}", linestyle=styles[i % len(styles)])
|
||||
ax_ratio.axhline(1.0, color="gray", linewidth=0.5)
|
||||
ax_ratio.set_ylabel(f"/ {ref}")
|
||||
ax_ratio.set_xlabel("Energy [ADU]")
|
||||
ax_ratio.set_ylim(0.5, 2.0)
|
||||
ax_ratio.legend(fontsize=8)
|
||||
ax_ratio.grid(alpha=0.3)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
return fig
|
||||
|
||||
|
||||
def print_comparison(totals, pairs, frames_scanned):
|
||||
"""Pretty-print the output of :func:`compare_finders`."""
|
||||
print(f"Scanned {frames_scanned} frames\n")
|
||||
print("Total clusters per finder:")
|
||||
for n, t in totals.items():
|
||||
print(f" {n:<16} {t:>12,}")
|
||||
print("\nPairwise exact mismatches (tol=0):")
|
||||
print(f" {'pair':<28} {'A-only':>10} {'B-only':>10} {'total':>10}")
|
||||
for (a, b), acc in pairs.items():
|
||||
ref = max(totals[a], totals[b], 1)
|
||||
pct = 100.0 * acc["mismatch"] / ref
|
||||
print(f" {a+' vs '+b:<28} {acc['a_only']:>10,} "
|
||||
f"{acc['b_only']:>10,} {acc['mismatch']:>10,} ({pct:.4f}%)")
|
||||
|
||||
|
||||
def walkthrough(show, data, rx, ry, rows, cols, n_sigma, pick=0,
|
||||
labels=('A', 'B')):
|
||||
"""Manual Test1/Test3 recompute of the strongest residual mismatch.
|
||||
|
||||
Dissects ``show[pick]`` (from :func:`scan_mismatches`, run as ``(a, b)``)
|
||||
under each finder's decision-time snapshot pedestal — ``a`` = ``ped_cpu`` /
|
||||
``noise_cpu``, ``b`` = ``ped_cu`` / ``noise_cu`` — and prints the raw and
|
||||
pedestal-subtracted window plus the accept/reject each finder reaches. With
|
||||
the double/double build the recompute reproduces the kernel exactly, so a
|
||||
lone surviving mismatch (e.g. the single 7x7 residual) is fully explained:
|
||||
the test that flips (Test1/Test3) and the pedestal gap name the cause.
|
||||
"""
|
||||
if not show:
|
||||
print("No residual mismatches to walk through.")
|
||||
return
|
||||
e = show[pick]
|
||||
sx, sy = 2 * rx + 1, 2 * ry + 1
|
||||
c3 = np.sqrt(sx * sy)
|
||||
frame = data[e['fid']].astype(np.float64)
|
||||
sub_a = frame - e['ped_cpu']
|
||||
sub_b = frame - e['ped_cu']
|
||||
|
||||
# strongest mismatch pixel whose full window stays inside the frame
|
||||
cand = [p for p in e['mism']
|
||||
if rx <= p[0] < cols - rx and ry <= p[1] < rows - ry]
|
||||
if not cand:
|
||||
print(f"frame {e['fid']}: all mismatches on the border — pick another.")
|
||||
return
|
||||
X0, Y0 = max(cand, key=lambda p: max(sub_a[p[1], p[0]], sub_b[p[1], p[0]]))
|
||||
|
||||
def evaluate(mean, rms_img):
|
||||
sig = (frame[Y0 - ry:Y0 + ry + 1, X0 - rx:X0 + rx + 1]
|
||||
- mean[Y0 - ry:Y0 + ry + 1, X0 - rx:X0 + rx + 1])
|
||||
value = frame[Y0, X0] - mean[Y0, X0]
|
||||
rms = rms_img[Y0, X0]
|
||||
thr1, thr3 = n_sigma * rms, c3 * n_sigma * rms
|
||||
mx, total = sig.max(), sig.sum()
|
||||
localmax = bool(value >= mx)
|
||||
t1, t3 = bool(mx > thr1), bool(total > thr3)
|
||||
accept = bool(value >= -thr1) and localmax and (t1 or t3)
|
||||
return dict(sig=sig, value=value, rms=rms, thr1=thr1, thr3=thr3,
|
||||
mx=mx, total=total, localmax=localmax, t1=t1, t3=t3,
|
||||
accept=accept)
|
||||
|
||||
la, lb = labels
|
||||
owner = la if (X0, Y0) in e['cpu_c'] else lb
|
||||
print(f"frame {e['fid']} centre (x={X0}, y={Y0}) accepted only by {owner}")
|
||||
print("raw window (ADU):")
|
||||
print(np.array2string(frame[Y0 - ry:Y0 + ry + 1,
|
||||
X0 - rx:X0 + rx + 1].astype(int)))
|
||||
print()
|
||||
|
||||
ra = evaluate(e['ped_cpu'], e['noise_cpu'])
|
||||
rb = evaluate(e['ped_cu'], e['noise_cu'])
|
||||
|
||||
def detail(tag, r):
|
||||
print(f"--- {tag} ---")
|
||||
print(" subtracted window:")
|
||||
print(" ", np.array2string(r['sig'], precision=1, prefix=' '))
|
||||
print(f" centre value = {float(r['value']):8.2f} "
|
||||
f"(local max? {r['localmax']})")
|
||||
print(f" max = {float(r['mx']):8.2f} "
|
||||
f"total = {float(r['total']):8.2f}")
|
||||
print(f" rms(centre) = {float(r['rms']):8.3f}")
|
||||
print(f" Test1: max > {n_sigma}*rms = {float(r['thr1']):8.2f} -> {r['t1']}")
|
||||
print(f" Test3: total > {c3:.0f}*{n_sigma}*rms = {float(r['thr3']):8.2f} -> {r['t3']}")
|
||||
print(f" ACCEPT = {r['accept']}")
|
||||
print()
|
||||
|
||||
detail(la, ra)
|
||||
detail(lb, rb)
|
||||
print(f"RESULT: {la} = {'ACCEPT' if ra['accept'] else 'reject'} , "
|
||||
f"{lb} = {'ACCEPT' if rb['accept'] else 'reject'}")
|
||||
gap = abs(float(e['ped_cpu'][Y0, X0] - e['ped_cu'][Y0, X0]))
|
||||
rgap = abs(float(e['noise_cpu'][Y0, X0] - e['noise_cu'][Y0, X0]))
|
||||
print(f"pedestal mean gap @centre = {gap:.4f} ADU; rms gap = {rgap:.4f}")
|
||||
if ra['accept'] == rb['accept']:
|
||||
print("NOTE: recompute agrees for this centre pixel — the split is FP "
|
||||
"rounding right at threshold or a window-neighbour effect; "
|
||||
"try another pick.")
|
||||
|
||||
|
||||
def plot_masked_mismatch(show, data, rx, ry, rows, cols,
|
||||
zoom=30, show_vals=True):
|
||||
"""Side-by-side masked view of each frame in ``show``.
|
||||
|
||||
Cluster pixels are coloured by pedestal-subtracted value (value printed
|
||||
when ``show_vals``); non-cluster pixels are white; a red dot marks every
|
||||
cluster centre. The cut is centred on the strongest mismatch pixel of the
|
||||
frame. No box, no tolerance discarding — a shifted twin in the other
|
||||
finder stays visible. Returns the Figure.
|
||||
"""
|
||||
cmap = plt.cm.viridis.copy()
|
||||
cmap.set_bad("white")
|
||||
fig, axes = plt.subplots(len(show), 2, figsize=(11, 5.3 * len(show)),
|
||||
squeeze=False)
|
||||
for r, e in enumerate(show):
|
||||
fid = e["fid"]
|
||||
sub_cpu = data[fid].astype(np.float64) - e["ped_cpu"]
|
||||
sub_cu = data[fid].astype(np.float64) - e["ped_cu"]
|
||||
X0, Y0 = max(e["mism"],
|
||||
key=lambda p: max(sub_cpu[p[1], p[0]], sub_cu[p[1], p[0]]))
|
||||
half = zoom // 2
|
||||
r0, r1 = max(0, Y0 - half), min(rows, Y0 + half)
|
||||
c0, c1 = max(0, X0 - half), min(cols, X0 + half)
|
||||
win_cpu, win_cu = sub_cpu[r0:r1, c0:c1], sub_cu[r0:r1, c0:c1]
|
||||
mask_cpu = footprint_mask(e["cpu_c"], sub_cpu.shape, rx, ry)[r0:r1, c0:c1]
|
||||
mask_cu = footprint_mask(e["cu_c"], sub_cu.shape, rx, ry)[r0:r1, c0:c1]
|
||||
union = mask_cpu | mask_cu
|
||||
vmax = max(np.percentile(np.concatenate([win_cpu[union], win_cu[union]]),
|
||||
99) if union.any() else 50.0, 50.0)
|
||||
|
||||
for ax, win, mask, cs, name in [
|
||||
(axes[r][0], win_cpu, mask_cpu, e["cpu_c"], "CPU"),
|
||||
(axes[r][1], win_cu, mask_cu, e["cu_c"], "CUDA")]:
|
||||
ax.imshow(np.ma.masked_where(~mask, win), cmap=cmap, vmin=0,
|
||||
vmax=vmax, interpolation="nearest")
|
||||
for (cx, cy) in cs:
|
||||
if c0 <= cx < c1 and r0 <= cy < r1:
|
||||
ax.plot(cx - c0, cy - r0, ".", color="red", ms=7)
|
||||
if show_vals:
|
||||
for i in range(win.shape[0]):
|
||||
for j in range(win.shape[1]):
|
||||
if mask[i, j]:
|
||||
ax.text(j, i, f"{win[i, j]:.0f}", ha="center",
|
||||
va="center", fontsize=6,
|
||||
color="white" if win[i, j] < 0.55 * vmax
|
||||
else "black")
|
||||
ax.set_title(f"frame {fid} — {name}: {len(cs)} clusters", fontsize=10)
|
||||
ax.set_xticks([])
|
||||
ax.set_yticks([])
|
||||
axes[r][0].set_ylabel(f"{e['score']} mismatches\nzoom @ ({X0},{Y0})",
|
||||
fontsize=9)
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
return fig
|
||||
Reference in New Issue
Block a user