boreholeio 0.1.0

A library for interacting with borehole.io, a subsurface data management, delivery and visualisation platform
Documentation
mod channel_axis;
mod composite_axis;
mod coordinate_axis;
mod labelled_axis;
mod linear_axis;
mod logarithmic_axis;
mod numeric_axis;
mod tile_axis;
mod utils;

use ndarray::{Array1, SliceInfoElem};
use ordered_float::OrderedFloat;
use std::fmt::Debug;

pub use channel_axis::ChannelAxis;
pub use composite_axis::CompositeAxis;
pub use coordinate_axis::CoordinateAxis;
pub use labelled_axis::LabelledAxis;
pub use linear_axis::LinearAxis;
pub use logarithmic_axis::LogarithmicAxis;
pub use numeric_axis::NumericAxis;
pub use tile_axis::TileAxis;
pub use utils::{Monotonicity, SortOrder};

pub type Error = String;

/// Common properties of any axis.
#[ambassador::delegatable_trait]
pub trait Base: Debug {
    /// Coordinates array length of this axis.
    fn size(&self) -> Result<usize, Error>;

    /// Returns a subsection of this axis based on the provided indexing.
    fn slice(&self, slice_info: &SliceInfoElem) -> Result<ChannelAxis, Error>;
}

/// Numeric axes coordinates and indices interpolation methods.
///
/// @see `Numeric::{interp, rinterp}`.
pub enum InterpolationType {
    NearestNeighbor,
}

/// Numeric axes specific properties.
#[ambassador::delegatable_trait]
pub trait Numeric: Base {
    /// Name of this axis.
    fn name(&self) -> &str;

    /// The units of this axis.
    fn units(&self) -> &Option<String>;

    /// The coordinates associated with this axis.
    fn coordinates(&self) -> Result<Array1<f64>, Error>;

    /// Decomposes `self` into a collection of monotonic axes.
    fn make_monotonic(&self) -> Result<Vec<NumericAxis>, Error>;

    /// Analyzes whether coordinates() are monotonic. Distinguishes between
    /// "strictly monotonic" and "monotonic".
    fn monotonicity(&self) -> Result<Monotonicity, Error> {
        let coordinates = self.coordinates()?;
        if coordinates.len() <= 1 {
            return Ok(Monotonicity {
                order: SortOrder::Ascending,
                strict: true,
            });
        }

        let order = if coordinates.windows(2).into_iter().all(|w| w[0] <= w[1]) {
            SortOrder::Ascending
        } else if coordinates.windows(2).into_iter().all(|w| w[0] >= w[1]) {
            SortOrder::Descending
        } else {
            SortOrder::Unsorted
        };

        let strict = coordinates.windows(2).into_iter().all(|w| w[0] != w[1]);

        Ok(Monotonicity { order, strict })
    }

    /// Returns floating point index which corresponds to the given
    /// `coordinate` and `interpolation_type`.
    fn interp(&self, coordinate: f64, interpolation_type: InterpolationType) -> Result<f64, Error> {
        match interpolation_type {
            InterpolationType::NearestNeighbor => {
                let (nearest_index, _nearest_coordinate) = self
                    .coordinates()?
                    .iter()
                    .enumerate()
                    .min_by_key(|(_index, value)|
                        // 1. `OrderedFloat` is because of:
                        //    https://github.com/rust-lang/rust/issues/67417
                        // 2. `[[0]]` will `panic` when coordinates are not 1D.
                        OrderedFloat((*value - coordinate).abs()))
                    .ok_or("Empty coordinates array".to_string())?;
                Ok(nearest_index as f64)
            }
        }
    }

    /// Returns floating point coordinate which corresponds to the given
    /// `index` and `interpolation_type`.
    fn rinterp(&self, index: f64, interpolation_type: InterpolationType) -> Result<f64, Error> {
        match interpolation_type {
            InterpolationType::NearestNeighbor => {
                let coordinates = self.coordinates()?;
                if coordinates.is_empty() {
                    return Err("Empty coordinates array".into());
                }
                let nearest_integer = (index.round() as usize).clamp(0, coordinates.len() - 1);
                Ok(coordinates[nearest_integer])
            }
        }
    }

    /// Coordinates bounds (`[min, max]`) of this axis.
    fn bounds(&self) -> Result<[f64; 2], Error> {
        let coordinates: Vec<_> = self.coordinates()?.into_iter().map(OrderedFloat).collect();
        if coordinates.is_empty() {
            return Err("Cannot calculate empty coordinates bounds".into());
        }
        Ok([
            f64::from(*coordinates.iter().min().unwrap()),
            f64::from(*coordinates.iter().max().unwrap()),
        ])
    }

    /// Returns `true` if `coordinate` lies within `bounds`, inclusive; or
    /// `false` otherwise.
    fn covers(&self, coordinate: f64) -> Result<bool, Error> {
        let bounds = self.bounds()?;
        Ok(coordinate >= bounds[0] && coordinate <= bounds[1])
    }
}

/// Properties specific to ordered numeric axes (such as linear or
/// logarithmic).
pub trait OrderedNumeric: Numeric {
    /// Returns ordered coordinate extents as `[first, last]`.
    fn extents(&self) -> [f64; 2];
}