use crate::error::{CoreError, CoreResult, ErrorContext, ErrorLocation};
use ::ndarray::{Array, ArrayBase, Data, Dimension, RawData, ShapeBuilder};
use std::mem;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum LayoutOrder {
#[default]
C,
Fortran,
Any,
Keep,
}
impl LayoutOrder {
pub const fn as_str(&self) -> &'static str {
match self {
LayoutOrder::C => "C",
LayoutOrder::Fortran => "F",
LayoutOrder::Any => "A",
LayoutOrder::Keep => "K",
}
}
pub fn parse(s: &str) -> CoreResult<Self> {
match s.to_uppercase().as_str() {
"C" => Ok(LayoutOrder::C),
"F" | "FORTRAN" => Ok(LayoutOrder::Fortran),
"A" | "ANY" => Ok(LayoutOrder::Any),
"K" | "KEEP" => Ok(LayoutOrder::Keep),
_ => Err(CoreError::ValidationError(
ErrorContext::new(format!("Invalid layout order: {s}"))
.with_location(ErrorLocation::new(file!(), line!())),
)),
}
}
}
#[derive(Debug, Clone)]
pub struct MemoryLayout {
pub shape: Vec<usize>,
pub strides: Vec<isize>,
pub element_size: usize,
pub total_size: usize,
pub order: LayoutOrder,
pub is_contiguous: bool,
pub alignment: usize,
}
impl MemoryLayout {
pub fn new_c_order(shape: &[usize]) -> Self {
Self::new_with_order(shape, LayoutOrder::C, mem::size_of::<f64>(), 64)
}
pub fn new_f_order(shape: &[usize]) -> Self {
Self::new_with_order(shape, LayoutOrder::Fortran, mem::size_of::<f64>(), 64)
}
pub fn new_with_order(
shape: &[usize],
order: LayoutOrder,
element_size: usize,
alignment: usize,
) -> Self {
let strides = Self::calculate_strides(shape, order, element_size);
let total_size = shape.iter().product::<usize>() * element_size;
let is_contiguous = Self::check_contiguous(shape, &strides, element_size);
Self {
shape: shape.to_vec(),
strides,
element_size,
total_size,
order,
is_contiguous,
alignment,
}
}
pub fn from_array<S, D>(array: &ArrayBase<S, D>) -> Self
where
S: RawData,
D: Dimension,
{
let shape = array.shape().to_vec();
let strides: Vec<isize> = array.strides().to_vec();
let element_size = mem::size_of::<S::Elem>();
let total_size = array.len() * element_size;
let order = if Self::is_c_order(&shape, &strides, element_size) {
LayoutOrder::C
} else if Self::is_f_order(&shape, &strides, element_size) {
LayoutOrder::Fortran
} else {
LayoutOrder::Any
};
let is_contiguous = Self::check_contiguous(&shape, &strides, element_size);
Self {
shape,
strides,
element_size,
total_size,
order,
is_contiguous,
alignment: 64, }
}
pub fn calculate_strides(
shape: &[usize],
order: LayoutOrder,
element_size: usize,
) -> Vec<isize> {
if shape.is_empty() {
return Vec::new();
}
let mut strides = vec![0; shape.len()];
match order {
LayoutOrder::C => {
let mut stride = element_size as isize;
for i in (0..shape.len()).rev() {
strides[i] = stride;
stride *= shape[i] as isize;
}
}
LayoutOrder::Fortran => {
let mut stride = element_size as isize;
for i in 0..shape.len() {
strides[i] = stride;
stride *= shape[i] as isize;
}
}
LayoutOrder::Any | LayoutOrder::Keep => {
return Self::calculate_strides(shape, LayoutOrder::C, element_size);
}
}
strides
}
pub fn is_c_contiguous(&self) -> bool {
Self::is_c_order(&self.shape, &self.strides, self.element_size)
}
pub fn is_f_contiguous(&self) -> bool {
Self::is_f_order(&self.shape, &self.strides, self.element_size)
}
fn is_c_order(shape: &[usize], strides: &[isize], elementsize: usize) -> bool {
if shape.len() != strides.len() {
return false;
}
let expected_strides = Self::calculate_strides(shape, LayoutOrder::C, elementsize);
strides == expected_strides
}
fn is_f_order(shape: &[usize], strides: &[isize], elementsize: usize) -> bool {
if shape.len() != strides.len() {
return false;
}
let expected_strides = Self::calculate_strides(shape, LayoutOrder::Fortran, elementsize);
strides == expected_strides
}
fn check_contiguous(shape: &[usize], strides: &[isize], elementsize: usize) -> bool {
Self::is_c_order(shape, strides, elementsize)
|| Self::is_f_order(shape, strides, elementsize)
}
pub fn to_order(&self, neworder: LayoutOrder) -> CoreResult<Self> {
if neworder == LayoutOrder::Keep {
return Ok(self.clone());
}
if neworder == LayoutOrder::Any {
return Ok(self.clone());
}
Ok(Self::new_with_order(
&self.shape,
neworder,
self.element_size,
self.alignment,
))
}
pub fn linear_index(&self, indices: &[usize]) -> CoreResult<usize> {
if indices.len() != self.shape.len() {
return Err(CoreError::ShapeError(
ErrorContext::new(format!(
"Index dimensions {} don't match array dimensions {}",
indices.len(),
self.shape.len()
))
.with_location(ErrorLocation::new(file!(), line!())),
));
}
for (i, (&idx, &dim)) in indices.iter().zip(self.shape.iter()).enumerate() {
if idx >= dim {
return Err(CoreError::IndexError(
ErrorContext::new(format!(
"Index {idx} is out of bounds for axis {i} with size {dim}"
))
.with_location(ErrorLocation::new(file!(), line!())),
));
}
}
let mut linear_idx = 0;
for (idx, stride) in indices.iter().zip(self.strides.iter()) {
linear_idx += (*idx as isize) * stride;
}
Ok((linear_idx / self.element_size as isize) as usize)
}
pub fn multi_indices(&self, linearidx: usize) -> CoreResult<Vec<usize>> {
let total_elements = self.shape.iter().product::<usize>();
if linearidx >= total_elements {
return Err(CoreError::IndexError(
ErrorContext::new(format!(
"Linear index {linearidx} is out of bounds for array with {total_elements} elements"
))
.with_location(ErrorLocation::new(file!(), line!())),
));
}
let mut indices = vec![0; self.shape.len()];
let mut remaining = linearidx;
match self.order {
LayoutOrder::C => {
for (i, &shape_dim) in self.shape.iter().enumerate().rev() {
indices[i] = remaining % shape_dim;
remaining /= shape_dim;
}
}
LayoutOrder::Fortran => {
for (idx, shape_dim) in indices.iter_mut().zip(&self.shape) {
*idx = remaining % shape_dim;
remaining /= shape_dim;
}
}
LayoutOrder::Any | LayoutOrder::Keep => {
remaining *= self.element_size;
for (i, &stride) in self.strides.iter().enumerate().take(self.shape.len()) {
if stride > 0 {
indices[i] = (remaining as isize / stride) as usize;
remaining = (remaining as isize % stride) as usize;
}
}
}
}
Ok(indices)
}
pub fn memory_offset(&self, indices: &[usize]) -> CoreResult<usize> {
if indices.len() != self.shape.len() {
return Err(CoreError::ShapeError(
ErrorContext::new(format!(
"Index dimensions {} don't match array dimensions {}",
indices.len(),
self.shape.len()
))
.with_location(ErrorLocation::new(file!(), line!())),
));
}
let mut offset = 0isize;
for (idx, stride) in indices.iter().zip(self.strides.iter()) {
offset += (*idx as isize) * stride;
}
Ok(offset as usize)
}
pub fn ndim(&self) -> usize {
self.shape.len()
}
pub fn size(&self) -> usize {
self.shape.iter().product()
}
pub fn nbytes(&self) -> usize {
self.total_size
}
pub fn is_compatible_with(&self, other: &MemoryLayout) -> bool {
self.shape == other.shape && self.element_size == other.element_size
}
pub fn transpose(&self, axes: Option<&[usize]>) -> CoreResult<Self> {
let ndim = self.shape.len();
let axes = if let Some(axes) = axes {
if axes.len() != ndim {
return Err(CoreError::ShapeError(
ErrorContext::new(format!(
"Axes length {} doesn't match number of dimensions {}",
axes.len(),
ndim
))
.with_location(ErrorLocation::new(file!(), line!())),
));
}
let mut sorted_axes = axes.to_vec();
sorted_axes.sort_unstable();
let expected: Vec<usize> = (0..ndim).collect();
if sorted_axes != expected {
return Err(CoreError::ValidationError(
ErrorContext::new("Invalid transpose axes".to_string())
.with_location(ErrorLocation::new(file!(), line!())),
));
}
axes.to_vec()
} else {
(0..ndim).rev().collect()
};
let mut newshape = vec![0; ndim];
let mut new_strides = vec![0; ndim];
for (i, &axis) in axes.iter().enumerate() {
newshape[i] = self.shape[axis];
new_strides[i] = self.strides[axis];
}
let new_order = if Self::is_c_order(&newshape, &new_strides, self.element_size) {
LayoutOrder::C
} else if Self::is_f_order(&newshape, &new_strides, self.element_size) {
LayoutOrder::Fortran
} else {
LayoutOrder::Any
};
let is_contiguous = Self::check_contiguous(&newshape, &new_strides, self.element_size);
Ok(Self {
shape: newshape,
strides: new_strides,
element_size: self.element_size,
total_size: self.total_size,
order: new_order,
is_contiguous,
alignment: self.alignment,
})
}
pub fn reshape(&self, newshape: &[usize]) -> CoreResult<Self> {
let new_size = newshape.iter().product::<usize>();
let old_size = self.size();
if new_size != old_size {
return Err(CoreError::ShapeError(
ErrorContext::new(format!(
"Cannot reshape array of size {old_size} into shape with size {new_size}"
))
.with_location(ErrorLocation::new(file!(), line!())),
));
}
let order = if self.is_contiguous {
self.order
} else {
LayoutOrder::C };
Ok(Self::new_with_order(
newshape,
order,
self.element_size,
self.alignment,
))
}
pub fn new_withshape_and_strides(
&self,
newshape: &[usize],
new_strides: Option<&[isize]>,
) -> CoreResult<Self> {
let strides = if let Some(strides) = new_strides {
if strides.len() != newshape.len() {
return Err(CoreError::ShapeError(
ErrorContext::new(format!(
"Strides length {} doesn't match shape length {}",
strides.len(),
newshape.len()
))
.with_location(ErrorLocation::new(file!(), line!())),
));
}
strides.to_vec()
} else {
Self::calculate_strides(newshape, self.order, self.element_size)
};
let order = if Self::is_c_order(newshape, &strides, self.element_size) {
LayoutOrder::C
} else if Self::is_f_order(newshape, &strides, self.element_size) {
LayoutOrder::Fortran
} else {
LayoutOrder::Any
};
let is_contiguous = Self::check_contiguous(newshape, &strides, self.element_size);
let total_size = newshape.iter().product::<usize>() * self.element_size;
Ok(Self {
shape: newshape.to_vec(),
strides,
element_size: self.element_size,
total_size,
order,
is_contiguous,
alignment: self.alignment,
})
}
}
pub struct ArrayLayout;
impl ArrayLayout {
pub fn to_c_order<A, S, D>(array: ArrayBase<S, D>) -> Array<A, D>
where
A: Clone + num_traits::Zero,
S: Data<Elem = A>,
D: Dimension,
{
if array.is_standard_layout() {
array.to_owned()
} else {
let mut result = Array::zeros(array.raw_dim());
result.assign(&array);
result
}
}
pub fn to_f_order<A, S, D>(array: ArrayBase<S, D>) -> Array<A, D>
where
A: Clone + num_traits::Zero,
S: Data<Elem = A>,
D: Dimension,
{
array.to_owned()
}
pub fn zeros_with_order<A, D>(shape: D, order: LayoutOrder) -> Array<A, D>
where
A: Clone + Default + num_traits::Zero,
D: Dimension,
{
match order {
LayoutOrder::C => Array::zeros(shape),
LayoutOrder::Fortran => Array::zeros(shape.f()),
LayoutOrder::Any => Array::zeros(shape), LayoutOrder::Keep => Array::zeros(shape), }
}
pub fn is_c_order<S, D>(array: &ArrayBase<S, D>) -> bool
where
S: RawData,
D: Dimension,
{
array.is_standard_layout()
}
pub fn is_f_order<S, D>(array: &ArrayBase<S, D>) -> bool
where
S: RawData,
D: Dimension,
{
let strides = array.strides();
if strides.len() <= 1 {
return true;
}
for i in 1..strides.len() {
if strides[i] < strides[i.saturating_sub(1)] {
return false;
}
}
true
}
pub fn get_layout<S, D>(array: &ArrayBase<S, D>) -> MemoryLayout
where
S: RawData,
D: Dimension,
{
MemoryLayout::from_array(array)
}
pub fn optimize_for_access<A, S, D>(
array: ArrayBase<S, D>,
access_pattern: AccessPattern,
) -> Array<A, D>
where
A: Clone + num_traits::Zero,
S: Data<Elem = A>,
D: Dimension,
{
match access_pattern {
AccessPattern::RowMajor => Self::to_c_order(array),
AccessPattern::ColumnMajor => Self::to_f_order(array),
AccessPattern::Random => {
if array.len() > 10000 {
Self::to_c_order(array) } else {
array.to_owned()
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessPattern {
RowMajor,
ColumnMajor,
Random,
}
pub struct LayoutConverter;
impl LayoutConverter {
pub fn convert_chunked<A>(
source_layout: &MemoryLayout,
target_order: LayoutOrder,
data: &[A],
chunk_size: usize,
) -> CoreResult<Vec<A>>
where
A: Clone + Copy + Default,
{
let target_layout = source_layout.to_order(target_order)?;
if source_layout.order == target_order || source_layout.is_compatible_with(&target_layout) {
return Ok(data.to_vec());
}
let total_elements = source_layout.size();
let mut result = vec![A::default(); total_elements];
let num_chunks = total_elements.div_ceil(chunk_size);
for chunk_idx in 0..num_chunks {
let start = chunk_idx * chunk_size;
let end = std::cmp::min(start + chunk_size, total_elements);
#[allow(clippy::needless_range_loop)]
for linear_idx in start..end {
let source_indices = source_layout.multi_indices(linear_idx)?;
let target_linear_idx = target_layout.linear_index(&source_indices)?;
result[target_linear_idx] = data[linear_idx];
}
}
Ok(result)
}
pub fn convert_inplace<A>(
layout: &MemoryLayout,
target_order: LayoutOrder,
data: &mut [A],
) -> CoreResult<MemoryLayout>
where
A: Clone + Copy + Default,
{
if layout.order == target_order {
return Ok(layout.clone());
}
let converted = Self::convert_chunked(layout, target_order, data, 8192)?;
data.copy_from_slice(&converted);
layout.to_order(target_order)
}
}
pub struct ArrayCreation;
impl ArrayCreation {
pub fn array_with_order<A, D>(
data: Vec<A>,
shape: D,
order: LayoutOrder,
) -> CoreResult<Array<A, D>>
where
A: Clone,
D: Dimension,
{
let expected_size = shape.size();
if data.len() != expected_size {
return Err(CoreError::ShapeError(
ErrorContext::new(format!(
"Data length {} doesn't match shape size {}",
data.len(),
expected_size
))
.with_location(ErrorLocation::new(file!(), line!())),
));
}
match order {
LayoutOrder::C => Array::from_shape_vec(shape, data).map_err(|e| {
CoreError::ShapeError(
ErrorContext::new(format!("error: {e}"))
.with_location(ErrorLocation::new(file!(), line!())),
)
}),
LayoutOrder::Fortran => Array::from_shape_vec(shape.f(), data).map_err(|e| {
CoreError::ShapeError(
ErrorContext::new(format!("error: {e}"))
.with_location(ErrorLocation::new(file!(), line!())),
)
}),
LayoutOrder::Any | LayoutOrder::Keep => {
Self::array_with_order(data, shape, LayoutOrder::C)
}
}
}
pub fn zeros_with_order<A, D>(shape: D, order: LayoutOrder) -> Array<A, D>
where
A: Clone + Default + num_traits::Zero,
D: Dimension,
{
ArrayLayout::zeros_with_order(shape, order)
}
pub fn ones_with_order<A, D>(shape: D, order: LayoutOrder) -> Array<A, D>
where
A: Clone + num_traits::One,
D: Dimension,
{
match order {
LayoutOrder::C => Array::ones(shape),
LayoutOrder::Fortran => Array::ones(shape.f()),
LayoutOrder::Any | LayoutOrder::Keep => Array::ones(shape),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ::ndarray::Array2;
#[test]
fn test_layout_order_parsing() {
assert_eq!(
LayoutOrder::parse("C").expect("Operation failed"),
LayoutOrder::C
);
assert_eq!(
LayoutOrder::parse("F").expect("Operation failed"),
LayoutOrder::Fortran
);
assert_eq!(
LayoutOrder::parse("fortran").expect("Operation failed"),
LayoutOrder::Fortran
);
assert_eq!(
LayoutOrder::parse("A").expect("Operation failed"),
LayoutOrder::Any
);
assert_eq!(
LayoutOrder::parse("K").expect("Operation failed"),
LayoutOrder::Keep
);
assert!(LayoutOrder::parse("X").is_err());
}
#[test]
fn test_memory_layout_creation() {
let c_layout = MemoryLayout::new_c_order(&[10, 20]);
assert_eq!(c_layout.shape, vec![10, 20]);
assert_eq!(c_layout.order, LayoutOrder::C);
assert!(c_layout.is_c_contiguous());
assert!(!c_layout.is_f_contiguous());
let f_layout = MemoryLayout::new_f_order(&[10, 20]);
assert_eq!(f_layout.shape, vec![10, 20]);
assert_eq!(f_layout.order, LayoutOrder::Fortran);
assert!(!f_layout.is_c_contiguous());
assert!(f_layout.is_f_contiguous());
}
#[test]
fn test_stride_calculation() {
let element_size = 8;
let c_strides = MemoryLayout::calculate_strides(&[10, 20], LayoutOrder::C, element_size);
assert_eq!(c_strides, vec![160, 8]);
let f_strides =
MemoryLayout::calculate_strides(&[10, 20], LayoutOrder::Fortran, element_size);
assert_eq!(f_strides, vec![8, 80]); }
#[test]
fn test_linear_indexing() {
let layout = MemoryLayout::new_c_order(&[3, 4]);
assert_eq!(layout.linear_index(&[0, 0]).expect("Operation failed"), 0);
assert_eq!(layout.linear_index(&[0, 1]).expect("Operation failed"), 1);
assert_eq!(layout.linear_index(&[1, 0]).expect("Operation failed"), 4);
assert_eq!(layout.linear_index(&[2, 3]).expect("Operation failed"), 11);
assert!(layout.linear_index(&[3, 0]).is_err());
assert!(layout.linear_index(&[0, 4]).is_err());
}
#[test]
fn test_multi_indexing() {
let layout = MemoryLayout::new_c_order(&[3, 4]);
assert_eq!(
layout.multi_indices(0).expect("Operation failed"),
vec![0, 0]
);
assert_eq!(
layout.multi_indices(1).expect("Operation failed"),
vec![0, 1]
);
assert_eq!(
layout.multi_indices(4).expect("Operation failed"),
vec![1, 0]
);
assert_eq!(
layout.multi_indices(11).expect("Operation failed"),
vec![2, 3]
);
assert!(layout.multi_indices(12).is_err());
}
#[test]
fn test_layout_conversion() {
let c_layout = MemoryLayout::new_c_order(&[5, 6]);
let f_layout = c_layout
.to_order(LayoutOrder::Fortran)
.expect("Operation failed");
assert_eq!(f_layout.order, LayoutOrder::Fortran);
assert_eq!(f_layout.shape, c_layout.shape);
assert!(f_layout.is_f_contiguous());
}
#[test]
fn test_transpose() {
let layout = MemoryLayout::new_c_order(&[3, 4, 5]);
let transposed = layout
.transpose(Some(&[2, 0, 1]))
.expect("Operation failed");
assert_eq!(transposed.shape, vec![5, 3, 4]);
let default_transposed = layout.transpose(None).expect("Operation failed");
assert_eq!(default_transposed.shape, vec![5, 4, 3]);
}
#[test]
fn test_reshape() {
let layout = MemoryLayout::new_c_order(&[6, 4]);
let reshaped = layout.reshape(&[3, 8]).expect("Operation failed");
assert_eq!(reshaped.shape, vec![3, 8]);
assert_eq!(reshaped.size(), layout.size());
assert!(layout.reshape(&[5, 5]).is_err());
}
#[test]
fn test_array_layout_utilities() {
let arr = Array2::<f64>::zeros((10, 20));
assert!(ArrayLayout::is_c_order(&arr));
assert!(!ArrayLayout::is_f_order(&arr));
let f_arr = Array2::<f64>::zeros((10, 20).f());
assert!(!ArrayLayout::is_c_order(&f_arr));
assert!(ArrayLayout::is_f_order(&f_arr));
}
#[test]
fn test_array_creation_with_order() {
let data: Vec<f64> = (0..12).map(|x| x as f64).collect();
let c_array: crate::ndarray::Array2<f64> = ArrayCreation::array_with_order(
data.clone(),
crate::ndarray::Ix2(3, 4),
LayoutOrder::C,
)
.expect("Operation failed");
assert!(ArrayLayout::is_c_order(&c_array));
let f_array: crate::ndarray::Array2<f64> =
ArrayCreation::array_with_order(data, crate::ndarray::Ix2(3, 4), LayoutOrder::Fortran)
.expect("Operation failed");
assert!(ArrayLayout::is_f_order(&f_array));
}
}