use laddu_expr::parameters::{Bounds, InitialSpec, ParamState, Parameter, ParameterUpdate};
use pyo3::{prelude::*, types::PyDict};
use super::error::to_py_err;
struct UpdateArg<T>(Option<T>);
impl<'a, 'py, T: FromPyObject<'a, 'py>> FromPyObject<'a, 'py> for UpdateArg<T> {
type Error = T::Error;
const INPUT_TYPE: pyo3::inspect::PyStaticExpr = T::INPUT_TYPE;
fn extract(value: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
let value = T::extract(value)?;
Ok(Self(Some(value)))
}
}
#[derive(Clone, FromPyObject, IntoPyObject, IntoPyObjectRef)]
enum InitialValue {
Value(f64),
Uniform((f64, f64)),
}
fn initial_value(initial: &InitialSpec) -> Option<InitialValue> {
match initial {
InitialSpec::Default => None,
InitialSpec::Value(value) => Some(InitialValue::Value(*value)),
InitialSpec::Uniform { min, max } => Some(InitialValue::Uniform((*min, *max))),
}
}
#[pyclass(
name = "ParameterUpdate",
module = "laddu",
frozen,
skip_from_py_object
)]
#[derive(Clone)]
pub struct PyParameterUpdate {
pub(crate) inner: ParameterUpdate,
}
#[pymethods]
impl PyParameterUpdate {
#[new]
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
#[pyo3(signature = (
*,
fixed: "float | None" = UpdateArg(None),
initial: "float | tuple[float, float] | None" = UpdateArg(None),
bounds: "tuple[float | None, float | None] | None" = UpdateArg(None),
periodic: "bool" = UpdateArg(None),
scale: "float | None" = UpdateArg(None),
unit: "str | None" = UpdateArg(None),
latex: "str | None" = UpdateArg(None),
description: "str | None" = UpdateArg(None)
))]
fn new(
fixed: UpdateArg<Option<f64>>,
initial: UpdateArg<Option<InitialValue>>,
bounds: UpdateArg<Option<(Option<f64>, Option<f64>)>>,
periodic: UpdateArg<bool>,
scale: UpdateArg<Option<f64>>,
unit: UpdateArg<Option<String>>,
latex: UpdateArg<Option<String>>,
description: UpdateArg<Option<String>>,
) -> PyResult<Self> {
let inner = ParameterUpdate {
state: fixed.0.map(|value| match value {
Some(value) => ParamState::Fixed(value),
None => ParamState::Free,
}),
initial: initial.0.map(|value| match value {
Some(InitialValue::Value(value)) => InitialSpec::Value(value),
Some(InitialValue::Uniform((min, max))) => InitialSpec::Uniform { min, max },
None => InitialSpec::Default,
}),
bounds: bounds.0.map(|value| match value {
Some((min, max)) => Bounds { min, max },
None => Bounds::default(),
}),
periodic: periodic.0,
scale: scale.0,
unit: unit.0,
latex: latex.0,
description: description.0,
};
inner.validate().map_err(to_py_err)?;
Ok(Self { inner })
}
fn __repr__(&self, py: Python<'_>) -> PyResult<String> {
let fields = PyDict::new(py);
if let Some(state) = &self.inner.state {
fields.set_item(
"fixed",
match state {
ParamState::Free => None,
ParamState::Fixed(value) => Some(*value),
},
)?;
}
if let Some(initial) = &self.inner.initial {
fields.set_item("initial", initial_value(initial))?;
}
if let Some(bounds) = &self.inner.bounds {
fields.set_item(
"bounds",
(bounds.min.is_some() || bounds.max.is_some()).then_some((bounds.min, bounds.max)),
)?;
}
if let Some(periodic) = self.inner.periodic {
fields.set_item("periodic", periodic)?;
}
if let Some(scale) = self.inner.scale {
fields.set_item("scale", scale)?;
}
for (name, value) in [
("unit", &self.inner.unit),
("latex", &self.inner.latex),
("description", &self.inner.description),
] {
if let Some(value) = value {
fields.set_item(name, value)?;
}
}
let fields = fields
.iter()
.map(|(key, value)| Ok(format!("{}={}", key, value.repr()?)))
.collect::<PyResult<Vec<_>>>()?;
Ok(format!("ParameterUpdate({})", fields.join(", ")))
}
}
#[pyclass(
name = "ParameterSpec",
module = "laddu",
frozen,
get_all,
skip_from_py_object
)]
#[derive(Clone)]
pub struct PyParameterSpec {
name: String,
fixed: Option<f64>,
initial: Option<InitialValue>,
bounds: Option<(Option<f64>, Option<f64>)>,
periodic: bool,
scale: Option<f64>,
unit: Option<String>,
latex: Option<String>,
description: Option<String>,
}
impl From<&Parameter> for PyParameterSpec {
fn from(parameter: &Parameter) -> Self {
let bounds = parameter.bounds_spec();
Self {
name: parameter.name().to_owned(),
fixed: match parameter.state() {
ParamState::Free => None,
ParamState::Fixed(value) => Some(*value),
},
initial: initial_value(parameter.initial_spec()),
bounds: (bounds.min.is_some() || bounds.max.is_some())
.then_some((bounds.min, bounds.max)),
periodic: parameter.is_periodic(),
scale: parameter.scale(),
unit: parameter.unit_label().map(str::to_owned),
latex: parameter.latex_label().map(str::to_owned),
description: parameter.description_text().map(str::to_owned),
}
}
}
#[pymethods]
impl PyParameterSpec {
fn __repr__(&self, py: Python<'_>) -> PyResult<String> {
let fields = PyDict::new(py);
fields.set_item("name", &self.name)?;
fields.set_item("fixed", self.fixed)?;
fields.set_item("initial", &self.initial)?;
fields.set_item("bounds", self.bounds)?;
fields.set_item("periodic", self.periodic)?;
fields.set_item("scale", self.scale)?;
fields.set_item("unit", &self.unit)?;
fields.set_item("latex", &self.latex)?;
fields.set_item("description", &self.description)?;
let fields = fields
.iter()
.map(|(key, value)| Ok(format!("{}={}", key, value.repr()?)))
.collect::<PyResult<Vec<_>>>()?;
Ok(format!("ParameterSpec({})", fields.join(", ")))
}
}