Skip to main content

surrealml_core/execution/
compute.rs

1//! Defines the operations around performing computations on a loaded model.
2use std::collections::HashMap;
3
4use ndarray::ArrayD;
5use ort::session::Session;
6use ort::value::ValueType;
7
8use crate::errors::error::{SurrealError, SurrealErrorStatus};
9use crate::execution::session::get_session;
10use crate::safe_eject;
11use crate::storage::surml_file::SurMlFile;
12
13/// A wrapper for the loaded machine learning model so we can perform computations on the loaded
14/// model.
15///
16/// # Attributes
17/// * `surml_file` - The loaded machine learning model using interior mutability to allow mutable
18///   access to the model
19pub struct ModelComputation<'a> {
20	pub surml_file: &'a mut SurMlFile,
21}
22
23impl ModelComputation<'_> {
24	/// Creates a Tensor that can be used as input to the loaded model from a hashmap of keys and
25	/// values.
26	///
27	/// # Arguments
28	/// * `input_values` - A hashmap of keys and values that will be used to create the input
29	///   tensor.
30	///
31	/// # Returns
32	/// A Tensor that can be used as input to the loaded model.
33	pub fn input_tensor_from_key_bindings(
34		&self,
35		input_values: HashMap<String, f32>,
36	) -> Result<ArrayD<f32>, SurrealError> {
37		let buffer = self.input_vector_from_key_bindings(input_values)?;
38		Ok(ndarray::arr1::<f32>(&buffer).into_dyn())
39	}
40
41	/// Creates a vector of dimensions for the input tensor from the loaded model.
42	///
43	/// # Arguments
44	/// * `session_ref` - A reference to the session to get the input shape
45	///
46	/// # Returns
47	/// A vector of dimensions for the input tensor to be reshaped into from the loaded model.
48	fn process_input_dims(session_ref: &Session) -> Result<Vec<usize>, SurrealError> {
49		// In ort 2.0.0-rc.11, we access input metadata through session.inputs()
50		let inputs = session_ref.inputs();
51		if inputs.is_empty() {
52			return Err(SurrealError {
53				message: "No inputs found in session".into(),
54				status: SurrealErrorStatus::Unknown,
55			});
56		}
57
58		// Get the first input's dtype
59		let dtype = inputs[0].dtype();
60
61		// Extract dimensions from the ValueType
62		let unwrapped_dims = match dtype {
63			ValueType::Tensor {
64				ty: _,
65				shape,
66				dimension_symbols: _,
67			} => shape,
68			_ => {
69				return Err(SurrealError {
70					message: "input dims not found".into(),
71					status: SurrealErrorStatus::Unknown,
72				});
73			}
74		};
75
76		let mut dims_cache = Vec::new();
77		for dim in unwrapped_dims.iter() {
78			if dim < &0 {
79				dims_cache.push((dim * -1) as usize);
80			} else {
81				dims_cache.push(*dim as usize);
82			}
83		}
84		Ok(dims_cache)
85	}
86
87	/// Creates a Vector that can be used manipulated with other operations such as normalisation
88	/// from a hashmap of keys and values.
89	///
90	/// # Arguments
91	/// * `input_values` - A hashmap of keys and values that will be used to create the input
92	///   vector.
93	///
94	/// # Returns
95	/// A Vector that can be used manipulated with other operations such as normalisation.
96	pub fn input_vector_from_key_bindings(
97		&self,
98		mut input_values: HashMap<String, f32>,
99	) -> Result<Vec<f32>, SurrealError> {
100		let mut buffer = Vec::with_capacity(self.surml_file.header.keys.store.len());
101
102		for key in &self.surml_file.header.keys.store {
103			let value = match input_values.get_mut(key) {
104				Some(value) => value,
105				None => {
106					return Err(SurrealError::new(
107						format!(
108							"src/execution/compute.rs 67: Key {} not found in input values",
109							key
110						),
111						SurrealErrorStatus::NotFound,
112					));
113				}
114			};
115			buffer.push(std::mem::take(value));
116		}
117
118		Ok(buffer)
119	}
120
121	/// Performs a raw computation on the loaded model.
122	///
123	/// # Arguments
124	/// * `tensor` - The input tensor to the loaded model.
125	///
126	/// # Returns
127	/// The computed output tensor from the loaded model.
128	pub fn raw_compute(
129		&self,
130		tensor: ArrayD<f32>,
131		_dims: Option<(i32, i32)>,
132	) -> Result<Vec<f32>, SurrealError> {
133		let mut session = get_session(self.surml_file.model.clone())?;
134		let dims_cache = ModelComputation::process_input_dims(&session)?;
135		let tensor = if dims_cache.is_empty() {
136			// If we couldn't get dimensions from the session, use the tensor as-is
137			tensor
138		} else {
139			match tensor.into_shape_with_order(dims_cache) {
140				Ok(tensor) => tensor,
141				Err(_) => {
142					return Err(SurrealError::new(
143						"Failed to reshape tensor to input dimensions".to_string(),
144						SurrealErrorStatus::Unknown,
145					));
146				}
147			}
148		};
149		let tensor = match ort::value::Tensor::from_array(tensor) {
150			Ok(tensor) => tensor,
151			Err(_) => {
152				return Err(SurrealError::new(
153					"Failed to convert tensor to ort tensor".to_string(),
154					SurrealErrorStatus::Unknown,
155				));
156			}
157		};
158		let x = ort::inputs![tensor];
159		let outputs = safe_eject!(session.run(x), SurrealErrorStatus::Unknown);
160
161		let mut buffer: Vec<f32> = Vec::new();
162
163		// extract the output tensor converting the values to f32 if they are i64
164		match outputs[0].try_extract_tensor::<f32>() {
165			Ok((_shape, data)) => {
166				for i in data.iter() {
167					buffer.push(*i);
168				}
169			}
170			Err(_) => {
171				let (_shape, data) = safe_eject!(
172					outputs[0].try_extract_tensor::<i64>(),
173					SurrealErrorStatus::Unknown
174				);
175				for i in data.iter() {
176					buffer.push(*i as f32);
177				}
178			}
179		};
180		Ok(buffer)
181	}
182
183	/// Checks the header applying normalisers if present and then performs a raw computation on the
184	/// loaded model. Will also apply inverse normalisers if present on the outputs.
185	///
186	/// # Notes
187	/// This function is fairly coupled and will consider breaking out the functions later on if
188	/// needed.
189	///
190	/// # Arguments
191	/// * `input_values` - A hashmap of keys and values that will be used to create the input
192	///   tensor.
193	///
194	/// # Returns
195	/// The computed output tensor from the loaded model.
196	pub fn buffered_compute(
197		&self,
198		input_values: &mut HashMap<String, f32>,
199	) -> Result<Vec<f32>, SurrealError> {
200		// applying normalisers if present
201		for (key, value) in &mut *input_values {
202			let value_ref = *value;
203			if let Some(normaliser) = self.surml_file.header.get_normaliser(&key.to_string())? {
204				*value = normaliser.normalise(value_ref);
205			}
206		}
207		let tensor = self.input_tensor_from_key_bindings(input_values.clone())?;
208		let output = self.raw_compute(tensor, None)?;
209
210		// if no normaliser is present, return the output
211		if self.surml_file.header.output.normaliser.is_none() {
212			return Ok(output);
213		}
214
215		// apply the normaliser to the output
216		let output_normaliser = match self.surml_file.header.output.normaliser.as_ref() {
217			Some(normaliser) => normaliser,
218			None => {
219				return Err(SurrealError::new(
220					String::from(
221						"No normaliser present for output which shouldn't happen as passed initial check for",
222					)
223					.to_string(),
224					SurrealErrorStatus::Unknown,
225				));
226			}
227		};
228		let mut buffer = Vec::with_capacity(output.len());
229
230		for value in output {
231			buffer.push(output_normaliser.inverse_normalise(value));
232		}
233		Ok(buffer)
234	}
235}
236
237#[cfg(test)]
238mod tests {
239
240	#[cfg(any(
241		feature = "sklearn-tests",
242		feature = "onnx-tests",
243		feature = "torch-tests",
244		feature = "tensorflow-tests"
245	))]
246	use super::*;
247	#[cfg(any(
248		feature = "sklearn-tests",
249		feature = "onnx-tests",
250		feature = "torch-tests",
251		feature = "tensorflow-tests"
252	))]
253	#[cfg(feature = "sklearn-tests")]
254	#[test]
255	fn test_raw_compute_linear_sklearn() {
256		let mut file = SurMlFile::from_file("./model_stash/sklearn/surml/linear.surml").unwrap();
257		let model_computation = ModelComputation {
258			surml_file: &mut file,
259		};
260
261		let mut input_values = HashMap::new();
262		input_values.insert(String::from("squarefoot"), 1000.0);
263		input_values.insert(String::from("num_floors"), 2.0);
264
265		let raw_input = model_computation.input_tensor_from_key_bindings(input_values).unwrap();
266
267		let output = model_computation.raw_compute(raw_input, Some((1, 2))).unwrap();
268		assert_eq!(output.len(), 1);
269		assert_eq!(output[0], 985.57745);
270	}
271
272	#[cfg(feature = "sklearn-tests")]
273	#[test]
274	fn test_buffered_compute_linear_sklearn() {
275		let mut file = SurMlFile::from_file("./model_stash/sklearn/surml/linear.surml").unwrap();
276		let model_computation = ModelComputation {
277			surml_file: &mut file,
278		};
279
280		let mut input_values = HashMap::new();
281		input_values.insert(String::from("squarefoot"), 1000.0);
282		input_values.insert(String::from("num_floors"), 2.0);
283
284		let output = model_computation.buffered_compute(&mut input_values).unwrap();
285		assert_eq!(output.len(), 1);
286	}
287
288	#[cfg(feature = "onnx-tests")]
289	#[test]
290	fn test_raw_compute_linear_onnx() {
291		let mut file = SurMlFile::from_file("./model_stash/onnx/surml/linear.surml").unwrap();
292		let model_computation = ModelComputation {
293			surml_file: &mut file,
294		};
295
296		let mut input_values = HashMap::new();
297		input_values.insert(String::from("squarefoot"), 1000.0);
298		input_values.insert(String::from("num_floors"), 2.0);
299
300		let raw_input = model_computation.input_tensor_from_key_bindings(input_values).unwrap();
301
302		let output = model_computation.raw_compute(raw_input, Some((1, 2))).unwrap();
303		assert_eq!(output.len(), 1);
304		assert_eq!(output[0], 985.57745);
305	}
306
307	#[cfg(feature = "onnx-tests")]
308	#[test]
309	fn test_buffered_compute_linear_onnx() {
310		let mut file = SurMlFile::from_file("./model_stash/onnx/surml/linear.surml").unwrap();
311		let model_computation = ModelComputation {
312			surml_file: &mut file,
313		};
314
315		let mut input_values = HashMap::new();
316		input_values.insert(String::from("squarefoot"), 1000.0);
317		input_values.insert(String::from("num_floors"), 2.0);
318
319		let output = model_computation.buffered_compute(&mut input_values).unwrap();
320		assert_eq!(output.len(), 1);
321	}
322
323	#[cfg(feature = "torch-tests")]
324	#[test]
325	fn test_raw_compute_linear_torch() {
326		let mut file = SurMlFile::from_file("./model_stash/torch/surml/linear.surml").unwrap();
327		let model_computation = ModelComputation {
328			surml_file: &mut file,
329		};
330
331		let mut input_values = HashMap::new();
332		input_values.insert(String::from("squarefoot"), 1000.0);
333		input_values.insert(String::from("num_floors"), 2.0);
334
335		let raw_input = model_computation.input_tensor_from_key_bindings(input_values).unwrap();
336
337		let output = model_computation.raw_compute(raw_input, None).unwrap();
338		assert_eq!(output.len(), 1);
339	}
340
341	#[cfg(feature = "torch-tests")]
342	#[test]
343	fn test_buffered_compute_linear_torch() {
344		let mut file = SurMlFile::from_file("./model_stash/torch/surml/linear.surml").unwrap();
345		let model_computation = ModelComputation {
346			surml_file: &mut file,
347		};
348
349		let mut input_values = HashMap::new();
350		input_values.insert(String::from("squarefoot"), 1000.0);
351		input_values.insert(String::from("num_floors"), 2.0);
352
353		let output = model_computation.buffered_compute(&mut input_values).unwrap();
354		assert_eq!(output.len(), 1);
355	}
356
357	#[cfg(feature = "tensorflow-tests")]
358	#[test]
359	fn test_raw_compute_linear_tensorflow() {
360		let mut file = SurMlFile::from_file("./model_stash/tensorflow/surml/linear.surml").unwrap();
361		let model_computation = ModelComputation {
362			surml_file: &mut file,
363		};
364
365		let mut input_values = HashMap::new();
366		input_values.insert(String::from("squarefoot"), 1000.0);
367		input_values.insert(String::from("num_floors"), 2.0);
368
369		let raw_input = model_computation.input_tensor_from_key_bindings(input_values).unwrap();
370
371		let output = model_computation.raw_compute(raw_input, None).unwrap();
372		assert_eq!(output.len(), 1);
373	}
374
375	#[cfg(feature = "tensorflow-tests")]
376	#[test]
377	fn test_buffered_compute_linear_tensorflow() {
378		let mut file = SurMlFile::from_file("./model_stash/tensorflow/surml/linear.surml").unwrap();
379		let model_computation = ModelComputation {
380			surml_file: &mut file,
381		};
382
383		let mut input_values = HashMap::new();
384		input_values.insert(String::from("squarefoot"), 1000.0);
385		input_values.insert(String::from("num_floors"), 2.0);
386
387		let output = model_computation.buffered_compute(&mut input_values).unwrap();
388		assert_eq!(output.len(), 1);
389	}
390}