1use ndarray::{Array1, Array3};
5use numpy::{IntoPyArray, PyArray1, PyArray2, PyReadonlyArray1, PyReadonlyArray3};
6use pyo3::exceptions::{PyRuntimeError, PyValueError};
7use pyo3::prelude::*;
8use pyo3::types::PyDict;
9
10use npls1::{Npls as RustNpls, NplsError};
11
12fn map_err(e: NplsError) -> PyErr {
14 match e {
15 NplsError::LinalgError(msg) => PyRuntimeError::new_err(format!("LinalgError: {msg}")),
16 NplsError::ValueError(msg) => PyValueError::new_err(msg),
17 NplsError::NotFitted => PyRuntimeError::new_err(
18 "Model has not been fitted yet. Call fit() before predict().",
19 ),
20 }
21}
22
23#[pyclass(name = "Npls")]
25pub struct PyNpls {
26 inner: RustNpls,
27}
28
29#[pymethods]
30impl PyNpls {
31 #[new]
32 #[pyo3(signature = (
33 n_components,
34 a,
35 derivative_rang = None,
36 norm_func = None,
37 crash_norm_name = None,
38 crash_norm_value = None,
39 excitation_wavelenth = None,
40 emission_wavelenth = None,
41 ))]
42 #[allow(clippy::too_many_arguments)]
43 fn new(
44 n_components: usize,
45 a: f64,
46 derivative_rang: Option<Vec<usize>>,
47 norm_func: Option<Vec<String>>,
48 crash_norm_name: Option<String>,
49 crash_norm_value: Option<f64>,
50 excitation_wavelenth: Option<PyReadonlyArray1<f64>>,
51 emission_wavelenth: Option<PyReadonlyArray1<f64>>,
52 ) -> PyResult<Self> {
53 let deriv = derivative_rang.unwrap_or_default();
54 let norms = norm_func.unwrap_or_default();
55
56 let use_snr = !deriv.is_empty()
57 || crash_norm_name.is_some()
58 || excitation_wavelenth.is_some()
59 || emission_wavelenth.is_some();
60
61 let inner = if use_snr {
62 let exc = match excitation_wavelenth {
63 Some(arr) => arr.as_array().to_owned(),
64 None => Array1::zeros(1),
65 };
66 let emi = match emission_wavelenth {
67 Some(arr) => arr.as_array().to_owned(),
68 None => Array1::zeros(1),
69 };
70 RustNpls::with_snr(
71 n_components,
72 a,
73 deriv,
74 norms,
75 crash_norm_name,
76 crash_norm_value,
77 exc,
78 emi,
79 )
80 } else {
81 RustNpls::new(n_components, a)
82 };
83
84 Ok(PyNpls { inner })
85 }
86
87 fn fit<'py>(
89 mut slf: PyRefMut<'py, Self>,
90 xtrain: PyReadonlyArray3<'py, f64>,
91 ytrain: PyReadonlyArray1<'py, f64>,
92 ) -> PyResult<PyRefMut<'py, Self>> {
93 let x: Array3<f64> = xtrain.as_array().to_owned();
94 let y: Array1<f64> = ytrain.as_array().to_owned();
95 slf.inner.fit(&x, &y).map_err(map_err)?;
96 Ok(slf)
97 }
98
99 fn predict<'py>(
101 &self,
102 py: Python<'py>,
103 xtest: PyReadonlyArray3<'py, f64>,
104 ) -> PyResult<Bound<'py, PyArray1<f64>>> {
105 let x: Array3<f64> = xtest.as_array().to_owned();
106 let preds = self.inner.predict(&x).map_err(map_err)?;
107 Ok(preds.into_pyarray(py))
108 }
109
110 #[getter]
113 fn n_components(&self) -> usize {
114 self.inner.n_components
115 }
116
117 #[getter]
118 fn a(&self) -> f64 {
119 self.inner.a
120 }
121
122 #[getter]
123 fn train_error(&self) -> Option<f64> {
124 self.inner.train_error
125 }
126
127 #[getter]
128 fn w_k<'py>(&self, py: Python<'py>) -> Option<Vec<Bound<'py, PyArray2<f64>>>> {
129 self.inner
130 .w_k
131 .as_ref()
132 .map(|vec| vec.iter().map(|m| m.clone().into_pyarray(py)).collect())
133 }
134
135 #[getter]
136 fn w_i<'py>(&self, py: Python<'py>) -> Option<Vec<Bound<'py, PyArray2<f64>>>> {
137 self.inner
138 .w_i
139 .as_ref()
140 .map(|vec| vec.iter().map(|m| m.clone().into_pyarray(py)).collect())
141 }
142
143 #[getter]
144 fn bf_array<'py>(&self, py: Python<'py>) -> Option<Vec<Bound<'py, PyArray2<f64>>>> {
145 self.inner
146 .bf_array
147 .as_ref()
148 .map(|vec| vec.iter().map(|m| m.clone().into_pyarray(py)).collect())
149 }
150
151 #[getter]
152 fn snr_emission<'py>(&self, py: Python<'py>) -> PyResult<Option<Vec<Bound<'py, PyDict>>>> {
153 snr_to_py(py, &self.inner.snr_emission)
154 }
155
156 #[getter]
157 fn snr_excitation<'py>(&self, py: Python<'py>) -> PyResult<Option<Vec<Bound<'py, PyDict>>>> {
158 snr_to_py(py, &self.inner.snr_excitation)
159 }
160
161 fn __repr__(&self) -> String {
162 format!(
163 "Npls(n_components={}, a={}, fitted={})",
164 self.inner.n_components,
165 self.inner.a,
166 self.inner.bf_array.is_some()
167 )
168 }
169}
170
171fn snr_to_py<'py>(
173 py: Python<'py>,
174 snr: &Option<Vec<npls1::SnrResponse>>,
175) -> PyResult<Option<Vec<Bound<'py, PyDict>>>> {
176 match snr {
177 None => Ok(None),
178 Some(list) => {
179 let mut out = Vec::with_capacity(list.len());
180 for comp in list {
181 let d = PyDict::new(py);
182 for (k, v) in comp.iter() {
183 d.set_item(k, v.clone())?;
184 }
185 out.push(d);
186 }
187 Ok(Some(out))
188 }
189 }
190}
191
192#[pymodule]
194fn npls1_python(m: &Bound<'_, PyModule>) -> PyResult<()> {
195 m.add_class::<PyNpls>()?;
196 m.add("__version__", env!("CARGO_PKG_VERSION"))?;
197 Ok(())
198}