Skip to content
dmarkhamPublic

About

Pure-Go FITS library — read, write, WCS, tile compression, HEALPix, image stretching. Cross-validated against cfitsio, wcslib, astropy, healpy, and Siril.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

38 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fits

Pure-Go FITS library for reading, writing, and processing astronomical data files.

Philosophy

This library is not trying to replace cfitsio, wcslib, healpy, or any other established C/C++/Python implementation. Those tools have decades of production mileage and are the right choice for many workflows. Our goal is to bring Go onto level footing — giving Go programs native access to the same FITS capabilities without cgo, while staying as compatible as possible with the files and conventions those reference implementations define.

Algorithms are ported from the reference source (cfitsio for I/O and compression, wcslib 8.10 for WCS, healpix_cxx — healpy's backend — for HEALPix, Siril 1.4.0 for statistics and stretches) and checked against that reference. Where our output differs from the reference, we treat it as a bug in our code, unless the difference is a deliberate, documented choice (for example, refusing a header that wcslib would silently misread). Every such difference is listed in docs/VALIDATION.md.

Guiding principle

This library is 100% scientific fidelity. It returns the bytes, values, and types exactly as the file stores them. It does not auto-scale, auto-window, auto-percentile, auto-display, silently coerce types, or silently map NaN/BLANK to zero. Where it cannot represent something exactly, it returns an error rather than a wrong value. Any interpretive helper that rescales, reshapes, or transforms data for display belongs in a sibling package (stretch, stats) — never in the root fits package.

One deliberate exception: fits.ReadFloat32, ReadFrame and ReadFrameRGB are processing-pipeline helpers that normalise integer data to [0, 1] float32 (the Siril convention). Their exact rules (BLANK to NaN, rounding to the float32 nearest the exact value, errors for BSCALE = 0, float32 overflow and a BSCALE / BZERO / BLANK card that is not a valid value card; a duplicated scaling card is applied as cfitsio and astropy apply it: the last card on a plain image, the first on a tile-compressed one) are in the ReadFloat32 doc.

How claims are proven

Every capability below is backed by a committed test that compares this library against an independent truth source — libcfitsio 4.7.0, fpack/funpack, fitsverify, astropy 8.0.2, wcslib 8.10, WCSTools 3.9.7, healpy 1.20.1 / healpix_cxx 8.3.0, Siril 1.4.0 compiled from source, OpenCV 5.0.0, numpy 2.4.6, mpmath — using committed samples and goldens that are regenerated by committed harnesses. docs/VALIDATION.md is the ledger: capability → proving test → truth source and version → sample files → tolerance, plus the list of what is not supported or not yet proven, and how to regenerate every golden. TestValidationLedger fails if the ledger names a test or sample that does not exist.

Testing

go test ./...                    # needs only Go
go test -tags interop ./...      # needs the reference tools below

go test ./... needs nothing but Go and the files committed in the repository: no C compiler, no make, no cfitsio, fpack, funpack, fitsverify, wcslib or WCSTools program, no Python, no environment variables, no network. Every test compares the library with committed goldens, samples and recorded tool readings. The proofs still hold there: where a test judges a file the library writes by an external reader (cfitsio, fitsverify, funpack, astropy, wcslib), the reader's reading of exactly those bytes was recorded when the tools verified them (testdata/recorded), so the test's assertions run on the real reading, and a writer that produced different bytes fails. No skip is caused by a missing tool; the few skips left are platform ones (editing on Windows, a file system without POSIX ACLs).

go test -tags interop ./... is for golden regeneration and development: it runs the reference programs live (and the *Reproduce* tests regenerate every golden). It needs the C harnesses built (make -C <dir> for each Makefile in the tree, against libcfitsio 4.7.0, wcslib 8.10, healpix_cxx, glib2 and OpenCV), fitsverify, fpack, funpack, xy2sky/sky2xy (WCSTools 3.9.7), HPXcvt, go, qemu-aarch64-static, and the environment variables FITS_ASTROPY_PYTHON (a Python with numpy 2.4.6, astropy 8.0.2, healpy, mpmath and opencv-python-headless; the repo venv), FITS_SIRIL_SRC (Siril 1.4.0 source tree), FITS_HEALPIX_CXX (healpix_cxx 8.3.0 source) and FITS_SIRIL_CLI (a siril-cli 1.4.0, stretch/testdata/build_siril_cli.sh). A missing one is a failure that names it. FITS_RECORD=1 re-records the readings of the tests that pass. docs/VALIDATION.md ("Test tiers") has the details. A missing committed golden or sample is a failure in both tiers.

Capabilities

Files, images, headers (fits, fits/header)

  • HDU framing matches cfitsio (fits_get_hduaddrll) for every sample, including random groups (sized per FITS eq. 4, then refused with ErrRandomGroups), stray NAXISn beyond NAXIS, trailing zero/junk blocks and a missing final pad (File.TrailingBytes reports the ignored bytes), and an extension of a type the library does not decode (XTENSION = 'FOREIGN', framed like cfitsio, then refused with ErrUnsupportedExtension). A truncated trailing XTENSION header is an error (stricter than cfitsio, like astropy). A malformed non-structural card does not make a file unopenable; HeaderOf returns the parse error. HDUByName returns the first HDU (primary included) whose EXTNAME matches case-sensitively. Header lookups are case-insensitive and act on the first card of a duplicated keyword, as cfitsio's. The exception is image scaling, which follows one rule on every image path (ReadPixels, ReadPixelsMasked, CanConvert, ImageHDU.BSCALE/BZERO/BLANK, ReadFloat32, ReadFrame, AsImage, OverwritePixels, WriteImage's caller cards, tile-compressed images): of duplicated BSCALE/BZERO/BLANK cards the last applies on a plain image and the first on a tile-compressed header, as both cfitsio and astropy read them; a card the two references read differently is ErrInvalidScaling (a value that is not a valid FITS number, a lowercase or misplaced-= card, HIERARCH BSCALE/BZERO/BLANK, a BLANK outside the BITPIX range or equal to 1234554321).
  • Image read (ReadPixels, ReadPixelsMasked, ReadSubset, CanConvert) for every BITPIX: float targets equal cfitsio's TFLOAT/TDOUBLE reads bit for bit (beyond float32 range we return ±Inf, cfitsio ±FLT_MAX); integer targets are accepted only when they hold every possible physical value exactly (otherwise ErrTypeMismatch), and are computed in 128-bit integer arithmetic, so int64 and uint64 (BZERO 2^63) are exact. The BLANK/NaN mask equals cfitsio's null flags.
  • Image write (WriteImage, AppendImage, OverwritePixels): the data unit is byte-identical to cfitsio's for every BITPIX and convention in a 53-case matrix (uint16/32/64 via BZERO 2^15/2^31/2^63, int8 via BZERO -128, caller BSCALE/BZERO including negative and non-integer, up to both edges of the stored range). One exception: BITPIX 64 with an integral BZERO other than 0 and 2^63 stores v - BZERO exactly, where cfitsio rounds it through a double (e.g. 2^62 + 1 with BZERO 1000). A value that cannot be stored is never clamped: WriteImage/OverwritePixels return ErrNumOverflow and write nothing, exactly where cfitsio reports NUM_OVERFLOW, and also where cfitsio stores a wrong value silently (BITPIX 64 scaled value of exactly 2^63, float scaled to ±Inf). Header bytes are not compared with cfitsio's. cfitsio, fitsverify and astropy read the written files back identically.
  • CHECKSUM/DATASUM: ChecksumStatus/VerifyChecksum agree with fits_verify_chksum; WriteChecksum output verifies in cfitsio and fitsverify.
  • Header cards decode exactly as cfitsio reads them (key, value, type, comment) for every card of every sample, including malformed cards (kept as TypeUnparsed with RawValue), integers beyond int64 (TypeBigInt, read with Header.BigInt; Int returns ErrIntOverflow), CONTINUE long strings and HIERARCH. Unmodified headers re-encode byte for byte (Flush, CopyHDUWithHeader). Cards copied from a file into a new HDU keep their original bytes (Card.RawCards). Cards built in memory are read back with identical values by cfitsio and astropy, or the write returns an error. Trailing blanks of string values, value-card comments and commentary text are not significant in FITS and are not written (cfitsio, astropy and this package drop them on read). fitsverify reports no errors for headers built with Set/Add/AddCard and for cards copied from a file. A caller header with a decoded lowercase (non-HIERARCH) keyword is refused with header.ErrInvalidKeyword by WriteImage, AppendBinaryTable and AppendCompressedImage (readers disagree on such a card); upper-case the copy's Key to write it.
  • Header write rules: keywords longer than 8 characters need HIERARCH and HIERARCH paths need single blanks (ErrInvalidKeyword); non-ASCII text is refused (ErrInvalidText); CONTINUE cannot be set; COMMENT/HISTORY take one text; a CONTINUE'd string ending in & is refused; NaN/±Inf are refused; a RawValue is accepted only when it reads back unchanged; Header.AddCard adds a card under the same rules; Encode refuses cards that would join or split on read. The writers return an error for an invalid in-memory card or non-ASCII column Name/Unit/Display; they never drop a card silently.

Tables

  • Binary tables: every TFORM (L X B I J K A E D C M, scalar and vector, P/Q variable-length arrays of every element type), TNULL, TSCAL/TZERO including the unsigned conventions, read and written. Every numeric column of every sample is read as every Go target type and compared with the exact expected outcome: integer targets never truncate or wrap (ErrTypeMismatch) on the exact paths; integer TZERO literals are read exactly even where no double holds them. String arrays (TDIM (w, d2, ...) and rAw substring arrays) are read per element with ReadStringArrayColumn; writing them, and layouts the references disagree on, are errors. Not supported: TSCAL/TZERO on complex columns (an error), a general TSCAL inverse on write (with TScal/TZero the caller writes stored values). Under TSCAL 1 and an integer TZERO, float targets get the exact value correctly rounded (float32 targets: the float32 nearest it); integer targets on the float64 scaling path get the exact value or ErrTypeMismatch.
  • ASCII tables read (exact integers at any size, implied decimals, TNULL per §7.2.2) and write (AppendASCIITable; Fw.d/Ew.d/Dw.d, TNULL; no TSCAL/TZERO/TDISP).

Editing

  • OpenForEdit + Flush: a same-size header rewrite uses an undo journal (<name>.journal, pre-image + CRC; removing it is the commit point). A header resize, and EditFile, write <name>.fits-tmp, fsync it, rename it over the file and fsync the directory. After a process crash at any step, the next OpenForEdit or EditFile recovers the file to exactly the original or exactly the result; until then a read-only Open fails with ErrPendingJournal (for any journal, including one from an older release). OpenForEdit and EditFile report ErrLegacyJournal for a journal written by an older release, which cannot be recovered automatically. Power-loss safety rests on the fsync order (proven by call order, not by a power cut). OverwritePixels and appends in edit mode run under the same undo journal. Editing works on Linux and macOS (ErrEditUnsupported elsewhere, e.g. Windows); on macOS every sync is F_FULLFSYNC and an ACL is not carried over by the temp-file rewrite (see docs/VALIDATION.md). A second OpenForEdit/EditFile of the same file gets ErrLocked; an in-place write (Flush without a resize, OverwritePixels, an append in edit mode) while the file is open for reading through this package gets ErrOpenForReading and writes nothing. Create refuses ErrPendingJournal, ErrLocked and ErrOpenForReading and holds the edit lock until Close. The locks are advisory: programs that do not use this package are not stopped.
  • Close does not flush; use Flush or CloseAndFlush. Flush and CopyHDUWithHeader refuse structural keyword changes, any card the encoder cannot represent exactly, and a HIERARCH card whose path is a standard keyword name (HIERARCH BSCALE: cfitsio reads it as BSCALE, astropy does not), leaving the file byte-identical; every writer refuses such a card too. A card named END that is not the END card (HIERARCH END = ... included) is ErrMalformedEND.
  • HDUByName uses the first EXTNAME card of each HDU. It differs from cfitsio's fits_movnam_hdu: the match is case-sensitive, there is no HDUNAME fallback, a non-string EXTNAME is no name, so it can return a different, later HDU than cfitsio.
  • EditFile converts HDUs that change position exactly like cfitsio fits_copy_hdu (including its EXTEND / standard-COMMENT rules; a converted header drops reserved blank cards). New images go through WriteImage(out, ...) and become IMAGE extensions after position 0; a Writer cannot create tables. CopyHDUWithHeader refuses a changed data size or HDU kind. EditFile refuses empty output, keeps the file mode and follows symlinks. Tile-compressed HDUs can be copied.
  • cmd/fitsdump exits 1 when an HDU, a header or a table's columns cannot be decoded (reported in place). cmd/fitscopy copies byte for byte, exits 3 when the copy differs and 2 when input and output are the same file.

Tile compression (fits, fits/compress)

  • Read with ReadPixelsCompressed[T] / ReadPixelsCompressedMasked[T] (ReadPixels takes *ImageHDU): bit-exact against libcfitsio 4.7.0 over a 148-case fpack/libcfitsio corpus (plus 12 ZBITPIX 64 cases) with a sample for every algorithm x ZBITPIX x BZERO convention x dither cell cfitsio can write — RICE_1 (and its RICE_ONE alias), GZIP_1, GZIP_2, HCOMPRESS_1 (including SMOOTH and lossy 32-bit), PLIO_1 (including the uint16 +32768 storage); BITPIX 8/16/32/-32/-64; BSCALE/BZERO incl. uint16/uint32/int8; SUBTRACTIVE_DITHER_1/2, NO_DITHER, ZQUANTIZ NONE; ZBLANK, BLANK and per-tile ZBLANK nulls; 3-D tiles; 4-D and 6-D images; GZIP_COMPRESSED_DATA and UNCOMPRESSED_DATA fallbacks (in fallback tiles of scaled images we follow funpack/astropy, where fits_read_img is inconsistent); tile integers wider or narrower than ZBITPIX, as old cfitsio wrote them (GZIP width from the inflated length, RICE_1 without ZVAL2 = BYTEPIX 4), converted to ZBITPIX as cfitsio does. ZBITPIX 64 is read with GZIP_1, GZIP_2, NOCOMPRESS, RICE_1, HCOMPRESS_1 and PLIO_1. The reader also decodes all 91 images cfitsio's fits_img_compress wrote for the writer-parity goldens exactly as cfitsio does (incl. the dither random-sequence wrap in 10000-pixel tiles). Integer targets follow the ReadPixels rule (refused with ErrTypeMismatch unless they hold every physical value). ZCMPTYPE is matched case-sensitively, as cfitsio does. A ZBITPIX that is no BITPIX, a ZNAXISn below 1, an HCOMPRESS_1 reconstruction outside int32 (cfitsio wraps it), HCOMPRESS_1/PLIO_1 values the ZBITPIX type cannot hold (except 16-bit lossy HCOMPRESS, which is clipped as funpack and cfitsio's TSHORT read do; cfitsio's TUSHORT read of a uint16 image refuses it with status 412), ZNAXIS outside 1..6, ZTILEn <= 0, a missing COMPRESSED_DATA column, a GZIP tile whose inflated length is not 1/2/4/8 x its pixels (or a tile width no sample proves), tile values the ZBITPIX type cannot hold, RICE_1 BYTEPIX 8 and PLIO_1 with BZERO 32768 at ZBITPIX 8/32 are refused, and so is tiled TABLE compression (ZTABLE = T, ErrTiledTableCompression). NOCOMPRESS is decoded but has no cfitsio truth (astropy only). Real files in the corpus: astropy's M13 (a 2006 SkyView int16 image) in GZIP_1 (4-byte tiles), RICE_1 (BYTEPIX 4), HCOMPRESS_1 and PLIO_1, and two other astropy test files. Open gap: no modern camera/mission .fz file (DECam, HSC, ...) is committed yet; a row-tiled one can be cut by whole tiles, as the M13 files were.
  • Write (AppendCompressedImage, AppendCompressedFloat32Image, AppendCompressedFloat64Image): RICE_1, GZIP_1, GZIP_2, HCOMPRESS_1 and PLIO_1 for uint8/int8/int16/uint16/int32/uint32, 1-D to 6-D (cfitsio's MAX_COMPRESS_DIM; more axes are refused), and float32 with quantization (SUBTRACTIVE_DITHER_1/2, NO_DITHER, ZDITHER0 1..10000; NaN and ±Inf stored as nulls, ±0/denormals as +0, as cfitsio does), and float64 the same way. Output is the same compressed image as cfitsio's fits_img_compress for 91 golden images incl. 4-D and 6-D (RICE/HCOMPRESS/PLIO byte-identical, GZIP inflated-identical) and 19 of the 20 float64 goldens (the 20th is a cfitsio overflow that this writer stores losslessly); 5 cfitsio refusals (incl. 7 axes) and 2 deliberate refusals are refused too. Where cfitsio's quantizer overflows (a tiny absolute QLevel, the float rounding of the centered zero point, a pixel landing on the null sentinel), cfitsio 4.7.0 writes wrong pixels with status 0; this writer stores those tiles losslessly instead. Nullcheck compares NullValue with the raw pixel value, as fits_write_imgnull does; the float64 writer takes NullValue64 only, and the other width's null value is refused. Shape errors (data length, empty shape, non-positive axes, tile rank, non-positive tile axes) are refused before anything is written (the length with ErrShapeMismatch). On a fresh Create, an empty primary is written in front of the first extension. Every written variant passes fitsverify and decodes identically in cfitsio, funpack and astropy. Not supported: 64-bit integers (cfitsio cannot write them), float HCOMPRESS/PLIO, noise-relative HCOMPRESS scale, PLIO uint16, NOCOMPRESS write. PLIO takes values 0..2^24-1; HCOMPRESS needs 2-D tiles ≥ 4 px (edge tiles too).

World coordinates (fits/wcs, fits/wcs/transform)

  • Projections: all 28 codes wcslib 8.10 implements (Paper II's 26 including AIR, plus HPX and XPH), plus the AIPS aliases NCP (→ SIN) and GLS (→ SFL) translated as wcslib wcsset does, ported from wcslib prj.c. The trigonometry is Go's math functions under wcslib's wcstrig structure (exact results at multiples of 90°, cancellation-free asin/acos), so values can differ from wcslib's (which calls glibc) in the last bits. Each projection is checked point by point against wcslib in both directions within the stated budget (1e-11 deg; a row the projection makes ill-conditioned, next to a pole, horizon or cube-face centre, within its own libm sensitivity), with every accept/reject boundary pinned to 1e-7 deg, through the full header pipeline, and XPH and HPX also against their defining papers. Accept/reject decisions are identical to wcslib's except at boundary-ambiguous points, where a few-ULP nudge of the input flips the answer.
  • Header pipeline (wcs.Parse → transform.New): checked against wcslib 8.10 in both directions, including rejection status, on real HST ACS/WFC3, JWST NIRCam, Spitzer IRAC, astrometry.net, ZTF and PTF headers (SIP and TPV), ROSAT (GLON-AIT) and MSX (GLON-CAR) survey headers, and Calabretta's Paper II example maps, DSS plate solutions (CNPIX/PPO/PLT*/AMDX/AMDY) applied as wcslib 8.10 does (including a real STScI DSS header), SIP keywords applied with or without the -SIP suffix, as wcslib does, except on a RA---TPV header: there (the TPV + redundant SIP form of ATLAS images, proven on a real ATLAS header) the SIP keywords are ignored, as astropy does, where wcslib applies both and moves every position by 13-14 px; RA---TPV-SIP applies both; plus synthetic cases for LONPOLE/LATPOLE/PVi_0..4, CROTA2 (precedence PC > CD > CROTA2), CUNIT (deg/arcmin/arcsec/mas/rad), PC+CD (PC wins), partial CD, swapped axes, cubes, WCSAXESa and alternate WCS, SLON/SLAT and HLON/HLAT pairs, -SIP on projections other than TAN, and the defunct PC00i00j / CD00i00j / PROJPn keywords (read as wcslib's WCSHDR_all and astropy do; a real Pan-STARRS skycell header among them). TPV in PC+CDELT form follows wcslib; a missing TPV PVi_1 defaults to 1 per the TPV registry (wcslib uses 0). SIP of orders 7 and 9, TPV with every PVi_0..PVi_39 set and ZPN of degree 12 and with PV2_29 are checked against wcslib too. PixelToSky(p1, p2) takes the celestial pixel axes in increasing axis order. Also checked against astropy through wcs.Parse.
  • wcs.Parse errors on wrong-typed WCS keywords, invalid SIP orders (not an integer 0..9), partial SIP declarations, SIP coefficient cards with a multi-digit index or of degree > 9 (wcslib refuses the latter too), non-numeric or above-order non-zero SIP coefficients, and SIP keywords on a TNX/ZPX header. transform.New refuses what wcslib refuses too (PVi_m with m > 4 on the longitude axis, TPV PVi_40+, theta0 outside [-90, 90]) and, stricter than wcslib (which computes something): CPDIS/CQDIS/D2IM lookup tables, -SIP on alternate descriptions, -SIP on one celestial axis only, unknown CTYPE distortion codes, wcslib's TPU alias, matrices that couple non-celestial pixel axes into RA/DEC, TPV with non-degree CUNIT, a singular CD, and a defunct PC00i00j / CD00i00j / PROJPn card that disagrees with its modern twin. Accepted although wcslib 8.10 refuses: SIP without AP/BP (or with one of them empty) and a DSS header whose PLATEID card follows the plate keywords; both give wcslib's answer for the equivalent header.
  • TNX (IRAF; Chebyshev, Legendre and polynomial surfaces) through transform.NewTNX(*header.Header), proven against WCSTools 3.9.7: Chebyshev and polynomial surfaces on real CTIO 4 m Mosaic II and KPNO 4 m Mosaic-1 headers, Legendre (type 2) on synthetic headers only (no public real Legendre header was found). Headers that WCSTools and wcslib read differently are refused with an error: SIP keywords on a TNX header (stricter than wcslib on purpose: wcslib would apply SIP + TAN and ignore the WAT correction, WCSTools applies TNX), CD together with PC, a partial CDELT/PC set (one CDELT alone, or a PC matrix without CDELT1/PC1_1, when there is no CD), CD00i00j without CDi_j, PVi_m that would move the fiducial point (longitude-axis PVi_m, latitude PVi_m with no correction surface), CUNIT other than deg, lowercase CTYPE, LONPOLE > 360, and WAT gaps, duplicates, CONTINUE or strings over 1999 characters.
  • Sky frames: ICRS and FK5 J2000 ↔ galactic and supergalactic, ICRS ↔ IAU 2006 mean ecliptic of date, FK5 precession (IAU 1976), FK4 B1950 (with E-terms) ↔ FK5 J2000, checked point by point against astropy 8.0.2/ERFA goldens to 1e-8 arcsec. Directions only (no proper motion); FK4 at equinox B1950 only.

HEALPix (fits/healpix)

RING (any nside) and NESTED (power of 2) up to nside 2^29, following healpix_cxx, validated against healpy 1.20.1 and the real healpix_cxx 8.3.0, including the cos/sin switch-over points. Ang2Pix/Vec2Pix use Go's math.Cos/Sin/Atan2; fed the correctly rounded values the references were made with, every pixel equals healpix_cxx's exactly, and with Go's libm every pixel equals healpy's except at boundary-ambiguous directions (within a few ULP of a pixel boundary, where a few-ULP nudge changes the pixel), which the tests count and cap.

Statistics and stretches (fits/stats, fits/stretch)

  • stats: MeanStdevSiril, Percentile/Median/MAD (float32 path) and FilterNonZero are bit-identical to Siril 1.4.0 compiled from its source; SigmaClip follows astropy.stats.sigma_clip (maxIter <= 0 iterates until stable with no cap, astropy maxiters=None; NIter is reported). BuildHistogram returns (Histogram, error): its counts and edges are bit-identical to numpy.histogram, including numpy's one-off index corrections at bin edges, and it returns ErrHistogramRange wherever numpy raises. Histogram.Percentile is the left-continuous inverse of the piecewise-linear CDF, cross-checked against scipy.stats.rv_histogram.ppf. Percentile/Median/MAD return NaN on ±Inf data or a max-min that overflows. MeanStdev is a float64 one-pass sample stdev (N−1) that agrees with numpy std(ddof=1) within the one-pass rounding bound and loses precision on large offsets (1e9 + {0,1} gives 95.83 on amd64 and 96.15 on arm64, not 0.50025; its low bits depend on the build's FMA fusion, each build pinned); MeanStdevTwoPass is the accurate generic version (its mean is correctly rounded).
  • stretch: autostretch (linked/unlinked), MTF, asinh (incl. -human), linear and GHT are bit-exact against Siril 1.4.0 (GHT allows glibc's 1-ULP logf/powf/expf difference), including NaN, ±Inf and negative input; CLAHE matches Siril's OpenCV 5.0.0 pipeline, including OpenCV's x86-64 clip-limit conversion (clip*tilePixels/65536 >= 2^31 or +Inf acts as limit 1, NaN or clip <= 0 as no limit). Only float32 image paths are ported. Parameters: Linear returns ErrLinearParams (BP outside [0,1] or NaN); Autostretch/AutostretchUnlinked/AutostretchLinked return ErrAutostretchParams (targetBG outside [0,1] or NaN, NaN shadows clip) and the *Params functions panic on the same arguments; GHT returns ErrGHTParams (outside D in [0,10], B in [-5,15], 0 <= LP <= SP <= HP <= 1, or NaN; GHTParams.HPZero asks for HP = 0, since HP: 0 means Siril's default 1); Asinh/AsinhRGB/AsinhRGBHuman return ErrAsinhParams (NaN or infinite beta or offset, beta < 0, offset >= 1; beta in [0,1) is accepted as in Siril's GUI); MTF returns ErrMTFParams (NaN or infinite parameter, midtones outside [0,1]; other finite shadows/highlights and midtones 0/1 are accepted as Siril's autostretch uses them). CLAHE has no error result (it panics on a bad size or tile < 1). NormalizeChannels leaves ±Inf out of its range and maps them to 1 / 0.

image.Image adapter

AsImage/image.Decode: 2-D BITPIX 8 (unscaled) → image.Gray, BITPIX 16 with BZERO 32768 → image.Gray16, float BITPIX → FloatGray when every value is exactly representable in float32. BSCALE = 0, BLANK pixels, an invalid or malformed last BSCALE/BZERO/BLANK card and everything else → ErrNotImageCompatible. No rescaling.

API changes from the proof audit

Breaking changes against v1 master (d4c5067), as reported by apidiff. They ship in a v1 minor release (the module path stays github.com/dmarkham/fits); each one is fixed at the call site by handling the added return value:

  • fits: (*ImageHDU).BSCALE, (*ImageHDU).BZERO, (*CompressedImageHDU).BSCALE and (*CompressedImageHDU).BZERO now return (float64, error); the error wraps ErrInvalidScaling for a card the scaling-card rule refuses. (*CompressedImageHDU).BSCALE of a header with BSCALE = 0 now returns 0 (it returned 1).
  • fits: VarColumn[T Numeric] is now VarColumn[T any] (it also holds the bool results of ReadVarLogicalColumn/ReadVarBitColumn and complex elements).
  • header: Card is no longer comparable (it carries the CONTINUE cards it was decoded from); compare fields instead of using ==.
  • stats: BuildHistogram(data, nbins) now returns (Histogram, error) (ErrHistogramRange where numpy raises).
  • stretch: Asinh, AsinhRGB, Autostretch, AutostretchLinked, GHT, Linear and MTF now return an error (ErrAsinhParams, ErrAutostretchParams, ErrGHTParams, ErrLinearParams, ErrMTFParams) for parameters outside the ranges listed above.

Additions: (*ImageHDU).BLANK() (int64, bool, error), stretch.AsinhRGBHuman, stretch.AutostretchUnlinked. For valid inputs the values are unchanged except where the old code was wrong (see docs/VALIDATION.md).

Requirements

  • Go 1.24 or later.
  • Tested on Linux: amd64 (GOAMD64=v1 and v3) and arm64 (under qemu-aarch64); every bit-exact claim holds on all three, because the library wraps float products in explicit conversions so the compiler cannot fuse them into FMA instructions. Other architectures are not tested.
  • macOS (arm64 and amd64): supported, including editing, with the same lock and crash-safety guarantees as Linux, to be proven by running the whole suite on a real Mac (pending; see docs/VALIDATION.md). Windows and other systems: reading and writing new files compile but are not tested; editing (OpenForEdit, EditFile) is refused with ErrEditUnsupported.
  • New dependency for macOS: golang.org/x/sys (extended-attribute calls, which the standard library lacks on darwin).

Concurrency

*File is not safe for concurrent use by multiple goroutines. Callers that want parallelism must coordinate externally (one *File per goroutine, or an external mutex).

See plan.md for the original design and scope.

License

MIT. See LICENSE.

About

Pure-Go FITS library — read, write, WCS, tile compression, HEALPix, image stretching. Cross-validated against cfitsio, wcslib, astropy, healpy, and Siril.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages