1 # Copyright 2014 MINES ParisTech
3 # This file is part of LinPy.
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.
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.
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/>.
22 from abc
import ABC
, abstractproperty
, abstractmethod
23 from collections
import OrderedDict
, Mapping
25 from .linexprs
import Symbol
35 class GeometricObject(ABC
):
37 GeometricObject is an abstract class to represent objects with a
38 geometric representation in space. Subclasses of GeometricObject are
39 Polyhedron, Domain and Point.
45 The tuple of symbols present in the object expression, sorted according
53 The dimension of the object, i.e. the number of symbols present in it.
55 return len(self
.symbols
)
58 def aspolyhedron(self
):
60 Return a Polyhedron object that approximates the geometric object.
66 Return a Domain object that approximates the geometric object.
68 return self
.aspolyhedron()
73 This class represents coordinate systems.
80 def __new__(cls
, coordinates
):
82 Create a coordinate system from a dictionary or a sequence that maps the
83 symbols to their coordinates. Coordinates must be rational numbers.
85 if isinstance(coordinates
, Mapping
):
86 coordinates
= coordinates
.items()
87 self
= object().__new
__(cls
)
88 self
._coordinates
= OrderedDict()
89 for symbol
, coordinate
in sorted(coordinates
,
90 key
=lambda item
: item
[0].sortkey()):
91 if not isinstance(symbol
, Symbol
):
92 raise TypeError('symbols must be Symbol instances')
93 if not isinstance(coordinate
, numbers
.Real
):
94 raise TypeError('coordinates must be real numbers')
95 self
._coordinates
[symbol
] = coordinate
101 The tuple of symbols present in the coordinate system, sorted according
104 return tuple(self
._coordinates
)
109 The dimension of the coordinate system, i.e. the number of symbols
112 return len(self
.symbols
)
114 def coordinate(self
, symbol
):
116 Return the coordinate value of the given symbol. Raise KeyError if the
117 symbol is not involved in the coordinate system.
119 if not isinstance(symbol
, Symbol
):
120 raise TypeError('symbol must be a Symbol instance')
121 return self
._coordinates
[symbol
]
123 __getitem__
= coordinate
125 def coordinates(self
):
127 Iterate over the pairs (symbol, value) of coordinates in the coordinate
130 yield from self
._coordinates
.items()
134 Iterate over the coordinate values in the coordinate system.
136 yield from self
._coordinates
.values()
140 Return True if not all coordinates are 0.
142 return any(self
._coordinates
.values())
145 return hash(tuple(self
.coordinates()))
148 string
= ', '.join(['{!r}: {!r}'.format(symbol
, coordinate
)
149 for symbol
, coordinate
in self
.coordinates()])
150 return '{}({{{}}})'.format(self
.__class
__.__name
__, string
)
152 def _map(self
, func
):
153 for symbol
, coordinate
in self
.coordinates():
154 yield symbol
, func(coordinate
)
156 def _iter2(self
, other
):
157 if self
.symbols
!= other
.symbols
:
158 raise ValueError('arguments must belong to the same space')
159 coordinates1
= self
._coordinates
.values()
160 coordinates2
= other
._coordinates
.values()
161 yield from zip(self
.symbols
, coordinates1
, coordinates2
)
163 def _map2(self
, other
, func
):
164 for symbol
, coordinate1
, coordinate2
in self
._iter
2(other
):
165 yield symbol
, func(coordinate1
, coordinate2
)
168 class Point(Coordinates
, GeometricObject
):
170 This class represents points in space.
172 Point instances are hashable and should be treated as immutable.
177 Return True if all coordinates are 0.
179 return not bool(self
)
182 return super().__hash
__()
184 def __add__(self
, other
):
186 Translate the point by a Vector object and return the resulting point.
188 if not isinstance(other
, Vector
):
189 return NotImplemented
190 coordinates
= self
._map
2(other
, operator
.add
)
191 return Point(coordinates
)
193 def __sub__(self
, other
):
195 If other is a point, substract a point from another and returns the
196 resulting vector. If other is a vector, translate the point by the
197 opposite vector and returns the resulting point.
200 if isinstance(other
, Point
):
201 coordinates
= self
._map
2(other
, operator
.sub
)
202 return Vector(coordinates
)
203 elif isinstance(other
, Vector
):
204 coordinates
= self
._map
2(other
, operator
.sub
)
205 return Point(coordinates
)
207 return NotImplemented
209 def __eq__(self
, other
):
211 Test whether two points are equal.
213 return isinstance(other
, Point
) and \
214 self
._coordinates
== other
._coordinates
216 def aspolyhedron(self
):
217 from .polyhedra
import Polyhedron
219 for symbol
, coordinate
in self
.coordinates():
220 equalities
.append(symbol
- coordinate
)
221 return Polyhedron(equalities
)
224 class Vector(Coordinates
):
226 This class represents vectors in space.
228 Vector instances are hashable and should be treated as immutable.
231 def __new__(cls
, initial
, terminal
=None):
233 Create a vector from a dictionary or a sequence that maps the symbols to
234 their coordinates, or as the difference between two points.
236 if not isinstance(initial
, Point
):
237 initial
= Point(initial
)
239 coordinates
= initial
._coordinates
241 if not isinstance(terminal
, Point
):
242 terminal
= Point(terminal
)
243 coordinates
= terminal
._map
2(initial
, operator
.sub
)
244 return super().__new
__(cls
, coordinates
)
248 Return True if all coordinates are 0.
250 return not bool(self
)
253 return super().__hash
__()
255 def __add__(self
, other
):
257 If other is a point, translate it with the vector self and return the
258 resulting point. If other is a vector, return the vector self + other.
260 if isinstance(other
, (Point
, Vector
)):
261 coordinates
= self
._map
2(other
, operator
.add
)
262 return other
.__class
__(coordinates
)
263 return NotImplemented
265 def __sub__(self
, other
):
267 If other is a point, substract it from the vector self and return the
268 resulting point. If other is a vector, return the vector self - other.
270 if isinstance(other
, (Point
, Vector
)):
271 coordinates
= self
._map
2(other
, operator
.sub
)
272 return other
.__class
__(coordinates
)
273 return NotImplemented
277 Return the vector -self.
279 coordinates
= self
._map
(operator
.neg
)
280 return Vector(coordinates
)
282 def __mul__(self
, other
):
284 Multiplies a Vector by a scalar value.
286 if not isinstance(other
, numbers
.Real
):
287 return NotImplemented
288 coordinates
= self
._map
(lambda coordinate
: other
* coordinate
)
289 return Vector(coordinates
)
293 def __truediv__(self
, other
):
295 Divide the vector by the specified scalar and returns the result as a
298 if not isinstance(other
, numbers
.Real
):
299 return NotImplemented
300 coordinates
= self
._map
(lambda coordinate
: coordinate
/ other
)
301 return Vector(coordinates
)
303 def __eq__(self
, other
):
305 Test whether two vectors are equal.
307 return isinstance(other
, Vector
) and \
308 self
._coordinates
== other
._coordinates
310 def angle(self
, other
):
312 Retrieve the angle required to rotate the vector into the vector passed
313 in argument. The result is an angle in radians, ranging between -pi and
316 if not isinstance(other
, Vector
):
317 raise TypeError('argument must be a Vector instance')
318 cosinus
= self
.dot(other
) / (self
.norm()*other
.norm())
319 return math
.acos(cosinus
)
321 def cross(self
, other
):
323 Compute the cross product of two 3D vectors. If either one of the
324 vectors is not tridimensional, a ValueError exception is raised.
326 if not isinstance(other
, Vector
):
327 raise TypeError('other must be a Vector instance')
328 if self
.dimension
!= 3 or other
.dimension
!= 3:
329 raise ValueError('arguments must be three-dimensional vectors')
330 if self
.symbols
!= other
.symbols
:
331 raise ValueError('arguments must belong to the same space')
332 x
, y
, z
= self
.symbols
334 coordinates
.append((x
, self
[y
]*other
[z
] - self
[z
]*other
[y
]))
335 coordinates
.append((y
, self
[z
]*other
[x
] - self
[x
]*other
[z
]))
336 coordinates
.append((z
, self
[x
]*other
[y
] - self
[y
]*other
[x
]))
337 return Vector(coordinates
)
339 def dot(self
, other
):
341 Compute the dot product of two vectors.
343 if not isinstance(other
, Vector
):
344 raise TypeError('argument must be a Vector instance')
346 for symbol
, coordinate1
, coordinate2
in self
._iter
2(other
):
347 result
+= coordinate1
* coordinate2
351 return hash(tuple(self
.coordinates()))
355 Return the norm of the vector.
357 return math
.sqrt(self
.norm2())
361 Return the squared norm of the vector.
364 for coordinate
in self
._coordinates
.values():
365 result
+= coordinate
** 2
370 Return the normalized vector, i.e. the vector of same direction but with
373 return self
/ self
.norm()