use std::collections::HashMap;
use energyml_analysis::profile::{Corridor, Head, Kept, Profile, Reason};
use crate::crosses::{meet, traces};
use crate::inputs::{InputsError, validate};
use crate::model::{
Catalogue, Contribution, CutShape, EnvelopeItem, FORMAT, Inputs, Item, Mark, ObjectId, ObjectRow,
Outcome, Plan, PlanWell, RowKind, Section, SectionKey, SectionTrace, Side, SurfaceRead, TraceItem,
WellItem, WellKind, WellRead,
};
type V = [f64; 2];
pub const BAND_ARC_STEP: f64 = std::f64::consts::PI / 12.0;
pub fn assemble(inputs: &Inputs, contributions: &[Contribution]) -> Result<Catalogue, InputsError> {
validate(inputs)?;
let (sections, mut notes) = traces(inputs)?;
let all: Vec<&WellRead> = contributions.iter().flat_map(|c| c.wells.iter()).collect();
let lines = fold_wells(&all);
let heads: Vec<Head> = lines
.iter()
.map(|l| Head::new(l.well.object.key(), [l.well.head[0], l.well.head[1]]))
.collect();
let mut built: Vec<Built> = Vec::with_capacity(sections.len());
for st in §ions {
let corridor = Corridor::new(&st.trace, st.left, st.right)
.map_err(|e| InputsError::Setting(format!("{}: {e}", describe(st.key))))?;
let band = corridor
.band(BAND_ARC_STEP)
.map_err(|e| InputsError::Setting(format!("{}: {e}", describe(st.key))))?;
let profile = corridor.project(&heads);
built.push(Built {
corridor,
band,
profile,
});
}
let mut cross_meets: Vec<(SectionKey, SectionKey, [f64; 2])> = Vec::new();
for i in 1..sections.len() {
for j in i + 1..sections.len() {
if let Some(at) = meet(§ions[i].trace, §ions[j].trace) {
cross_meets.push((sections[i].key, sections[j].key, at));
}
}
}
let mut out_sections: Vec<Section> = Vec::with_capacity(sections.len());
for (st, b) in sections.iter().zip(&built) {
let mut items: Vec<Item> = Vec::new();
for surface in contributions.iter().flat_map(|c| c.surfaces.iter()) {
for cut in surface.cuts.iter().filter(|c| c.section == st.key) {
items.push(match &cut.shape {
CutShape::Trace { spans } => Item::Trace(TraceItem {
object: surface.object.clone(),
role: surface.role,
spans: spans.clone(),
time: surface.time,
notes: cut.notes.clone(),
colour: surface.colour.clone(),
age: surface.age,
feature: surface.feature.clone(),
sources: cut.sources.clone(),
}),
CutShape::Envelope {
top,
bottom,
fills,
faults,
} => Item::Envelope(EnvelopeItem {
object: surface.object.clone(),
top: top.clone(),
bottom: bottom.clone(),
fills: fills.clone(),
faults: faults.clone(),
time: surface.time,
colour: surface.colour.clone(),
}),
});
}
}
for (line, head) in lines.iter().zip(&heads) {
let Some(kept) = b.profile.kept.iter().find(|k| k.id == head.id) else {
continue;
};
items.push(Item::Well(well_item(line, kept)));
}
let mut marks: Vec<Mark> = Vec::new();
let mut at = 0.0;
marks.push(Mark::Vertex { s: 0.0, index: 0 });
for (i, p) in st.trace.windows(2).enumerate() {
at += (p[1][0] - p[0][0]).hypot(p[1][1] - p[0][1]);
marks.push(Mark::Vertex {
s: at,
index: i as u32 + 1,
});
}
match st.key {
SectionKey::Long => {
for other in sections.iter().filter(|o| o.key != SectionKey::Long) {
if let Some([s, _]) = other.crossing {
marks.push(Mark::Crossing { s, other: other.key });
}
}
}
SectionKey::Cross { .. } => {
if let Some([_, own]) = st.crossing {
marks.push(Mark::Crossing {
s: own,
other: SectionKey::Long,
});
}
}
}
for &(a, c, [sa, sc]) in &cross_meets {
if a == st.key {
marks.push(Mark::Crossing { s: sa, other: c });
} else if c == st.key {
marks.push(Mark::Crossing { s: sc, other: a });
}
}
marks.sort_by(|x, y| mark_s(x).total_cmp(&mark_s(y)));
let z_range = z_range_of(&items);
out_sections.push(Section {
key: st.key,
trace: st.trace.clone(),
length: b.corridor.length(),
marks,
band: b.band.clone(),
items,
z_range,
});
}
let drawn_on: Vec<Vec<SectionKey>> = lines
.iter()
.zip(&heads)
.map(|(_, head)| {
sections
.iter()
.zip(&built)
.filter(|(_, b)| b.profile.kept.iter().any(|k| k.id == head.id))
.map(|(st, _)| st.key)
.collect()
})
.collect();
let origin = inputs.trace[0];
let local = |p: V| [p[0] - origin[0], p[1] - origin[1]];
let mut plan = Plan {
origin,
system: inputs.system.clone(),
sections: sections
.iter()
.map(|st| (st.key, st.trace.iter().map(|&p| local(p)).collect()))
.collect(),
bands: sections
.iter()
.zip(&built)
.map(|(st, b)| (st.key, b.band.iter().map(|&p| local(p)).collect()))
.collect(),
wells: Vec::new(),
};
let long_profile = &built[0].profile;
let mut reasons: Vec<Option<String>> = Vec::with_capacity(lines.len());
for (k, (line, head)) in lines.iter().zip(&heads).enumerate() {
let feet: Vec<(SectionKey, V)> = sections
.iter()
.zip(&built)
.filter_map(|(st, b)| {
b.profile
.kept
.iter()
.find(|p| p.id == head.id)
.map(|p| (st.key, local(p.foot)))
})
.collect();
let rejected = if drawn_on[k].is_empty() {
Some(not_projected(
line.well,
long_profile,
&head.id,
inputs,
§ions[0],
))
} else {
None
};
reasons.push(rejected.clone());
plan.wells.push(PlanWell {
object: line.well.object.clone(),
head: local([line.well.head[0], line.well.head[1]]),
feet,
rejected,
});
}
let mut objects: Vec<ObjectRow> = Vec::new();
for contribution in contributions {
for well in &contribution.wells {
let key = well.object.key();
if let Some(k) = lines.iter().position(|l| l.well.object.key() == key) {
objects.push(ObjectRow {
object: well.object.clone(),
kind: RowKind::Well,
outcome: match &reasons[k] {
None => Outcome::Drawn {
sections: drawn_on[k].clone(),
},
Some(reason) => Outcome::Refused {
reason: reason.clone(),
},
},
fillable: false,
});
} else if let Some(line) = lines
.iter()
.find(|l| l.merged.iter().any(|m| m.object.key() == key))
{
objects.push(ObjectRow {
object: well.object.clone(),
kind: RowKind::Well,
outcome: Outcome::Merged {
into: line.well.object.clone(),
},
fillable: false,
});
}
}
for surface in &contribution.surfaces {
objects.push(surface_row(surface, §ions));
}
for refused in &contribution.refused {
objects.push(ObjectRow {
object: refused.object.clone(),
kind: kind_of(&refused.object),
outcome: Outcome::Refused {
reason: refused.reason.clone(),
},
fillable: false,
});
}
}
let beyond = long_profile.beyond_an_end();
if beyond > 0 {
notes.push(crate::model::Note {
code: "beyondAnEnd".to_string(),
sections: vec![SectionKey::Long],
objects: Vec::new(),
said: format!("{beyond} well(s) project beyond an end of the long section and were left out"),
});
}
notes.extend(contributions.iter().flat_map(|c| c.notes.iter().cloned()));
notes.extend(crossing_notes(&out_sections));
notes.extend(crate::erosion::notes_of(&out_sections));
let long = out_sections.remove(0);
Ok(Catalogue {
format: FORMAT,
inputs: inputs.clone(),
long,
crosses: out_sections,
plan,
objects,
notes,
})
}
struct Built {
corridor: Corridor,
band: Vec<V>,
profile: Profile,
}
struct Line<'a> {
well: &'a WellRead,
merged: Vec<&'a WellRead>,
}
fn fold_wells<'a>(all: &[&'a WellRead]) -> Vec<Line<'a>> {
let mut lines: Vec<Line<'a>> = Vec::new();
let mut by_wellbore: HashMap<&str, usize> = HashMap::new();
for &w in all {
let wellbore = if w.head_only { None } else { w.wellbore.as_deref() };
let Some(wb) = wellbore else {
lines.push(Line {
well: w,
merged: Vec::new(),
});
continue;
};
match by_wellbore.get(wb) {
None => {
by_wellbore.insert(wb, lines.len());
lines.push(Line {
well: w,
merged: Vec::new(),
});
}
Some(&at) => {
let line = &mut lines[at];
if rank(w) > rank(line.well) {
let old = std::mem::replace(&mut line.well, w);
line.merged.insert(0, old);
} else {
line.merged.push(w);
}
}
}
}
lines
}
fn rank(well: &WellRead) -> (bool, bool, bool, usize) {
(
!well.intervals.is_empty(),
!well.borrowed,
well.kind == WellKind::Frame,
well.intervals.iter().filter(|one| one.to > one.from).count(),
)
}
fn reach(well: &WellRead) -> f64 {
let (low, high) = well
.path
.iter()
.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), n| {
(lo.min(n[2]), hi.max(n[2]))
});
if high >= low { high - low } else { 0.0 }
}
fn geometry_of<'a>(line: &Line<'a>) -> &'a WellRead {
let own = line.well;
std::iter::once(own)
.chain(line.merged.iter().copied())
.filter(|one| std::ptr::eq(*one, own) || one.md.is_some() || own.md.is_none())
.fold(own, |best, one| if reach(one) > reach(best) { one } else { best })
}
fn well_item(line: &Line<'_>, kept: &Kept) -> WellItem {
let w = line.well;
let geometry = geometry_of(line);
let dir = kept.direction;
let path: Vec<V> = geometry
.path
.iter()
.map(|n| {
let along = (n[0] - geometry.head[0]) * dir[0] + (n[1] - geometry.head[1]) * dir[1];
[kept.s + along, n[2]]
})
.collect();
WellItem {
object: w.object.clone(),
s: kept.s,
d: kept.d,
side: match kept.side {
energyml_analysis::profile::Side::Left => Side::Left,
energyml_analysis::profile::Side::Right => Side::Right,
},
head: [kept.s, w.head[2]],
path,
md: geometry.md.clone(),
intervals: w.intervals.clone(),
strata: w.strata.clone(),
merged: line.merged.iter().map(|m| m.object.clone()).collect(),
}
}
fn not_projected(well: &WellRead, long: &Profile, id: &str, inputs: &Inputs, st: &SectionTrace) -> String {
let name = if well.object.title.is_empty() {
&well.object.uuid
} else {
&well.object.title
};
let why = match long.rejected.iter().find(|r| r.id == id).map(|r| r.reason) {
Some(Reason::TooFarOnItsSide) => format!(
"its head is close to the long section but outside the corridor of its side \
(left {}, right {})",
inputs.left, inputs.right
),
Some(Reason::BeyondAnEnd) => "its head projects beyond an end of the long section".to_string(),
Some(Reason::NoPosition) => "its head has no usable position".to_string(),
None => format!(
"its head is farther than {} from the long section",
st.left.max(st.right)
),
};
format!("well {name}: {why}, and it falls on no cross section either")
}
fn surface_row(surface: &SurfaceRead, sections: &[SectionTrace]) -> ObjectRow {
let on: Vec<SectionKey> = sections
.iter()
.map(|s| s.key)
.filter(|k| surface.cuts.iter().any(|c| c.section == *k))
.collect();
let grid = surface
.cuts
.iter()
.any(|c| matches!(c.shape, CutShape::Envelope { .. }))
|| is_ijk(&surface.object.qualified_type);
ObjectRow {
object: surface.object.clone(),
kind: if grid { RowKind::Grid } else { RowKind::Surface },
outcome: if on.is_empty() {
Outcome::Missed
} else {
Outcome::Drawn { sections: on }
},
fillable: !grid && surface.role != crate::model::TraceRole::Fault,
}
}
fn is_ijk(qualified_type: &str) -> bool {
qualified_type.contains("Grid") && !qualified_type.contains("Grid2d")
}
fn kind_of(object: &ObjectId) -> RowKind {
let t = &object.qualified_type;
if t.contains("Wellbore") {
RowKind::Well
} else if is_ijk(t) {
RowKind::Grid
} else {
RowKind::Surface
}
}
fn describe(key: SectionKey) -> String {
match key {
SectionKey::Long => "the long section".to_string(),
SectionKey::Cross { index } => format!("cross section {index}"),
}
}
fn mark_s(m: &Mark) -> f64 {
match m {
Mark::Vertex { s, .. } | Mark::Crossing { s, .. } => *s,
}
}
fn z_range_of(items: &[Item]) -> Option<[f64; 2]> {
let mut range: Option<[f64; 2]> = None;
let mut take = |z: f64| {
if z.is_finite() {
range = Some(match range {
None => [z, z],
Some([lo, hi]) => [lo.min(z), hi.max(z)],
});
}
};
let spans = |v: &Vec<Vec<V>>, take: &mut dyn FnMut(f64)| v.iter().flatten().for_each(|p| take(p[1]));
for item in items {
match item {
Item::Well(w) => {
take(w.head[1]);
w.path.iter().for_each(|p| take(p[1]));
}
Item::Trace(t) => spans(&t.spans, &mut take),
Item::Envelope(e) => {
spans(&e.top, &mut take);
spans(&e.bottom, &mut take);
e.fills.iter().flatten().for_each(|p| take(p[1]));
e.faults.iter().flatten().for_each(|p| take(p[1]));
}
}
}
range
}
pub const CROSSING_TOLERANCE_SHARE: f64 = 0.005;
pub const CROSSING_TOLERANCE_FLOOR: f64 = 0.01;
const CROSSING_S_SLACK: f64 = 1e-6;
type Elevations = Vec<(&'static str, Vec<f64>)>;
fn crossing_notes(sections: &[Section]) -> Vec<crate::model::Note> {
let mut notes = Vec::new();
for a in sections {
for mark in &a.marks {
let Mark::Crossing { s: sa, other } = mark else {
continue;
};
if a.key >= *other {
continue;
}
let Some(b) = sections.iter().find(|s| s.key == *other) else {
continue;
};
let Some(sb) = b.marks.iter().find_map(|m| match m {
Mark::Crossing { s, other } if *other == a.key => Some(*s),
_ => None,
}) else {
continue;
};
let extent = match (a.z_range, b.z_range) {
(Some([al, ah]), Some([bl, bh])) => ah.max(bh) - al.min(bl),
(Some([l, h]), None) | (None, Some([l, h])) => h - l,
(None, None) => 0.0,
};
let tolerance = (extent * CROSSING_TOLERANCE_SHARE).max(CROSSING_TOLERANCE_FLOOR);
for item_a in &a.items {
let Some(object) = surface_of(item_a) else {
continue;
};
let Some(item_b) = b
.items
.iter()
.find(|i| surface_of(i).is_some_and(|o| o.key() == object.key()))
else {
continue;
};
let on_a = elevations(item_a, *sa, a.length);
let on_b = elevations(item_b, sb, b.length);
for ((part, za), (_, zb)) in on_a.iter().zip(&on_b) {
let name = if part.is_empty() {
format!("surface '{}'", label(object))
} else {
format!("the {part} of grid '{}'", label(object))
};
let there = format!(
"at the crossing of the {} (s = {sa:.2}) and the {} (s = {sb:.2})",
describe(a.key),
describe(b.key)
);
let note = |code: &str, said: String| crate::model::Note {
code: code.to_string(),
sections: vec![a.key, b.key],
objects: vec![object.clone()],
said,
};
match (za.is_empty(), zb.is_empty()) {
(true, true) => {}
(false, true) | (true, false) => {
let (has, lacks, z) = if zb.is_empty() {
(a.key, b.key, za[0])
} else {
(b.key, a.key, zb[0])
};
notes.push(note(
"crossingOneSided",
format!(
"{name} {there}: the {} gives it at z = {z:.2}, the {} has \
nothing there (a hole or an end of the surface)",
describe(has),
describe(lacks)
),
));
}
(false, false) => {
let (x, y) = worst_pair(za, zb);
let gap = (x - y).abs();
if gap > tolerance {
notes.push(note(
"crossingMismatch",
format!(
"{name} {there}: z = {x:.2} on the {} but {y:.2} on the \
{}, a gap of {gap:.2} (tolerance {tolerance:.2})",
describe(a.key),
describe(b.key)
),
));
}
}
}
}
}
}
}
notes
}
fn surface_of(item: &Item) -> Option<&ObjectId> {
match item {
Item::Trace(t) if t.role != crate::model::TraceRole::Fault => Some(&t.object),
Item::Envelope(e) => Some(&e.object),
_ => None,
}
}
fn label(object: &ObjectId) -> &str {
if object.title.is_empty() {
&object.uuid
} else {
&object.title
}
}
fn elevations(item: &Item, s: f64, length: f64) -> Elevations {
let slack = length * CROSSING_S_SLACK;
match item {
Item::Trace(t) => vec![("", at(&t.spans, s, slack))],
Item::Envelope(e) => {
vec![("top", at(&e.top, s, slack)), ("bottom", at(&e.bottom, s, slack))]
}
Item::Well(_) => Vec::new(),
}
}
fn at(spans: &[Vec<V>], s: f64, slack: f64) -> Vec<f64> {
let mut out: Vec<f64> = Vec::new();
let mut push = |z: f64| {
if z.is_finite() && !out.iter().any(|o| (o - z).abs() < 1e-9) {
out.push(z);
}
};
for span in spans {
for w in span.windows(2) {
let (p, q) = (w[0], w[1]);
let (lo, hi) = (p[0].min(q[0]), p[0].max(q[0]));
if s < lo - slack || s > hi + slack {
continue;
}
if hi - lo < 1e-12 {
push(p[1]);
push(q[1]);
} else {
let t = ((s - p[0]) / (q[0] - p[0])).clamp(0.0, 1.0);
push(p[1] + t * (q[1] - p[1]));
}
}
}
out
}
fn worst_pair(a: &[f64], b: &[f64]) -> (f64, f64) {
let nearest = |x: f64, set: &[f64]| {
set.iter()
.copied()
.min_by(|p, q| (p - x).abs().total_cmp(&(q - x).abs()))
.unwrap_or(x)
};
let mut worst = (a[0], nearest(a[0], b));
for &x in a {
let y = nearest(x, b);
if (x - y).abs() > (worst.0 - worst.1).abs() {
worst = (x, y);
}
}
for &y in b {
let x = nearest(y, a);
if (x - y).abs() > (worst.0 - worst.1).abs() {
worst = (x, y);
}
}
worst
}