Skip to content

Tile-image compression (read + write) and rename to fitskit#1

Merged
ssmichael1 merged 9 commits into
mainfrom
feature/tile-compression
May 31, 2026
Merged

Tile-image compression (read + write) and rename to fitskit#1
ssmichael1 merged 9 commits into
mainfrom
feature/tile-compression

Conversation

@ssmichael1
Copy link
Copy Markdown
Owner

Summary

Adds full FITS tiled-image compression support and renames the crate to fitskit.

Compression — read (bit/byte-exact vs cfitsio funpack)

  • RICE_1, GZIP_1, GZIP_2, PLIO_1, HCOMPRESS_1 for integer and floating-point images
  • Float quantization + subtractive dithering (NO_DITHER / SUBTRACTIVE_DITHER_1 / _2); float reconstruction uses an FMA to bit-match cfitsio
  • Lazy API: hdu.as_compressed_image()?.decompress()?; HduData stays BinTable so compressed tiles survive for lossless round-trip

Compression — write (the unique niche)

  • Encode RICE_1 (integer lossless + quantized/dithered float) and GZIP_1/GZIP_2 (integer; lossless float via GZIP_1)
  • ImageData::compress(&CompressOptions) -> Hdu; output verified byte-exact through funpack (and imcopy/astropy)
  • No other pure-Rust FITS crate writes compressed FITS

Validation

  • Byte-exact round-trips against fpack-generated fixtures; 6 funpack-reads-fitskit interop tests; float bit-exactness vs funpack. Tests skip cleanly when fixtures / funpack are absent.
  • scripts/gen_compressed_fixtures.sh generates the fixtures (served from the fits4_samples GCS bucket); CI downloads them best-effort.

Rename

  • Crate fits4 -> fitskit with crates.io publish metadata (repository, keywords, categories, readme, ...). The fits4_samples bucket name is unchanged.

Docs

  • README rewritten (features, examples, Core types, comparison table, supported/unsupported)
  • lib.rs crate docs expanded to mirror the README

All tests pass (cargo test --all-features) and clippy is clean (--all-features --all-targets -D warnings).

ssmichael1 and others added 9 commits May 31, 2026 13:56
Implement Phase 1 of the FITS tiled-image compression convention
(ZIMAGE/ZCMPTYPE/ZTILEn/COMPRESSED_DATA), decoding compressed images
out of the BINTABLE form into ImageData:

- New src/tile_compress.rs: CompressionType/Quantize/TileGeometry,
  CompressedImage<'a> view (detect/from_bintable), RICE_1 decoder,
  tile reassembly (unravel/scatter_tile), and GZIP_1/GZIP_2 behind an
  optional, feature-gated `gzip` dependency (miniz_oxide) so the core
  stays zero-dependency.
- Lazy, backward-compatible API: Hdu::as_compressed_image() ->
  Option<CompressedImage>, then .decompress()/.compression()/.geometry().
  HduData is unchanged; compressed images stay stored as BinTable so the
  original tiles survive for lossless round-trip.
- Fix RICE_1 seed handling: the first value is a seed (lastpix), not a
  stored pixel; decode a full nvals diffs so array[0] = seed + diff[0].
  The previous off-by-one shifted every reconstructed pixel one position.
- Tests: byte-exact integration tests against fpack-generated fixtures
  (RICE row-tiled, square edge-truncated, I32, gzip1), skip-if-absent;
  corrected synthetic RICE unit tests that had masked the bug.
- scripts/gen_compressed_fixtures.sh generates/verifies fpack fixtures;
  CI downloads them best-effort (cache key v2).
- COMPRESSION_PLAN.md documents the staged rollout (float quant+dither,
  PLIO_1, HCOMPRESS_1, write path remain TODO).

Also includes accompanying in-progress crate refinements across
ascii_table/bintable/header/keyword/types and their tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 3 of FITS tiled-image compression: decode floating-point
compressed images (RICE/GZIP-quantized) back to F32/F64.

- unquantize/decompress_float in tile_compress.rs: per-tile ZSCALE/ZZERO
  columns, ZQUANTIZ NO_DITHER / SUBTRACTIVE_DITHER_1 / SUBTRACTIVE_DITHER_2,
  ZBLANK -> NaN, DITHER_2 exact-zero preservation, and lossless-float
  (ZQUANTIZ=NONE) passthrough.
- Port cfitsio's dither RNG exactly: Park-Miller LCG (a=16807, m=2^31-1,
  seed 1) -> 10000 f32s via OnceLock; iseed = (tile + ZDITHER0 - 1) % 10000.
- Reconstruct with a fused multiply-add (f64::mul_add) to bit-match
  cfitsio/funpack; without the FMA, large-ZZERO cancellation pixels
  diverge by ~1 ULP.
- Tests: 6 float cases asserting bit-identity to the funpack CLI at test
  time, skip-if-absent on both fixture and binary so CI stays green
  without cfitsio. Added a SUBTRACTIVE_DITHER_2 fixture (fpack -qz5).
- CI: float fixtures added to best-effort download, cache key v2 -> v3,
  clippy/test switched to --all-features.

HCOMPRESS_1, PLIO_1, and the write path remain TODO.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Complete the tile-compression read path with the remaining two
algorithms; all image compression types in the standard now decode.

- PLIO_1 (plio_decompress): port of cfitsio pl_l2pi line-list decode.
  The 1PI VLA is read as big-endian i16 words; opcodes 0..7 reconstruct
  the integer mask. Byte-exact (lossless). Note: the ll_src[3] header
  test is a signed i16 comparison (magic -100) — comparing unsigned
  decodes garbage.
- HCOMPRESS_1 (hcompress_decompress): full port of fits_hdecompress /
  fits_hdecompress64 — quadtree bit-plane decode, undigitize, inverse
  H-transform, and unshuffle. 32-bit transform for ZBITPIX 8/16, 64-bit
  otherwise (avoids overflow); all arithmetic wrapping to match C.
  Lossless for integer scale=0; float feeds the existing unquantize +
  dither path. SMOOTH != 0 is rejected with a documented Err.
- Tests: plio_roundtrip_euv and hcompress_int_roundtrip_euv (byte-exact
  vs original), float_hcompress_dither1_matches_funpack (bit-exact vs
  funpack), plus PLIO unit tests. Skip-if-absent as before.
- Added a lossless integer HCOMPRESS fixture (fpack -h -s 0); CI cache
  key v3 -> v4, new fixtures added to best-effort download.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- README: list PLIO_1 and HCOMPRESS_1 as supported; drop the "growing"
  hedge; note SMOOTH!=0 as the only HCOMPRESS gap; clarify which
  algorithms work without the gzip feature.
- CLAUDE.md: add tile_compress.rs to the module table; add tile
  compression, the FMA/funpack bit-exactness note, and the compressed
  fixture workflow to Design Decisions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Describe the public type surface (FitsFile/Hdu/HduData/Header/ImageData/
BinTable/AsciiTable/CompressedImage/Bitpix) and the HDU data model so the
README is a complete description of the crate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fits4 can now WRITE tile-compressed FITS images — the capability no other
pure-Rust crate offers. Output is read back byte-for-byte by cfitsio's
funpack (and imcopy/astropy).

- ImageData::compress(&CompressOptions) -> Hdu produces a compressed-image
  BINTABLE HDU (HduData::BinTable + ZIMAGE header + COMPRESSED_DATA VLA);
  caller does fits.push_extension(img.compress(&opts)?). Mirrors the read
  side's as_compressed_image()/decompress().
- RICE_1 encoder: exact inverse of the decoder — seed verbatim, per-block
  zig-zag diffs, FS selection minimizing block_n*(fs+1)+(sum>>fs); constants
  match the decoder (fsbits/fsmax/bbits 5/25/32, 4/14/16, 3/6/8).
- Lossless: RICE_1/GZIP_1/GZIP_2 for integer (8/16/32), GZIP_1 for raw
  float. Lossy: RICE_1/GZIP on quantized + dithered floats. PLIO_1,
  HCOMPRESS_1, NOCOMPRESS, 64-bit int, and GZIP_2-lossless-float rejected
  with a clear Error.
- Z* keyword order is load-bearing: funpack rebuilds the image header by
  walking cards, so ZTENSION must precede ZBITPIX/ZNAXIS (else "1st key not
  SIMPLE or XTENSION"). build_z_header emits fpack's order. Documented.
- Tests: synthetic RICE encode->decode (i8/i16/i32), internal round-trips,
  and 6 funpack-reads-fits4 interop tests (byte-exact), skip-if-absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a write-compressed-image example (ImageData::compress), list encode
in features/supported, note RICE works in the zero-dep build, and add a
"Compressed write" column to the comparison table — fits4 is the only
pure-Rust crate that writes compressed FITS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename the crate from fits4 to fitskit throughout (Cargo.toml, source,
tests, doc examples, README, CLAUDE.md, COMPRESSION_PLAN.md). The
fits4_samples GCS bucket name is unchanged (external resource).

Add publish metadata: repository, documentation, readme, keywords,
categories, authors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add binary-table and tile-compression (compress -> decompress round-trip)
runnable examples, a Core types overview, and note that the gzip feature
now enables encoding as well as decoding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ssmichael1 ssmichael1 merged commit 2db52fe into main May 31, 2026
1 check passed
@ssmichael1 ssmichael1 deleted the feature/tile-compression branch May 31, 2026 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant