5 from abc
import ABC
, abstractmethod
6 from collections
import OrderedDict
8 from .linexprs
import Symbol
17 class Coordinates(ABC
):
29 return tuple(self
._coordinates
)
33 return len(self
.symbols
)
35 def coordinates(self
):
36 yield from self
._coordinates
.items()
38 def coordinate(self
, symbol
):
39 if not isinstance(symbol
, Symbol
):
40 raise TypeError('symbol must be a Symbol instance')
41 return self
._coordinates
[symbol
]
43 __getitem__
= coordinate
46 return any(self
._coordinates
.values())
49 return hash(tuple(self
.coordinates()))
52 string
= ', '.join(['{!r}: {!r}'.format(symbol
, coordinate
)
53 for symbol
, coordinate
in self
.coordinates()])
54 return '{}({{{}}})'.format(self
.__class
__.__name
__, string
)
57 for symbol
, coordinate
in self
.coordinates():
58 yield symbol
, func(coordinate
)
60 def _iter2(self
, other
):
61 if self
.symbols
!= other
.symbols
:
62 raise ValueError('arguments must belong to the same space')
63 coordinates1
= self
._coordinates
.values()
64 coordinates2
= other
._coordinates
.values()
65 yield from zip(self
.symbols
, coordinates1
, coordinates2
)
67 def _map2(self
, other
, func
):
68 for symbol
, coordinate1
, coordinate2
in self
._iter
2(other
):
69 yield symbol
, func(coordinate1
, coordinate2
)
72 class Point(Coordinates
):
74 This class represents points in space.
77 def __new__(cls
, coordinates
=None):
78 if isinstance(coordinates
, dict):
79 coordinates
= coordinates
.items()
80 self
= object().__new
__(cls
)
81 self
._coordinates
= OrderedDict()
82 for symbol
, coordinate
in sorted(coordinates
,
83 key
=lambda item
: item
[0].sortkey()):
84 if not isinstance(symbol
, Symbol
):
85 raise TypeError('symbols must be Symbol instances')
86 if not isinstance(coordinate
, numbers
.Real
):
87 raise TypeError('coordinates must be real numbers')
88 self
._coordinates
[symbol
] = coordinate
94 def __add__(self
, other
):
95 if not isinstance(other
, Vector
):
97 coordinates
= self
._map
2(other
, operator
.add
)
98 return Point(coordinates
)
100 def __sub__(self
, other
):
102 if isinstance(other
, Point
):
103 coordinates
= self
._map
2(other
, operator
.sub
)
104 return Vector(coordinates
)
105 elif isinstance(other
, Vector
):
106 coordinates
= self
._map
2(other
, operator
.sub
)
107 return Point(coordinates
)
109 return NotImplemented
111 def __eq__(self
, other
):
112 return isinstance(other
, Point
) and \
113 self
._coordinates
== other
._coordinates
116 class Vector(Coordinates
):
118 This class represents displacements in space.
125 def __new__(cls
, initial
, terminal
=None):
126 self
= object().__new
__(cls
)
127 if not isinstance(initial
, Point
):
128 initial
= Point(initial
)
130 self
._coordinates
= initial
._coordinates
131 elif not isinstance(terminal
, Point
):
132 terminal
= Point(terminal
)
133 self
._coordinates
= terminal
._map
2(initial
, operator
.sub
)
138 return tuple(self
._coordinates
)
142 return len(self
.symbols
)
144 def coordinates(self
):
145 yield from self
._coordinates
.items()
147 def coordinate(self
, symbol
):
148 if not isinstance(symbol
, Symbol
):
149 raise TypeError('symbol must be a Symbol instance')
150 return self
._coordinates
[symbol
]
152 __getitem__
= coordinate
155 return not bool(self
)
158 return any(self
._coordinates
.values())
160 def __add__(self
, other
):
161 if isinstance(other
, (Point
, Vector
)):
162 coordinates
= self
._map
2(other
, operator
.add
)
163 return other
.__class
__(coordinates
)
164 return NotImplemented
166 def angle(self
, other
):
168 Retrieve the angle required to rotate the vector into the vector passed
169 in argument. The result is an angle in radians, ranging between -pi and
172 if not isinstance(other
, Vector
):
173 raise TypeError('argument must be a Vector instance')
174 cosinus
= self
.dot(other
) / (self
.norm()*other
.norm())
175 return math
.acos(cosinus
)
177 def cross(self
, other
):
179 Calculate the cross product of two Vector3D structures.
181 if not isinstance(other
, Vector
):
182 raise TypeError('other must be a Vector instance')
183 if self
.dimension
!= 3 or other
.dimension
!= 3:
184 raise ValueError('arguments must be three-dimensional vectors')
185 if self
.symbols
!= other
.symbols
:
186 raise ValueError('arguments must belong to the same space')
187 x
, y
, z
= self
.symbols
189 coordinates
.append((x
, self
[y
]*other
[z
] - self
[z
]*other
[y
]))
190 coordinates
.append((y
, self
[z
]*other
[x
] - self
[x
]*other
[z
]))
191 coordinates
.append((z
, self
[x
]*other
[y
] - self
[y
]*other
[x
]))
192 return Vector(coordinates
)
194 def __truediv__(self
, other
):
196 Divide the vector by the specified scalar and returns the result as a
199 if not isinstance(other
, numbers
.Real
):
200 return NotImplemented
201 coordinates
= self
._map
(lambda coordinate
: coordinate
/ other
)
202 return Vector(coordinates
)
204 def dot(self
, other
):
206 Calculate the dot product of two vectors.
208 if not isinstance(other
, Vector
):
209 raise TypeError('argument must be a Vector instance')
211 for symbol
, coordinate1
, coordinate2
in self
._iter
2(other
):
212 result
+= coordinate1
* coordinate2
215 def __eq__(self
, other
):
216 return isinstance(other
, Vector
) and \
217 self
._coordinates
== other
._coordinates
220 return hash(tuple(self
.coordinates()))
222 def __mul__(self
, other
):
223 if not isinstance(other
, numbers
.Real
):
224 return NotImplemented
225 coordinates
= self
._map
(lambda coordinate
: other
* coordinate
)
226 return Vector(coordinates
)
231 coordinates
= self
._map
(operator
.neg
)
232 return Vector(coordinates
)
235 return math
.sqrt(self
.norm2())
239 for coordinate
in self
._coordinates
.values():
240 result
+= coordinate
** 2
244 return self
/ self
.norm()
246 def __sub__(self
, other
):
247 if isinstance(other
, (Point
, Vector
)):
248 coordinates
= self
._map
2(other
, operator
.sub
)
249 return other
.__class
__(coordinates
)
250 return NotImplemented
253 string
= ', '.join(['{!r}: {!r}'.format(symbol
, coordinate
)
254 for symbol
, coordinate
in self
.coordinates()])
255 return '{}({{{}}})'.format(self
.__class
__.__name
__, string
)