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 SemanticClasses,
25}
26
27#[derive(Clone, Debug, Default)]
29pub struct MockInferenceBackend;
30
31impl InferenceBackend for MockInferenceBackend {
32 fn name(&self) -> &str {
33 "mock"
34 }
35
36 fn create_session(
37 &self,
38 source: &ModelSource,
39 options: &SessionOptions,
40 ) -> AiResult<Box<dyn ModelSession>> {
41 options.validate()?;
42 let profile = match source {
43 ModelSource::Mock(profile) => *profile,
44 ModelSource::Path(_) | ModelSource::Bytes(_) => {
45 return Err(AiError::Unsupported {
46 backend: "mock".into(),
47 operation: "filesystem or byte-backed model loading".into(),
48 });
49 }
50 };
51 Ok(Box::new(MockSession { profile, info: profile.model_info() }))
52 }
53}
54
55struct MockSession {
56 profile: MockProfile,
57 info: ModelInfo,
58}
59
60impl ModelSession for MockSession {
61 fn backend_name(&self) -> &str {
62 "mock"
63 }
64
65 fn model_info(&self) -> &ModelInfo {
66 &self.info
67 }
68
69 fn run_with_options(
70 &mut self,
71 inputs: NamedTensors,
72 options: RunOptions,
73 ) -> AiResult<NamedTensors> {
74 self.info.validate_inputs(&inputs)?;
75 if options.output_copy == CopyPolicy::Forbid {
77 let name = match self.profile {
78 MockProfile::SyntheticDepth => "depth",
79 MockProfile::SemanticClasses => "class_ids",
80 };
81 return Err(AiError::CopyRequired { direction: "output host", name: name.into() });
82 }
83 match self.profile {
84 MockProfile::SyntheticDepth => run_synthetic_depth(inputs, options.input_copy),
85 MockProfile::SemanticClasses => run_semantic_classes(inputs, options.input_copy),
86 }
87 }
88}
89
90impl MockProfile {
91 fn model_info(self) -> ModelInfo {
92 match self {
93 Self::SyntheticDepth => ModelInfo {
94 name: Some("mock-synthetic-depth".into()),
95 inputs: vec![TensorSpec::new(
96 "images",
97 DataType::F32,
98 vec![
99 Dimension::Fixed(1),
100 Dimension::Fixed(3),
101 Dimension::Dynamic,
102 Dimension::Dynamic,
103 ],
104 )],
105 outputs: vec![TensorSpec::new(
106 "depth",
107 DataType::F32,
108 vec![
109 Dimension::Fixed(1),
110 Dimension::Fixed(1),
111 Dimension::Dynamic,
112 Dimension::Dynamic,
113 ],
114 )],
115 },
116 Self::SemanticClasses => ModelInfo {
117 name: Some("mock-semantic-classes".into()),
118 inputs: vec![TensorSpec::new(
119 "features",
120 DataType::F32,
121 vec![Dimension::Fixed(1), Dimension::Fixed(4), Dimension::Dynamic],
122 )],
123 outputs: vec![
124 TensorSpec::new(
125 "class_ids",
126 DataType::U32,
127 vec![Dimension::Fixed(1), Dimension::Dynamic],
128 ),
129 TensorSpec::new(
130 "confidence",
131 DataType::F32,
132 vec![Dimension::Fixed(1), Dimension::Dynamic],
133 ),
134 ],
135 },
136 }
137 }
138}
139
140fn run_synthetic_depth(inputs: NamedTensors, input_copy: CopyPolicy) -> AiResult<NamedTensors> {
141 let input = inputs.get("images").ok_or_else(|| AiError::MissingInput("images".into()))?;
142 let descriptor = input.descriptor();
143 let shape = descriptor.shape();
144 if shape.len() != 4 || shape[0] != 1 || shape[1] != 3 {
145 return Err(AiError::ShapeMismatch {
146 name: "images".into(),
147 expected: vec![
148 Dimension::Fixed(1),
149 Dimension::Fixed(3),
150 Dimension::Dynamic,
151 Dimension::Dynamic,
152 ],
153 actual: shape.to_vec(),
154 });
155 }
156 if !descriptor.is_c_contiguous() || descriptor.byte_offset() != 0 {
157 if input_copy == CopyPolicy::Forbid {
158 return Err(AiError::CopyRequired { direction: "input host", name: "images".into() });
159 }
160 return Err(AiError::Unsupported {
161 backend: "mock".into(),
162 operation: "non-contiguous input packing for SyntheticDepth".into(),
163 });
164 }
165 let height = shape[2];
166 let width = shape[3];
167 let values = f32_values(input)?;
168 let plane = height.saturating_mul(width);
169 if values.len() != plane.saturating_mul(3) {
170 return Err(AiError::InvalidConfiguration(
171 "images storage length does not match NCHW shape".into(),
172 ));
173 }
174 let mut depth = Vec::with_capacity(plane);
175 for index in 0..plane {
176 let r = values[index];
177 let g = values[plane + index];
178 let b = values[2 * plane + index];
179 let luma = (0.299 * r + 0.587 * g + 0.114 * b).clamp(0.0, 1.0);
180 depth.push(1.0 + 0.5 * (1.0 - luma));
181 }
182 let output = TensorBuffer::try_from_f32(
183 depth,
184 TensorDescriptor::contiguous(DataType::F32, vec![1, 1, height, width], Device::CPU),
185 )
186 .map_err(|error| AiError::InvalidConfiguration(error.to_string()))?;
187 let mut outputs = NamedTensors::new();
188 outputs.insert("depth", output)?;
189 Ok(outputs)
190}
191
192fn run_semantic_classes(inputs: NamedTensors, input_copy: CopyPolicy) -> AiResult<NamedTensors> {
193 let input = inputs.get("features").ok_or_else(|| AiError::MissingInput("features".into()))?;
194 let descriptor = input.descriptor();
195 let shape = descriptor.shape();
196 if shape.len() != 3 || shape[0] != 1 || shape[1] != 4 {
197 return Err(AiError::ShapeMismatch {
198 name: "features".into(),
199 expected: vec![Dimension::Fixed(1), Dimension::Fixed(4), Dimension::Dynamic],
200 actual: shape.to_vec(),
201 });
202 }
203 if !descriptor.is_c_contiguous() || descriptor.byte_offset() != 0 {
204 if input_copy == CopyPolicy::Forbid {
205 return Err(AiError::CopyRequired { direction: "input host", name: "features".into() });
206 }
207 return Err(AiError::Unsupported {
208 backend: "mock".into(),
209 operation: "non-contiguous input packing for SemanticClasses".into(),
210 });
211 }
212 let count = shape[2];
213 let values = f32_values(input)?;
214 let plane = count;
215 if values.len() != plane.saturating_mul(4) {
216 return Err(AiError::InvalidConfiguration(
217 "features storage length does not match [1,4,N] shape".into(),
218 ));
219 }
220 let mut class_ids = Vec::with_capacity(count);
221 let mut confidence = Vec::with_capacity(count);
222 for index in 0..count {
223 let x = values[index];
224 let y = values[plane + index];
225 let z = values[2 * plane + index];
226 let radial = values[3 * plane + index];
227 if [x, y, z, radial].iter().any(|value| !value.is_finite()) {
228 return Err(AiError::InvalidConfiguration(
229 "SemanticClasses features must be finite".into(),
230 ));
231 }
232 let class_id = if z < 0.28 {
233 0
234 } else if radial > 0.55 {
235 1
236 } else {
237 2
238 };
239 let margin = match class_id {
240 0 => (0.28 - z).abs(),
241 1 => (radial - 0.55).abs(),
242 _ => (z - 0.28).abs().min((radial - 0.55).abs()),
243 };
244 class_ids.push(class_id);
245 confidence.push((0.60 + 0.40 * (margin * 2.0).clamp(0.0, 1.0)).clamp(0.0, 1.0));
246 }
247 let class_output = TensorBuffer::try_from_u32(
248 class_ids,
249 TensorDescriptor::contiguous(DataType::U32, vec![1, count], Device::CPU),
250 )
251 .map_err(|error| AiError::InvalidConfiguration(error.to_string()))?;
252 let confidence_output = TensorBuffer::try_from_f32(
253 confidence,
254 TensorDescriptor::contiguous(DataType::F32, vec![1, count], Device::CPU),
255 )
256 .map_err(|error| AiError::InvalidConfiguration(error.to_string()))?;
257 let mut outputs = NamedTensors::new();
258 outputs.insert("class_ids", class_output)?;
259 outputs.insert("confidence", confidence_output)?;
260 Ok(outputs)
261}
262
263fn f32_values(tensor: &TensorBuffer) -> AiResult<Vec<f32>> {
264 if tensor.descriptor().dtype() != DataType::F32 {
265 return Err(AiError::DataTypeMismatch {
266 name: "images".into(),
267 expected: DataType::F32,
268 actual: tensor.descriptor().dtype(),
269 });
270 }
271 if let Some(values) = tensor.shared_f32() {
272 return Ok(values.to_vec());
273 }
274 let bytes = tensor.allocation_bytes();
275 if bytes.len() % 4 != 0 {
276 return Err(AiError::InvalidConfiguration(
277 "f32 tensor allocation is not a multiple of 4 bytes".into(),
278 ));
279 }
280 Ok(bytemuck::cast_slice(bytes).to_vec())
281}
282
283#[cfg(test)]
284mod tests {
285 use super::{MockInferenceBackend, MockProfile};
286 use crate::{
287 AiError, CopyPolicy, InferenceBackend, ModelSource, NamedTensors, RunOptions,
288 SessionOptions,
289 };
290 use spatialrust_tensor::{DataType, Device, TensorBuffer, TensorDescriptor};
291
292 #[test]
293 fn synthetic_depth_emits_metric_plane() {
294 let backend = MockInferenceBackend;
295 let mut session = backend
296 .create_session(
297 &ModelSource::Mock(MockProfile::SyntheticDepth),
298 &SessionOptions::default(),
299 )
300 .unwrap();
301 let values = vec![
303 0.0, 1.0, 0.5, 0.25, 0.0, 1.0, 0.5, 0.25, 0.0, 1.0, 0.5, 0.25, ];
307 let input = TensorBuffer::try_from_f32(
308 values,
309 TensorDescriptor::contiguous(DataType::F32, vec![1, 3, 2, 2], Device::CPU),
310 )
311 .unwrap();
312 let mut inputs = NamedTensors::new();
313 inputs.insert("images", input).unwrap();
314 let outputs = session
315 .run_with_options(
316 inputs,
317 RunOptions { input_copy: CopyPolicy::Forbid, output_copy: CopyPolicy::Allow },
318 )
319 .unwrap();
320 let depth = outputs.get("depth").unwrap();
321 assert_eq!(depth.descriptor().shape(), &[1, 1, 2, 2]);
322 assert_eq!(session.model_info().name.as_deref(), Some("mock-synthetic-depth"));
323 let depth_values = depth.shared_f32().unwrap();
324 let near = depth_values[1]; let far = depth_values[0];
326 assert!(near < far);
327 }
328
329 #[test]
330 fn synthetic_depth_requires_output_copy_permission() {
331 let backend = MockInferenceBackend;
332 let mut session = backend
333 .create_session(
334 &ModelSource::Mock(MockProfile::SyntheticDepth),
335 &SessionOptions::default(),
336 )
337 .unwrap();
338 let input = TensorBuffer::try_from_f32(
339 vec![0.0; 12],
340 TensorDescriptor::contiguous(DataType::F32, vec![1, 3, 2, 2], Device::CPU),
341 )
342 .unwrap();
343 let mut inputs = NamedTensors::new();
344 inputs.insert("images", input).unwrap();
345 assert!(matches!(
346 session.run(inputs),
347 Err(AiError::CopyRequired { direction: "output host", .. })
348 ));
349 }
350
351 #[test]
352 fn semantic_classes_are_deterministic_and_explicitly_host_bound() {
353 let backend = MockInferenceBackend;
354 let mut session = backend
355 .create_session(
356 &ModelSource::Mock(MockProfile::SemanticClasses),
357 &SessionOptions { deterministic: true, ..SessionOptions::default() },
358 )
359 .unwrap();
360 let values = vec![
361 0.1, 0.2, 0.3, 0.1, 0.2, 0.3, 0.1, 0.5, 0.8, 0.1, 0.7, 0.2, ];
366 let input = TensorBuffer::try_from_f32(
367 values,
368 TensorDescriptor::contiguous(DataType::F32, vec![1, 4, 3], Device::CPU),
369 )
370 .unwrap();
371 let mut inputs = NamedTensors::new();
372 inputs.insert("features", input).unwrap();
373 let outputs = session
374 .run_with_options(
375 inputs,
376 RunOptions { input_copy: CopyPolicy::Forbid, output_copy: CopyPolicy::Allow },
377 )
378 .unwrap();
379 assert_eq!(outputs.get("class_ids").unwrap().shared_u32().unwrap().as_ref(), &[0, 1, 2]);
380 assert_eq!(outputs.get("confidence").unwrap().descriptor().shape(), &[1, 3]);
381 assert_eq!(session.model_info().name.as_deref(), Some("mock-semantic-classes"));
382 }
383}