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.system;
006
007import edu.wpi.first.math.Matrix;
008import edu.wpi.first.math.Num;
009import edu.wpi.first.math.Pair;
010import org.ejml.simple.SimpleMatrix;
011
012@SuppressWarnings({"ParameterName", "MethodTypeParameterName"})
013public final class Discretization {
014  private Discretization() {
015    // Utility class
016  }
017
018  /**
019   * Discretizes the given continuous A matrix.
020   *
021   * @param <States> Num representing the number of states.
022   * @param contA Continuous system matrix.
023   * @param dtSeconds Discretization timestep.
024   * @return the discrete matrix system.
025   */
026  public static <States extends Num> Matrix<States, States> discretizeA(
027      Matrix<States, States> contA, double dtSeconds) {
028    return contA.times(dtSeconds).exp();
029  }
030
031  /**
032   * Discretizes the given continuous A and B matrices.
033   *
034   * @param <States> Nat representing the states of the system.
035   * @param <Inputs> Nat representing the inputs to the system.
036   * @param contA Continuous system matrix.
037   * @param contB Continuous input matrix.
038   * @param dtSeconds Discretization timestep.
039   * @return a Pair representing discA and diskB.
040   */
041  @SuppressWarnings("LocalVariableName")
042  public static <States extends Num, Inputs extends Num>
043      Pair<Matrix<States, States>, Matrix<States, Inputs>> discretizeAB(
044          Matrix<States, States> contA, Matrix<States, Inputs> contB, double dtSeconds) {
045    var scaledA = contA.times(dtSeconds);
046    var scaledB = contB.times(dtSeconds);
047
048    int states = contA.getNumRows();
049    int inputs = contB.getNumCols();
050    var M = new Matrix<>(new SimpleMatrix(states + inputs, states + inputs));
051    M.assignBlock(0, 0, scaledA);
052    M.assignBlock(0, scaledA.getNumCols(), scaledB);
053    var phi = M.exp();
054
055    var discA = new Matrix<States, States>(new SimpleMatrix(states, states));
056    var discB = new Matrix<States, Inputs>(new SimpleMatrix(states, inputs));
057
058    discA.extractFrom(0, 0, phi);
059    discB.extractFrom(0, contB.getNumRows(), phi);
060
061    return new Pair<>(discA, discB);
062  }
063
064  /**
065   * Discretizes the given continuous A and Q matrices.
066   *
067   * @param <States> Nat representing the number of states.
068   * @param contA Continuous system matrix.
069   * @param contQ Continuous process noise covariance matrix.
070   * @param dtSeconds Discretization timestep.
071   * @return a pair representing the discrete system matrix and process noise covariance matrix.
072   */
073  @SuppressWarnings("LocalVariableName")
074  public static <States extends Num>
075      Pair<Matrix<States, States>, Matrix<States, States>> discretizeAQ(
076          Matrix<States, States> contA, Matrix<States, States> contQ, double dtSeconds) {
077    int states = contA.getNumRows();
078
079    // Make continuous Q symmetric if it isn't already
080    Matrix<States, States> Q = contQ.plus(contQ.transpose()).div(2.0);
081
082    // Set up the matrix M = [[-A, Q], [0, A.T]]
083    final var M = new Matrix<>(new SimpleMatrix(2 * states, 2 * states));
084    M.assignBlock(0, 0, contA.times(-1.0));
085    M.assignBlock(0, states, Q);
086    M.assignBlock(states, 0, new Matrix<>(new SimpleMatrix(states, states)));
087    M.assignBlock(states, states, contA.transpose());
088
089    final var phi = M.times(dtSeconds).exp();
090
091    // Phi12 = phi[0:States,        States:2*States]
092    // Phi22 = phi[States:2*States, States:2*States]
093    final Matrix<States, States> phi12 = phi.block(states, states, 0, states);
094    final Matrix<States, States> phi22 = phi.block(states, states, states, states);
095
096    final var discA = phi22.transpose();
097
098    Q = discA.times(phi12);
099
100    // Make discrete Q symmetric if it isn't already
101    final var discQ = Q.plus(Q.transpose()).div(2.0);
102
103    return new Pair<>(discA, discQ);
104  }
105
106  /**
107   * Discretizes the given continuous A and Q matrices.
108   *
109   * <p>Rather than solving a 2N x 2N matrix exponential like in DiscretizeQ() (which is expensive),
110   * we take advantage of the structure of the block matrix of A and Q.
111   *
112   * <p>The exponential of A*t, which is only N x N, is relatively cheap. 2) The upper-right quarter
113   * of the 2N x 2N matrix, which we can approximate using a taylor series to several terms and
114   * still be substantially cheaper than taking the big exponential.
115   *
116   * @param <States> Nat representing the number of states.
117   * @param contA Continuous system matrix.
118   * @param contQ Continuous process noise covariance matrix.
119   * @param dtSeconds Discretization timestep.
120   * @return a pair representing the discrete system matrix and process noise covariance matrix.
121   */
122  @SuppressWarnings("LocalVariableName")
123  public static <States extends Num>
124      Pair<Matrix<States, States>, Matrix<States, States>> discretizeAQTaylor(
125          Matrix<States, States> contA, Matrix<States, States> contQ, double dtSeconds) {
126    // Make continuous Q symmetric if it isn't already
127    Matrix<States, States> Q = contQ.plus(contQ.transpose()).div(2.0);
128
129    Matrix<States, States> lastTerm = Q.copy();
130    double lastCoeff = dtSeconds;
131
132    // Aᵀⁿ
133    Matrix<States, States> Atn = contA.transpose();
134    Matrix<States, States> phi12 = lastTerm.times(lastCoeff);
135
136    // i = 6 i.e. 5th order should be enough precision
137    for (int i = 2; i < 6; ++i) {
138      lastTerm = contA.times(-1).times(lastTerm).plus(Q.times(Atn));
139      lastCoeff *= dtSeconds / ((double) i);
140
141      phi12 = phi12.plus(lastTerm.times(lastCoeff));
142
143      Atn = Atn.times(contA.transpose());
144    }
145
146    var discA = discretizeA(contA, dtSeconds);
147    Q = discA.times(phi12);
148
149    // Make Q symmetric if it isn't already
150    var discQ = Q.plus(Q.transpose()).div(2.0);
151
152    return new Pair<>(discA, discQ);
153  }
154
155  /**
156   * Returns a discretized version of the provided continuous measurement noise covariance matrix.
157   * Note that dt=0.0 divides R by zero.
158   *
159   * @param <O> Nat representing the number of outputs.
160   * @param R Continuous measurement noise covariance matrix.
161   * @param dtSeconds Discretization timestep.
162   * @return Discretized version of the provided continuous measurement noise covariance matrix.
163   */
164  public static <O extends Num> Matrix<O, O> discretizeR(Matrix<O, O> R, double dtSeconds) {
165    return R.div(dtSeconds);
166  }
167}