use crate::spatial::{FourBoxRegion, HemisphericRegion};
use crate::state::{FourBoxSlice, HemisphericSlice, StateValue};
use crate::timeseries::FloatValue;
use numpy::{PyArray1, ToPyArray};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::{pymodule, Bound, PyResult};
#[pyclass]
#[pyo3(name = "FourBoxSlice")]
#[derive(Debug, Clone)]
pub struct PyFourBoxSlice(pub FourBoxSlice);
#[pymethods]
impl PyFourBoxSlice {
#[new]
#[pyo3(signature = (northern_ocean=f64::NAN, northern_land=f64::NAN, southern_ocean=f64::NAN, southern_land=f64::NAN))]
fn new(
northern_ocean: FloatValue,
northern_land: FloatValue,
southern_ocean: FloatValue,
southern_land: FloatValue,
) -> Self {
Self(FourBoxSlice::from_array([
northern_ocean,
northern_land,
southern_ocean,
southern_land,
]))
}
#[staticmethod]
fn uniform(value: FloatValue) -> Self {
Self(FourBoxSlice::uniform(value))
}
#[staticmethod]
fn from_array(values: [FloatValue; 4]) -> Self {
Self(FourBoxSlice::from_array(values))
}
#[getter]
fn northern_ocean(&self) -> FloatValue {
self.0.get(FourBoxRegion::NorthernOcean)
}
#[setter]
fn set_northern_ocean(&mut self, value: FloatValue) {
self.0.set(FourBoxRegion::NorthernOcean, value);
}
#[getter]
fn northern_land(&self) -> FloatValue {
self.0.get(FourBoxRegion::NorthernLand)
}
#[setter]
fn set_northern_land(&mut self, value: FloatValue) {
self.0.set(FourBoxRegion::NorthernLand, value);
}
#[getter]
fn southern_ocean(&self) -> FloatValue {
self.0.get(FourBoxRegion::SouthernOcean)
}
#[setter]
fn set_southern_ocean(&mut self, value: FloatValue) {
self.0.set(FourBoxRegion::SouthernOcean, value);
}
#[getter]
fn southern_land(&self) -> FloatValue {
self.0.get(FourBoxRegion::SouthernLand)
}
#[setter]
fn set_southern_land(&mut self, value: FloatValue) {
self.0.set(FourBoxRegion::SouthernLand, value);
}
fn get(&self, region: usize) -> PyResult<FloatValue> {
match region {
0 => Ok(self.0.get(FourBoxRegion::NorthernOcean)),
1 => Ok(self.0.get(FourBoxRegion::NorthernLand)),
2 => Ok(self.0.get(FourBoxRegion::SouthernOcean)),
3 => Ok(self.0.get(FourBoxRegion::SouthernLand)),
_ => Err(PyValueError::new_err(format!(
"Invalid region index: {}. Must be 0-3.",
region
))),
}
}
fn set(&mut self, region: usize, value: FloatValue) -> PyResult<()> {
match region {
0 => self.0.set(FourBoxRegion::NorthernOcean, value),
1 => self.0.set(FourBoxRegion::NorthernLand, value),
2 => self.0.set(FourBoxRegion::SouthernOcean, value),
3 => self.0.set(FourBoxRegion::SouthernLand, value),
_ => {
return Err(PyValueError::new_err(format!(
"Invalid region index: {}. Must be 0-3.",
region
)))
}
}
Ok(())
}
fn to_array<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<FloatValue>> {
self.0.as_array().to_pyarray(py)
}
fn to_list(&self) -> Vec<FloatValue> {
self.0.to_vec()
}
fn to_dict(&self) -> std::collections::HashMap<String, FloatValue> {
let mut map = std::collections::HashMap::new();
map.insert(
"northern_ocean".to_string(),
self.0.get(FourBoxRegion::NorthernOcean),
);
map.insert(
"northern_land".to_string(),
self.0.get(FourBoxRegion::NorthernLand),
);
map.insert(
"southern_ocean".to_string(),
self.0.get(FourBoxRegion::SouthernOcean),
);
map.insert(
"southern_land".to_string(),
self.0.get(FourBoxRegion::SouthernLand),
);
map
}
fn __repr__(&self) -> String {
format!(
"FourBoxSlice(northern_ocean={}, northern_land={}, southern_ocean={}, southern_land={})",
self.0.get(FourBoxRegion::NorthernOcean),
self.0.get(FourBoxRegion::NorthernLand),
self.0.get(FourBoxRegion::SouthernOcean),
self.0.get(FourBoxRegion::SouthernLand)
)
}
fn __getitem__(&self, index: usize) -> PyResult<FloatValue> {
self.get(index)
}
fn __setitem__(&mut self, index: usize, value: FloatValue) -> PyResult<()> {
self.set(index, value)
}
fn __len__(&self) -> usize {
4
}
}
#[pyclass]
#[pyo3(name = "HemisphericSlice")]
#[derive(Debug, Clone)]
pub struct PyHemisphericSlice(pub HemisphericSlice);
#[pymethods]
impl PyHemisphericSlice {
#[new]
#[pyo3(signature = (northern=f64::NAN, southern=f64::NAN))]
fn new(northern: FloatValue, southern: FloatValue) -> Self {
Self(HemisphericSlice::from_array([northern, southern]))
}
#[staticmethod]
fn uniform(value: FloatValue) -> Self {
Self(HemisphericSlice::uniform(value))
}
#[staticmethod]
fn from_array(values: [FloatValue; 2]) -> Self {
Self(HemisphericSlice::from_array(values))
}
#[getter]
fn northern(&self) -> FloatValue {
self.0.get(HemisphericRegion::Northern)
}
#[setter]
fn set_northern(&mut self, value: FloatValue) {
self.0.set(HemisphericRegion::Northern, value);
}
#[getter]
fn southern(&self) -> FloatValue {
self.0.get(HemisphericRegion::Southern)
}
#[setter]
fn set_southern(&mut self, value: FloatValue) {
self.0.set(HemisphericRegion::Southern, value);
}
fn get(&self, region: usize) -> PyResult<FloatValue> {
match region {
0 => Ok(self.0.get(HemisphericRegion::Northern)),
1 => Ok(self.0.get(HemisphericRegion::Southern)),
_ => Err(PyValueError::new_err(format!(
"Invalid region index: {}. Must be 0-1.",
region
))),
}
}
fn set(&mut self, region: usize, value: FloatValue) -> PyResult<()> {
match region {
0 => self.0.set(HemisphericRegion::Northern, value),
1 => self.0.set(HemisphericRegion::Southern, value),
_ => {
return Err(PyValueError::new_err(format!(
"Invalid region index: {}. Must be 0-1.",
region
)))
}
}
Ok(())
}
fn to_array<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<FloatValue>> {
self.0.as_array().to_pyarray(py)
}
fn to_list(&self) -> Vec<FloatValue> {
self.0.to_vec()
}
fn to_dict(&self) -> std::collections::HashMap<String, FloatValue> {
let mut map = std::collections::HashMap::new();
map.insert(
"northern".to_string(),
self.0.get(HemisphericRegion::Northern),
);
map.insert(
"southern".to_string(),
self.0.get(HemisphericRegion::Southern),
);
map
}
fn __repr__(&self) -> String {
format!(
"HemisphericSlice(northern={}, southern={})",
self.0.get(HemisphericRegion::Northern),
self.0.get(HemisphericRegion::Southern)
)
}
fn __getitem__(&self, index: usize) -> PyResult<FloatValue> {
self.get(index)
}
fn __setitem__(&mut self, index: usize, value: FloatValue) -> PyResult<()> {
self.set(index, value)
}
fn __len__(&self) -> usize {
2
}
}
#[pyclass]
#[pyo3(name = "StateValue")]
#[derive(Debug, Clone)]
pub struct PyStateValue(pub StateValue);
#[pymethods]
impl PyStateValue {
#[staticmethod]
fn scalar(value: FloatValue) -> Self {
Self(StateValue::Scalar(value))
}
#[staticmethod]
fn four_box(slice: PyFourBoxSlice) -> Self {
Self(StateValue::FourBox(slice.0))
}
#[staticmethod]
fn hemispheric(slice: PyHemisphericSlice) -> Self {
Self(StateValue::Hemispheric(slice.0))
}
fn is_scalar(&self) -> bool {
self.0.is_scalar()
}
fn is_four_box(&self) -> bool {
self.0.is_four_box()
}
fn is_hemispheric(&self) -> bool {
self.0.is_hemispheric()
}
fn as_scalar(&self) -> Option<FloatValue> {
self.0.as_scalar()
}
fn as_four_box(&self) -> Option<PyFourBoxSlice> {
self.0.as_four_box().map(|s| PyFourBoxSlice(*s))
}
fn as_hemispheric(&self) -> Option<PyHemisphericSlice> {
self.0.as_hemispheric().map(|s| PyHemisphericSlice(*s))
}
fn to_scalar(&self) -> FloatValue {
self.0.to_scalar()
}
fn __repr__(&self) -> String {
match &self.0 {
StateValue::Scalar(v) => format!("StateValue.scalar({})", v),
StateValue::FourBox(slice) => {
format!(
"StateValue.four_box(FourBoxSlice(northern_ocean={}, northern_land={}, southern_ocean={}, southern_land={}))",
slice.get(FourBoxRegion::NorthernOcean),
slice.get(FourBoxRegion::NorthernLand),
slice.get(FourBoxRegion::SouthernOcean),
slice.get(FourBoxRegion::SouthernLand)
)
}
StateValue::Hemispheric(slice) => {
format!(
"StateValue.hemispheric(HemisphericSlice(northern={}, southern={}))",
slice.get(HemisphericRegion::Northern),
slice.get(HemisphericRegion::Southern)
)
}
}
}
}
#[pyclass]
#[pyo3(name = "TimeseriesWindow")]
#[derive(Debug, Clone)]
pub struct PyTimeseriesWindow {
values: Vec<FloatValue>,
current_index: usize,
}
#[pymethods]
impl PyTimeseriesWindow {
#[new]
#[pyo3(signature = (values, current_index))]
pub fn new(values: Vec<FloatValue>, current_index: usize) -> PyResult<Self> {
if current_index >= values.len() && !values.is_empty() {
return Err(PyValueError::new_err(format!(
"current_index {} out of bounds for values of length {}",
current_index,
values.len()
)));
}
Ok(Self {
values,
current_index,
})
}
#[getter]
fn current(&self) -> PyResult<FloatValue> {
self.values
.get(self.current_index)
.copied()
.ok_or_else(|| PyValueError::new_err("No current value available"))
}
#[getter]
fn previous(&self) -> PyResult<FloatValue> {
if self.current_index == 0 {
return Err(PyValueError::new_err("No previous value available"));
}
self.values
.get(self.current_index - 1)
.copied()
.ok_or_else(|| PyValueError::new_err("No previous value available"))
}
fn at_offset(&self, offset: isize) -> PyResult<FloatValue> {
let index = self.current_index as isize + offset;
if index < 0 || index >= self.values.len() as isize {
return Err(PyValueError::new_err(format!(
"Offset {} results in index {} which is out of bounds",
offset, index
)));
}
Ok(self.values[index as usize])
}
fn last_n<'py>(&self, py: Python<'py>, n: usize) -> PyResult<Bound<'py, PyArray1<FloatValue>>> {
if n == 0 {
return Ok(Vec::<FloatValue>::new().to_pyarray(py));
}
let start = (self.current_index + 1).saturating_sub(n);
let end = self.current_index + 1;
let slice: Vec<FloatValue> = self.values[start..end].to_vec();
Ok(slice.to_pyarray(py))
}
fn to_array<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<FloatValue>> {
self.values.clone().to_pyarray(py)
}
#[getter]
fn current_index(&self) -> usize {
self.current_index
}
fn __len__(&self) -> usize {
self.values.len()
}
fn __repr__(&self) -> String {
format!(
"TimeseriesWindow(len={}, current_index={}, current={})",
self.values.len(),
self.current_index,
self.values
.get(self.current_index)
.map(|v| v.to_string())
.unwrap_or_else(|| "N/A".to_string())
)
}
}
#[pyclass]
#[pyo3(name = "FourBoxTimeseriesWindow")]
#[derive(Debug, Clone)]
pub struct PyFourBoxTimeseriesWindow {
values: Vec<[FloatValue; 4]>,
current_index: usize,
}
#[pymethods]
impl PyFourBoxTimeseriesWindow {
#[new]
#[pyo3(signature = (values, current_index))]
pub fn new(values: Vec<[FloatValue; 4]>, current_index: usize) -> PyResult<Self> {
if current_index >= values.len() && !values.is_empty() {
return Err(PyValueError::new_err(format!(
"current_index {} out of bounds for values of length {}",
current_index,
values.len()
)));
}
Ok(Self {
values,
current_index,
})
}
#[getter]
fn current(&self) -> PyResult<PyFourBoxSlice> {
self.values
.get(self.current_index)
.map(|v| PyFourBoxSlice(FourBoxSlice::from_array(*v)))
.ok_or_else(|| PyValueError::new_err("No current value available"))
}
#[getter]
fn previous(&self) -> PyResult<PyFourBoxSlice> {
if self.current_index == 0 {
return Err(PyValueError::new_err("No previous value available"));
}
self.values
.get(self.current_index - 1)
.map(|v| PyFourBoxSlice(FourBoxSlice::from_array(*v)))
.ok_or_else(|| PyValueError::new_err("No previous value available"))
}
fn region(&self, region: usize) -> PyResult<PyTimeseriesWindow> {
if region >= 4 {
return Err(PyValueError::new_err(format!(
"Invalid region index: {}. Must be 0-3.",
region
)));
}
let values: Vec<FloatValue> = self.values.iter().map(|v| v[region]).collect();
Ok(PyTimeseriesWindow {
values,
current_index: self.current_index,
})
}
#[getter]
fn current_index(&self) -> usize {
self.current_index
}
fn __len__(&self) -> usize {
self.values.len()
}
fn __repr__(&self) -> String {
format!(
"FourBoxTimeseriesWindow(len={}, current_index={})",
self.values.len(),
self.current_index
)
}
}
#[pyclass]
#[pyo3(name = "HemisphericTimeseriesWindow")]
#[derive(Debug, Clone)]
pub struct PyHemisphericTimeseriesWindow {
values: Vec<[FloatValue; 2]>,
current_index: usize,
}
#[pymethods]
impl PyHemisphericTimeseriesWindow {
#[new]
#[pyo3(signature = (values, current_index))]
pub fn new(values: Vec<[FloatValue; 2]>, current_index: usize) -> PyResult<Self> {
if current_index >= values.len() && !values.is_empty() {
return Err(PyValueError::new_err(format!(
"current_index {} out of bounds for values of length {}",
current_index,
values.len()
)));
}
Ok(Self {
values,
current_index,
})
}
#[getter]
fn current(&self) -> PyResult<PyHemisphericSlice> {
self.values
.get(self.current_index)
.map(|v| PyHemisphericSlice(HemisphericSlice::from_array(*v)))
.ok_or_else(|| PyValueError::new_err("No current value available"))
}
#[getter]
fn previous(&self) -> PyResult<PyHemisphericSlice> {
if self.current_index == 0 {
return Err(PyValueError::new_err("No previous value available"));
}
self.values
.get(self.current_index - 1)
.map(|v| PyHemisphericSlice(HemisphericSlice::from_array(*v)))
.ok_or_else(|| PyValueError::new_err("No previous value available"))
}
fn region(&self, region: usize) -> PyResult<PyTimeseriesWindow> {
if region >= 2 {
return Err(PyValueError::new_err(format!(
"Invalid region index: {}. Must be 0-1.",
region
)));
}
let values: Vec<FloatValue> = self.values.iter().map(|v| v[region]).collect();
Ok(PyTimeseriesWindow {
values,
current_index: self.current_index,
})
}
#[getter]
fn current_index(&self) -> usize {
self.current_index
}
fn __len__(&self) -> usize {
self.values.len()
}
fn __repr__(&self) -> String {
format!(
"HemisphericTimeseriesWindow(len={}, current_index={})",
self.values.len(),
self.current_index
)
}
}
pub use crate::component::GridType;
#[pymodule]
pub fn state(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyFourBoxSlice>()?;
m.add_class::<PyHemisphericSlice>()?;
m.add_class::<PyStateValue>()?;
m.add_class::<PyTimeseriesWindow>()?;
m.add_class::<PyFourBoxTimeseriesWindow>()?;
m.add_class::<PyHemisphericTimeseriesWindow>()?;
Ok(())
}