1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/// Python API to access core aspects of the RSCM library.
///
/// This module wraps the core functionality of the RSCM library for use in Python.
/// This doesn't create an extension module that can be imported into python,
/// this is done in the rscm crate, but uses the core pymodule that is exposed here.
///
/// ## Background
///
/// pyo3 provides an interface to be able to interact with the Python interpreter and PyObjects
/// which are allocated on the Python heap.
/// This allows us to write Rust code that can be called from Python,
/// and also Python code that can be called from Rust.
///
/// Rust and Python differ in how they access memory.
/// Rust has strong guarantees about reference checks with only one immutable reference
/// or many mutable reference at a time.
/// Python instead uses reference checks and the GIL to ensure that only one thread can
/// access a PyObject at any time.
///
/// These differences can cause some difficulties when trying to use Rust-owned data in Python.
/// One particular issue is that we can't return references (mutable or not) to Rust objects into
/// Python because Python can't guarantee that Rust's memory safety rules are followed.
/// Generally we return clones of the underlying Rust object which mean that changes to these
/// objects in Python won't be reflected in Rust.
/// Instead, we need to provide methods to update the Rust object from Python.
///
/// An example is the ['TimeseriesCollection'] which owns some ['Timeseries'] structs.
/// In Rust we can return mutable references so we have more control about where data is modified.
///
/// ```rust
/// use numpy::array;
/// use rscm_core::spatial::ScalarRegion;
/// use rscm_core::timeseries::Timeseries;
/// use rscm_core::timeseries_collection::{TimeseriesCollection, VariableType};
/// let mut collection = TimeseriesCollection::new();
/// let timeseries = Timeseries::from_values(array![1.0, 2.0, 3.0], array![1.0, 2.0, 3.0]);
///
/// collection.add_timeseries("Surface Temperature".to_string(), timeseries, VariableType::Exogenous);
/// // We can't access timeseries anymore because the collection has taken ownership
/// // The line below generates a compiler error if uncommented
/// // timeseries.at(0, ScalarRegion::Global)
///
/// {
/// // Get mutable data which enables modifying the timeseries
/// if let Some(data) = collection.get_data_mut("Surface Temperature") {
/// if let Some(ts) = data.as_scalar_mut() {
/// ts.set(0, ScalarRegion::Global, 2.0);
/// }
/// }
/// }
/// {
/// // Get an immutable reference to the timeseries data
/// let timeseries = collection.get_data("Surface Temperature")
/// .and_then(|data| data.as_scalar())
/// .unwrap();
/// assert_eq!(timeseries.at(0, ScalarRegion::Global).unwrap(), 2.0);
/// }
/// ```
///
/// We don't have quite the same options for the Python interface which means that we need to
/// add methods to enable modification.
/// This results in a less flexible API, but is better than nothing!
///
/// ```py
/// import numpy as np
/// from rscm.core import TimeseriesCollection, Timeseries, InterpolationStrategy, VariableType
///
/// collection = TimeseriesCollection()
/// timeseries = Timeseries(np.arange(2000.0, 2010.0), np.arange(2000.0, 2010.0), "units", InterpolationStrategy.Linear)
/// collection.add_timeseries("Surface Temperature", timeseries, VariableType.Exogenous) # A clone is performed so that Rust can control ownership
///
/// # timeseries can still be used as it doesn't follow Rust's ownership model
/// # Modifications to timeseries aren't reflected in the collection so we don't expose any methods to modify it
///
/// # Setting values in the collection requires a method call instead of being able to modify a timeseries
/// collection.set_value("Surface Temperature", 0, 2.0)
/// assert timeseries.at(0) != 2
/// timeseries_2 = collection.get_timeseries_by_name("Surface Temperature")
/// assert timeseries_2.at(0) == 2
/// ```
///
/// There might be ways around this, but it would likely require deep integration of pyo3 throughout
/// the library which results in code that is less readable.
/// This tradeoff might be worth it if we need to expose a lot of functionality to Python,
/// and it would also reduce the amount of boilerplate code.
use crateRSCMError;
use PyRuntimeError;
use *;
use ;
pub use PyRustComponent;
pub use ;