mod distribution;
mod engine;
mod fast;
mod kronrod;
mod lin;
mod matrix;
mod moments;
mod particle;
mod range;
use num_complex::Complex;
pub use distribution::SizeDistribution;
use crate::mie::{GreekCoefficients, MAX_GREEK_ORDER, Normalization};
use crate::{Error, Result};
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum Particle {
Sphere {
refractive_index: Vec<Complex<f64>>,
},
LayeredSphere {
radius_fractions: Vec<f64>,
refractive_index: Vec<Vec<Complex<f64>>>,
},
}
impl Particle {
pub fn sphere(refractive_index: Vec<Complex<f64>>) -> Self {
Particle::Sphere { refractive_index }
}
pub fn layered_sphere(
radius_fractions: Vec<f64>,
refractive_index: Vec<Vec<Complex<f64>>>,
) -> Self {
Particle::LayeredSphere {
radius_fractions,
refractive_index,
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Component {
pub distribution: SizeDistribution,
pub particle: Particle,
pub weight: f64,
}
impl Component {
pub fn new(
distribution: SizeDistribution,
refractive_index: Vec<Complex<f64>>,
weight: f64,
) -> Self {
Self::with_particle(distribution, Particle::sphere(refractive_index), weight)
}
pub fn with_particle(distribution: SizeDistribution, particle: Particle, weight: f64) -> Self {
Self {
distribution,
particle,
weight,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum GreekOrder {
All,
Order(usize),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ParamKind {
Distribution(&'static str),
RefrReal,
RefrImag,
LayerRefrReal(usize),
LayerRefrImag(usize),
RadiusFraction(usize),
Weight,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct EnsembleParam {
pub component: usize,
pub mode: Option<usize>,
pub kind: ParamKind,
}
impl EnsembleParam {
pub fn new(component: usize, mode: Option<usize>, kind: ParamKind) -> Self {
Self {
component,
mode,
kind,
}
}
pub fn name(&self, mixture: bool) -> String {
let base = match self.kind {
ParamKind::Distribution(name) => name.to_owned(),
ParamKind::RefrReal => "m_real".to_owned(),
ParamKind::RefrImag => "m_imag".to_owned(),
ParamKind::LayerRefrReal(l) => format!("layer{l}.m_real"),
ParamKind::LayerRefrImag(l) => format!("layer{l}.m_imag"),
ParamKind::RadiusFraction(l) => format!("layer{l}.radius_fraction"),
ParamKind::Weight => "weight".to_owned(),
};
let mut out = String::new();
if mixture {
out += &format!("c{}.", self.component);
}
if let Some(j) = self.mode {
out += &format!("mode{j}.");
}
out + &base
}
}
fn component_parameters(
index: usize,
distribution: &SizeDistribution,
particle: &Particle,
weight: bool,
) -> Vec<EnsembleParam> {
let mut out = Vec::new();
if let SizeDistribution::MultiModal { modes } = distribution {
for (j, (_, mode)) in modes.iter().enumerate() {
for name in mode.parameter_names() {
out.push(EnsembleParam::new(
index,
Some(j),
ParamKind::Distribution(name),
));
}
out.push(EnsembleParam::new(index, Some(j), ParamKind::Weight));
}
} else {
for name in distribution.parameter_names() {
out.push(EnsembleParam::new(
index,
None,
ParamKind::Distribution(name),
));
}
}
match particle {
Particle::Sphere { .. } => {
out.push(EnsembleParam::new(index, None, ParamKind::RefrReal));
out.push(EnsembleParam::new(index, None, ParamKind::RefrImag));
}
Particle::LayeredSphere {
radius_fractions,
refractive_index,
} => {
for l in 0..refractive_index.len() {
out.push(EnsembleParam::new(index, None, ParamKind::LayerRefrReal(l)));
out.push(EnsembleParam::new(index, None, ParamKind::LayerRefrImag(l)));
}
for l in 0..radius_fractions.len() {
out.push(EnsembleParam::new(
index,
None,
ParamKind::RadiusFraction(l),
));
}
}
}
if weight {
out.push(EnsembleParam::new(index, None, ParamKind::Weight));
}
out
}
pub fn parameters(distribution: &SizeDistribution) -> Vec<EnsembleParam> {
parameters_with_particle(distribution, &Particle::sphere(Vec::new()))
}
pub fn parameters_with_particle(
distribution: &SizeDistribution,
particle: &Particle,
) -> Vec<EnsembleParam> {
component_parameters(0, distribution, particle, false)
}
pub fn mixture_parameters(components: &[Component]) -> Vec<EnsembleParam> {
components
.iter()
.enumerate()
.flat_map(|(i, c)| component_parameters(i, &c.distribution, &c.particle, true))
.collect()
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct EnsembleOptions {
pub rtol: f64,
pub max_panels: usize,
pub greek: Option<GreekOrder>,
pub mu: Vec<f64>,
pub normalization: Normalization,
pub derivatives: Vec<EnsembleParam>,
pub allow_unvalidated: bool,
pub refine_order: Option<usize>,
pub refine_backscatter: bool,
}
impl Default for EnsembleOptions {
fn default() -> Self {
Self {
rtol: 1e-6,
max_panels: 4096,
greek: None,
mu: Vec::new(),
normalization: Normalization::Normalized,
derivatives: Vec::new(),
allow_unvalidated: false,
refine_order: None,
refine_backscatter: false,
}
}
}
impl EnsembleOptions {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_rtol(mut self, rtol: f64) -> Self {
self.rtol = rtol;
self
}
#[must_use]
pub fn with_max_panels(mut self, max_panels: usize) -> Self {
self.max_panels = max_panels;
self
}
#[must_use]
pub fn with_greek(mut self, order: GreekOrder) -> Self {
self.greek = Some(order);
self
}
#[must_use]
pub fn with_mu(mut self, mu: Vec<f64>) -> Self {
self.mu = mu;
self
}
#[must_use]
pub fn with_normalization(mut self, normalization: Normalization) -> Self {
self.normalization = normalization;
self
}
#[must_use]
pub fn with_derivatives(mut self, params: Vec<EnsembleParam>) -> Self {
self.derivatives = params;
self
}
#[must_use]
pub fn with_allow_unvalidated(mut self, allow: bool) -> Self {
self.allow_unvalidated = allow;
self
}
#[must_use]
pub fn with_refine_order(mut self, order: usize) -> Self {
self.refine_order = Some(order);
self
}
#[must_use]
pub fn with_refine_backscatter(mut self, refine: bool) -> Self {
self.refine_backscatter = refine;
self
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct PhaseValues {
pub f11: Vec<f64>,
pub f12: Vec<f64>,
pub f33: Vec<f64>,
pub f34: Vec<f64>,
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Outputs {
pub c_ext: f64,
pub c_sca: f64,
pub c_abs: f64,
pub c_back: f64,
pub ssa: f64,
pub g: f64,
pub lidar_ratio: f64,
pub depolarization: f64,
pub effective_radius: f64,
pub mean_area: f64,
pub mean_volume: f64,
pub greek: Option<GreekCoefficients<f64>>,
pub phase: Option<PhaseValues>,
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct EnsembleResult {
pub values: Outputs,
pub errors: Outputs,
pub derivatives: Vec<Outputs>,
pub derivative_errors: Vec<Outputs>,
pub params: Vec<EnsembleParam>,
pub converged: bool,
pub panels: usize,
pub ranges: Vec<(f64, f64)>,
}
pub const MAX_REFINE_ORDER: usize = 1024;
fn invalid(msg: String) -> Error {
Error::InvalidInput(msg)
}
pub fn compute(
distribution: &SizeDistribution,
wavelengths: &[f64],
refractive_index: &[Complex<f64>],
options: &EnsembleOptions,
) -> Result<Vec<EnsembleResult>> {
let particle = Particle::sphere(refractive_index.to_vec());
compute_with_particle(distribution, wavelengths, &particle, options)
}
pub fn compute_with_particle(
distribution: &SizeDistribution,
wavelengths: &[f64],
particle: &Particle,
options: &EnsembleOptions,
) -> Result<Vec<EnsembleResult>> {
let component = Component::with_particle(distribution.clone(), particle.clone(), 1.0);
let allowed = parameters_with_particle(distribution, particle);
validate(
core::slice::from_ref(&component),
wavelengths,
options,
&allowed,
)?;
engine::run(&[component], wavelengths, options)
}
pub fn compute_mixture(
components: &[Component],
wavelengths: &[f64],
options: &EnsembleOptions,
) -> Result<Vec<EnsembleResult>> {
if components.is_empty() {
return Err(invalid("a mixture needs a component".into()));
}
let allowed = mixture_parameters(components);
validate(components, wavelengths, options, &allowed)?;
engine::run(components, wavelengths, options)
}
pub fn compute_batch(
distributions: &[SizeDistribution],
wavelengths: &[f64],
refractive_index: &[Complex<f64>],
options: &EnsembleOptions,
) -> Result<Vec<Vec<EnsembleResult>>> {
let particle = Particle::sphere(refractive_index.to_vec());
compute_batch_with_particle(distributions, wavelengths, &particle, options)
}
pub fn compute_batch_with_particle(
distributions: &[SizeDistribution],
wavelengths: &[f64],
particle: &Particle,
options: &EnsembleOptions,
) -> Result<Vec<Vec<EnsembleResult>>> {
if distributions.is_empty() {
return Err(invalid("a batch needs a distribution".into()));
}
if !options.derivatives.is_empty() {
return Err(invalid(
"a batch computes values only: request derivatives from compute".into(),
));
}
particle.validate().map_err(invalid)?;
if particle.wavelengths() != wavelengths.len() {
return Err(invalid(format!(
"{} refractive indices for {} wavelengths",
particle.wavelengths(),
wavelengths.len()
)));
}
for (i, d) in distributions.iter().enumerate() {
d.validate()
.map_err(|e| invalid(format!("distribution {i}: {e}")))?;
}
let components: Vec<Component> = distributions
.iter()
.map(|d| Component::with_particle(d.clone(), particle.clone(), 1.0))
.collect();
validate(&components, wavelengths, options, &[])?;
engine::run_batch(&components, wavelengths, options)
}
fn validate(
components: &[Component],
wavelengths: &[f64],
options: &EnsembleOptions,
allowed: &[EnsembleParam],
) -> Result<()> {
if !(1e-13..=0.1).contains(&options.rtol) {
return Err(invalid(format!(
"rtol must be in [1e-13, 0.1], got {}",
options.rtol
)));
}
if options.max_panels == 0 {
return Err(invalid("max_panels must be at least 1".into()));
}
let greek_order = match options.greek {
Some(GreekOrder::Order(l)) => Some(l),
_ => None,
};
if let Some(l) = greek_order.filter(|&l| l > MAX_GREEK_ORDER) {
return Err(invalid(format!(
"the Greek order must be at most {MAX_GREEK_ORDER}, got {l}"
)));
}
if let Some(l) = options.refine_order.filter(|&l| l > MAX_REFINE_ORDER) {
return Err(invalid(format!(
"refine_order must be at most {MAX_REFINE_ORDER}, got {l}"
)));
}
if let Some((j, v)) = options
.mu
.iter()
.enumerate()
.find(|&(_, v)| !(-1.0..=1.0).contains(v))
{
return Err(invalid(format!("cosine {j} must be in [-1, 1], got {v}")));
}
if let Some((i, w)) = wavelengths
.iter()
.enumerate()
.find(|&(_, w)| !(w.is_finite() && *w > 0.0))
{
return Err(invalid(format!(
"wavelength {i} must be finite and > 0, got {w}"
)));
}
let mut total = 0.0;
for (i, c) in components.iter().enumerate() {
c.distribution
.validate()
.map_err(|e| invalid(format!("component {i}: {e}")))?;
if let SizeDistribution::MultiModal { modes } = &c.distribution {
if modes
.iter()
.any(|(_, m)| matches!(m, SizeDistribution::MultiModal { .. }))
{
return Err(invalid(format!(
"component {i}: the modes of a multimodal distribution cannot be multimodal"
)));
}
}
c.particle
.validate()
.map_err(|e| invalid(format!("component {i}: {e}")))?;
if c.particle.wavelengths() != wavelengths.len() {
return Err(invalid(format!(
"component {i} has {} refractive indices for {} wavelengths",
c.particle.wavelengths(),
wavelengths.len()
)));
}
if !(c.weight.is_finite() && c.weight >= 0.0) {
return Err(invalid(format!(
"the weight of component {i} must be finite and >= 0, got {}",
c.weight
)));
}
if let SizeDistribution::MultiModal { modes } = &c.distribution {
let sum: f64 = modes.iter().map(|(w, _)| w).sum();
if !sum.is_finite() {
return Err(invalid(format!(
"component {i}: the mode weights must have a finite sum"
)));
}
}
total += c.weight;
}
if !(total.is_finite() && total > 0.0) {
return Err(invalid(format!(
"the mixture weights must have a finite, positive sum, got {total}"
)));
}
let mixture = components.len() > 1
|| allowed
.iter()
.any(|p| p.kind == ParamKind::Weight && p.mode.is_none());
for (k, p) in options.derivatives.iter().enumerate() {
if options.derivatives[..k].contains(p) {
return Err(invalid(format!(
"parameter {} requested twice",
p.name(mixture)
)));
}
if !allowed.contains(p) {
return Err(invalid(format!(
"no parameter {} here; the parameters are {:?}",
p.name(mixture),
allowed.iter().map(|q| q.name(mixture)).collect::<Vec<_>>()
)));
}
}
Ok(())
}