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