Skip to content

Commit cf1b37d

Browse files
authored
Add 1D triangulation support (#3)
* feat: add 1d rust triangulation support * refactor: clarify 1d simplex filtering * refactor: consolidate 1d triangulation tests * docs: summarize issue-095 cleanup * Remove cleanup reports and fix pre-commit * fix: make 1D triangulation numerically robust Three related fixes for failures found on small intervals away from the origin (e.g. Triangulation(np.linspace(100, 100.001, 50).reshape(-1, 1)) crashed with an AssertionError from the volume-conservation check): - circumsphere: solve in coordinates relative to the first vertex. The generic path computed |x_i|^2 - |x_0|^2, which cancels catastrophically when a simplex is small compared to its distance from the origin (center error ~1e-11 for a 1e-6 interval at x=0.5, vs the 5e-15 circumcircle tolerance). 1D always uses this path; dims >= 4 had the same latent inaccuracy. - point_in_circumcircle: short-circuit to true when the point is a vertex of the simplex, so rounding in the circumcenter cannot flip the answer bowyer_watson relies on after extend_hull. - bowyer_watson: skip the circumcircle cascade in 1D. Splitting an interval never invalidates neighbouring intervals, and cascading on the (1 + eps) tolerance could delete a neighbour the point is strictly outside of, orphaning the shared vertex and leaving overlapping segments. Also fixes clippy lints from newer toolchains and renames a test that now asserts a deliberate divergence from the reference. * perf: construct 1D triangulations from sorted adjacency 1D Delaunay triangulation is just the chain of adjacent points in sorted order, so build it directly instead of running O(n^2) incremental insertion (full-face scans in extend_hull made constructing 2000 sorted points take ~860 ms; now 50k points build in ~17 ms). Exact duplicates keep the first occurrence, matching the incremental path's behaviour.
1 parent 4a16a9d commit cf1b37d

3 files changed

Lines changed: 444 additions & 59 deletions

File tree

‎src/geometry.rs‎

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -288,34 +288,31 @@ pub fn circumsphere(pts: &[Vec<f64>]) -> Result<(Vec<f64>, f64), GeometryError>
288288
return Ok((center.into_iter().collect(), radius));
289289
}
290290

291+
// Solve in coordinates relative to the first vertex: computing
292+
// |x_i|^2 - |x_0|^2 directly cancels catastrophically when the simplex
293+
// is small compared to its distance from the origin.
291294
let x0 = &pts[0];
292-
let x0_sq = squared_norm(x0);
293295
let mut matrix = vec![vec![0.0; dim]; dim];
294296
let mut rhs = vec![0.0; dim];
295297

296298
for row in 0..dim {
297-
let point = &pts[row + 1];
298-
rhs[row] = squared_norm(point) - x0_sq;
299-
for col in 0..dim {
300-
matrix[row][col] = 2.0 * (point[col] - x0[col]);
299+
let translated: Vec<f64> = pts[row + 1].iter().zip(x0).map(|(a, b)| a - b).collect();
300+
rhs[row] = squared_norm(&translated);
301+
for (col, value) in translated.iter().enumerate() {
302+
matrix[row][col] = 2.0 * value;
301303
}
302304
}
303305

304-
let center = match solve_square(&matrix, &rhs) {
306+
let relative_center = match solve_square(&matrix, &rhs) {
305307
Ok(center) => center,
306308
Err(GeometryError::SingularMatrix) => {
307309
return Ok((vec![f64::NAN; dim], f64::NAN));
308310
}
309311
Err(err) => return Err(err),
310312
};
311313

312-
let radius = fast_norm(
313-
&center
314-
.iter()
315-
.zip(&pts[0])
316-
.map(|(x, y)| x - y)
317-
.collect::<Vec<_>>(),
318-
);
314+
let radius = fast_norm(&relative_center);
315+
let center: Vec<f64> = relative_center.iter().zip(x0).map(|(c, o)| c + o).collect();
319316
Ok((center, radius))
320317
}
321318

@@ -424,10 +421,12 @@ pub fn simplex_volume_in_embedding(vertices: &[Vec<f64>]) -> Result<f64, Geometr
424421
));
425422
}
426423

427-
if vertices[0].len() == 2 && vertices.len() != 3 {
428-
return Err(GeometryError::InvalidDimensions(
429-
"Expected three 2D vertices".to_string(),
430-
));
424+
if vertices.len() == 2 {
425+
let length_sq = squared_distance(&vertices[0], &vertices[1]);
426+
if length_sq == 0.0 {
427+
return Err(GeometryError::DegenerateSimplex);
428+
}
429+
return Ok(length_sq.sqrt());
431430
}
432431

433432
if vertices.len() == 3 {
@@ -465,3 +464,48 @@ pub fn simplex_volume_in_embedding(vertices: &[Vec<f64>]) -> Result<f64, Geometr
465464
}
466465
Ok(vol_square.sqrt())
467466
}
467+
468+
#[cfg(test)]
469+
mod tests {
470+
use super::*;
471+
472+
#[test]
473+
fn simplex_volume_in_embedding_returns_segment_length() {
474+
let length =
475+
simplex_volume_in_embedding(&[vec![0.0, 0.0, 0.0], vec![3.0, 4.0, 0.0]]).unwrap();
476+
assert!((length - 5.0).abs() < 1e-12);
477+
}
478+
479+
#[test]
480+
fn simplex_volume_in_embedding_rejects_identical_endpoints() {
481+
let err = simplex_volume_in_embedding(&[vec![1.0, 2.0], vec![1.0, 2.0]]).unwrap_err();
482+
assert!(matches!(err, GeometryError::DegenerateSimplex));
483+
}
484+
485+
#[test]
486+
fn circumsphere_supports_one_dimensional_segment() {
487+
let (center, radius) = circumsphere(&[vec![0.0], vec![2.0]]).unwrap();
488+
assert_eq!(center, vec![1.0]);
489+
assert!((radius - 1.0).abs() < 1e-12);
490+
}
491+
492+
#[test]
493+
fn circumsphere_is_accurate_for_small_simplices_far_from_origin() {
494+
let (center, radius) = circumsphere(&[vec![0.5], vec![0.5 + 1e-6]]).unwrap();
495+
assert!((center[0] - (0.5 + 5e-7)).abs() < 1e-15);
496+
assert!((radius - 5e-7).abs() < 1e-15);
497+
498+
let (center, radius) = circumsphere(&[
499+
vec![100.0, 100.0, 100.0, 100.0],
500+
vec![100.0 + 1e-5, 100.0, 100.0, 100.0],
501+
vec![100.0, 100.0 + 1e-5, 100.0, 100.0],
502+
vec![100.0, 100.0, 100.0 + 1e-5, 100.0],
503+
vec![100.0, 100.0, 100.0, 100.0 + 1e-5],
504+
])
505+
.unwrap();
506+
for coord in &center {
507+
assert!((coord - (100.0 + 5e-6)).abs() < 1e-12);
508+
}
509+
assert!((radius - 1e-5).abs() < 1e-12);
510+
}
511+
}

0 commit comments

Comments
 (0)