Releases: dmarkham/fits
v1.1.0 — stats: hot-path API additions
New functions
Five additions to the stats package, all designed to support hot-path stacking pipelines that need to avoid allocations on every call. Existing public APIs (Percentile, Median, MAD, MADWithMedian, MeanStdev) are semantically unchanged.
Zero-alloc *Buf variants
const MaxHistoSize = 65536
func PercentileBuf[T Numeric](data []T, p float64, histo []uint32) float64
func MedianBuf[T Numeric](data []T, histo []uint32) float64
func MADWithMedianBuf[T Numeric](data []T, histo []uint32, absDev []float64) (median, mad float64)Caller supplies the scratch slices (typically pre-allocated once per goroutine). The histo slice is zeroed and overwritten on each call, so no manual reset is needed between calls. Both buffers are bounds-checked — undersized inputs panic with a clear message.
QuickSelect
func QuickSelect[T Numeric](data []T, k int) TGeneric line-for-line port of Siril's quickmedian_float (sorting.c) — Hoare partition with middle-element pivot, in place. For small-n medians (e.g. the 21–41 pixel rejection columns) it is both faster and more accurate than the histogram method, since it returns an exact element instead of a histogram-interpolated value.
NaN handling: caller must pre-filter; NaN comparisons break partitioning.
MeanStdevSiril
func MeanStdevSiril(data []float32) (mean, stdev float32)Bit-for-bit port of Siril's siril_stats_float_sd (statistics_float.c). Two-pass algorithm with float32 mean truncation between passes and float32 deviation arithmetic, returning float32 to preserve rejection-boundary precision. Rejection pipelines using this fix a visible artifact (red hotspot in stacked output) traced to ULP-level differences at sigma-clip boundaries vs the generic float64 MeanStdev.
Benchmarks
Measured on AMD Ryzen AI 9 HX 370. n=50 is the rejection-step size; n=500_000 is the overlap-norm size.
| Function | Allocating | Buf | Speedup | Allocs eliminated |
|---|---|---|---|---|
| Median (n=50) | 229 ns / 208 B / 1 alloc | 134 ns / 0 B / 0 | 1.71× | 1 alloc |
| Median (n=500K) | 1411 µs / 262 KB / 1 alloc | 1195 µs / 0 B / 0 | 1.18× | 262 KB |
| MAD (n=50) | 680 ns / 624 B / 2 allocs | 354 ns / 0 B / 0 | 1.92× | 2 allocs |
| MAD (n=500K) | 6238 µs / 4.27 MB / 2 allocs | 5481 µs / 0 B / 0 | 1.14× | 4.27 MB |
| Percentile p99 (n=50) | 240 ns / 208 B / 1 alloc | 200 ns / 0 B / 0 | 1.20× | 1 alloc |
| MeanStdev (n=50) | 38 ns / 0 / 0 | (Siril) 15 ns / 0 / 0 | 2.58× | — |
| MeanStdev (n=500K) | 379 µs / 0 / 0 | (Siril) 248 µs / 0 / 0 | 1.53× | — |
| QuickSelect (n=50) | — | 66.84 ns / 0 / 0 | 2.49× faster than MedianBuf at n=50 | — |
Highlights
MeanStdevSirilis 2.5× faster than the genericMeanStdevat small n in addition to fixing the rejection-boundary precision — for 150M-call use cases this is both a correctness fix and a perf win.QuickSelectis 2.49× faster thanMedianBufat n=50, and returns an exact element instead of a histogram-interpolated value. Recommended for the small-n median use case.- Every Buf variant is verified bit-for-bit equivalent to its allocating counterpart across the full fixture suite.
v1.0.4 — stats: MeanStdev N-1 denominator
Bug fix
stats.MeanStdev now uses the Bessel-corrected N-1 denominator (sample standard deviation) to match Siril's siril_stats_float_sd. Previously it used N (population stdev), causing a small systematic difference for any code cross-validating against Siril's stacking pipeline.
n < 2returns 0 stdev- Golden fixture stdev values updated; median/MAD/percentile values unchanged
Not changed — worth noting
Cross-validation showed that Median, MAD, and Percentile already match Siril's findMinMaxPercentile bit-for-bit in float32 arithmetic, verified by TestPercentile_Siril (1e-6 tolerance) and TestAutostretch_Siril (1e-4 tolerance on real kstars data). No changes needed there.
v1.0.3
Fix: WriteImage BSCALE/BZERO round-trip
WriteImage now applies BSCALE/BZERO inverse scaling before serialization when the header contains these keywords. Integer BITPIX formats (8/16/32/64 bit) are clamped to the target type's range to prevent wraparound — matching Siril's savefits() behavior. Float BITPIX (-32/-64) preserves the full range.
This fixes a gap where the doc promised BSCALE/BZERO round-trip fidelity but the code did not apply the inverse scaling.
Full changelog
v1.0.2
New: fits/stats package
Generic, NaN-aware statistics functions for astronomical image data. Extracted from existing cross-validated implementations and validated against Siril's findMinMaxPercentile via a C++ reference harness.
| Function | Description |
|---|---|
MinMax[T] |
Min and max, skipping NaN |
Mean[T], MeanStdev[T] |
Arithmetic mean and population stdev |
Percentile[T], Median[T] |
Histogram-based interpolated percentile (Siril port) |
MAD[T], MADWithMedian[T] |
Median Absolute Deviation (raw, no 1.4826) |
BuildHistogram[T] |
Returns Histogram struct with CDF() and Percentile() methods |
SigmaClip[T] |
Iterative sigma clipping with mean or median center |
FilterNonZero[T] |
Zero/NaN exclusion (astronomical blank-pixel convention) |
Refactor
stretch/mtf.go now calls stats.Median, stats.MAD, stats.FilterNonZero instead of private copies. Zero regression on all 18 Siril stretch golden fixtures.
Full changelog
v1.0.1
What's new
-
stretch.NormalizeChannels— scales all channels to [0, 1] using a shared global min/max, preserving relative intensity between channels. This is the input-side companion to the existing stretch pipeline helpers. -
macOS and Windows support — fixed path separator in the journal crash-recovery code. The library is now portable across Linux, macOS, and Windows.
Fixes
- Hardcoded developer paths replaced with
FITS_ASTROPY_PYTHONenv var (astropy cross-validation tests skip gracefully when not available) - Broken cfitsio submodule reference removed
plan.mdscrubbed for public visibility
Full changelog
v1.0.0
fits v1.0.0
Pure-Go FITS library for reading, writing, and processing astronomical data files. No cgo, no external dependencies.
What's included
| Package | Capability |
|---|---|
fits |
FITS file I/O — images (all BITPIX), binary tables (VLAs, heap), ASCII tables, headers (CONTINUE, HIERARCH), in-place edit, streaming rebuild, image.Image adapter |
fits/compress |
Tile compression — all 6 algorithms (RICE_1, GZIP_1, GZIP_2, HCOMPRESS_1, PLIO_1, NOCOMPRESS), float quantization with SUBTRACTIVE_DITHER_1/2 |
fits/wcs + fits/wcs/transform |
World Coordinate System — all 27 Paper II projections + HEALPix, SIP/TPV/TNX distortion, 6 sky frames (ICRS/FK5/FK4/galactic/ecliptic/supergalactic) |
fits/healpix |
HEALPix pixel indexing — RING + NESTED ordering, ang2pix/pix2ang, ring↔nest conversion, 8-connected neighbors |
fits/stretch |
Image stretching — 18 presets (autostretch/MTF, asinh, GHT, linear, CLAHE) matching Siril within 1e-4 |
Cross-validation
Every algorithm is ported from a reference implementation and validated against it:
- cfitsio — byte-for-byte round-trip on all fixtures; float quantization bit-exact on 11 golden cases
- wcslib — 14 golden fixtures, bit-exact on all 27 projections
- astropy — bidirectional compression interop (Go writes → astropy reads, and vice versa)
- healpy — 42 golden fixtures, integer-exact pixel indices, 1e-10 rad angles
- Siril — 18 stretch presets validated within 1e-4
Platforms
Linux, macOS, Windows. Pure Go — go get github.com/dmarkham/fits@v1.0.0
Philosophy
This library is not trying to replace cfitsio, wcslib, healpy, or Siril. The goal is to bring Go onto level footing with the existing ecosystem while staying as compatible as possible.