Tables¶
fitsy reads both binary tables (XTENSION = 'BINTABLE') and
ASCII tables (XTENSION = 'TABLE') into the
fitsy.BinTable and fitsy.AsciiTable types.
Both expose the same access patterns:
len(tbl)– number of rows.tbl.column_names– list of column names in declared order.tbl.data– structurednumpy.ndarray(one record per row).tbl["COLNAME"]– a 1-Dnumpy.ndarrayfor that column.tbl[i]– a row dict{colname: value}.tbl[a:b]– a list of row dicts.
Column edits do not currently round-trip through writeto –
table data is re-emitted from the bytes captured at load time.
Use fitsy.write() with a fresh fitsy.BinTableBuilder
to author a new table.
An ASCII integer column reads back as float64, and a cell that
matched TNULL is nan. The Rust API keeps the two apart:
AsciiTableHdu::cell_value returns None for a TNULL match
and an AsciiCell::Int otherwise.
Examples¶
Reading a binary table:
"""Read a binary table: columns, rows, and structured arrays.
Run from the repository root:
python examples/python/tables.py
"""
import os
import tempfile
import fitsy
import numpy as np
with tempfile.TemporaryDirectory() as td:
path = os.path.join(td, "catalog.fits")
fitsy.write(
path,
# A table-only file needs no placeholder image: `write` adds an
# empty primary HDU when the first builder is not an image.
[
fitsy.bintable(
{
"RA": np.array([10.0, 20.0, 30.0]),
"DEC": np.array([-5.0, 0.0, 5.0]),
"NAME": ["alpha", "beta", "gamma"],
},
extname="CATALOG",
),
],
)
with fitsy.open(path) as f:
tbl = f["CATALOG"]
print("columns:", tbl.column_names)
print("nrows :", len(tbl))
# Whole-table structured numpy array (zero-copy where possible).
arr = tbl.data
print("structured dtype:", arr.dtype)
# Single row by integer index.
row = tbl[0]
print("row 0 :", row["RA"], row["DEC"], row["NAME"])
# Slice of rows -> list of row dicts.
first_two = tbl[:2]
print("first two RAs:", [r["RA"] for r in first_two])
# Pull a column by name -> 1-D numpy array.
ras = tbl["RA"]
print("RA column dtype:", ras.dtype)
Writing and reading an ASCII table, including the TNULL sentinel
that marks an undefined numeric cell:
"""Write and read an ASCII table: fixed-width columns and TNULL.
Run from the repository root:
python examples/python/ascii_tables.py
"""
import os
import tempfile
import fitsy
with tempfile.TemporaryDirectory() as td:
path = os.path.join(td, "catalog.fits")
# `ascii_table` picks a TFORM code per column from the value kind.
# `formats` overrides that choice. A numeric column that holds an
# undefined cell needs a matching `tnulls` entry, because Standard
# Sec.7.2.5 gives a blank numeric field the value zero, so TNULL is
# the only marker of an undefined value. The sentinel must fit the
# field width, which is why COUNT declares `I6` rather than taking
# the narrower automatic width.
fitsy.write(
path,
[
fitsy.ascii_table(
{
"NAME": ["alpha", "beta", "gamma"],
"COUNT": [12, None, 37],
"FLUX": [1.5, 2.25, 3.125],
},
formats={"COUNT": "I6", "FLUX": "F9.3"},
tnulls={"COUNT": "---"},
units={"FLUX": "Jy"},
extname="CATALOG",
),
],
)
with fitsy.open(path) as f:
tbl = f["CATALOG"]
print("columns:", tbl.column_names)
print("nrows :", tbl.n_rows)
# An ASCII integer column reads back as float64, and a cell
# that matched TNULL is nan. A string cell keeps the padding
# that its fixed-width field carries.
counts = tbl.column("COUNT")
print("COUNT dtype:", counts.dtype, " values:", counts)
for i in range(tbl.n_rows):
row = tbl.row(i)
name, count, flux = row["NAME"], row["COUNT"], row["FLUX"]
print(f" {name!r:9} count={count!r:7} flux={flux}")
print("FLUX column:", tbl.column("FLUX"))
The same in Rust:
//! Build, write and read back an ASCII table extension.
//!
//! This shows `AsciiTableBuilder` declaring fixed-width columns, the
//! `TNULL` sentinel that marks an undefined integer cell, and the
//! typed cells that come back on the read side.
//!
//! Run from the repository root:
//!
//! cargo run --example ascii_table
use fitsy::hdu::builder::AsciiColumnData;
use fitsy::{AsciiCell, AsciiFormat, AsciiTableBuilder, FitsFile, FitsWriter, Hdu, ImageBuilder};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::temp_dir().join("fitsy_example_ascii_table.fits");
// A file needs a primary HDU. An ASCII table is an extension, so
// write an empty image first.
let primary = ImageBuilder::new(Vec::<u64>::new(), Vec::<f32>::new())?
.primary(true)
.build()?;
// Each column declares its own fixed field width through its
// `TFORMn` code. `A8` holds eight characters, `I6` holds a
// six-column integer, and `F9.3` holds a fixed-point real with
// three decimal places.
let mut b = AsciiTableBuilder::new();
b.add_column(
"NAME",
AsciiFormat::A(8),
AsciiColumnData::Str(vec![
"alpha".to_string(),
"beta".to_string(),
"gamma".to_string(),
]),
)?;
b.add_column(
"COUNT",
AsciiFormat::I(6),
// The middle cell is undefined, so this column needs a TNULL.
AsciiColumnData::Int(vec![Some(12), None, Some(37)]),
)?;
// TNULL is the only way an ASCII table marks a value undefined:
// Standard Sec.7.2.5 gives a blank numeric field the value zero.
b.tnull("---")?;
b.add_column(
"FLUX",
AsciiFormat::F(9, 3),
AsciiColumnData::Float(vec![1.5, 2.25, 3.125]),
)?;
b.unit("Jy")?;
b.extname("CATALOG");
let table = b.build()?;
let mut out = std::fs::File::create(&path)?;
let mut w = FitsWriter::new(&mut out);
w.write_hdu(&primary)?;
w.write_hdu(&table)?;
w.finish()?;
// Read it back.
let f = FitsFile::open(&path)?;
let Hdu::AsciiTable(tbl) = f.hdu_by_name("CATALOG", None)? else {
return Err("CATALOG is not an ASCII table".into());
};
println!(
"rows: {} row width: {} bytes",
tbl.n_rows(),
tbl.row_size()
);
for col in tbl.columns() {
println!(
" col {} {:8} TFORM={:?} TBCOL={} unit={:?}",
col.index, col.name, col.format, col.start, col.unit
);
}
// `cell_value` returns `Ok(None)` for a cell that matches TNULL.
let count = tbl.column_by_name("COUNT").ok_or("no COUNT column")?;
for row in 0..tbl.n_rows() {
match tbl.cell_value(row, count)? {
Some(AsciiCell::Int(v)) => println!(" COUNT[{row}] = {v}"),
Some(other) => println!(" COUNT[{row}] = {other:?}"),
None => println!(" COUNT[{row}] = undefined (matched TNULL)"),
}
}
std::fs::remove_file(&path)?;
Ok(())
}