X-Git-Url: https://scm.cri.ensmp.fr/git/linpy.git/blobdiff_plain/7b93cea1daf2889e9ee10ca9c22a1b5124404937..cc1d83eaadffc1d5de296e2ec2b401d04de70c41:/linpy/geometry.py diff --git a/linpy/geometry.py b/linpy/geometry.py index 335a826..80f7771 100644 --- a/linpy/geometry.py +++ b/linpy/geometry.py @@ -19,8 +19,8 @@ import math import numbers import operator -from abc import ABC, abstractproperty, abstractmethod -from collections import OrderedDict, Mapping +from abc import ABC, abstractmethod, abstractproperty +from collections import Mapping, OrderedDict from .linexprs import Symbol @@ -33,73 +33,129 @@ __all__ = [ class GeometricObject(ABC): + """ + GeometricObject is an abstract class to represent objects with a + geometric representation in space. Subclasses of GeometricObject are + Polyhedron, Domain and Point. + """ @abstractproperty def symbols(self): + """ + The tuple of symbols present in the object expression, sorted according + to Symbol.sortkey(). + """ pass @property def dimension(self): + """ + The dimension of the object, i.e. the number of symbols present in it. + """ return len(self.symbols) @abstractmethod def aspolyhedron(self): + """ + Return a Polyhedron object that approximates the geometric object. + """ pass def asdomain(self): + """ + Return a Domain object that approximates the geometric object. + """ return self.aspolyhedron() class Coordinates: + """ + This class represents coordinate systems. + """ __slots__ = ( '_coordinates', ) def __new__(cls, coordinates): + """ + Create a coordinate system from a dictionary or a sequence that maps + the symbols to their coordinates. Coordinates must be rational numbers. + """ if isinstance(coordinates, Mapping): coordinates = coordinates.items() self = object().__new__(cls) - self._coordinates = OrderedDict() - for symbol, coordinate in sorted(coordinates, - key=lambda item: item[0].sortkey()): + self._coordinates = [] + for symbol, coordinate in coordinates: if not isinstance(symbol, Symbol): raise TypeError('symbols must be Symbol instances') if not isinstance(coordinate, numbers.Real): raise TypeError('coordinates must be real numbers') - self._coordinates[symbol] = coordinate + self._coordinates.append((symbol, coordinate)) + self._coordinates.sort(key=lambda item: item[0].sortkey()) + self._coordinates = OrderedDict(self._coordinates) return self @property def symbols(self): + """ + The tuple of symbols present in the coordinate system, sorted according + to Symbol.sortkey(). + """ return tuple(self._coordinates) @property def dimension(self): + """ + The dimension of the coordinate system, i.e. the number of symbols + present in it. + """ return len(self.symbols) - def coordinates(self): - yield from self._coordinates.items() - def coordinate(self, symbol): + """ + Return the coordinate value of the given symbol. Raise KeyError if the + symbol is not involved in the coordinate system. + """ if not isinstance(symbol, Symbol): raise TypeError('symbol must be a Symbol instance') return self._coordinates[symbol] __getitem__ = coordinate + def coordinates(self): + """ + Iterate over the pairs (symbol, value) of coordinates in the coordinate + system. + """ + yield from self._coordinates.items() + def values(self): + """ + Iterate over the coordinate values in the coordinate system. + """ yield from self._coordinates.values() def __bool__(self): + """ + Return True if not all coordinates are 0. + """ return any(self._coordinates.values()) + def __eq__(self, other): + """ + Return True if two coordinate systems are equal. + """ + if isinstance(other, self.__class__): + return self._coordinates == other._coordinates + return NotImplemented + def __hash__(self): return hash(tuple(self.coordinates())) def __repr__(self): string = ', '.join(['{!r}: {!r}'.format(symbol, coordinate) - for symbol, coordinate in self.coordinates()]) + for symbol, coordinate in self.coordinates()]) return '{}({{{}}})'.format(self.__class__.__name__, string) def _map(self, func): @@ -121,11 +177,13 @@ class Coordinates: class Point(Coordinates, GeometricObject): """ This class represents points in space. + + Point instances are hashable and should be treated as immutable. """ def isorigin(self): """ - Return True if a Point is the origin. + Return True if all coordinates are 0. """ return not bool(self) @@ -134,16 +192,18 @@ class Point(Coordinates, GeometricObject): def __add__(self, other): """ - Adds a Point to a Vector and returns the result as a Point. + Translate the point by a Vector object and return the resulting point. """ - if not isinstance(other, Vector): - return NotImplemented - coordinates = self._map2(other, operator.add) - return Point(coordinates) + if isinstance(other, Vector): + coordinates = self._map2(other, operator.add) + return Point(coordinates) + return NotImplemented def __sub__(self, other): """ - Returns the difference between two Points as a Vector. + If other is a point, substract it from self and return the resulting + vector. If other is a vector, translate the point by the opposite + vector and returns the resulting point. """ coordinates = [] if isinstance(other, Point): @@ -152,20 +212,9 @@ class Point(Coordinates, GeometricObject): elif isinstance(other, Vector): coordinates = self._map2(other, operator.sub) return Point(coordinates) - else: - return NotImplemented - - def __eq__(self, other): - """ - Compares two Points for equality. - """ - return isinstance(other, Point) and \ - self._coordinates == other._coordinates + return NotImplemented def aspolyhedron(self): - """ - Return a Point as a polyhedron. - """ from .polyhedra import Polyhedron equalities = [] for symbol, coordinate in self.coordinates(): @@ -175,10 +224,16 @@ class Point(Coordinates, GeometricObject): class Vector(Coordinates): """ - This class represents displacements in space. + This class represents vectors in space. + + Vector instances are hashable and should be treated as immutable. """ def __new__(cls, initial, terminal=None): + """ + Create a vector from a dictionary or a sequence that maps the symbols + to their coordinates, or as the displacement between two points. + """ if not isinstance(initial, Point): initial = Point(initial) if terminal is None: @@ -191,7 +246,7 @@ class Vector(Coordinates): def isnull(self): """ - Returns true if a Vector is null. + Return True if all coordinates are 0. """ return not bool(self) @@ -200,13 +255,52 @@ class Vector(Coordinates): def __add__(self, other): """ - Adds either a Point or Vector to a Vector. + If other is a point, translate it with the vector self and return the + resulting point. If other is a vector, return the vector self + other. """ if isinstance(other, (Point, Vector)): coordinates = self._map2(other, operator.add) return other.__class__(coordinates) return NotImplemented + def __sub__(self, other): + """ + If other is a point, substract it from the vector self and return the + resulting point. If other is a vector, return the vector self - other. + """ + if isinstance(other, (Point, Vector)): + coordinates = self._map2(other, operator.sub) + return other.__class__(coordinates) + return NotImplemented + + def __neg__(self): + """ + Return the vector -self. + """ + coordinates = self._map(operator.neg) + return Vector(coordinates) + + def __mul__(self, other): + """ + Multiplies a Vector by a scalar value. + """ + if isinstance(other, numbers.Real): + coordinates = self._map(lambda coordinate: other * coordinate) + return Vector(coordinates) + return NotImplemented + + __rmul__ = __mul__ + + def __truediv__(self, other): + """ + Divide the vector by the specified scalar and returns the result as a + vector. + """ + if isinstance(other, numbers.Real): + coordinates = self._map(lambda coordinate: coordinate / other) + return Vector(coordinates) + return NotImplemented + def angle(self, other): """ Retrieve the angle required to rotate the vector into the vector passed @@ -220,7 +314,8 @@ class Vector(Coordinates): def cross(self, other): """ - Calculate the cross product of two Vector3D structures. + Compute the cross product of two 3D vectors. If either one of the + vectors is not three-dimensional, a ValueError exception is raised. """ if not isinstance(other, Vector): raise TypeError('other must be a Vector instance') @@ -235,19 +330,9 @@ class Vector(Coordinates): coordinates.append((z, self[x]*other[y] - self[y]*other[x])) return Vector(coordinates) - def __truediv__(self, other): - """ - Divide the vector by the specified scalar and returns the result as a - vector. - """ - if not isinstance(other, numbers.Real): - return NotImplemented - coordinates = self._map(lambda coordinate: coordinate / other) - return Vector(coordinates) - def dot(self, other): """ - Calculate the dot product of two vectors. + Compute the dot product of two vectors. """ if not isinstance(other, Vector): raise TypeError('argument must be a Vector instance') @@ -256,54 +341,24 @@ class Vector(Coordinates): result += coordinate1 * coordinate2 return result - def __eq__(self, other): - """ - Compares two Vectors for equality. - """ - return isinstance(other, Vector) and \ - self._coordinates == other._coordinates - - def __hash__(self): - return hash(tuple(self.coordinates())) - - def __mul__(self, other): - """ - Multiplies a Vector by a scalar value. - """ - if not isinstance(other, numbers.Real): - return NotImplemented - coordinates = self._map(lambda coordinate: other * coordinate) - return Vector(coordinates) - - __rmul__ = __mul__ - - def __neg__(self): - """ - Returns the negated form of a Vector. - """ - coordinates = self._map(operator.neg) - return Vector(coordinates) - def norm(self): """ - Normalizes a Vector. + Return the norm of the vector. """ return math.sqrt(self.norm2()) def norm2(self): + """ + Return the squared norm of the vector. + """ result = 0 for coordinate in self._coordinates.values(): result += coordinate ** 2 return result def asunit(self): - return self / self.norm() - - def __sub__(self, other): """ - Subtract a Point or Vector from a Vector. + Return the normalized vector, i.e. the vector of same direction but + with norm 1. """ - if isinstance(other, (Point, Vector)): - coordinates = self._map2(other, operator.sub) - return other.__class__(coordinates) - return NotImplemented + return self / self.norm()