Example #1
0
def test_estimator_iosys():
    sys = ct.drss(4, 2, 2, strictly_proper=True)

    Q, R = np.eye(sys.nstates), np.eye(sys.ninputs)
    K, _, _ = ct.dlqr(sys, Q, R)

    P0 = np.eye(sys.nstates)
    QN = np.eye(sys.ninputs)
    RN = np.eye(sys.noutputs)
    estim = ct.create_estimator_iosystem(sys, QN, RN, P0)

    ctrl, clsys = ct.create_statefbk_iosystem(sys, K, estimator=estim)

    # Extract the elements of the estimator
    est = estim.linearize(0, 0)
    Be1 = est.B[:sys.nstates, :sys.noutputs]
    Be2 = est.B[:sys.nstates, sys.noutputs:]
    A_clchk = np.block(
        [[sys.A, -sys.B @ K],
         [Be1 @ sys.C, est.A[:sys.nstates, :sys.nstates] - Be2 @ K]])
    B_clchk = np.block([[sys.B @ K, sys.B], [Be2 @ K, Be2]])
    C_clchk = np.block([[sys.C, np.zeros((sys.noutputs, sys.nstates))],
                        [np.zeros_like(K), -K]])
    D_clchk = np.block([[np.zeros((sys.noutputs, sys.nstates + sys.ninputs))],
                        [K, np.eye(sys.ninputs)]])

    # Check to make sure everything matches
    cls = clsys.linearize(0, 0)
    nstates = sys.nstates
    np.testing.assert_almost_equal(cls.A[:2 * nstates, :2 * nstates], A_clchk)
    np.testing.assert_almost_equal(cls.B[:2 * nstates, :], B_clchk)
    np.testing.assert_almost_equal(cls.C[:, :2 * nstates], C_clchk)
    np.testing.assert_almost_equal(cls.D, D_clchk)
    def test_lqr_integral_discrete(self):
        # Generate a discrete time system for testing
        sys = ct.drss(4, 4, 2, strictly_proper=True)
        sys.C = np.eye(4)  # reset output to be full state
        C_int = np.eye(2, 4)  # integrate outputs for first two states
        nintegrators = C_int.shape[0]

        # Generate a controller with integral action
        K, _, _ = ct.lqr(sys,
                         np.eye(sys.nstates + nintegrators),
                         np.eye(sys.ninputs),
                         integral_action=C_int)
        Kp, Ki = K[:, :sys.nstates], K[:, sys.nstates:]

        # Create an I/O system for the controller
        ctrl, clsys = ct.create_statefbk_iosystem(sys,
                                                  K,
                                                  integral_action=C_int)

        # Construct the state space matrices by hand
        A_ctrl = np.eye(nintegrators)
        B_ctrl = np.block(
            [[-C_int, np.zeros((nintegrators, sys.ninputs)), C_int]])
        C_ctrl = -K[:, sys.nstates:]
        D_ctrl = np.block([[Kp, np.eye(nintegrators), -Kp]])

        # Check to make sure everything matches
        assert ct.isdtime(clsys)
        np.testing.assert_array_almost_equal(ctrl.A, A_ctrl)
        np.testing.assert_array_almost_equal(ctrl.B, B_ctrl)
        np.testing.assert_array_almost_equal(ctrl.C, C_ctrl)
        np.testing.assert_array_almost_equal(ctrl.D, D_ctrl)
    def test_statefbk_errors(self):
        sys = ct.rss(4, 4, 2, strictly_proper=True)
        K, _, _ = ct.lqr(sys, np.eye(sys.nstates), np.eye(sys.ninputs))

        with pytest.raises(ControlArgument, match="must be I/O system"):
            sys_tf = ct.tf([1], [1, 1])
            ctrl, clsys = ct.create_statefbk_iosystem(sys_tf, K)

        with pytest.raises(ControlArgument, match="output size must match"):
            est = ct.rss(3, 3, 2)
            ctrl, clsys = ct.create_statefbk_iosystem(sys, K, estimator=est)

        with pytest.raises(ControlArgument, match="must be the full state"):
            sys_nf = ct.rss(4, 3, 2, strictly_proper=True)
            ctrl, clsys = ct.create_statefbk_iosystem(sys_nf, K)

        with pytest.raises(ControlArgument, match="gain must be an array"):
            ctrl, clsys = ct.create_statefbk_iosystem(sys, "bad argument")

        with pytest.raises(ControlArgument, match="unknown type"):
            ctrl, clsys = ct.create_statefbk_iosystem(sys, K, type=1)

        # Errors involving integral action
        C_int = np.eye(2, 4)
        K_int, _, _ = ct.lqr(sys,
                             np.eye(sys.nstates + C_int.shape[0]),
                             np.eye(sys.ninputs),
                             integral_action=C_int)

        with pytest.raises(ControlArgument, match="must pass an array"):
            ctrl, clsys = ct.create_statefbk_iosystem(
                sys, K_int, integral_action="bad argument")

        with pytest.raises(ControlArgument, match="must be an array of size"):
            ctrl, clsys = ct.create_statefbk_iosystem(sys,
                                                      K,
                                                      integral_action=C_int)
    def test_lqr_integral_continuous(self):
        # Generate a continuous time system for testing
        sys = ct.rss(4, 4, 2, strictly_proper=True)
        sys.C = np.eye(4)  # reset output to be full state
        C_int = np.eye(2, 4)  # integrate outputs for first two states
        nintegrators = C_int.shape[0]

        # Generate a controller with integral action
        K, _, _ = ct.lqr(sys,
                         np.eye(sys.nstates + nintegrators),
                         np.eye(sys.ninputs),
                         integral_action=C_int)
        Kp, Ki = K[:, :sys.nstates], K[:, sys.nstates:]

        # Create an I/O system for the controller
        ctrl, clsys = ct.create_statefbk_iosystem(sys,
                                                  K,
                                                  integral_action=C_int)

        # Construct the state space matrices for the controller
        # Controller inputs = xd, ud, x
        # Controller state = z (integral of x-xd)
        # Controller output = ud - Kp(x - xd) - Ki z
        A_ctrl = np.zeros((nintegrators, nintegrators))
        B_ctrl = np.block(
            [[-C_int, np.zeros((nintegrators, sys.ninputs)), C_int]])
        C_ctrl = -K[:, sys.nstates:]
        D_ctrl = np.block([[Kp, np.eye(nintegrators), -Kp]])

        # Check to make sure everything matches
        np.testing.assert_array_almost_equal(ctrl.A, A_ctrl)
        np.testing.assert_array_almost_equal(ctrl.B, B_ctrl)
        np.testing.assert_array_almost_equal(ctrl.C, C_ctrl)
        np.testing.assert_array_almost_equal(ctrl.D, D_ctrl)

        # Construct the state space matrices for the closed loop system
        A_clsys = np.block([[sys.A - sys.B @ Kp, -sys.B @ Ki],
                            [C_int,
                             np.zeros((nintegrators, nintegrators))]])
        B_clsys = np.block([[sys.B @ Kp, sys.B],
                            [-C_int,
                             np.zeros((nintegrators, sys.ninputs))]])
        C_clsys = np.block(
            [[np.eye(sys.nstates),
              np.zeros((sys.nstates, nintegrators))], [-Kp, -Ki]])
        D_clsys = np.block(
            [[np.zeros((sys.nstates, sys.nstates + sys.ninputs))],
             [Kp, np.eye(sys.ninputs)]])

        # Check to make sure closed loop matches
        np.testing.assert_array_almost_equal(clsys.A, A_clsys)
        np.testing.assert_array_almost_equal(clsys.B, B_clsys)
        np.testing.assert_array_almost_equal(clsys.C, C_clsys)
        np.testing.assert_array_almost_equal(clsys.D, D_clsys)

        # Check the poles of the closed loop system
        assert all(np.real(clsys.pole()) < 0)

        # Make sure controller infinite zero frequency gain
        if slycot_check():
            ctrl_tf = tf(ctrl)
            assert abs(ctrl_tf(1e-9)[0][0]) > 1e6
            assert abs(ctrl_tf(1e-9)[1][1]) > 1e6
    def test_lqr_iosys(self, nstates, ninputs, noutputs, nintegrators, type):
        # Create the system to be controlled (and estimator)
        # TODO: make sure it is controllable?
        if noutputs == 0:
            # Create a system with full state output
            sys = ct.rss(nstates, nstates, ninputs, strictly_proper=True)
            sys.C = np.eye(nstates)
            est = None

        else:
            # Create a system with of the desired size
            sys = ct.rss(nstates, noutputs, ninputs, strictly_proper=True)

            # Create an estimator with different signal names
            L, _, _ = ct.lqe(sys.A, sys.B, sys.C, np.eye(ninputs),
                             np.eye(noutputs))
            est = ss(sys.A - L @ sys.C,
                     np.hstack([L, sys.B]),
                     np.eye(nstates),
                     0,
                     inputs=sys.output_labels + sys.input_labels,
                     outputs=[f'xhat[{i}]' for i in range(nstates)])

        # Decide whether to include integral action
        if nintegrators:
            # Choose the first 'n' outputs as integral terms
            C_int = np.eye(nintegrators, nstates)

            # Set up an augmented system for LQR computation
            # TODO: move this computation into LQR
            A_aug = np.block([[sys.A,
                               np.zeros((sys.nstates, nintegrators))],
                              [C_int,
                               np.zeros((nintegrators, nintegrators))]])
            B_aug = np.vstack([sys.B, np.zeros((nintegrators, ninputs))])
            C_aug = np.hstack(
                [sys.C, np.zeros((sys.C.shape[0], nintegrators))])
            aug = ss(A_aug, B_aug, C_aug, 0)
        else:
            C_int = np.zeros((0, nstates))
            aug = sys

        # Design an LQR controller
        K, _, _ = ct.lqr(aug, np.eye(nstates + nintegrators), np.eye(ninputs))
        Kp, Ki = K[:, :nstates], K[:, nstates:]

        # Create an I/O system for the controller
        ctrl, clsys = ct.create_statefbk_iosystem(sys,
                                                  K,
                                                  integral_action=C_int,
                                                  estimator=est,
                                                  type=type)

        # If we used a nonlinear controller, linearize it for testing
        if type == 'nonlinear':
            clsys = clsys.linearize(0, 0)

        # Make sure the linear system elements are correct
        if noutputs == 0:
            # No estimator
            Ac = np.block([[sys.A - sys.B @ Kp, -sys.B @ Ki],
                           [C_int,
                            np.zeros((nintegrators, nintegrators))]])
            Bc = np.block([[sys.B @ Kp, sys.B],
                           [-C_int, np.zeros((nintegrators, ninputs))]])
            Cc = np.block(
                [[np.eye(nstates),
                  np.zeros((nstates, nintegrators))], [-Kp, -Ki]])
            Dc = np.block([[np.zeros((nstates, nstates + ninputs))],
                           [Kp, np.eye(ninputs)]])
        else:
            # Estimator
            Be1, Be2 = est.B[:, :noutputs], est.B[:, noutputs:]
            Ac = np.block(
                [[sys.A, -sys.B @ Ki, -sys.B @ Kp],
                 [np.zeros((nintegrators, nstates + nintegrators)), C_int],
                 [Be1 @ sys.C, -Be2 @ Ki, est.A - Be2 @ Kp]])
            Bc = np.block([[sys.B @ Kp, sys.B],
                           [-C_int, np.zeros((nintegrators, ninputs))],
                           [Be2 @ Kp, Be2]])
            Cc = np.block(
                [[sys.C, np.zeros((noutputs, nintegrators + nstates))],
                 [np.zeros_like(Kp), -Ki, -Kp]])
            Dc = np.block([[np.zeros((noutputs, nstates + ninputs))],
                           [Kp, np.eye(ninputs)]])

        # Check to make sure everything matches
        np.testing.assert_array_almost_equal(clsys.A, Ac)
        np.testing.assert_array_almost_equal(clsys.B, Bc)
        np.testing.assert_array_almost_equal(clsys.C, Cc)
        np.testing.assert_array_almost_equal(clsys.D, Dc)