pub use deltae::{DEMethod, LabValue, Delta};
use crate::*;
impl Cgats {
pub fn delta(&self, other: &Self, method: DEMethod) -> Result<Self> {
self.can_delta(other)?;
let lab_ref_l = self.get_col_by_field(&LAB_L).ok_or("unable to find reference L* column")?;
let lab_ref_a = self.get_col_by_field(&LAB_A).ok_or("unable to find reference a* column")?;
let lab_ref_b = self.get_col_by_field(&LAB_B).ok_or("unable to find reference b* column")?;
let lab_sam_l = other.get_col_by_field(&LAB_L).ok_or("unable to find sample L* column")?;
let lab_sam_a = other.get_col_by_field(&LAB_A).ok_or("unable to find sample a* column")?;
let lab_sam_b = other.get_col_by_field(&LAB_B).ok_or("unable to find sample b* column")?;
let labify = |lab: ((&DataPoint, &DataPoint), &DataPoint)| {
LabValue {
l: lab.0.0.to_float_unchecked(),
a: lab.0.1.to_float_unchecked(),
b: lab.1.to_float_unchecked(),
}
};
let lab_ref = lab_ref_l
.zip(lab_ref_a)
.zip(lab_ref_b)
.map(labify);
let lab_sam = lab_sam_l
.zip(lab_sam_a)
.zip(lab_sam_b)
.map(labify);
let delta = lab_ref.zip(lab_sam)
.map(|(lab_ref, lab_sam)| DataPoint::new_float(*lab_ref.delta(lab_sam, method).value()))
.collect::<Vec<_>>();
let mut cgats = Cgats::default();
cgats.data_format.fields = vec![Field::from(method)];
cgats.data = delta;
cgats.reindex_sample_id();
Ok(cgats)
}
pub fn de_report(&self, other: &Self, method: DEMethod, upper_pct: f32) -> Result<DeReport> {
let delta = self.delta(other, method)?;
let mut values: Vec<f32> = delta.get_col_by_field(&Field::from(method))
.unwrap()
.map(DataPoint::to_float_unchecked)
.collect();
values.sort_by(|a,b| a.partial_cmp(b).unwrap());
Ok(DeReport {
method,
values,
upper_pct,
})
}
pub fn can_delta(&self, other: &Self) -> Result<()> {
if !self.has_color_type(&ColorType::Lab) || !other.has_color_type(&ColorType::Lab) {
return err!("data set does not contain Lab data")
}
if self.n_rows() != other.n_rows() {
return err!("data sets do not contain the same number of samples")
}
Ok(())
}
}
impl From<DEMethod> for Field {
fn from(method: DEMethod) -> Self {
match method {
DEMethod::DE2000 => Field::DE_2000,
DEMethod::DE1976 => Field::DE_1976,
DEMethod::DE1994G => Field::DE_1994,
DEMethod::DE1994T => Field::DE_1994T,
DEMethod::DECMC(a, b) => {
if a == b {
Field::DE_CMC
} else if a / b == 2.0 {
Field::DE_CMC2
} else {
Field::Other(format!("DECMC({a},{b})"))
}
}
}
}
}
impl TryFrom<Field> for DEMethod {
type Error = BoxErr;
fn try_from(field: Field) -> Result<Self> {
Ok(match field {
Field::DE_2000 => DEMethod::DE2000,
Field::DE_1976 => DEMethod::DE1976,
Field::DE_1994 => DEMethod::DE1994G,
Field::DE_1994T => DEMethod::DE1994T,
Field::DE_CMC => DEMethod::DECMC(1.0, 1.0),
Field::DE_CMC2 => DEMethod::DECMC(2.0, 1.0),
other => return err!(format!("not a valid DEMethod: '{}'", other)),
})
}
}
pub struct DeReport {
method: DEMethod,
values: Vec<f32>,
upper_pct: f32,
}
impl DeReport {
pub fn n_samples(&self) -> usize {
self.values.len()
}
fn n_samples_pct(&self, pct: f32) -> usize {
n_samples_pct(self.n_samples(), pct)
}
pub fn method(&self) -> &DEMethod {
&self.method
}
fn overall(&self) -> AvgMinMax {
AvgMinMax {
name: "OVERALL".to_string(),
pct: 1.0,
count: self.n_samples(),
avg: self.values.iter()
.partial_avg()
.expect("DE is empty"),
min: *self.values.iter().min_by(|a,b| a.partial_cmp(b).unwrap()).expect("DE is empty"),
max: *self.values.iter().max_by(|a,b| a.partial_cmp(b).unwrap()).expect("DE is empty"),
stdev: stdev(self.values.iter()),
}
}
fn best(&self) -> AvgMinMax {
let take = self.n_samples_pct(self.upper_pct);
AvgMinMax {
name: "BEST".to_string(),
pct: self.upper_pct,
count: self.n_samples_pct(self.upper_pct),
avg: self.values.iter()
.take(take)
.partial_avg()
.expect("DE is empty"),
min: *self.values.iter().take(take).min_by(|a,b| a.partial_cmp(b).unwrap()).expect("DE is empty"),
max: *self.values.iter().take(take).max_by(|a,b| a.partial_cmp(b).unwrap()).expect("DE is empty"),
stdev: stdev(self.values.iter().take(take)),
}
}
fn worst(&self) -> AvgMinMax {
let skip = self.n_samples_pct(self.upper_pct);
AvgMinMax {
name: "WORST".to_string(),
pct: 1.0 - self.upper_pct,
count: self.n_samples_pct(1.0 - self.upper_pct),
avg: self.values.iter()
.skip(skip)
.partial_avg()
.expect("DE is empty"),
min: *self.values.iter().skip(skip).min_by(|a,b| a.partial_cmp(b).unwrap()).expect("DE is empty"),
max: *self.values.iter().skip(skip).max_by(|a,b| a.partial_cmp(b).unwrap()).expect("DE is empty"),
stdev: stdev(self.values.iter().skip(skip)),
}
}
pub fn efactor(&self) -> f32 {
*self.values.get(self.n_samples_pct(0.95)).unwrap()
}
pub fn values(&self) -> impl Iterator<Item=&f32> {
self.values.iter()
}
}
impl fmt::Display for DeReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Number of Samples: {}", self.n_samples())?;
writeln!(f, "DE Formula: {}", self.method)?;
writeln!(f, "E-Factor (95th Percentile): {:0.2}\n", self.efactor())?;
match f.precision() {
Some(p) => {
writeln!(f, "{:.p$}", self.overall())?;
writeln!(f, "{:.p$}", self.best())?;
writeln!(f, "{:.p$}", self.worst())
}
None => {
writeln!(f, "{}", self.overall())?;
writeln!(f, "{}", self.best())?;
writeln!(f, "{}", self.worst())
}
}
}
}
fn n_samples_pct(pop: usize, pct: f32) -> usize {
(pct.clamp(0.0, 1.0) * pop as f32).round() as usize
}
#[test]
fn percentile() {
assert_eq!(n_samples_pct(100, 0.95), 95);
assert_eq!(n_samples_pct(100, 0.05), 5);
assert_eq!(n_samples_pct(126, 0.90), 113);
assert_eq!(n_samples_pct(126, 0.10), 13);
assert_eq!(n_samples_pct(1, 0.95), 1);
assert_eq!(n_samples_pct(1, 0.50), 1);
assert_eq!(n_samples_pct(1, 0.49), 0);
assert_eq!(n_samples_pct(1, 0.05), 0);
assert_eq!(n_samples_pct(2, 0.49), 1);
assert_eq!(n_samples_pct(2, 0.50), 1);
assert_eq!(n_samples_pct(2, 0.51), 1);
}
fn stdev<'a, I: Iterator<Item=&'a f32>>(iter: I) -> f32 {
let values = iter.copied().collect::<Vec<_>>();
let len = values.len() as f32;
let avg = values.iter().sum::<f32>() / len as f32;
f32::sqrt(values.into_iter().map(|val| (val - avg).powi(2)).sum::<f32>() / len)
}
#[test]
fn stdev_test() {
let values = [1.0, 3.0, 4.0, 7.0, 8.0];
assert_eq!(stdev(values.iter()), 2.57682);
let values = [9.0, 2.0, 5.0, 4.0, 12.0, 7.0, 8.0, 11.0, 9.0, 3.0, 7.0, 4.0, 12.0, 5.0, 4.0, 10.0, 9.0, 6.0, 9.0, 4.0];
assert_eq!(stdev(values.iter()), 2.9832866);
}
struct AvgMinMax {
name: String,
pct: f32,
count: usize,
avg: f32,
min: f32,
max: f32,
stdev: f32,
}
impl fmt::Display for AvgMinMax {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let p = f.precision().unwrap_or(4);
writeln!(f, "{} {:0.0}% - ({} colors)", self.name, self.pct * 100.0, self.count)?;
writeln!(f, "{:>18}: {:0.p$}", "Average DE", self.avg)?;
writeln!(f, "{:>18}: {:0.p$}", "Max DE", self.max)?;
writeln!(f, "{:>18}: {:0.p$}", "Min DE", self.min)?;
writeln!(f, "{:>18}: {:0.p$}", "StDev DE", self.stdev)
}
}