Fix unitary tests
[linpy.git] / linpy / geometry.py
1 # Copyright 2014 MINES ParisTech
2 #
3 # This file is part of LinPy.
4 #
5 # LinPy is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # LinPy is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with LinPy. If not, see <http://www.gnu.org/licenses/>.
17
18 import math
19 import numbers
20 import operator
21
22 from abc import ABC, abstractproperty, abstractmethod
23 from collections import OrderedDict, Mapping
24
25 from .linexprs import Symbol
26
27
28 __all__ = [
29 'GeometricObject',
30 'Point',
31 'Vector',
32 ]
33
34
35 class GeometricObject(ABC):
36
37 @abstractproperty
38 def symbols(self):
39 pass
40
41 @property
42 def dimension(self):
43 return len(self.symbols)
44
45 @abstractmethod
46 def aspolyhedron(self):
47 pass
48
49 def asdomain(self):
50 return self.aspolyhedron()
51
52
53 class Coordinates:
54
55 __slots__ = (
56 '_coordinates',
57 )
58
59 def __new__(cls, coordinates):
60 if isinstance(coordinates, Mapping):
61 coordinates = coordinates.items()
62 self = object().__new__(cls)
63 self._coordinates = OrderedDict()
64 for symbol, coordinate in sorted(coordinates,
65 key=lambda item: item[0].sortkey()):
66 if not isinstance(symbol, Symbol):
67 raise TypeError('symbols must be Symbol instances')
68 if not isinstance(coordinate, numbers.Real):
69 raise TypeError('coordinates must be real numbers')
70 self._coordinates[symbol] = coordinate
71 return self
72
73 @property
74 def symbols(self):
75 return tuple(self._coordinates)
76
77 @property
78 def dimension(self):
79 return len(self.symbols)
80
81 def coordinates(self):
82 yield from self._coordinates.items()
83
84 def coordinate(self, symbol):
85 if not isinstance(symbol, Symbol):
86 raise TypeError('symbol must be a Symbol instance')
87 return self._coordinates[symbol]
88
89 __getitem__ = coordinate
90
91 def values(self):
92 yield from self._coordinates.values()
93
94 def __bool__(self):
95 return any(self._coordinates.values())
96
97 def __hash__(self):
98 return hash(tuple(self.coordinates()))
99
100 def __repr__(self):
101 string = ', '.join(['{!r}: {!r}'.format(symbol, coordinate)
102 for symbol, coordinate in self.coordinates()])
103 return '{}({{{}}})'.format(self.__class__.__name__, string)
104
105 def _map(self, func):
106 for symbol, coordinate in self.coordinates():
107 yield symbol, func(coordinate)
108
109 def _iter2(self, other):
110 if self.symbols != other.symbols:
111 raise ValueError('arguments must belong to the same space')
112 coordinates1 = self._coordinates.values()
113 coordinates2 = other._coordinates.values()
114 yield from zip(self.symbols, coordinates1, coordinates2)
115
116 def _map2(self, other, func):
117 for symbol, coordinate1, coordinate2 in self._iter2(other):
118 yield symbol, func(coordinate1, coordinate2)
119
120
121 class Point(Coordinates, GeometricObject):
122 """
123 This class represents points in space.
124 """
125
126 def isorigin(self):
127 """
128 Return True if a Point is the origin.
129 """
130 return not bool(self)
131
132 def __hash__(self):
133 return super().__hash__()
134
135 def __add__(self, other):
136 """
137 Adds a Point to a Vector and returns the result as a Point.
138 """
139 if not isinstance(other, Vector):
140 return NotImplemented
141 coordinates = self._map2(other, operator.add)
142 return Point(coordinates)
143
144 def __sub__(self, other):
145 """
146 Returns the difference between two Points as a Vector.
147 """
148 coordinates = []
149 if isinstance(other, Point):
150 coordinates = self._map2(other, operator.sub)
151 return Vector(coordinates)
152 elif isinstance(other, Vector):
153 coordinates = self._map2(other, operator.sub)
154 return Point(coordinates)
155 else:
156 return NotImplemented
157
158 def __eq__(self, other):
159 """
160 Compares two Points for equality.
161 """
162 return isinstance(other, Point) and \
163 self._coordinates == other._coordinates
164
165 def aspolyhedron(self):
166 """
167 Return a Point as a polyhedron.
168 """
169 from .polyhedra import Polyhedron
170 equalities = []
171 for symbol, coordinate in self.coordinates():
172 equalities.append(symbol - coordinate)
173 return Polyhedron(equalities)
174
175
176 class Vector(Coordinates):
177 """
178 This class represents displacements in space.
179 """
180
181 def __new__(cls, initial, terminal=None):
182 if not isinstance(initial, Point):
183 initial = Point(initial)
184 if terminal is None:
185 coordinates = initial._coordinates
186 else:
187 if not isinstance(terminal, Point):
188 terminal = Point(terminal)
189 coordinates = terminal._map2(initial, operator.sub)
190 return super().__new__(cls, coordinates)
191
192 def isnull(self):
193 """
194 Returns true if a Vector is null.
195 """
196 return not bool(self)
197
198 def __hash__(self):
199 return super().__hash__()
200
201 def __add__(self, other):
202 """
203 Adds either a Point or Vector to a Vector.
204 """
205 if isinstance(other, (Point, Vector)):
206 coordinates = self._map2(other, operator.add)
207 return other.__class__(coordinates)
208 return NotImplemented
209
210 def angle(self, other):
211 """
212 Retrieve the angle required to rotate the vector into the vector passed
213 in argument. The result is an angle in radians, ranging between -pi and
214 pi.
215 """
216 if not isinstance(other, Vector):
217 raise TypeError('argument must be a Vector instance')
218 cosinus = self.dot(other) / (self.norm()*other.norm())
219 return math.acos(cosinus)
220
221 def cross(self, other):
222 """
223 Calculate the cross product of two Vector3D structures.
224 """
225 if not isinstance(other, Vector):
226 raise TypeError('other must be a Vector instance')
227 if self.dimension != 3 or other.dimension != 3:
228 raise ValueError('arguments must be three-dimensional vectors')
229 if self.symbols != other.symbols:
230 raise ValueError('arguments must belong to the same space')
231 x, y, z = self.symbols
232 coordinates = []
233 coordinates.append((x, self[y]*other[z] - self[z]*other[y]))
234 coordinates.append((y, self[z]*other[x] - self[x]*other[z]))
235 coordinates.append((z, self[x]*other[y] - self[y]*other[x]))
236 return Vector(coordinates)
237
238 def __truediv__(self, other):
239 """
240 Divide the vector by the specified scalar and returns the result as a
241 vector.
242 """
243 if not isinstance(other, numbers.Real):
244 return NotImplemented
245 coordinates = self._map(lambda coordinate: coordinate / other)
246 return Vector(coordinates)
247
248 def dot(self, other):
249 """
250 Calculate the dot product of two vectors.
251 """
252 if not isinstance(other, Vector):
253 raise TypeError('argument must be a Vector instance')
254 result = 0
255 for symbol, coordinate1, coordinate2 in self._iter2(other):
256 result += coordinate1 * coordinate2
257 return result
258
259 def __eq__(self, other):
260 """
261 Compares two Vectors for equality.
262 """
263 return isinstance(other, Vector) and \
264 self._coordinates == other._coordinates
265
266 def __hash__(self):
267 return hash(tuple(self.coordinates()))
268
269 def __mul__(self, other):
270 """
271 Multiplies a Vector by a scalar value.
272 """
273 if not isinstance(other, numbers.Real):
274 return NotImplemented
275 coordinates = self._map(lambda coordinate: other * coordinate)
276 return Vector(coordinates)
277
278 __rmul__ = __mul__
279
280 def __neg__(self):
281 """
282 Returns the negated form of a Vector.
283 """
284 coordinates = self._map(operator.neg)
285 return Vector(coordinates)
286
287 def norm(self):
288 """
289 Normalizes a Vector.
290 """
291 return math.sqrt(self.norm2())
292
293 def norm2(self):
294 result = 0
295 for coordinate in self._coordinates.values():
296 result += coordinate ** 2
297 return result
298
299 def asunit(self):
300 return self / self.norm()
301
302 def __sub__(self, other):
303 """
304 Subtract a Point or Vector from a Vector.
305 """
306 if isinstance(other, (Point, Vector)):
307 coordinates = self._map2(other, operator.sub)
308 return other.__class__(coordinates)
309 return NotImplemented