blob: 4c49bb8bc4a1631b150e52b5e88d4cbd38e3fabb [file] [log] [blame]
Austin Schuh085eab92020-11-26 13:54:51 -08001#!/usr/bin/python3
brians343bc112013-02-10 01:53:46 +00002"""
3Polyhedral set library.
4
5This library implements convex regions of the form H x <= k, where H, x, and k
6are matricies. It also provides convenient methods to find all the verticies.
7"""
8
9__author__ = 'Austin Schuh (austin.linux@gmail.com)'
10
Austin Schuhedc317c2015-11-08 14:07:42 -080011from frc971.control_loops.python import libcdd
brians343bc112013-02-10 01:53:46 +000012import numpy
13import string
14import sys
15
16
17def _PiecewiseConcat(*args):
Ravago Jones26f7ad02021-02-05 15:45:59 -080018 """Concatenates strings inside lists, elementwise.
brians343bc112013-02-10 01:53:46 +000019
20 Given ['a', 's'] and ['d', 'f'], returns ['ad', 'sf']
21 """
Ravago Jones26f7ad02021-02-05 15:45:59 -080022 return map(''.join, zip(*args))
brians343bc112013-02-10 01:53:46 +000023
24
25def _SplitAndPad(s):
Ravago Jones26f7ad02021-02-05 15:45:59 -080026 """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
brians343bc112013-02-10 01:53:46 +000029
Ravago Jones26f7ad02021-02-05 15:45:59 -080030 padded_strings = [
31 stringpiece.ljust(width, ' ') for stringpiece in split_string
32 ]
33 return padded_strings
brians343bc112013-02-10 01:53:46 +000034
35
36def _PadHeight(padded_array, min_height):
Ravago Jones26f7ad02021-02-05 15:45:59 -080037 """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
brians343bc112013-02-10 01:53:46 +000046
47
48class HPolytope(object):
Ravago Jones26f7ad02021-02-05 15:45:59 -080049 """This object represents a H-polytope.
brians343bc112013-02-10 01:53:46 +000050
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 Jones26f7ad02021-02-05 15:45:59 -080056 def __init__(self, H, k):
57 """Constructs a H-polytope from the H and k matricies.
brians343bc112013-02-10 01:53:46 +000058
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 Jones26f7ad02021-02-05 15:45:59 -080064 self._H = H
65 self._k = k
brians343bc112013-02-10 01:53:46 +000066
Ravago Jones26f7ad02021-02-05 15:45:59 -080067 @property
68 def k(self):
69 """Returns the k in H x <= k."""
70 return self._k
brians343bc112013-02-10 01:53:46 +000071
Ravago Jones26f7ad02021-02-05 15:45:59 -080072 @property
73 def H(self):
74 """Returns the H in H x <= k."""
75 return self._H
brians343bc112013-02-10 01:53:46 +000076
Ravago Jones26f7ad02021-02-05 15:45:59 -080077 @property
78 def ndim(self):
79 """Returns the dimension of the set."""
80 return self._H.shape[1]
brians343bc112013-02-10 01:53:46 +000081
Ravago Jones26f7ad02021-02-05 15:45:59 -080082 @property
83 def num_constraints(self):
84 """Returns the number of constraints defining the set."""
85 return self._k.shape[0]
brians343bc112013-02-10 01:53:46 +000086
Ravago Jones26f7ad02021-02-05 15:45:59 -080087 def IsInside(self, point):
88 """Returns true if the point is inside the polytope, edges included."""
89 return (self._H * point <= self._k).all()
brians343bc112013-02-10 01:53:46 +000090
Ravago Jones26f7ad02021-02-05 15:45:59 -080091 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.
brians343bc112013-02-10 01:53:46 +000096
Ravago Jones26f7ad02021-02-05 15:45:59 -080097 # Create an empty matrix with the correct size.
98 matrixptr = libcdd.dd_CreateMatrix(self.num_constraints, self.ndim + 1)
99 matrix = matrixptr.contents
brians343bc112013-02-10 01:53:46 +0000100
101 try:
Ravago Jones26f7ad02021-02-05 15:45:59 -0800102 # 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])
brians343bc112013-02-10 01:53:46 +0000107
Ravago Jones26f7ad02021-02-05 15:45:59 -0800108 # Set enums to the correct values.
109 matrix.representation = libcdd.DD_INEQUALITY
110 matrix.numbtype = libcdd.DD_REAL
brians343bc112013-02-10 01:53:46 +0000111
Ravago Jones26f7ad02021-02-05 15:45:59 -0800112 # TODO(aschuh): Set linearity if it is useful.
113 # This would be useful if we had any constraints saying B - A x = 0
brians343bc112013-02-10 01:53:46 +0000114
Ravago Jones26f7ad02021-02-05 15:45:59 -0800115 # 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
brians343bc112013-02-10 01:53:46 +0000168 finally:
Ravago Jones26f7ad02021-02-05 15:45:59 -0800169 libcdd.dd_FreeMatrix(matrixptr)
brians343bc112013-02-10 01:53:46 +0000170
Ravago Jones26f7ad02021-02-05 15:45:59 -0800171 # Rays are unsupported right now. This may change in the future.
172 assert (rays.shape[0] == 0)
brians343bc112013-02-10 01:53:46 +0000173
Ravago Jones26f7ad02021-02-05 15:45:59 -0800174 return vertices
brians343bc112013-02-10 01:53:46 +0000175
Ravago Jones26f7ad02021-02-05 15:45:59 -0800176 def __str__(self):
177 """Returns a formatted version of the polytope.
brians343bc112013-02-10 01:53:46 +0000178
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 Jones26f7ad02021-02-05 15:45:59 -0800187 height = max(self.ndim, self.num_constraints)
brians343bc112013-02-10 01:53:46 +0000188
Ravago Jones26f7ad02021-02-05 15:45:59 -0800189 # 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)
brians343bc112013-02-10 01:53:46 +0000194
Ravago Jones26f7ad02021-02-05 15:45:59 -0800195 return '\n'.join(
196 _PiecewiseConcat(H_strings, x_strings, cmp_strings, v_strings))
brians343bc112013-02-10 01:53:46 +0000197
Ravago Jones26f7ad02021-02-05 15:45:59 -0800198 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]] "]
brians343bc112013-02-10 01:53:46 +0000203 else:
Ravago Jones26f7ad02021-02-05 15:45:59 -0800204 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
brians343bc112013-02-10 01:53:46 +0000213
Ravago Jones26f7ad02021-02-05 15:45:59 -0800214 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