use deke_types::glam::{DAffine3, DQuat, DVec3};
use deke_types::{
ContinuousFKChain, DekeError, DekeResult, IkOutcome, IkSolver, Planner, SRobotPath, SRobotQ,
Validator,
};
use crate::constraints::PlannerOptions;
use crate::diagnostic::RedundantDiagnostic;
use crate::error::LinearError;
use crate::path::CartesianRun;
use crate::planner::is_reconfiguration;
use crate::util::{interp, ladder_dp};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum RedundantAxis {
PosX,
NegX,
PosY,
NegY,
PosZ,
NegZ,
Custom(DVec3),
}
impl RedundantAxis {
pub fn vector(&self) -> DVec3 {
match self {
RedundantAxis::PosX => DVec3::X,
RedundantAxis::NegX => DVec3::NEG_X,
RedundantAxis::PosY => DVec3::Y,
RedundantAxis::NegY => DVec3::NEG_Y,
RedundantAxis::PosZ => DVec3::Z,
RedundantAxis::NegZ => DVec3::NEG_Z,
RedundantAxis::Custom(v) => v.normalize_or_zero(),
}
}
}
#[derive(Clone, Debug)]
pub struct RedundantOptions {
pub axis: RedundantAxis,
pub yaw_window: (f64, f64),
pub yaw_samples: usize,
pub dp_ds: f64,
pub yaw_rate_weight: f64,
pub max_yaw_step: f64,
}
impl Default for RedundantOptions {
fn default() -> Self {
Self {
axis: RedundantAxis::PosZ,
yaw_window: (-std::f64::consts::PI, std::f64::consts::PI),
yaw_samples: 24,
dp_ds: 5e-3,
yaw_rate_weight: 0.2,
max_yaw_step: 0.6,
}
}
}
#[derive(Clone, Debug)]
pub struct RedundantConfig<const N: usize> {
pub planner: PlannerOptions<N>,
pub redundant: RedundantOptions,
}
struct YawNode<const N: usize> {
yaw: f64,
q: SRobotQ<N, f64>,
cost: f64,
}
#[derive(Clone, Debug)]
pub struct RedundantLinearPlanner<'a, const N: usize, FK> {
fk: &'a FK,
}
impl<'a, const N: usize, FK> RedundantLinearPlanner<'a, N, FK>
where
FK: ContinuousFKChain<N, f64> + IkSolver<N, f64>,
{
pub fn new(fk: &'a FK) -> Self {
Self { fk }
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn plan_run<V: Validator<N, (), f64>>(
&self,
run: &CartesianRun,
planner: &PlannerOptions<N>,
red: &RedundantOptions,
validator: &V,
ctx: &V::Context<'_>,
seed: Option<&SRobotQ<N, f64>>,
run_idx: usize,
) -> Result<(SRobotPath<N, f64>, RedundantDiagnostic), LinearError> {
let axis = red.axis.vector();
let length = run.length();
let n_dp = ((length / red.dp_ds).ceil() as usize).max(1) + 1;
let stations = self.build_stations(
run, red, planner, axis, length, n_dp, validator, ctx, run_idx,
)?;
let (coarse_s, coarse_psi) = solve_global(&stations, red, planner, length, n_dp)
.ok_or(LinearError::NoContinuousTrack { run: run_idx })?;
self.refine(
run,
planner,
axis,
length,
&coarse_s,
&coarse_psi,
validator,
ctx,
seed,
run_idx,
)
}
#[allow(clippy::too_many_arguments)]
fn build_stations<V: Validator<N, (), f64>>(
&self,
run: &CartesianRun,
red: &RedundantOptions,
planner: &PlannerOptions<N>,
axis: DVec3,
length: f64,
n_dp: usize,
validator: &V,
ctx: &V::Context<'_>,
run_idx: usize,
) -> Result<Vec<Vec<YawNode<N>>>, LinearError> {
let samples = red.yaw_samples.max(1);
let yaw_at = |m: usize| -> f64 {
if samples <= 1 {
0.5 * (red.yaw_window.0 + red.yaw_window.1)
} else {
red.yaw_window.0
+ (red.yaw_window.1 - red.yaw_window.0) * m as f64 / (samples - 1) as f64
}
};
let mut stations = Vec::with_capacity(n_dp);
for k in 0..n_dp {
let s = (k as f64 * red.dp_ds).min(length);
let ref_pose = run.eval(s);
let ref_rot = DQuat::from_mat3(&ref_pose.matrix3);
let mut nodes = Vec::with_capacity(samples * 4);
let mut had_ik = false;
for m in 0..samples {
let psi = yaw_at(m);
let rot = ref_rot * DQuat::from_axis_angle(axis, psi);
let target = DAffine3::from_rotation_translation(rot, ref_pose.translation);
if let IkOutcome::Solved(sols) = self.fk.ik(target)? {
had_ik |= !sols.is_empty();
for q in sols {
if validator.validate(q, ctx).is_err() {
continue;
}
let w = self.fk.manipulability(&q).map_err(LinearError::Deke)?;
nodes.push(YawNode {
yaw: psi,
q,
cost: planner.manip_weight / (w + 1e-9),
});
}
}
}
if nodes.is_empty() {
return Err(if had_ik {
LinearError::Obstructed { run: run_idx, s }
} else {
LinearError::Unreachable { run: run_idx, s }
});
}
stations.push(nodes);
}
Ok(stations)
}
#[allow(clippy::too_many_arguments)]
fn refine<V: Validator<N, (), f64>>(
&self,
run: &CartesianRun,
planner: &PlannerOptions<N>,
axis: DVec3,
length: f64,
coarse_s: &[f64],
coarse_psi: &[f64],
validator: &V,
ctx: &V::Context<'_>,
seed: Option<&SRobotQ<N, f64>>,
run_idx: usize,
) -> Result<(SRobotPath<N, f64>, RedundantDiagnostic), LinearError> {
let n_fine = ((length / planner.sample_ds).ceil() as usize).max(1) + 1;
let mut fine: Vec<SRobotQ<N, f64>> = Vec::with_capacity(n_fine);
let mut min_manip = f64::INFINITY;
let mut yaw_lo = f64::INFINITY;
let mut yaw_hi = f64::NEG_INFINITY;
let mut prev: Option<(SRobotQ<N, f64>, f64)> = None;
for i in 0..n_fine {
let s = (i as f64 * planner.sample_ds).min(length);
let psi = interp(coarse_s, coarse_psi, s);
yaw_lo = yaw_lo.min(psi);
yaw_hi = yaw_hi.max(psi);
let ref_pose = run.eval(s);
let rot = DQuat::from_mat3(&ref_pose.matrix3) * DQuat::from_axis_angle(axis, psi);
let target = DAffine3::from_rotation_translation(rot, ref_pose.translation);
let raw = match self.fk.ik(target)? {
IkOutcome::Solved(sols) if !sols.is_empty() => sols,
_ => return Err(LinearError::Unreachable { run: run_idx, s }),
};
let sols: Vec<SRobotQ<N, f64>> = raw
.into_iter()
.filter(|q| validator.validate(*q, ctx).is_ok())
.collect();
if sols.is_empty() {
return Err(LinearError::Obstructed { run: run_idx, s });
}
let q = match prev {
Some((pq, _)) => sols
.into_iter()
.min_by(|a, b| pq.distance(a).total_cmp(&pq.distance(b)))
.unwrap(),
None => match seed {
Some(s) => sols
.into_iter()
.filter(|q| !is_reconfiguration(s, q, f64::INFINITY, planner))
.min_by(|a, b| s.distance(a).total_cmp(&s.distance(b)))
.ok_or(LinearError::NoContinuousTrack { run: run_idx })?,
None => {
let mut best = sols[0];
let mut best_w = -1.0;
for q in sols {
let w = self.fk.manipulability(&q).map_err(LinearError::Deke)?;
if w > best_w {
best_w = w;
best = q;
}
}
best
}
},
};
if let Some((pq, ps)) = prev {
let dsf = (s - ps).max(1e-12);
if is_reconfiguration(&pq, &q, dsf, planner) {
return Err(LinearError::NoContinuousTrack { run: run_idx });
}
}
min_manip = min_manip.min(self.fk.manipulability(&q).map_err(LinearError::Deke)?);
fine.push(q);
prev = Some((q, s));
}
let path = SRobotPath::try_new(fine).map_err(LinearError::from)?;
Ok((
path,
RedundantDiagnostic {
samples: n_fine,
min_manipulability: min_manip,
yaw_range: (yaw_lo, yaw_hi),
},
))
}
}
impl<'a, const N: usize, FK> Planner<N, f64> for RedundantLinearPlanner<'a, N, FK>
where
FK: ContinuousFKChain<N, f64> + IkSolver<N, f64>,
{
type Diagnostic = RedundantDiagnostic;
type Config = RedundantConfig<N>;
type Waypoints = CartesianRun;
fn plan<E: Into<DekeError>, V: Validator<N, (), f64>>(
&self,
config: &Self::Config,
waypoints: &Self::Waypoints,
validator: &V,
ctx: &V::Context<'_>,
) -> (DekeResult<SRobotPath<N, f64>>, Self::Diagnostic) {
match self.plan_run(
waypoints,
&config.planner,
&config.redundant,
validator,
ctx,
None,
0,
) {
Ok((path, diag)) => (Ok(path), diag),
Err(e) => (
Err(e.into()),
RedundantDiagnostic {
samples: 0,
min_manipulability: 0.0,
yaw_range: (0.0, 0.0),
},
),
}
}
}
fn solve_global<const N: usize>(
stations: &[Vec<YawNode<N>>],
red: &RedundantOptions,
planner: &PlannerOptions<N>,
length: f64,
n_dp: usize,
) -> Option<(Vec<f64>, Vec<f64>)> {
let ds_at = |k: usize| {
((k as f64 * red.dp_ds).min(length) - ((k - 1) as f64 * red.dp_ds).min(length)).max(1e-9)
};
let layer_sizes: Vec<usize> = stations.iter().map(Vec::len).collect();
let (chosen, _) = ladder_dp(
&layer_sizes,
|k, i| stations[k][i].cost,
|k, p, i| {
let na = &stations[k - 1][p];
let nb = &stations[k][i];
let dyaw = (nb.yaw - na.yaw).abs();
let ds = ds_at(k);
if dyaw > red.max_yaw_step || is_reconfiguration(&na.q, &nb.q, ds, planner) {
None
} else {
Some(na.q.distance(&nb.q) + red.yaw_rate_weight * dyaw / ds)
}
},
)?;
let s = (0..n_dp)
.map(|k| (k as f64 * red.dp_ds).min(length))
.collect();
let psi = chosen
.iter()
.enumerate()
.map(|(k, &i)| stations[k][i].yaw)
.collect();
Some((s, psi))
}