Skip to content

Changelog

All notable changes to torchfits are documented here.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

Docs

  • backlog: Drop r7c-22, commentary replay is fixed in 1.3.2

1.3.2 — 2026-11-03

Final release of 1.3.2rc1 with no code changes: the full wheel matrix (Linux x86_64/aarch64 for Python 3.11–3.15, macOS arm64 for 3.11–3.14) was built and validated from the candidate. See 1.3.2rc1 below for the highlights, behaviour changes, security fixes and the complete list.

1.3.2rc1 — 2026-11-03

Highlights

A correctness and safety release from a full audit of the library. Many fixes change results that were silently wrong in 1.3.1 (BIT columns, where= on integer and NULL columns, uint64 and int8 data, scaled-column edits, stale handles after a file replace), close four ways around the remote-URL SSRF guard, and make every write atomic. Read Behaviour changes before upgrading.

Behaviour changes

  • Exceptions. A missing HDU (unknown EXTNAME, out-of-range or negative index) raises torchfits.HDUNotFoundError on every route; it is also a KeyError, IndexError, ValueError and RuntimeError, so existing except clauses keep working. A missing local file raises FileNotFoundError (also a RuntimeError) on every route. write() raises ValueError / TypeError for bad arguments instead of wrapping them in RuntimeError. read_extname raises for a missing file or HDU (it still returns None when the HDU has no EXTNAME); read_batch(strict=False) keeps those two error types.
  • TNULL is compared with the raw stored value (FITS standard), and torchfits now writes it that way. Files written by 1.3.1 with a value-domain unsigned TNULL still read their nulls.
  • uint64 and int8. uint64 images and columns (BZERO/TZERO = 2**63) read as torch.uint64 instead of float64 and can be written. int8 table columns are written with TZERO = -128 and read back as int8 (1.3.1 stored them as uint8).
  • where=: IS NULL also matches NaN; == NULL / != NULL on numeric columns raise; quoted literals are taken literally (only '' escapes); predicates on vector columns raise on every backend.
  • Refused operations (they used to corrupt or silently misbehave):
  • HDU edits that would renumber other HDUs, edits on a read-only file, and delete_hdu(..., compress=...);
  • update_rows with out-of-range integers, non-ASCII strings in table mutations, delete_rows(path, None);
  • a leading ! or a CFITSIO [...] / (...) section in a write path;
  • tile compression of 64-bit integer images;
  • remote URLs containing @, non-canonical numeric hosts, non-global addresses, and redirects to another host.
  • HDU edits (insert_hdu, replace_hdu, delete_hdu) copy untouched HDUs byte for byte, so they keep their tile compression and scaled storage; compress= applies only to the HDU being written.
  • Table datasets return the same torch-native values with and without where= (nulls as stored sentinels; use TNullToNan).
  • replace_column takes physical values on scaled or unsigned columns, and an unsigned dtype without format= replaces the column layout.
  • Transforms: rgb() returns float32; MeshBackgroundSubtract now really sigma-clips each tile with n_sigma / max_iter (max_iter=0 restores the plain tile median).
  • CLI: diff compares pixels and table rows exactly (exit 0 means identical data); stats skips NaN/BLANK pixels, reports them as nbad, uses float64 and a midpoint median, and gives exact integer min/max for integer images; convert to PNG puts pixel (1,1) bottom-left by default (--origin upper for the 1.3.1 orientation); setkey refuses structural keywords and refreshes existing checksums; cutout --box shifts CRPIX.
  • clear_cache(disk=True) removes the cache root and only torchfits' own download files from a TORCHFITS_REMOTE_CACHE directory set outside it.

Security

  • The remote-URL guard covers every CFITSIO network driver and refuses non-canonical numeric hosts (e.g. 0177.0.0.1), non-global addresses (including CGNAT) and @ anywhere in the URL. Patched CFITSIO no longer follows redirects to another host, honours @ only in the authority, and no longer reports uninitialised memory in HTTP errors.

Added

  • io: Read and write uint64 with the FITS BZERO/TZERO = 2**63 convention
  • io: Accept an EXTNAME in write_checksums and verify_checksums
  • data: Add return_label= to the image, cube, tensor and table datasets
  • data: Accept labels= and label_key= on the image and cube iterable datasets
  • io: Add HDUNotFoundError for unknown or out-of-range HDUs

Fixed

  • http: Block IANA special-purpose ranges on every Python version
  • io: Serve Python-fetched https cutouts on builds without libcurl
  • table: Allow update_rows/append_rows on unsigned columns
  • table: Gather complex columns at full width in filtered mmap reads
  • table: Convert VLA row payloads to the column type on append/update
  • io: Validate every HDU header before the native writers create a file
  • table: Skip caller cards that would rewrite a written table's layout
  • table: Read an ASCII schema 'Aw' width after the code letter
  • io: Refuse non-CPU arrays in native image and table writes
  • table: Refuse a foreign capsule as an mmap TableReader
  • table: Stop reading stride(0) of a 0-d BIT or byte-string payload
  • table: Flush a borrowed handle before reading a table through it
  • io: Stop handles opened before a file replace using the new file's metadata
  • table: Read 'PA' VLA columns as bytes and reject 'PC'/'PM'/'PX'
  • table: Write rAw sub-string columns one row per value
  • table: Validate the whole rename_columns mapping before writing TTYPEn
  • io: Keep FITSFile handles safe across get_num_hdus, table reads and close
  • io: Reset the compression quantize level for every HDU
  • table: Stop truncating float literals in integer-column pushdown filters
  • table: Read BIT columns row by row when the repeat is not byte-aligned
  • stats: Promote float32 to float64 for quantiles over > 2**24 elements
  • build: Apply NIOBUF/MINDIRECT to every target via a vendored-header patch
  • build: Require the core to define every CFITSIO symbol _C imports on ELF
  • build: Ship the bzip2 license with wheels that bundle libbz2
  • build: Drop the dead CFITSIO_HAVE_SSSE3 define and document SSSE3
  • build: Clear a cached USE_BZIP2 when TORCHFITS_USE_BZIP2 is OFF
  • core: Bound the shared per-path metadata registry
  • core: Keep started pool workers when a worker thread cannot start
  • core: Create the core thread pool once under concurrent first use
  • core: Re-validate extended-syntax paths and stop caching URLs
  • core: Publish path-level probe answers only for the current generation
  • core: Type read_keys values the way read_header does
  • table: Reject a schema bit repeat that disagrees with the payload width
  • build: Size BZIP2 float tiles correctly and reject short bzip2 tiles
  • build: Keep the vendored CFITSIO tree when vendor.sh fails
  • io: Compress uint16 images with PLIO_1
  • io: Refuse HDU edits that would renumber the other HDUs
  • io: Keep untouched scaled images' stored codes across HDU edits
  • cache: Clear overridden remote and sample roots with clear_cache(disk=True)
  • io: Take the read-cache file signature before reading
  • io: Invalidate path caches after write_checksums
  • io: Write numpy images with negative strides
  • io: Match EXTNAME case-insensitively in replace_hdu and delete_hdu
  • io: Stop rewrites from stacking CFITSIO's standard COMMENT block
  • io: Keep x.fits.gz gzip-compressed on overwrite and HDU edits
  • io: Drop BSCALE/BZERO copied onto an integer tensor of another BITPIX
  • io: Let write() raise the documented ValueError for invalid input
  • io: Reject CFITSIO's '!' clobber prefix in write paths
  • io: Never clobber or delete a file that appears during overwrite=False
  • io: Keep a source TFORM on rewrite only for the same column layout
  • table: Write and read int8 columns with the signed-byte convention
  • table: Write uint64 TZERO = 2**63 exactly on every path
  • io: Honor mode= and EXTNAMEs on every read route
  • io: Refuse HTTP range cutouts of table HDUs
  • io: Validate cached reads of file.fits[N] and file:// paths against the file
  • where: Take string literals literally and align read_torch literals
  • table: Key cached native table readers by the resolved path
  • hdu: Resolve TableHDURef column-file edits against the file's columns
  • hdu: Renumber column cards when TableHDU.to_fits writes a subset
  • table: Carry typed header cards through insert/replace_column
  • table: Re-stamp CHECKSUM/DATASUM after row and column edits
  • where: Refuse vector-column predicates on every backend
  • where: Resolve cpp-backend string predicates with the caller's decoding
  • table: Check ASCII and width for strings in every row mutation
  • table: Write None in string columns as a blank value
  • table: Reject delete_rows(path, None) instead of deleting every row
  • table: Compare TNULL with the stored value before TSCAL/TZERO
  • table: Refuse out-of-range integers in update_rows before writing
  • table: Keep scaled columns' stored values across insert/replace_column
  • where: Compare integer columns exactly against float and wide literals
  • where: Keep NULL OR TRUE rows with Kleene and/or
  • table: Write signed payloads into uint64 columns as physical values
  • table: Infer ASCII TFORMs for column edits on ASCII tables
  • table: Write 0-row numpy string columns with their dtype width
  • hdu: Give refreshed TableHDURef views typed header cards
  • where: Read quoted literals literally in the public parse helpers
  • hdu: Keep untouched HDUs byte-identical across insert/replace/delete
  • table: Size numpy string columns from their dtype width
  • table: Read scaled cells of gzip tables from the decompressed stream
  • io: Write None, complex and big-int header values read_header returns
  • header: Keep orphan CONTINUE cards off earlier string values
  • hdu: Report the DataView dtype with the native reader's exact rule
  • hdu: Look up HDUList names like CFITSIO and support "in"
  • hdu: Keep Header.copy, | and |= on the card list
  • hdu: Rebuild Header from its cards under pickle and copy
  • http: Connect to the configured proxy instead of the pinned target
  • data: Never cache a non-FITS HTTP body as a download
  • http: Guard every CFITSIO network driver scheme
  • io: Report missing libcurl and CFITSIO's reason on open failures
  • http: Stop CFITSIO following redirects to unguarded hosts
  • http: Refuse non-canonical numeric hosts and non-global addresses
  • scripts: No --user cibuildwheel install; fail CFHT fetch on missing frames
  • release: Refuse Unreleased notes for a final release
  • scripts: Make ci-local build first and run mypy, changelog-check, full tests
  • scripts: Never pick a pre-release tag for the stable docs root
  • scripts: Count launcher failures in the CANFAR CUDA verify summary
  • scripts: Report failing wheel build/verify cells as FAIL
  • scripts: Parse breaking and revert subjects; reject non-conventional ones
  • data: Collate nested per-arm dicts in fits_collate_fn
  • data: Keep table dataset rows torch-native under where=
  • transforms: Broadcast spatial masks in weighted stats and IRAF zscale
  • transforms: Ignore empty tiles in the mesh background 3x3 filter
  • transforms: Run IRAF zscale on MPS in float32
  • transforms: Keep NaN/masked pixels out of the IRAF zscale fit
  • cli: Decompress restores a compressed primary image as the primary
  • cli: Setkey coerces only plain numeric literals
  • cli: Convert reports bad --columns/--where as usage errors
  • cli: Cutout --box shifts CRPIX to the cutout origin
  • cli: Arith keeps huge integral scalars as floats
  • cli: Arith output dtype no longer follows a float64 operand B
  • cli: Arith stacks HDUs only when their dtypes match
  • cli: Setkey validates before copying to -o/--out-dir
  • cli: Setkey refreshes CHECKSUM/DATASUM in edited HDUs that had them
  • cli: Setkey refuses structural and tile-compression keywords
  • cli: Setkey --delete/--rename find HIERARCH keywords
  • cli: Report compressed image shapes row-major like uncompressed ones
  • cli: Cutout checks compressed image size and refuses -e with a section
  • cli: Stats over finite pixels in float64 with midpoint median
  • cli: Diff compares pixels and table rows exactly
  • transforms: Compose accepts plain callables
  • hdu: TableHDURef reads its header lazily and validates every window
  • table: Keep where= reads torch-free unless torch is already imported
  • transforms: Sigma-clip mesh background tiles with n_sigma and max_iter
  • table: Accept pathlib.Path in table.write cache invalidation
  • io: Let remote cutouts of tile-compressed HDUs fall back to a download
  • io: Create and open paths with '[' in a directory literally
  • io: Raise argument errors from open(mode=) and table.read(hdu<0) as is
  • io: Keep delete_hdu torch-free and correct the torch-free table
  • hdu: Refuse insert/replace/delete_hdu on a read-only file
  • hdu: Splice HDUs in files with trailing bytes after the last HDU
  • write: Skip an empty in-memory primary when writing a compressed MEF
  • hdu: Route HDUList.write through torchfits.write's path contract
  • io: Raise a readable open error when CFITSIO's error stack holds raw bytes
  • http: Refuse '@' in CFITSIO URLs and parse userinfo only in the authority
  • io: Let write() raise argument TypeErrors unchanged
  • table: Infer the TZERO convention for unsigned and int8 in insert_column
  • table: Raise ValueError naming TNULL/TSCAL/TZERO for bad schema numerics
  • table: Return typed empty columns for 0-row tables and num_rows=0
  • io: Write the uint64 BZERO/TZERO offset as the exact integer 2**63
  • table: Let replace_column adopt the layout of unsigned and int8 values
  • io: Reject tile compression of 64-bit integer images up front
  • io: Raise FileNotFoundError for a missing local file on every route
  • cli: Report an out-of-range -e with the library's HDU message
  • io: Raise HDUNotFoundError from table, HDU-edit and HDUList lookups
  • table: Compare where= literals beyond int64 exactly in the native scan
  • build: Stop the ELF link gate reading libc fflush/ffs* as CFITSIO
  • write: Keep the empty primary's header cards on a compressed MEF
  • io: Reject CFITSIO [..]/(..) sections on write paths
  • io: Flush a write handle on close() while a TableReader borrows it
  • table: Replace_column takes physical values on a scaled column
  • table: Accept a uint64 tnull at or above 2**63
  • table: Read 1.3.1 value-domain unsigned TNULL as a null
  • errors: Keep except RuntimeError catching missing HDUs and files
  • cache: Never delete user-configured cache override directories
  • build: Stop the ELF link gate reading mangled C++ names as CFITSIO

Performance

  • png: Export PNG scanlines in one copy instead of per-element Python loop
  • rgb: Float32 display path, in-place prep, subsampled p90; convert --origin

  • io: Read a missing EXTNAME without an exception or lookup re-check

  • io: Answer .fits.gz metadata from one decompression per file
  • table: Read dense rows= selections through merged block reads
  • io: Read EXTNAME/HDUNAME in one missing_ok call in the name scan
  • io: Cache explicit mode= reads under a mode-keyed entry

Docs

  • Say the real-CFHT suites never run in CI and list both files
  • Correct AGENTS.md claims about test markers, changelog-check, stubs
  • examples: Say the cutout command now shifts CRPIX
  • cli: Document convert --origin; align docs contract with CRPIX shift
  • contributing: Make the torch-boundary audit recipe run
  • architecture: Correct dtype map, image mmap ownership and env var list
  • index: Say device= copies a host-decoded tensor, not native GPU decode
  • examples-ml: Correct dataset output table, bash continuations and numbering
  • api-transforms: Match signatures, rgb dtypes and weighted support to code
  • api-data: Match zeropoint and rank lookup order to the code
  • api-tables: To_duckdb returns a relation, not a connection
  • install: Mark the bzip2/libcurl build flags as a diagnostic
  • api-core-io: Correct read_batch_info, compression losslessness and errors
  • api: Name Header.cards instead of a nonexistent read_header_cards
  • io: Note that PLIO_1 uint16 images read 32768 off in astropy
  • io: Document HDUNotFoundError and FileNotFoundError for lookups

1.3.1 — 2026-10-26

Changed

  • Make torchfits.read fully discoverable with explicit keyword-only options and remove ineffective cache-policy objects, ignored tuning arguments, and private compatibility exports.
  • Make every CLI command self-describing, add root --version, register the supported transforms explicitly, and reject inapplicable or conflicting options before touching output files.

Fixed

  • Reject overflowing, truncated, or out-of-bounds binary, ASCII, and variable-length table geometry before allocation or I/O while preserving valid empty tables.
  • Preserve every normalized option in multi-file reads and keep explicit mode, cache, scaling, dtype, row-window, and device behavior consistent across single-file and batch paths.
  • Keep compression lossless by default, validate codec/dtype combinations, compare repeated header cards in order, complete multi-block remote headers, enforce probe deadlines, and prevent case or Unicode-equivalent output collisions.
  • Build self-contained source archives and validate the exact wheel matrix, metadata, licenses, native linkage, macOS deployment floor, release-candidate provenance, archive safety, and artifact digests before publication.

Performance

  • Avoid redundant floating-point NaN masks and unnecessary table-read dispatch work without benchmark-specific routing or sampling heuristics.

Documentation

  • Synchronize the public API, CLI recipes, examples, supported Python/PyTorch lane, platform requirements, and guarded release procedure with the shipped implementation.

1.3.1rc3 — 2026-10-26

Fixed

  • Accept auditwheel's compound manylinux tags when they include the required manylinux 2.28 tag plus a compatible lower glibc floor for the same architecture.

1.3.1rc2 — 2026-10-26

Removed

  • Remove ineffective cache-policy configuration, private re-export shims, implementation-only exports, ignored loader tuning arguments, and ignored native image-write parameters instead of preserving no-op compatibility surfaces.
  • Replace the hidden read(options=...) object and catch-all keyword surface with explicit, discoverable keyword-only read options.

Fixed

  • Reject overflowing or out-of-bounds binary, ASCII, and variable-length table geometry before allocation or I/O while preserving valid empty binary tables.
  • Preserve every normalized read option across multi-file reads and prevent a top-level mode default from overriding an explicitly selected mode.
  • Make every CLI option self-describing and enforce option applicability, destination collision checks, transform registration, lossless compression defaults, complete remote headers, ordered duplicate header cards, stdin-only setkey, and root version reporting.
  • Require an exact release-candidate source tree, validate the complete wheel matrix and native platform floor, ship self-contained source archives, and require explicit approval before stable PyPI and GitHub publication.

Performance

  • Avoid redundant floating-point NaN masks for comparisons whose IEEE result already excludes NaNs, and remove redundant table-read dispatch work.

Docs

  • home: Focus the home page on arrays, tables, and cutouts
  • roadmap: Drop the out-of-scope list
  • Synchronize the public API, CLI, platform requirements, examples, and release procedure with the 1.3.1 implementation.

1.3.1rc1 — 2026-10-24

Fixed

  • Correct the cache docs and enforce the rc gate clear_file_cache is documented for what it actually clears, the cache policy is expanded in one place, TableHDU.select keeps its source path, and the wheel workflow refuses a stable tag with no vX.Y.Zrc* tag.
  • Drop the stale handles flag and pin TableHDU header to keyword cache_subsystem_policy() no longer reports the removed handles flag, so clear_file_cache(**cache_subsystem_policy("all")) works again. TableHDU takes header, source_path and source_hdu as keyword-only, so a positional header can no longer bind as source_path and drop the header cards on write.

  • release: Build the sdist without installing torch The sdist job no longer installs the torch wheel. An isolated build pulled that 2 GB wheel, filled the runner disk, and left the tag without an sdist.

  • release: Map wheel tags with portable sed Wheel filenames are parsed with a sed expression that works on both GNU and BSD sed, so macOS can map a wheel back to its Python tag.
  • release: Skip the build dependency check for the sdist --no-isolation still checks build requirements, so the sdist build passes --skip-dependency-check and does not require torch.

Docs

  • release: Require an rc tag before every stable release scripts/check_release_rc.py runs before PyPI upload and refuses a stable tag with no matching vX.Y.Zrc* tag.

Added

  • build: Add an opt-in instrumented coverage build -DTORCHFITS_COVERAGE=ON instruments the extension, the torch-free core and vendored CFITSIO with --coverage at -O0 (IPO off) for gcovr reports; the option fails at configure time on a compiler without gcov support.

Dependencies

  • packaging: Require numpy 2.4.6 and pyarrow 25

1.3.0 — 2026-10-22

Added

  • hdu: Make open() and header inspection torch-free (torchfits.open() and header/HDU inspection never import PyTorch).
  • table: table.read (Arrow) no longer imports PyTorch either: the native engine exposes column storage through a read-only buffer view, so the Arrow destination is torch-free even though the extension stays torch-linked.
  • table: uint16, uint32, and uint64 tensor columns keep their Arrow dtype instead of silently degrading to int64.

Changed

  • PyTorch 2.14 lane: wheels are ABI-matched to torch>=2.14,<2.15, and the CUDA flavors are cu126 and cu130 (cu128 and cu129 are not published for 2.14). The extension now builds as C++20, which torch 2.14's headers require.
  • Supported Python is now 3.11-3.15. 3.10 is dropped: PyTorch 2.14 is the last release that ships cp310 wheels, and 3.15 is stable as of 3.15.0.
  • Python 3.15 support is Linux-only in this release: PyTorch 2.14's cp315 macOS/Windows wheels were built against CPython 3.15.0b3 headers and crash on the first tensor operation outside the main thread (pytorch#200779). macOS keeps cp311-cp314 wheels; Linux publishes cp315.

Removed

  • Deprecated no-op cache knobs: torchfits.cache.configure_cache(), CacheManager.configure_cpp_cache(), clear_file_cache(handles=), ReadOptions.handle_cache_capacity, and TableHDU(col_stats=...).

Fixed

  • scripts: Anchor the duplicate-cpp guard to the repository root
  • real-data tests: Skip per dataset instead of failing on a partial corpus
  • ci: Stop publishing macOS cp315 wheels for the broken torch 2.14 ABI
  • table: Accept NumPy scalars as filter literals
  • table: Reject NumPy bool literals on every NumPy release
  • table: Return a memoryview for the VLA offsets fallback so the Arrow conversion no longer rejects it with TypeError.
  • table: Retry a buffered read when an mmap=True row slice cannot use the mmap layout (VLA, scaled, ASCII) instead of dropping to the row-by-row scan.

Performance

  • table: Wrap native column buffers instead of copying through NumPy (the extension exports a read-only buffer view, so fixed columns, numeric vectors, and fixed-width byte strings are zero-copy end to end; Arrow keeps the tensor storage alive).
  • table: Apply the FITS uint16/uint32 offset by flipping the sign bit in place instead of upcasting to int64 and allocating twice.
  • table: Wrap VLA offsets and 2-D numeric columns without a second copy.

Docs

  • Finish the 2.14 lane sweep
  • Note the macOS Python 3.14 wheel cap

1.2.3 — 2026-10-20

Fixed

  • table: Stop listing a nameless column twice in filtered and sliced reads

1.2.2 — 2026-10-20

Added

  • InterquantileScale.compute_scale() returns the scale statistic without changing the cached state used by inverse(); eps must be finite and positive.
  • transforms: Expose interquantile scale statistic

Fixed

  • build: Avoid CUDA discovery for CPU extension
  • header: Unfold HISTORY/COMMENT lists instead of a std::bad_cast
  • schema: Keep nameless table columns under the readers' COL name

Docs

  • Point parity and real-data evaluation at torchfits-harness

[1.2.1] — 2026-10-14

Fixed

  • table.read_torch(where=) translates negative TNULL sentinels to physical nulls via _tnull_in_tensor on unsigned integer columns, matching table.read.
  • fits_set_bscale errors during raw image reads now raise RuntimeError rather than silently reading scaled pixels.
  • write() with compress=True raises QuantizeError when quantize= is passed for multi-HDU sequence inputs, matching uncompressed sequence writes.

Removed

  • Seven test-only native entry points (read_full_numpy, read_full_numpy_cached, read_full_unmapped, read_full_scaled_cpu, read_tensor_from_handle, read_hdus_sequence_last, echo_tensor) and 32 redundant hasattr(cpp, ...) probe branches are removed.

1.2.0 — 2026-10-14

Removed

  • torchfits.cpp legacy alias is removed. The native surface is private (torchfits._cpp); user-facing code should use torchfits.read, torchfits.table, and torchfits.open.

Fixed

  • Scaled BITPIX=8 and BITPIX=16 images are read as float32. 1.2.0rc1 returned float64 for every scaled image that was not an integer convention. BITPIX=32 and 64 stay float64, so 16777217 is not rounded.

1.2.0rc1 — 2026-10-07

Candidate for 1.2.0. Same PyTorch requirement as 1.1.3 (torch>=2.13,<2.14), Python 3.10–3.14. The differences from 1.1.3:

Results that change

  • A scaled image that is not an integer convention is read as float64. 1.1.3 used float32, so a BITPIX=32 value such as 16777217 rounded to 16777216. The signed-byte and unsigned conventions (BSCALE=1 with BZERO=-128, 32768, or 2147483648) stay integers. quantize="robust" is BITPIX=16, so this candidate reads it as float64 too.
  • LogStretch and SqrtStretch keep integer input in floating point. 1.1.3 cast the stretched values back to the storage dtype, so a uint16 image collapsed toward 0 and 1.
  • A companion DQ extension of zeros is a mask of valid pixels. 1.1.3 treated every zero as invalid, so mask-aware stats became NaN. Decode a real bitfield with mask_is_dq=True.
  • table.read_torch(where=) drops rows whose compared value is NaN, and a duplicated TTYPE keeps the first column. A numeric filter no longer builds a Python index of every kept row; that list is only for strings and variable-length columns.

New since 1.1.3

  • Header and metadata calls (read_header, read_keys, read_shape, read_num_hdus, and the other path-based probes) go through libtorchfits_core / torchfits._core and do not load PyTorch. torchfits.open() still does.
  • Arrow reads of fixed columns and flat variable-length values do not import PyTorch. table.read_torch and table.scan_torch are still the tensor APIs.
  • Transforms record calibration state (STORED, PHYSICAL, CONTINUUM_NORMALIZED, NORMALIZED) and raise if they would calibrate data twice. Affine transforms propagate inverse-variance and masks. New transforms: MeshBackgroundSubtract, SigmaNormalize, AffineTransform, IRAF zscale (algorithm="iraf").
  • Datasets can take spectrum wavelengths, DQ masks, an IFU spectral window, table row shards, and a band list. rgb(..., dtype=) can keep a display composite in float32.
  • The CLI commands info, header, probe, verify, table, copy, and setkey do not import PyTorch. Commands that return pixels do. -j / --jobs is applied. A repeated -e index is an error, and cutout --box must hit the image. A tile-compressed image is reported as an image, not a catalog.

Also fixed since 1.1.3

  • write(..., quantize="robust") no longer flattens a sparse image to the global min/max.
  • Paths may be os.PathLike. Non-ASCII header text is rejected instead of rewritten. A repeated keyword resolves to its first value, including HISTORY / COMMENT chains.
  • A where= read rejects a request the same call would reject without a filter, and it no longer drops a variable-length column. table.update_rows does not invent rows past the end of the table.
  • A failed write no longer destroys the file it was replacing, and deleting the last HDU no longer deletes the file.
  • Transforms accept a {"flux", "ivar", "mask"} dict, not only a tensor. Non-finite pixels are left out of statistics, so one infinity no longer zeros a SigmaClip frame.
  • numpy>=1.26 and pyarrow>=17. The wheel ships _C, _core, and libtorchfits_core from one build; a mismatched set fails at import.

1.1.3 — 2026-09-09

Fixed

  • FITSHeaderScale / FITSScaleColumns keep integer inputs as float after BSCALE*x+BZERO (the unsigned convention BZERO=32768 on int16 no longer wraps; fractional BSCALE is no longer truncated).
  • _median / _quantile promote float16/bfloat16 to float32 (torch.quantile rejects half dtypes); LogStretch upcasts before 1 + a*x so float16 no longer overflows to inf.
  • SigmaClip(dim=(), fill="median") fills with the global median instead of a per-element identity.
  • FITSHeaderNormalize maps integer counts through float64 when the storage width is 32 bits or more, so values above 2**24 stay distinct.
  • table.read_torch(where=...) with no columns= returns every column (matching Arrow table.read); table.schema(columns=[...]) uses the requested field order.
  • FITSFile::read_subset returns the image's true dtype for degenerate (zero-width/height) cutout boxes instead of always float32.
  • HDUList.fromfile no longer reads every header twice (headers from the batch open are reused), and the HDUInfo.header binding preserves duplicate HISTORY/COMMENT cards.
  • TableHDURef.replace_column_file mirrors the new TFORM into the in-memory header (matching insert_column_file).
  • Header constructed from 3-tuples normalizes keys and scalar values like every other set path.
  • HTTP Range HDU walks account for BINTABLE heap size (PCOUNT/THEAP), so cutouts past VLA columns or compressed images no longer fall back to a full-file download when the heap exceeds the scan cap.
  • CLI arith --hdu2 reads operand B once instead of per A-HDU; arith/compress --out-dir names strip CFITSIO [section] suffixes; stats upcasts integer images once.
  • bench_arrow_tables.py uses the current table.read signature.

Changed

  • Derived TableHDUs (filter, select, head, add_column, ...) own a copy of the source header, so later mutations of the derived header no longer leak into the parent.
  • CLI transform preserves safe header keys (WCS, EXTNAME, etc.) on float outputs instead of dropping the whole header; only scaling keywords (BSCALE/BZERO/BLANK, DATAMIN/DATAMAX) and checksum stamps are dropped.
  • CLI compress resolves IO pairs through the shared batch resolver.
  • get_cache_stats no longer reports counters that were never updated.
  • Removed the unreachable tzero is None branch in fits_schema.

Added

  • InterquantileScale transform for robust scaling by interquantile range; FitsStagedCutoutIterableDataset can load companion HDUs (e.g. label or mask maps) alongside the image HDU (#241).

Docs

  • Documentation rewritten in plain user-facing language: internal tracking codes and process jargon removed, benchmark and install pages glossed.

1.1.1 — 2026-08-29

Fixed

  • table.read_torch(where=) drops TNULL sentinels and uses range-safe compares, matching Arrow table.read.
  • Robust-quantize BLANK pixels are NaN on torchfits.read, including uncompressed scaled images. Native IEEE float/double HDUs keep Inf and signed zero (fits_read_img nulval is only for BLANK and compressed tiles; CFITSIO fnan would otherwise replace them).
  • torchfits copy is a byte copy (shutil.copy2); same-path I/O is refused; HDUList.write to an existing path uses tempfile+replace.
  • TableHDURef.head composes an existing row window.
  • Header cache hits clone Header from cards; raw_scale reaches the fallback reader.
  • replace_hdu strips stale Z* cards; checksum rewrites restamp when the input had stamps; verify_checksums reports present.
  • Table .fits.gz / .zip refuse the buffered pread path; TSBYTE mmap matches CFITSIO; TFORM repeat overflow raises.
  • CLI: unsigned diff min/max, Ctrl-C exit 130, JSON without NaN.
  • HTTP Range cutouts of integer images with BLANK fall back to CFITSIO so missing pixels are NaN rather than the sentinel code.
  • Writing an already-decoded float with a copied integer header drops BSCALE/BZERO/BLANK so CFITSIO does not scale twice (CLI cutout and arith).
  • Quantized table TNULL values are Arrow nulls, so where="V IS NULL" matches. Native float NaN without TNULLn is unchanged.
  • Import sets KMP_DUPLICATE_LIB_OK when unset (macOS libomp).
  • build: Portable sha256 verification in vendor.sh (macOS runners)
  • changelog: Rank final tags above prereleases in latest_tag
  • build: Select sha256 tooling by OS, not binary presence

  • Completed the 1.1 correctness review for silent NaN/TNULL/copy bugs (#237)

  • Completed the 1.1.1 release-readiness review (19 tracked findings closed)

Changed

  • torchfits._cpp no longer re-exports undocumented _C names.
  • Root to_astropy(path) delegates to table.to_astropy.
  • read(hdu=[...], mmap=False) and path-list batch honor mmap.
  • quantize= on HDUList + compress= raises instead of being ignored.
  • Linux wheels install bzip2-devel in manylinux so HAS_BZIP2 matches macOS.
  • Sanitizer CI uses the pixi test env (not uv) and quotes cmake define flags so bash does not split on ;.

Docs

  • Benchmark headline cites git-mirrored exhaustive_*_20260807_013736. GPU copy is host-decode then device=. Complex columns are Partial on Arrow. Release runbook uses OIDC, not a PyPI token. Windows is unsupported. CacheConfig.max_files is a no-op.

Added

  • 1.1.1 identity-check test suite and example-gallery additions

1.1.0 — 2026-08-26

Feature + correctness release on the same 2.13 torch ABI lane. New capabilities: checksum-stamped writes, GIL-free hot reads, clean truncated-file errors, high-fidelity Astropy interop, memory-bounded streaming filters, multiprocess-safe remote downloads, auto-adaptive FITS RGB, and native whole-file .bz2 FITS reads — plus a major-release-readiness review that cleared a round of silent-corruption, thread-safety, and supply-chain fixes.

Features

  • Native whole-file .bz2 FITS reads on builds with bzip2 support (capability flag torchfits._C.HAS_BZIP2; vendored CFITSIO links libbz2 from the conda prefix or the system): every reader entry point decompresses transparently, while direct-I/O fast paths automatically route through CFITSIO. Writing a .bz2-named output is rejected — CFITSIO would silently create an uncompressed file; use compress="BZIP2_1" for tile compression instead.

  • transforms.rgb(*bands) auto-adaptive RGB from 1–7 aligned filters (shortest wavelength first): scarlet-style mix, per-band sky-median subtract + MAD equalize unless calibrated=True / zeropoints=, fitspng-style MAD stretch, coupled asinh, saturation, and sRGB. NaN mosaic holes stay black (they are not treated as sky). Scene classification is blow-out-safe: a bright extended object (planet disk, galaxy core) whose post-equalize p90 towers above the noise is stretched against its own p90 instead of the faint-feature anchor, so extended targets never clip while deep/star fields keep their stretch. lupton_rgb stays the Astropy-parity 3-band mapping (reddest first).

  • write(..., checksum=True) stamps CFITSIO DATASUM/CHECKSUM keywords on every HDU at write time (all payload types, compressed included); verify later with torchfits.verify_checksums.
  • Multithreaded reads scale: hot C++ paths (read_full, read_full_numpy, header/shape/HDU-count probes) now release the GIL for open + I/O, so DataLoader threads no longer serialize behind one Python thread during disk or network access.
  • Truncated files raise instead of crashing: mmap table reads, filtered scans, and row updates validate the header-claimed extent against the real file size and raise a clear "truncated" error rather than SIGBUS-killing the interpreter.
  • Multi-chunk buffered reads are correct again: tables whose rows span more than one 16 MiB scratch chunk (payload > ~16 MB with caching disabled) could read from an unsized staging buffer after a prefetch rewrite — slow, garbage results on wide/mixed projections. Buffer rotation now happens only while prefetching; regression tests pin

    16 MB reads against astropy.

  • High-fidelity table.to_astropy(): TNULL-bearing columns become real MaskedColumns (no more object-dtype degradation), TUNIT maps to .unit, and fixed-size vector columns keep their (N, repeat) shape.
  • Memory-bounded streaming filters: scan(..., where=...) now evaluates predicates per batch as rows stream past — peak RAM tracks batch_size, not table size. Hidden predicate columns are projected out after filtering, and a fully filtered-out scan still yields one typed empty batch.
  • Multiprocess-safe remote downloads: concurrent DataLoader workers sharing a cache directory are serialized by an OS file lock (exactly one fetch per URL), interrupted transfers resume via If-Range validators (stale partials restart cleanly instead of producing hybrid files), and servers without Content-Length trigger an explicit completeness warning.

Changed

  • torchfits convert PNG default is auto RGB (--recipe auto): 1–7 files, blue→red order, --brightness / --saturation / --calibrated / --zeropoints. --recipe lupton keeps the previous 3-band reddest-first --q / --stretch mapping.
  • Scalar-column shapes are now rank-1 everywhere: FITS repeat==1 columns read as (N,) through every path — hdul[n].data[col], hdul[n][col], TableHDURef, read_torch, iter_rows, to_tensor_dict, streaming chunks, and buffered/mmap reads alike. Vector columns (repeat>1) keep their shapes; packed string columns are never squeezed. Completes the accessor-only squeeze shipped in #234.
  • read_torch(start_row=..., where=...) now filters within the row window, matching table.read(row_slice=..., where=...) and the Arrow engine. Previously the two entry points returned different populations for the same query.
  • WHERE negation follows SQL three-valued logic on every engine: NOT (X == 5), X NOT IN (...), and X NOT BETWEEN ... exclude NULL rows exactly like their unnegated counterparts.
  • TNULL sentinel values can no longer satisfy numeric predicates on the C++ pushdown / torch-mask paths; sentinel-only matches are excluded so all engines return identical rows for the same query.
  • Batch fast paths (read(list_of_paths), list-of-HDUs) honor fp16 / bf16 / raw_scale instead of silently returning raw data.

  • torchfits.cpp is deprecated in favor of private torchfits._cpp; every attribute access warns. No-op native cache stubs (configure_cache, get_cache_size, clear_file_cache) left the public surface.

  • TORCHFITS_CFITSIO_CACHE_MB/_FILES env vars removed; they configured a native cache that no longer exists. cache.configure_cache() / CacheManager.configure_cpp_cache() remain as documented no-ops emitting DeprecationWarning for one cycle.
  • write_tensor() accepts checksum= for parity with write().
  • torchfits.open() accepts only mode="r"; in-place update modes are rejected with an actionable error.
  • TensorHDU.chunks() is implemented: lazy row-band slabs equal to slices of to_tensor(). It previously called a binding that never existed and always raised AttributeError.
  • DataView.dtype reports convention dtypes (uint16/uint32/int8) matching reader output rather than raw storage BITPIX.
  • TableHDU schema caches hold strong header refs so GC id-reuse cannot serve stale schemas; select() rejects unknown columns and head() validates its argument.
  • Vendored CFITSIO fetches are sha256-pinned and verified; unpinned tags fail closed unless TORCHFITS_VENDOR_ALLOW_UNPINNED=1; "latest" resolution removed. Conda/pixi builds compile the same pinned+patched CFITSIO as the wheels — PLIO buffer fix + BZIP2_1 on every channel.

Fixed

  • BIT ('X') table columns wrote corrupted bits when repeat % 8 != 0 (e.g. '12X'): both the initial writer and the buffered row-update writer passed flat element runs to fits_write_col(TBIT), which maps them onto raw data-unit bits ignoring per-row padding. Every write call is now confined to a single row; verified byte-exact against astropy for 8X/12X/16X/23X across all three write paths.
  • Buffered table reads no longer leak a closed fd after a transient pread failure — a recycled descriptor could later serve an unrelated file's bytes as table data.
  • Cached reads are isolated from caller mutation: in-place edits of a returned tensor no longer poison subsequent identical reads (the default read cache stores/hands out private copies).
  • replace_hdu header preservation drops stale BSCALE / BZERO / DATASUM / CHECKSUM; grafting an unsigned-convention BZERO onto new float data previously made every reader misinterpret the replacement.
  • SigmaClip: a single NaN no longer wipes the whole frame (valid mask is seeded from finite values even without a user mask); median fill of fully-masked groups yields 0 instead of NaN.
  • GlobalScalarNorm: negative statistics divide sign-preservingly instead of exploding to ~1e30 scales; inverse() round-trips exactly; NaN no longer poisons mean / rms statistics.
  • WHERE string literals survive parsing: NAME == 'AT&T' matches AT&T (not AT AND T), FITS doubled-quote escapes work ('O''NEIL'), and unterminated quotes raise instead of mangling.
  • Header parser: LONGSTRN chains keep assembling correctly when any card carries a comment, and ESO HIERARCH cards parse to typed values under their full keyword (ESO TEL AMBI TEMP -> 12.5).
  • Thread-local HDU metadata cache rotates its generation id when an out-of-band file replacement is detected, so stale shape/dtype/scale cannot be paired with new bytes on the mmap path.
  • Unsigned-convention detection tolerates floating-point imprecision in stored BZERO/TZERO uniformly for images and tables (a file whose offset was serialized as 32767.999… now reads as uint16/uint32 on every path).

  • Compressed-image null pixels decode as NaN (was silent 0): the null probe targeted a CFITSIO symbol that exists in no upstream release. ZBLANK is probed directly and float CompImage reads always pass NaN nulval. Regression suite vs astropy: tests/test_compressed_nulls.py.

  • torchfits arith no longer wraps/truncates integer images: ops run in int64/float64 with saturating cast-back plus warning; --dtype overrides; div produces floats under auto.
  • torchfits stats works on unsigned-convention images: min/max run after upcast instead of raising on missing uint reduction kernels.
  • quantize="robust" maps NaN/Inf to BLANK sentinel codes with the keyword written, instead of packing non-finite values into valid codes that dequantize as real data.
  • WHERE float equality is engine-independent; schema() stops lying about complex columns; row windows keep VLA/string columns aligned; scan(mmap=True) handles scaled tables and ASCII HDUs; chunk buffers zero-initialized; read_batch warnings document skip semantics; interop kwargs no longer leak to pandas; FITS numeric parsing accepts D-exponents / rejects 1_0.
  • Transforms are functional (no caller-tensor mutation via .to() aliasing); medians interpolate like numpy/astropy; SigmaClip/AsymmetricSigmaClip gain fill="nan".
  • Random Groups images fail loudly instead of decoding garbage.
  • torchfits diff treats NaN == NaN: byte-identical files containing NaN pixels compare clean instead of reporting spurious differences.
  • torchfits --transform -J fan-out builds one transform instance per worker file, so stateful transforms never share _last_state across threads.
  • Thread-safety: TensorHDU reads use private per-call handles; shared TableReader instances serialize I/O. Stale-cache windows after header mutations closed by invalidating SharedReadMeta/readers in the header-card/key/checksum writers.
  • packaging: SPDX license expression for license-files; boundary test tracks _cpp move
  • tables: Engine-aligned WHERE floats, honest complex schema, aligned windows, streaming fallbacks
  • cli: Integer-safe arith, uint stats, NaN-aware diff, per-worker transforms

Security

  • read_hdus enforces the same SSRF/path guards as every other entry point (loopback/link-local/private targets were reachable before).
  • HTTP credentials (TORCHFITS_HTTP_TOKEN / TORCHFITS_HTTP_AUTHORIZATION) are stripped when a redirect crosses origins; kept only for same-origin hops and plain http->https upgrades.

Performance

  • Policy/meta caches (image_meta, cold-nommap, auto-mmap, hdu-type) are validated against the file's stat signature, eliminating stale dispatch after in-place rewrites. Measured cost of the validation: ~1.6 us per os.stat on this host.
  • Cached reads now hand out private copies, so callers can mutate results. Measured on a 64 MiB float32 image (local NVMe): uncached read 46.5 ms (~1.4 GB/s) vs warm cache hit 45.3 ms including the isolation copy — a hit still beats the I/O it replaces, and repeated table hits stay sub-millisecond. If a cached-hot workload regresses measurably for you, read(..., cache_capacity=0) restores v1.0 semantics at the cost of re-reading.
  • Full CPU + CUDA exhaustive benchmark re-runs on Linux CANFAR headless (published CSVs exhaustive_cpu_20260807_013736 / exhaustive_cuda_20260807_013736 under docs/assets/bench/; lab profile, mmap on+off matrix, 3057 + 4315 rows): 100% of significant image comparisons won on both hosts. Exactly one case family remains where a peer leads: narrow-table full reads with mmap=False trail fitsio by 21-36% on CPU and 8% on CUDA (buffered path stages whole rows; single-pass decode lands in 1.2). Image HCOMPRESS lags vs fitsio are sub-1.03× noise. A double-buffered chunk prefetch now overlaps the buffered path's I/O with decode for payloads >= 64 MB (gated from an earlier 4 MB threshold after CANFAR A/B showed thread handoff regressing warm-cache 13 MB tables). Benchmark harness fairness fixes: device synchronization on GPU timings, seeded interleaving order, medians over means, and cache-symmetric peer comparisons.
  • Multi-HDU writes flush process-global caches once per operation instead of twice per HDU.
  • BIT ('X') writes now issue one fits_write_col call per row; only tables containing bit columns pay for the extra calls.

Added

  • Table mutations warn on silent value loss: float payloads into integer columns (truncation/non-finite counts), out-of-range integers, non-ASCII characters dropped from string columns, and over-width string clipping.
  • io: Native whole-file .bz2 FITS reads on bzip2-capable builds

Dependencies

  • Vendored CFITSIO updated to 4.7.0 for wheel, source, conda and pixi builds alike (extern/VERSIONS.txt, sha256-pinned): every channel ships the PLIO buffer fix and BZIP2_1 support (see architecture).

Docs

  • Scalar-column shape contract documented; architecture note reconciled with the wheel-vs-conda CFITSIO split.
  • Corrected inaccurate benchmark and compatibility claims in the docs and toned down unverifiable ones
  • changelog: Versionless Unreleased + generator tooling; refresh roadmap
  • Changelog entries curated under Unreleased; changelog-check green
  • io: Document BZIP2_1 availability, lossiness and interop caveat in write()

1.0.0 — 2026-08-09

Version cut on the 2.13 torch ABI lane: buffered table reads through a thread-local reader cache (process-wide eviction, stat-identity stale guard), single-open insert/update of table rows, and torch-mask predicate materialization. Final pre-release checks complete; awaiting collaborator docs/usability testing before the tag.

Added

  • torchfits.to_astropy(): Direct conversion of PyTorch tensor dictionary structures to Astropy Table instances via zero-copy Arrow intermediate buffers.
  • Streaming ML Datasets: FitsCubeIterableDataset, FitsSpectrumIterableDataset, and FitsStagedCutoutIterableDataset with rank and world size sharding for distributed PyTorch training.
  • MegaCam cosmic-ray denoise example (example_megacam_cr_denoise.py): Noise2Noise on real dark/bias calibration twins (zero-field N2N), with self-normalizing pair transforms, held-out CCD evaluation, and honest transfer metrics (CR suppression, star fluxes, background, noise-injection test). Full rationale and results: Denoise pipeline.
  • scripts/fetch_cfht_calib_frames.sh: idempotent download of CFHT MegaCam darks/biases from the CADC data service.
  • scripts/canfar_denoise_incontainer.sh + scripts/launch_canfar_denoise.sh: headless Skaha job running the denoise example on a CUDA GPU (defaults to the 4-epoch setting; longer fixed-LR runs diverge — see the pipeline page).
  • Denoise example writes a before/after gallery figure (examples/output/megacam_cr_denoise_dark.png, rendered in the ML guide and the pipeline page).
  • Exemplary science-pipeline benchmark (bench_science_pipeline.py: robust sigma-clipped coadd + ML cutout serving, torchfits vs astropy) with per-stage code-lines rows (bench_contract.code_lines); bench-denoise pixi task for the MegaCam CR-cleaning benchmark.

Performance

  • Cached table reader: read_table/read_batch reuse an open CFITSIO handle per thread instead of open/read/close per call; benchmark-host side effects drop from ~5861 to ~750 minor faults per read after warmup; isolated narrow_1000000 read_full window 17.8 ms -> 6.7 ms (lab window, exhaustive_cpu_20260807_082144_reader_cache).
  • exhaustive_cpu_20260807_082931_reader_cache run: fitstable read_full ratio vs astropy 1.365x -> 1.072x; predicate_filter on narrow_1000000 16.60 -> 11.01 ms (1.21x -> 1.64x vs astropy); selective 10.68 -> 9.33 ms (1.50x -> 1.71x) via torch-gather where= (mmap off, lab). The _082144 / _082931 run CSVs were lost in a local workspace reset (noted in benchmarks.md); the numbers survive in the benchmarks page's generated tables, and the reader-cache direction was spot-verified on current HEAD 2026-08-09 (narrow_1000000 read_full: torchfits 7.9 ms vs astropy 16.5 ms, mmap on).
  • Insert/update rows open the table once per operation (6d2338c).

Fixed

  • Read-then-mutate regression: appending rows after a cached read no longer hits CFITSIO error 104 (reader cache eviction is now process-wide, covering cross-thread writers).
  • Stale reads: replaced files (new inode) are re-detected via stat identity; cached pread handles are dropped, never serving old data.
  • Schema metadata with nanobind >= 2.14 (dropped str->int coercion): tnull/bscale/bzero/null values parsed from header strings instead of raising std::bad_cast on the write path.
  • Tile compression: reject unsupported floating-point payloads on PLIO_1 writes with a clean ValueError.
  • Table mutation & schema: strict validation for VLA and object dictionary-table columns.
  • Stats transforms accept integer dtypes (BZERO-scaled read_subset results come back as UInt16, which torch cannot reduce): helpers upcast to float32 (int64 keeps precision as float64), masked fills use dtype-safe sentinels, and SigmaClip promotes integer inputs instead of raising.

Dependencies

  • Vendored CFITSIO at the 1.0.0 tag: 4.6.4 (extern/VERSIONS.txt). (An earlier revision of this entry claimed 4.7.0; that bump landed after the tag and is recorded under Unreleased below.)

Packaging

  • Linux wheels now cover x86_64 and aarch64 for CPython 3.10–3.14 (cibuildwheel 4.1; GHA ubuntu-24.04-arm for aarch64). macOS arm64 is unchanged; run bash scripts/cibuildwheel.sh on a Mac to build locally.
  • PyPI no longer receives an sdist — pip install cannot fall back to a source compile (the rc5 trap on Ubuntu 26.04 / Python 3.14).
  • Wheel cmake args no longer point CUDA_TOOLKIT_ROOT_DIR at conda. CUDA torch at runtime is the same CPU-linked wheel; verify on CANFAR with scripts/verify_wheel_cuda_canfar.sh.

Docs

  • Benchmark tables refreshed from the 2026-08-07 exhaustive runs (CPU + CUDA); docs/assets/bench/ mirrors the surviving 2026-08-07 CPU/CUDA run CSVs.
  • Denoise pipeline page with honest dark-vs-bias results and stated limitations; MegaCam CR-cleaning section in the ML guide; examples index row.

1.0.0rc5 — 2026-08-06

Fifth release candidate on the 2.13 torch ABI lane; adds prerelease-aware release tooling, a root cache reset entry point, and the macOS compressed-float parity fix.

Added

  • clear_all_caches() — root-level clear of in-process and disk caches (including cache_root() downloads/samples); clear_cache() stays in-process-only by default.
  • CI/scripts: release_lane.py --prerelease rc<N> --apply renders a lane's release version plus a PEP 440 prerelease suffix (e.g. 1.0.0rc5 on the 2.13 lane) across all five pinned files; --check / check-lane accept rc states as the lane base; unit tests cover suffix render/check/reject paths.
  • Opt-in robust float→int16 packing: write / write_tensor(..., quantize=) and table.write(..., quantize=) ("robust" or {"lo_q","hi_q","keep_zero"}). Default remains native float (BITPIX=-32 / float TFORM).
  • Example: examples/example_quantize_int16.py.
  • CLI compress / decompress: multiple inputs via --out-dir; --split file|hdu (one output per file or per image HDU); -j/--jobs = PyTorch intra-op threads; -J/--file-jobs = multi-file thread pool.
  • CLI -J/--file-jobs on verify / stats / arith (and compress).
  • CLI arith: image–image operand, multi-HDU stack+ATen, multi-file --out-dir.
  • CLI Wave 2: batch copy / transform / cutout via --out-dir + -J; stats std/median; compress --algorithm; header -k wildcards; setkey --delete / @list via CFITSIO fits_delete_key (keeps compression).
  • Docs: CPU-only (no CUDA libs) install recipe; “not only for ML” blurb; roadmap 2.0 native engine / GPU-direct (drop CFITSIO).
  • Packaging: torchfits[cpu] / torchfits[cuda] extras (both Linux-only — macOS no-ops, MPS ships in the default wheel) plus one-line CUDA/CPU install recipes (PyPI's default torch already bundles CUDA on Linux x86_64; CUDA builds also run on GPU-less machines via CPU fallback).
  • Docs: full docs↔code sync review — one-line pinned installs in quickstart / CLI docs, write() payload types corrected (no top-level ndarray), CLI-recipe transform kwargs documented, architecture freshness rc5.
  • CI/scripts: check-torch-pins resolves the [cpu] / [cuda] extra pins against the PyTorch indexes on the wheel ABI lane, run as the first step of both CI jobs and the wheel-build workflow so lane drift fails fast (CI lint job, build_wheels tests + wheel jobs, ci-local). macOS passes vacuously (both flavor pins are Linux-only); the doc-drift guard now also requires every extra's exact pin string (e.g. torch==2.10.0+cpu) to appear in install.md / README; unit tests cover the lane guard, the marker-skip path, the missing-extras failure, and the exact-pin doc-drift check.
  • Lossless compressed float writes: GZIP_1 and integer RICE_1 no longer silently quantize (fits_set_quantize_level(0)); float RICE_1 / HCOMPRESS_1 keep CFITSIO default quantization (lossless unsupported), matching astropy/fitsio defaults. Documented in io.write().
  • Compression matrix suite (48 tests): all algorithms × int dtypes with astropy oracles, PLIO range rejection, float quantization bound, per-algorithm cutouts, fpack byte-identity.
  • Output-parity suite (tests/test_output_parity.py, 69 tests): bitwise cross-library read parity (torchfits == fitsio == astropy), write round-trips through fitsio for every compression type + quantize + LONGSTRN + uint64 rejection, seeded fuzz-lite sweep.
  • Bit-faithful write/read fidelity suite (55 tests) with astropy as an independent oracle.
  • BZIP2_1 image codec (vendored CFITSIO patch, opt-in via TORCHFITS_USE_BZIP2, default ON when libbz2 is available; not part of the FITS standard — astropy refuses it).
  • CANFAR matrix bench mode: any python × torch lane × device grid from a VOS wheel bundle (scripts/launch_canfar_matrix_grid.sh), 41-leg grid run.

Changed

  • open_subset_reader mmap path covers unsigned FITS conventions (BZERO/BSCALE).
  • Warm read_shape hits shared image-info cache; read_header caches cards LRU.
  • Landing one-liner + transparent nav/favicon logos; contributing / release checklists aligned with verify tiers (preflight-push / ci-local / release-gate).
  • torchfits header text mode dumps all HDUs in fitsheader-style blocks.
  • CLI parallelism docs: -j (torch) vs -J (file workers).
  • setkey --rename / --delete remove keywords via CFITSIO delete (no decompressing rewrite); --split hdu rejects colliding stems.
  • table.read_torch(..., where=) applies a torch mask after reading projected columns; dialect is simple compare / BETWEEN / AND only (full dialect on table.read).
  • write() no longer rejects numpy arrays: torchfits.write(numpy_array) writes image HDUs (plain, quantized, compressed) instead of raising TypeError. Fixed the public-API bug where docs documented broken behavior.
  • LONGSTRN/CONTINUE read+write: header values > 68 chars assemble via CONTINUE chains (& + bare CONTINUE) on read and fits_update_key_longstr on write.
  • uint64 image/table writes raise ValueError with guidance (was an unsupported-column error); the rejection covers numpy table columns too.
  • TSCAL/TZERO scaling applied to FLOAT/DOUBLE table columns; scaled tables fall back to buffered (non-mmap) reads with physical values.
  • int8 images use the BZERO=-128 signed-byte convention instead of raw bytes without BZERO (values ≥ 128 corrupted on read); unsigned table prep registers untouched columns in the synthesized schema.
  • ASCII string columns use code-first Aw tforms; fits_schema.parse_tform understands the ASCII Aw form so update_rows stops truncating ASCII strings.
  • LOGICAL decode accepts 'T' / '1' / 1 (CFITSIO returns converted 1/0 on fits_read_col(TBYTE)); uint64 images (BZERO=2^63) detected as scaled instead of read raw.
  • Header.remove fast path for huge HISTORY lists; 20k-delete regression smoke.
  • TableHDU caches version-gated on the header (were stale after TTYPE/TFORM mutation).
  • HCOMPRESS uses 2D 16-row tiles like fpack's default (1D tiles rejected by CFITSIO with status 413).
  • Concurrency: Python FITS-cache LRUs and the thread-local metadata cache are lock-protected / size-bounded for concurrent reads; uint16 BZERO offset is fused into the SIMD bswap mmap path.
  • Bench docs: multi-host GPU/CPU results from the CANFAR matrix grid.
  • Bench docs: rc5 re-run snapshot — CANFAR CPU exhaustive_cpu_20260806_012620, CUDA exhaustive_cuda_20260806_012651, local CPU exhaustive_cpu_20260806_022603. CUDA 100% fits win rate (smart/specialized), fitstable ≥98.9%; only residual lags are table read_full / predicate rows (≤1.15×, fitsio/astropy) and HCOMPRESS_1 (≤1.03×).
  • Bench labeling: the per-platform results table derives the platform from the benchmark data (metadata device field / host column token), not the run-id tag — the local bench script names every run exhaustive_mps_* regardless of platform, so a CPU run on a Linux box used to be mislabeled "macOS arm64 / MPS". The script now tags runs exhaustive_cpu_* on non-Darwin hosts.
  • SSRF hardening waves: private/loopback/link-local/reserved-address guards on read_header, read_batch, HDU write paths, and public cpp; scan_polars guards before importing optional polars.

Fixed

  • macOS compressed-float parity: the vendored CFITSIO now builds with -ffp-contract=off (clang/aarch64 FMA contraction shifted low-ULP bits of decompressed float tiles, e.g. exact 0.0 read back as ~1.9e-16); the compressed-image parity suite additionally allows ≤1 dtype eps vs fitsio/astropy on macOS, staying bitwise-exact everywhere else.
  • Table int16 columns with TSCAL/TZERO: disable CFITSIO auto-scale on read before casting, then apply scale in memory (avoids int16 overflow).
  • Silent data loss / corruption fixes: compressed float writes silently quantized by CFITSIO defaults (documented behavior change above); int8 images without BZERO corrupted values ≥ 128; table column ordering scrambled on rewrite (unordered_map → ordered vectors); ASCII string columns truncated to one character on update_rows.
  • PLIO heap overflow in vendored CFITSIO (imcomp_calc_max_elem sizing, ASAN- confirmed 16-byte overwrite on incompressible data), fixed by auto-applied patch; bzip2 image codec re-enabled upstream.
  • LOGICAL (T/F) decode accepts all of 'T'/'1'/1.
  • scan_polars guarded before importing optional polars.
  • CI lint packaging dep fix; astropy < 6.0 uint32 tile-compression test failures fixed; py3.10 uint32 astropy oracle skipped below 7.0.
  • check-torch-pins misleading pass on rc states: main() re-initialized the failed flag after the lane-consistency loop, so a genuine [FAIL] line never gated CI (exit 0), and lane_for_version rejected the rc<N> prerelease suffix — the gate re-fired a spurious FAIL on every rc cut. Suffix is now accepted as the lane base and lane-consistency failures propagate to the exit code (regression tests added).

1.0.0rc4 — 2026-07-20

Fourth release candidate for collaborator testing after prep / deep-review cleanup.

Removed

  • Root aliases read_table, stream_table, read_table_rows, get_header, get_batch_info — use table.read_torch / table.scan_torch / read_header / read_batch_info.
  • Spectral and continuum transforms (spectral.py, continuum.py) — torchfits keeps FITS I/O–adjacent viz/ML preprocess only; spectroscopy analysis moves out of this package.
  • Dead core.py / ChecksumVerifier — checksums go through _C via checksum_api.
  • CLI deprecated aliases --fitsort, --bytes, --preview — use --keyword-table, --header-bytes, -n/--rows.
  • Deprecated table_module= dual-path on cache invalidate/clear.
  • table.to_polars_lazy — use scan_polars or to_polars(...).lazy().
  • torchfits.cli.rgb shim — import lupton_rgb / write_rgb_image from torchfits.transforms.
  • No-op handle_cache= on read_tensor and handle_cache_capacity= on read_subset (persistent reuse stays on open_subset_reader).

Added

  • Skinny metadata: read_nrows, read_keys, read_shape, read_hdu_type, read_num_hdus, read_colnames, read_extname, read_table_info — CFITSIO structural/key queries without a full header dump.
  • open_table_reader(path, hdu=1) — reusable table handle (mirror of open_subset_reader).
  • table.read_torch(..., where=) — fused C++ project+predicate path.
  • FITSHeaderScale.from_path / FITSHeaderNormalize.from_path via skinny keys.
  • transforms.as_module / AsModule — thin nn.Module adapter for nn.Sequential.
  • CLI transform --name Class:key=val,... constructor kwargs.
  • HTTP Range cutouts + vos/vault remote fetch (prior unreleased work).
  • Public TensorHDU.shape_str / dtype_str; optional FitsTableDataset(labels=).

Changed

  • read() rejects unknown kwargs with TypeError (no silent swallow of leftovers like policy=).
  • Table read_torch uses a thin C++ path (skips read_unified image probes).
  • Datasets label_key, get_image_meta, benches/examples lean on skinny meta.
  • Lazy root __getattr__ uses a lock-backed attribute cache (no globals() mutation).
  • Library logger uses NullHandler (no import-time StreamHandler).
  • Bench deficit CSV always lists raw lags; significance is noise or significant (floors label only).
  • Removed disconnected benchmarks/bench_fast.py and pixi aliases bench-fast / bench-fast-stable / bench-core (use bench-fits).
  • Dead private read_large_table leftover and unused _unsigned.py (unsigned paths live in _read_pipeline / write_api / fits_schema).
  • Table mutation: single _mutation_cache_barrier pre/post; dtype maps via _ensure_dtype_maps().
  • FitsTableDataset.__getitem__ returns (row_dict, label) for make_loader / fits_collate_fn parity with image datasets.
  • Lupton examples/docs use lupton_rgb(r=..., g=..., b=...) (reddest → R).
  • Example smoke runner auto-discovers examples/*.py (skips _*.py helpers).
  • DataView BITPIX 64 → torch.int64; C++ write_image supports int8/int64.

Fixed

  • HTTP Range cutouts: NumPy view byteswap(True) on frombuffer tensor (this torch build has no Tensor.byteswap); drop redundant cutout .clone().
  • SigmaClip: 0-d new_zeros(()) fill for torch.where(..., out=) (no zeros_like buffer).
  • Filtered table zero-match where=: keyed empty tensors (not {}).
  • lupton_rgb: Astropy-parity Lupton asinh mapping (per-pixel peak clip). Gallery SDSS / MegaPipe figures regenerated with readable stretch.
  • SubsetReader: uncompressed 2D images mmap the data segment once and slice+bswap into torch (MegaPipe-class mosaics); CFITSIO fits_read_subset remains the fallback for compressed / scaled / non-2D.
  • Clearer TypeError when inferring FITS TFORM from uint16/uint32/uint64.
  • LogStretch.inverse clamps exponents to avoid float overflow.
  • Remote prefetch bookkeeping cleaned after completed downloads (locks retained).
  • WHERE BETWEEN boundaries no longer use bare \S+ (operators/parens).
  • fast_parse_header_cards / Header keep empty comments as "" (not "None").
  • Wheel builds pin torch 2.10 ABI (--no-build-isolation + before-build install) so release smoke no longer fails against a 2.13-built extension.
  • Deep-review P0–P4 harden (WHERE OOM gate, batch exception narrowing, HDU close race, prefetch errors, mutation cache barrier, NAXIS overflow guard).

Docs

  • Removed-names table lists root read_table / stream_table / read_table_rows / get_header / get_batch_info.
  • Core I/O cache section documents root vs torchfits.cache layers.
  • Examples: open_table_reader + EXTNAME table.read_torch.
  • July 2026 CANFAR/local benchmark refresh: MPS exhaustive_mps_20260719_143706, CANFAR CPU exhaustive_cpu_20260719_144337, CUDA exhaustive_cuda_20260719_144457; MegaCam 20260719_075555; ML ml_20260719_145743.
  • Slim transform gallery; real Lupton RGB figure; removed spectral/continuum docs.
  • Core I/O docs point at table.read_torch / table.scan_torch (root aliases gone).
  • User Guide ML with FITS: Galaxy Zoo 1 + Legacy Survey one-epoch CNN train; MegaPipe mosaic collage + cutout timing.
  • Canonical TORCHFITS_* env tables in architecture; slimmed duplicates elsewhere.
  • release-gate runs docs-contract (example sync + zensical build) and docs-links (internal hyperlink crawl of site/).

Examples

  • example_ml_galaxyzoo_legacy.py, example_megapipe_cutout_collage.py, scripts/fetch_cfht_megapipe_sample.sh.

1.0.0rc3 — 2026-07-18

Third release candidate for collaborator testing.

Docs

  • Readability pass on user-facing pages: rc honesty, corrected migration threading (private CFITSIO handles since rc2), API notes for EXTNAME / 3D read_subset, cache vs disk-cache / make_loader layering.
  • Examples gallery: MaNGA LOGCUBE (example_manga_logcube.py), Lupton RGB from real SDSS g/r/i (example_lupton_rgb_sdss.py, stdlib bz2 inflate before read), CFHT MegaCam MEF cutouts (example_megacam_mef_cutouts.py).
  • Fill API / cache / loader doc gaps: TORCHFITS_CACHE_DIR vs in-process handle cache, when optimize_cache no-ops on table datasets, make_loader vs plain DataLoader.

Developer workflow

  • Added AGENTS.md + JULES.md agent configuration: weekly automation retargets to bug/perf-only passes; ledger in .cursor/jules-ledger.md; out-of-scope cosmetic PRs are out of scope.

Fixed

  • String HDU / EXTNAME: read_tensor and read_subset accept hdu="EXTNAME" (e.g. hdu="MYDATA"). hdu="auto" still raises a clear ValueError.
  • 3D subset: read_subset / open_subset_reader preserve the leading cube axis; window applies to trailing (y, x) only.
  • Zero-size cutout box: a degenerate width or height keeps the other axis length (no longer collapses both dims to 0).
  • Table where= + TNULL: filtered reads honor apply_fits_nulls=True so sentinel nulls do not leak as real values.
  • Remote prefetch race: resolve_local_path waits on an in-flight prefetch for the same URL instead of racing a second download onto the same .partial.
  • Lupton RGB: zero-size bands raise a clear error instead of a cryptic RuntimeError.
  • .fits.bz2: clear ValueError when CFITSIO cannot read bzip2-compressed paths (decompress first — see Lupton example).

Docs site

  • GitHub Pages deploys stable (/, latest v* tag) and edge (/edge/, tip of main) from docs.yml — use edge to debug docs without a SemVer release. Docs “stable” may be an rc tag; PyPI non-prerelease can lag.

1.0.0rc2 — 2026-07-18

Second release candidate on the 1.0 line. CFITSIO concurrent-read correctness, leftover API/docs/CLI, and cleanup. SemVer 1.0.0 still waits for extended testing.

Install / compatibility

  • Runtime / build metadata: torch>=2.10 (wheels and pixi stay on the 2.10 ABI lane). Source builds embed the detected torch major.minor as the ABI tag.
  • Docs: wheels vs source, unified GPU/accelerator install, configure_for_environment called once at import. Dropped ipykernel from [dev].
  • Disk cache root: TORCHFITS_CACHE_DIR (default XDG / ~/.cache/torchfits); remotes and samples as subdirs; Dataset / make_loader honor cache_dir=.

CLI

  • Short options: -e/--hdu, -f/--format, -o/--out, -w/--where, -c/--columns, -n/--rows, -k/--keyword (and setkey -k/--key).
  • header --keyword-table (deprecated alias --fitsort).
  • convert --where / --columns filter+export; optional FITS table out.
  • probe --header-bytes (alias --bytes) / --timeout.
  • probe SSRF guard: blocks private/loopback/link-local/reserved addresses via getaddrinfo (all records) and re-validates every HTTP redirect hop.

API / ML

  • read / read_header default hdu=0 (hdu=None still autodetection).
  • Dataset peers: FitsTensorDataset (general N-D), FitsImageDataset, FitsCubeDataset, FitsSpectrumDataset (multi-arm layout=, IVAR companions).
  • HTTP(S) remote prefetch under the configurable cache root.
  • mmap guidance for DataLoader / network FS.
  • Lift _HDUInfo / _TableWriteProxy to module scope with __slots__.
  • HDU _repr_html_ uses scope=col / scope=row for accessibility.
  • Individual HDU HTML reprs: keyboard-focusable container + theme-aware borders (aligned with HDUList/Header).

Correctness / bindings

  • Concurrent reads open a private fitsfile* per call (CFITSIO R2); no shared-handle LRU across threads. SharedReadMeta + shared raw fd remain.
  • Python table/subset paths no longer share one cpp handle across threads.
  • Table writes ensure C-contiguous buffers (signed-stride safe) before fits_write_col; image writes force contiguous host tensors before fits_write_img.
  • write_table_hdu uses RAII vector<string> for fits_create_tbl name/ttype pointers (no mid-throw char** leak).
  • evaluate_where rejects == NULL / != NULL on numeric arrays; prefer isnull / notnull or table.read(..., where=).
  • Remove duplicate num_rows binding; clean dead analyze_table comments.
  • append_rows / insert_rows best-effort rollback via fits_delete_rows after a failed post-insert write.
  • FITS header integer keys use PyLong_Check + overflow-checked TLONGLONG.
  • Empty-primary MEF compressed write HDU indexing fixed.
  • Automation integrations: probe SSRF (#216), hoist inner classes (#214), HDU HTML a11y (#213/#219).

Benchmarks / tests

  • ML loader: on-disk size_mb for compressed cases; pin compressed torchfits to hdu=1 (matches fitsio ext=1); missing numeric CSV fields use None.
  • GPU transports: pass quick= into table _build_cases.
  • Table filter tests assert exact fixture row counts.
  • Concurrent same-file image/table read smoke tests.
  • Benchmarks: new local MPS run exhaustive_mps_20260718_180230; Linux CPU/CUDA hosts remain the rc1 benchmark runs until CANFAR is re-driven against this tag.

Docs / transforms

  • Mermaid diagrams in architecture (zensical superfences).
  • Architecture: per-read handles, deliberate skip of CFITSIO iterator/where.
  • Roadmap: CFITSIO 1.1 leftovers + permanent design choices from the design review.
  • Advanced transforms frozen for 1.0; Lupton RGB wrapper in transforms.lupton_rgb; richer multi-band RGB deferred to 1.1.
  • MegaCam cutout bench: ZNAXIS-aware HDU discovery; peer fitsio ranking; materialize-once baseline; payload-based throughput (not whole-file MB/s).
  • Docs landing: lean browse grid; nav uses mark-only logo (torchfits-logo-mark.png).
  • Vendored CFITSIO docs pin 4.6.4; note fits_iterate_data intentionally unused.
  • cfitsio-direct: Rice + optional MegaCam cutout_rep jobs.

1.0.0rc1 — 2026-07-17

Release candidate for the 1.0 API. SemVer 1.0.0 waits for post-rc extended testing; do not treat this tag as the final 1.0.0 freeze.

Changed

  • verify / verify_checksums: missing checksum keywords are success. Files without DATASUM/CHECKSUM now return ok=True, status="no_checksums", CLI text OK (no checksum keywords), exit 0. Previously CLI exited 4 (FAIL). Aligns with fitsverify (missing keywords are not corruption). Scripts that treated any nonzero verify exit as “bad file” must key off status == "fail" / exit 4 instead.

  • Root table helpers deprecated. read_table, stream_table, and read_table_rows emit DeprecationWarning. Prefer torchfits.table.read / read_torch / scan_torch.

Fixed

  • ArcsinhStretch / LogStretch: validate a > 0 in __init__. Previously a=0 silently produced NaN (div-by-zero in inverse / forward). Now raises ValueError with a clear message. (transforms/stretch.py)

  • _normalize_row_slice: reject negative stop with ValueError. Previously slice(0, -1) silently returned 0 rows — the function cannot resolve negative indices without knowing the total row count. Now raises ValueError with an actionable message. (_table/utils.py)

  • Empty WHERE / row_slice / rows results: preserve column schema. Previously all empty-result paths returned pa.table({}), losing all column names/types and causing KeyError on valid queries (e.g. where="ID > 9999" on a table with no matches). New _empty_table_with_schema() helper builds typed empty tables from FITS header cards, preserving requested column ordering. When header schema is unavailable but columns were requested, returns null-typed empty columns instead of {}. (_table/read.py)

  • io.write() header type: widen to Header | dict[str, Any] | None. Removed TODO(1.0) and type:ignore[arg-type] in _table/write.py. The runtime already accepted dicts; only the type annotation was narrow. (io.py, _io_engine/write_api.py, _table/write.py)

  • Example runner: REQUIRED examples can no longer silently skip. Only OPTIONAL examples (e.g. example_polars.py) may skip on missing deps. REQUIRED examples always surface failures. (examples/test_examples.py)

Added

  • docs/cli.md: ### verify section (three labels, exit codes, fitsverify note); CLI cold-start / process-tax note.
  • docs/compatibility.md: Python / PyTorch / Arrow / platform matrix.
  • scripts/clean_install_smoke.sh: local wheel → fresh venv install smoke.
  • tests/test_http_probe_fixture.py: Range HTTP replay for probe.
  • HTTP probe JSON records include "source": "http" (matches vos probe).
  • Tests: stretch a<=0, empty schema preservation, verify messaging contract, deprecation warnings, HTTP probe fixture.

Benchmark evidence

  • Multi-host benchmark results (from b1 same-day refresh, still current for rc1): exhaustive_mps_20260717_040150, exhaustive_cpu_20260717_040146, exhaustive_cuda_20260717_042840.
  • Local release-suite (20260717_212321, Mac MPS, mmap matrix, --no-gpu): 2,825 rows, 3 deficit rows, exit 0. No domain failures.

Validation

848+ tests; mypy / ruff clean; docs integrity; examples runner REQUIRED green; bash scripts/clean_install_smoke.sh; HTTP probe fixture.

1.0b1 — 2026-07-17

Beta freeze of the public FITS → tensor / dataframe story. Not a SemVer 1.0.0 API freeze (rc line followed for extended testing + blockers).

Added

  • torchfits.table.read_torch (tensor-column dataframe path) and table.read_arrow (synonym of table.read).
  • Docs gallery: KaTeX math, transform before/after figures, CLI recipes, real-sample cache helpers (merged via docs gallery work).
  • Release reviews: rendered docs, API adoption, deep code, real-data CLI vs astropy/fitsio/gnuastro/CFITSIO (FITSH skipped).

Changed

  • Docs teach FITS tables as dataframes while keeping the torchfits.table namespace; which-reader box demotes compatibility aliases.
  • Landing / site_description: tensors and dataframes (columnar catalogs).

Fixed

  • torchfits transform on integer HDUs: promote to float before transform and write float outputs without reusing integer BITPIX headers.

[0.9.3] — 2026-07-17

Added

  • torchfits header --fitsort --keyword … multi-file keyword table (same idea as qfits dfits | fitsort).
  • Optional vos: / vos:// probe when the vos package is installed.
  • Invalid --hdu values exit with usage code 2 instead of a traceback.
  • Lean _repr_html_ on TensorHDU, TableHDU, and TableHDURef for notebooks.
  • torchfits convert --to png Lupton RGB preview via stdlib PNG (no Pillow / NumPy). PPM removed.
  • Table convert formats: parquet, csv, tsv, and arrow (Arrow IPC / Feather V2). Streaming writers for large catalogs (CSV/TSV: flat columns only).

Changed

  • torchfits.transforms is a package split by domain (stretch, normalize, fits_meta, spectral, continuum, clip) with the same public __all__.
  • Transforms docs: not nn.Module; instance-local inverse state; Advanced notes for BandMath, PhaseFold, AsymmetricLeastSquares, AlphaShapeContinuum; invertibility + helpers tables.
  • Parquet convert uses streaming write_parquet(..., stream=True) (out-of-core).
  • Multi-host benchmark refresh (exhaustive_mps_20260717_040150, exhaustive_cpu_20260717_040146, exhaustive_cuda_20260717_042840): CUDA 0 deficits, CPU 1, MPS 16.
  • scripts/gpu-bootstrap.sh pins torch>=2.10,<2.11 so CANFAR cu128 installs do not pull PyTorch 2.11 and fail the ABI gate.

Fixed

  • Block CFITSIO sh:// filenames (command injection via /bin/sh), extending the existing | checks.

[0.9.2] — 2026-07-16

Added

  • torchfits CLI — MEF-aware shell tools: info, header, verify, diff, stats, table, convert, copy, arith, cutout, compress, decompress, transform, probe, setkey. JSON/JSONL output and stable exit codes. Guide: docs/cli.md.

Changed

  • Public imports — root is I/O + HDU only. Import transforms from torchfits.transforms. torchfits.hdu is a documented namespace.
  • Removed read_fast and read_image (use read / read_tensor). Deleted the unused _fastio module.
  • Table policy helpers (can_use_*, …) are no longer listed in table.__all__.

Fixed

  • Signed-byte (BZERO=-128) and unsigned smart device reads convert on the host then copy once to CUDA/MPS.
  • read_subset / SubsetReader keep signed-byte and unsigned integer conventions as narrow dtypes (int8/uint16/uint32) instead of float-promoting every cutout.
  • Automatic table where= with mmap=True uses native mmap-scan pushdown when safe; mmap=False reads then filters in Arrow/tensor space.
  • CFITSIO MINDIRECT reset to 8640 so ~13 KB HCOMPRESS tiles use direct tile I/O.
  • Multi-byte mmap image reads use NEON/SSSE3 endian convert for all sizes.
  • Uncompressed BYTE_IMG reads use direct pread for mmap on and off.
  • One-shot image reads use thin cpp.read_full instead of handle-cache scaffolding on the cold path.
  • Repeated cutout benches use the persistent subset reader (open once).
  • Deficit table: images any lag above ε; Arrow tables allow ≤1.05×; fitsio excluded from mmap-on peers. Linux CPU/CUDA strict-gate 0 deficits; Mac MPS 4 on exhaustive_mps_20260717_000853.

Docs

  • Site logo/favicon: torchfits-logo.png.
  • README / benchmark run IDs aligned with docs/benchmarks.md.

[0.9.1] - 2026-07-14

Fixed

  • Native wheel metadata now constrains PyTorch to the 2.10 ABI used to build the extension. Torchfits 0.9.0 incorrectly allowed newer incompatible libtorch releases, which could segfault during image or table conversion.
  • Native builds and imports now reject mismatched PyTorch ABIs, and every CI build path installs the same PyTorch minor used by the release wheels.

0.9.0 - 2026-07-14

Fixed

  • Writing one FITS file no longer invalidates borrowed native handles for unrelated files. Native cache clearing now defers closing in-use handles, preventing a subsequent read from dereferencing a closed CFITSIO handle.
  • Atomic table-column rewrites now close every managed HDUList borrower for the target path, so nested open contexts cannot retain an old inode and erase an earlier mutation.
  • write() normalizes os.PathLike targets before native cache invalidation.
  • Wheels no longer include the C++ build-source directory.
  • Numeric tensor-to-Arrow conversion now shares the tensor's NumPy buffer instead of iterating through PyTorch storage one byte at a time.
  • Automatic table predicates use the fast native full-read path followed by Arrow filtering; native row-wise pushdown remains available through the explicit backend="cpp" policy.

Added

  • read_polars() — one-call FITS-to-Polars convenience function. Calls read() with include_fits_metadata=True, converts via pl.from_arrow(rechunk=False), and returns a FITSPolarsFrame wrapper that preserves FITS column metadata (TFORM, TUNIT, TDIM, TNULL, TSCAL, TZERO) alongside the pl.DataFrame. Delegates __getattr__, __getitem__, __len__ to the wrapped DataFrame.
  • scan_polars() — genuine streaming Polars path. Yields pl.DataFrame batches via pl.from_arrow(batch, rechunk=False) over scan(), without materializing the entire Arrow table. Unlike to_polars_lazy(), no full table is built.
  • FITSPolarsFrame — lightweight dataclass wrapper around pl.DataFrame with field_meta and table_meta dicts for FITS metadata preservation.
  • Transform masks now thread through FITS-aware normalization and clipping; spectral resampling uses torch-native interpolation with parity references for vectorized continuum, phase-folding, wavelet, and sigma-clipping paths.
  • CANFAR CUDA exhaustive (exhaustive_cuda_0.9.0_20260714_065950) — 3,648 normalized rows across the mmap on/off and CUDA matrix; 7 deficits, all at or below 1.439×, with no large-N deficit.

Changed

  • Removed the never-implemented TensorHDU.stats() and its empty native result from the supported torchfits.cpp inventory instead of inventing statistics semantics during the 0.8 API freeze.
  • ci-local now runs its pre-build package-isolation checks against src/, so a clean Linux clone no longer depends on a pre-existing editable install.
  • Native cache environment limits are validated before loading Torch or the extension module, preserving useful configuration errors in clean installs.
  • Scoped extension-only visibility and semantic-interposition optimizations to _C; applying them directory-wide also changed vendored CFITSIO's C ABI and aborted Linux ASCII-table writes.
  • Raw, unmapped image reads now support FITS BITPIX=64 images as torch.int64, matching the mapped and scaled readers.
  • GitHub workflows use the Node 24-based actions/checkout@v5 and actions/setup-python@v6, and pin Apple Silicon testing to macos-15 instead of following the rolling macos-latest migration.
  • Removed the environment-dependent optional torch_frame inheritance from TableHDU and the torchfits.hdu.TensorFrame alias. FITS table columns stay as tensor/list mappings, Arrow is the interchange boundary, and Polars is the dataframe surface. Any legacy dataframe bridge remains outside torchfits.

  • rechunk=False default on to_polars(), to_polars_lazy(), scan_polars(), read_polars(), and top-level to_polars(). Avoids Polars' unnecessary chunk concatenation when Arrow data is already single-chunk (the common case from read()). Pass rechunk=True explicitly to restore the old behavior.

  • to_polars_lazy() docstring — clarified that it materializes the entire Arrow table eagerly before wrapping as LazyFrame. Users seeking true streaming should use scan_polars() instead.

Removed

  • "cpp_numpy" table backend alias — the deprecation alias introduced in 0.7.0 is removed. Pass backend="cpp" instead of "cpp_numpy". The DeprecationWarning is now a hard ValueError.
  • should_skip_cpp_numpy_for_where — internal alias removed from torchfits._table_engine. Use should_skip_cpp_for_where.

0.7.0 - 2026-07-11

Added

  • FitsTableIterableDataset — constant-memory table streaming via table.scan with worker sharding by scan batch index.
  • FitsCutoutDataset — map-style patch training from (path, hdu, x, y, …) cutout specs.
  • Zensical documentation site — zensical.toml, docs/index.md, GitHub Pages workflow, and pixi run docs-build / docs-serve.
  • migration_datasets.md — breaking-change guide for removed legacy datasets.
  • transforms.__all__ — explicit public transform catalog.
  • CI release-gate job — upstream parity, docs contract, data, transforms, and security smokes on Python 3.13.
  • Lab benchmark refresh (exhaustive_0.7.0_20260711_022156) — full exhaustive lab run (3516 rows, mmap matrix + MPS); CPU performance floor unchanged (core deficits ≤1.33×).
  • CANFAR CUDA exhaustive (exhaustive_cuda_0.7.0_20260711_055635) — 3626 rows, 11 deficits on staging GPU; artifacts archived to vos:sfabbro/torchfits-gpu-bench/.
  • CANFAR bench launcher — headless GPU sessions on staging with VOS persistence via vcp (scripts/launch_canfar_gpu_bench.sh, scripts/fetch_canfar_bench_vos.sh).

Changed

  • Torch-first table.read C++ path — backend="cpp" reads via read_fits_table_rows / TableReader.read_rows (torch tensors) instead of the numpy hop; Arrow conversion stays at the PyArrow boundary only.
  • Table backend rename — public backend "cpp_numpy" renamed to "cpp"; the old name still accepted with DeprecationWarning.
  • Legacy datasets removed — torchfits.FITSDataset and torchfits.IterableFITSDataset deleted; use torchfits.data typed datasets.
  • table.py trim — re-exports public API only (private _ helpers no longer re-exported from torchfits.table).
  • Package description — PyPI/README positioning for ML datasets + transforms.

Removed

  • src/torchfits/datasets.py — superseded by torchfits.data.

0.6.0 - 2026-07-09

Changed

  • Unified C++ table chunk reads: Refactored _read_cpp_numpy_table to clean up the 7-deep C++ dispatch fallback chain and hasattr checks, delegating directly to the modern C++ TableReader and read_fits_table_rows_numpy APIs. This successfully resolves Roadmap Track B1.
  • Version synchronization: Unified package version triplet to 0.6.0 across pyproject.toml, pixi.toml, and package source.
  • Blocking mypy in CI — the mypy src/ step in GitHub Actions is now a hard gate (previously non-blocking via || echo). All 103 type errors have been resolved across 18+ source files. Added [[tool.mypy.overrides]] in pyproject.toml for pyarrow.compute (attr-defined) and pyarrow.* (ignore_missing_imports).

[0.6.0b2] - 2026-07-09

Added

  • Predicate filter improvements (all sizes now use C++ pushdown): The predicate_filter path delegates to C++ for all table sizes, eliminating the Python fallback for narrow tables. The read_policy.py size threshold is removed — safe non-VLA tables always use C++ pushdown. Narrow-table predicate_filter lag vs fitsio reduced from ~2.86× (smallest) to ≤1.07×.
  • Lightweight is_compressed check: Compressed images use a fast O(1) header probe instead of opening and parsing the full HDU, reducing overhead on batched compressed-image reads.
  • Thread-safe caches: CacheManager internal data structures use std::shared_mutex for concurrent reader access, safe under multi-worker DataLoader patterns without global GIL serialisation.
  • Parallel scan with sequential fallback: The C++ mmap pushdown scan is now parallelised via at::parallel_for when torch::get_num_threads() > 1, with a zero-overhead sequential path when single-threaded. Added posix_madvise(POSIX_MADV_SEQUENTIAL) to the filtered scan path for kernel prefetch hints.
  • Lab benchmark refresh (mmap-on+off, 0.6.0b2): 2754 rows, 3 deficits in 20260709_163739 — down from 0.6.0b1's 14 deficits and 0.5.0b4's 22 deficits. Remaining 3-deficit breakdown:
  • 3 fitstable (narrow): predicate_filter on narrow_{10000,100000,1000000} (1.07–1.25× behind fitsio; narrow_1000 dropped below the deficit threshold). The gap is now dominated by Python dispatch + Arrow conversion overhead, not the C++ scan itself (which reaches near-parity with fitsio at ~11.4 ms vs ~11.0 ms for 1 M rows). All compressed-image deficits eliminated. The uint16/uint32 mmap-on regression that motivated 0.5.0b4's bswap+BZERO merge is no longer in the deficit table.
  • Multi-worker DataLoader coverage: tests/test_data.py now exercises make_loader(..., num_workers=2) for both FitsImageDataset and FitsImageIterableDataset. Tests fork a subprocess to keep CFITSIO's threadpool away from pytest's own threadpool, and verify that every file is seen exactly once regardless of num_workers and shuffle seed.
  • End-to-end FITS round-trip coverage in tests/test_transforms_e2e.py:
  • TestEndToEndImageRoundTrip — write / read / scale / inverse for INT16 with custom BSCALE/BZERO, BZERO=32768 unsigned convention, and INT32 with rescaling.
  • TestEndToEndTableRoundTrip — FITS binary tables with TSCAL/TZERO use real on-disk encoding (via astropy), then FITSScaleColumns.from_header + TNullToNan.from_header round-trip is verified to within the storage precision.
  • TestEndToEndFITSHeaderNormalize — full int16 BZERO=32768 round-trip through the header-driven normaliser.
  • Release-gate now includes tests/test_data.py, tests/test_transforms.py, and tests/test_transforms_e2e.py. This closes the torchfits.data documented with multi-worker test coverage and torchfits.transforms round-trip tests for scaled images and tables gate items.
  • AsymmetricLeastSquares(lam, p, max_iter, dim) — Eilers 2003 penalised baseline correction with asymmetric weights. Iteratively solves the Whittaker smoother (W + λD^T D)z = Wy with differential weighting (p above baseline, 1-p below). Standard in Raman/NIR spectroscopy. D^T D penalty matrix built in float64 for numerical stability at large λ. Additive decomposition (invertible).
  • AlphaShapeContinuum(half_window, iterations, dim) — Morphological closing (dilation→erosion) via unfold + max/min. Produces a guaranteed upper envelope (always ≥ signal). Practical approximation to the full alpha-shape algorithm (RASSINE). Additive decomposition (invertible).
  • AsymmetricSigmaClip(n_low, n_high, dim) — Simple one-pass asymmetric sigma-clipping outlier rejection using estimate_background (median + MAD). Supports different lower/upper sigma thresholds; replaces outliers with per-group median. Lossy (no inverse).
  • _build_d2_matrix internal helper for the n×n pentadiagonal second-difference penalty matrix D^T D used by the Whittaker smoother / AsLS.
  • 27 new tests for the three transforms (201 transforms tests total, all passing).
  • Example coverage for AsymmetricLeastSquares, AlphaShapeContinuum, and AsymmetricSigmaClip (later removed with the spectral/continuum hard-cut).
  • All three transforms exported to the root package for direct from torchfits import AsymmetricLeastSquares access.
  • Documentation for all three transforms in docs/api.md and README.md transform tables.

0.6.0b1 - 2026-07-08

Removed

  • Removed deprecated read_large_table function (use stream_table or read_table instead).
  • Custom WHERE AST evaluator runtime (~120 lines) from _where.py: _evaluate_cmp, _evaluate_in, _evaluate_between, _evaluate_isnull, _evaluate_where, and the evaluate_where public alias. Replaced with pyarrow.compute native predicates via _where_mask_for_table. The parser, tokenizer, normalizers, and where_columns_from_ast stay for C++ pushdown path compatibility.
  • Compressed parallel decompression path (~350 lines): try_read_compressed_rows_parallel, compressed_parallel_enabled/min_pixels/min_rows_per_thread/max_threads/hcompress_enabled helpers, load_bswap templates, FitsHandleGuard local class, is_parallel_compressed_codec_cached, compressed_parallel_cache, and hardware_concurrency dependency. CFITSIO's built-in decompression already covers this serially — the 2-thread cap meant the heuristic rarely activated.
  • Unused read_rice_parallel (~320 lines) from compression.cpp — vendored Rice decompression, nanobind binding, and the entire compression.cpp/compression.h files. Dead after compressed parallel path removal.
  • bind_compression from bindings.cpp — only bound read_rice_parallel.

Changed

  • 3→1 C++ read path merge: Extracted a single read_tensor_canonical() in fits_detail.h and converted three read paths (read_full_cached, read_full_nocache, FITSFile::read_tensor) into thin wrappers, eliminating ~455 lines of duplication.
  • API naming consistency: Renamed read_image_canonical → read_tensor_canonical and FITSFile::read_image → FITSFile::read_tensor, aligning C++ with the Python read_tensor/write_tensor API.
  • bswap+BZERO merge: Merged the two-pass byte-swap and BZERO offset into a single parallel_for in the multi-byte mmap fast path. For unsigned images (uint16 with BZERO=32768, uint32 with BZERO=2147483648), bswap + add executes in one traversal instead of two.
  • Unsigned mmap fast path unlocked: _read_unsigned_image_if_needed now defers to the C++ path when mmap=True, letting read_tensor_canonical handle unsigned conventions natively (single-pass bswap+BZERO returning uint16/uint32 directly). Previously Python preempted C++ by calling read_full_raw and doing a second offset pass — making the bswap+BZERO merge dead code. uint32_2d: 8.3× faster (now beats fitsio); uint16_2d: 3.5× faster; 5 deficits eliminated.
  • Vectorized string decode: Replaced per-row Python for loops in interop.py (to_pandas, to_arrow), table_hdu.py (get_string_column, to_fits), and table_hdu_ref.py (get_string_column) with np.char.decode() + np.char.rstrip() for significant speedup on large string columns.
  • Deduplicated fits_schema.py: column_tnull_map() delegates to _iter_tfields_indexed() instead of reimplementing the TTYPE/TNULL iteration loop.
  • Deduplicated unsigned dtype and TFORM parsing: _table/read.py now delegates to fits_schema.unsigned_column_dtypes_from_header() and fits_schema.iter_table_columns() instead of reimplementing TZERO/unsigned detection and TTYPE/TFORM header walks.
  • Table schema fast path: table.schema() skips data reads when where=None, inferring the Arrow schema directly from FITS TFORM header cards (≤1 header pass).
  • C++ source extraction: Split fits.cpp (4552 lines) into fits_detail.h, fits_file.h/.cpp, fits_rw.h; split table.cpp (3432 lines) into table_types.h, table_reader.h (header-only), table_mutation.h/.cpp. Removed extern "C" linkage from table mutation functions to fix UB from C++ exceptions crossing C ABI boundaries. Removed dead declarations, unused types, stale comments, and double includes.
  • Merged cache stats: CacheManager.get_stats() now pulls I/O engine metrics (io_hits, io_misses, io_total_requests) from the cache subsystem.
  • WHERE evaluator → Arrow compute: TableHDU.filter() now builds a minimal Arrow table and delegates to _where_mask_for_table (pyarrow.compute native predicates) instead of running the old NumPy-based custom evaluator. The parser stays for C++ pushdown path compatibility.
  • Table read unification: Extracted _read_ranges_as_chunk from _read_cpp_numpy_table into shared _table/engine.py, removing ~50 lines of duplicated code.
  • CI: Added non-blocking mypy src/ step to the GitHub Actions lint job.
  • Benchmark fairness fix: fitsio is no longer unconditionally skipped — runs when mmap=off for fair buffered-read comparisons (449 fitsio OK rows in fits domain, 180 in fitstable).
  • examples/example_image_dataset.py: optimize_for_dataset + correct pin_memory when reading directly to CUDA.
  • scripts/run_exhaustive_bench_and_patch_docs.sh skips rebuild when extension imports.

Fixed

  • Root I/O attributes now resolve to the actual public functions, preserving inspectable signatures, tracebacks, and identity while keeping bare import torchfits free of PyTorch, NumPy, Arrow, and the native extension.
  • torchfits.cpp now has an explicit FITS-native __all__; future compiled symbols no longer become public accidentally. Direct attribute delegation is retained for pre-1.0 compatibility.
  • Every lazy root export now has a matching TYPE_CHECKING declaration, so the shipped py.typed marker covers the complete documented root API.
  • Removed the empty, misleading cache extra: adaptive cache sizing uses the standard library. Documentation now states that PyArrow is the core table runtime while Pandas, Polars, and DuckDB are optional.
  • Runtime initialization no longer swallows native-load or invalid cache configuration errors and then marks the failed initialization as complete.
  • Header-card write failures and HDU header-preservation failures are no longer silently ignored; callers now receive the native error instead of a successful return with lost metadata. A dead duplicate header helper was removed.
  • Overwriting an existing FITS file is now transactional: the complete replacement is written beside the target and atomically installed only after success. Validation or native-write failures preserve the original bytes and file mode instead of deleting the user's file.
  • HDU insert, replace, and delete operations use the same transactional rewrite rule, so a partial multi-HDU rewrite cannot replace the original file.
  • Iterable HDU writes reject empty sequences, unsupported objects, header-only dictionaries, and non-tensor image payloads instead of silently emitting empty HDUs.
  • Image datasets now route through the unified image reader, so their documented mmap="auto" policy works instead of reaching the bool-only read_tensor boundary. Remaining immutable column tuples are normalized at public list boundaries.
  • TableHDU validates its trust boundary: non-mapping inputs and columns with inconsistent row counts fail immediately instead of creating an internally inconsistent table.
  • TableHDU.from_fits() now uses the public read_table() pipeline instead of opening a separate native table/header path, keeping cache, validation, and runtime initialization behavior consistent with the rest of the package.
  • Removed the duplicate where entry from the package root __all__ contract.
  • Scoped mypy's missing-import exceptions to optional dataframe integrations and the compiled extension, allowing real Python type errors to surface. Mypy now checks untyped function bodies and is a blocking local preflight and CI check; the resulting TableHDURef column-sequence mismatch was fixed at the Arrow boundary.
  • Release wheels now run image and table round-trip tests against the installed artifact, and macOS arm64 wheels use the platform's real minimum deployment target (11.0). Platform documentation now matches the wheel matrix. Vendored CFITSIO remains statically linked but its development headers, archive, and CMake/pkg-config metadata are no longer copied into wheels.
  • Multi-worker DataLoader tests now use a real __main__ guard, matching the macOS spawn contract instead of recursively creating workers from python -c; timeout failures preserve worker stderr for diagnosis.
  • Vectorized NULL evaluation: _where.py replaced Python-loop np.array([v is None for v in val]) with vectorized (val == None) for element-wise null checks.
  • Fixed unused imports in tests/test_cache_config.py.
  • Security: Block CFITSIO pipe injection bypass via leading ! prefix (!|command) in check_fits_filename_security; also enforced on unified cache open path.
  • GPU scale_on_device preserves narrow integer H2D for FITS signed-byte (int8) and unsigned uint16/uint32 conventions instead of promoting through float32 or int64 on CPU.

Added

  • Header: O(N) construction for large dict inputs via keyed fast-path in _set_card (2000 keys ~0.002s locally vs ~2.5s pre-fix).
  • Jupyter: Scrollable, sticky-header HTML repr for Header and HDUList.
  • tests/test_scale_on_device.py — signed-byte, unsigned, and fitsio parity checks.
  • Release gate includes test_scale_on_device.py.
  • .cursor/skills/release-api-freeze-review/ — pre-tag API/feature freeze review workflow.
  • _table/engine.py — shared C++ table read dispatch module with extracted _read_ranges_as_chunk helper (de-duplicated from _read_cpp_numpy_table).

Performance notes

  • Local bench_ml_loader.py diagnostic (30×512² float32, CPU, 2 epochs): Rice-compressed 1.12× vs fitsio; uncompressed within ~4% (tune handle cache for your file count).
  • Lab exhaustive refresh (exhaustive_mmap_0.5.0b4_20260630_162835, H100 MIG): 3626 rows, 13 deficits (down from 22). Integer CUDA gaps closed; remaining are marginal int8 (≤1.2×) and cold large_uint32_2d CPU vs astropy (~1.5×).
  • User-profile refresh (unsigned_mmap_fix_20260708, CPU, mmap=on): 1,377 rows, 25 deficits. torchfits_specialized uint32_2d now beats fitsio (was 5–10× behind); uint16_2d at ~1.6× vs fitsio (was ~5×). Remaining deficits dominated by medium-size unsigned reads and compressed HCOMPRESS. Torchfits dominates table I/O (886×–2,318× vs astropy), image reads (7.92× vs astropy, 1.76× vs fitsio on large float32), and repeated cutouts (17× vs astropy, 1.09× vs fitsio).

[0.5.0b4] - 2026-06-30

Changed

  • Centralized FITS binary-table header parsing in fits_schema (TFORM/VLA/string/bit/unsigned).
  • table.read no longer recurses for where=; strategy lives in _table_engine.read_policy.
  • Table C++ handle caches moved to _table.cache; I/O cache invalidation no longer depends on importing torchfits.table.
  • README highlights 0.5.0 features and published benchmark speedups; API docs document table backends and where= tuning environment variables.

Added

  • Unit tests for fits_schema, table where-read policy, and runnable example scripts.
  • Public torchfits.table.TABLE_BACKENDS constant.
  • pixi run release-gate task matching the release checklist parity/docs/examples gates.

0.5.0b3 - 2026-06-30

Changed

  • Refocused torchfits as a FITS I/O package: images, HDUs, headers, checksums, compression, FITS tables, caching, and table interop.
  • Removed stale public claims that torchfits owns WCS, sphere geometry, HEALPix, sky-domain simulation, or training pipelines. Those domains belong outside torchfits.
  • Added a roadmap and compatibility matrix that distinguish supported, partial, unsupported, and out-of-scope behavior.
  • Replaced broad parity claims with test-backed parity tiers for common fitsio, Astropy, and selected CFITSIO-backed workflows.

Added

  • Extended benchmark matrix: native uint16/uint32 2D image fixtures, typed binary tables (BIT/complex/string columns), and ASCII table fixtures.
  • bench_all.py --mmap-matrix runs mmap-on and mmap-off passes in one CSV so the I/O transport table can populate both disk→CPU and disk→RAM→CPU (plus GPU disk→CPU→GPU / disk→RAM→GPU when CUDA/MPS is available).
  • scripts/run_exhaustive_bench_and_patch_docs.sh for lab-profile bench-all on CUDA/MPS hardware with automatic docs/benchmarks.md refresh.
  • Lab CUDA benchmark snapshot exhaustive_mmap_0.5.0b3_20260630_063118 (3474 rows, mmap on+off matrix, 720 GPU transport rows on H100).
  • docs/parity.md for the public compatibility matrix.
  • Astropy upstream smoke coverage for common image, HDU, compressed-image, table, ASCII table, VLA, complex column, and scaled-image workflows.
  • Documentation integrity checks for stale WCS/sphere/HEALPix ownership claims.
  • Supported-status promotion for in-place mmap table updates on COMPLEX (1C/1M), BIT (8X), and fixed-width STRING (12A-style) columns. torchfits.table.update_rows(..., mmap=True) now writes these column types correctly on disk. Verified via raw byte inspection and an astropy upstream-reader roundtrip. VLA columns remain explicitly unsupported in the mmap fast path by design.
  • Astropy and fitsio upstream smoke coverage that exercises the COMPLEX / BIT / fixed-width STRING mmap-update parity shift, including right-padding to the declared column width and verification vs the upstream readers. The 8A-string assertion falls back to astropy because the local fitsio upstream misdecodes updated 8A rows (the on-disk bytes are bit-exact to the expected layout; this is an upstream-reader limitation, not a torchfits writer bug).
  • tests/test_astropy_upstream_smoke.py::test_astropy_compimage_compression_variants_match_torchfits exercising additional astropy.io.fits.CompImageHDU compression variants (RICE / HCOMPRESS / PLIO) round-tripped against torchfits.

Fixed

  • API docs and install guide now reference torchfits.cache for cache tuning (configure_for_environment, get_cache_stats, clear_cache) and the root I/O helpers get_cache_performance / clear_file_cache where appropriate.
  • Roadmap mmap limitations updated to match the parity matrix (BIT and fixed-width STRING mmap updates are supported; VLA and scaled columns remain partial).

Removed

  • Dataset/training helper namespace from the torchfits package contract.

0.5.0b2 - 2026-06-30

Fixed

  • Patched fitstable specialised column projection and row slicing benchmark errors due to invalid policy argument.
  • Cleaned up C++ build flags in bench-gpu to remove strict CUDA and Torch pins.
  • Reviewed C++ codebase for potential memory leaks, redundant hardware heuristics, and API bounds.

Added

  • Restored core FITS benchmarks from v0.3.2: ML DataLoader performance (bench_ml_loader.py) and GPU Memory usage/leak validator (bench_gpu_memory.py).
  • Added exhaustive progress print logging during benchmark execution.
  • Added persistent cutout / multi-cutout repeated read benchmarks (SubsetReader / open_subset_reader) for both CPU and GPU.
  • Added read_tensor for reading N-dimensional arrays (1D spectra, 2D images, 3D cubes, xD arrays) directly to a single PyTorch Tensor.
  • Added write_tensor as the specialized PyTorch-native writer for writing single PyTorch Tensors directly to FITS files.

Deprecated

  • Deprecated read_image in favor of the more general and PyTorch-native read_tensor.

0.5.0b1 - 2026-06-29

Changed

  • Repository home: github.com/astroai/torchfits.
  • Default development Python is 3.13 (pixi); supported install range remains 3.10+.
  • Development Status classifier promoted to Beta.
  • Removed obsolete diagnostic benchmarks, scratch scripts, and legacy HEALPix/WCS artifacts.
  • CI rewritten: ruff-only lint, multi-OS/Python test matrix, CFITSIO vendoring via extern/VERSIONS.txt.
  • Wheel builds: portable flags (no -march=native), cp310–cp313 on macOS and Linux.

Added

  • GPU I/O transport benchmark rows (bench_gpu_transports.py) with MPS on Apple Silicon and CUDA on Linux.
  • pixi run bench-mps for Apple Silicon accelerator benchmarks.
  • Automated benchmark report workflow (.github/workflows/bench-report.yml).
  • scripts/render_bench_deficits.py for documenting performance deficits without fixing them.

Fixed

  • Table mutations now invalidate FITS path caches via internal io helper (fixes torchfits._invalidate_path_caches AttributeError).

Earlier releases

Earlier 0.1.x through 0.3.x releases included broader experimental astronomy domains. The current package contract is FITS I/O only; consult the current README, API reference, roadmap, and parity matrix for supported behavior.