Rename coordinates.py into geometry.py
[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 __bool__(self):
75 return any(self._coordinates.values())
76
77 def __hash__(self):
78 return hash(tuple(self.coordinates()))
79
80 def __repr__(self):
81 string = ', '.join(['{!r}: {!r}'.format(symbol, coordinate)
82 for symbol, coordinate in self.coordinates()])
83 return '{}({{{}}})'.format(self.__class__.__name__, string)
84
85 def _map(self, func):
86 for symbol, coordinate in self.coordinates():
87 yield symbol, func(coordinate)
88
89 def _iter2(self, other):
90 if self.symbols != other.symbols:
91 raise ValueError('arguments must belong to the same space')
92 coordinates1 = self._coordinates.values()
93 coordinates2 = other._coordinates.values()
94 yield from zip(self.symbols, coordinates1, coordinates2)
95
96 def _map2(self, other, func):
97 for symbol, coordinate1, coordinate2 in self._iter2(other):
98 yield symbol, func(coordinate1, coordinate2)
99
100
101 class Point(Coordinates, GeometricObject):
102 """
103 This class represents points in space.
104 """
105
106 def isorigin(self):
107 return not bool(self)
108
109 def __add__(self, other):
110 if not isinstance(other, Vector):
111 return NotImplemented
112 coordinates = self._map2(other, operator.add)
113 return Point(coordinates)
114
115 def __sub__(self, other):
116 coordinates = []
117 if isinstance(other, Point):
118 coordinates = self._map2(other, operator.sub)
119 return Vector(coordinates)
120 elif isinstance(other, Vector):
121 coordinates = self._map2(other, operator.sub)
122 return Point(coordinates)
123 else:
124 return NotImplemented
125
126 def __eq__(self, other):
127 return isinstance(other, Point) and \
128 self._coordinates == other._coordinates
129
130 def aspolyhedron(self):
131 from .polyhedra import Polyhedron
132 equalities = []
133 for symbol, coordinate in self.coordinates():
134 equalities.append(symbol - coordinate)
135 return Polyhedron(equalities)
136
137
138 class Vector(Coordinates):
139 """
140 This class represents displacements in space.
141 """
142
143 def __new__(cls, initial, terminal=None):
144 if not isinstance(initial, Point):
145 initial = Point(initial)
146 if terminal is None:
147 coordinates = initial._coordinates
148 elif not isinstance(terminal, Point):
149 terminal = Point(terminal)
150 coordinates = terminal._map2(initial, operator.sub)
151 return super().__new__(cls, coordinates)
152
153 def isnull(self):
154 return not bool(self)
155
156 def __add__(self, other):
157 if isinstance(other, (Point, Vector)):
158 coordinates = self._map2(other, operator.add)
159 return other.__class__(coordinates)
160 return NotImplemented
161
162 def angle(self, other):
163 """
164 Retrieve the angle required to rotate the vector into the vector passed
165 in argument. The result is an angle in radians, ranging between -pi and
166 pi.
167 """
168 if not isinstance(other, Vector):
169 raise TypeError('argument must be a Vector instance')
170 cosinus = self.dot(other) / (self.norm()*other.norm())
171 return math.acos(cosinus)
172
173 def cross(self, other):
174 """
175 Calculate the cross product of two Vector3D structures.
176 """
177 if not isinstance(other, Vector):
178 raise TypeError('other must be a Vector instance')
179 if self.dimension != 3 or other.dimension != 3:
180 raise ValueError('arguments must be three-dimensional vectors')
181 if self.symbols != other.symbols:
182 raise ValueError('arguments must belong to the same space')
183 x, y, z = self.symbols
184 coordinates = []
185 coordinates.append((x, self[y]*other[z] - self[z]*other[y]))
186 coordinates.append((y, self[z]*other[x] - self[x]*other[z]))
187 coordinates.append((z, self[x]*other[y] - self[y]*other[x]))
188 return Vector(coordinates)
189
190 def __truediv__(self, other):
191 """
192 Divide the vector by the specified scalar and returns the result as a
193 vector.
194 """
195 if not isinstance(other, numbers.Real):
196 return NotImplemented
197 coordinates = self._map(lambda coordinate: coordinate / other)
198 return Vector(coordinates)
199
200 def dot(self, other):
201 """
202 Calculate the dot product of two vectors.
203 """
204 if not isinstance(other, Vector):
205 raise TypeError('argument must be a Vector instance')
206 result = 0
207 for symbol, coordinate1, coordinate2 in self._iter2(other):
208 result += coordinate1 * coordinate2
209 return result
210
211 def __eq__(self, other):
212 return isinstance(other, Vector) and \
213 self._coordinates == other._coordinates
214
215 def __hash__(self):
216 return hash(tuple(self.coordinates()))
217
218 def __mul__(self, other):
219 if not isinstance(other, numbers.Real):
220 return NotImplemented
221 coordinates = self._map(lambda coordinate: other * coordinate)
222 return Vector(coordinates)
223
224 __rmul__ = __mul__
225
226 def __neg__(self):
227 coordinates = self._map(operator.neg)
228 return Vector(coordinates)
229
230 def norm(self):
231 return math.sqrt(self.norm2())
232
233 def norm2(self):
234 result = 0
235 for coordinate in self._coordinates.values():
236 result += coordinate ** 2
237 return result
238
239 def asunit(self):
240 return self / self.norm()
241
242 def __sub__(self, other):
243 if isinstance(other, (Point, Vector)):
244 coordinates = self._map2(other, operator.sub)
245 return other.__class__(coordinates)
246 return NotImplemented