Skip to content

Commit 7363e1e

Browse files
committed
Add test to verify robustness
Tests to produce png output of output from `orient2d` over a grid of close-by floats. These are ignored by default, and should be explicitly run.
1 parent 7523d68 commit 7363e1e

File tree

7 files changed

+314
-59
lines changed

7 files changed

+314
-59
lines changed

geo/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ use-serde = ["serde", "geo-types/serde"]
3434
[dev-dependencies]
3535
approx = "0.3.0"
3636
criterion = { version = "0.3" }
37+
png = "0.16.7"
38+
float_extras = "0.1.6"
3739

3840
[[bench]]
3941
name = "area"

geo/src/algorithm/kernels/mod.rs

+21-16
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,44 @@
11
use crate::{CoordinateType, Coordinate};
22
use super::winding_order::WindingOrder;
33

4+
/// Kernel trait to provide predicates to operate on
5+
/// different scalar types.
46
pub trait Kernel {
57
type Scalar: CoordinateType;
68

9+
/// Gives the orientation of 3 2-dimensional points:
10+
/// ccw, cw or colinear (None)
711
fn orient2d(
812
p: Coordinate<Self::Scalar>,
913
q: Coordinate<Self::Scalar>,
1014
r: Coordinate<Self::Scalar>,
1115
) -> Option<WindingOrder>;
1216
}
1317

14-
/// Marker trait
18+
/// Marker trait to assign Kernel for scalars
1519
pub trait HasKernel: CoordinateType {
1620
type Ker: Kernel<Scalar = Self>;
1721
}
1822

23+
#[macro_export]
24+
macro_rules! has_kernel {
25+
($t:ident, $k:ident) => {
26+
impl $crate::algorithm::kernels::HasKernel for $t {
27+
type Ker = $k<$t>;
28+
}
29+
};
30+
}
31+
1932
pub mod robust;
2033
pub use self::robust::RobustKernel;
34+
has_kernel!(f64, RobustKernel);
35+
has_kernel!(f32, RobustKernel);
36+
2137

2238
pub mod simple;
2339
pub use self::simple::SimpleKernel;
40+
has_kernel!(i64, SimpleKernel);
41+
has_kernel!(i32, SimpleKernel);
2442

25-
impl HasKernel for f64 {
26-
type Ker = RobustKernel<f64>;
27-
}
28-
29-
impl HasKernel for f32 {
30-
type Ker = RobustKernel<f32>;
31-
}
32-
33-
impl HasKernel for i64 {
34-
type Ker = SimpleKernel<i64>;
35-
}
36-
37-
impl HasKernel for i32 {
38-
type Ker = SimpleKernel<i32>;
39-
}
43+
#[cfg(test)]
44+
mod test;

geo/src/algorithm/kernels/robust.rs

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ use crate::Coordinate;
33
use crate::algorithm::winding_order::WindingOrder;
44
use std::marker::PhantomData;
55

6+
/// Robust kernel that uses [fast robust
7+
/// predicates](//www.cs.cmu.edu/~quake/robust.html) to
8+
/// provide robust floating point predicates. Should only be
9+
/// used with types that can _always_ be casted to `f64`
10+
/// _without loss in precision_.
611
#[derive(Default)]
712
pub struct RobustKernel<T>(PhantomData<T>);
813

geo/src/algorithm/kernels/simple.rs

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ use crate::{Coordinate, CoordinateType};
33
use crate::algorithm::winding_order::WindingOrder;
44
use std::marker::PhantomData;
55

6+
/// Simple kernel provides the direct implementation of the
7+
/// predicates. These are meant to be used with exact
8+
/// arithmetic signed tpyes (eg. i32, i64).
69
#[derive(Default)]
710
pub struct SimpleKernel<T>(PhantomData<T>);
811

geo/src/algorithm/kernels/test.rs

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
use super::*;
2+
use num_traits::*;
3+
4+
// Define a SimpleFloat to test non-robustness of naive
5+
// implementation.
6+
#[derive(Copy, Clone, PartialEq, PartialOrd)]
7+
struct SimpleFloat(pub f64);
8+
9+
use std::ops::*;
10+
use crate::has_kernel;
11+
12+
macro_rules! impl_ops {
13+
($t:ident, $m:ident) => {
14+
impl $t for SimpleFloat {
15+
type Output = Self;
16+
fn $m(self, rhs: Self) -> Self {
17+
SimpleFloat(self.0.$m(rhs.0))
18+
}
19+
}
20+
};
21+
}
22+
23+
impl_ops!(Rem, rem);
24+
impl_ops!(Div, div);
25+
impl_ops!(Mul, mul);
26+
impl_ops!(Sub, sub);
27+
impl_ops!(Add, add);
28+
29+
impl One for SimpleFloat {
30+
fn one() -> Self {
31+
SimpleFloat(One::one())
32+
}
33+
}
34+
impl Zero for SimpleFloat {
35+
fn zero() -> Self {
36+
SimpleFloat(Zero::zero())
37+
}
38+
39+
fn is_zero(&self) -> bool {
40+
Zero::is_zero(&self.0)
41+
}
42+
}
43+
44+
45+
impl Num for SimpleFloat {
46+
type FromStrRadixErr = ParseFloatError;
47+
48+
fn from_str_radix(str: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
49+
Num::from_str_radix(str, radix).map(SimpleFloat)
50+
}
51+
}
52+
53+
macro_rules! tp_method {
54+
($t:ident, $m:ident) => {
55+
fn $m(&self) -> Option<$t> {
56+
Some(self.0 as $t)
57+
}
58+
};
59+
}
60+
61+
impl ToPrimitive for SimpleFloat{
62+
tp_method!(i64, to_i64);
63+
tp_method!(u64, to_u64);
64+
tp_method!(f64, to_f64);
65+
}
66+
67+
impl NumCast for SimpleFloat {
68+
fn from<T: ToPrimitive>(n: T) -> Option<Self> {
69+
NumCast::from(n).map(SimpleFloat)
70+
}
71+
}
72+
73+
impl From<f64> for SimpleFloat {
74+
fn from(f: f64) -> Self {
75+
SimpleFloat(f)
76+
}
77+
}
78+
79+
has_kernel!(SimpleFloat, SimpleKernel);
80+
81+
fn orient2d_tests<T: From<f64> + CoordinateType + HasKernel>(
82+
x1: f64, y1: f64,
83+
x2: f64, y2: f64,
84+
x3: f64, y3: f64,
85+
width: usize, height: usize,
86+
) -> Vec<Option<WindingOrder>> {
87+
let p1 = Coordinate{
88+
x: <T as From<_>>::from(x1),
89+
y: <T as From<_>>::from(y1),
90+
};
91+
let p3 = Coordinate{
92+
x: <T as From<_>>::from(x3),
93+
y: <T as From<_>>::from(y3),
94+
};
95+
96+
use float_extras::f64::nextafter;
97+
let mut yd2 = y2;
98+
let mut data = Vec::with_capacity(width * height);
99+
100+
for _ in 0..height {
101+
let mut xd2 = x2;
102+
for _ in 0..width {
103+
let p2 = Coordinate{
104+
x: <T as From<_>>::from(xd2),
105+
y: <T as From<_>>::from(yd2),
106+
};
107+
xd2 = nextafter(xd2, f64::INFINITY);
108+
data.push(<T as HasKernel>::Ker::orient2d(p1, p2, p3));
109+
}
110+
yd2 = nextafter(yd2, f64::INFINITY);
111+
}
112+
113+
data
114+
}
115+
116+
use std::path::Path;
117+
fn write_png(
118+
data: &[Option<WindingOrder>],
119+
path: &Path,
120+
width: usize, height: usize,
121+
) {
122+
assert_eq!(data.len(), width * height);
123+
124+
use std::fs::File;
125+
use std::io::BufWriter;
126+
127+
let file = File::create(path).unwrap();
128+
let ref mut w = BufWriter::new(file);
129+
130+
let mut encoder = png::Encoder::new(w, width as u32, height as u32);
131+
encoder.set_color(png::ColorType::Grayscale);
132+
encoder.set_depth(png::BitDepth::Eight);
133+
134+
let mut writer = encoder.write_header().unwrap();
135+
let data = data.iter().map(|w| {
136+
match w {
137+
Some(WindingOrder::Clockwise) => 0u8,
138+
None => 127,
139+
Some(WindingOrder::CounterClockwise) => 255,
140+
}
141+
}).collect::<Vec<_>>();
142+
writer.write_image_data(&data).unwrap();
143+
}
144+
145+
#[test]
146+
#[ignore]
147+
fn test_naive() {
148+
let data = orient2d_tests::<SimpleFloat>(
149+
12.0, 12.0,
150+
0.5, 0.5,
151+
24.0, 24.0,
152+
256, 256
153+
);
154+
write_png(&data, Path::new("naive-orientation-map.png"), 256, 256);
155+
}
156+
157+
#[test]
158+
#[ignore]
159+
fn test_robust() {
160+
let data = orient2d_tests::<f64>(
161+
12.0, 12.0,
162+
0.5, 0.5,
163+
24.0, 24.0,
164+
256, 256
165+
);
166+
write_png(&data, Path::new("robust-orientation-map.png"), 256, 256);
167+
}

0 commit comments

Comments
 (0)