use std::time::Duration;
const CALIBRATION_PERIODS: u32 = 4;
const CALIBRATION_SPREAD: f64 = 2.236;
const CALIBRATION_FLOOR: f64 = 1000.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BiasValues {
pub refractory: u16,
pub photoreceptor: u16,
pub follower: u16,
pub on_threshold: u16,
pub off_threshold: u16,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Authority {
Tracking,
Starved,
Flooded,
Hunting,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BiasConfig {
pub period: Duration,
pub target_rate: (f64, f64),
pub calibrate: u32,
pub throttle_range: (u16, u16),
pub max_slew: u16,
pub patience: u32,
pub step: u16,
pub limits: BiasLimits,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BiasLimits {
pub photoreceptor: (u16, u16),
pub follower: (u16, u16),
pub on_threshold: (u16, u16),
pub off_threshold: (u16, u16),
}
impl BiasLimits {
pub fn uniform(low: u16, high: u16) -> Self {
Self {
photoreceptor: (low, high),
follower: (low, high),
on_threshold: (low, high),
off_threshold: (low, high),
}
}
fn each(&self) -> [(u16, u16); 4] {
[
self.photoreceptor,
self.follower,
self.on_threshold,
self.off_threshold,
]
}
}
impl BiasConfig {
pub fn davis346() -> Self {
Self {
period: Duration::from_millis(300),
target_rate: (5e5, 2.5e6),
calibrate: CALIBRATION_PERIODS,
throttle_range: (801, 1084),
max_slew: 16,
patience: 2,
step: 6,
limits: BiasLimits::uniform(0, 2040),
}
}
pub fn prophesee_evk4() -> Self {
Self {
period: Duration::from_millis(300),
target_rate: (5e6, 2.5e7),
calibrate: CALIBRATION_PERIODS,
throttle_range: (0, 235),
max_slew: 12,
patience: 2,
step: 2,
limits: BiasLimits {
photoreceptor: (60, 160),
follower: (55, 130),
on_threshold: (83, 140),
off_threshold: (35, 76),
},
}
}
pub fn validate(&self) -> Result<(), String> {
let (rate_min, rate_max) = self.target_rate;
if !rate_min.is_finite() || !rate_max.is_finite() || rate_min < 0.0 {
return Err("target_rate must be finite and non-negative".to_owned());
}
if rate_max <= rate_min {
return Err("target_rate must be (low, high) with high greater than low".to_owned());
}
if self.period.is_zero() {
return Err("period must be greater than zero".to_owned());
}
if self.throttle_range.0 > self.throttle_range.1 {
return Err(
"throttle_range must be (most throttled, least throttled), lowest index first"
.to_owned(),
);
}
if self.limits.each().iter().any(|(low, high)| low > high) {
return Err("every entry in limits must be (low, high)".to_owned());
}
if self.max_slew == 0 {
return Err("max_slew must be at least 1, or the fast loop can never move".to_owned());
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct BiasOverrides {
pub period: Option<Duration>,
pub target_rate: Option<(f64, f64)>,
pub throttle_range: Option<(u16, u16)>,
pub max_slew: Option<u16>,
pub patience: Option<u32>,
pub step: Option<u16>,
pub limits: Option<BiasLimits>,
pub calibrate: Option<u32>,
}
impl BiasOverrides {
pub fn apply(&self, base: BiasConfig) -> BiasConfig {
BiasConfig {
period: self.period.unwrap_or(base.period),
target_rate: self.target_rate.unwrap_or(base.target_rate),
calibrate: self
.calibrate
.unwrap_or(if self.target_rate.is_some() { 0 } else { base.calibrate }),
throttle_range: self.throttle_range.unwrap_or(base.throttle_range),
max_slew: self.max_slew.unwrap_or(base.max_slew),
patience: self.patience.unwrap_or(base.patience),
step: self.step.unwrap_or(base.step),
limits: self.limits.unwrap_or(base.limits),
}
}
pub fn validate(&self) -> Result<(), String> {
self.apply(BiasConfig::davis346()).validate()?;
self.apply(BiasConfig::prophesee_evk4()).validate()
}
}
fn shift(value: u16, step: i32, (low, high): (u16, u16)) -> u16 {
(i32::from(value) + step).clamp(i32::from(low), i32::from(high)) as u16
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BiasState {
pub event_rate: f64,
pub values: BiasValues,
pub target_rate: (f64, f64),
pub calibrating: bool,
pub authority: Authority,
pub slow_steps: u64,
}
#[derive(Clone, Debug)]
pub struct BiasController {
config: BiasConfig,
values: BiasValues,
events: u64,
elapsed: Duration,
undercounted: bool,
too_few: u32,
too_many: u32,
hunting: u32,
unreachable_events: u64,
unreachable_elapsed: Duration,
last_want: i32,
calibrating: u32,
calibration_events: u64,
calibration_elapsed: Duration,
event_rate: f64,
authority: Authority,
slow_steps: u64,
}
impl BiasController {
pub fn new(config: BiasConfig, start: BiasValues) -> Self {
let limits = config.limits;
let start = BiasValues {
refractory: start.refractory,
photoreceptor: shift(start.photoreceptor, 0, limits.photoreceptor),
follower: shift(start.follower, 0, limits.follower),
on_threshold: shift(start.on_threshold, 0, limits.on_threshold),
off_threshold: shift(start.off_threshold, 0, limits.off_threshold),
};
Self {
config,
values: start,
events: 0,
elapsed: Duration::ZERO,
undercounted: false,
too_few: 0,
too_many: 0,
hunting: 0,
unreachable_events: 0,
unreachable_elapsed: Duration::ZERO,
last_want: 0,
calibrating: config.calibrate,
calibration_events: 0,
calibration_elapsed: Duration::ZERO,
event_rate: 0.0,
authority: Authority::Tracking,
slow_steps: 0,
}
}
pub fn state(&self) -> BiasState {
BiasState {
event_rate: self.event_rate,
values: self.values,
target_rate: self.config.target_rate,
calibrating: self.calibrating > 0,
authority: self.authority,
slow_steps: self.slow_steps,
}
}
pub fn observe(&mut self, events: u64, elapsed: Duration) -> Option<BiasValues> {
self.events += events;
self.elapsed += elapsed;
if self.elapsed < self.config.period {
return None;
}
let (events, elapsed) = (self.events, self.elapsed);
let rate = events as f64 / elapsed.as_secs_f64();
let undercounted = self.undercounted;
self.events = 0;
self.elapsed = Duration::ZERO;
self.undercounted = false;
self.control(rate, events, elapsed, undercounted)
}
pub fn invalidate(&mut self) {
self.undercounted = true;
}
fn control(
&mut self,
rate: f64,
events: u64,
elapsed: Duration,
undercounted: bool,
) -> Option<BiasValues> {
self.event_rate = rate;
if self.calibrating > 0 {
if !undercounted {
self.calibration_events += events;
self.calibration_elapsed += elapsed;
}
self.calibrating -= 1;
if self.calibrating == 0 {
self.finish_calibration();
}
self.authority = Authority::Tracking;
return None;
}
let (rate_min, rate_max) = self.config.target_rate;
if undercounted && rate < rate_max {
self.authority = Authority::Tracking;
return None;
}
let before = self.values;
let (throttled, open) = self.config.throttle_range;
let current = self.values.refractory;
let target = self.throttle_target(rate);
let want = (target as i32 - current as i32).signum();
let reversed = want != 0 && self.last_want != 0 && want != self.last_want;
let out_of_band = rate < rate_min || rate > rate_max;
self.last_want = want;
self.values.refractory = target.clamp(
current.saturating_sub(self.config.max_slew),
current.saturating_add(self.config.max_slew),
);
let moved = self.values.refractory;
let railed =
(target >= open && moved >= open) || (target <= throttled && moved <= throttled);
self.authority = match (out_of_band, railed, reversed) {
(false, _, _) => Authority::Tracking,
(true, true, _) if target >= open => Authority::Starved,
(true, true, _) => Authority::Flooded,
(true, false, true) => Authority::Hunting,
(true, false, false) => Authority::Tracking,
};
if !out_of_band {
self.forget_unreachable();
} else if reversed || self.hunting > 0 {
self.unreachable_events += events;
self.unreachable_elapsed += elapsed;
}
match self.authority {
Authority::Starved => {
self.too_many = 0;
self.too_few += 1;
if self.too_few >= self.config.patience {
self.too_few = 0;
self.step_slow(1);
}
}
Authority::Flooded => {
self.too_few = 0;
self.too_many += 1;
if self.too_many >= self.config.patience {
self.too_many = 0;
self.step_slow(-1);
}
}
Authority::Hunting => {
self.too_few = 0;
self.too_many = 0;
self.hunting += 1;
if self.hunting >= self.hunting_patience() {
let average = self.unreachable_events as f64
/ self.unreachable_elapsed.as_secs_f64().max(f64::EPSILON);
let direction = if average > rate_max {
-1
} else if average < rate_min {
1
} else {
0 };
self.forget_unreachable();
if direction != 0 {
self.step_slow(direction);
}
}
}
Authority::Tracking => {
self.too_few = 0;
self.too_many = 0;
}
}
(self.values != before).then_some(self.values)
}
fn finish_calibration(&mut self) {
let seconds = self.calibration_elapsed.as_secs_f64();
if seconds <= 0.0 {
return;
}
let measured = self.calibration_events as f64 / seconds;
if measured < CALIBRATION_FLOOR {
return;
}
self.config.target_rate = (measured / CALIBRATION_SPREAD, measured * CALIBRATION_SPREAD);
}
fn hunting_patience(&self) -> u32 {
self.config.patience.saturating_mul(2).max(2)
}
fn forget_unreachable(&mut self) {
self.hunting = 0;
self.unreachable_events = 0;
self.unreachable_elapsed = Duration::ZERO;
}
fn throttle_target(&self, rate: f64) -> u16 {
let (rate_min, rate_max) = self.config.target_rate;
let (throttled, open) = self.config.throttle_range;
let fraction = ((rate - rate_min) / (rate_max - rate_min)).clamp(0.0, 1.0);
let travel = open.saturating_sub(throttled);
open.saturating_sub((f64::from(travel) * fraction).round() as u16)
}
fn step_slow(&mut self, direction: i32) {
let step = i32::from(self.config.step) * direction;
let limits = self.config.limits;
self.values.photoreceptor = shift(self.values.photoreceptor, step, limits.photoreceptor);
self.values.follower = shift(self.values.follower, step, limits.follower);
self.values.on_threshold = shift(self.values.on_threshold, -step, limits.on_threshold);
self.values.off_threshold = shift(self.values.off_threshold, step, limits.off_threshold);
self.slow_steps += 1;
}
}
#[cfg(test)]
mod tests {
use super::{Authority, BiasConfig, BiasController, BiasLimits, BiasValues};
use std::time::Duration;
fn start() -> BiasValues {
BiasValues {
refractory: 993,
photoreceptor: 575,
follower: 153,
on_threshold: 1564,
off_threshold: 583,
}
}
fn config() -> BiasConfig {
BiasConfig { calibrate: 0, ..BiasConfig::davis346() }
}
fn period(controller: &mut BiasController, rate: f64) -> Option<BiasValues> {
let span = controller.config.period;
let events = (rate * span.as_secs_f64()).round() as u64;
controller.observe(events, span)
}
fn drive(controller: &mut BiasController, rate: f64, periods: usize) {
for _ in 0..periods {
period(controller, rate);
}
}
fn periods_to(target: u16) -> usize {
let travel = target.abs_diff(start().refractory);
(travel as usize).div_ceil(config().max_slew as usize)
}
#[test]
fn every_shipped_configuration_is_valid() {
assert_eq!(BiasConfig::davis346().validate(), Ok(()));
assert_eq!(BiasConfig::prophesee_evk4().validate(), Ok(()));
}
#[test]
fn the_evk4_thresholds_stay_the_right_side_of_its_diff_reference() {
let limits = BiasConfig::prophesee_evk4().limits;
assert!(limits.on_threshold.0 > 77, "ON threshold must stay above diff");
assert!(limits.off_threshold.1 < 77, "OFF threshold must stay below diff");
}
#[test]
fn evk4_biases_stay_inside_a_u8() {
let config = BiasConfig::prophesee_evk4();
for (_, high) in config.limits.each() {
assert!(high <= u16::from(u8::MAX), "{high} does not fit a byte register");
}
assert!(config.throttle_range.1 <= u16::from(u8::MAX));
}
#[test]
fn calibration_centres_the_band_on_the_scene_and_touches_nothing_first() {
let calibrated = BiasConfig::davis346();
let mut controller = BiasController::new(calibrated, start());
assert!(controller.state().calibrating);
for remaining in (0..calibrated.calibrate).rev() {
assert_eq!(period(&mut controller, 3e4), None, "no bias may move while measuring");
assert_eq!(controller.state().calibrating, remaining > 0);
}
assert_eq!(controller.state().values, start());
let (low, high) = controller.state().target_rate;
assert!(!controller.state().calibrating);
assert!(low < 3e4 && 3e4 < high, "the band must bracket the measured rate");
assert!((high / low - 5.0).abs() < 0.1, "spanning the reference paper's 5:1");
assert!(period(&mut controller, 1e6).is_some());
}
#[test]
fn calibration_keeps_the_default_band_when_there_is_nothing_to_measure() {
let calibrated = BiasConfig::davis346();
let mut controller = BiasController::new(calibrated, start());
drive(&mut controller, 0.0, calibrated.calibrate as usize);
assert_eq!(controller.state().target_rate, calibrated.target_rate);
}
#[test]
fn an_explicit_band_skips_calibration() {
let overrides = super::BiasOverrides {
target_rate: Some((1e4, 1e5)),
..Default::default()
};
let config = overrides.apply(BiasConfig::davis346());
assert_eq!(config.calibrate, 0, "an explicit band must not be measured over");
assert_eq!(config.target_rate, (1e4, 1e5));
let both = super::BiasOverrides {
calibrate: Some(4),
..overrides
};
assert_eq!(both.apply(BiasConfig::davis346()).calibrate, 4);
}
#[test]
fn an_undercounted_calibration_period_is_not_measured() {
let calibrated = BiasConfig::davis346();
let mut controller = BiasController::new(calibrated, start());
controller.invalidate();
period(&mut controller, 0.0); drive(&mut controller, 4e4, (calibrated.calibrate - 1) as usize);
let (low, high) = controller.state().target_rate;
assert!(low < 4e4 && 4e4 < high, "the band follows only the periods that counted");
}
#[test]
fn a_start_outside_the_limits_is_pulled_into_range() {
let narrow = BiasConfig {
limits: BiasLimits {
off_threshold: (35, 60),
..BiasConfig::prophesee_evk4().limits
},
..BiasConfig::prophesee_evk4()
};
let controller = BiasController::new(narrow, BiasValues { off_threshold: 73, ..start() });
assert_eq!(controller.state().values.off_threshold, 60);
}
#[test]
fn overrides_leave_unnamed_fields_to_the_sensor() {
let overrides = super::BiasOverrides {
target_rate: Some((1e4, 1e5)),
..Default::default()
};
let davis = overrides.apply(BiasConfig::davis346());
let evk4 = overrides.apply(BiasConfig::prophesee_evk4());
assert_eq!(davis.target_rate, (1e4, 1e5));
assert_eq!(evk4.target_rate, (1e4, 1e5));
assert_eq!(davis.throttle_range, BiasConfig::davis346().throttle_range);
assert_eq!(evk4.throttle_range, BiasConfig::prophesee_evk4().throttle_range);
assert!(overrides.validate().is_ok());
}
#[test]
fn overrides_are_rejected_for_either_sensor() {
let overrides = super::BiasOverrides {
target_rate: Some((1e6, 1e5)),
..Default::default()
};
assert!(overrides.validate().is_err());
}
#[test]
fn validate_rejects_unusable_tuning() {
for broken in [
BiasConfig { target_rate: (2.5e6, 5e5), ..config() },
BiasConfig { period: Duration::ZERO, ..config() },
BiasConfig { throttle_range: (1084, 801), ..config() },
BiasConfig { limits: BiasLimits::uniform(2040, 0), ..config() },
BiasConfig { max_slew: 0, ..config() },
] {
assert!(broken.validate().is_err(), "{broken:?} should be rejected");
}
}
#[test]
fn the_fast_loop_moves_by_at_most_one_slew_per_period() {
let slew = config().max_slew;
let mut controller = BiasController::new(config(), start());
assert_eq!(period(&mut controller, 1e4).unwrap().refractory, start().refractory + slew);
assert_eq!(period(&mut controller, 1e4).unwrap().refractory, start().refractory + 2 * slew);
let mut controller = BiasController::new(config(), start());
assert_eq!(period(&mut controller, 1e8).unwrap().refractory, start().refractory - slew);
}
#[test]
fn the_fast_loop_reaches_the_rails_and_stops_there() {
let (throttled, open) = config().throttle_range;
let mut controller = BiasController::new(config(), start());
drive(&mut controller, 1e4, periods_to(open) + 4);
assert_eq!(controller.state().values.refractory, open, "never past the rail");
let mut controller = BiasController::new(config(), start());
drive(&mut controller, 1e8, periods_to(throttled) + 4);
assert_eq!(controller.state().values.refractory, throttled);
}
#[test]
fn a_mid_band_rate_settles_mid_travel() {
let (throttled, open) = config().throttle_range;
let middle = (throttled + open) / 2;
let mut controller = BiasController::new(config(), start());
drive(&mut controller, 1.5e6, periods_to(middle) + 4);
assert_eq!(controller.state().values.refractory, middle);
assert_eq!(controller.state().authority, Authority::Tracking);
assert_eq!(controller.state().slow_steps, 0);
}
#[test]
fn nothing_is_reported_until_the_period_completes() {
let mut controller = BiasController::new(config(), start());
let slice = Duration::from_millis(100);
assert_eq!(controller.observe(50_000, slice), None);
assert_eq!(controller.observe(50_000, slice), None);
let update = controller.observe(50_000, slice).expect("period completed");
assert_eq!(update.refractory, start().refractory + config().max_slew);
assert_eq!(controller.state().event_rate, 5e5);
}
#[test]
fn a_settled_rate_leaves_the_slow_biases_alone() {
let mut controller = BiasController::new(config(), start());
drive(&mut controller, 1.5e6, 10);
let state = controller.state();
assert_eq!(state.authority, Authority::Tracking);
assert_eq!(state.slow_steps, 0);
assert_eq!(state.values.photoreceptor, start().photoreceptor);
assert_eq!(state.values.on_threshold, start().on_threshold);
}
#[test]
fn the_slow_loop_waits_until_the_fast_loop_is_out_of_travel() {
let open = config().throttle_range.1;
let mut controller = BiasController::new(config(), start());
drive(&mut controller, 1e4, periods_to(open) - 1);
assert_eq!(controller.state().authority, Authority::Tracking, "still slewing");
assert_eq!(controller.state().slow_steps, 0);
period(&mut controller, 1e4); assert_eq!(controller.state().authority, Authority::Starved);
assert_eq!(controller.state().slow_steps, 0);
period(&mut controller, 1e4); let state = controller.state();
assert_eq!(state.slow_steps, 1);
let step = config().step;
assert_eq!(state.values.photoreceptor, start().photoreceptor + step);
assert_eq!(state.values.follower, start().follower + step);
assert_eq!(state.values.on_threshold, start().on_threshold - step);
assert_eq!(state.values.off_threshold, start().off_threshold + step);
}
#[test]
fn a_flooded_rail_steps_the_other_way() {
let throttled = config().throttle_range.0;
let mut controller = BiasController::new(config(), start());
drive(&mut controller, 1e8, periods_to(throttled) + config().patience as usize);
let state = controller.state();
assert_eq!(state.authority, Authority::Flooded);
assert_eq!(state.slow_steps, 1);
let step = config().step;
assert_eq!(state.values.photoreceptor, start().photoreceptor - step);
assert_eq!(state.values.follower, start().follower - step);
assert_eq!(state.values.on_threshold, start().on_threshold + step);
assert_eq!(state.values.off_threshold, start().off_threshold - step);
}
#[test]
fn a_settled_period_resets_the_saturation_count() {
let open = config().throttle_range.1;
let mut controller = BiasController::new(config(), start());
drive(&mut controller, 1e4, periods_to(open)); assert_eq!(controller.state().slow_steps, 0);
period(&mut controller, 1.5e6); assert_eq!(controller.state().slow_steps, 0);
period(&mut controller, 1e4); assert_eq!(controller.state().slow_steps, 0, "count restarted, not resumed");
period(&mut controller, 1e4); assert_eq!(controller.state().slow_steps, 1);
}
#[test]
fn the_rails_do_not_share_a_count() {
let (throttled, open) = config().throttle_range;
let mut controller = BiasController::new(config(), start());
drive(&mut controller, 1e4, periods_to(open));
assert_eq!(controller.state().authority, Authority::Starved);
drive(&mut controller, 1e8, periods_to(throttled) + 4);
let mut controller2 = BiasController::new(config(), start());
drive(&mut controller2, 1e8, periods_to(throttled));
assert_eq!(controller2.state().slow_steps, 0);
}
#[test]
fn the_slow_loop_keeps_stepping_while_the_rail_holds() {
let open = config().throttle_range.1;
let mut controller = BiasController::new(config(), start());
drive(&mut controller, 1e4, periods_to(open) - 1 + 6);
assert_eq!(controller.state().slow_steps, 3);
assert_eq!(controller.state().values.photoreceptor, start().photoreceptor + 3 * config().step);
}
#[test]
fn biases_stop_at_the_ladder_limits() {
let limits = config().limits;
let (low, high) = limits.photoreceptor;
let mut controller = BiasController::new(
config(),
BiasValues {
refractory: 993,
photoreceptor: high - 2, follower: 1000,
on_threshold: 1000,
off_threshold: 1000,
},
);
drive(&mut controller, 1e4, 40);
let values = controller.state().values;
assert_eq!(values.photoreceptor, high, "clamped, never wrapped past the ladder");
assert!(values.on_threshold >= low);
}
#[test]
fn no_update_is_reported_when_nothing_moves() {
let open = config().throttle_range.1;
let mut controller = BiasController::new(config(), start());
drive(&mut controller, 1e4, periods_to(open) + config().patience as usize - 1);
assert_eq!(controller.state().slow_steps, 1);
assert_eq!(period(&mut controller, 1e4), None);
}
#[test]
fn a_stream_with_no_events_at_all_still_opens_the_sensor_up() {
let mut controller = BiasController::new(config(), start());
let update = controller
.observe(0, config().period)
.expect("an empty period is still a measurement");
assert_eq!(update.refractory, start().refractory + config().max_slew);
assert_eq!(controller.state().event_rate, 0.0);
drive(&mut controller, 0.0, periods_to(config().throttle_range.1) + 1);
assert_eq!(controller.state().authority, Authority::Starved);
assert!(controller.state().slow_steps >= 1, "the slow loop takes over");
}
#[test]
fn an_undercounted_period_never_increases_sensitivity() {
let mut controller = BiasController::new(config(), start());
controller.invalidate();
assert_eq!(period(&mut controller, 1e4), None);
let state = controller.state();
assert_eq!(state.authority, Authority::Tracking);
assert_eq!(state.slow_steps, 0);
assert_eq!(state.values, start());
}
#[test]
fn an_undercounted_period_still_throttles() {
let mut controller = BiasController::new(config(), start());
controller.invalidate();
let update = period(&mut controller, 1e8).expect("throttling is still safe");
assert_eq!(update.refractory, start().refractory - config().max_slew);
}
#[test]
fn invalidation_only_applies_to_the_period_in_flight() {
let mut controller = BiasController::new(config(), start());
controller.invalidate();
period(&mut controller, 1e4);
assert_eq!(controller.state().values, start(), "the marked period was ignored");
let update = period(&mut controller, 1e4).expect("a clean period acts");
assert_eq!(update.refractory, start().refractory + config().max_slew);
}
fn cliff(refractory: u16) -> f64 {
if refractory >= 1070 {
8e6
} else {
6e3
}
}
fn against(controller: &mut BiasController, plant: fn(u16) -> f64, periods: usize) {
for _ in 0..periods {
let rate = plant(controller.state().values.refractory);
period(controller, rate);
}
}
#[test]
fn a_hunting_loop_still_reaches_the_slow_loop() {
let mut controller = BiasController::new(config(), start());
against(&mut controller, cliff, 12);
let state = controller.state();
assert_eq!(state.authority, Authority::Hunting);
assert!(state.slow_steps >= 1, "the slow loop must engage on an unreachable band");
assert!(state.values.photoreceptor < start().photoreceptor, "photoreceptor turned down");
assert!(state.values.on_threshold > start().on_threshold, "ON threshold raised");
assert!(state.values.off_threshold < start().off_threshold, "OFF threshold raised");
}
#[test]
fn a_hunt_that_averages_inside_the_band_is_left_alone() {
fn gentle_cliff(refractory: u16) -> f64 {
if refractory >= 1070 {
2.6e6
} else {
1e3
}
}
let mut controller = BiasController::new(config(), start());
against(&mut controller, gentle_cliff, 24);
let state = controller.state();
assert_eq!(state.authority, Authority::Hunting, "it is still hunting");
assert_eq!(state.slow_steps, 0, "but the average is fine, so nothing is moved");
assert_eq!(state.values.photoreceptor, start().photoreceptor);
assert_eq!(state.values.on_threshold, start().on_threshold);
}
#[test]
fn a_single_overshoot_is_not_mistaken_for_hunting() {
let mut controller = BiasController::new(config(), start());
period(&mut controller, 1e4); period(&mut controller, 1e8); assert_eq!(controller.state().authority, Authority::Hunting);
assert_eq!(controller.state().slow_steps, 0, "one reversal is not enough");
period(&mut controller, 1.5e6); assert_eq!(controller.state().authority, Authority::Tracking);
assert_eq!(controller.state().slow_steps, 0);
}
#[test]
fn slewing_between_reversals_does_not_erase_the_hunt() {
fn wide_cliff(refractory: u16) -> f64 {
if refractory >= 1070 {
8e6
} else {
6e3
}
}
let patient = BiasConfig { max_slew: 4, ..config() }; let mut controller = BiasController::new(patient, start());
against(&mut controller, wide_cliff, 60);
assert!(
controller.state().slow_steps >= 1,
"a hunt with multi-period excursions must still reach the slow loop"
);
}
#[test]
fn a_long_slew_across_the_band_is_not_mistaken_for_hunting() {
let mut controller = BiasController::new(config(), start());
for _ in 0..(periods_to(config().throttle_range.1) - 1) {
period(&mut controller, 1e4);
assert_eq!(controller.state().authority, Authority::Tracking);
}
assert_eq!(controller.state().slow_steps, 0);
}
#[test]
fn a_step_response_sensor_does_not_limit_cycle() {
let mut controller = BiasController::new(config(), start());
let mut visited = Vec::new();
for _ in 0..80 {
let rate = cliff(controller.state().values.refractory);
period(&mut controller, rate);
visited.push(controller.state().values.refractory);
}
let settled = &visited[20..];
let low = *settled.iter().min().expect("80 periods recorded");
let high = *settled.iter().max().expect("80 periods recorded");
let (throttled, open) = config().throttle_range;
assert!(
high - low <= 4 * config().max_slew,
"hunting {low}..{high} must stay local, not sweep the {throttled}..{open} travel"
);
}
}