Stacking Suite

SASpro's complete calibration-and-integration pipeline: from raw camera RAW/TIFF through master darks and flats, debayer, cosmetic correction, satellite trail masking, registration against any alignment model up to 4th-order polynomial, drizzle, up to multi-panel mosaic sets, multi-frame deconvolution, and a final rejected-and-weighted integrated master — all driven from one window, with CPU, CUDA, MPS and DirectML backends shared across every heavy stage.

The Stacking Suite is laid out as six tabs along the top (Quick Stack, Convert Camera RAW/TIFF, Darks, Flats, Lights, Image Integration) with a header strip above them (wrench, folder path, Change…, Open Log, document) and almost every parameter you'll want to touch living in the orange-wrench Stacking Settings dialog. The sections below walk through each tab in the order you'll meet them, then cover the Settings dialog, then the maths and plumbing behind the scenes.

Contents

Top row & the wrench

A single strip sits above the tabs and stays visible at all times:

ControlWhat it does
πŸ”§ wrench (orange) Opens the Stacking Settings dialog — profiles, distortion/transform, alignment, performance, drizzle, MFDeconv tuning, rejection, cosmetic correction, comet, comet star removal. See §7.
Current Stacking Folder Live display of the folder every intermediate file and master will land in. Reads from stacking/dir and tooltips the full path.
Change… Picks a new stacking folder. The suite creates Calibrated/, Normalized_Images/, Aligned_Images/ and Masters/ underneath it on demand.
Open Log Raises the dockable Stacking Suite log window. Everything the suite prints is tee'd to this log and to stacking_log.txt inside the stacking folder, so you have a timestamped record of every run.
πŸ“„ document This help page. Added at the far right of the row.

1. Quick Stack

The default tab, and the easiest way to get a stack out the door. Five cards walk you top-left to bottom-right through the pipeline, each with a traffic-light indicator:

Workflow
1 Darks → 2 Flats → 3 Lights → 4 Calibrate → 5 Register + Integrate.
Cards go red when nothing is loaded, yellow when raw frames are present but no master has been built, and green when the step is complete.

1.1 Card controls

1.2 Pipeline buttons at the bottom

2. Convert Camera RAW/TIFF

A standalone batch converter from camera formats (CR2, NEF, ARW, RAF, DNG, TIFF, PNG, JPEG, … — anything rawpy can read, plus TIFF via tifffile) into debayered FITS. It never touches the Lights/Darks/Flats trees — the output goes to a folder you choose.

When to use this tab: only when your lights are in DSLR raw formats and you'd rather preprocess them to FITS once than let the Lights tab decode them on every run. Modern SASpro will happily eat raw files directly through the Lights tab too, so this is primarily for mixed sessions or archival.

3. Darks

Groups raw dark/flat-dark/bias frames and turns them into master darks. The tab is split in two: raw darks on the left, controls on the right, and the master-dark list across the bottom.

3.1 Left pane: raw darks tree

3.2 Right pane

3.3 Right-click & context

Right-click any raw-dark row for Set Session Tag…. Darks are session-agnostic for integration (see 3.1), but the tag is preserved in the tree for your reference.

3.4 Master Darks list

Shows every master dark currently loaded, with its exposure and file path. Load Master Dark adds a pre-built master from disk; Clear Selection removes items from the list and persists the change.

4. Flats

Same shape as Darks but with filter awareness and per-group dark-override controls.

4.1 Left pane: raw flats tree

4.2 Right pane

4.3 Right-click & context

Right-click any flat group for:

5. Lights

Where you load the actual signal data, apply the calibration pipeline, and optionally hand off straight to registration.

5.1 Lights tree

Five columns: Filter & Exposure, Metadata, Master Dark, Master Flat, Corrections. Grouped by filter at the top level and by (exposure, size) beneath. Leaves show individual frames with their status and chosen masters.

5.2 Options row

Manual Session field: same as Flats, shared via stacking/session_keyword.

5.3 Corrections row

5.4 πŸ”¬ Tune Cosmetic Correction σ…

Opens the standalone Cosmetic Correction dialog on your selected light (or the first available). Use the β—€ / picker / β–Ά bar at the top to bounce between frames. Adjust Hot σ and Cold σ with the live preview until you're happy with how it handles hot pixels without eating star cores. “Save σ Settings” writes the values back here so calibration uses them.

5.5 Satellite trail masking

5.6 Overrides & context

6. Image Integration

The back half of the pipeline. The tree lists calibrated lights ready for registration + integration; the row stack below configures tolerances, output cropping, drizzle, registration sets, MFDeconv, comet/trail modes, GPU backend and the big red launch buttons.

6.1 Calibrated Light Frames tree

Four columns: Filter – Exposure – Size, Metadata, Drizzle (per-group indicator), Set (registration-set membership). Grouping follows filter + (exposure ± tolerance) + size, with the tolerances set below.

6.2 Tolerance & output row

6.3 Drizzle controls

6.4 Reference frame

6.5 Registration Sets

A full subsystem for independent references — use it for mosaic panels or completely different targets loaded in one run. See §9.9 for the full logic. Controls:

Each registration set integrates separately into its own master, with its own reference. Unassigned frames use the Default set and the global reference above. “Already registered” precheck decisions are reused across every set in a single multi-set run so you only get prompted once.

6.6 MFDeconv — Multi-Frame Deconvolution

SASpro's multi-frame deconvolution engine, implementing the ImageMM method of Sukurdeep et al. (2025) with original engineering extensions for real astrophotography data — most notably a rejection-aware MM update that reuses the stacking rejection maps to exclude satellite trails, aircraft, transient defects and hot-pixel outliers from the per-iteration updates (this is exactly what the Rejection Strength control blends).

Implementation whitepaper — Franklin Marek (2026): “ImageMM: Multi-Frame PSF-Aware Deconvolution — Practical Notes & Implementation Diagram”, Zenodo working paper, DOI 10.5281/zenodo.17252270 (GPL v3). Documents SASpro's forward model, MM update, super-resolution path, and the rejection-aware extension that is original to this implementation.

Controls (see §9.8 for the maths):

6.7 Comet + Star-Trail row

6.8 Comet options row

6.9 Output options

Save rejection layers with master lights — when on (default), master light FITS files are saved as multi-extension FITS (MEF) with extra HDUs containing the rejection any-mask, fraction map, and count map — useful for post-mortem analysis and for feeding back into MFDeconv. Off = smaller single-HDU FITS.

6.10 Backend row

6.11 Action buttons

7. Stacking Settings dialog (πŸ”§ wrench)

Two scrollable columns. Every parameter is live-persisted to QSettings (stacking/… keys), and OK triggers save_stacking_settings for a few derived fields. Cancel reverts the dialog's uncommitted changes but keeps anything already persisted mid-session.

7.1 Profiles (top strip)

Snapshot/restore your entire stacking configuration under named profiles. New… saves the current state under a new name; Save overwrites the selected profile; Load replaces the current configuration (and restarts the Stacking Suite dialog to force every control to re-sync); Delete removes a profile; Export… / Import… round-trip profiles via JSON. See §8 for the detail.

7.2 Distortion / Transform

7.3 Alignment

Why this matters more with the GPU detector than with SEP. SEP is pure CPU and scales cleanly to every core. The GPU star detector does not: each worker process builds its own CUDA context, so past a certain count the card thrashes instead of going faster. Measured on an 8 GB card at 12 MP, 8 workers was the optimum and 16 wedged the card outright. Leave this on Auto unless you have a reason not to.

7.4 Performance

7.5 Normalization & Gradient (ABE Poly²)

Optional 2nd-order background removal applied to every frame before stacking. See §9.3.

7.6 Drizzle (advanced)

7.7 Multi-frame Deconvolution (tuning)

7.8 Rejection

7.9 Cosmetic Correction (Advanced)

7.10 Comet (High-Clip Percentile tuning)

High-clip k / Percentile p — parameters of the High-Clip Percentile reducer used for comet nucleus stacks (defaults 1.30 / 25.0). See _high_clip_percentile in §9.6.

7.11 Comet Star Removal (Optional)

7.12 Optimize… — tuning for your hardware

Beside Rows per pass in the General column is an Optimize… button. It measures this computer rather than guessing, and writes three settings: which star detector registration uses, how many worker processes it uses, and how many rows go into one integration pass.

The answers are genuinely machine-specific. The same code on the same card picks different values at different sensor sizes, and on a high-core-count CPU with a modest GPU the CPU path can win outright. There is no constant that is right for everyone, which is why this exists.

Running it

Frame size (H / W) — use your real sensor. The answer depends on it: the same 8 GB card was happy with 16 workers at 2048×2048 and wedged solid at 3000×4000.
Frames in stack — a typical session depth. Deeper stacks shift the band-height answer; at 24 frames one machine wanted 32 rows, at 128 frames the same machine wanted 16.
Measure — the two sweeps are independent and either can be run alone.

All four inputs are remembered, saved both when you press Run and when you close the window, so a large sensor is typed once. If the lights you have loaded disagree with the remembered size, the dialog says so beside the field rather than overriding your choice.

A run takes a few minutes and the GPU will be busy. Cancel stops at the next batch boundary. A configuration that overloads the card is abandoned automatically — see §9.11 for why that matters.

Reading the result

GPU (torch) sweep: gpu x1 3.14s 30.55 frames/s gpu x2 2.21s 43.35 frames/s gpu x4 1.98s 48.47 frames/s gpu x8 2.29s 41.91 frames/s gpu: past the peak at 4 workers — stopping CPU (SEP) sweep: cpu x32 3.13s 30.67 frames/s GPU wins: 4 workers, 48.47 frames/s — 1.60x the best CPU (32 workers, 30.67 frames/s).

Each line is a real batch of detections through a spawn pool built exactly like registration's, so the numbers are what the pipeline will actually see rather than a synthetic benchmark.

The winner is not always the fastest row, and the log says why. Anything within 3% of the best worker count, or 10% of the best band height, is treated as a tie and the smaller value wins — fewer workers means less VRAM and one less CUDA start-up each; a smaller band means less host RAM. When that happens the sweep names the faster option it passed over, the gap, and the threshold:
✓ Band height 16 rows: 4.5 Mpx/s. 32 rows was fastest at 4.62 Mpx/s, but 16 is within 2.6% of it and holds 32.4 MB per band against 64.8 MB. Anything within 10% of the best counts as a tie and the smaller band wins, because band timings move about that much between runs. Set 'Rows per pass' by hand for the outright fastest.

What it writes

SettingKeyWhere it shows
Star detector (GPU torch or SEP)stacking/align/gpu_detectchosen automatically; a CPU verdict turns the GPU detector off
Registration workersstacking/align/workersAlignment workers, §7.3 — and the same box in Star Registration
Integration band heightstacking/chunk_heightRows per pass, General column

All three are ordinary settings afterwards: override any of them by hand, and they travel with profiles (§8) like everything else. Re-run the optimizer after changing GPU or camera — a stored measurement records the hardware it was taken on and is discarded if that no longer matches, with the hint beside Alignment workers saying so.

What Optimize does not tune. Only the registration solve pass and the integration band height. The calibration, measurement, write and prefetch pools are CPU- and I/O-bound and stay at full core count — capping those to a GPU-derived number would make stacking slower, not faster.

8. Profiles

Profiles exist to let you save a complete working configuration — sigma thresholds, weighting mode, drizzle, MFDeconv tuning, cosmetic sigmas, every setting in §7 — under a name, and bring it back later.

9. Technical details

What the suite actually does under the hood, in the order a stack passes through it.

9.1 Calibration pipeline

Calibration is a two-phase pipelined worker system (calibrate_lights):

Phase 0 — master upload. Every master dark, master flat and master bias is loaded to CPU, normalized where needed, and uploaded to the GPU exactly once. Interactive flat-strength dialogs (if enabled) are collected upfront so you can configure and walk away.

Phase 1 — three-thread pipeline. A producer thread reads raw lights from disk; a GPU thread runs dark subtraction, flat division and cosmetic correction; a consumer thread writes calibrated frames to Calibrated/. The three run concurrently so I/O, compute and I/O overlap. Per frame:

P → L' = \mathrm{clip}(L + P - D_{\mathrm{scaled}},\;0,\;\infty)
C = L' / F_{\mathrm{norm}}

where P is the pedestal (if enabled), D is the chosen master dark (bias-scaled when a master bias is loaded), and F_norm is the normalized master flat (median = 1 per channel). With interactive flat strength on, the effective flat is

F_{\mathrm{eff}} = 1 + \alpha(F_{\mathrm{norm}} - 1)

with your chosen α. Cosmetic correction then replaces hot and cold outliers with a 3×3 ring median using a MAD-based sigma estimate:

\sigma = 1.4826 \cdot \mathrm{MAD} = 1.4826 \cdot \mathrm{median}(|x - \mathrm{median}(x)|)
x_i \to \mathrm{median}(\mathrm{ring}_i)\quad \mathrm{if}\;|x_i - m_i| > k \cdot \sigma_i

with k taken from Hot σ / Cold σ. Patches above the protect_sigma threshold are left untouched (star / nebula cores). The GPU uses unfold for the 3×3 neighbourhoods; the Numba fallback handles cold-cluster guards.

Phase 2 — satellite trail removal. After Phase 1 fully completes, every master tensor and cosmetic buffer is freed and the CUDA cache emptied, then the satellite CNN runs on the saved frames one at a time. Separate phases mean the CNN never competes with the calibration working set for VRAM — critical on small cards where GPU memory cannot page to host.

9.2 Master-frame integration

Master darks group by (exposure, size, temperature bucket, gain, offset); master flats by (filter, size, session). Each group is read in row-band tiles sized for your RAM budget, stacked with the Calibration Rejection Algorithm (Winsorized Sigma Clipping by default), and written as 32-bit FITS with a header merged from the input group's representative frame.

Temperature bucketing:

T_{\mathrm{bucket}} = \mathrm{round}(T / \Delta T) \cdot \Delta T

with ΔT = 1°C by default (stacking/temp_group_step). Bias scaling for CCD users:

B^{*} = B + (\mathrm{median}(L) - \mathrm{median}(B))

9.3 Normalization & gradient

Before registration, each frame is normalized to a common target median. The method is min-offset + median-match on the luminance channel, with no clipping:

L = 0.2126\,R + 0.7152\,G + 0.0722\,B
f' = (f - \min L)\cdot \frac{m^{*}}{\mathrm{median}(L) - \min L}

Rec. 601 luminance for mono treatment of RGB, so colour balance is preserved and only the overall level is matched.

Optional ABE-style poly² gradient removal fits the model

G(x,y) = a + bx + cy + dx^2 + exy + fy^2

to robustly selected background sample points on a downsampled frame, then either subtracts or divides it back on the full resolution. The “Skip if strength <” guard compares peak-to-peak of G against the frame's robust noise so clean skies pass through untouched.

9.4 Registration

Reference selection: either you lock a frame (Pick… / Select Reference Frame), or the suite measures every candidate by its combined quality score (see §9.5) and picks the best. Reference lock survives tab changes; auto-accept unlocks it.

Each non-reference frame is solved against the reference with astroalign-style triangle matching on detected stars (SEP, with filters: min area, min FWHM, max ellipticity), then refined by the chosen transform model:

Accurate mode re-runs the solve 3 times with the previous solution as warm start; shift tolerance and max shift gate acceptance. Rejected frames are logged and skipped.

9.5 Frame weighting

Every frame gets a weight from five measured terms — detected star count (c), mean eccentricity (ε), background level (bg), FWHM proxy, and MAD-based noise estimate — combined multiplicatively with per-mode exponents:

w = c^{a_c}\,(1 - \varepsilon)^{a_e}\,b^{a_b}\,s^{a_s}\,n^{a_n}

with

b = \frac{2}{\sqrt{\mathrm{bg}} + 1} - 1
s = \mathrm{clip}\!\left(\frac{\mathrm{FWHM}_{\mathrm{ref}}}{\mathrm{FWHM}},\;\frac{1}{3},\;3\right)
n = \mathrm{clip}\!\left(\frac{\mathrm{noise}_{\mathrm{ref}}}{\mathrm{noise}},\;\frac{1}{3},\;3\right)

The modes change only the exponents (as):

Modecounteccbgsharpsnr
Balanced (default)1.01.01.01.00.0
PSF-weighted0.51.00.52.00.0
SNR-weighted0.50.51.00.52.0
Star-count-weighted2.00.50.50.50.0

The sharpness and SNR ratios are measured against the population median and clamped to [1/3, 3] so a single mismeasured frame can't dominate. A frame measured far tighter than the population (ratio < 0.4× or < 0.25×) is treated as a mismeasurement and set neutral.

9.6 Rejection algorithms

Rejection runs per output pixel across the registered stack, honouring per-frame weights where applicable. All reducers are implemented in NumPy (CPU) and PyTorch (GPU; cuDNN-free fallback and DirectML workarounds included). Each writes a 3-layer rejection map alongside its scalar output: any-mask, fraction (fraction of frames rejected), count (total frames rejected). These layers can be saved into master lights as additional FITS HDUs.

Weighted Winsorized Sigma Clipping (default)

The Pix Insight / APP style: seed with median and robust sigma, Winsorize at k sigma, re-measure, iterate:

\mu_{\mathrm{win}} = \mathrm{mean}(\mathrm{clip}(x,\,\mu - k\sigma,\,\mu + k\sigma))
\sigma_{\mathrm{winsor}} = 1.134\,\sqrt{\mathrm{Var}(x_{\mathrm{clipped}})}

The 1.134 factor is Huber's exact correction for k = 1.5. Outliers (satellite trails) cannot inflate σ, so the rejection bounds track the clean distribution; final output is the weighted mean of survivors only.

Kappa-Sigma Clipping

\mathrm{keep}_i = (\mu - \kappa\sigma \leq x_i \leq \mu + \kappa\sigma)

Median center, std of the kept sample, monotone shrink (rejected pixels stay rejected across iterations), weighted mean of survivors.

Extreme Studentized Deviate (ESD)

Generalized ESD (Rosner), externally studentized, vectorized per pixel. Sequentially removes the most extreme remaining value and tests it against the leave-one-out mean/std of the remaining sample (defeats masking); no early stop — the outlier count is the largest step whose test passed. The critical value adjusts for sample size:

R = \frac{|x_i - \overline{x}|}{s},\quad R > \lambda_{n,\alpha}
\lambda_{n,\alpha} = t_{n-2,\,1-\alpha}

so your threshold keeps its literal “sigmas” meaning on large stacks and inflates automatically on small ones (controls the false-rejection cascade a fixed cutoff would cause).

Biweight Estimator (Tukey)

\hat{\mu} = M + \frac{\sum_{|u_i|<1} (x_i - M)(1-u_i^2)^2}{\sum_{|u_i|<1}(1-u_i^2)^2},\quad u_i = \frac{x_i - M}{c\,\mathrm{MAD}}

with c from the tuning-constant setting. Smooth down-weighting of outliers, no hard clip.

Trimmed Mean

\overline{x}_{\alpha} = \frac{1}{N - 2k} \sum_{i=k+1}^{N-k} x_{(i)},\quad k = \lfloor \alpha N \rfloor

Drops the lowest and highest αN frames at each pixel before weighted averaging.

Modified Z-Score Clipping (Iglewicz & Hoaglin)

M_i = \frac{0.6745\,(x_i - \mathrm{median}(x))}{\mathrm{MAD}}

Rejects where |M_i| exceeds the threshold. Robust to small samples where mean/std fail.

Simple Average, Simple Median, Max Value

No rejection. Max Value is the engine behind Star-Trail mode: per-pixel maximum across all frames.

Comet reducers

Specialised for comet-aligned stacks where stars become trails that must be rejected: Comet Median, Comet High-Clip Percentile (parameters k and p in §7.10), Comet Lower-Trim (30%), Comet Percentile (40th). The high-clip percentile combines a percentile-centre with a sigma-style cap:

P_{40}(x_{xy}) = \mathrm{quantile}_{0.40}(x_{xy})
\mathrm{keep}: x_i \leq \mu + k\,|x_i - P_p|

9.7 Drizzle

Variable-pixel linear reconstruction, after Fruchter & Hook (2002):

I'(X,Y) = \frac{\sum_i w_i\,A_i(X,Y)\,I_i(x_i,y_i)}{\sum_i w_i\,A_i(X,Y)}

where each input pixel is “drizzled” onto the output grid as a kernel of area A_i(X,Y) centred on the warped position. The kernel is:

Scale 2× / 3× means the output grid has 4× / 9× the pixels of the input. With CFA Drizzle on, R/G/B samples from the Bayer pattern are drizzled directly into the three output channels — no interpolation step, which is the only way to gain resolution from one-shot colour.

9.8 Multi-frame deconvolution

ImageMM — a majorization-minimization (MM) framework for joint multi-frame restoration and super-resolution (Sukurdeep et al. 2025). The latent sky image I is recovered by iteratively minimising a surrogate of the robust data fit across all frames, with per-frame PSFs H_j and per-pixel noise variances v_j. The multiplicative update, with your κ cap and ρ loss:

I^{(k+1)} = I^{(k)} \cdot \mathrm{clip}\!\left(\frac{\sum_j H_j^{T}\!\left(\rho'(d_j)/v_j\right)}{\sum_j H_j^{T}(1/v_j)},\;\frac{1}{\kappa},\;\kappa\right)

where H_j is the forward operator (blur + warp + down-sample) for frame j, d_j = D_j - H_j I^{(k)} is the data residual, and v_j is the per-pixel variance:

v_j(x,y) = g\,I_j(x,y) + \sigma_r^2

Huber loss (L2 if you prefer the quadratic regime everywhere):

rho(d) = (1/2) d^2 if |d| <= delta delta|d| - (1/2) delta^2 if |d| > delta

Negative δ means scale×RMS (so δ = −2 uses 2×RMS as the cutoff). Lower |δ| → more pixels treated as outliers.

Star masks protect stellar cores from ringing by clamping update ratios near 1:

M = 1 - \mathrm{keep\_floor} \cdot (1 - M_{\mathrm{raw}})

Super-resolution reconstructs I on an r× grid; each H_j then includes an r× down-sampling step. Cost scales roughly as r^4.

Rejection-aware MM — SASpro extension (Marek 2026, DOI 10.5281/zenodo.17252270). Before MFDeconv runs, the suite performs its usual robust integration prepass and keeps the per-frame rejection maps that would normally live in the master light's extra FITS HDUs (any-mask, fraction, count — see §9.6 and §6.9). Those rejected pixels — satellite trails, aircraft, cosmic rays, transient defects, hot-pixel outliers — are then excluded from the forward operator for the frames that saw them, so the sums Σ_j H_j^T(·) only run over good samples per output pixel. The Rejection Strength control (§6.6) blends between the rejection-ignoring solve (0.0) and the fully rejection-aware solve (1.0); intermediate values run both endpoints and interpolate the final image. This is why the suite recommends leaving Save rejection layers with master lights on when you plan to run MFDeconv — those layers are the extension's input.

9.9 Registration Sets

A set is an orthogonal partition that sits above the filter / exposure grouping. Each set has its own reference and integrates to its own master. The queue runs sets serially, buffering the per-set summaries and flushing them as a single run report; the “already registered” precheck choice is remembered across all sets in one run so you aren't prompted repeatedly.

Filename-based assignment uses a regex equivalent to

keyword[\s_\-.]([^\s_\-.]+)

so M31_Panel 1_2024-01-02.fit, M31_Panel_02.fit and M31.Panel.3.fit all match. Unmatched frames stay in the Default set, which keeps the global reference.

9.10 GPU backends

The Stacking Suite shares one PyTorch runtime (installed via Install/Update GPU Acceleration…) across every heavy stage: calibration, cosmetic correction, satellite CNN, rejection reducers, MFDeconv. Backend auto-detection picks the first available of CUDA, MPS (Apple Silicon), DirectML (Windows GPUs via torch-directml), falling back to CPU. The stacking/use_hardware_accel master toggle disables the GPU path app-wide.

DirectML needs a workaround for a known torch-directml bug: nanmedian/nanquantile/nanstd can return empty tensors, so those reductions are forced to CPU and the results moved back to device (one-time warning per session). CUDA uses PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True to reduce fragmentation between the cosmetic pass and the satellite CNN; a device-wide RLock serialises the two so they don't both resident at once (which OOMs small cards on Linux/CUDA where VRAM cannot page).

Supported Python runtimes for GPU install: 3.12, 3.13, 3.14. The installer refuses older/newer versions with platform-specific tips.

9.11 Hardware tuning — how the optimizer decides

Two independent questions, measured separately because one does not imply the other. Worker count is about how many processes may hold a CUDA context at once; band height is about how much work goes into one rejection call in the parent process.

Why registration needs a worker cap at all

star_alignment._make_executor spawns up to max(2, min(cpu_count, 32)) processes and every one of them calls find_transform. With SEP that pool was free parallelism — pure CPU, no shared resource. With the GPU detector each worker builds its own CUDA context, several hundred megabytes and seconds of start-up apiece, and the pool becomes a contended one. Measured on a 36-core box with an 8 GB card, 24 frames of 2048×2048:

workersGPU frames/sSEP frames/s
133.115.33
498.4022.31
8108.6241.00
1676.1251.23
320.1170.95

32 workers on the GPU is not a plateau, it is a thousandfold collapse with the card pegged at 100% — 32 CUDA contexts do not fit in 8 GB, so it thrashes. Note also what the last column says about the comparison: SEP spreads over every core, so on a 36-core machine the GPU's advantage through the pool is 1.5×, not the 8× a single-frame measurement suggests. The GPU wins by more as core count falls, which is most users.

Resolution order

When registration asks how many workers to use, the answer comes from the first of these that applies:

  1. an explicit stacking/align/workers above zero — honoured exactly as given, never second-guessed;
  2. a stored measurement whose hardware fingerprint and frame size still match;
  3. a VRAM-derived default, roughly 400 MB per worker plus 25 MB per megapixel against 75% of the card — 8 workers on an 8 GB card at 12 MP, 3 at 47 MP.

So an install that never opens the optimizer still gets a safe number, and one that does gets a measured one.

The watchdog

A worker count too large for the card does not fail, it thrashes: the batch never returns and the GPU sits at 100%. Cancellation cannot help, because a task already running on a wedged device will not notice it. So each batch gets a budget derived from the measurement itself — 1.5× the single-worker time for the same batch, since N workers can never legitimately be slower than one — and on expiry the child processes are terminated, joined and killed. A rung that blows its budget ends the climb; the rung below it stands.

One exception: a timeout on the first rung does not end the sweep. One worker is the slowest configuration there is, so a timeout there says the batch is too big for a single process, not that the backend is hopeless — the sweep keeps climbing until a rung lands and sets the scale.

The ladder (1, 2, 4, 8, 12, 16, 24, 32) is coarse, so after the climb stops up to two midpoint probes run between the best rung and whatever ended the sweep above it. If 16 works and 24 is abandoned, 20 and then 22 are tried.

Band height

torch_rejection and torch_calibration have no worker count — they run in the parent process and the former already serialises GPU access through its own lock. Their tunable is the number of full-width rows handed to one rejection call. Measured on a 3070 Ti at 12 MP, 24 deep, time is almost perfectly linear in rows and throughput varies only 1.2× across a 64× range of band sizes, so integration is bound by the rejection arithmetic itself — three iterations of nanmedian/nanstd, each sorting N values per pixel — not by per-band overhead. The shipped default of 16 is a sound starting point; the optimizer's job here is confirming that on your hardware rather than finding a large win.

Band timings move around 18% between runs, which is why the band tie-break is 10% where the worker one is 3%. Tightening it does not help: on one measured curve a 3% threshold picks 128 rows — eight times the band RAM — to chase a difference smaller than the run-to-run noise.

9.12 File layout & cleanup

Inside your chosen stacking folder the suite creates:

FolderContents
Masters/Master darks, flats, biases — keep these.
Calibrated/Calibrated lights (*_c.fit). Delete after integration if disk is tight.
Normalized_Images/Normalized, pre-registration frames. Intermediate; always deletable.
Aligned_Images/Registered *_r.fit frames. Keep if you plan to re-integrate with different rejection.
loose .dat / .npy / .tmpMemmap scratch, transform caches, star catalogues. Always deletable.
stacking_log.txtTimestamped log of every run.

Both Clean Up buttons remove the intermediate folders and loose scratch files, but never Masters/ or Calibrated/.

Credits. SASpro's Stacking Suite is written and maintained by Franklin Marek (SetiAstro). It integrates many standard astronomy-image-processing conventions, which are credited to their originators here. Pipeline design, the quick-stack card UI, the shared GPU runtime, the three-thread calibration pipeline, Registration Sets, dual-band advanced split, the frame-weighting modes, and the per-group dark/flat overrides are original to this suite.