use ndarray::{Array1, Array3};
use numpy::{IntoPyArray, PyArray1, PyArray2, PyReadonlyArray1, PyReadonlyArray3};
use pyo3::exceptions::{PyRuntimeError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::PyDict;
use npls1::{Npls as RustNpls, NplsError};
fn map_err(e: NplsError) -> PyErr {
match e {
NplsError::LinalgError(msg) => PyRuntimeError::new_err(format!("LinalgError: {msg}")),
NplsError::ValueError(msg) => PyValueError::new_err(msg),
NplsError::NotFitted => PyRuntimeError::new_err(
"Model has not been fitted yet. Call fit() before predict().",
),
}
}
#[pyclass(name = "Npls")]
pub struct PyNpls {
inner: RustNpls,
}
#[pymethods]
impl PyNpls {
#[new]
#[pyo3(signature = (
n_components,
a,
derivative_rang = None,
norm_func = None,
crash_norm_name = None,
crash_norm_value = None,
excitation_wavelenth = None,
emission_wavelenth = None,
))]
#[allow(clippy::too_many_arguments)]
fn new(
n_components: usize,
a: f64,
derivative_rang: Option<Vec<usize>>,
norm_func: Option<Vec<String>>,
crash_norm_name: Option<String>,
crash_norm_value: Option<f64>,
excitation_wavelenth: Option<PyReadonlyArray1<f64>>,
emission_wavelenth: Option<PyReadonlyArray1<f64>>,
) -> PyResult<Self> {
let deriv = derivative_rang.unwrap_or_default();
let norms = norm_func.unwrap_or_default();
let use_snr = !deriv.is_empty()
|| crash_norm_name.is_some()
|| excitation_wavelenth.is_some()
|| emission_wavelenth.is_some();
let inner = if use_snr {
let exc = match excitation_wavelenth {
Some(arr) => arr.as_array().to_owned(),
None => Array1::zeros(1),
};
let emi = match emission_wavelenth {
Some(arr) => arr.as_array().to_owned(),
None => Array1::zeros(1),
};
RustNpls::with_snr(
n_components,
a,
deriv,
norms,
crash_norm_name,
crash_norm_value,
exc,
emi,
)
} else {
RustNpls::new(n_components, a)
};
Ok(PyNpls { inner })
}
fn fit<'py>(
mut slf: PyRefMut<'py, Self>,
xtrain: PyReadonlyArray3<'py, f64>,
ytrain: PyReadonlyArray1<'py, f64>,
) -> PyResult<PyRefMut<'py, Self>> {
let x: Array3<f64> = xtrain.as_array().to_owned();
let y: Array1<f64> = ytrain.as_array().to_owned();
slf.inner.fit(&x, &y).map_err(map_err)?;
Ok(slf)
}
fn predict<'py>(
&self,
py: Python<'py>,
xtest: PyReadonlyArray3<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let x: Array3<f64> = xtest.as_array().to_owned();
let preds = self.inner.predict(&x).map_err(map_err)?;
Ok(preds.into_pyarray(py))
}
#[getter]
fn n_components(&self) -> usize {
self.inner.n_components
}
#[getter]
fn a(&self) -> f64 {
self.inner.a
}
#[getter]
fn train_error(&self) -> Option<f64> {
self.inner.train_error
}
#[getter]
fn w_k<'py>(&self, py: Python<'py>) -> Option<Vec<Bound<'py, PyArray2<f64>>>> {
self.inner
.w_k
.as_ref()
.map(|vec| vec.iter().map(|m| m.clone().into_pyarray(py)).collect())
}
#[getter]
fn w_i<'py>(&self, py: Python<'py>) -> Option<Vec<Bound<'py, PyArray2<f64>>>> {
self.inner
.w_i
.as_ref()
.map(|vec| vec.iter().map(|m| m.clone().into_pyarray(py)).collect())
}
#[getter]
fn bf_array<'py>(&self, py: Python<'py>) -> Option<Vec<Bound<'py, PyArray2<f64>>>> {
self.inner
.bf_array
.as_ref()
.map(|vec| vec.iter().map(|m| m.clone().into_pyarray(py)).collect())
}
#[getter]
fn snr_emission<'py>(&self, py: Python<'py>) -> PyResult<Option<Vec<Bound<'py, PyDict>>>> {
snr_to_py(py, &self.inner.snr_emission)
}
#[getter]
fn snr_excitation<'py>(&self, py: Python<'py>) -> PyResult<Option<Vec<Bound<'py, PyDict>>>> {
snr_to_py(py, &self.inner.snr_excitation)
}
fn __repr__(&self) -> String {
format!(
"Npls(n_components={}, a={}, fitted={})",
self.inner.n_components,
self.inner.a,
self.inner.bf_array.is_some()
)
}
}
fn snr_to_py<'py>(
py: Python<'py>,
snr: &Option<Vec<npls1::SnrResponse>>,
) -> PyResult<Option<Vec<Bound<'py, PyDict>>>> {
match snr {
None => Ok(None),
Some(list) => {
let mut out = Vec::with_capacity(list.len());
for comp in list {
let d = PyDict::new(py);
for (k, v) in comp.iter() {
d.set_item(k, v.clone())?;
}
out.push(d);
}
Ok(Some(out))
}
}
}
#[pymodule]
fn npls1_python(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyNpls>()?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
Ok(())
}