Skip to main content

LiveTuning

Struct LiveTuning 

Source
pub struct LiveTuning {
    pub joint_stiffness: [f64; 7],
    pub joint_damping: [f64; 7],
    pub cartesian_stiffness: f64,
    pub ik_damping: f64,
    pub ik_nullspace_gain: f64,
    pub velocity_feedforward_gain: f64,
    pub velocity_feedforward_cutoff: f64,
    pub budget: [f64; 3],
    pub rotation_budget: [f64; 3],
}
Expand description

The targets a running session will accept. These are targets, not the values in force: the loop keeps its own current values and moves them here under each field’s policy.

cartesian_stiffness is one number here and twelve in the law. The operator sets a single translational stiffness; whoever owns the preset derives the six stiffnesses and six dampings from it, which is what holds the damping ratio the preset was tuned for. Neither the panel nor this struct should know that rule, and it is not duplicated here.

Deliberately not #[non_exhaustive], unlike FieldBound and TuningUpdate: a consumer builds one of these — it is the session’s seed, and it is what an update is validated against. Closing the struct expression would leave the positional from_words as the only way in, which is the mis-indexing hazard this module’s tests exist to catch, promoted to the sole public constructor.

Fields§

§joint_stiffness: [f64; 7]

Kq, per joint, Nm/rad.

§joint_damping: [f64; 7]

Kqd, per joint, Nm s/rad.

§cartesian_stiffness: f64

The translational Cartesian stiffness the six-axis Kx and Kxd are derived from, N/m.

§ik_damping: f64

λ of the IK’s damped least squares.

§ik_nullspace_gain: f64

Gain, 1/s, of the IK’s nullspace pull toward the posture.

§velocity_feedforward_gain: f64

The weight on the goal velocity in the damping term, within [0, 1]; zero is the law with the feedforward off.

§velocity_feedforward_cutoff: f64

The low-pass cutoff on that goal velocity, Hz; the maximum is the filter off.

§budget: [f64; 3]

The translational budget as a norm: m/s, m/s^2, m/s^3.

§rotation_budget: [f64; 3]

The rotational budget as a norm: rad/s, rad/s^2, rad/s^3.

Implementations§

Source§

impl LiveTuning

Source

pub const BOUNDS: &'static [FieldBound]

How far each scalar may be moved, in the order of to_words.

Publish this; never copy the numbers, and publish joint_damping_floor with it. The joint damping ceilings are the reference joint-impedance stack’s, less the crate’s own velocity barrier gain, which is added on top of the law’s damping and is what the stability bound applies to, and lower still on the wrist; the budget ceilings sit just past where the robot’s own joint-velocity guard can fire.

Source

pub fn joint_damping_floor(stiffness: f64) -> f64

The least damping, Nm s/rad, a joint spring of stiffness Nm/rad is allowed to run with. apply_update raises joint_damping[i] to this, and a caller that publishes BOUNDS has to publish this rule beside it: it is the one limit in this file that a per-field row cannot express.

Why it exists: the pair is the only one where both halves are exposed and independent, and joint_damping’s own floor is zero, so without it one slider move reaches a spring of up to 1200 Nm/rad with no damping at all. The VELOCITY_BARRIER_GAIN the damping ceiling is derived against is no help here — it only engages beyond velocity_barrier_fraction of the joint’s velocity limit, so at working speed it contributes nothing, and what is left is an undamped spring behind a low-pass that adds lag rather than damping.

MIN_JOINT_DAMPING_RATIO is below every shipped preset’s own D / sqrt(K), so this forbids the corner and no configuration anyone runs today; it is not a tuning opinion and it is far enough under the ceiling never to fight it.

Source§

impl LiveTuning

Source

pub fn slew_gains(dt: f64) -> SlewGains

The SlewGains of a loop running at dt seconds a cycle. Build it once, at the loop’s construction, with the cycle time the loop runs at.

Source

pub fn slew_toward(&mut self, target: &LiveTuning, gains: &SlewGains) -> bool

Moves every word one cycle toward target under gains, and reports whether any of them moved. false means the values in force are the targets, bit for bit, so a caller can skip the work of putting them into effect until the next change arrives.

A gated word – the budget’s velocity and acceleration – is not filtered at all: its target is raised to a floor one cycle of the next order’s limit below where it is, so raising it is a step and lowering it a ramp. The rate is read off the values in force at the top of the cycle, so the order the words are walked in cannot change the answer.

The realtime half of the tuning path: no allocation, no lock, no fallible call, and Self::WORDS multiply-adds whatever the targets are. gains comes from slew_gains so that no exp runs here.

It walks the word array rather than the named fields, which is what makes it impossible for a field to be slewed under another field’s policy, or left out.

use franka::robot::target_control::{LiveTuning, TuningPolicy};
let mut current =
    LiveTuning::from_options(&impedance, options.limits, options.rotation_limits);
let target = LiveTuning {
    ik_damping: current.ik_damping * 2.0,
    ..current
};
let gains = LiveTuning::slew_gains(0.001);

// One cycle of a 0.3 s time constant is a third of a percent of the gap, not the gap.
assert!(current.slew_toward(&target, &gains));
assert!(current.ik_damping < impedance.ik.damping * 1.01);

// Converged, it reports that it has nothing left to do.
for _ in 0..20_000 {
    current.slew_toward(&target, &gains);
}
assert_eq!(current, target);
assert!(!current.slew_toward(&target, &gains));
Source§

impl LiveTuning

Source

pub const WORDS: usize = 25

The number of scalars, and the length of both to_words and BOUNDS.

Source

pub fn to_words(&self) -> [f64; 25]

The scalars in the order of BOUNDS.

Source

pub fn from_words(words: &[f64; 25]) -> LiveTuning

The inverse of to_words.

Source

pub fn from_options( impedance: &ImpedanceOptions, budget: OtgLimits, rotation_budget: OtgLimits, ) -> LiveTuning

A session’s seed: the values impedance and the two budgets are already running with, which is what the loop slews from, so that the first change an operator makes starts where the arm is and not at a default.

The budgets are the arguments’; of what impedance supplies, two words are not one of its fields read straight off.

cartesian_stiffness is the first axis of ImpedanceGains::cartesian_stiffness, because that is the number scaled_cartesian scales, and gains derives all twelve Cartesian words back from it. A gains set that is not a scaled CARTESIAN therefore loses whatever else it changed the moment anything is retuned: seeded from DROID, the first retune moves the translational damping from DROID’s 37 to the preset’s 50 and 90. That is the parity preset’s price for one slider, and the reason gains is the only mapping.

velocity_feedforward_gain is zero when velocity_feedforward is off, which is the same law: the weighted goal velocity is all the switch gates. Carrying the switch in the gain is what lets it be crossed continuously rather than thrown.

Source

pub fn budgets(&self) -> (OtgLimits, OtgLimits)

The two budgets as OtgLimits, which is the form a generator takes them in: the other side of from_options’s reading of them, and the only mapping, so that nobody has to remember which of the three words is which.

Still norms, as the fields are. Whoever hands them to a generator owes them the per-axis division the OTG’s first rule asks for.

Source

pub fn gains(&self) -> ImpedanceGains

The law’s gains these targets stand for: the joint springs as they are, and the six Cartesian stiffnesses and six dampings scaled_cartesian derives from the one translational stiffness, which is where that rule lives and the only place it may.

The other side of from_options’s reading of the gains, and the mapping the loop applies every cycle it moves.

Source

pub fn apply_update( &mut self, update: &TuningUpdate, ) -> FrankaResult<Vec<&'static FieldBound>>

Applies every field update carries and returns the bounds that had to clamp one, leaving self at the accepted, clamped values. Runs on the arm thread: it allocates, and must not be called from the loop.

All or nothing: the update is built on a scratch copy, and if any value it carries is rejected — not finite, or zero where zero means something else (reject_non_positive) — self is left bit for bit as it was and nothing is clamped.

A field the update does not carry keeps its current value and is not re-checked, so a partial update is a real partial update and cannot quietly retune a field nobody moved. A value seeded outside BOUNDS therefore survives until someone moves that field; whoever builds the seed has to check it against BOUNDS first, and the way to do that is this method with an update carrying every field, never a second copy of the table.

The pair rule is the one thing that reaches past the carried fields. An update carrying joint_stiffness raises any of the seven joint_damping values that sit under joint_damping_floor of the spring they damp — the pair, not either half, is what has to stay damped. A damping the update does not carry is only ever raised: its ceiling is not applied, because lowering a field nobody moved is the thing this method exists not to do. The floor is read off the stiffness BOUNDS allows rather than the raw seeded one, so a seed the table would never have accepted cannot push the floor above the ceiling.

Clamping is deliberate rather than a refusal, and the clamped bounds come back so that a caller can show the value it will actually get; that echo is also what would expose a caller whose own idea of the limits has drifted from BOUNDS. Read the value out of self, not out of the bound: where the pair rule bit, the value in force is the floor and FieldBound::min is still zero.

§Errors

FrankaError::InvalidArgument naming the field, with self unchanged.

Trait Implementations§

Source§

impl Clone for LiveTuning

Source§

fn clone(&self) -> LiveTuning

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Copy for LiveTuning

Source§

impl Debug for LiveTuning

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl PartialEq for LiveTuning

Source§

fn eq(&self, other: &LiveTuning) -> bool

Tests for self and other values to be equal, and is used by ==.
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Tests for !=. The default implementation is almost always sufficient, and should not be overridden without very good reason.
Source§

impl StructuralPartialEq for LiveTuning

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> Scalar for T
where T: 'static + Clone + PartialEq + Debug,

§

impl<SS, SP> SupersetOf<SS> for SP
where SS: SubsetOf<SP>,

§

fn to_subset(&self) -> Option<SS>

The inverse inclusion map: attempts to construct self from the equivalent element of its superset. Read more
§

fn is_in_subset(&self) -> bool

Checks if self is actually part of its subset T (and can be converted to it).
§

fn to_subset_unchecked(&self) -> SS

Use with care! Same as self.to_subset but without any property checks. Always succeeds.
§

fn from_subset(element: &SS) -> SP

The inclusion map: converts self to the equivalent element of its superset.
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.