NAME

contextUnits.pl - Implements a MathObject class for numbers with units

DESCRIPTION

This file implements a MathObject Unit class that provides the ability to use units within computations, within lists, and so on. There are two pre-defined units contexts, but you can add units to other existing contexts, if they are compatible with units.

To load, use

loadMacros('contextUnits.pl');

and then select the Units or LimitedUnits context and enable the units that you want to use. E.g.,

Context("Units")->withUnitsFor("length");

or

Context("LimitedUnits")->withUnitsFor("angles");

For the LimitedUnits context, you are not allowed to perform any operations, like addition or multiplication, or any function calls, so can only enter a single number, unit, or number with unit.

You can include as many categories as you want, as in

Context("Units")->withUnitsFor("length", "volume");

The categories of units are the following:

angles           (fundamental units "rad")
time             (fundamental units "s")
length           (fundamental units "m", except for those in "atomics" and "astronomy" below)
metric-length    (same as length except no imperial lengths)
imperial-length  (in, ft, mi, furlong, and their aliases)
volume           (fundamental units "m^3")
velocity         (fundamental units "m/s")
mass             (fundamental units "kg", except for those in "astronomy" below)
temperature      (fundamental units "defC", "defF", "K")
frequency        (fundamental units "rad/s")
force            (fundamental units "(kg m)/(s^2)")
energy           (fundamental units "(kg m^2)/(s^2)")
power            (fundamental units "(kg m^2)/(s^3)" except for those in "astronomy" below)
pressure         (fundamental units "kg/(m s^2)")
electricity      (fundamental units "amp", "amp/s", "(kg m)/(amp s^-3)", "(amp s^-3)/(kg m)",
                                    "(amp^2 s^4)/(kg m^2)", "(kg m^2)/(amp^2 s^3)", and "(amp^2 s^3)/(kg m^2)")
magnetism        (fundamental units "kg/(amp s^2)" and "(kg m)/(amp s^2)")
luminosity       (fundamental units "cd/(rad^2)" and "cd/(rad m)^2")
atomics          (amu, me, barn, a0, dalton)
radiation        (fundamental units "(m^2)/(s^2)" and "s^-1")
biochem          (fundamental units "mol" or "mol/s")
astronomy        (kpc, Mpc, solar-mass, solar-radii, solar-lum, light-year, AU, parsec)
fundamental      (m, kg, s, rad, degC, degF, K, mol, amp, cd)

You can add specific named units via the addUnits() method of the context, as in

Context("Units")->withUnitsFor("volume")->addUnits("m", "cm");

or

$context = Context("Units");
$context->withUnitsFor("volume");
$context->addUnits("m", "cm");

to get a units context with units for volume as well as m and cm and any aliases for these units (e.g., meter, meters, etc.). Use addUnitsNotAliases() in place of addUnits() to add just the named units without adding any aliases for them.

You can remove individual units from the context using the removeUnits() method of the context. For example

Context("Units")->withUnitsFor("length")->removeUnits("ft", "inch", "mile", "furlong");

removes the English units and their aliases, leaving only the metric units. To remove a unit without removing its aliases, use removeUnitsNotAliases() instead.

Note that the units are stored in the context as constants, so to list all the units, together with other constants, use

Context()->constants->names;

The constants that are units have the isUnit property set. So

grep {Context()->constants->get($_)->{isUnit}} (Context()->constants->names);

will get the list of units.

Custom units

You can define your own units in terms of the fundamental units. E.g., to define the unit acres, you could use

Context("Units")->withUnitsFor("length")->addUnits(acres => {factor => 4046.86, m => 2});

which indicates that 1 acre is equal to 4046.86 square meters.

You can even make up your own fundamental units. For example, to define apples and oranges as units, you could do

Context("Units")->addUnits(
  apples => { apples => 1, aliases => ["apple"] },
  oranges => { oranges => 1, aliases => ["orange"] }
);

BEGIN_PGML
If you have 5 apples and give your friend 2 of them,
what do you have left? [___________]{"3 apples"}
END_PGML

and the student can answer 3 oranges but will be marked incorrect (with a message about the units being incorrect). Note that apples and apple are synonymous in this context, and that 1 apple is accepted, but is displayed as 1 apples, as no attempt is made to handle plurals.

On the other hand, you could also do

Context("Units")->addUnits(
  apples => { fruit => 1, aliases => ["apple"] },
  oranges => { fruit => 1, aliases => ["orange"] }
);

Compute("3 apples") == Compute("3 oranges"); # returns 1

will consider apple and oranges as the same unit (both are the fundamental unit of fruit).

Finally,

Context("Units")->addUnits(
  apples => { fruit => 1, aliases => ["apple"] },
  oranges => { fruit => 1, aliases => ["orange"], factor => 2 }
);

Compute("2 apples") == Compute("1 orange"); # returns 1

will make an orange equivalent to two apples by making both apples and oranges be examples of the fundamental unit fruit, but with oranges having a factor of 2 times the fundamental unit, so an orange is considered to be 2 fruit, while an apple is one fruit.

Adding units to other contexts

The Units and LimitedUnits contexts are based on the Numeric and LimitedNumeric contexts. You can add units to other contexts using the context::Units::extends() function. For example,

loadMacros("contextUnits.pl", "contextFraction.pl");
Context(context::Units::extending("Fraction")->withUnitsFor("length"));

would allow you to use fractions with units.

In addition to the name of the context to extend, you can pass options to context::Units::extending(), as in

loadMacros("contextUnits.pl", "contextFraction.pl");
$context = Context(context::Units::extending("LimitedFraction", limited => 1));
$context->addUnitsFor("length");

In this case, the <limited = 1>> option indicates that no operations are allowed between numbers with units, and since the LimitedFraction context doesn't allow operations otherwise, you will only be able to enter fractions or whole numbers, with or without units, or a unit without a number.

The available options and their defaults are

keepNegativePowers => 1,     Preserve use of negative powers so C<m s^-1> will
                             not be shown as C<m/s> (but will still match it).
useNegativePowers => 0       Always use negative powers instead of fractions?
limited => 0                 Don't allow operations on numbers with units.
exactUnits => 0              Require student units to exactly match correct ones
                               in both order and use of negative powers
sameUnits => 0               Require student units to match correct ones
                               not scaled versions
partialCredit => .5          Partial credit if answer is right but units
                               are not correct
factorUnits => 1             Factor the units out of sums and differences of
                               formulas with the same units

The first two and last three can also be set as context flags after the context is created. There is a limitedOperators flag that is set by the limited option that controls whether operations are allowed on numbers with units, but if you set it, you might also need to do

Context()->parens->set( '(' => { close => ')', type => 'Units' } );

to allow parentheses around units if the parentheses have been removed from the original context (as they are in the LimitedNumeric context, for instance). This makes it possible to enter units of the form kg/(m s) in such contexts.

Creating unit and number-with-unit objects

In the units contexts, units are first-class citizens, and unit and number-with-unit objects can be created just like any other MathObject. So you can use

$n = Compute("3 m/s");

to get a number-with-units object for 3 meters per second. You can also use the word per in place of /, as in

$n = Compute("3 meters per second");

You can use the words squared and cubed with units in place of ^2 and ^3, so that

$n = Compute("3 meters per second squared");

will produce an equivalent result to

$n = Compute("3 m/s^2");

There are also square and cubic that can be used to precede a unit, such as

$n = Compute("3 square meters");

as an alternative to Compute("3 m^2").

Note that the space between the number and units is not strictly necessary, and neither is the space between units, unless the combined unit names have a different meaning. For example

$n = Compute("3m");     # instead of "3 m"
$n = Compute("3 kgm");  # instead of "3 kg m"

are both fine, but

$n = Compute("3 ms");

would treat ms as the single unit for milliseconds, rather than meter-seconds, in a context that includes both length and time units.

In order to have more than one unit in the denominator, you can either use multiple division signs (or per operations), or enclose the denominator in parentheses, as in

$n = Compute("3 kg/m/s");
$n = Compute("3 kg/(m s)");
$n = Compute("3 kg per meter per second");

Units can be preceded by formulas as well as numbers. For example

$f = Compute("2x meters");

makes $f be a Formula returning a Number-with-Unit. Note, however, that since the space before the unit has the same precedence as multiplication (just as it does within a formula), if the expression before the unit includes addition or subtraction, you need to enclose it in parentheses:

$n = Compute("(1+4) meters");
$f = Compute("(1+2x) meters");

Using Compute() is not the only way to produce a number or formula with units; there are also constructor functions that are sometimes useful when writing a problem involving units.

$n = NumberWithUnits(3, "m/s");
$f = FormulaWithUnits("1+2x", "meters");

These are most useful when the numeric part is the result of a computation or a value held in a variable:

$n = NumberWithUnits(random(1,5), "m");

Since units are themselves MathObjects, you can work with units without a preceding number. These can be created through Compute() just as with other MathObject, or you can use the Unit() constructor.

$u = Compute("meters per second per second");
$u = Unit("m/s^2");

This allows you to ask a student to say what units should be used for a particular setting, without the need for a quantity.

Working with numbers with units

Because units and numbers with units are full-fledged MathObjects, you can do computations with them, just as with other MathObejcts. For example, you can do

$n = Compute("3 m + 10 cm");

to get the equivalent of 3.1 m. Similarly, you can do

$velocity = Compute("100 miles / (2 hours)");  # equals "50 mi/h"
$area = Compute("(5 m) * (3 m)");              # equals "15 m^2"

to get numbers with compound units.

As with other MathObjects, units and numbers with units can be combined using perl operations:

$distance = Compute("100 miles");
$time = Compute("2 hours");
$velocity = $distance / $time;  # equivalent to "50 miles/hour"

$m = Compute("m");
$s = Compute("s");
$a = 9.8 * $m / $s**2;

$x = Compute("x");
$f = (3 * $x**2 - 2) * $m;  # equivalent to Compute("(3x^2 - 2) m");

The units objects provide functions for converting from one set of units to another (compatible) set via the toUnits() and toBaseUnits() methods. For example:

$m = Compute("5 m");
$ft = $m->toUnits("ft");                 # returns "16.4042 ft"

$cm = Compute("5.21 m")->toUnits("cm");  # returns "521 cm"

$a = Compute("32 ft/s^2")->toBaseUnits;  # returns "9.7536 m/s^2"

For a given number with units, you may wish to obtain the numeric portion or the units portion separately. This can be done using the number and unit methods:

$n = Compute("5 m");
$r = $n->number;         # returns 5 as a Real MathObject
$u = $n->unit;           # returns "m" as a Unit MathObject

You can also use the Real() and Unit() constructors to do the same thing:

$n = Compute("5 m");
$r = Real($n);           # returns 5 as a Real MathObject
$u = Unit($n);           # returns "m" as a Unit MathObject

You can get the numeric portion of the number-with-units object relative to the base units using the quantity method:

$q = Compute("3 ft")->quantity;    # returns .9144

Using $m->quantity is equivalent to calling $m->toBaseUnits->number.

Finally, you can get the factor by which the given units must be multiplied to obtain the quantity in the fundamental base units using the factor method:

$f = Compute("3 ft")->factor;    # returns 0.3048

Similarly, you can use the factor method of a unit object to get the factor for that unit.

Most functions, such as sqrt() and ln(), will report an error if they are passed a number with units (or a bare unit). Important exceptions are the trigonometric and hyperbolic functions, which accept a number with units provided the units are angular units. For example,

$v = Compute("sin(30 deg)");

will return 0.5, and so will

$a = Compute("60 deg");
$sin_a = sin($a);

as the perl functions have been overloaded to handle numbers with units when the units are angular units.

The other exception is abs(), which can be applied to numbers with units, and returns a number with units having the same units, but the quantity is the absolute value of the original quantity.

Differentiation of numbers with units

In order to be able to differentiate a formula that returns a number with units, the MathObjects library needs to know the units of the variable you are differentiating by. For example, if you have

$s = Compute("(3t^2 - 2t) m");

as a function of time, t, then you would like

$v = $s->D("t");

to be equivalent to Compute("(6t - 2) m/s").

To enable this, you must tell the Units context that t has units of seconds. That is done using the assignUnits() function of the context:

Context("Units")
  ->withUnitsFor("length", "time")
  ->assignUnits(t => "s");

You can pass as many unit assignments to a single assignUnits() call as you like. E.g.

Context("Units")
  ->withUnitsFor("length", "time")
  ->assignUnits(
    t => "s",
    s => "m"
  );

to assign the variable t units of seconds and s units of meters. These values are only used in differentiation, so don't affect other formulas, and aren't involved in type-checking or other operations.

If you assign units to a variable that hasn't yet been added to the context, assignUnits will first add the variable as a real-valued one and then assign the units to it.

Answer checking for units and numbers with units

You can use units and numbers with units within PGML or ANS() calls in the same way that you use any other MathObject. For example

BEGIN_PGML
What are the units for acceleration? [_______]{"m/sec^2"}
END_PGML

Here, the student can answer any equivalent units, such as ft/s^2 or even mi/h^2, and get full credit. If you wish to require the units to being the same as the correct answer, you can use the sameUnits option on the answer checker (or set the sameUnits flag in the units context):

$u = Compute("m/s^2");
BEGIN_PGML
What are the metric units for acceleration? [_______]{$u->cmp(sameUnits => 1)}
END_PGML

If the student entered ft/sec^2, they would get partial credit, and a message indicating that their units are correct but are not the same as the expected units. The amount of partial credit is determined by the partialCredit answer-checker option (or context flag), whose default value is .5 for half credit. So you can use

$u->cmp(sameUnits => 1, partialCredit => .75)

to increase the credit to 75%, or

$u->cmp(sameUnits => 1, partialCredit => 0)

to give no partial credit.

Similarly, if the correct answer is given with units of m, then when sameUnits => 1 is set, an answer using cm instead will be given only partial credit.

In the case where the units include products of units, like m s, the sameUnits option requires both be present, but they can be in either order. So a student can enter s m and still get full credit. If you want to require the order to be the same as in the correct answer, then use the exactUnits option. Again, partial credit is given for answers that have the right units but not in the right order.

If the correct answer is m/s^2, a student usually can enter m s^-2 and their answer will be counted as correct. Similarly, if the correct answer is given as m s^-2, then m/s^2 is also marked as correct. When exactUnits => 1 is set, however, in addition to using the units in the same order, the student's answer must use the same form (either fraction or negative power) for units in the denominator, and will only get the particalCredit value for using the other form.

Answers that are numbers with units are treated in a similar manner, and can use the sameUnits, exactUnits, and partialCredit flags to control what answers are given full credit.

Note that in the Units context, students can perform operations on numbers with units, as described in the previous section. For example, if the correct answer is 3.02 m, then a student can enter 3 m + 2 cm and be marked correct. Similarly, for the answer 50 mi/h a student could enter (100 miles) / (2 hours).

If you want to prevent students from performing such computations, then set the limitedOperations flag in the context or in the cmp() call. So

$ans = Compute("50 mi/h")->cmp(limitedOperations => 1);
BEGIN_PGML
If you travel 100 miles in 2 hours, then your
average velocity is [_______]{$ans}
END_PGML

will prevent the student from dividing two numbers with units, though they can still enter (100/2) mi/h. To prevent any operations at all, use the LimitedUnits context instead of the Units context.

Note that you can add the limitedOperations and other flags to the MathObject itself, rather than the context or answer checker, as in

$av = Compute("50 mi/h")->with(limitedOperations => 1, sameUnits => 1);
BEGIN_PGML
If you travel 100 miles in 2 hours, then your
average velocity is [_______]{$av}
END_PGML

and still be able to use the result in computations in the perl code. Note that the flags will be passed on to any results involving the original that had the flags set.