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