// Copyright (c) FIRST and other WPILib contributors. // Open Source Software; you can modify and/or share it under the terms of // the WPILib BSD license file in the root directory of this project. package edu.wpi.first.math; import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.jni.StateSpaceUtilJNI; import edu.wpi.first.math.numbers.N1; import edu.wpi.first.math.numbers.N3; import edu.wpi.first.math.numbers.N4; import java.util.Random; import org.ejml.simple.SimpleMatrix; /** State-space utilities. */ public final class StateSpaceUtil { private static Random rand = new Random(); private StateSpaceUtil() { throw new UnsupportedOperationException("This is a utility class!"); } /** * Creates a covariance matrix from the given vector for use with Kalman filters. * *

Each element is squared and placed on the covariance matrix diagonal. * * @param Num representing the states of the system. * @param states A Nat representing the states of the system. * @param stdDevs For a Q matrix, its elements are the standard deviations of each state from how * the model behaves. For an R matrix, its elements are the standard deviations for each * output measurement. * @return Process noise or measurement noise covariance matrix. */ public static Matrix makeCovarianceMatrix( Nat states, Matrix stdDevs) { var result = new Matrix<>(states, states); for (int i = 0; i < states.getNum(); i++) { result.set(i, i, Math.pow(stdDevs.get(i, 0), 2)); } return result; } /** * Creates a vector of normally distributed white noise with the given noise intensities for each * element. * * @param Num representing the dimensionality of the noise vector to create. * @param stdDevs A matrix whose elements are the standard deviations of each element of the noise * vector. * @return White noise vector. */ public static Matrix makeWhiteNoiseVector(Matrix stdDevs) { Matrix result = new Matrix<>(new SimpleMatrix(stdDevs.getNumRows(), 1)); for (int i = 0; i < stdDevs.getNumRows(); i++) { result.set(i, 0, rand.nextGaussian() * stdDevs.get(i, 0)); } return result; } /** * Creates a cost matrix from the given vector for use with LQR. * *

The cost matrix is constructed using Bryson's rule. The inverse square of each tolerance is * placed on the cost matrix diagonal. If a tolerance is infinity, its cost matrix entry is set to * zero. * * @param Nat representing the number of system states or inputs. * @param tolerances An array. For a Q matrix, its elements are the maximum allowed excursions of * the states from the reference. For an R matrix, its elements are the maximum allowed * excursions of the control inputs from no actuation. * @return State excursion or control effort cost matrix. */ public static Matrix makeCostMatrix( Matrix tolerances) { Matrix result = new Matrix<>(new SimpleMatrix(tolerances.getNumRows(), tolerances.getNumRows())); result.fill(0.0); for (int i = 0; i < tolerances.getNumRows(); i++) { if (tolerances.get(i, 0) == Double.POSITIVE_INFINITY) { result.set(i, i, 0.0); } else { result.set(i, i, 1.0 / Math.pow(tolerances.get(i, 0), 2)); } } return result; } /** * Returns true if (A, B) is a stabilizable pair. * *

(A, B) is stabilizable if and only if the uncontrollable eigenvalues of A, if any, have * absolute values less than one, where an eigenvalue is uncontrollable if rank([λI - A, B]) %3C n * where n is the number of states. * * @param Num representing the size of A. * @param Num representing the columns of B. * @param A System matrix. * @param B Input matrix. * @return If the system is stabilizable. */ public static boolean isStabilizable( Matrix A, Matrix B) { return StateSpaceUtilJNI.isStabilizable( A.getNumRows(), B.getNumCols(), A.getData(), B.getData()); } /** * Returns true if (A, C) is a detectable pair. * *

(A, C) is detectable if and only if the unobservable eigenvalues of A, if any, have absolute * values less than one, where an eigenvalue is unobservable if rank([λI - A; C]) %3C n where n is * the number of states. * * @param Num representing the size of A. * @param Num representing the rows of C. * @param A System matrix. * @param C Output matrix. * @return If the system is detectable. */ public static boolean isDetectable( Matrix A, Matrix C) { return StateSpaceUtilJNI.isStabilizable( A.getNumRows(), C.getNumRows(), A.transpose().getData(), C.transpose().getData()); } /** * Convert a {@link Pose2d} to a vector of [x, y, theta], where theta is in radians. * * @param pose A pose to convert to a vector. * @return The given pose in vector form, with the third element, theta, in radians. * @deprecated Create the vector manually instead. If you were using this as an intermediate step * for constructing affine transformations, use {@link Pose2d#toMatrix()} instead. */ @Deprecated(forRemoval = true, since = "2025") public static Matrix poseToVector(Pose2d pose) { return VecBuilder.fill(pose.getX(), pose.getY(), pose.getRotation().getRadians()); } /** * Clamp the input u to the min and max. * * @param u The input to clamp. * @param umin The minimum input magnitude. * @param umax The maximum input magnitude. * @param Number of inputs. * @return The clamped input. */ public static Matrix clampInputMaxMagnitude( Matrix u, Matrix umin, Matrix umax) { var result = new Matrix(new SimpleMatrix(u.getNumRows(), 1)); for (int i = 0; i < u.getNumRows(); i++) { result.set(i, 0, Math.clamp(u.get(i, 0), umin.get(i, 0), umax.get(i, 0))); } return result; } /** * Renormalize all inputs if any exceeds the maximum magnitude. Useful for systems such as * differential drivetrains. * * @param u The input vector. * @param maxMagnitude The maximum magnitude any input can have. * @param Number of inputs. * @return The normalizedInput */ public static Matrix desaturateInputVector( Matrix u, double maxMagnitude) { double maxValue = u.maxAbs(); boolean isCapped = maxValue > maxMagnitude; if (isCapped) { return u.times(maxMagnitude / maxValue); } return u; } /** * Convert a {@link Pose2d} to a vector of [x, y, cos(theta), sin(theta)], where theta is in * radians. * * @param pose A pose to convert to a vector. * @return The given pose in as a 4x1 vector of x, y, cos(theta), and sin(theta). * @deprecated Create the vector manually instead. If you were using this as an intermediate step * for constructing affine transformations, use {@link Pose2d#toMatrix()} instead. */ @Deprecated(forRemoval = true, since = "2025") public static Matrix poseTo4dVector(Pose2d pose) { return VecBuilder.fill( pose.getTranslation().getX(), pose.getTranslation().getY(), pose.getRotation().getCos(), pose.getRotation().getSin()); } /** * Convert a {@link Pose2d} to a vector of [x, y, theta], where theta is in radians. * * @param pose A pose to convert to a vector. * @return The given pose in vector form, with the third element, theta, in radians. * @deprecated Create the vector manually instead. If you were using this as an intermediate step * for constructing affine transformations, use {@link Pose2d#toMatrix()} instead. */ @Deprecated(forRemoval = true, since = "2025") public static Matrix poseTo3dVector(Pose2d pose) { return VecBuilder.fill( pose.getTranslation().getX(), pose.getTranslation().getY(), pose.getRotation().getRadians()); } }