use crate::Real;
use crate::error::KrigingError;
pub type PolygonRealizations = Vec<Real>;
pub fn polygon_weighted_mean_per_realization(
samples: &[Real],
n_realizations: usize,
n_targets: usize,
indices: &[usize],
weights: &[Real],
) -> Result<PolygonRealizations, KrigingError> {
validate_ensemble_shape(samples.len(), n_realizations, n_targets)?;
let total = validate_polygon(indices, weights, n_targets)?;
let inv = (1.0 as Real) / total;
let mut out = vec![0.0 as Real; n_realizations];
for (k, slot) in out.iter_mut().enumerate() {
let row_off = k * n_targets;
let mut acc: Real = 0.0;
for (idx, w) in indices.iter().zip(weights.iter()) {
acc += *w * samples[row_off + *idx];
}
*slot = acc * inv;
}
Ok(out)
}
#[derive(Debug, Clone)]
pub struct PolygonAggregationSummary {
pub n_realizations: usize,
pub total_weight: Real,
pub mean: Real,
pub variance: Option<Real>,
pub quantiles: Vec<(Real, Real)>,
}
pub fn polygon_weighted_summary(
samples: &[Real],
n_realizations: usize,
n_targets: usize,
indices: &[usize],
weights: &[Real],
quantile_probs: &[Real],
) -> Result<PolygonAggregationSummary, KrigingError> {
let realizations = polygon_weighted_mean_per_realization(
samples,
n_realizations,
n_targets,
indices,
weights,
)?;
let total_weight = weights.iter().copied().sum::<Real>();
let mean = if realizations.is_empty() {
0.0
} else {
realizations.iter().copied().sum::<Real>() / (realizations.len() as Real)
};
let variance = if realizations.len() >= 2 {
let inv = (1.0 as Real) / ((realizations.len() - 1) as Real);
let v = realizations
.iter()
.map(|x| {
let d = *x - mean;
d * d
})
.sum::<Real>()
* inv;
Some(v)
} else {
None
};
let quantiles = if quantile_probs.is_empty() {
Vec::new()
} else {
compute_quantiles_linear(&realizations, quantile_probs)?
};
Ok(PolygonAggregationSummary {
n_realizations,
total_weight,
mean,
variance,
quantiles,
})
}
pub fn polygon_weighted_summaries_batch(
samples: &[Real],
n_realizations: usize,
n_targets: usize,
polygons: &[(&[usize], &[Real])],
quantile_probs: &[Real],
) -> Result<Vec<PolygonAggregationSummary>, KrigingError> {
validate_ensemble_shape(samples.len(), n_realizations, n_targets)?;
let mut out = Vec::with_capacity(polygons.len());
for (indices, weights) in polygons.iter() {
let s = polygon_weighted_summary(
samples,
n_realizations,
n_targets,
indices,
weights,
quantile_probs,
)?;
out.push(s);
}
Ok(out)
}
fn validate_ensemble_shape(
sample_len: usize,
n_realizations: usize,
n_targets: usize,
) -> Result<(), KrigingError> {
if n_realizations == 0 {
return Err(KrigingError::InsufficientData(1));
}
if n_targets == 0 {
return Err(KrigingError::InsufficientData(1));
}
let expected = n_realizations
.checked_mul(n_targets)
.ok_or_else(|| KrigingError::DimensionMismatch("ensemble size overflows usize".into()))?;
if sample_len != expected {
return Err(KrigingError::DimensionMismatch(format!(
"ensemble buffer length ({}) must equal n_realizations * n_targets ({})",
sample_len, expected
)));
}
Ok(())
}
fn validate_polygon(
indices: &[usize],
weights: &[Real],
n_targets: usize,
) -> Result<Real, KrigingError> {
if indices.is_empty() {
return Err(KrigingError::InsufficientData(1));
}
if indices.len() != weights.len() {
return Err(KrigingError::DimensionMismatch(format!(
"polygon indices ({}) and weights ({}) must have the same length",
indices.len(),
weights.len()
)));
}
let mut total: Real = 0.0;
for (i, w) in indices.iter().zip(weights.iter()) {
if *i >= n_targets {
return Err(KrigingError::InvalidInput(format!(
"polygon cell index {} is out of range (n_targets = {})",
i, n_targets
)));
}
if !w.is_finite() {
return Err(KrigingError::InvalidInput(
"polygon weights must be finite".into(),
));
}
if *w < 0.0 {
return Err(KrigingError::InvalidInput(
"polygon weights must be non-negative".into(),
));
}
total += *w;
}
if total <= 0.0 {
return Err(KrigingError::InvalidInput(
"polygon weights must sum to a strictly positive value".into(),
));
}
Ok(total)
}
fn compute_quantiles_linear(
values: &[Real],
probs: &[Real],
) -> Result<Vec<(Real, Real)>, KrigingError> {
for p in probs {
if !p.is_finite() || *p < 0.0 || *p > 1.0 {
return Err(KrigingError::InvalidInput(format!(
"quantile probabilities must be in [0, 1] (got {})",
p
)));
}
}
if values.is_empty() {
return Err(KrigingError::InsufficientData(1));
}
let mut sorted: Vec<Real> = values.to_vec();
if sorted.iter().any(|v| v.is_nan()) {
return Err(KrigingError::InvalidInput(
"ensemble values contained NaN; cannot compute quantiles".into(),
));
}
sorted.sort_by(|a, b| a.partial_cmp(b).expect("checked: no NaN"));
let last_idx = (sorted.len() - 1) as Real;
let mut out = Vec::with_capacity(probs.len());
for p in probs {
let pos = last_idx * *p;
let lo = pos.floor() as usize;
let hi = (lo + 1).min(sorted.len() - 1);
let frac = pos - (lo as Real);
let v = if hi == lo {
sorted[lo]
} else {
sorted[lo] + (sorted[hi] - sorted[lo]) * frac
};
out.push((*p, v));
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn buf(rows: usize, cols: usize, f: impl Fn(usize, usize) -> Real) -> Vec<Real> {
let mut v = Vec::with_capacity(rows * cols);
for k in 0..rows {
for j in 0..cols {
v.push(f(k, j));
}
}
v
}
#[test]
fn polygon_weighted_mean_uniform_weights_matches_simple_mean() {
let samples = buf(3, 4, |k, j| (k * 10 + j) as Real);
let indices = vec![0, 1, 2, 3];
let weights = vec![1.0; 4];
let per =
polygon_weighted_mean_per_realization(&samples, 3, 4, &indices, &weights).unwrap();
assert_eq!(per.len(), 3);
assert!((per[0] - 1.5).abs() < 1e-6);
assert!((per[1] - 11.5).abs() < 1e-6);
assert!((per[2] - 21.5).abs() < 1e-6);
}
#[test]
fn polygon_weighted_mean_respects_unnormalized_weights() {
let samples = vec![0.0, 4.0, 1.0, 5.0]; let per =
polygon_weighted_mean_per_realization(&samples, 2, 2, &[0, 1], &[1.0, 3.0]).unwrap();
assert!((per[0] - 3.0).abs() < 1e-6); assert!((per[1] - 4.0).abs() < 1e-6); }
#[test]
fn polygon_weighted_summary_reports_mean_variance_and_quantiles() {
let samples: Vec<Real> = (0..10).map(|i| (i / 2) as Real).collect();
let s = polygon_weighted_summary(&samples, 5, 2, &[0], &[1.0], &[0.0, 0.5, 1.0]).unwrap();
assert_eq!(s.n_realizations, 5);
assert!((s.total_weight - 1.0).abs() < 1e-6);
assert!((s.mean - 2.0).abs() < 1e-6);
let var = s.variance.expect("variance with 5 samples");
assert!((var - 2.5).abs() < 1e-6);
assert_eq!(s.quantiles.len(), 3);
assert!((s.quantiles[0].1 - 0.0).abs() < 1e-6);
assert!((s.quantiles[1].1 - 2.0).abs() < 1e-6);
assert!((s.quantiles[2].1 - 4.0).abs() < 1e-6);
}
#[test]
fn polygon_weighted_summary_handles_single_realization() {
let s = polygon_weighted_summary(&[7.0, 9.0], 1, 2, &[0, 1], &[1.0, 1.0], &[0.5]).unwrap();
assert!((s.mean - 8.0).abs() < 1e-6);
assert!(s.variance.is_none());
assert!((s.quantiles[0].1 - 8.0).abs() < 1e-6);
}
#[test]
fn polygon_weighted_summary_rejects_bad_inputs() {
let samples = vec![0.0; 6]; assert!(polygon_weighted_summary(&samples, 3, 3, &[0], &[1.0], &[]).is_err());
assert!(polygon_weighted_summary(&samples, 3, 2, &[], &[], &[]).is_err());
assert!(polygon_weighted_summary(&samples, 3, 2, &[2], &[1.0], &[]).is_err());
assert!(polygon_weighted_summary(&samples, 3, 2, &[0], &[-1.0], &[]).is_err());
assert!(polygon_weighted_summary(&samples, 3, 2, &[0, 1], &[0.0, 0.0], &[]).is_err());
assert!(polygon_weighted_summary(&samples, 3, 2, &[0], &[1.0], &[1.5]).is_err());
}
#[test]
fn polygon_weighted_summaries_batch_matches_individual_calls() {
let samples = buf(4, 3, |k, j| (k as Real) - (j as Real));
let polys: &[(&[usize], &[Real])] = &[
(&[0, 2], &[1.0, 1.0]),
(&[1], &[2.0]),
(&[0, 1, 2], &[1.0, 2.0, 3.0]),
];
let probs = [0.25, 0.75];
let batch = polygon_weighted_summaries_batch(&samples, 4, 3, polys, &probs).unwrap();
for (i, (idx, w)) in polys.iter().enumerate() {
let single = polygon_weighted_summary(&samples, 4, 3, idx, w, &probs).unwrap();
assert!((batch[i].mean - single.mean).abs() < 1e-9);
for q in 0..probs.len() {
assert!((batch[i].quantiles[q].1 - single.quantiles[q].1).abs() < 1e-9);
}
}
}
}