brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | |
| 3 | """ |
| 4 | Polyhedral set library. |
| 5 | |
| 6 | This library implements convex regions of the form H x <= k, where H, x, and k |
| 7 | are matricies. It also provides convenient methods to find all the verticies. |
| 8 | """ |
| 9 | |
| 10 | __author__ = 'Austin Schuh (austin.linux@gmail.com)' |
| 11 | |
| 12 | |
Austin Schuh | edc317c | 2015-11-08 14:07:42 -0800 | [diff] [blame] | 13 | from frc971.control_loops.python import libcdd |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 14 | import numpy |
| 15 | import string |
| 16 | import sys |
| 17 | |
| 18 | |
| 19 | def _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 | |
| 27 | def _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 | |
| 37 | def _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 Schuh | 5ea4847 | 2021-02-02 20:46:41 -0800 | [diff] [blame^] | 43 | return (pad_array * int((height_error) / 2) + |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 44 | padded_array + |
Austin Schuh | 5ea4847 | 2021-02-02 20:46:41 -0800 | [diff] [blame^] | 45 | pad_array * int((height_error + 1) / 2)) |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 46 | return padded_array |
| 47 | |
| 48 | |
| 49 | class 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 Schuh | 5ea4847 | 2021-02-02 20:46:41 -0800 | [diff] [blame^] | 104 | for i in range(self.num_constraints): |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 105 | libcdd.dd_set_d(matrix.matrix[i][0], self._k[i, 0]) |
Austin Schuh | 5ea4847 | 2021-02-02 20:46:41 -0800 | [diff] [blame^] | 106 | for j in range(self.ndim): |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 107 | 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 Schuh | 5ea4847 | 2021-02-02 20:46:41 -0800 | [diff] [blame^] | 135 | for i in range(vertex_matrix.rowsize): |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 136 | 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 Schuh | 5ea4847 | 2021-02-02 20:46:41 -0800 | [diff] [blame^] | 151 | for index in range(vertex_matrix.rowsize): |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 152 | if libcdd.dd_get_d(vertex_matrix.matrix[index][0]) == 0.0: |
Austin Schuh | 5ea4847 | 2021-02-02 20:46:41 -0800 | [diff] [blame^] | 153 | for j in range(vertex_matrix.colsize - 1): |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 154 | rays[ray_index, j] = libcdd.dd_get_d( |
| 155 | vertex_matrix.matrix[index][j + 1]) |
| 156 | ray_index += 1 |
| 157 | else: |
Austin Schuh | 5ea4847 | 2021-02-02 20:46:41 -0800 | [diff] [blame^] | 158 | for j in range(vertex_matrix.colsize - 1): |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 159 | 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 Schuh | 5ea4847 | 2021-02-02 20:46:41 -0800 | [diff] [blame^] | 206 | for index in range(self.ndim): |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 207 | 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 Schuh | 5ea4847 | 2021-02-02 20:46:41 -0800 | [diff] [blame^] | 219 | for index in range(height): |
| 220 | if index == int((height - 1) / 2): |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 221 | cmp_strings.append("<= ") |
| 222 | else: |
| 223 | cmp_strings.append(" ") |
| 224 | return cmp_strings |