#!/www/python/bin/python
"""
$URL: svn+ssh://svn/repos/trunk/mems/lib/lib/test/test_pvalue.py $
$Id: test_pvalue.py 24663 2004-07-12 16:16:28Z dbinger $

Test script for the PhysicalValue class.
"""

from sancho.unittest import TestScenario, parse_args, run_scenarios
from dulcinea.range_value import RangeValue
from dulcinea.physical_unit import UnitCollection
from dulcinea.physical_value import PhysicalValue, UnitError

tested_modules = [ "dulcinea.physical_value" ]

class PhysicalValueTest (TestScenario):

    def setup (self):
        self.units = units = UnitCollection()
        units.add_unit('ft', units['inch'], 12)
        units.add_unit('km', units['m'], 1000)
        units.add_unit('mile', units['ft'], 5280)
        units.add_unit('L', units['m^3'], 0.1**3)
        units.add_unit('galUS', units['L'], 3.785411784)

        metres = units.get_unit('m')

        # an amusing collection of physical values to play with
        self.len1 = PhysicalValue(1.0, metres)
        self.len1a = PhysicalValue(1, metres)
        self.len2 = PhysicalValue(2.0, metres)
        self.len3 = PhysicalValue(3, units['ft'])
        self.len4 = PhysicalValue(36, units['inch'])
        self.len5 = PhysicalValue(RangeValue(3.0, 5.0), units['cm'])

        self.time1 = PhysicalValue(15, units['s'])
        self.time2 = PhysicalValue(0.5, units['min'])
        self.nounit1 = PhysicalValue(0.5)
        self.nounit2 = PhysicalValue(1.0)

    def check_basics(self):
        metres = self.units.get_unit('m')
        (len1, len1a, len2, len5) = \
            (self.len1, self.len1a, self.len2, self.len5)

        self.test_true("len1.unit is len2.unit")
        self.test_true("len1.get_unit() is len2.get_unit()")
        self.test_val("len2.get_unit()", metres)
        self.test_val("len2.get_unit_name()", "m")
        self.test_val("len1.value", 1.0)
        self.test_val("len1.get_value()", 1.0)
        self.test_val("len2.value", 2.0)
        self.test_val("len2.get_value()", 2.0)

        self.test_val("self.nounit1.get_unit_name()", None)

        self.test_val("len1.get_type()", float)
        self.test_val("len1a.get_type()", float)
        self.test_val("len5.get_type()", RangeValue)

        self.test_exc("PhysicalValue(RangeValue(1L, 2L))", TypeError)
        self.test_exc("PhysicalValue( (1,2) )", TypeError)

    def check_format(self):
        (nounit1, len4, len5) = (self.nounit1, self.len4, self.len5)

        self.test_val("str(nounit1)", "0.5")
        self.test_val("nounit1.format()", "0.5")
        self.test_val("str(len4)", "36.0 inch")
        self.test_val("len4.format()", "36 inch")
        self.test_val("str(len5)", "3.0 .. 5.0 cm")
        self.test_val("len5.format()", "3 .. 5 cm")

        self.test_val("nounit1.format(html=1)", "0.5")
        self.test_val("nounit1.format(show_unit=0)", "0.5")
        self.test_val("len4.format(html=1)", "36&nbsp;inch")
        self.test_val("len4.format(show_unit=0)", "36")
        self.test_val("len4.format(show_unit=0, html=1)", "36")

        self.units.get_unit("um").set_name("um", "&micro;m")
        a = PhysicalValue(2, self.units.get_unit("um"))
        self.test_val("a.format()","2 um")
        self.test_val("a.format(html=1)","2&nbsp;&micro;m")
        b = a/3
        self.test_val("b.format()","0.666667 um")
        self.test_val("b.format(html=1)","0.666667&nbsp;&micro;m")
        c = a*4
        self.test_val("c.format()","8 um")
        self.test_val("c.format(html=1)","8&nbsp;&micro;m")
        d = a/1
        self.test_val("d.format()","2 um")
        self.test_val("d.format(html=1)","2&nbsp;&micro;m")
        e = a*1
        self.test_val("e.format()","2 um")
        self.test_val("e.format(html=1)","2&nbsp;&micro;m")

        pv = PhysicalValue(32078.1, self.units.get_unit("Ang"))
        self.test_val("pv.format()", "32078.1 Ang")
        self.test_val("pv.format(html=1)", "32078.1&nbsp;&Aring;")
        pv = pv.convert(self.units.get_unit("m"))
        self.test_val("pv.format()", "3.20781e-06 m")
        self.test_val("pv.format(html=1)", "3.20781e-06&nbsp;m")


    def check_compatible(self):
        metres = self.units.get_unit('m')
        sec = self.units.get_unit('s')
        mps = metres/sec

        (len1, len5) = (self.len1, self.len5)
        t1 = PhysicalValue(30, sec)

        self.test_true("len1.unit_compatible(metres)")
        self.test_true("len5.unit_compatible(metres)")

        self.test_true("len1.unit_compatible(len5)")
        self.test_true("t1.unit_compatible(self.units.get_unit('hour'))")
        self.test_true("t1.unit_compatible(t1)")
        self.test_true("(len1/t1).unit_compatible(mps)")

        self.test_false("len1.unit_compatible(metres/sec)")
        self.test_false("t1.unit_compatible(metres)")

        nodim = metres/metres
        self.test_false("self.nounit1.unit_compatible(metres)")
        self.test_false("self.nounit1.unit_compatible(len1)")
        self.test_true("self.nounit1.unit_compatible(self.nounit2)")
        self.test_true("self.nounit1.unit_compatible(None)")
        self.test_true("self.nounit1.unit_compatible(nodim)")


    def check_dimension(self):
        self.test_true("self.len1.is_dimension('length')")
        self.test_false("self.len1.is_dimension('time')")
        self.test_false("self.nounit1.is_dimension('length')")


    def check_cmp(self):
        (len1, len1a, len2) = (self.len1, self.len1a, self.len2)

        # compare "1.0 m" and "2.0 m"
        self.test_true("len1 != len2")
        self.test_true("len1 < len2")
        self.test_true("len1 <= len2")
        self.test_true("len2 > len1")
        self.test_true("len2 >= len1")

        # compare "1.0 m" and "1 m" (float vs int)
        self.test_true("len1 == len1a")
        self.test_true("len1 <= len1a")
        self.test_true("len1 >= len1a")

        # compare compatible but different units
        (time1, time2) = (self.time1, self.time2)
        self.test_true("time1 < time2")
        self.test_true("time2 > time1")
        self.test_true("time1 != time2")
        time2.value = 0.25            # 0.25 min = 15 s
        self.test_true("time1 == time2")
        self.test_true("time2 == time1")
        self.test_false("time1 < time2")
        self.test_false("time1 > time2")
        self.test_true("time1 <= time2")
        self.test_true("time1 >= time2")

        # sign differences (-1 m != 1 m)
        len1_neg = PhysicalValue(-self.len1.value, self.len1.unit)
        self.test_true("len1 != len1_neg")
        self.test_true("len1_neg != len1")
        self.test_true("len1_neg == len1_neg")

        # compare unitless PhysicalValues
        (nounit1, nounit2) = (self.nounit1, self.nounit2)
        self.test_true("nounit1 < nounit2")
        self.test_true("nounit2 > nounit1")
        self.test_false("nounit2 == nounit1")
        self.test_true("nounit1 == 0.5")
        self.test_true("nounit1 != 25")
        self.test_true("nounit1 != 'bogus'")
        self.test_true("nounit1 > 0.4")
        self.test_true("nounit2 < 2")

        # nonsensical comparisons (units vs no units, or incompatible units)
        self.test_true("len1 != 1")
        self.test_true("len2 != 0.0")
        self.test_false("len1 == nounit1")
        self.test_false("len2 == time1")
        self.test_exc("len1 < 1", UnitError)
        self.test_exc("len2 >= 0.0", UnitError)
        self.test_exc("len1 < nounit1", UnitError)
        self.test_exc("len2 > 'bogus'", UnitError)

    def check_cmp_roundoff(self):
        "compare values that expose round-off errors: 16"

        pv1 = PhysicalValue(100, self.units.get_unit("um"))
        pv2 = PhysicalValue(100000, self.units.get_unit("nm"))
        # these all require conversion; which direction the conversion
        # goes determines which are susceptible to round-off problems!
        self.test_true("pv1 == pv2")
        self.test_true("pv2 == pv1")
        self.test_true("pv1 <= pv2")
        self.test_true("pv2 <= pv1")
        self.test_true("pv1 >= pv2")
        self.test_true("pv2 >= pv1")

        pv3 = pv1.convert(pv2.unit)
        pv4 = pv2.convert(pv1.unit)
        # these should require no conversion: the arithmetic has been done
        self.test_true("pv3 == pv2")
        self.test_true("pv2 == pv3")
        self.test_true("pv4 == pv1")
        self.test_true("pv1 == pv4")

        # these require even more conversions (eg. *back* to original
        # units), so the round-off should be even worse
        self.test_true("pv3 == pv1")
        self.test_true("pv1 == pv3")
        self.test_true("pv4 == pv2")
        self.test_true("pv2 == pv4")
        self.test_true("pv3 == pv4")
        self.test_true("pv4 == pv3")

    def check_convert(self):
        (len1, len1a, len2) = (self.len1, self.len1a, self.len2)
        units = self.units
        m = units.get_unit('m')
        s = units.get_unit('s')
        cm = units.get_unit('cm')
        inch = units.get_unit('inch')

        # convert to same unit (m -> m)
        self.test_val("len1a.convert(m).get_tuple()", (1, m))

        # convert m -> cm
        self.test_val("len1.convert(cm).get_tuple()", (100, cm))

        # convert m -> inch
        self.test_stmt("metre = len1a.convert(inch)")
        self.test_val("round(metre.get_value(), 2)", 39.37)

        # error case: m -> sec
        self.test_exc("len1.convert(s)", UnitError)


    def check_add(self):
        foot = self.units.get_unit('ft')
        inch = self.units.get_unit('inch')
        (len1, len1a, len2, len3, len4) = \
            (self.len1, self.len1a, self.len2, self.len3, self.len4)

        # simple addition/subtraction (identical units)
        self.test_val("len1 + len1", len2) # 1.0 m + 1.0 m = 2.0 m
        self.test_val("len1 + len1a", len2) # 1.0 m + 1 m = 2.0 m
        self.test_val("len2 - len1", len1) # 2.0 m - 1.0 m = 1.0 m
        self.test_val("len2 - len1a", len1) # 2.0 m - 1 m = 1.0 m

        # compatible units -- make sure it preserves the unit of the
        # first one!
        self.test_val("(len3 + len4).get_tuple()", (6, foot))
        self.test_val("(len4 + len3).get_tuple()", (72, inch))

        self.test_val("round((len3 - len4).get_value(), 4)", 0)
        self.test_val("round((len4 - len3).get_value(), 4)", 0)

        # error case: incompatible units
        time1 = self.time1
        self.test_exc("len1 + time1", UnitError)
        self.test_exc("time1 - len1", UnitError)

        # two unitless PhysicalValues
        (nounit1, nounit2) = (self.nounit1, self.nounit2)
        self.test_val("nounit1 + nounit2", PhysicalValue(1.5))
        self.test_val("nounit1 - nounit2", PhysicalValue(-0.5))

        # unitless PhysicalValue with a regular number (and vice-versa)
        self.test_val("nounit1 + 1", PhysicalValue(1.5))
        self.test_val("nounit1 - 1", PhysicalValue(-0.5))
        self.test_val("1 + nounit1", PhysicalValue(1.5))
        self.test_val("1 - nounit1", PhysicalValue(0.5))

        # unitless PhysicalValue with a RangeValue
        rv = RangeValue(3.0, 4.5)
        self.test_val("nounit1 + rv", PhysicalValue(RangeValue(3.5, 5.0)))
        self.test_val("rv + nounit1", PhysicalValue(RangeValue(3.5, 5.0)))
        self.test_val("nounit1 - rv", PhysicalValue(RangeValue(-4.0, -2.5)))
        self.test_val("rv - nounit1", PhysicalValue(RangeValue(2.5, 4.0)))

        # error case: PhysicalValue-with-unit with a unitless
        # PhysicalValue
        self.test_exc("len1 + nounit1", UnitError)
        self.test_exc("nounit1 + len1", UnitError)

        # error case: PhysicalValue-with-unit with a regular number
        self.test_exc("len1 + 2.0", UnitError)
        self.test_exc("2.0 + len1", UnitError)
        self.test_exc("2.0 - len1", UnitError)

        # error case: PhysicalValue with some random object (not another
        # PV, not a simple number, not a range)
        self.test_exc("len1 + 'foo'", TypeError)

    def check_mult(self):
        units = self.units
        m = units.get_unit('m')
        s = units.get_unit('s')
        min = units.get_unit('min')
        msq = units.get_unit('m*m', 1)
        foot = units.get_unit('ft')
        fsq = units['ft'] * units['ft']
        ftm = foot * m                  # "foot-metres"? whatever...
        ftinch = units.get_unit('ft') * units.get_unit('inch')
        minv = units.create_unit("minv", None, (-1, 0, 0, 0, 0, 0, 0), 1.0)

        (len1, len2, len3, len4) = (self.len1, self.len2, self.len3, self.len4)
        (time1, time2) = (self.time1, self.time2)

        # identical units
        self.test_val("(len1 * len2).get_tuple()", (2, msq))
        self.test_val("(len2 * len2).get_tuple()", (4, msq))
        self.test_val("(len1 * len2).get_tuple()", (2, msq))
        self.test_val("(len1 / len1).get_tuple()", (1, None))
        self.test_val("(len1 / len2).get_tuple()", (0.5, None))

        # compatible units: 1 m * 3 ft = 3 ft*m = .9144 m^2 = 9.84 ft^2
        self.test_stmt("area = len1 * len3")
        self.test_val("area.get_tuple()", (3, ftm))
        self.test_val("round(area.convert(msq).get_value(),4)", .9144)
        self.test_val("round(area.convert(fsq).get_value(),4)", 9.8425)

        # 1 m / 3 ft = 1 m / .9144 m = 1.0936 (dimensionless quantity)
        self.test_val("round((len1 / len3).get_value(), 4)", 1.0936)

        # incompatible units: the dimensions just pile up
        self.test_val("(len2 * time1).get_tuple()", (2.0*15, m*s))
        self.test_val("(len2 / time1).get_tuple()", (2.0/15, m/s))
        self.test_val("round((len2 / time1).convert(foot/min).get_value(), 4)",
                      26.2467)

        # Combine unitless and "unitfull" PhysicalValue
        (nounit1, nounit2) = (self.nounit1, self.nounit2)
        self.test_val("(nounit1 * len2).get_tuple()", (1.0, m))
        self.test_val("(len2 * nounit1).get_tuple()", (1.0, m))
        self.test_val("(nounit1 / len2).get_tuple()", (0.25, minv))
        self.test_val("(len2 / nounit1).get_tuple()", (4.0, m))

        # Ditto, but now use a regular number in place of a unitless
        # PhysicalValue
        self.test_val("(0.5 * len2).get_tuple()", (1.0, m))
        self.test_val("(len2 * 0.5).get_tuple()", (1.0, m))
        self.test_val("(0.5 / len2).get_tuple()", (0.25, minv))
        self.test_val("(len2 / 0.5).get_tuple()", (4.0, m))

        rv = RangeValue(1.5, 2.5)
        self.test_val("(len1 * rv).get_tuple()", (RangeValue(1.5, 2.5), m))
        self.test_val("(rv * len1).get_tuple()", (RangeValue(1.5, 2.5), m))
        self.test_exc("len1 / rv", TypeError)
        self.test_val("(rv / len1).get_tuple()", (RangeValue(1.5, 2.5), minv))

        self.test_exc("len1 * 'foo'", TypeError)



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