Feat: Add --fast-mode supersampled point-source accumulation

Add an optional CPU preview path that deposits each point-source image as a
supersampled delta and resolves the whole frame with one global Moffat
convolution plus an N x N box average, instead of splatting a per-event PSF.

- optics: FastPsfAccumulator builds the pixel-area-integral kernel of the
  target Moffat at the supersampled scale (width N*alpha, same beta).  The
  1/N^2 box average then reproduces the final pixel-area integral, so the
  requested FWHM and beta are preserved without renormalisation.  Deposits are
  per-cell atomic adds; resolve accumulates into the caller's HDR buffer.
- frame: fast branch in frame_splat_catalog with one shared supersampled
  buffer and a single resolve per frame; the accumulator is reused across
  movie frames and built from the map dimensions on lens-map import.
- main: --fast-mode, --fast-supersample N (1..8, default 2) and
  --fast-deposit nearest|bilinear (default nearest).  CPU-only and rejected in
  the HIP/dummy backends; --psf-min-y still applies per event while
  --max-cache-psf-flux does not.
- The deposition scheme was chosen by scripts/fast_mode_deposit_error.py:
  nearest keeps the PSF shape exactly with <= 0.5/N px position quantization;
  bilinear keeps the exact centroid but broadens FWHM and beta.  Recorded in
  benchmarks/fast_mode_deposit_2026-09-18.md.
- tests/test_frame.c covers fast nearest vs the direct evaluator at the snapped
  centre, flux conservation, bilinear centroid, min-Y discard, frame plumbing,
  and HDR accumulation onto a non-zero background.
- benchmarks/fast_mode_cpu_2026-09-18.md records a ~10x speedup on the 2MASS
  galactic-centre field with small tone-mapped differences.
This commit is contained in:
wyj committed 2026-09-24 01:36:25 -04:00
1 parent 7ddc58515a
commit 3deebfb2fa
13 files changed
+1249 -36

No files matched your search

+2 -2
View File
@@ -119,7 +119,7 @@ int main(int argc, char **argv) {
#pragma omp for schedule(dynamic,1)
for(size_t t=first;t<last;++t) {
CatalogSplatStats result=splat_catalog_triangles(&map.frames[0].mesh,&catalog,NULL,
map.width,map.height,exposure,&psf,&cache,1e6,max_flux,1e-8,min_y,t,t+1,&local);
map.width,map.height,exposure,&psf,&cache,1e6,max_flux,1e-8,min_y,t,t+1,&local,NULL);
failed|=result.failed;
}
for(int k=0;k<4;++k)totals[k]+=classified[k];
@@ -134,7 +134,7 @@ int main(int argc, char **argv) {
PsfEventSink sink={.cache=&cache};
double start=omp_get_wtime();
CatalogSplatStats stats=splat_catalog_triangles(&map.frames[0].mesh,&catalog,NULL,
map.width,map.height,exposure,&psf,&cache,1e6,max_flux,1e-8,min_y,first,last,&sink);
map.width,map.height,exposure,&psf,&cache,1e6,max_flux,1e-8,min_y,first,last,&sink,NULL);
printf("DIAGNOSTIC ONLY, no HDR: workers=1 catalog=%s stars=%zu triangles=[%zu,%zu) emitting_triangles=%zu last_emitting_triangle=%zu cached=%zu wing=%zu direct=%zu discarded=%zu events=%zu capped=%d producer_wall=%.9f digest=%016llx\n",
kind,catalog.count,first,last,triangles_seen,last_triangle,classified[0]+classified[2],classified[2],classified[1],classified[3],captured_count,frame_psf_diagnostic_stopped(),omp_get_wtime()-start,(unsigned long long)digest);
printf("checksum=%016llx\n",(unsigned long long)checksum);
+141 -7
View File
@@ -82,7 +82,7 @@ int main(void) {
const size_t images =
frame_splat_catalog(&mesh, &catalog, hdr, width, height, test_exposure,
&psf, NULL, INFINITY, 1.0, psf_relative_tail,
0.0, 0, 1, NULL, NULL, NULL);
0.0, 0, 1, NULL, NULL, NULL, NULL);
if (images != 1 || hdr[3 * (50 * width + 50)] <= 0.0) {
fputs("flat-space inverse lens-map regression failed\n", stderr);
goto done;
@@ -104,7 +104,7 @@ int main(void) {
loaded_map.frames[0].mesh.vertex_count != mesh.vertex_count ||
frame_splat_catalog(&loaded_map.frames[0].mesh, &catalog, roundtrip_hdr,
width, height, test_exposure, &psf, NULL, INFINITY,
1.0, psf_relative_tail, 0.0, 0, 1, NULL, NULL, NULL) != images) {
1.0, psf_relative_tail, 0.0, 0, 1, NULL, NULL, NULL, NULL) != images) {
fputs("lens-map round-trip regression failed\n", stderr);
free(roundtrip_hdr); lens_map_destroy(&loaded_map); unlink(lens_map_path);
goto done;
@@ -140,7 +140,7 @@ int main(void) {
PsfSplatStats min_y_stats = {0};
if (frame_splat_catalog(&mesh, &catalog, hdr, width, height, test_exposure,
&psf, NULL, INFINITY, 1.0, psf_relative_tail,
1e300, 0, 1, NULL, &min_y_stats, NULL) != 1 ||
1e300, 0, 1, NULL, &min_y_stats, NULL, NULL) != 1 ||
min_y_stats.discarded_below_min_y != 1 ||
hdr[3 * (50 * width + 50)] != 0.0) {
fputs("PSF minimum-Y discard regression failed\n", stderr);
@@ -159,11 +159,11 @@ int main(void) {
omp_set_num_threads(1);
const size_t serial_images = frame_splat_catalog(
&mesh, &catalog, serial_hdr, width, height, test_exposure, &psf, NULL,
INFINITY, 1.0, psf_relative_tail, 0.0, 0, 1, NULL, NULL, NULL);
INFINITY, 1.0, psf_relative_tail, 0.0, 0, 1, NULL, NULL, NULL, NULL);
omp_set_num_threads(4);
const size_t parallel_images = frame_splat_catalog(
&mesh, &catalog, parallel_hdr, width, height, test_exposure, &psf, NULL,
INFINITY, 1.0, psf_relative_tail, 0.0, 0, 1, NULL, NULL, NULL);
INFINITY, 1.0, psf_relative_tail, 0.0, 0, 1, NULL, NULL, NULL, NULL);
omp_set_num_threads(original_threads);
for (int value = 0; value < width * height * 3; ++value)
if (fabs(serial_hdr[value] - parallel_hdr[value]) >
@@ -304,6 +304,140 @@ int main(void) {
free(cached_hdr);
free(reference_hdr);
psf_kernel_cache_destroy(&cache);
/* Fast mode: a nearest deposit must reproduce the current pixel-integrated
* Moffat at the snapped supersampled centre, preserve total flux, and honour
* the min-Y discard rule; bilinear deposition must preserve the centroid. */
{
int fast_ok = 1;
const int supersample = 2;
FastPsfAccumulator fast = {0};
FastPsfAccumulator bilinear = {0};
FastPsfAccumulator min_y_fast = {0};
FastPsfAccumulator accumulation = {0};
double *fast_hdr = calloc((size_t)width * height * 3, sizeof *fast_hdr);
double *direct_hdr = calloc((size_t)width * height * 3, sizeof *direct_hdr);
double *background_hdr =
calloc((size_t)width * height * 3, sizeof *background_hdr);
if (fast_hdr == NULL || direct_hdr == NULL || background_hdr == NULL ||
fast_psf_accumulator_init(&fast, width, height, supersample,
FAST_PSF_DEPOSIT_NEAREST, &psf,
psf_relative_tail, 0.0, 1) ||
fast_psf_accumulator_init(&bilinear, width, height, supersample,
FAST_PSF_DEPOSIT_BILINEAR, &psf,
psf_relative_tail, 0.0, 1) ||
fast_psf_accumulator_init(&min_y_fast, width, height, supersample,
FAST_PSF_DEPOSIT_NEAREST, &psf,
psf_relative_tail, 0.5, 1) ||
fast_psf_accumulator_init(&accumulation, width, height, supersample,
FAST_PSF_DEPOSIT_NEAREST, &psf,
psf_relative_tail, 0.0, 1)) {
fputs("fast-mode accumulator construction regression failed\n", stderr);
fast_ok = 0;
goto fast_done;
}
/* (50.2, 50.2) snaps to supersampled cell 100, centre (100.5, 100.5) in
* ss coordinates, i.e. final position (50.25, 50.25). */
if (fast_psf_accumulator_deposit(&fast, 50.2, 50.2,
(LinearRgb){1.0, 1.0, 1.0}, 1.0) == 3 ||
fast_psf_accumulator_resolve(&fast, fast_hdr, 4)) {
fputs("fast-mode nearest deposit regression failed\n", stderr);
fast_ok = 0;
goto fast_done;
}
splat_moffat_direct(direct_hdr, width, height, 50.25, 50.25,
(LinearRgb){1.0, 1.0, 1.0}, 1.0, &psf,
psf_relative_tail, 0.0);
double peak = 0.0, max_error = 0.0, fast_flux = 0.0, direct_flux = 0.0;
for (int value = 0; value < width * height * 3; ++value) {
peak = fmax(peak, direct_hdr[value]);
max_error = fmax(max_error, fabs(fast_hdr[value] - direct_hdr[value]));
fast_flux += fast_hdr[value];
direct_flux += direct_hdr[value];
}
if (!(peak > 0.0) || max_error > 1e-4 * peak ||
fabs(fast_flux - direct_flux) > 1e-4) {
fputs("fast-mode nearest semantics regression failed\n", stderr);
fast_ok = 0;
goto fast_done;
}
/* Bilinear keeps the exact continuous centroid. */
memset(fast_hdr, 0, (size_t)width * height * 3 * sizeof *fast_hdr);
if (fast_psf_accumulator_deposit(&bilinear, 50.37, 50.62,
(LinearRgb){1.0, 1.0, 1.0}, 1.0) == 3 ||
fast_psf_accumulator_resolve(&bilinear, fast_hdr, 4)) {
fputs("fast-mode bilinear deposit regression failed\n", stderr);
fast_ok = 0;
goto fast_done;
}
double weight_sum = 0.0, cx = 0.0, cy = 0.0;
for (int row = 0; row < height; ++row)
for (int column = 0; column < width; ++column) {
const double weight = fast_hdr[3 * (row * width + column)];
weight_sum += weight;
cx += weight * (column + 0.5);
cy += weight * (row + 0.5);
}
if (!(weight_sum > 0.0) ||
hypot(cx / weight_sum - 50.37, cy / weight_sum - 50.62) > 1e-6) {
fputs("fast-mode bilinear centroid regression failed\n", stderr);
fast_ok = 0;
goto fast_done;
}
/* The min-Y cutoff discards an event whose peak luminance is below it. */
if (fast_psf_accumulator_deposit(&fast, 10.5, 10.5,
(LinearRgb){1.0, 1.0, 1.0}, 1.0) != 0 ||
fast_psf_accumulator_deposit(&min_y_fast, 10.5, 10.5,
(LinearRgb){1.0, 1.0, 1.0}, 1.0) != 3) {
fputs("fast-mode min-Y discard regression failed\n", stderr);
fast_ok = 0;
goto fast_done;
}
/* End-to-end plumbing through the frame splat path. */
memset(fast_hdr, 0, (size_t)width * height * 3 * sizeof *fast_hdr);
PsfSplatStats fast_stats = {0};
const size_t fast_images = frame_splat_catalog(
&mesh, &catalog, fast_hdr, width, height, test_exposure, &psf, NULL,
INFINITY, 1.0, psf_relative_tail, 0.0, 0, 1, NULL, &fast_stats, NULL,
&fast);
if (fast_images != 1 || fast_stats.discarded_below_min_y != 0) {
fputs("fast-mode frame splat regression failed\n", stderr);
fast_ok = 0;
goto fast_done;
}
/* HDR accumulation semantics: resolve must add onto an existing
* background, not overwrite it. A prefilled buffer plus one deposit must
* preserve the far-field background exactly and add the PSF core. */
for (int value = 0; value < width * height * 3; ++value)
background_hdr[value] = 0.25;
if (fast_psf_accumulator_deposit(&accumulation, 10.5, 10.5,
(LinearRgb){1.0, 1.0, 1.0}, 1.0) == 3 ||
fast_psf_accumulator_resolve(&accumulation, background_hdr, 4)) {
fputs("fast-mode HDR accumulation regression failed\n", stderr);
fast_ok = 0;
goto fast_done;
}
/* (90, 90) is far outside the kernel support of a star at (10.5, 10.5). */
if (background_hdr[3 * (90 * width + 90)] != 0.25) {
fputs("fast-mode HDR accumulation lost the background\n", stderr);
fast_ok = 0;
goto fast_done;
}
if (!(background_hdr[3 * (10 * width + 10)] > 0.25)) {
fputs("fast-mode HDR accumulation did not add the deposit\n", stderr);
fast_ok = 0;
goto fast_done;
}
fast_done:
free(fast_hdr);
free(direct_hdr);
free(background_hdr);
fast_psf_accumulator_destroy(&fast);
fast_psf_accumulator_destroy(&bilinear);
fast_psf_accumulator_destroy(&min_y_fast);
fast_psf_accumulator_destroy(&accumulation);
if (!fast_ok)
goto done;
}
frame_draw_mesh(&mesh, hdr, width, height, 0.5, 0.5);
if (hdr[3 * (10 * width + 20)] != 0.25) {
fputs("mesh diagnostic overlay regression failed\n", stderr);
@@ -325,7 +459,7 @@ int main(void) {
frame_splat_catalog(&fine_mesh, &fine_catalog, hdr, width, height,
test_exposure, &psf, NULL, INFINITY, 1.0,
psf_relative_tail, 0.0, 0, 1, NULL,
NULL, NULL) != 1) {
NULL, NULL, NULL) != 1) {
fputs("fine source-triangle containment regression failed\n", stderr);
frame_lens_mesh_destroy(&fine_mesh);
goto done;
@@ -369,7 +503,7 @@ int main(void) {
memset(hdr, 0, (size_t)width * height * 3 * sizeof *hdr);
if (frame_splat_catalog(&thin_mesh, &thin_catalog, hdr, width, height,
test_exposure, &psf, NULL, 1.0, 1.0,
psf_relative_tail, 0.0, 0, 1, NULL, NULL, NULL) != 1 ||
psf_relative_tail, 0.0, 0, 1, NULL, NULL, NULL, NULL) != 1 ||
hdr[3 * (43 * width + 43)] <= 0.0) {
fputs("thin source-triangle inverse-map regression failed\n", stderr);
goto done;