blob: a1cba579a7791f4900ac61531eb48353acfa28ac [file] [log] [blame]
brians343bc112013-02-10 01:53:46 +00001#!/usr/bin/python
2
3"""
4Polyhedral set library.
5
6This library implements convex regions of the form H x <= k, where H, x, and k
7are matricies. It also provides convenient methods to find all the verticies.
8"""
9
10__author__ = 'Austin Schuh (austin.linux@gmail.com)'
11
12
Austin Schuhedc317c2015-11-08 14:07:42 -080013from frc971.control_loops.python import libcdd
brians343bc112013-02-10 01:53:46 +000014import numpy
15import string
16import sys
17
18
19def _PiecewiseConcat(*args):
20 """Concatenates strings inside lists, elementwise.
21
22 Given ['a', 's'] and ['d', 'f'], returns ['ad', 'sf']
23 """
24 return map(''.join, zip(*args))
25
26
27def _SplitAndPad(s):
28 """Splits a string on newlines, and pads the lines to be the same width."""
29 split_string = s.split('\n')
30 width = max(len(stringpiece) for stringpiece in split_string) + 1
31
32 padded_strings = [string.ljust(stringpiece, width, ' ')
33 for stringpiece in split_string]
34 return padded_strings
35
36
37def _PadHeight(padded_array, min_height):
38 """Adds lines of spaces to the top and bottom of an array symmetrically."""
39 height = len(padded_array)
40 if height < min_height:
41 pad_array = [' ' * len(padded_array[0])]
42 height_error = min_height - height
Austin Schuh5ea48472021-02-02 20:46:41 -080043 return (pad_array * int((height_error) / 2) +
brians343bc112013-02-10 01:53:46 +000044 padded_array +
Austin Schuh5ea48472021-02-02 20:46:41 -080045 pad_array * int((height_error + 1) / 2))
brians343bc112013-02-10 01:53:46 +000046 return padded_array
47
48
49class HPolytope(object):
50 """This object represents a H-polytope.
51
52 Polytopes are convex regions in n-dimensional space.
53 For H-polytopes, this is represented as the intersection of a set of half
54 planes. The mathematic equation that represents this is H x <= k.
55 """
56
57 def __init__(self, H, k):
58 """Constructs a H-polytope from the H and k matricies.
59
60 Args:
61 H: numpy.Matrix (n by k), where n is the number of constraints, and k is
62 the number of dimensions in the space. Does not copy the matrix.
63 k: numpy.Matrix (n by 1). Does not copy the matrix.
64 """
65 self._H = H
66 self._k = k
67
68 @property
69 def k(self):
70 """Returns the k in H x <= k."""
71 return self._k
72
73 @property
74 def H(self):
75 """Returns the H in H x <= k."""
76 return self._H
77
78 @property
79 def ndim(self):
80 """Returns the dimension of the set."""
81 return self._H.shape[1]
82
83 @property
84 def num_constraints(self):
85 """Returns the number of constraints defining the set."""
86 return self._k.shape[0]
87
88 def IsInside(self, point):
89 """Returns true if the point is inside the polytope, edges included."""
90 return (self._H * point <= self._k).all()
91
92 def Vertices(self):
93 """Returns a matrix with the vertices of the set in its rows."""
94 # TODO(aschuh): It would be better to write some small C/C++ function that
95 # does all of this and takes in a numpy array.
96 # The copy is expensive in Python and cheaper in C.
97
98 # Create an empty matrix with the correct size.
99 matrixptr = libcdd.dd_CreateMatrix(self.num_constraints, self.ndim + 1)
100 matrix = matrixptr.contents
101
102 try:
103 # Copy the data into the matrix.
Austin Schuh5ea48472021-02-02 20:46:41 -0800104 for i in range(self.num_constraints):
brians343bc112013-02-10 01:53:46 +0000105 libcdd.dd_set_d(matrix.matrix[i][0], self._k[i, 0])
Austin Schuh5ea48472021-02-02 20:46:41 -0800106 for j in range(self.ndim):
brians343bc112013-02-10 01:53:46 +0000107 libcdd.dd_set_d(matrix.matrix[i][j + 1], -self._H[i, j])
108
109 # Set enums to the correct values.
110 matrix.representation = libcdd.DD_INEQUALITY
111 matrix.numbtype = libcdd.DD_REAL
112
113 # TODO(aschuh): Set linearity if it is useful.
114 # This would be useful if we had any constraints saying B - A x = 0
115
116 # Build a Polyhedra
117 polyhedraptr = libcdd.dd_DDMatrix2Poly(matrixptr)
118
119 if not polyhedraptr:
120 return None
121
122 try:
123 # Return None on error.
124 # The error values are enums, so they aren't exposed.
125
126 # Magic happens here. Computes the vertices
127 vertex_matrixptr = libcdd.dd_CopyGenerators(
128 polyhedraptr)
129 vertex_matrix = vertex_matrixptr.contents
130
131 try:
132 # Count the number of vertices and rays in the result.
133 num_vertices = 0
134 num_rays = 0
Austin Schuh5ea48472021-02-02 20:46:41 -0800135 for i in range(vertex_matrix.rowsize):
brians343bc112013-02-10 01:53:46 +0000136 if libcdd.dd_get_d(vertex_matrix.matrix[i][0]) == 0:
137 num_rays += 1
138 else:
139 num_vertices += 1
140
141 # Build zeroed matricies for the new vertices and rays.
142 vertices = numpy.matrix(numpy.zeros((num_vertices,
143 vertex_matrix.colsize - 1)))
144 rays = numpy.matrix(numpy.zeros((num_rays,
145 vertex_matrix.colsize - 1)))
146
147 ray_index = 0
148 vertex_index = 0
149
150 # Copy the data out of the matrix.
Austin Schuh5ea48472021-02-02 20:46:41 -0800151 for index in range(vertex_matrix.rowsize):
brians343bc112013-02-10 01:53:46 +0000152 if libcdd.dd_get_d(vertex_matrix.matrix[index][0]) == 0.0:
Austin Schuh5ea48472021-02-02 20:46:41 -0800153 for j in range(vertex_matrix.colsize - 1):
brians343bc112013-02-10 01:53:46 +0000154 rays[ray_index, j] = libcdd.dd_get_d(
155 vertex_matrix.matrix[index][j + 1])
156 ray_index += 1
157 else:
Austin Schuh5ea48472021-02-02 20:46:41 -0800158 for j in range(vertex_matrix.colsize - 1):
brians343bc112013-02-10 01:53:46 +0000159 vertices[vertex_index, j] = libcdd.dd_get_d(
160 vertex_matrix.matrix[index][j + 1])
161 vertex_index += 1
162 finally:
163 # Free everything.
164 libcdd.dd_FreeMatrix(vertex_matrixptr)
165
166 finally:
167 libcdd.dd_FreePolyhedra(polyhedraptr)
168
169 finally:
170 libcdd.dd_FreeMatrix(matrixptr)
171
172 # Rays are unsupported right now. This may change in the future.
173 assert(rays.shape[0] == 0)
174
175 return vertices
176
177
178 def __str__(self):
179 """Returns a formatted version of the polytope.
180
181 The dump will look something like the following, which prints out the matrix
182 comparison.
183
184 [[ 1 0] [[12]
185 [-1 0] [[x0] <= [12]
186 [ 0 1] [x1]] [12]
187 [ 0 -1]] [12]]
188 """
189 height = max(self.ndim, self.num_constraints)
190
191 # Split the print up into 4 parts and concatenate them all.
192 H_strings = _PadHeight(_SplitAndPad(str(self.H)), height)
193 v_strings = _PadHeight(_SplitAndPad(str(self.k)), height)
194 x_strings = _PadHeight(self._MakeXStrings(), height)
195 cmp_strings = self._MakeCmpStrings(height)
196
197 return '\n'.join(_PiecewiseConcat(H_strings, x_strings,
198 cmp_strings, v_strings))
199
200 def _MakeXStrings(self):
201 """Builds an array of strings with constraint names in it for printing."""
202 x_strings = []
203 if self.ndim == 1:
204 x_strings = ["[[x0]] "]
205 else:
Austin Schuh5ea48472021-02-02 20:46:41 -0800206 for index in range(self.ndim):
brians343bc112013-02-10 01:53:46 +0000207 if index == 0:
208 x = "[[x%d] " % index
209 elif index == self.ndim - 1:
210 x = " [x%d]] " % index
211 else:
212 x = " [x%d] " % index
213 x_strings.append(x)
214 return x_strings
215
216 def _MakeCmpStrings(self, height):
217 """Builds an array of strings with the comparison in it for printing."""
218 cmp_strings = []
Austin Schuh5ea48472021-02-02 20:46:41 -0800219 for index in range(height):
220 if index == int((height - 1) / 2):
brians343bc112013-02-10 01:53:46 +0000221 cmp_strings.append("<= ")
222 else:
223 cmp_strings.append(" ")
224 return cmp_strings