boreholeio 0.1.0

A library for interacting with borehole.io, a subsurface data management, delivery and visualisation platform
Documentation
mod image_data;
mod measurement_array_view;
mod strided_array_data;
mod utils;

use super::{
    schema::{
        axes::{Base, ChannelAxis},
        Channel, Label,
    },
    strided_array_file::ArrayProxy,
};
use image_data::ImageData;
use measurement_array_view::MeasurementArrayView;
use ndarray::SliceInfoElem;
use std::{fmt::Debug, path::Path, sync::Arc};
use strided_array_data::StridedArrayData;
use utils::{calculate_bounds, update_labelled_axis};

pub use utils::{slice_coordinate_system, Error};

/// An object that either owns or references Channel data and can quickly
/// provide its views.
pub trait MeasurementArray<'a>: Debug {
    /// Gets the data view whose elements type is incapsulated into
    /// `ArrayProxy`.
    fn data(&self) -> ArrayProxy<'a>;

    /// Gets the data axes.
    ///
    /// Channel metadata, in general, includes only a subset of the data axes.
    /// For example, the order of colour channels is defined in the PNG file
    /// but not in the metadata itself. That's why this method is defined here
    /// rather than at the metadata level.
    fn axes(&self) -> &Vec<ChannelAxis>;

    /// Gets an (optional) label object which defines the implicit measurement
    /// axis, if one exists. The return value is `Some` for e.g. grayscale
    /// images.
    fn label(&self) -> &Option<Label>;

    /// Slices itself.
    ///
    /// The order of the input arguments' dimensions must comply with the one
    /// of `Self::axes()`.
    ///
    /// Technical note. A trait used as a function argument cannot have generic
    /// methods, due to compiler limitations. That's why the method below
    /// doesn't return a generic `ArrayViewD<'a, T>`.
    fn slice(
        &self,
        slice_info: Vec<SliceInfoElem>,
    ) -> Result<Arc<dyn MeasurementArray<'a> + 'a>, Error> {
        // Slice the data.
        let data = self.data().slice(slice_info.clone())?;
        // Update the label to match the data, if any.
        let label = update_label(self.label(), &data)?;
        // Slice the axes.
        let mut axes = slice_coordinate_system(self.axes(), &slice_info)?;
        // Update the labelled axes (if any) to match the sliced data.
        update_coordinate_system(&mut axes, &data)?;

        Ok(Arc::new(MeasurementArrayView::<'a>::new(axes, label, data)))
    }

    /// Deducts channel data type based on the metadata owned.
    fn data_type(&self) -> ChannelDataType {
        if self.axes().len() == 1 {
            ChannelDataType::Curve
        } else {
            ChannelDataType::GridData
        }
    }

    /// Returns `true` if none of the `axes()` are of `CompositeAxis` type, and
    /// `false` otherwise.
    fn is_decomposed(&self) -> bool {
        for axis in self.axes() {
            match axis {
                ChannelAxis::Composite(_) => return false,
                _ => continue,
            }
        }
        true
    }

    /// Breaks down each `CompositeAxis` (if any) and returns a collection of
    /// views which fully represents `Self`.
    fn decompose(&self) -> Result<Vec<Arc<dyn MeasurementArray<'a> + 'a>>, Error> {
        // Decompose the coordinate system.
        let axes_sets = decompose_coordinate_system(self.axes().clone());
        // Build the measurement arrays.
        let mut decomposed = vec![];
        for axes in axes_sets {
            decomposed.push(create_data_view(self.data(), axes, self.label().clone())?);
        }
        Ok(decomposed)
    }
}

/// Different data types are treated (stored, tiled, plotted, etc) differently.
/// This enum lists the types we have.
#[derive(Clone, Copy, Debug)]
pub enum ChannelDataType {
    Curve,
    GridData,
}

/// Reads `MeasurementArray` from disk.
pub fn get_data_from_path<'a>(
    metadata_path: &Path,
) -> Result<Arc<dyn MeasurementArray<'a> + 'a>, Error> {
    // Read the metadata.
    let metadata = Channel::from_json_file(metadata_path)?.data;

    // Choose proper file reader depending on the format.
    if metadata.format.starts_with("image/") {
        ImageData::from_uri(&metadata.uri, metadata.axes, metadata.label)
    } else if metadata.format == "application/vnd.alt.strided-array" {
        StridedArrayData::from_uri(&metadata.uri, metadata.axes, metadata.label)
    } else {
        Err(format!("Unsupported format: {}", metadata.format))
    }
}

/// Creates `MeasurementArray` view.
pub fn create_data_view<'a>(
    data: ArrayProxy<'a>,
    axes: Vec<ChannelAxis>,
    label: Option<Label>,
) -> Result<Arc<dyn MeasurementArray<'a> + 'a>, Error> {
    // Some validation.
    if data.dimensions() != axes.len() {
        return Err("Inconsistent data and axes dimensionality".to_string());
    }
    for (data_size, axis) in data.shape().iter().zip(&axes) {
        if *data_size != axis.size()? {
            return Err("Inconsistent data and axis size".to_string());
        }
    }

    Ok(Arc::new(MeasurementArrayView::new(axes, label, data)))
}

/// Updates the `label.bounds` to match the `data`.
pub fn update_label(label: &Option<Label>, data: &ArrayProxy<'_>) -> Result<Option<Label>, Error> {
    // See if input `label` is `Some`.
    if let Some(mut label) = label.clone() {
        // Update its `bounds`.
        label.bounds = calculate_bounds(data)?;

        Ok(Some(label))
    } else {
        // Do nothing otherwise.
        Ok(None)
    }
}

/// Updates every `label.bounds` of every `CoordinateAxis` to match the `data`.
fn update_coordinate_system(axes: &mut [ChannelAxis], data: &ArrayProxy<'_>) -> Result<(), Error> {
    // Process all the axes one by one independently from each other.
    for (axis_index, axis) in axes.iter_mut().enumerate() {
        // Modify only `LabelledAxis`.
        match axis {
            ChannelAxis::Labelled(labelled) => update_labelled_axis(labelled, axis_index, data)?,
            _ => continue,
        }
    }
    Ok(())
}

// The problem is solved in a dynamic programming manner: having `N - 1` axes
// already decomposed on the "previous" steps, we decompose the "next" one and
// append every of its `M` components to each of the previously decomposed sets
// (thus, increasing the total amount of the sets by a factor of `M`).
fn decompose_coordinate_system(mut composite_axes: Vec<ChannelAxis>) -> Vec<Vec<ChannelAxis>> {
    let mut result_prev: Vec<Vec<ChannelAxis>> = vec![vec![]];
    while !composite_axes.is_empty() {
        let mut result_next: Vec<Vec<ChannelAxis>> = vec![];
        for component in decompose_axis(composite_axes.remove(0)) {
            for axes_set_prev in &result_prev {
                let mut axes_set_next = axes_set_prev.clone();
                axes_set_next.push(component.clone());
                result_next.push(axes_set_next);
            }
        }
        result_prev = result_next;
    }
    result_prev
}

/// Breaks down the `ChannelAxis::Composite` variant into a collection of
/// numeric axes, or puts every other variant unchanged into a `Vec`.
fn decompose_axis(axis: ChannelAxis) -> Vec<ChannelAxis> {
    match axis {
        ChannelAxis::Composite(composite) => composite
            .descriptors
            .into_iter()
            .map(|numeric_axis| numeric_axis.into())
            .collect(),
        non_composite => vec![non_composite],
    }
}