1use crate::component::{Component, InputState, OutputState};
3use crate::errors::RSCMResult;
4use crate::python::state::{
5 PyFourBoxTimeseriesWindow, PyHemisphericTimeseriesWindow, PyStateValue, PyTimeseriesWindow,
6};
7use crate::timeseries::{FloatValue, Time};
8use crate::timeseries_collection::TimeseriesData;
9use pyo3::prelude::*;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::ffi::CString;
13use std::sync::Arc;
14pub use crate::component::{GridType, RequirementDefinition, RequirementType};
16
17#[macro_export]
19macro_rules! create_component_builder {
20 ($builder_name:ident, $rust_component:ty, $component_parameters:ty) => {
21 #[pyclass]
22 pub struct $builder_name {
23 parameters: $component_parameters,
24 }
25
26 #[pymethods]
27 impl $builder_name {
28 #[staticmethod]
29 pub fn from_parameters(parameters: Bound<PyAny>) -> PyResult<Self> {
30 use pyo3::exceptions::PyValueError;
31
32 let parameters = pythonize::depythonize::<$component_parameters>(¶meters);
34 match parameters {
35 Ok(parameters) => Ok(Self { parameters }),
36 Err(e) => Err(PyValueError::new_err(format!("{}", e))),
37 }
38 }
39 pub fn build(&self) -> PyRustComponent {
40 PyRustComponent(std::sync::Arc::new(<$rust_component>::from_parameters(
41 self.parameters.clone(),
42 )))
43 }
44 }
45 };
46}
47
48#[macro_export]
50macro_rules! impl_component {
51 ($py_component:ty) => {
52 #[pymethods]
53 impl $py_component {
54 fn definitions(&self) -> Vec<RequirementDefinition> {
55 self.0.definitions()
56 }
57
58 fn input_names(&self) -> Vec<String> {
59 self.0.input_names()
60 }
61
62 fn output_names(&self) -> Vec<String> {
63 self.0.output_names()
64 }
65
66 pub fn solve(
67 &mut self,
68 t_current: Time,
69 t_next: Time,
70 collection: $crate::python::timeseries_collection::PyTimeseriesCollection,
71 ) -> PyResult<HashMap<String, $crate::python::state::PyStateValue>> {
72 let input_state =
73 $crate::model::extract_state(&collection.0, self.0.input_names(), t_current);
74
75 let output_state = self.0.solve(t_current, t_next, &input_state)?;
76 let py_output = output_state
78 .into_iter()
79 .map(|(key, state_value)| {
80 (key, $crate::python::state::PyStateValue(state_value))
81 })
82 .collect();
83 Ok(py_output)
84 }
85 }
86 };
87}
88
89#[pymethods]
90impl RequirementDefinition {
91 #[new]
92 #[pyo3(signature = (name, unit, requirement_type, grid_type=GridType::Scalar))]
93 pub fn new_python(
94 name: String,
95 unit: String,
96 requirement_type: RequirementType,
97 grid_type: GridType,
98 ) -> Self {
99 Self {
100 name,
101 unit,
102 requirement_type,
103 grid_type,
104 }
105 }
106}
107
108#[derive(Debug, Clone)]
113#[pyclass]
114#[pyo3{name = "RustComponent"}]
115pub struct PyRustComponent(pub Arc<dyn Component + Send + Sync>);
116
117impl_component!(PyRustComponent);
118
119#[derive(Debug)]
121pub struct PythonComponent {
122 pub component: Py<PyAny>,
123}
124
125#[typetag::serde]
126impl Component for PythonComponent {
127 fn definitions(&self) -> Vec<RequirementDefinition> {
128 Python::attach(|py| {
129 let py_result = self
130 .component
131 .bind(py)
132 .call_method("definitions", (), None)
133 .unwrap();
134 let py_result: Vec<RequirementDefinition> = py_result.extract().unwrap();
135 py_result
136 })
137 }
138
139 fn solve(
140 &self,
141 t_current: Time,
142 t_next: Time,
143 input_state: &InputState,
144 ) -> RSCMResult<OutputState> {
145 Python::attach(|py| {
146 let component = self.component.bind(py);
147
148 let is_typed = component.hasattr("_component_inputs").unwrap_or(false);
150
151 let py_result = if is_typed {
152 let windows = input_state_to_py_windows(py, input_state)
154 .expect("Failed to create Python window objects");
155
156 let inputs_class = component.getattr("Inputs").unwrap();
158 let typed_inputs = inputs_class
159 .call_method("from_input_state", (windows,), None)
160 .unwrap();
161
162 let result = component
163 .call_method("solve", (t_current, t_next, typed_inputs), None)
164 .unwrap();
165
166 result.call_method("to_dict", (), None).unwrap()
168 } else {
169 component
171 .call_method(
172 "solve",
173 (t_current, t_next, input_state.clone().to_hashmap()),
174 None,
175 )
176 .unwrap()
177 };
178
179 let py_dict = py_result
181 .cast::<pyo3::types::PyDict>()
182 .expect("solve() must return a dict");
183
184 let mut output_state = OutputState::new();
185 for (key, value) in py_dict.iter() {
186 let key: String = key.extract().expect("dict key must be a string");
187
188 let state_value = if let Ok(py_state_value) = value.extract::<PyStateValue>() {
190 py_state_value.0
191 } else if let Ok(float_value) = value.extract::<FloatValue>() {
192 crate::state::StateValue::Scalar(float_value)
194 } else {
195 panic!(
196 "Component output value for '{}' must be StateValue or float, got: {:?}",
197 key, value
198 );
199 };
200
201 output_state.insert(key, state_value);
202 }
203 Ok(output_state)
204 })
205 }
206}
207
208impl Serialize for PythonComponent {
209 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
210 where
211 S: serde::Serializer,
212 {
213 Python::attach(|py| {
214 let py_result = self
215 .component
216 .bind(py)
217 .call_method("to_json", (), None)
218 .unwrap();
219 let py_result: String = py_result.extract().unwrap();
220 serializer.serialize_str(&py_result)
221 })
222 }
223}
224
225impl<'de> Deserialize<'de> for PythonComponent {
226 fn deserialize<D>(deserializer: D) -> Result<PythonComponent, D::Error>
227 where
228 D: serde::Deserializer<'de>,
229 {
230 let s: String = Deserialize::deserialize(deserializer)?;
231 Python::attach(|py| {
232 let code = CString::new(format!("Component.from_json('{}')", s)).unwrap();
233 let component = py.eval(code.as_c_str(), None, None).unwrap().unbind();
234 Ok(PythonComponent { component })
235 })
236 }
237}
238
239#[pyclass]
241#[pyo3(name = "PythonComponent")]
242pub struct PyPythonComponent(pub Arc<PythonComponent>);
243
244#[pymethods]
245impl PyPythonComponent {
246 #[staticmethod]
247 pub fn build(component: Py<PyAny>) -> Self {
248 Self(Arc::new(PythonComponent { component }))
249 }
250}
251
252impl_component!(PyPythonComponent);
253
254fn input_state_to_py_windows(py: Python<'_>, input_state: &InputState) -> PyResult<Py<PyAny>> {
259 let dict = pyo3::types::PyDict::new(py);
260 let current_time = input_state.current_time();
261
262 for item in input_state.clone().into_iter() {
263 let name = &item.name;
264 let current_index = item
266 .data
267 .index_of_time(current_time)
268 .unwrap_or_else(|| item.data.latest());
269
270 let window: Py<PyAny> = match &item.data {
271 TimeseriesData::Scalar(ts) => {
272 let values: Vec<FloatValue> = (0..=current_index)
274 .filter_map(|i| ts.at_scalar(i))
275 .collect();
276 PyTimeseriesWindow::new(values, current_index)
277 .map(|w| w.into_pyobject(py).unwrap().into_any().unbind())?
278 }
279 TimeseriesData::FourBox(ts) => {
280 let values: Vec<[FloatValue; 4]> = (0..=current_index)
282 .filter_map(|i| {
283 ts.at_time_index(i)
284 .map(|vals| [vals[0], vals[1], vals[2], vals[3]])
285 })
286 .collect();
287 PyFourBoxTimeseriesWindow::new(values, current_index)
288 .map(|w| w.into_pyobject(py).unwrap().into_any().unbind())?
289 }
290 TimeseriesData::Hemispheric(ts) => {
291 let values: Vec<[FloatValue; 2]> = (0..=current_index)
293 .filter_map(|i| ts.at_time_index(i).map(|vals| [vals[0], vals[1]]))
294 .collect();
295 PyHemisphericTimeseriesWindow::new(values, current_index)
296 .map(|w| w.into_pyobject(py).unwrap().into_any().unbind())?
297 }
298 };
299
300 dict.set_item(name, window)?;
301 }
302
303 Ok(dict.into_any().unbind())
304}