Beispiel #1
0
    def test_linear_program(self):

        # loop over solvers
        #for solver in ['pnnls', 'gurobi']:
        for solver in ['pnnls']:
            # trivial LP with only inequalities
            A = -np.eye(2)
            b = np.zeros((2, 1))
            f = np.ones((2, 1))
            sol = linear_program(f, A, b, solver=solver)
            self.assertAlmostEqual(sol['min'], 0.)
            np.testing.assert_array_almost_equal(sol['argmin'], np.zeros(
                (2, 1)))
            self.assertEqual(sol['active_set'], [0, 1])
            np.testing.assert_array_almost_equal(sol['multiplier_inequality'],
                                                 np.ones((2, 1)))
            self.assertTrue(sol['multiplier_equality'] is None)

            # add equality
            C = np.array([[2., 1.]])
            d = np.array([[2.]])
            sol = linear_program(f, A, b, C, d, solver=solver)
            self.assertAlmostEqual(sol['min'], 1.)
            np.testing.assert_array_almost_equal(sol['argmin'],
                                                 np.array([[1.], [0.]]))
            self.assertEqual(sol['active_set'], [1])
            np.testing.assert_array_almost_equal(sol['multiplier_inequality'],
                                                 np.array([[0.], [.5]]))
            np.testing.assert_array_almost_equal(sol['multiplier_equality'],
                                                 np.array([[-.5]]))
Beispiel #2
0
    def test_linear_program(self, solver='pnnls'):

        # trivial LP with only inequalities
        A = -np.eye(2)
        b = np.zeros(2)
        f = np.ones(2)
        sol = linear_program(f, A, b, solver=solver)
        self.assertAlmostEqual(
            sol['min'],
            0.
            )
        np.testing.assert_array_almost_equal(
            sol['argmin'],
            np.zeros(2)
            )
        self.assertEqual(
            sol['active_set'],
            [0,1]
            )
        np.testing.assert_array_almost_equal(
            sol['multiplier_inequality'],
            np.ones(2)
            )
        self.assertTrue(
            sol['multiplier_equality'] is None
            )

        # add equality
        C = np.array([[2., 1.]])
        d = np.array([2.])
        sol = linear_program(f, A, b, C, d, solver=solver)
        self.assertAlmostEqual(
            sol['min'],
            1.
            )
        np.testing.assert_array_almost_equal(
            sol['argmin'],
            np.array([1.,0.])
            )
        self.assertEqual(
            sol['active_set'],
            [1]
            )
        np.testing.assert_array_almost_equal(
            sol['multiplier_inequality'],
            np.array([0.,.5])
            )
        np.testing.assert_array_almost_equal(
            sol['multiplier_equality'],
            np.array([-.5])
            )
Beispiel #3
0
def _expand_simplex(A, b, hull, tol=1.e-7):
    """
    Expands the internal simplex to cover all the projection.

    Arguments
    ----------
    A : numpy.ndarray
        Left-hand side of the inequalities describing the higher dimensional polytope.
    b : numpy.ndarray
        Right-hand side of the inequalities describing the higher dimensional polytope.
    hull : instance of ConvexHull
        Convex hull of vertices of the input simplex.
    tol : float
        Maximal expansion of a facet to consider it a facet of the projection.

    Returns
    ----------
    hull : instance of ConvexHull
        Convex hull of vertices of the projection.
    """

    # initialize algorithm's variables
    n = hull.points[0].shape[0]
    a_explored = []

    # start convex-hull method
    convergence = False
    while not convergence:
        convergence = True

        # check if every facet of the inner approximation belongs to the projection
        for i in range(hull.equations.shape[0]):

            # get normalized halfplane {x | a' x <= d} of the ith facet
            a = hull.equations[i:i+1, :-1].T
            d = - hull.equations[i, -1]
            a_norm = np.linalg.norm(a)
            a /= a_norm
            b /= a_norm

            # check it the direction a has been explored so far
            is_explored = any((np.allclose(a, a2) for a2 in a_explored))
            if not is_explored:
                a_explored.append(a)

                # maximize in the direction a
                f = np.vstack((
                    - a,
                    np.zeros((A.shape[1]-n, 1))
                    ))
                sol = linear_program(f, A, b)

                # check if expansion wrt to the halfplane is greater than zero
                expansion = - sol['min'] - d # >= 0
                if expansion > tol:
                    convergence = False
                    hull.add_points(sol['argmin'][:n,:].T)
                    break

    return hull
Beispiel #4
0
def _get_two_vertices(A, b, n):
    """
    Findes two vertices of the projection.

    Arguments
    ----------
    A : numpy.ndarray
        Left-hand side of the inequalities describing the higher dimensional polytope.
    b : numpy.ndarray
        Right-hand side of the inequalities describing the higher dimensional polytope.
    n : int
        Dimensionality of the space onto which the polytope has to be projected.

    Returns
    ----------
    vertices : list of numpy.ndarray
        List of two vertices of the projection.
    """

    # select any direction to explore (it has to belong to the projected space, i.e. a_i = 0 for all i > n)
    a = np.vstack((
        np.ones((1,1)),
        np.zeros((A.shape[1]-1, 1))
        ))

    # minimize and maximize in the given direction
    vertices = []
    for f in [a, -a]:
        sol = linear_program(f, A, b)
        vertices.append(sol['argmin'][:n,:])

    return vertices
Beispiel #5
0
def _get_inner_simplex(A, b, vertices, tol=1.e-7):
    """
    Constructs a simplex contained in the porjection.

    Arguments
    ----------
    A : numpy.ndarray
        Left-hand side of the inequalities describing the higher dimensional polytope.
    b : numpy.ndarray
        Right-hand side of the inequalities describing the higher dimensional polytope.
    vertices : list of numpy.ndarray
        List of two vertices of the projection.
    tol : float
        Maximal expansion of a facet to consider it a facet of the projection.

    Returns
    ----------
    vertices : list of numpy.ndarray
        List of vertices of the simplex contained in the projection.
    """

    # initialize LPs
    n = vertices[0].size

    # expand increasing at every iteration the dimension of the space
    for i in range(2, n + 1):
        a, d = plane_through_points([v[:i] for v in vertices])
        f = np.concatenate((a, np.zeros(A.shape[1] - i)))
        sol = linear_program(f, A, b)

        # check the length of the expansion wrt to the plane, if zero expand in the opposite direction
        expansion = np.abs(a.dot(sol['argmin'][:i]) - d)  # >= 0
        if expansion < tol:
            sol = linear_program(-f, A, b)
        vertices.append(sol['argmin'][:n])

    return vertices
Beispiel #6
0
    def bounded(self):
        """
        Checks if the polyhedron is bounded (returns True or False).

        Math
        ----------
        Consider the non-empty polyhedron P := {x | A x <= b}.
        We have that necessary and sufficient condition for P to be unbounded is the existence of a nonzero x | A x <= 0.
        (Proof: Given x_1 that verifies the latter condition and x_2 in P, consider x_3 := a x_1 + x_2, with a in R. We have x_3 in P for all a >= 0, in fact A x_3 = a A x_1 + A x_2 <= b. Considering a -> inf the unboundedness of P follows.)
        It follows that sufficient condition for P to be unbounded is that ker(A) is not empty; hence in the following we consider only the case ker(A) = 0.
        Stiemke's Theorem of alternatives (see, e.g., Mangasarian, Nonlinear Programming, pag. 32) states that either there exists an x | A x <= 0, A x != 0, or there exists a y > 0 | A' y = 0.
        Note that: i) being ker(A) = 0, the condition A x != 0 is equivalent to x != 0; ii) in this case, y > 0 is equilvaent e.g. to y >= 1.
        In conclusion we have that: under the assumptions non-empty P and ker(A) = 0, necessary and sufficient conditions for the boundedness of P is the existence of y >= 1 | A' y = 0.
        Here we search for the y with minimum norm 1 that satisfies the latter condition (note that y >= 1 implies ||y||_1 = 1' y).

        Returns
        ----------
        bounded : bool
            True if the polyhedron is bounded (if the polyhedron is empty also True), False otherwise.
        """

        # check if it has been already checked
        if self._bounded is not None:
            return self._bounded

        # check emptyness
        if self.empty:
            return True

        # include equalities
        A = np.vstack((self.A, self.C, -self.C))

        # check kernel of A
        if nullspace_basis(A).shape[1] > 0:
            return False

        # check Stiemke's theorem of alternatives
        n, m = A.shape
        sol = linear_program(
            np.ones((n, 1)), # f
            -np.eye(n),      # A
            -np.ones((n,1)), # b
            A.T,             # C
            np.zeros((m, 1)) # d
            )
        self._bounded = sol['min'] is not None

        return self._bounded
Beispiel #7
0
    def minimal_facets(self, tol=1.e-7):
        """
        Computes the indices of the facets that generate a minimal representation of the polyhedron solving an LP for each facet of the redundant representation.
        (See "Fukuda - Frequently asked questions in polyhedral computation" Sec.2.21.)
        In case of equalities, first the problem is projected in the nullspace of the equalities.

        Arguments
        ----------
        tol : float
            Minimum distance of a redundant facet from the interior of the polyhedron to be considered as such.

        Returns
        ----------
        minimal_facets : list of int
            List of indices of the non-redundant inequalities A x <= b (None if the polyhedron in empty).
        """

        # check emptyness
        if self.empty:
            return None

        # if there are equalities, project
        if self.C.shape[0] != 0:
            E, f, _, _ = self._remove_equalities()
        else:
            E = self.A
            f = self.b

        # initialize list of non-redundant facets
        minimal_facets = list(range(E.shape[0]))

        # check each facet
        for i in range(E.shape[0]):

            # remove redundant facets and relax ith inequality
            E_minimal = E[minimal_facets,:]
            f_relaxation = np.zeros(np.shape(f))
            f_relaxation[i] += 1.
            f_relaxed = (f + f_relaxation)[minimal_facets];

            # solve linear program
            sol = linear_program(-E[i,:].T, E_minimal, f_relaxed)

            # remove redundant facets from the list
            if  - sol['min'] - f[i] < tol:
                minimal_facets.remove(i)

        return minimal_facets
Beispiel #8
0
    def _get_bigM_domains(self):
        """
        Computes all the bigMs for the domains of the PWA system.
        Each one of the s domains of the PWA system has the form
        D_i = {(x,u) | F_i x + G_i u <= h_i}.
        The bigM reformulation (for t = 0, ..., N-1) of this constraint is
        F_i x(t) + G_i u(t) <= h_i + sum_{j=1, j!=i}^s gamma_ij delta_j(t). (5)
        Here gamma_ij (>> 0) is a vector of bigMs and delta_j(t) is a binary variable (equal to 1 if the system is in mode j, zero otherwise).
        If the system is in mode k at time t (i.e. delta_k(t) = 1), we have that
        F_i x(t) + G_i u(t) <= h_i + gamma_ik, for all i != k,
        F_k x(t) + G_k u(t) <= h_k,
        hence we force (x(t),u(t)) to belong to D_k, whereas the other constraints are redundant because gamma_ik >> 0.
        It is very important to choose the bigMs as tight as possible, for this reason we set
        gamma_ij := max_{(x,u) in D_j} F_i x + G_i u - h_i.
        (Note that the previous are a number of LPs equal to the number of rows of F_i.)
        The previous ensures that when the system is mode j != i, gamma_ij is always bigger than the left-hand side (i.e. F_i x + G_i u - h_i).

        Returns
        ----------
        gamma : list of lists of numpy.ndarray
            gamma[i][j] is the vector gamma_ij defined above.
        """

        # initialize list of bigMs
        gamma = []

        # outer loop over the number of affine systems
        for i, D_i in enumerate(self.S.domains):
            gamma_i = []

            # inner loop over the number of affine systems
            for j, D_j in enumerate(self.S.domains):
                gamma_ij = []

                # solve one LP for each inequality of the ith domain
                for k in range(D_i.A.shape[0]):
                    f = -D_i.A[k:k + 1, :].T
                    sol = linear_program(f, D_j.A, D_j.b, D_j.C, D_j.d)
                    gamma_ij.append(-sol['min'] - D_i.b[k, 0])

                # close inner loop appending bigMs
                gamma_i.append(np.vstack(gamma_ij))

            # close outer loop appending bigMs
            gamma.append(gamma_i)

        return gamma
Beispiel #9
0
    def _chebyshev(self):
        """
        Returns the Chebyshev radius and center of the polyhedron P := {x | A x <= b, C x = d} solving the LP: min_{z, e}  e s.t. F z <= g + F_{row_norm} e.
        If no equalities are provided, F = A, z = x, g = b.
        In case of equality constraints, F = A N, g = b - A R r, with: N basis of the nullspace of C, R orthogonal complement to N, r = (C R)^-1 d and x is retrived as x = N n + R r.
        (For the details of this operation see the method _remove_equalities().)
        Here F_{row_norm} dentes the vector whose ith entry is the 2-norm of the ith row of F.

        Returns
        ----------
        radius : float
            Chebyshev radius of the polytope (negative if the polyhedron is empty, None if it is unbounded).
        center : numpy.ndarray
            Chebyshev center of the polytope (None if the polyhedron is unbounded).
        """

        # project in case of equalities
        if self.C.shape[0] > 0:
            A, b, N, R = self._remove_equalities()
        else:
            A = self.A
            b = self.b

        # assemble linear program
        f_lp = np.vstack((
            np.zeros((A.shape[1], 1)),
            np.ones((1, 1))
            ))
        A_row_norm = np.reshape(np.linalg.norm(A, axis=1), (A.shape[0], 1))
        A_lp = np.hstack((A, -A_row_norm))

        # solve and reshape result
        sol = linear_program(f_lp, A_lp, b)
        radius = sol['min']
        center = sol['argmin']
        if radius is not None:
            radius = -radius
            center = center[:-1]

        # go back to the original coordinates in case of equalities
        if self.C.shape[0] > 0:
            r = np.linalg.inv(self.C.dot(R)).dot(self.d)
            center = np.hstack((N, R)).dot(np.vstack((center, r)))

        return radius, center
Beispiel #10
0
    def is_included_in_with_ce(self, P2, tol=1.e-7):
        A1 = np.vstack((self.A, self.C, -self.C))
        b1 = np.vstack((self.b, self.d, -self.d))
        P1 = Polyhedron(A1, b1)
        A2 = np.vstack((P2.A, P2.C, -P2.C))
        b2 = np.vstack((P2.b, P2.d, -P2.d))

        # check inclusion, one facet per time
        included = True

        for i in range(A2.shape[0]):
            f = -A2[i:i+1,:].T
            sol = linear_program(f, P1.A, P1.b)
            penetration = - sol['min'] - b2[i]
            if penetration > tol:
                return sol['argmin']
                break

        return None
Beispiel #11
0
def big_m(P_list, tol=1.e-6):
    '''
    For the list of Polyhedron P_list in the from Pi = {x | Ai x <= bi} returns a list of lists of numpy arrays, where m[i][j] := max_{x in Pj} Ai x - bi.

    '''
    m = []
    for i, Pi in enumerate(P_list):
        mi = []
        for j, Pj in enumerate(P_list):
            mij = []
            for k in range(Pi.A.shape[0]):
                sol = linear_program(-Pi.A[k], Pj.A, Pj.b)
                mijk = - sol['min'] - Pi.b[k]
                if np.abs(mijk) < tol:
                    mijk = 0.
                mij.append(mijk)
            mi.append(np.array(mij))
        m.append(mi)
    return m
Beispiel #12
0
    def is_included_in(self, P2, tol=1.e-7):
        """
        Checks if the polyhedron P is a subset of the polyhedron P2 (returns True or False).
        For each halfspace H descibed a facet of P2, it solves an LP to check if the intersection of H with P1 is euqual to P1.
        If this is the case for all H, then P1 is in P2.

        Arguments
        ----------
        P2 : instance of Polyhedron
            Polyhedron within which we want to check if this polyhedron is contained.
        tol : float
            Maximum distance of a point from P2 to be considered an internal point.

        Returns
        ----------
        included : bool
            True if this polyhedron is contained in P2, False otherwise.
        """

        # augment inequalities with equalities
        A1 = np.vstack((self.A, self.C, -self.C))
        b1 = np.vstack((self.b, self.d, -self.d))
        P1 = Polyhedron(A1, b1)
        A2 = np.vstack((P2.A, P2.C, -P2.C))
        b2 = np.vstack((P2.b, P2.d, -P2.d))

        # check inclusion, one facet per time
        included = True
        for i in range(A2.shape[0]):
            f = -A2[i:i+1,:].T
            sol = linear_program(f, P1.A, P1.b)
            penetration = - sol['min'] - b2[i]
            if penetration > tol:
                included = False
                break

        return included
Beispiel #13
0
def mcais(A, X, verbose=False):
    """
    Returns the maximal constraint-admissible (positive) invariant set O_inf for the system x(t+1) = A x(t) subject to the constraint x in X.
    O_inf is also known as maximum output admissible set.
    It holds that x(0) in O_inf <=> x(t) in X for all t >= 0.
    (Implementation of Algorithm 3.2 from: Gilbert, Tan - Linear Systems with State and Control Constraints, The Theory and Application of Maximal Output Admissible Sets.)
    Sufficient conditions for this set to be finitely determined (i.e. defined by a finite number of facets) are: A stable, X bounded and containing the origin.

    Math
    ----------
    At each time step t, we want to verify if at the next time step t+1 the system will go outside X.
    Let's consider X := {x | D_i x <= e_i, i = 1,...,n} and t = 0.
    In order to ensure that x(1) = A x(0) is inside X, we need to consider one by one all the constraints and for each of them, the worst-case x(0).
    We can do this solvin an LP
    V(t=0, i) = max_{x in X} D_i A x - e_i for i = 1,...,n
    if all these LPs has V < 0 there is no x(0) such that x(1) is outside X.
    The previous implies that all the time-evolution x(t) will lie in X (see Gilbert and Tan).
    In case one of the LPs gives a V > 0, we iterate and consider
    V(t=1, i) = max_{x in X, x in A X} D_i A^2 x - e_i for i = 1,...,n
    where A X := {x | D A x <= e}.
    If now all V < 0, then O_inf = X U AX, otherwise we iterate until convergence
    V(t, i) = max_{x in X, x in A X, ..., x in A^t X} D_i A^(t+1) x - e_i for i = 1,...,n
    Once at convergence O_Inf = X U A X U ... U A^t X.

    Arguments
    ----------
    A : numpy.ndarray
        State transition matrix.
    X : instance of Polyhedron
        State-space domain of the dynamical system.
    verbose : bool
        If True prints at each iteration the convergence parameters.

    Returns:
    ----------
    O_inf : instance of Polyhedron
        Maximal constraint-admissible (positive) ivariant.
    t : int
        Determinedness index.
    """

    # ensure convergence of the algorithm
    eig_max = np.max(np.absolute(np.linalg.eig(A)[0]))
    if eig_max > 1.:
        raise ValueError(
            'unstable system, cannot derive maximal constraint-admissible set.'
        )
    [nc, nx] = X.A.shape
    if not X.contains(np.zeros((nx, 1))):
        raise ValueError(
            'the origin is not contained in the constraint set, cannot derive maximal constraint-admissible set.'
        )
    if not X.bounded:
        raise ValueError(
            'unbounded constraint set, cannot derive maximal constraint-admissible set.'
        )

    # initialize mcais
    O_inf = copy(X)

    # loop over time
    t = 1
    convergence = False
    while not convergence:

        # solve one LP per facet
        J = X.A.dot(np.linalg.matrix_power(A, t))
        residuals = []
        for i in range(X.A.shape[0]):
            sol = linear_program(-J[i], O_inf.A, O_inf.b)
            residuals.append(-sol['min'] - X.b[i])

        # print status of the algorithm
        if verbose:
            print('Time horizon: ' + str(t) + '.'),
            print('Convergence index: ' + str(max(residuals)) + '.'),
            print('Number of facets: ' + str(O_inf.A.shape[0]) + '.   \r'),

        # convergence check
        new_facets = [i for i, r in enumerate(residuals) if r > 0.]
        if len(new_facets) == 0:
            convergence = True
        else:

            # add (only non-redundant!) facets
            O_inf.add_inequality(J[new_facets], X.b[new_facets])
            t += 1

    # remove redundant facets
    if verbose:
        print('\nMaximal constraint-admissible invariant set found.')
        print('Removing redundant facets ...'),
    O_inf.remove_redundant_inequalities()
    if verbose:
        print('minimal facets are ' + str(O_inf.A.shape[0]) + '.')

    return O_inf
Beispiel #14
0
    def _get_bigM_dynamics(self):
        """
        Computes all the bigMs for the dynamics of the PWA system.
        The PWA system has the dynamics
        x(t+1) = A_i x(t) + B_i u(t) + c_i if (x(t),u(t)) in D_i,
        where i in {1, ..., s}.
        In order to express it in mixed-integer form, for t = 0, ..., N-1, we introduce the auxiliary variables z_i(t), and we set
        x(t+1) = sum_{i=1}^s z_i(t).
        We now reformulate the dynamics as
        z_i(t) >= alpha_ii delta_i(t),                                               (1)
        z_i(t) <= beta_ii delta_i(t),                                                (2)
        A_i x(t) + B_i u(t) + c_i - z_i(t) >= sum_{j=1, j!=i}^s alpha_ij delta_j(t), (3)
        A_i x(t) + B_i u(t) + c_i - z_i(t) <= sum_{j=1, j!=i}^s beta_ij delta_j(t).  (4)
        Here alpha_ij (<< 0) and beta_ij (>> 0) are both vectors of bigMs and delta_j(t) is a binary variable (equal to 1 if the system is in mode j, zero otherwise).
        If the system is in mode k at time t (i.e. delta_k(t) = 1), we have that
        z_i(t) = 0, for all i != k,
        z_k(t) >= alpha_kk,
        z_k(t) <= beta_kk,
        A_i x(t) + B_i u(t) + c_i - z_i(t) >= alpha_ik, for all i != k,
        A_i x(t) + B_i u(t) + c_i - z_i(t) <= beta_ik, for all i != k,
        A_k x(t) + B_k u(t) + c_k = z_k(t),
        that sets
        x(t+1) = z_k(t) = A_k x(t) + B_k u(t) + c_k
        as desired.
        It is very important to choose the bigMs as tight as possible, for this reason we set
        alpha_ij := min_{(x,u) in D_j} A_i x + B_i u + c_i,
        beta_ij := max_{(x,u) in D_j} A_i x + B_i u + c_i.
        (Note that the previous are a number of LPs equal to the number of states.)
        The previous ensures that when the system is mode j != i, the dynamics A_i x + B_i u + c_i is lower bounded by alpha_ik and upper bounded by beta_ij.

        Returns
        ----------
        alpha : list of lists of numpy.ndarray
            alpha[i][j] is the vector alpha_ij defined above.
        beta : list of lists of numpy.ndarray
            beta[i][j] is the vector beta_ij defined above.
        """

        # initialize list of bigMs
        alpha = []
        beta = []

        # outer loop over the number of affine systems
        for i, S_i in enumerate(self.S.affine_systems):
            alpha_i = []
            beta_i = []
            A_i = np.hstack((S_i.A, S_i.B))

            # inner loop over the number of affine systems
            for j, S_j in enumerate(self.S.affine_systems):
                alpha_ij = []
                beta_ij = []
                D_j = self.S.domains[j]

                # solve two LPs for each component of the state vector
                for k in range(S_i.nx):
                    f = A_i[k:k + 1, :].T
                    sol = linear_program(f, D_j.A, D_j.b, D_j.C, D_j.d)
                    alpha_ij.append(sol['min'] + S_i.c[k, 0])
                    sol = linear_program(-f, D_j.A, D_j.b, D_j.C, D_j.d)
                    beta_ij.append(-sol['min'] + S_i.c[k, 0])

                # close inner loop appending bigMs
                alpha_i.append(np.vstack(alpha_ij))
                beta_i.append(np.vstack(beta_ij))

            # close outer loop appending bigMs
            alpha.append(alpha_i)
            beta.append(beta_i)

        return alpha, beta