use crate::curves::Point2D;
use crate::error::decimal::DecimalError;
use crate::error::{InterpolationError, MetricsError, SurfaceError};
use crate::geometrics::{
Arithmetic, AxisOperations, BasicMetrics, BiLinearInterpolation, ConstructionMethod,
ConstructionParams, CubicInterpolation, GeometricObject, GeometricTransformations, Interpolate,
InterpolationType, LinearInterpolation, MergeAxisInterpolate, MergeOperation, MetricsExtractor,
RangeMetrics, RiskMetrics, ShapeMetrics, SplineInterpolation, TrendMetrics, powu_checked,
};
use crate::model::decimal::{d_add, d_div, d_mul, d_product_iter, d_sub, d_sum_iter};
use crate::surfaces::Point3D;
use crate::surfaces::types::Axis;
use crate::utils::Len;
use crate::visualization::{Graph, GraphData, Surface3D};
use num_traits::ToPrimitive;
use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator};
use rust_decimal::{Decimal, MathematicalOps};
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::ops::Index;
use std::sync::Arc;
use tracing::warn;
use utoipa::ToSchema;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(deny_unknown_fields)]
pub struct Surface {
pub points: BTreeSet<Point3D>,
pub x_range: (Decimal, Decimal),
pub y_range: (Decimal, Decimal),
}
impl Surface {
#[must_use]
pub fn new(points: BTreeSet<Point3D>) -> Self {
let x_range = Self::calculate_range(points.iter().map(|p| p.x));
let y_range = Self::calculate_range(points.iter().map(|p| p.y));
Self {
points,
x_range,
y_range,
}
}
#[must_use]
pub fn project_onto(&self, axis: Axis) -> Vec<Point2D> {
let mut points: Vec<Point2D> = self
.points
.iter()
.map(|p| match axis {
Axis::X => Point2D::new(p.y, p.z),
Axis::Y => Point2D::new(p.x, p.z),
Axis::Z => Point2D::new(p.x, p.y),
})
.collect();
points.sort();
points
}
fn one_dimensional_spline_interpolation<T>(
&self,
points: &[T],
target: Decimal,
x_selector: fn(&T) -> Decimal,
z_selector: fn(&T) -> Decimal,
) -> Result<Decimal, InterpolationError>
where
T: Clone,
{
let mut sorted_points = points.to_vec();
sorted_points.sort_by(|a, b| {
x_selector(a)
.partial_cmp(&x_selector(b))
.unwrap_or(std::cmp::Ordering::Equal)
});
if sorted_points.len() < 2 {
return Err(InterpolationError::Spline(
"Insufficient points for interpolation".to_string(),
));
}
let missing = |index: usize| {
InterpolationError::Spline(format!(
"spline knot {index} is out of bounds for {} samples",
sorted_points.len()
))
};
let first = sorted_points.first().ok_or_else(|| missing(0))?;
if target <= x_selector(first) {
return Ok(z_selector(first));
}
let last_index = sorted_points.len() - 1;
let last = sorted_points.last().ok_or_else(|| missing(last_index))?;
if target >= x_selector(last) {
return Ok(z_selector(last));
}
let (left_index, right_index) = match sorted_points
.iter()
.enumerate()
.find(|(_, p)| x_selector(p) > target)
{
Some((index, _)) => (
index.checked_sub(1).ok_or_else(|| {
InterpolationError::Spline(
"spline bracket starts before the first knot".to_string(),
)
})?,
index,
),
None => (
last_index.checked_sub(1).ok_or_else(|| {
InterpolationError::Spline(
"spline bracket needs at least two knots".to_string(),
)
})?,
last_index,
),
};
let left = sorted_points
.get(left_index)
.ok_or_else(|| missing(left_index))?;
let right = sorted_points
.get(right_index)
.ok_or_else(|| missing(right_index))?;
let x0 = x_selector(left);
let x1 = x_selector(right);
let z0 = z_selector(left);
let z1 = z_selector(right);
let op = "Surface::one_dimensional_spline_interpolation";
let run = d_sub(x1, x0, op).map_err(interp_err(InterpolationError::Spline))?;
if run.is_zero() {
return Err(InterpolationError::DegenerateInterval);
}
let rise = d_sub(z1, z0, op).map_err(interp_err(InterpolationError::Spline))?;
let offset = d_sub(target, x0, op).map_err(interp_err(InterpolationError::Spline))?;
let ratio = d_div(offset, run, op).map_err(interp_err(InterpolationError::Spline))?;
let step = d_mul(rise, ratio, op).map_err(interp_err(InterpolationError::Spline))?;
let interpolated_z = d_add(z0, step, op).map_err(interp_err(InterpolationError::Spline))?;
Ok(interpolated_z)
}
fn point_at(&self, index: usize) -> Result<&Point3D, SurfaceError> {
self.points.iter().nth(index).ok_or_else(|| {
SurfaceError::AnalysisError(format!(
"point index {index} is out of bounds for a surface of {} points",
self.points.len()
))
})
}
#[must_use]
pub fn get_f64_points(&self) -> Vec<(f64, f64, f64)> {
self.points
.iter()
.map(|p| {
(
p.x.to_f64().unwrap_or(0.0),
p.z.to_f64().unwrap_or(0.0),
p.y.to_f64().unwrap_or(0.0),
)
})
.collect()
}
}
fn interp_err(
kind: fn(String) -> InterpolationError,
) -> impl Fn(DecimalError) -> InterpolationError {
move |err| kind(err.to_string())
}
fn construction_err(err: DecimalError) -> SurfaceError {
SurfaceError::ConstructionError(err.to_string())
}
fn analysis_err(err: DecimalError) -> SurfaceError {
SurfaceError::AnalysisError(err.to_string())
}
fn trend_err(err: DecimalError) -> MetricsError {
MetricsError::TrendError(err.to_string())
}
fn risk_err(err: DecimalError) -> MetricsError {
MetricsError::RiskError(err.to_string())
}
fn squared_distance(
point: &Point3D,
x: Decimal,
y: Decimal,
op: &'static str,
) -> Result<Decimal, DecimalError> {
let dx = d_sub(point.x, x, op)?;
let dy = d_sub(point.y, y, op)?;
d_add(powu_checked(dx, 2, op)?, powu_checked(dy, 2, op)?, op)
}
fn sort_by_distance<'a>(
points: &mut [&'a Point3D],
x: Decimal,
y: Decimal,
op: &'static str,
) -> Result<(), DecimalError> {
let mut keyed: Vec<(Decimal, &'a Point3D)> = Vec::with_capacity(points.len());
for point in points.iter() {
keyed.push((squared_distance(point, x, y, op)?, point));
}
keyed.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
for (slot, (_, point)) in points.iter_mut().zip(keyed) {
*slot = point;
}
Ok(())
}
fn nth<'a>(
points: &[&'a Point3D],
index: usize,
kind: fn(String) -> InterpolationError,
) -> Result<&'a Point3D, InterpolationError> {
points.get(index).copied().ok_or_else(|| {
kind(format!(
"neighbour {index} is out of bounds for a window of {} points",
points.len()
))
})
}
fn sample_at(
values: &[Decimal],
index: usize,
what: &'static str,
) -> Result<Decimal, MetricsError> {
values.get(index).copied().ok_or_else(|| {
MetricsError::BasicError(format!(
"{what}: sample {index} is out of bounds for {} values",
values.len()
))
})
}
fn mean_of(values: &[Decimal], op: &'static str) -> Result<Decimal, DecimalError> {
let sum = d_sum_iter(values.iter().copied(), op)?;
d_div(sum, Decimal::from(values.len()), op)
}
fn central_moment(
values: &[Decimal],
mean: Decimal,
order: u64,
op: &'static str,
) -> Result<Decimal, DecimalError> {
let mut acc = Decimal::ZERO;
for &value in values {
let centered = d_sub(value, mean, op)?;
acc = d_add(acc, powu_checked(centered, order, op)?, op)?;
}
Ok(acc)
}
impl Default for Surface {
fn default() -> Self {
Self {
points: BTreeSet::new(),
x_range: (Decimal::ZERO, Decimal::ZERO),
y_range: (Decimal::ZERO, Decimal::ZERO),
}
}
}
impl Graph for Surface {
fn graph_data(&self) -> GraphData {
GraphData::GraphSurface(Surface3D {
x: self.points.iter().map(|p| p.x).collect(),
y: self.points.iter().map(|p| p.y).collect(),
z: self.points.iter().map(|p| p.z).collect(),
name: "Surface".to_string(),
})
}
}
impl GeometricObject<Point3D, Point2D> for Surface {
type Error = SurfaceError;
fn get_points(&self) -> BTreeSet<&Point3D> {
self.points.iter().collect()
}
fn from_vector<T>(points: Vec<T>) -> Self
where
T: Into<Point3D> + Clone,
{
let points: BTreeSet<Point3D> = points.into_iter().map(|p| p.into()).collect();
let x_range = Self::calculate_range(points.iter().map(|p| p.x));
let y_range = Self::calculate_range(points.iter().map(|p| p.y));
Surface {
points,
x_range,
y_range,
}
}
fn construct<T>(method: T) -> Result<Self, Self::Error>
where
Self: Sized,
T: Into<ConstructionMethod<Point3D, Point2D>>,
{
let method = method.into();
match method {
ConstructionMethod::FromData { points } => {
if points.is_empty() {
return Err(SurfaceError::Point3DError {
reason: "Empty points array",
});
}
Ok(Surface::new(points))
}
ConstructionMethod::Parametric { f, params } => {
let (x_start, x_end, y_start, y_end, x_steps, y_steps) = match params {
ConstructionParams::D3 {
x_start,
x_end,
y_start,
y_end,
x_steps,
y_steps,
} => (x_start, x_end, y_start, y_end, x_steps, y_steps),
_ => {
return Err(SurfaceError::ConstructionError(
"Invalid parameters".to_string(),
));
}
};
if x_steps == 0 || y_steps == 0 {
return Err(SurfaceError::ConstructionError(
"Parametric construction needs at least one step on each axis".to_string(),
));
}
let op = "Surface::construct::step";
let x_span = d_sub(x_end, x_start, op).map_err(construction_err)?;
let x_step = d_div(x_span, Decimal::from(x_steps), op).map_err(construction_err)?;
let y_span = d_sub(y_end, y_start, op).map_err(construction_err)?;
let y_step = d_div(y_span, Decimal::from(y_steps), op).map_err(construction_err)?;
let f = Arc::new(f);
let points: Result<BTreeSet<Point3D>, SurfaceError> = (0..=x_steps)
.into_par_iter()
.flat_map(|i| {
let f = Arc::clone(&f);
(0..=y_steps).into_par_iter().map(move |j| {
let x_offset =
d_mul(x_step, Decimal::from(i), op).map_err(construction_err)?;
let x = d_add(x_start, x_offset, op).map_err(construction_err)?;
let y_offset =
d_mul(y_step, Decimal::from(j), op).map_err(construction_err)?;
let y = d_add(y_start, y_offset, op).map_err(construction_err)?;
let t = Point2D::new(x, y);
f(t).map_err(|e| SurfaceError::ConstructionError(e.to_string()))
})
})
.collect();
points.map(Surface::new)
}
}
}
}
impl Index<usize> for Surface {
type Output = Point3D;
fn index(&self, index: usize) -> &Self::Output {
match self.points.iter().nth(index) {
Some(p) => p,
None => {
let len = self.points.len();
panic!("Surface::index: out of bounds (index = {index}, len = {len})") }
}
}
}
impl Interpolate<Point3D, Point2D> for Surface {}
impl LinearInterpolation<Point3D, Point2D> for Surface {
fn linear_interpolate(&self, xy: Point2D) -> Result<Point3D, InterpolationError> {
let first = match self.points.iter().next() {
Some(p) => p,
None => {
return Err(InterpolationError::Linear(
"No points in the surface".to_string(),
));
}
};
let all_same_xy = self.points.iter().all(|p| p.x == first.x && p.y == first.y);
if all_same_xy && (first.x == xy.x && first.y == xy.y) {
return Err(InterpolationError::Linear(
"Degenerate triangle detected".to_string(),
));
}
if xy.x < self.x_range.0
|| xy.x > self.x_range.1
|| xy.y < self.y_range.0
|| xy.y > self.y_range.1
{
return Err(InterpolationError::Linear(
"Point is outside the surface's range".to_string(),
));
}
let unique_coords = self
.points
.iter()
.map(|p| (p.x, p.y))
.collect::<BTreeSet<_>>();
if unique_coords.len() == 1 {
return Err(InterpolationError::Linear(
"Degenerate triangle detected".to_string(),
));
}
if let Some(point) = self.points.iter().find(|p| p.x == xy.x && p.y == xy.y) {
return Ok(*point);
}
if self.points.len() < 3 {
return Err(InterpolationError::Linear(
"Need at least three points for linear interpolation".to_string(),
));
}
let op = "Surface::linear_interpolate";
let mut nearest_points: Vec<&Point3D> = self.points.iter().collect();
sort_by_distance(&mut nearest_points, xy.x, xy.y, op)
.map_err(interp_err(InterpolationError::Linear))?;
let p1 = nth(&nearest_points, 0, InterpolationError::Linear)?;
let p2 = nth(&nearest_points, 1, InterpolationError::Linear)?;
let p3 = nth(&nearest_points, 2, InterpolationError::Linear)?;
let y23 = d_sub(p2.y, p3.y, op).map_err(interp_err(InterpolationError::Linear))?;
let x13 = d_sub(p1.x, p3.x, op).map_err(interp_err(InterpolationError::Linear))?;
let x32 = d_sub(p3.x, p2.x, op).map_err(interp_err(InterpolationError::Linear))?;
let y13 = d_sub(p1.y, p3.y, op).map_err(interp_err(InterpolationError::Linear))?;
let denominator = d_add(
d_mul(y23, x13, op).map_err(interp_err(InterpolationError::Linear))?,
d_mul(x32, y13, op).map_err(interp_err(InterpolationError::Linear))?,
op,
)
.map_err(interp_err(InterpolationError::Linear))?;
if denominator.is_zero() {
return Err(InterpolationError::Linear(
"Degenerate triangle detected: the three nearest points are collinear".to_string(),
));
}
let qx3 = d_sub(xy.x, p3.x, op).map_err(interp_err(InterpolationError::Linear))?;
let qy3 = d_sub(xy.y, p3.y, op).map_err(interp_err(InterpolationError::Linear))?;
let y31 = d_sub(p3.y, p1.y, op).map_err(interp_err(InterpolationError::Linear))?;
let w1_num = d_add(
d_mul(y23, qx3, op).map_err(interp_err(InterpolationError::Linear))?,
d_mul(x32, qy3, op).map_err(interp_err(InterpolationError::Linear))?,
op,
)
.map_err(interp_err(InterpolationError::Linear))?;
let w1 = d_div(w1_num, denominator, op).map_err(interp_err(InterpolationError::Linear))?;
let w2_num = d_add(
d_mul(y31, qx3, op).map_err(interp_err(InterpolationError::Linear))?,
d_mul(x13, qy3, op).map_err(interp_err(InterpolationError::Linear))?,
op,
)
.map_err(interp_err(InterpolationError::Linear))?;
let w2 = d_div(w2_num, denominator, op).map_err(interp_err(InterpolationError::Linear))?;
let w3 = d_sub(
d_sub(Decimal::ONE, w1, op).map_err(interp_err(InterpolationError::Linear))?,
w2,
op,
)
.map_err(interp_err(InterpolationError::Linear))?;
let z = d_sum_iter(
[
d_mul(w1, p1.z, op).map_err(interp_err(InterpolationError::Linear))?,
d_mul(w2, p2.z, op).map_err(interp_err(InterpolationError::Linear))?,
d_mul(w3, p3.z, op).map_err(interp_err(InterpolationError::Linear))?,
],
op,
)
.map_err(interp_err(InterpolationError::Linear))?;
Ok(Point3D::new(xy.x, xy.y, z))
}
}
impl BiLinearInterpolation<Point3D, Point2D> for Surface {
fn bilinear_interpolate(&self, xy: Point2D) -> Result<Point3D, InterpolationError> {
if self.points.len() < 4 {
return Err(InterpolationError::Bilinear(
"Need at least four points for bilinear interpolation".to_string(),
));
}
if xy.x < self.x_range.0
|| xy.x > self.x_range.1
|| xy.y < self.y_range.0
|| xy.y > self.y_range.1
{
return Err(InterpolationError::Bilinear(
"Point is outside the surface's range".to_string(),
));
}
let xy_points: Vec<&Point3D> = self
.points
.iter()
.filter(|p| p.x == xy.x && p.y == xy.y)
.collect();
if xy_points.len() == 4 {
let z_values: Vec<Decimal> = xy_points.iter().map(|p| p.z).collect();
let unique_z_values: Vec<Decimal> = z_values.clone();
if unique_z_values.len() > 1 {
return Err(InterpolationError::Bilinear(
"Invalid quadrilateral".to_string(),
));
}
}
if let Some(point) = self.points.iter().find(|p| p.x == xy.x && p.y == xy.y) {
return Ok(*point);
}
let op = "Surface::bilinear_interpolate";
let mut sorted_points: Vec<&Point3D> = self.points.iter().collect();
sort_by_distance(&mut sorted_points, xy.x, xy.y, op)
.map_err(interp_err(InterpolationError::Bilinear))?;
let closest_points = sorted_points.get(0..4).ok_or_else(|| {
InterpolationError::Bilinear(format!(
"need four neighbours, found {}",
sorted_points.len()
))
})?;
let mut quad_points: Vec<&Point3D> = closest_points.to_vec();
quad_points.sort_by(|a, b| {
let a_key = (a.y, a.x);
let b_key = (b.y, b.x);
a_key
.partial_cmp(&b_key)
.unwrap_or(std::cmp::Ordering::Equal)
});
let q11 = nth(&quad_points, 0, InterpolationError::Bilinear)?; let q12 = nth(&quad_points, 1, InterpolationError::Bilinear)?; let q21 = nth(&quad_points, 2, InterpolationError::Bilinear)?; let q22 = nth(&quad_points, 3, InterpolationError::Bilinear)?;
let x_span = d_sub(q12.x, q11.x, op).map_err(interp_err(InterpolationError::Bilinear))?;
let y_span = d_sub(q21.y, q11.y, op).map_err(interp_err(InterpolationError::Bilinear))?;
if x_span.is_zero() || y_span.is_zero() {
return Err(InterpolationError::DegenerateInterval);
}
let x_offset = d_sub(xy.x, q11.x, op).map_err(interp_err(InterpolationError::Bilinear))?;
let x_ratio =
d_div(x_offset, x_span, op).map_err(interp_err(InterpolationError::Bilinear))?;
let y_offset = d_sub(xy.y, q11.y, op).map_err(interp_err(InterpolationError::Bilinear))?;
let y_ratio =
d_div(y_offset, y_span, op).map_err(interp_err(InterpolationError::Bilinear))?;
let inv_x =
d_sub(Decimal::ONE, x_ratio, op).map_err(interp_err(InterpolationError::Bilinear))?;
let inv_y =
d_sub(Decimal::ONE, y_ratio, op).map_err(interp_err(InterpolationError::Bilinear))?;
let corner = |a: Decimal, b: Decimal, z: Decimal| -> Result<Decimal, InterpolationError> {
let weight = d_mul(a, b, op).map_err(interp_err(InterpolationError::Bilinear))?;
d_mul(weight, z, op).map_err(interp_err(InterpolationError::Bilinear))
};
let z = d_sum_iter(
[
corner(inv_x, inv_y, q11.z)?,
corner(x_ratio, inv_y, q12.z)?,
corner(inv_x, y_ratio, q21.z)?,
corner(x_ratio, y_ratio, q22.z)?,
],
op,
)
.map_err(interp_err(InterpolationError::Bilinear))?;
Ok(Point3D::new(xy.x, xy.y, z))
}
}
impl CubicInterpolation<Point3D, Point2D> for Surface {
fn cubic_interpolate(&self, xy: Point2D) -> Result<Point3D, InterpolationError> {
if self.points.len() < 9 {
return Err(InterpolationError::Cubic(
"Need at least nine points for cubic interpolation".to_string(),
));
}
if xy.x < self.x_range.0
|| xy.x > self.x_range.1
|| xy.y < self.y_range.0
|| xy.y > self.y_range.1
{
return Err(InterpolationError::Cubic(
"Point is outside the surface's range".to_string(),
));
}
if let Some(point) = self.points.iter().find(|p| p.x == xy.x && p.y == xy.y) {
return Ok(*point);
}
let op = "Surface::cubic_interpolate";
let mut sorted_points: Vec<&Point3D> = self.points.iter().collect();
sort_by_distance(&mut sorted_points, xy.x, xy.y, op)
.map_err(interp_err(InterpolationError::Cubic))?;
let closest_points = sorted_points.get(0..9).ok_or_else(|| {
InterpolationError::Cubic(format!(
"need nine neighbours, found {}",
sorted_points.len()
))
})?;
let mut weights: Vec<Decimal> = Vec::with_capacity(closest_points.len());
for &point in closest_points {
let sq = squared_distance(point, xy.x, xy.y, op)
.map_err(interp_err(InterpolationError::Cubic))?;
let dist = match sq.sqrt() {
Some(d) => d,
None => {
warn!(
"cubic_interpolate: sqrt failed for operand ({sq}); dropping point from weighting"
);
weights.push(Decimal::ZERO);
continue;
}
};
let shifted = d_add(dist, Decimal::new(1, 6), op)
.map_err(interp_err(InterpolationError::Cubic))?;
weights.push(
d_div(Decimal::ONE, shifted, op).map_err(interp_err(InterpolationError::Cubic))?,
);
}
let mut numerator_z = Decimal::ZERO;
let mut denominator = Decimal::ZERO;
for (&point, &weight) in closest_points.iter().zip(weights.iter()) {
let cubic_weight =
powu_checked(weight, 3, op).map_err(interp_err(InterpolationError::Cubic))?;
let contribution =
d_mul(point.z, cubic_weight, op).map_err(interp_err(InterpolationError::Cubic))?;
numerator_z = d_add(numerator_z, contribution, op)
.map_err(interp_err(InterpolationError::Cubic))?;
denominator = d_add(denominator, cubic_weight, op)
.map_err(interp_err(InterpolationError::Cubic))?;
}
let interpolated_z = if denominator != Decimal::ZERO {
d_div(numerator_z, denominator, op).map_err(interp_err(InterpolationError::Cubic))?
} else {
let zs: Vec<Decimal> = closest_points.iter().map(|p| p.z).collect();
let sum = d_sum_iter(zs.iter().copied(), op)
.map_err(interp_err(InterpolationError::Cubic))?;
d_div(sum, Decimal::from(closest_points.len()), op)
.map_err(interp_err(InterpolationError::Cubic))?
};
Ok(Point3D::new(xy.x, xy.y, interpolated_z))
}
}
impl SplineInterpolation<Point3D, Point2D> for Surface {
fn spline_interpolate(&self, xy: Point2D) -> Result<Point3D, InterpolationError> {
if self.points.len() < 9 {
return Err(InterpolationError::Spline(
"Need at least nine points for spline interpolation".to_string(),
));
}
if xy.x < self.x_range.0
|| xy.x > self.x_range.1
|| xy.y < self.y_range.0
|| xy.y > self.y_range.1
{
return Err(InterpolationError::Spline(
"Point is outside the surface's range".to_string(),
));
}
if let Some(point) = self.points.iter().find(|p| p.x == xy.x && p.y == xy.y) {
return Ok(*point);
}
let mut sorted_points: Vec<&Point3D> = self.points.iter().collect();
sorted_points.sort_by(|a, b| {
let a_key = (a.x, a.y);
let b_key = (b.x, b.y);
a_key
.partial_cmp(&b_key)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut x_groups: std::collections::HashMap<Decimal, Vec<&Point3D>> =
std::collections::HashMap::new();
let mut y_groups: std::collections::HashMap<Decimal, Vec<&Point3D>> =
std::collections::HashMap::new();
for &point in &sorted_points {
x_groups.entry(point.x).or_default().push(point);
y_groups.entry(point.y).or_default().push(point);
}
let y_values: Vec<Decimal> = y_groups.keys().cloned().collect();
let mut interpolated_x_points: Vec<Point3D> = Vec::new();
for &y in &y_values {
let y_points: Vec<&Point3D> = sorted_points
.iter()
.filter(|&&p| p.y == y)
.cloned()
.collect();
if y_points.len() < 2 {
continue;
}
let x_interpolated =
self.one_dimensional_spline_interpolation(&y_points, xy.x, |p| p.x, |p| p.z);
if let Ok(z) = x_interpolated {
interpolated_x_points.push(Point3D::new(xy.x, y, z));
}
}
if interpolated_x_points.is_empty() {
return Err(InterpolationError::Spline(
"Could not interpolate along x-axis".to_string(),
));
}
let y_interpolated = self.one_dimensional_spline_interpolation(
&interpolated_x_points,
xy.y,
|p| p.y,
|p| p.z,
);
y_interpolated.map(|z| Point3D::new(xy.x, xy.y, z))
}
}
impl Len for Surface {
fn len(&self) -> usize {
self.points.len()
}
fn is_empty(&self) -> bool {
self.points.is_empty()
}
}
impl MetricsExtractor for Surface {
fn compute_basic_metrics(&self) -> Result<BasicMetrics, MetricsError> {
let z_values: Vec<Decimal> = self.points.iter().map(|p| p.z).collect();
if z_values.is_empty() {
return Ok(BasicMetrics {
mean: Decimal::ZERO,
median: Decimal::ZERO,
mode: Decimal::ZERO,
std_dev: Decimal::ZERO,
});
}
let op = "Surface::compute_basic_metrics";
let mean = mean_of(&z_values, op).map_err(|e| MetricsError::BasicError(e.to_string()))?;
let mut sorted = z_values.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median = sample_at(&sorted, sorted.len() / 2, "median")?;
let mode = {
let mut freq_map = std::collections::HashMap::new();
for &val in &z_values {
*freq_map.entry(val).or_insert(0) += 1;
}
freq_map
.into_iter()
.max_by_key(|&(_, count)| count)
.map(|(val, _)| val)
.unwrap_or(Decimal::ZERO)
};
let sum_sq = central_moment(&z_values, mean, 2, op)
.map_err(|e| MetricsError::BasicError(e.to_string()))?;
let variance = d_div(sum_sq, Decimal::from(z_values.len()), op)
.map_err(|e| MetricsError::BasicError(e.to_string()))?;
let std_dev = variance.sqrt().unwrap_or(Decimal::ZERO);
Ok(BasicMetrics {
mean,
median,
mode,
std_dev,
})
}
fn compute_shape_metrics(&self) -> Result<ShapeMetrics, MetricsError> {
let z_values: Vec<Decimal> = self.points.iter().map(|p| p.z).collect();
if z_values.len() < 2 {
return Ok(ShapeMetrics {
skewness: Decimal::ZERO,
kurtosis: Decimal::ZERO,
peaks: vec![],
valleys: vec![],
inflection_points: vec![],
});
}
let op = "Surface::compute_shape_metrics";
let mean = mean_of(&z_values, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?;
let sum_sq = central_moment(&z_values, mean, 2, op)
.map_err(|e| MetricsError::ShapeError(e.to_string()))?;
let variance = d_div(sum_sq, Decimal::from(z_values.len()), op)
.map_err(|e| MetricsError::ShapeError(e.to_string()))?;
let std_dev = variance.sqrt().unwrap_or(Decimal::ONE);
if std_dev.is_zero() {
return Err(MetricsError::ShapeError(format!(
"standard deviation ({std_dev}) is too small to compute skewness/kurtosis; the surface is degenerate"
)));
}
let n = Decimal::from(z_values.len());
let skew_num = central_moment(&z_values, mean, 3, op)
.map_err(|e| MetricsError::ShapeError(e.to_string()))?;
let skew_den = d_mul(
n,
powu_checked(std_dev, 3, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?,
op,
)
.map_err(|e| MetricsError::ShapeError(e.to_string()))?;
let skewness =
d_div(skew_num, skew_den, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?;
let kurt_num = central_moment(&z_values, mean, 4, op)
.map_err(|e| MetricsError::ShapeError(e.to_string()))?;
let kurt_den = d_mul(
n,
powu_checked(std_dev, 4, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?,
op,
)
.map_err(|e| MetricsError::ShapeError(e.to_string()))?;
let kurtosis =
d_div(kurt_num, kurt_den, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?;
Ok(ShapeMetrics {
skewness,
kurtosis,
peaks: vec![],
valleys: vec![],
inflection_points: vec![],
})
}
fn compute_range_metrics(&self) -> Result<RangeMetrics, MetricsError> {
let z_values: Vec<Decimal> = self.points.iter().map(|p| p.z).collect();
if z_values.is_empty() {
return Ok(RangeMetrics {
min: Point2D::new(Decimal::ZERO, Decimal::ZERO),
max: Point2D::new(Decimal::ZERO, Decimal::ZERO),
range: Decimal::ZERO,
quartiles: (Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
interquartile_range: Decimal::ZERO,
});
}
let mut sorted = z_values.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let min = sorted.first().copied().unwrap_or(Decimal::ZERO);
let max = sorted.last().copied().unwrap_or(Decimal::ZERO);
let op = "Surface::compute_range_metrics";
let len = sorted.len();
let q1 = sample_at(&sorted, len / 4, "first quartile")?;
let q2 = sample_at(&sorted, len / 2, "median")?;
let q3 = sample_at(&sorted, 3 * len / 4, "third quartile")?;
let range = d_sub(max, min, op).map_err(|e| MetricsError::RangeError(e.to_string()))?;
let iqr = d_sub(q3, q1, op).map_err(|e| MetricsError::RangeError(e.to_string()))?;
Ok(RangeMetrics {
min: Point2D::new(Decimal::ZERO, min),
max: Point2D::new(Decimal::ZERO, max),
range,
quartiles: (q1, q2, q3),
interquartile_range: iqr,
})
}
fn compute_trend_metrics(&self) -> Result<TrendMetrics, MetricsError> {
let points: Vec<Point2D> = self.points.iter().map(|p| Point2D::new(p.x, p.z)).collect();
if points.len() < 2 {
return Ok(TrendMetrics {
slope: Decimal::ZERO,
intercept: Decimal::ZERO,
r_squared: Decimal::ONE,
moving_average: vec![],
});
}
let n = Decimal::from(points.len());
let x_vals: Vec<Decimal> = points.iter().map(|p| p.x).collect();
let z_vals: Vec<Decimal> = points.iter().map(|p| p.y).collect();
let op = "Surface::compute_trend_metrics";
let sum_x = d_sum_iter(x_vals.iter().copied(), op).map_err(trend_err)?;
let sum_z = d_sum_iter(z_vals.iter().copied(), op).map_err(trend_err)?;
let first_z = sample_at(&z_vals, 0, "trend baseline")?;
let is_identical_points = z_vals.iter().all(|&z| z == first_z);
let (slope, intercept, r_squared) = if is_identical_points {
(Decimal::ZERO, first_z, Decimal::ONE)
} else {
let regression = || -> Result<(Decimal, Decimal, Decimal), DecimalError> {
let mut sum_xz = Decimal::ZERO;
let mut sum_xx = Decimal::ZERO;
for (x, z) in x_vals.iter().zip(&z_vals) {
sum_xz = d_add(sum_xz, d_mul(*x, *z, op)?, op)?;
sum_xx = d_add(sum_xx, d_mul(*x, *x, op)?, op)?;
}
let numerator = d_sub(d_mul(n, sum_xz, op)?, d_mul(sum_x, sum_z, op)?, op)?;
let denominator = d_sub(d_mul(n, sum_xx, op)?, d_mul(sum_x, sum_x, op)?, op)?;
let slope = d_div(numerator, denominator, op)?;
let intercept = d_div(d_sub(sum_z, d_mul(slope, sum_x, op)?, op)?, n, op)?;
let mean_z = d_div(sum_z, n, op)?;
let sst = central_moment(&z_vals, mean_z, 2, op)?;
let mut ssr = Decimal::ZERO;
for (z, x) in z_vals.iter().zip(&x_vals) {
let z_predicted = d_add(d_mul(slope, *x, op)?, intercept, op)?;
let residual = d_sub(*z, z_predicted, op)?;
ssr = d_add(ssr, powu_checked(residual, 2, op)?, op)?;
}
let r_squared = if sst == Decimal::ZERO {
Decimal::ONE
} else {
d_sub(Decimal::ONE, d_div(ssr, sst, op)?, op)?
};
Ok((slope, intercept, r_squared))
};
regression().map_err(trend_err)?
};
let window_sizes = [3, 5, 7];
let mut moving_average: Vec<Point2D> = Vec::new();
for window in window_sizes {
if window > points.len() {
continue;
}
for window_points in points.windows(window) {
let xs: Vec<Decimal> = window_points.iter().map(|p| p.x).collect();
let ys: Vec<Decimal> = window_points.iter().map(|p| p.y).collect();
let avg_x = mean_of(&xs, op).map_err(trend_err)?;
let avg_y = mean_of(&ys, op).map_err(trend_err)?;
moving_average.push(Point2D::new(avg_x, avg_y));
}
}
Ok(TrendMetrics {
slope,
intercept,
r_squared,
moving_average,
})
}
fn compute_risk_metrics(&self) -> Result<RiskMetrics, MetricsError> {
let z_values: Vec<Decimal> = self.points.iter().map(|p| p.z).collect();
if z_values.is_empty() {
return Ok(RiskMetrics {
volatility: Decimal::ZERO,
value_at_risk: Decimal::ZERO,
expected_shortfall: Decimal::ZERO,
beta: Decimal::ZERO,
sharpe_ratio: Decimal::ZERO,
});
}
let op = "Surface::compute_risk_metrics";
let mean = mean_of(&z_values, op).map_err(risk_err)?;
let sum_sq = central_moment(&z_values, mean, 2, op).map_err(risk_err)?;
let variance = d_div(sum_sq, Decimal::from(z_values.len()), op).map_err(risk_err)?;
let volatility = variance.sqrt().unwrap_or(Decimal::ZERO);
let z_score = dec!(1.645); let scaled_vol = d_mul(z_score, volatility, op).map_err(risk_err)?;
let var = d_sub(mean, scaled_vol, op).map_err(risk_err)?;
let tail: Vec<Decimal> = z_values.iter().copied().filter(|&x| x < var).collect();
let expected_shortfall = if tail.is_empty() {
Decimal::ZERO
} else {
mean_of(&tail, op).map_err(risk_err)?
};
let beta = Decimal::ZERO;
let sharpe_ratio = if volatility.is_zero() {
Decimal::ZERO
} else {
d_div(mean, volatility, op).map_err(risk_err)?
};
Ok(RiskMetrics {
volatility,
value_at_risk: var,
expected_shortfall,
beta,
sharpe_ratio,
})
}
}
impl Arithmetic<Surface> for Surface {
type Error = SurfaceError;
fn merge(surfaces: &[&Surface], operation: MergeOperation) -> Result<Surface, Self::Error> {
if surfaces.is_empty() {
return Err(SurfaceError::invalid_parameters(
"merge_surfaces",
"No surfaces provided for merging",
));
}
if let [only] = surfaces {
return Ok((*only).clone());
}
let min_x = surfaces
.iter()
.map(|s| s.x_range.0)
.max()
.unwrap_or(Decimal::ZERO);
let max_x = surfaces
.iter()
.map(|s| s.x_range.1)
.min()
.unwrap_or(Decimal::ZERO);
let min_y = surfaces
.iter()
.map(|s| s.y_range.0)
.max()
.unwrap_or(Decimal::ZERO);
let max_y = surfaces
.iter()
.map(|s| s.y_range.1)
.min()
.unwrap_or(Decimal::ZERO);
if min_x >= max_x || min_y >= max_y {
return Err(SurfaceError::invalid_parameters(
"merge_surfaces",
"Surfaces have incompatible ranges",
));
}
let steps = 50;
let op = "Surface::merge";
let x_span = d_sub(max_x, min_x, op).map_err(construction_err)?;
let x_step = d_div(x_span, Decimal::from(steps), op).map_err(construction_err)?;
let y_span = d_sub(max_y, min_y, op).map_err(construction_err)?;
let y_step = d_div(y_span, Decimal::from(steps), op).map_err(construction_err)?;
let result_points: Result<Vec<Point3D>, SurfaceError> = (0..=steps)
.into_par_iter()
.flat_map(|i| {
(0..=steps).into_par_iter().map(move |j| {
let x_offset = d_mul(x_step, Decimal::from(i), op).map_err(construction_err)?;
let x = d_add(min_x, x_offset, op).map_err(construction_err)?;
let y_offset = d_mul(y_step, Decimal::from(j), op).map_err(construction_err)?;
let y = d_add(min_y, y_offset, op).map_err(construction_err)?;
let point = Point2D::new(x, y);
let z_values: Result<Vec<Decimal>, SurfaceError> = surfaces
.iter()
.map(|surface| {
surface
.interpolate(point, InterpolationType::Cubic)
.map(|point3d| point3d.z)
.map_err(SurfaceError::from)
})
.collect();
let z_values = z_values?;
let result_z = match operation {
MergeOperation::Add => {
d_sum_iter(z_values.iter().copied(), op).map_err(construction_err)?
}
MergeOperation::Subtract => {
let first = z_values.first().cloned().unwrap_or(Decimal::ZERO);
let remaining_sum = d_sum_iter(z_values.iter().skip(1).copied(), op)
.map_err(construction_err)?;
d_sub(first, remaining_sum, op).map_err(construction_err)?
}
MergeOperation::Multiply => d_product_iter(z_values.iter().copied(), op)
.map_err(construction_err)?,
MergeOperation::Divide => {
let mut divisors = z_values.iter().copied();
let first = divisors.next().unwrap_or(Decimal::ONE);
divisors.try_fold(first, |acc, val| {
if val == Decimal::ZERO {
Ok(acc)
} else {
d_div(acc, val, op).map_err(construction_err)
}
})?
}
MergeOperation::Max => z_values
.par_iter()
.cloned()
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(Decimal::ZERO),
MergeOperation::Min => z_values
.par_iter()
.cloned()
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(Decimal::ZERO),
};
Ok(Point3D::new(x, y, result_z))
})
})
.collect();
let result_points = result_points?;
Ok(Surface::from_vector(result_points))
}
fn merge_with(
&self,
other: &Surface,
operation: MergeOperation,
) -> Result<Surface, Self::Error> {
Self::merge(&[self, other], operation)
}
}
impl AxisOperations<Point3D, Point2D> for Surface {
type Error = SurfaceError;
fn contains_point(&self, x: &Point2D) -> bool {
self.points.iter().any(|p| p.x == x.x && p.y == x.y)
}
fn get_index_values(&self) -> Vec<Point2D> {
self.points.iter().map(|p| Point2D::new(p.x, p.y)).collect()
}
fn get_values(&self, x: Point2D) -> Vec<&Decimal> {
self.points
.iter()
.filter(|p| p.x == x.x && p.y == x.y)
.map(|p| &p.z)
.collect()
}
fn get_closest_point(&self, x: &Point2D) -> Result<&Point3D, Self::Error> {
let mut closest: Option<(&Point3D, Decimal)> = None;
for point in &self.points {
let squared = squared_distance(point, x.x, x.y, "Surface::get_closest_point")
.map_err(analysis_err)?;
match closest {
Some((_, best)) if best <= squared => {}
_ => closest = Some((point, squared)),
}
}
closest
.map(|(point, _)| point)
.ok_or(SurfaceError::Point3DError {
reason: "No points found",
})
}
fn get_point(&self, x: &Point2D) -> Option<&Point3D> {
self.points.iter().find(|p| p.x == x.x && p.y == x.y)
}
}
impl MergeAxisInterpolate<Point3D, Point2D> for Surface
where
Self: Sized,
{
fn merge_axis_interpolate(
&self,
other: &Self,
interpolation: InterpolationType,
) -> Result<(Self, Self), Self::Error> {
let merged_xy_values = self.merge_axis_index(other);
let mut interpolated_self_points = BTreeSet::new();
let mut interpolated_other_points = BTreeSet::new();
for xy in &merged_xy_values {
if self.contains_point(xy) {
let pt = self
.points
.iter()
.find(|p| p.x == xy.x && p.y == xy.y)
.ok_or_else(|| {
SurfaceError::AnalysisError(format!(
"merge_axis_interpolate: missing self point at ({},{}) despite contains_point()",
xy.x, xy.y
))
})?;
interpolated_self_points.insert(*pt);
} else {
let interpolated_point = self.interpolate(*xy, interpolation)?;
interpolated_self_points.insert(interpolated_point);
}
if other.contains_point(xy) {
let pt = other
.points
.iter()
.find(|p| p.x == xy.x && p.y == xy.y)
.ok_or_else(|| {
SurfaceError::AnalysisError(format!(
"merge_axis_interpolate: missing other point at ({},{}) despite contains_point()",
xy.x, xy.y
))
})?;
interpolated_other_points.insert(*pt);
} else {
let interpolated_point = other.interpolate(*xy, interpolation)?;
interpolated_other_points.insert(interpolated_point);
}
}
Ok((
Surface::new(interpolated_self_points),
Surface::new(interpolated_other_points),
))
}
}
impl GeometricTransformations<Point3D> for Surface {
type Error = SurfaceError;
fn translate(&self, deltas: Vec<&Decimal>) -> Result<Self, Self::Error> {
if deltas.len() != 3 {
return Err(SurfaceError::invalid_parameters(
"translate",
"Expected 3 deltas for 3D translation",
));
}
let (Some(dx), Some(dy), Some(dz)) = (deltas.first(), deltas.get(1), deltas.get(2)) else {
return Err(SurfaceError::invalid_parameters(
"translate",
"Expected 3 deltas for 3D translation",
));
};
let translated_points = self
.points
.iter()
.map(|point| {
let x = d_add(point.x, **dx, "Surface::translate::x")?;
let y = d_add(point.y, **dy, "Surface::translate::y")?;
let z = d_add(point.z, **dz, "Surface::translate::z")?;
Ok(Point3D::new(x, y, z))
})
.collect::<Result<BTreeSet<Point3D>, DecimalError>>()
.map_err(construction_err)?;
Ok(Surface::new(translated_points))
}
fn scale(&self, factors: Vec<&Decimal>) -> Result<Self, Self::Error> {
if factors.len() != 3 {
return Err(SurfaceError::invalid_parameters(
"scale",
"Expected 3 factors for 3D scaling",
));
}
let (Some(fx), Some(fy), Some(fz)) = (factors.first(), factors.get(1), factors.get(2))
else {
return Err(SurfaceError::invalid_parameters(
"scale",
"Expected 3 factors for 3D scaling",
));
};
let scaled_points = self
.points
.iter()
.map(|point| {
let x = d_mul(point.x, **fx, "Surface::scale::x")?;
let y = d_mul(point.y, **fy, "Surface::scale::y")?;
let z = d_mul(point.z, **fz, "Surface::scale::z")?;
Ok(Point3D::new(x, y, z))
})
.collect::<Result<BTreeSet<Point3D>, DecimalError>>()
.map_err(construction_err)?;
Ok(Surface::new(scaled_points))
}
fn intersect_with(&self, other: &Self) -> Result<Vec<Point3D>, Self::Error> {
let mut intersections = Vec::new();
let epsilon = Decimal::new(1, 6); let op = "Surface::intersect_with";
for p1 in self.points.iter() {
for p2 in other.points.iter() {
let dx = d_sub(p1.x, p2.x, op).map_err(analysis_err)?.abs();
if dx >= epsilon {
continue;
}
let dy = d_sub(p1.y, p2.y, op).map_err(analysis_err)?.abs();
if dy >= epsilon {
continue;
}
let dz = d_sub(p1.z, p2.z, op).map_err(analysis_err)?.abs();
if dz < epsilon {
intersections.push(*p1);
}
}
}
Ok(intersections)
}
fn derivative_at(&self, point: &Point3D) -> Result<Vec<Decimal>, Self::Error> {
if self.points.len() < 2 {
return Err(SurfaceError::invalid_parameters(
"derivative_at",
"Surface needs at least 2 points for derivative calculation",
));
}
let op = "Surface::derivative_at";
if self.points.len() <= 3 {
let p0 = self.point_at(0)?;
let p1 = self.point_at(1)?;
if p0 == p1 {
return Err(SurfaceError::invalid_parameters(
"derivative_at",
"Points are identical, cannot calculate derivatives",
));
}
let rise = d_sub(p1.z, p0.z, op).map_err(analysis_err)?;
let run_x = d_sub(p1.x, p0.x, op).map_err(analysis_err)?;
let dx = if run_x == Decimal::ZERO {
Decimal::MAX
} else {
d_div(rise, run_x, op).map_err(analysis_err)?
};
let run_y = d_sub(p1.y, p0.y, op).map_err(analysis_err)?;
let dy = if run_y == Decimal::ZERO {
Decimal::MAX
} else {
d_div(rise, run_y, op).map_err(analysis_err)?
};
return Ok(vec![dx, dy]);
}
if !(self.x_range.0..=self.x_range.1).contains(&point.x)
|| !(self.y_range.0..=self.y_range.1).contains(&point.y)
{
return Err(SurfaceError::invalid_parameters(
"derivative_at",
"Point is outside the surface's range",
));
}
let tolerance = dec!(0.5);
let mut x_points: BTreeSet<Point3D> = BTreeSet::new();
let mut y_points: BTreeSet<Point3D> = BTreeSet::new();
for candidate in self.get_points() {
if d_sub(candidate.x, point.x, op).map_err(analysis_err)?.abs() < tolerance {
x_points.insert(*candidate);
}
if d_sub(candidate.y, point.y, op).map_err(analysis_err)?.abs() < tolerance {
y_points.insert(*candidate);
}
}
let x_candidates = if x_points.len() < 2 {
&self.points
} else {
&x_points
};
let y_candidates = if y_points.len() < 2 {
&self.points
} else {
&y_points
};
if x_candidates.len() < 2 || y_candidates.len() < 2 {
return Err(SurfaceError::invalid_parameters(
"derivative_at",
"Could not find suitable points for derivative calculation",
));
}
let mut x_sorted: Vec<_> = x_candidates.iter().collect();
x_sorted.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
let mut y_sorted: Vec<_> = y_candidates.iter().collect();
y_sorted.sort_by(|a, b| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal));
let missing = || {
SurfaceError::AnalysisError(
"derivative_at: fewer than two candidates after filtering".to_string(),
)
};
let (Some(x0), Some(x1)) = (x_sorted.first(), x_sorted.get(1)) else {
return Err(missing());
};
let (Some(y0), Some(y1)) = (y_sorted.first(), y_sorted.get(1)) else {
return Err(missing());
};
let dx = if x0.x == x1.x {
Decimal::ZERO
} else {
let rise = d_sub(x1.z, x0.z, op).map_err(analysis_err)?;
let run = d_sub(x1.x, x0.x, op).map_err(analysis_err)?;
d_div(rise, run, op).map_err(analysis_err)?
};
let dy = if y0.y == y1.y {
Decimal::ZERO
} else {
let rise = d_sub(y1.z, y0.z, op).map_err(analysis_err)?;
let run = d_sub(y1.y, y0.y, op).map_err(analysis_err)?;
d_div(rise, run, op).map_err(analysis_err)?
};
Ok(vec![dx, dy])
}
fn extrema(&self) -> Result<(Point3D, Point3D), Self::Error> {
if self.points.is_empty() {
return Err(SurfaceError::invalid_parameters(
"extrema",
"Surface has no points",
));
}
let min_point = self
.points
.iter()
.min_by(|a, b| a.z.partial_cmp(&b.z).unwrap_or(std::cmp::Ordering::Equal))
.cloned()
.ok_or_else(|| {
SurfaceError::AnalysisError("extrema: empty point set in min_by".to_string())
})?;
let max_point = self
.points
.iter()
.max_by(|a, b| a.z.partial_cmp(&b.z).unwrap_or(std::cmp::Ordering::Equal))
.cloned()
.ok_or_else(|| {
SurfaceError::AnalysisError("extrema: empty point set in max_by".to_string())
})?;
Ok((min_point, max_point))
}
fn measure_under(&self, base_value: &Decimal) -> Result<Decimal, Self::Error> {
if self.points.len() < 3 {
return Ok(Decimal::ZERO);
}
let mut volume = Decimal::ZERO;
let points: Vec<_> = self.points.iter().collect();
let op = "Surface::measure_under";
for window in points.windows(3) {
let (Some(p1), Some(p2), Some(p3)) = (window.first(), window.get(1), window.get(2))
else {
return Err(SurfaceError::AnalysisError(
"measure_under: triangle window is shorter than three points".to_string(),
));
};
let cross_a = d_mul(
d_sub(p2.x, p1.x, op).map_err(analysis_err)?,
d_sub(p3.y, p1.y, op).map_err(analysis_err)?,
op,
)
.map_err(analysis_err)?;
let cross_b = d_mul(
d_sub(p3.x, p1.x, op).map_err(analysis_err)?,
d_sub(p2.y, p1.y, op).map_err(analysis_err)?,
op,
)
.map_err(analysis_err)?;
let cross = d_sub(cross_a, cross_b, op).map_err(analysis_err)?;
let area = d_div(cross.abs(), dec!(2), op).map_err(analysis_err)?;
let heights = d_sum_iter(
[
d_sub(p1.z, *base_value, op).map_err(analysis_err)?,
d_sub(p2.z, *base_value, op).map_err(analysis_err)?,
d_sub(p3.z, *base_value, op).map_err(analysis_err)?,
],
op,
)
.map_err(analysis_err)?;
let avg_height = d_div(heights, dec!(3), op).map_err(analysis_err)?;
let prism = d_mul(area, avg_height, op).map_err(analysis_err)?;
volume = d_add(volume, prism, op).map_err(analysis_err)?;
}
Ok(volume.abs())
}
}
#[cfg(test)]
mod tests_surface_basic {
use super::*;
use rust_decimal_macros::dec;
fn create_test_points() -> BTreeSet<Point3D> {
BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
Point3D::new(dec!(0.5), dec!(0.5), dec!(1.5)),
])
}
#[test]
fn test_surface_new() {
let points = create_test_points();
let surface = Surface::new(points.clone());
assert_eq!(surface.points, points);
assert_eq!(surface.x_range.0, dec!(0.0));
assert_eq!(surface.x_range.1, dec!(1.0));
assert_eq!(surface.y_range.0, dec!(0.0));
assert_eq!(surface.y_range.1, dec!(1.0));
}
#[test]
fn test_project_onto_x_axis_keeps_every_projected_point() {
let points = create_test_points();
let surface = Surface::new(points);
let projection = surface.project_onto(Axis::X);
assert_eq!(
projection,
vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(0.0), dec!(1.0)),
Point2D::new(dec!(0.5), dec!(1.5)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(1.0), dec!(2.0)),
]
);
assert_eq!(projection.len(), surface.points.len());
let points = surface.get_f64_points();
assert_eq!(points.len(), 5);
assert_eq!(points[0].0, 0.0);
assert_eq!(points[0].1, 0.0);
assert_eq!(points[0].2, 0.0);
let default = Surface::default();
assert_eq!(default.points.len(), 0);
assert_eq!(default.x_range, (Decimal::ZERO, Decimal::ZERO));
assert_eq!(default.y_range, (Decimal::ZERO, Decimal::ZERO));
let graph_data = surface.graph_data();
assert!(matches!(
graph_data,
GraphData::GraphSurface(Surface3D { .. })
));
}
#[test]
fn test_project_onto_y_axis_keeps_every_projected_point() {
let points = create_test_points();
let surface = Surface::new(points);
let projection = surface.project_onto(Axis::Y);
assert_eq!(
projection,
vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(0.0), dec!(1.0)),
Point2D::new(dec!(0.5), dec!(1.5)),
Point2D::new(dec!(1.0), dec!(1.0)),
Point2D::new(dec!(1.0), dec!(2.0)),
]
);
assert_eq!(projection.len(), surface.points.len());
}
#[test]
fn test_project_onto_z_axis_keeps_every_projected_point() {
let points = create_test_points();
let surface = Surface::new(points);
let projection = surface.project_onto(Axis::Z);
assert_eq!(
projection,
vec![
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(0.0), dec!(1.0)),
Point2D::new(dec!(0.5), dec!(0.5)),
Point2D::new(dec!(1.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
]
);
assert_eq!(projection.len(), surface.points.len());
}
#[test]
fn test_project_onto_coincident_images_keeps_both_points() {
let surface = Surface::new(BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(7.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(9.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(7.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(9.0)),
]));
let projection = surface.project_onto(Axis::X);
assert_eq!(projection.len(), 4);
assert_eq!(
projection,
vec![
Point2D::new(dec!(0.0), dec!(7.0)),
Point2D::new(dec!(0.0), dec!(7.0)),
Point2D::new(dec!(1.0), dec!(9.0)),
Point2D::new(dec!(1.0), dec!(9.0)),
]
);
let as_set: BTreeSet<Point2D> = projection.into_iter().collect();
assert_eq!(as_set.len(), 2);
}
#[test]
fn test_one_dimensional_spline_interpolation_basic() {
let surface = Surface::new(create_test_points());
let test_points = vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(0.5), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(2.0)),
];
let test_cases = vec![
(dec!(0.25), dec!(0.5)), (dec!(0.0), dec!(0.0)), (dec!(1.0), dec!(2.0)), (dec!(0.75), dec!(1.5)), ];
for (target, expected) in test_cases {
let result = surface
.one_dimensional_spline_interpolation(&test_points, target, |p| p.x, |p| p.z)
.unwrap();
assert!(
(result - expected).abs() < dec!(0.1),
"Failed for target {target}, expected {expected}, got {result}"
);
}
}
#[test]
fn test_one_dimensional_spline_interpolation_insufficient_points() {
let surface = Surface::new(create_test_points());
let test_points = vec![Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0))];
let result =
surface.one_dimensional_spline_interpolation(&test_points, dec!(0.5), |p| p.x, |p| p.z);
assert!(matches!(
result,
Err(InterpolationError::Spline(msg)) if msg.contains("Insufficient points")
));
}
#[test]
fn test_one_dimensional_spline_interpolation_out_of_range() {
let surface = Surface::new(create_test_points());
let test_points = vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(2.0)),
];
let out_of_range_cases = vec![
(dec!(-0.5), dec!(0.0)), (dec!(1.5), dec!(2.0)), ];
for (target, expected) in out_of_range_cases {
let result = surface
.one_dimensional_spline_interpolation(&test_points, target, |p| p.x, |p| p.z)
.unwrap();
assert_eq!(result, expected, "Failed for out-of-range target {target}");
}
}
}
#[cfg(test)]
mod tests_surface_geometric_object {
use super::*;
use crate::geometrics::ResultPoint;
use rust_decimal_macros::dec;
fn create_test_points() -> Vec<Point3D> {
vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
]
}
#[test]
fn test_get_points() {
let points = create_test_points();
let surface = Surface::from_vector(points.clone());
let retrieved_points: Vec<&Point3D> = surface.get_points().into_iter().collect();
assert_eq!(retrieved_points.len(), points.len());
for point in &points {
assert!(retrieved_points.contains(&point));
}
}
#[test]
fn test_from_vector() {
let points = create_test_points();
let surface = Surface::from_vector(points.clone());
assert_eq!(surface.points.len(), points.len());
assert_eq!(surface.x_range.0, dec!(0.0));
assert_eq!(surface.x_range.1, dec!(1.0));
assert_eq!(surface.y_range.0, dec!(0.0));
assert_eq!(surface.y_range.1, dec!(1.0));
}
#[test]
fn test_construct_from_data() {
let points = BTreeSet::from_iter(create_test_points());
let result = Surface::construct(ConstructionMethod::FromData { points });
assert!(result.is_ok());
let surface = result.unwrap();
assert_eq!(surface.points.len(), 4);
}
#[test]
fn test_construct_from_data_empty() {
let points: BTreeSet<Point3D> = BTreeSet::new();
let result = Surface::construct(ConstructionMethod::FromData { points });
assert!(matches!(
result,
Err(SurfaceError::Point3DError { reason: _ })
));
}
#[test]
fn test_construct_parametric() {
let parametric_func: Box<dyn Fn(Point2D) -> ResultPoint<Point3D> + Send + Sync> =
Box::new(move |t: Point2D| -> ResultPoint<Point3D> {
Ok(Point3D::new(
t.x,
t.y,
t.x * t.y, ))
});
let params = ConstructionParams::D3 {
x_start: dec!(0.0),
x_end: dec!(1.0),
y_start: dec!(0.0),
y_end: dec!(1.0),
x_steps: 2,
y_steps: 2,
};
let result = Surface::construct(ConstructionMethod::Parametric {
f: parametric_func,
params,
});
assert!(result.is_ok());
let surface = result.unwrap();
assert_eq!(surface.points.len(), 9); }
#[test]
fn test_construct_parametric_invalid_params() {
let parametric_func: Box<dyn Fn(Point2D) -> ResultPoint<Point3D> + Send + Sync> =
Box::new(move |_: Point2D| -> ResultPoint<Point3D> {
Ok(Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)))
});
let params = ConstructionParams::D2 {
t_start: Decimal::ZERO,
t_end: Decimal::ONE,
steps: 2,
};
let result = Surface::construct(ConstructionMethod::Parametric {
f: parametric_func,
params,
});
assert!(matches!(result, Err(SurfaceError::ConstructionError(_))));
}
#[test]
fn test_construct_parametric_error_handling() {
let parametric_func: Box<dyn Fn(Point2D) -> ResultPoint<Point3D> + Send + Sync> =
Box::new(move |t: Point2D| -> ResultPoint<Point3D> {
if t.x > dec!(0.5) && t.y > dec!(0.5) {
Err(crate::error::ChainError::invalid_parameters(
"parametric_f",
"Test error",
))
} else {
Ok(Point3D::new(t.x, t.y, t.x * t.y))
}
});
let params = ConstructionParams::D3 {
x_start: dec!(0.0),
x_end: dec!(1.0),
y_start: dec!(0.0),
y_end: dec!(1.0),
x_steps: 2,
y_steps: 2,
};
let result = Surface::construct(ConstructionMethod::Parametric {
f: parametric_func,
params,
});
assert!(matches!(result, Err(SurfaceError::ConstructionError(_))));
}
#[test]
fn test_range_calculation() {
let points = create_test_points();
let surface = Surface::from_vector(points);
assert_eq!(surface.x_range.0, dec!(0.0));
assert_eq!(surface.x_range.1, dec!(1.0));
assert_eq!(surface.y_range.0, dec!(0.0));
assert_eq!(surface.y_range.1, dec!(1.0));
}
}
#[cfg(test)]
mod tests_surface_linear_interpolation {
use super::*;
use rust_decimal_macros::dec;
fn create_test_surface() -> Surface {
let points = BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
]);
Surface::new(points)
}
#[test]
fn test_point_out_of_range() {
let surface = create_test_surface();
let result = surface.linear_interpolate(Point2D::new(dec!(-1.0), dec!(0.5)));
assert!(matches!(
result,
Err(InterpolationError::Linear(msg)) if msg.contains("outside the surface's range")
));
}
#[test]
fn test_exact_point_match() {
let surface = create_test_surface();
let result = surface
.linear_interpolate(Point2D::new(dec!(0.0), dec!(0.0)))
.unwrap();
assert_eq!(result.z, dec!(0.0));
}
#[test]
fn test_midpoint_interpolation() {
let surface = create_test_surface();
let result = surface
.linear_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
.unwrap();
assert_eq!(result.z, dec!(1.0));
}
#[test]
fn test_quarter_point_interpolation() {
let surface = create_test_surface();
let result = surface
.linear_interpolate(Point2D::new(dec!(0.25), dec!(0.25)))
.unwrap();
assert!(result.z > dec!(0.0) && result.z < dec!(1.0));
}
#[test]
fn test_degenerate_triangle() {
let points = BTreeSet::from_iter(vec![
Point3D::new(dec!(1.0), dec!(1.0), dec!(0.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
]);
let surface = Surface::new(points);
let result = surface.linear_interpolate(Point2D::new(dec!(1.0), dec!(1.0)));
assert!(matches!(
result,
Err(InterpolationError::Linear(msg)) if msg.contains("Degenerate triangle")
));
}
#[test]
fn test_boundary_interpolation() {
let surface = create_test_surface();
let result = surface
.linear_interpolate(Point2D::new(dec!(0.0), dec!(0.5)))
.unwrap();
assert_eq!(result.z, dec!(0.5));
}
#[test]
fn test_uniform_gradient() {
let points = BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
]);
let surface = Surface::new(points);
let result = surface
.linear_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
.unwrap();
assert_eq!(result.z, dec!(1.0));
}
#[test]
fn test_interpolation_precision() {
let surface = create_test_surface();
let result = surface
.linear_interpolate(Point2D::new(dec!(0.333333), dec!(0.333333)))
.unwrap();
assert!(result.z >= dec!(0.0) && result.z <= dec!(2.0));
}
}
#[cfg(test)]
mod tests_surface_bilinear_interpolation {
use super::*;
use rust_decimal_macros::dec;
fn create_test_surface() -> Surface {
let points = BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)), Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)), Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)), Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)), ]);
Surface::new(points)
}
#[test]
fn test_insufficient_points() {
let points = BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
Point3D::new(dec!(2.0), dec!(2.0), dec!(2.0)),
]);
let surface = Surface::new(points);
let result = surface.bilinear_interpolate(Point2D::new(dec!(0.5), dec!(0.5)));
assert!(matches!(
result,
Err(InterpolationError::Bilinear(msg)) if msg.contains("Need at least four points")
));
}
#[test]
fn test_point_out_of_range() {
let surface = create_test_surface();
let result = surface.bilinear_interpolate(Point2D::new(dec!(-1.0), dec!(0.5)));
assert!(matches!(
result,
Err(InterpolationError::Bilinear(msg)) if msg.contains("outside the surface's range")
));
}
#[test]
fn test_exact_point_match() {
let surface = create_test_surface();
let result = surface
.bilinear_interpolate(Point2D::new(dec!(0.0), dec!(0.0)))
.unwrap();
assert_eq!(result.z, dec!(0.0));
}
#[test]
fn test_midpoint_interpolation() {
let surface = create_test_surface();
let result = surface
.bilinear_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
.unwrap();
assert_eq!(result.z, dec!(1.0));
}
#[test]
fn test_quarter_point_interpolation() {
let surface = create_test_surface();
let result = surface
.bilinear_interpolate(Point2D::new(dec!(0.25), dec!(0.25)))
.unwrap();
assert!(result.z > dec!(0.0) && result.z < dec!(1.0));
}
#[test]
fn test_invalid_quadrilateral() {
let points = BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(0.0), dec!(0.0), dec!(2.0)),
Point3D::new(dec!(0.0), dec!(0.0), dec!(3.0)),
]);
assert_eq!(points.len(), 4, "all four heights are distinct points");
let surface = Surface::new(points);
let result = surface.bilinear_interpolate(Point2D::new(dec!(0.0), dec!(0.0)));
assert!(result.is_err());
assert!(matches!(
result,
Err(InterpolationError::Bilinear(msg)) if msg.contains("Invalid quadrilateral")
));
}
#[test]
fn test_boundary_interpolation() {
let surface = create_test_surface();
let result = surface
.bilinear_interpolate(Point2D::new(dec!(0.0), dec!(0.5)))
.unwrap();
assert_eq!(result.z, dec!(0.5));
}
#[test]
fn test_uniform_gradient() {
let points = BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
]);
let surface = Surface::new(points);
let result = surface
.bilinear_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
.unwrap();
assert_eq!(result.z, dec!(1.0));
}
#[test]
fn test_interpolation_precision() {
let surface = create_test_surface();
let result = surface
.bilinear_interpolate(Point2D::new(dec!(0.333333), dec!(0.333333)))
.unwrap();
assert!(result.z >= dec!(0.0) && result.z <= dec!(2.0));
}
#[test]
fn test_corners_interpolation() {
let surface = create_test_surface();
let bl = surface
.bilinear_interpolate(Point2D::new(dec!(0.0), dec!(0.0)))
.unwrap();
let br = surface
.bilinear_interpolate(Point2D::new(dec!(1.0), dec!(0.0)))
.unwrap();
let tl = surface
.bilinear_interpolate(Point2D::new(dec!(0.0), dec!(1.0)))
.unwrap();
let tr = surface
.bilinear_interpolate(Point2D::new(dec!(1.0), dec!(1.0)))
.unwrap();
assert_eq!(bl.z, dec!(0.0));
assert_eq!(br.z, dec!(1.0));
assert_eq!(tl.z, dec!(1.0));
assert_eq!(tr.z, dec!(2.0));
}
}
#[cfg(test)]
mod tests_surface_cubic_interpolation {
use super::*;
use rust_decimal_macros::dec;
fn create_test_surface() -> Surface {
let points = BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
Point3D::new(dec!(0.5), dec!(0.5), dec!(1.5)),
Point3D::new(dec!(0.2), dec!(0.8), dec!(0.7)),
Point3D::new(dec!(0.8), dec!(0.2), dec!(0.7)),
Point3D::new(dec!(0.3), dec!(0.3), dec!(0.3)),
Point3D::new(dec!(0.7), dec!(0.7), dec!(1.7)),
]);
Surface::new(points)
}
#[test]
fn test_insufficient_points() {
let points = BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
Point3D::new(dec!(2.0), dec!(2.0), dec!(2.0)),
]);
let surface = Surface::new(points);
let result = surface.cubic_interpolate(Point2D::new(dec!(0.5), dec!(0.5)));
assert!(matches!(
result,
Err(InterpolationError::Cubic(msg)) if msg.contains("Need at least nine points")
));
}
#[test]
fn test_point_out_of_range() {
let surface = create_test_surface();
let result = surface.cubic_interpolate(Point2D::new(dec!(2.0), dec!(2.0)));
assert!(matches!(
result,
Err(InterpolationError::Cubic(msg)) if msg.contains("outside the surface's range")
));
}
#[test]
fn test_exact_point_match() {
let surface = create_test_surface();
let result = surface
.cubic_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
.unwrap();
assert_eq!(result.z, dec!(1.5));
}
#[test]
fn test_midpoint_interpolation() {
let surface = create_test_surface();
let result = surface
.cubic_interpolate(Point2D::new(dec!(0.4), dec!(0.4)))
.unwrap();
assert!(result.z > dec!(0.3) && result.z < dec!(1.5));
}
#[test]
fn test_interpolation_consistency() {
let surface = create_test_surface();
let test_points = vec![
Point2D::new(dec!(0.2), dec!(0.2)),
Point2D::new(dec!(0.6), dec!(0.6)),
Point2D::new(dec!(0.8), dec!(0.3)),
];
for point in test_points {
let result = surface.cubic_interpolate(point).unwrap();
assert!(
result.z >= dec!(0.0) && result.z <= dec!(2.0),
"Failed for point {point:?}"
);
assert_eq!(result.x, point.x);
assert_eq!(result.y, point.y);
}
}
#[test]
fn test_boundary_interpolation() {
let surface = create_test_surface();
let boundary_points = vec![
Point2D::new(dec!(0.0), dec!(0.5)),
Point2D::new(dec!(0.5), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(0.5)),
Point2D::new(dec!(0.5), dec!(1.0)),
];
for point in boundary_points {
let result = surface.cubic_interpolate(point).unwrap();
assert!(
result.z > dec!(0.0) && result.z < dec!(2.0),
"Failed for boundary point {point:?}"
);
}
}
#[test]
fn test_interpolation_precision() {
let surface = create_test_surface();
let result = surface
.cubic_interpolate(Point2D::new(dec!(0.333333), dec!(0.333333)))
.unwrap();
assert!(result.z > dec!(0.0) && result.z < dec!(2.0));
}
#[test]
fn test_repeated_interpolation() {
let surface = create_test_surface();
let point = Point2D::new(dec!(0.4), dec!(0.4));
let results: Vec<Decimal> = (0..5)
.map(|_| surface.cubic_interpolate(point).unwrap().z)
.collect();
let max_diff = results
.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap()
- results
.iter()
.min_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap();
assert!(
max_diff < dec!(0.001),
"Interpolation results should be consistent"
);
}
#[test]
fn test_extreme_point_locations() {
let surface = create_test_surface();
let extreme_points = vec![
Point2D::new(dec!(0.001), dec!(0.001)),
Point2D::new(dec!(0.999), dec!(0.999)),
];
for point in extreme_points {
let result = surface.cubic_interpolate(point).unwrap();
assert!(
result.z >= dec!(0.0) && result.z <= dec!(2.0),
"Failed for extreme point {point:?}"
);
}
}
}
#[cfg(test)]
mod tests_surface_spline_interpolation {
use super::*;
use rust_decimal_macros::dec;
fn create_test_surface() -> Surface {
let points = BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
Point3D::new(dec!(0.5), dec!(0.5), dec!(1.5)),
Point3D::new(dec!(0.2), dec!(0.8), dec!(0.7)),
Point3D::new(dec!(0.8), dec!(0.2), dec!(0.7)),
Point3D::new(dec!(0.3), dec!(0.3), dec!(0.3)),
Point3D::new(dec!(0.7), dec!(0.7), dec!(1.7)),
Point3D::new(dec!(0.4), dec!(0.6), dec!(1.1)),
Point3D::new(dec!(0.6), dec!(0.4), dec!(1.2)),
]);
Surface::new(points)
}
#[test]
fn test_insufficient_points() {
let points = BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
]);
let surface = Surface::new(points);
let result = surface.spline_interpolate(Point2D::new(dec!(0.5), dec!(0.5)));
assert!(matches!(
result,
Err(InterpolationError::Spline(msg)) if msg.contains("Need at least nine points")
));
}
#[test]
fn test_point_out_of_range() {
let surface = create_test_surface();
let result = surface.spline_interpolate(Point2D::new(dec!(2.0), dec!(2.0)));
assert!(matches!(
result,
Err(InterpolationError::Spline(msg)) if msg.contains("outside the surface's range")
));
}
#[test]
fn test_exact_point_match() {
let surface = create_test_surface();
let result = surface
.spline_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
.unwrap();
assert_eq!(result.z, dec!(1.5));
}
#[test]
fn test_midpoint_interpolation() {
let surface = create_test_surface();
let result = surface
.spline_interpolate(Point2D::new(dec!(0.4), dec!(0.4)))
.unwrap();
assert!(result.z > dec!(0.3) && result.z < dec!(1.5));
}
#[test]
fn test_interpolation_consistency() {
let surface = create_test_surface();
let test_points = vec![
Point2D::new(dec!(0.2), dec!(0.2)),
Point2D::new(dec!(0.6), dec!(0.6)),
Point2D::new(dec!(0.8), dec!(0.3)),
];
for point in test_points {
let result = surface.spline_interpolate(point).unwrap();
assert!(
result.z >= dec!(0.0) && result.z <= dec!(2.0),
"Failed for point {point:?}"
);
assert_eq!(result.x, point.x);
assert_eq!(result.y, point.y);
}
}
#[test]
fn test_boundary_interpolation() {
let surface = create_test_surface();
let boundary_points = vec![
Point2D::new(dec!(0.0), dec!(0.5)),
Point2D::new(dec!(0.5), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(0.5)),
Point2D::new(dec!(0.5), dec!(1.0)),
];
for point in boundary_points {
let result = surface.spline_interpolate(point).unwrap();
assert!(
result.z > dec!(0.0) && result.z < dec!(2.0),
"Failed for boundary point {point:?}"
);
}
}
#[test]
fn test_interpolation_precision() {
let surface = create_test_surface();
let result = surface
.spline_interpolate(Point2D::new(dec!(0.333333), dec!(0.333333)))
.unwrap();
assert!(result.z > dec!(0.0) && result.z < dec!(2.0));
}
#[test]
fn test_repeated_interpolation() {
let surface = create_test_surface();
let point = Point2D::new(dec!(0.4), dec!(0.4));
let results: Vec<Decimal> = (0..5)
.map(|_| surface.spline_interpolate(point).unwrap().z)
.collect();
let max_diff = results
.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap()
- results
.iter()
.min_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap();
assert!(
max_diff < dec!(0.001),
"Interpolation results should be consistent"
);
}
#[test]
fn test_extreme_point_locations() {
let surface = create_test_surface();
let extreme_points = vec![
Point2D::new(dec!(0.001), dec!(0.001)),
Point2D::new(dec!(0.999), dec!(0.999)),
];
for point in extreme_points {
let result = surface.spline_interpolate(point).unwrap();
assert!(
result.z >= dec!(0.0) && result.z <= dec!(2.0),
"Failed for extreme point {point:?}"
);
}
}
#[test]
fn test_one_dimensional_spline_interpolation() {
let surface = create_test_surface();
let points = vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(0.5), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(2.0)),
];
let test_points = vec![
(dec!(0.25), dec!(0.5)), (dec!(0.0), dec!(0.0)), (dec!(1.0), dec!(2.0)), (dec!(0.75), dec!(1.5)), ];
for (target, expected) in test_points {
let result = surface
.one_dimensional_spline_interpolation(&points, target, |p| p.x, |p| p.z)
.unwrap();
assert!(
(result - expected).abs() < dec!(0.1),
"Failed for target {target}, expected {expected}, got {result}"
);
}
}
#[test]
fn test_interpolation_edge_cases() {
let surface = create_test_surface();
let edge_points = vec![
Point2D::new(dec!(0.001), dec!(0.001)),
Point2D::new(dec!(0.999), dec!(0.999)),
Point2D::new(dec!(0.5), dec!(0.5)),
];
for point in edge_points {
let result = surface.spline_interpolate(point);
assert!(result.is_ok(), "Failed for point {point:?}");
let interpolated_point = result.unwrap();
assert_eq!(interpolated_point.x, point.x);
assert_eq!(interpolated_point.y, point.y);
}
}
}
#[cfg(test)]
mod tests_surface_arithmetic {
use super::*;
use crate::error::OperationErrorKind;
use rust_decimal_macros::dec;
fn create_test_surface() -> Surface {
let points = BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(0.5), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(0.0), dec!(0.5), dec!(1.0)),
Point3D::new(dec!(0.5), dec!(0.5), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(0.5), dec!(1.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
Point3D::new(dec!(0.5), dec!(1.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
]);
Surface::new(points)
}
#[test]
fn test_merge_empty_surfaces() {
let result = Surface::merge(&[], MergeOperation::Add);
assert!(matches!(
result,
Err(SurfaceError::OperationError(OperationErrorKind::InvalidParameters { operation, reason }))
if operation == "merge_surfaces" && reason.contains("No surfaces")
));
}
#[test]
fn test_merge_single_surface() {
let surface = create_test_surface();
let result = Surface::merge(&[&surface], MergeOperation::Add).unwrap();
assert_eq!(result.points.len(), surface.points.len());
}
#[test]
fn test_merge_add() {
let surface1 = create_test_surface();
let surface2 = create_test_surface();
let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Add).unwrap();
let mid_point = result
.interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
.unwrap();
assert_eq!(mid_point.z, dec!(2.0));
}
#[test]
fn test_merge_subtract() {
let surface1 = create_test_surface();
let surface2 = create_test_surface();
let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Subtract).unwrap();
let mid_point = result
.interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
.unwrap();
assert_eq!(mid_point.z, dec!(0.0));
}
fn constant_surface(height: Decimal) -> Surface {
let points =
BTreeSet::from_iter([dec!(0.0), dec!(0.5), dec!(1.0)].into_iter().flat_map(|x| {
[dec!(0.0), dec!(0.5), dec!(1.0)]
.into_iter()
.map(move |y| Point3D::new(x, y, height))
}));
Surface::new(points)
}
#[test]
fn test_merge_divide_folds_every_divisor() {
let eight = constant_surface(dec!(8));
let two_a = constant_surface(dec!(2));
let two_b = constant_surface(dec!(2));
let result = Surface::merge(&[&eight, &two_a, &two_b], MergeOperation::Divide).unwrap();
let mid_point = result
.interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
.unwrap();
assert!(
(mid_point.z - dec!(2)).abs() < dec!(0.0001),
"expected 8 / 2 / 2 = 2, got {}",
mid_point.z
);
}
const DETERMINISM_REPEATS: usize = 24;
fn height_fingerprint(surface: &Surface) -> Vec<(i128, u32)> {
surface
.points
.iter()
.map(|point| (point.z.mantissa(), point.z.scale()))
.collect()
}
#[test]
fn test_merge_surfaces_multiply_is_reproducible() {
let a = constant_surface(dec!(0.010989010989010989010989011));
let b = constant_surface(dec!(0.010752688172043010752688172));
let c = constant_surface(dec!(0.0094339622641509433962264151));
let expected = dec!(0.0000011147302687168785768908);
let first = Surface::merge(&[&a, &b, &c], MergeOperation::Multiply).unwrap();
for point in first.points.iter() {
assert_eq!(
(point.z.mantissa(), point.z.scale()),
(expected.mantissa(), expected.scale()),
"expected the left-to-right product {expected} at ({}, {}), got {}",
point.x,
point.y,
point.z
);
}
let reference = height_fingerprint(&first);
for run in 1..DETERMINISM_REPEATS {
let again = Surface::merge(&[&a, &b, &c], MergeOperation::Multiply).unwrap();
assert_eq!(
height_fingerprint(&again),
reference,
"run {run} of {DETERMINISM_REPEATS} produced different digits from the first"
);
}
}
#[test]
fn test_merge_divide_reports_overflow_instead_of_dropping_it() {
let tiny = constant_surface(Decimal::new(1, 20));
let big = constant_surface(dec!(100000000000000000000));
let result = Surface::merge(&[&big, &tiny], MergeOperation::Divide);
assert!(
matches!(result, Err(SurfaceError::ConstructionError(_))),
"a quotient outside the Decimal range must be reported, got {result:?}"
);
}
#[test]
fn test_incompatible_ranges() {
let surface1 = Surface::new(BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
]));
let surface2 = Surface::new(BTreeSet::from_iter(vec![
Point3D::new(dec!(2.0), dec!(2.0), dec!(1.0)),
Point3D::new(dec!(3.0), dec!(3.0), dec!(1.0)),
]));
let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Add);
assert!(matches!(
result,
Err(SurfaceError::OperationError(OperationErrorKind::InvalidParameters { operation, reason }))
if operation == "merge_surfaces" && reason.contains("incompatible ranges")
));
}
#[test]
fn test_merge_with() {
let surface1 = create_test_surface();
let surface2 = create_test_surface();
let result1 = surface1.merge_with(&surface2, MergeOperation::Add).unwrap();
let result2 = Surface::merge(&[&surface1, &surface2], MergeOperation::Add).unwrap();
assert_eq!(result1.points.len(), result2.points.len());
let test_point = Point2D::new(dec!(0.5), dec!(0.5));
let z1 = result1
.interpolate(test_point, InterpolationType::Cubic)
.unwrap();
let z2 = result2
.interpolate(test_point, InterpolationType::Cubic)
.unwrap();
assert_eq!(z1.z, z2.z);
}
#[test]
fn test_merge_multiply() {
let surface1 = create_test_surface();
let surface2 = create_test_surface();
let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Multiply).unwrap();
let mid_point = result
.interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
.unwrap();
assert_eq!(mid_point.z, dec!(1.0)); }
#[test]
fn test_merge_divide() {
let surface1 = create_test_surface();
let surface2 = create_test_surface();
let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Divide).unwrap();
let mid_point = result
.interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
.unwrap();
assert_eq!(mid_point.z, dec!(1.0)); }
#[test]
fn test_merge_max() {
let surface1 = create_test_surface();
let points2 = BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(2.0)),
Point3D::new(dec!(0.5), dec!(0.0), dec!(2.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(2.0)),
Point3D::new(dec!(0.0), dec!(0.5), dec!(2.0)),
Point3D::new(dec!(0.5), dec!(0.5), dec!(2.0)),
Point3D::new(dec!(1.0), dec!(0.5), dec!(2.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(2.0)),
Point3D::new(dec!(0.5), dec!(1.0), dec!(2.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
]);
let surface2 = Surface::new(points2);
let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Max).unwrap();
let mid_point = result
.interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
.unwrap();
assert_eq!(mid_point.z, dec!(2.0));
}
#[test]
fn test_merge_min() {
let surface1 = create_test_surface();
let mut surface2 = create_test_surface();
surface2
.points
.insert(Point3D::new(dec!(0.5), dec!(0.5), dec!(0.5)));
let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Min).unwrap();
let mid_point = result
.interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
.unwrap();
assert_eq!(mid_point.z, dec!(0.5));
}
}
#[cfg(test)]
mod tests_metrics {
use super::*;
use rust_decimal_macros::dec;
fn create_test_surface() -> Surface {
let points = BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(0.5), dec!(0.0), dec!(2.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(3.0)),
Point3D::new(dec!(0.0), dec!(0.5), dec!(2.0)),
Point3D::new(dec!(0.5), dec!(0.5), dec!(3.0)),
Point3D::new(dec!(1.0), dec!(0.5), dec!(4.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(3.0)),
Point3D::new(dec!(0.5), dec!(1.0), dec!(4.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(5.0)),
]);
Surface::new(points)
}
#[test]
fn test_basic_metrics() {
let surface = create_test_surface();
let metrics = surface.compute_basic_metrics().unwrap();
assert_eq!(metrics.mean, dec!(3.0));
assert_eq!(metrics.median, dec!(3.0));
assert_eq!(metrics.std_dev, dec!(1.1547005383792515290182975610));
}
#[test]
fn test_shape_metrics() {
let surface = create_test_surface();
let metrics = surface.compute_shape_metrics().unwrap();
assert!(metrics.skewness.abs() < dec!(0.001));
assert!((metrics.kurtosis - dec!(2.25)).abs() < dec!(0.001));
}
#[test]
fn test_range_metrics() {
let surface = create_test_surface();
let metrics = surface.compute_range_metrics().unwrap();
assert_eq!(metrics.min.y, dec!(1.0));
assert_eq!(metrics.max.y, dec!(5.0));
assert_eq!(metrics.range, dec!(4.0));
let (q1, q2, q3) = metrics.quartiles;
assert_eq!(q1, dec!(2.0));
assert_eq!(q2, dec!(3.0));
assert_eq!(q3, dec!(4.0));
assert_eq!(metrics.interquartile_range, dec!(2.0));
}
#[test]
fn test_trend_metrics() {
let surface = create_test_surface();
let metrics = surface.compute_trend_metrics().unwrap();
assert!((metrics.slope - dec!(2.0)).abs() < dec!(0.001));
assert!((metrics.intercept - dec!(2.0)).abs() < dec!(0.001));
}
#[test]
fn test_risk_metrics_flat_surface_keeps_deterministic_var() {
let points = BTreeSet::from_iter((0..3i64).flat_map(|i| {
(0..3i64).map(move |j| Point3D::new(Decimal::from(i), Decimal::from(j), dec!(5)))
}));
let surface = Surface::new(points);
let metrics = surface.compute_risk_metrics().unwrap();
assert_eq!(metrics.volatility, Decimal::ZERO);
assert_eq!(metrics.value_at_risk, dec!(5));
assert_eq!(metrics.expected_shortfall, Decimal::ZERO);
assert_eq!(metrics.beta, Decimal::ZERO);
assert_eq!(metrics.sharpe_ratio, Decimal::ZERO);
}
#[test]
fn test_risk_metrics_flat_negative_surface_keeps_deterministic_var() {
let points = BTreeSet::from_iter((0..3i64).flat_map(|i| {
(0..3i64).map(move |j| Point3D::new(Decimal::from(i), Decimal::from(j), dec!(-2.5)))
}));
let surface = Surface::new(points);
let metrics = surface.compute_risk_metrics().unwrap();
assert_eq!(metrics.volatility, Decimal::ZERO);
assert_eq!(metrics.value_at_risk, dec!(-2.5));
assert_eq!(metrics.sharpe_ratio, Decimal::ZERO);
}
}
#[cfg(test)]
mod tests_trend_metrics {
use super::*;
use crate::assert_decimal_eq;
use rust_decimal_macros::dec;
fn create_linear_surface() -> Surface {
let points = BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
Point3D::new(dec!(2.0), dec!(2.0), dec!(4.0)),
Point3D::new(dec!(3.0), dec!(3.0), dec!(6.0)),
Point3D::new(dec!(4.0), dec!(4.0), dec!(8.0)),
]);
Surface::new(points)
}
fn create_non_linear_surface() -> Surface {
let points = BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(3.0)),
Point3D::new(dec!(2.0), dec!(2.0), dec!(2.0)),
Point3D::new(dec!(3.0), dec!(3.0), dec!(5.0)),
Point3D::new(dec!(4.0), dec!(4.0), dec!(4.0)),
]);
Surface::new(points)
}
#[test]
fn test_compute_trend_metrics_linear_surface() {
let surface = create_linear_surface();
let metrics = surface.compute_trend_metrics().unwrap();
assert_decimal_eq!(metrics.slope, dec!(2.0), dec!(0.001));
assert_decimal_eq!(metrics.intercept, dec!(0.0), dec!(0.001));
assert_decimal_eq!(metrics.r_squared, dec!(1.0), dec!(0.001));
assert_eq!(metrics.moving_average.len(), 4);
}
#[test]
fn test_compute_trend_metrics_non_linear_surface() {
let surface = create_non_linear_surface();
let metrics = surface.compute_trend_metrics().unwrap();
assert!(metrics.r_squared < dec!(1.0));
assert!(metrics.slope != dec!(0.0));
assert!(metrics.intercept != dec!(0.0));
}
#[test]
fn test_moving_average_calculation() {
let surface = create_linear_surface();
let metrics = surface.compute_trend_metrics().unwrap();
let window_sizes = [3, 5, 7];
let surface_points_count = surface.points.len();
let expected_total_points = window_sizes
.iter()
.map(|&window| {
if window > surface_points_count {
0
} else {
surface_points_count
.saturating_sub(window)
.saturating_add(1)
}
})
.sum::<usize>();
assert_eq!(
metrics.moving_average.len(),
expected_total_points,
"Mismatch in moving average points calculation"
);
for point in &metrics.moving_average {
assert!(point.x >= dec!(0.0), "x value should be non-negative");
assert!(point.y >= dec!(0.0), "y value should be non-negative");
}
}
#[test]
fn test_edge_cases() {
let single_point_surface = Surface::new(BTreeSet::from_iter(vec![Point3D::new(
dec!(1.0),
dec!(1.0),
dec!(1.0),
)]));
let metrics = single_point_surface.compute_trend_metrics();
assert!(metrics.is_ok());
let identical_points_surface = Surface::new(BTreeSet::from_iter(vec![
Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
]));
let metrics = identical_points_surface.compute_trend_metrics().unwrap();
assert_decimal_eq!(metrics.r_squared, dec!(1.0), dec!(0.001));
assert_decimal_eq!(metrics.slope, dec!(0.0), dec!(0.001));
}
}
#[cfg(test)]
mod tests_axis_operations {
use super::*;
use rust_decimal_macros::dec;
fn create_test_surface() -> Surface {
let points = BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(2.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(3.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(4.0)),
]);
Surface::new(points)
}
#[test]
fn test_contains_point() {
let surface = create_test_surface();
assert!(surface.contains_point(&Point2D::new(dec!(0.0), dec!(0.0))));
assert!(!surface.contains_point(&Point2D::new(dec!(2.0), dec!(2.0))));
}
#[test]
fn test_get_index_values() {
let surface = create_test_surface();
let indexes = surface.get_index_values();
assert_eq!(indexes.len(), 4);
assert!(indexes.contains(&Point2D::new(dec!(0.0), dec!(0.0))));
assert!(indexes.contains(&Point2D::new(dec!(1.0), dec!(1.0))));
}
#[test]
fn test_get_values() {
let surface = create_test_surface();
let values = surface.get_values(Point2D::new(dec!(0.0), dec!(0.0)));
assert_eq!(values.len(), 1);
assert_eq!(*values[0], dec!(1.0));
}
#[test]
fn test_get_closest_point() {
let surface = create_test_surface();
let point = surface
.get_closest_point(&Point2D::new(dec!(0.5), dec!(0.5)))
.unwrap();
assert_eq!(point.x, dec!(0.0));
assert_eq!(point.y, dec!(0.0));
assert_eq!(point.z, dec!(1.0));
}
#[test]
fn test_get_point() {
let surface = create_test_surface();
let point = surface
.get_point(&Point2D::new(dec!(0.0), dec!(0.0)))
.unwrap();
assert_eq!(point.x, dec!(0.0));
assert_eq!(point.y, dec!(0.0));
assert_eq!(point.z, dec!(1.0));
assert!(
surface
.get_point(&Point2D::new(dec!(2.0), dec!(2.0)))
.is_none()
);
}
#[test]
fn test_merge_indexes() {
let surface1 = create_test_surface();
let surface2 = create_test_surface();
let merged = surface1.merge_indexes(surface2.get_index_values());
assert_eq!(merged.len(), 4);
for expected in [
Point2D::new(dec!(0.0), dec!(0.0)),
Point2D::new(dec!(0.0), dec!(1.0)),
Point2D::new(dec!(1.0), dec!(0.0)),
Point2D::new(dec!(1.0), dec!(1.0)),
] {
assert!(merged.contains(&expected), "missing index {expected}");
}
}
}
#[cfg(test)]
mod tests_surface_geometric_transformations {
use super::*;
use rust_decimal_macros::dec;
fn create_test_surface() -> Surface {
Surface::new(BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
]))
}
mod test_translate {
use super::*;
#[test]
fn test_translate_positive() {
let surface = create_test_surface();
let result = surface
.translate(vec![&dec!(1.0), &dec!(1.0), &dec!(1.0)])
.unwrap();
let translated_points: Vec<_> = result.points.iter().collect();
assert_eq!(translated_points[0].x, dec!(1.0));
assert_eq!(translated_points[0].y, dec!(1.0));
assert_eq!(translated_points[0].z, dec!(1.0));
}
#[test]
fn test_translate_negative() {
let surface = create_test_surface();
let result = surface
.translate(vec![&dec!(-1.0), &dec!(-1.0), &dec!(-1.0)])
.unwrap();
let translated_points: Vec<_> = result.points.iter().collect();
assert_eq!(translated_points[0].x, dec!(-1.0));
assert_eq!(translated_points[0].y, dec!(-1.0));
assert_eq!(translated_points[0].z, dec!(-1.0));
}
#[test]
fn test_translate_zero() {
let surface = create_test_surface();
let result = surface
.translate(vec![&dec!(0.0), &dec!(0.0), &dec!(0.0)])
.unwrap();
assert_eq!(surface.points, result.points);
}
#[test]
fn test_translate_wrong_dimensions() {
let surface = create_test_surface();
let result = surface.translate(vec![&dec!(1.0), &dec!(1.0)]);
assert!(result.is_err());
}
#[test]
fn test_translate_preserves_distances() {
let surface = create_test_surface();
let result = surface
.translate(vec![&dec!(1.0), &dec!(1.0), &dec!(1.0)])
.unwrap();
let original_points: Vec<_> = surface.points.iter().collect();
let translated_points: Vec<_> = result.points.iter().collect();
let orig_dist = ((original_points[1].x - original_points[0].x).powi(2)
+ (original_points[1].y - original_points[0].y).powi(2)
+ (original_points[1].z - original_points[0].z).powi(2))
.sqrt();
let trans_dist = ((translated_points[1].x - translated_points[0].x).powi(2)
+ (translated_points[1].y - translated_points[0].y).powi(2)
+ (translated_points[1].z - translated_points[0].z).powi(2))
.sqrt();
assert_eq!(orig_dist, trans_dist);
}
}
mod test_scale {
use super::*;
#[test]
fn test_scale_uniform() {
let surface = create_test_surface();
let result = surface
.scale(vec![&dec!(2.0), &dec!(2.0), &dec!(2.0)])
.unwrap();
assert_eq!(result[1].x, dec!(0.0));
assert_eq!(result[1].y, dec!(2.0));
assert_eq!(result[1].z, dec!(2.0));
}
#[test]
fn test_scale_non_uniform() {
let surface = create_test_surface();
let result = surface
.scale(vec![&dec!(2.0), &dec!(3.0), &dec!(4.0)])
.unwrap();
assert_eq!(result[0].x, dec!(0.0));
assert_eq!(result[0].y, dec!(0.0));
assert_eq!(result[0].z, dec!(0.0));
assert_eq!(result[1].x, dec!(0.0));
assert_eq!(result[1].y, dec!(3.0));
assert_eq!(result[1].z, dec!(4.0));
assert_eq!(result[2].x, dec!(2.0));
assert_eq!(result[2].y, dec!(0.0));
assert_eq!(result[2].z, dec!(4.0));
assert_eq!(result[2].x, dec!(2.0));
assert_eq!(result[2].y, dec!(0.0));
assert_eq!(result[2].z, dec!(4.0));
}
#[test]
fn test_scale_zero() {
let surface = create_test_surface();
let result = surface
.scale(vec![&dec!(0.0), &dec!(0.0), &dec!(0.0)])
.unwrap();
assert!(
result
.points
.iter()
.all(|p| p.x == dec!(0.0) && p.y == dec!(0.0) && p.z == dec!(0.0))
);
}
#[test]
fn test_scale_wrong_dimensions() {
let surface = create_test_surface();
let result = surface.scale(vec![&dec!(2.0), &dec!(2.0)]);
assert!(result.is_err());
}
#[test]
fn test_scale_negative() {
let surface = create_test_surface();
let result = surface
.scale(vec![&dec!(-1.0), &dec!(-1.0), &dec!(-1.0)])
.unwrap();
let scaled_points: Vec<_> = result.points.iter().collect();
assert_eq!(scaled_points[1].x, dec!(-1.0));
assert_eq!(scaled_points[1].y, dec!(0.0));
assert_eq!(scaled_points[1].z, dec!(-1.0));
}
}
mod test_intersect_with {
use super::*;
#[test]
fn test_surfaces_intersect() {
let surface1 = create_test_surface();
let surface2 = Surface::new(BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
]));
let intersections = surface1.intersect_with(&surface2).unwrap();
assert_eq!(intersections.len(), 2);
}
#[test]
fn test_no_intersection() {
let surface1 = create_test_surface();
let surface2 = Surface::new(BTreeSet::from_iter(vec![
Point3D::new(dec!(10.0), dec!(10.0), dec!(10.0)),
Point3D::new(dec!(11.0), dec!(11.0), dec!(11.0)),
]));
let intersections = surface1.intersect_with(&surface2).unwrap();
assert!(intersections.is_empty());
}
#[test]
fn test_multiple_intersections() {
let surface1 = create_test_surface();
let surface2 = create_test_surface();
let intersections = surface1.intersect_with(&surface2).unwrap();
assert_eq!(intersections.len(), surface1.points.len());
}
#[test]
fn test_self_intersection() {
let surface = create_test_surface();
let intersections = surface.intersect_with(&surface).unwrap();
assert_eq!(intersections.len(), surface.points.len());
}
#[test]
fn test_empty_surfaces() {
let surface1 = Surface::new(BTreeSet::new());
let surface2 = Surface::new(BTreeSet::new());
let intersections = surface1.intersect_with(&surface2).unwrap();
assert!(intersections.is_empty());
}
}
mod test_derivative_at {
use super::*;
#[test]
fn test_planar_derivative() {
let surface = Surface::new(BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
]));
let derivatives = surface
.derivative_at(&Point3D::new(dec!(0.5), dec!(0.5), dec!(0.5)))
.unwrap();
assert_eq!(derivatives.len(), 2);
assert_eq!(derivatives[0], Decimal::MAX); assert_eq!(derivatives[1], dec!(1.0)); }
#[test]
fn test_non_planar_derivative() {
let surface = create_test_surface();
let derivatives = surface
.derivative_at(&Point3D::new(dec!(0.5), dec!(0.5), dec!(1.0)))
.unwrap();
assert_eq!(derivatives.len(), 2);
}
#[test]
fn test_out_of_range() {
let surface = create_test_surface();
let result = surface.derivative_at(&Point3D::new(dec!(10.0), dec!(10.0), dec!(10.0)));
assert!(result.is_err());
}
#[test]
fn test_at_corner() {
let surface = create_test_surface();
let derivatives = surface
.derivative_at(&Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)))
.unwrap();
assert_eq!(derivatives.len(), 2);
}
#[test]
fn test_single_point_surface() {
let surface = Surface::new(BTreeSet::from_iter(vec![Point3D::new(
dec!(1.0),
dec!(1.0),
dec!(1.0),
)]));
let result = surface.derivative_at(&Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)));
assert!(result.is_err());
}
}
mod test_extrema {
use super::*;
#[test]
fn test_find_extrema() {
let surface = create_test_surface();
let (min, max) = surface.extrema().unwrap();
assert_eq!(min.z, dec!(0.0));
assert_eq!(max.z, dec!(2.0));
}
#[test]
fn test_empty_surface() {
let surface = Surface::new(BTreeSet::new());
let result = surface.extrema();
assert!(result.is_err());
}
#[test]
fn test_single_point() {
let surface = Surface::new(BTreeSet::from_iter(vec![Point3D::new(
dec!(1.0),
dec!(1.0),
dec!(1.0),
)]));
let (min, max) = surface.extrema().unwrap();
assert_eq!(min, max);
}
#[test]
fn test_flat_surface() {
let surface = Surface::new(BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
]));
let (min, max) = surface.extrema().unwrap();
assert_eq!(min.z, max.z);
}
#[test]
fn test_multiple_extrema() {
let surface = Surface::new(BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
Point3D::new(dec!(2.0), dec!(2.0), dec!(0.0)),
]));
let (min, max) = surface.extrema().unwrap();
assert_eq!(min.z, dec!(0.0));
assert_eq!(max.z, dec!(2.0));
}
}
mod test_measure_under {
use super::*;
#[test]
fn test_volume_under_planar() {
let surface = Surface::new(BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
]));
let volume = surface.measure_under(&dec!(0.0)).unwrap();
assert_eq!(volume, dec!(0.5)); }
#[test]
fn test_volume_empty_surface() {
let surface = Surface::new(BTreeSet::new());
let volume = surface.measure_under(&dec!(0.0)).unwrap();
assert_eq!(volume, dec!(0.0));
}
#[test]
fn test_volume_single_triangle() {
let surface = Surface::new(BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(0.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
]));
let volume = surface.measure_under(&dec!(0.0)).unwrap();
assert!(volume > dec!(0.0));
}
#[test]
fn test_volume_with_base_value() {
let surface = create_test_surface();
let volume1 = surface.measure_under(&dec!(0.0)).unwrap();
let volume2 = surface.measure_under(&dec!(1.0)).unwrap();
assert!(volume1 > volume2);
}
#[test]
fn test_negative_volume() {
let surface = Surface::new(BTreeSet::from_iter(vec![
Point3D::new(dec!(0.0), dec!(0.0), dec!(-1.0)),
Point3D::new(dec!(1.0), dec!(0.0), dec!(-1.0)),
Point3D::new(dec!(0.0), dec!(1.0), dec!(-1.0)),
]));
let volume = surface.measure_under(&dec!(0.0)).unwrap();
assert!(volume > dec!(0.0));
}
}
}
#[cfg(test)]
mod tests_surface_serde {
use super::*;
use rust_decimal_macros::dec;
fn create_test_surface() -> Surface {
let mut points = BTreeSet::new();
points.insert(Point3D {
x: dec!(1.0),
y: dec!(2.0),
z: dec!(3.0),
});
points.insert(Point3D {
x: dec!(4.0),
y: dec!(5.0),
z: dec!(6.0),
});
points.insert(Point3D {
x: dec!(7.0),
y: dec!(8.0),
z: dec!(9.0),
});
Surface {
points,
x_range: (dec!(1.0), dec!(7.0)),
y_range: (dec!(2.0), dec!(8.0)),
}
}
#[test]
fn test_basic_serialization() {
let surface = create_test_surface();
let serialized = serde_json::to_string(&surface).unwrap();
let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
assert_eq!(surface.points, deserialized.points);
assert_eq!(surface.x_range, deserialized.x_range);
assert_eq!(surface.y_range, deserialized.y_range);
}
#[test]
fn test_pretty_print() {
let surface = create_test_surface();
let serialized = serde_json::to_string_pretty(&surface).unwrap();
assert!(serialized.contains('\n'));
assert!(serialized.contains(" "));
let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
assert_eq!(surface.points, deserialized.points);
}
#[test]
fn test_empty_surface() {
let surface = Surface {
points: BTreeSet::new(),
x_range: (dec!(0.0), dec!(0.0)),
y_range: (dec!(0.0), dec!(0.0)),
};
let serialized = serde_json::to_string(&surface).unwrap();
let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
assert!(deserialized.points.is_empty());
assert_eq!(deserialized.x_range, (dec!(0.0), dec!(0.0)));
assert_eq!(deserialized.y_range, (dec!(0.0), dec!(0.0)));
}
#[test]
fn test_surface_with_negative_values() {
let mut points = BTreeSet::new();
points.insert(Point3D {
x: dec!(-1.0),
y: dec!(-2.0),
z: dec!(-3.0),
});
points.insert(Point3D {
x: dec!(-4.0),
y: dec!(-5.0),
z: dec!(-6.0),
});
let surface = Surface {
points,
x_range: (dec!(-4.0), dec!(-1.0)),
y_range: (dec!(-5.0), dec!(-2.0)),
};
let serialized = serde_json::to_string(&surface).unwrap();
let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
assert_eq!(surface.points, deserialized.points);
assert_eq!(surface.x_range, deserialized.x_range);
assert_eq!(surface.y_range, deserialized.y_range);
}
#[test]
fn test_surface_with_high_precision() {
let mut points = BTreeSet::new();
points.insert(Point3D {
x: dec!(1.12345678901234567890),
y: dec!(2.12345678901234567890),
z: dec!(3.12345678901234567890),
});
points.insert(Point3D {
x: dec!(4.12345678901234567890),
y: dec!(5.12345678901234567890),
z: dec!(6.12345678901234567890),
});
let surface = Surface {
points,
x_range: (dec!(1.12345678901234567890), dec!(4.12345678901234567890)),
y_range: (dec!(2.12345678901234567890), dec!(5.12345678901234567890)),
};
let serialized = serde_json::to_string(&surface).unwrap();
let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
assert_eq!(surface.points, deserialized.points);
assert_eq!(surface.x_range, deserialized.x_range);
assert_eq!(surface.y_range, deserialized.y_range);
}
#[test]
fn test_invalid_json() {
let json_str = r#"{"points": []}"#;
let result = serde_json::from_str::<Surface>(json_str);
assert!(result.is_err());
let json_str = r#"{"points": [1, 2, 3], "x_range": [0, 1], "y_range": [0, 1]}"#;
let result = serde_json::from_str::<Surface>(json_str);
assert!(result.is_err());
let json_str = r#"{"points": [], "x_range": "invalid", "y_range": [0, 1]}"#;
let result = serde_json::from_str::<Surface>(json_str);
assert!(result.is_err());
}
#[test]
fn test_json_structure() {
let surface = create_test_surface();
let serialized = serde_json::to_string(&surface).unwrap();
let json: serde_json::Value = serde_json::from_str(&serialized).unwrap();
assert!(json.is_object());
assert!(json.get("points").is_some());
assert!(json.get("x_range").is_some());
assert!(json.get("y_range").is_some());
assert!(json.get("points").unwrap().is_array());
let x_range = json.get("x_range").unwrap().as_array().unwrap();
let y_range = json.get("y_range").unwrap().as_array().unwrap();
assert_eq!(x_range.len(), 2);
assert_eq!(y_range.len(), 2);
}
#[test]
fn test_multiple_surfaces() {
let surface1 = create_test_surface();
let mut surface2 = create_test_surface();
surface2.x_range = (dec!(8.0), dec!(14.0));
surface2.y_range = (dec!(9.0), dec!(15.0));
let surfaces = vec![surface1, surface2];
let serialized = serde_json::to_string(&surfaces).unwrap();
let deserialized: Vec<Surface> = serde_json::from_str(&serialized).unwrap();
assert_eq!(surfaces.len(), deserialized.len());
assert_eq!(surfaces[0].points, deserialized[0].points);
assert_eq!(surfaces[1].points, deserialized[1].points);
}
#[test]
fn test_ordering_preservation() {
let surface = create_test_surface();
let serialized = serde_json::to_string(&surface).unwrap();
let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
let original_points: Vec<_> = surface.points.into_iter().collect();
let deserialized_points: Vec<_> = deserialized.points.into_iter().collect();
assert_eq!(original_points, deserialized_points);
}
#[test]
fn test_surface_with_extremes() {
let mut points = BTreeSet::new();
points.insert(Point3D {
x: Decimal::MAX,
y: Decimal::MAX,
z: Decimal::MAX,
});
points.insert(Point3D {
x: Decimal::MIN,
y: Decimal::MIN,
z: Decimal::MIN,
});
let surface = Surface {
points,
x_range: (Decimal::MIN, Decimal::MAX),
y_range: (Decimal::MIN, Decimal::MAX),
};
let serialized = serde_json::to_string(&surface).unwrap();
let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
assert_eq!(surface.points, deserialized.points);
assert_eq!(surface.x_range, deserialized.x_range);
assert_eq!(surface.y_range, deserialized.y_range);
}
#[test]
fn test_surface_points_array_format() {
let json_str = r#"{
"points": [
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0]
],
"x_range": [1.0, 4.0],
"y_range": [2.0, 5.0]
}"#;
let result = serde_json::from_str::<Surface>(json_str);
assert!(result.is_ok());
let surface = result.unwrap();
assert_eq!(surface.points.len(), 2);
}
}