subsume 0.8.1

Geometric region embeddings (boxes, cones, octagons, Gaussians, hyperbolic intervals, sheaf networks) for subsumption, entailment, and logical query answering
//! Backend-specific distance implementations for ndarray.

use crate::ndarray_backend::NdarrayBox;
use crate::utils::BOUNDARY_CONTAINMENT_THRESHOLD;
use crate::{Box, BoxError};
use ndarray::Array1;

/// Compute distance from a point (vector) to a box.
///
/// This implements the Concept2Box (2023) vector-to-box distance metric
/// for hybrid representations where concepts are boxes and entities are vectors.
///
/// ## Mathematical Formulation
///
/// For axis-aligned boxes, the distance from point v to box B is:
///
///
/// $$
/// d(v, B) = \sqrt{\sum_{i=1}^{d} \max(0, \min(v_i, B_{\min,i}) - B_{\min,i})^2 + \max(0, B_{\max,i} - \max(v_i, B_{\min,i}))^2}
/// $$
///
///
/// Simplified: for each dimension i:
/// - If v\[i\] < min\[i\]: distance contribution = (min\[i\] - v\[i\])²
/// - If v\[i\] > max\[i\]: distance contribution = (v\[i\] - max\[i\])²
/// - Otherwise: distance contribution = 0 (point is inside box in this dimension)
///
/// # Parameters
///
/// - `point`: The point/vector as `Array1<f32>`
/// - `box_`: The box
///
/// # Returns
///
/// Distance from point to box. Returns 0.0 if point is inside the box.
///
/// # Errors
///
/// Returns `BoxError` if point and box have dimension mismatch.
///
/// # Example
///
/// ```rust
/// use subsume::ndarray_backend::{NdarrayBox, distance::vector_to_box_distance};
/// use ndarray::array;
///
/// let box_ = NdarrayBox::new(array![0.0, 0.0], array![1.0, 1.0], 1.0).unwrap();
/// let point = array![0.5, 0.5]; // Point inside box
/// let dist = vector_to_box_distance(&point, &box_).unwrap();
/// assert_eq!(dist, 0.0);
/// ```
pub fn vector_to_box_distance(point: &Array1<f32>, box_: &NdarrayBox) -> Result<f32, BoxError> {
    if point.len() != box_.dim() {
        return Err(BoxError::DimensionMismatch {
            expected: box_.dim(),
            actual: point.len(),
        });
    }

    // For axis-aligned boxes, compute distance to nearest point on box boundary
    let mut dist_sq = 0.0;
    for i in 0..box_.dim() {
        let point_val = point[i];
        let min_val = box_.min()[i];
        let max_val = box_.max()[i];

        if point_val < min_val {
            // Point is below box in this dimension
            let gap = min_val - point_val;
            dist_sq += gap * gap;
        } else if point_val > max_val {
            // Point is above box in this dimension
            let gap = point_val - max_val;
            dist_sq += gap * gap;
        }
        // Otherwise point is inside box in this dimension, no contribution
    }

    Ok(dist_sq.sqrt())
}

/// Compute boundary distance between two boxes (RegD 2025).
///
/// Optimized implementation for ndarray that computes exact boundary distance.
pub fn boundary_distance(outer: &NdarrayBox, inner: &NdarrayBox) -> Result<Option<f32>, BoxError> {
    // Check if inner is contained in outer
    let containment = outer.containment_prob(inner)?;
    if containment < BOUNDARY_CONTAINMENT_THRESHOLD {
        // Not fully contained
        return Ok(None);
    }

    // For axis-aligned boxes, boundary distance is the minimum distance from
    // any point in inner to the boundary of outer
    // This is the minimum "gap" between inner and outer boundaries in any dimension

    let mut min_gap = f32::INFINITY;

    for i in 0..outer.dim() {
        // Gap on the min side: inner.min\[i\] - outer.min\[i\]
        let gap_min = inner.min()[i] - outer.min()[i];
        // Gap on the max side: outer.max\[i\] - inner.max\[i\]
        let gap_max = outer.max()[i] - inner.max()[i];
        // Minimum gap in this dimension
        let gap = gap_min.min(gap_max);
        min_gap = min_gap.min(gap);
    }

    // If min_gap is still infinity, something went wrong
    if min_gap == f32::INFINITY {
        return Ok(Some(0.0));
    }

    Ok(Some(min_gap))
}

/// Compute Query2Box alpha-weighted distance from an entity point to a query box.
///
/// This is the ndarray-specific implementation. See [`crate::distance::query2box_distance`]
/// for the generic (slice-based) version and full documentation.
///
/// # Parameters
///
/// - `query_box`: The query box (center = `(min + max) / 2`, offset = `(max - min) / 2`)
/// - `entity_point`: The entity point as `Array1<f32>`
/// - `alpha`: Weight for inside distance (typically 0.02)
pub fn query2box_distance(
    query_box: &NdarrayBox,
    entity_point: &Array1<f32>,
    alpha: f32,
) -> Result<f32, BoxError> {
    let center: Vec<f32> = query_box
        .min()
        .iter()
        .zip(query_box.max().iter())
        .map(|(lo, hi)| (lo + hi) * 0.5)
        .collect();
    let offset: Vec<f32> = query_box
        .min()
        .iter()
        .zip(query_box.max().iter())
        .map(|(lo, hi)| (hi - lo) * 0.5)
        .collect();
    crate::distance::query2box_distance(&center, &offset, entity_point.as_slice().unwrap(), alpha)
}

#[cfg(test)]
mod tests {
    use super::*;
    use ndarray::array;

    #[test]
    fn test_vector_to_box_distance_inside() {
        let box_ = NdarrayBox::new(array![0.0, 0.0], array![1.0, 1.0], 1.0).unwrap();
        let point = array![0.5, 0.5];
        let dist = vector_to_box_distance(&point, &box_).unwrap();
        assert_eq!(dist, 0.0);
    }

    #[test]
    fn test_vector_to_box_distance_outside() {
        let box_ = NdarrayBox::new(array![0.0, 0.0], array![1.0, 1.0], 1.0).unwrap();
        let point = array![2.0, 2.0];
        let dist = vector_to_box_distance(&point, &box_).unwrap();
        // Distance should be sqrt((2-1)^2 + (2-1)^2) = sqrt(2) ≈ 1.414
        assert!((dist - 2.0_f32.sqrt()).abs() < 1e-5);
    }

    #[test]
    fn test_vector_to_box_distance_partial() {
        let box_ = NdarrayBox::new(array![0.0, 0.0], array![1.0, 1.0], 1.0).unwrap();
        let point = array![0.5, 2.0]; // Inside in x, outside in y
        let dist = vector_to_box_distance(&point, &box_).unwrap();
        // Distance should be 1.0 (gap in y dimension)
        assert!((dist - 1.0).abs() < 1e-5);
    }

    #[test]
    fn test_boundary_distance_contained() {
        let outer = NdarrayBox::new(array![0.0, 0.0], array![1.0, 1.0], 1.0).unwrap();
        let inner = NdarrayBox::new(array![0.2, 0.2], array![0.8, 0.8], 1.0).unwrap();
        let dist = boundary_distance(&outer, &inner).unwrap();
        assert!(dist.is_some());
        let dist_val = dist.unwrap();
        assert!(dist_val >= 0.0);
        assert!(dist_val <= 0.2); // Should be at most 0.2 (the gap)
    }

    #[test]
    fn test_boundary_distance_not_contained() {
        let outer = NdarrayBox::new(array![0.0, 0.0], array![1.0, 1.0], 1.0).unwrap();
        let inner = NdarrayBox::new(array![0.5, 0.5], array![1.5, 1.5], 1.0).unwrap();
        let dist = boundary_distance(&outer, &inner).unwrap();
        assert!(dist.is_none()); // Not contained
    }

    // ---- query2box_distance ----

    #[test]
    fn test_query2box_inside() {
        let q = NdarrayBox::new(array![0.0, 0.0], array![2.0, 2.0], 1.0).unwrap();
        let e = array![1.0, 1.0]; // center
        let d = query2box_distance(&q, &e, 0.02).unwrap();
        assert_eq!(d, 0.0, "entity at center: distance should be 0");
    }

    #[test]
    fn test_query2box_outside() {
        let q = NdarrayBox::new(array![0.0, 0.0], array![2.0, 2.0], 1.0).unwrap();
        let e = array![5.0, 5.0];
        let d = query2box_distance(&q, &e, 0.02).unwrap();
        // d_out = (5-2) + (5-2) = 6
        assert!((d - 6.0).abs() < 1e-5, "expected 6.0, got {d}");
    }

    #[test]
    fn test_query2box_dim_mismatch() {
        let q = NdarrayBox::new(array![0.0, 0.0], array![2.0, 2.0], 1.0).unwrap();
        let e = array![1.0];
        assert!(query2box_distance(&q, &e, 0.02).is_err());
    }
}