001// Copyright (c) FIRST and other WPILib contributors. 002// Open Source Software; you can modify and/or share it under the terms of 003// the WPILib BSD license file in the root directory of this project. 004 005package edu.wpi.first.math; 006 007import edu.wpi.first.math.geometry.Pose2d; 008import edu.wpi.first.math.numbers.N1; 009import edu.wpi.first.math.numbers.N3; 010import edu.wpi.first.math.numbers.N4; 011import java.util.Random; 012import org.ejml.simple.SimpleMatrix; 013 014/** State-space utilities. */ 015public final class StateSpaceUtil { 016 private static Random rand = new Random(); 017 018 private StateSpaceUtil() { 019 throw new UnsupportedOperationException("This is a utility class!"); 020 } 021 022 /** 023 * Creates a covariance matrix from the given vector for use with Kalman filters. 024 * 025 * <p>Each element is squared and placed on the covariance matrix diagonal. 026 * 027 * @param <States> Num representing the states of the system. 028 * @param states A Nat representing the states of the system. 029 * @param stdDevs For a Q matrix, its elements are the standard deviations of each state from how 030 * the model behaves. For an R matrix, its elements are the standard deviations for each 031 * output measurement. 032 * @return Process noise or measurement noise covariance matrix. 033 */ 034 public static <States extends Num> Matrix<States, States> makeCovarianceMatrix( 035 Nat<States> states, Matrix<States, N1> stdDevs) { 036 var result = new Matrix<>(states, states); 037 for (int i = 0; i < states.getNum(); i++) { 038 result.set(i, i, Math.pow(stdDevs.get(i, 0), 2)); 039 } 040 return result; 041 } 042 043 /** 044 * Creates a vector of normally distributed white noise with the given noise intensities for each 045 * element. 046 * 047 * @param <N> Num representing the dimensionality of the noise vector to create. 048 * @param stdDevs A matrix whose elements are the standard deviations of each element of the noise 049 * vector. 050 * @return White noise vector. 051 */ 052 public static <N extends Num> Matrix<N, N1> makeWhiteNoiseVector(Matrix<N, N1> stdDevs) { 053 Matrix<N, N1> result = new Matrix<>(new SimpleMatrix(stdDevs.getNumRows(), 1)); 054 for (int i = 0; i < stdDevs.getNumRows(); i++) { 055 result.set(i, 0, rand.nextGaussian() * stdDevs.get(i, 0)); 056 } 057 return result; 058 } 059 060 /** 061 * Creates a cost matrix from the given vector for use with LQR. 062 * 063 * <p>The cost matrix is constructed using Bryson's rule. The inverse square of each tolerance is 064 * placed on the cost matrix diagonal. If a tolerance is infinity, its cost matrix entry is set to 065 * zero. 066 * 067 * @param <Elements> Nat representing the number of system states or inputs. 068 * @param tolerances An array. For a Q matrix, its elements are the maximum allowed excursions of 069 * the states from the reference. For an R matrix, its elements are the maximum allowed 070 * excursions of the control inputs from no actuation. 071 * @return State excursion or control effort cost matrix. 072 */ 073 public static <Elements extends Num> Matrix<Elements, Elements> makeCostMatrix( 074 Matrix<Elements, N1> tolerances) { 075 Matrix<Elements, Elements> result = 076 new Matrix<>(new SimpleMatrix(tolerances.getNumRows(), tolerances.getNumRows())); 077 result.fill(0.0); 078 079 for (int i = 0; i < tolerances.getNumRows(); i++) { 080 if (tolerances.get(i, 0) == Double.POSITIVE_INFINITY) { 081 result.set(i, i, 0.0); 082 } else { 083 result.set(i, i, 1.0 / Math.pow(tolerances.get(i, 0), 2)); 084 } 085 } 086 087 return result; 088 } 089 090 /** 091 * Returns true if (A, B) is a stabilizable pair. 092 * 093 * <p>(A, B) is stabilizable if and only if the uncontrollable eigenvalues of A, if any, have 094 * absolute values less than one, where an eigenvalue is uncontrollable if rank([λI - A, B]) %3C n 095 * where n is the number of states. 096 * 097 * @param <States> Num representing the size of A. 098 * @param <Inputs> Num representing the columns of B. 099 * @param A System matrix. 100 * @param B Input matrix. 101 * @return If the system is stabilizable. 102 */ 103 public static <States extends Num, Inputs extends Num> boolean isStabilizable( 104 Matrix<States, States> A, Matrix<States, Inputs> B) { 105 return WPIMathJNI.isStabilizable(A.getNumRows(), B.getNumCols(), A.getData(), B.getData()); 106 } 107 108 /** 109 * Returns true if (A, C) is a detectable pair. 110 * 111 * <p>(A, C) is detectable if and only if the unobservable eigenvalues of A, if any, have absolute 112 * values less than one, where an eigenvalue is unobservable if rank([λI - A; C]) %3C n where n is 113 * the number of states. 114 * 115 * @param <States> Num representing the size of A. 116 * @param <Outputs> Num representing the rows of C. 117 * @param A System matrix. 118 * @param C Output matrix. 119 * @return If the system is detectable. 120 */ 121 public static <States extends Num, Outputs extends Num> boolean isDetectable( 122 Matrix<States, States> A, Matrix<Outputs, States> C) { 123 return WPIMathJNI.isStabilizable( 124 A.getNumRows(), C.getNumRows(), A.transpose().getData(), C.transpose().getData()); 125 } 126 127 /** 128 * Convert a {@link Pose2d} to a vector of [x, y, theta], where theta is in radians. 129 * 130 * @param pose A pose to convert to a vector. 131 * @return The given pose in vector form, with the third element, theta, in radians. 132 */ 133 public static Matrix<N3, N1> poseToVector(Pose2d pose) { 134 return VecBuilder.fill(pose.getX(), pose.getY(), pose.getRotation().getRadians()); 135 } 136 137 /** 138 * Clamp the input u to the min and max. 139 * 140 * @param u The input to clamp. 141 * @param umin The minimum input magnitude. 142 * @param umax The maximum input magnitude. 143 * @param <I> Number of inputs. 144 * @return The clamped input. 145 */ 146 public static <I extends Num> Matrix<I, N1> clampInputMaxMagnitude( 147 Matrix<I, N1> u, Matrix<I, N1> umin, Matrix<I, N1> umax) { 148 var result = new Matrix<I, N1>(new SimpleMatrix(u.getNumRows(), 1)); 149 for (int i = 0; i < u.getNumRows(); i++) { 150 result.set(i, 0, MathUtil.clamp(u.get(i, 0), umin.get(i, 0), umax.get(i, 0))); 151 } 152 return result; 153 } 154 155 /** 156 * Renormalize all inputs if any exceeds the maximum magnitude. Useful for systems such as 157 * differential drivetrains. 158 * 159 * @param u The input vector. 160 * @param maxMagnitude The maximum magnitude any input can have. 161 * @param <I> Number of inputs. 162 * @return The normalizedInput 163 */ 164 public static <I extends Num> Matrix<I, N1> desaturateInputVector( 165 Matrix<I, N1> u, double maxMagnitude) { 166 double maxValue = u.maxAbs(); 167 boolean isCapped = maxValue > maxMagnitude; 168 169 if (isCapped) { 170 return u.times(maxMagnitude / maxValue); 171 } 172 return u; 173 } 174 175 /** 176 * Convert a {@link Pose2d} to a vector of [x, y, cos(theta), sin(theta)], where theta is in 177 * radians. 178 * 179 * @param pose A pose to convert to a vector. 180 * @return The given pose in as a 4x1 vector of x, y, cos(theta), and sin(theta). 181 */ 182 public static Matrix<N4, N1> poseTo4dVector(Pose2d pose) { 183 return VecBuilder.fill( 184 pose.getTranslation().getX(), 185 pose.getTranslation().getY(), 186 pose.getRotation().getCos(), 187 pose.getRotation().getSin()); 188 } 189 190 /** 191 * Convert a {@link Pose2d} to a vector of [x, y, theta], where theta is in radians. 192 * 193 * @param pose A pose to convert to a vector. 194 * @return The given pose in vector form, with the third element, theta, in radians. 195 */ 196 public static Matrix<N3, N1> poseTo3dVector(Pose2d pose) { 197 return VecBuilder.fill( 198 pose.getTranslation().getX(), 199 pose.getTranslation().getY(), 200 pose.getRotation().getRadians()); 201 } 202}