#![deny(clippy::indexing_slicing)]
use crate::error::ControlError;
use crate::kinematics::BodyTwist;
use crate::scalar::Numeric;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FollowTheGap<const BEAMS: usize, T: Numeric = f64> {
field_of_view: T,
maximum_range: T,
chassis_width: T,
free_range_threshold: T,
cruise_speed: T,
steering_gain: T,
goal_bias: T,
stopping_distance: T,
clear_distance: T,
frontal_half_angle: T,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FollowTheGapOutput<T: Numeric = f64> {
body_twist: BodyTwist<T>,
heading: T,
gap_start_index: usize,
gap_end_index: usize,
minimum_clearance: T,
blocked: bool,
}
impl<T: Numeric> FollowTheGapOutput<T> {
#[inline]
#[must_use]
fn stopped(minimum_clearance: T) -> Self {
FollowTheGapOutput {
body_twist: BodyTwist::new(T::ZERO, T::ZERO),
heading: T::ZERO,
gap_start_index: 0,
gap_end_index: 0,
minimum_clearance,
blocked: true,
}
}
#[inline]
#[must_use]
pub fn body_twist(&self) -> BodyTwist<T> {
self.body_twist
}
#[inline]
#[must_use]
pub fn heading(&self) -> T {
self.heading
}
#[inline]
#[must_use]
pub fn gap_start_index(&self) -> usize {
self.gap_start_index
}
#[inline]
#[must_use]
pub fn gap_end_index(&self) -> usize {
self.gap_end_index
}
#[inline]
#[must_use]
pub fn minimum_clearance(&self) -> T {
self.minimum_clearance
}
#[inline]
#[must_use]
pub fn is_blocked(&self) -> bool {
self.blocked
}
}
impl<const BEAMS: usize, T: Numeric> FollowTheGap<BEAMS, T> {
pub fn try_new(
field_of_view: T,
maximum_range: T,
chassis_width: T,
free_range_threshold: T,
cruise_speed: T,
) -> Result<Self, ControlError> {
if BEAMS < 2 {
return Err(ControlError::InvalidBeamCount);
}
if !field_of_view.is_finite()
|| !maximum_range.is_finite()
|| !chassis_width.is_finite()
|| !free_range_threshold.is_finite()
|| !cruise_speed.is_finite()
{
return Err(ControlError::NonFinite);
}
if field_of_view <= T::ZERO || field_of_view > T::TWO_PI {
return Err(ControlError::InvalidFieldOfView);
}
if maximum_range <= T::ZERO
|| free_range_threshold <= T::ZERO
|| free_range_threshold > maximum_range
{
return Err(ControlError::NonPositiveRange);
}
if chassis_width <= T::ZERO || chassis_width * T::HALF >= maximum_range {
return Err(ControlError::NonPositiveChassisWidth);
}
if cruise_speed <= T::ZERO {
return Err(ControlError::NonPositiveSpeed);
}
Ok(Self {
field_of_view,
maximum_range,
chassis_width,
free_range_threshold,
cruise_speed,
steering_gain: T::from_f64(1.5),
goal_bias: T::from_f64(0.5),
stopping_distance: chassis_width * T::HALF,
clear_distance: maximum_range,
frontal_half_angle: field_of_view / T::from_f64(4.0),
})
}
pub fn with_steering_gain(mut self, steering_gain: T) -> Result<Self, ControlError> {
if !steering_gain.is_finite() {
return Err(ControlError::NonFinite);
}
if steering_gain <= T::ZERO {
return Err(ControlError::NonPositiveSpeed);
}
self.steering_gain = steering_gain;
Ok(self)
}
pub fn with_goal_bias(mut self, goal_bias: T) -> Result<Self, ControlError> {
if !goal_bias.is_finite() {
return Err(ControlError::NonFinite);
}
if goal_bias < T::ZERO {
return Err(ControlError::NegativeGoalBias);
}
self.goal_bias = goal_bias;
Ok(self)
}
pub fn with_speed_scaling(
mut self,
stopping_distance: T,
clear_distance: T,
) -> Result<Self, ControlError> {
if !stopping_distance.is_finite() || !clear_distance.is_finite() {
return Err(ControlError::NonFinite);
}
if stopping_distance < T::ZERO || stopping_distance >= clear_distance {
return Err(ControlError::InvalidSpeedScaling);
}
self.stopping_distance = stopping_distance;
self.clear_distance = clear_distance;
Ok(self)
}
pub fn with_frontal_half_angle(mut self, frontal_half_angle: T) -> Result<Self, ControlError> {
if !frontal_half_angle.is_finite() {
return Err(ControlError::NonFinite);
}
if frontal_half_angle <= T::ZERO || frontal_half_angle > self.field_of_view * T::HALF {
return Err(ControlError::InvalidFieldOfView);
}
self.frontal_half_angle = frontal_half_angle;
Ok(self)
}
#[inline]
#[must_use]
pub fn beam_angle(&self, index: usize) -> Option<T> {
(index < BEAMS).then(|| self.beam_angle_unchecked(index))
}
pub fn compute(
&self,
beam_ranges: &[T; BEAMS],
goal_angle: T,
) -> Result<FollowTheGapOutput<T>, ControlError> {
if !goal_angle.is_finite() {
return Err(ControlError::NonFinite);
}
let mut minimum_clearance = self.maximum_range;
for index in 0..BEAMS {
let range = self.sanitized_range(beam_ranges, index);
if range < minimum_clearance {
minimum_clearance = range;
}
}
let mut best_score = T::NEG_INFINITY;
let mut best_gap: Option<(usize, usize, T)> = None;
let mut run_start: Option<usize> = None;
for index in 0..=BEAMS {
let is_free = index < BEAMS
&& self.sanitized_range(beam_ranges, index) >= self.free_range_threshold;
match (is_free, run_start) {
(true, None) => run_start = Some(index),
(false, Some(start)) => {
let end = index - 1;
run_start = None;
if self
.gap_width(beam_ranges, start, end)
.is_some_and(|width| width < self.chassis_width)
{
continue;
}
let (low, high) = self.aim_bounds(beam_ranges, start, end);
let aim = if low > high {
(self.beam_angle_unchecked(start) + self.beam_angle_unchecked(end))
* T::HALF
} else {
goal_angle.max(low).min(high)
};
let score =
(high - low).max(T::ZERO) - self.goal_bias * (aim - goal_angle).abs();
let wins = match best_gap {
None => true,
Some((_, _, best_aim)) => {
score > best_score
|| (score == best_score && aim.abs() < best_aim.abs())
}
};
if wins {
best_score = score;
best_gap = Some((start, end, aim));
}
}
_ => {}
}
}
let (gap_start_index, gap_end_index, heading) = match best_gap {
Some(gap) => gap,
None => return Ok(FollowTheGapOutput::stopped(minimum_clearance)),
};
let mut frontal_clearance = self.maximum_range;
for index in 0..BEAMS {
let range = self.sanitized_range(beam_ranges, index);
if self.beam_angle_unchecked(index).abs() <= self.frontal_half_angle
&& range < frontal_clearance
{
frontal_clearance = range;
}
}
let span = self.clear_distance - self.stopping_distance;
let speed_scale = ((frontal_clearance - self.stopping_distance) / span)
.max(T::ZERO)
.min(T::ONE);
Ok(FollowTheGapOutput {
body_twist: BodyTwist::new(
self.cruise_speed * speed_scale,
self.steering_gain * heading,
),
heading,
gap_start_index,
gap_end_index,
minimum_clearance,
blocked: false,
})
}
#[inline]
#[must_use]
fn sanitized_range(&self, beam_ranges: &[T; BEAMS], index: usize) -> T {
match beam_ranges.get(index) {
Some(&range) if range.is_finite() && range > T::ZERO => range.min(self.maximum_range),
_ => self.maximum_range,
}
}
#[inline]
#[must_use]
fn beam_angle_unchecked(&self, index: usize) -> T {
crate::mapping::beam_angle_across(self.field_of_view, BEAMS, index)
}
#[must_use]
fn gap_width(&self, beam_ranges: &[T; BEAMS], start: usize, end: usize) -> Option<T> {
let before = start.checked_sub(1)?;
let after = end.checked_add(1).filter(|&index| index < BEAMS)?;
let range_a = self.sanitized_range(beam_ranges, before);
let range_b = self.sanitized_range(beam_ranges, after);
let separation = self.beam_angle_unchecked(after) - self.beam_angle_unchecked(before);
let squared =
range_a * range_a + range_b * range_b - T::TWO * range_a * range_b * separation.cos();
Some(squared.max(T::ZERO).sqrt())
}
#[must_use]
fn aim_bounds(&self, beam_ranges: &[T; BEAMS], start: usize, end: usize) -> (T, T) {
let half_width = self.chassis_width * T::HALF;
let inset = |index: usize| (half_width / self.sanitized_range(beam_ranges, index)).atan();
let low = self.beam_angle_unchecked(start) + start.checked_sub(1).map_or(T::ZERO, inset);
let high = self.beam_angle_unchecked(end)
- end
.checked_add(1)
.filter(|&index| index < BEAMS)
.map_or(T::ZERO, inset);
(low, high)
}
}