use crate::{DEPTH_SHAPE, INPUT_HEIGHT, INPUT_WIDTH, VitFly, VitFlyState};
use candle_core::{Device, Tensor};
use cu_ahrs::AhrsPose;
use cu_zed::ZedDepthMap;
use cu29::bincode::{Decode, Encode};
use cu29::prelude::*;
use cu29::units::si::angle::radian;
use cu29::units::si::f32::Velocity;
use cu29::units::si::length::meter;
use cu29::units::si::velocity::meter_per_second;
pub type VitFlyVelocity = [Velocity; 3];
const DEFAULT_MAX_DEPTH_M: f32 = 12.5;
const DEFAULT_INVALID_DEPTH: f32 = 0.8;
const RECURRENT_VALUES: usize = 3 * 128;
#[derive(Reflect)]
#[reflect(from_reflect = false)]
pub struct VitFlyTask {
#[reflect(ignore)]
model: VitFly,
#[reflect(ignore)]
device: Device,
#[reflect(ignore)]
recurrent: Option<VitFlyState>,
#[reflect(ignore)]
resized_depth: Vec<f32>,
max_depth_m: f32,
invalid_depth: f32,
}
impl VitFlyTask {
fn from_config(config: Option<&ComponentConfig>) -> CuResult<Self> {
let max_depth_m = config_f32(config, "max_depth_m", DEFAULT_MAX_DEPTH_M)?;
if !max_depth_m.is_finite() || max_depth_m <= 0.0 {
return Err(CuError::from(
"vitfly max_depth_m must be finite and positive",
));
}
let invalid_depth = config_f32(config, "invalid_depth", DEFAULT_INVALID_DEPTH)?;
if !invalid_depth.is_finite() {
return Err(CuError::from("vitfly invalid_depth must be finite"));
}
let requested_device = config
.map(|cfg| cfg.get::<String>("device"))
.transpose()?
.flatten();
let cuda_ordinal = config
.map(|cfg| cfg.get::<u32>("cuda_ordinal"))
.transpose()?
.flatten()
.unwrap_or(0) as usize;
let device = select_device(requested_device.as_deref(), cuda_ordinal)?;
let model = VitFly::load(&device)
.map_err(|err| CuError::new_with_cause("failed to load ViTFly model", err))?;
Ok(Self {
model,
device,
recurrent: None,
resized_depth: vec![0.0; INPUT_HEIGHT * INPUT_WIDTH],
max_depth_m,
invalid_depth,
})
}
fn run_model(
&mut self,
depth: &ZedDepthMap,
pose: &AhrsPose,
desired_speed: Velocity,
) -> CuResult<VitFlyVelocity> {
let format = depth.format;
if format.width == 0
|| format.height == 0
|| !format.is_valid()
|| format.required_elements() == 0
{
return Err(CuError::from(
"vitfly received an invalid ZED raster format",
));
}
depth.with_samples(|samples, format| {
resize_and_normalize_depth(
samples,
format.width as usize,
format.height as usize,
format.stride as usize,
self.max_depth_m,
self.invalid_depth,
&mut self.resized_depth,
);
});
let speed_mps = desired_speed.get::<meter_per_second>();
if !speed_mps.is_finite() || speed_mps < 0.0 {
return Err(CuError::from(
"vitfly desired velocity must be finite and non-negative",
));
}
let quaternion = euler_to_scalar_first_quaternion(
pose.roll.get::<radian>(),
pose.pitch.get::<radian>(),
pose.yaw.get::<radian>(),
);
let depth = Tensor::from_slice(&self.resized_depth, &DEPTH_SHAPE, &self.device)
.map_err(|err| CuError::new_with_cause("failed to upload ViTFly depth", err))?;
let desired_velocity = Tensor::new(&[[speed_mps]], &self.device)
.map_err(|err| CuError::new_with_cause("failed to upload ViTFly speed", err))?;
let attitude = Tensor::new(&[quaternion], &self.device)
.map_err(|err| CuError::new_with_cause("failed to upload ViTFly attitude", err))?;
let (prediction, recurrent) = self
.model
.forward(
&depth,
&desired_velocity,
&attitude,
self.recurrent.as_ref(),
)
.map_err(|err| CuError::new_with_cause("ViTFly inference failed", err))?;
let prediction = prediction
.to_vec2::<f32>()
.map_err(|err| CuError::new_with_cause("failed to download ViTFly output", err))?;
self.recurrent = Some(recurrent);
let [forward, left, up]: [f32; 3] = prediction
.first()
.and_then(|row| row.as_slice().try_into().ok())
.ok_or_else(|| CuError::from("ViTFly returned an invalid output shape"))?;
Ok([forward, left, up]
.map(|component| Velocity::new::<meter_per_second>(component * speed_mps)))
}
}
impl Freezable for VitFlyTask {
fn freeze<E: cu29::bincode::enc::Encoder>(
&self,
encoder: &mut E,
) -> Result<(), cu29::bincode::error::EncodeError> {
let snapshot = self
.recurrent
.as_ref()
.map(|state| {
let hidden = state.hidden_tensor()?.flatten_all()?.to_vec1::<f32>()?;
let cell = state.cell_tensor()?.flatten_all()?.to_vec1::<f32>()?;
candle_core::Result::Ok((hidden, cell))
})
.transpose()
.map_err(|err| cu29::bincode::error::EncodeError::OtherString(err.to_string()))?;
Encode::encode(&snapshot, encoder)
}
fn thaw<D: cu29::bincode::de::Decoder>(
&mut self,
decoder: &mut D,
) -> Result<(), cu29::bincode::error::DecodeError> {
let snapshot: Option<(Vec<f32>, Vec<f32>)> = Decode::decode(decoder)?;
self.recurrent = snapshot
.map(|(hidden, cell)| {
if hidden.len() != RECURRENT_VALUES || cell.len() != RECURRENT_VALUES {
return Err(cu29::bincode::error::DecodeError::Other(
"invalid ViTFly recurrent state length",
));
}
let hidden = Tensor::from_vec(hidden, (3, 128), &self.device)
.map_err(candle_decode_error)?;
let cell =
Tensor::from_vec(cell, (3, 128), &self.device).map_err(candle_decode_error)?;
VitFlyState::from_tensors(&hidden, &cell).map_err(candle_decode_error)
})
.transpose()?;
Ok(())
}
}
impl CuTask for VitFlyTask {
type Resources<'r> = ();
type Input<'m> = input_msg!('m, ZedDepthMap, AhrsPose, Velocity);
type Output<'m> = output_msg!(VitFlyVelocity);
fn new(config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self> {
Self::from_config(config)
}
fn process(
&mut self,
_ctx: &CuContext,
input: &Self::Input<'_>,
output: &mut Self::Output<'_>,
) -> CuResult<()> {
let (depth_msg, pose_msg, speed_msg) = *input;
output.tov = depth_msg.tov;
let (Some(depth), Some(pose), Some(speed)) = (
depth_msg.payload(),
pose_msg.payload(),
speed_msg.payload().copied(),
) else {
output.clear_payload();
output.metadata.set_status("missing input");
return Ok(());
};
output.set_payload(self.run_model(depth, pose, speed)?);
output.metadata.set_status("ok");
Ok(())
}
}
fn select_device(requested: Option<&str>, cuda_ordinal: usize) -> CuResult<Device> {
let requested = requested.unwrap_or(if cfg!(feature = "cuda") {
"cuda"
} else {
"cpu"
});
match requested {
"cpu" => Ok(Device::Cpu),
"cuda" => {
#[cfg(feature = "cuda")]
{
Device::new_cuda(cuda_ordinal)
.map_err(|err| CuError::new_with_cause("failed to initialize CUDA", err))
}
#[cfg(not(feature = "cuda"))]
{
let _ = cuda_ordinal;
Err(CuError::from(
"vitfly device=cuda requires the cu-vitfly cuda feature",
))
}
}
_ => Err(CuError::from("vitfly device must be either cpu or cuda")),
}
}
fn config_f32(config: Option<&ComponentConfig>, key: &str, default: f32) -> CuResult<f32> {
Ok(config
.map(|cfg| cfg.get::<f32>(key))
.transpose()?
.flatten()
.unwrap_or(default))
}
fn candle_decode_error(err: candle_core::Error) -> cu29::bincode::error::DecodeError {
cu29::bincode::error::DecodeError::OtherString(err.to_string())
}
fn normalize_depth(sample: u16, max_depth_m: f32, invalid_depth: f32) -> f32 {
ZedDepthMap::decode_sample(sample)
.map(|depth| (depth.get::<meter>() / max_depth_m).clamp(0.0, 1.0))
.unwrap_or(invalid_depth)
}
#[allow(clippy::too_many_arguments)]
fn resize_and_normalize_depth(
source: &[u16],
source_width: usize,
source_height: usize,
source_stride: usize,
max_depth_m: f32,
invalid_depth: f32,
destination: &mut [f32],
) {
debug_assert_eq!(destination.len(), INPUT_HEIGHT * INPUT_WIDTH);
let scale_x = source_width as f32 / INPUT_WIDTH as f32;
let scale_y = source_height as f32 / INPUT_HEIGHT as f32;
for output_y in 0..INPUT_HEIGHT {
let source_y =
((output_y as f32 + 0.5) * scale_y - 0.5).clamp(0.0, (source_height - 1) as f32);
let y0 = source_y.floor() as usize;
let y1 = (y0 + 1).min(source_height - 1);
let wy = source_y - y0 as f32;
for output_x in 0..INPUT_WIDTH {
let source_x =
((output_x as f32 + 0.5) * scale_x - 0.5).clamp(0.0, (source_width - 1) as f32);
let x0 = source_x.floor() as usize;
let x1 = (x0 + 1).min(source_width - 1);
let wx = source_x - x0 as f32;
let top_left =
normalize_depth(source[y0 * source_stride + x0], max_depth_m, invalid_depth);
let top_right =
normalize_depth(source[y0 * source_stride + x1], max_depth_m, invalid_depth);
let bottom_left =
normalize_depth(source[y1 * source_stride + x0], max_depth_m, invalid_depth);
let bottom_right =
normalize_depth(source[y1 * source_stride + x1], max_depth_m, invalid_depth);
let top = top_left * (1.0 - wx) + top_right * wx;
let bottom = bottom_left * (1.0 - wx) + bottom_right * wx;
destination[output_y * INPUT_WIDTH + output_x] = top * (1.0 - wy) + bottom * wy;
}
}
}
fn euler_to_scalar_first_quaternion(roll: f32, pitch: f32, yaw: f32) -> [f32; 4] {
let (sr, cr) = (0.5 * roll).sin_cos();
let (sp, cp) = (0.5 * pitch).sin_cos();
let (sy, cy) = (0.5 * yaw).sin_cos();
[
cr * cp * cy + sr * sp * sy,
sr * cp * cy - cr * sp * sy,
cr * sp * cy + sr * cp * sy,
cr * cp * sy - sr * sp * cy,
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_pose_maps_to_scalar_first_identity() {
assert_eq!(
euler_to_scalar_first_quaternion(0.0, 0.0, 0.0),
[1.0, 0.0, 0.0, 0.0]
);
}
#[test]
fn resize_respects_stride_and_normalizes_meters() {
let mut source = vec![u16::MAX; 6 * 2];
source[..4].copy_from_slice(&[0, 6_250, 12_500, 25_000]);
source[6..10].copy_from_slice(&[0, 6_250, 12_500, 25_000]);
let mut destination = vec![0.0; INPUT_HEIGHT * INPUT_WIDTH];
resize_and_normalize_depth(&source, 4, 2, 6, 12.5, 0.8, &mut destination);
assert_eq!(destination[0], 0.8);
assert!((destination[INPUT_WIDTH - 1] - 1.0).abs() < 1.0e-6);
}
}