[yt-svn] commit/yt-3.0: 2 new changesets

commits-noreply at bitbucket.org commits-noreply at bitbucket.org
Wed Mar 6 13:36:10 PST 2013


2 new commits in yt-3.0:

https://bitbucket.org/yt_analysis/yt-3.0/commits/1547e8bdcf78/
changeset:   1547e8bdcf78
branch:      yt-3.0
user:        caseywstark
date:        2012-11-09 18:14:56
summary:     Adding symbolic units to yt. This is just a draft of the units module and tests for it. The tests are passing on my machine, but others should check.
affected #:  2 files

diff -r aeff6830301d7f126f40be62e09fd899e3816037 -r 1547e8bdcf7834ab673b6128553e3290d18b2200 yt/utilities/tests/test_units.py
--- /dev/null
+++ b/yt/utilities/tests/test_units.py
@@ -0,0 +1,378 @@
+"""
+Test symbolic unit handling.
+
+Author: Casey W. Stark <caseywstark at gmail.com>
+Affiliation: UC Berkeley
+
+License:
+  Copyright (C) 2012 Casey W. Stark.  All Rights Reserved.
+
+  This file is part of yt.
+
+  yt is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+
+import nose
+from numpy.testing import assert_approx_equal
+from sympy import Symbol
+
+# base dimensions
+from yt.utilities.units import mass, length, time, temperature
+# functions
+from yt.utilities.units import get_conversion_factor, make_symbols_positive
+# classes
+from yt.utilities.units import Unit, UnitParseError
+# objects
+from yt.utilities.units import unit_symbols_dict, unit_prefixes
+
+
+def test_no_conflicting_symbols():
+    """
+    Check unit symbol definitions for conflicts.
+
+    """
+    full_set = set(unit_symbols_dict.keys())
+
+    # go through all possible prefix combos
+    for symbol in unit_symbols_dict.keys():
+        for prefix in unit_prefixes.keys():
+            new_symbol = "%s%s" % (prefix, symbol)
+
+            # test if we have seen this symbol
+            if new_symbol in full_set:
+                print "Duplicate symbol: %s" % new_symbol
+                assert False
+
+            full_set.add(new_symbol)
+
+
+def test_dimensionless():
+    """
+    Create dimensionless unit and check attributes.
+
+    """
+    u1 = Unit()
+
+    assert u1.is_dimensionless
+    assert u1.expr == 1
+    assert u1.cgs_value == 1
+    assert u1.dimensions == 1
+
+#
+# Start init tests
+#
+
+def test_create_from_string():
+    """
+    Create units with strings and check attributes.
+
+    """
+    from yt.utilities.units import energy
+
+    u1 = Unit("g * cm**2 * s**-2")
+    assert u1.dimensions == energy
+    assert u1.cgs_value == 1.0
+
+    # make sure order doesn't matter
+    u2 = Unit("cm**2 * s**-2 * g")
+    assert u2.dimensions == energy
+    assert u2.cgs_value == 1.0
+
+
+def test_create_from_expr():
+    """
+    Create units from sympy Exprs and check attributes.
+
+    """
+    pc_cgs = 3.08568e18
+    yr_cgs = 31536000
+
+    # Symbol expr
+    s1 = Symbol("pc", positive=True)
+    s2 = Symbol("yr", positive=True)
+    # Mul expr
+    s3 = s1 * s2
+    # Pow expr
+    s4  = s1**2 * s2**(-1)
+
+    u1 = Unit(s1)
+    u2 = Unit(s2)
+    u3 = Unit(s3)
+    u4 = Unit(s4)
+
+    assert u1.expr == s1
+    assert u2.expr == s2
+    assert u3.expr == s3
+    assert u4.expr == s4
+
+    assert u1.cgs_value == pc_cgs
+    assert u2.cgs_value == yr_cgs
+    assert u3.cgs_value == pc_cgs * yr_cgs
+    assert u4.cgs_value == pc_cgs**2 / yr_cgs
+
+    assert u1.dimensions == length
+    assert u2.dimensions == time
+    assert u3.dimensions == length * time
+    assert u4.dimensions == length**2 / time
+
+
+def test_create_with_duplicate_dimensions():
+    """
+    Create units with overlapping dimensions. Ex: km/Mpc.
+
+    """
+    from yt.utilities.units import power, rate
+
+    u1 = Unit("erg * s**-1")
+    u2 = Unit("km/s/Mpc")
+    km_cgs = 1e5
+    Mpc_cgs = 3.08568e24
+
+    print u2.expr
+    print u2.dimensions
+    print u2.cgs_value
+
+    assert u1.cgs_value == 1
+    assert u1.dimensions == power
+
+    assert u2.cgs_value == km_cgs / Mpc_cgs
+    assert u2.dimensions == rate
+
+
+def test_create_new_symbol():
+    """
+    Create unit with unknown symbol.
+
+    """
+    u1 = Unit("abc", cgs_value=42, dimensions=(mass/time))
+
+    assert u1.expr == Symbol("abc", positive=True)
+    assert u1.cgs_value == 42
+    assert u1.dimensions == mass / time
+
+
+def test_create_fail_on_unknown_symbol():
+    """
+    Fail to create unit with unknown symbol, without cgs_value and dimensions.
+
+    """
+    try:
+        u1 = Unit(Symbol("jigawatts"))
+    except UnitParseError:
+        pass
+    else:
+        assert False
+
+
+def test_create_fail_on_bad_symbol_type():
+    """
+    Fail to create unit with bad symbol type.
+
+    """
+    try:
+        u1 = Unit([1])  # something other than Expr and str
+    except UnitParseError:
+        pass
+    else:
+        assert False
+
+
+def test_create_fail_on_bad_dimensions_type():
+    """
+    Fail to create unit with bad dimensions type.
+
+    """
+    try:
+        u1 = Unit("a", cgs_value=1, dimensions="(mass)")
+    except UnitParseError:
+        pass
+    else:
+        assert False
+
+
+def test_create_fail_on_dimensions_content():
+    """
+    Fail to create unit with bad dimensions expr.
+
+    """
+    a = Symbol("a")
+
+    try:
+        u1 = Unit("a", cgs_value=1, dimensions=a)
+    except UnitParseError:
+        pass
+    else:
+        assert False
+
+
+def test_create_fail_on_cgs_value_type():
+    """
+    Fail to create unit with bad cgs_value type.
+
+    """
+    try:
+        u1 = Unit("a", cgs_value="a", dimensions=(mass/time))
+    except UnitParseError:
+        pass
+    else:
+        assert False
+
+#
+# End init tests
+#
+
+def test_string_representation():
+    """
+    Check unit string representation.
+
+    """
+    pc = Unit("pc")
+    Myr = Unit("Myr")
+    speed = pc / Myr
+    dimensionless = Unit()
+
+    assert str(pc) == "pc"
+    assert str(Myr) == "Myr"
+    assert str(speed) == "pc/Myr"
+    assert repr(speed) == "pc/Myr"
+    assert str(dimensionless) == "(dimensionless)"
+
+#
+# Start operation tests
+#
+
+def test_multiplication():
+    """
+    Multiply two units.
+
+    """
+    msun_cgs = 1.98892e33
+    pc_cgs = 3.08568e18
+
+    # Create symbols
+    msun_sym = Symbol("Msun", positive=True)
+    pc_sym = Symbol("pc", positive=True)
+    s_sym = Symbol("s", positive=True)
+
+    # Create units
+    u1 = Unit("Msun")
+    u2 = Unit("pc")
+
+    # Mul operation
+    u3 = u1 * u2
+
+    assert u3.expr == msun_sym * pc_sym
+    assert u3.cgs_value == msun_cgs * pc_cgs
+    assert u3.dimensions == mass * length
+
+    # Pow and Mul operations
+    u4 = Unit("pc**2")
+    u5 = Unit("Msun * s")
+
+    u6 = u4 * u5
+
+    assert u6.expr == pc_sym**2 * msun_sym * s_sym
+    assert u6.cgs_value == pc_cgs**2 * msun_cgs
+    assert u6.dimensions == length**2 * mass * time
+
+
+def test_division():
+    """
+    Divide two units.
+
+    """
+    pc_cgs = 3.08568e18
+    km_cgs = 1e5
+
+    # Create symbols
+    pc_sym = Symbol("pc", positive=True)
+    km_sym = Symbol("km", positive=True)
+    s_sym = Symbol("s", positive=True)
+
+    # Create units
+    u1 = Unit("pc")
+    u2 = Unit("km * s")
+
+    u3 = u1 / u2
+
+    assert u3.expr == pc_sym / (km_sym * s_sym)
+    assert u3.cgs_value == pc_cgs / km_cgs
+    assert u3.dimensions == 1 / time
+
+
+def test_power():
+    """
+    Take units to some power.
+
+    """
+    from sympy import nsimplify
+
+    pc_cgs = 3.08568e18
+    mK_cgs = 1e-3
+    u1_dims = mass * length**2 * time**-3 * temperature**4
+    u1 = Unit("g * pc**2 * s**-3 * mK**4")
+
+    u2 = u1**2
+
+    assert u2.dimensions == u1_dims**2
+    assert u2.cgs_value == (pc_cgs**2 * mK_cgs**4)**2
+
+    u3 = u1**(-1.0/3)
+
+    assert u3.dimensions == nsimplify(u1_dims**(-1.0/3))
+    assert u3.cgs_value == (pc_cgs**2 * mK_cgs**4)**(-1.0/3)
+
+
+def test_equality():
+    """
+    Check unit equality with different symbols, but same dimensions and cgs_value.
+
+    """
+    u1 = Unit("km * s**-1")
+    u2 = Unit("m * ms**-1")
+
+    assert u1 == u2
+
+#
+# End operation tests.
+#
+
+def test_cgs_equivalent():
+    """
+    Check cgs equivalent of a unit.
+
+    """
+    Msun_cgs = 1.98892e33
+    Mpc_cgs = 3.08568e24
+
+    u1 = Unit("Msun * Mpc**-3")
+    u2 = Unit("g * cm**-3")
+    u3 = u1.get_cgs_equivalent()
+
+    assert u2.expr == u3.expr
+    assert u2 == u3
+
+    assert_approx_equal( u1.cgs_value, Msun_cgs / Mpc_cgs**3, significant=16)
+    assert u2.cgs_value == 1
+    assert u3.cgs_value == 1
+
+    mass_density = mass / length**3
+
+    assert u1.dimensions == mass_density
+    assert u2.dimensions == mass_density
+    assert u3.dimensions == mass_density
+
+    assert_approx_equal( get_conversion_factor(u1, u3), Msun_cgs / Mpc_cgs**3,
+                         significant=16 )

diff -r aeff6830301d7f126f40be62e09fd899e3816037 -r 1547e8bdcf7834ab673b6128553e3290d18b2200 yt/utilities/units.py
--- /dev/null
+++ b/yt/utilities/units.py
@@ -0,0 +1,448 @@
+"""
+Symbolic unit handling.
+
+Author: Casey W. Stark <caseywstark at gmail.com>
+Affiliation: UC Berkeley
+
+License:
+  Copyright (C) 2012 Casey W. Stark.  All Rights Reserved.
+
+  This file is part of yt.
+
+  yt is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+
+from sympy import Expr, Mul, Number, Pow, Rational, Symbol
+from sympy import nsimplify, posify, sympify
+from sympy.parsing.sympy_parser import parse_expr
+
+# The base dimensions
+mass = Symbol("(mass)", positive=True)
+length = Symbol("(length)", positive=True)
+time = Symbol("(time)", positive=True)
+temperature = Symbol("(temperature)", positive=True)
+base_dimensions = [mass, length, time, temperature]
+
+### Misc. dimensions
+rate = 1 / time
+
+velocity     = length / time
+acceleration = length / time**2
+jerk         = length / time**3
+snap         = length / time**4
+crackle      = length / time**5
+pop          = length / time**6
+
+momentum = mass * velocity
+force    = mass * acceleration
+energy   = force * length
+power    = energy / time
+charge   = (energy * length)**Rational(1, 2)  # proper 1/2 power
+
+electric_field = charge / length**2
+magnetic_field = electric_field
+
+# Dictionary that holds information of known unit symbols.
+# The key is the symbol, the value is a tuple with the conversion factor to cgs,
+# and the dimensionality.
+unit_symbols_dict = {
+    # base
+    "g":  (1.0, mass),
+    #"cm": (1.0, length),  # duplicate with meter below...
+    "s":  (1.0, time),
+    "K":  (1.0, temperature),
+
+    # other cgs
+    "dyne": (1.0, force),
+    "erg":  (1.0, energy),
+    "esu":  (1.0, charge),
+
+    # some SI
+    "m": (1.0e2, length),
+    "J": (1.0e7, energy),
+    "Hz": (1.0, rate),
+
+    # times
+    "min": (60.0, time),
+    "hr":  (3600.0, time),
+    "day": (86400.0, time),
+    "yr":  (31536000.0, time),
+
+    # Solar units
+    "Msun": (1.98892e33, mass),
+    "Rsun": (6.96e10, length),
+    "Lsun": (3.9e33, power),
+    "Tsun": (5870.0, temperature),
+
+    # astro distances
+    "AU": (1.49598e13, length),
+    "ly": (9.46053e17, length),
+    "pc": (3.08568e18, length),
+
+    # other astro
+    "H_0": (2.3e-18, rate),  # check cf
+
+    # other energy units
+    "eV": (1.6021766e-12, energy),
+
+    # electric stuff
+    "gauss": (1.0, magnetic_field),
+}
+
+# This dictionary formatting from magnitude package, credit to Juan Reyero.
+unit_prefixes = {
+    'Y': 1e24,   # yotta
+    'Z': 1e21,   # zetta
+    'E': 1e18,   # exa
+    'P': 1e15,   # peta
+    'T': 1e12,   # tera
+    'G': 1e9,    # giga
+    'M': 1e6,    # mega
+    'k': 1e3,    # kilo
+    'd': 1e1,    # deci
+    'c': 1e-2,   # centi
+    'm': 1e-3,   # mili
+    'u': 1e-6,   # micro
+    'n': 1e-9,   # nano
+    'p': 1e-12,  # pico
+    'f': 1e-15,  # femto
+    'a': 1e-18,  # atto
+    'z': 1e-21,  # zepto
+    'y': 1e-24,  # yocto
+}
+
+
+class UnitParseError(Exception):
+    pass
+class UnitOperationError(Exception):
+    pass
+
+
+class Unit(Expr):
+    """
+    A symbolic unit, using sympy functionality. We only add "dimensions" so that
+    sympy understands relations between different units.
+
+    """
+
+    is_positive = True    # make sqrt(m**2) --> m
+    is_commutative = True
+    is_number = False
+
+    __slots__ = ["expr", "cgs_value", "dimensions", "is_atomic"]
+
+    def __new__(cls, unit_expr=sympify(1), cgs_value=None, dimensions=None,
+                **assumptions):
+        """
+        Create a new unit. May be an atomic unit (like a gram) or a combination
+        of other units (like g / cm**3).
+
+        Parameters
+        ----------
+        unit_expr : string or sympy.core.expr.Expr
+            The unit symbol expression.
+        cgs_value : float
+            This unit's value in cgs.
+        dimensions : sympy.core.expr.Expr
+            An expression representing the dimensionality of this unit. This
+            should be a product (sympy.core.mul.Mul object) of mass, length,
+            time, and temperature objects to various powers.
+
+        """
+        # if we have a string, parse into an expression
+        if isinstance(unit_expr, str):
+            unit_expr = parse_expr(unit_expr)
+
+        if not isinstance(unit_expr, Expr):
+            raise UnitParseError("Unit representation must be a string or sympy Expr. %s is a %s." % (unit_expr, type(unit_expr)))
+        # done with argument checking...
+
+        # sympify, make positive symbols, and nsimplify the expr
+        unit_expr = sympify(unit_expr)
+        unit_expr = make_symbols_positive(unit_expr)
+        unit_expr = nsimplify(unit_expr)
+
+        # see if the unit is atomic.
+        is_atomic = False
+        if isinstance(unit_expr, Symbol):
+            is_atomic = True
+
+        # Did the user supply cgs_value and dimensions?
+        if cgs_value and not dimensions or dimensions and not cgs_value:
+            raise Exception("If you provide cgs_vale or dimensions, you must provide both. cgs_value is %s, dimensions is %s." % (cgs_value, dimensions))
+
+        if cgs_value and dimensions:
+            # check that cgs_value is a float or can be converted to one
+            try:
+                cgs_value = float(cgs_value)
+            except ValueError:
+                raise UnitParseError("Please provide a float for the cgs_value kwarg. I got '%s'." % cgs_value)
+            # check that dimensions is valid
+            dimensions = verify_dimensions(dimensions)
+            # save the values
+            this_cgs_value, this_dimensions = cgs_value, dimensions
+
+        else:  # lookup the unit symbols
+            this_cgs_value, this_dimensions = \
+                get_unit_data_from_expr(unit_expr)
+
+        # Trick to get dimensions powers as Rationals
+        this_dimensions = nsimplify(this_dimensions)
+
+        # create obj with superclass construct
+        obj = Expr.__new__(cls, **assumptions)
+
+        # attach attributes to obj
+        obj.expr = unit_expr
+        obj.is_atomic = is_atomic
+        obj.cgs_value = this_cgs_value
+        obj.dimensions = this_dimensions
+
+        # return `obj` so __init__ can handle it.
+        return obj
+
+    ### some sympy conventions I guess
+    def __getnewargs__(self):
+        return (self.expr, self.is_atomic, self.cgs_value, self.dimensions)
+
+    def __hash__(self):
+        return super(Unit, self).__hash__()
+
+    def _hashable_content(self):
+        return (self.expr, self.is_atomic, self.cgs_value, self.dimensions)
+    ### end sympy conventions
+
+    def __repr__(self):
+        if self.expr == 1:
+            return "(dimensionless)"
+        return str(self.expr)
+
+    def __str__(self):
+        if self.expr == 1:
+            return "(dimensionless)"
+        return str(self.expr)
+
+    # for sympy.printing
+    def _sympystr(self, *args):
+        return str(self.expr)
+
+    ### override sympy operations
+    def __mul__(self, u):
+        """ Multiply Unit with u (Unit object). """
+        if not isinstance(u, Unit):
+            raise UnitOperationError("Tried to multiply Unit object with '%s'. This behavior is undefined." % u)
+
+        return Unit(self.expr * u.expr, self.cgs_value * u.cgs_value,
+                    self.dimensions * u.dimensions)
+
+    def __div__(self, u):
+        """ Divide Unit by u (Unit object). """
+        if not isinstance(u, Unit):
+            raise UnitOperationError("Tried to divide Unit object by '%s'. This behavior is undefined." % u)
+
+        return Unit(self.expr / u.expr, self.cgs_value / u.cgs_value,
+                    self.dimensions / u.dimensions)
+
+    def __pow__(self, p):
+        """ Take Unit to power p (float). """
+        try:
+            p = sympify(p)
+        except ValueError:
+            raise UnitOperationError("Tried to take Unit object to the power '%s'. I could not cast this to a float." % p)
+
+        return Unit(self.expr**p, self.cgs_value**p, self.dimensions**p)
+
+    ### Comparison operators
+    def same_dimensions_as(self, other_unit):
+        """ Test if dimensions are the same. """
+        return (self.dimensions / other_unit.dimensions) == 1
+
+    def __eq__(self, u):
+        """ Test unit equality. """
+        if not isinstance(u, Unit):
+            raise UnitOperationError("Tried to test equality between Unit object and '%s'. This behavior is undefined." % u)
+
+        return (self.cgs_value == u.cgs_value and self.dimensions == u.dimensions)
+
+    @property
+    def is_dimensionless(self):
+        return self.dimensions == 1
+
+    def get_cgs_equivalent(self):
+        """ Create and return dimensionally-equivalent cgs units. """
+        cgs_units_string = "g**(%s) * cm**(%s) * s**(%s) * K**(%s)" % \
+            (self.dimensions.expand().as_coeff_exponent(mass)[1],
+             self.dimensions.expand().as_coeff_exponent(length)[1],
+             self.dimensions.expand().as_coeff_exponent(time)[1],
+             self.dimensions.expand().as_coeff_exponent(temperature)[1])
+        return Unit(cgs_units_string, 1, self.dimensions)
+
+
+def make_symbols_positive(expr):
+    """
+    Grabs all symbols from expr, makes new positive symbols with the same names,
+    and substitutes them back into the expression.
+
+    """
+    expr_symbols = expr.atoms(Symbol)  # grab all symbols
+
+    # Replace one at a time
+    for s in expr_symbols:
+        # replace this symbol with a positive version
+        expr = expr.subs(s, Symbol(s.name, positive=True))
+
+    return expr
+
+
+# @todo: simpler method that doesn't use recursion would be better...
+# We could check if dimensions.atoms are all numbers or symbols, but we should
+# check the functions also...
+def verify_dimensions(d):
+    """
+    Make sure that `d` is a valid dimension expression. It must consist of only
+    the base dimension symbols, to powers, multiplied together. If valid, return
+    the simplified expression. If not, raise an Exception.
+
+    """
+    # in the case of a Number of Symbol, we can just return
+    if isinstance(d, Number):
+        return d
+    elif isinstance(d, Symbol):
+        if d in base_dimensions:
+            return d
+        else:
+            raise UnitParseError("dimensionality expression contains an unknown symbol '%s'." % d)
+
+    # validate args of a Pow or Mul separately
+    elif isinstance(d, Pow):
+        return verify_dimensions(d.args[0])**verify_dimensions(d.args[1])
+
+    elif isinstance(d, Mul):
+        total_mul = 1
+        for arg in d.args:
+            total_mul *= verify_dimensions(arg)
+        return total_mul
+
+    # should never get here
+    raise UnitParseError("Bad dimensionality expression '%s'." % d)
+
+
+def get_unit_data_from_expr(unit_expr):
+    """
+    Gets total cgs_value and dimensions from a valid unit expression.
+
+    """
+    # The simplest case first
+    if isinstance(unit_expr, Unit):
+        return (unit_expr.cgs_value, unit_expr.dimensions)
+
+    # Now for the sympy possibilities
+    if isinstance(unit_expr, Symbol):
+        return lookup_unit_symbol(str(unit_expr))
+
+    if isinstance(unit_expr, Number):
+        return (1, 1)
+
+    if isinstance(unit_expr, Pow):
+        unit_data = get_unit_data_from_expr(unit_expr.args[0])
+        power = unit_expr.args[1]
+        return (unit_data[0]**power, unit_data[1]**power)
+
+    if isinstance(unit_expr, Mul):
+        cgs_value = 1
+        dimensions = 1
+        for expr in unit_expr.args:
+            unit_data = get_unit_data_from_expr(expr)
+            cgs_value *= unit_data[0]
+            dimensions *= unit_data[1]
+
+        return (cgs_value, dimensions)
+
+    raise UnitParseError("Cannot parse for unit data from '%s'. Please supply an expression of only Unit, Symbol, Pow, and Mul objects." % str(unit_expr))
+
+
+def lookup_unit_symbol(symbol_str):
+    """ Searches for the unit data typle corresponding to the given symbol. """
+
+    if symbol_str in unit_symbols_dict:
+        # lookup successful, return the tuple directly
+        return unit_symbols_dict[symbol_str]
+
+    # could still be a known symbol with a prefix
+    possible_prefix = symbol_str[0]
+    if possible_prefix in unit_prefixes:
+        # the first character could be a prefix, check the rest of the symbol
+        symbol_wo_prefix = symbol_str[1:]
+
+        if symbol_wo_prefix in unit_symbols_dict:
+            # lookup successful, it's a symbol with a prefix
+            unit_data = unit_symbols_dict[symbol_wo_prefix]
+            prefix_value = unit_prefixes[possible_prefix]
+
+            # don't forget to account for the prefix value!
+            return (unit_data[0] * prefix_value, unit_data[1])
+
+    # no dice
+    raise UnitParseError("Could not find unit symbol '%s'. Please supply the dimensions and cgs value when creating this object." % symbol_str)
+
+
+def get_conversion_factor(old_units, new_units):
+    """
+    Get the conversion factor between two units of equivalent dimensions. This
+    is the number you multiply data by to convert from values in `old_units` to
+    values in `new_units`.
+
+    Parameters
+    ----------
+    old_units: str or Unit object
+        The current units.
+    new_units : str or Unit object
+        The units we want.
+
+    Returns
+    -------
+    conversion_factor : float
+        `old_units / new_units`
+
+    """
+    # Make units out of strings if we need to.
+    if isinstance(old_units, str):
+        old_units = Unit(old_units)
+    if isinstance(new_units, str):
+        new_units = Unit(new_units)
+
+    if not old_units.same_dimensions_as(new_units):
+        raise UnitOperationError("Cannot convert from %s to %s because the dimensions do not match: %s and %s" % (old_units, new_units, old_units.dimensions, new_units.dimensions))
+
+    return old_units.cgs_value / new_units.cgs_value
+
+
+def convert_values(values, old_units, new_units):
+    """
+    Take data given in old units and convert to values in new units.
+
+    Parameters
+    ----------
+    values : array_like
+        The number or array we will convert.
+    old_units : str or Unit object
+        The units values are supplied in.
+    new_units : str or Unit object
+        The units values will be returned in.
+
+    Returns values in new units.
+
+    """
+    return values * get_conversion_factor(old_units, new_units)


https://bitbucket.org/yt_analysis/yt-3.0/commits/50e6460dade1/
changeset:   50e6460dade1
branch:      yt-3.0
user:        MatthewTurk
date:        2013-03-06 22:35:59
summary:     Merging Casey's changes
affected #:  2 files

diff -r 162feb61f27ac820077d7bc9bd81e92cb5a414c2 -r 50e6460dade1bc6cdd62012f7427f5c079f2fbe3 yt/utilities/tests/test_units.py
--- /dev/null
+++ b/yt/utilities/tests/test_units.py
@@ -0,0 +1,378 @@
+"""
+Test symbolic unit handling.
+
+Author: Casey W. Stark <caseywstark at gmail.com>
+Affiliation: UC Berkeley
+
+License:
+  Copyright (C) 2012 Casey W. Stark.  All Rights Reserved.
+
+  This file is part of yt.
+
+  yt is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+
+import nose
+from numpy.testing import assert_approx_equal
+from sympy import Symbol
+
+# base dimensions
+from yt.utilities.units import mass, length, time, temperature
+# functions
+from yt.utilities.units import get_conversion_factor, make_symbols_positive
+# classes
+from yt.utilities.units import Unit, UnitParseError
+# objects
+from yt.utilities.units import unit_symbols_dict, unit_prefixes
+
+
+def test_no_conflicting_symbols():
+    """
+    Check unit symbol definitions for conflicts.
+
+    """
+    full_set = set(unit_symbols_dict.keys())
+
+    # go through all possible prefix combos
+    for symbol in unit_symbols_dict.keys():
+        for prefix in unit_prefixes.keys():
+            new_symbol = "%s%s" % (prefix, symbol)
+
+            # test if we have seen this symbol
+            if new_symbol in full_set:
+                print "Duplicate symbol: %s" % new_symbol
+                assert False
+
+            full_set.add(new_symbol)
+
+
+def test_dimensionless():
+    """
+    Create dimensionless unit and check attributes.
+
+    """
+    u1 = Unit()
+
+    assert u1.is_dimensionless
+    assert u1.expr == 1
+    assert u1.cgs_value == 1
+    assert u1.dimensions == 1
+
+#
+# Start init tests
+#
+
+def test_create_from_string():
+    """
+    Create units with strings and check attributes.
+
+    """
+    from yt.utilities.units import energy
+
+    u1 = Unit("g * cm**2 * s**-2")
+    assert u1.dimensions == energy
+    assert u1.cgs_value == 1.0
+
+    # make sure order doesn't matter
+    u2 = Unit("cm**2 * s**-2 * g")
+    assert u2.dimensions == energy
+    assert u2.cgs_value == 1.0
+
+
+def test_create_from_expr():
+    """
+    Create units from sympy Exprs and check attributes.
+
+    """
+    pc_cgs = 3.08568e18
+    yr_cgs = 31536000
+
+    # Symbol expr
+    s1 = Symbol("pc", positive=True)
+    s2 = Symbol("yr", positive=True)
+    # Mul expr
+    s3 = s1 * s2
+    # Pow expr
+    s4  = s1**2 * s2**(-1)
+
+    u1 = Unit(s1)
+    u2 = Unit(s2)
+    u3 = Unit(s3)
+    u4 = Unit(s4)
+
+    assert u1.expr == s1
+    assert u2.expr == s2
+    assert u3.expr == s3
+    assert u4.expr == s4
+
+    assert u1.cgs_value == pc_cgs
+    assert u2.cgs_value == yr_cgs
+    assert u3.cgs_value == pc_cgs * yr_cgs
+    assert u4.cgs_value == pc_cgs**2 / yr_cgs
+
+    assert u1.dimensions == length
+    assert u2.dimensions == time
+    assert u3.dimensions == length * time
+    assert u4.dimensions == length**2 / time
+
+
+def test_create_with_duplicate_dimensions():
+    """
+    Create units with overlapping dimensions. Ex: km/Mpc.
+
+    """
+    from yt.utilities.units import power, rate
+
+    u1 = Unit("erg * s**-1")
+    u2 = Unit("km/s/Mpc")
+    km_cgs = 1e5
+    Mpc_cgs = 3.08568e24
+
+    print u2.expr
+    print u2.dimensions
+    print u2.cgs_value
+
+    assert u1.cgs_value == 1
+    assert u1.dimensions == power
+
+    assert u2.cgs_value == km_cgs / Mpc_cgs
+    assert u2.dimensions == rate
+
+
+def test_create_new_symbol():
+    """
+    Create unit with unknown symbol.
+
+    """
+    u1 = Unit("abc", cgs_value=42, dimensions=(mass/time))
+
+    assert u1.expr == Symbol("abc", positive=True)
+    assert u1.cgs_value == 42
+    assert u1.dimensions == mass / time
+
+
+def test_create_fail_on_unknown_symbol():
+    """
+    Fail to create unit with unknown symbol, without cgs_value and dimensions.
+
+    """
+    try:
+        u1 = Unit(Symbol("jigawatts"))
+    except UnitParseError:
+        pass
+    else:
+        assert False
+
+
+def test_create_fail_on_bad_symbol_type():
+    """
+    Fail to create unit with bad symbol type.
+
+    """
+    try:
+        u1 = Unit([1])  # something other than Expr and str
+    except UnitParseError:
+        pass
+    else:
+        assert False
+
+
+def test_create_fail_on_bad_dimensions_type():
+    """
+    Fail to create unit with bad dimensions type.
+
+    """
+    try:
+        u1 = Unit("a", cgs_value=1, dimensions="(mass)")
+    except UnitParseError:
+        pass
+    else:
+        assert False
+
+
+def test_create_fail_on_dimensions_content():
+    """
+    Fail to create unit with bad dimensions expr.
+
+    """
+    a = Symbol("a")
+
+    try:
+        u1 = Unit("a", cgs_value=1, dimensions=a)
+    except UnitParseError:
+        pass
+    else:
+        assert False
+
+
+def test_create_fail_on_cgs_value_type():
+    """
+    Fail to create unit with bad cgs_value type.
+
+    """
+    try:
+        u1 = Unit("a", cgs_value="a", dimensions=(mass/time))
+    except UnitParseError:
+        pass
+    else:
+        assert False
+
+#
+# End init tests
+#
+
+def test_string_representation():
+    """
+    Check unit string representation.
+
+    """
+    pc = Unit("pc")
+    Myr = Unit("Myr")
+    speed = pc / Myr
+    dimensionless = Unit()
+
+    assert str(pc) == "pc"
+    assert str(Myr) == "Myr"
+    assert str(speed) == "pc/Myr"
+    assert repr(speed) == "pc/Myr"
+    assert str(dimensionless) == "(dimensionless)"
+
+#
+# Start operation tests
+#
+
+def test_multiplication():
+    """
+    Multiply two units.
+
+    """
+    msun_cgs = 1.98892e33
+    pc_cgs = 3.08568e18
+
+    # Create symbols
+    msun_sym = Symbol("Msun", positive=True)
+    pc_sym = Symbol("pc", positive=True)
+    s_sym = Symbol("s", positive=True)
+
+    # Create units
+    u1 = Unit("Msun")
+    u2 = Unit("pc")
+
+    # Mul operation
+    u3 = u1 * u2
+
+    assert u3.expr == msun_sym * pc_sym
+    assert u3.cgs_value == msun_cgs * pc_cgs
+    assert u3.dimensions == mass * length
+
+    # Pow and Mul operations
+    u4 = Unit("pc**2")
+    u5 = Unit("Msun * s")
+
+    u6 = u4 * u5
+
+    assert u6.expr == pc_sym**2 * msun_sym * s_sym
+    assert u6.cgs_value == pc_cgs**2 * msun_cgs
+    assert u6.dimensions == length**2 * mass * time
+
+
+def test_division():
+    """
+    Divide two units.
+
+    """
+    pc_cgs = 3.08568e18
+    km_cgs = 1e5
+
+    # Create symbols
+    pc_sym = Symbol("pc", positive=True)
+    km_sym = Symbol("km", positive=True)
+    s_sym = Symbol("s", positive=True)
+
+    # Create units
+    u1 = Unit("pc")
+    u2 = Unit("km * s")
+
+    u3 = u1 / u2
+
+    assert u3.expr == pc_sym / (km_sym * s_sym)
+    assert u3.cgs_value == pc_cgs / km_cgs
+    assert u3.dimensions == 1 / time
+
+
+def test_power():
+    """
+    Take units to some power.
+
+    """
+    from sympy import nsimplify
+
+    pc_cgs = 3.08568e18
+    mK_cgs = 1e-3
+    u1_dims = mass * length**2 * time**-3 * temperature**4
+    u1 = Unit("g * pc**2 * s**-3 * mK**4")
+
+    u2 = u1**2
+
+    assert u2.dimensions == u1_dims**2
+    assert u2.cgs_value == (pc_cgs**2 * mK_cgs**4)**2
+
+    u3 = u1**(-1.0/3)
+
+    assert u3.dimensions == nsimplify(u1_dims**(-1.0/3))
+    assert u3.cgs_value == (pc_cgs**2 * mK_cgs**4)**(-1.0/3)
+
+
+def test_equality():
+    """
+    Check unit equality with different symbols, but same dimensions and cgs_value.
+
+    """
+    u1 = Unit("km * s**-1")
+    u2 = Unit("m * ms**-1")
+
+    assert u1 == u2
+
+#
+# End operation tests.
+#
+
+def test_cgs_equivalent():
+    """
+    Check cgs equivalent of a unit.
+
+    """
+    Msun_cgs = 1.98892e33
+    Mpc_cgs = 3.08568e24
+
+    u1 = Unit("Msun * Mpc**-3")
+    u2 = Unit("g * cm**-3")
+    u3 = u1.get_cgs_equivalent()
+
+    assert u2.expr == u3.expr
+    assert u2 == u3
+
+    assert_approx_equal( u1.cgs_value, Msun_cgs / Mpc_cgs**3, significant=16)
+    assert u2.cgs_value == 1
+    assert u3.cgs_value == 1
+
+    mass_density = mass / length**3
+
+    assert u1.dimensions == mass_density
+    assert u2.dimensions == mass_density
+    assert u3.dimensions == mass_density
+
+    assert_approx_equal( get_conversion_factor(u1, u3), Msun_cgs / Mpc_cgs**3,
+                         significant=16 )

diff -r 162feb61f27ac820077d7bc9bd81e92cb5a414c2 -r 50e6460dade1bc6cdd62012f7427f5c079f2fbe3 yt/utilities/units.py
--- /dev/null
+++ b/yt/utilities/units.py
@@ -0,0 +1,448 @@
+"""
+Symbolic unit handling.
+
+Author: Casey W. Stark <caseywstark at gmail.com>
+Affiliation: UC Berkeley
+
+License:
+  Copyright (C) 2012 Casey W. Stark.  All Rights Reserved.
+
+  This file is part of yt.
+
+  yt is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+
+from sympy import Expr, Mul, Number, Pow, Rational, Symbol
+from sympy import nsimplify, posify, sympify
+from sympy.parsing.sympy_parser import parse_expr
+
+# The base dimensions
+mass = Symbol("(mass)", positive=True)
+length = Symbol("(length)", positive=True)
+time = Symbol("(time)", positive=True)
+temperature = Symbol("(temperature)", positive=True)
+base_dimensions = [mass, length, time, temperature]
+
+### Misc. dimensions
+rate = 1 / time
+
+velocity     = length / time
+acceleration = length / time**2
+jerk         = length / time**3
+snap         = length / time**4
+crackle      = length / time**5
+pop          = length / time**6
+
+momentum = mass * velocity
+force    = mass * acceleration
+energy   = force * length
+power    = energy / time
+charge   = (energy * length)**Rational(1, 2)  # proper 1/2 power
+
+electric_field = charge / length**2
+magnetic_field = electric_field
+
+# Dictionary that holds information of known unit symbols.
+# The key is the symbol, the value is a tuple with the conversion factor to cgs,
+# and the dimensionality.
+unit_symbols_dict = {
+    # base
+    "g":  (1.0, mass),
+    #"cm": (1.0, length),  # duplicate with meter below...
+    "s":  (1.0, time),
+    "K":  (1.0, temperature),
+
+    # other cgs
+    "dyne": (1.0, force),
+    "erg":  (1.0, energy),
+    "esu":  (1.0, charge),
+
+    # some SI
+    "m": (1.0e2, length),
+    "J": (1.0e7, energy),
+    "Hz": (1.0, rate),
+
+    # times
+    "min": (60.0, time),
+    "hr":  (3600.0, time),
+    "day": (86400.0, time),
+    "yr":  (31536000.0, time),
+
+    # Solar units
+    "Msun": (1.98892e33, mass),
+    "Rsun": (6.96e10, length),
+    "Lsun": (3.9e33, power),
+    "Tsun": (5870.0, temperature),
+
+    # astro distances
+    "AU": (1.49598e13, length),
+    "ly": (9.46053e17, length),
+    "pc": (3.08568e18, length),
+
+    # other astro
+    "H_0": (2.3e-18, rate),  # check cf
+
+    # other energy units
+    "eV": (1.6021766e-12, energy),
+
+    # electric stuff
+    "gauss": (1.0, magnetic_field),
+}
+
+# This dictionary formatting from magnitude package, credit to Juan Reyero.
+unit_prefixes = {
+    'Y': 1e24,   # yotta
+    'Z': 1e21,   # zetta
+    'E': 1e18,   # exa
+    'P': 1e15,   # peta
+    'T': 1e12,   # tera
+    'G': 1e9,    # giga
+    'M': 1e6,    # mega
+    'k': 1e3,    # kilo
+    'd': 1e1,    # deci
+    'c': 1e-2,   # centi
+    'm': 1e-3,   # mili
+    'u': 1e-6,   # micro
+    'n': 1e-9,   # nano
+    'p': 1e-12,  # pico
+    'f': 1e-15,  # femto
+    'a': 1e-18,  # atto
+    'z': 1e-21,  # zepto
+    'y': 1e-24,  # yocto
+}
+
+
+class UnitParseError(Exception):
+    pass
+class UnitOperationError(Exception):
+    pass
+
+
+class Unit(Expr):
+    """
+    A symbolic unit, using sympy functionality. We only add "dimensions" so that
+    sympy understands relations between different units.
+
+    """
+
+    is_positive = True    # make sqrt(m**2) --> m
+    is_commutative = True
+    is_number = False
+
+    __slots__ = ["expr", "cgs_value", "dimensions", "is_atomic"]
+
+    def __new__(cls, unit_expr=sympify(1), cgs_value=None, dimensions=None,
+                **assumptions):
+        """
+        Create a new unit. May be an atomic unit (like a gram) or a combination
+        of other units (like g / cm**3).
+
+        Parameters
+        ----------
+        unit_expr : string or sympy.core.expr.Expr
+            The unit symbol expression.
+        cgs_value : float
+            This unit's value in cgs.
+        dimensions : sympy.core.expr.Expr
+            An expression representing the dimensionality of this unit. This
+            should be a product (sympy.core.mul.Mul object) of mass, length,
+            time, and temperature objects to various powers.
+
+        """
+        # if we have a string, parse into an expression
+        if isinstance(unit_expr, str):
+            unit_expr = parse_expr(unit_expr)
+
+        if not isinstance(unit_expr, Expr):
+            raise UnitParseError("Unit representation must be a string or sympy Expr. %s is a %s." % (unit_expr, type(unit_expr)))
+        # done with argument checking...
+
+        # sympify, make positive symbols, and nsimplify the expr
+        unit_expr = sympify(unit_expr)
+        unit_expr = make_symbols_positive(unit_expr)
+        unit_expr = nsimplify(unit_expr)
+
+        # see if the unit is atomic.
+        is_atomic = False
+        if isinstance(unit_expr, Symbol):
+            is_atomic = True
+
+        # Did the user supply cgs_value and dimensions?
+        if cgs_value and not dimensions or dimensions and not cgs_value:
+            raise Exception("If you provide cgs_vale or dimensions, you must provide both. cgs_value is %s, dimensions is %s." % (cgs_value, dimensions))
+
+        if cgs_value and dimensions:
+            # check that cgs_value is a float or can be converted to one
+            try:
+                cgs_value = float(cgs_value)
+            except ValueError:
+                raise UnitParseError("Please provide a float for the cgs_value kwarg. I got '%s'." % cgs_value)
+            # check that dimensions is valid
+            dimensions = verify_dimensions(dimensions)
+            # save the values
+            this_cgs_value, this_dimensions = cgs_value, dimensions
+
+        else:  # lookup the unit symbols
+            this_cgs_value, this_dimensions = \
+                get_unit_data_from_expr(unit_expr)
+
+        # Trick to get dimensions powers as Rationals
+        this_dimensions = nsimplify(this_dimensions)
+
+        # create obj with superclass construct
+        obj = Expr.__new__(cls, **assumptions)
+
+        # attach attributes to obj
+        obj.expr = unit_expr
+        obj.is_atomic = is_atomic
+        obj.cgs_value = this_cgs_value
+        obj.dimensions = this_dimensions
+
+        # return `obj` so __init__ can handle it.
+        return obj
+
+    ### some sympy conventions I guess
+    def __getnewargs__(self):
+        return (self.expr, self.is_atomic, self.cgs_value, self.dimensions)
+
+    def __hash__(self):
+        return super(Unit, self).__hash__()
+
+    def _hashable_content(self):
+        return (self.expr, self.is_atomic, self.cgs_value, self.dimensions)
+    ### end sympy conventions
+
+    def __repr__(self):
+        if self.expr == 1:
+            return "(dimensionless)"
+        return str(self.expr)
+
+    def __str__(self):
+        if self.expr == 1:
+            return "(dimensionless)"
+        return str(self.expr)
+
+    # for sympy.printing
+    def _sympystr(self, *args):
+        return str(self.expr)
+
+    ### override sympy operations
+    def __mul__(self, u):
+        """ Multiply Unit with u (Unit object). """
+        if not isinstance(u, Unit):
+            raise UnitOperationError("Tried to multiply Unit object with '%s'. This behavior is undefined." % u)
+
+        return Unit(self.expr * u.expr, self.cgs_value * u.cgs_value,
+                    self.dimensions * u.dimensions)
+
+    def __div__(self, u):
+        """ Divide Unit by u (Unit object). """
+        if not isinstance(u, Unit):
+            raise UnitOperationError("Tried to divide Unit object by '%s'. This behavior is undefined." % u)
+
+        return Unit(self.expr / u.expr, self.cgs_value / u.cgs_value,
+                    self.dimensions / u.dimensions)
+
+    def __pow__(self, p):
+        """ Take Unit to power p (float). """
+        try:
+            p = sympify(p)
+        except ValueError:
+            raise UnitOperationError("Tried to take Unit object to the power '%s'. I could not cast this to a float." % p)
+
+        return Unit(self.expr**p, self.cgs_value**p, self.dimensions**p)
+
+    ### Comparison operators
+    def same_dimensions_as(self, other_unit):
+        """ Test if dimensions are the same. """
+        return (self.dimensions / other_unit.dimensions) == 1
+
+    def __eq__(self, u):
+        """ Test unit equality. """
+        if not isinstance(u, Unit):
+            raise UnitOperationError("Tried to test equality between Unit object and '%s'. This behavior is undefined." % u)
+
+        return (self.cgs_value == u.cgs_value and self.dimensions == u.dimensions)
+
+    @property
+    def is_dimensionless(self):
+        return self.dimensions == 1
+
+    def get_cgs_equivalent(self):
+        """ Create and return dimensionally-equivalent cgs units. """
+        cgs_units_string = "g**(%s) * cm**(%s) * s**(%s) * K**(%s)" % \
+            (self.dimensions.expand().as_coeff_exponent(mass)[1],
+             self.dimensions.expand().as_coeff_exponent(length)[1],
+             self.dimensions.expand().as_coeff_exponent(time)[1],
+             self.dimensions.expand().as_coeff_exponent(temperature)[1])
+        return Unit(cgs_units_string, 1, self.dimensions)
+
+
+def make_symbols_positive(expr):
+    """
+    Grabs all symbols from expr, makes new positive symbols with the same names,
+    and substitutes them back into the expression.
+
+    """
+    expr_symbols = expr.atoms(Symbol)  # grab all symbols
+
+    # Replace one at a time
+    for s in expr_symbols:
+        # replace this symbol with a positive version
+        expr = expr.subs(s, Symbol(s.name, positive=True))
+
+    return expr
+
+
+# @todo: simpler method that doesn't use recursion would be better...
+# We could check if dimensions.atoms are all numbers or symbols, but we should
+# check the functions also...
+def verify_dimensions(d):
+    """
+    Make sure that `d` is a valid dimension expression. It must consist of only
+    the base dimension symbols, to powers, multiplied together. If valid, return
+    the simplified expression. If not, raise an Exception.
+
+    """
+    # in the case of a Number of Symbol, we can just return
+    if isinstance(d, Number):
+        return d
+    elif isinstance(d, Symbol):
+        if d in base_dimensions:
+            return d
+        else:
+            raise UnitParseError("dimensionality expression contains an unknown symbol '%s'." % d)
+
+    # validate args of a Pow or Mul separately
+    elif isinstance(d, Pow):
+        return verify_dimensions(d.args[0])**verify_dimensions(d.args[1])
+
+    elif isinstance(d, Mul):
+        total_mul = 1
+        for arg in d.args:
+            total_mul *= verify_dimensions(arg)
+        return total_mul
+
+    # should never get here
+    raise UnitParseError("Bad dimensionality expression '%s'." % d)
+
+
+def get_unit_data_from_expr(unit_expr):
+    """
+    Gets total cgs_value and dimensions from a valid unit expression.
+
+    """
+    # The simplest case first
+    if isinstance(unit_expr, Unit):
+        return (unit_expr.cgs_value, unit_expr.dimensions)
+
+    # Now for the sympy possibilities
+    if isinstance(unit_expr, Symbol):
+        return lookup_unit_symbol(str(unit_expr))
+
+    if isinstance(unit_expr, Number):
+        return (1, 1)
+
+    if isinstance(unit_expr, Pow):
+        unit_data = get_unit_data_from_expr(unit_expr.args[0])
+        power = unit_expr.args[1]
+        return (unit_data[0]**power, unit_data[1]**power)
+
+    if isinstance(unit_expr, Mul):
+        cgs_value = 1
+        dimensions = 1
+        for expr in unit_expr.args:
+            unit_data = get_unit_data_from_expr(expr)
+            cgs_value *= unit_data[0]
+            dimensions *= unit_data[1]
+
+        return (cgs_value, dimensions)
+
+    raise UnitParseError("Cannot parse for unit data from '%s'. Please supply an expression of only Unit, Symbol, Pow, and Mul objects." % str(unit_expr))
+
+
+def lookup_unit_symbol(symbol_str):
+    """ Searches for the unit data typle corresponding to the given symbol. """
+
+    if symbol_str in unit_symbols_dict:
+        # lookup successful, return the tuple directly
+        return unit_symbols_dict[symbol_str]
+
+    # could still be a known symbol with a prefix
+    possible_prefix = symbol_str[0]
+    if possible_prefix in unit_prefixes:
+        # the first character could be a prefix, check the rest of the symbol
+        symbol_wo_prefix = symbol_str[1:]
+
+        if symbol_wo_prefix in unit_symbols_dict:
+            # lookup successful, it's a symbol with a prefix
+            unit_data = unit_symbols_dict[symbol_wo_prefix]
+            prefix_value = unit_prefixes[possible_prefix]
+
+            # don't forget to account for the prefix value!
+            return (unit_data[0] * prefix_value, unit_data[1])
+
+    # no dice
+    raise UnitParseError("Could not find unit symbol '%s'. Please supply the dimensions and cgs value when creating this object." % symbol_str)
+
+
+def get_conversion_factor(old_units, new_units):
+    """
+    Get the conversion factor between two units of equivalent dimensions. This
+    is the number you multiply data by to convert from values in `old_units` to
+    values in `new_units`.
+
+    Parameters
+    ----------
+    old_units: str or Unit object
+        The current units.
+    new_units : str or Unit object
+        The units we want.
+
+    Returns
+    -------
+    conversion_factor : float
+        `old_units / new_units`
+
+    """
+    # Make units out of strings if we need to.
+    if isinstance(old_units, str):
+        old_units = Unit(old_units)
+    if isinstance(new_units, str):
+        new_units = Unit(new_units)
+
+    if not old_units.same_dimensions_as(new_units):
+        raise UnitOperationError("Cannot convert from %s to %s because the dimensions do not match: %s and %s" % (old_units, new_units, old_units.dimensions, new_units.dimensions))
+
+    return old_units.cgs_value / new_units.cgs_value
+
+
+def convert_values(values, old_units, new_units):
+    """
+    Take data given in old units and convert to values in new units.
+
+    Parameters
+    ----------
+    values : array_like
+        The number or array we will convert.
+    old_units : str or Unit object
+        The units values are supplied in.
+    new_units : str or Unit object
+        The units values will be returned in.
+
+    Returns values in new units.
+
+    """
+    return values * get_conversion_factor(old_units, new_units)

Repository URL: https://bitbucket.org/yt_analysis/yt-3.0/

--

This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.



More information about the yt-svn mailing list