# -*- coding: utf-8 -*- """ pint.util ~~~~~~~~~ Miscellaneous functions for pint. :copyright: 2013 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ from __future__ import division, unicode_literals, print_function, absolute_import import re import operator from numbers import Number from fractions import Fraction import logging from token import STRING, NAME, OP from tokenize import untokenize from .compat import string_types, tokenizer, lru_cache, NullHandler, maketrans logger = logging.getLogger(__name__) logger.addHandler(NullHandler()) def matrix_to_string(matrix, row_headers=None, col_headers=None, fmtfun=lambda x: str(int(x))): """Takes a 2D matrix (as nested list) and returns a string. """ ret = [] if col_headers: ret.append(('\t' if row_headers else '') + '\t'.join(col_headers)) if row_headers: ret += [rh + '\t' + '\t'.join(fmtfun(f) for f in row) for rh, row in zip(row_headers, matrix)] else: ret += ['\t'.join(fmtfun(f) for f in row) for row in matrix] return '\n'.join(ret) def transpose(matrix): """Takes a 2D matrix (as nested list) and returns the transposed version. """ return [list(val) for val in zip(*matrix)] def column_echelon_form(matrix, ntype=Fraction, transpose_result=False): """Calculates the column echelon form using Gaussian elimination. :param matrix: a 2D matrix as nested list. :param ntype: the numerical type to use in the calculation. :param transpose_result: indicates if the returned matrix should be transposed. :return: column echelon form, transformed identity matrix, swapped rows """ lead = 0 M = transpose(matrix) _transpose = transpose if transpose_result else lambda x: x rows, cols = len(M), len(M[0]) new_M = [] for row in M: r = [] for x in row: if isinstance(x, float): x = ntype.from_float(x) else: x = ntype(x) r.append(x) new_M.append(r) M = new_M # M = [[ntype(x) for x in row] for row in M] I = [[ntype(1) if n == nc else ntype(0) for nc in range(rows)] for n in range(rows)] swapped = [] for r in range(rows): if lead >= cols: return _transpose(M), _transpose(I), swapped i = r while M[i][lead] == 0: i += 1 if i != rows: continue i = r lead += 1 if cols == lead: return _transpose(M), _transpose(I), swapped M[i], M[r] = M[r], M[i] I[i], I[r] = I[r], I[i] swapped.append(i) lv = M[r][lead] M[r] = [mrx / lv for mrx in M[r]] I[r] = [mrx / lv for mrx in I[r]] for i in range(rows): if i == r: continue lv = M[i][lead] M[i] = [iv - lv*rv for rv, iv in zip(M[r], M[i])] I[i] = [iv - lv*rv for rv, iv in zip(I[r], I[i])] lead += 1 return _transpose(M), _transpose(I), swapped def pi_theorem(quantities, registry=None): """Builds dimensionless quantities using the Buckingham π theorem :param quantities: mapping between variable name and units :type quantities: dict :return: a list of dimensionless quantities expressed as dicts """ # Preprocess input and build the dimensionality Matrix quant = [] dimensions = set() if registry is None: getdim = lambda x: x else: getdim = registry.get_dimensionality for name, value in quantities.items(): if isinstance(value, string_types): value = ParserHelper.from_string(value) if isinstance(value, dict): dims = getdim(value) elif not hasattr(value, 'dimensionality'): dims = getdim(value) else: dims = value.dimensionality if not registry and any(not key.startswith('[') for key in dims): logger.warning('A non dimension was found and a registry was not provided. ' 'Assuming that it is a dimension name: {0}.'.format(dims)) quant.append((name, dims)) dimensions = dimensions.union(dims.keys()) dimensions = list(dimensions) # Calculate dimensionless quantities M = [[dimensionality[dimension] for name, dimensionality in quant] for dimension in dimensions] M, identity, pivot = column_echelon_form(M, transpose_result=False) # Collect results # Make all numbers integers and minimize the number of negative exponents. # Remove zeros results = [] for rowm, rowi in zip(M, identity): if any(el != 0 for el in rowm): continue max_den = max(f.denominator for f in rowi) neg = -1 if sum(f < 0 for f in rowi) > sum(f > 0 for f in rowi) else 1 results.append(dict((q[0], neg * f.numerator * max_den / f.denominator) for q, f in zip(quant, rowi) if f.numerator != 0)) return results def solve_dependencies(dependencies): """Solve a dependency graph. :param dependencies: dependency dictionary. For each key, the value is an iterable indicating its dependencies. :return: list of sets, each containing keys of independents tasks dependent only of the previous tasks in the list. """ d = dict((key, set(dependencies[key])) for key in dependencies) r = [] while d: # values not in keys (items without dep) t = set(i for v in d.values() for i in v) - set(d.keys()) # and keys without value (items without dep) t.update(k for k, v in d.items() if not v) # can be done right away r.append(t) # and cleaned up d = dict(((k, v - t) for k, v in d.items() if v)) return r def find_shortest_path(graph, start, end, path=None): path = (path or []) + [start] if start == end: return path if not start in graph: return None shortest = None for node in graph[start]: if node not in path: newpath = find_shortest_path(graph, node, end, path) if newpath: if not shortest or len(newpath) < len(shortest): shortest = newpath return shortest def find_connected_nodes(graph, start, visited=None): if not start in graph: return None visited = (visited or set()) visited.add(start) for node in graph[start]: if node not in visited: find_connected_nodes(graph, node, visited) return visited class ParserHelper(dict): """The ParserHelper stores in place the product of variables and their respective exponent and implements the corresponding operations. """ __slots__ = ('scale', ) def __init__(self, scale=1, *args, **kwargs): self.scale = scale dict.__init__(self, *args, **kwargs) @classmethod def from_word(cls, input_word): """Creates a ParserHelper object with a single variable with exponent one. Equivalent to: ParserHelper({'word': 1}) """ ret = cls() ret.add(input_word, 1) return ret @classmethod def from_string(cls, input_string): return cls._from_string(input_string).copy() @classmethod @lru_cache() def _from_string(cls, input_string): """Parse linear expression mathematical units and return a quantity object. """ if not input_string: return cls() input_string = string_preprocessor(input_string) if '[' in input_string: input_string = input_string.replace('[', '__obra__').replace(']', '__cbra__') reps = True else: reps = False gen = tokenizer(input_string) result = [] for toknum, tokval, _, _, _ in gen: if toknum == NAME: if not tokval: continue result.extend([ (NAME, 'L_'), (OP, '('), (STRING, '"' + tokval + '"'), (OP, ')') ]) else: result.append((toknum, tokval)) ret = eval(untokenize(result), {'__builtins__': None}, {'L_': cls.from_word}) if isinstance(ret, Number): return ParserHelper(ret) if not reps: return ret return ParserHelper(ret.scale, dict((key.replace('__obra__', '[').replace('__cbra__', ']'), value) for key, value in ret.items())) def copy(self): return ParserHelper(scale=self.scale, **self) def __missing__(self, key): return 0.0 def __eq__(self, other): if isinstance(other, self.__class__): return self.scale == other.scale and super(ParserHelper, self).__eq__(other) elif isinstance(other, dict): return self.scale == 1 and super(ParserHelper, self).__eq__(other) elif isinstance(other, string_types): return self == ParserHelper.from_string(other) elif isinstance(other, Number): return self.scale == other and not len(self) else: return False def __ne__(self, other): return not self.__eq__(other) def add(self, key, value): newval = self.__getitem__(key) + value if newval: self.__setitem__(key, newval) else: del self[key] def operate(self, items, op=operator.iadd, cleanup=True): for key, value in items: self[key] = op(self[key], value) if cleanup: keys = [key for key, value in self.items() if value == 0] for key in keys: del self[key] def __str__(self): tmp = '{%s}' % ', '.join(["'{0}': {1}".format(key, value) for key, value in sorted(self.items())]) return '{0} {1}'.format(self.scale, tmp) def __repr__(self): tmp = '{%s}' % ', '.join(["'{0}': {1}".format(key, value) for key, value in sorted(self.items())]) return ''.format(self.scale, tmp) def __mul__(self, other): if isinstance(other, string_types): self.add(other, 1) elif isinstance(other, Number): self.scale *= other elif isinstance(other, self.__class__): self.scale *= other.scale self.operate(other.items()) else: self.operate(other.items()) return self __imul__ = __mul__ __rmul__ = __mul__ def __pow__(self, other): self.scale **= other for key in self.keys(): self[key] *= other return self __ipow__ = __pow__ def __truediv__(self, other): if isinstance(other, string_types): self.add(other, -1) elif isinstance(other, Number): self.scale /= other elif isinstance(other, self.__class__): self.scale /= other.scale self.operate(other.items(), operator.sub) else: self.operate(other.items(), operator.sub) return self __itruediv__ = __truediv__ __floordiv__ = __truediv__ def __rtruediv__(self, other): self.__pow__(-1) if isinstance(other, string_types): self.add(other, 1) elif isinstance(other, Number): self.scale *= other elif isinstance(other, self.__class__): self.scale *= other.scale self.operate(other.items(), operator.add) else: self.operate(other.items(), operator.add) return self #: List of regex substitution pairs. _subs_re = [(r"([\w\.\-\+\*\\\^])\s+", r"\1 "), # merge multiple spaces (r"({0}) squared", r"\1**2"), # Handle square and cube (r"({0}) cubed", r"\1**3"), (r"cubic ({0})", r"\1**3"), (r"square ({0})", r"\1**2"), (r"sq ({0})", r"\1**2"), (r"\b([0-9]+\.?[0-9]*)(?=[e|E][a-zA-Z]|[a-df-zA-DF-Z])", r"\1*"), # Handle numberLetter for multiplication (r"([\w\.\-])\s+(?=\w)", r"\1*"), # Handle space for multiplication ] #: Compiles the regex and replace {0} by a regex that matches an identifier. _subs_re = [(re.compile(a.format(r"[_a-zA-Z][_a-zA-Z0-9]*")), b) for a, b in _subs_re] _pretty_table = maketrans('⁰¹²³⁴⁵⁶⁷⁸⁹·⁻', '0123456789*-') _pretty_exp_re = re.compile(r"⁻?[⁰¹²³⁴⁵⁶⁷⁸⁹]+(?:\.[⁰¹²³⁴⁵⁶⁷⁸⁹]*)?") def string_preprocessor(input_string): input_string = input_string.replace(",", "") input_string = input_string.replace(" per ", "/") for a, b in _subs_re: input_string = a.sub(b, input_string) # Replace pretty format characters for pretty_exp in _pretty_exp_re.findall(input_string): exp = '**' + pretty_exp.translate(_pretty_table) input_string = input_string.replace(pretty_exp, exp) input_string = input_string.translate(_pretty_table) # Handle caret exponentiation input_string = input_string.replace("^", "**") return input_string