use crate::error::{IoError, Result};
use crate::hdf5::{CompressionOptions, DatasetOptions, FileMode, HDF5File};
use scirs2_core::ndarray::{ArrayBase, ArrayD, IxDyn};
use std::collections::HashMap;
use std::ops::Range;
use std::path::Path;
use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::thread;
use std::time::Instant;
use super::convert::dataset_to_f64;
fn lock_mutex<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn lock_read<T>(lock: &RwLock<T>) -> RwLockReadGuard<'_, T> {
lock.read().unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn lock_write<T>(lock: &RwLock<T>) -> RwLockWriteGuard<'_, T> {
lock.write()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[derive(Debug, Clone, PartialEq)]
pub enum ExtendedDataType {
Int8,
UInt8,
Int16,
UInt16,
Int32,
UInt32,
Int64,
UInt64,
Float32,
Float64,
Complex64,
Complex128,
Bool,
String,
FixedString(usize),
}
#[derive(Debug, Clone)]
pub struct ParallelConfig {
pub num_workers: usize,
pub chunk_size: usize,
pub collective_io: bool,
pub buffer_size: usize,
}
impl Default for ParallelConfig {
fn default() -> Self {
Self {
num_workers: thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4),
chunk_size: 1024 * 1024, collective_io: false,
buffer_size: 64 * 1024 * 1024, }
}
}
pub struct EnhancedHDF5File {
base_file: HDF5File,
parallel_config: Option<ParallelConfig>,
file_lock: Arc<RwLock<()>>,
compression_stats: Arc<Mutex<CompressionStats>>,
}
#[derive(Debug, Clone, Default)]
pub struct CompressionStats {
pub original_size: usize,
pub compressed_size: usize,
pub compression_ratio: f64,
pub compression_time_ms: f64,
}
impl EnhancedHDF5File {
pub fn create<P: AsRef<Path>>(
path: P,
parallel_config: Option<ParallelConfig>,
) -> Result<Self> {
let base_file = HDF5File::create(path)?;
Ok(Self {
base_file,
parallel_config,
file_lock: Arc::new(RwLock::new(())),
compression_stats: Arc::new(Mutex::new(CompressionStats::default())),
})
}
pub fn open<P: AsRef<Path>>(
path: P,
mode: FileMode,
parallel_config: Option<ParallelConfig>,
) -> Result<Self> {
let base_file = HDF5File::open(path, mode)?;
Ok(Self {
base_file,
parallel_config,
file_lock: Arc::new(RwLock::new(())),
compression_stats: Arc::new(Mutex::new(CompressionStats::default())),
})
}
pub fn create_dataset_with_compression<A, D>(
&mut self,
path: &str,
array: &ArrayBase<A, D>,
_data_type: ExtendedDataType,
options: DatasetOptions,
) -> Result<()>
where
A: scirs2_core::ndarray::Data,
A::Elem: Clone + Into<f64> + std::fmt::Debug,
D: scirs2_core::ndarray::Dimension,
{
let start_time = Instant::now();
let shape: Vec<usize> = array.shape().to_vec();
let payload_bytes = array.len() * std::mem::size_of::<f64>();
let mut options = options;
if options
.chunk_size
.as_ref()
.is_some_and(|chunks| chunks.len() != shape.len())
{
options.chunk_size = Some(self.calculate_optimal_chunks(&shape, array.len()));
}
{
let file_lock = Arc::clone(&self.file_lock);
let _guard = lock_write(&file_lock);
self.base_file
.create_dataset_from_array(path, array, Some(options))?;
}
let mut stats = lock_mutex(&self.compression_stats);
stats.original_size += payload_bytes;
stats.compression_time_ms += start_time.elapsed().as_secs_f64() * 1000.0;
Ok(())
}
fn calculate_optimal_chunks(&self, shape: &[usize], _totalelements: usize) -> Vec<usize> {
const TARGET_CHUNK_SIZE: usize = 64 * 1024; const MIN_CHUNK_SIZE: usize = 1024; const MAX_CHUNK_SIZE: usize = 1024 * 1024;
let element_size = 8; let elements_per_chunk = (TARGET_CHUNK_SIZE / element_size)
.clamp(MIN_CHUNK_SIZE / element_size, MAX_CHUNK_SIZE / element_size);
let mut chunks = shape.to_vec();
let current_chunk_elements: usize = chunks.iter().product();
if current_chunk_elements > elements_per_chunk {
let scale_factor = (elements_per_chunk as f64 / current_chunk_elements as f64)
.powf(1.0 / shape.len() as f64);
for chunk in &mut chunks {
*chunk = (*chunk as f64 * scale_factor).max(1.0) as usize;
}
}
chunks
}
pub fn read_dataset_parallel(&self, path: &str) -> Result<ArrayD<f64>> {
let _guard = lock_read(&self.file_lock);
match self.parallel_config.as_ref() {
Some(config) => self.read_dataset_parallel_impl(path, config),
None => self.base_file.read_dataset(path),
}
}
fn read_dataset_parallel_impl(
&self,
path: &str,
parallel_config: &ParallelConfig,
) -> Result<ArrayD<f64>> {
if self.base_file.mode != FileMode::ReadOnly {
return self.base_file.read_dataset(path);
}
let file_path = self.base_file.path.clone();
let dataset_path = path.trim_start_matches('/').to_string();
if oxih5::dataset_data_extent(&file_path, &dataset_path).is_ok() {
return self.base_file.read_dataset(path);
}
let shape = self.base_file.get_dataset(path)?.shape.clone();
let bands = Self::split_into_bands(&shape, parallel_config);
if bands.len() < 2 {
return self.base_file.read_dataset(path);
}
Self::read_bands_parallel(&file_path, &dataset_path, &shape, &bands)
}
fn split_into_bands(shape: &[usize], config: &ParallelConfig) -> Vec<Range<usize>> {
let Some(&rows) = shape.first() else {
return Vec::new();
};
if rows == 0 {
return Vec::new();
}
let row_len: usize = shape[1..].iter().product::<usize>().max(1);
let rows_per_band = config.chunk_size.div_ceil(row_len).max(1);
let band_count = rows
.div_ceil(rows_per_band)
.min(config.num_workers.max(1))
.max(1);
let rows_per_band = rows.div_ceil(band_count);
(0..band_count)
.map(|i| (i * rows_per_band).min(rows)..((i + 1) * rows_per_band).min(rows))
.filter(|band| !band.is_empty())
.collect()
}
fn read_bands_parallel(
file_path: &str,
dataset_path: &str,
shape: &[usize],
bands: &[Range<usize>],
) -> Result<ArrayD<f64>> {
let row_len: usize = shape[1..].iter().product::<usize>().max(1);
let total: usize = shape.iter().product();
let collected: Vec<Result<(usize, Vec<f64>)>> = thread::scope(|scope| {
let handles: Vec<_> = bands
.iter()
.map(|band| {
let band = band.clone();
scope.spawn(move || -> Result<(usize, Vec<f64>)> {
let file = oxih5::File::open_mmap(file_path).map_err(|e| {
IoError::FormatError(format!(
"Failed to map '{file_path}' for a parallel read: {e}"
))
})?;
let mut ranges: Vec<Range<usize>> = Vec::with_capacity(shape.len());
ranges.push(band.clone());
ranges.extend(shape[1..].iter().map(|&len| 0..len));
let slice = file.dataset_slice(dataset_path, &ranges).map_err(|e| {
IoError::FormatError(format!(
"Failed to read rows {}..{} of '{dataset_path}': {e}",
band.start, band.end
))
})?;
Ok((band.start * row_len, dataset_to_f64(&slice)?))
})
})
.collect();
handles
.into_iter()
.map(|handle| {
handle.join().unwrap_or_else(|_| {
Err(IoError::Other(
"a parallel HDF5 read worker panicked".to_string(),
))
})
})
.collect()
});
let mut full = vec![0.0f64; total];
let mut written = 0usize;
for outcome in collected {
let (offset, values) = outcome?;
let end = offset
.checked_add(values.len())
.filter(|&end| end <= total)
.ok_or_else(|| {
IoError::FormatError(format!(
"a band starting at element {offset} returned {} values, past the \
{total} the dataset holds",
values.len()
))
})?;
full[offset..end].copy_from_slice(&values);
written += values.len();
}
if written != total {
return Err(IoError::FormatError(format!(
"parallel read of '{dataset_path}' covered {written} of {total} elements"
)));
}
ArrayD::from_shape_vec(IxDyn(shape), full).map_err(|e| IoError::FormatError(e.to_string()))
}
pub fn get_compression_stats(&self) -> Result<CompressionStats> {
let serialized = self.base_file.serialized_len()?;
let mut stats = lock_mutex(&self.compression_stats);
stats.compressed_size = serialized;
stats.compression_ratio = if serialized > 0 {
stats.original_size as f64 / serialized as f64
} else {
0.0
};
Ok(stats.clone())
}
pub fn write_datasets_parallel(
&mut self,
datasets: HashMap<String, (ArrayD<f64>, ExtendedDataType, DatasetOptions)>,
) -> Result<()> {
let mut ordered: Vec<_> = datasets.into_iter().collect();
ordered.sort_by(|a, b| a.0.cmp(&b.0));
for (path, (array, data_type, options)) in ordered {
self.create_dataset_with_compression(&path, &array, data_type, options)?;
}
Ok(())
}
pub fn close(self) -> Result<()> {
self.base_file.close()
}
}
pub fn write_hdf5_enhanced<P: AsRef<Path>>(
path: P,
datasets: HashMap<String, (ArrayD<f64>, ExtendedDataType, DatasetOptions)>,
parallel_config: Option<ParallelConfig>,
) -> Result<()> {
let mut file = EnhancedHDF5File::create(path, parallel_config)?;
file.write_datasets_parallel(datasets)?;
file.close()?;
Ok(())
}
pub fn read_hdf5_enhanced<P: AsRef<Path>>(
path: P,
parallel_config: Option<ParallelConfig>,
) -> Result<EnhancedHDF5File> {
EnhancedHDF5File::open(path, FileMode::ReadOnly, parallel_config)
}
pub fn create_optimal_compression_options(
data_type: &ExtendedDataType,
estimated_size: usize,
) -> CompressionOptions {
let mut options = CompressionOptions::default();
match data_type {
ExtendedDataType::Float32 | ExtendedDataType::Float64 => {
options.shuffle = true;
options.gzip = Some(if estimated_size > 1024 * 1024 { 6 } else { 9 });
}
ExtendedDataType::Int8 | ExtendedDataType::UInt8 => {
options.lzf = true;
options.shuffle = true;
}
_ => {
options.gzip = Some(6);
options.shuffle = true;
}
}
options
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::{Array, Array2};
fn temp_path(tag: &str) -> std::path::PathBuf {
use std::sync::atomic::{AtomicUsize, Ordering};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
std::env::temp_dir().join(format!(
"scirs2_io_enhanced_{tag}_{}_{}.h5",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
))
}
#[test]
fn test_enhanced_compression_options() {
let options =
create_optimal_compression_options(&ExtendedDataType::Float64, 2 * 1024 * 1024);
assert_eq!(options.gzip, Some(6));
assert!(options.shuffle);
}
#[test]
fn test_optimal_chunks_calculation() {
let path = temp_path("chunks");
let file = EnhancedHDF5File::create(&path, None).expect("create in-memory handle");
let shape = vec![1000, 1000];
let total_elements = 1_000_000;
let chunks = file.calculate_optimal_chunks(&shape, total_elements);
assert!(chunks.len() == 2);
assert!(chunks[0] > 0 && chunks[1] > 0);
let chunk_elements: usize = chunks.iter().product();
assert!(chunk_elements <= 1024 * 1024 / 8);
drop(file);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_parallel_config_default() {
let config = ParallelConfig::default();
assert!(config.num_workers > 0);
assert!(config.chunk_size > 0);
assert!(config.buffer_size > 0);
}
#[test]
fn test_create_dataset_with_compression_round_trips_values() {
let path = temp_path("round_trip");
let values = Array2::from_shape_vec((2, 3), vec![1.5, -2.5, 3.0, 4.25, 5.0, -6.75])
.expect("2x3 literal");
let mut file = EnhancedHDF5File::create(&path, None).expect("create");
file.create_dataset_with_compression(
"measurements",
&values,
ExtendedDataType::Float64,
DatasetOptions::default(),
)
.expect("write dataset");
file.close().expect("flush to disk");
let reopened = EnhancedHDF5File::open(&path, FileMode::ReadOnly, None).expect("reopen");
let read_back = reopened
.read_dataset_parallel("measurements")
.expect("read dataset");
assert_eq!(read_back.shape(), &[2, 3]);
assert_eq!(
read_back.iter().copied().collect::<Vec<f64>>(),
values.iter().copied().collect::<Vec<f64>>(),
"every element must survive the write/read round trip"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_create_dataset_with_compression_widens_integers() {
let path = temp_path("widen");
let values = Array::from_vec(vec![-7i32, 0, 42]).into_dyn();
let mut file = EnhancedHDF5File::create(&path, None).expect("create");
file.create_dataset_with_compression(
"counts",
&values,
ExtendedDataType::Int32,
DatasetOptions::default(),
)
.expect("write dataset");
file.close().expect("flush to disk");
let reopened = EnhancedHDF5File::open(&path, FileMode::ReadOnly, None).expect("reopen");
let read_back = reopened.base_file.read_dataset("counts").expect("read");
assert_eq!(
read_back.iter().copied().collect::<Vec<f64>>(),
vec![-7.0, 0.0, 42.0]
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_write_datasets_parallel_stores_every_dataset() {
let path = temp_path("multi");
let mut datasets = HashMap::new();
datasets.insert(
"alpha".to_string(),
(
Array::from_vec(vec![1.0, 2.0]).into_dyn(),
ExtendedDataType::Float64,
DatasetOptions::default(),
),
);
datasets.insert(
"beta".to_string(),
(
Array::from_vec(vec![3.0, 4.0, 5.0]).into_dyn(),
ExtendedDataType::Float64,
DatasetOptions::default(),
),
);
write_hdf5_enhanced(&path, datasets, None).expect("write");
let reopened = EnhancedHDF5File::open(&path, FileMode::ReadOnly, None).expect("reopen");
let alpha = reopened.base_file.read_dataset("alpha").expect("alpha");
let beta = reopened.base_file.read_dataset("beta").expect("beta");
assert_eq!(alpha.iter().copied().collect::<Vec<f64>>(), vec![1.0, 2.0]);
assert_eq!(
beta.iter().copied().collect::<Vec<f64>>(),
vec![3.0, 4.0, 5.0]
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_compression_stats_are_measured_not_assumed() {
let path = temp_path("stats");
let values = Array::from_vec(vec![0.0f64; 512]).into_dyn();
let mut file = EnhancedHDF5File::create(&path, None).expect("create");
file.create_dataset_with_compression(
"bulk",
&values,
ExtendedDataType::Float64,
DatasetOptions::default(),
)
.expect("write dataset");
let stats = file.get_compression_stats().expect("measure stats");
assert_eq!(
stats.original_size,
512 * 8,
"the raw payload is counted exactly"
);
assert!(
stats.compressed_size > 0,
"the serialised size must be queried, not left at zero"
);
assert!(
stats.compressed_size >= stats.original_size,
"uncompressed storage plus HDF5 metadata cannot be smaller than the payload"
);
let expected_ratio = stats.original_size as f64 / stats.compressed_size as f64;
assert!(
(stats.compression_ratio - expected_ratio).abs() < f64::EPSILON,
"the ratio must be derived from the two measured sizes"
);
file.close().expect("flush");
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_read_dataset_parallel_matches_sequential() {
let path = temp_path("parallel");
let values: Vec<f64> = (0..256).map(|i| f64::from(i) * 0.5).collect();
let array = Array2::from_shape_vec((32, 8), values.clone()).expect("32x8");
let mut file = EnhancedHDF5File::create(&path, None).expect("create");
file.create_dataset_with_compression(
"grid",
&array,
ExtendedDataType::Float64,
DatasetOptions::default(),
)
.expect("write dataset");
file.close().expect("flush");
let config = ParallelConfig {
num_workers: 4,
chunk_size: 16,
collective_io: false,
buffer_size: 1024,
};
let parallel = EnhancedHDF5File::open(&path, FileMode::ReadOnly, Some(config))
.expect("reopen with a parallel config");
let read_back = parallel
.read_dataset_parallel("grid")
.expect("parallel read");
assert_eq!(read_back.shape(), &[32, 8]);
assert_eq!(read_back.iter().copied().collect::<Vec<f64>>(), values);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_split_into_bands_tiles_the_leading_axis() {
let config = ParallelConfig {
num_workers: 4,
chunk_size: 8,
collective_io: false,
buffer_size: 0,
};
let bands = EnhancedHDF5File::split_into_bands(&[10, 4], &config);
assert!(!bands.is_empty());
assert_eq!(bands.first().map(|band| band.start), Some(0));
assert_eq!(bands.last().map(|band| band.end), Some(10));
assert!(
bands.len() <= config.num_workers,
"never more bands than workers were asked for"
);
for pair in bands.windows(2) {
assert_eq!(
pair[0].end, pair[1].start,
"bands must tile the axis with no gap and no overlap"
);
}
}
#[test]
fn test_split_into_bands_handles_degenerate_shapes() {
let config = ParallelConfig::default();
assert!(EnhancedHDF5File::split_into_bands(&[], &config).is_empty());
assert!(EnhancedHDF5File::split_into_bands(&[0, 5], &config).is_empty());
assert_eq!(
EnhancedHDF5File::split_into_bands(&[1, 5], &config).len(),
1
);
}
}
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub enum AttributeValue {
String(String),
Integer(i64),
Float(f64),
FloatArray(Vec<f64>),
IntArray(Vec<i64>),
StringArray(Vec<String>),
Boolean(bool),
}
#[derive(Debug, Clone, Default)]
pub struct ScientificMetadata {
pub attributes: BTreeMap<String, AttributeValue>,
pub units: Option<String>,
pub scale_factor: Option<f64>,
pub add_offset: Option<f64>,
pub fill_value: Option<f64>,
pub valid_range: Option<(f64, f64)>,
pub calibration: Option<CalibrationInfo>,
pub provenance: Option<ProvenanceInfo>,
}
#[derive(Debug, Clone)]
pub struct CalibrationInfo {
pub date: String,
pub method: String,
pub parameters: BTreeMap<String, f64>,
pub accuracy: Option<f64>,
pub precision: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct ProvenanceInfo {
pub source: String,
pub processing_history: Vec<String>,
pub creation_time: String,
pub creator: String,
pub software_version: String,
pub input_files: Vec<String>,
}
impl ScientificMetadata {
pub fn new() -> Self {
Self::default()
}
pub fn add_string_attr<S: Into<String>>(mut self, name: S, value: S) -> Self {
self.attributes
.insert(name.into(), AttributeValue::String(value.into()));
self
}
pub fn add_float_attr<S: Into<String>>(mut self, name: S, value: f64) -> Self {
self.attributes
.insert(name.into(), AttributeValue::Float(value));
self
}
pub fn with_units<S: Into<String>>(mut self, units: S) -> Self {
self.units = Some(units.into());
self
}
pub fn with_scaling(mut self, scale_factor: f64, add_offset: f64) -> Self {
self.scale_factor = Some(scale_factor);
self.add_offset = Some(add_offset);
self
}
pub fn with_valid_range(mut self, min: f64, max: f64) -> Self {
self.valid_range = Some((min, max));
self
}
pub fn with_provenance(mut self, provenance: ProvenanceInfo) -> Self {
self.provenance = Some(provenance);
self
}
}
#[derive(Debug, Clone, Default)]
pub struct HDF5PerformanceMonitor {
pub timings: BTreeMap<String, Vec<f64>>,
pub transfer_stats: TransferStats,
pub memory_stats: MemoryStats,
pub compression_efficiency: Vec<CompressionStats>,
}
#[derive(Debug, Clone, Default)]
pub struct TransferStats {
pub bytes_read: usize,
pub bytes_written: usize,
pub read_operations: usize,
pub write_operations: usize,
pub avg_read_speed: f64,
pub avg_write_speed: f64,
}
#[derive(Debug, Clone, Default)]
pub struct MemoryStats {
pub peak_memory_bytes: usize,
pub current_memory_bytes: usize,
pub allocation_count: usize,
pub deallocation_count: usize,
}
impl HDF5PerformanceMonitor {
pub fn new() -> Self {
Self::default()
}
pub fn record_timing(&mut self, operation: &str, durationms: f64) {
self.timings
.entry(operation.to_string())
.or_default()
.push(durationms);
}
pub fn record_read(&mut self, bytes: usize, durationms: f64) {
self.transfer_stats.bytes_read += bytes;
self.transfer_stats.read_operations += 1;
if durationms > 0.0 {
let speed = bytes as f64 / (durationms / 1000.0);
let total_ops = self.transfer_stats.read_operations as f64;
self.transfer_stats.avg_read_speed =
(self.transfer_stats.avg_read_speed * (total_ops - 1.0) + speed) / total_ops;
}
}
pub fn record_write(&mut self, bytes: usize, durationms: f64) {
self.transfer_stats.bytes_written += bytes;
self.transfer_stats.write_operations += 1;
if durationms > 0.0 {
let speed = bytes as f64 / (durationms / 1000.0);
let total_ops = self.transfer_stats.write_operations as f64;
self.transfer_stats.avg_write_speed =
(self.transfer_stats.avg_write_speed * (total_ops - 1.0) + speed) / total_ops;
}
}
pub fn avg_timing(&self, operation: &str) -> Option<f64> {
self.timings
.get(operation)
.map(|times| times.iter().sum::<f64>() / times.len() as f64)
}
pub fn get_summary(&self) -> PerformanceSummary {
let mut operation_averages = BTreeMap::new();
for (op, times) in &self.timings {
let avg = times.iter().sum::<f64>() / times.len() as f64;
operation_averages.insert(op.clone(), avg);
}
PerformanceSummary {
operation_averages,
total_bytes_transferred: self.transfer_stats.bytes_read
+ self.transfer_stats.bytes_written,
avg_read_speed_mbps: self.transfer_stats.avg_read_speed / 1_000_000.0,
avg_write_speed_mbps: self.transfer_stats.avg_write_speed / 1_000_000.0,
peak_memory_mb: self.memory_stats.peak_memory_bytes as f64 / 1_000_000.0,
compression_ratio: self
.compression_efficiency
.iter()
.map(|c| c.compression_ratio)
.fold(0.0, |acc, x| acc + x)
/ self.compression_efficiency.len().max(1) as f64,
}
}
}
#[derive(Debug, Clone)]
pub struct PerformanceSummary {
pub operation_averages: BTreeMap<String, f64>,
pub total_bytes_transferred: usize,
pub avg_read_speed_mbps: f64,
pub avg_write_speed_mbps: f64,
pub peak_memory_mb: f64,
pub compression_ratio: f64,
}
#[derive(Debug, Clone)]
pub enum LayoutOptimization {
RowMajor,
ColumnMajor,
Chunked(Vec<usize>),
Tiled {
tile_width: usize,
tile_height: usize,
},
Striped {
strip_size: usize,
},
}
#[derive(Debug, Clone)]
pub struct AccessPatternAnalyzer {
access_patterns: Vec<AccessPattern>,
recommendations: Vec<LayoutOptimization>,
}
#[derive(Debug, Clone)]
pub struct AccessPattern {
pub operation: String,
pub region: Vec<(usize, usize)>,
pub frequency: usize,
pub timestamp: std::time::Instant,
}
impl AccessPatternAnalyzer {
pub fn new() -> Self {
Self {
access_patterns: Vec::new(),
recommendations: Vec::new(),
}
}
pub fn record_access(&mut self, operation: String, region: Vec<(usize, usize)>) {
for pattern in &mut self.access_patterns {
if pattern.operation == operation && pattern.region == region {
pattern.frequency += 1;
pattern.timestamp = std::time::Instant::now();
return;
}
}
self.access_patterns.push(AccessPattern {
operation,
region,
frequency: 1,
timestamp: std::time::Instant::now(),
});
}
pub fn analyze(&mut self) -> &Vec<LayoutOptimization> {
self.recommendations.clear();
if self.access_patterns.is_empty() {
return &self.recommendations;
}
let mut pattern_analysis = BTreeMap::new();
for pattern in &self.access_patterns {
let key = format!("{:?}", pattern.region);
let entry = pattern_analysis
.entry(key)
.or_insert((0, pattern.region.clone()));
entry.0 += pattern.frequency;
}
if let Some((_, (_, most_common_region))) =
pattern_analysis.iter().max_by_key(|(_, (freq_, _))| *freq_)
{
if most_common_region.len() == 1 {
let optimal_strip = most_common_region[0].1.max(1024);
self.recommendations.push(LayoutOptimization::Striped {
strip_size: optimal_strip,
});
} else if most_common_region.len() == 2 {
let (_row_access, row_size) = most_common_region[0];
let (_col_access, col_size) = most_common_region[1];
if row_size > col_size * 10 {
self.recommendations.push(LayoutOptimization::RowMajor);
} else if col_size > row_size * 10 {
self.recommendations.push(LayoutOptimization::ColumnMajor);
} else {
let tile_width = col_size.clamp(64, 512);
let tile_height = row_size.clamp(64, 512);
self.recommendations.push(LayoutOptimization::Tiled {
tile_width,
tile_height,
});
}
} else {
let optimal_chunks: Vec<usize> = most_common_region
.iter()
.map(|(_, size)| size.clamp(&64, &1024))
.cloned()
.collect();
self.recommendations
.push(LayoutOptimization::Chunked(optimal_chunks));
}
}
&self.recommendations
}
pub fn get_statistics(&self) -> AccessPatternStats {
let total_accesses = self.access_patterns.iter().map(|p| p.frequency).sum();
let unique_patterns = self.access_patterns.len();
let read_count = self
.access_patterns
.iter()
.filter(|p| p.operation.contains("read"))
.map(|p| p.frequency)
.sum();
let write_count = total_accesses - read_count;
AccessPatternStats {
total_accesses,
unique_patterns,
read_count,
write_count,
most_frequent_pattern: self
.access_patterns
.iter()
.max_by_key(|p| p.frequency)
.map(|p| p.region.clone()),
}
}
}
impl Default for AccessPatternAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct AccessPatternStats {
pub total_accesses: usize,
pub unique_patterns: usize,
pub read_count: usize,
pub write_count: usize,
pub most_frequent_pattern: Option<Vec<(usize, usize)>>,
}
pub struct OptimizedHDF5File {
pub base_file: EnhancedHDF5File,
pub performance_monitor: Arc<Mutex<HDF5PerformanceMonitor>>,
pub access_analyzer: Arc<Mutex<AccessPatternAnalyzer>>,
pub metadata_cache: Arc<RwLock<BTreeMap<String, ScientificMetadata>>>,
}
impl OptimizedHDF5File {
pub fn create<P: AsRef<Path>>(
path: P,
parallel_config: Option<ParallelConfig>,
) -> Result<Self> {
let base_file = EnhancedHDF5File::create(path, parallel_config)?;
Ok(Self {
base_file,
performance_monitor: Arc::new(Mutex::new(HDF5PerformanceMonitor::new())),
access_analyzer: Arc::new(Mutex::new(AccessPatternAnalyzer::new())),
metadata_cache: Arc::new(RwLock::new(BTreeMap::new())),
})
}
pub fn open<P: AsRef<Path>>(
path: P,
mode: FileMode,
parallel_config: Option<ParallelConfig>,
) -> Result<Self> {
let base_file = EnhancedHDF5File::open(path, mode, parallel_config)?;
Ok(Self {
base_file,
performance_monitor: Arc::new(Mutex::new(HDF5PerformanceMonitor::new())),
access_analyzer: Arc::new(Mutex::new(AccessPatternAnalyzer::new())),
metadata_cache: Arc::new(RwLock::new(BTreeMap::new())),
})
}
pub fn add_scientific_metadata(
&mut self,
dataset_path: &str,
metadata: ScientificMetadata,
) -> Result<()> {
{
let mut cache = lock_write(&self.metadata_cache);
cache.insert(dataset_path.to_string(), metadata.clone());
}
Ok(())
}
pub fn get_scientific_metadata(&self, datasetpath: &str) -> Option<ScientificMetadata> {
let cache = lock_read(&self.metadata_cache);
cache.get(datasetpath).cloned()
}
pub fn get_performance_report(&self) -> PerformanceSummary {
let monitor = lock_mutex(&self.performance_monitor);
monitor.get_summary()
}
pub fn get_layout_recommendations(&self) -> Vec<LayoutOptimization> {
let mut analyzer = lock_mutex(&self.access_analyzer);
analyzer.analyze().clone()
}
pub fn record_access(&self, operation: &str, region: Vec<(usize, usize)>) {
let mut analyzer = lock_mutex(&self.access_analyzer);
analyzer.record_access(operation.to_string(), region);
}
pub fn get_access_statistics(&self) -> AccessPatternStats {
let analyzer = lock_mutex(&self.access_analyzer);
analyzer.get_statistics()
}
pub fn benchmark_operation<F, R>(&self, operationname: &str, operation: F) -> Result<R>
where
F: FnOnce() -> Result<R>,
{
let start_time = Instant::now();
let result = operation()?;
let duration = start_time.elapsed().as_secs_f64() * 1000.0;
{
let mut monitor = lock_mutex(&self.performance_monitor);
monitor.record_timing(operationname, duration);
}
Ok(result)
}
}
#[cfg(test)]
mod enhanced_tests {
use super::*;
#[test]
fn test_scientific_metadata() {
let metadata = ScientificMetadata::new()
.add_string_attr("instrument", "spectrometer")
.add_float_attr("wavelength", 550.0)
.with_units("nanometers")
.with_scaling(1.0, 0.0)
.with_valid_range(0.0, 1000.0);
assert_eq!(metadata.units, Some("nanometers".to_string()));
assert_eq!(metadata.scale_factor, Some(1.0));
assert_eq!(metadata.valid_range, Some((0.0, 1000.0)));
}
#[test]
fn test_performance_monitor() {
let mut monitor = HDF5PerformanceMonitor::new();
monitor.record_timing("read", 10.0);
monitor.record_timing("read", 20.0);
monitor.record_read(1024, 10.0);
assert_eq!(monitor.avg_timing("read"), Some(15.0));
assert_eq!(monitor.transfer_stats.bytes_read, 1024);
assert_eq!(monitor.transfer_stats.read_operations, 1);
}
#[test]
fn test_access_pattern_analyzer() {
let mut analyzer = AccessPatternAnalyzer::new();
analyzer.record_access("read".to_string(), vec![(0, 100), (0, 50)]);
analyzer.record_access("read".to_string(), vec![(0, 100), (0, 50)]);
analyzer.record_access("write".to_string(), vec![(100, 100), (50, 50)]);
let stats = analyzer.get_statistics();
assert_eq!(stats.total_accesses, 3);
assert_eq!(stats.unique_patterns, 2);
assert_eq!(stats.read_count, 2);
let recommendations = analyzer.analyze();
assert!(!recommendations.is_empty());
}
}