use super::placement::point_in_polygon;
use super::*;
use crate::foundation::{BBox, Lattice};
use crate::sampling::Sampler;
use ndarray::Array2;
fn extent() -> BBox {
BBox {
xmin: 0.0,
ymin: 0.0,
xmax: 100.0,
ymax: 200.0,
}
}
#[test]
fn place_wells_in_extent_and_reproducible() {
let e = extent();
let a = place_wells(&e, 25, 42).unwrap();
let b = place_wells(&e, 25, 42).unwrap();
assert_eq!(a, b);
assert_eq!(a.len(), 25);
for w in &a {
assert!(w[0] >= e.xmin && w[0] < e.xmax && w[1] >= e.ymin && w[1] < e.ymax);
}
assert!(place_wells(&e, 0, 1).unwrap().is_empty());
}
#[test]
fn degenerate_extent_errors() {
let bad = BBox {
xmin: 1.0,
ymin: 0.0,
xmax: 1.0,
ymax: 10.0,
};
assert!(place_wells(&bad, 5, 1).is_err());
}
#[test]
fn polygon_placement_stays_inside() {
let poly = [[0.0, 0.0], [100.0, 0.0], [0.0, 100.0]];
let ws = place_wells_in_polygon(&poly, 40, 7).unwrap();
assert_eq!(ws.len(), 40);
for w in &ws {
assert!(point_in_polygon(w[0], w[1], &poly), "point outside polygon");
}
let ws2 = place_wells_in_polygon(&poly, 40, 7).unwrap();
assert_eq!(ws, ws2);
}
#[test]
fn polygon_needs_three_vertices() {
assert!(place_wells_in_polygon(&[[0.0, 0.0], [1.0, 1.0]], 3, 1).is_err());
}
#[test]
fn tops_sample_and_add_residual() {
let lat = Lattice::regular(0.0, 0.0, 10.0, 10.0, 11, 11);
let surface = Array2::from_shape_fn((11, 11), |(i, _j)| lat.node_xy(i, 0).0);
let wells = vec![[15.0, 20.0], [55.0, 80.0]];
let resid = Sampler::new_uniform(-1e-9, 1e-9).unwrap();
let tops = tops_from_surface(&surface, &lat, &wells, &resid, 3);
assert!((tops[0] - 15.0).abs() < 1e-3, "top0 {}", tops[0]);
assert!((tops[1] - 55.0).abs() < 1e-3, "top1 {}", tops[1]);
}
#[test]
fn tops_outside_extent_are_nan() {
let lat = Lattice::regular(0.0, 0.0, 10.0, 10.0, 11, 11);
let surface = Array2::from_elem((11, 11), 5.0);
let wells = vec![[1000.0, 1000.0]];
let resid = Sampler::new_normal(0.0, 1.0).unwrap();
let tops = tops_from_surface(&surface, &lat, &wells, &resid, 1);
assert!(tops[0].is_nan());
}
#[test]
fn tops_residual_reproducible() {
let lat = Lattice::regular(0.0, 0.0, 10.0, 10.0, 11, 11);
let surface = Array2::from_elem((11, 11), 50.0);
let wells = vec![[15.0, 20.0], [55.0, 80.0]];
let resid = Sampler::new_normal(0.0, 10.0).unwrap();
let a = tops_from_surface(&surface, &lat, &wells, &resid, 99);
let b = tops_from_surface(&surface, &lat, &wells, &resid, 99);
assert_eq!(a, b);
assert!(
a.iter().any(|&v| (v - 50.0).abs() > 1e-6),
"residual not applied"
);
}
#[test]
fn vertical_trajectory_is_well_formed() {
let t = synth_trajectory([500.0, 900.0], 30.0, 2500.0, 100.0, 1).unwrap();
assert_eq!(t.stations.first().unwrap().md, 0.0);
assert!((t.stations.last().unwrap().tvd - 2500.0).abs() < 1e-9);
for s in &t.stations {
assert_eq!(s.x, 500.0);
assert_eq!(s.y, 900.0);
assert_eq!(s.md, s.tvd);
assert_eq!(s.incl, 0.0);
assert_eq!(s.azim, 0.0);
assert!((s.z - (30.0 - s.tvd)).abs() < 1e-9);
}
for w in t.stations.windows(2) {
assert!(w[1].md > w[0].md);
}
}
#[test]
fn trajectory_bad_args_error() {
assert!(synth_trajectory([0.0, 0.0], 0.0, 0.0, 100.0, 1).is_err());
assert!(synth_trajectory([0.0, 0.0], 0.0, 1000.0, 0.0, 1).is_err());
assert!(synth_trajectory([f64::NAN, 0.0], 0.0, 1000.0, 100.0, 1).is_err());
}
fn build_hold(kickoff: f64, rate: f64, hold: f64, az: f64) -> WellProfile {
WellProfile::BuildHold(BuildHold::new(kickoff, rate, hold, az).unwrap())
}
#[test]
fn vertical_profile_equals_synth_trajectory() {
let a = synth_trajectory([431_500.0, 6_521_900.0], 30.0, 2500.0, 25.0, 7).unwrap();
let b = synth_trajectory_profile(
[431_500.0, 6_521_900.0],
30.0,
2500.0,
25.0,
&WellProfile::Vertical,
7,
)
.unwrap();
assert_eq!(a, b);
}
#[test]
fn build_hold_matches_the_analytic_profile() {
let (kickoff, rate, hold, az) = (800.0, 3.0, 45.0, 90.0);
let wh = [431_000.0, 6_521_000.0];
let t = synth_trajectory_profile(
wh,
25.0,
2600.0,
10.0,
&build_hold(kickoff, rate, hold, az),
1,
)
.unwrap();
for s in t.stations.iter().filter(|s| s.md < kickoff - 1e-9) {
assert_eq!(s.incl, 0.0, "incl before kickoff");
assert!((s.tvd - s.md).abs() < 1e-6, "tvd!=md before kickoff");
assert!(
(s.x - wh[0]).abs() < 1e-6 && (s.y - wh[1]).abs() < 1e-6,
"moved before kickoff"
);
}
let build_len = hold / (rate / 30.0); for s in t.stations.iter() {
if s.md > kickoff + 1e-6 && s.md < kickoff + build_len - 1e-6 {
let expect = (s.md - kickoff) * (rate / 30.0);
assert!(
(s.incl - expect).abs() < 1e-9,
"build incl {} vs {}",
s.incl,
expect
);
}
}
let last = t.stations.last().unwrap();
assert!((last.incl - hold).abs() < 1e-9, "hold incl {}", last.incl);
assert!((last.azim - az).abs() < 1e-9, "hold azim {}", last.azim);
assert!(
last.x - wh[0] > 400.0,
"no eastward reach: {}",
last.x - wh[0]
);
assert!(
(last.y - wh[1]).abs() < 1e-6,
"unexpected northing move: {}",
last.y - wh[1]
);
assert!(
last.tvd < last.md - 100.0,
"tvd {} not shallower than md {}",
last.tvd,
last.md
);
}
#[test]
fn build_hold_is_bit_deterministic() {
let p = build_hold(700.0, 2.5, 55.0, 210.0);
let a = synth_trajectory_profile([431_000.0, 6_521_000.0], 30.0, 2800.0, 15.0, &p, 4).unwrap();
let b = synth_trajectory_profile([431_000.0, 6_521_000.0], 30.0, 2800.0, 15.0, &p, 4).unwrap();
assert_eq!(a, b);
}
#[test]
fn dogleg_severity_is_bounded_and_believable() {
let rate = 3.0;
let p = build_hold(600.0, rate, 60.0, 45.0);
let t = synth_trajectory_profile([431_000.0, 6_521_000.0], 20.0, 2600.0, 20.0, &p, 1).unwrap();
let dls = max_dogleg_severity(&t);
assert!(
(dls - rate).abs() < 0.15,
"max DLS {dls} not ~= build rate {rate}"
);
assert!(
dls <= MAX_BUILD_RATE_DEG_PER_30M + 1e-9,
"DLS {dls} over ceiling"
);
let v = synth_trajectory([431_000.0, 6_521_000.0], 20.0, 2000.0, 50.0, 1).unwrap();
assert_eq!(max_dogleg_severity(&v), 0.0);
}
#[test]
fn build_hold_drop_returns_toward_vertical() {
let bh = BuildHold::new(700.0, 3.0, 60.0, 30.0).unwrap();
let bhd = BuildHoldDrop::new(bh, 1800.0, 3.0, 20.0).unwrap();
let t = synth_trajectory_profile(
[431_000.0, 6_521_000.0],
20.0,
2800.0,
10.0,
&WellProfile::BuildHoldDrop(bhd),
1,
)
.unwrap();
let peak = t.stations.iter().map(|s| s.incl).fold(0.0, f64::max);
assert!((peak - 60.0).abs() < 1e-6, "peak incl {peak}");
let last = t.stations.last().unwrap();
assert!(
(last.incl - 20.0).abs() < 1e-6,
"final incl {} not the drop target",
last.incl
);
assert!(max_dogleg_severity(&t) <= MAX_BUILD_RATE_DEG_PER_30M + 1e-9);
}
#[test]
fn deviated_bore_sweeps_many_columns_at_reservoir_depth() {
let p = build_hold(700.0, 3.0, 55.0, 90.0);
let t = synth_trajectory_profile([431_000.0, 6_521_000.0], 20.0, 3200.0, 20.0, &p, 1).unwrap();
let reservoir: Vec<&Station> = t.stations.iter().filter(|s| s.tvd >= 1800.0).collect();
assert!(reservoir.len() >= 5, "too few reservoir stations");
let x0 = reservoir.first().unwrap().x;
let x1 = reservoir.last().unwrap().x;
let cols = ((x1 / 100.0).floor() - (x0 / 100.0).floor()).abs();
assert!(
cols >= 3.0,
"reservoir section crosses only {cols} 100 m columns"
);
}
#[test]
fn profile_builders_validate() {
assert!(BuildHold::new(-1.0, 3.0, 45.0, 0.0).is_err());
assert!(BuildHold::new(500.0, 0.0, 45.0, 0.0).is_err());
assert!(BuildHold::new(500.0, 99.0, 45.0, 0.0).is_err());
assert!(BuildHold::new(500.0, 3.0, 0.0, 0.0).is_err());
assert!(BuildHold::new(500.0, 3.0, 90.0, 0.0).is_err());
assert!(BuildHold::new(500.0, 3.0, 45.0, f64::NAN).is_err());
assert!((BuildHold::new(500.0, 3.0, 45.0, -90.0).unwrap().azimuth_deg - 270.0).abs() < 1e-9);
let bh = BuildHold::new(700.0, 3.0, 60.0, 0.0).unwrap();
assert!(BuildHoldDrop::new(bh, 500.0, 3.0, 20.0).is_err()); assert!(BuildHoldDrop::new(bh, 2000.0, 3.0, 70.0).is_err()); assert!(BuildHoldDrop::new(bh, 2000.0, 99.0, 20.0).is_err()); assert!(BuildHoldDrop::new(bh, 2000.0, 3.0, 20.0).is_ok());
}
#[test]
fn deviated_bad_args_error() {
let p = build_hold(500.0, 3.0, 45.0, 0.0);
assert!(synth_trajectory_profile([0.0, 0.0], 0.0, 0.0, 10.0, &p, 1).is_err());
assert!(synth_trajectory_profile([0.0, 0.0], 0.0, 1000.0, 0.0, &p, 1).is_err());
assert!(synth_trajectory_profile([f64::NAN, 0.0], 0.0, 1000.0, 10.0, &p, 1).is_err());
}
#[test]
fn world_frame_top_lands_at_the_deviated_reservoir_crossing_not_the_wellhead() {
use crate::synth::Georef;
let g = Georef::fictional();
let (x0, y0) = (g.east0, g.north0);
let lat = g.lattice(50.0, 50.0, 100, 100); let (a, b, c) = (0.04_f64, -0.02_f64, 2000.0_f64);
let surface = Array2::from_shape_fn((100, 100), |(i, j)| {
let (x, y) = lat.node_xy(i, j);
c + a * (x - x0) + b * (y - y0)
});
let analytic = |x: f64, y: f64| c + a * (x - x0) + b * (y - y0);
let wellhead = [x0 + 800.0, y0 + 800.0];
let p = build_hold(700.0, 3.0, 50.0, 60.0);
let t = synth_trajectory_profile(wellhead, 25.0, 3000.0, 15.0, &p, 1).unwrap();
let cross = t
.stations
.iter()
.min_by(|u, v| {
(u.tvd - 2000.0)
.abs()
.partial_cmp(&(v.tvd - 2000.0).abs())
.unwrap()
})
.unwrap();
let bb = lat.bbox();
assert!(cross.x > bb.xmin && cross.x < bb.xmax && cross.y > bb.ymin && cross.y < bb.ymax);
let offset = ((cross.x - wellhead[0]).powi(2) + (cross.y - wellhead[1]).powi(2)).sqrt();
assert!(
offset > 300.0,
"crossing barely moved from the wellhead: {offset} m"
);
let resid = Sampler::new_uniform(-1e-9, 1e-9).unwrap();
let tops = tops_from_surface(
&surface,
&lat,
&[[cross.x, cross.y], [wellhead[0], wellhead[1]]],
&resid,
3,
);
assert!(
(tops[0] - analytic(cross.x, cross.y)).abs() < 1e-3,
"crossing top {} != analytic {}",
tops[0],
analytic(cross.x, cross.y)
);
assert!(
(tops[0] - tops[1]).abs() > 5.0,
"crossing top == wellhead top ({} vs {}): frame not honoured",
tops[0],
tops[1]
);
assert!((tops[1] - analytic(wellhead[0], wellhead[1])).abs() < 1e-3);
}