def create_class_Tolerant_Cartesian_2D_Vector(name, component_names, *, brackets='<>', sep=', ', cnull=0, cunit=1, functions=None, abs_tol=1e-12, rel_tol=1e-9): """ Function that creates a tolerant cartesian vector class with 2 dimensions The number of dimensions are determined by the number of component names """ hf.verify_class_name(name) if functions is None: functions = { } C2DV = \ create_class_Cartesian_2D_Vector( name = 'C2DV_' + name, component_names = component_names, brackets = brackets, sep = sep, cnull = cnull, cunit = cunit, functions = functions ) TC2DV = \ make_Cartesian_Vector_Tolerant( cartesian_vector_class = C2DV, name = name, functions = functions, abs_tol = abs_tol, rel_tol = rel_tol ) @hf.ensure_other_is_vector def are_parallel(self, other): """Check if two vectors are parallel (within a calculated tolerance)""" try: vsn = self.normalize() except ZeroDivisionError: vsn = None try: von = other.normalize() except ZeroDivisionError: von = None if (vsn is None) or (von is None): parallel = self._true else: perp_dot = vsn.perp_dot(von) parallel = self._equal_cnull(perp_dot) parallel = self.component_or(parallel, self.is_zero_vector()) parallel = self.component_or(parallel, other.is_zero_vector()) return parallel TC2DV.are_parallel = are_parallel return TC2DV
def create_class_Tolerant_Cartesian_Vector(name, component_names, *, brackets='<>', sep=', ', cnull=0, cunit=1, functions=None, abs_tol=1e-12, rel_tol=1e-9): """ Function that creates a tolerant cartesian vector class The number of dimensions are determined by the number of component names """ hf.verify_class_name(name) if functions is None: functions = {} CV = \ create_class_Cartesian_Vector( name = 'CV_' + name, component_names = component_names, brackets = brackets, sep = sep, cnull = cnull, cunit = cunit, functions = functions ) TCV = \ make_Cartesian_Vector_Tolerant( cartesian_vector_class = CV, name = name, functions = functions, abs_tol = abs_tol, rel_tol = rel_tol ) return TCV
def create_class_Simple_Vector(name, component_names, *, brackets='<>', sep=', '): """ Function that creates a simple vector class The number of dimensions are determined by the number of component names """ hf.verify_class_name(name) FV = \ create_class_Fundamental_Vector( name = 'FV_' + name, component_names = component_names, brackets = brackets, sep = sep ) def init_Simple_Vector(cls): """Initialize class""" hf.setup_vector_class(cls=cls, name=name, functions=None) hf.make_dunder_methods(cls, [(1, '', 'abs', operator.abs), (1, '', 'neg', operator.neg), (1, '', 'pos', operator.pos), (2, '', 'add', operator.add), (2, '', 'sub', operator.sub), (2, '', 'mul', operator.mul), (2, '', 'pow', operator.pow), (2, '', 'truediv', operator.truediv), (2, '', 'floordiv', operator.floordiv), (2, '', 'mod', operator.mod), (2, 'r', 'add', operator.add), (2, 'r', 'sub', operator.sub), (2, 'r', 'mul', operator.mul), (2, 'r', 'pow', operator.pow), (2, 'r', 'truediv', operator.truediv), (2, 'r', 'floordiv', operator.floordiv), (2, 'r', 'mod', operator.mod), (2, 'i', 'add', operator.add), (2, 'i', 'sub', operator.sub), (2, 'i', 'mul', operator.mul), (2, 'i', 'pow', operator.pow), (2, 'i', 'truediv', operator.truediv), (2, 'i', 'floordiv', operator.floordiv), (2, 'i', 'mod', operator.mod)]) return cls @init_Simple_Vector class Simple_Vector(FV): """ A simple vector class with {dimensions} dimensions and the component names '{cs_cnames}' """ _component_operators = \ { 'arg1_n': { 'abs': operator.abs, 'neg': operator.neg, 'pos': operator.pos, 'floor': math.floor, 'ceil': math.ceil, 'trunc': math.trunc }, 'arg2_n': { 'round': round, 'add': operator.add, 'sub': operator.sub, 'mul': operator.mul, 'pow': operator.pow, 'truediv': operator.truediv, 'floordiv': operator.floordiv, 'mod': operator.mod, }, 'arg1_o': { }, 'arg2_o': { }, 'arg2_i': { 'iadd': operator.iadd, 'isub': operator.isub, 'imul': operator.imul, 'ipow': operator.ipow, 'itruediv': operator.itruediv, 'ifloordiv': operator.ifloordiv, 'imod': operator.imod } } def _cvalues_present(self, cnames): cnames_present = \ ( cns in cnames for cns in self._cnames ) result = zip(self._cvalues, cnames_present) return result def _apply_operator_arg1_n(self, op, cnames): cvalues_present = self._cvalues_present(cnames) def apply_op_arg1_n(): vector = \ self._vector( op(cvs) if present else cvs for cvs, present in cvalues_present ) return vector return apply_op_arg1_n def _apply_operator_arg2_n(self, op, cnames): cvalues_present = self._cvalues_present(cnames) def apply_op_arg2_n(value): vector = \ self._vector( op(cvs, value) if present else cvs for cvs, present in cvalues_present ) return vector return apply_op_arg2_n def _apply_operator_arg1_o(self, op, cnames): cvalues_present = self._cvalues_present(cnames) def apply_op(cvs, value): cvs = copy(cvs) op(cvs, value) return cvs def apply_op_arg1_o(value): vector = \ self._vector( apply_op(cvs, value) if present else cvs for cvs, present in cvalues_present ) return vector return apply_op_arg1_o def _apply_operator_arg2_o(self, op, cnames): cvalues_present = self._cvalues_present(cnames) def apply_op(cvs, value0, value1): cvs = copy(cvs) op(cvs, value0, value1) return cvs def apply_op_arg2_o(value0, value1): vector = \ self._vector( apply_op(cvs, value0, value1) if present else cvs for cvs, present in cvalues_present ) return vector return apply_op_arg2_o def _apply_operator_arg2_i(self, op, cnames): cvalues_present = self._cvalues_present(cnames) def apply_op_arg2_i(value): self._cvalues = \ [ op(cvs, value) if present else cvs for cvs, present in cvalues_present ] return apply_op_arg2_i def __round__(self, ndigits=0): """Round each component value to a given precision in decimal digits""" vector = \ self._vector( round(cvs, ndigits) for cvs in self._cvalues ) return vector def __floor__(self): """Apply math.floor to each of the vector component values""" vector = self._vector(map(math.floor, self._cvalues)) return vector def __ceil__(self): """Apply math.ceil to each of the vector component values""" vector = self._vector(map(math.ceil, self._cvalues)) return vector def __trunc__(self): """Apply math.trunc to each of the vector component values""" vector = self._vector(map(math.trunc, self._cvalues)) return vector def _decode_attr_name(self, attr_name): if attr_name.startswith('c_'): attr_name = attr_name[2:] method_name, *decoded_cnames = attr_name.split('_') for op_type, operator_names in self._component_operators.items( ): if method_name in operator_names: break else: op_type = None if len(decoded_cnames) == 0: bar = False else: bar = decoded_cnames[0] == 'bar' if bar: del decoded_cnames[0] if verify_names(chk_names=decoded_cnames, ref_names=self._cnames): if bar: decoded_cnames = \ [ cns for cns in self._cnames if cns not in decoded_cnames ] else: decoded_cnames = None else: method_name, op_type, decoded_cnames = None, None, None return method_name, op_type, decoded_cnames def __setattr__(self, attr_name, value): """TODO""" decoded_attr_name = self._decode_attr_name(attr_name) if any(val is None for val in decoded_attr_name): super().__setattr__(attr_name, value) else: cls = type(self) msg = "'{cls.__name__}' object attribute '{attr_name}' is read-only" raise AttributeError(msg.format_map(vars())) def __getattr__(self, attr_name): """TODO""" try: attr = super().__getattribute__(attr_name) except AttributeError: attr = None if attr is None: decoded_attr_name = self._decode_attr_name(attr_name) if any(val is None for val in decoded_attr_name): cls = type(self) msg = "'{cls.__name__}' object has no attribute '{attr_name}'" raise AttributeError(msg.format_map(vars())) else: method_name, op_type, cnames = decoded_attr_name op = self._component_operators[op_type][method_name] methods_apply_op = \ { 'arg1_n': self._apply_operator_arg1_n, 'arg2_n': self._apply_operator_arg2_n, 'arg1_o': self._apply_operator_arg1_o, 'arg2_o': self._apply_operator_arg2_o, 'arg2_i': self._apply_operator_arg2_i } method = methods_apply_op[op_type] attr = method(op, cnames) attr.__name__ = attr_name attr.__doc__ = \ "Applies operator '{method_name}' to these vector components: {component_names}" \ .format(method_name=method_name, component_names=', '.join(cnames)) return attr return Simple_Vector
def create_class_Fundamental_Vector(name, component_names, *, brackets='<>', sep=', '): """ Function that creates a fundamental vector class The number of dimensions are determined by the number of component names """ def verify_arguments(component_names, dimensions, brackets): if len(component_names) != dimensions: msg = "Some of the component names are not unique" raise ValueError(msg) if dimensions == 0: msg = "The number of component names is 0" raise ValueError(msg) for cname in component_names: if not isinstance(cname, str): msg = "One (or more) of the component names is not a string" raise TypeError(msg) if not check_identifier(cname): msg = \ "All characters in the component names must be either " \ "a latin letter, a greek letter or a decimal digit, except " \ "for the first character which should not be a digit" raise ValueError(msg) if len(brackets) != 2: msg = "The number of characters (or strings) for the brackets is not 2" raise ValueError(msg) hf.verify_class_name(name) component_names = [*component_names] dimensions = len(set(component_names)) brackets = [str(br) for br in brackets] sep = str(sep) verify_arguments(component_names, dimensions, brackets) def setup_components_access(cls): for index, cname in enumerate(cls._cnames): def cget(self, _index=index): value = copy(self._cvalues[_index]) return value def cset(self, value, _index=index): self._cvalues[_index] = copy(value) cdoc = \ "Value of {cname}-component of vector (component no. {index})" \ .format_map(vars()) setattr(cls, cname, property(fget=cget, fset=cset, doc=cdoc)) def init_Fundamental_Vector(cls): """Initialize class""" cls._dimensions = dimensions cls._cnames = component_names cls.brackets = brackets cls.sep = sep hf.setup_vector_class(cls=cls, name=name, functions=None) setup_components_access(cls) return cls @init_Fundamental_Vector class Fundamental_Vector: """ A fundamental vector class with {dimensions} dimensions and the component names '{cs_cnames}' """ @classmethod def dimensions(cls): """Number of dimensions for vectors in the class""" dim = cls._dimensions return dim @classmethod def component_names(cls): """List of component names for vectors in the class""" cnames = cls._cnames.copy() return cnames @classmethod def is_vector(cls, va): """Check if something is a vector""" result = isinstance(va, cls) return result @classmethod def fill(cls, value): """A vector with all component values set to value""" cvalues = \ ( copy(value) for _ in range(cls._dimensions) ) vector = cls(*cvalues, _internal=True) return vector @classmethod def _ensure_all_are_vectors(cls, vectors): vectors = \ ( v if cls.is_vector(v) else cls.fill(v) for v in vectors ) return vectors def __init__(self, *cvalues, _internal=False, **named_cvalues): """TODO""" if _internal: self._cvalues = [*cvalues] else: self._check_arguments(cvalues, named_cvalues) if len(named_cvalues) > 0: self._cvalues = \ [ copy(named_cvalues[cns]) for cns in self._cnames ] else: self._cvalues = \ [ copy(cv) for cv in cvalues ] def _check_arguments(self, cvalues, named_cvalues): no_of_cvalues = len(cvalues) no_of_named_cvalues = len(named_cvalues) cls = type(self) if (no_of_cvalues == self._dimensions) and (no_of_named_cvalues == 0): pass elif (no_of_cvalues == 0) and (no_of_named_cvalues == self._dimensions): if set(named_cvalues) == set(self._cnames): pass else: msg = "One or more keyword argument to {cls.__name__}() was given with wrong name)" raise ValueError(msg.format_map(vars())) else: if no_of_cvalues == 0: if no_of_named_cvalues == 0: msg = "{cls.__name__}() takes {self._dimensions} argument(s) (0 was given)" else: msg = "{cls.__name__}() takes {self._dimensions} argument(s) ({no_of_named_cvalues} was given)" else: if no_of_named_cvalues == 0: msg = "{cls.__name__}() takes {self._dimensions} argument(s) ({no_of_cvalues} was given)" else: msg = "Arguments to {cls.__name__}() must either be given with or without keywords (both was given)" raise TypeError(msg.format_map(vars())) def component_values(self): """List of a vector's component values""" cvalues = \ [ copy(cvs) for cvs in self._cvalues ] return cvalues def _vector(self, cvalues): cls = type(self) vector = cls(*cvalues, _internal=True) return vector def copy(self): """Copy of vector""" vector = self._vector(map(copy, self._cvalues)) return vector @property def cnames(self): """List of a vector's component names""" component_names = self.component_names() return component_names @property def cvalues(self): """List of a vector's component values""" component_values = self.component_values() return component_values def as_dict(self): """A vector's component names and values in a dictionary""" result = \ dict( zip( self.component_names(), self.component_values() ) ) return result def __len__(self): """Number of dimensions for vector""" dimensions = self._dimensions return dimensions def __str__(self): """Apply str() to each component value""" csv = self.sep.join(map(str, self._cvalues)) string = csv.join(self.brackets) return string def __repr__(self): """Apply repr() to each component value""" csv = \ self.sep.join( cns + '=' + repr(cvs) for cns, cvs in zip(self._cnames, self._cvalues) ) cls = type(self) string = cls.__name__ + '(' + csv + ')' return string def __format__(self, format_spec=''): """Apply format() to each component value""" csv = \ self.sep.join( format(cvs, format_spec) for cvs in self._cvalues ) string = csv.join(self.brackets) return string def __iter__(self): """Iterable for iterating over the vector component values""" yield from map(copy, self._cvalues) def __getitem__(self, index): """Retrive vector component values by indexing""" cvalues = self.component_values() return cvalues[index] def __setitem__(self, index, values): """Change vector component values by indexing""" if isinstance(index, int): cvalues = copy(values) elif isinstance(index, slice): cvalues = \ [ copy(cv) for cv in values ] indices = range(*index.indices(self._dimensions)) no_of_cvalues = len(cvalues) no_of_indices = len(indices) if no_of_cvalues != no_of_indices: msg = \ "The number of given values ({no_of_cvalues}) does not match " \ "the number of components to be set ({no_of_indices})" \ .format_map(vars()) raise ValueError(msg) else: msg = \ "Vector indices must be integers or slices, not {type_index.__name__}" \ .format(type_index=type(index)) raise TypeError(msg) self._cvalues[index] = cvalues @hf.ensure_other_is_vector def __eq__(self, other): for cvs, cvo in zip(self._cvalues, other._cvalues): if cvs != cvo: equal = False break else: equal = True return equal @hf.ensure_other_is_vector def __ne__(self, other): for cvs, cvo in zip(self._cvalues, other._cvalues): if cvs != cvo: not_equal = True break else: not_equal = False return not_equal def __contains__(self, value): """Check if a value is equal to any of the vector component values""" contains = value in self._cvalues return contains def __call__(self, function, needs_index=False): """Apply a function to each of the vector component values""" if needs_index: dimensions = self._dimensions vector = \ self._vector( function(cvs, index, dimensions) for index, cvs in enumerate(self._cvalues) ) else: vector = self._vector(map(function, self._cvalues)) return vector @classmethod def _verify_function(cls, name, function, no_of_arguments): if function is None: method_name = 'component_' + name if not hasattr(cls, method_name): msg = \ "class {cls.__name__} has no method named '{method_name}'" \ .format_map(vars()) raise AttributeError(msg) else: # function_types = \ # ( # types.FunctionType, # types.BuiltinFunctionType, # functools.partial # ) # if not isinstance(function, function_types): function_name = getattr(function, '__name__', str(function)) if not (isfunction(function) or ismethod(function)): msg = \ "{function_name} can not be used here" \ .format_map(vars()) raise TypeError(msg) msg = \ "{function_name} can not be called with {n} argument{s}" \ .format( function_name = function_name, n = no_of_arguments, s = '' if no_of_arguments == 1 else 's' ) argspec = getfullargspec(function) arg_count = len(argspec.args) if argspec.defaults is not None: arg_count -= len(argspec.defaults) if arg_count > no_of_arguments: raise TypeError(msg) if arg_count < no_of_arguments and argspec.varargs is None: raise TypeError(msg) kwonly_arg_count = len(argspec.kwonlyargs) if argspec.defaults is not None: kwonly_arg_count -= len(argspec.kwonly_defaults) if kwonly_arg_count > 0: raise TypeError(msg) @classmethod def create_vector_method_arg1(cls, name, function=None): """TODO""" cls._verify_function(name, function, 1) if function is None: function = getattr(cls, 'component_' + name) name = 'vector_' + name vector_function = hf.make_method_arg1(name, function) setattr(cls, name, vector_function) @classmethod def create_vector_method_arg2(cls, name, function=None): """TODO""" cls._verify_function(name, function, 2) if function is None: function = getattr(cls, 'component_' + name) name = 'vector_' + name vector_function = hf.make_method_arg2(name, function) setattr(cls, name, vector_function) @classmethod def create_vector_method_arg3(cls, name, function=None): """TODO""" cls._verify_function(name, function, 3) if function is None: function = getattr(cls, 'component_' + name) name = 'vector_' + name vector_function = hf.make_method_arg3(name, function) setattr(cls, name, vector_function) return Fundamental_Vector
def create_class_Vector(name, component_names, *, brackets='<>', sep=', ', cnull=None, cunit=None, functions=None): """ Function that makes a creates class The number of dimensions are determined by the number of component names """ hf.verify_class_name(name) if functions is None: functions = {} def verify_equal(eq, op_all): try: equal = op_all(eq) except TypeError: equal = None if equal is None: equal = bool(eq) if not equal: msg = "Invalid value(s) for cnull and/or cunit" raise ValueError(msg) def verify_equal_args(arg0, arg1): if functions is None: op_eq = operator.eq op_all = all else: op_eq = functions.get('eq', operator.eq) op_all = functions.get('all', all) verify_equal(op_eq(arg0, arg1), op_all) verify_equal(op_eq(arg1, arg0), op_all) def verify_units(cnull, cunit): """Verify that some fundamental statements with cnull and cunit are true""" verify_equal_args(cnull, -cnull) # ? verify_equal_args(cnull, +cnull) verify_equal_args(cnull, --cnull) verify_equal_args(cunit, --cunit) verify_equal_args(cunit, +cunit) verify_equal_args(cnull, cnull + cnull) verify_equal_args(cnull, cnull - cnull) verify_equal_args(cnull, +cnull - cnull) verify_equal_args(cnull, -cnull + cnull) verify_equal_args(cunit, cnull + cunit) verify_equal_args(cunit, cunit + cnull) verify_equal_args(cnull, +cunit - cunit) verify_equal_args(cnull, -cunit + cunit) verify_equal_args(cnull, cnull * cnull) verify_equal_args(cnull, cnull * cunit) verify_equal_args(cnull, cunit * cnull) verify_equal_args(cunit, cunit * cunit) verify_equal_args(cnull, cnull / cunit) verify_equal_args(cunit, cunit / cunit) verify_equal_args(cunit, cnull**cnull) # ? verify_equal_args(cnull, cnull**cunit) verify_equal_args(cunit, cunit**cnull) verify_equal_args(cunit, cunit**cunit) # verify_equal_args(cunit, cunit**-cunit) # Problem with Pandas verify_equal_args(cnull, cnull * 0) verify_equal_args(cnull, 0 * cnull) verify_equal_args(cnull, cnull * 1) verify_equal_args(cnull, 1 * cnull) verify_equal_args(cnull, cunit * 0) verify_equal_args(cnull, 0 * cunit) verify_equal_args(cunit, cunit * 1) verify_equal_args(cunit, 1 * cunit) verify_equal_args(cnull, cnull / 1) verify_equal_args(cnull, 0 / cunit) verify_equal_args(cunit, cunit / 1) verify_equal_args(cunit, 1 / cunit) verify_equal_args(cunit, cnull**0) # ? verify_equal_args(cnull, cnull**1) verify_equal_args(cunit, cunit**0) verify_equal_args(cunit, cunit**1) # verify_equal_args(cunit, cunit**-1) # Problem with Pandas verify_equal_args(cunit, 0**cnull) # ? verify_equal_args(cnull, 0**cunit) verify_equal_args(cunit, 1**cnull) verify_equal_args(cunit, 1**cunit) # verify_equal_args(cunit, 1**-cunit) # Problem with Pandas if (cnull is None) and (cunit is None): cnull = 0 cunit = 1 else: if (cnull is None) or (cunit is None): msg = \ "If a value for either cnull or cunit is provided, " \ "a value for the other must also be provided" raise TypeError(msg) cnull = copy(cnull) cunit = copy(cunit) verify_units(cnull, cunit) SV = \ create_class_Simple_Vector( name = 'SV_' + name, component_names = component_names, brackets = brackets, sep = sep ) def make_zero_vector_method(cls): cnull = cls._cnull cvalues = [cnull] * cls._dimensions def zero(cls): """Vector with all components values set to 'cnull'""" return cls(*cvalues, _internal=True) cls.zero = classmethod(zero) def make_one_vector_method(cls): cunit = cls._cunit cvalues = [cunit] * cls._dimensions def one(cls): """Vector with all components values set to 'cunit'""" return cls(*cvalues, _internal=True) cls.one = classmethod(one) def setup_vector_bases(cls): class Basis: """Descriptor class for basis vectors""" def __init__(self, owner, index, cname, basis_name): self.method_name = basis_name self.cname = cname cnull = owner._cnull cunit = owner._cunit self.cvalues = \ [ cunit if (i == index) else cnull for i in range(owner._dimensions) ] def __set__(self, instance, value): owner = type(instance) msg = \ "'{owner.__name__}' object attribute '{self.method_name}' is read-only" \ .format_map(vars()) raise AttributeError(msg) def __get__(self, instance, owner): def basis_vector(): vector = owner(*self.cvalues, _internal=True) return vector basis_vector.__name__ = self.method_name basis_vector.__doc__ = \ "Basis vector, with length 'cunit' along the {self.cname}-axis" \ .format_map(vars()) return basis_vector for index, cname in enumerate(cls._cnames): basis_name = 'basis_' + cname setattr(cls, basis_name, Basis(cls, index, cname, basis_name)) def init_Vector(cls): """Initialize class""" hf.setup_vector_class(cls=cls, name=name, functions=functions) cls._cnull = cnull cls._cunit = cunit cls._true = cls.component_eq(cnull, cnull) cls._false = cls.component_ne(cnull, cnull) make_zero_vector_method(cls) make_one_vector_method(cls) setup_vector_bases(cls) hf.make_dunder_methods( cls, [ (2, '', 'matmul', operator.matmul), (2, 'r', 'matmul', operator.matmul), # (2, 'i', 'matmul', operator.matmul) ]) return cls @init_Vector class Vector(SV): """ A vector class with {dimensions} dimensions and the component names '{cs_cnames}' """ _internal_functions = \ [ 'eq', 'ne', 'and', 'or', 'all', 'floor', 'ceil', 'trunc' ] _component_operators = \ { 'arg1_n': { 'abs': operator.abs, 'neg': operator.neg, 'pos': operator.pos, 'floor': math.floor, 'ceil': math.ceil, 'trunc': math.trunc }, 'arg2_n': { 'round': round, 'add': operator.add, 'sub': operator.sub, 'mul': operator.mul, 'pow': operator.pow, 'matmul': operator.matmul, # ? 'truediv': operator.truediv, 'floordiv': operator.floordiv, 'mod': operator.mod, }, 'arg1_o': { }, 'arg2_o': { }, 'arg2_i': { 'iadd': operator.iadd, 'isub': operator.isub, 'imul': operator.imul, 'ipow': operator.ipow, 'imatmul': operator.imatmul, # ? 'itruediv': operator.itruediv, 'ifloordiv': operator.ifloordiv, 'imod': operator.imod } } @classmethod def component_null(cls): """Null value for vector components in class""" cnull = copy(cls._cnull) return cnull @classmethod def component_unit(cls): """Unit value for vector components in class""" cunit = copy(cls._cunit) return cunit @classmethod def fill(cls, value): """A vector with all component values set to value""" cunit = cls._cunit cvalues = \ ( cunit * value for _ in range(cls._dimensions) ) vector = cls(*cvalues, _internal=True) return vector @classmethod def sum_of_vectors(cls, vectors): """The sum of several vectors""" vectors = cls._ensure_all_are_vectors(vectors) vector = reduce(operator.add, vectors, cls.zero()) return vector @classmethod def prod_of_vectors(cls, vectors): """The product of several vectors""" vectors = cls._ensure_all_are_vectors(vectors) vector = reduce(operator.mul, vectors, cls.one()) return vector def __init__(self, *cvalues, _internal=False, **named_cvalues): """TODO""" if _internal: self._cvalues = [*cvalues] else: self._check_arguments(cvalues, named_cvalues) cunit = self._cunit if len(named_cvalues) > 0: self._cvalues = \ [ cunit * named_cvalues[cns] for cns in self._cnames ] else: self._cvalues = \ [ cunit * cv for cv in cvalues ] def is_zero_vector(self): """Check if the vector is a zero vector""" cnull = self._cnull are_zeros = \ ( self.component_eq(cvs, cnull) for cvs in self._cvalues ) is_zero = reduce(self.component_and, are_zeros) return is_zero def contains(self, value): """Check if a value is equal to any of the vector component values""" are_present = \ ( self.component_eq(value, cvs) for cvs in self._cvalues ) does_contain = reduce(self.component_or, are_present) return does_contain def contains_not(self, value): """Check if a value is not equal to any of the vector component values""" are_not_present = \ ( self.component_ne(value, cvs) for cvs in self._cvalues ) does_not_contain = reduce(self.component_and, are_not_present) return does_not_contain def __setitem__(self, index, values): """Change vector component values by indexing""" cunit = self._cunit if isinstance(index, int): cvalues = cunit * values elif isinstance(index, slice): cvalues = \ [ cunit * cv for cv in values ] indices = range(*index.indices(self._dimensions)) no_of_cvalues = len(cvalues) no_of_indices = len(indices) if no_of_cvalues != no_of_indices: msg = \ "The number of given values ({no_of_cvalues}) does not match " \ "the number of components to be set ({no_of_indices})" \ .format_map(vars()) raise ValueError(msg) else: msg = \ "Vector index must be an integer or a slice, not {type_index.__name__}" \ .format(type_index=type(index)) raise TypeError(msg) self._cvalues[index] = cvalues def __floor__(self): """Apply 'component_floor' to each of the vector component values""" vector = self._vector(map(self.component_floor, self._cvalues)) return vector def __ceil__(self): """Apply 'component_ceil' to each of the vector component values""" vector = self._vector(map(self.component_ceil, self._cvalues)) return vector def __trunc__(self): """Apply 'component_trunc' to each of the vector component values""" vector = self._vector(map(self.component_trunc, self._cvalues)) return vector def __bool__(self): """Check if the vector is not a zero vector""" is_zero_vector = self.is_zero_vector() try: not_zero_vector = not self.component_all(is_zero_vector) except TypeError: not_zero_vector = None if not_zero_vector is None: not_zero_vector = not is_zero_vector return not_zero_vector def sum_of_components(self): """The sum of a vector's component values""" csum = self.component_null() for cvs in self._cvalues: csum += cvs return csum def prod_of_components(self): """The product of a vector's component values""" cprod = self.component_unit() for cvs in self._cvalues: cprod *= cvs return cprod @property def cnull(self): """Null value for vector components""" component_null = self.component_null() return component_null @property def cunit(self): """Unit value for vector components""" component_unit = self.component_unit() return component_unit @property def csum(self): """The sum of a vector's component values""" sum_of_components = self.sum_of_components() return sum_of_components @property def cprod(self): """The product of a vector's component values""" prod_of_components = self.prod_of_components() return prod_of_components return Vector
def create_class_Versatile_Vector(name, component_names, *, brackets='<>', sep=', ', functions=None): """ Function that creates a versatile vector class The number of dimensions are determined by the number of component names """ hf.verify_class_name(name) if functions is None: functions = { } SV = \ create_class_Simple_Vector( name = 'SV_' + name, component_names = component_names, brackets = brackets, sep = sep ) def init_Versatile_Vector(cls): """Initialize class""" hf.setup_vector_class(cls=cls, name=name, functions=functions) hf.make_dunder_methods( cls, [ (1, '', 'not', operator.not_), (1, '', 'index', operator.index), (1, '', 'inv', operator.inv), (1, '', 'invert', operator.invert), (2, '', 'matmul', operator.matmul), (2, '', 'eq', operator.eq), (2, '', 'ne', operator.ne), (2, '', 'lt', operator.lt), (2, '', 'gt', operator.gt), (2, '', 'le', operator.le), (2, '', 'ge', operator.ge), (2, '', 'and', operator.and_), (2, '', 'or', operator.or_), (2, '', 'xor', operator.xor), (2, '', 'lshift', operator.lshift), (2, '', 'rshift', operator.rshift), (2, 'r', 'matmul', operator.matmul), (2, 'r', 'and', operator.and_), (2, 'r', 'or', operator.or_), (2, 'r', 'xor', operator.xor), (2, 'r', 'lshift', operator.lshift), (2, 'r', 'rshift', operator.rshift), (2, 'i', 'matmul', operator.matmul), (2, 'i', 'and', operator.and_), (2, 'i', 'or', operator.or_), (2, 'i', 'xor', operator.xor), (2, 'i', 'lshift', operator.lshift), (2, 'i', 'rshift', operator.rshift) ] ) return cls @init_Versatile_Vector class Versatile_Vector(SV): """ A versatile vector class with {dimensions} dimensions and the component names '{cs_cnames}' """ _internal_functions = \ [ 'any' ] _component_operators = \ { 'arg1_n': { 'not': operator.not_, 'truth': operator.truth, 'abs': operator.abs, 'neg': operator.neg, 'pos': operator.pos, 'index': operator.index, 'inv': operator.inv, 'invert': operator.invert, 'floor': math.floor, 'ceil': math.ceil, 'trunc': math.trunc }, 'arg2_n': { 'round': round, 'and': operator.and_, 'or': operator.or_, 'xor': operator.xor, 'eq': operator.eq, 'ne': operator.ne, 'lt': operator.lt, 'gt': operator.gt, 'le': operator.le, 'ge': operator.ge, 'add': operator.add, 'sub': operator.sub, 'mul': operator.mul, 'pow': operator.pow, 'matmul': operator.matmul, 'truediv': operator.truediv, 'floordiv': operator.floordiv, 'mod': operator.mod, 'lshift': operator.lshift, 'rshift': operator.rshift, 'is': operator.is_, 'isnot': operator.is_not, 'concat': operator.concat, 'contains': operator.contains, 'countof': operator.countOf, 'indexof': operator.indexOf, 'getitem': operator.getitem }, 'arg1_o': { 'delitem': operator.delitem }, 'arg2_o': { 'setitem': operator.setitem }, 'arg2_i': { 'iand': operator.iand, 'ior': operator.ior, 'ixor': operator.ixor, 'iadd': operator.iadd, 'isub': operator.isub, 'imul': operator.imul, 'ipow': operator.ipow, 'imatmul': operator.imatmul, 'itruediv': operator.itruediv, 'ifloordiv': operator.ifloordiv, 'imod': operator.imod, 'ilshift': operator.ilshift, 'irshift': operator.irshift } } return Versatile_Vector
def create_class_Cartesian_2D_Vector(name, component_names, *, brackets='<>', sep=', ', cnull=0, cunit=1, functions=None): """ Function that creates a cartesian vector class with 2 dimensions """ hf.verify_class_name(name) dimensions = 2 component_names = [*component_names] if len(component_names) != dimensions: msg = "The number of component names must be {dimensions}" raise ValueError(msg.format_map(vars())) if functions is None: functions = {} CV = \ create_class_Cartesian_Vector( name = 'CV_' + name, component_names = component_names, brackets = brackets, sep = sep, cnull = cnull, cunit = cunit, functions = functions ) def init_Cartesian_2D_Vector(cls): """Initialize class""" hf.setup_vector_class(cls=cls, name=name, functions=functions) return cls @init_Cartesian_2D_Vector class Cartesian_2D_Vector(CV): """ A cartesian vector class with {dimensions} dimensions and the component names '{cs_cnames}' """ _internal_functions = \ [ # 'eq', # 'ne', # 'and', # 'or', # 'all', # 'min', # 'max', # 'floor', # 'ceil', # 'trunc', # 'pi', # 'atan2', 'cos', 'sin' ] @classmethod def from_polar(cls, radius, azimuth): """A vector created from polar coordinates""" cunit = cls._cunit azimuth = cunit * azimuth cos_a = cls.component_cos(azimuth) sin_a = cls.component_sin(azimuth) radius = cunit * radius vector = \ cls( radius * cos_a, radius * sin_a, _internal = True ) return vector def perp(self): """A vector that is perpendicular to a vector""" cvs0, cvs1 = self._cvalues vector = self._vector((-cvs1, cvs0)) return vector def perp_dot(self, other): """The dot product of a vector that is perpendicular to a vector and another vector""" scalar = self.perp().dot(other) return scalar @hf.ensure_other_is_vector def sin(self, other, clip=False): """The sine of the angle between two vectors (from -cunit to +cunit)""" ls = self.length() lo = other.length() pd = self.perp_dot(other) try: sine = pd / (ls * lo) except ZeroDivisionError as err: msg = "One (or both) of the vectors is a zero vector" raise ZeroDivisionError(msg) from err if clip: cunit = self._cunit sine = self.clip(sine, -cunit, cunit) return sine @hf.ensure_other_is_vector def angle(self, other): """The angle in radians (from -cunit*pi to +cunit*pi) between two vectors""" ### Verify cvs0, cvs1 = self._cvalues cvo0, cvo1 = other._cvalues angle_s = self.component_atan2(cvs1, cvs0) angle_o = other.component_atan2(cvo1, cvo0) angle_between = angle_o - angle_s # angle_between = self._angle_to_minus_plus_pi(angle_between) return angle_between def rotate(self, angle): """The vector rotated by an angle in radiands""" cunit = self._cunit angle = cunit * angle cos = self.component_cos(angle) sin = self.component_sin(angle) vector = \ self._mmult( self._vector(( cos, -sin)), self._vector(( sin, cos)) ) return vector @hf.ensure_other_is_vector def are_parallel(self, other): """Check if two vectors are parallel""" try: vsn = self.normalize() except ZeroDivisionError: vsn = None try: von = other.normalize() except ZeroDivisionError: von = None if (vsn is None) or (von is None): parallel = self._true else: perp_dot = vsn.perp_dot(von) parallel = self._equal_cnull(perp_dot) parallel = self.component_or(parallel, self.is_zero_vector()) parallel = self.component_or(parallel, other.is_zero_vector()) return parallel @hf.ensure_others_are_vectors def reorient(self, other, other_): """Reorient a vector from one direction to another direction""" angle = other.angle(other_) vector = self.rotate(angle) return vector def polar_as_dict(self): """Polar coordinates of a vector as a dictionary""" result = \ { 'radius': self.radius, 'azimuth': self.azimuth } return result @property def radius(self): """TODO""" length = self.length() return length @property def azimuth(self): """TODO""" cvs0, cvs1 = self._cvalues angle = self.component_atan2(cvs1, cvs0) # angle = self._angle_to_minus_plus_pi(angle) return angle return Cartesian_2D_Vector
def create_class_Tolerant_Versatile_Vector(name, component_names, *, brackets='<>', sep=', ', functions=None, abs_tol=1e-12, rel_tol=1e-9): """ Function that creates a tolerant versatile vector class The number of dimensions are determined by the number of component names """ hf.verify_class_name(name) if functions is None: functions = {} VV = \ create_class_Versatile_Vector( name = 'VV_' + name, component_names = component_names, brackets = brackets, sep = sep, functions = functions ) def init_Tolerant_Versatile_Vector(cls): """Initialize class""" hf.setup_vector_class(cls=cls, name=name, functions=functions) def prepare_tolerances(name, tolerances, dimensions): try: all_tolerances = [*tolerances] except TypeError: all_tolerances = None if all_tolerances is None: all_tolerances = [tolerances] * dimensions else: if len(all_tolerances) != dimensions: msg = "The number of tolerances in {name} does not match the number of dimensions" raise ValueError(msg.format_map(vars())) result = \ [ float(tol) for tol in all_tolerances ] return result cls.abs_tolerances = prepare_tolerances('abs_tol', abs_tol, cls._dimensions) cls.rel_tolerances = prepare_tolerances('rel_tol', rel_tol, cls._dimensions) compare_methods = \ { 'eq': \ "Check if the corresponding component values of " \ "two vectors are equal (within calculated tolerances)", 'ne': \ "Check if the corresponding component values of " \ "two vectors are not equal (within calculated tolerances)", 'lt': \ "TODO", 'gt': \ "TODO", 'le': \ "TODO", 'ge': "TODO" } for comp_type, method_doc in compare_methods.items(): def compare_method(self, other, _comp_type=comp_type): vector = self._vector(self._compare_cvalues(other, _comp_type)) return vector method_name = comp_type compare_method.__name__ = method_name compare_method.__doc__ = method_doc setattr(cls, method_name, compare_method) dunder_compare_methods = \ { 'eq': \ "Check if all corresponding component values of " \ "two vectors are equal (within calculated tolerances)", 'ne': \ "Check if all corresponding component values of " \ "two vectors are not equal (within calculated tolerances)", 'lt': \ "TODO", 'gt': \ "TODO", 'le': \ "TODO", 'ge': "TODO" } for comp_type, method_doc in dunder_compare_methods.items(): def dunder_compare_method(self, other, _comp_type=comp_type): compare_results = self._compare_cvalues(other, _comp_type) cmp = reduce(self.component_and, compare_results) try: result = self.component_all(cmp) except TypeError: result = None if result is None: result = bool(cmp) return result method_name = '__' + comp_type + '__' dunder_compare_method.__name__ = method_name dunder_compare_method.__doc__ = method_doc setattr(cls, method_name, dunder_compare_method) return cls @init_Tolerant_Versatile_Vector class Tolerant_Versatile_Vector(VV): """ A tolerant versatile vector class with {dimensions} dimensions and the component names '{cs_cnames}' """ _internal_functions = \ [ 'and', 'or', # 'any', 'max', 'abs' ] ### Needs more testing @classmethod def _epsilon_1(cls, cvalues): eps = \ ( cls.component_max( cls.abs_tolerances[i], cvalues[i]*cls.rel_tolerances[i] ) for i in range(cls._dimensions) ) return eps ### Needs more testing @classmethod def _epsilon_2(cls, cvalues_1, cvalues_2): eps = \ ( cls.component_max( cls.abs_tolerances[i], cls.component_max(cvalues_1[i], cvalues_2[i])*cls.rel_tolerances[i] ) for i in range(cls._dimensions) ) return eps ### Needs more testing @classmethod def _epsilon_n(cls, cvalues_n, i): """Calculates a common tolerance value (epsilon) for all the values""" abs_tol = cls.abs_tolerances[i] rel_tol = cls.rel_tolerances[i] eps = \ cls.component_max( abs_tol, cls.component_max( cls.component_abs(cv) for cv in cvalues_n )*rel_tol ### This does not always work ) return eps @classmethod def _component_from_vectors(cls, vectors, index): vectors = cls._ensure_all_are_vectors(vectors) result = \ ( v._cvalues[index] for v in vectors ) return result ### Needs more testing @classmethod def tolerance_all(cls, vectors): """Calculate a tolerance for each of the components of several vectors""" ### vectors = tuple(cls._ensure_all_are_vectors(vectors)) cvalues = \ ( cls._epsilon_n(cls._component_from_vectors(vectors, i), i) for i in range(cls._dimensions) ) vector = cls(*cvalues, _internal=True) return vector ### TODO # def tolerance_with(self, other): # """Calculate a common tolerance for two vectors based on their lengths""" # # return @hf.ensure_other_is_vector def _compare_cvalues(self, other, comp_type): compare_fn = \ { 'eq': lambda s, o, t: self.component_and((o - t) <= s, s <= (o + t)), 'ne': lambda s, o, t: self.component_or(s < (o - t), (o + t) < s), 'lt': lambda s, o, t: s < (o - t), 'gt': lambda s, o, t: (o + t) < s, 'le': lambda s, o, t: s <= (o + t), 'ge': lambda s, o, t: (o - t) <= s } fn = compare_fn[comp_type] csot = \ zip( self._cvalues, other._cvalues, self.tolerance_with(other)._cvalues ) result = \ ( fn(cvs, cvo, cvt) for cvs, cvo, cvt in csot ) return result return Tolerant_Versatile_Vector
def create_class_Cartesian_Vector(name, component_names, *, brackets='<>', sep=', ', cnull=None, cunit=None, functions=None): """ Function that creates a cartesian vector class The number of dimensions are determined by the number of component names """ hf.verify_class_name(name) if functions is None: functions = {} V = \ create_class_Vector( name = 'V_' + name, component_names = component_names, brackets = brackets, sep = sep, cnull = cnull, cunit = cunit, functions = functions ) def init_Cartesian_Vector(cls): """Initialize class""" hf.setup_vector_class(cls=cls, name=name, functions=functions) cls.__abs__ = cls.length cls.__matmul__ = cls.dot cls.__rmatmul__ = cls.dot return cls @init_Cartesian_Vector class Cartesian_Vector(V): """ A cartesian vector class with {dimensions} dimensions and the component names '{cs_cnames}' """ _internal_functions = \ [ # 'eq', # 'ne', # 'and', # 'or', # 'all', 'min', 'max', # 'floor', # 'ceil', # 'trunc', 'pi', 'atan2' ] @classmethod def clip(cls, value, min_value, max_value): """Limits a value so that it lies between two values""" cunit = cls._cunit value = cunit * value min_value = cunit * min_value max_value = cunit * max_value clipped_value = cls.component_max( min_value, cls.component_min(value, max_value)) return clipped_value @classmethod def _equal_cnull(cls, value): cnull = cls._cnull cunit = cls._cunit value = cunit * value result = cls.component_eq(value, cnull) return result @classmethod def _not_equal_cnull(cls, value): cnull = cls._cnull cunit = cls._cunit value = cunit * value result = cls.component_ne(value, cnull) return result @classmethod def _equal_cunit(cls, value): cunit = cls._cunit value = cunit * value result = cls.component_eq(value, cunit) return result @classmethod def _not_equal_cunit(cls, value): cunit = cls._cunit value = cunit * value result = cls.component_ne(value, cunit) return result def is_zero_vector(self): """Check if the length of a vector is equal to cnull""" ls = self.length() result = self._equal_cnull(ls) return result def is_unit_vector(self): """Check if the length of a vector is equal to cunit""" ls = self.length() result = self._equal_cunit(ls) return result @hf.ensure_other_is_vector def __eq__(self, other): """ Check if two vectors are equal based on their distance from each other """ distance_between = self.distance(other) equal = self._equal_cnull(distance_between) return equal @hf.ensure_other_is_vector def __ne__(self, other): """ Check if two vectors are not equal based on their distance from each other """ distance_between = self.distance(other) not_equal = self._not_equal_cnull(distance_between) return not_equal @hf.ensure_other_is_vector def are_orthogonal(self, other): """Check if two vectors are orthogonal""" dot = self.dot(other) orthogonal = self._equal_cnull(dot) ### Perhaps these are needed with numpy: # orthogonal = self.component_or(orthogonal, self.is_zero_vector()) # orthogonal = self.component_or(orthogonal, other.is_zero_vector()) return orthogonal @hf.ensure_other_is_vector def equal_lengths(self, other): """Check if two vectors have equal lengths""" ls = self.length() lo = other.length() equal_vector_lengths = self.component_eq(ls, lo) return equal_vector_lengths @hf.ensure_other_is_vector def shorter(self, other): """Check if a vector is shorter than another""" ls = self.length() lo = other.length() is_shorter = ls < lo return is_shorter @hf.ensure_other_is_vector def longer(self, other): """Check if a vector is longer than another""" ls = self.length() lo = other.length() is_longer = ls > lo return is_longer @hf.ensure_other_is_vector def dot(self, other): """The dot product (inner product) of two vectors""" scalar = (self * other).sum_of_components() return scalar def length(self): """The length (norm) of a vector""" cunit = self._cunit length_of_vector = (self**(cunit * 2)).sum_of_components()**(cunit / 2) return length_of_vector @hf.ensure_other_is_vector def distance(self, other): """The distance between two vectors""" length_between = (other - self).length() return length_between def normalize(self): """Vector scaled so that its length is cunit""" ls = self.length() try: vector = self / ls except ZeroDivisionError as err: msg = "The length of the vector is zero" raise ZeroDivisionError(msg) from err return vector @hf.ensure_other_is_vector def project(self, other): """Projection of a vector onto another""" num = self.dot(other) den = other.dot(other) try: s = num / den except ZeroDivisionError as err: msg = "The length of the vector to project onto is zero" raise ZeroDivisionError(msg) from err vector = other * s return vector @hf.ensure_other_is_vector def inv_project(self, other): """Inverse projection of a vector from another""" num = self.dot(self) den = self.dot(other) try: s = num / den except ZeroDivisionError as err: msg = "The vectors are orthogonal to each other" raise ZeroDivisionError(msg) from err vector = other * s return vector @hf.ensure_other_is_vector def reject(self, other): """Rejection of a vector from another""" num = self.dot(other) den = other.dot(other) try: s = num / den except ZeroDivisionError as err: msg = "The length of the vector to reject from is zero" raise ZeroDivisionError(msg) from err vector = self - other * s return vector @hf.ensure_other_is_vector def scalar_project(self, other): """Scalar projection of a vector onto another vector""" try: von = other.normalize() except ZeroDivisionError as err: msg = "The length of the vector to project onto is zero" raise ZeroDivisionError(msg) from err scalar = self.dot(von) return scalar @hf.ensure_other_is_vector def angle(self, other): """ The smallest angle in radians (from cnull to +cunit*pi) between two vectors Kahan, W. (2016). Computing Cross-Products and Rotations in 2- and 3-Dimensional Euclidean Spaces https://people.eecs.berkeley.edu/~wkahan/MathH110/Cross.pdf """ ls = self.length() lo = other.length() vs = self * lo vo = other * ls ln = (vs - vo).length() ld = (vs + vo).length() angle_between = self.component_atan2(ln, ld) * 2 return angle_between @hf.ensure_other_is_vector def cos(self, other, clip=False): """The cosine of the angle between two vectors (from -cunit to +cunit)""" ls = self.length() lo = other.length() dot = self.dot(other) try: cosine = dot / (ls * lo) except ZeroDivisionError as err: msg = "One (or both) of the vectors is a zero vector" raise ZeroDivisionError(msg) from err if clip: cunit = self._cunit cosine = self.clip(cosine, -cunit, cunit) return cosine def _mmult(self, *others): assert len(others) == self._dimensions vector = self._vector(map(self.dot, others)) return vector ### Consider if this should be added # def __imatmul__(self, other): # """TODO""" # # self = self * self.dot(other) # # return self return Cartesian_Vector