mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-20 00:51:42 +00:00
[wpimath] Move math functionality into new wpimath library (#2629)
The wpimath library is a new library designed to separate the reusable math functionality from the common utility library (wpiutil) and the hardware-dependent library (wpilibc/j). Package names / include file names were NOT changed to minimize breakage. In a future year it would be good to revamp these for a more uniform user experience and to reduce the risk of accidental naming conflicts. While theoretically all of this functionality could be placed into wpiutil, several pieces of this library (e.g. DARE) are very time-consuming to compile, so it's nice to avoid this expense for users who only want cscore or ntcore. It also allows for easy future separation of build tasks vs number of workers on memory-constrained machines. This moves the following functionality from wpiutil into wpimath: - Eigen - ejml - Drake - DARE - wpiutil.math package (Matrix etc) - units And the following functionality from wpilibc/j into wpimath: - Geometry - Kinematics - Spline - Trajectory - LinearFilter - MedianFilter - Feed-forward controllers
This commit is contained in:
25
wpimath/src/main/java/edu/wpi/first/math/MathShared.java
Normal file
25
wpimath/src/main/java/edu/wpi/first/math/MathShared.java
Normal file
@@ -0,0 +1,25 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.math;
|
||||
|
||||
public interface MathShared {
|
||||
/**
|
||||
* Report an error.
|
||||
*
|
||||
* @param error the error to set
|
||||
*/
|
||||
void reportError(String error, StackTraceElement[] stackTrace);
|
||||
|
||||
/**
|
||||
* Report usage.
|
||||
*
|
||||
* @param id the usage id
|
||||
* @param count the usage count
|
||||
*/
|
||||
void reportUsage(MathUsageId id, int count);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.math;
|
||||
|
||||
public final class MathSharedStore {
|
||||
private static MathShared mathShared;
|
||||
|
||||
private MathSharedStore() {
|
||||
}
|
||||
|
||||
/**
|
||||
* get the MathShared object.
|
||||
*/
|
||||
public static synchronized MathShared getMathShared() {
|
||||
if (mathShared == null) {
|
||||
mathShared = new MathShared() {
|
||||
@Override
|
||||
public void reportError(String error, StackTraceElement[] stackTrace) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportUsage(MathUsageId id, int count) {
|
||||
}
|
||||
};
|
||||
}
|
||||
return mathShared;
|
||||
}
|
||||
|
||||
/**
|
||||
* set the MathShared object.
|
||||
*/
|
||||
public static synchronized void setMathShared(MathShared shared) {
|
||||
mathShared = shared;
|
||||
}
|
||||
|
||||
/**
|
||||
* Report an error.
|
||||
*
|
||||
* @param error the error to set
|
||||
*/
|
||||
public static void reportError(String error, StackTraceElement[] stackTrace) {
|
||||
getMathShared().reportError(error, stackTrace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Report usage.
|
||||
*
|
||||
* @param id the usage id
|
||||
* @param count the usage count
|
||||
*/
|
||||
public static void reportUsage(MathUsageId id, int count) {
|
||||
getMathShared().reportUsage(id, count);
|
||||
}
|
||||
}
|
||||
19
wpimath/src/main/java/edu/wpi/first/math/MathUsageId.java
Normal file
19
wpimath/src/main/java/edu/wpi/first/math/MathUsageId.java
Normal file
@@ -0,0 +1,19 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.math;
|
||||
|
||||
public enum MathUsageId {
|
||||
kKinematics_DifferentialDrive,
|
||||
kKinematics_MecanumDrive,
|
||||
kKinematics_SwerveDrive,
|
||||
kTrajectory_TrapezoidProfile,
|
||||
kFilter_Linear,
|
||||
kOdometry_DifferentialDrive,
|
||||
kOdometry_SwerveDrive,
|
||||
kOdometry_MecanumDrive
|
||||
}
|
||||
171
wpimath/src/main/java/edu/wpi/first/wpilibj/LinearFilter.java
Normal file
171
wpimath/src/main/java/edu/wpi/first/wpilibj/LinearFilter.java
Normal file
@@ -0,0 +1,171 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2015-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import edu.wpi.first.math.MathSharedStore;
|
||||
import edu.wpi.first.math.MathUsageId;
|
||||
import edu.wpi.first.wpiutil.CircularBuffer;
|
||||
|
||||
/**
|
||||
* This class implements a linear, digital filter. All types of FIR and IIR filters are supported.
|
||||
* Static factory methods are provided to create commonly used types of filters.
|
||||
*
|
||||
* <p>Filters are of the form: y[n] = (b0*x[n] + b1*x[n-1] + ... + bP*x[n-P]) - (a0*y[n-1] +
|
||||
* a2*y[n-2] + ... + aQ*y[n-Q])
|
||||
*
|
||||
* <p>Where: y[n] is the output at time "n" x[n] is the input at time "n" y[n-1] is the output from
|
||||
* the LAST time step ("n-1") x[n-1] is the input from the LAST time step ("n-1") b0...bP are the
|
||||
* "feedforward" (FIR) gains a0...aQ are the "feedback" (IIR) gains IMPORTANT! Note the "-" sign in
|
||||
* front of the feedback term! This is a common convention in signal processing.
|
||||
*
|
||||
* <p>What can linear filters do? Basically, they can filter, or diminish, the effects of
|
||||
* undesirable input frequencies. High frequencies, or rapid changes, can be indicative of sensor
|
||||
* noise or be otherwise undesirable. A "low pass" filter smooths out the signal, reducing the
|
||||
* impact of these high frequency components. Likewise, a "high pass" filter gets rid of
|
||||
* slow-moving signal components, letting you detect large changes more easily.
|
||||
*
|
||||
* <p>Example FRC applications of filters: - Getting rid of noise from an analog sensor input (note:
|
||||
* the roboRIO's FPGA can do this faster in hardware) - Smoothing out joystick input to prevent the
|
||||
* wheels from slipping or the robot from tipping - Smoothing motor commands so that unnecessary
|
||||
* strain isn't put on electrical or mechanical components - If you use clever gains, you can make a
|
||||
* PID controller out of this class!
|
||||
*
|
||||
* <p>For more on filters, we highly recommend the following articles:<br>
|
||||
* https://en.wikipedia.org/wiki/Linear_filter<br>
|
||||
* https://en.wikipedia.org/wiki/Iir_filter<br>
|
||||
* https://en.wikipedia.org/wiki/Fir_filter<br>
|
||||
*
|
||||
* <p>Note 1: calculate() should be called by the user on a known, regular period. You can use a
|
||||
* Notifier for this or do it "inline" with code in a periodic function.
|
||||
*
|
||||
* <p>Note 2: For ALL filters, gains are necessarily a function of frequency. If you make a filter
|
||||
* that works well for you at, say, 100Hz, you will most definitely need to adjust the gains if you
|
||||
* then want to run it at 200Hz! Combining this with Note 1 - the impetus is on YOU as a developer
|
||||
* to make sure calculate() gets called at the desired, constant frequency!
|
||||
*/
|
||||
public class LinearFilter {
|
||||
private final CircularBuffer m_inputs;
|
||||
private final CircularBuffer m_outputs;
|
||||
private final double[] m_inputGains;
|
||||
private final double[] m_outputGains;
|
||||
|
||||
private static int instances;
|
||||
|
||||
/**
|
||||
* Create a linear FIR or IIR filter.
|
||||
*
|
||||
* @param ffGains The "feed forward" or FIR gains.
|
||||
* @param fbGains The "feed back" or IIR gains.
|
||||
*/
|
||||
public LinearFilter(double[] ffGains, double[] fbGains) {
|
||||
m_inputs = new CircularBuffer(ffGains.length);
|
||||
m_outputs = new CircularBuffer(fbGains.length);
|
||||
m_inputGains = Arrays.copyOf(ffGains, ffGains.length);
|
||||
m_outputGains = Arrays.copyOf(fbGains, fbGains.length);
|
||||
|
||||
instances++;
|
||||
MathSharedStore.reportUsage(MathUsageId.kFilter_Linear, instances);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a one-pole IIR low-pass filter of the form: y[n] = (1-gain)*x[n] + gain*y[n-1] where
|
||||
* gain = e^(-dt / T), T is the time constant in seconds.
|
||||
*
|
||||
* <p>This filter is stable for time constants greater than zero.
|
||||
*
|
||||
* @param timeConstant The discrete-time time constant in seconds.
|
||||
* @param period The period in seconds between samples taken by the user.
|
||||
*/
|
||||
public static LinearFilter singlePoleIIR(double timeConstant,
|
||||
double period) {
|
||||
double gain = Math.exp(-period / timeConstant);
|
||||
double[] ffGains = {1.0 - gain};
|
||||
double[] fbGains = {-gain};
|
||||
|
||||
return new LinearFilter(ffGains, fbGains);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a first-order high-pass filter of the form: y[n] = gain*x[n] + (-gain)*x[n-1] +
|
||||
* gain*y[n-1] where gain = e^(-dt / T), T is the time constant in seconds.
|
||||
*
|
||||
* <p>This filter is stable for time constants greater than zero.
|
||||
*
|
||||
* @param timeConstant The discrete-time time constant in seconds.
|
||||
* @param period The period in seconds between samples taken by the user.
|
||||
*/
|
||||
public static LinearFilter highPass(double timeConstant,
|
||||
double period) {
|
||||
double gain = Math.exp(-period / timeConstant);
|
||||
double[] ffGains = {gain, -gain};
|
||||
double[] fbGains = {-gain};
|
||||
|
||||
return new LinearFilter(ffGains, fbGains);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a K-tap FIR moving average filter of the form: y[n] = 1/k * (x[k] + x[k-1] + ... +
|
||||
* x[0]).
|
||||
*
|
||||
* <p>This filter is always stable.
|
||||
*
|
||||
* @param taps The number of samples to average over. Higher = smoother but slower.
|
||||
* @throws IllegalArgumentException if number of taps is less than 1.
|
||||
*/
|
||||
public static LinearFilter movingAverage(int taps) {
|
||||
if (taps <= 0) {
|
||||
throw new IllegalArgumentException("Number of taps was not at least 1");
|
||||
}
|
||||
|
||||
double[] ffGains = new double[taps];
|
||||
for (int i = 0; i < ffGains.length; i++) {
|
||||
ffGains[i] = 1.0 / taps;
|
||||
}
|
||||
|
||||
double[] fbGains = new double[0];
|
||||
|
||||
return new LinearFilter(ffGains, fbGains);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the filter state.
|
||||
*/
|
||||
public void reset() {
|
||||
m_inputs.clear();
|
||||
m_outputs.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the next value of the filter.
|
||||
*
|
||||
* @param input Current input value.
|
||||
*
|
||||
* @return The filtered value at this step
|
||||
*/
|
||||
public double calculate(double input) {
|
||||
double retVal = 0.0;
|
||||
|
||||
// Rotate the inputs
|
||||
m_inputs.addFirst(input);
|
||||
|
||||
// Calculate the new value
|
||||
for (int i = 0; i < m_inputGains.length; i++) {
|
||||
retVal += m_inputs.get(i) * m_inputGains[i];
|
||||
}
|
||||
for (int i = 0; i < m_outputGains.length; i++) {
|
||||
retVal -= m_outputs.get(i) * m_outputGains[i];
|
||||
}
|
||||
|
||||
// Rotate the outputs
|
||||
m_outputs.addFirst(retVal);
|
||||
|
||||
return retVal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import edu.wpi.first.wpiutil.CircularBuffer;
|
||||
|
||||
/**
|
||||
* A class that implements a moving-window median filter. Useful for reducing measurement noise,
|
||||
* especially with processes that generate occasional, extreme outliers (such as values from
|
||||
* vision processing, LIDAR, or ultrasonic sensors).
|
||||
*/
|
||||
public class MedianFilter {
|
||||
private final CircularBuffer m_valueBuffer;
|
||||
private final List<Double> m_orderedValues;
|
||||
private final int m_size;
|
||||
|
||||
/**
|
||||
* Creates a new MedianFilter.
|
||||
*
|
||||
* @param size The number of samples in the moving window.
|
||||
*/
|
||||
public MedianFilter(int size) {
|
||||
// Circular buffer of values currently in the window, ordered by time
|
||||
m_valueBuffer = new CircularBuffer(size);
|
||||
// List of values currently in the window, ordered by value
|
||||
m_orderedValues = new ArrayList<>(size);
|
||||
// Size of rolling window
|
||||
m_size = size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the moving-window median for the next value of the input stream.
|
||||
*
|
||||
* @param next The next input value.
|
||||
* @return The median of the moving window, updated to include the next value.
|
||||
*/
|
||||
public double calculate(double next) {
|
||||
// Find insertion point for next value
|
||||
int index = Collections.binarySearch(m_orderedValues, next);
|
||||
|
||||
// Deal with binarySearch behavior for element not found
|
||||
if (index < 0) {
|
||||
index = Math.abs(index + 1);
|
||||
}
|
||||
|
||||
// Place value at proper insertion point
|
||||
m_orderedValues.add(index, next);
|
||||
|
||||
int curSize = m_orderedValues.size();
|
||||
|
||||
// If buffer is at max size, pop element off of end of circular buffer
|
||||
// and remove from ordered list
|
||||
if (curSize > m_size) {
|
||||
m_orderedValues.remove(m_valueBuffer.removeLast());
|
||||
curSize = curSize - 1;
|
||||
}
|
||||
|
||||
// Add next value to circular buffer
|
||||
m_valueBuffer.addFirst(next);
|
||||
|
||||
if (curSize % 2 == 1) {
|
||||
// If size is odd, return middle element of sorted list
|
||||
return m_orderedValues.get(curSize / 2);
|
||||
} else {
|
||||
// If size is even, return average of middle elements
|
||||
return (m_orderedValues.get(curSize / 2 - 1) + m_orderedValues.get(curSize / 2)) / 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the filter, clearing the window of all elements.
|
||||
*/
|
||||
public void reset() {
|
||||
m_orderedValues.clear();
|
||||
m_valueBuffer.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.controller;
|
||||
|
||||
/**
|
||||
* A helper class that computes feedforward outputs for a simple arm (modeled as a motor
|
||||
* acting against the force of gravity on a beam suspended at an angle).
|
||||
*/
|
||||
@SuppressWarnings("MemberName")
|
||||
public class ArmFeedforward {
|
||||
public final double ks;
|
||||
public final double kcos;
|
||||
public final double kv;
|
||||
public final double ka;
|
||||
|
||||
/**
|
||||
* Creates a new ArmFeedforward with the specified gains. Units of the gain values
|
||||
* will dictate units of the computed feedforward.
|
||||
*
|
||||
* @param ks The static gain.
|
||||
* @param kcos The gravity gain.
|
||||
* @param kv The velocity gain.
|
||||
* @param ka The acceleration gain.
|
||||
*/
|
||||
public ArmFeedforward(double ks, double kcos, double kv, double ka) {
|
||||
this.ks = ks;
|
||||
this.kcos = kcos;
|
||||
this.kv = kv;
|
||||
this.ka = ka;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ArmFeedforward with the specified gains. Acceleration gain is
|
||||
* defaulted to zero. Units of the gain values will dictate units of the computed feedforward.
|
||||
*
|
||||
* @param ks The static gain.
|
||||
* @param kcos The gravity gain.
|
||||
* @param kv The velocity gain.
|
||||
*/
|
||||
public ArmFeedforward(double ks, double kcos, double kv) {
|
||||
this(ks, kcos, kv, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the feedforward from the gains and setpoints.
|
||||
*
|
||||
* @param positionRadians The position (angle) setpoint.
|
||||
* @param velocityRadPerSec The velocity setpoint.
|
||||
* @param accelRadPerSecSquared The acceleration setpoint.
|
||||
* @return The computed feedforward.
|
||||
*/
|
||||
public double calculate(double positionRadians, double velocityRadPerSec,
|
||||
double accelRadPerSecSquared) {
|
||||
return ks * Math.signum(velocityRadPerSec) + kcos * Math.cos(positionRadians)
|
||||
+ kv * velocityRadPerSec
|
||||
+ ka * accelRadPerSecSquared;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the feedforward from the gains and velocity setpoint (acceleration is assumed to
|
||||
* be zero).
|
||||
*
|
||||
* @param positionRadians The position (angle) setpoint.
|
||||
* @param velocity The velocity setpoint.
|
||||
* @return The computed feedforward.
|
||||
*/
|
||||
public double calculate(double positionRadians, double velocity) {
|
||||
return calculate(positionRadians, velocity, 0);
|
||||
}
|
||||
|
||||
// Rearranging the main equation from the calculate() method yields the
|
||||
// formulas for the methods below:
|
||||
|
||||
/**
|
||||
* Calculates the maximum achievable velocity given a maximum voltage supply,
|
||||
* a position, and an acceleration. Useful for ensuring that velocity and
|
||||
* acceleration constraints for a trapezoidal profile are simultaneously
|
||||
* achievable - enter the acceleration constraint, and this will give you
|
||||
* a simultaneously-achievable velocity constraint.
|
||||
*
|
||||
* @param maxVoltage The maximum voltage that can be supplied to the arm.
|
||||
* @param angle The angle of the arm.
|
||||
* @param acceleration The acceleration of the arm.
|
||||
* @return The maximum possible velocity at the given acceleration and angle.
|
||||
*/
|
||||
public double maxAchievableVelocity(double maxVoltage, double angle, double acceleration) {
|
||||
// Assume max velocity is positive
|
||||
return (maxVoltage - ks - Math.cos(angle) * kcos - acceleration * ka) / kv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the minimum achievable velocity given a maximum voltage supply,
|
||||
* a position, and an acceleration. Useful for ensuring that velocity and
|
||||
* acceleration constraints for a trapezoidal profile are simultaneously
|
||||
* achievable - enter the acceleration constraint, and this will give you
|
||||
* a simultaneously-achievable velocity constraint.
|
||||
*
|
||||
* @param maxVoltage The maximum voltage that can be supplied to the arm.
|
||||
* @param angle The angle of the arm.
|
||||
* @param acceleration The acceleration of the arm.
|
||||
* @return The minimum possible velocity at the given acceleration and angle.
|
||||
*/
|
||||
public double minAchievableVelocity(double maxVoltage, double angle, double acceleration) {
|
||||
// Assume min velocity is negative, ks flips sign
|
||||
return (-maxVoltage + ks - Math.cos(angle) * kcos - acceleration * ka) / kv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the maximum achievable acceleration given a maximum voltage
|
||||
* supply, a position, and a velocity. Useful for ensuring that velocity and
|
||||
* acceleration constraints for a trapezoidal profile are simultaneously
|
||||
* achievable - enter the velocity constraint, and this will give you
|
||||
* a simultaneously-achievable acceleration constraint.
|
||||
*
|
||||
* @param maxVoltage The maximum voltage that can be supplied to the arm.
|
||||
* @param angle The angle of the arm.
|
||||
* @param velocity The velocity of the arm.
|
||||
* @return The maximum possible acceleration at the given velocity.
|
||||
*/
|
||||
public double maxAchievableAcceleration(double maxVoltage, double angle, double velocity) {
|
||||
return (maxVoltage - ks * Math.signum(velocity) - Math.cos(angle) * kcos - velocity * kv) / ka;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the minimum achievable acceleration given a maximum voltage
|
||||
* supply, a position, and a velocity. Useful for ensuring that velocity and
|
||||
* acceleration constraints for a trapezoidal profile are simultaneously
|
||||
* achievable - enter the velocity constraint, and this will give you
|
||||
* a simultaneously-achievable acceleration constraint.
|
||||
*
|
||||
* @param maxVoltage The maximum voltage that can be supplied to the arm.
|
||||
* @param angle The angle of the arm.
|
||||
* @param velocity The velocity of the arm.
|
||||
* @return The minimum possible acceleration at the given velocity.
|
||||
*/
|
||||
public double minAchievableAcceleration(double maxVoltage, double angle, double velocity) {
|
||||
return maxAchievableAcceleration(-maxVoltage, angle, velocity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.controller;
|
||||
|
||||
/**
|
||||
* A helper class that computes feedforward outputs for a simple elevator (modeled as a motor
|
||||
* acting against the force of gravity).
|
||||
*/
|
||||
@SuppressWarnings("MemberName")
|
||||
public class ElevatorFeedforward {
|
||||
public final double ks;
|
||||
public final double kg;
|
||||
public final double kv;
|
||||
public final double ka;
|
||||
|
||||
/**
|
||||
* Creates a new ElevatorFeedforward with the specified gains. Units of the gain values
|
||||
* will dictate units of the computed feedforward.
|
||||
*
|
||||
* @param ks The static gain.
|
||||
* @param kg The gravity gain.
|
||||
* @param kv The velocity gain.
|
||||
* @param ka The acceleration gain.
|
||||
*/
|
||||
public ElevatorFeedforward(double ks, double kg, double kv, double ka) {
|
||||
this.ks = ks;
|
||||
this.kg = kg;
|
||||
this.kv = kv;
|
||||
this.ka = ka;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ElevatorFeedforward with the specified gains. Acceleration gain is
|
||||
* defaulted to zero. Units of the gain values will dictate units of the computed feedforward.
|
||||
*
|
||||
* @param ks The static gain.
|
||||
* @param kg The gravity gain.
|
||||
* @param kv The velocity gain.
|
||||
*/
|
||||
public ElevatorFeedforward(double ks, double kg, double kv) {
|
||||
this(ks, kg, kv, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the feedforward from the gains and setpoints.
|
||||
*
|
||||
* @param velocity The velocity setpoint.
|
||||
* @param acceleration The acceleration setpoint.
|
||||
* @return The computed feedforward.
|
||||
*/
|
||||
public double calculate(double velocity, double acceleration) {
|
||||
return ks * Math.signum(velocity) + kg + kv * velocity + ka * acceleration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the feedforward from the gains and velocity setpoint (acceleration is assumed to
|
||||
* be zero).
|
||||
*
|
||||
* @param velocity The velocity setpoint.
|
||||
* @return The computed feedforward.
|
||||
*/
|
||||
public double calculate(double velocity) {
|
||||
return calculate(velocity, 0);
|
||||
}
|
||||
|
||||
// Rearranging the main equation from the calculate() method yields the
|
||||
// formulas for the methods below:
|
||||
|
||||
/**
|
||||
* Calculates the maximum achievable velocity given a maximum voltage supply
|
||||
* and an acceleration. Useful for ensuring that velocity and
|
||||
* acceleration constraints for a trapezoidal profile are simultaneously
|
||||
* achievable - enter the acceleration constraint, and this will give you
|
||||
* a simultaneously-achievable velocity constraint.
|
||||
*
|
||||
* @param maxVoltage The maximum voltage that can be supplied to the elevator.
|
||||
* @param acceleration The acceleration of the elevator.
|
||||
* @return The maximum possible velocity at the given acceleration.
|
||||
*/
|
||||
public double maxAchievableVelocity(double maxVoltage, double acceleration) {
|
||||
// Assume max velocity is positive
|
||||
return (maxVoltage - ks - kg - acceleration * ka) / kv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the minimum achievable velocity given a maximum voltage supply
|
||||
* and an acceleration. Useful for ensuring that velocity and
|
||||
* acceleration constraints for a trapezoidal profile are simultaneously
|
||||
* achievable - enter the acceleration constraint, and this will give you
|
||||
* a simultaneously-achievable velocity constraint.
|
||||
*
|
||||
* @param maxVoltage The maximum voltage that can be supplied to the elevator.
|
||||
* @param acceleration The acceleration of the elevator.
|
||||
* @return The minimum possible velocity at the given acceleration.
|
||||
*/
|
||||
public double minAchievableVelocity(double maxVoltage, double acceleration) {
|
||||
// Assume min velocity is negative, ks flips sign
|
||||
return (-maxVoltage + ks - kg - acceleration * ka) / kv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the maximum achievable acceleration given a maximum voltage
|
||||
* supply and a velocity. Useful for ensuring that velocity and
|
||||
* acceleration constraints for a trapezoidal profile are simultaneously
|
||||
* achievable - enter the velocity constraint, and this will give you
|
||||
* a simultaneously-achievable acceleration constraint.
|
||||
*
|
||||
* @param maxVoltage The maximum voltage that can be supplied to the elevator.
|
||||
* @param velocity The velocity of the elevator.
|
||||
* @return The maximum possible acceleration at the given velocity.
|
||||
*/
|
||||
public double maxAchievableAcceleration(double maxVoltage, double velocity) {
|
||||
return (maxVoltage - ks * Math.signum(velocity) - kg - velocity * kv) / ka;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the minimum achievable acceleration given a maximum voltage
|
||||
* supply and a velocity. Useful for ensuring that velocity and
|
||||
* acceleration constraints for a trapezoidal profile are simultaneously
|
||||
* achievable - enter the velocity constraint, and this will give you
|
||||
* a simultaneously-achievable acceleration constraint.
|
||||
*
|
||||
* @param maxVoltage The maximum voltage that can be supplied to the elevator.
|
||||
* @param velocity The velocity of the elevator.
|
||||
* @return The minimum possible acceleration at the given velocity.
|
||||
*/
|
||||
public double minAchievableAcceleration(double maxVoltage, double velocity) {
|
||||
return maxAchievableAcceleration(-maxVoltage, velocity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.controller;
|
||||
|
||||
/**
|
||||
* A helper class that computes feedforward outputs for a simple permanent-magnet DC motor.
|
||||
*/
|
||||
@SuppressWarnings("MemberName")
|
||||
public class SimpleMotorFeedforward {
|
||||
public final double ks;
|
||||
public final double kv;
|
||||
public final double ka;
|
||||
|
||||
/**
|
||||
* Creates a new SimpleMotorFeedforward with the specified gains. Units of the gain values
|
||||
* will dictate units of the computed feedforward.
|
||||
*
|
||||
* @param ks The static gain.
|
||||
* @param kv The velocity gain.
|
||||
* @param ka The acceleration gain.
|
||||
*/
|
||||
public SimpleMotorFeedforward(double ks, double kv, double ka) {
|
||||
this.ks = ks;
|
||||
this.kv = kv;
|
||||
this.ka = ka;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new SimpleMotorFeedforward with the specified gains. Acceleration gain is
|
||||
* defaulted to zero. Units of the gain values will dictate units of the computed feedforward.
|
||||
*
|
||||
* @param ks The static gain.
|
||||
* @param kv The velocity gain.
|
||||
*/
|
||||
public SimpleMotorFeedforward(double ks, double kv) {
|
||||
this(ks, kv, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the feedforward from the gains and setpoints.
|
||||
*
|
||||
* @param velocity The velocity setpoint.
|
||||
* @param acceleration The acceleration setpoint.
|
||||
* @return The computed feedforward.
|
||||
*/
|
||||
public double calculate(double velocity, double acceleration) {
|
||||
return ks * Math.signum(velocity) + kv * velocity + ka * acceleration;
|
||||
}
|
||||
|
||||
// Rearranging the main equation from the calculate() method yields the
|
||||
// formulas for the methods below:
|
||||
|
||||
/**
|
||||
* Calculates the feedforward from the gains and velocity setpoint (acceleration is assumed to
|
||||
* be zero).
|
||||
*
|
||||
* @param velocity The velocity setpoint.
|
||||
* @return The computed feedforward.
|
||||
*/
|
||||
public double calculate(double velocity) {
|
||||
return calculate(velocity, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the maximum achievable velocity given a maximum voltage supply
|
||||
* and an acceleration. Useful for ensuring that velocity and
|
||||
* acceleration constraints for a trapezoidal profile are simultaneously
|
||||
* achievable - enter the acceleration constraint, and this will give you
|
||||
* a simultaneously-achievable velocity constraint.
|
||||
*
|
||||
* @param maxVoltage The maximum voltage that can be supplied to the motor.
|
||||
* @param acceleration The acceleration of the motor.
|
||||
* @return The maximum possible velocity at the given acceleration.
|
||||
*/
|
||||
public double maxAchievableVelocity(double maxVoltage, double acceleration) {
|
||||
// Assume max velocity is positive
|
||||
return (maxVoltage - ks - acceleration * ka) / kv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the minimum achievable velocity given a maximum voltage supply
|
||||
* and an acceleration. Useful for ensuring that velocity and
|
||||
* acceleration constraints for a trapezoidal profile are simultaneously
|
||||
* achievable - enter the acceleration constraint, and this will give you
|
||||
* a simultaneously-achievable velocity constraint.
|
||||
*
|
||||
* @param maxVoltage The maximum voltage that can be supplied to the motor.
|
||||
* @param acceleration The acceleration of the motor.
|
||||
* @return The minimum possible velocity at the given acceleration.
|
||||
*/
|
||||
public double minAchievableVelocity(double maxVoltage, double acceleration) {
|
||||
// Assume min velocity is negative, ks flips sign
|
||||
return (-maxVoltage + ks - acceleration * ka) / kv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the maximum achievable acceleration given a maximum voltage
|
||||
* supply and a velocity. Useful for ensuring that velocity and
|
||||
* acceleration constraints for a trapezoidal profile are simultaneously
|
||||
* achievable - enter the velocity constraint, and this will give you
|
||||
* a simultaneously-achievable acceleration constraint.
|
||||
*
|
||||
* @param maxVoltage The maximum voltage that can be supplied to the motor.
|
||||
* @param velocity The velocity of the motor.
|
||||
* @return The maximum possible acceleration at the given velocity.
|
||||
*/
|
||||
public double maxAchievableAcceleration(double maxVoltage, double velocity) {
|
||||
return (maxVoltage - ks * Math.signum(velocity) - velocity * kv) / ka;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the maximum achievable acceleration given a maximum voltage
|
||||
* supply and a velocity. Useful for ensuring that velocity and
|
||||
* acceleration constraints for a trapezoidal profile are simultaneously
|
||||
* achievable - enter the velocity constraint, and this will give you
|
||||
* a simultaneously-achievable acceleration constraint.
|
||||
*
|
||||
* @param maxVoltage The maximum voltage that can be supplied to the motor.
|
||||
* @param velocity The velocity of the motor.
|
||||
* @return The minimum possible acceleration at the given velocity.
|
||||
*/
|
||||
public double minAchievableAcceleration(double maxVoltage, double velocity) {
|
||||
return maxAchievableAcceleration(-maxVoltage, velocity);
|
||||
}
|
||||
}
|
||||
252
wpimath/src/main/java/edu/wpi/first/wpilibj/geometry/Pose2d.java
Normal file
252
wpimath/src/main/java/edu/wpi/first/wpilibj/geometry/Pose2d.java
Normal file
@@ -0,0 +1,252 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.geometry;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Represents a 2d pose containing translational and rotational elements.
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
public class Pose2d {
|
||||
private final Translation2d m_translation;
|
||||
private final Rotation2d m_rotation;
|
||||
|
||||
/**
|
||||
* Constructs a pose at the origin facing toward the positive X axis.
|
||||
* (Translation2d{0, 0} and Rotation{0})
|
||||
*/
|
||||
public Pose2d() {
|
||||
m_translation = new Translation2d();
|
||||
m_rotation = new Rotation2d();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a pose with the specified translation and rotation.
|
||||
*
|
||||
* @param translation The translational component of the pose.
|
||||
* @param rotation The rotational component of the pose.
|
||||
*/
|
||||
@JsonCreator
|
||||
public Pose2d(@JsonProperty(required = true, value = "translation") Translation2d translation,
|
||||
@JsonProperty(required = true, value = "rotation") Rotation2d rotation) {
|
||||
m_translation = translation;
|
||||
m_rotation = rotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience constructors that takes in x and y values directly instead of
|
||||
* having to construct a Translation2d.
|
||||
*
|
||||
* @param x The x component of the translational component of the pose.
|
||||
* @param y The y component of the translational component of the pose.
|
||||
* @param rotation The rotational component of the pose.
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
public Pose2d(double x, double y, Rotation2d rotation) {
|
||||
m_translation = new Translation2d(x, y);
|
||||
m_rotation = rotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the pose by the given transformation and returns the new
|
||||
* transformed pose.
|
||||
*
|
||||
* <p>The matrix multiplication is as follows
|
||||
* [x_new] [cos, -sin, 0][transform.x]
|
||||
* [y_new] += [sin, cos, 0][transform.y]
|
||||
* [t_new] [0, 0, 1][transform.t]
|
||||
*
|
||||
* @param other The transform to transform the pose by.
|
||||
* @return The transformed pose.
|
||||
*/
|
||||
public Pose2d plus(Transform2d other) {
|
||||
return transformBy(other);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Transform2d that maps the one pose to another.
|
||||
*
|
||||
* @param other The initial pose of the transformation.
|
||||
* @return The transform that maps the other pose to the current pose.
|
||||
*/
|
||||
public Transform2d minus(Pose2d other) {
|
||||
final var pose = this.relativeTo(other);
|
||||
return new Transform2d(pose.getTranslation(), pose.getRotation());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translation component of the transformation.
|
||||
*
|
||||
* @return The translational component of the pose.
|
||||
*/
|
||||
@JsonProperty
|
||||
public Translation2d getTranslation() {
|
||||
return m_translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the X component of the pose's translation.
|
||||
*
|
||||
* @return The x component of the pose's translation.
|
||||
*/
|
||||
public double getX() {
|
||||
return m_translation.getX();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Y component of the pose's translation.
|
||||
*
|
||||
* @return The y component of the pose's translation.
|
||||
*/
|
||||
public double getY() {
|
||||
return m_translation.getY();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the rotational component of the transformation.
|
||||
*
|
||||
* @return The rotational component of the pose.
|
||||
*/
|
||||
@JsonProperty
|
||||
public Rotation2d getRotation() {
|
||||
return m_rotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the pose by the given transformation and returns the new pose.
|
||||
* See + operator for the matrix multiplication performed.
|
||||
*
|
||||
* @param other The transform to transform the pose by.
|
||||
* @return The transformed pose.
|
||||
*/
|
||||
public Pose2d transformBy(Transform2d other) {
|
||||
return new Pose2d(m_translation.plus(other.getTranslation().rotateBy(m_rotation)),
|
||||
m_rotation.plus(other.getRotation()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the other pose relative to the current pose.
|
||||
*
|
||||
* <p>This function can often be used for trajectory tracking or pose
|
||||
* stabilization algorithms to get the error between the reference and the
|
||||
* current pose.
|
||||
*
|
||||
* @param other The pose that is the origin of the new coordinate frame that
|
||||
* the current pose will be converted into.
|
||||
* @return The current pose relative to the new origin pose.
|
||||
*/
|
||||
public Pose2d relativeTo(Pose2d other) {
|
||||
var transform = new Transform2d(other, this);
|
||||
return new Pose2d(transform.getTranslation(), transform.getRotation());
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a new Pose2d from a (constant curvature) velocity.
|
||||
*
|
||||
* <p>See <a href="https://file.tavsys.net/control/state-space-guide.pdf">
|
||||
* Controls Engineering in the FIRST Robotics Competition</a>
|
||||
* section on nonlinear pose estimation for derivation.
|
||||
*
|
||||
* <p>The twist is a change in pose in the robot's coordinate frame since the
|
||||
* previous pose update. When the user runs exp() on the previous known
|
||||
* field-relative pose with the argument being the twist, the user will
|
||||
* receive the new field-relative pose.
|
||||
*
|
||||
* <p>"Exp" represents the pose exponential, which is solving a differential
|
||||
* equation moving the pose forward in time.
|
||||
*
|
||||
* @param twist The change in pose in the robot's coordinate frame since the
|
||||
* previous pose update. For example, if a non-holonomic robot moves forward
|
||||
* 0.01 meters and changes angle by 0.5 degrees since the previous pose update,
|
||||
* the twist would be Twist2d{0.01, 0.0, toRadians(0.5)}
|
||||
* @return The new pose of the robot.
|
||||
*/
|
||||
@SuppressWarnings("LocalVariableName")
|
||||
public Pose2d exp(Twist2d twist) {
|
||||
double dx = twist.dx;
|
||||
double dy = twist.dy;
|
||||
double dtheta = twist.dtheta;
|
||||
|
||||
double sinTheta = Math.sin(dtheta);
|
||||
double cosTheta = Math.cos(dtheta);
|
||||
|
||||
double s;
|
||||
double c;
|
||||
if (Math.abs(dtheta) < 1E-9) {
|
||||
s = 1.0 - 1.0 / 6.0 * dtheta * dtheta;
|
||||
c = 0.5 * dtheta;
|
||||
} else {
|
||||
s = sinTheta / dtheta;
|
||||
c = (1 - cosTheta) / dtheta;
|
||||
}
|
||||
var transform = new Transform2d(new Translation2d(dx * s - dy * c, dx * c + dy * s),
|
||||
new Rotation2d(cosTheta, sinTheta));
|
||||
|
||||
return this.plus(transform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Twist2d that maps this pose to the end pose. If c is the output
|
||||
* of a.Log(b), then a.Exp(c) would yield b.
|
||||
*
|
||||
* @param end The end pose for the transformation.
|
||||
* @return The twist that maps this to end.
|
||||
*/
|
||||
public Twist2d log(Pose2d end) {
|
||||
final var transform = end.relativeTo(this);
|
||||
final var dtheta = transform.getRotation().getRadians();
|
||||
final var halfDtheta = dtheta / 2.0;
|
||||
|
||||
final var cosMinusOne = transform.getRotation().getCos() - 1;
|
||||
|
||||
double halfThetaByTanOfHalfDtheta;
|
||||
if (Math.abs(cosMinusOne) < 1E-9) {
|
||||
halfThetaByTanOfHalfDtheta = 1.0 - 1.0 / 12.0 * dtheta * dtheta;
|
||||
} else {
|
||||
halfThetaByTanOfHalfDtheta = -(halfDtheta * transform.getRotation().getSin()) / cosMinusOne;
|
||||
}
|
||||
|
||||
Translation2d translationPart = transform.getTranslation().rotateBy(
|
||||
new Rotation2d(halfThetaByTanOfHalfDtheta, -halfDtheta)
|
||||
).times(Math.hypot(halfThetaByTanOfHalfDtheta, halfDtheta));
|
||||
|
||||
return new Twist2d(translationPart.getX(), translationPart.getY(), dtheta);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Pose2d(%s, %s)", m_translation, m_rotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks equality between this Pose2d and another object.
|
||||
*
|
||||
* @param obj The other object.
|
||||
* @return Whether the two objects are equal or not.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Pose2d) {
|
||||
return ((Pose2d) obj).m_translation.equals(m_translation)
|
||||
&& ((Pose2d) obj).m_rotation.equals(m_rotation);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(m_translation, m_rotation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.geometry;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* A rotation in a 2d coordinate frame represented a point on the unit circle
|
||||
* (cosine and sine).
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
public class Rotation2d {
|
||||
private final double m_value;
|
||||
private final double m_cos;
|
||||
private final double m_sin;
|
||||
|
||||
/**
|
||||
* Constructs a Rotation2d with a default angle of 0 degrees.
|
||||
*/
|
||||
public Rotation2d() {
|
||||
m_value = 0.0;
|
||||
m_cos = 1.0;
|
||||
m_sin = 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Rotation2d with the given radian value.
|
||||
* The x and y don't have to be normalized.
|
||||
*
|
||||
* @param value The value of the angle in radians.
|
||||
*/
|
||||
@JsonCreator
|
||||
public Rotation2d(@JsonProperty(required = true, value = "radians") double value) {
|
||||
m_value = value;
|
||||
m_cos = Math.cos(value);
|
||||
m_sin = Math.sin(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Rotation2d with the given x and y (cosine and sine)
|
||||
* components.
|
||||
*
|
||||
* @param x The x component or cosine of the rotation.
|
||||
* @param y The y component or sine of the rotation.
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
public Rotation2d(double x, double y) {
|
||||
double magnitude = Math.hypot(x, y);
|
||||
if (magnitude > 1e-6) {
|
||||
m_sin = y / magnitude;
|
||||
m_cos = x / magnitude;
|
||||
} else {
|
||||
m_sin = 0.0;
|
||||
m_cos = 1.0;
|
||||
}
|
||||
m_value = Math.atan2(m_sin, m_cos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs and returns a Rotation2d with the given degree value.
|
||||
*
|
||||
* @param degrees The value of the angle in degrees.
|
||||
* @return The rotation object with the desired angle value.
|
||||
*/
|
||||
public static Rotation2d fromDegrees(double degrees) {
|
||||
return new Rotation2d(Math.toRadians(degrees));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds two rotations together, with the result being bounded between -pi and
|
||||
* pi.
|
||||
*
|
||||
* <p>For example, Rotation2d.fromDegrees(30) + Rotation2d.fromDegrees(60) =
|
||||
* Rotation2d{-pi/2}
|
||||
*
|
||||
* @param other The rotation to add.
|
||||
* @return The sum of the two rotations.
|
||||
*/
|
||||
public Rotation2d plus(Rotation2d other) {
|
||||
return rotateBy(other);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtracts the new rotation from the current rotation and returns the new
|
||||
* rotation.
|
||||
*
|
||||
* <p>For example, Rotation2d.fromDegrees(10) - Rotation2d.fromDegrees(100) =
|
||||
* Rotation2d{-pi/2}
|
||||
*
|
||||
* @param other The rotation to subtract.
|
||||
* @return The difference between the two rotations.
|
||||
*/
|
||||
public Rotation2d minus(Rotation2d other) {
|
||||
return rotateBy(other.unaryMinus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the inverse of the current rotation. This is simply the negative of
|
||||
* the current angular value.
|
||||
*
|
||||
* @return The inverse of the current rotation.
|
||||
*/
|
||||
public Rotation2d unaryMinus() {
|
||||
return new Rotation2d(-m_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies the current rotation by a scalar.
|
||||
*
|
||||
* @param scalar The scalar.
|
||||
* @return The new scaled Rotation2d.
|
||||
*/
|
||||
public Rotation2d times(double scalar) {
|
||||
return new Rotation2d(m_value * scalar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the new rotation to the current rotation using a rotation matrix.
|
||||
*
|
||||
* <p>The matrix multiplication is as follows:
|
||||
* [cos_new] [other.cos, -other.sin][cos]
|
||||
* [sin_new] = [other.sin, other.cos][sin]
|
||||
* value_new = atan2(cos_new, sin_new)
|
||||
*
|
||||
* @param other The rotation to rotate by.
|
||||
* @return The new rotated Rotation2d.
|
||||
*/
|
||||
public Rotation2d rotateBy(Rotation2d other) {
|
||||
return new Rotation2d(
|
||||
m_cos * other.m_cos - m_sin * other.m_sin,
|
||||
m_cos * other.m_sin + m_sin * other.m_cos
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the radian value of the rotation.
|
||||
*
|
||||
* @return The radian value of the rotation.
|
||||
*/
|
||||
@JsonProperty
|
||||
public double getRadians() {
|
||||
return m_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the degree value of the rotation.
|
||||
*
|
||||
* @return The degree value of the rotation.
|
||||
*/
|
||||
public double getDegrees() {
|
||||
return Math.toDegrees(m_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cosine of the rotation.
|
||||
*
|
||||
* @return The cosine of the rotation.
|
||||
*/
|
||||
public double getCos() {
|
||||
return m_cos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sine of the rotation.
|
||||
*
|
||||
* @return The sine of the rotation.
|
||||
*/
|
||||
public double getSin() {
|
||||
return m_sin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tangent of the rotation.
|
||||
*
|
||||
* @return The tangent of the rotation.
|
||||
*/
|
||||
public double getTan() {
|
||||
return m_sin / m_cos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Rotation2d(Rads: %.2f, Deg: %.2f)", m_value, Math.toDegrees(m_value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks equality between this Rotation2d and another object.
|
||||
*
|
||||
* @param obj The other object.
|
||||
* @return Whether the two objects are equal or not.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Rotation2d) {
|
||||
return Math.abs(((Rotation2d) obj).m_value - m_value) < 1E-9;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(m_value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.geometry;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents a transformation for a Pose2d.
|
||||
*/
|
||||
public class Transform2d {
|
||||
private final Translation2d m_translation;
|
||||
private final Rotation2d m_rotation;
|
||||
|
||||
/**
|
||||
* Constructs the transform that maps the initial pose to the final pose.
|
||||
*
|
||||
* @param initial The initial pose for the transformation.
|
||||
* @param last The final pose for the transformation.
|
||||
*/
|
||||
public Transform2d(Pose2d initial, Pose2d last) {
|
||||
// We are rotating the difference between the translations
|
||||
// using a clockwise rotation matrix. This transforms the global
|
||||
// delta into a local delta (relative to the initial pose).
|
||||
m_translation = last.getTranslation().minus(initial.getTranslation())
|
||||
.rotateBy(initial.getRotation().unaryMinus());
|
||||
|
||||
m_rotation = last.getRotation().minus(initial.getRotation());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a transform with the given translation and rotation components.
|
||||
*
|
||||
* @param translation Translational component of the transform.
|
||||
* @param rotation Rotational component of the transform.
|
||||
*/
|
||||
public Transform2d(Translation2d translation, Rotation2d rotation) {
|
||||
m_translation = translation;
|
||||
m_rotation = rotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the identity transform -- maps an initial pose to itself.
|
||||
*/
|
||||
public Transform2d() {
|
||||
m_translation = new Translation2d();
|
||||
m_rotation = new Rotation2d();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales the transform by the scalar.
|
||||
*
|
||||
* @param scalar The scalar.
|
||||
* @return The scaled Transform2d.
|
||||
*/
|
||||
public Transform2d times(double scalar) {
|
||||
return new Transform2d(m_translation.times(scalar), m_rotation.times(scalar));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translation component of the transformation.
|
||||
*
|
||||
* @return The translational component of the transform.
|
||||
*/
|
||||
public Translation2d getTranslation() {
|
||||
return m_translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the X component of the transformation's translation.
|
||||
*
|
||||
* @return The x component of the transformation's translation.
|
||||
*/
|
||||
public double getX() {
|
||||
return m_translation.getX();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Y component of the transformation's translation.
|
||||
*
|
||||
* @return The y component of the transformation's translation.
|
||||
*/
|
||||
public double getY() {
|
||||
return m_translation.getY();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the rotational component of the transformation.
|
||||
*
|
||||
* @return Reference to the rotational component of the transform.
|
||||
*/
|
||||
public Rotation2d getRotation() {
|
||||
return m_rotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invert the transformation. This is useful for undoing a transformation.
|
||||
*
|
||||
* @return The inverted transformation.
|
||||
*/
|
||||
public Transform2d inverse() {
|
||||
// We are rotating the difference between the translations
|
||||
// using a clockwise rotation matrix. This transforms the global
|
||||
// delta into a local delta (relative to the initial pose).
|
||||
return new Transform2d(getTranslation().unaryMinus().rotateBy(getRotation().unaryMinus()),
|
||||
getRotation().unaryMinus());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Transform2d(%s, %s)", m_translation, m_rotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks equality between this Transform2d and another object.
|
||||
*
|
||||
* @param obj The other object.
|
||||
* @return Whether the two objects are equal or not.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Transform2d) {
|
||||
return ((Transform2d) obj).m_translation.equals(m_translation)
|
||||
&& ((Transform2d) obj).m_rotation.equals(m_rotation);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(m_translation, m_rotation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.geometry;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Represents a translation in 2d space.
|
||||
* This object can be used to represent a point or a vector.
|
||||
*
|
||||
* <p>This assumes that you are using conventional mathematical axes.
|
||||
* When the robot is placed on the origin, facing toward the X direction,
|
||||
* moving forward increases the X, whereas moving to the left increases the Y.
|
||||
*/
|
||||
@SuppressWarnings({"ParameterName", "MemberName"})
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
public class Translation2d {
|
||||
private final double m_x;
|
||||
private final double m_y;
|
||||
|
||||
/**
|
||||
* Constructs a Translation2d with X and Y components equal to zero.
|
||||
*/
|
||||
public Translation2d() {
|
||||
this(0.0, 0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Translation2d with the X and Y components equal to the
|
||||
* provided values.
|
||||
*
|
||||
* @param x The x component of the translation.
|
||||
* @param y The y component of the translation.
|
||||
*/
|
||||
@JsonCreator
|
||||
public Translation2d(@JsonProperty(required = true, value = "x") double x,
|
||||
@JsonProperty(required = true, value = "y") double y) {
|
||||
m_x = x;
|
||||
m_y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the distance between two translations in 2d space.
|
||||
*
|
||||
* <p>This function uses the pythagorean theorem to calculate the distance.
|
||||
* distance = sqrt((x2 - x1)^2 + (y2 - y1)^2)
|
||||
*
|
||||
* @param other The translation to compute the distance to.
|
||||
* @return The distance between the two translations.
|
||||
*/
|
||||
public double getDistance(Translation2d other) {
|
||||
return Math.hypot(other.m_x - m_x, other.m_y - m_y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the X component of the translation.
|
||||
*
|
||||
* @return The x component of the translation.
|
||||
*/
|
||||
@JsonProperty
|
||||
public double getX() {
|
||||
return m_x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Y component of the translation.
|
||||
*
|
||||
* @return The y component of the translation.
|
||||
*/
|
||||
@JsonProperty
|
||||
public double getY() {
|
||||
return m_y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the norm, or distance from the origin to the translation.
|
||||
*
|
||||
* @return The norm of the translation.
|
||||
*/
|
||||
public double getNorm() {
|
||||
return Math.hypot(m_x, m_y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a rotation to the translation in 2d space.
|
||||
*
|
||||
* <p>This multiplies the translation vector by a counterclockwise rotation
|
||||
* matrix of the given angle.
|
||||
* [x_new] [other.cos, -other.sin][x]
|
||||
* [y_new] = [other.sin, other.cos][y]
|
||||
*
|
||||
* <p>For example, rotating a Translation2d of {2, 0} by 90 degrees will return a
|
||||
* Translation2d of {0, 2}.
|
||||
*
|
||||
* @param other The rotation to rotate the translation by.
|
||||
* @return The new rotated translation.
|
||||
*/
|
||||
public Translation2d rotateBy(Rotation2d other) {
|
||||
return new Translation2d(
|
||||
m_x * other.getCos() - m_y * other.getSin(),
|
||||
m_x * other.getSin() + m_y * other.getCos()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds two translations in 2d space and returns the sum. This is similar to
|
||||
* vector addition.
|
||||
*
|
||||
* <p>For example, Translation2d{1.0, 2.5} + Translation2d{2.0, 5.5} =
|
||||
* Translation2d{3.0, 8.0}
|
||||
*
|
||||
* @param other The translation to add.
|
||||
* @return The sum of the translations.
|
||||
*/
|
||||
public Translation2d plus(Translation2d other) {
|
||||
return new Translation2d(m_x + other.m_x, m_y + other.m_y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtracts the other translation from the other translation and returns the
|
||||
* difference.
|
||||
*
|
||||
* <p>For example, Translation2d{5.0, 4.0} - Translation2d{1.0, 2.0} =
|
||||
* Translation2d{4.0, 2.0}
|
||||
*
|
||||
* @param other The translation to subtract.
|
||||
* @return The difference between the two translations.
|
||||
*/
|
||||
public Translation2d minus(Translation2d other) {
|
||||
return new Translation2d(m_x - other.m_x, m_y - other.m_y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the inverse of the current translation. This is equivalent to
|
||||
* rotating by 180 degrees, flipping the point over both axes, or simply
|
||||
* negating both components of the translation.
|
||||
*
|
||||
* @return The inverse of the current translation.
|
||||
*/
|
||||
public Translation2d unaryMinus() {
|
||||
return new Translation2d(-m_x, -m_y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies the translation by a scalar and returns the new translation.
|
||||
*
|
||||
* <p>For example, Translation2d{2.0, 2.5} * 2 = Translation2d{4.0, 5.0}
|
||||
*
|
||||
* @param scalar The scalar to multiply by.
|
||||
* @return The scaled translation.
|
||||
*/
|
||||
public Translation2d times(double scalar) {
|
||||
return new Translation2d(m_x * scalar, m_y * scalar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Divides the translation by a scalar and returns the new translation.
|
||||
*
|
||||
* <p>For example, Translation2d{2.0, 2.5} / 2 = Translation2d{1.0, 1.25}
|
||||
*
|
||||
* @param scalar The scalar to multiply by.
|
||||
* @return The reference to the new mutated object.
|
||||
*/
|
||||
public Translation2d div(double scalar) {
|
||||
return new Translation2d(m_x / scalar, m_y / scalar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Translation2d(X: %.2f, Y: %.2f)", m_x, m_y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks equality between this Translation2d and another object.
|
||||
*
|
||||
* @param obj The other object.
|
||||
* @return Whether the two objects are equal or not.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Translation2d) {
|
||||
return Math.abs(((Translation2d) obj).m_x - m_x) < 1E-9
|
||||
&& Math.abs(((Translation2d) obj).m_y - m_y) < 1E-9;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(m_x, m_y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.geometry;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A change in distance along arc since the last pose update. We can use ideas
|
||||
* from differential calculus to create new Pose2ds from a Twist2d and vise
|
||||
* versa.
|
||||
*
|
||||
* <p>A Twist can be used to represent a difference between two poses.
|
||||
*/
|
||||
@SuppressWarnings("MemberName")
|
||||
public class Twist2d {
|
||||
/**
|
||||
* Linear "dx" component.
|
||||
*/
|
||||
public double dx;
|
||||
|
||||
/**
|
||||
* Linear "dy" component.
|
||||
*/
|
||||
public double dy;
|
||||
|
||||
/**
|
||||
* Angular "dtheta" component (radians).
|
||||
*/
|
||||
public double dtheta;
|
||||
|
||||
public Twist2d() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Twist2d with the given values.
|
||||
* @param dx Change in x direction relative to robot.
|
||||
* @param dy Change in y direction relative to robot.
|
||||
* @param dtheta Change in angle relative to robot.
|
||||
*/
|
||||
public Twist2d(double dx, double dy, double dtheta) {
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
this.dtheta = dtheta;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Twist2d(dX: %.2f, dY: %.2f, dTheta: %.2f)", dx, dy, dtheta);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks equality between this Twist2d and another object.
|
||||
*
|
||||
* @param obj The other object.
|
||||
* @return Whether the two objects are equal or not.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Twist2d) {
|
||||
return Math.abs(((Twist2d) obj).dx - dx) < 1E-9
|
||||
&& Math.abs(((Twist2d) obj).dy - dy) < 1E-9
|
||||
&& Math.abs(((Twist2d) obj).dtheta - dtheta) < 1E-9;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(dx, dy, dtheta);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.kinematics;
|
||||
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
|
||||
/**
|
||||
* Represents the speed of a robot chassis. Although this struct contains
|
||||
* similar members compared to a Twist2d, they do NOT represent the same thing.
|
||||
* Whereas a Twist2d represents a change in pose w.r.t to the robot frame of reference,
|
||||
* this ChassisSpeeds struct represents a velocity w.r.t to the robot frame of
|
||||
* reference.
|
||||
*
|
||||
* <p>A strictly non-holonomic drivetrain, such as a differential drive, should
|
||||
* never have a dy component because it can never move sideways. Holonomic
|
||||
* drivetrains such as swerve and mecanum will often have all three components.
|
||||
*/
|
||||
@SuppressWarnings("MemberName")
|
||||
public class ChassisSpeeds {
|
||||
/**
|
||||
* Represents forward velocity w.r.t the robot frame of reference. (Fwd is +)
|
||||
*/
|
||||
public double vxMetersPerSecond;
|
||||
|
||||
/**
|
||||
* Represents sideways velocity w.r.t the robot frame of reference. (Left is +)
|
||||
*/
|
||||
public double vyMetersPerSecond;
|
||||
|
||||
/**
|
||||
* Represents the angular velocity of the robot frame. (CCW is +)
|
||||
*/
|
||||
public double omegaRadiansPerSecond;
|
||||
|
||||
/**
|
||||
* Constructs a ChassisSpeeds with zeros for dx, dy, and theta.
|
||||
*/
|
||||
public ChassisSpeeds() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a ChassisSpeeds object.
|
||||
*
|
||||
* @param vxMetersPerSecond Forward velocity.
|
||||
* @param vyMetersPerSecond Sideways velocity.
|
||||
* @param omegaRadiansPerSecond Angular velocity.
|
||||
*/
|
||||
public ChassisSpeeds(double vxMetersPerSecond, double vyMetersPerSecond,
|
||||
double omegaRadiansPerSecond) {
|
||||
this.vxMetersPerSecond = vxMetersPerSecond;
|
||||
this.vyMetersPerSecond = vyMetersPerSecond;
|
||||
this.omegaRadiansPerSecond = omegaRadiansPerSecond;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a user provided field-relative set of speeds into a robot-relative
|
||||
* ChassisSpeeds object.
|
||||
*
|
||||
* @param vxMetersPerSecond The component of speed in the x direction relative to the field.
|
||||
* Positive x is away from your alliance wall.
|
||||
* @param vyMetersPerSecond The component of speed in the y direction relative to the field.
|
||||
* Positive y is to your left when standing behind the alliance wall.
|
||||
* @param omegaRadiansPerSecond The angular rate of the robot.
|
||||
* @param robotAngle The angle of the robot as measured by a gyroscope. The robot's
|
||||
* angle is considered to be zero when it is facing directly away
|
||||
* from your alliance station wall. Remember that this should
|
||||
* be CCW positive.
|
||||
* @return ChassisSpeeds object representing the speeds in the robot's frame of reference.
|
||||
*/
|
||||
public static ChassisSpeeds fromFieldRelativeSpeeds(
|
||||
double vxMetersPerSecond, double vyMetersPerSecond,
|
||||
double omegaRadiansPerSecond, Rotation2d robotAngle) {
|
||||
return new ChassisSpeeds(
|
||||
vxMetersPerSecond * robotAngle.getCos() + vyMetersPerSecond * robotAngle.getSin(),
|
||||
-vxMetersPerSecond * robotAngle.getSin() + vyMetersPerSecond * robotAngle.getCos(),
|
||||
omegaRadiansPerSecond
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("ChassisSpeeds(Vx: %.2f m/s, Vy: %.2f m/s, Omega: %.2f rad/s)",
|
||||
vxMetersPerSecond, vyMetersPerSecond, omegaRadiansPerSecond);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.kinematics;
|
||||
|
||||
import edu.wpi.first.math.MathSharedStore;
|
||||
import edu.wpi.first.math.MathUsageId;
|
||||
|
||||
/**
|
||||
* Helper class that converts a chassis velocity (dx and dtheta components) to
|
||||
* left and right wheel velocities for a differential drive.
|
||||
*
|
||||
* <p>Inverse kinematics converts a desired chassis speed into left and right
|
||||
* velocity components whereas forward kinematics converts left and right
|
||||
* component velocities into a linear and angular chassis speed.
|
||||
*/
|
||||
@SuppressWarnings("MemberName")
|
||||
public class DifferentialDriveKinematics {
|
||||
public final double trackWidthMeters;
|
||||
|
||||
/**
|
||||
* Constructs a differential drive kinematics object.
|
||||
*
|
||||
* @param trackWidthMeters The track width of the drivetrain. Theoretically, this is
|
||||
* the distance between the left wheels and right wheels.
|
||||
* However, the empirical value may be larger than the physical
|
||||
* measured value due to scrubbing effects.
|
||||
*/
|
||||
public DifferentialDriveKinematics(double trackWidthMeters) {
|
||||
this.trackWidthMeters = trackWidthMeters;
|
||||
MathSharedStore.reportUsage(MathUsageId.kKinematics_DifferentialDrive, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a chassis speed from left and right component velocities using
|
||||
* forward kinematics.
|
||||
*
|
||||
* @param wheelSpeeds The left and right velocities.
|
||||
* @return The chassis speed.
|
||||
*/
|
||||
public ChassisSpeeds toChassisSpeeds(DifferentialDriveWheelSpeeds wheelSpeeds) {
|
||||
return new ChassisSpeeds(
|
||||
(wheelSpeeds.leftMetersPerSecond + wheelSpeeds.rightMetersPerSecond) / 2, 0,
|
||||
(wheelSpeeds.rightMetersPerSecond - wheelSpeeds.leftMetersPerSecond)
|
||||
/ trackWidthMeters
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns left and right component velocities from a chassis speed using
|
||||
* inverse kinematics.
|
||||
*
|
||||
* @param chassisSpeeds The linear and angular (dx and dtheta) components that
|
||||
* represent the chassis' speed.
|
||||
* @return The left and right velocities.
|
||||
*/
|
||||
public DifferentialDriveWheelSpeeds toWheelSpeeds(ChassisSpeeds chassisSpeeds) {
|
||||
return new DifferentialDriveWheelSpeeds(
|
||||
chassisSpeeds.vxMetersPerSecond - trackWidthMeters / 2
|
||||
* chassisSpeeds.omegaRadiansPerSecond,
|
||||
chassisSpeeds.vxMetersPerSecond + trackWidthMeters / 2
|
||||
* chassisSpeeds.omegaRadiansPerSecond
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.kinematics;
|
||||
|
||||
import edu.wpi.first.math.MathSharedStore;
|
||||
import edu.wpi.first.math.MathUsageId;
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Twist2d;
|
||||
|
||||
/**
|
||||
* Class for differential drive odometry. Odometry allows you to track the
|
||||
* robot's position on the field over the course of a match using readings from
|
||||
* 2 encoders and a gyroscope.
|
||||
*
|
||||
* <p>Teams can use odometry during the autonomous period for complex tasks like
|
||||
* path following. Furthermore, odometry can be used for latency compensation
|
||||
* when using computer-vision systems.
|
||||
*
|
||||
* <p>It is important that you reset your encoders to zero before using this class.
|
||||
* Any subsequent pose resets also require the encoders to be reset to zero.
|
||||
*/
|
||||
public class DifferentialDriveOdometry {
|
||||
private Pose2d m_poseMeters;
|
||||
|
||||
private Rotation2d m_gyroOffset;
|
||||
private Rotation2d m_previousAngle;
|
||||
|
||||
private double m_prevLeftDistance;
|
||||
private double m_prevRightDistance;
|
||||
|
||||
/**
|
||||
* Constructs a DifferentialDriveOdometry object.
|
||||
*
|
||||
* @param gyroAngle The angle reported by the gyroscope.
|
||||
* @param initialPoseMeters The starting position of the robot on the field.
|
||||
*/
|
||||
public DifferentialDriveOdometry(Rotation2d gyroAngle,
|
||||
Pose2d initialPoseMeters) {
|
||||
m_poseMeters = initialPoseMeters;
|
||||
m_gyroOffset = m_poseMeters.getRotation().minus(gyroAngle);
|
||||
m_previousAngle = initialPoseMeters.getRotation();
|
||||
MathSharedStore.reportUsage(MathUsageId.kOdometry_DifferentialDrive, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a DifferentialDriveOdometry object with the default pose at the origin.
|
||||
*
|
||||
* @param gyroAngle The angle reported by the gyroscope.
|
||||
*/
|
||||
public DifferentialDriveOdometry(Rotation2d gyroAngle) {
|
||||
this(gyroAngle, new Pose2d());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the robot's position on the field.
|
||||
*
|
||||
* <p>You NEED to reset your encoders (to zero) when calling this method.
|
||||
*
|
||||
* <p>The gyroscope angle does not need to be reset here on the user's robot code.
|
||||
* The library automatically takes care of offsetting the gyro angle.
|
||||
*
|
||||
* @param poseMeters The position on the field that your robot is at.
|
||||
* @param gyroAngle The angle reported by the gyroscope.
|
||||
*/
|
||||
public void resetPosition(Pose2d poseMeters, Rotation2d gyroAngle) {
|
||||
m_poseMeters = poseMeters;
|
||||
m_previousAngle = poseMeters.getRotation();
|
||||
m_gyroOffset = m_poseMeters.getRotation().minus(gyroAngle);
|
||||
|
||||
m_prevLeftDistance = 0.0;
|
||||
m_prevRightDistance = 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the robot on the field.
|
||||
*
|
||||
* @return The pose of the robot (x and y are in meters).
|
||||
*/
|
||||
public Pose2d getPoseMeters() {
|
||||
return m_poseMeters;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the robot position on the field using distance measurements from encoders. This
|
||||
* method is more numerically accurate than using velocities to integrate the pose and
|
||||
* is also advantageous for teams that are using lower CPR encoders.
|
||||
*
|
||||
* @param gyroAngle The angle reported by the gyroscope.
|
||||
* @param leftDistanceMeters The distance traveled by the left encoder.
|
||||
* @param rightDistanceMeters The distance traveled by the right encoder.
|
||||
* @return The new pose of the robot.
|
||||
*/
|
||||
public Pose2d update(Rotation2d gyroAngle, double leftDistanceMeters,
|
||||
double rightDistanceMeters) {
|
||||
double deltaLeftDistance = leftDistanceMeters - m_prevLeftDistance;
|
||||
double deltaRightDistance = rightDistanceMeters - m_prevRightDistance;
|
||||
|
||||
m_prevLeftDistance = leftDistanceMeters;
|
||||
m_prevRightDistance = rightDistanceMeters;
|
||||
|
||||
double averageDeltaDistance = (deltaLeftDistance + deltaRightDistance) / 2.0;
|
||||
var angle = gyroAngle.plus(m_gyroOffset);
|
||||
|
||||
var newPose = m_poseMeters.exp(
|
||||
new Twist2d(averageDeltaDistance, 0.0, angle.minus(m_previousAngle).getRadians()));
|
||||
|
||||
m_previousAngle = angle;
|
||||
|
||||
m_poseMeters = new Pose2d(newPose.getTranslation(), angle);
|
||||
return m_poseMeters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.kinematics;
|
||||
|
||||
/**
|
||||
* Represents the wheel speeds for a differential drive drivetrain.
|
||||
*/
|
||||
@SuppressWarnings("MemberName")
|
||||
public class DifferentialDriveWheelSpeeds {
|
||||
/**
|
||||
* Speed of the left side of the robot.
|
||||
*/
|
||||
public double leftMetersPerSecond;
|
||||
|
||||
/**
|
||||
* Speed of the right side of the robot.
|
||||
*/
|
||||
public double rightMetersPerSecond;
|
||||
|
||||
/**
|
||||
* Constructs a DifferentialDriveWheelSpeeds with zeros for left and right speeds.
|
||||
*/
|
||||
public DifferentialDriveWheelSpeeds() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a DifferentialDriveWheelSpeeds.
|
||||
*
|
||||
* @param leftMetersPerSecond The left speed.
|
||||
* @param rightMetersPerSecond The right speed.
|
||||
*/
|
||||
public DifferentialDriveWheelSpeeds(double leftMetersPerSecond, double rightMetersPerSecond) {
|
||||
this.leftMetersPerSecond = leftMetersPerSecond;
|
||||
this.rightMetersPerSecond = rightMetersPerSecond;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the wheel speeds using some max attainable speed. Sometimes,
|
||||
* after inverse kinematics, the requested speed from a/several modules may be
|
||||
* above the max attainable speed for the driving motor on that module. To fix
|
||||
* this issue, one can "normalize" all the wheel speeds to make sure that all
|
||||
* requested module speeds are below the absolute threshold, while maintaining
|
||||
* the ratio of speeds between modules.
|
||||
*
|
||||
* @param attainableMaxSpeedMetersPerSecond The absolute max speed that a wheel can reach.
|
||||
*/
|
||||
public void normalize(double attainableMaxSpeedMetersPerSecond) {
|
||||
double realMaxSpeed = Math.max(Math.abs(leftMetersPerSecond), Math.abs(rightMetersPerSecond));
|
||||
|
||||
if (realMaxSpeed > attainableMaxSpeedMetersPerSecond) {
|
||||
leftMetersPerSecond = leftMetersPerSecond / realMaxSpeed
|
||||
* attainableMaxSpeedMetersPerSecond;
|
||||
rightMetersPerSecond = rightMetersPerSecond / realMaxSpeed
|
||||
* attainableMaxSpeedMetersPerSecond;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("DifferentialDriveWheelSpeeds(Left: %.2f m/s, Right: %.2f m/s)",
|
||||
leftMetersPerSecond, rightMetersPerSecond);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.kinematics;
|
||||
|
||||
import org.ejml.simple.SimpleMatrix;
|
||||
|
||||
import edu.wpi.first.math.MathSharedStore;
|
||||
import edu.wpi.first.math.MathUsageId;
|
||||
import edu.wpi.first.wpilibj.geometry.Translation2d;
|
||||
|
||||
/**
|
||||
* Helper class that converts a chassis velocity (dx, dy, and dtheta components)
|
||||
* into individual wheel speeds.
|
||||
*
|
||||
* <p>The inverse kinematics (converting from a desired chassis velocity to
|
||||
* individual wheel speeds) uses the relative locations of the wheels with
|
||||
* respect to the center of rotation. The center of rotation for inverse
|
||||
* kinematics is also variable. This means that you can set your set your center
|
||||
* of rotation in a corner of the robot to perform special evasion manuevers.
|
||||
*
|
||||
* <p>Forward kinematics (converting an array of wheel speeds into the overall
|
||||
* chassis motion) is performs the exact opposite of what inverse kinematics
|
||||
* does. Since this is an overdetermined system (more equations than variables),
|
||||
* we use a least-squares approximation.
|
||||
*
|
||||
* <p>The inverse kinematics: [wheelSpeeds] = [wheelLocations] * [chassisSpeeds]
|
||||
* We take the Moore-Penrose pseudoinverse of [wheelLocations] and then
|
||||
* multiply by [wheelSpeeds] to get our chassis speeds.
|
||||
*
|
||||
* <p>Forward kinematics is also used for odometry -- determining the position of
|
||||
* the robot on the field using encoders and a gyro.
|
||||
*/
|
||||
public class MecanumDriveKinematics {
|
||||
private SimpleMatrix m_inverseKinematics;
|
||||
private final SimpleMatrix m_forwardKinematics;
|
||||
|
||||
private final Translation2d m_frontLeftWheelMeters;
|
||||
private final Translation2d m_frontRightWheelMeters;
|
||||
private final Translation2d m_rearLeftWheelMeters;
|
||||
private final Translation2d m_rearRightWheelMeters;
|
||||
|
||||
private Translation2d m_prevCoR = new Translation2d();
|
||||
|
||||
/**
|
||||
* Constructs a mecanum drive kinematics object.
|
||||
*
|
||||
* @param frontLeftWheelMeters The location of the front-left wheel relative to the
|
||||
* physical center of the robot.
|
||||
* @param frontRightWheelMeters The location of the front-right wheel relative to
|
||||
* the physical center of the robot.
|
||||
* @param rearLeftWheelMeters The location of the rear-left wheel relative to the
|
||||
* physical center of the robot.
|
||||
* @param rearRightWheelMeters The location of the rear-right wheel relative to the
|
||||
* physical center of the robot.
|
||||
*/
|
||||
public MecanumDriveKinematics(Translation2d frontLeftWheelMeters,
|
||||
Translation2d frontRightWheelMeters,
|
||||
Translation2d rearLeftWheelMeters,
|
||||
Translation2d rearRightWheelMeters) {
|
||||
m_frontLeftWheelMeters = frontLeftWheelMeters;
|
||||
m_frontRightWheelMeters = frontRightWheelMeters;
|
||||
m_rearLeftWheelMeters = rearLeftWheelMeters;
|
||||
m_rearRightWheelMeters = rearRightWheelMeters;
|
||||
|
||||
m_inverseKinematics = new SimpleMatrix(4, 3);
|
||||
|
||||
setInverseKinematics(frontLeftWheelMeters, frontRightWheelMeters,
|
||||
rearLeftWheelMeters, rearRightWheelMeters);
|
||||
m_forwardKinematics = m_inverseKinematics.pseudoInverse();
|
||||
|
||||
MathSharedStore.reportUsage(MathUsageId.kKinematics_MecanumDrive, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs inverse kinematics to return the wheel speeds from a desired chassis velocity. This
|
||||
* method is often used to convert joystick values into wheel speeds.
|
||||
*
|
||||
* <p>This function also supports variable centers of rotation. During normal
|
||||
* operations, the center of rotation is usually the same as the physical
|
||||
* center of the robot; therefore, the argument is defaulted to that use case.
|
||||
* However, if you wish to change the center of rotation for evasive
|
||||
* manuevers, vision alignment, or for any other use case, you can do so.
|
||||
*
|
||||
* @param chassisSpeeds The desired chassis speed.
|
||||
* @param centerOfRotationMeters The center of rotation. For example, if you set the
|
||||
* center of rotation at one corner of the robot and provide
|
||||
* a chassis speed that only has a dtheta component, the robot
|
||||
* will rotate around that corner.
|
||||
* @return The wheel speeds. Use caution because they are not normalized. Sometimes, a user
|
||||
* input may cause one of the wheel speeds to go above the attainable max velocity. Use
|
||||
* the {@link MecanumDriveWheelSpeeds#normalize(double)} function to rectify this issue.
|
||||
*/
|
||||
public MecanumDriveWheelSpeeds toWheelSpeeds(ChassisSpeeds chassisSpeeds,
|
||||
Translation2d centerOfRotationMeters) {
|
||||
// We have a new center of rotation. We need to compute the matrix again.
|
||||
if (!centerOfRotationMeters.equals(m_prevCoR)) {
|
||||
var fl = m_frontLeftWheelMeters.minus(centerOfRotationMeters);
|
||||
var fr = m_frontRightWheelMeters.minus(centerOfRotationMeters);
|
||||
var rl = m_rearLeftWheelMeters.minus(centerOfRotationMeters);
|
||||
var rr = m_rearRightWheelMeters.minus(centerOfRotationMeters);
|
||||
|
||||
setInverseKinematics(fl, fr, rl, rr);
|
||||
m_prevCoR = centerOfRotationMeters;
|
||||
}
|
||||
|
||||
var chassisSpeedsVector = new SimpleMatrix(3, 1);
|
||||
chassisSpeedsVector.setColumn(0, 0,
|
||||
chassisSpeeds.vxMetersPerSecond, chassisSpeeds.vyMetersPerSecond,
|
||||
chassisSpeeds.omegaRadiansPerSecond);
|
||||
|
||||
var wheelsMatrix = m_inverseKinematics.mult(chassisSpeedsVector);
|
||||
return new MecanumDriveWheelSpeeds(
|
||||
wheelsMatrix.get(0, 0),
|
||||
wheelsMatrix.get(1, 0),
|
||||
wheelsMatrix.get(2, 0),
|
||||
wheelsMatrix.get(3, 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs inverse kinematics. See {@link #toWheelSpeeds(ChassisSpeeds, Translation2d)} for more
|
||||
* information.
|
||||
*
|
||||
* @param chassisSpeeds The desired chassis speed.
|
||||
* @return The wheel speeds.
|
||||
*/
|
||||
public MecanumDriveWheelSpeeds toWheelSpeeds(ChassisSpeeds chassisSpeeds) {
|
||||
return toWheelSpeeds(chassisSpeeds, new Translation2d());
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs forward kinematics to return the resulting chassis state from the given wheel speeds.
|
||||
* This method is often used for odometry -- determining the robot's position on the field using
|
||||
* data from the real-world speed of each wheel on the robot.
|
||||
*
|
||||
* @param wheelSpeeds The current mecanum drive wheel speeds.
|
||||
* @return The resulting chassis speed.
|
||||
*/
|
||||
public ChassisSpeeds toChassisSpeeds(MecanumDriveWheelSpeeds wheelSpeeds) {
|
||||
var wheelSpeedsMatrix = new SimpleMatrix(4, 1);
|
||||
wheelSpeedsMatrix.setColumn(0, 0,
|
||||
wheelSpeeds.frontLeftMetersPerSecond, wheelSpeeds.frontRightMetersPerSecond,
|
||||
wheelSpeeds.rearLeftMetersPerSecond, wheelSpeeds.rearRightMetersPerSecond
|
||||
);
|
||||
var chassisSpeedsVector = m_forwardKinematics.mult(wheelSpeedsMatrix);
|
||||
|
||||
return new ChassisSpeeds(chassisSpeedsVector.get(0, 0), chassisSpeedsVector.get(1, 0),
|
||||
chassisSpeedsVector.get(2, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct inverse kinematics matrix from wheel locations.
|
||||
*
|
||||
* @param fl The location of the front-left wheel relative to the physical center of the robot.
|
||||
* @param fr The location of the front-right wheel relative to the physical center of the robot.
|
||||
* @param rl The location of the rear-left wheel relative to the physical center of the robot.
|
||||
* @param rr The location of the rear-right wheel relative to the physical center of the robot.
|
||||
*/
|
||||
private void setInverseKinematics(Translation2d fl, Translation2d fr,
|
||||
Translation2d rl, Translation2d rr) {
|
||||
m_inverseKinematics.setRow(0, 0, 1, -1, -(fl.getX() + fl.getY()));
|
||||
m_inverseKinematics.setRow(1, 0, 1, 1, fr.getX() - fr.getY());
|
||||
m_inverseKinematics.setRow(2, 0, 1, 1, rl.getX() - rl.getY());
|
||||
m_inverseKinematics.setRow(3, 0, 1, -1, -(rr.getX() + rr.getY()));
|
||||
m_inverseKinematics = m_inverseKinematics.scale(1.0 / Math.sqrt(2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.kinematics;
|
||||
|
||||
/**
|
||||
* Represents the motor voltages for a mecanum drive drivetrain.
|
||||
*/
|
||||
@SuppressWarnings("MemberName")
|
||||
public class MecanumDriveMotorVoltages {
|
||||
/**
|
||||
* Voltage of the front left motor.
|
||||
*/
|
||||
public double frontLeftVoltage;
|
||||
|
||||
/**
|
||||
* Voltage of the front right motor.
|
||||
*/
|
||||
public double frontRightVoltage;
|
||||
|
||||
/**
|
||||
* Voltage of the rear left motor.
|
||||
*/
|
||||
public double rearLeftVoltage;
|
||||
|
||||
/**
|
||||
* Voltage of the rear right motor.
|
||||
*/
|
||||
public double rearRightVoltage;
|
||||
|
||||
/**
|
||||
* Constructs a MecanumDriveMotorVoltages with zeros for all member fields.
|
||||
*/
|
||||
public MecanumDriveMotorVoltages() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a MecanumDriveMotorVoltages.
|
||||
*
|
||||
* @param frontLeftVoltage Voltage of the front left motor.
|
||||
* @param frontRightVoltage Voltage of the front right motor.
|
||||
* @param rearLeftVoltage Voltage of the rear left motor.
|
||||
* @param rearRightVoltage Voltage of the rear right motor.
|
||||
*/
|
||||
public MecanumDriveMotorVoltages(double frontLeftVoltage,
|
||||
double frontRightVoltage,
|
||||
double rearLeftVoltage,
|
||||
double rearRightVoltage) {
|
||||
this.frontLeftVoltage = frontLeftVoltage;
|
||||
this.frontRightVoltage = frontRightVoltage;
|
||||
this.rearLeftVoltage = rearLeftVoltage;
|
||||
this.rearRightVoltage = rearRightVoltage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("MecanumDriveMotorVoltages(Front Left: %.2f V, Front Right: %.2f V, "
|
||||
+ "Rear Left: %.2f V, Rear Right: %.2f V)", frontLeftVoltage, frontRightVoltage,
|
||||
rearLeftVoltage, rearRightVoltage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.kinematics;
|
||||
|
||||
import edu.wpi.first.math.MathSharedStore;
|
||||
import edu.wpi.first.math.MathUsageId;
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Twist2d;
|
||||
import edu.wpi.first.wpiutil.WPIUtilJNI;
|
||||
|
||||
/**
|
||||
* Class for mecanum drive odometry. Odometry allows you to track the robot's
|
||||
* position on the field over a course of a match using readings from your
|
||||
* mecanum wheel encoders.
|
||||
*
|
||||
* <p>Teams can use odometry during the autonomous period for complex tasks like
|
||||
* path following. Furthermore, odometry can be used for latency compensation
|
||||
* when using computer-vision systems.
|
||||
*/
|
||||
public class MecanumDriveOdometry {
|
||||
private final MecanumDriveKinematics m_kinematics;
|
||||
private Pose2d m_poseMeters;
|
||||
private double m_prevTimeSeconds = -1;
|
||||
|
||||
private Rotation2d m_gyroOffset;
|
||||
private Rotation2d m_previousAngle;
|
||||
|
||||
/**
|
||||
* Constructs a MecanumDriveOdometry object.
|
||||
*
|
||||
* @param kinematics The mecanum drive kinematics for your drivetrain.
|
||||
* @param gyroAngle The angle reported by the gyroscope.
|
||||
* @param initialPoseMeters The starting position of the robot on the field.
|
||||
*/
|
||||
public MecanumDriveOdometry(MecanumDriveKinematics kinematics, Rotation2d gyroAngle,
|
||||
Pose2d initialPoseMeters) {
|
||||
m_kinematics = kinematics;
|
||||
m_poseMeters = initialPoseMeters;
|
||||
m_gyroOffset = m_poseMeters.getRotation().minus(gyroAngle);
|
||||
m_previousAngle = initialPoseMeters.getRotation();
|
||||
MathSharedStore.reportUsage(MathUsageId.kOdometry_MecanumDrive, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a MecanumDriveOdometry object with the default pose at the origin.
|
||||
*
|
||||
* @param kinematics The mecanum drive kinematics for your drivetrain.
|
||||
* @param gyroAngle The angle reported by the gyroscope.
|
||||
*/
|
||||
public MecanumDriveOdometry(MecanumDriveKinematics kinematics, Rotation2d gyroAngle) {
|
||||
this(kinematics, gyroAngle, new Pose2d());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the robot's position on the field.
|
||||
*
|
||||
* <p>The gyroscope angle does not need to be reset here on the user's robot code.
|
||||
* The library automatically takes care of offsetting the gyro angle.
|
||||
*
|
||||
* @param poseMeters The position on the field that your robot is at.
|
||||
* @param gyroAngle The angle reported by the gyroscope.
|
||||
*/
|
||||
public void resetPosition(Pose2d poseMeters, Rotation2d gyroAngle) {
|
||||
m_poseMeters = poseMeters;
|
||||
m_previousAngle = poseMeters.getRotation();
|
||||
m_gyroOffset = m_poseMeters.getRotation().minus(gyroAngle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the robot on the field.
|
||||
*
|
||||
* @return The pose of the robot (x and y are in meters).
|
||||
*/
|
||||
public Pose2d getPoseMeters() {
|
||||
return m_poseMeters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the robot's position on the field using forward kinematics and
|
||||
* integration of the pose over time. This method takes in the current time as
|
||||
* a parameter to calculate period (difference between two timestamps). The
|
||||
* period is used to calculate the change in distance from a velocity. This
|
||||
* also takes in an angle parameter which is used instead of the
|
||||
* angular rate that is calculated from forward kinematics.
|
||||
*
|
||||
* @param currentTimeSeconds The current time in seconds.
|
||||
* @param gyroAngle The angle reported by the gyroscope.
|
||||
* @param wheelSpeeds The current wheel speeds.
|
||||
* @return The new pose of the robot.
|
||||
*/
|
||||
public Pose2d updateWithTime(double currentTimeSeconds, Rotation2d gyroAngle,
|
||||
MecanumDriveWheelSpeeds wheelSpeeds) {
|
||||
double period = m_prevTimeSeconds >= 0 ? currentTimeSeconds - m_prevTimeSeconds : 0.0;
|
||||
m_prevTimeSeconds = currentTimeSeconds;
|
||||
|
||||
var angle = gyroAngle.plus(m_gyroOffset);
|
||||
|
||||
var chassisState = m_kinematics.toChassisSpeeds(wheelSpeeds);
|
||||
var newPose = m_poseMeters.exp(
|
||||
new Twist2d(chassisState.vxMetersPerSecond * period,
|
||||
chassisState.vyMetersPerSecond * period,
|
||||
angle.minus(m_previousAngle).getRadians()));
|
||||
|
||||
m_previousAngle = angle;
|
||||
m_poseMeters = new Pose2d(newPose.getTranslation(), angle);
|
||||
return m_poseMeters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the robot's position on the field using forward kinematics and
|
||||
* integration of the pose over time. This method automatically calculates the
|
||||
* current time to calculate period (difference between two timestamps). The
|
||||
* period is used to calculate the change in distance from a velocity. This
|
||||
* also takes in an angle parameter which is used instead of the
|
||||
* angular rate that is calculated from forward kinematics.
|
||||
*
|
||||
* @param gyroAngle The angle reported by the gyroscope.
|
||||
* @param wheelSpeeds The current wheel speeds.
|
||||
* @return The new pose of the robot.
|
||||
*/
|
||||
public Pose2d update(Rotation2d gyroAngle,
|
||||
MecanumDriveWheelSpeeds wheelSpeeds) {
|
||||
return updateWithTime(WPIUtilJNI.now() * 1.0e-6, gyroAngle,
|
||||
wheelSpeeds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.kinematics;
|
||||
|
||||
import java.util.stream.DoubleStream;
|
||||
|
||||
@SuppressWarnings("MemberName")
|
||||
public class MecanumDriveWheelSpeeds {
|
||||
/**
|
||||
* Speed of the front left wheel.
|
||||
*/
|
||||
public double frontLeftMetersPerSecond;
|
||||
|
||||
/**
|
||||
* Speed of the front right wheel.
|
||||
*/
|
||||
public double frontRightMetersPerSecond;
|
||||
|
||||
/**
|
||||
* Speed of the rear left wheel.
|
||||
*/
|
||||
public double rearLeftMetersPerSecond;
|
||||
|
||||
/**
|
||||
* Speed of the rear right wheel.
|
||||
*/
|
||||
public double rearRightMetersPerSecond;
|
||||
|
||||
/**
|
||||
* Constructs a MecanumDriveWheelSpeeds with zeros for all member fields.
|
||||
*/
|
||||
public MecanumDriveWheelSpeeds() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a MecanumDriveWheelSpeeds.
|
||||
*
|
||||
* @param frontLeftMetersPerSecond Speed of the front left wheel.
|
||||
* @param frontRightMetersPerSecond Speed of the front right wheel.
|
||||
* @param rearLeftMetersPerSecond Speed of the rear left wheel.
|
||||
* @param rearRightMetersPerSecond Speed of the rear right wheel.
|
||||
*/
|
||||
public MecanumDriveWheelSpeeds(double frontLeftMetersPerSecond,
|
||||
double frontRightMetersPerSecond,
|
||||
double rearLeftMetersPerSecond,
|
||||
double rearRightMetersPerSecond) {
|
||||
this.frontLeftMetersPerSecond = frontLeftMetersPerSecond;
|
||||
this.frontRightMetersPerSecond = frontRightMetersPerSecond;
|
||||
this.rearLeftMetersPerSecond = rearLeftMetersPerSecond;
|
||||
this.rearRightMetersPerSecond = rearRightMetersPerSecond;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the wheel speeds using some max attainable speed. Sometimes,
|
||||
* after inverse kinematics, the requested speed from a/several modules may be
|
||||
* above the max attainable speed for the driving motor on that module. To fix
|
||||
* this issue, one can "normalize" all the wheel speeds to make sure that all
|
||||
* requested module speeds are below the absolute threshold, while maintaining
|
||||
* the ratio of speeds between modules.
|
||||
*
|
||||
* @param attainableMaxSpeedMetersPerSecond The absolute max speed that a wheel can reach.
|
||||
*/
|
||||
public void normalize(double attainableMaxSpeedMetersPerSecond) {
|
||||
double realMaxSpeed = DoubleStream.of(frontLeftMetersPerSecond,
|
||||
frontRightMetersPerSecond, rearLeftMetersPerSecond, rearRightMetersPerSecond)
|
||||
.max().getAsDouble();
|
||||
|
||||
if (realMaxSpeed > attainableMaxSpeedMetersPerSecond) {
|
||||
frontLeftMetersPerSecond = frontLeftMetersPerSecond / realMaxSpeed
|
||||
* attainableMaxSpeedMetersPerSecond;
|
||||
frontRightMetersPerSecond = frontRightMetersPerSecond / realMaxSpeed
|
||||
* attainableMaxSpeedMetersPerSecond;
|
||||
rearLeftMetersPerSecond = rearLeftMetersPerSecond / realMaxSpeed
|
||||
* attainableMaxSpeedMetersPerSecond;
|
||||
rearRightMetersPerSecond = rearRightMetersPerSecond / realMaxSpeed
|
||||
* attainableMaxSpeedMetersPerSecond;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("MecanumDriveWheelSpeeds(Front Left: %.2f m/s, Front Right: %.2f m/s, "
|
||||
+ "Rear Left: %.2f m/s, Rear Right: %.2f m/s)", frontLeftMetersPerSecond,
|
||||
frontRightMetersPerSecond, rearLeftMetersPerSecond, rearRightMetersPerSecond);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.kinematics;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.ejml.simple.SimpleMatrix;
|
||||
|
||||
import edu.wpi.first.math.MathSharedStore;
|
||||
import edu.wpi.first.math.MathUsageId;
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Translation2d;
|
||||
|
||||
/**
|
||||
* Helper class that converts a chassis velocity (dx, dy, and dtheta components)
|
||||
* into individual module states (speed and angle).
|
||||
*
|
||||
* <p>The inverse kinematics (converting from a desired chassis velocity to
|
||||
* individual module states) uses the relative locations of the modules with
|
||||
* respect to the center of rotation. The center of rotation for inverse
|
||||
* kinematics is also variable. This means that you can set your set your center
|
||||
* of rotation in a corner of the robot to perform special evasion manuevers.
|
||||
*
|
||||
* <p>Forward kinematics (converting an array of module states into the overall
|
||||
* chassis motion) is performs the exact opposite of what inverse kinematics
|
||||
* does. Since this is an overdetermined system (more equations than variables),
|
||||
* we use a least-squares approximation.
|
||||
*
|
||||
* <p>The inverse kinematics: [moduleStates] = [moduleLocations] * [chassisSpeeds]
|
||||
* We take the Moore-Penrose pseudoinverse of [moduleLocations] and then
|
||||
* multiply by [moduleStates] to get our chassis speeds.
|
||||
*
|
||||
* <p>Forward kinematics is also used for odometry -- determining the position of
|
||||
* the robot on the field using encoders and a gyro.
|
||||
*/
|
||||
public class SwerveDriveKinematics {
|
||||
private final SimpleMatrix m_inverseKinematics;
|
||||
private final SimpleMatrix m_forwardKinematics;
|
||||
|
||||
private final int m_numModules;
|
||||
private final Translation2d[] m_modules;
|
||||
private Translation2d m_prevCoR = new Translation2d();
|
||||
|
||||
/**
|
||||
* Constructs a swerve drive kinematics object. This takes in a variable
|
||||
* number of wheel locations as Translation2ds. The order in which you pass in
|
||||
* the wheel locations is the same order that you will recieve the module
|
||||
* states when performing inverse kinematics. It is also expected that you
|
||||
* pass in the module states in the same order when calling the forward
|
||||
* kinematics methods.
|
||||
*
|
||||
* @param wheelsMeters The locations of the wheels relative to the physical center
|
||||
* of the robot.
|
||||
*/
|
||||
public SwerveDriveKinematics(Translation2d... wheelsMeters) {
|
||||
if (wheelsMeters.length < 2) {
|
||||
throw new IllegalArgumentException("A swerve drive requires at least two modules");
|
||||
}
|
||||
m_numModules = wheelsMeters.length;
|
||||
m_modules = Arrays.copyOf(wheelsMeters, m_numModules);
|
||||
m_inverseKinematics = new SimpleMatrix(m_numModules * 2, 3);
|
||||
|
||||
for (int i = 0; i < m_numModules; i++) {
|
||||
m_inverseKinematics.setRow(i * 2 + 0, 0, /* Start Data */ 1, 0, -m_modules[i].getY());
|
||||
m_inverseKinematics.setRow(i * 2 + 1, 0, /* Start Data */ 0, 1, +m_modules[i].getX());
|
||||
}
|
||||
m_forwardKinematics = m_inverseKinematics.pseudoInverse();
|
||||
|
||||
MathSharedStore.reportUsage(MathUsageId.kKinematics_SwerveDrive, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs inverse kinematics to return the module states from a desired
|
||||
* chassis velocity. This method is often used to convert joystick values into
|
||||
* module speeds and angles.
|
||||
*
|
||||
* <p>This function also supports variable centers of rotation. During normal
|
||||
* operations, the center of rotation is usually the same as the physical
|
||||
* center of the robot; therefore, the argument is defaulted to that use case.
|
||||
* However, if you wish to change the center of rotation for evasive
|
||||
* manuevers, vision alignment, or for any other use case, you can do so.
|
||||
*
|
||||
* @param chassisSpeeds The desired chassis speed.
|
||||
* @param centerOfRotationMeters The center of rotation. For example, if you set the
|
||||
* center of rotation at one corner of the robot and provide
|
||||
* a chassis speed that only has a dtheta component, the robot
|
||||
* will rotate around that corner.
|
||||
* @return An array containing the module states. Use caution because these
|
||||
* module states are not normalized. Sometimes, a user input may cause one of
|
||||
* the module speeds to go above the attainable max velocity. Use the
|
||||
* {@link #normalizeWheelSpeeds(SwerveModuleState[], double) normalizeWheelSpeeds}
|
||||
* function to rectify this issue.
|
||||
*/
|
||||
@SuppressWarnings({"LocalVariableName", "PMD.AvoidInstantiatingObjectsInLoops"})
|
||||
public SwerveModuleState[] toSwerveModuleStates(ChassisSpeeds chassisSpeeds,
|
||||
Translation2d centerOfRotationMeters) {
|
||||
if (!centerOfRotationMeters.equals(m_prevCoR)) {
|
||||
for (int i = 0; i < m_numModules; i++) {
|
||||
m_inverseKinematics.setRow(i * 2 + 0, 0, /* Start Data */ 1, 0,
|
||||
-m_modules[i].getY() + centerOfRotationMeters.getY());
|
||||
m_inverseKinematics.setRow(i * 2 + 1, 0, /* Start Data */ 0, 1,
|
||||
+m_modules[i].getX() - centerOfRotationMeters.getX());
|
||||
}
|
||||
m_prevCoR = centerOfRotationMeters;
|
||||
}
|
||||
|
||||
var chassisSpeedsVector = new SimpleMatrix(3, 1);
|
||||
chassisSpeedsVector.setColumn(0, 0,
|
||||
chassisSpeeds.vxMetersPerSecond, chassisSpeeds.vyMetersPerSecond,
|
||||
chassisSpeeds.omegaRadiansPerSecond);
|
||||
|
||||
var moduleStatesMatrix = m_inverseKinematics.mult(chassisSpeedsVector);
|
||||
SwerveModuleState[] moduleStates = new SwerveModuleState[m_numModules];
|
||||
|
||||
for (int i = 0; i < m_numModules; i++) {
|
||||
double x = moduleStatesMatrix.get(i * 2, 0);
|
||||
double y = moduleStatesMatrix.get(i * 2 + 1, 0);
|
||||
|
||||
double speed = Math.hypot(x, y);
|
||||
Rotation2d angle = new Rotation2d(x, y);
|
||||
|
||||
moduleStates[i] = new SwerveModuleState(speed, angle);
|
||||
}
|
||||
|
||||
return moduleStates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs inverse kinematics. See {@link #toSwerveModuleStates(ChassisSpeeds, Translation2d)}
|
||||
* toSwerveModuleStates for more information.
|
||||
*
|
||||
* @param chassisSpeeds The desired chassis speed.
|
||||
* @return An array containing the module states.
|
||||
*/
|
||||
public SwerveModuleState[] toSwerveModuleStates(ChassisSpeeds chassisSpeeds) {
|
||||
return toSwerveModuleStates(chassisSpeeds, new Translation2d());
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs forward kinematics to return the resulting chassis state from the
|
||||
* given module states. This method is often used for odometry -- determining
|
||||
* the robot's position on the field using data from the real-world speed and
|
||||
* angle of each module on the robot.
|
||||
*
|
||||
* @param wheelStates The state of the modules (as a SwerveModuleState type)
|
||||
* as measured from respective encoders and gyros. The order of the swerve
|
||||
* module states should be same as passed into the constructor of this class.
|
||||
* @return The resulting chassis speed.
|
||||
*/
|
||||
public ChassisSpeeds toChassisSpeeds(SwerveModuleState... wheelStates) {
|
||||
if (wheelStates.length != m_numModules) {
|
||||
throw new IllegalArgumentException(
|
||||
"Number of modules is not consistent with number of wheel locations provided in "
|
||||
+ "constructor"
|
||||
);
|
||||
}
|
||||
var moduleStatesMatrix = new SimpleMatrix(m_numModules * 2, 1);
|
||||
|
||||
for (int i = 0; i < m_numModules; i++) {
|
||||
var module = wheelStates[i];
|
||||
moduleStatesMatrix.set(i * 2, 0, module.speedMetersPerSecond * module.angle.getCos());
|
||||
moduleStatesMatrix.set(i * 2 + 1, module.speedMetersPerSecond * module.angle.getSin());
|
||||
}
|
||||
|
||||
var chassisSpeedsVector = m_forwardKinematics.mult(moduleStatesMatrix);
|
||||
return new ChassisSpeeds(chassisSpeedsVector.get(0, 0), chassisSpeedsVector.get(1, 0),
|
||||
chassisSpeedsVector.get(2, 0));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the wheel speeds using some max attainable speed. Sometimes,
|
||||
* after inverse kinematics, the requested speed from a/several modules may be
|
||||
* above the max attainable speed for the driving motor on that module. To fix
|
||||
* this issue, one can "normalize" all the wheel speeds to make sure that all
|
||||
* requested module speeds are below the absolute threshold, while maintaining
|
||||
* the ratio of speeds between modules.
|
||||
*
|
||||
* @param moduleStates Reference to array of module states. The array will be
|
||||
* mutated with the normalized speeds!
|
||||
* @param attainableMaxSpeedMetersPerSecond The absolute max speed that a module can reach.
|
||||
*/
|
||||
public static void normalizeWheelSpeeds(SwerveModuleState[] moduleStates,
|
||||
double attainableMaxSpeedMetersPerSecond) {
|
||||
double realMaxSpeed = Collections.max(Arrays.asList(moduleStates)).speedMetersPerSecond;
|
||||
if (realMaxSpeed > attainableMaxSpeedMetersPerSecond) {
|
||||
for (SwerveModuleState moduleState : moduleStates) {
|
||||
moduleState.speedMetersPerSecond = moduleState.speedMetersPerSecond / realMaxSpeed
|
||||
* attainableMaxSpeedMetersPerSecond;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.kinematics;
|
||||
|
||||
import edu.wpi.first.math.MathSharedStore;
|
||||
import edu.wpi.first.math.MathUsageId;
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Twist2d;
|
||||
import edu.wpi.first.wpiutil.WPIUtilJNI;
|
||||
|
||||
/**
|
||||
* Class for swerve drive odometry. Odometry allows you to track the robot's
|
||||
* position on the field over a course of a match using readings from your
|
||||
* swerve drive encoders and swerve azimuth encoders.
|
||||
*
|
||||
* <p>Teams can use odometry during the autonomous period for complex tasks like
|
||||
* path following. Furthermore, odometry can be used for latency compensation
|
||||
* when using computer-vision systems.
|
||||
*/
|
||||
public class SwerveDriveOdometry {
|
||||
private final SwerveDriveKinematics m_kinematics;
|
||||
private Pose2d m_poseMeters;
|
||||
private double m_prevTimeSeconds = -1;
|
||||
|
||||
private Rotation2d m_gyroOffset;
|
||||
private Rotation2d m_previousAngle;
|
||||
|
||||
/**
|
||||
* Constructs a SwerveDriveOdometry object.
|
||||
*
|
||||
* @param kinematics The swerve drive kinematics for your drivetrain.
|
||||
* @param gyroAngle The angle reported by the gyroscope.
|
||||
* @param initialPose The starting position of the robot on the field.
|
||||
*/
|
||||
public SwerveDriveOdometry(SwerveDriveKinematics kinematics, Rotation2d gyroAngle,
|
||||
Pose2d initialPose) {
|
||||
m_kinematics = kinematics;
|
||||
m_poseMeters = initialPose;
|
||||
m_gyroOffset = m_poseMeters.getRotation().minus(gyroAngle);
|
||||
m_previousAngle = initialPose.getRotation();
|
||||
MathSharedStore.reportUsage(MathUsageId.kOdometry_SwerveDrive, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a SwerveDriveOdometry object with the default pose at the origin.
|
||||
*
|
||||
* @param kinematics The swerve drive kinematics for your drivetrain.
|
||||
* @param gyroAngle The angle reported by the gyroscope.
|
||||
*/
|
||||
public SwerveDriveOdometry(SwerveDriveKinematics kinematics, Rotation2d gyroAngle) {
|
||||
this(kinematics, gyroAngle, new Pose2d());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the robot's position on the field.
|
||||
*
|
||||
* <p>The gyroscope angle does not need to be reset here on the user's robot code.
|
||||
* The library automatically takes care of offsetting the gyro angle.
|
||||
*
|
||||
* @param pose The position on the field that your robot is at.
|
||||
* @param gyroAngle The angle reported by the gyroscope.
|
||||
*/
|
||||
public void resetPosition(Pose2d pose, Rotation2d gyroAngle) {
|
||||
m_poseMeters = pose;
|
||||
m_previousAngle = pose.getRotation();
|
||||
m_gyroOffset = m_poseMeters.getRotation().minus(gyroAngle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the robot on the field.
|
||||
*
|
||||
* @return The pose of the robot (x and y are in meters).
|
||||
*/
|
||||
public Pose2d getPoseMeters() {
|
||||
return m_poseMeters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the robot's position on the field using forward kinematics and
|
||||
* integration of the pose over time. This method takes in the current time as
|
||||
* a parameter to calculate period (difference between two timestamps). The
|
||||
* period is used to calculate the change in distance from a velocity. This
|
||||
* also takes in an angle parameter which is used instead of the
|
||||
* angular rate that is calculated from forward kinematics.
|
||||
*
|
||||
* @param currentTimeSeconds The current time in seconds.
|
||||
* @param gyroAngle The angle reported by the gyroscope.
|
||||
* @param moduleStates The current state of all swerve modules. Please provide
|
||||
* the states in the same order in which you instantiated your
|
||||
* SwerveDriveKinematics.
|
||||
* @return The new pose of the robot.
|
||||
*/
|
||||
public Pose2d updateWithTime(double currentTimeSeconds, Rotation2d gyroAngle,
|
||||
SwerveModuleState... moduleStates) {
|
||||
double period = m_prevTimeSeconds >= 0 ? currentTimeSeconds - m_prevTimeSeconds : 0.0;
|
||||
m_prevTimeSeconds = currentTimeSeconds;
|
||||
|
||||
var angle = gyroAngle.plus(m_gyroOffset);
|
||||
|
||||
var chassisState = m_kinematics.toChassisSpeeds(moduleStates);
|
||||
var newPose = m_poseMeters.exp(
|
||||
new Twist2d(chassisState.vxMetersPerSecond * period,
|
||||
chassisState.vyMetersPerSecond * period,
|
||||
angle.minus(m_previousAngle).getRadians()));
|
||||
|
||||
m_previousAngle = angle;
|
||||
m_poseMeters = new Pose2d(newPose.getTranslation(), angle);
|
||||
|
||||
return m_poseMeters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the robot's position on the field using forward kinematics and
|
||||
* integration of the pose over time. This method automatically calculates the
|
||||
* current time to calculate period (difference between two timestamps). The
|
||||
* period is used to calculate the change in distance from a velocity. This
|
||||
* also takes in an angle parameter which is used instead of the angular
|
||||
* rate that is calculated from forward kinematics.
|
||||
*
|
||||
* @param gyroAngle The angle reported by the gyroscope.
|
||||
* @param moduleStates The current state of all swerve modules. Please provide
|
||||
* the states in the same order in which you instantiated your
|
||||
* SwerveDriveKinematics.
|
||||
* @return The new pose of the robot.
|
||||
*/
|
||||
public Pose2d update(Rotation2d gyroAngle, SwerveModuleState... moduleStates) {
|
||||
return updateWithTime(WPIUtilJNI.now() * 1.0e-6, gyroAngle, moduleStates);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.kinematics;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
|
||||
/**
|
||||
* Represents the state of one swerve module.
|
||||
*/
|
||||
@SuppressWarnings("MemberName")
|
||||
public class SwerveModuleState implements Comparable<SwerveModuleState> {
|
||||
|
||||
/**
|
||||
* Speed of the wheel of the module.
|
||||
*/
|
||||
public double speedMetersPerSecond;
|
||||
|
||||
/**
|
||||
* Angle of the module.
|
||||
*/
|
||||
public Rotation2d angle = Rotation2d.fromDegrees(0);
|
||||
|
||||
/**
|
||||
* Constructs a SwerveModuleState with zeros for speed and angle.
|
||||
*/
|
||||
public SwerveModuleState() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a SwerveModuleState.
|
||||
*
|
||||
* @param speedMetersPerSecond The speed of the wheel of the module.
|
||||
* @param angle The angle of the module.
|
||||
*/
|
||||
public SwerveModuleState(double speedMetersPerSecond, Rotation2d angle) {
|
||||
this.speedMetersPerSecond = speedMetersPerSecond;
|
||||
this.angle = angle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two swerve module states. One swerve module is "greater" than the other if its speed
|
||||
* is higher than the other.
|
||||
*
|
||||
* @param o The other swerve module.
|
||||
* @return 1 if this is greater, 0 if both are equal, -1 if other is greater.
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("ParameterName")
|
||||
public int compareTo(SwerveModuleState o) {
|
||||
return Double.compare(this.speedMetersPerSecond, o.speedMetersPerSecond);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("SwerveModuleState(Speed: %.2f m/s, Angle: %s)", speedMetersPerSecond,
|
||||
angle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.spline;
|
||||
|
||||
import org.ejml.simple.SimpleMatrix;
|
||||
|
||||
public class CubicHermiteSpline extends Spline {
|
||||
private static SimpleMatrix hermiteBasis;
|
||||
private final SimpleMatrix m_coefficients;
|
||||
|
||||
/**
|
||||
* Constructs a cubic hermite spline with the specified control vectors. Each
|
||||
* control vector contains info about the location of the point and its first
|
||||
* derivative.
|
||||
*
|
||||
* @param xInitialControlVector The control vector for the initial point in
|
||||
* the x dimension.
|
||||
* @param xFinalControlVector The control vector for the final point in
|
||||
* the x dimension.
|
||||
* @param yInitialControlVector The control vector for the initial point in
|
||||
* the y dimension.
|
||||
* @param yFinalControlVector The control vector for the final point in
|
||||
* the y dimension.
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
public CubicHermiteSpline(double[] xInitialControlVector, double[] xFinalControlVector,
|
||||
double[] yInitialControlVector, double[] yFinalControlVector) {
|
||||
super(3);
|
||||
|
||||
// Populate the coefficients for the actual spline equations.
|
||||
// Row 0 is x coefficients
|
||||
// Row 1 is y coefficients
|
||||
final var hermite = makeHermiteBasis();
|
||||
final var x = getControlVectorFromArrays(xInitialControlVector, xFinalControlVector);
|
||||
final var y = getControlVectorFromArrays(yInitialControlVector, yFinalControlVector);
|
||||
|
||||
final var xCoeffs = (hermite.mult(x)).transpose();
|
||||
final var yCoeffs = (hermite.mult(y)).transpose();
|
||||
|
||||
m_coefficients = new SimpleMatrix(6, 4);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
m_coefficients.set(0, i, xCoeffs.get(0, i));
|
||||
m_coefficients.set(1, i, yCoeffs.get(0, i));
|
||||
|
||||
// Populate Row 2 and Row 3 with the derivatives of the equations above.
|
||||
// Then populate row 4 and 5 with the second derivatives.
|
||||
// Here, we are multiplying by (3 - i) to manually take the derivative. The
|
||||
// power of the term in index 0 is 3, index 1 is 2 and so on. To find the
|
||||
// coefficient of the derivative, we can use the power rule and multiply
|
||||
// the existing coefficient by its power.
|
||||
m_coefficients.set(2, i, m_coefficients.get(0, i) * (3 - i));
|
||||
m_coefficients.set(3, i, m_coefficients.get(1, i) * (3 - i));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
// Here, we are multiplying by (2 - i) to manually take the derivative. The
|
||||
// power of the term in index 0 is 2, index 1 is 1 and so on. To find the
|
||||
// coefficient of the derivative, we can use the power rule and multiply
|
||||
// the existing coefficient by its power.
|
||||
m_coefficients.set(4, i, m_coefficients.get(2, i) * (2 - i));
|
||||
m_coefficients.set(5, i, m_coefficients.get(3, i) * (2 - i));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the coefficients matrix.
|
||||
*
|
||||
* @return The coefficients matrix.
|
||||
*/
|
||||
@Override
|
||||
protected SimpleMatrix getCoefficients() {
|
||||
return m_coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hermite basis matrix for cubic hermite spline interpolation.
|
||||
*
|
||||
* @return The hermite basis matrix for cubic hermite spline interpolation.
|
||||
*/
|
||||
private SimpleMatrix makeHermiteBasis() {
|
||||
if (hermiteBasis == null) {
|
||||
hermiteBasis = new SimpleMatrix(4, 4, true, new double[]{
|
||||
+2.0, +1.0, -2.0, +1.0,
|
||||
-3.0, -2.0, +3.0, -1.0,
|
||||
+0.0, +1.0, +0.0, +0.0,
|
||||
+1.0, +0.0, +0.0, +0.0
|
||||
});
|
||||
}
|
||||
return hermiteBasis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the control vector for each dimension as a matrix from the
|
||||
* user-provided arrays in the constructor.
|
||||
*
|
||||
* @param initialVector The control vector for the initial point.
|
||||
* @param finalVector The control vector for the final point.
|
||||
* @return The control vector matrix for a dimension.
|
||||
*/
|
||||
private SimpleMatrix getControlVectorFromArrays(double[] initialVector, double[] finalVector) {
|
||||
if (initialVector.length != 2 || finalVector.length != 2) {
|
||||
throw new IllegalArgumentException("Size of vectors must be 2");
|
||||
}
|
||||
return new SimpleMatrix(4, 1, true, new double[]{
|
||||
initialVector[0], initialVector[1],
|
||||
finalVector[0], finalVector[1]});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.spline;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
|
||||
/**
|
||||
* Represents a pair of a pose and a curvature.
|
||||
*/
|
||||
@SuppressWarnings("MemberName")
|
||||
public class PoseWithCurvature {
|
||||
// Represents the pose.
|
||||
public Pose2d poseMeters;
|
||||
|
||||
// Represents the curvature.
|
||||
public double curvatureRadPerMeter;
|
||||
|
||||
/**
|
||||
* Constructs a PoseWithCurvature.
|
||||
*
|
||||
* @param poseMeters The pose.
|
||||
* @param curvatureRadPerMeter The curvature.
|
||||
*/
|
||||
public PoseWithCurvature(Pose2d poseMeters, double curvatureRadPerMeter) {
|
||||
this.poseMeters = poseMeters;
|
||||
this.curvatureRadPerMeter = curvatureRadPerMeter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a PoseWithCurvature with default values.
|
||||
*/
|
||||
public PoseWithCurvature() {
|
||||
poseMeters = new Pose2d();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.spline;
|
||||
|
||||
import org.ejml.simple.SimpleMatrix;
|
||||
|
||||
public class QuinticHermiteSpline extends Spline {
|
||||
private static SimpleMatrix hermiteBasis;
|
||||
private final SimpleMatrix m_coefficients;
|
||||
|
||||
/**
|
||||
* Constructs a quintic hermite spline with the specified control vectors.
|
||||
* Each control vector contains into about the location of the point, its
|
||||
* first derivative, and its second derivative.
|
||||
*
|
||||
* @param xInitialControlVector The control vector for the initial point in
|
||||
* the x dimension.
|
||||
* @param xFinalControlVector The control vector for the final point in
|
||||
* the x dimension.
|
||||
* @param yInitialControlVector The control vector for the initial point in
|
||||
* the y dimension.
|
||||
* @param yFinalControlVector The control vector for the final point in
|
||||
* the y dimension.
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
public QuinticHermiteSpline(double[] xInitialControlVector, double[] xFinalControlVector,
|
||||
double[] yInitialControlVector, double[] yFinalControlVector) {
|
||||
super(5);
|
||||
|
||||
// Populate the coefficients for the actual spline equations.
|
||||
// Row 0 is x coefficients
|
||||
// Row 1 is y coefficients
|
||||
final var hermite = makeHermiteBasis();
|
||||
final var x = getControlVectorFromArrays(xInitialControlVector, xFinalControlVector);
|
||||
final var y = getControlVectorFromArrays(yInitialControlVector, yFinalControlVector);
|
||||
|
||||
final var xCoeffs = (hermite.mult(x)).transpose();
|
||||
final var yCoeffs = (hermite.mult(y)).transpose();
|
||||
|
||||
m_coefficients = new SimpleMatrix(6, 6);
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
m_coefficients.set(0, i, xCoeffs.get(0, i));
|
||||
m_coefficients.set(1, i, yCoeffs.get(0, i));
|
||||
}
|
||||
for (int i = 0; i < 6; i++) {
|
||||
// Populate Row 2 and Row 3 with the derivatives of the equations above.
|
||||
// Here, we are multiplying by (5 - i) to manually take the derivative. The
|
||||
// power of the term in index 0 is 5, index 1 is 4 and so on. To find the
|
||||
// coefficient of the derivative, we can use the power rule and multiply
|
||||
// the existing coefficient by its power.
|
||||
m_coefficients.set(2, i, m_coefficients.get(0, i) * (5 - i));
|
||||
m_coefficients.set(3, i, m_coefficients.get(1, i) * (5 - i));
|
||||
}
|
||||
for (int i = 0; i < 5; i++) {
|
||||
// Then populate row 4 and 5 with the second derivatives.
|
||||
// Here, we are multiplying by (4 - i) to manually take the derivative. The
|
||||
// power of the term in index 0 is 4, index 1 is 3 and so on. To find the
|
||||
// coefficient of the derivative, we can use the power rule and multiply
|
||||
// the existing coefficient by its power.
|
||||
m_coefficients.set(4, i, m_coefficients.get(2, i) * (4 - i));
|
||||
m_coefficients.set(5, i, m_coefficients.get(3, i) * (4 - i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the coefficients matrix.
|
||||
*
|
||||
* @return The coefficients matrix.
|
||||
*/
|
||||
@Override
|
||||
protected SimpleMatrix getCoefficients() {
|
||||
return m_coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hermite basis matrix for quintic hermite spline interpolation.
|
||||
*
|
||||
* @return The hermite basis matrix for quintic hermite spline interpolation.
|
||||
*/
|
||||
private SimpleMatrix makeHermiteBasis() {
|
||||
if (hermiteBasis == null) {
|
||||
hermiteBasis = new SimpleMatrix(6, 6, true, new double[]{
|
||||
-06.0, -03.0, -00.5, +06.0, -03.0, +00.5,
|
||||
+15.0, +08.0, +01.5, -15.0, +07.0, +01.0,
|
||||
-10.0, -06.0, -01.5, +10.0, -04.0, +00.5,
|
||||
+00.0, +00.0, +00.5, +00.0, +00.0, +00.0,
|
||||
+00.0, +01.0, +00.0, +00.0, +00.0, +00.0,
|
||||
+01.0, +00.0, +00.0, +00.0, +00.0, +00.0
|
||||
});
|
||||
}
|
||||
return hermiteBasis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the control vector for each dimension as a matrix from the
|
||||
* user-provided arrays in the constructor.
|
||||
*
|
||||
* @param initialVector The control vector for the initial point.
|
||||
* @param finalVector The control vector for the final point.
|
||||
* @return The control vector matrix for a dimension.
|
||||
*/
|
||||
private SimpleMatrix getControlVectorFromArrays(double[] initialVector, double[] finalVector) {
|
||||
if (initialVector.length != 3 || finalVector.length != 3) {
|
||||
throw new IllegalArgumentException("Size of vectors must be 3");
|
||||
}
|
||||
return new SimpleMatrix(6, 1, true, new double[]{
|
||||
initialVector[0], initialVector[1], initialVector[2],
|
||||
finalVector[0], finalVector[1], finalVector[2]});
|
||||
}
|
||||
}
|
||||
116
wpimath/src/main/java/edu/wpi/first/wpilibj/spline/Spline.java
Normal file
116
wpimath/src/main/java/edu/wpi/first/wpilibj/spline/Spline.java
Normal file
@@ -0,0 +1,116 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.spline;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.ejml.simple.SimpleMatrix;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
|
||||
/**
|
||||
* Represents a two-dimensional parametric spline that interpolates between two
|
||||
* points.
|
||||
*/
|
||||
public abstract class Spline {
|
||||
private final int m_degree;
|
||||
|
||||
/**
|
||||
* Constructs a spline with the given degree.
|
||||
*
|
||||
* @param degree The degree of the spline.
|
||||
*/
|
||||
Spline(int degree) {
|
||||
m_degree = degree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the coefficients of the spline.
|
||||
*
|
||||
* @return The coefficients of the spline.
|
||||
*/
|
||||
protected abstract SimpleMatrix getCoefficients();
|
||||
|
||||
/**
|
||||
* Gets the pose and curvature at some point t on the spline.
|
||||
*
|
||||
* @param t The point t
|
||||
* @return The pose and curvature at that point.
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
public PoseWithCurvature getPoint(double t) {
|
||||
SimpleMatrix polynomialBases = new SimpleMatrix(m_degree + 1, 1);
|
||||
final var coefficients = getCoefficients();
|
||||
|
||||
// Populate the polynomial bases.
|
||||
for (int i = 0; i <= m_degree; i++) {
|
||||
polynomialBases.set(i, 0, Math.pow(t, m_degree - i));
|
||||
}
|
||||
|
||||
// This simply multiplies by the coefficients. We need to divide out t some
|
||||
// n number of times where n is the derivative we want to take.
|
||||
SimpleMatrix combined = coefficients.mult(polynomialBases);
|
||||
|
||||
// Get x and y
|
||||
final double x = combined.get(0, 0);
|
||||
final double y = combined.get(1, 0);
|
||||
|
||||
double dx;
|
||||
double dy;
|
||||
double ddx;
|
||||
double ddy;
|
||||
|
||||
if (t == 0) {
|
||||
dx = coefficients.get(2, m_degree - 1);
|
||||
dy = coefficients.get(3, m_degree - 1);
|
||||
ddx = coefficients.get(4, m_degree - 2);
|
||||
ddy = coefficients.get(5, m_degree - 2);
|
||||
} else {
|
||||
// Divide out t once for first derivative.
|
||||
dx = combined.get(2, 0) / t;
|
||||
dy = combined.get(3, 0) / t;
|
||||
|
||||
// Divide out t twice for second derivative.
|
||||
ddx = combined.get(4, 0) / t / t;
|
||||
ddy = combined.get(5, 0) / t / t;
|
||||
}
|
||||
|
||||
// Find the curvature.
|
||||
final double curvature =
|
||||
(dx * ddy - ddx * dy) / ((dx * dx + dy * dy) * Math.hypot(dx, dy));
|
||||
|
||||
return new PoseWithCurvature(
|
||||
new Pose2d(x, y, new Rotation2d(dx, dy)),
|
||||
curvature
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a control vector for a spline.
|
||||
*
|
||||
* <p>Each element in each array represents the value of the derivative at the index. For
|
||||
* example, the value of x[2] is the second derivative in the x dimension.
|
||||
*/
|
||||
@SuppressWarnings("MemberName")
|
||||
public static class ControlVector {
|
||||
public double[] x;
|
||||
public double[] y;
|
||||
|
||||
/**
|
||||
* Instantiates a control vector.
|
||||
* @param x The x dimension of the control vector.
|
||||
* @param y The y dimension of the control vector.
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
public ControlVector(double[] x, double[] y) {
|
||||
this.x = Arrays.copyOf(x, x.length);
|
||||
this.y = Arrays.copyOf(y, y.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.spline;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Translation2d;
|
||||
|
||||
public final class SplineHelper {
|
||||
/**
|
||||
* Private constructor because this is a utility class.
|
||||
*/
|
||||
private SplineHelper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 2 cubic control vectors from a set of exterior waypoints and
|
||||
* interior translations.
|
||||
*
|
||||
* @param start The starting pose.
|
||||
* @param interiorWaypoints The interior waypoints.
|
||||
* @param end The ending pose.
|
||||
* @return 2 cubic control vectors.
|
||||
*/
|
||||
public static Spline.ControlVector[] getCubicControlVectorsFromWaypoints(
|
||||
Pose2d start, Translation2d[] interiorWaypoints, Pose2d end
|
||||
) {
|
||||
// Generate control vectors from poses.
|
||||
Spline.ControlVector initialCV;
|
||||
Spline.ControlVector endCV;
|
||||
|
||||
// Chooses a magnitude automatically that makes the splines look better.
|
||||
if (interiorWaypoints.length < 1) {
|
||||
double scalar = start.getTranslation().getDistance(end.getTranslation()) * 1.2;
|
||||
initialCV = getCubicControlVector(scalar, start);
|
||||
endCV = getCubicControlVector(scalar, end);
|
||||
} else {
|
||||
double scalar = start.getTranslation().getDistance(interiorWaypoints[0]) * 1.2;
|
||||
initialCV = getCubicControlVector(scalar, start);
|
||||
scalar = end.getTranslation().getDistance(interiorWaypoints[interiorWaypoints.length - 1])
|
||||
* 1.2;
|
||||
endCV = getCubicControlVector(scalar, end);
|
||||
}
|
||||
return new Spline.ControlVector[]{initialCV, endCV};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns quintic control vectors from a set of waypoints.
|
||||
*
|
||||
* @param waypoints The waypoints
|
||||
* @return List of control vectors
|
||||
*/
|
||||
public static List<Spline.ControlVector> getQuinticControlVectorsFromWaypoints(
|
||||
List<Pose2d> waypoints
|
||||
) {
|
||||
List<Spline.ControlVector> vectors = new ArrayList<>();
|
||||
for (int i = 0; i < waypoints.size() - 1; i++) {
|
||||
var p0 = waypoints.get(i);
|
||||
var p1 = waypoints.get(i + 1);
|
||||
|
||||
// This just makes the splines look better.
|
||||
final var scalar = 1.2 * p0.getTranslation().getDistance(p1.getTranslation());
|
||||
|
||||
vectors.add(getQuinticControlVector(scalar, p0));
|
||||
vectors.add(getQuinticControlVector(scalar, p1));
|
||||
}
|
||||
return vectors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of cubic splines corresponding to the provided control vectors. The
|
||||
* user is free to set the direction of the start and end point. The
|
||||
* directions for the middle waypoints are determined automatically to ensure
|
||||
* continuous curvature throughout the path.
|
||||
*
|
||||
* @param start The starting control vector.
|
||||
* @param waypoints The middle waypoints. This can be left blank if you only
|
||||
* wish to create a path with two waypoints.
|
||||
* @param end The ending control vector.
|
||||
* @return A vector of cubic hermite splines that interpolate through the
|
||||
* provided waypoints and control vectors.
|
||||
*/
|
||||
@SuppressWarnings({"LocalVariableName", "PMD.ExcessiveMethodLength",
|
||||
"PMD.AvoidInstantiatingObjectsInLoops"})
|
||||
public static CubicHermiteSpline[] getCubicSplinesFromControlVectors(
|
||||
Spline.ControlVector start, Translation2d[] waypoints, Spline.ControlVector end) {
|
||||
|
||||
CubicHermiteSpline[] splines = new CubicHermiteSpline[waypoints.length + 1];
|
||||
|
||||
double[] xInitial = start.x;
|
||||
double[] yInitial = start.y;
|
||||
double[] xFinal = end.x;
|
||||
double[] yFinal = end.y;
|
||||
|
||||
if (waypoints.length > 1) {
|
||||
Translation2d[] newWaypts = new Translation2d[waypoints.length + 2];
|
||||
|
||||
// Create an array of all waypoints, including the start and end.
|
||||
newWaypts[0] = new Translation2d(xInitial[0], yInitial[0]);
|
||||
System.arraycopy(waypoints, 0, newWaypts, 1, waypoints.length);
|
||||
newWaypts[newWaypts.length - 1] = new Translation2d(xFinal[0], yFinal[0]);
|
||||
|
||||
// Populate tridiagonal system for clamped cubic
|
||||
/* See:
|
||||
https://www.uio.no/studier/emner/matnat/ifi/nedlagte-emner/INF-MAT4350/h08
|
||||
/undervisningsmateriale/chap7alecture.pdf
|
||||
*/
|
||||
// Above-diagonal of tridiagonal matrix, zero-padded
|
||||
final double[] a = new double[newWaypts.length - 2];
|
||||
|
||||
// Diagonal of tridiagonal matrix
|
||||
final double[] b = new double[newWaypts.length - 2];
|
||||
Arrays.fill(b, 4.0);
|
||||
|
||||
// Below-diagonal of tridiagonal matrix, zero-padded
|
||||
final double[] c = new double[newWaypts.length - 2];
|
||||
|
||||
// rhs vectors
|
||||
final double[] dx = new double[newWaypts.length - 2];
|
||||
final double[] dy = new double[newWaypts.length - 2];
|
||||
|
||||
// solution vectors
|
||||
final double[] fx = new double[newWaypts.length - 2];
|
||||
final double[] fy = new double[newWaypts.length - 2];
|
||||
|
||||
// populate above-diagonal and below-diagonal vectors
|
||||
a[0] = 0.0;
|
||||
for (int i = 0; i < newWaypts.length - 3; i++) {
|
||||
a[i + 1] = 1;
|
||||
c[i] = 1;
|
||||
}
|
||||
c[c.length - 1] = 0.0;
|
||||
|
||||
// populate rhs vectors
|
||||
dx[0] = 3 * (newWaypts[2].getX() - newWaypts[0].getX()) - xInitial[1];
|
||||
dy[0] = 3 * (newWaypts[2].getY() - newWaypts[0].getY()) - yInitial[1];
|
||||
|
||||
if (newWaypts.length > 4) {
|
||||
for (int i = 1; i <= newWaypts.length - 4; i++) {
|
||||
// dx and dy represent the derivatives of the internal waypoints. The derivative
|
||||
// of the second internal waypoint should involve the third and first internal waypoint,
|
||||
// which have indices of 1 and 3 in the newWaypts list (which contains ALL waypoints).
|
||||
dx[i] = 3 * (newWaypts[i + 2].getX() - newWaypts[i].getX());
|
||||
dy[i] = 3 * (newWaypts[i + 2].getY() - newWaypts[i].getY());
|
||||
}
|
||||
}
|
||||
|
||||
dx[dx.length - 1] = 3 * (newWaypts[newWaypts.length - 1].getX()
|
||||
- newWaypts[newWaypts.length - 3].getX()) - xFinal[1];
|
||||
dy[dy.length - 1] = 3 * (newWaypts[newWaypts.length - 1].getY()
|
||||
- newWaypts[newWaypts.length - 3].getY()) - yFinal[1];
|
||||
|
||||
// Compute solution to tridiagonal system
|
||||
thomasAlgorithm(a, b, c, dx, fx);
|
||||
thomasAlgorithm(a, b, c, dy, fy);
|
||||
|
||||
double[] newFx = new double[fx.length + 2];
|
||||
double[] newFy = new double[fy.length + 2];
|
||||
|
||||
newFx[0] = xInitial[1];
|
||||
newFy[0] = yInitial[1];
|
||||
System.arraycopy(fx, 0, newFx, 1, fx.length);
|
||||
System.arraycopy(fy, 0, newFy, 1, fy.length);
|
||||
newFx[newFx.length - 1] = xFinal[1];
|
||||
newFy[newFy.length - 1] = yFinal[1];
|
||||
|
||||
for (int i = 0; i < newFx.length - 1; i++) {
|
||||
splines[i] = new CubicHermiteSpline(
|
||||
new double[]{newWaypts[i].getX(), newFx[i]},
|
||||
new double[]{newWaypts[i + 1].getX(), newFx[i + 1]},
|
||||
new double[]{newWaypts[i].getY(), newFy[i]},
|
||||
new double[]{newWaypts[i + 1].getY(), newFy[i + 1]}
|
||||
);
|
||||
}
|
||||
} else if (waypoints.length == 1) {
|
||||
final var xDeriv = (3 * (xFinal[0]
|
||||
- xInitial[0])
|
||||
- xFinal[1] - xInitial[1])
|
||||
/ 4.0;
|
||||
final var yDeriv = (3 * (yFinal[0]
|
||||
- yInitial[0])
|
||||
- yFinal[1] - yInitial[1])
|
||||
/ 4.0;
|
||||
|
||||
double[] midXControlVector = {waypoints[0].getX(), xDeriv};
|
||||
double[] midYControlVector = {waypoints[0].getY(), yDeriv};
|
||||
|
||||
splines[0] = new CubicHermiteSpline(xInitial, midXControlVector,
|
||||
yInitial, midYControlVector);
|
||||
splines[1] = new CubicHermiteSpline(midXControlVector, xFinal,
|
||||
midYControlVector, yFinal);
|
||||
} else {
|
||||
splines[0] = new CubicHermiteSpline(xInitial, xFinal,
|
||||
yInitial, yFinal);
|
||||
}
|
||||
return splines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of quintic splines corresponding to the provided control vectors.
|
||||
* The user is free to set the direction of all control vectors. Continuous
|
||||
* curvature is guaranteed throughout the path.
|
||||
*
|
||||
* @param controlVectors The control vectors.
|
||||
* @return A vector of quintic hermite splines that interpolate through the
|
||||
* provided waypoints.
|
||||
*/
|
||||
@SuppressWarnings({"LocalVariableName", "PMD.AvoidInstantiatingObjectsInLoops"})
|
||||
public static QuinticHermiteSpline[] getQuinticSplinesFromControlVectors(
|
||||
Spline.ControlVector[] controlVectors) {
|
||||
// There are twice as many control vectors are there are splines.
|
||||
QuinticHermiteSpline[] splines = new QuinticHermiteSpline[controlVectors.length / 2];
|
||||
for (int i = 0; i < controlVectors.length - 1; i += 2) {
|
||||
var xInitial = controlVectors[i].x;
|
||||
var xFinal = controlVectors[i + 1].x;
|
||||
var yInitial = controlVectors[i].y;
|
||||
var yFinal = controlVectors[i + 1].y;
|
||||
splines[i / 2] = new QuinticHermiteSpline(xInitial, xFinal,
|
||||
yInitial, yFinal);
|
||||
}
|
||||
return splines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thomas algorithm for solving tridiagonal systems Af = d.
|
||||
*
|
||||
* @param a the values of A above the diagonal
|
||||
* @param b the values of A on the diagonal
|
||||
* @param c the values of A below the diagonal
|
||||
* @param d the vector on the rhs
|
||||
* @param solutionVector the unknown (solution) vector, modified in-place
|
||||
*/
|
||||
@SuppressWarnings({"ParameterName", "LocalVariableName"})
|
||||
private static void thomasAlgorithm(double[] a, double[] b,
|
||||
double[] c, double[] d, double[] solutionVector) {
|
||||
int N = d.length;
|
||||
|
||||
double[] cStar = new double[N];
|
||||
double[] dStar = new double[N];
|
||||
|
||||
// This updates the coefficients in the first row
|
||||
// Note that we should be checking for division by zero here
|
||||
cStar[0] = c[0] / b[0];
|
||||
dStar[0] = d[0] / b[0];
|
||||
|
||||
// Create the c_star and d_star coefficients in the forward sweep
|
||||
for (int i = 1; i < N; i++) {
|
||||
double m = 1.0 / (b[i] - a[i] * cStar[i - 1]);
|
||||
cStar[i] = c[i] * m;
|
||||
dStar[i] = (d[i] - a[i] * dStar[i - 1]) * m;
|
||||
}
|
||||
solutionVector[N - 1] = dStar[N - 1];
|
||||
// This is the reverse sweep, used to update the solution vector f
|
||||
for (int i = N - 2; i >= 0; i--) {
|
||||
solutionVector[i] = dStar[i] - cStar[i] * solutionVector[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
private static Spline.ControlVector getCubicControlVector(double scalar, Pose2d point) {
|
||||
return new Spline.ControlVector(
|
||||
new double[]{point.getX(), scalar * point.getRotation().getCos()},
|
||||
new double[]{point.getY(), scalar * point.getRotation().getSin()}
|
||||
);
|
||||
}
|
||||
|
||||
private static Spline.ControlVector getQuinticControlVector(double scalar, Pose2d point) {
|
||||
return new Spline.ControlVector(
|
||||
new double[]{point.getX(), scalar * point.getRotation().getCos(), 0.0},
|
||||
new double[]{point.getY(), scalar * point.getRotation().getSin(), 0.0}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2018 Team 254
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package edu.wpi.first.wpilibj.spline;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class used to parameterize a spline by its arc length.
|
||||
*/
|
||||
public final class SplineParameterizer {
|
||||
private static final double kMaxDx = 0.127;
|
||||
private static final double kMaxDy = 0.00127;
|
||||
private static final double kMaxDtheta = 0.0872;
|
||||
|
||||
/**
|
||||
* A malformed spline does not actually explode the LIFO stack size. Instead, the stack size
|
||||
* stays at a relatively small number (e.g. 30) and never decreases. Because of this, we must
|
||||
* count iterations. Even long, complex paths don't usually go over 300 iterations, so hitting
|
||||
* this maximum should definitely indicate something has gone wrong.
|
||||
*/
|
||||
private static final int kMaxIterations = 5000;
|
||||
|
||||
@SuppressWarnings("MemberName")
|
||||
private static class StackContents {
|
||||
final double t1;
|
||||
final double t0;
|
||||
|
||||
StackContents(double t0, double t1) {
|
||||
this.t0 = t0;
|
||||
this.t1 = t1;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static class MalformedSplineException extends RuntimeException {
|
||||
/**
|
||||
* Create a new exception with the given message.
|
||||
*
|
||||
* @param message the message to pass with the exception
|
||||
*/
|
||||
private MalformedSplineException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor because this is a utility class.
|
||||
*/
|
||||
private SplineParameterizer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameterizes the spline. This method breaks up the spline into various
|
||||
* arcs until their dx, dy, and dtheta are within specific tolerances.
|
||||
*
|
||||
* @param spline The spline to parameterize.
|
||||
* @return A list of poses and curvatures that represents various points on the spline.
|
||||
* @throws MalformedSplineException When the spline is malformed (e.g. has close adjacent points
|
||||
* with approximately opposing headings)
|
||||
*/
|
||||
public static List<PoseWithCurvature> parameterize(Spline spline) {
|
||||
return parameterize(spline, 0.0, 1.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameterizes the spline. This method breaks up the spline into various
|
||||
* arcs until their dx, dy, and dtheta are within specific tolerances.
|
||||
*
|
||||
* @param spline The spline to parameterize.
|
||||
* @param t0 Starting internal spline parameter. It is recommended to use 0.0.
|
||||
* @param t1 Ending internal spline parameter. It is recommended to use 1.0.
|
||||
* @return A list of poses and curvatures that represents various points on the spline.
|
||||
* @throws MalformedSplineException When the spline is malformed (e.g. has close adjacent points
|
||||
* with approximately opposing headings)
|
||||
*/
|
||||
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||
public static List<PoseWithCurvature> parameterize(Spline spline, double t0, double t1) {
|
||||
var splinePoints = new ArrayList<PoseWithCurvature>();
|
||||
|
||||
// The parameterization does not add the initial point. Let's add that.
|
||||
splinePoints.add(spline.getPoint(t0));
|
||||
|
||||
// We use an "explicit stack" to simulate recursion, instead of a recursive function call
|
||||
// This give us greater control, instead of a stack overflow
|
||||
var stack = new ArrayDeque<StackContents>();
|
||||
stack.push(new StackContents(t0, t1));
|
||||
|
||||
StackContents current;
|
||||
PoseWithCurvature start;
|
||||
PoseWithCurvature end;
|
||||
int iterations = 0;
|
||||
|
||||
while (!stack.isEmpty()) {
|
||||
current = stack.removeFirst();
|
||||
start = spline.getPoint(current.t0);
|
||||
end = spline.getPoint(current.t1);
|
||||
|
||||
final var twist = start.poseMeters.log(end.poseMeters);
|
||||
if (
|
||||
Math.abs(twist.dy) > kMaxDy
|
||||
|| Math.abs(twist.dx) > kMaxDx
|
||||
|| Math.abs(twist.dtheta) > kMaxDtheta
|
||||
) {
|
||||
stack.addFirst(new StackContents((current.t0 + current.t1) / 2, current.t1));
|
||||
stack.addFirst(new StackContents(current.t0, (current.t0 + current.t1) / 2));
|
||||
} else {
|
||||
splinePoints.add(spline.getPoint(current.t1));
|
||||
}
|
||||
|
||||
iterations++;
|
||||
if (iterations >= kMaxIterations) {
|
||||
throw new MalformedSplineException(
|
||||
"Could not parameterize a malformed spline. "
|
||||
+ "This means that you probably had two or more adjacent waypoints that were very close "
|
||||
+ "together with headings in opposing directions."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return splinePoints;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Transform2d;
|
||||
|
||||
/**
|
||||
* Represents a time-parameterized trajectory. The trajectory contains of
|
||||
* various States that represent the pose, curvature, time elapsed, velocity,
|
||||
* and acceleration at that point.
|
||||
*/
|
||||
public class Trajectory {
|
||||
private final double m_totalTimeSeconds;
|
||||
private final List<State> m_states;
|
||||
|
||||
/**
|
||||
* Constructs an empty trajectory.
|
||||
*/
|
||||
public Trajectory() {
|
||||
m_states = new ArrayList<>();
|
||||
m_totalTimeSeconds = 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a trajectory from a vector of states.
|
||||
*
|
||||
* @param states A vector of states.
|
||||
*/
|
||||
public Trajectory(final List<State> states) {
|
||||
m_states = states;
|
||||
m_totalTimeSeconds = m_states.get(m_states.size() - 1).timeSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Linearly interpolates between two values.
|
||||
*
|
||||
* @param startValue The start value.
|
||||
* @param endValue The end value.
|
||||
* @param t The fraction for interpolation.
|
||||
* @return The interpolated value.
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
private static double lerp(double startValue, double endValue, double t) {
|
||||
return startValue + (endValue - startValue) * t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Linearly interpolates between two poses.
|
||||
*
|
||||
* @param startValue The start pose.
|
||||
* @param endValue The end pose.
|
||||
* @param t The fraction for interpolation.
|
||||
* @return The interpolated pose.
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
private static Pose2d lerp(Pose2d startValue, Pose2d endValue, double t) {
|
||||
return startValue.plus((endValue.minus(startValue)).times(t));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the initial pose of the trajectory.
|
||||
*
|
||||
* @return The initial pose of the trajectory.
|
||||
*/
|
||||
public Pose2d getInitialPose() {
|
||||
return sample(0).poseMeters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the overall duration of the trajectory.
|
||||
*
|
||||
* @return The duration of the trajectory.
|
||||
*/
|
||||
public double getTotalTimeSeconds() {
|
||||
return m_totalTimeSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the states of the trajectory.
|
||||
*
|
||||
* @return The states of the trajectory.
|
||||
*/
|
||||
public List<State> getStates() {
|
||||
return m_states;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample the trajectory at a point in time.
|
||||
*
|
||||
* @param timeSeconds The point in time since the beginning of the trajectory to sample.
|
||||
* @return The state at that point in time.
|
||||
*/
|
||||
public State sample(double timeSeconds) {
|
||||
if (timeSeconds <= m_states.get(0).timeSeconds) {
|
||||
return m_states.get(0);
|
||||
}
|
||||
if (timeSeconds >= m_totalTimeSeconds) {
|
||||
return m_states.get(m_states.size() - 1);
|
||||
}
|
||||
|
||||
// To get the element that we want, we will use a binary search algorithm
|
||||
// instead of iterating over a for-loop. A binary search is O(std::log(n))
|
||||
// whereas searching using a loop is O(n).
|
||||
|
||||
// This starts at 1 because we use the previous state later on for
|
||||
// interpolation.
|
||||
int low = 1;
|
||||
int high = m_states.size() - 1;
|
||||
|
||||
while (low != high) {
|
||||
int mid = (low + high) / 2;
|
||||
if (m_states.get(mid).timeSeconds < timeSeconds) {
|
||||
// This index and everything under it are less than the requested
|
||||
// timestamp. Therefore, we can discard them.
|
||||
low = mid + 1;
|
||||
} else {
|
||||
// t is at least as large as the element at this index. This means that
|
||||
// anything after it cannot be what we are looking for.
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
|
||||
// High and Low should be the same.
|
||||
|
||||
// The sample's timestamp is now greater than or equal to the requested
|
||||
// timestamp. If it is greater, we need to interpolate between the
|
||||
// previous state and the current state to get the exact state that we
|
||||
// want.
|
||||
final State sample = m_states.get(low);
|
||||
final State prevSample = m_states.get(low - 1);
|
||||
|
||||
// If the difference in states is negligible, then we are spot on!
|
||||
if (Math.abs(sample.timeSeconds - prevSample.timeSeconds) < 1E-9) {
|
||||
return sample;
|
||||
}
|
||||
// Interpolate between the two states for the state that we want.
|
||||
return prevSample.interpolate(sample,
|
||||
(timeSeconds - prevSample.timeSeconds) / (sample.timeSeconds - prevSample.timeSeconds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms all poses in the trajectory by the given transform. This is
|
||||
* useful for converting a robot-relative trajectory into a field-relative
|
||||
* trajectory. This works with respect to the first pose in the trajectory.
|
||||
*
|
||||
* @param transform The transform to transform the trajectory by.
|
||||
* @return The transformed trajectory.
|
||||
*/
|
||||
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||
public Trajectory transformBy(Transform2d transform) {
|
||||
var firstState = m_states.get(0);
|
||||
var firstPose = firstState.poseMeters;
|
||||
|
||||
// Calculate the transformed first pose.
|
||||
var newFirstPose = firstPose.plus(transform);
|
||||
List<State> newStates = new ArrayList<>();
|
||||
|
||||
newStates.add(new State(
|
||||
firstState.timeSeconds, firstState.velocityMetersPerSecond,
|
||||
firstState.accelerationMetersPerSecondSq, newFirstPose, firstState.curvatureRadPerMeter
|
||||
));
|
||||
|
||||
for (int i = 1; i < m_states.size(); i++) {
|
||||
var state = m_states.get(i);
|
||||
// We are transforming relative to the coordinate frame of the new initial pose.
|
||||
newStates.add(new State(
|
||||
state.timeSeconds, state.velocityMetersPerSecond,
|
||||
state.accelerationMetersPerSecondSq, newFirstPose.plus(state.poseMeters.minus(firstPose)),
|
||||
state.curvatureRadPerMeter
|
||||
));
|
||||
}
|
||||
|
||||
return new Trajectory(newStates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms all poses in the trajectory so that they are relative to the
|
||||
* given pose. This is useful for converting a field-relative trajectory
|
||||
* into a robot-relative trajectory.
|
||||
*
|
||||
* @param pose The pose that is the origin of the coordinate frame that
|
||||
* the current trajectory will be transformed into.
|
||||
* @return The transformed trajectory.
|
||||
*/
|
||||
public Trajectory relativeTo(Pose2d pose) {
|
||||
return new Trajectory(m_states.stream().map(state -> new State(state.timeSeconds,
|
||||
state.velocityMetersPerSecond, state.accelerationMetersPerSecondSq,
|
||||
state.poseMeters.relativeTo(pose), state.curvatureRadPerMeter))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a time-parameterized trajectory. The trajectory contains of
|
||||
* various States that represent the pose, curvature, time elapsed, velocity,
|
||||
* and acceleration at that point.
|
||||
*/
|
||||
@SuppressWarnings("MemberName")
|
||||
public static class State {
|
||||
// The time elapsed since the beginning of the trajectory.
|
||||
@JsonProperty("time")
|
||||
public double timeSeconds;
|
||||
|
||||
// The speed at that point of the trajectory.
|
||||
@JsonProperty("velocity")
|
||||
public double velocityMetersPerSecond;
|
||||
|
||||
// The acceleration at that point of the trajectory.
|
||||
@JsonProperty("acceleration")
|
||||
public double accelerationMetersPerSecondSq;
|
||||
|
||||
// The pose at that point of the trajectory.
|
||||
@JsonProperty("pose")
|
||||
public Pose2d poseMeters;
|
||||
|
||||
// The curvature at that point of the trajectory.
|
||||
@JsonProperty("curvature")
|
||||
public double curvatureRadPerMeter;
|
||||
|
||||
public State() {
|
||||
poseMeters = new Pose2d();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a State with the specified parameters.
|
||||
*
|
||||
* @param timeSeconds The time elapsed since the beginning of the trajectory.
|
||||
* @param velocityMetersPerSecond The speed at that point of the trajectory.
|
||||
* @param accelerationMetersPerSecondSq The acceleration at that point of the trajectory.
|
||||
* @param poseMeters The pose at that point of the trajectory.
|
||||
* @param curvatureRadPerMeter The curvature at that point of the trajectory.
|
||||
*/
|
||||
public State(double timeSeconds, double velocityMetersPerSecond,
|
||||
double accelerationMetersPerSecondSq, Pose2d poseMeters,
|
||||
double curvatureRadPerMeter) {
|
||||
this.timeSeconds = timeSeconds;
|
||||
this.velocityMetersPerSecond = velocityMetersPerSecond;
|
||||
this.accelerationMetersPerSecondSq = accelerationMetersPerSecondSq;
|
||||
this.poseMeters = poseMeters;
|
||||
this.curvatureRadPerMeter = curvatureRadPerMeter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates between two States.
|
||||
*
|
||||
* @param endValue The end value for the interpolation.
|
||||
* @param i The interpolant (fraction).
|
||||
* @return The interpolated state.
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
State interpolate(State endValue, double i) {
|
||||
// Find the new t value.
|
||||
final double newT = lerp(timeSeconds, endValue.timeSeconds, i);
|
||||
|
||||
// Find the delta time between the current state and the interpolated state.
|
||||
final double deltaT = newT - timeSeconds;
|
||||
|
||||
// If delta time is negative, flip the order of interpolation.
|
||||
if (deltaT < 0) {
|
||||
return endValue.interpolate(this, 1 - i);
|
||||
}
|
||||
|
||||
// Check whether the robot is reversing at this stage.
|
||||
final boolean reversing = velocityMetersPerSecond < 0
|
||||
|| Math.abs(velocityMetersPerSecond) < 1E-9 && accelerationMetersPerSecondSq < 0;
|
||||
|
||||
// Calculate the new velocity
|
||||
// v_f = v_0 + at
|
||||
final double newV = velocityMetersPerSecond + (accelerationMetersPerSecondSq * deltaT);
|
||||
|
||||
// Calculate the change in position.
|
||||
// delta_s = v_0 t + 0.5 at^2
|
||||
final double newS = (velocityMetersPerSecond * deltaT
|
||||
+ 0.5 * accelerationMetersPerSecondSq * Math.pow(deltaT, 2)) * (reversing ? -1.0 : 1.0);
|
||||
|
||||
// Return the new state. To find the new position for the new state, we need
|
||||
// to interpolate between the two endpoint poses. The fraction for
|
||||
// interpolation is the change in position (delta s) divided by the total
|
||||
// distance between the two endpoints.
|
||||
final double interpolationFrac = newS
|
||||
/ endValue.poseMeters.getTranslation().getDistance(poseMeters.getTranslation());
|
||||
|
||||
return new State(
|
||||
newT, newV, accelerationMetersPerSecondSq,
|
||||
lerp(poseMeters, endValue.poseMeters, interpolationFrac),
|
||||
lerp(curvatureRadPerMeter, endValue.curvatureRadPerMeter, interpolationFrac)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"State(Sec: %.2f, Vel m/s: %.2f, Accel m/s/s: %.2f, Pose: %s, Curvature: %.2f)",
|
||||
timeSeconds, velocityMetersPerSecond, accelerationMetersPerSecondSq,
|
||||
poseMeters, curvatureRadPerMeter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof State)) {
|
||||
return false;
|
||||
}
|
||||
State state = (State) obj;
|
||||
return Double.compare(state.timeSeconds, timeSeconds) == 0
|
||||
&& Double.compare(state.velocityMetersPerSecond, velocityMetersPerSecond) == 0
|
||||
&& Double.compare(state.accelerationMetersPerSecondSq,
|
||||
accelerationMetersPerSecondSq) == 0
|
||||
&& Double.compare(state.curvatureRadPerMeter, curvatureRadPerMeter) == 0
|
||||
&& Objects.equals(poseMeters, state.poseMeters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(timeSeconds, velocityMetersPerSecond,
|
||||
accelerationMetersPerSecondSq, poseMeters, curvatureRadPerMeter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String stateList = m_states.stream().map(State::toString).collect(Collectors.joining(", \n"));
|
||||
return String.format("Trajectory - Seconds: %.2f, States:\n%s", m_totalTimeSeconds, stateList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return m_states.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof Trajectory && m_states.equals(((Trajectory) obj).getStates());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import edu.wpi.first.wpilibj.kinematics.DifferentialDriveKinematics;
|
||||
import edu.wpi.first.wpilibj.kinematics.MecanumDriveKinematics;
|
||||
import edu.wpi.first.wpilibj.kinematics.SwerveDriveKinematics;
|
||||
import edu.wpi.first.wpilibj.trajectory.constraint.DifferentialDriveKinematicsConstraint;
|
||||
import edu.wpi.first.wpilibj.trajectory.constraint.MecanumDriveKinematicsConstraint;
|
||||
import edu.wpi.first.wpilibj.trajectory.constraint.SwerveDriveKinematicsConstraint;
|
||||
import edu.wpi.first.wpilibj.trajectory.constraint.TrajectoryConstraint;
|
||||
|
||||
/**
|
||||
* Represents the configuration for generating a trajectory. This class stores the start velocity,
|
||||
* end velocity, max velocity, max acceleration, custom constraints, and the reversed flag.
|
||||
*
|
||||
* <p>The class must be constructed with a max velocity and max acceleration. The other parameters
|
||||
* (start velocity, end velocity, constraints, reversed) have been defaulted to reasonable
|
||||
* values (0, 0, {}, false). These values can be changed via the setXXX methods.
|
||||
*/
|
||||
public class TrajectoryConfig {
|
||||
private final double m_maxVelocity;
|
||||
private final double m_maxAcceleration;
|
||||
private final List<TrajectoryConstraint> m_constraints;
|
||||
private double m_startVelocity;
|
||||
private double m_endVelocity;
|
||||
private boolean m_reversed;
|
||||
|
||||
/**
|
||||
* Constructs the trajectory configuration class.
|
||||
*
|
||||
* @param maxVelocityMetersPerSecond The max velocity for the trajectory.
|
||||
* @param maxAccelerationMetersPerSecondSq The max acceleration for the trajectory.
|
||||
*/
|
||||
public TrajectoryConfig(double maxVelocityMetersPerSecond,
|
||||
double maxAccelerationMetersPerSecondSq) {
|
||||
m_maxVelocity = maxVelocityMetersPerSecond;
|
||||
m_maxAcceleration = maxAccelerationMetersPerSecondSq;
|
||||
m_constraints = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a user-defined constraint to the trajectory.
|
||||
*
|
||||
* @param constraint The user-defined constraint.
|
||||
* @return Instance of the current config object.
|
||||
*/
|
||||
public TrajectoryConfig addConstraint(TrajectoryConstraint constraint) {
|
||||
m_constraints.add(constraint);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all user-defined constraints from a list to the trajectory.
|
||||
* @param constraints List of user-defined constraints.
|
||||
* @return Instance of the current config object.
|
||||
*/
|
||||
public TrajectoryConfig addConstraints(List<? extends TrajectoryConstraint> constraints) {
|
||||
m_constraints.addAll(constraints);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a differential drive kinematics constraint to ensure that
|
||||
* no wheel velocity of a differential drive goes above the max velocity.
|
||||
*
|
||||
* @param kinematics The differential drive kinematics.
|
||||
* @return Instance of the current config object.
|
||||
*/
|
||||
public TrajectoryConfig setKinematics(DifferentialDriveKinematics kinematics) {
|
||||
addConstraint(new DifferentialDriveKinematicsConstraint(kinematics, m_maxVelocity));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a mecanum drive kinematics constraint to ensure that
|
||||
* no wheel velocity of a mecanum drive goes above the max velocity.
|
||||
*
|
||||
* @param kinematics The mecanum drive kinematics.
|
||||
* @return Instance of the current config object.
|
||||
*/
|
||||
public TrajectoryConfig setKinematics(MecanumDriveKinematics kinematics) {
|
||||
addConstraint(new MecanumDriveKinematicsConstraint(kinematics, m_maxVelocity));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a swerve drive kinematics constraint to ensure that
|
||||
* no wheel velocity of a swerve drive goes above the max velocity.
|
||||
*
|
||||
* @param kinematics The swerve drive kinematics.
|
||||
* @return Instance of the current config object.
|
||||
*/
|
||||
public TrajectoryConfig setKinematics(SwerveDriveKinematics kinematics) {
|
||||
addConstraint(new SwerveDriveKinematicsConstraint(kinematics, m_maxVelocity));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the starting velocity of the trajectory.
|
||||
*
|
||||
* @return The starting velocity of the trajectory.
|
||||
*/
|
||||
public double getStartVelocity() {
|
||||
return m_startVelocity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the start velocity of the trajectory.
|
||||
*
|
||||
* @param startVelocityMetersPerSecond The start velocity of the trajectory.
|
||||
* @return Instance of the current config object.
|
||||
*/
|
||||
public TrajectoryConfig setStartVelocity(double startVelocityMetersPerSecond) {
|
||||
m_startVelocity = startVelocityMetersPerSecond;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the starting velocity of the trajectory.
|
||||
*
|
||||
* @return The starting velocity of the trajectory.
|
||||
*/
|
||||
public double getEndVelocity() {
|
||||
return m_endVelocity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the end velocity of the trajectory.
|
||||
*
|
||||
* @param endVelocityMetersPerSecond The end velocity of the trajectory.
|
||||
* @return Instance of the current config object.
|
||||
*/
|
||||
public TrajectoryConfig setEndVelocity(double endVelocityMetersPerSecond) {
|
||||
m_endVelocity = endVelocityMetersPerSecond;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum velocity of the trajectory.
|
||||
*
|
||||
* @return The maximum velocity of the trajectory.
|
||||
*/
|
||||
public double getMaxVelocity() {
|
||||
return m_maxVelocity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum acceleration of the trajectory.
|
||||
*
|
||||
* @return The maximum acceleration of the trajectory.
|
||||
*/
|
||||
public double getMaxAcceleration() {
|
||||
return m_maxAcceleration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user-defined constraints of the trajectory.
|
||||
*
|
||||
* @return The user-defined constraints of the trajectory.
|
||||
*/
|
||||
public List<TrajectoryConstraint> getConstraints() {
|
||||
return m_constraints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the trajectory is reversed or not.
|
||||
*
|
||||
* @return whether the trajectory is reversed or not.
|
||||
*/
|
||||
public boolean isReversed() {
|
||||
return m_reversed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the reversed flag of the trajectory.
|
||||
*
|
||||
* @param reversed Whether the trajectory should be reversed or not.
|
||||
* @return Instance of the current config object.
|
||||
*/
|
||||
public TrajectoryConfig setReversed(boolean reversed) {
|
||||
m_reversed = reversed;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import edu.wpi.first.math.MathSharedStore;
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Transform2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Translation2d;
|
||||
import edu.wpi.first.wpilibj.spline.PoseWithCurvature;
|
||||
import edu.wpi.first.wpilibj.spline.Spline;
|
||||
import edu.wpi.first.wpilibj.spline.SplineHelper;
|
||||
import edu.wpi.first.wpilibj.spline.SplineParameterizer;
|
||||
import edu.wpi.first.wpilibj.spline.SplineParameterizer.MalformedSplineException;
|
||||
|
||||
public final class TrajectoryGenerator {
|
||||
private static final Trajectory kDoNothingTrajectory =
|
||||
new Trajectory(Arrays.asList(new Trajectory.State()));
|
||||
private static BiConsumer<String, StackTraceElement[]> errorFunc;
|
||||
|
||||
/**
|
||||
* Private constructor because this is a utility class.
|
||||
*/
|
||||
private TrajectoryGenerator() {
|
||||
}
|
||||
|
||||
private static void reportError(String error, StackTraceElement[] stackTrace) {
|
||||
if (errorFunc != null) {
|
||||
errorFunc.accept(error, stackTrace);
|
||||
} else {
|
||||
MathSharedStore.reportError(error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set error reporting function. By default, DriverStation.reportError() is used.
|
||||
*
|
||||
* @param func Error reporting function, arguments are error and stackTrace.
|
||||
*/
|
||||
public static void setErrorHandler(BiConsumer<String, StackTraceElement[]> func) {
|
||||
errorFunc = func;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a trajectory from the given control vectors and config. This method uses clamped
|
||||
* cubic splines -- a method in which the exterior control vectors and interior waypoints
|
||||
* are provided. The headings are automatically determined at the interior points to
|
||||
* ensure continuous curvature.
|
||||
*
|
||||
* @param initial The initial control vector.
|
||||
* @param interiorWaypoints The interior waypoints.
|
||||
* @param end The ending control vector.
|
||||
* @param config The configuration for the trajectory.
|
||||
* @return The generated trajectory.
|
||||
*/
|
||||
public static Trajectory generateTrajectory(
|
||||
Spline.ControlVector initial,
|
||||
List<Translation2d> interiorWaypoints,
|
||||
Spline.ControlVector end,
|
||||
TrajectoryConfig config
|
||||
) {
|
||||
final var flip = new Transform2d(new Translation2d(), Rotation2d.fromDegrees(180.0));
|
||||
|
||||
// Clone the control vectors.
|
||||
var newInitial = new Spline.ControlVector(initial.x, initial.y);
|
||||
var newEnd = new Spline.ControlVector(end.x, end.y);
|
||||
|
||||
// Change the orientation if reversed.
|
||||
if (config.isReversed()) {
|
||||
newInitial.x[1] *= -1;
|
||||
newInitial.y[1] *= -1;
|
||||
newEnd.x[1] *= -1;
|
||||
newEnd.y[1] *= -1;
|
||||
}
|
||||
|
||||
// Get the spline points
|
||||
List<PoseWithCurvature> points;
|
||||
try {
|
||||
points = splinePointsFromSplines(SplineHelper.getCubicSplinesFromControlVectors(newInitial,
|
||||
interiorWaypoints.toArray(new Translation2d[0]), newEnd));
|
||||
} catch (MalformedSplineException ex) {
|
||||
reportError(ex.getMessage(), ex.getStackTrace());
|
||||
return kDoNothingTrajectory;
|
||||
}
|
||||
|
||||
// Change the points back to their original orientation.
|
||||
if (config.isReversed()) {
|
||||
for (var point : points) {
|
||||
point.poseMeters = point.poseMeters.plus(flip);
|
||||
point.curvatureRadPerMeter *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate and return trajectory.
|
||||
return TrajectoryParameterizer.timeParameterizeTrajectory(points, config.getConstraints(),
|
||||
config.getStartVelocity(), config.getEndVelocity(), config.getMaxVelocity(),
|
||||
config.getMaxAcceleration(), config.isReversed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a trajectory from the given waypoints and config. This method uses clamped
|
||||
* cubic splines -- a method in which the initial pose, final pose, and interior waypoints
|
||||
* are provided. The headings are automatically determined at the interior points to
|
||||
* ensure continuous curvature.
|
||||
*
|
||||
* @param start The starting pose.
|
||||
* @param interiorWaypoints The interior waypoints.
|
||||
* @param end The ending pose.
|
||||
* @param config The configuration for the trajectory.
|
||||
* @return The generated trajectory.
|
||||
*/
|
||||
public static Trajectory generateTrajectory(
|
||||
Pose2d start, List<Translation2d> interiorWaypoints, Pose2d end,
|
||||
TrajectoryConfig config
|
||||
) {
|
||||
var controlVectors = SplineHelper.getCubicControlVectorsFromWaypoints(
|
||||
start, interiorWaypoints.toArray(new Translation2d[0]), end
|
||||
);
|
||||
|
||||
// Return the generated trajectory.
|
||||
return generateTrajectory(controlVectors[0], interiorWaypoints, controlVectors[1], config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a trajectory from the given quintic control vectors and config. This method
|
||||
* uses quintic hermite splines -- therefore, all points must be represented by control
|
||||
* vectors. Continuous curvature is guaranteed in this method.
|
||||
*
|
||||
* @param controlVectors List of quintic control vectors.
|
||||
* @param config The configuration for the trajectory.
|
||||
* @return The generated trajectory.
|
||||
*/
|
||||
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||
public static Trajectory generateTrajectory(
|
||||
ControlVectorList controlVectors,
|
||||
TrajectoryConfig config
|
||||
) {
|
||||
final var flip = new Transform2d(new Translation2d(), Rotation2d.fromDegrees(180.0));
|
||||
final var newControlVectors = new ArrayList<Spline.ControlVector>(controlVectors.size());
|
||||
|
||||
// Create a new control vector list, flipping the orientation if reversed.
|
||||
for (final var vector : controlVectors) {
|
||||
var newVector = new Spline.ControlVector(vector.x, vector.y);
|
||||
if (config.isReversed()) {
|
||||
newVector.x[1] *= -1;
|
||||
newVector.y[1] *= -1;
|
||||
}
|
||||
newControlVectors.add(newVector);
|
||||
}
|
||||
|
||||
// Get the spline points
|
||||
List<PoseWithCurvature> points;
|
||||
try {
|
||||
points = splinePointsFromSplines(SplineHelper.getQuinticSplinesFromControlVectors(
|
||||
newControlVectors.toArray(new Spline.ControlVector[]{})
|
||||
));
|
||||
} catch (MalformedSplineException ex) {
|
||||
reportError(ex.getMessage(), ex.getStackTrace());
|
||||
return kDoNothingTrajectory;
|
||||
}
|
||||
|
||||
// Change the points back to their original orientation.
|
||||
if (config.isReversed()) {
|
||||
for (var point : points) {
|
||||
point.poseMeters = point.poseMeters.plus(flip);
|
||||
point.curvatureRadPerMeter *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate and return trajectory.
|
||||
return TrajectoryParameterizer.timeParameterizeTrajectory(points, config.getConstraints(),
|
||||
config.getStartVelocity(), config.getEndVelocity(), config.getMaxVelocity(),
|
||||
config.getMaxAcceleration(), config.isReversed());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a trajectory from the given waypoints and config. This method
|
||||
* uses quintic hermite splines -- therefore, all points must be represented by Pose2d
|
||||
* objects. Continuous curvature is guaranteed in this method.
|
||||
*
|
||||
* @param waypoints List of waypoints..
|
||||
* @param config The configuration for the trajectory.
|
||||
* @return The generated trajectory.
|
||||
*/
|
||||
@SuppressWarnings("LocalVariableName")
|
||||
public static Trajectory generateTrajectory(List<Pose2d> waypoints, TrajectoryConfig config) {
|
||||
var originalList = SplineHelper.getQuinticControlVectorsFromWaypoints(waypoints);
|
||||
var newList = new ControlVectorList(originalList);
|
||||
return generateTrajectory(newList, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate spline points from a vector of splines by parameterizing the
|
||||
* splines.
|
||||
*
|
||||
* @param splines The splines to parameterize.
|
||||
* @return The spline points for use in time parameterization of a trajectory.
|
||||
* @throws MalformedSplineException When the spline is malformed (e.g. has close adjacent points
|
||||
* with approximately opposing headings)
|
||||
*/
|
||||
public static List<PoseWithCurvature> splinePointsFromSplines(
|
||||
Spline[] splines) {
|
||||
// Create the vector of spline points.
|
||||
var splinePoints = new ArrayList<PoseWithCurvature>();
|
||||
|
||||
// Add the first point to the vector.
|
||||
splinePoints.add(splines[0].getPoint(0.0));
|
||||
|
||||
// Iterate through the vector and parameterize each spline, adding the
|
||||
// parameterized points to the final vector.
|
||||
for (final var spline : splines) {
|
||||
var points = SplineParameterizer.parameterize(spline);
|
||||
|
||||
// Append the array of poses to the vector. We are removing the first
|
||||
// point because it's a duplicate of the last point from the previous
|
||||
// spline.
|
||||
splinePoints.addAll(points.subList(1, points.size()));
|
||||
}
|
||||
return splinePoints;
|
||||
}
|
||||
|
||||
// Work around type erasure signatures
|
||||
@SuppressWarnings("serial")
|
||||
public static class ControlVectorList extends ArrayList<Spline.ControlVector> {
|
||||
public ControlVectorList(int initialCapacity) {
|
||||
super(initialCapacity);
|
||||
}
|
||||
|
||||
public ControlVectorList() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ControlVectorList(Collection<? extends Spline.ControlVector> collection) {
|
||||
super(collection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2018 Team 254
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import edu.wpi.first.wpilibj.spline.PoseWithCurvature;
|
||||
import edu.wpi.first.wpilibj.trajectory.constraint.TrajectoryConstraint;
|
||||
|
||||
/**
|
||||
* Class used to parameterize a trajectory by time.
|
||||
*/
|
||||
public final class TrajectoryParameterizer {
|
||||
/**
|
||||
* Private constructor because this is a utility class.
|
||||
*/
|
||||
private TrajectoryParameterizer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameterize the trajectory by time. This is where the velocity profile is
|
||||
* generated.
|
||||
*
|
||||
* <p>The derivation of the algorithm used can be found
|
||||
* <a href="http://www2.informatik.uni-freiburg.de/~lau/students/Sprunk2008.pdf">
|
||||
* here</a>.
|
||||
*
|
||||
* @param points Reference to the spline points.
|
||||
* @param constraints A vector of various velocity and acceleration.
|
||||
* constraints.
|
||||
* @param startVelocityMetersPerSecond The start velocity for the trajectory.
|
||||
* @param endVelocityMetersPerSecond The end velocity for the trajectory.
|
||||
* @param maxVelocityMetersPerSecond The max velocity for the trajectory.
|
||||
* @param maxAccelerationMetersPerSecondSq The max acceleration for the trajectory.
|
||||
* @param reversed Whether the robot should move backwards.
|
||||
* Note that the robot will still move from
|
||||
* a -> b -> ... -> z as defined in the
|
||||
* waypoints.
|
||||
* @return The trajectory.
|
||||
*/
|
||||
@SuppressWarnings({"PMD.ExcessiveMethodLength", "PMD.CyclomaticComplexity",
|
||||
"PMD.NPathComplexity", "PMD.AvoidInstantiatingObjectsInLoops"})
|
||||
public static Trajectory timeParameterizeTrajectory(
|
||||
List<PoseWithCurvature> points,
|
||||
List<TrajectoryConstraint> constraints,
|
||||
double startVelocityMetersPerSecond,
|
||||
double endVelocityMetersPerSecond,
|
||||
double maxVelocityMetersPerSecond,
|
||||
double maxAccelerationMetersPerSecondSq,
|
||||
boolean reversed
|
||||
) {
|
||||
var constrainedStates = new ArrayList<ConstrainedState>(points.size());
|
||||
var predecessor = new ConstrainedState(points.get(0), 0, startVelocityMetersPerSecond,
|
||||
-maxAccelerationMetersPerSecondSq, maxAccelerationMetersPerSecondSq);
|
||||
|
||||
// Forward pass
|
||||
for (int i = 0; i < points.size(); i++) {
|
||||
constrainedStates.add(new ConstrainedState());
|
||||
var constrainedState = constrainedStates.get(i);
|
||||
constrainedState.pose = points.get(i);
|
||||
|
||||
// Begin constraining based on predecessor.
|
||||
double ds = constrainedState.pose.poseMeters.getTranslation().getDistance(
|
||||
predecessor.pose.poseMeters.getTranslation());
|
||||
constrainedState.distanceMeters = predecessor.distanceMeters + ds;
|
||||
|
||||
// We may need to iterate to find the maximum end velocity and common
|
||||
// acceleration, since acceleration limits may be a function of velocity.
|
||||
while (true) {
|
||||
// Enforce global max velocity and max reachable velocity by global
|
||||
// acceleration limit. vf = std::sqrt(vi^2 + 2*a*d).
|
||||
constrainedState.maxVelocityMetersPerSecond = Math.min(
|
||||
maxVelocityMetersPerSecond,
|
||||
Math.sqrt(predecessor.maxVelocityMetersPerSecond
|
||||
* predecessor.maxVelocityMetersPerSecond
|
||||
+ predecessor.maxAccelerationMetersPerSecondSq * ds * 2.0)
|
||||
);
|
||||
|
||||
constrainedState.minAccelerationMetersPerSecondSq = -maxAccelerationMetersPerSecondSq;
|
||||
constrainedState.maxAccelerationMetersPerSecondSq = maxAccelerationMetersPerSecondSq;
|
||||
|
||||
// At this point, the constrained state is fully constructed apart from
|
||||
// all the custom-defined user constraints.
|
||||
for (final var constraint : constraints) {
|
||||
constrainedState.maxVelocityMetersPerSecond = Math.min(
|
||||
constrainedState.maxVelocityMetersPerSecond,
|
||||
constraint.getMaxVelocityMetersPerSecond(
|
||||
constrainedState.pose.poseMeters, constrainedState.pose.curvatureRadPerMeter,
|
||||
constrainedState.maxVelocityMetersPerSecond)
|
||||
);
|
||||
}
|
||||
|
||||
// Now enforce all acceleration limits.
|
||||
enforceAccelerationLimits(reversed, constraints, constrainedState);
|
||||
|
||||
if (ds < 1E-6) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If the actual acceleration for this state is higher than the max
|
||||
// acceleration that we applied, then we need to reduce the max
|
||||
// acceleration of the predecessor and try again.
|
||||
double actualAcceleration = (constrainedState.maxVelocityMetersPerSecond
|
||||
* constrainedState.maxVelocityMetersPerSecond
|
||||
- predecessor.maxVelocityMetersPerSecond * predecessor.maxVelocityMetersPerSecond)
|
||||
/ (ds * 2.0);
|
||||
|
||||
// If we violate the max acceleration constraint, let's modify the
|
||||
// predecessor.
|
||||
if (constrainedState.maxAccelerationMetersPerSecondSq < actualAcceleration - 1E-6) {
|
||||
predecessor.maxAccelerationMetersPerSecondSq
|
||||
= constrainedState.maxAccelerationMetersPerSecondSq;
|
||||
} else {
|
||||
// Constrain the predecessor's max acceleration to the current
|
||||
// acceleration.
|
||||
if (actualAcceleration > predecessor.minAccelerationMetersPerSecondSq) {
|
||||
predecessor.maxAccelerationMetersPerSecondSq = actualAcceleration;
|
||||
}
|
||||
// If the actual acceleration is less than the predecessor's min
|
||||
// acceleration, it will be repaired in the backward pass.
|
||||
break;
|
||||
}
|
||||
}
|
||||
predecessor = constrainedState;
|
||||
}
|
||||
|
||||
var successor = new ConstrainedState(points.get(points.size() - 1),
|
||||
constrainedStates.get(constrainedStates.size() - 1).distanceMeters,
|
||||
endVelocityMetersPerSecond,
|
||||
-maxAccelerationMetersPerSecondSq, maxAccelerationMetersPerSecondSq);
|
||||
|
||||
// Backward pass
|
||||
for (int i = points.size() - 1; i >= 0; i--) {
|
||||
var constrainedState = constrainedStates.get(i);
|
||||
double ds = constrainedState.distanceMeters - successor.distanceMeters; // negative
|
||||
|
||||
while (true) {
|
||||
// Enforce max velocity limit (reverse)
|
||||
// vf = std::sqrt(vi^2 + 2*a*d), where vi = successor.
|
||||
double newMaxVelocity = Math.sqrt(
|
||||
successor.maxVelocityMetersPerSecond * successor.maxVelocityMetersPerSecond
|
||||
+ successor.minAccelerationMetersPerSecondSq * ds * 2.0
|
||||
);
|
||||
|
||||
// No more limits to impose! This state can be finalized.
|
||||
if (newMaxVelocity >= constrainedState.maxVelocityMetersPerSecond) {
|
||||
break;
|
||||
}
|
||||
|
||||
constrainedState.maxVelocityMetersPerSecond = newMaxVelocity;
|
||||
|
||||
// Check all acceleration constraints with the new max velocity.
|
||||
enforceAccelerationLimits(reversed, constraints, constrainedState);
|
||||
|
||||
if (ds > -1E-6) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If the actual acceleration for this state is lower than the min
|
||||
// acceleration, then we need to lower the min acceleration of the
|
||||
// successor and try again.
|
||||
double actualAcceleration = (constrainedState.maxVelocityMetersPerSecond
|
||||
* constrainedState.maxVelocityMetersPerSecond
|
||||
- successor.maxVelocityMetersPerSecond * successor.maxVelocityMetersPerSecond)
|
||||
/ (ds * 2.0);
|
||||
|
||||
if (constrainedState.minAccelerationMetersPerSecondSq > actualAcceleration + 1E-6) {
|
||||
successor.minAccelerationMetersPerSecondSq
|
||||
= constrainedState.minAccelerationMetersPerSecondSq;
|
||||
} else {
|
||||
successor.minAccelerationMetersPerSecondSq = actualAcceleration;
|
||||
break;
|
||||
}
|
||||
}
|
||||
successor = constrainedState;
|
||||
}
|
||||
|
||||
// Now we can integrate the constrained states forward in time to obtain our
|
||||
// trajectory states.
|
||||
var states = new ArrayList<Trajectory.State>(points.size());
|
||||
double timeSeconds = 0.0;
|
||||
double distanceMeters = 0.0;
|
||||
double velocityMetersPerSecond = 0.0;
|
||||
|
||||
for (int i = 0; i < constrainedStates.size(); i++) {
|
||||
final var state = constrainedStates.get(i);
|
||||
|
||||
// Calculate the change in position between the current state and the previous
|
||||
// state.
|
||||
double ds = state.distanceMeters - distanceMeters;
|
||||
|
||||
// Calculate the acceleration between the current state and the previous
|
||||
// state.
|
||||
double accel = (state.maxVelocityMetersPerSecond * state.maxVelocityMetersPerSecond
|
||||
- velocityMetersPerSecond * velocityMetersPerSecond) / (ds * 2);
|
||||
|
||||
// Calculate dt
|
||||
double dt = 0.0;
|
||||
if (i > 0) {
|
||||
states.get(i - 1).accelerationMetersPerSecondSq = reversed ? -accel : accel;
|
||||
if (Math.abs(accel) > 1E-6) {
|
||||
// v_f = v_0 + a * t
|
||||
dt = (state.maxVelocityMetersPerSecond - velocityMetersPerSecond) / accel;
|
||||
} else if (Math.abs(velocityMetersPerSecond) > 1E-6) {
|
||||
// delta_x = v * t
|
||||
dt = ds / velocityMetersPerSecond;
|
||||
} else {
|
||||
throw new TrajectoryGenerationException("Something went wrong at iteration " + i
|
||||
+ " of time parameterization.");
|
||||
}
|
||||
}
|
||||
|
||||
velocityMetersPerSecond = state.maxVelocityMetersPerSecond;
|
||||
distanceMeters = state.distanceMeters;
|
||||
|
||||
timeSeconds += dt;
|
||||
|
||||
states.add(new Trajectory.State(
|
||||
timeSeconds,
|
||||
reversed ? -velocityMetersPerSecond : velocityMetersPerSecond,
|
||||
reversed ? -accel : accel,
|
||||
state.pose.poseMeters, state.pose.curvatureRadPerMeter
|
||||
));
|
||||
}
|
||||
|
||||
return new Trajectory(states);
|
||||
}
|
||||
|
||||
private static void enforceAccelerationLimits(boolean reverse,
|
||||
List<TrajectoryConstraint> constraints,
|
||||
ConstrainedState state) {
|
||||
|
||||
for (final var constraint : constraints) {
|
||||
double factor = reverse ? -1.0 : 1.0;
|
||||
final var minMaxAccel = constraint.getMinMaxAccelerationMetersPerSecondSq(
|
||||
state.pose.poseMeters, state.pose.curvatureRadPerMeter,
|
||||
state.maxVelocityMetersPerSecond * factor);
|
||||
|
||||
if (minMaxAccel.minAccelerationMetersPerSecondSq
|
||||
> minMaxAccel.maxAccelerationMetersPerSecondSq) {
|
||||
throw new TrajectoryGenerationException("The constraint's min acceleration "
|
||||
+ "was greater than its max acceleration.\n Offending Constraint: "
|
||||
+ constraint.getClass().getName()
|
||||
+ "\n If the offending constraint was packaged with WPILib, please file a bug report.");
|
||||
}
|
||||
|
||||
state.minAccelerationMetersPerSecondSq = Math.max(state.minAccelerationMetersPerSecondSq,
|
||||
reverse ? -minMaxAccel.maxAccelerationMetersPerSecondSq
|
||||
: minMaxAccel.minAccelerationMetersPerSecondSq);
|
||||
|
||||
state.maxAccelerationMetersPerSecondSq = Math.min(state.maxAccelerationMetersPerSecondSq,
|
||||
reverse ? -minMaxAccel.minAccelerationMetersPerSecondSq
|
||||
: minMaxAccel.maxAccelerationMetersPerSecondSq);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("MemberName")
|
||||
private static class ConstrainedState {
|
||||
PoseWithCurvature pose;
|
||||
double distanceMeters;
|
||||
double maxVelocityMetersPerSecond;
|
||||
double minAccelerationMetersPerSecondSq;
|
||||
double maxAccelerationMetersPerSecondSq;
|
||||
|
||||
ConstrainedState(PoseWithCurvature pose, double distanceMeters,
|
||||
double maxVelocityMetersPerSecond,
|
||||
double minAccelerationMetersPerSecondSq,
|
||||
double maxAccelerationMetersPerSecondSq) {
|
||||
this.pose = pose;
|
||||
this.distanceMeters = distanceMeters;
|
||||
this.maxVelocityMetersPerSecond = maxVelocityMetersPerSecond;
|
||||
this.minAccelerationMetersPerSecondSq = minAccelerationMetersPerSecondSq;
|
||||
this.maxAccelerationMetersPerSecondSq = maxAccelerationMetersPerSecondSq;
|
||||
}
|
||||
|
||||
ConstrainedState() {
|
||||
pose = new PoseWithCurvature();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static class TrajectoryGenerationException extends RuntimeException {
|
||||
public TrajectoryGenerationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectReader;
|
||||
import com.fasterxml.jackson.databind.ObjectWriter;
|
||||
|
||||
public final class TrajectoryUtil {
|
||||
private static final ObjectReader READER = new ObjectMapper().readerFor(Trajectory.State[].class);
|
||||
private static final ObjectWriter WRITER = new ObjectMapper().writerFor(Trajectory.State[].class);
|
||||
|
||||
private TrajectoryUtil() {
|
||||
throw new UnsupportedOperationException("This is a utility class!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a Trajectory from a PathWeaver-style JSON file.
|
||||
* @param path the path of the json file to import from
|
||||
* @return The trajectory represented by the file.
|
||||
* @throws IOException if reading from the file fails
|
||||
*/
|
||||
public static Trajectory fromPathweaverJson(Path path) throws IOException {
|
||||
try (BufferedReader reader = Files.newBufferedReader(path)) {
|
||||
Trajectory.State[] state = READER.readValue(reader);
|
||||
return new Trajectory(Arrays.asList(state));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a Trajectory to a PathWeaver-style JSON file.
|
||||
* @param trajectory the trajectory to export
|
||||
* @param path the path of the file to export to
|
||||
* @throws IOException if writing to the file fails
|
||||
*/
|
||||
public static void toPathweaverJson(Trajectory trajectory, Path path) throws IOException {
|
||||
Files.createDirectories(path.getParent());
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
|
||||
WRITER.writeValue(writer, trajectory.getStates().toArray(new Trajectory.State[0]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a Trajectory from PathWeaver-style JSON.
|
||||
* @param json the string containing the serialized JSON
|
||||
* @return the trajectory represented by the JSON
|
||||
* @throws JsonProcessingException if deserializing the JSON fails
|
||||
*/
|
||||
public static Trajectory deserializeTrajectory(String json) throws JsonProcessingException {
|
||||
Trajectory.State[] state = READER.readValue(json);
|
||||
return new Trajectory(Arrays.asList(state));
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a Trajectory to PathWeaver-style JSON.
|
||||
* @param trajectory the trajectory to export
|
||||
* @return the string containing the serialized JSON
|
||||
* @throws JsonProcessingException if serializing the Trajectory fails
|
||||
*/
|
||||
public static String serializeTrajectory(Trajectory trajectory) throws JsonProcessingException {
|
||||
return WRITER.writeValueAsString(trajectory.getStates().toArray(new Trajectory.State[0]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import edu.wpi.first.math.MathSharedStore;
|
||||
import edu.wpi.first.math.MathUsageId;
|
||||
|
||||
/**
|
||||
* A trapezoid-shaped velocity profile.
|
||||
*
|
||||
* <p>While this class can be used for a profiled movement from start to finish,
|
||||
* the intended usage is to filter a reference's dynamics based on trapezoidal
|
||||
* velocity constraints. To compute the reference obeying this constraint, do
|
||||
* the following.
|
||||
*
|
||||
* <p>Initialization:
|
||||
* <pre><code>
|
||||
* TrapezoidProfile.Constraints constraints =
|
||||
* new TrapezoidProfile.Constraints(kMaxV, kMaxA);
|
||||
* TrapezoidProfile.State previousProfiledReference =
|
||||
* new TrapezoidProfile.State(initialReference, 0.0);
|
||||
* </code></pre>
|
||||
*
|
||||
* <p>Run on update:
|
||||
* <pre><code>
|
||||
* TrapezoidProfile profile =
|
||||
* new TrapezoidProfile(constraints, unprofiledReference, previousProfiledReference);
|
||||
* previousProfiledReference = profile.calculate(timeSincePreviousUpdate);
|
||||
* </code></pre>
|
||||
*
|
||||
* <p>where `unprofiledReference` is free to change between calls. Note that when
|
||||
* the unprofiled reference is within the constraints, `calculate()` returns the
|
||||
* unprofiled reference unchanged.
|
||||
*
|
||||
* <p>Otherwise, a timer can be started to provide monotonic values for
|
||||
* `calculate()` and to determine when the profile has completed via
|
||||
* `isFinished()`.
|
||||
*/
|
||||
public class TrapezoidProfile {
|
||||
// The direction of the profile, either 1 for forwards or -1 for inverted
|
||||
private int m_direction;
|
||||
|
||||
private Constraints m_constraints;
|
||||
private State m_initial;
|
||||
private State m_goal;
|
||||
|
||||
private double m_endAccel;
|
||||
private double m_endFullSpeed;
|
||||
private double m_endDeccel;
|
||||
|
||||
public static class Constraints {
|
||||
@SuppressWarnings("MemberName")
|
||||
public double maxVelocity;
|
||||
@SuppressWarnings("MemberName")
|
||||
public double maxAcceleration;
|
||||
|
||||
public Constraints() {
|
||||
MathSharedStore.reportUsage(MathUsageId.kTrajectory_TrapezoidProfile, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct constraints for a TrapezoidProfile.
|
||||
*
|
||||
* @param maxVelocity maximum velocity
|
||||
* @param maxAcceleration maximum acceleration
|
||||
*/
|
||||
public Constraints(double maxVelocity, double maxAcceleration) {
|
||||
this.maxVelocity = maxVelocity;
|
||||
this.maxAcceleration = maxAcceleration;
|
||||
MathSharedStore.reportUsage(MathUsageId.kTrajectory_TrapezoidProfile, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public static class State {
|
||||
@SuppressWarnings("MemberName")
|
||||
public double position;
|
||||
@SuppressWarnings("MemberName")
|
||||
public double velocity;
|
||||
|
||||
public State() {
|
||||
}
|
||||
|
||||
public State(double position, double velocity) {
|
||||
this.position = position;
|
||||
this.velocity = velocity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other instanceof State) {
|
||||
State rhs = (State) other;
|
||||
return this.position == rhs.position && this.velocity == rhs.velocity;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(position, velocity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a TrapezoidProfile.
|
||||
*
|
||||
* @param constraints The constraints on the profile, like maximum velocity.
|
||||
* @param goal The desired state when the profile is complete.
|
||||
* @param initial The initial state (usually the current state).
|
||||
*/
|
||||
public TrapezoidProfile(Constraints constraints, State goal, State initial) {
|
||||
m_direction = shouldFlipAcceleration(initial, goal) ? -1 : 1;
|
||||
m_constraints = constraints;
|
||||
m_initial = direct(initial);
|
||||
m_goal = direct(goal);
|
||||
|
||||
if (m_initial.velocity > m_constraints.maxVelocity) {
|
||||
m_initial.velocity = m_constraints.maxVelocity;
|
||||
}
|
||||
|
||||
// Deal with a possibly truncated motion profile (with nonzero initial or
|
||||
// final velocity) by calculating the parameters as if the profile began and
|
||||
// ended at zero velocity
|
||||
double cutoffBegin = m_initial.velocity / m_constraints.maxAcceleration;
|
||||
double cutoffDistBegin = cutoffBegin * cutoffBegin * m_constraints.maxAcceleration / 2.0;
|
||||
|
||||
double cutoffEnd = m_goal.velocity / m_constraints.maxAcceleration;
|
||||
double cutoffDistEnd = cutoffEnd * cutoffEnd * m_constraints.maxAcceleration / 2.0;
|
||||
|
||||
// Now we can calculate the parameters as if it was a full trapezoid instead
|
||||
// of a truncated one
|
||||
|
||||
double fullTrapezoidDist = cutoffDistBegin + (m_goal.position - m_initial.position)
|
||||
+ cutoffDistEnd;
|
||||
double accelerationTime = m_constraints.maxVelocity / m_constraints.maxAcceleration;
|
||||
|
||||
double fullSpeedDist = fullTrapezoidDist - accelerationTime * accelerationTime
|
||||
* m_constraints.maxAcceleration;
|
||||
|
||||
// Handle the case where the profile never reaches full speed
|
||||
if (fullSpeedDist < 0) {
|
||||
accelerationTime = Math.sqrt(fullTrapezoidDist / m_constraints.maxAcceleration);
|
||||
fullSpeedDist = 0;
|
||||
}
|
||||
|
||||
m_endAccel = accelerationTime - cutoffBegin;
|
||||
m_endFullSpeed = m_endAccel + fullSpeedDist / m_constraints.maxVelocity;
|
||||
m_endDeccel = m_endFullSpeed + accelerationTime - cutoffEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a TrapezoidProfile.
|
||||
*
|
||||
* @param constraints The constraints on the profile, like maximum velocity.
|
||||
* @param goal The desired state when the profile is complete.
|
||||
*/
|
||||
public TrapezoidProfile(Constraints constraints, State goal) {
|
||||
this(constraints, goal, new State(0, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the correct position and velocity for the profile at a time t
|
||||
* where the beginning of the profile was at time t = 0.
|
||||
*
|
||||
* @param t The time since the beginning of the profile.
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
public State calculate(double t) {
|
||||
State result = new State(m_initial.position, m_initial.velocity);
|
||||
|
||||
if (t < m_endAccel) {
|
||||
result.velocity += t * m_constraints.maxAcceleration;
|
||||
result.position += (m_initial.velocity + t * m_constraints.maxAcceleration / 2.0) * t;
|
||||
} else if (t < m_endFullSpeed) {
|
||||
result.velocity = m_constraints.maxVelocity;
|
||||
result.position += (m_initial.velocity + m_endAccel * m_constraints.maxAcceleration
|
||||
/ 2.0) * m_endAccel + m_constraints.maxVelocity * (t - m_endAccel);
|
||||
} else if (t <= m_endDeccel) {
|
||||
result.velocity = m_goal.velocity + (m_endDeccel - t) * m_constraints.maxAcceleration;
|
||||
double timeLeft = m_endDeccel - t;
|
||||
result.position = m_goal.position - (m_goal.velocity + timeLeft
|
||||
* m_constraints.maxAcceleration / 2.0) * timeLeft;
|
||||
} else {
|
||||
result = m_goal;
|
||||
}
|
||||
|
||||
return direct(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time left until a target distance in the profile is reached.
|
||||
*
|
||||
* @param target The target distance.
|
||||
*/
|
||||
public double timeLeftUntil(double target) {
|
||||
double position = m_initial.position * m_direction;
|
||||
double velocity = m_initial.velocity * m_direction;
|
||||
|
||||
double endAccel = m_endAccel * m_direction;
|
||||
double endFullSpeed = m_endFullSpeed * m_direction - endAccel;
|
||||
|
||||
if (target < position) {
|
||||
endAccel = -endAccel;
|
||||
endFullSpeed = -endFullSpeed;
|
||||
velocity = -velocity;
|
||||
}
|
||||
|
||||
endAccel = Math.max(endAccel, 0);
|
||||
endFullSpeed = Math.max(endFullSpeed, 0);
|
||||
double endDeccel = m_endDeccel - endAccel - endFullSpeed;
|
||||
endDeccel = Math.max(endDeccel, 0);
|
||||
|
||||
final double acceleration = m_constraints.maxAcceleration;
|
||||
final double decceleration = -m_constraints.maxAcceleration;
|
||||
|
||||
double distToTarget = Math.abs(target - position);
|
||||
if (distToTarget < 1e-6) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
double accelDist = velocity * endAccel + 0.5 * acceleration * endAccel * endAccel;
|
||||
|
||||
double deccelVelocity;
|
||||
if (endAccel > 0) {
|
||||
deccelVelocity = Math.sqrt(Math.abs(velocity * velocity + 2 * acceleration * accelDist));
|
||||
} else {
|
||||
deccelVelocity = velocity;
|
||||
}
|
||||
|
||||
double deccelDist = deccelVelocity * endDeccel + 0.5 * decceleration * endDeccel * endDeccel;
|
||||
|
||||
deccelDist = Math.max(deccelDist, 0);
|
||||
|
||||
double fullSpeedDist = m_constraints.maxVelocity * endFullSpeed;
|
||||
|
||||
if (accelDist > distToTarget) {
|
||||
accelDist = distToTarget;
|
||||
fullSpeedDist = 0;
|
||||
deccelDist = 0;
|
||||
} else if (accelDist + fullSpeedDist > distToTarget) {
|
||||
fullSpeedDist = distToTarget - accelDist;
|
||||
deccelDist = 0;
|
||||
} else {
|
||||
deccelDist = distToTarget - fullSpeedDist - accelDist;
|
||||
}
|
||||
|
||||
double accelTime = (-velocity + Math.sqrt(Math.abs(velocity * velocity + 2 * acceleration
|
||||
* accelDist))) / acceleration;
|
||||
|
||||
double deccelTime = (-deccelVelocity + Math.sqrt(Math.abs(deccelVelocity * deccelVelocity
|
||||
+ 2 * decceleration * deccelDist))) / decceleration;
|
||||
|
||||
double fullSpeedTime = fullSpeedDist / m_constraints.maxVelocity;
|
||||
|
||||
return accelTime + fullSpeedTime + deccelTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total time the profile takes to reach the goal.
|
||||
*/
|
||||
public double totalTime() {
|
||||
return m_endDeccel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the profile has reached the goal.
|
||||
*
|
||||
* <p>The profile has reached the goal if the time since the profile started
|
||||
* has exceeded the profile's total time.
|
||||
*
|
||||
* @param t The time since the beginning of the profile.
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
public boolean isFinished(double t) {
|
||||
return t >= totalTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the profile inverted.
|
||||
*
|
||||
* <p>The profile is inverted if goal position is less than the initial position.
|
||||
*
|
||||
* @param initial The initial state (usually the current state).
|
||||
* @param goal The desired state when the profile is complete.
|
||||
*/
|
||||
private static boolean shouldFlipAcceleration(State initial, State goal) {
|
||||
return initial.position > goal.position;
|
||||
}
|
||||
|
||||
// Flip the sign of the velocity and position if the profile is inverted
|
||||
private State direct(State in) {
|
||||
State result = new State(in.position, in.velocity);
|
||||
result.position = result.position * m_direction;
|
||||
result.velocity = result.velocity * m_direction;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory.constraint;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
|
||||
/**
|
||||
* A constraint on the maximum absolute centripetal acceleration allowed when
|
||||
* traversing a trajectory. The centripetal acceleration of a robot is defined
|
||||
* as the velocity squared divided by the radius of curvature.
|
||||
*
|
||||
* <p>Effectively, limiting the maximum centripetal acceleration will cause the
|
||||
* robot to slow down around tight turns, making it easier to track trajectories
|
||||
* with sharp turns.
|
||||
*/
|
||||
public class CentripetalAccelerationConstraint implements TrajectoryConstraint {
|
||||
private final double m_maxCentripetalAccelerationMetersPerSecondSq;
|
||||
|
||||
/**
|
||||
* Constructs a centripetal acceleration constraint.
|
||||
*
|
||||
* @param maxCentripetalAccelerationMetersPerSecondSq The max centripetal acceleration.
|
||||
*/
|
||||
public CentripetalAccelerationConstraint(double maxCentripetalAccelerationMetersPerSecondSq) {
|
||||
m_maxCentripetalAccelerationMetersPerSecondSq = maxCentripetalAccelerationMetersPerSecondSq;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the max velocity given the current pose and curvature.
|
||||
*
|
||||
* @param poseMeters The pose at the current point in the trajectory.
|
||||
* @param curvatureRadPerMeter The curvature at the current point in the trajectory.
|
||||
* @param velocityMetersPerSecond The velocity at the current point in the trajectory before
|
||||
* constraints are applied.
|
||||
* @return The absolute maximum velocity.
|
||||
*/
|
||||
@Override
|
||||
public double getMaxVelocityMetersPerSecond(Pose2d poseMeters, double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond) {
|
||||
// ac = v^2 / r
|
||||
// k (curvature) = 1 / r
|
||||
|
||||
// therefore, ac = v^2 * k
|
||||
// ac / k = v^2
|
||||
// v = std::sqrt(ac / k)
|
||||
|
||||
return Math.sqrt(m_maxCentripetalAccelerationMetersPerSecondSq
|
||||
/ Math.abs(curvatureRadPerMeter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum and maximum allowable acceleration for the trajectory
|
||||
* given pose, curvature, and speed.
|
||||
*
|
||||
* @param poseMeters The pose at the current point in the trajectory.
|
||||
* @param curvatureRadPerMeter The curvature at the current point in the trajectory.
|
||||
* @param velocityMetersPerSecond The speed at the current point in the trajectory.
|
||||
* @return The min and max acceleration bounds.
|
||||
*/
|
||||
@Override
|
||||
public MinMax getMinMaxAccelerationMetersPerSecondSq(Pose2d poseMeters,
|
||||
double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond) {
|
||||
// The acceleration of the robot has no impact on the centripetal acceleration
|
||||
// of the robot.
|
||||
return new MinMax();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory.constraint;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.kinematics.ChassisSpeeds;
|
||||
import edu.wpi.first.wpilibj.kinematics.DifferentialDriveKinematics;
|
||||
|
||||
/**
|
||||
* A class that enforces constraints on the differential drive kinematics.
|
||||
* This can be used to ensure that the trajectory is constructed so that the
|
||||
* commanded velocities for both sides of the drivetrain stay below a certain
|
||||
* limit.
|
||||
*/
|
||||
public class DifferentialDriveKinematicsConstraint implements TrajectoryConstraint {
|
||||
private final double m_maxSpeedMetersPerSecond;
|
||||
private final DifferentialDriveKinematics m_kinematics;
|
||||
|
||||
/**
|
||||
* Constructs a differential drive dynamics constraint.
|
||||
*
|
||||
* @param kinematics A kinematics component describing the drive geometry.
|
||||
* @param maxSpeedMetersPerSecond The max speed that a side of the robot can travel at.
|
||||
*/
|
||||
public DifferentialDriveKinematicsConstraint(final DifferentialDriveKinematics kinematics,
|
||||
double maxSpeedMetersPerSecond) {
|
||||
m_maxSpeedMetersPerSecond = maxSpeedMetersPerSecond;
|
||||
m_kinematics = kinematics;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the max velocity given the current pose and curvature.
|
||||
*
|
||||
* @param poseMeters The pose at the current point in the trajectory.
|
||||
* @param curvatureRadPerMeter The curvature at the current point in the trajectory.
|
||||
* @param velocityMetersPerSecond The velocity at the current point in the trajectory before
|
||||
* constraints are applied.
|
||||
* @return The absolute maximum velocity.
|
||||
*/
|
||||
@Override
|
||||
public double getMaxVelocityMetersPerSecond(Pose2d poseMeters, double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond) {
|
||||
// Create an object to represent the current chassis speeds.
|
||||
var chassisSpeeds = new ChassisSpeeds(velocityMetersPerSecond,
|
||||
0, velocityMetersPerSecond * curvatureRadPerMeter);
|
||||
|
||||
// Get the wheel speeds and normalize them to within the max velocity.
|
||||
var wheelSpeeds = m_kinematics.toWheelSpeeds(chassisSpeeds);
|
||||
wheelSpeeds.normalize(m_maxSpeedMetersPerSecond);
|
||||
|
||||
// Return the new linear chassis speed.
|
||||
return m_kinematics.toChassisSpeeds(wheelSpeeds).vxMetersPerSecond;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum and maximum allowable acceleration for the trajectory
|
||||
* given pose, curvature, and speed.
|
||||
*
|
||||
* @param poseMeters The pose at the current point in the trajectory.
|
||||
* @param curvatureRadPerMeter The curvature at the current point in the trajectory.
|
||||
* @param velocityMetersPerSecond The speed at the current point in the trajectory.
|
||||
* @return The min and max acceleration bounds.
|
||||
*/
|
||||
@Override
|
||||
public MinMax getMinMaxAccelerationMetersPerSecondSq(Pose2d poseMeters,
|
||||
double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond) {
|
||||
return new MinMax();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory.constraint;
|
||||
|
||||
import edu.wpi.first.wpilibj.controller.SimpleMotorFeedforward;
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.kinematics.ChassisSpeeds;
|
||||
import edu.wpi.first.wpilibj.kinematics.DifferentialDriveKinematics;
|
||||
|
||||
import static edu.wpi.first.wpiutil.ErrorMessages.requireNonNullParam;
|
||||
|
||||
/**
|
||||
* A class that enforces constraints on differential drive voltage expenditure based on the motor
|
||||
* dynamics and the drive kinematics. Ensures that the acceleration of any wheel of the robot
|
||||
* while following the trajectory is never higher than what can be achieved with the given
|
||||
* maximum voltage.
|
||||
*/
|
||||
public class DifferentialDriveVoltageConstraint implements TrajectoryConstraint {
|
||||
private final SimpleMotorFeedforward m_feedforward;
|
||||
private final DifferentialDriveKinematics m_kinematics;
|
||||
private final double m_maxVoltage;
|
||||
|
||||
/**
|
||||
* Creates a new DifferentialDriveVoltageConstraint.
|
||||
*
|
||||
* @param feedforward A feedforward component describing the behavior of the drive.
|
||||
* @param kinematics A kinematics component describing the drive geometry.
|
||||
* @param maxVoltage The maximum voltage available to the motors while following the path.
|
||||
* Should be somewhat less than the nominal battery voltage (12V) to account
|
||||
* for "voltage sag" due to current draw.
|
||||
*/
|
||||
public DifferentialDriveVoltageConstraint(SimpleMotorFeedforward feedforward,
|
||||
DifferentialDriveKinematics kinematics,
|
||||
double maxVoltage) {
|
||||
m_feedforward = requireNonNullParam(feedforward, "feedforward",
|
||||
"DifferentialDriveVoltageConstraint");
|
||||
m_kinematics = requireNonNullParam(kinematics, "kinematics",
|
||||
"DifferentialDriveVoltageConstraint");
|
||||
m_maxVoltage = maxVoltage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getMaxVelocityMetersPerSecond(Pose2d poseMeters, double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond) {
|
||||
return Double.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MinMax getMinMaxAccelerationMetersPerSecondSq(Pose2d poseMeters,
|
||||
double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond) {
|
||||
|
||||
var wheelSpeeds = m_kinematics.toWheelSpeeds(new ChassisSpeeds(velocityMetersPerSecond, 0,
|
||||
velocityMetersPerSecond
|
||||
* curvatureRadPerMeter));
|
||||
|
||||
double maxWheelSpeed = Math.max(wheelSpeeds.leftMetersPerSecond,
|
||||
wheelSpeeds.rightMetersPerSecond);
|
||||
double minWheelSpeed = Math.min(wheelSpeeds.leftMetersPerSecond,
|
||||
wheelSpeeds.rightMetersPerSecond);
|
||||
|
||||
// Calculate maximum/minimum possible accelerations from motor dynamics
|
||||
// and max/min wheel speeds
|
||||
double maxWheelAcceleration =
|
||||
m_feedforward.maxAchievableAcceleration(m_maxVoltage, maxWheelSpeed);
|
||||
double minWheelAcceleration =
|
||||
m_feedforward.minAchievableAcceleration(m_maxVoltage, minWheelSpeed);
|
||||
|
||||
// Robot chassis turning on radius = 1/|curvature|. Outer wheel has radius
|
||||
// increased by half of the trackwidth T. Inner wheel has radius decreased
|
||||
// by half of the trackwidth. Achassis / radius = Aouter / (radius + T/2), so
|
||||
// Achassis = Aouter * radius / (radius + T/2) = Aouter / (1 + |curvature|T/2).
|
||||
// Inner wheel is similar.
|
||||
|
||||
// sgn(speed) term added to correctly account for which wheel is on
|
||||
// outside of turn:
|
||||
// If moving forward, max acceleration constraint corresponds to wheel on outside of turn
|
||||
// If moving backward, max acceleration constraint corresponds to wheel on inside of turn
|
||||
|
||||
// When velocity is zero, then wheel velocities are uniformly zero (robot cannot be
|
||||
// turning on its center) - we have to treat this as a special case, as it breaks
|
||||
// the signum function. Both max and min acceleration are *reduced in magnitude*
|
||||
// in this case.
|
||||
|
||||
double maxChassisAcceleration;
|
||||
double minChassisAcceleration;
|
||||
|
||||
if (velocityMetersPerSecond == 0) {
|
||||
maxChassisAcceleration =
|
||||
maxWheelAcceleration
|
||||
/ (1 + m_kinematics.trackWidthMeters * Math.abs(curvatureRadPerMeter) / 2);
|
||||
minChassisAcceleration =
|
||||
minWheelAcceleration
|
||||
/ (1 + m_kinematics.trackWidthMeters * Math.abs(curvatureRadPerMeter) / 2);
|
||||
} else {
|
||||
maxChassisAcceleration =
|
||||
maxWheelAcceleration
|
||||
/ (1 + m_kinematics.trackWidthMeters * Math.abs(curvatureRadPerMeter)
|
||||
* Math.signum(velocityMetersPerSecond) / 2);
|
||||
minChassisAcceleration =
|
||||
minWheelAcceleration
|
||||
/ (1 - m_kinematics.trackWidthMeters * Math.abs(curvatureRadPerMeter)
|
||||
* Math.signum(velocityMetersPerSecond) / 2);
|
||||
}
|
||||
|
||||
// When turning about a point inside of the wheelbase (i.e. radius less than half
|
||||
// the trackwidth), the inner wheel's direction changes, but the magnitude remains
|
||||
// the same. The formula above changes sign for the inner wheel when this happens.
|
||||
// We can accurately account for this by simply negating the inner wheel.
|
||||
|
||||
if ((m_kinematics.trackWidthMeters / 2) > (1 / Math.abs(curvatureRadPerMeter))) {
|
||||
if (velocityMetersPerSecond > 0) {
|
||||
minChassisAcceleration = -minChassisAcceleration;
|
||||
} else if (velocityMetersPerSecond < 0) {
|
||||
maxChassisAcceleration = -maxChassisAcceleration;
|
||||
}
|
||||
}
|
||||
|
||||
return new MinMax(minChassisAcceleration, maxChassisAcceleration);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory.constraint;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Translation2d;
|
||||
|
||||
/**
|
||||
* Enforces a particular constraint only within an elliptical region.
|
||||
*/
|
||||
public class EllipticalRegionConstraint implements TrajectoryConstraint {
|
||||
private final Translation2d m_center;
|
||||
private final Translation2d m_radii;
|
||||
private final TrajectoryConstraint m_constraint;
|
||||
|
||||
/**
|
||||
* Constructs a new EllipticalRegionConstraint.
|
||||
*
|
||||
* @param center The center of the ellipse in which to enforce the constraint.
|
||||
* @param xWidth The width of the ellipse in which to enforce the constraint.
|
||||
* @param yWidth The height of the ellipse in which to enforce the constraint.
|
||||
* @param rotation The rotation to apply to all radii around the origin.
|
||||
* @param constraint The constraint to enforce when the robot is within the region.
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
public EllipticalRegionConstraint(Translation2d center, double xWidth, double yWidth,
|
||||
Rotation2d rotation, TrajectoryConstraint constraint) {
|
||||
m_center = center;
|
||||
m_radii = new Translation2d(xWidth / 2.0, yWidth / 2.0).rotateBy(rotation);
|
||||
m_constraint = constraint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getMaxVelocityMetersPerSecond(Pose2d poseMeters, double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond) {
|
||||
if (isPoseInRegion(poseMeters)) {
|
||||
return m_constraint.getMaxVelocityMetersPerSecond(poseMeters, curvatureRadPerMeter,
|
||||
velocityMetersPerSecond);
|
||||
} else {
|
||||
return Double.POSITIVE_INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MinMax getMinMaxAccelerationMetersPerSecondSq(Pose2d poseMeters,
|
||||
double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond) {
|
||||
if (isPoseInRegion(poseMeters)) {
|
||||
return m_constraint.getMinMaxAccelerationMetersPerSecondSq(poseMeters,
|
||||
curvatureRadPerMeter, velocityMetersPerSecond);
|
||||
} else {
|
||||
return new MinMax();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the specified robot pose is within the region that the constraint
|
||||
* is enforced in.
|
||||
*
|
||||
* @param robotPose The robot pose.
|
||||
* @return Whether the robot pose is within the constraint region.
|
||||
*/
|
||||
public boolean isPoseInRegion(Pose2d robotPose) {
|
||||
// The region (disk) bounded by the ellipse is given by the equation:
|
||||
// ((x-h)^2)/Rx^2) + ((y-k)^2)/Ry^2) <= 1
|
||||
// If the inequality is satisfied, then it is inside the ellipse; otherwise
|
||||
// it is outside the ellipse.
|
||||
// Both sides have been multiplied by Rx^2 * Ry^2 for efficiency reasons.
|
||||
return Math.pow(robotPose.getX() - m_center.getX(), 2)
|
||||
* Math.pow(m_radii.getY(), 2)
|
||||
+ Math.pow(robotPose.getY() - m_center.getY(), 2)
|
||||
* Math.pow(m_radii.getX(), 2) <= Math.pow(m_radii.getX(), 2) * Math.pow(m_radii.getY(), 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory.constraint;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
|
||||
/**
|
||||
* Represents a constraint that enforces a max velocity. This can be composed with the
|
||||
* {@link EllipticalRegionConstraint} or {@link RectangularRegionConstraint} to enforce
|
||||
* a max velocity in a region.
|
||||
*/
|
||||
public class MaxVelocityConstraint implements TrajectoryConstraint {
|
||||
private final double m_maxVelocity;
|
||||
|
||||
/**
|
||||
* Constructs a new MaxVelocityConstraint.
|
||||
*
|
||||
* @param maxVelocityMetersPerSecond The max velocity.
|
||||
*/
|
||||
public MaxVelocityConstraint(double maxVelocityMetersPerSecond) {
|
||||
m_maxVelocity = maxVelocityMetersPerSecond;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getMaxVelocityMetersPerSecond(Pose2d poseMeters, double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond) {
|
||||
return m_maxVelocity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrajectoryConstraint.MinMax getMinMaxAccelerationMetersPerSecondSq(
|
||||
Pose2d poseMeters, double curvatureRadPerMeter, double velocityMetersPerSecond) {
|
||||
return new MinMax();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory.constraint;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.kinematics.ChassisSpeeds;
|
||||
import edu.wpi.first.wpilibj.kinematics.MecanumDriveKinematics;
|
||||
|
||||
/**
|
||||
* A class that enforces constraints on the mecanum drive kinematics.
|
||||
* This can be used to ensure that the trajectory is constructed so that the
|
||||
* commanded velocities for all 4 wheels of the drivetrain stay below a certain
|
||||
* limit.
|
||||
*/
|
||||
public class MecanumDriveKinematicsConstraint implements TrajectoryConstraint {
|
||||
private final double m_maxSpeedMetersPerSecond;
|
||||
private final MecanumDriveKinematics m_kinematics;
|
||||
|
||||
/**
|
||||
* Constructs a mecanum drive dynamics constraint.
|
||||
*
|
||||
* @param maxSpeedMetersPerSecond The max speed that a side of the robot can travel at.
|
||||
*/
|
||||
public MecanumDriveKinematicsConstraint(final MecanumDriveKinematics kinematics,
|
||||
double maxSpeedMetersPerSecond) {
|
||||
m_maxSpeedMetersPerSecond = maxSpeedMetersPerSecond;
|
||||
m_kinematics = kinematics;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the max velocity given the current pose and curvature.
|
||||
*
|
||||
* @param poseMeters The pose at the current point in the trajectory.
|
||||
* @param curvatureRadPerMeter The curvature at the current point in the trajectory.
|
||||
* @param velocityMetersPerSecond The velocity at the current point in the trajectory before
|
||||
* constraints are applied.
|
||||
* @return The absolute maximum velocity.
|
||||
*/
|
||||
@Override
|
||||
public double getMaxVelocityMetersPerSecond(Pose2d poseMeters, double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond) {
|
||||
// Represents the velocity of the chassis in the x direction
|
||||
var xdVelocity = velocityMetersPerSecond * poseMeters.getRotation().getCos();
|
||||
|
||||
// Represents the velocity of the chassis in the y direction
|
||||
var ydVelocity = velocityMetersPerSecond * poseMeters.getRotation().getSin();
|
||||
|
||||
// Create an object to represent the current chassis speeds.
|
||||
var chassisSpeeds = new ChassisSpeeds(xdVelocity,
|
||||
ydVelocity, velocityMetersPerSecond * curvatureRadPerMeter);
|
||||
|
||||
// Get the wheel speeds and normalize them to within the max velocity.
|
||||
var wheelSpeeds = m_kinematics.toWheelSpeeds(chassisSpeeds);
|
||||
wheelSpeeds.normalize(m_maxSpeedMetersPerSecond);
|
||||
|
||||
// Convert normalized wheel speeds back to chassis speeds
|
||||
var normSpeeds = m_kinematics.toChassisSpeeds(wheelSpeeds);
|
||||
|
||||
// Return the new linear chassis speed.
|
||||
return Math.hypot(normSpeeds.vxMetersPerSecond, normSpeeds.vyMetersPerSecond);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum and maximum allowable acceleration for the trajectory
|
||||
* given pose, curvature, and speed.
|
||||
*
|
||||
* @param poseMeters The pose at the current point in the trajectory.
|
||||
* @param curvatureRadPerMeter The curvature at the current point in the trajectory.
|
||||
* @param velocityMetersPerSecond The speed at the current point in the trajectory.
|
||||
* @return The min and max acceleration bounds.
|
||||
*/
|
||||
@Override
|
||||
public MinMax getMinMaxAccelerationMetersPerSecondSq(Pose2d poseMeters,
|
||||
double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond) {
|
||||
return new MinMax();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory.constraint;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Translation2d;
|
||||
|
||||
/**
|
||||
* Enforces a particular constraint only within a rectangular region.
|
||||
*/
|
||||
public class RectangularRegionConstraint implements TrajectoryConstraint {
|
||||
private final Translation2d m_bottomLeftPoint;
|
||||
private final Translation2d m_topRightPoint;
|
||||
private final TrajectoryConstraint m_constraint;
|
||||
|
||||
/**
|
||||
* Constructs a new RectangularRegionConstraint.
|
||||
*
|
||||
* @param bottomLeftPoint The bottom left point of the rectangular region in which to
|
||||
* enforce the constraint.
|
||||
* @param topRightPoint The top right point of the rectangular region in which to enforce
|
||||
* the constraint.
|
||||
* @param constraint The constraint to enforce when the robot is within the region.
|
||||
*/
|
||||
public RectangularRegionConstraint(Translation2d bottomLeftPoint, Translation2d topRightPoint,
|
||||
TrajectoryConstraint constraint) {
|
||||
m_bottomLeftPoint = bottomLeftPoint;
|
||||
m_topRightPoint = topRightPoint;
|
||||
m_constraint = constraint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getMaxVelocityMetersPerSecond(Pose2d poseMeters, double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond) {
|
||||
if (isPoseInRegion(poseMeters)) {
|
||||
return m_constraint.getMaxVelocityMetersPerSecond(poseMeters, curvatureRadPerMeter,
|
||||
velocityMetersPerSecond);
|
||||
} else {
|
||||
return Double.POSITIVE_INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MinMax getMinMaxAccelerationMetersPerSecondSq(Pose2d poseMeters,
|
||||
double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond) {
|
||||
if (isPoseInRegion(poseMeters)) {
|
||||
return m_constraint.getMinMaxAccelerationMetersPerSecondSq(poseMeters,
|
||||
curvatureRadPerMeter, velocityMetersPerSecond);
|
||||
} else {
|
||||
return new MinMax();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the specified robot pose is within the region that the constraint
|
||||
* is enforced in.
|
||||
*
|
||||
* @param robotPose The robot pose.
|
||||
* @return Whether the robot pose is within the constraint region.
|
||||
*/
|
||||
public boolean isPoseInRegion(Pose2d robotPose) {
|
||||
return robotPose.getX() >= m_bottomLeftPoint.getX()
|
||||
&& robotPose.getX() <= m_topRightPoint.getX()
|
||||
&& robotPose.getY() >= m_bottomLeftPoint.getY()
|
||||
&& robotPose.getY() <= m_topRightPoint.getY();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory.constraint;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.kinematics.ChassisSpeeds;
|
||||
import edu.wpi.first.wpilibj.kinematics.SwerveDriveKinematics;
|
||||
|
||||
/**
|
||||
* A class that enforces constraints on the swerve drive kinematics.
|
||||
* This can be used to ensure that the trajectory is constructed so that the
|
||||
* commanded velocities for all 4 wheels of the drivetrain stay below a certain
|
||||
* limit.
|
||||
*/
|
||||
public class SwerveDriveKinematicsConstraint implements TrajectoryConstraint {
|
||||
private final double m_maxSpeedMetersPerSecond;
|
||||
private final SwerveDriveKinematics m_kinematics;
|
||||
|
||||
/**
|
||||
* Constructs a swerve drive dynamics constraint.
|
||||
*
|
||||
* @param maxSpeedMetersPerSecond The max speed that a side of the robot can travel at.
|
||||
*/
|
||||
public SwerveDriveKinematicsConstraint(final SwerveDriveKinematics kinematics,
|
||||
double maxSpeedMetersPerSecond) {
|
||||
m_maxSpeedMetersPerSecond = maxSpeedMetersPerSecond;
|
||||
m_kinematics = kinematics;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the max velocity given the current pose and curvature.
|
||||
*
|
||||
* @param poseMeters The pose at the current point in the trajectory.
|
||||
* @param curvatureRadPerMeter The curvature at the current point in the trajectory.
|
||||
* @param velocityMetersPerSecond The velocity at the current point in the trajectory before
|
||||
* constraints are applied.
|
||||
* @return The absolute maximum velocity.
|
||||
*/
|
||||
@Override
|
||||
public double getMaxVelocityMetersPerSecond(Pose2d poseMeters, double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond) {
|
||||
// Represents the velocity of the chassis in the x direction
|
||||
var xdVelocity = velocityMetersPerSecond * poseMeters.getRotation().getCos();
|
||||
|
||||
// Represents the velocity of the chassis in the y direction
|
||||
var ydVelocity = velocityMetersPerSecond * poseMeters.getRotation().getSin();
|
||||
|
||||
// Create an object to represent the current chassis speeds.
|
||||
var chassisSpeeds = new ChassisSpeeds(xdVelocity,
|
||||
ydVelocity, velocityMetersPerSecond * curvatureRadPerMeter);
|
||||
|
||||
// Get the wheel speeds and normalize them to within the max velocity.
|
||||
var wheelSpeeds = m_kinematics.toSwerveModuleStates(chassisSpeeds);
|
||||
SwerveDriveKinematics.normalizeWheelSpeeds(wheelSpeeds, m_maxSpeedMetersPerSecond);
|
||||
|
||||
// Convert normalized wheel speeds back to chassis speeds
|
||||
var normSpeeds = m_kinematics.toChassisSpeeds(wheelSpeeds);
|
||||
|
||||
// Return the new linear chassis speed.
|
||||
return Math.hypot(normSpeeds.vxMetersPerSecond, normSpeeds.vyMetersPerSecond);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum and maximum allowable acceleration for the trajectory
|
||||
* given pose, curvature, and speed.
|
||||
*
|
||||
* @param poseMeters The pose at the current point in the trajectory.
|
||||
* @param curvatureRadPerMeter The curvature at the current point in the trajectory.
|
||||
* @param velocityMetersPerSecond The speed at the current point in the trajectory.
|
||||
* @return The min and max acceleration bounds.
|
||||
*/
|
||||
@Override
|
||||
public MinMax getMinMaxAccelerationMetersPerSecondSq(Pose2d poseMeters,
|
||||
double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond) {
|
||||
return new MinMax();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory.constraint;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
|
||||
/**
|
||||
* An interface for defining user-defined velocity and acceleration constraints
|
||||
* while generating trajectories.
|
||||
*/
|
||||
public interface TrajectoryConstraint {
|
||||
|
||||
/**
|
||||
* Returns the max velocity given the current pose and curvature.
|
||||
*
|
||||
* @param poseMeters The pose at the current point in the trajectory.
|
||||
* @param curvatureRadPerMeter The curvature at the current point in the trajectory.
|
||||
* @param velocityMetersPerSecond The velocity at the current point in the trajectory before
|
||||
* constraints are applied.
|
||||
* @return The absolute maximum velocity.
|
||||
*/
|
||||
double getMaxVelocityMetersPerSecond(Pose2d poseMeters, double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond);
|
||||
|
||||
/**
|
||||
* Returns the minimum and maximum allowable acceleration for the trajectory
|
||||
* given pose, curvature, and speed.
|
||||
*
|
||||
* @param poseMeters The pose at the current point in the trajectory.
|
||||
* @param curvatureRadPerMeter The curvature at the current point in the trajectory.
|
||||
* @param velocityMetersPerSecond The speed at the current point in the trajectory.
|
||||
* @return The min and max acceleration bounds.
|
||||
*/
|
||||
MinMax getMinMaxAccelerationMetersPerSecondSq(Pose2d poseMeters, double curvatureRadPerMeter,
|
||||
double velocityMetersPerSecond);
|
||||
|
||||
/**
|
||||
* Represents a minimum and maximum acceleration.
|
||||
*/
|
||||
@SuppressWarnings("MemberName")
|
||||
class MinMax {
|
||||
public double minAccelerationMetersPerSecondSq = -Double.MAX_VALUE;
|
||||
public double maxAccelerationMetersPerSecondSq = +Double.MAX_VALUE;
|
||||
|
||||
/**
|
||||
* Constructs a MinMax.
|
||||
*
|
||||
* @param minAccelerationMetersPerSecondSq The minimum acceleration.
|
||||
* @param maxAccelerationMetersPerSecondSq The maximum acceleration.
|
||||
*/
|
||||
public MinMax(double minAccelerationMetersPerSecondSq,
|
||||
double maxAccelerationMetersPerSecondSq) {
|
||||
this.minAccelerationMetersPerSecondSq = minAccelerationMetersPerSecondSq;
|
||||
this.maxAccelerationMetersPerSecondSq = maxAccelerationMetersPerSecondSq;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a MinMax with default values.
|
||||
*/
|
||||
public MinMax() {
|
||||
}
|
||||
}
|
||||
}
|
||||
104
wpimath/src/main/java/edu/wpi/first/wpilibj/util/Units.java
Normal file
104
wpimath/src/main/java/edu/wpi/first/wpilibj/util/Units.java
Normal file
@@ -0,0 +1,104 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.util;
|
||||
|
||||
/**
|
||||
* Utility class that converts between commonly used units in FRC.
|
||||
*/
|
||||
public final class Units {
|
||||
private static final double kInchesPerFoot = 12.0;
|
||||
private static final double kMetersPerInch = 0.0254;
|
||||
private static final double kSecondsPerMinute = 60;
|
||||
|
||||
/**
|
||||
* Utility class, so constructor is private.
|
||||
*/
|
||||
private Units() {
|
||||
throw new UnsupportedOperationException("This is a utility class!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts given meters to feet.
|
||||
*
|
||||
* @param meters The meters to convert to feet.
|
||||
* @return Feet converted from meters.
|
||||
*/
|
||||
public static double metersToFeet(double meters) {
|
||||
return metersToInches(meters) / kInchesPerFoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts given feet to meters.
|
||||
*
|
||||
* @param feet The feet to convert to meters.
|
||||
* @return Meters converted from feet.
|
||||
*/
|
||||
public static double feetToMeters(double feet) {
|
||||
return inchesToMeters(feet * kInchesPerFoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts given meters to inches.
|
||||
*
|
||||
* @param meters The meters to convert to inches.
|
||||
* @return Inches converted from meters.
|
||||
*/
|
||||
public static double metersToInches(double meters) {
|
||||
return meters / kMetersPerInch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts given inches to meters.
|
||||
*
|
||||
* @param inches The inches to convert to meters.
|
||||
* @return Meters converted from inches.
|
||||
*/
|
||||
public static double inchesToMeters(double inches) {
|
||||
return inches * kMetersPerInch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts given degrees to radians.
|
||||
*
|
||||
* @param degrees The degrees to convert to radians.
|
||||
* @return Radians converted from degrees.
|
||||
*/
|
||||
public static double degreesToRadians(double degrees) {
|
||||
return Math.toRadians(degrees);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts given radians to degrees.
|
||||
*
|
||||
* @param radians The radians to convert to degrees.
|
||||
* @return Degrees converted from radians.
|
||||
*/
|
||||
public static double radiansToDegrees(double radians) {
|
||||
return Math.toDegrees(radians);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts rotations per minute to radians per second.
|
||||
*
|
||||
* @param rpm The rotations per minute to convert to radians per second.
|
||||
* @return Radians per second converted from rotations per minute.
|
||||
*/
|
||||
public static double rotationsPerMinuteToRadiansPerSecond(double rpm) {
|
||||
return rpm * Math.PI / (kSecondsPerMinute / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts radians per second to rotations per minute.
|
||||
*
|
||||
* @param radiansPerSecond The radians per second to convert to from rotations per minute.
|
||||
* @return Rotations per minute converted from radians per second.
|
||||
*/
|
||||
public static double radiansPerSecondToRotationsPerMinute(double radiansPerSecond) {
|
||||
return radiansPerSecond * (kSecondsPerMinute / 2) / Math.PI;
|
||||
}
|
||||
}
|
||||
56
wpimath/src/main/java/edu/wpi/first/wpiutil/math/Drake.java
Normal file
56
wpimath/src/main/java/edu/wpi/first/wpiutil/math/Drake.java
Normal file
@@ -0,0 +1,56 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpiutil.math;
|
||||
|
||||
import org.ejml.simple.SimpleMatrix;
|
||||
|
||||
public final class Drake {
|
||||
private Drake() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Solves the discrete alegebraic Riccati equation.
|
||||
*
|
||||
* @param A System matrix.
|
||||
* @param B Input matrix.
|
||||
* @param Q State cost matrix.
|
||||
* @param R Input cost matrix.
|
||||
* @return Solution of DARE.
|
||||
*/
|
||||
@SuppressWarnings({"LocalVariableName", "ParameterName"})
|
||||
public static SimpleMatrix discreteAlgebraicRiccatiEquation(
|
||||
SimpleMatrix A,
|
||||
SimpleMatrix B,
|
||||
SimpleMatrix Q,
|
||||
SimpleMatrix R) {
|
||||
var S = new SimpleMatrix(A.numRows(), A.numCols());
|
||||
DrakeJNI.discreteAlgebraicRiccatiEquation(A.getDDRM().getData(), B.getDDRM().getData(),
|
||||
Q.getDDRM().getData(), R.getDDRM().getData(), A.numCols(), B.numCols(),
|
||||
S.getDDRM().getData());
|
||||
return S;
|
||||
}
|
||||
|
||||
/**
|
||||
* Solves the discrete alegebraic Riccati equation.
|
||||
*
|
||||
* @param A System matrix.
|
||||
* @param B Input matrix.
|
||||
* @param Q State cost matrix.
|
||||
* @param R Input cost matrix.
|
||||
* @return Solution of DARE.
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
public static SimpleMatrix discreteAlgebraicRiccatiEquation(
|
||||
Matrix A,
|
||||
Matrix B,
|
||||
Matrix Q,
|
||||
Matrix R) {
|
||||
return discreteAlgebraicRiccatiEquation(A.getStorage(), B.getStorage(), Q.getStorage(),
|
||||
R.getStorage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpiutil.math;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import edu.wpi.first.wpiutil.RuntimeLoader;
|
||||
|
||||
public final class DrakeJNI {
|
||||
static boolean libraryLoaded = false;
|
||||
static RuntimeLoader<DrakeJNI> loader = null;
|
||||
|
||||
static {
|
||||
if (Helper.getExtractOnStaticLoad()) {
|
||||
try {
|
||||
loader = new RuntimeLoader<>("wpimathjni", RuntimeLoader.getDefaultExtractionRoot(),
|
||||
DrakeJNI.class);
|
||||
loader.loadLibrary();
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
libraryLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force load the library.
|
||||
*
|
||||
* @throws IOException If the library could not be loaded or found.
|
||||
*/
|
||||
public static synchronized void forceLoad() throws IOException {
|
||||
if (libraryLoaded) {
|
||||
return;
|
||||
}
|
||||
loader = new RuntimeLoader<>("wpimathjni", RuntimeLoader.getDefaultExtractionRoot(),
|
||||
DrakeJNI.class);
|
||||
loader.loadLibrary();
|
||||
libraryLoaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Solves the discrete alegebraic Riccati equation.
|
||||
*
|
||||
* @param A Array containing elements of A in row-major order.
|
||||
* @param B Array containing elements of B in row-major order.
|
||||
* @param Q Array containing elements of Q in row-major order.
|
||||
* @param R Array containing elements of R in row-major order.
|
||||
* @param states Number of states in A matrix.
|
||||
* @param inputs Number of inputs in B matrix.
|
||||
* @param S Array storage for DARE solution.
|
||||
*/
|
||||
public static native void discreteAlgebraicRiccatiEquation(
|
||||
double[] A,
|
||||
double[] B,
|
||||
double[] Q,
|
||||
double[] R,
|
||||
int states,
|
||||
int inputs,
|
||||
double[] S);
|
||||
|
||||
public static class Helper {
|
||||
private static AtomicBoolean extractOnStaticLoad = new AtomicBoolean(true);
|
||||
|
||||
public static boolean getExtractOnStaticLoad() {
|
||||
return extractOnStaticLoad.get();
|
||||
}
|
||||
|
||||
public static void setExtractOnStaticLoad(boolean load) {
|
||||
extractOnStaticLoad.set(load);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpiutil.math;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ejml.simple.SimpleMatrix;
|
||||
|
||||
/**
|
||||
* A class for constructing arbitrary RxC matrices.
|
||||
*
|
||||
* @param <R> The number of rows of the desired matrix.
|
||||
* @param <C> The number of columns of the desired matrix.
|
||||
*/
|
||||
public class MatBuilder<R extends Num, C extends Num> {
|
||||
private final Nat<R> m_rows;
|
||||
private final Nat<C> m_cols;
|
||||
|
||||
/**
|
||||
* Fills the matrix with the given data, encoded in row major form.
|
||||
* (The matrix is filled row by row, left to right with the given data).
|
||||
*
|
||||
* @param data The data to fill the matrix with.
|
||||
* @return The constructed matrix.
|
||||
*/
|
||||
@SuppressWarnings("LineLength")
|
||||
public final Matrix<R, C> fill(double... data) {
|
||||
if (Objects.requireNonNull(data).length != this.m_rows.getNum() * this.m_cols.getNum()) {
|
||||
throw new IllegalArgumentException("Invalid matrix data provided. Wanted " + this.m_rows.getNum()
|
||||
+ " x " + this.m_cols.getNum() + " matrix, but got " + data.length + " elements");
|
||||
} else {
|
||||
return new Matrix<>(new SimpleMatrix(this.m_rows.getNum(), this.m_cols.getNum(), true, data));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link MatBuilder} with the given dimensions.
|
||||
* @param rows The number of rows of the matrix.
|
||||
* @param cols The number of columns of the matrix.
|
||||
*/
|
||||
public MatBuilder(Nat<R> rows, Nat<C> cols) {
|
||||
this.m_rows = Objects.requireNonNull(rows);
|
||||
this.m_cols = Objects.requireNonNull(cols);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpiutil.math;
|
||||
|
||||
public final class MathUtil {
|
||||
private MathUtil() {
|
||||
throw new AssertionError("utility class");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns value clamped between low and high boundaries.
|
||||
*
|
||||
* @param value Value to clamp.
|
||||
* @param low The lower boundary to which to clamp value.
|
||||
* @param high The higher boundary to which to clamp value.
|
||||
*/
|
||||
public static int clamp(int value, int low, int high) {
|
||||
return Math.max(low, Math.min(value, high));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns value clamped between low and high boundaries.
|
||||
*
|
||||
* @param value Value to clamp.
|
||||
* @param low The lower boundary to which to clamp value.
|
||||
* @param high The higher boundary to which to clamp value.
|
||||
*/
|
||||
public static double clamp(double value, double low, double high) {
|
||||
return Math.max(low, Math.min(value, high));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains theta to within the range (-pi, pi].
|
||||
*
|
||||
* @param theta The angle to normalize.
|
||||
* @return The normalized angle.
|
||||
*/
|
||||
@SuppressWarnings("LocalVariableName")
|
||||
public static double normalizeAngle(double theta) {
|
||||
// Constraint theta to within (-3pi, pi)
|
||||
int nPiPos = (int) ((theta + Math.PI) / 2.0 / Math.PI);
|
||||
theta -= nPiPos * 2.0 * Math.PI;
|
||||
|
||||
// Cut off the bottom half of the above range to constrain within
|
||||
// (-pi, pi]
|
||||
int nPiNeg = (int) ((theta - Math.PI) / 2.0 / Math.PI);
|
||||
theta -= nPiNeg * 2.0 * Math.PI;
|
||||
|
||||
return theta;
|
||||
}
|
||||
}
|
||||
327
wpimath/src/main/java/edu/wpi/first/wpiutil/math/Matrix.java
Normal file
327
wpimath/src/main/java/edu/wpi/first/wpiutil/math/Matrix.java
Normal file
@@ -0,0 +1,327 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpiutil.math;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ejml.dense.row.CommonOps_DDRM;
|
||||
import org.ejml.dense.row.NormOps_DDRM;
|
||||
import org.ejml.simple.SimpleMatrix;
|
||||
|
||||
/**
|
||||
* A shape-safe wrapper over Efficient Java Matrix Library (EJML) matrices.
|
||||
*
|
||||
* <p>This class is intended to be used alongside the state space library.
|
||||
*
|
||||
* @param <R> The number of rows in this matrix.
|
||||
* @param <C> The number of columns in this matrix.
|
||||
*/
|
||||
@SuppressWarnings("PMD.TooManyMethods")
|
||||
public class Matrix<R extends Num, C extends Num> {
|
||||
|
||||
private final SimpleMatrix m_storage;
|
||||
|
||||
/**
|
||||
* Gets the number of columns in this matrix.
|
||||
*
|
||||
* @return The number of columns, according to the internal storage.
|
||||
*/
|
||||
public final int getNumCols() {
|
||||
return this.m_storage.numCols();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of rows in this matrix.
|
||||
*
|
||||
* @return The number of rows, according to the internal storage.
|
||||
*/
|
||||
public final int getNumRows() {
|
||||
return this.m_storage.numRows();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an element of this matrix.
|
||||
*
|
||||
* @param row The row of the element.
|
||||
* @param col The column of the element.
|
||||
* @return The element in this matrix at row,col.
|
||||
*/
|
||||
public final double get(int row, int col) {
|
||||
return this.m_storage.get(row, col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value at the given indices.
|
||||
*
|
||||
* @param row The row of the element.
|
||||
* @param col The column of the element.
|
||||
* @param value The value to insert at the given location.
|
||||
*/
|
||||
public final void set(int row, int col, double value) {
|
||||
this.m_storage.set(row, col, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* If a vector then a square matrix is returned
|
||||
* if a matrix then a vector of diagonal elements is returned.
|
||||
*
|
||||
* @return Diagonal elements inside a vector or a square matrix with the same diagonal elements.
|
||||
*/
|
||||
public final Matrix<R, C> diag() {
|
||||
return new Matrix<>(this.m_storage.diag());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the largest element of this matrix.
|
||||
*
|
||||
* @return The largest element of this matrix.
|
||||
*/
|
||||
public final double maxInternal() {
|
||||
return CommonOps_DDRM.elementMax(this.m_storage.getDDRM());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the smallest element of this matrix.
|
||||
*
|
||||
* @return The smallest element of this matrix.
|
||||
*/
|
||||
public final double minInternal() {
|
||||
return CommonOps_DDRM.elementMin(this.m_storage.getDDRM());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the mean of the elements in this matrix.
|
||||
*
|
||||
* @return The mean value of this matrix.
|
||||
*/
|
||||
public final double mean() {
|
||||
return this.elementSum() / (double) this.m_storage.getNumElements();
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies this matrix with another that has C rows.
|
||||
*
|
||||
* <p>As matrix multiplication is only defined if the number of columns
|
||||
* in the first matrix matches the number of rows in the second,
|
||||
* this operation will fail to compile under any other circumstances.
|
||||
*
|
||||
* @param other The other matrix to multiply by.
|
||||
* @param <C2> The number of columns in the second matrix.
|
||||
* @return The result of the matrix multiplication between this and the given matrix.
|
||||
*/
|
||||
public final <C2 extends Num> Matrix<R, C2> times(Matrix<C, C2> other) {
|
||||
return new Matrix<>(this.m_storage.mult(other.m_storage));
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies all the elements of this matrix by the given scalar.
|
||||
*
|
||||
* @param value The scalar value to multiply by.
|
||||
* @return A new matrix with all the elements multiplied by the given value.
|
||||
*/
|
||||
public final Matrix<R, C> times(double value) {
|
||||
return new Matrix<>(this.m_storage.scale(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Returns a matrix which is the result of an element by element multiplication of 'this' and 'b'.
|
||||
* c<sub>i,j</sub> = a<sub>i,j</sub>*b<sub>i,j</sub>
|
||||
* </p>
|
||||
*
|
||||
* @param other A matrix.
|
||||
* @return The element by element multiplication of 'this' and 'b'.
|
||||
*/
|
||||
public final Matrix<R, C> elementTimes(Matrix<R, C> other) {
|
||||
return new Matrix<>(this.m_storage.elementMult(Objects.requireNonNull(other).m_storage));
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtracts the given value from all the elements of this matrix.
|
||||
*
|
||||
* @param value The value to subtract.
|
||||
* @return The resultant matrix.
|
||||
*/
|
||||
public final Matrix<R, C> minus(double value) {
|
||||
return new Matrix<>(this.m_storage.minus(value));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Subtracts the given matrix from this matrix.
|
||||
*
|
||||
* @param value The matrix to subtract.
|
||||
* @return The resultant matrix.
|
||||
*/
|
||||
public final Matrix<R, C> minus(Matrix<R, C> value) {
|
||||
return new Matrix<>(this.m_storage.minus(Objects.requireNonNull(value).m_storage));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds the given value to all the elements of this matrix.
|
||||
*
|
||||
* @param value The value to add.
|
||||
* @return The resultant matrix.
|
||||
*/
|
||||
public final Matrix<R, C> plus(double value) {
|
||||
return new Matrix<>(this.m_storage.plus(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given matrix to this matrix.
|
||||
*
|
||||
* @param value The matrix to add.
|
||||
* @return The resultant matrix.
|
||||
*/
|
||||
public final Matrix<R, C> plus(Matrix<R, C> value) {
|
||||
return new Matrix<>(this.m_storage.plus(value.m_storage));
|
||||
}
|
||||
|
||||
/**
|
||||
* Divides all elements of this matrix by the given value.
|
||||
*
|
||||
* @param value The value to divide by.
|
||||
* @return The resultant matrix.
|
||||
*/
|
||||
public final Matrix<R, C> div(int value) {
|
||||
return new Matrix<>(this.m_storage.divide((double) value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Divides all elements of this matrix by the given value.
|
||||
*
|
||||
* @param value The value to divide by.
|
||||
* @return The resultant matrix.
|
||||
*/
|
||||
public final Matrix<R, C> div(double value) {
|
||||
return new Matrix<>(this.m_storage.divide(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the transpose, M^T of this matrix.
|
||||
*
|
||||
* @return The tranpose matrix.
|
||||
*/
|
||||
public final Matrix<C, R> transpose() {
|
||||
return new Matrix<>(this.m_storage.transpose());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a copy of this matrix.
|
||||
*
|
||||
* @return A copy of this matrix.
|
||||
*/
|
||||
public final Matrix<R, C> copy() {
|
||||
return new Matrix<>(this.m_storage.copy());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the inverse matrix of this matrix.
|
||||
*
|
||||
* @return The inverse of this matrix.
|
||||
* @throws org.ejml.data.SingularMatrixException If this matrix is non-invertable.
|
||||
*/
|
||||
public final Matrix<R, C> inv() {
|
||||
return new Matrix<>(this.m_storage.invert());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the determinant of this matrix.
|
||||
*
|
||||
* @return The determinant of this matrix.
|
||||
*/
|
||||
public final double det() {
|
||||
return this.m_storage.determinant();
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the Frobenius normal of the matrix.<br>
|
||||
* <br>
|
||||
* normF = Sqrt{ ∑<sub>i=1:m</sub> ∑<sub>j=1:n</sub> { a<sub>ij</sub><sup>2</sup>} }
|
||||
*
|
||||
* @return The matrix's Frobenius normal.
|
||||
*/
|
||||
public final double normF() {
|
||||
return this.m_storage.normF();
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the induced p = 1 matrix norm.<br>
|
||||
* <br>
|
||||
* ||A||<sub>1</sub>= max(j=1 to n; sum(i=1 to m; |a<sub>ij</sub>|))
|
||||
*
|
||||
* @return The norm.
|
||||
*/
|
||||
public final double normIndP1() {
|
||||
return NormOps_DDRM.inducedP1(this.m_storage.getDDRM());
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the sum of all the elements in the matrix.
|
||||
*
|
||||
* @return Sum of all the elements.
|
||||
*/
|
||||
public final double elementSum() {
|
||||
return this.m_storage.elementSum();
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the trace of the matrix.
|
||||
*
|
||||
* @return The trace of the matrix.
|
||||
*/
|
||||
public final double trace() {
|
||||
return this.m_storage.trace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a matrix which is the result of an element by element power of 'this' and 'b':
|
||||
* c<sub>i,j</sub> = a<sub>i,j</sub> ^ b.
|
||||
*
|
||||
* @param b Scalar
|
||||
* @return The element by element power of 'this' and 'b'.
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
public final Matrix<R, C> epow(double b) {
|
||||
return new Matrix<>(this.m_storage.elementPower(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a matrix which is the result of an element by element power of 'this' and 'b':
|
||||
* c<sub>i,j</sub> = a<sub>i,j</sub> ^ b.
|
||||
*
|
||||
* @param b Scalar.
|
||||
* @return The element by element power of 'this' and 'b'.
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
public final Matrix<R, C> epow(int b) {
|
||||
return new Matrix<>(this.m_storage.elementPower((double) b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the EJML {@link SimpleMatrix} backing this wrapper.
|
||||
*
|
||||
* @return The untyped EJML {@link SimpleMatrix}.
|
||||
*/
|
||||
public final SimpleMatrix getStorage() {
|
||||
return this.m_storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new matrix with the given storage.
|
||||
* Caller should make sure that the provided generic bounds match the shape of the provided matrix
|
||||
*
|
||||
* @param storage The {@link SimpleMatrix} to back this value
|
||||
*/
|
||||
public Matrix(SimpleMatrix storage) {
|
||||
this.m_storage = Objects.requireNonNull(storage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpiutil.math;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.ejml.simple.SimpleMatrix;
|
||||
|
||||
import edu.wpi.first.wpiutil.math.numbers.N1;
|
||||
|
||||
public final class MatrixUtils {
|
||||
private MatrixUtils() {
|
||||
throw new AssertionError("utility class");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new matrix of zeros.
|
||||
*
|
||||
* @param rows The number of rows in the matrix.
|
||||
* @param cols The number of columns in the matrix.
|
||||
* @param <R> The number of rows in the matrix as a generic.
|
||||
* @param <C> The number of columns in the matrix as a generic.
|
||||
* @return An RxC matrix filled with zeros.
|
||||
*/
|
||||
@SuppressWarnings("LineLength")
|
||||
public static <R extends Num, C extends Num> Matrix<R, C> zeros(Nat<R> rows, Nat<C> cols) {
|
||||
return new Matrix<>(
|
||||
new SimpleMatrix(Objects.requireNonNull(rows).getNum(), Objects.requireNonNull(cols).getNum()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new vector of zeros.
|
||||
*
|
||||
* @param nums The size of the desired vector.
|
||||
* @param <N> The size of the desired vector as a generic.
|
||||
* @return A vector of size N filled with zeros.
|
||||
*/
|
||||
public static <N extends Num> Matrix<N, N1> zeros(Nat<N> nums) {
|
||||
return new Matrix<>(new SimpleMatrix(Objects.requireNonNull(nums).getNum(), 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the identity matrix of the given dimension.
|
||||
*
|
||||
* @param dim The dimension of the desired matrix.
|
||||
* @param <D> The dimension of the desired matrix as a generic.
|
||||
* @return The DxD identity matrix.
|
||||
*/
|
||||
public static <D extends Num> Matrix<D, D> eye(Nat<D> dim) {
|
||||
return new Matrix<>(SimpleMatrix.identity(Objects.requireNonNull(dim).getNum()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint to the MatBuilder class for creation
|
||||
* of custom matrices with the given dimensions and contents.
|
||||
*
|
||||
* @param rows The number of rows of the desired matrix.
|
||||
* @param cols The number of columns of the desired matrix.
|
||||
* @param <R> The number of rows of the desired matrix as a generic.
|
||||
* @param <C> The number of columns of the desired matrix as a generic.
|
||||
* @return A builder to construct the matrix.
|
||||
*/
|
||||
public static <R extends Num, C extends Num> MatBuilder<R, C> mat(Nat<R> rows, Nat<C> cols) {
|
||||
return new MatBuilder<>(rows, cols);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint to the VecBuilder class for creation
|
||||
* of custom vectors with the given size and contents.
|
||||
*
|
||||
* @param dim The dimension of the vector.
|
||||
* @param <D> The dimension of the vector as a generic.
|
||||
* @return A builder to construct the vector.
|
||||
*/
|
||||
public static <D extends Num> VecBuilder<D> vec(Nat<D> dim) {
|
||||
return new VecBuilder<>(dim);
|
||||
}
|
||||
}
|
||||
20
wpimath/src/main/java/edu/wpi/first/wpiutil/math/Num.java
Normal file
20
wpimath/src/main/java/edu/wpi/first/wpiutil/math/Num.java
Normal file
@@ -0,0 +1,20 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpiutil.math;
|
||||
|
||||
/**
|
||||
* A number expressed as a java class.
|
||||
*/
|
||||
public abstract class Num {
|
||||
/**
|
||||
* The number this is backing.
|
||||
*
|
||||
* @return The number represented by this class.
|
||||
*/
|
||||
public abstract int getNum();
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpiutil.math;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.ejml.dense.row.NormOps_DDRM;
|
||||
import org.ejml.simple.SimpleBase;
|
||||
import org.ejml.simple.SimpleMatrix;
|
||||
|
||||
public class SimpleMatrixUtils {
|
||||
private SimpleMatrixUtils() {}
|
||||
|
||||
/**
|
||||
* Compute the matrix exponential, e^M of the given matrix.
|
||||
*
|
||||
* @param matrix The matrix to compute the exponential of.
|
||||
* @return The resultant matrix.
|
||||
*/
|
||||
@SuppressWarnings({"LocalVariableName", "LineLength"})
|
||||
public static SimpleMatrix expm(SimpleMatrix matrix) {
|
||||
BiFunction<SimpleMatrix, SimpleMatrix, SimpleMatrix> solveProvider = SimpleBase::solve;
|
||||
SimpleMatrix A = matrix;
|
||||
double A_L1 = NormOps_DDRM.inducedP1(matrix.getDDRM());
|
||||
int n_squarings = 0;
|
||||
|
||||
if (A_L1 < 1.495585217958292e-002) {
|
||||
Pair<SimpleMatrix, SimpleMatrix> pair = _pade3(A);
|
||||
return dispatchPade(pair.getFirst(), pair.getSecond(), n_squarings, solveProvider);
|
||||
} else if (A_L1 < 2.539398330063230e-001) {
|
||||
Pair<SimpleMatrix, SimpleMatrix> pair = _pade5(A);
|
||||
return dispatchPade(pair.getFirst(), pair.getSecond(), n_squarings, solveProvider);
|
||||
} else if (A_L1 < 9.504178996162932e-001) {
|
||||
Pair<SimpleMatrix, SimpleMatrix> pair = _pade7(A);
|
||||
return dispatchPade(pair.getFirst(), pair.getSecond(), n_squarings, solveProvider);
|
||||
} else if (A_L1 < 2.097847961257068e+000) {
|
||||
Pair<SimpleMatrix, SimpleMatrix> pair = _pade9(A);
|
||||
return dispatchPade(pair.getFirst(), pair.getSecond(), n_squarings, solveProvider);
|
||||
} else {
|
||||
double maxNorm = 5.371920351148152;
|
||||
double log = Math.log(A_L1 / maxNorm) / Math.log(2); // logb(2, arg)
|
||||
n_squarings = (int) Math.max(0, Math.ceil(log));
|
||||
A = A.divide(Math.pow(2.0, n_squarings));
|
||||
Pair<SimpleMatrix, SimpleMatrix> pair = _pade13(A);
|
||||
return dispatchPade(pair.getFirst(), pair.getSecond(), n_squarings, solveProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"LocalVariableName", "ParameterName", "LineLength"})
|
||||
private static SimpleMatrix dispatchPade(SimpleMatrix U, SimpleMatrix V,
|
||||
int nSquarings, BiFunction<SimpleMatrix, SimpleMatrix, SimpleMatrix> solveProvider) {
|
||||
SimpleMatrix P = U.plus(V);
|
||||
SimpleMatrix Q = U.negative().plus(V);
|
||||
|
||||
SimpleMatrix R = solveProvider.apply(Q, P);
|
||||
|
||||
for (int i = 0; i < nSquarings; i++) {
|
||||
R = R.mult(R);
|
||||
}
|
||||
|
||||
return R;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"MethodName", "LocalVariableName", "ParameterName"})
|
||||
private static Pair<SimpleMatrix, SimpleMatrix> _pade3(SimpleMatrix A) {
|
||||
double[] b = new double[]{120, 60, 12, 1};
|
||||
SimpleMatrix ident = eye(A.numRows(), A.numCols());
|
||||
|
||||
SimpleMatrix A2 = A.mult(A);
|
||||
SimpleMatrix U = A.mult(A2.mult(ident.scale(b[1]).plus(b[3])));
|
||||
SimpleMatrix V = A2.scale(b[2]).plus(ident.scale(b[0]));
|
||||
return new Pair<>(U, V);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"MethodName", "LocalVariableName", "ParameterName"})
|
||||
private static Pair<SimpleMatrix, SimpleMatrix> _pade5(SimpleMatrix A) {
|
||||
double[] b = new double[]{30240, 15120, 3360, 420, 30, 1};
|
||||
SimpleMatrix ident = eye(A.numRows(), A.numCols());
|
||||
SimpleMatrix A2 = A.mult(A);
|
||||
SimpleMatrix A4 = A2.mult(A2);
|
||||
|
||||
SimpleMatrix U = A.mult(A4.scale(b[5]).plus(A2.scale(b[3])).plus(ident.scale(b[1])));
|
||||
SimpleMatrix V = A4.scale(b[4]).plus(A2.scale(b[2])).plus(ident.scale(b[0]));
|
||||
|
||||
return new Pair<>(U, V);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"MethodName", "LocalVariableName", "LineLength", "ParameterName"})
|
||||
private static Pair<SimpleMatrix, SimpleMatrix> _pade7(SimpleMatrix A) {
|
||||
double[] b = new double[]{17297280, 8648640, 1995840, 277200, 25200, 1512, 56, 1};
|
||||
SimpleMatrix ident = eye(A.numRows(), A.numCols());
|
||||
SimpleMatrix A2 = A.mult(A);
|
||||
SimpleMatrix A4 = A2.mult(A2);
|
||||
SimpleMatrix A6 = A4.mult(A2);
|
||||
|
||||
SimpleMatrix U = A.mult(A6.scale(b[7]).plus(A4.scale(b[5])).plus(A2.scale(b[3])).plus(ident.scale(b[1])));
|
||||
SimpleMatrix V = A6.scale(b[6]).plus(A4.scale(b[4])).plus(A2.scale(b[2])).plus(ident.scale(b[0]));
|
||||
|
||||
return new Pair<>(U, V);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"MethodName", "LocalVariableName", "ParameterName", "LineLength"})
|
||||
private static Pair<SimpleMatrix, SimpleMatrix> _pade9(SimpleMatrix A) {
|
||||
double[] b = new double[]{17643225600.0, 8821612800.0, 2075673600, 302702400, 30270240,
|
||||
2162160, 110880, 3960, 90, 1};
|
||||
SimpleMatrix ident = eye(A.numRows(), A.numCols());
|
||||
SimpleMatrix A2 = A.mult(A);
|
||||
SimpleMatrix A4 = A2.mult(A2);
|
||||
SimpleMatrix A6 = A4.mult(A2);
|
||||
SimpleMatrix A8 = A6.mult(A2);
|
||||
|
||||
SimpleMatrix U = A.mult(A8.scale(b[9]).plus(A6.scale(b[7])).plus(A4.scale(b[5])).plus(A2.scale(b[3])).plus(ident.scale(b[1])));
|
||||
SimpleMatrix V = A8.scale(b[8]).plus(A6.scale(b[6])).plus(A4.scale(b[4])).plus(A2.scale(b[2])).plus(ident.scale(b[0]));
|
||||
|
||||
return new Pair<>(U, V);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"MethodName", "LocalVariableName", "LineLength", "ParameterName"})
|
||||
private static Pair<SimpleMatrix, SimpleMatrix> _pade13(SimpleMatrix A) {
|
||||
double[] b = new double[]{64764752532480000.0, 32382376266240000.0, 7771770303897600.0,
|
||||
1187353796428800.0, 129060195264000.0, 10559470521600.0, 670442572800.0,
|
||||
33522128640.0, 1323241920, 40840800, 960960, 16380, 182, 1};
|
||||
SimpleMatrix ident = eye(A.numRows(), A.numCols());
|
||||
|
||||
SimpleMatrix A2 = A.mult(A);
|
||||
SimpleMatrix A4 = A2.mult(A2);
|
||||
SimpleMatrix A6 = A4.mult(A2);
|
||||
|
||||
SimpleMatrix U = A.mult(A6.scale(b[13]).plus(A4.scale(b[11])).plus(A2.scale(b[9])).plus(A6.scale(b[7])).plus(A4.scale(b[5])).plus(A2.scale(b[3])).plus(ident.scale(b[1])));
|
||||
SimpleMatrix V = A6.mult(A6.scale(b[12]).plus(A4.scale(b[10])).plus(A2.scale(b[8]))).plus(A6.scale(b[6]).plus(A4.scale(b[4])).plus(A2.scale(b[2])).plus(ident.scale(b[0])));
|
||||
|
||||
return new Pair<>(U, V);
|
||||
}
|
||||
|
||||
private static SimpleMatrix eye(int rows, int cols) {
|
||||
return SimpleMatrix.identity(Math.min(rows, cols));
|
||||
}
|
||||
|
||||
private static class Pair<A, B> {
|
||||
private final A m_first;
|
||||
private final B m_second;
|
||||
|
||||
Pair(A first, B second) {
|
||||
m_first = first;
|
||||
m_second = second;
|
||||
}
|
||||
|
||||
public A getFirst() {
|
||||
return m_first;
|
||||
}
|
||||
|
||||
public B getSecond() {
|
||||
return m_second;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpiutil.math;
|
||||
|
||||
import edu.wpi.first.wpiutil.math.numbers.N1;
|
||||
|
||||
/**
|
||||
* A specialization of {@link MatBuilder} for constructing vectors (Nx1 matrices).
|
||||
*
|
||||
* @param <N> The dimension of the vector to be constructed.
|
||||
*/
|
||||
public class VecBuilder<N extends Num> extends MatBuilder<N, N1> {
|
||||
public VecBuilder(Nat<N> rows) {
|
||||
super(rows, Nat.N1());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user