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