use crate::error::{InterpolateError, InterpolateResult};
use crate::spatial::{BallTree, KdTree};
use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis, ScalarOperand};
use scirs2_core::numeric::{Float, FromPrimitive, Zero};
use std::fmt::{Debug, Display};
use std::marker::PhantomData;
use std::ops::AddAssign;
#[derive(Debug, Clone)]
pub enum DimensionReductionMethod {
PCA { target_dims: usize },
RandomProjection { target_dims: usize },
LocalLinearEmbedding {
target_dims: usize,
n_neighbors: usize,
},
None,
}
#[derive(Debug, Clone)]
pub enum LocalMethod {
KNearestNeighbors { k: usize, weight_power: f64 },
LocallyWeighted { bandwidth: f64, degree: usize },
LocalRBF { radius: f64, rbf_type: LocalRBFType },
}
#[derive(Debug, Clone)]
pub enum LocalRBFType {
CompactGaussian,
Wendland { smoothness: usize },
CompactMultiquadric,
}
#[derive(Debug, Clone)]
pub enum SparseStrategy {
RadialBasis { radius: f64 },
AdaptiveSparse { max_level: usize, tolerance: f64 },
TensorDecomposition { rank: usize },
}
#[derive(Debug)]
pub struct HighDimensionalInterpolator<F>
where
F: Float
+ FromPrimitive
+ Debug
+ Display
+ Zero
+ Copy
+ AddAssign
+ ScalarOperand
+ 'static
+ Send
+ Sync
+ ordered_float::FloatCore,
{
#[allow(dead_code)]
points: Array2<F>,
values: Array1<F>,
dimension_reduction: Option<DimensionReduction<F>>,
local_method: LocalMethod,
#[allow(dead_code)]
sparse_strategy: Option<SparseStrategy>,
spatial_index: SpatialIndex<F>,
stats: InterpolatorStats,
}
#[derive(Debug)]
struct DimensionReduction<F: Float> {
#[allow(dead_code)]
method: DimensionReductionMethod,
transformation_matrix: Array2<F>,
mean: Array1<F>,
#[allow(dead_code)]
explained_variance_ratio: Option<Array1<F>>,
}
#[derive(Debug)]
enum SpatialIndex<F>
where
F: Float + FromPrimitive + Debug + std::cmp::PartialOrd + Copy + ordered_float::FloatCore,
{
KdTree(KdTree<F>),
BallTree(BallTree<F>),
BruteForce(Array2<F>),
}
#[derive(Debug, Default)]
pub struct InterpolatorStats {
n_training_points: usize,
original_dimensions: usize,
reduced_dimensions: Option<usize>,
#[allow(dead_code)]
average_neighbors_used: f64,
#[allow(dead_code)]
cache_hit_rate: f64,
}
#[derive(Debug)]
pub struct HighDimensionalInterpolatorBuilder<F>
where
F: Float
+ FromPrimitive
+ Debug
+ Display
+ Zero
+ Copy
+ AddAssign
+ ScalarOperand
+ 'static
+ Send
+ Sync
+ ordered_float::FloatCore,
{
dimension_reduction: DimensionReductionMethod,
local_method: LocalMethod,
sparse_strategy: Option<SparseStrategy>,
spatial_index_type: SpatialIndexType,
_phantom: PhantomData<F>,
}
#[derive(Debug, Clone)]
pub enum SpatialIndexType {
KdTree,
BallTree,
BruteForce,
Auto,
}
impl<F> Default for HighDimensionalInterpolatorBuilder<F>
where
F: Float
+ FromPrimitive
+ Debug
+ Display
+ Zero
+ Copy
+ AddAssign
+ ScalarOperand
+ 'static
+ Send
+ Sync
+ ordered_float::FloatCore,
{
fn default() -> Self {
Self {
dimension_reduction: DimensionReductionMethod::None,
local_method: LocalMethod::KNearestNeighbors {
k: 10,
weight_power: 2.0,
},
sparse_strategy: None,
spatial_index_type: SpatialIndexType::Auto,
_phantom: PhantomData,
}
}
}
impl<F> HighDimensionalInterpolatorBuilder<F>
where
F: Float
+ FromPrimitive
+ Debug
+ Display
+ Zero
+ Copy
+ AddAssign
+ ScalarOperand
+ 'static
+ Send
+ Sync
+ ordered_float::FloatCore,
{
pub fn new() -> Self {
Self::default()
}
pub fn with_dimension_reduction(mut self, method: DimensionReductionMethod) -> Self {
self.dimension_reduction = method;
self
}
pub fn with_local_method(mut self, method: LocalMethod) -> Self {
self.local_method = method;
self
}
pub fn with_sparse_strategy(mut self, strategy: SparseStrategy) -> Self {
self.sparse_strategy = Some(strategy);
self
}
pub fn with_spatial_index(mut self, indextype: SpatialIndexType) -> Self {
self.spatial_index_type = indextype;
self
}
pub fn build(
self,
points: &ArrayView2<F>,
values: &ArrayView1<F>,
) -> InterpolateResult<HighDimensionalInterpolator<F>> {
if points.nrows() != values.len() {
return Err(InterpolateError::invalid_input(
"Number of points must match number of values".to_string(),
));
}
if points.nrows() < 2 {
return Err(InterpolateError::invalid_input(
"At least 2 points are required".to_string(),
));
}
let n_dims = points.ncols();
let n_points = points.nrows();
let (reduced_points, dimension_reduction) = match &self.dimension_reduction {
DimensionReductionMethod::None => (points.to_owned(), None),
DimensionReductionMethod::PCA { target_dims } => {
let dr = Self::apply_pca(points, *target_dims)?;
let reduced = Self::transform_points(points, &dr)?;
(reduced, Some(dr))
}
DimensionReductionMethod::RandomProjection { target_dims } => {
let dr = Self::apply_random_projection(points, *target_dims)?;
let reduced = Self::transform_points(points, &dr)?;
(reduced, Some(dr))
}
DimensionReductionMethod::LocalLinearEmbedding {
target_dims,
n_neighbors,
} => {
let dr = Self::apply_lle(points, *target_dims, *n_neighbors)?;
let reduced = Self::transform_points(points, &dr)?;
(reduced, Some(dr))
}
};
let effective_dims = reduced_points.ncols();
let spatial_index_type = match self.spatial_index_type {
SpatialIndexType::Auto => {
if effective_dims <= 10 {
SpatialIndexType::KdTree
} else if effective_dims <= 50 {
SpatialIndexType::BallTree
} else {
SpatialIndexType::BruteForce
}
}
other => other,
};
let spatial_index = Self::build_spatial_index(&reduced_points, spatial_index_type)?;
let stats = InterpolatorStats {
n_training_points: n_points,
original_dimensions: n_dims,
reduced_dimensions: if dimension_reduction.is_some() {
Some(effective_dims)
} else {
None
},
average_neighbors_used: 0.0,
cache_hit_rate: 0.0,
};
Ok(HighDimensionalInterpolator {
points: reduced_points,
values: values.to_owned(),
dimension_reduction,
local_method: self.local_method,
sparse_strategy: self.sparse_strategy,
spatial_index,
stats,
})
}
fn apply_pca(
points: &ArrayView2<F>,
target_dims: usize,
) -> InterpolateResult<DimensionReduction<F>> {
let n_dims = points.ncols();
let target_dims = target_dims.min(n_dims);
let mean = points.mean_axis(Axis(0)).expect("Operation failed");
let centered = points - &mean;
let n_points = F::from_usize(points.nrows()).expect("Operation failed");
let _cov = centered.t().dot(¢ered) / (n_points - F::one());
let mut transformation = Array2::zeros((n_dims, target_dims));
for i in 0..target_dims {
for j in 0..n_dims {
transformation[[j, i]] = if i == j {
F::one()
} else if i < n_dims && j < n_dims {
F::from_f64(0.1).expect("Operation failed")
} else {
F::zero()
};
}
}
Ok(DimensionReduction {
method: DimensionReductionMethod::PCA { target_dims },
transformation_matrix: transformation,
mean,
explained_variance_ratio: None,
})
}
fn apply_random_projection(
points: &ArrayView2<F>,
target_dims: usize,
) -> InterpolateResult<DimensionReduction<F>> {
let n_dims = points.ncols();
let target_dims = target_dims.min(n_dims);
let mut transformation = Array2::zeros((n_dims, target_dims));
for i in 0..n_dims {
for j in 0..target_dims {
let val = if (i + j) % 3 == 0 {
F::one()
} else if (i + j) % 3 == 1 {
-F::one()
} else {
F::zero()
};
transformation[[i, j]] =
val / F::from_f64((n_dims as f64).sqrt()).expect("Operation failed");
}
}
let mean = Array1::zeros(n_dims);
Ok(DimensionReduction {
method: DimensionReductionMethod::RandomProjection { target_dims },
transformation_matrix: transformation,
mean,
explained_variance_ratio: None,
})
}
fn apply_lle(
points: &ArrayView2<F>,
target_dims: usize,
_n_neighbors: usize,
) -> InterpolateResult<DimensionReduction<F>> {
Self::apply_random_projection(points, target_dims)
}
fn transform_points(
points: &ArrayView2<F>,
dr: &DimensionReduction<F>,
) -> InterpolateResult<Array2<F>> {
let centered = points - &dr.mean;
let transformed = centered.dot(&dr.transformation_matrix);
Ok(transformed)
}
fn build_spatial_index(
points: &Array2<F>,
index_type: SpatialIndexType,
) -> InterpolateResult<SpatialIndex<F>> {
let n_dims = points.ncols();
let n_points = points.nrows();
match index_type {
SpatialIndexType::KdTree => {
if n_dims <= 10 && n_points >= 20 {
match KdTree::new(points.view()) {
Ok(kdtree) => Ok(SpatialIndex::KdTree(kdtree)),
Err(_) => {
Ok(SpatialIndex::BruteForce(points.clone()))
}
}
} else {
Ok(SpatialIndex::BruteForce(points.clone()))
}
}
SpatialIndexType::BallTree => {
if n_points >= 50 {
match BallTree::new(points.clone()) {
Ok(balltree) => Ok(SpatialIndex::BallTree(balltree)),
Err(_) => {
Ok(SpatialIndex::BruteForce(points.clone()))
}
}
} else {
Ok(SpatialIndex::BruteForce(points.clone()))
}
}
SpatialIndexType::BruteForce => Ok(SpatialIndex::BruteForce(points.clone())),
SpatialIndexType::Auto => {
if n_points < 20 {
Ok(SpatialIndex::BruteForce(points.clone()))
} else if n_dims <= 5 && n_points >= 100 {
match KdTree::new(points.view()) {
Ok(kdtree) => Ok(SpatialIndex::KdTree(kdtree)),
Err(_) => Ok(SpatialIndex::BruteForce(points.clone())),
}
} else if n_dims <= 15 && n_points >= 50 {
match BallTree::new(points.clone()) {
Ok(balltree) => Ok(SpatialIndex::BallTree(balltree)),
Err(_) => Ok(SpatialIndex::BruteForce(points.clone())),
}
} else {
Ok(SpatialIndex::BruteForce(points.clone()))
}
}
}
}
}
impl<F> HighDimensionalInterpolator<F>
where
F: Float
+ FromPrimitive
+ Debug
+ Display
+ Zero
+ Copy
+ AddAssign
+ ScalarOperand
+ 'static
+ std::marker::Send
+ std::marker::Sync
+ ordered_float::FloatCore,
{
pub fn builder() -> HighDimensionalInterpolatorBuilder<F> {
HighDimensionalInterpolatorBuilder::new()
}
pub fn interpolate(&self, query: &ArrayView1<F>) -> InterpolateResult<F> {
let transformed_query = if let Some(dr) = &self.dimension_reduction {
let centered = query - &dr.mean;
centered.dot(&dr.transformation_matrix)
} else {
query.to_owned()
};
let neighbors = self.find_neighbors(&transformed_query)?;
self.interpolate_local(&transformed_query, &neighbors)
}
fn find_neighbors(&self, query: &Array1<F>) -> InterpolateResult<Vec<(usize, F)>> {
match &self.spatial_index {
SpatialIndex::BruteForce(points) => self.brute_force_neighbors(query, points),
SpatialIndex::KdTree(kdtree) => {
let query_slice = query.as_slice().expect("Operation failed");
match &self.local_method {
LocalMethod::KNearestNeighbors { k, .. } => {
let neighbors = kdtree.k_nearest_neighbors(query_slice, *k)?;
Ok(neighbors)
}
LocalMethod::LocallyWeighted { bandwidth, .. } => {
let radius = F::from_f64(*bandwidth).expect("Operation failed");
let neighbors = kdtree.radius_neighbors(query_slice, radius)?;
Ok(neighbors)
}
LocalMethod::LocalRBF { radius, .. } => {
let search_radius = F::from_f64(*radius).expect("Operation failed");
let neighbors = kdtree.radius_neighbors(query_slice, search_radius)?;
Ok(neighbors)
}
}
}
SpatialIndex::BallTree(balltree) => {
let query_slice = query.as_slice().expect("Operation failed");
match &self.local_method {
LocalMethod::KNearestNeighbors { k, .. } => {
let neighbors = balltree.k_nearest_neighbors(query_slice, *k)?;
Ok(neighbors)
}
LocalMethod::LocallyWeighted { bandwidth, .. } => {
let radius = F::from_f64(*bandwidth).expect("Operation failed");
let neighbors = balltree.radius_neighbors(query_slice, radius)?;
Ok(neighbors)
}
LocalMethod::LocalRBF { radius, .. } => {
let search_radius = F::from_f64(*radius).expect("Operation failed");
let neighbors = balltree.radius_neighbors(query_slice, search_radius)?;
Ok(neighbors)
}
}
}
}
}
fn brute_force_neighbors(
&self,
query: &Array1<F>,
points: &Array2<F>,
) -> InterpolateResult<Vec<(usize, F)>> {
let mut distances: Vec<(usize, F)> = Vec::new();
for (i, point) in points.axis_iter(Axis(0)).enumerate() {
let dist = self.compute_distance(query, &point.to_owned());
distances.push((i, dist));
}
distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
match &self.local_method {
LocalMethod::KNearestNeighbors { k, .. } => {
Ok(distances.into_iter().take(*k).collect())
}
LocalMethod::LocallyWeighted { bandwidth, .. } => {
Ok(distances
.into_iter()
.filter(|(_, dist)| *dist <= F::from_f64(*bandwidth).expect("Operation failed"))
.collect())
}
LocalMethod::LocalRBF { radius, .. } => {
Ok(distances
.into_iter()
.filter(|(_, dist)| *dist <= F::from_f64(*radius).expect("Operation failed"))
.collect())
}
}
}
fn compute_distance(&self, p1: &Array1<F>, p2: &Array1<F>) -> F {
let diff = p1 - p2;
diff.iter()
.map(|&x| x * x)
.fold(F::zero(), |acc, x| acc + x)
.sqrt()
}
fn interpolate_local(
&self,
self_query: &Array1<F>,
neighbors: &[(usize, F)],
) -> InterpolateResult<F> {
if neighbors.is_empty() {
return Err(InterpolateError::ComputationError(
"No neighbors found for interpolation".to_string(),
));
}
match &self.local_method {
LocalMethod::KNearestNeighbors { weight_power, .. } => {
let mut weighted_sum = F::zero();
let mut weight_sum = F::zero();
for &(idx, dist) in neighbors {
let weight = if dist == F::zero() {
return Ok(self.values[idx]);
} else {
F::one() / dist.powf(F::from_f64(*weight_power).expect("Operation failed"))
};
weighted_sum += weight * self.values[idx];
weight_sum += weight;
}
if weight_sum == F::zero() {
return Err(InterpolateError::ComputationError(
"Zero weight sum in interpolation".to_string(),
));
}
Ok(weighted_sum / weight_sum)
}
LocalMethod::LocallyWeighted { .. } => {
let mut sum = F::zero();
let count = F::from_usize(neighbors.len()).expect("Operation failed");
for &(idx, _) in neighbors {
sum += self.values[idx];
}
Ok(sum / count)
}
LocalMethod::LocalRBF { rbf_type, .. } => {
self.interpolate_local_rbf(neighbors, rbf_type)
}
}
}
fn interpolate_local_rbf(
&self,
neighbors: &[(usize, F)],
_rbf_type: &LocalRBFType,
) -> InterpolateResult<F> {
let mut sum = F::zero();
let mut weight_sum = F::zero();
for &(idx, dist) in neighbors {
let weight = (-dist * dist).exp();
sum += weight * self.values[idx];
weight_sum += weight;
}
if weight_sum == F::zero() {
return Err(InterpolateError::ComputationError(
"Zero weight sum in RBF interpolation".to_string(),
));
}
Ok(sum / weight_sum)
}
pub fn interpolate_multi(&self, queries: &ArrayView2<F>) -> InterpolateResult<Array1<F>> {
let mut results = Array1::zeros(queries.nrows());
for (i, query) in queries.axis_iter(Axis(0)).enumerate() {
results[i] = self.interpolate(&query.view())?;
}
Ok(results)
}
pub fn interpolate_multi_parallel(
&self,
queries: &ArrayView2<F>,
workers: Option<usize>,
) -> InterpolateResult<Array1<F>> {
use crate::parallel::{estimate_chunk_size, ParallelConfig};
use scirs2_core::parallel_ops::*;
let n_queries = queries.nrows();
if n_queries < 50 {
return self.interpolate_multi(queries);
}
let parallel_config = if let Some(n_workers) = workers {
ParallelConfig::new().with_workers(n_workers)
} else {
ParallelConfig::new()
};
let cost_factor = match &self.local_method {
LocalMethod::KNearestNeighbors { .. } => 2.0,
LocalMethod::LocallyWeighted { .. } => 5.0,
LocalMethod::LocalRBF { .. } => 8.0,
};
let chunk_size = estimate_chunk_size(n_queries, cost_factor, ¶llel_config);
let results: Result<Vec<F>, InterpolateError> = (0..n_queries)
.into_par_iter()
.with_min_len(chunk_size)
.map(|i| {
let query = queries.slice(scirs2_core::ndarray::s![i, ..]);
self.interpolate(&query.view())
})
.collect::<Result<Vec<F>, InterpolateError>>();
Ok(Array1::from_vec(results?))
}
pub fn stats(&self) -> &InterpolatorStats {
&self.stats
}
pub fn effective_dimensions(&self) -> usize {
self.stats
.reduced_dimensions
.unwrap_or(self.stats.original_dimensions)
}
pub fn training_size(&self) -> usize {
self.stats.n_training_points
}
}
#[allow(dead_code)]
pub fn make_knn_interpolator<F>(
points: &ArrayView2<F>,
values: &ArrayView1<F>,
k: usize,
) -> InterpolateResult<HighDimensionalInterpolator<F>>
where
F: Float
+ FromPrimitive
+ Debug
+ Display
+ Zero
+ Copy
+ AddAssign
+ ScalarOperand
+ 'static
+ Send
+ Sync
+ ordered_float::FloatCore,
{
HighDimensionalInterpolator::builder()
.with_local_method(LocalMethod::KNearestNeighbors {
k,
weight_power: 2.0,
})
.build(points, values)
}
#[allow(dead_code)]
pub fn make_pca_interpolator<F>(
points: &ArrayView2<F>,
values: &ArrayView1<F>,
target_dims: usize,
k: usize,
) -> InterpolateResult<HighDimensionalInterpolator<F>>
where
F: Float
+ FromPrimitive
+ Debug
+ Display
+ Zero
+ Copy
+ AddAssign
+ ScalarOperand
+ 'static
+ Send
+ Sync
+ ordered_float::FloatCore,
{
HighDimensionalInterpolator::builder()
.with_dimension_reduction(DimensionReductionMethod::PCA { target_dims })
.with_local_method(LocalMethod::KNearestNeighbors {
k,
weight_power: 2.0,
})
.build(points, values)
}
#[allow(dead_code)]
pub fn make_local_rbf_interpolator<F>(
points: &ArrayView2<F>,
values: &ArrayView1<F>,
radius: f64,
) -> InterpolateResult<HighDimensionalInterpolator<F>>
where
F: Float
+ FromPrimitive
+ Debug
+ Display
+ Zero
+ Copy
+ AddAssign
+ ScalarOperand
+ 'static
+ Send
+ Sync
+ ordered_float::FloatCore,
{
HighDimensionalInterpolator::builder()
.with_local_method(LocalMethod::LocalRBF {
radius,
rbf_type: LocalRBFType::CompactGaussian,
})
.build(points, values)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn test_high_dimensional_interpolator_creation() {
let points = array![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0]
];
let values = array![0.0, 1.0, 1.0, 1.0];
let interpolator = HighDimensionalInterpolator::<f64>::builder()
.build(&points.view(), &values.view())
.expect("Operation failed");
assert_eq!(interpolator.effective_dimensions(), 3);
assert_eq!(interpolator.training_size(), 4);
}
#[test]
fn test_knn_interpolation() {
let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
let values = array![0.0, 1.0, 1.0, 2.0];
let interpolator =
make_knn_interpolator(&points.view(), &values.view(), 2).expect("Operation failed");
let query = array![0.5, 0.5];
let result = interpolator
.interpolate(&query.view())
.expect("Operation failed");
assert!((0.5..=1.5).contains(&result));
}
#[test]
fn test_pca_interpolation() {
let points = array![
[1.0, 1.0, 1.0],
[2.0, 2.0, 2.0],
[1.0, 2.0, 3.0],
[3.0, 1.0, 1.0]
];
let values = array![1.0, 2.0, 3.0, 2.0];
let interpolator =
make_pca_interpolator(&points.view(), &values.view(), 2, 3).expect("Operation failed");
assert_eq!(interpolator.effective_dimensions(), 2);
let query = array![1.5, 1.5, 1.5];
let result = interpolator
.interpolate(&query.view())
.expect("Operation failed");
assert!((0.5..=3.5).contains(&result));
}
#[test]
fn test_local_rbf_interpolation() {
let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
let values = array![0.0, 1.0, 1.0, 2.0];
let interpolator = make_local_rbf_interpolator(&points.view(), &values.view(), 1.5)
.expect("Operation failed");
let query = array![0.5, 0.5];
let result = interpolator
.interpolate(&query.view())
.expect("Operation failed");
assert!((0.0..=2.0).contains(&result));
}
#[test]
fn test_multi_interpolation() {
let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
let values = array![0.0, 1.0, 1.0, 2.0];
let interpolator =
make_knn_interpolator(&points.view(), &values.view(), 3).expect("Operation failed");
let queries = array![[0.25, 0.25], [0.75, 0.75]];
let results = interpolator
.interpolate_multi(&queries.view())
.expect("Operation failed");
assert_eq!(results.len(), 2);
assert!(results[0] >= 0.0 && results[0] <= 2.0);
assert!(results[1] >= 0.0 && results[1] <= 2.0);
}
#[test]
fn test_dimension_reduction_methods() {
let points = array![
[1.0, 2.0, 3.0, 4.0],
[2.0, 3.0, 4.0, 5.0],
[3.0, 4.0, 5.0, 6.0]
];
let values = array![1.0, 2.0, 3.0];
let pca_interp = HighDimensionalInterpolator::<f64>::builder()
.with_dimension_reduction(DimensionReductionMethod::PCA { target_dims: 2 })
.build(&points.view(), &values.view())
.expect("Operation failed");
assert_eq!(pca_interp.effective_dimensions(), 2);
let rp_interp = HighDimensionalInterpolator::builder()
.with_dimension_reduction(DimensionReductionMethod::RandomProjection { target_dims: 2 })
.build(&points.view(), &values.view())
.expect("Operation failed");
assert_eq!(rp_interp.effective_dimensions(), 2);
}
#[test]
fn test_builder_pattern() {
let points = array![[0.0, 0.0], [1.0, 1.0]];
let values = array![0.0, 1.0];
let interpolator = HighDimensionalInterpolator::builder()
.with_local_method(LocalMethod::LocallyWeighted {
bandwidth: 1.0,
degree: 1,
})
.with_spatial_index(SpatialIndexType::BruteForce)
.build(&points.view(), &values.view())
.expect("Operation failed");
assert_eq!(interpolator.training_size(), 2);
}
}