use crate::copp::CoppObjective;
use numpy::{PyArray1, PyReadonlyArray1};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::{PyAnyMethods, PyDict};
pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyTimeObjective>()?;
m.add_class::<PyLinearObjective>()?;
m.add_class::<PyThermalEnergyObjective>()?;
m.add_class::<PyTotalVariationTorqueObjective>()?;
Ok(())
}
#[derive(Clone)]
pub(crate) enum OwnedObjective {
Time {
weight: f64,
},
Linear {
weight: f64,
alpha: Vec<f64>,
beta: Vec<f64>,
},
ThermalEnergy {
weight: f64,
normalize: Vec<f64>,
},
TotalVariationTorque {
weight: f64,
normalize: Vec<f64>,
},
}
impl OwnedObjective {
pub(crate) fn as_rust(&self) -> CoppObjective<'_> {
match self {
Self::Time { weight } => CoppObjective::Time(*weight),
Self::Linear {
weight,
alpha,
beta,
} => CoppObjective::Linear(*weight, alpha.as_slice(), beta.as_slice()),
Self::ThermalEnergy { weight, normalize } => {
CoppObjective::ThermalEnergy(*weight, normalize.as_slice())
}
Self::TotalVariationTorque { weight, normalize } => {
CoppObjective::TotalVariationTorque(*weight, normalize.as_slice())
}
}
}
}
#[pyclass(name = "Time", module = "copp_py._native")]
pub(crate) struct PyTimeObjective {
weight: f64,
}
#[pymethods]
impl PyTimeObjective {
#[new]
#[pyo3(signature = (weight), text_signature = "(weight)")]
fn new(weight: f64) -> Self {
Self { weight }
}
#[getter]
fn weight(&self) -> f64 {
self.weight
}
fn __repr__(&self) -> String {
format!("Time(weight={})", self.weight)
}
}
#[pyclass(name = "Linear", module = "copp_py._native")]
pub(crate) struct PyLinearObjective {
weight: f64,
alpha: Vec<f64>,
beta: Vec<f64>,
}
#[pymethods]
impl PyLinearObjective {
#[new]
#[pyo3(signature = (weight, alpha, beta), text_signature = "(weight, alpha, beta)")]
fn new(weight: f64, alpha: &Bound<'_, PyAny>, beta: &Bound<'_, PyAny>) -> PyResult<Self> {
Ok(Self {
weight,
alpha: array_like_to_vec("alpha", alpha)?,
beta: array_like_to_vec("beta", beta)?,
})
}
#[getter]
fn weight(&self) -> f64 {
self.weight
}
#[getter]
fn alpha<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<f64>> {
PyArray1::from_vec(py, self.alpha.clone())
}
#[getter]
fn beta<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<f64>> {
PyArray1::from_vec(py, self.beta.clone())
}
fn __repr__(&self) -> String {
format!(
"Linear(weight={}, alpha_len={}, beta_len={})",
self.weight,
self.alpha.len(),
self.beta.len()
)
}
}
#[pyclass(name = "ThermalEnergy", module = "copp_py._native")]
pub(crate) struct PyThermalEnergyObjective {
weight: f64,
normalize: Vec<f64>,
}
#[pymethods]
impl PyThermalEnergyObjective {
#[new]
#[pyo3(signature = (weight, normalize), text_signature = "(weight, normalize)")]
fn new(weight: f64, normalize: &Bound<'_, PyAny>) -> PyResult<Self> {
Ok(Self {
weight,
normalize: array_like_to_vec("normalize", normalize)?,
})
}
#[getter]
fn weight(&self) -> f64 {
self.weight
}
#[getter]
fn normalize<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<f64>> {
PyArray1::from_vec(py, self.normalize.clone())
}
fn __repr__(&self) -> String {
format!(
"ThermalEnergy(weight={}, normalize_len={})",
self.weight,
self.normalize.len()
)
}
}
#[pyclass(name = "TotalVariationTorque", module = "copp_py._native")]
pub(crate) struct PyTotalVariationTorqueObjective {
weight: f64,
normalize: Vec<f64>,
}
#[pymethods]
impl PyTotalVariationTorqueObjective {
#[new]
#[pyo3(signature = (weight, normalize), text_signature = "(weight, normalize)")]
fn new(weight: f64, normalize: &Bound<'_, PyAny>) -> PyResult<Self> {
Ok(Self {
weight,
normalize: array_like_to_vec("normalize", normalize)?,
})
}
#[getter]
fn weight(&self) -> f64 {
self.weight
}
#[getter]
fn normalize<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<f64>> {
PyArray1::from_vec(py, self.normalize.clone())
}
fn __repr__(&self) -> String {
format!(
"TotalVariationTorque(weight={}, normalize_len={})",
self.weight,
self.normalize.len()
)
}
}
pub(crate) fn parse_objectives(objectives: &Bound<'_, PyAny>) -> PyResult<Vec<OwnedObjective>> {
if let Some(objective) = parse_objective_item(objectives)? {
return Ok(vec![objective]);
}
let iter = objectives.try_iter().map_err(|_| {
PyValueError::new_err(
"`objectives` must be an objective object, an objective dict, or an iterable of them",
)
})?;
let mut parsed = Vec::new();
for item in iter {
let item = item?;
let objective = parse_objective_item(&item)?.ok_or_else(|| {
PyValueError::new_err(
"each objective must be Time, Linear, ThermalEnergy, TotalVariationTorque, or a dict",
)
})?;
parsed.push(objective);
}
Ok(parsed)
}
fn parse_objective_item(obj: &Bound<'_, PyAny>) -> PyResult<Option<OwnedObjective>> {
if let Ok(value) = obj.extract::<PyRef<'_, PyTimeObjective>>() {
return Ok(Some(OwnedObjective::Time {
weight: value.weight,
}));
}
if let Ok(value) = obj.extract::<PyRef<'_, PyLinearObjective>>() {
return Ok(Some(OwnedObjective::Linear {
weight: value.weight,
alpha: value.alpha.clone(),
beta: value.beta.clone(),
}));
}
if let Ok(value) = obj.extract::<PyRef<'_, PyThermalEnergyObjective>>() {
return Ok(Some(OwnedObjective::ThermalEnergy {
weight: value.weight,
normalize: value.normalize.clone(),
}));
}
if let Ok(value) = obj.extract::<PyRef<'_, PyTotalVariationTorqueObjective>>() {
return Ok(Some(OwnedObjective::TotalVariationTorque {
weight: value.weight,
normalize: value.normalize.clone(),
}));
}
if let Ok(dict) = obj.cast::<PyDict>() {
return parse_objective_dict(dict).map(Some);
}
Ok(None)
}
fn parse_objective_dict(dict: &Bound<'_, PyDict>) -> PyResult<OwnedObjective> {
let kind = required_item(dict, "kind")?;
let kind = normalize_token(
kind.extract::<&str>()
.map_err(|_| PyValueError::new_err("objective dict field `kind` must be a string"))?,
);
let weight = required_item(dict, "weight")?
.extract::<f64>()
.map_err(|_| PyValueError::new_err("objective dict field `weight` must be a float"))?;
match kind.as_str() {
"time" => Ok(OwnedObjective::Time { weight }),
"linear" => Ok(OwnedObjective::Linear {
weight,
alpha: array_like_to_vec("alpha", &required_item(dict, "alpha")?)?,
beta: array_like_to_vec("beta", &required_item(dict, "beta")?)?,
}),
"thermal_energy" | "thermalenergy" | "thermal" => Ok(OwnedObjective::ThermalEnergy {
weight,
normalize: array_like_to_vec("normalize", &required_item(dict, "normalize")?)?,
}),
"total_variation_torque" | "totalvariationtorque" | "tv_torque" | "tvtorque" => {
Ok(OwnedObjective::TotalVariationTorque {
weight,
normalize: array_like_to_vec("normalize", &required_item(dict, "normalize")?)?,
})
}
_ => Err(PyValueError::new_err(
"objective dict field `kind` must be one of \"time\", \"linear\", \"thermal_energy\", or \"total_variation_torque\"",
)),
}
}
fn required_item<'py>(dict: &Bound<'py, PyDict>, key: &str) -> PyResult<Bound<'py, PyAny>> {
dict.get_item(key)?.ok_or_else(|| {
PyValueError::new_err(format!("objective dict is missing required field `{key}`"))
})
}
fn array_like_to_vec(name: &str, obj: &Bound<'_, PyAny>) -> PyResult<Vec<f64>> {
let py = obj.py();
let numpy = py.import("numpy")?;
let dtype = numpy.getattr("float64")?;
let array_obj = numpy.getattr("ascontiguousarray")?.call1((obj, dtype))?;
let array = array_obj
.extract::<PyReadonlyArray1<'_, f64>>()
.map_err(|_| {
PyValueError::new_err(format!(
"`{name}` must be convertible to a one-dimensional float64 NumPy array"
))
})?;
let values = array.as_slice().map_err(|_| {
PyValueError::new_err(format!(
"`{name}` must be a contiguous one-dimensional float64 array"
))
})?;
Ok(values.to_vec())
}
fn normalize_token(text: &str) -> String {
text.trim().to_ascii_lowercase().replace('-', "_")
}