1use pyo3::prelude::*;
6use pyo3::types::PyDict;
7use scirs2_core::ndarray::Array2 as Array2_17;
8use scirs2_core::python::numpy_compat::{
9 scirs_to_numpy_array1, scirs_to_numpy_array2, Array1, Array2,
10};
11use scirs2_numpy::{PyArray1, PyArray2, PyReadonlyArray1, PyReadonlyArray2};
12
13use scirs2_spatial::distance::EuclideanDistance;
15use scirs2_spatial::KDTree;
16
17use scirs2_spatial::convex_hull::ConvexHull;
19
20#[pyfunction]
26fn euclidean_py(u: PyReadonlyArray1<f64>, v: PyReadonlyArray1<f64>) -> PyResult<f64> {
27 let u_arr = u.as_array();
28 let v_arr = v.as_array();
29
30 if u_arr.len() != v_arr.len() {
31 return Err(pyo3::exceptions::PyValueError::new_err(
32 "Arrays must have same length",
33 ));
34 }
35
36 let dist: f64 = u_arr
37 .iter()
38 .zip(v_arr.iter())
39 .map(|(a, b)| (a - b).powi(2))
40 .sum::<f64>()
41 .sqrt();
42
43 Ok(dist)
44}
45
46#[pyfunction]
48fn cityblock_py(u: PyReadonlyArray1<f64>, v: PyReadonlyArray1<f64>) -> PyResult<f64> {
49 let u_arr = u.as_array();
50 let v_arr = v.as_array();
51
52 if u_arr.len() != v_arr.len() {
53 return Err(pyo3::exceptions::PyValueError::new_err(
54 "Arrays must have same length",
55 ));
56 }
57
58 let dist: f64 = u_arr
59 .iter()
60 .zip(v_arr.iter())
61 .map(|(a, b)| (a - b).abs())
62 .sum();
63
64 Ok(dist)
65}
66
67#[pyfunction]
69fn chebyshev_py(u: PyReadonlyArray1<f64>, v: PyReadonlyArray1<f64>) -> PyResult<f64> {
70 let u_arr = u.as_array();
71 let v_arr = v.as_array();
72
73 if u_arr.len() != v_arr.len() {
74 return Err(pyo3::exceptions::PyValueError::new_err(
75 "Arrays must have same length",
76 ));
77 }
78
79 let dist: f64 = u_arr
80 .iter()
81 .zip(v_arr.iter())
82 .map(|(a, b)| (a - b).abs())
83 .fold(0.0, f64::max);
84
85 Ok(dist)
86}
87
88#[pyfunction]
90fn minkowski_py(u: PyReadonlyArray1<f64>, v: PyReadonlyArray1<f64>, p: f64) -> PyResult<f64> {
91 let u_arr = u.as_array();
92 let v_arr = v.as_array();
93
94 if u_arr.len() != v_arr.len() {
95 return Err(pyo3::exceptions::PyValueError::new_err(
96 "Arrays must have same length",
97 ));
98 }
99
100 let dist: f64 = u_arr
101 .iter()
102 .zip(v_arr.iter())
103 .map(|(a, b)| (a - b).abs().powf(p))
104 .sum::<f64>()
105 .powf(1.0 / p);
106
107 Ok(dist)
108}
109
110#[pyfunction]
112fn cosine_py(u: PyReadonlyArray1<f64>, v: PyReadonlyArray1<f64>) -> PyResult<f64> {
113 let u_arr = u.as_array();
114 let v_arr = v.as_array();
115
116 if u_arr.len() != v_arr.len() {
117 return Err(pyo3::exceptions::PyValueError::new_err(
118 "Arrays must have same length",
119 ));
120 }
121
122 let dot: f64 = u_arr.iter().zip(v_arr.iter()).map(|(a, b)| a * b).sum();
123 let norm_u: f64 = u_arr.iter().map(|a| a.powi(2)).sum::<f64>().sqrt();
124 let norm_v: f64 = v_arr.iter().map(|a| a.powi(2)).sum::<f64>().sqrt();
125
126 if norm_u == 0.0 || norm_v == 0.0 {
127 return Err(pyo3::exceptions::PyValueError::new_err("Zero vector"));
128 }
129
130 Ok(1.0 - dot / (norm_u * norm_v))
131}
132
133#[pyfunction]
149fn spatial_hamming_distance_py(
150 u: PyReadonlyArray1<f64>,
151 v: PyReadonlyArray1<f64>,
152) -> PyResult<f64> {
153 let u_arr = u.as_array();
154 let v_arr = v.as_array();
155
156 if u_arr.len() != v_arr.len() {
157 return Err(pyo3::exceptions::PyValueError::new_err(
158 "Arrays must have same length",
159 ));
160 }
161
162 if u_arr.is_empty() {
163 return Ok(0.0);
164 }
165
166 let n_diff = u_arr
167 .iter()
168 .zip(v_arr.iter())
169 .filter(|&(a, b)| (a - b).abs() > f64::EPSILON)
170 .count();
171
172 Ok(n_diff as f64 / u_arr.len() as f64)
173}
174
175#[pyfunction]
181#[pyo3(signature = (x, metric="euclidean"))]
182fn pdist_py(py: Python, x: PyReadonlyArray2<f64>, metric: &str) -> PyResult<Py<PyArray1<f64>>> {
183 let x_arr = x.as_array();
184 let n = x_arr.nrows();
185
186 let n_dist = n * (n - 1) / 2;
188 let mut result = Vec::with_capacity(n_dist);
189
190 for i in 0..n {
191 for j in (i + 1)..n {
192 let dist = match metric {
193 "euclidean" => x_arr
194 .row(i)
195 .iter()
196 .zip(x_arr.row(j).iter())
197 .map(|(a, b)| (a - b).powi(2))
198 .sum::<f64>()
199 .sqrt(),
200 "cityblock" | "manhattan" => x_arr
201 .row(i)
202 .iter()
203 .zip(x_arr.row(j).iter())
204 .map(|(a, b)| (a - b).abs())
205 .sum(),
206 "chebyshev" => x_arr
207 .row(i)
208 .iter()
209 .zip(x_arr.row(j).iter())
210 .map(|(a, b)| (a - b).abs())
211 .fold(0.0, f64::max),
212 "hamming" => {
213 let n_cols = x_arr.ncols();
214 if n_cols == 0 {
215 0.0
216 } else {
217 let n_diff = x_arr
218 .row(i)
219 .iter()
220 .zip(x_arr.row(j).iter())
221 .filter(|&(a, b)| (a - b).abs() > f64::EPSILON)
222 .count();
223 n_diff as f64 / n_cols as f64
224 }
225 }
226 _ => x_arr
227 .row(i)
228 .iter()
229 .zip(x_arr.row(j).iter())
230 .map(|(a, b)| (a - b).powi(2))
231 .sum::<f64>()
232 .sqrt(),
233 };
234 result.push(dist);
235 }
236 }
237
238 scirs_to_numpy_array1(Array1::from_vec(result), py)
239}
240
241#[pyfunction]
243#[pyo3(signature = (xa, xb, metric="euclidean"))]
244fn cdist_py(
245 py: Python,
246 xa: PyReadonlyArray2<f64>,
247 xb: PyReadonlyArray2<f64>,
248 metric: &str,
249) -> PyResult<Py<PyArray2<f64>>> {
250 let xa_arr = xa.as_array();
251 let xb_arr = xb.as_array();
252 let na = xa_arr.nrows();
253 let nb = xb_arr.nrows();
254
255 if xa_arr.ncols() != xb_arr.ncols() {
256 return Err(pyo3::exceptions::PyValueError::new_err(
257 "Arrays must have same number of columns",
258 ));
259 }
260
261 let mut result = Vec::with_capacity(na * nb);
262
263 for i in 0..na {
264 for j in 0..nb {
265 let dist = match metric {
266 "euclidean" => xa_arr
267 .row(i)
268 .iter()
269 .zip(xb_arr.row(j).iter())
270 .map(|(a, b)| (a - b).powi(2))
271 .sum::<f64>()
272 .sqrt(),
273 "cityblock" | "manhattan" => xa_arr
274 .row(i)
275 .iter()
276 .zip(xb_arr.row(j).iter())
277 .map(|(a, b)| (a - b).abs())
278 .sum(),
279 "chebyshev" => xa_arr
280 .row(i)
281 .iter()
282 .zip(xb_arr.row(j).iter())
283 .map(|(a, b)| (a - b).abs())
284 .fold(0.0, f64::max),
285 "hamming" => {
286 let n_cols = xa_arr.ncols();
287 if n_cols == 0 {
288 0.0
289 } else {
290 let n_diff = xa_arr
291 .row(i)
292 .iter()
293 .zip(xb_arr.row(j).iter())
294 .filter(|&(a, b)| (a - b).abs() > f64::EPSILON)
295 .count();
296 n_diff as f64 / n_cols as f64
297 }
298 }
299 _ => xa_arr
300 .row(i)
301 .iter()
302 .zip(xb_arr.row(j).iter())
303 .map(|(a, b)| (a - b).powi(2))
304 .sum::<f64>()
305 .sqrt(),
306 };
307 result.push(dist);
308 }
309 }
310
311 let arr = Array2::from_shape_vec((na, nb), result)
313 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
314
315 scirs_to_numpy_array2(arr, py)
316}
317
318#[pyfunction]
320fn squareform_py(py: Python, x: PyReadonlyArray1<f64>) -> PyResult<Py<PyArray2<f64>>> {
321 let x_arr = x.as_array();
322 let n_dist = x_arr.len();
323
324 let n = ((1.0 + (1.0 + 8.0 * n_dist as f64).sqrt()) / 2.0) as usize;
326
327 let mut result = Array2::zeros((n, n));
328
329 let mut idx = 0;
330 for i in 0..n {
331 for j in (i + 1)..n {
332 result[[i, j]] = x_arr[idx];
333 result[[j, i]] = x_arr[idx];
334 idx += 1;
335 }
336 }
337
338 scirs_to_numpy_array2(result, py)
339}
340
341#[pyfunction]
349fn convex_hull_py(py: Python, points: PyReadonlyArray2<f64>) -> PyResult<Py<PyAny>> {
350 let points_arr = points.as_array();
351 let n = points_arr.nrows();
352 let k = points_arr.ncols();
353
354 let mut pts = Vec::with_capacity(n * k);
356 for row in points_arr.rows() {
357 for &val in row.iter() {
358 pts.push(val);
359 }
360 }
361 let arr = Array2_17::from_shape_vec((n, k), pts)
362 .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
363
364 let hull = ConvexHull::new(&arr.view())
365 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
366
367 let vertices: Vec<i64> = hull.vertex_indices().iter().map(|&i| i as i64).collect();
369 let simplices: Vec<Vec<i64>> = hull
370 .simplices()
371 .iter()
372 .map(|s| s.iter().map(|&i| i as i64).collect())
373 .collect();
374
375 let volume = hull.volume().unwrap_or(0.0);
377 let area = hull.area().unwrap_or(0.0);
378
379 let dict = PyDict::new(py);
380 dict.set_item(
381 "vertices",
382 scirs_to_numpy_array1(Array1::from_vec(vertices), py)?,
383 )?;
384
385 let simplices_py: Vec<Vec<i64>> = simplices;
387 dict.set_item("simplices", simplices_py)?;
388 dict.set_item("volume", volume)?;
389 dict.set_item("area", area)?;
390
391 Ok(dict.into())
392}
393
394#[pyclass(name = "ConvexHullPy", unsendable)]
396pub struct PyConvexHull {
397 hull: ConvexHull,
398}
399
400#[pymethods]
401impl PyConvexHull {
402 #[new]
407 fn new(points: PyReadonlyArray2<f64>) -> PyResult<Self> {
408 let points_arr = points.as_array();
409 let n = points_arr.nrows();
410 let k = points_arr.ncols();
411
412 let mut pts = Vec::with_capacity(n * k);
414 for row in points_arr.rows() {
415 for &val in row.iter() {
416 pts.push(val);
417 }
418 }
419 let arr = Array2_17::from_shape_vec((n, k), pts)
420 .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
421
422 let hull = ConvexHull::new(&arr.view())
423 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
424
425 Ok(PyConvexHull { hull })
426 }
427
428 fn vertices(&self, py: Python) -> PyResult<Py<PyArray1<i64>>> {
430 let vertices: Vec<i64> = self
431 .hull
432 .vertex_indices()
433 .iter()
434 .map(|&i| i as i64)
435 .collect();
436 scirs_to_numpy_array1(Array1::from_vec(vertices), py)
437 }
438
439 fn simplices(&self) -> Vec<Vec<i64>> {
441 self.hull
442 .simplices()
443 .iter()
444 .map(|s| s.iter().map(|&i| i as i64).collect())
445 .collect()
446 }
447
448 fn volume(&self) -> PyResult<f64> {
450 self.hull
451 .volume()
452 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
453 }
454
455 fn area(&self) -> PyResult<f64> {
457 self.hull
458 .area()
459 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
460 }
461
462 fn contains(&self, point: PyReadonlyArray1<f64>) -> PyResult<bool> {
464 let point_vec: Vec<f64> = point.as_array().to_vec();
465 self.hull
466 .contains(&point_vec)
467 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
468 }
469}
470
471#[pyclass(name = "KDTree")]
477pub struct PyKDTree {
478 tree: KDTree<f64, EuclideanDistance<f64>>,
479}
480
481#[pymethods]
482impl PyKDTree {
483 #[new]
488 fn new(data: PyReadonlyArray2<f64>) -> PyResult<Self> {
489 let data_arr = data.as_array();
490 let n = data_arr.nrows();
491 let k = data_arr.ncols();
492
493 let mut points = Vec::with_capacity(n * k);
495 for row in data_arr.rows() {
496 for &val in row.iter() {
497 points.push(val);
498 }
499 }
500 let arr = Array2_17::from_shape_vec((n, k), points)
501 .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
502
503 let tree = KDTree::new(&arr)
504 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
505
506 Ok(PyKDTree { tree })
507 }
508
509 fn query(&self, py: Python, point: PyReadonlyArray1<f64>, k: usize) -> PyResult<Py<PyAny>> {
518 let point_vec: Vec<f64> = point.as_array().to_vec();
519
520 let (indices, distances) = self
521 .tree
522 .query(&point_vec, k)
523 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
524
525 let dict = PyDict::new(py);
526 dict.set_item(
527 "indices",
528 scirs_to_numpy_array1(
529 Array1::from_vec(indices.iter().map(|&i| i as i64).collect()),
530 py,
531 )?,
532 )?;
533 dict.set_item(
534 "distances",
535 scirs_to_numpy_array1(Array1::from_vec(distances), py)?,
536 )?;
537
538 Ok(dict.into())
539 }
540
541 fn query_radius(
550 &self,
551 py: Python,
552 point: PyReadonlyArray1<f64>,
553 r: f64,
554 ) -> PyResult<Py<PyAny>> {
555 let point_vec: Vec<f64> = point.as_array().to_vec();
556
557 let (indices, distances) = self
558 .tree
559 .query_radius(&point_vec, r)
560 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
561
562 let dict = PyDict::new(py);
563 dict.set_item(
564 "indices",
565 scirs_to_numpy_array1(
566 Array1::from_vec(indices.iter().map(|&i| i as i64).collect()),
567 py,
568 )?,
569 )?;
570 dict.set_item(
571 "distances",
572 scirs_to_numpy_array1(Array1::from_vec(distances), py)?,
573 )?;
574
575 Ok(dict.into())
576 }
577}
578
579pub fn register_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
581 m.add_function(wrap_pyfunction!(euclidean_py, m)?)?;
583 m.add_function(wrap_pyfunction!(cityblock_py, m)?)?;
584 m.add_function(wrap_pyfunction!(chebyshev_py, m)?)?;
585 m.add_function(wrap_pyfunction!(minkowski_py, m)?)?;
586 m.add_function(wrap_pyfunction!(cosine_py, m)?)?;
587 m.add_function(wrap_pyfunction!(spatial_hamming_distance_py, m)?)?;
588
589 m.add_function(wrap_pyfunction!(pdist_py, m)?)?;
591 m.add_function(wrap_pyfunction!(cdist_py, m)?)?;
592 m.add_function(wrap_pyfunction!(squareform_py, m)?)?;
593
594 m.add_function(wrap_pyfunction!(convex_hull_py, m)?)?;
596 m.add_class::<PyConvexHull>()?;
597
598 m.add_class::<PyKDTree>()?;
600
601 Ok(())
602}