#!/www/python/bin/python
"""$URL: svn+ssh://svn/repos/trunk/mems/lib/lib/test/test_unit.py $
$Id: test_unit.py 24661 2004-07-12 15:58:26Z nascheme $

Test script for the PhysicalUnit and UnitCollection classes.
"""

from sancho.unittest import TestScenario, parse_args, run_scenarios
from dulcinea.errors import UsageError
from dulcinea.physical_unit import PhysicalUnit, UnitCollection, UnitError

tested_modules = [ "dulcinea.physical_unit" ]

class PhysicalUnitTest (TestScenario):

    def setup (self):
        self.units = UnitCollection()

    def shutdown (self):
        del self.units

    def get_units (self, *names):
        return [self.units[name] for name in names]


    def check_basics (self):
        "basic unit finding and conversion"
        units = self.units

        # Assume these units already exist.
        kilogram = units['kg']
        gram = units['g']
        metre = units['m']
        inch = units['inch']

        self.test_val("kilogram.name", "kg")
        self.test_val("kilogram.get_name()", "kg")
        self.test_val("kilogram.get_name(html=1)", "kg")

        self.test_val("kilogram.factor", 1.0)
        self.test_val("kilogram.offset", 0.0)
        self.test_false("kilogram.artificial")

        self.test_val("kilogram.convert_value(1.5, gram)", 1500.0)
        self.test_val("round(metre.get_conversion_factor(inch), 2)", 39.37)

        get_unit = units.get_unit
        self.test_val("get_unit('kg')", kilogram, match_ident=1)
        self.test_true("(kilogram*metre).artificial")
        self.test_true("get_unit('kg*m').artificial")
        self.test_exc("get_unit(666)", TypeError)

        self.test_true("kilogram.is_dimension('mass')")
        self.test_true("metre.is_dimension('length')")
        self.test_true("units['Torr'].is_dimension('pressure')")

    def check_temperature (self):
        "temperature unit conversion"
        units = self.units
        degF = units.get_unit('degF')
        degC = units.get_unit('degC')
        kelvin = units.get_unit('K')

        warm_day = 80                   # in fahrenheit
        cold_day = 0                    # in fahrenheit
        fpw = 0                         # "freezing point of water" in celsius
        htsc = 70                       # "high temperature" superconductivity
                                        # (at a brisk 70 K)
        abs_zero = 0                    # in kelvin

        self.test_val("round(degF.convert_value(warm_day, degC),2)", 26.67)
        self.test_val("round(degF.convert_value(warm_day, kelvin),2)", 299.82)

        self.test_val("round(degF.convert_value(cold_day, degC),2)", -17.78)
        self.test_val("round(degF.convert_value(cold_day, kelvin),2)", 255.37)

        self.test_val("degC.convert_value(fpw, degC)", fpw)
        self.test_val("round(degC.convert_value(fpw, degF),2)", 32.00)
        self.test_val("round(degC.convert_value(fpw, kelvin),2)", 273.15)

        self.test_val("round(kelvin.convert_value(htsc, degC),2)", -203.15)
        self.test_val("round(kelvin.convert_value(htsc, degF),2)", -333.67)

        self.test_val("round(kelvin.convert_value(abs_zero, kelvin),2)", 0)
        self.test_val("round(kelvin.convert_value(abs_zero, degC),2)", -273.15)
        self.test_val("round(kelvin.convert_value(abs_zero, degF),2)", -459.67)

        self.test_true("degC.is_dimension('temperature')")
        self.test_true("degF.is_dimension('temperature')")
        self.test_true("kelvin.is_dimension('temperature')")
        self.test_false("kelvin.is_dimension('mass')")

    # check_temp_units ()


    def check_conversion (self):
        "conversion functions on unit collection"
        units = self.units
        inch = units.get_unit('inch')
        mm = units.get_unit('mm')
        degC = units.get_unit('degC')
        degF = units.get_unit('degF')
        K = units.get_unit('K')
        self.test_val("round(units.convert_value(6, inch, mm), 2)", 152.4)
        self.test_val("round(units.convert_value(0, degC, degC), 2)", 0.0)
        self.test_val("round(units.convert_value(0, degC, degF), 2)", 32.00)
        self.test_val("round(units.convert_value(0, degC, K), 2)", 273.15)

    def check_add_units (self):
        "define alternate units and convert to/from them"

        units = self.units
        units.add_unit('ft', units['inch'], 12)
        units.add_unit('yard', units['ft'], 3)

        self.test_stmt("yard = units.get_unit('yard')")
        self.test_val("round(yard.get_conversion_factor(), 4)", .9144)
        inch = units.get_unit('inch')
        self.test_val("inch.powers", (1, 0, 0, 0, 0, 0, 0))
        self.test_val("round(inch.factor, 5)", 0.0254)

        self.test_stmt("unit = units['ft'] / units['min']")
        self.test_val("unit.powers", (1, 0, -1, 0, 0, 0, 0))
        self.test_val("round(unit.factor, 5)", 0.00508) # .3048 m / 60 s

        self.test_val("round(units.convert_value(3, units['yard'],"
                      "                          units['m']), 4)", 2.7432)
        self.test_val("round(units.convert_value(500, units['s'],"
                      "                          units['hour']), 4)", .1389)

    def check_implicit (self):
        "implicit unit definition"

        units = self.units

        units.add_unit('ft', units['inch'], 12)
        units.add_unit('mile', units['ft'], 5280)
        mph = units['mile'] / units['hour']
        self.test_true("units['mile/hour'] is mph")

        self.test_stmt("mps = units.get_unit('m/s', 1)")
        self.test_stmt("ipm = units['inch'] / units['min']")
        self.test_val("round(ipm.convert_value(1.5, mps), 7)", 6.35e-4)
        self.test_val("round(ipm.convert_value(1.5, mph), 6)", 1.42e-3)

    def check_compare (self):
        "comparison and compatibility"

        units = self.units
        units.get_unit("cm")
        (metre, centimetre, inch, sec, hertz) = \
            self.get_units('m', 'cm', 'inch', 's', 'Hz')

        self.test_true("metre == metre")
        self.test_false("metre == centimetre")
        self.test_true("metre.is_compatible(centimetre)")
        self.test_true("metre.is_compatible(inch)")
        self.test_false("metre.is_compatible(None)")

        self.test_false("sec.is_compatible(hertz)")
        self.test_true("sec.is_dimension('time')")
        self.test_true("sec.is_inverse_compatible(hertz)")

        self.test_true("hertz.is_dimension('frequency')")

        metre2 = PhysicalUnit("m", metre.powers, units)
        metre3 = PhysicalUnit("foo", metre.powers, units)
        self.test_true("metre == metre2")
        self.test_true("metre == metre3")

        # This depends on the list of temperature units defined in
        # UnitCollection.  There aren't that many temperature units in
        # the world, though, so it's unlikely that we'd add more and
        # break this test.
        temp_units = units.get_compatible_units('degC')
        temp_units = [(u.factor, u.name) for u in temp_units]
        temp_units.sort()
        self.test_val("temp_units", [(5./9, 'degF'), (1, 'K'), (1, 'degC')])

        time_units = [(u.factor, u.name)
                      for u in units.get_compatible_units('s')]
        time_units.sort()
        expect = [(1, 's'), (60, 'min'), (60*60, 'hour'),
                  (60*60*24, 'day'), (60*60*24*7, 'week')]
        self.test_val("time_units", expect)

        # Test that implicit addition affects get_compatible_units()
        units.get_unit("us")
        units.get_unit("ms")
        time_units = [(u.factor, u.name)
                      for u in units.get_compatible_units('s')]
        time_units.sort()
        expect = [(1e-6, "us"), (1e-3, "ms")] + expect
        self.test_val("time_units", expect)


    def check_arith1 (self):
        "arithmetic on physical units and scalars"

        self.units.get_unit("cm")
        (metre, centimetre, inch) = \
            self.get_units('m', 'cm', 'inch')

        scaled_cm = centimetre*100.0
        self.test_val("scaled_cm.powers", metre.powers)
        self.test_val("scaled_cm.factor", metre.factor)
        self.test_true("metre is scaled_cm")
        self.test_val("str(scaled_cm)", "m")

        alt_cm = centimetre*1
        self.test_true("centimetre is alt_cm")
        self.test_true("alt_cm*100 is metre")
        self.test_val("str(alt_cm)", "cm")

        scaled_cm = centimetre * inch.get_conversion_factor(centimetre)
        self.test_val("scaled_cm.powers", inch.powers)
        self.test_val("scaled_cm.factor", inch.factor)
        self.test_true("inch is scaled_cm")
        self.test_val("str(scaled_cm)", "inch")

    def check_arith2 (self):
        "arithmetic on physical units with other physical units"

        self.units.get_unit("um")
        (inch, um, sec, metre,) = \
               self.get_units('inch', 'um', 's', 'm')

        self.test_stmt("ips = inch/sec")
        self.test_stmt("mps = metre/sec")
        self.test_val("ips.get_name()", "inch/s")
        self.test_val("mps.get_name()", "m/s")
        self.test_val("ips.get_powers()", (1, 0, -1, 0, 0, 0, 0))
        self.test_val("mps.get_powers()", (1, 0, -1, 0, 0, 0, 0))
        self.test_true("ips.is_compatible(mps)")
        self.test_val("round(ips.convert_value(10.0, mps),3)", 0.254)

        self.test_stmt("minv = metre.inv()")
        self.test_val("minv.get_name()", "1/m")
        self.test_val("minv.get_powers()", (-1, 0, 0, 0, 0, 0, 0))
        self.test_val("minv.get_conversion_factor()", 1.0)

        self.test_val("(ips*metre).get_name()", "(inch/s)*m")
        self.test_val("(minv/ips).get_name()", "(1/m)/(inch/s)")
        self.test_val("(minv/4).get_name()", "1/m/4")

        self.test_stmt("umps = um/sec")
        self.test_val("(umps*metre).get_name()", "(um/s)*m")
        self.test_val("(minv/umps).get_name()", "(1/m)/(um/s)")
        self.test_val("(minv/4).get_name(html=1)", "1/m/4")
        self.test_val("(minv/(1/3.0)).get_name(html=1)", "1/m/0.333333")

        self.test_stmt("msq = metre*metre")
        self.test_true("msq.is_compatible(self.units['m^2'])")
        self.test_val("msq.get_name()", "m*m")
        self.test_val("msq.get_powers()", (2, 0, 0, 0, 0, 0, 0))
        self.test_false("msq.is_compatible(metre)")
        self.test_exc("msq.convert_value(1, mps)", UnitError)

    def check_delete (self):
        "delete units"

        # Can't delete standard SI units
        self.test_exc("self.units.delete_unit('m')", TypeError)
        self.test_exc("self.units.delete_unit(self.units['g'])", UsageError)
        self.test_exc("self.units.delete_unit(self.units['Wb'])", UsageError)

        # Can't delete units that don't exist
        self.test_exc("self.units.delete_unit(self.units['foo'])", KeyError)

        # Some deletions that should work
        cmps = self.units.get_unit('cm/s')
        self.test_val("self.units['cm/s']", cmps, match_ident=1)
        self.test_stmt("self.units.delete_unit(cmps)")
        self.test_exc("self.units.get_unit('cm/s', create=False)", ValueError)

        # get_unit() can bring it back (but as a different object)
        self.test_true("self.units.get_unit('cm/s') == cmps")
        self.test_true("self.units.get_unit('cm/s') is not cmps")

        # This one can't be brought back
        mil = self.units['mil']
        self.test_stmt("self.units.delete_unit(mil)")
        self.test_exc("self.units['mil']", KeyError)
        self.test_exc("self.units.get_unit('mil')", ValueError)

    def check_massquery (self):
        "mass query methods"

        # Don't make any assumptions about which units are "natural" or
        # "artificial" -- it's non-obvious and a bit surprising in a
        # newly-created UnitCollection.  Doesn't really matter in real
        # life, though, since the 'artificial' flag was added well after
        # all the important units already existed in our live database.
        all_units = self.units.get_all_units()
        nat_units = self.units.get_natural_units()
        art_units = self.units.get_artificial_units()

        self.test_true("len(all_units) == len(self.units)")
        self.test_true("len(all_units) == len(nat_units) + len(art_units)")
        all_units_2 = [(unit.name.lower(), unit)
                       for unit in nat_units + art_units]
        all_units_2.sort()
        all_units_2 = [unit for (name, unit) in all_units_2]
        self.test_true("all_units == all_units_2")

# class PhysicalUnitTest


if __name__ == "__main__":
    (scenarios, options) = parse_args()
    run_scenarios(scenarios, options)
