1use spatialrust_tensor::{DataType, Device, TensorBuffer, TensorDescriptor};
4
5use crate::{
6 AiError, AiResult, CopyPolicy, Dimension, InferenceBackend, ModelInfo, ModelSession,
7 ModelSource, NamedTensors, RunOptions, SessionOptions, TensorSpec,
8};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub enum MockProfile {
13 SyntheticDepth,
18}
19
20#[derive(Clone, Debug, Default)]
22pub struct MockInferenceBackend;
23
24impl InferenceBackend for MockInferenceBackend {
25 fn name(&self) -> &str {
26 "mock"
27 }
28
29 fn create_session(
30 &self,
31 source: &ModelSource,
32 options: &SessionOptions,
33 ) -> AiResult<Box<dyn ModelSession>> {
34 options.validate()?;
35 let profile = match source {
36 ModelSource::Mock(profile) => *profile,
37 ModelSource::Path(_) | ModelSource::Bytes(_) => {
38 return Err(AiError::Unsupported {
39 backend: "mock".into(),
40 operation: "filesystem or byte-backed model loading".into(),
41 });
42 }
43 };
44 Ok(Box::new(MockSession { profile, info: profile.model_info() }))
45 }
46}
47
48struct MockSession {
49 profile: MockProfile,
50 info: ModelInfo,
51}
52
53impl ModelSession for MockSession {
54 fn backend_name(&self) -> &str {
55 "mock"
56 }
57
58 fn model_info(&self) -> &ModelInfo {
59 &self.info
60 }
61
62 fn run_with_options(
63 &mut self,
64 inputs: NamedTensors,
65 options: RunOptions,
66 ) -> AiResult<NamedTensors> {
67 self.info.validate_inputs(&inputs)?;
68 if options.output_copy == CopyPolicy::Forbid {
70 return Err(AiError::CopyRequired { direction: "output host", name: "depth".into() });
71 }
72 match self.profile {
73 MockProfile::SyntheticDepth => run_synthetic_depth(inputs, options.input_copy),
74 }
75 }
76}
77
78impl MockProfile {
79 fn model_info(self) -> ModelInfo {
80 match self {
81 Self::SyntheticDepth => ModelInfo {
82 name: Some("mock-synthetic-depth".into()),
83 inputs: vec![TensorSpec::new(
84 "images",
85 DataType::F32,
86 vec![
87 Dimension::Fixed(1),
88 Dimension::Fixed(3),
89 Dimension::Dynamic,
90 Dimension::Dynamic,
91 ],
92 )],
93 outputs: vec![TensorSpec::new(
94 "depth",
95 DataType::F32,
96 vec![
97 Dimension::Fixed(1),
98 Dimension::Fixed(1),
99 Dimension::Dynamic,
100 Dimension::Dynamic,
101 ],
102 )],
103 },
104 }
105 }
106}
107
108fn run_synthetic_depth(inputs: NamedTensors, input_copy: CopyPolicy) -> AiResult<NamedTensors> {
109 let input = inputs.get("images").ok_or_else(|| AiError::MissingInput("images".into()))?;
110 let descriptor = input.descriptor();
111 let shape = descriptor.shape();
112 if shape.len() != 4 || shape[0] != 1 || shape[1] != 3 {
113 return Err(AiError::ShapeMismatch {
114 name: "images".into(),
115 expected: vec![
116 Dimension::Fixed(1),
117 Dimension::Fixed(3),
118 Dimension::Dynamic,
119 Dimension::Dynamic,
120 ],
121 actual: shape.to_vec(),
122 });
123 }
124 if !descriptor.is_c_contiguous() || descriptor.byte_offset() != 0 {
125 if input_copy == CopyPolicy::Forbid {
126 return Err(AiError::CopyRequired { direction: "input host", name: "images".into() });
127 }
128 return Err(AiError::Unsupported {
129 backend: "mock".into(),
130 operation: "non-contiguous input packing for SyntheticDepth".into(),
131 });
132 }
133 let height = shape[2];
134 let width = shape[3];
135 let values = f32_values(input)?;
136 let plane = height.saturating_mul(width);
137 if values.len() != plane.saturating_mul(3) {
138 return Err(AiError::InvalidConfiguration(
139 "images storage length does not match NCHW shape".into(),
140 ));
141 }
142 let mut depth = Vec::with_capacity(plane);
143 for index in 0..plane {
144 let r = values[index];
145 let g = values[plane + index];
146 let b = values[2 * plane + index];
147 let luma = (0.299 * r + 0.587 * g + 0.114 * b).clamp(0.0, 1.0);
148 depth.push(1.0 + 0.5 * (1.0 - luma));
149 }
150 let output = TensorBuffer::try_from_f32(
151 depth,
152 TensorDescriptor::contiguous(DataType::F32, vec![1, 1, height, width], Device::CPU),
153 )
154 .map_err(|error| AiError::InvalidConfiguration(error.to_string()))?;
155 let mut outputs = NamedTensors::new();
156 outputs.insert("depth", output)?;
157 Ok(outputs)
158}
159
160fn f32_values(tensor: &TensorBuffer) -> AiResult<Vec<f32>> {
161 if tensor.descriptor().dtype() != DataType::F32 {
162 return Err(AiError::DataTypeMismatch {
163 name: "images".into(),
164 expected: DataType::F32,
165 actual: tensor.descriptor().dtype(),
166 });
167 }
168 if let Some(values) = tensor.shared_f32() {
169 return Ok(values.to_vec());
170 }
171 let bytes = tensor.allocation_bytes();
172 if bytes.len() % 4 != 0 {
173 return Err(AiError::InvalidConfiguration(
174 "f32 tensor allocation is not a multiple of 4 bytes".into(),
175 ));
176 }
177 Ok(bytemuck::cast_slice(bytes).to_vec())
178}
179
180#[cfg(test)]
181mod tests {
182 use super::{MockInferenceBackend, MockProfile};
183 use crate::{
184 AiError, CopyPolicy, InferenceBackend, ModelSource, NamedTensors, RunOptions,
185 SessionOptions,
186 };
187 use spatialrust_tensor::{DataType, Device, TensorBuffer, TensorDescriptor};
188
189 #[test]
190 fn synthetic_depth_emits_metric_plane() {
191 let backend = MockInferenceBackend;
192 let mut session = backend
193 .create_session(
194 &ModelSource::Mock(MockProfile::SyntheticDepth),
195 &SessionOptions::default(),
196 )
197 .unwrap();
198 let values = vec![
200 0.0, 1.0, 0.5, 0.25, 0.0, 1.0, 0.5, 0.25, 0.0, 1.0, 0.5, 0.25, ];
204 let input = TensorBuffer::try_from_f32(
205 values,
206 TensorDescriptor::contiguous(DataType::F32, vec![1, 3, 2, 2], Device::CPU),
207 )
208 .unwrap();
209 let mut inputs = NamedTensors::new();
210 inputs.insert("images", input).unwrap();
211 let outputs = session
212 .run_with_options(
213 inputs,
214 RunOptions { input_copy: CopyPolicy::Forbid, output_copy: CopyPolicy::Allow },
215 )
216 .unwrap();
217 let depth = outputs.get("depth").unwrap();
218 assert_eq!(depth.descriptor().shape(), &[1, 1, 2, 2]);
219 assert_eq!(session.model_info().name.as_deref(), Some("mock-synthetic-depth"));
220 let depth_values = depth.shared_f32().unwrap();
221 let near = depth_values[1]; let far = depth_values[0];
223 assert!(near < far);
224 }
225
226 #[test]
227 fn synthetic_depth_requires_output_copy_permission() {
228 let backend = MockInferenceBackend;
229 let mut session = backend
230 .create_session(
231 &ModelSource::Mock(MockProfile::SyntheticDepth),
232 &SessionOptions::default(),
233 )
234 .unwrap();
235 let input = TensorBuffer::try_from_f32(
236 vec![0.0; 12],
237 TensorDescriptor::contiguous(DataType::F32, vec![1, 3, 2, 2], Device::CPU),
238 )
239 .unwrap();
240 let mut inputs = NamedTensors::new();
241 inputs.insert("images", input).unwrap();
242 assert!(matches!(
243 session.run(inputs),
244 Err(AiError::CopyRequired { direction: "output host", .. })
245 ));
246 }
247}