1use crate::spatial::{FourBoxRegion, HemisphericRegion};
7use crate::state::{FourBoxSlice, HemisphericSlice, StateValue};
8use crate::timeseries::FloatValue;
9use numpy::{PyArray1, ToPyArray};
10use pyo3::exceptions::PyValueError;
11use pyo3::prelude::*;
12use pyo3::{pymodule, Bound, PyResult};
13
14#[pyclass]
30#[pyo3(name = "FourBoxSlice")]
31#[derive(Debug, Clone)]
32pub struct PyFourBoxSlice(pub FourBoxSlice);
33
34#[pymethods]
35impl PyFourBoxSlice {
36 #[new]
37 #[pyo3(signature = (northern_ocean=f64::NAN, northern_land=f64::NAN, southern_ocean=f64::NAN, southern_land=f64::NAN))]
38 fn new(
39 northern_ocean: FloatValue,
40 northern_land: FloatValue,
41 southern_ocean: FloatValue,
42 southern_land: FloatValue,
43 ) -> Self {
44 Self(FourBoxSlice::from_array([
45 northern_ocean,
46 northern_land,
47 southern_ocean,
48 southern_land,
49 ]))
50 }
51
52 #[staticmethod]
54 fn uniform(value: FloatValue) -> Self {
55 Self(FourBoxSlice::uniform(value))
56 }
57
58 #[staticmethod]
60 fn from_array(values: [FloatValue; 4]) -> Self {
61 Self(FourBoxSlice::from_array(values))
62 }
63
64 #[getter]
66 fn northern_ocean(&self) -> FloatValue {
67 self.0.get(FourBoxRegion::NorthernOcean)
68 }
69
70 #[setter]
72 fn set_northern_ocean(&mut self, value: FloatValue) {
73 self.0.set(FourBoxRegion::NorthernOcean, value);
74 }
75
76 #[getter]
78 fn northern_land(&self) -> FloatValue {
79 self.0.get(FourBoxRegion::NorthernLand)
80 }
81
82 #[setter]
84 fn set_northern_land(&mut self, value: FloatValue) {
85 self.0.set(FourBoxRegion::NorthernLand, value);
86 }
87
88 #[getter]
90 fn southern_ocean(&self) -> FloatValue {
91 self.0.get(FourBoxRegion::SouthernOcean)
92 }
93
94 #[setter]
96 fn set_southern_ocean(&mut self, value: FloatValue) {
97 self.0.set(FourBoxRegion::SouthernOcean, value);
98 }
99
100 #[getter]
102 fn southern_land(&self) -> FloatValue {
103 self.0.get(FourBoxRegion::SouthernLand)
104 }
105
106 #[setter]
108 fn set_southern_land(&mut self, value: FloatValue) {
109 self.0.set(FourBoxRegion::SouthernLand, value);
110 }
111
112 fn get(&self, region: usize) -> PyResult<FloatValue> {
114 match region {
115 0 => Ok(self.0.get(FourBoxRegion::NorthernOcean)),
116 1 => Ok(self.0.get(FourBoxRegion::NorthernLand)),
117 2 => Ok(self.0.get(FourBoxRegion::SouthernOcean)),
118 3 => Ok(self.0.get(FourBoxRegion::SouthernLand)),
119 _ => Err(PyValueError::new_err(format!(
120 "Invalid region index: {}. Must be 0-3.",
121 region
122 ))),
123 }
124 }
125
126 fn set(&mut self, region: usize, value: FloatValue) -> PyResult<()> {
128 match region {
129 0 => self.0.set(FourBoxRegion::NorthernOcean, value),
130 1 => self.0.set(FourBoxRegion::NorthernLand, value),
131 2 => self.0.set(FourBoxRegion::SouthernOcean, value),
132 3 => self.0.set(FourBoxRegion::SouthernLand, value),
133 _ => {
134 return Err(PyValueError::new_err(format!(
135 "Invalid region index: {}. Must be 0-3.",
136 region
137 )))
138 }
139 }
140 Ok(())
141 }
142
143 fn to_array<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<FloatValue>> {
145 self.0.as_array().to_pyarray(py)
146 }
147
148 fn to_list(&self) -> Vec<FloatValue> {
150 self.0.to_vec()
151 }
152
153 fn to_dict(&self) -> std::collections::HashMap<String, FloatValue> {
155 let mut map = std::collections::HashMap::new();
156 map.insert(
157 "northern_ocean".to_string(),
158 self.0.get(FourBoxRegion::NorthernOcean),
159 );
160 map.insert(
161 "northern_land".to_string(),
162 self.0.get(FourBoxRegion::NorthernLand),
163 );
164 map.insert(
165 "southern_ocean".to_string(),
166 self.0.get(FourBoxRegion::SouthernOcean),
167 );
168 map.insert(
169 "southern_land".to_string(),
170 self.0.get(FourBoxRegion::SouthernLand),
171 );
172 map
173 }
174
175 fn __repr__(&self) -> String {
176 format!(
177 "FourBoxSlice(northern_ocean={}, northern_land={}, southern_ocean={}, southern_land={})",
178 self.0.get(FourBoxRegion::NorthernOcean),
179 self.0.get(FourBoxRegion::NorthernLand),
180 self.0.get(FourBoxRegion::SouthernOcean),
181 self.0.get(FourBoxRegion::SouthernLand)
182 )
183 }
184
185 fn __getitem__(&self, index: usize) -> PyResult<FloatValue> {
186 self.get(index)
187 }
188
189 fn __setitem__(&mut self, index: usize, value: FloatValue) -> PyResult<()> {
190 self.set(index, value)
191 }
192
193 fn __len__(&self) -> usize {
194 4
195 }
196}
197
198#[pyclass]
205#[pyo3(name = "HemisphericSlice")]
206#[derive(Debug, Clone)]
207pub struct PyHemisphericSlice(pub HemisphericSlice);
208
209#[pymethods]
210impl PyHemisphericSlice {
211 #[new]
212 #[pyo3(signature = (northern=f64::NAN, southern=f64::NAN))]
213 fn new(northern: FloatValue, southern: FloatValue) -> Self {
214 Self(HemisphericSlice::from_array([northern, southern]))
215 }
216
217 #[staticmethod]
219 fn uniform(value: FloatValue) -> Self {
220 Self(HemisphericSlice::uniform(value))
221 }
222
223 #[staticmethod]
225 fn from_array(values: [FloatValue; 2]) -> Self {
226 Self(HemisphericSlice::from_array(values))
227 }
228
229 #[getter]
231 fn northern(&self) -> FloatValue {
232 self.0.get(HemisphericRegion::Northern)
233 }
234
235 #[setter]
237 fn set_northern(&mut self, value: FloatValue) {
238 self.0.set(HemisphericRegion::Northern, value);
239 }
240
241 #[getter]
243 fn southern(&self) -> FloatValue {
244 self.0.get(HemisphericRegion::Southern)
245 }
246
247 #[setter]
249 fn set_southern(&mut self, value: FloatValue) {
250 self.0.set(HemisphericRegion::Southern, value);
251 }
252
253 fn get(&self, region: usize) -> PyResult<FloatValue> {
255 match region {
256 0 => Ok(self.0.get(HemisphericRegion::Northern)),
257 1 => Ok(self.0.get(HemisphericRegion::Southern)),
258 _ => Err(PyValueError::new_err(format!(
259 "Invalid region index: {}. Must be 0-1.",
260 region
261 ))),
262 }
263 }
264
265 fn set(&mut self, region: usize, value: FloatValue) -> PyResult<()> {
267 match region {
268 0 => self.0.set(HemisphericRegion::Northern, value),
269 1 => self.0.set(HemisphericRegion::Southern, value),
270 _ => {
271 return Err(PyValueError::new_err(format!(
272 "Invalid region index: {}. Must be 0-1.",
273 region
274 )))
275 }
276 }
277 Ok(())
278 }
279
280 fn to_array<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<FloatValue>> {
282 self.0.as_array().to_pyarray(py)
283 }
284
285 fn to_list(&self) -> Vec<FloatValue> {
287 self.0.to_vec()
288 }
289
290 fn to_dict(&self) -> std::collections::HashMap<String, FloatValue> {
292 let mut map = std::collections::HashMap::new();
293 map.insert(
294 "northern".to_string(),
295 self.0.get(HemisphericRegion::Northern),
296 );
297 map.insert(
298 "southern".to_string(),
299 self.0.get(HemisphericRegion::Southern),
300 );
301 map
302 }
303
304 fn __repr__(&self) -> String {
305 format!(
306 "HemisphericSlice(northern={}, southern={})",
307 self.0.get(HemisphericRegion::Northern),
308 self.0.get(HemisphericRegion::Southern)
309 )
310 }
311
312 fn __getitem__(&self, index: usize) -> PyResult<FloatValue> {
313 self.get(index)
314 }
315
316 fn __setitem__(&mut self, index: usize, value: FloatValue) -> PyResult<()> {
317 self.set(index, value)
318 }
319
320 fn __len__(&self) -> usize {
321 2
322 }
323}
324
325#[pyclass]
338#[pyo3(name = "StateValue")]
339#[derive(Debug, Clone)]
340pub struct PyStateValue(pub StateValue);
341
342#[pymethods]
343impl PyStateValue {
344 #[staticmethod]
346 fn scalar(value: FloatValue) -> Self {
347 Self(StateValue::Scalar(value))
348 }
349
350 #[staticmethod]
352 fn four_box(slice: PyFourBoxSlice) -> Self {
353 Self(StateValue::FourBox(slice.0))
354 }
355
356 #[staticmethod]
358 fn hemispheric(slice: PyHemisphericSlice) -> Self {
359 Self(StateValue::Hemispheric(slice.0))
360 }
361
362 fn is_scalar(&self) -> bool {
364 self.0.is_scalar()
365 }
366
367 fn is_four_box(&self) -> bool {
369 self.0.is_four_box()
370 }
371
372 fn is_hemispheric(&self) -> bool {
374 self.0.is_hemispheric()
375 }
376
377 fn as_scalar(&self) -> Option<FloatValue> {
379 self.0.as_scalar()
380 }
381
382 fn as_four_box(&self) -> Option<PyFourBoxSlice> {
384 self.0.as_four_box().map(|s| PyFourBoxSlice(*s))
385 }
386
387 fn as_hemispheric(&self) -> Option<PyHemisphericSlice> {
389 self.0.as_hemispheric().map(|s| PyHemisphericSlice(*s))
390 }
391
392 fn to_scalar(&self) -> FloatValue {
400 self.0.to_scalar()
401 }
402
403 fn __repr__(&self) -> String {
404 match &self.0 {
405 StateValue::Scalar(v) => format!("StateValue.scalar({})", v),
406 StateValue::FourBox(slice) => {
407 format!(
408 "StateValue.four_box(FourBoxSlice(northern_ocean={}, northern_land={}, southern_ocean={}, southern_land={}))",
409 slice.get(FourBoxRegion::NorthernOcean),
410 slice.get(FourBoxRegion::NorthernLand),
411 slice.get(FourBoxRegion::SouthernOcean),
412 slice.get(FourBoxRegion::SouthernLand)
413 )
414 }
415 StateValue::Hemispheric(slice) => {
416 format!(
417 "StateValue.hemispheric(HemisphericSlice(northern={}, southern={}))",
418 slice.get(HemisphericRegion::Northern),
419 slice.get(HemisphericRegion::Southern)
420 )
421 }
422 }
423 }
424}
425
426#[pyclass]
441#[pyo3(name = "TimeseriesWindow")]
442#[derive(Debug, Clone)]
443pub struct PyTimeseriesWindow {
444 values: Vec<FloatValue>,
445 current_index: usize,
446}
447
448#[pymethods]
449impl PyTimeseriesWindow {
450 #[new]
451 #[pyo3(signature = (values, current_index))]
452 pub fn new(values: Vec<FloatValue>, current_index: usize) -> PyResult<Self> {
453 if current_index >= values.len() && !values.is_empty() {
454 return Err(PyValueError::new_err(format!(
455 "current_index {} out of bounds for values of length {}",
456 current_index,
457 values.len()
458 )));
459 }
460 Ok(Self {
461 values,
462 current_index,
463 })
464 }
465
466 fn at_start(&self) -> PyResult<FloatValue> {
472 self.values
473 .get(self.current_index)
474 .copied()
475 .ok_or_else(|| PyValueError::new_err("No value available at current index"))
476 }
477
478 fn at_end(&self) -> Option<FloatValue> {
485 let next_index = self.current_index + 1;
486 self.values.get(next_index).copied()
487 }
488
489 #[getter]
493 fn previous(&self) -> PyResult<FloatValue> {
494 if self.current_index == 0 {
495 return Err(PyValueError::new_err("No previous value available"));
496 }
497 self.values
498 .get(self.current_index - 1)
499 .copied()
500 .ok_or_else(|| PyValueError::new_err("No previous value available"))
501 }
502
503 fn at_offset(&self, offset: isize) -> PyResult<FloatValue> {
505 let index = self.current_index as isize + offset;
506 if index < 0 || index >= self.values.len() as isize {
507 return Err(PyValueError::new_err(format!(
508 "Offset {} results in index {} which is out of bounds",
509 offset, index
510 )));
511 }
512 Ok(self.values[index as usize])
513 }
514
515 fn last_n<'py>(&self, py: Python<'py>, n: usize) -> PyResult<Bound<'py, PyArray1<FloatValue>>> {
517 if n == 0 {
518 return Ok(Vec::<FloatValue>::new().to_pyarray(py));
519 }
520 let start = (self.current_index + 1).saturating_sub(n);
521 let end = self.current_index + 1;
522 let slice: Vec<FloatValue> = self.values[start..end].to_vec();
523 Ok(slice.to_pyarray(py))
524 }
525
526 fn to_array<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<FloatValue>> {
528 self.values.clone().to_pyarray(py)
529 }
530
531 #[getter]
533 fn current_index(&self) -> usize {
534 self.current_index
535 }
536
537 fn __len__(&self) -> usize {
539 self.values.len()
540 }
541
542 fn __repr__(&self) -> String {
543 format!(
544 "TimeseriesWindow(len={}, current_index={}, current={})",
545 self.values.len(),
546 self.current_index,
547 self.values
548 .get(self.current_index)
549 .map(|v| v.to_string())
550 .unwrap_or_else(|| "N/A".to_string())
551 )
552 }
553}
554
555#[pyclass]
564#[pyo3(name = "FourBoxTimeseriesWindow")]
565#[derive(Debug, Clone)]
566pub struct PyFourBoxTimeseriesWindow {
567 values: Vec<[FloatValue; 4]>,
569 current_index: usize,
570}
571
572#[pymethods]
573impl PyFourBoxTimeseriesWindow {
574 #[new]
575 #[pyo3(signature = (values, current_index))]
576 pub fn new(values: Vec<[FloatValue; 4]>, current_index: usize) -> PyResult<Self> {
577 if current_index >= values.len() && !values.is_empty() {
578 return Err(PyValueError::new_err(format!(
579 "current_index {} out of bounds for values of length {}",
580 current_index,
581 values.len()
582 )));
583 }
584 Ok(Self {
585 values,
586 current_index,
587 })
588 }
589
590 fn at_start(&self, region: usize) -> PyResult<FloatValue> {
596 if region >= 4 {
597 return Err(PyValueError::new_err(format!(
598 "Invalid region index: {}. Must be 0-3.",
599 region
600 )));
601 }
602 self.values
603 .get(self.current_index)
604 .map(|v| v[region])
605 .ok_or_else(|| PyValueError::new_err("No value available at current index"))
606 }
607
608 fn at_end(&self, region: usize) -> PyResult<Option<FloatValue>> {
615 if region >= 4 {
616 return Err(PyValueError::new_err(format!(
617 "Invalid region index: {}. Must be 0-3.",
618 region
619 )));
620 }
621 let next_index = self.current_index + 1;
622 Ok(self.values.get(next_index).map(|v| v[region]))
623 }
624
625 fn at_start_all(&self) -> PyResult<PyFourBoxSlice> {
627 self.values
628 .get(self.current_index)
629 .map(|v| PyFourBoxSlice(FourBoxSlice::from_array(*v)))
630 .ok_or_else(|| PyValueError::new_err("No value available at current index"))
631 }
632
633 fn at_end_all(&self) -> Option<PyFourBoxSlice> {
637 let next_index = self.current_index + 1;
638 self.values
639 .get(next_index)
640 .map(|v| PyFourBoxSlice(FourBoxSlice::from_array(*v)))
641 }
642
643 #[getter]
647 fn previous(&self) -> PyResult<PyFourBoxSlice> {
648 if self.current_index == 0 {
649 return Err(PyValueError::new_err("No previous value available"));
650 }
651 self.values
652 .get(self.current_index - 1)
653 .map(|v| PyFourBoxSlice(FourBoxSlice::from_array(*v)))
654 .ok_or_else(|| PyValueError::new_err("No previous value available"))
655 }
656
657 fn region(&self, region: usize) -> PyResult<PyTimeseriesWindow> {
659 if region >= 4 {
660 return Err(PyValueError::new_err(format!(
661 "Invalid region index: {}. Must be 0-3.",
662 region
663 )));
664 }
665 let values: Vec<FloatValue> = self.values.iter().map(|v| v[region]).collect();
666 Ok(PyTimeseriesWindow {
667 values,
668 current_index: self.current_index,
669 })
670 }
671
672 #[getter]
674 fn current_index(&self) -> usize {
675 self.current_index
676 }
677
678 fn __len__(&self) -> usize {
679 self.values.len()
680 }
681
682 fn __repr__(&self) -> String {
683 format!(
684 "FourBoxTimeseriesWindow(len={}, current_index={})",
685 self.values.len(),
686 self.current_index
687 )
688 }
689}
690
691#[pyclass]
693#[pyo3(name = "HemisphericTimeseriesWindow")]
694#[derive(Debug, Clone)]
695pub struct PyHemisphericTimeseriesWindow {
696 values: Vec<[FloatValue; 2]>,
697 current_index: usize,
698}
699
700#[pymethods]
701impl PyHemisphericTimeseriesWindow {
702 #[new]
703 #[pyo3(signature = (values, current_index))]
704 pub fn new(values: Vec<[FloatValue; 2]>, current_index: usize) -> PyResult<Self> {
705 if current_index >= values.len() && !values.is_empty() {
706 return Err(PyValueError::new_err(format!(
707 "current_index {} out of bounds for values of length {}",
708 current_index,
709 values.len()
710 )));
711 }
712 Ok(Self {
713 values,
714 current_index,
715 })
716 }
717
718 fn at_start(&self, region: usize) -> PyResult<FloatValue> {
724 if region >= 2 {
725 return Err(PyValueError::new_err(format!(
726 "Invalid region index: {}. Must be 0-1.",
727 region
728 )));
729 }
730 self.values
731 .get(self.current_index)
732 .map(|v| v[region])
733 .ok_or_else(|| PyValueError::new_err("No value available at current index"))
734 }
735
736 fn at_end(&self, region: usize) -> PyResult<Option<FloatValue>> {
743 if region >= 2 {
744 return Err(PyValueError::new_err(format!(
745 "Invalid region index: {}. Must be 0-1.",
746 region
747 )));
748 }
749 let next_index = self.current_index + 1;
750 Ok(self.values.get(next_index).map(|v| v[region]))
751 }
752
753 fn at_start_all(&self) -> PyResult<PyHemisphericSlice> {
755 self.values
756 .get(self.current_index)
757 .map(|v| PyHemisphericSlice(HemisphericSlice::from_array(*v)))
758 .ok_or_else(|| PyValueError::new_err("No value available at current index"))
759 }
760
761 fn at_end_all(&self) -> Option<PyHemisphericSlice> {
765 let next_index = self.current_index + 1;
766 self.values
767 .get(next_index)
768 .map(|v| PyHemisphericSlice(HemisphericSlice::from_array(*v)))
769 }
770
771 #[getter]
775 fn previous(&self) -> PyResult<PyHemisphericSlice> {
776 if self.current_index == 0 {
777 return Err(PyValueError::new_err("No previous value available"));
778 }
779 self.values
780 .get(self.current_index - 1)
781 .map(|v| PyHemisphericSlice(HemisphericSlice::from_array(*v)))
782 .ok_or_else(|| PyValueError::new_err("No previous value available"))
783 }
784
785 fn region(&self, region: usize) -> PyResult<PyTimeseriesWindow> {
787 if region >= 2 {
788 return Err(PyValueError::new_err(format!(
789 "Invalid region index: {}. Must be 0-1.",
790 region
791 )));
792 }
793 let values: Vec<FloatValue> = self.values.iter().map(|v| v[region]).collect();
794 Ok(PyTimeseriesWindow {
795 values,
796 current_index: self.current_index,
797 })
798 }
799
800 #[getter]
802 fn current_index(&self) -> usize {
803 self.current_index
804 }
805
806 fn __len__(&self) -> usize {
807 self.values.len()
808 }
809
810 fn __repr__(&self) -> String {
811 format!(
812 "HemisphericTimeseriesWindow(len={}, current_index={})",
813 self.values.len(),
814 self.current_index
815 )
816 }
817}
818
819pub use crate::component::GridType;
825
826#[pymodule]
827pub fn state(m: &Bound<'_, PyModule>) -> PyResult<()> {
828 m.add_class::<PyFourBoxSlice>()?;
829 m.add_class::<PyHemisphericSlice>()?;
830 m.add_class::<PyStateValue>()?;
831 m.add_class::<PyTimeseriesWindow>()?;
832 m.add_class::<PyFourBoxTimeseriesWindow>()?;
833 m.add_class::<PyHemisphericTimeseriesWindow>()?;
834 Ok(())
835}