lux-rs 0.1.1

Pure Rust lighting and color science library inspired by LuxPy
Documentation
use crate::error::{LuxError, LuxResult};
use crate::spectrum::{getwld, Spectrum};

struct SpectralMismatchContext {
    spacing: Vec<f64>,
    target: Vec<f64>,
    relative_detectors: Vec<Vec<f64>>,
}

pub fn spectral_mismatch_f1prime(
    detector: &Spectrum,
    calibration_illuminant: &Spectrum,
    target_responsivity: &Spectrum,
) -> LuxResult<f64> {
    Ok(spectral_mismatch_f1primes(
        &Spectrum::new(
            detector.wavelengths().to_vec(),
            vec![detector.values().to_vec()],
        )?,
        calibration_illuminant,
        target_responsivity,
    )?[0])
}

pub fn spectral_mismatch_f1primes(
    detectors: &Spectrum,
    calibration_illuminant: &Spectrum,
    target_responsivity: &Spectrum,
) -> LuxResult<Vec<f64>> {
    let context = build_context(
        detectors,
        calibration_illuminant,
        target_responsivity,
        detectors.wavelengths(),
    )?;
    let target_integral = weighted_sum(&context.target, &context.spacing);
    if target_integral == 0.0 {
        return Err(LuxError::InvalidInput(
            "target responsivity integral must be non-zero",
        ));
    }

    Ok(context
        .relative_detectors
        .iter()
        .map(|relative_detector| {
            weighted_sum_abs_difference(relative_detector, &context.target, &context.spacing)
                / target_integral
        })
        .collect())
}

pub fn spectral_mismatch_correction_factor(
    measured_source: &Spectrum,
    detector: &Spectrum,
    calibration_illuminant: &Spectrum,
    target_responsivity: &Spectrum,
) -> LuxResult<f64> {
    Ok(spectral_mismatch_correction_factors(
        &Spectrum::new(
            measured_source.wavelengths().to_vec(),
            vec![measured_source.values().to_vec()],
        )?,
        &Spectrum::new(
            detector.wavelengths().to_vec(),
            vec![detector.values().to_vec()],
        )?,
        calibration_illuminant,
        target_responsivity,
    )?[0][0])
}

pub fn spectral_mismatch_correction_factors(
    measured_sources: &Spectrum,
    detectors: &Spectrum,
    calibration_illuminant: &Spectrum,
    target_responsivity: &Spectrum,
) -> LuxResult<Vec<Vec<f64>>> {
    let context = build_context(
        detectors,
        calibration_illuminant,
        target_responsivity,
        measured_sources.wavelengths(),
    )?;

    measured_sources
        .spectra()
        .iter()
        .map(|source| {
            let numerator = weighted_dot(source, &context.target, &context.spacing);
            context
                .relative_detectors
                .iter()
                .map(|relative_detector| {
                    let denominator = weighted_dot(source, relative_detector, &context.spacing);
                    if denominator == 0.0 {
                        Err(LuxError::InvalidInput(
                            "spectral mismatch denominator must be non-zero",
                        ))
                    } else {
                        Ok(numerator / denominator)
                    }
                })
                .collect()
        })
        .collect()
}

fn build_context(
    detectors: &Spectrum,
    calibration_illuminant: &Spectrum,
    target_responsivity: &Spectrum,
    wavelengths: &[f64],
) -> LuxResult<SpectralMismatchContext> {
    let spacing = getwld(wavelengths)?;
    let target = target_responsivity.interpolate_linear(wavelengths)?;
    let calibration = calibration_illuminant.interpolate_linear(wavelengths)?;
    let detectors = detectors.cie_interp_linear(wavelengths, true)?;

    let calibration_weights: Vec<f64> = calibration
        .values()
        .iter()
        .zip(spacing.iter())
        .map(|(value, dl)| value * dl)
        .collect();
    let target_weighted_sum = dot(target.values(), &calibration_weights);
    if target_weighted_sum == 0.0 {
        return Err(LuxError::InvalidInput(
            "target responsivity under calibration illuminant must be non-zero",
        ));
    }

    let relative_detectors = detectors
        .spectra()
        .iter()
        .map(|detector| {
            let detector_weighted_sum = dot(detector, &calibration_weights);
            if detector_weighted_sum == 0.0 {
                Err(LuxError::InvalidInput(
                    "detector responsivity under calibration illuminant must be non-zero",
                ))
            } else {
                let scale = target_weighted_sum / detector_weighted_sum;
                Ok(detector.iter().map(|value| value * scale).collect())
            }
        })
        .collect::<LuxResult<Vec<Vec<f64>>>>()?;

    Ok(SpectralMismatchContext {
        spacing,
        target: target.values().to_vec(),
        relative_detectors,
    })
}

fn weighted_dot(lhs: &[f64], rhs: &[f64], weights: &[f64]) -> f64 {
    lhs.iter()
        .zip(rhs.iter())
        .zip(weights.iter())
        .map(|((lhs, rhs), weight)| lhs * rhs * weight)
        .sum()
}

fn weighted_sum(values: &[f64], weights: &[f64]) -> f64 {
    values
        .iter()
        .zip(weights.iter())
        .map(|(value, weight)| value * weight)
        .sum()
}

fn weighted_sum_abs_difference(lhs: &[f64], rhs: &[f64], weights: &[f64]) -> f64 {
    lhs.iter()
        .zip(rhs.iter())
        .zip(weights.iter())
        .map(|((lhs, rhs), weight)| (lhs - rhs).abs() * weight)
        .sum()
}

fn dot(lhs: &[f64], rhs: &[f64]) -> f64 {
    lhs.iter().zip(rhs.iter()).map(|(lhs, rhs)| lhs * rhs).sum()
}