Source code for sisl.unit.base

from __future__ import print_function, division

import pyparsing as pp


__all__ = ['unit_group', 'unit_convert', 'unit_default', 'units']


# We do not import anything as it depends on the package.
# Here we only add the conversions according to the
# standard. Other programs may use their units as they
# please with non-standard conversion factors.

unit_table = {
    'mass': {
        'DEFAULT': 'amu',
        'kg': 1.,
        'g': 1.e-3,
        'amu': 1.66054e-27,
        },
    'length': {
        'DEFAULT': 'Ang',
        'm': 1.,
        'cm': 0.01,
        'nm': 1.e-9,
        'pm': 1.e-12,
        'fm': 1.e-15,
        'Ang': 1.e-10,
        'Bohr': 5.29177249e-11,
        },
    'time': {
        'DEFAULT': 'fs',
        's': 1.,
        'ns': 1.e-9,
        'ps': 1.e-12,
        'fs': 1.e-15,
        'min': 60.,
        'hour': 3600.,
        'day': 86400.,
        },
    'energy': {
        'DEFAULT': 'eV',
        'J': 1.,
        'erg': 1.e-7,
        'meV': 1.60217733e-22,
        'eV': 1.60217733e-19,
        'mRy': 2.1798741e-21,
        'Ry': 2.1798741e-18,
        'mHa': 4.3597482e-21,
        'Ha': 4.3597482e-18,
        'Hartree': 4.3597482e-18,
        'K': 1.380648780669e-23,
        },
    'force': {
        'DEFAULT': 'eV/Ang',
        'N': 1.,
        'eV/Ang': 1.60217733e-9,
        }
    }


[docs]def unit_group(unit, tbl=None): """ The group of units that `unit` belong to Parameters ---------- unit : str unit, e.g. kg, Ang, eV etc. returns the type of unit it is. tbl : dict, optional dictionary of units (default to the global table) Examples -------- >>> unit_group('kg') 'mass' >>> unit_group('eV') 'energy' """ if tbl is None: global unit_table tbl = unit_table for k in tbl: if unit in tbl[k]: return k raise ValueError('The unit "'+str(unit)+'" could not be located in the table.')
[docs]def unit_default(group, tbl=None): """ The default unit of the unit group `group`. Parameters ---------- group : str look-up in the table for the default unit. tbl : dict, optional dictionary of units (default to the global table) Examples -------- >>> unit_default('energy') 'eV' """ if tbl is None: global unit_table tbl = unit_table for k in tbl: if group == k: return tbl[k]['DEFAULT'] raise ValueError('The unit-group does not exist!')
[docs]def unit_convert(fr, to, opts=None, tbl=None): """ Factor that takes 'fr' to the units of 'to'. Parameters ---------- fr : str starting unit to : str ending unit opts : dict, optional controls whether the unit conversion is in powers or fractional units tbl : dict, optional dictionary of units (default to the global table) Examples -------- >>> unit_convert('kg','g') 1000.0 >>> unit_convert('eV','J') 1.60217733e-19 """ if tbl is None: global unit_table tbl = unit_table if opts is None: opts = dict() # In the case that the conversion to is None, we should do nothing. frU = 'FromNotFound' frV = None toU = 'ToNotFound' toV = None # Check that the unit types live in the same # space # TODO this currently does not handle if powers are taken into # consideration. for k in tbl: if fr in tbl[k]: frU = k frV = tbl[k][fr] if to in tbl[k]: toU = k toV = tbl[k][to] if frU != toU: raise ValueError('The unit conversion is not from the same group: '+frU+' to '+toU) # Calculate conversion factor val = frV / toV for opt in ['^', 'power', 'p']: if opt in opts: val = val ** opts[opt] for opt in ['*', 'factor', 'fac']: if opt in opts: val = val * opts[opt] for opt in ['/', 'divide', 'div']: if opt in opts: val = val / opts[opt] return val
# From here and on we implement the generalized parser required for # doing complex unit-specifications (i.e. eV/Ang etc.) class UnitParser(object): """ Object for converting between units for a set of unit-tables. Parameters ---------- unit_table : dict a table with the units parsable by the class """ __slots__ = ['_table', '_p_left', '_left', '_p_right', '_right'] def __init__(self, table): self._table = table def convert(fr, to): tbl = self._table for k in tbl: if fr in tbl[k]: if to in tbl[k]: return tbl[k][fr] / tbl[k][to] break raise ValueError('The unit conversion is not from the same group: {} to {}!'.format(fr, to)) def group(unit): tbl = self._table for k in tbl: if unit in tbl[k]: return k raise ValueError('The unit "'+str(unit)+'" could not be located in the table.') def default(group): tbl = self._table k = tbl.get(group, None) if k is None: raise ValueError('The unit-group {} does not exist!'.format(group)) return k['DEFAULT'] self._left = [] self._p_left = self.create_parser(convert, default, group, self._left) self._right = [] self._p_right = self.create_parser(convert, default, group, self._right) @staticmethod def _empty_list(lst): while len(lst) > 0: lst.pop() @staticmethod def create_parser(convert, default, group, group_table=None): """ Routine to internally create a parser with specified unit_convert, unit_default and unit_group routines """ # Any length of characters will be used as a word. if group_table is None: def _convert(t): return convert(t[0], default(group(t[0]))) def _float(t): return float(t[0]) else: def _convert(t): group_table.append(group(t[0])) return convert(t[0], default(group_table[-1])) def _float(t): f = float(t[0]) group_table.append(f) # append nothing return f # The unit extractor unit = pp.Word(pp.alphas).setParseAction(_convert) integer = pp.Word(pp.nums) plusorminus = pp.oneOf('+ -') point = pp.Literal('.') e = pp.CaselessLiteral('E') sign_integer = pp.Combine(pp.Optional(plusorminus) + integer) exponent = pp.Combine(e + sign_integer) number = pp.Or([pp.Combine(point + integer + pp.Optional(exponent)), # .[0-9][E+-[0-9]] pp.Combine(integer + pp.Optional(point + pp.Optional(integer)) + pp.Optional(exponent))] # [0-9].[0-9][E+-[0-9]] ).setParseAction(_float) #def _print_toks(name, op): # """ May be used in pow_op.setParseAction(_print_toks('pow', '^')) to debug """ # def T(t): # print('{}: {}'.format(name, t)) # return op # return T def _fix_toks(op): """ May be used in pow_op.setParseAction(_print_toks('pow', '^')) to debug """ def T(t): return op return T pow_op = pp.oneOf('^ **').setParseAction(lambda t: '^') mul_op = pp.Literal('*') div_op = pp.Literal('/') # Since any space in units are regarded as multiplication this will catch # those instances. base_op = pp.Empty() if group_table is None: def pow_action(toks): return toks[0][0] ** toks[0][2] def mul_action(toks): return toks[0][0] * toks[0][2] def div_action(toks): return toks[0][0] / toks[0][2] def base_action(toks): return toks[0][0] * toks[0][1] else: def pow_action(toks): # Fix table of units group = '{}^{}'.format(group_table[-2], group_table.pop()) group_table[-1] = group #print('^', toks[0], group_table) return toks[0][0] ** toks[0][2] def mul_action(toks): if isinstance(group_table[-2], float): group_table.pop(-2) if isinstance(group_table[-1], float): group_table.pop() #print('*', toks[0], group_table) return toks[0][0] * toks[0][2] def div_action(toks): if isinstance(group_table[-2], float): group_table.pop(-2) if isinstance(group_table[-1], float): group_table.pop() else: group_table[-1] = '/{}'.format(group_table[-1]) #print('/', toks[0]) return toks[0][0] / toks[0][2] def base_action(toks): if isinstance(group_table[-2], float): group_table.pop(-2) if isinstance(group_table[-1], float): group_table.pop() return toks[0][0] * toks[0][1] # We should parse numbers first parser = pp.infixNotation(number | unit, [(pow_op, 2, pp.opAssoc.RIGHT, pow_action), (mul_op, 2, pp.opAssoc.LEFT, mul_action), (div_op, 2, pp.opAssoc.LEFT, div_action), (base_op, 2, pp.opAssoc.LEFT, base_action)]) return parser @staticmethod def same_group(A, B): """ Return true if A and B have the same groups """ A.sort() B.sort() if len(A) != len(B): return False return all(a == b for a, b in zip(A, B)) def _convert(self, A, B): """ Internal routine used to convert unit `A` to unit `B` """ conv_A = self._p_left.parseString(A)[0] conv_B = self._p_right.parseString(B)[0] if not self.same_group(self._left, self._right): # Ensure lists are cleaned (in case the user catches stuff left = list(self._left) right = list(self._right) self._empty_list(self._left) self._empty_list(self._right) raise ValueError('The unit conversion is not from the same group: {} to {}!'.format(left, right)) self._empty_list(self._left) self._empty_list(self._right) return conv_A / conv_B def convert(self, *args): """ Return conversion between the different arguments If 1 parameter is passed a conversion to the default values will be returned. If 2 parameters are passed then a single float will be returned that converts between ``args[0]`` and ``args[1]``. If 3 or more2 parameters are passed then a tuple of floats will be returned where ``tuple[0]`` is the conversion between ``args[0]`` and ``args[1]``, ``tuple[1]`` is the conversion between ``args[1]`` and `args[2]`` and so on. Parameters ---------- *args : list of string units Raises ------ UnitSislError : if the units are not commensurate """ if len(args) == 2: # basic unit conversion return self._convert(args[0], args[1]) elif len(args) == 1: # to default conv = self._p_left(args[0]) self._empty_list(self._left) return conv return [self._convert(args[i], args[i+1]) for i in range(len(args)-1)] def __call__(self, *args): return self.convert(*args) # Create base sisl unit conversion object units = UnitParser(unit_table)