Packages

Exfits provides Elixir bindings to the CFITSIO library for working with FITS (Flexible Image Transport System) files, commonly used in astronomy.

Current section

Files

Jump to
exfits lib exfits.ex
Raw

lib/exfits.ex

defmodule ExFITS do
@moduledoc """
High-level functions for working with FITS files through the ExFITS.NIF module.
## Example Usage
Reading a FITS file:
```
# Read header information
{:ok, header} = ExFITS.FITS.read_header("example.fits")
# Read image data with dimensions
{:ok, {width, height, data}} = ExFITS.FITS.read_image("example.fits")
# Read both header and image data
{:ok, %{header: header, data: data, width: width, height: height}} = ExFITS.FITS.read("example.fits")
```
Writing a FITS file:
```
# Write a FITS file with just image data
:ok = ExFITS.FITS.write_image("new_image.fits", data, width, height)
# Write a complete FITS file with header information and image data
:ok = ExFITS.FITS.write_fits("new_image.fits", data, width, height, header, bitpix: 8)
# Copy a FITS file preserving all header information
:ok = ExFITS.FITS.copy_with_header("example.fits", "example_copy.fits")
```
"""
alias ExFITS.NIF
@doc """
Open a FITS file to verify it exists and is a valid FITS file.
## Parameters
- path: Path to the FITS file
## Returns
- :ok on success
- {:error, status} on failure
"""
def open(path) when is_binary(path) do
NIF.open_fits(path)
end
@doc """
Read image data from a FITS file.
## Parameters
- path: Path to the FITS file
## Returns
- {:ok, {width, height, data}} where data is a binary containing float32 values
- {:ok, data} when using older NIF version (for backward compatibility)
- {:error, status} on failure
"""
def read_image(path) when is_binary(path) do
case NIF.read_image(path) do
{:ok, {width, height, data}} when is_integer(width) and is_integer(height) and is_binary(data) ->
{:ok, {width, height, data}}
{:ok, data} when is_binary(data) ->
# Handle backward compatibility
IO.puts("Warning: Using older NIF version without dimension information")
{:ok, data}
error -> error
end
end
@doc """
Read header data from a FITS file.
## Parameters
- path: Path to the FITS file
## Returns
- {:ok, header} where header is a map of keyword-value pairs
- {:error, status} on failure
"""
def read_header(path) when is_binary(path) do
NIF.read_header(path)
end
@doc """
Read both header and image data from a FITS file.
## Parameters
- path: Path to the FITS file
## Returns
- {:ok, %{header: header, data: data, width: width, height: height}} on success
- {:ok, %{header: header, data: data}} with older NIF version
- {:error, status} on failure
"""
def read(path) when is_binary(path) do
with {:ok, header} <- read_header(path),
read_result <- read_image(path) do
case read_result do
{:ok, {width, height, data}} ->
{:ok, %{header: header, data: data, width: width, height: height}}
{:ok, data} ->
# Backward compatibility - try to get dimensions from header
try do
{width, height} = get_dimensions(header)
{:ok, %{header: header, data: data, width: width, height: height}}
rescue
_ -> {:ok, %{header: header, data: data}}
end
error -> error
end
end
end
@doc """
Write image data to a new FITS file with specified dimensions.
## Parameters
- path: Path to create the new FITS file
- data: Binary containing float32 pixel data
- width: Width of the image in pixels
- height: Height of the image in pixels
## Returns
- :ok on success
- {:error, status} or {:error, :dimensions_mismatch} on failure
"""
def write_image(path, data, width, height) when is_binary(path) and is_integer(width) and is_integer(height) do
NIF.write_image(path, data, width, height)
end
@doc """
Create a 2D image from a list of lists of floats.
## Parameters
- data: A list of lists of floats, where each inner list represents a row
## Returns
- {binary, width, height} tuple containing the binary data and dimensions
"""
def create_image_from_lists(data) when is_list(data) and is_list(hd(data)) do
height = length(data)
width = length(hd(data))
# Verify all rows have the same width
if Enum.any?(data, fn row -> length(row) != width end) do
raise ArgumentError, "All rows must have the same length"
end
# Flatten and convert to binary using native endianness (same as CFITSIO)
binary = data
|> List.flatten()
|> Enum.map(fn x -> <<x::float-32-native>> end)
|> IO.iodata_to_binary()
{binary, width, height}
end
@doc """
Write a 2D image from a list of lists of floats to a FITS file.
## Parameters
- path: Path to create the new FITS file
- data: A list of lists of floats, where each inner list represents a row
## Returns
- :ok on success
- {:error, status} on failure
"""
def write_image_from_lists(path, data) when is_binary(path) and is_list(data) do
{binary, width, height} = create_image_from_lists(data)
write_image(path, binary, width, height)
end
@doc """
Extract dimensions from the header of a FITS file.
## Parameters
- header: FITS header map as returned by read_header/1
## Returns
- {width, height} tuple
"""
def get_dimensions(header) do
width = Map.get(header, :"NAXIS1")
height = Map.get(header, :"NAXIS2")
{width, height}
end
@doc """
Copy a FITS file with its image data and header information.
## Parameters
- source_path: Path to the source FITS file
- dest_path: Path to the destination FITS file
- preserve_bitpix: Whether to preserve the original bit depth (default: true)
## Returns
- :ok on success
- {:error, reason} on failure
"""
def copy(source_path, dest_path, preserve_bitpix \\ true) when is_binary(source_path) and is_binary(dest_path) do
with {:ok, header} <- read_header(source_path),
read_result <- read_image(source_path) do
# Get the original BITPIX value if we're preserving it
bitpix = if preserve_bitpix, do: Map.get(header, :BITPIX, -32), else: -32 # Default to float if not preserving
case read_result do
{:ok, {width, height, data}} ->
# Write with the original BITPIX value
case NIF.write_image(dest_path, data, width, height, bitpix) do
:ok ->
# If image write is successful, copy the header cards
case copy_header_cards(source_path, dest_path, header) do
:ok -> :ok
error -> error
end
error -> error
end
{:ok, data} ->
# No dimensions, get them from the header
{width, height} = get_dimensions(header)
case NIF.write_image(dest_path, data, width, height, bitpix) do
:ok ->
# If image write is successful, copy the header cards
case copy_header_cards(source_path, dest_path, header) do
:ok -> :ok
error -> error
end
error -> error
end
error -> error
end
end
end
@doc """
Copy header cards from one FITS file to another.
## Parameters
- source_path: Path to the source FITS file (not used directly, header is passed separately)
- dest_path: Path to the destination FITS file to update
- header: Map containing header cards to copy
## Returns
- :ok on success
- {:error, reason} on failure
"""
def copy_header_cards(_source_path, dest_path, header) when is_binary(dest_path) and is_map(header) do
# Make sure the file exists before trying to update it
if File.exists?(dest_path) do
# Use the NIF function to write the header cards
result = NIF.write_header_cards(dest_path, header)
# Debug output
IO.puts("Writing header cards to #{dest_path}: #{inspect(result)}")
result
else
IO.puts("Cannot write header cards: file #{dest_path} does not exist")
{:error, :file_not_found}
end
end
@doc """
Convert binary float data from a FITS file to a list of lists.
## Parameters
- binary: Binary data from read_image/1
- width: Width of the image
- height: Height of the image
## Returns
- List of lists representing the 2D image
"""
def binary_to_lists(binary, width, height) do
for row <- 0..(height-1) do
for col <- 0..(width-1) do
offset = (row * width + col) * 4
<<value::float-32-native>> = binary_part(binary, offset, 4)
value
end
end
end
@doc """
Create a new FITS file with both image data and header information in a single operation.
## Parameters
- path: Path to create the new FITS file
- data: Binary containing float32 pixel data
- width: Width of the image in pixels
- height: Height of the image in pixels
- header: Map of header cards to include
- options: Keyword list of options:
- bitpix: FITS BITPIX value (default: value from header or -32 for float)
## Returns
- :ok on success
- {:error, reason} on failure
"""
def write_fits(path, data, width, height, header, options \\ []) do
# Get bitpix from options, header, or default to -32 (float)
bitpix = Keyword.get(options, :bitpix) ||
Map.get(header, :BITPIX) ||
-32
# Write the file with image data and header in a single operation
NIF.write_fits_file(path, data, width, height, bitpix, header)
end
@doc """
Copy a FITS file with header preservation in a single operation.
## Parameters
- source_path: Path to the source FITS file
- dest_path: Path to the destination FITS file
- options: Keyword list of options:
- preserve_bitpix: Whether to preserve the original bit depth (default: true)
## Returns
- :ok on success
- {:error, reason} on failure
"""
def copy_with_header(source_path, dest_path, options \\ []) do
preserve_bitpix = Keyword.get(options, :preserve_bitpix, true)
with {:ok, header} <- read_header(source_path),
{:ok, {width, height, data}} <- read_image(source_path) do
# Get the original BITPIX value if we're preserving it
bitpix = if preserve_bitpix, do: Map.get(header, :BITPIX, -32), else: -32
# Write the destination file with all header information
write_fits(dest_path, data, width, height, header, bitpix: bitpix)
else
{:ok, data} ->
# Backward compatibility - try to get dimensions from header
with {:ok, header} <- read_header(source_path),
{width, height} <- get_dimensions(header) do
# Get the original BITPIX value if we're preserving it
bitpix = if preserve_bitpix, do: Map.get(header, :BITPIX, -32), else: -32
# Write the destination file with all header information
write_fits(dest_path, data, width, height, header, bitpix: bitpix)
end
error -> error
end
end
@doc """
Convert FITS image data to an Nx tensor.
## Parameters
- path: Path to the FITS file
- options: Optional map of options (for future expansion)
## Returns
- {:ok, tensor} where tensor is an Nx tensor containing the image data
- {:error, reason} on failure
"""
def to_nx(path, _options \\ %{}) do
if Code.ensure_loaded?(Nx) do
with {:ok, {width, height, data}} <- read_image(path) do
# Convert binary data to Nx tensor with proper shape
# FITS data is stored in big-endian format, ensure proper conversion
tensor = data
|> maybe_swap_endianness()
|> Nx.from_binary(:f32)
|> Nx.reshape({height, width})
{:ok, tensor}
end
else
{:error, :nx_not_available}
end
end
@doc """
Write an Nx tensor to a FITS file.
## Parameters
- tensor: Nx tensor containing the image data
- path: Path to create the new FITS file
- options: Optional map of options:
- :bitpix - FITS BITPIX value (default: -32 for float)
- :header - Map of header keywords to add
## Returns
- :ok on success
- {:error, reason} on failure
## Example
iex> tensor = Nx.iota({100, 100})
iex> ExFITS.write_nx(tensor, "output.fits")
:ok
"""
def write_nx(tensor, path, options \\ %{}) do
if Code.ensure_loaded?(Nx) do
# Get the shape of the tensor
shape = Nx.shape(tensor)
# Extract dimensions based on shape
{height, width} = case shape do
{h, w} -> {h, w}
{h, w, _} -> {h, w} # For 3D tensors, flatten the last dimension
{h} -> {h, 1} # For 1D tensors, treat as single row
_ -> raise ArgumentError, "Unsupported tensor shape: #{inspect(shape)}"
end
# Convert to binary data in float32 format and handle endianness
data = tensor
|> Nx.as_type(:f32)
|> Nx.to_binary()
|> maybe_swap_endianness() # Ensure proper byte order for FITS
# Get header if provided or create basic headers
header = Map.get(options, :header, %{})
# Get bitpix from options or default to float (-32)
bitpix = Map.get(options, :bitpix, -32)
# Write to file
write_fits(path, data, width, height, header, bitpix: bitpix)
else
{:error, :nx_not_available}
end
end
# Helper to swap endianness if needed
# FITS uses big-endian format, but Nx typically works with the system's native endianness
defp maybe_swap_endianness(data) do
case :erlang.system_info(:endian) do
:little -> swap_float32_endianness(data)
:big -> data # FITS uses big-endian format
end
end
# Swap byte order for each 4-byte float
defp swap_float32_endianness(data) do
for <<a::8, b::8, c::8, d::8 <- data>>, into: <<>> do
<<d::8, c::8, b::8, a::8>>
end
end
end