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]]))
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]) )
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
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
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
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
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
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
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
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
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
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
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
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