import math import numbers import operator from abc import ABC, abstractproperty, abstractmethod from collections import OrderedDict, Mapping from .linexprs import Symbol __all__ = [ 'GeometricObject', 'Point', 'Vector', ] class GeometricObject(ABC): @abstractproperty def symbols(self): pass @property def dimension(self): return len(self.symbols) @abstractmethod def aspolyhedron(self): pass def asdomain(self): return self.aspolyhedron() class Coordinates: __slots__ = ( '_coordinates', ) def __new__(cls, coordinates): 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()): 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 return self @property def symbols(self): return tuple(self._coordinates) @property def dimension(self): return len(self.symbols) def coordinates(self): yield from self._coordinates.items() def coordinate(self, symbol): if not isinstance(symbol, Symbol): raise TypeError('symbol must be a Symbol instance') return self._coordinates[symbol] __getitem__ = coordinate def values(self): yield from self._coordinates.values() def __bool__(self): return any(self._coordinates.values()) def __hash__(self): return hash(tuple(self.coordinates())) def __repr__(self): string = ', '.join(['{!r}: {!r}'.format(symbol, coordinate) for symbol, coordinate in self.coordinates()]) return '{}({{{}}})'.format(self.__class__.__name__, string) def _map(self, func): for symbol, coordinate in self.coordinates(): yield symbol, func(coordinate) def _iter2(self, other): if self.symbols != other.symbols: raise ValueError('arguments must belong to the same space') coordinates1 = self._coordinates.values() coordinates2 = other._coordinates.values() yield from zip(self.symbols, coordinates1, coordinates2) def _map2(self, other, func): for symbol, coordinate1, coordinate2 in self._iter2(other): yield symbol, func(coordinate1, coordinate2) class Point(Coordinates, GeometricObject): """ This class represents points in space. """ def isorigin(self): """ Return True if a Point is the origin. """ return not bool(self) def __hash__(self): return super().__hash__() def __add__(self, other): if not isinstance(other, Vector): return NotImplemented coordinates = self._map2(other, operator.add) return Point(coordinates) def __sub__(self, other): coordinates = [] if isinstance(other, Point): coordinates = self._map2(other, operator.sub) return Vector(coordinates) 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 def aspolyhedron(self): """ Return a Point as a polyhedron. """ from .polyhedra import Polyhedron equalities = [] for symbol, coordinate in self.coordinates(): equalities.append(symbol - coordinate) return Polyhedron(equalities) class Vector(Coordinates): """ This class represents displacements in space. """ def __new__(cls, initial, terminal=None): if not isinstance(initial, Point): initial = Point(initial) if terminal is None: coordinates = initial._coordinates else: if not isinstance(terminal, Point): terminal = Point(terminal) coordinates = terminal._map2(initial, operator.sub) return super().__new__(cls, coordinates) def isnull(self): """ Returns true if a Vector is null. """ return not bool(self) def __hash__(self): return super().__hash__() def __add__(self, other): """ Adds either a Point or Vector to a Vector. """ if isinstance(other, (Point, Vector)): coordinates = self._map2(other, operator.add) return other.__class__(coordinates) return NotImplemented def angle(self, other): """ Retrieve the angle required to rotate the vector into the vector passed in argument. The result is an angle in radians, ranging between -pi and pi. """ if not isinstance(other, Vector): raise TypeError('argument must be a Vector instance') cosinus = self.dot(other) / (self.norm()*other.norm()) return math.acos(cosinus) def cross(self, other): """ Calculate the cross product of two Vector3D structures. """ if not isinstance(other, Vector): raise TypeError('other must be a Vector instance') if self.dimension != 3 or other.dimension != 3: raise ValueError('arguments must be three-dimensional vectors') if self.symbols != other.symbols: raise ValueError('arguments must belong to the same space') x, y, z = self.symbols coordinates = [] coordinates.append((x, self[y]*other[z] - self[z]*other[y])) coordinates.append((y, self[z]*other[x] - self[x]*other[z])) 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. """ if not isinstance(other, Vector): raise TypeError('argument must be a Vector instance') result = 0 for symbol, coordinate1, coordinate2 in self._iter2(other): 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 math.sqrt(self.norm2()) def norm2(self): 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. """ if isinstance(other, (Point, Vector)): coordinates = self._map2(other, operator.sub) return other.__class__(coordinates) return NotImplemented