HATS Guide#
Last updated: 2026-06-08 | HATS version: v0.9.2
Canonical reference for AI coding assistants working on HATS (Hierarchical Adaptive Tiling Scheme).
Tool-specific files (CLAUDE.md, .github/copilot-instructions.md) contain only tool-specific overrides
and reference this file for shared guidance. Edit this file for changes that should
apply to all AI assistants; edit tool-specific files only for tool-specific behavior.
Version notice for AI assistants: If anything in this guide conflicts with what you observe in the actual code (missing methods, different signatures, changed behaviour), the guide may be outdated. Check the version above against the installed package version (
python -c "import hats; print(hats.__version__)") and alert the user about the changes thatHATS_GUIDE.mdmight need. Do not silently assume the guide is correct.
What is HATS#
HATS is a storage format and Python library for partitioning large astronomical catalogs on the celestial sphere. It divides the sky into HEALPix pixels and stores each pixel’s data as a separate Parquet file, enabling efficient spatial queries and distributed computation without loading the full dataset.
The HATS Python library provides:
Classes for reading and representing catalog structure and metadata
Path utilities for navigating the on-disk layout
HEALPix pixel math and utilities
MOC (Multi-Order Coverage) map support
Plotting utilities for visualizing sky maps
LSDB operates on top of HATS — every lsdb.Catalog holds a hc_structure attribute
that is a hats.catalog.Catalog instance.
Design goals and north stars#
CRITICAL: Always keep these design principles in mind when making changes to HATS.
Storage format correctness is paramount. HATS defines the on-disk layout that LSDB and downstream tools rely on. The partition layout, metadata conventions, and path structure must be kept consistent. Any format change must be backward-compatible or explicitly versioned.
HEALPix NESTED ordering is the backbone. All partition identification, MOC operations,
and pixel-math utilities must be consistent with the HEALPix NESTED scheme. Never mix RING and NESTED
ordering silently. Use the cdshealpix and mocpy Python libraries for core HEALPix math operations.
Metadata, not computation. HATS classes describe what is on disk - they do not load row data or perform analytics. Keep catalog classes lightweight: they hold structural metadata and provide path resolution. Row-level computation belongs in LSDB.
LSDB compatibility is the primary consumer. LSDB depends on HATS classes extensively. API changes cascade to LSDB and downstream users. Maintain backward compatibility; if breaking changes are necessary, be explicit and loud about it.
Slim API surface. Do not add new public API methods unless asked. Prefer composing existing primitives. If you think a new method is needed, propose it first and get agreement on the design before implementing.
Backwards compatibility. Maintain backward compatibility where possible! If breaking changes are necessary, be loud about it.
Document current behavior. When migrating away from old patterns, use @deprecated
with a helpful message rather than silently removing behavior.
Docstrings and type safety. All public methods must have complete NumPy-style docstrings and accurate type annotations.
Coding advice#
Do not push or open PRs unless explicitly asked.
When changing code, ensure that the current assumptions of the change appear to have always been true.
Leave code better than you find it over keeping old assumptions around.
Development setup#
HATS and LSDB are typically developed in the same local environment. Prefer installing HATS into the existing LSDB environment rather than creating a new one. Only create a fresh environment if you need.
Python ≥ 3.11 (see
pyproject.tomlrequires-python)If you need a new env
Using conda:
conda create -n hats python=3.11 && conda activate hatsUsing venv:
python -m venv .venv && source .venv/bin/activate
Clone and install:
git clone https://github.com/astronomy-commons/hats.git && cd hatsRun the setup script:
echo 'y' | bash .setup_dev.shInstalls the package in editable mode with dev and full extras
Installs pre-commit hooks
Alternative manual install:
pip install -e .'[dev]' && pre-commit installFor full optional features (e.g. plotting, polygon search):
pip install -e '.[full]'For bleeding-edge dependency versions (nested-pandas from
main):pip install -r requirements.txtFor documentation dependencies:
pip install -r docs/requirements.txt
Common commands#
# Run the full test suite (includes doctests in src/ and docs/)
python -m pytest
# Run only unit tests (skip doctest collection from docs/)
python -m pytest tests/
# Run with coverage reporting
python -m pytest --cov=hats --cov-report=xml
# Lint
pylint src/ --rcfile=./src/.pylintrc
pylint tests/ --rcfile=./tests/.pylintrc
# Format
black src/ tests/ && isort src/ tests/
# Type check
mypy src/ tests/ --ignore-missing-imports
# Pre-commit (runs black, isort, pylint, mypy ...)
pre-commit run --all-files
# Build docs. Requires `docs/requirements.txt` dependencies installed.
cd docs && make html
# Run ASV benchmarks
cd benchmarks && asv run --quick
Repository structure#
src/hats/ Main package
src/hats/catalog/ Catalog class hierarchy (Catalog, MarginCatalog, MapCatalog, AssociationCatalog, IndexCatalog)
src/hats/pixel_math/ HEALPix pixel math and spatial index utilities
src/hats/pixel_tree/ PixelTree, MOC filtering, and pixel alignment
src/hats/io/ File I/O, path helpers, and metadata utilities
src/hats/inspection/ Plotting and visualization utilities
src/hats/loaders/ Catalog loading from disk or object store
src/hats/search/ Region search utilities (cone, box, polygon, MOC)
tests/hats/ Test suite (mirrors src/ layout)
tests/data/ Small HATS-formatted test catalogs
benchmarks/ ASV performance benchmarks
docs/ Sphinx documentation sources
docs/notebooks/ Jupyter notebook tutorials
Key files:
File |
Purpose |
|---|---|
|
Project metadata, dependencies, pytest/black/mypy config |
|
Public API - everything exported here is stable public surface |
|
Main |
|
Base |
|
|
|
|
|
|
|
|
|
|
|
|
|
Path helpers for constructing on-disk file paths |
Core concepts#
HEALPix partitioning#
HATS partitions the sky using the HEALPix pixelisation scheme in NESTED ordering:
At order
k, the sky is divided into12 × 4^kequal-area pixels.Each pixel at order
khas exactly 4 children at orderk+1.Higher orders mean finer resolution and more (smaller) pixels.
HATS catalogs are multi-order:
Dense regions use high-order (small) pixels; sparse regions use low-order (large) pixels.
Pixels are balanced in the number of rows or size in memory.
Each partition is identified by a HEALPix (Norder, Npix) pair:
Norder- HEALPix order (integer)Npix- pixel number in NESTED ordering at that order (integer)
Spatial index (_healpix_29)#
Rows in a HATS catalog often carry a _healpix_29 column: the HEALPix pixel number for that
row’s sky position at the fixed order 29. Order 29 is the highest order that fits in a 64-bit
integer (12 × 4^29 pixels), so it serves as a universal, lossless spatial key.
The column enables two key operations without re-reading ra/dec:
Partition assignment — right-shift by
2 × (29 − target_order)bits to get the containing pixel at any coarser order:pixel_at_order_k = _healpix_29 >> (2 * (29 - k)).Row-level filtering — compare
_healpix_29against the order-29 interval of a search region to efficiently skip rows outside the region of interest.
Helpers in spatial_index.py:
compute_spatial_index(ra, dec)— compute_healpix_29values from coordinates.spatial_index_to_healpix(ids, target_order)— convert index values to pixels at a coarser order.healpix_to_spatial_index(order, pixel)— get the minimum_healpix_29value inside a given pixel.
Catalog types#
Type |
|
Purpose |
|---|---|---|
Object / Source |
|
Standard point-source catalog |
Margin |
|
Boundary objects duplicated from adjacent pixels |
Index |
|
Secondary index on a non-spatial column (e.g. object ID) |
Map |
|
Continuous sky map (non-point-source data) |
Association |
|
Cross-catalog join table (with extra columns) |
On-disk directory layout#
my_catalog/
├── hats.properties # Primary metadata (key=value format)
├── partition_info.csv # One row per partition: Norder,Npix
├── skymap.fits # FITS image with counts per pixel at a fixed high order (e.g. 10)
└── dataset/
├── _metadata # Parquet metadata with statistics
├── _common_metadata # Parquet schema metadata
├── Norder=1/
│ └── Dir=0/
│ ├── Npix=44.parquet
│ └── Npix=45.parquet
└── Norder=2/
└── Dir=0/
└── Npix=176.parquet
hats.properties file#
The primary metadata file uses a simple key=value format. Key fields:
obs_collection=my_catalog # Catalog name
dataproduct_type=object # Catalog type
hats_nrows=1000000 # Total row count
hats_col_ra=ra # RA column name
hats_col_dec=dec # Dec column name
hats_col_healpix=_healpix_29 # High-precision HEALPix index column
hats_col_healpix_order=29 # Order of the index column (can be lower than 29)
hats_npix_suffix=.parquet # Partition file extension
hats_skymap_order=10 # HEALPix order of the skymap.fits density image
hats_max_rows=1000000 # Maximum row count in any single partition
hats_estsize=512 # Estimated catalog size on disk, in KiB
moc_sky_fraction=0.083 # Fraction of sky covered
hats_builder=hats-import v0.6.6, hats v0.6.6 # Tool(s) that created the catalog
hats_version=v1.0 # HATS format version
hats_release_date=2025-08-22 # Release date of the HATS standard
hats_creation_date=2025-10-06T14:20UTC # Catalog creation date
partition_info.csv#
Lists every partition present in the catalog, one per line:
Norder,Npix
1,44
1,45
1,46
2,176
This is the authoritative list of which Parquet files should exist under
dataset/. LSDB reads this at open time to build the Dask task graph.
Margin catalog#
Same layout as an object catalog, with two differences in hats.properties:
dataproduct_type=margin
hats_margin_threshold=3600.0 # margin radius in arcseconds
hats_primary_table_url=../my_catalog
Catalog collection#
A collection groups a primary catalog with its associated margin and index catalogs under a single root directory. A collection.properties file at the root lists the members. When LSDB opens a collection with open_catalog(), the default margin is automatically loaded and attached as catalog.margin.
my_collection/
├── collection.properties # Lists member catalog paths
├── my_catalog/ # Primary object catalog (full layout above)
├── my_catalog_margin/ # Margin catalog (full layout above)
└── my_catalog_index/ # Index catalog (optional)
Architecture: Catalog class hierarchy#
All catalog types inherit from HealpixDataset. The class hierarchy is:
Dataset
└── HealpixDataset
├── Catalog (object / source)
├── MarginCatalog (margin)
├── MapCatalog (map)
├── AssociationCatalog (association)
└── IndexCatalog (index)
hats.catalog.Catalog - the hc_structure object#
Every lsdb.Catalog exposes .hc_structure, which is an instance of
hats.catalog.Catalog. It holds the full structural description of the catalog
without any row data.
hc = cat.hc_structure
hc.catalog_info # TableProperties - metadata (see below)
hc.pixel_tree # PixelTree - which pixels exist and at what order
hc.schema # pyarrow.Schema - column names and types
hc.catalog_path # pathlib.Path - root directory on disk
hc.moc # mocpy.MOC | None - sky coverage as MOC
hc.on_disk # bool - True if loaded from disk
# Convenience delegation
hc.get_healpix_pixels() # list[HealpixPixel]
hc.filter_from_pixel_list(pixels) # new `hats.catalog.Catalog` restricted to pixels
hats.catalog.TableProperties - catalog metadata#
Accessed as cat.hc_structure.catalog_info. Contains every field from
hats.properties as typed Python attributes:
info = cat.hc_structure.catalog_info
info.catalog_name # str - "my_catalog"
info.catalog_type # str - "object" | "margin" | "index" | "association"
info.ra_column # str - "ra"
info.dec_column # str - "dec"
info.healpix_column # str - "_healpix_29"
info.healpix_order # int - 29
info.total_rows # int - total row count
info.hats_estsize # float - estimated size in KiB
info.default_columns # list[str] | None - columns loaded by default
info.npix_suffix # str - ".parquet"
# Margin-catalog-specific
info.margin_threshold # float - radius in arcseconds
# Index-catalog-specific
info.indexing_column # str - column being indexed
# Association-catalog-specific
info.primary_catalog # str - path to the primary (left) catalog
info.primary_column # str - ID column in the primary catalog
info.primary_column_association # str - matching column in the association table
info.join_catalog # str - path to the join (right) catalog
info.join_column # str - ID column in the join catalog
info.join_column_association # str - matching column in the association table
info.assn_max_separation # float - maximum match separation in arcseconds
info.contains_leaf_files # bool - whether leaf parquet files are present
hats.pixel_tree.PixelTree - the pixel tree#
Represents which HEALPix pixels exist in the catalog and their order. Accessed
as cat.hc_structure.pixel_tree.
tree = cat.hc_structure.pixel_tree
tree.get_healpix_pixels() # list[HealpixPixel] - all partitions
tree.get_max_depth() # int - highest order in the catalog
tree.to_moc() # mocpy.MOC - sky coverage
tree.to_depth29_ranges() # np.ndarray of shape (N, 2) - intervals at order 29
HealpixPixel(1, 44) in tree # bool - O(log N) containment check
How the pixel tree works#
The pixel tree stores each healpix pixel as a range of order-29 pixels. For example, a pixel at order 1 with Npix=44 corresponds to an interval of 4^28 pixels at order 29. The pixel tree is an ordered list of these intervals. To check if a pixel is in the tree, we convert it to its order-29 interval and do a binary search to see if it matches any of the stored intervals.
Why use order-29 intervals?#
Using order-29 intervals allows us to represent any pixel at any order as a contiguous range of pixels at a fixed high order. This simplifies the logic since we only need to deal with one fixed order internally. Order 29 is chosen because it is the highest order that can fit within a 64-bit integer, allowing us to use efficient integer arithmetic for pixel math and containment checks.
PixelAlignment#
One of the most important uses of the pixel tree is ‘aligning’ multiple catalogs to each other, figuring out which pixels overlap between them, and creating a mapping of which pixels in one catalog correspond to which pixels in the other. This is important for crossmatching and other operations that need to combine data from multiple catalogs.
To do this, we use the hats.pixel_tree.pixel_alignment.align_trees method which takes two pixel trees,
iterates through both of their pixel interval lists in order, checks for each pair of intervals whether they
overlap, and if so computes the intersection of those intervals and converts it back to the corresponding
pixels at the original orders, and iterating to the next intervals in one or both trees depending on which
one has the smaller next interval. This is an efficient O(N) operation where N is the total number of pixels
in both trees.
The result is a mapping of which pixels in one catalog correspond to which pixels in the other. The other result from aligning pixel trees is an output aligned pixel tree which is the union of the two input trees, covering the intersection of the two catalogs, with pixels split as needed to ensure that any pixel in the aligned tree is fully contained in a single pixel in each of the input trees. This means that the aligned tree can be the output structure for a crossmatched catalog, ensuring that each partition in the output is roughly no bigger than a single partition in either input catalog, which keeps file sizes manageable.
The pixel alignment also supports a how parameter that controls how to handle pixels present in one tree
but not the other:
inner(default): only pixels present in both trees.left: all pixels from the first tree; empty partitions for pixels absent from the second.right: all pixels from the second tree; empty partitions for pixels absent from the first.outer: all pixels from both trees; empty partitions wherever each is absent from the other.
hats.pixel_math.HealpixPixel - a single HEALPix pixel identifier#
from hats.pixel_math import HealpixPixel
pix = HealpixPixel(order=1, pixel=44)
pix.order # int - 1
pix.pixel # int - 44
Margin catalogs and why they matter#
A margin catalog stores a copy of every object that lies within
margin_threshold arcseconds of a pixel boundary, duplicated into the adjacent
pixel’s margin file. This ensures that spatial operations spanning partition
edges (crossmatch or joins near a boundary) see all relevant objects without
loading the entire neighboring partition.
Rule of thumb: To ensure completeness of the result, margin_threshold
must be ≥ the search radius used in any cross-catalog operation.
# The margin is attached automatically when opening a collection
cat = lsdb.open_catalog("/path/to/collection")
cat.margin # MarginCatalog | None
cat.margin.hc_structure.catalog_info.margin_threshold # float, arcseconds
# Crossmatch uses the margin automatically when cat.margin is set
xmatch = cat.crossmatch(other, radius_arcsec=1.0)
Path utilities#
hats.io.paths provides helpers for constructing file paths from
(Norder, Npix) pairs without hard-coding the layout:
from hats.io import paths
# Path to a single partition file
paths.pixel_catalog_file(catalog_base_path, HealpixPixel(order=1, pixel=44))
# → catalog_base_path/dataset/Norder=1/Dir=0/Npix=44.parquet
# Path to the parquet common metadata file
paths.get_common_metadata_pointer(catalog_base_path)
# → catalog_base_path/dataset/_common_metadata
# File pointer to FITS image file
paths.get_skymap_file_pointer(catalog_base_path)
# → catalog_base_path/skymap.fits or catalog_base_path/skymap.K.fits
Typical HATS workflow#
A typical HATS workflow (outside of LSDB) involves:
Loading a catalog structure from disk or object store.
Exploring metadata to understand the catalog shape, coverage, and schema.
Filtering by pixel or region to get a subset of partitions.
Constructing file paths for downstream readers using path utilities.
Load a catalog#
from hats.loaders import read_hats
# Load from local disk or object store (returns CatalogCollection or Dataset)
cat = read_hats("/path/to/catalog")
# Catalog collection
cat = read_hats("/path/to/collection")
cat.main_catalog # Main catalog (`hats.catalog.Catalog`)
cat.all_margins # All margins
cat.default_margin # Default margin name
cat.all_indexes # All indexes
cat.default_index_field # Default index field name
cat.get_index_dir_for_field("object_id") # Pointer to "object_id" index field catalog
Explore metadata#
# Partition layout
print(cat.get_healpix_pixels())
# Maximum partition order
max_order = cat.pixel_tree.get_max_depth()
# Metadata
info = cat.catalog_info
print(info.catalog_name, info.total_rows, info.ra_column, info.dec_column)
# Arrow schema (column names + types, no data loaded)
print(cat.schema)
# Sky coverage fraction
print(info.moc_sky_fraction)
Visualize coverage#
# Plot HEALPix partition map
cat.plot_pixels()
# Plot MOC sky coverage
cat.plot_moc()
# Plot point-density map
hats.inspection.plot_density(cat)
Filter by pixel or region#
from hats.pixel_math import HealpixPixel
# Restrict to a specific list of pixels
pixels = [HealpixPixel(order=1, pixel=44), HealpixPixel(order=1, pixel=45)]
filtered = cat.filter_from_pixel_list(pixels)
# Filter using region filters
filtered = cat.filter_by_cone(ra=47.1, dec=6, radius_arcsec=30 * 3600) # Cone
filtered = cat.filter_by_box(ra=(280, 300), dec=(-40, -30)) # Box
filtered = cat.filter_by_polygon(vertices=[(300, -50), (300, -55), (272, -55), (272, -50)]) # Polygon
# Filter using any other MOC
from mocpy import MOC
orders = np.array([1, 1, 2])
pixels = np.array([45, 46, 128])
max_depth = 2
moc = MOC.from_healpix_cells(pixels, orders, max_depth)
filtered = cat.filter_by_moc(moc)
Construct partition file paths#
from hats.io import paths
from hats.pixel_math import HealpixPixel
for pixel in cat.get_healpix_pixels():
path = paths.pixel_catalog_file(cat.catalog_path, pixel)
# pass `path` to pyarrow.parquet.read_table or similar
Testing Conventions#
File naming:
tests/hats/test_<name>.py, mirroring thesrc/hats/layout.Fixtures: defined in
tests/conftest.py. Use existing fixtures; do not duplicate test data. All fixtures are backed by tiny HATS catalogs intests/data/.Default test run:
python -m pytestDoctest enforcement:
pytestis configured with--doctest-modulesand--doctest-glob=*.rst. All public docstring examples must be runnable and correct.No network in unit tests. Test data lives in
tests/data/; do not fetch from the internet in unit tests.
Key Conventions#
Line length: 110 characters (
blackandisortboth enforce this).Import style:
isortwithprofile = "black". Do not hand-tune import order.Docstrings: NumPy style. All public functions and methods require a complete docstring including
ParametersandReturns. Try to also include anExamplesblock.Deprecation: use
@deprecated(version="X.Y", reason="...")from thedeprecatedpackage. Never silently remove behavior._version.pyis auto-generated bysetuptools_scmfrom git tags. Never edit it by hand; it is excluded from coverage and linting.All file paths use
UPathfromuniversal-pathlib. Do not use rawstrpaths in internal code; wrap withUPathto support both local and remote (S3, GCS) stores.CatalogTypeenum is the canonical source for catalog type strings. Do not comparecatalog_typeagainst raw string literals; useCatalogType.OBJECTetc.
CI/CD and GitHub Workflows#
testing-and-coverage.yml- runs on every PR and push tomain; matrix over Python 3.11–3.14; uploads coverage to Codecov.smoke-test.yml- daily at 06:45 UTC; tests both[dev]and[full]extras across Python 3.11–3.14.testing-windows.yml- Windows-specific test matrix.asv-main.yml/asv-pr.yml/asv-nightly.yml- ASV performance benchmarks; PR results are posted back to the PR.publish-to-pypi.yml- triggered on tagged releases.pre-commit-ci.yml- automated pre-commit hook checks for format/lint/mypy.