Skip to main content

kernelvex/control/
feedforward.rs

1//! Feedforward controllers for motion control.
2//!
3//! This module provides feedforward controllers that compute motor output based on
4//! desired velocity and acceleration, without feedback. When combined with PID,
5//! feedforward provides proactive control that anticipates required motor output.
6//!
7//! # Overview
8//!
9//! Feedforward uses a physics-based model to predict the motor voltage needed to
10//! achieve a desired motion:
11//!
12//! ```text
13//! voltage = ks * sign(v) + kv * v + ka * a
14//! ```
15//!
16//! Where:
17//! - `ks`: Static friction compensation (voltage to overcome stiction)
18//! - `kv`: Velocity gain (voltage per unit velocity)
19//! - `ka`: Acceleration gain (voltage per unit acceleration)
20//!
21//! # Controllers
22//!
23//! - [`FeedForward`]: Standard feedforward for linear motion
24//! - [`ArmFeedForward`]: Extended feedforward with gravity compensation for arms
25//!
26//! # Example
27//!
28//! ```no_run
29//! use kernelvex::FeedForward;
30//!
31//! let mut ff = FeedForward::new();
32//! ff.set_ks(0.1);  // Static friction
33//! ff.set_kv(0.5);  // Velocity gain
34//! ff.set_ka(0.01); // Acceleration gain
35//!
36//! // Calculate voltage for 2 m/s velocity, 0.5 m/s^2 acceleration
37//! let voltage = ff.calculate(2.0, 0.5);
38//! ```
39
40use crate::QAngle;
41
42/// Feedforward controller for linear motion.
43///
44/// Computes motor voltage based on desired velocity and acceleration using:
45///
46/// ```text
47/// voltage = ks * sign(v) + kv * v + ka * a
48/// ```
49///
50/// # Gains
51///
52/// - `ks`: Static friction compensation. The minimum voltage needed to start
53///   moving from rest. Applied in the direction of motion.
54/// - `kv`: Velocity feedforward. Voltage required per unit velocity to maintain
55///   constant speed against friction and back-EMF.
56/// - `ka`: Acceleration feedforward. Voltage required per unit acceleration to
57///   overcome inertia.
58///
59/// # Example
60///
61/// ```no_run
62/// let ff = FeedForward::new()
63///     .set_gains(0.1, 0.5, 0.01);
64///
65/// let voltage = ff.calculate(target_velocity, target_acceleration);
66/// ```
67pub struct FeedForward {
68    /// Static friction compensation (voltage to overcome stiction).
69    ks: f64,
70    /// Velocity feedforward gain (voltage per unit velocity).
71    kv: f64,
72    /// Acceleration feedforward gain (voltage per unit acceleration).
73    ka: f64,
74}
75
76impl FeedForward {
77    /// Creates a new feedforward controller with zero gains.
78    ///
79    /// Use [`set_gains`](Self::set_gains) or individual setters to configure.
80    #[inline]
81    pub const fn new() -> FeedForward {
82        Self {
83            ks: 0.,
84            kv: 0.,
85            ka: 0.,
86        }
87    }
88
89    /// Returns the static friction gain.
90    pub const fn ks(&self) -> f64 {
91        self.ks
92    }
93
94    /// Returns the velocity feedforward gain.
95    pub const fn kv(&self) -> f64 {
96        self.kv
97    }
98
99    /// Returns the acceleration feedforward gain.
100    pub const fn ka(&self) -> f64 {
101        self.ka
102    }
103
104    /// Sets all feedforward gains at once.
105    ///
106    /// # Arguments
107    ///
108    /// * `ks` - Static friction compensation
109    /// * `kv` - Velocity feedforward gain
110    /// * `ka` - Acceleration feedforward gain
111    ///
112    /// # Returns
113    ///
114    /// A new `FeedForward` with the specified gains.
115    pub const fn set_gains(&mut self, ks: f64, kv: f64, ka: f64) -> Self {
116        Self { ks, kv, ka }
117    }
118
119    /// Sets the static friction gain.
120    pub const fn set_ks(&mut self, ks: f64) {
121        self.ks = ks
122    }
123
124    /// Sets the velocity feedforward gain.
125    pub const fn set_kv(&mut self, kv: f64) {
126        self.kv = kv
127    }
128
129    /// Sets the acceleration feedforward gain.
130    pub const fn set_ka(&mut self, ka: f64) {
131        self.ka = ka
132    }
133
134    /// Calculates the feedforward voltage for the given velocity and acceleration.
135    ///
136    /// # Arguments
137    ///
138    /// * `velocity` - Desired velocity (units depend on your system)
139    /// * `acceleration` - Desired acceleration (units depend on your system)
140    ///
141    /// # Returns
142    ///
143    /// The feedforward voltage output.
144    ///
145    /// # Formula
146    ///
147    /// ```text
148    /// voltage = ks * sign(velocity) + kv * velocity + ka * acceleration
149    /// ```
150    pub fn calculate(&self, velocity: f64, acceleration: f64) -> f64 {
151        self.ks * velocity.signum() + self.kv * velocity + self.ka * acceleration
152    }
153}
154
155/// Feedforward controller for arm mechanisms with gravity compensation.
156///
157/// Extends [`FeedForward`] with a gravity compensation term that accounts for
158/// the arm's position. This is essential for arms that must hold position
159/// against gravity.
160///
161/// # Formula
162///
163/// ```text
164/// voltage = ks * sign(v) + kv * v + ka * a + kg * g(angle)
165/// ```
166///
167/// Where `g(angle)` is a function that returns the gravity effect at the given
168/// angle (typically `cos(angle)` for a simple arm).
169///
170/// # Example
171///
172/// ```no_run
173/// use kernelvex::{ArmFeedForward, QAngle};
174///
175/// let ff = ArmFeedForward::new(0.1, 0.5, 0.01, 0.3);
176///
177/// // Calculate with gravity compensation using cosine
178/// let voltage = ff.calculate(
179///     QAngle::from_degrees(45.0),
180///     target_velocity,
181///     target_acceleration,
182///     |angle| angle.cos(),
183/// );
184/// ```
185pub struct ArmFeedForward {
186    /// Static friction compensation.
187    ks: f64,
188    /// Velocity feedforward gain.
189    kv: f64,
190    /// Acceleration feedforward gain.
191    ka: f64,
192    /// Gravity compensation gain.
193    kg: f64,
194}
195
196impl ArmFeedForward {
197    /// Creates a new arm feedforward controller with the given gains.
198    ///
199    /// # Arguments
200    ///
201    /// * `ks` - Static friction compensation
202    /// * `kv` - Velocity feedforward gain
203    /// * `ka` - Acceleration feedforward gain
204    /// * `kg` - Gravity compensation gain
205    #[inline]
206    pub const fn new(ks: f64, kv: f64, ka: f64, kg: f64) -> Self {
207        Self { ks, kv, ka, kg }
208    }
209
210    /// Returns the static friction gain.
211    #[inline]
212    pub const fn ks(&self) -> f64 {
213        self.ks
214    }
215
216    /// Returns the velocity feedforward gain.
217    #[inline]
218    pub const fn kv(&self) -> f64 {
219        self.kv
220    }
221
222    /// Returns the acceleration feedforward gain.
223    #[inline]
224    pub const fn ka(&self) -> f64 {
225        self.ka
226    }
227
228    /// Returns the gravity compensation gain.
229    #[inline]
230    pub const fn kg(&self) -> f64 {
231        self.kg
232    }
233
234    /// Sets all gains at once, returning a new instance.
235    #[inline]
236    pub const fn set_gains(self, ks: f64, kv: f64, ka: f64, kg: f64) -> Self {
237        Self { ks, kv, ka, kg }
238    }
239
240    /// Sets the static friction gain, returning a new instance.
241    #[inline]
242    pub const fn set_ks(self, ks: f64) -> Self {
243        Self { ks, ..self }
244    }
245
246    /// Sets the velocity gain, returning a new instance.
247    #[inline]
248    pub const fn set_kv(self, kv: f64) -> Self {
249        Self { kv, ..self }
250    }
251
252    /// Sets the acceleration gain, returning a new instance.
253    #[inline]
254    pub const fn set_ka(self, ka: f64) -> Self {
255        Self { ka, ..self }
256    }
257
258    /// Sets the gravity gain, returning a new instance.
259    #[inline]
260    pub const fn set_kg(self, kg: f64) -> Self {
261        Self { kg, ..self }
262    }
263
264    /// Calculates the feedforward voltage with gravity compensation.
265    ///
266    /// # Arguments
267    ///
268    /// * `angle` - Current arm angle
269    /// * `velocity` - Desired velocity
270    /// * `acceleration` - Desired acceleration
271    /// * `g` - Gravity function that takes angle and returns gravity effect
272    ///
273    /// # Returns
274    ///
275    /// The feedforward voltage output including gravity compensation.
276    ///
277    /// # Example
278    ///
279    /// ```no_run
280    /// // Using cosine for simple arm
281    /// let voltage = ff.calculate(angle, vel, acc, |a| a.cos());
282    ///
283    /// // Using custom gravity function
284    /// let voltage = ff.calculate(angle, vel, acc, |a| {
285    ///     // Custom gravity calculation for 4-bar linkage
286    ///     (a + offset).cos() * linkage_factor
287    /// });
288    /// ```
289    #[inline]
290    pub fn calculate(
291        &self,
292        angle: QAngle,
293        velocity: f64,
294        acceleration: f64,
295        g: impl Fn(QAngle) -> f64,
296    ) -> f64 {
297        self.ks * velocity.signum()
298            + self.kv * velocity
299            + self.ka * acceleration
300            + self.kg * g(angle)
301    }
302}