use std::fs::File;
use std::io::{Read, Write};
use crate::data_array::DataArray;
use fitsrs::hdu::data::bintable::{ColumnId, DataValue};
use fitsrs::{Fits, HDU, card::Value};
use rayon::prelude::*;
fn parse_tform(tform: &str) -> Option<(usize, char)> {
let tform = tform.trim();
let type_char = tform.chars().last()?;
let count_str = tform.trim_end_matches(type_char);
let count: usize = if count_str.is_empty() {
1
} else {
count_str.parse().ok()?
};
Some((count, type_char))
}
fn try_read_float32_column_native(
_filename: &str,
mmap_data: &[u8],
col_idx: usize,
) -> Option<(Vec<f32>, i64)> {
use std::io::Cursor;
let cursor = Cursor::new(mmap_data);
let mut fits = Fits::from_reader(cursor);
let mut nside: i64 = 0;
while let Some(Ok(hdu)) = fits.next() {
if let HDU::XBinaryTable(hdu) = hdu {
let header = hdu.get_header();
let has_explicit_indexing = match header.get("INDXSCHM") {
Some(Value::String { value, .. }) => value.trim() == "EXPLICIT",
_ => false,
};
if has_explicit_indexing {
return None;
}
if nside == 0 {
match header.get("NSIDE") {
Some(Value::Integer { value, .. }) => nside = *value,
_ => return None,
};
}
let tform_key = format!("TFORM{}", col_idx + 1);
let tform_str = match header.get(&tform_key) {
Some(Value::String { value, .. }) => value.clone(),
_ => return None,
};
let (elem_count, type_char) = parse_tform(&tform_str)?;
if type_char != 'E' {
return None;
}
let toffset_key = format!("TOFFSET{}", col_idx + 1);
let col_offset: usize = match header.get(&toffset_key) {
Some(Value::Integer { value, .. }) => *value as usize,
_ => {
if col_idx == 0 {
0
} else {
return None;
}
}
};
let row_size: usize = match header.get("NAXIS1") {
Some(Value::Integer { value, .. }) => *value as usize,
_ => return None,
};
let num_rows: usize = match header.get("NAXIS2") {
Some(Value::Integer { value, .. }) => *value as usize,
_ => return None,
};
let data_offset = find_binary_table_data_offset(mmap_data)?;
let total_elems = elem_count * num_rows;
let mut result = vec![0f32; total_elems];
let file_data = &mmap_data[data_offset..];
for (row_idx, row_chunk) in file_data.chunks(row_size).enumerate() {
if row_idx >= num_rows {
break;
}
let col_end = col_offset + elem_count * 4;
if col_end > row_chunk.len() {
return None;
}
let col_bytes = &row_chunk[col_offset..col_end];
for (elem_idx, chunk) in col_bytes.chunks_exact(4).enumerate() {
let bytes = [chunk[0], chunk[1], chunk[2], chunk[3]];
let f32_val = f32::from_be_bytes(bytes);
result[row_idx * elem_count + elem_idx] = f32_val;
}
}
return Some((result, nside));
}
}
None
}
fn try_read_float64_column_native(
_filename: &str,
mmap_data: &[u8],
col_idx: usize,
) -> Option<(Vec<f64>, i64)> {
use std::io::Cursor;
let cursor = Cursor::new(mmap_data);
let mut fits = Fits::from_reader(cursor);
let mut nside: i64 = 0;
while let Some(Ok(hdu)) = fits.next() {
if let HDU::XBinaryTable(hdu) = hdu {
let header = hdu.get_header();
let has_explicit_indexing = match header.get("INDXSCHM") {
Some(Value::String { value, .. }) => value.trim() == "EXPLICIT",
_ => false,
};
if has_explicit_indexing {
return None;
}
if nside == 0 {
match header.get("NSIDE") {
Some(Value::Integer { value, .. }) => nside = *value,
_ => return None,
};
}
let tform_key = format!("TFORM{}", col_idx + 1);
let tform_str = match header.get(&tform_key) {
Some(Value::String { value, .. }) => value.clone(),
_ => return None,
};
let (elem_count, type_char) = parse_tform(&tform_str)?;
if type_char != 'D' {
return None;
}
let toffset_key = format!("TOFFSET{}", col_idx + 1);
let col_offset: usize = match header.get(&toffset_key) {
Some(Value::Integer { value, .. }) => *value as usize,
_ => {
if col_idx == 0 {
0
} else {
return None;
}
}
};
let row_size: usize = match header.get("NAXIS1") {
Some(Value::Integer { value, .. }) => *value as usize,
_ => return None,
};
let num_rows: usize = match header.get("NAXIS2") {
Some(Value::Integer { value, .. }) => *value as usize,
_ => return None,
};
let data_offset = find_binary_table_data_offset(mmap_data)?;
let total_elems = elem_count * num_rows;
let mut result = vec![0f64; total_elems];
let file_data = &mmap_data[data_offset..];
for (row_idx, row_chunk) in file_data.chunks(row_size).enumerate() {
if row_idx >= num_rows {
break;
}
let col_end = col_offset + elem_count * 8;
if col_end > row_chunk.len() {
return None;
}
let col_bytes = &row_chunk[col_offset..col_end];
for (elem_idx, chunk) in col_bytes.chunks_exact(8).enumerate() {
let bytes = [
chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6],
chunk[7],
];
let f64_val = f64::from_be_bytes(bytes);
result[row_idx * elem_count + elem_idx] = f64_val;
}
}
return Some((result, nside));
}
}
None
}
fn find_binary_table_data_offset(mmap_data: &[u8]) -> Option<usize> {
const FITS_BLOCK_SIZE: usize = 2880;
let mut last_end_block = None;
for block_num in 0..1000 {
let block_start = block_num * FITS_BLOCK_SIZE;
if block_start >= mmap_data.len() {
break;
}
let block = if block_start + FITS_BLOCK_SIZE <= mmap_data.len() {
&mmap_data[block_start..block_start + FITS_BLOCK_SIZE]
} else {
&mmap_data[block_start..]
};
for card_off in (0..block.len()).step_by(80) {
if card_off + 8 <= block.len() {
let card = &block[card_off..card_off + 8];
if card == b"END " {
last_end_block = Some(block_num);
break; }
}
}
}
last_end_block.map(|block_num| (block_num + 1) * FITS_BLOCK_SIZE)
}
pub fn read_healpix_column(filename: &str, col_idx: usize) -> DataArray {
use memmap2::Mmap;
use std::io::Cursor;
use std::time::Instant;
let enable_profile = std::env::var("MAP2FIG_PROFILE").is_ok();
let mmap_start = Instant::now();
let f = File::open(filename).expect("Failed to open FITS file");
let mmap = unsafe { Mmap::map(&f).expect("Failed to mmap FITS file") };
let mmap_elapsed = mmap_start.elapsed();
if enable_profile {
eprintln!("[I/O DIAG] mmap() took: {:.3}s", mmap_elapsed.as_secs_f64());
}
let f32_start = Instant::now();
if let Some((data, _nside)) = try_read_float32_column_native(filename, &mmap, col_idx) {
let f32_elapsed = f32_start.elapsed();
if enable_profile {
eprintln!(
"[I/O DIAG] float32 read took: {:.3}s",
f32_elapsed.as_secs_f64()
);
}
return DataArray::from_f32(data);
}
let f64_start = Instant::now();
if let Some((data, _nside)) = try_read_float64_column_native(filename, &mmap, col_idx) {
let f64_elapsed = f64_start.elapsed();
if enable_profile {
eprintln!(
"[I/O DIAG] float64 read took: {:.3}s",
f64_elapsed.as_secs_f64()
);
}
return DataArray::from_f64(data);
}
let cursor = Cursor::new(&mmap[..]);
let mut fits = Fits::from_reader(cursor);
let mut result: Vec<f64> = Vec::new();
let mut nside: i64 = 0;
while let Some(Ok(hdu)) = fits.next() {
if let HDU::XBinaryTable(hdu) = hdu {
let header = hdu.get_header();
let has_explicit_indexing = match header.get("INDXSCHM") {
Some(Value::String { value, .. }) => value.trim() == "EXPLICIT",
_ => false,
};
if nside == 0 {
nside = match header.get("NSIDE") {
Some(Value::Integer { value, .. }) => *value,
_ => 0,
};
}
let data = fits.get_data(&hdu);
let mut table = data.table_data();
if has_explicit_indexing && nside > 0 {
let file_col_for_data = col_idx + 1;
let all_values: Vec<DataValue> = table
.select_fields(&[ColumnId::Index(0), ColumnId::Index(file_col_for_data)])
.collect();
if all_values.is_empty() {
result = vec![crate::healpix::HPX_UNSEEN; (12 * nside * nside) as usize];
} else {
let n_rows = all_values.len() / 2;
let npix = (12 * nside * nside) as usize;
let mut full_map = vec![crate::healpix::HPX_UNSEEN; npix];
let chunk_size = 1_000_000; let num_chunks = n_rows.div_ceil(chunk_size);
let pairs: Vec<(usize, f64)> = (0..num_chunks)
.into_par_iter()
.flat_map(|chunk_idx| {
let start = chunk_idx * chunk_size;
let end = std::cmp::min((chunk_idx + 1) * chunk_size, n_rows);
(start..end)
.filter_map(|row_idx| {
let pix_idx = row_idx * 2;
let data_idx = row_idx * 2 + 1;
let pix = match &all_values[pix_idx] {
DataValue::Integer { value, .. } => *value as i64,
DataValue::Long { value, .. } => *value,
DataValue::Float { value, .. } => *value as i64,
DataValue::Double { value, .. } => *value as i64,
_ => -1,
};
let val = match &all_values[data_idx] {
DataValue::Double { value, .. } => *value,
DataValue::Float { value, .. } => *value as f64,
DataValue::Integer { value, .. } => *value as f64,
other => {
panic!(
"Unsupported column type in FITS table: {:?}",
other
)
}
};
if pix >= 0 && (pix as usize) < npix {
Some((pix as usize, val))
} else {
None
}
})
.collect::<Vec<_>>()
})
.collect();
for (pix_idx, val) in pairs {
full_map[pix_idx] = val;
}
result = full_map;
}
} else {
let values = table.select_fields(&[ColumnId::Index(col_idx)]);
for cell in values {
match cell {
DataValue::Double { value, .. } => result.push(value),
DataValue::Float { value, .. } => result.push(value as f64),
DataValue::Integer { value, .. } => result.push(value as f64),
other => panic!("Unsupported column type in FITS table: {:?}", other),
}
}
}
}
}
DataArray::from_f64(result)
}
use serde_json::{Value as JsonValue, json};
use std::path::{Path, PathBuf};
fn get_cache_dir() -> Option<PathBuf> {
let dirs = directories::ProjectDirs::from("", "", "map2fig")?;
Some(dirs.cache_dir().to_path_buf())
}
fn compute_cache_key(filepath: &str) -> Option<String> {
use std::fs;
let path = Path::new(filepath);
let metadata = fs::metadata(path).ok()?;
let mtime = metadata.modified().ok()?;
let mtime_seconds = mtime.duration_since(std::time::UNIX_EPOCH).ok()?.as_secs();
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(filepath.as_bytes());
let hash = format!("{:x}", hasher.finalize());
Some(format!("{}_{}", hash, mtime_seconds))
}
fn try_load_cache(filepath: &str) -> Option<(i64, String, String)> {
let cache_dir = get_cache_dir()?;
let cache_key = compute_cache_key(filepath)?;
let cache_file = cache_dir.join(format!("fits_meta_{}.json", cache_key));
let json_str = std::fs::read_to_string(cache_file).ok()?;
let json: JsonValue = serde_json::from_str(&json_str).ok()?;
let nside = json.get("nside")?.as_i64()?;
let order = json.get("ordering")?.as_str()?.to_string();
let indxschm = json.get("indxschm")?.as_str()?.to_string();
Some((nside, order, indxschm))
}
fn save_cache(filepath: &str, nside: i64, ordering: &str, indxschm: &str) {
let cache_dir = match get_cache_dir() {
Some(d) => d,
None => return, };
let _ = std::fs::create_dir_all(&cache_dir);
let cache_key = match compute_cache_key(filepath) {
Some(k) => k,
None => return,
};
let cache_file = cache_dir.join(format!("fits_meta_{}.json", cache_key));
let cache_data = json!({
"nside": nside,
"ordering": ordering,
"indxschm": indxschm,
});
let _ = std::fs::write(cache_file, cache_data.to_string());
}
pub fn read_healpix_meta_cached(filename: &str) -> Option<(i64, String, String)> {
read_healpix_meta_cached_mmap(filename)
}
fn get_column_cache_key(filepath: &str, col_idx: usize, mtime_secs: u64) -> Option<String> {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(filepath.as_bytes());
let hash = hasher.finalize();
let hash_str = format!("{:x}", hash);
let short_hash = &hash_str[..16.min(hash_str.len())];
Some(format!(
"fits_col_{}_{:03}_{}",
short_hash, col_idx, mtime_secs
))
}
fn try_load_column_cache(filepath: &str, col_idx: usize) -> Option<Vec<f64>> {
let cache_dir = get_cache_dir()?;
let metadata = std::fs::metadata(filepath).ok()?;
let mtime_secs = metadata
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_secs();
let cache_key = get_column_cache_key(filepath, col_idx, mtime_secs)?;
let cache_file = cache_dir.join(&cache_key);
let mut file = File::open(cache_file).ok()?;
let mut header = [0u8; 16];
file.read_exact(&mut header).ok()?;
let magic = u32::from_le_bytes([header[0], header[1], header[2], header[3]]);
let version = u32::from_le_bytes([header[4], header[5], header[6], header[7]]);
let num_pixels = u32::from_le_bytes([header[8], header[9], header[10], header[11]]);
if magic != 0xCAFEBABE || version != 1 {
return None;
}
let mut data = vec![0.0; num_pixels as usize];
let byte_data = unsafe {
std::slice::from_raw_parts_mut(data.as_mut_ptr() as *mut u8, num_pixels as usize * 8)
};
file.read_exact(byte_data).ok()?;
let enable_profile = std::env::var("MAP2FIG_PROFILE").is_ok();
if enable_profile {
eprintln!("[I/O DIAG] Column cache HIT: {} col#{}", filepath, col_idx);
}
Some(data)
}
fn save_column_cache(filepath: &str, col_idx: usize, data: &[f64]) -> Option<()> {
const MAX_CACHE_COLUMN_SIZE: usize = 128_000_000; if data.len() > MAX_CACHE_COLUMN_SIZE {
let enable_profile = std::env::var("MAP2FIG_PROFILE").is_ok();
if enable_profile {
eprintln!(
"[I/O DIAG] Column cache SKIP (too large): {} col#{} ({} pixels > {}M)",
filepath,
col_idx,
data.len(),
MAX_CACHE_COLUMN_SIZE / 1_000_000
);
}
return None; }
let cache_dir = get_cache_dir()?;
let _ = std::fs::create_dir_all(&cache_dir);
let metadata = std::fs::metadata(filepath).ok()?;
let mtime_secs = metadata
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_secs();
let cache_key = get_column_cache_key(filepath, col_idx, mtime_secs)?;
let cache_file = cache_dir.join(&cache_key);
let mut file = File::create(cache_file).ok()?;
file.write_all(&0xCAFEBABEu32.to_le_bytes()).ok()?;
file.write_all(&1u32.to_le_bytes()).ok()?;
file.write_all(&(data.len() as u32).to_le_bytes()).ok()?;
file.write_all(&0u32.to_le_bytes()).ok()?;
let byte_data =
unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * 8) };
file.write_all(byte_data).ok()?;
let enable_profile = std::env::var("MAP2FIG_PROFILE").is_ok();
if enable_profile {
eprintln!(
"[I/O DIAG] Column cache SAVE: {} col#{} ({} pixels)",
filepath,
col_idx,
data.len()
);
}
Some(())
}
fn enforce_cache_size_limit() {
const MAX_CACHE_SIZE: u64 = 2_000_000_000;
let cache_dir = match get_cache_dir() {
Some(d) => d,
None => return,
};
let mut total_size = 0u64;
let mut files = Vec::new();
for entry in std::fs::read_dir(&cache_dir)
.ok()
.into_iter()
.flatten()
.flatten()
{
let path = entry.path();
if let Ok(metadata) = entry.metadata() {
let size = metadata.len();
total_size += size;
if let Ok(modified) = metadata.modified() {
files.push((path, modified, size));
}
}
}
if total_size <= MAX_CACHE_SIZE {
return;
}
files.sort_by_key(|f| f.1);
let target_size = (MAX_CACHE_SIZE as f64 * 0.9) as u64;
let mut freed = 0u64;
for (path, _, size) in files.iter() {
if total_size - freed <= target_size {
break;
}
let _ = std::fs::remove_file(path);
freed += size;
}
}
pub fn read_healpix_column_cached(filename: &str, col_idx: usize) -> DataArray {
if let Some(data) = try_load_column_cache(filename, col_idx) {
return DataArray::from_f64(data);
}
let enable_profile = std::env::var("MAP2FIG_PROFILE").is_ok();
if enable_profile {
eprintln!("[I/O DIAG] Column cache MISS: {} col#{}", filename, col_idx);
}
let use_mmap = std::env::var("MAP2FIX_USE_MMAP").is_ok();
let fits_start = std::time::Instant::now();
let data_array: DataArray = if use_mmap {
read_healpix_column_mmap(filename, col_idx)
} else {
read_healpix_column(filename, col_idx)
};
let fits_elapsed = fits_start.elapsed();
if enable_profile {
eprintln!(
"[I/O DIAG] FITS parsing took: {:.3}s",
fits_elapsed.as_secs_f64()
);
}
if let DataArray::Float64(ref vec) = data_array {
let cache_start = std::time::Instant::now();
match save_column_cache(filename, col_idx, vec) {
Some(_) => {
let cache_elapsed = cache_start.elapsed();
if enable_profile {
eprintln!(
"[I/O DIAG] Column cache SAVE SUCCESS ({:.3}s): {} col#{}",
cache_elapsed.as_secs_f64(),
filename,
col_idx
);
}
}
None => {
let cache_elapsed = cache_start.elapsed();
if enable_profile {
eprintln!(
"[I/O DIAG] Column cache SAVE FAILED ({:.3}s): {} col#{}",
cache_elapsed.as_secs_f64(),
filename,
col_idx
);
}
}
}
} else if enable_profile {
eprintln!("[I/O DIAG] Skipping cache for f32 data (too large)");
}
enforce_cache_size_limit();
data_array
}
pub fn read_healpix_column_mmap(filename: &str, col_idx: usize) -> DataArray {
use memmap2::Mmap;
use std::io::Cursor;
let f = File::open(filename).expect("Failed to open FITS file");
let mmap = unsafe { Mmap::map(&f).expect("Failed to memory-map FITS file") };
if let Some((data, _nside)) = try_read_float32_column_native(filename, &mmap, col_idx) {
return DataArray::from_f32(data);
}
if let Some((data, _nside)) = try_read_float64_column_native(filename, &mmap, col_idx) {
return DataArray::from_f64(data);
}
let cursor = Cursor::new(&mmap[..]);
let mut fits = Fits::from_reader(cursor);
let mut result: Vec<f64> = Vec::new();
let mut nside: i64 = 0;
while let Some(Ok(hdu)) = fits.next() {
if let HDU::XBinaryTable(hdu) = hdu {
let header = hdu.get_header();
let has_explicit_indexing = match header.get("INDXSCHM") {
Some(Value::String { value, .. }) => value.trim() == "EXPLICIT",
_ => false,
};
if nside == 0 {
nside = match header.get("NSIDE") {
Some(Value::Integer { value, .. }) => *value,
_ => 0,
};
}
let data = fits.get_data(&hdu);
let mut table = data.table_data();
if has_explicit_indexing && nside > 0 {
let file_col_for_data = col_idx + 1;
let all_values: Vec<DataValue> = table
.select_fields(&[ColumnId::Index(0), ColumnId::Index(file_col_for_data)])
.collect();
if all_values.is_empty() {
result = vec![crate::healpix::HPX_UNSEEN; (12 * nside * nside) as usize];
} else {
let n_rows = all_values.len() / 2;
let npix = (12 * nside * nside) as usize;
let mut full_map = vec![crate::healpix::HPX_UNSEEN; npix];
let chunk_size = 1_000_000; let num_chunks = n_rows.div_ceil(chunk_size);
let pairs: Vec<(usize, f64)> = (0..num_chunks)
.into_par_iter()
.flat_map(|chunk_idx| {
let start = chunk_idx * chunk_size;
let end = std::cmp::min((chunk_idx + 1) * chunk_size, n_rows);
(start..end)
.filter_map(|row_idx| {
let pix_idx = row_idx * 2;
let data_idx = row_idx * 2 + 1;
let pix = match &all_values[pix_idx] {
DataValue::Integer { value, .. } => *value as i64,
DataValue::Long { value, .. } => *value,
DataValue::Float { value, .. } => *value as i64,
DataValue::Double { value, .. } => *value as i64,
_ => -1,
};
let val = match &all_values[data_idx] {
DataValue::Double { value, .. } => *value,
DataValue::Float { value, .. } => *value as f64,
DataValue::Integer { value, .. } => *value as f64,
other => {
panic!(
"Unsupported column type in FITS table: {:?}",
other
)
}
};
if pix >= 0 && (pix as usize) < npix {
Some((pix as usize, val))
} else {
None
}
})
.collect::<Vec<_>>()
})
.collect();
for (pix_idx, val) in pairs {
full_map[pix_idx] = val;
}
result = full_map;
}
} else {
let values = table.select_fields(&[ColumnId::Index(col_idx)]);
for cell in values {
match cell {
DataValue::Double { value, .. } => result.push(value),
DataValue::Float { value, .. } => result.push(value as f64),
DataValue::Integer { value, .. } => result.push(value as f64),
other => panic!("Unsupported column type in FITS table: {:?}", other),
}
}
}
}
}
DataArray::from_f64(result)
}
pub fn read_healpix_meta_cached_mmap(filename: &str) -> Option<(i64, String, String)> {
use memmap2::Mmap;
use std::io::Cursor;
let enable_profile = std::env::var("MAP2FIX_PROFILE").is_ok();
let cache_start = std::time::Instant::now();
if let Some((nside, order, indxschm)) = try_load_cache(filename) {
if enable_profile {
let elapsed = cache_start.elapsed();
eprintln!(
"[I/O DIAG] Cache HIT (mmap): {} ({:.3}µs)",
filename,
elapsed.as_micros()
);
}
return Some((nside, order, indxschm));
}
if enable_profile {
let elapsed = cache_start.elapsed();
eprintln!(
"[I/O DIAG] Cache MISS (mmap): {} (lookup took {:.3}µs)",
filename,
elapsed.as_micros()
);
}
let parse_start = std::time::Instant::now();
let f = File::open(filename).ok()?;
let mmap = unsafe { Mmap::map(&f).ok()? };
let cursor = Cursor::new(&mmap[..]);
let mut fits = Fits::from_reader(cursor);
let mut nside: i64 = 0;
let mut ordering = String::new();
let mut indxschm = String::from("IMPLICIT");
while let Some(Ok(hdu)) = fits.next() {
if let HDU::XBinaryTable(hdu) = hdu {
let header = hdu.get_header();
nside = match header.get("NSIDE") {
Some(Value::Integer { value, .. }) => *value,
_ => 0,
};
ordering = match header.get("ORDERING") {
Some(Value::String { value, .. }) => value.trim().to_string(),
_ => "RING".to_string(),
};
indxschm = match header.get("INDXSCHM") {
Some(Value::String { value, .. }) => value.trim().to_string(),
_ => "IMPLICIT".to_string(),
};
break;
}
}
let parse_end = std::time::Instant::now();
if enable_profile {
eprintln!(
"[I/O DIAG] Parse done (mmap): {:.3}ms",
parse_end.duration_since(parse_start).as_secs_f64() * 1000.0
);
}
if nside > 0 {
save_cache(filename, nside, &ordering, &indxschm);
Some((nside, ordering, indxschm))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
#[cfg(not(miri))]
fn test_sparse_fit_explicit_indexing() {
let test_file =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/sparse_nside1.fits");
if !test_file.exists() {
eprintln!(
"Skipping test: sparse FITS fixture not found at {}",
test_file.display()
);
return;
}
let data = read_healpix_column(test_file.to_str().unwrap(), 0);
assert_eq!(
data.len(),
12,
"NSIDE=1 should have 12 pixels, got {}",
data.len()
);
let expected_values = vec![(0, 1.0), (3, 2.0), (6, 3.0), (9, 4.0)];
for (idx, expected_val) in expected_values {
let actual = data.get(idx).expect("pixel out of bounds");
assert!(
(actual - expected_val).abs() < 0.01,
"Pixel {} should be {}, got {}",
idx,
expected_val,
actual
);
}
let unseen_indices = vec![1, 2, 4, 5, 7, 8, 10, 11];
for idx in unseen_indices {
let actual = data.get(idx).expect("pixel out of bounds");
let diff = (actual - crate::healpix::HPX_UNSEEN).abs();
let tolerance = (crate::healpix::HPX_UNSEEN.abs() * 1e-6).max(1e-20);
assert!(
diff < tolerance,
"Pixel {} should be HPX_UNSEEN ({}), got {} (diff: {:.2e})",
idx,
crate::healpix::HPX_UNSEEN,
actual,
diff
);
}
println!("✓ Sparse FITS explicit indexing test passed");
}
#[test]
#[cfg(not(miri))]
fn test_sparse_map_regression_fix() {
let test_file =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/sparse_nside1.fits");
if !test_file.exists() {
eprintln!(
"Skipping test: sparse FITS fixture not found at {}",
test_file.display()
);
return;
}
let data = read_healpix_column(test_file.to_str().unwrap(), 0);
let populated_count = data
.iter()
.filter(|&v| v > -1e29) .count();
let unseen_count = data
.iter()
.filter(|&v| v < -1e29) .count();
assert_eq!(
populated_count, 4,
"Expected 4 populated pixel values, got {}",
populated_count
);
assert_eq!(
unseen_count, 8,
"Expected 8 unseen pixels, got {}",
unseen_count
);
let inf_count = data.iter().filter(|v| v == &f64::NEG_INFINITY).count();
assert_eq!(
inf_count, 0,
"No pixels should be NEG_INFINITY, got {}",
inf_count
);
println!("✓ Sparse map regression fix verified");
}
}