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