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
= []
89 for symbol
, coordinate
in coordinates
:
90 if not isinstance(symbol
, Symbol
):
91 raise TypeError('symbols must be Symbol instances')
92 if not isinstance(coordinate
, numbers
.Real
):
93 raise TypeError('coordinates must be real numbers')
94 self
._coordinates
.append((symbol
, coordinate
))
95 self
._coordinates
.sort(key
=lambda item
: item
[0].sortkey())
96 self
._coordinates
= OrderedDict(self
._coordinates
)
102 The tuple of symbols present in the coordinate system, sorted according
105 return tuple(self
._coordinates
)
110 The dimension of the coordinate system, i.e. the number of symbols
113 return len(self
.symbols
)
115 def coordinate(self
, symbol
):
117 Return the coordinate value of the given symbol. Raise KeyError if the
118 symbol is not involved in the coordinate system.
120 if not isinstance(symbol
, Symbol
):
121 raise TypeError('symbol must be a Symbol instance')
122 return self
._coordinates
[symbol
]
124 __getitem__
= coordinate
126 def coordinates(self
):
128 Iterate over the pairs (symbol, value) of coordinates in the coordinate
131 yield from self
._coordinates
.items()
135 Iterate over the coordinate values in the coordinate system.
137 yield from self
._coordinates
.values()
141 Return True if not all coordinates are 0.
143 return any(self
._coordinates
.values())
145 def __eq__(self
, other
):
147 Return True if two coordinate systems are equal.
149 if isinstance(other
, self
.__class
__):
150 return self
._coordinates
== other
._coordinates
151 return NotImplemented
154 return hash(tuple(self
.coordinates()))
157 string
= ', '.join(['{!r}: {!r}'.format(symbol
, coordinate
)
158 for symbol
, coordinate
in self
.coordinates()])
159 return '{}({{{}}})'.format(self
.__class
__.__name
__, string
)
161 def _map(self
, func
):
162 for symbol
, coordinate
in self
.coordinates():
163 yield symbol
, func(coordinate
)
165 def _iter2(self
, other
):
166 if self
.symbols
!= other
.symbols
:
167 raise ValueError('arguments must belong to the same space')
168 coordinates1
= self
._coordinates
.values()
169 coordinates2
= other
._coordinates
.values()
170 yield from zip(self
.symbols
, coordinates1
, coordinates2
)
172 def _map2(self
, other
, func
):
173 for symbol
, coordinate1
, coordinate2
in self
._iter
2(other
):
174 yield symbol
, func(coordinate1
, coordinate2
)
177 class Point(Coordinates
, GeometricObject
):
179 This class represents points in space.
181 Point instances are hashable and should be treated as immutable.
186 Return True if all coordinates are 0.
188 return not bool(self
)
191 return super().__hash
__()
193 def __add__(self
, other
):
195 Translate the point by a Vector object and return the resulting point.
197 if isinstance(other
, Vector
):
198 coordinates
= self
._map
2(other
, operator
.add
)
199 return Point(coordinates
)
200 return NotImplemented
202 def __sub__(self
, other
):
204 If other is a point, substract it from self and return the resulting
205 vector. If other is a vector, translate the point by the opposite vector
206 and returns the resulting point.
209 if isinstance(other
, Point
):
210 coordinates
= self
._map
2(other
, operator
.sub
)
211 return Vector(coordinates
)
212 elif isinstance(other
, Vector
):
213 coordinates
= self
._map
2(other
, operator
.sub
)
214 return Point(coordinates
)
215 return NotImplemented
217 def aspolyhedron(self
):
218 from .polyhedra
import Polyhedron
220 for symbol
, coordinate
in self
.coordinates():
221 equalities
.append(symbol
- coordinate
)
222 return Polyhedron(equalities
)
225 class Vector(Coordinates
):
227 This class represents vectors in space.
229 Vector instances are hashable and should be treated as immutable.
232 def __new__(cls
, initial
, terminal
=None):
234 Create a vector from a dictionary or a sequence that maps the symbols to
235 their coordinates, or as the displacement between two points.
237 if not isinstance(initial
, Point
):
238 initial
= Point(initial
)
240 coordinates
= initial
._coordinates
242 if not isinstance(terminal
, Point
):
243 terminal
= Point(terminal
)
244 coordinates
= terminal
._map
2(initial
, operator
.sub
)
245 return super().__new
__(cls
, coordinates
)
249 Return True if all coordinates are 0.
251 return not bool(self
)
254 return super().__hash
__()
256 def __add__(self
, other
):
258 If other is a point, translate it with the vector self and return the
259 resulting point. If other is a vector, return the vector self + other.
261 if isinstance(other
, (Point
, Vector
)):
262 coordinates
= self
._map
2(other
, operator
.add
)
263 return other
.__class
__(coordinates
)
264 return NotImplemented
266 def __sub__(self
, other
):
268 If other is a point, substract it from the vector self and return the
269 resulting point. If other is a vector, return the vector self - other.
271 if isinstance(other
, (Point
, Vector
)):
272 coordinates
= self
._map
2(other
, operator
.sub
)
273 return other
.__class
__(coordinates
)
274 return NotImplemented
278 Return the vector -self.
280 coordinates
= self
._map
(operator
.neg
)
281 return Vector(coordinates
)
283 def __mul__(self
, other
):
285 Multiplies a Vector by a scalar value.
287 if isinstance(other
, numbers
.Real
):
288 coordinates
= self
._map
(lambda coordinate
: other
* coordinate
)
289 return Vector(coordinates
)
290 return NotImplemented
294 def __truediv__(self
, other
):
296 Divide the vector by the specified scalar and returns the result as a
299 if isinstance(other
, numbers
.Real
):
300 coordinates
= self
._map
(lambda coordinate
: coordinate
/ other
)
301 return Vector(coordinates
)
302 return NotImplemented
304 def angle(self
, other
):
306 Retrieve the angle required to rotate the vector into the vector passed
307 in argument. The result is an angle in radians, ranging between -pi and
310 if not isinstance(other
, Vector
):
311 raise TypeError('argument must be a Vector instance')
312 cosinus
= self
.dot(other
) / (self
.norm()*other
.norm())
313 return math
.acos(cosinus
)
315 def cross(self
, other
):
317 Compute the cross product of two 3D vectors. If either one of the
318 vectors is not three-dimensional, a ValueError exception is raised.
320 if not isinstance(other
, Vector
):
321 raise TypeError('other must be a Vector instance')
322 if self
.dimension
!= 3 or other
.dimension
!= 3:
323 raise ValueError('arguments must be three-dimensional vectors')
324 if self
.symbols
!= other
.symbols
:
325 raise ValueError('arguments must belong to the same space')
326 x
, y
, z
= self
.symbols
328 coordinates
.append((x
, self
[y
]*other
[z
] - self
[z
]*other
[y
]))
329 coordinates
.append((y
, self
[z
]*other
[x
] - self
[x
]*other
[z
]))
330 coordinates
.append((z
, self
[x
]*other
[y
] - self
[y
]*other
[x
]))
331 return Vector(coordinates
)
333 def dot(self
, other
):
335 Compute the dot product of two vectors.
337 if not isinstance(other
, Vector
):
338 raise TypeError('argument must be a Vector instance')
340 for symbol
, coordinate1
, coordinate2
in self
._iter
2(other
):
341 result
+= coordinate1
* coordinate2
345 return super().__hash
__()
349 Return the norm of the vector.
351 return math
.sqrt(self
.norm2())
355 Return the squared norm of the vector.
358 for coordinate
in self
._coordinates
.values():
359 result
+= coordinate
** 2
364 Return the normalized vector, i.e. the vector of same direction but with
367 return self
/ self
.norm()