use crate::core::facade::spec::LayersSpec;
use crate::units::SrsError;
use petekstatic::gridder::Conformity;
use petekstatic::model::{HorizonSource, HorizonStack, StackHorizon, StackZone};
use petekstatic::wireframe::{Contact, ContactKind, Hardness};
#[derive(Debug, Clone)]
pub struct ZoneSpec {
pub name: String,
pub below_horizon: String,
pub conformity: Conformity,
pub layers: LayersSpec,
pub goc_m: Option<f64>,
pub owc_m: Option<f64>,
}
pub fn validate(horizon_names: &[String], zones: &[ZoneSpec]) -> Result<(), SrsError> {
let n = horizon_names.len();
if n < 2 {
return Err(SrsError::InvalidInput(
"a zoned framework needs at least 2 horizons".into(),
));
}
if zones.len() != n - 1 {
return Err(SrsError::InvalidInput(format!(
"{n} horizons define {} zone(s), but {} zone spec(s) were given",
n - 1,
zones.len()
)));
}
for (i, z) in zones.iter().enumerate() {
let expected = &horizon_names[i + 1];
if &z.below_horizon != expected {
return Err(SrsError::InvalidInput(format!(
"zone '{}' has below_horizon '{}', but interval {i} is bounded below by \
horizon '{expected}' (zones must be declared top→down, one per horizon gap)",
z.name, z.below_horizon
)));
}
}
Ok(())
}
pub fn assemble_stack(
horizon_names: &[String],
resolved: &[StackHorizon],
zones: &[ZoneSpec],
) -> Result<HorizonStack, SrsError> {
validate(horizon_names, zones)?;
let mut zone_layers = Vec::with_capacity(zones.len());
for (i, z) in zones.iter().enumerate() {
let gross = zone_gross(&resolved[i].source, &resolved[i + 1].source);
let nk = z.layers.nk(gross).max(1);
zone_layers.push(StackZone::new(
z.name.clone(),
z.conformity,
nk,
build_contacts(z.goc_m, z.owc_m),
));
}
Ok(HorizonStack {
horizons: resolved.to_vec(),
zone_layers,
})
}
fn build_contacts(goc_m: Option<f64>, owc_m: Option<f64>) -> Vec<Contact> {
let mut cs = Vec::new();
if let Some(g) = goc_m {
cs.push(Contact {
kind: ContactKind::Goc,
depth_m: g,
hardness: Hardness::Hard,
});
}
if let Some(w) = owc_m {
cs.push(Contact {
kind: ContactKind::Owc,
depth_m: w,
hardness: Hardness::Hard,
});
}
cs
}
fn zone_gross(above: &HorizonSource, below: &HorizonSource) -> f64 {
if let (HorizonSource::Mapped(a), HorizonSource::Mapped(b)) = (above, below) {
if a.depth_m.len() == b.depth_m.len() {
let mut sum = 0.0;
let mut n = 0usize;
for (za, zb) in a.depth_m.iter().zip(&b.depth_m) {
if za.is_finite() && zb.is_finite() {
sum += zb - za;
n += 1;
}
}
if n > 0 {
return (sum / n as f64).abs().max(1e-3);
}
}
}
match (source_mean_depth(above), source_mean_depth(below)) {
(Some(a), Some(b)) => (b - a).abs().max(1e-3),
_ => 20.0,
}
}
fn source_mean_depth(source: &HorizonSource) -> Option<f64> {
let (mut sum, mut n) = (0.0, 0usize);
match source {
HorizonSource::Mapped(g) => {
for &z in &g.depth_m {
if z.is_finite() {
sum += z;
n += 1;
}
}
}
HorizonSource::Scatter(points) => {
for p in points {
if p.depth_m.is_finite() {
sum += p.depth_m;
n += 1;
}
}
}
HorizonSource::TopsOnly(_) => return None,
}
(n > 0).then(|| sum / n as f64)
}