426 lines
13 KiB
Python
426 lines
13 KiB
Python
# -*- 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 '<ParserHelper({0}, {1})>'.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
|