| #!/usr/bin/python3 |
| """ |
| Polyhedral set library. |
| |
| This library implements convex regions of the form H x <= k, where H, x, and k |
| are matricies. It also provides convenient methods to find all the verticies. |
| """ |
| |
| __author__ = 'Austin Schuh (austin.linux@gmail.com)' |
| |
| from frc971.control_loops.python import libcdd |
| import numpy |
| import string |
| import sys |
| |
| |
| def _PiecewiseConcat(*args): |
| """Concatenates strings inside lists, elementwise. |
| |
| Given ['a', 's'] and ['d', 'f'], returns ['ad', 'sf'] |
| """ |
| return map(''.join, zip(*args)) |
| |
| |
| def _SplitAndPad(s): |
| """Splits a string on newlines, and pads the lines to be the same width.""" |
| split_string = s.split('\n') |
| width = max(len(stringpiece) for stringpiece in split_string) + 1 |
| |
| padded_strings = [ |
| stringpiece.ljust(width, ' ') for stringpiece in split_string |
| ] |
| return padded_strings |
| |
| |
| def _PadHeight(padded_array, min_height): |
| """Adds lines of spaces to the top and bottom of an array symmetrically.""" |
| height = len(padded_array) |
| if height < min_height: |
| pad_array = [' ' * len(padded_array[0])] |
| height_error = min_height - height |
| return (pad_array * int( |
| (height_error) / 2) + padded_array + pad_array * int( |
| (height_error + 1) / 2)) |
| return padded_array |
| |
| |
| class HPolytope(object): |
| """This object represents a H-polytope. |
| |
| Polytopes are convex regions in n-dimensional space. |
| For H-polytopes, this is represented as the intersection of a set of half |
| planes. The mathematic equation that represents this is H x <= k. |
| """ |
| |
| def __init__(self, H, k): |
| """Constructs a H-polytope from the H and k matricies. |
| |
| Args: |
| H: numpy.Matrix (n by k), where n is the number of constraints, and k is |
| the number of dimensions in the space. Does not copy the matrix. |
| k: numpy.Matrix (n by 1). Does not copy the matrix. |
| """ |
| self._H = H |
| self._k = k |
| |
| @property |
| def k(self): |
| """Returns the k in H x <= k.""" |
| return self._k |
| |
| @property |
| def H(self): |
| """Returns the H in H x <= k.""" |
| return self._H |
| |
| @property |
| def ndim(self): |
| """Returns the dimension of the set.""" |
| return self._H.shape[1] |
| |
| @property |
| def num_constraints(self): |
| """Returns the number of constraints defining the set.""" |
| return self._k.shape[0] |
| |
| def IsInside(self, point): |
| """Returns true if the point is inside the polytope, edges included.""" |
| return (self._H * point <= self._k).all() |
| |
| def Vertices(self): |
| """Returns a matrix with the vertices of the set in its rows.""" |
| # TODO(aschuh): It would be better to write some small C/C++ function that |
| # does all of this and takes in a numpy array. |
| # The copy is expensive in Python and cheaper in C. |
| |
| # Create an empty matrix with the correct size. |
| matrixptr = libcdd.dd_CreateMatrix(self.num_constraints, self.ndim + 1) |
| matrix = matrixptr.contents |
| |
| try: |
| # Copy the data into the matrix. |
| for i in range(self.num_constraints): |
| libcdd.dd_set_d(matrix.matrix[i][0], self._k[i, 0]) |
| for j in range(self.ndim): |
| libcdd.dd_set_d(matrix.matrix[i][j + 1], -self._H[i, j]) |
| |
| # Set enums to the correct values. |
| matrix.representation = libcdd.DD_INEQUALITY |
| matrix.numbtype = libcdd.DD_REAL |
| |
| # TODO(aschuh): Set linearity if it is useful. |
| # This would be useful if we had any constraints saying B - A x = 0 |
| |
| # Build a Polyhedra |
| polyhedraptr = libcdd.dd_DDMatrix2Poly(matrixptr) |
| |
| if not polyhedraptr: |
| return None |
| |
| try: |
| # Return None on error. |
| # The error values are enums, so they aren't exposed. |
| |
| # Magic happens here. Computes the vertices |
| vertex_matrixptr = libcdd.dd_CopyGenerators(polyhedraptr) |
| vertex_matrix = vertex_matrixptr.contents |
| |
| try: |
| # Count the number of vertices and rays in the result. |
| num_vertices = 0 |
| num_rays = 0 |
| for i in range(vertex_matrix.rowsize): |
| if libcdd.dd_get_d(vertex_matrix.matrix[i][0]) == 0: |
| num_rays += 1 |
| else: |
| num_vertices += 1 |
| |
| # Build zeroed matricies for the new vertices and rays. |
| vertices = numpy.matrix( |
| numpy.zeros((num_vertices, vertex_matrix.colsize - 1))) |
| rays = numpy.matrix( |
| numpy.zeros((num_rays, vertex_matrix.colsize - 1))) |
| |
| ray_index = 0 |
| vertex_index = 0 |
| |
| # Copy the data out of the matrix. |
| for index in range(vertex_matrix.rowsize): |
| if libcdd.dd_get_d( |
| vertex_matrix.matrix[index][0]) == 0.0: |
| for j in range(vertex_matrix.colsize - 1): |
| rays[ray_index, j] = libcdd.dd_get_d( |
| vertex_matrix.matrix[index][j + 1]) |
| ray_index += 1 |
| else: |
| for j in range(vertex_matrix.colsize - 1): |
| vertices[vertex_index, j] = libcdd.dd_get_d( |
| vertex_matrix.matrix[index][j + 1]) |
| vertex_index += 1 |
| finally: |
| # Free everything. |
| libcdd.dd_FreeMatrix(vertex_matrixptr) |
| |
| finally: |
| libcdd.dd_FreePolyhedra(polyhedraptr) |
| |
| finally: |
| libcdd.dd_FreeMatrix(matrixptr) |
| |
| # Rays are unsupported right now. This may change in the future. |
| assert (rays.shape[0] == 0) |
| |
| return vertices |
| |
| def __str__(self): |
| """Returns a formatted version of the polytope. |
| |
| The dump will look something like the following, which prints out the matrix |
| comparison. |
| |
| [[ 1 0] [[12] |
| [-1 0] [[x0] <= [12] |
| [ 0 1] [x1]] [12] |
| [ 0 -1]] [12]] |
| """ |
| height = max(self.ndim, self.num_constraints) |
| |
| # Split the print up into 4 parts and concatenate them all. |
| H_strings = _PadHeight(_SplitAndPad(str(self.H)), height) |
| v_strings = _PadHeight(_SplitAndPad(str(self.k)), height) |
| x_strings = _PadHeight(self._MakeXStrings(), height) |
| cmp_strings = self._MakeCmpStrings(height) |
| |
| return '\n'.join( |
| _PiecewiseConcat(H_strings, x_strings, cmp_strings, v_strings)) |
| |
| def _MakeXStrings(self): |
| """Builds an array of strings with constraint names in it for printing.""" |
| x_strings = [] |
| if self.ndim == 1: |
| x_strings = ["[[x0]] "] |
| else: |
| for index in range(self.ndim): |
| if index == 0: |
| x = "[[x%d] " % index |
| elif index == self.ndim - 1: |
| x = " [x%d]] " % index |
| else: |
| x = " [x%d] " % index |
| x_strings.append(x) |
| return x_strings |
| |
| def _MakeCmpStrings(self, height): |
| """Builds an array of strings with the comparison in it for printing.""" |
| cmp_strings = [] |
| for index in range(height): |
| if index == int((height - 1) / 2): |
| cmp_strings.append("<= ") |
| else: |
| cmp_strings.append(" ") |
| return cmp_strings |