Harvey Mudd College
Computer Science 42
Assignment 7, Due Monday, March. 26, by 11:59pm
Unicalc Part 1
Unicalc is a calculator program
that handles numerical quantities including physical and other units, e.g.,
420 seconds
4.20 meters / second
42.42 mile / hour + 99 km / day
Units are important to computation because they eliminate an
element sometimes left to human interpretation. In the area of
engineering, failure to interpret numbers without their units specified
has been known to lead to failure of space missions. In the Wikipedia article about NASA's Mars Climate Orbiter,
it is stated:
“Twenty-four hours prior to orbital insertion, calculations placed the orbiter at an altitude of 110 kilometers; 80 kilometers is the minimum altitude that Mars Climate Orbiter was thought to be capable of surviving during this maneuver. Final calculations placed the spacecraft in a trajectory that would have taken the orbiter within 57 kilometers of the surface where the spacecraft likely disintegrated because of atmospheric stresses. The primary cause of this discrepancy was human error. Specifically, the flight system software on the Mars Climate Orbiter was written to calculate thruster performance using the metric unit Newtons (N), while the ground crew was entering course correction and thruster data using the Imperial measure Pound-force (lbf).”
A little closer to home, you will have to do many unit conversions and
operations in your other classes here at HMC. It is our hope
that this calculator will prove useful in those classes.
This week you will begin by implementing the back-end functionality for
simple unit calculations with error propagation. In doing so, you will be
implementing an API (Application Programming Interface), which is simply a
library of visible functions that others can use. In this
case, you will
be using those functions next week when implementing the Unicalc
calculator.
Error Propagation
One of the important things you will learn in some of your science
classes (if you haven't already) is that real calculations are rarely
exact. For example, consider the problem of calculating the
average velocity of a unicyclist as she races across campus. One
way you might do this is to measure the distance between West Dorm and
Hoch Shanahan, and you might even put tape marks on the ground.
Say this distance is 22.15m. Now, as soon as you see her cross
the first tape mark, you start your watch. When you see her cross
the second mark, you stop your watch. 9.95 seconds it says.
How fast was she going? Simple, right? v = x / t =
22.1m/9.95s = 2.32 m / s. A perfect problem for Unicalc!
But wait, not so fast... your friend was also timing her, and he says
that she took 10.32 seconds. Furthermore, your other friend
re-measured the distance between the tape marks, and she says that the
distance is actually 22.07m. Hmmm, things are getting a little more
complicated...
As you may have already learned, whenever you take a measurement, there
is some room for error in that measurement. As scientists, we
quantify this error, so that we can reason rationally about it,
rather than simply guessing about how it might affect our calculations.
In the above problem, each measurement has its own error. When
you put them together to calculate the velocity, you need to somehow
account for, or propagate,
the error through the equation. Fortunately there is a pricipled
way to do this, and this is what you will build into Unicalc this week.
The details of error propagation are beyond the scope of this class.
You'll learn about the theory behind error propagation in
Physics and Chemistry. (Though if you can't wait, you can take a
look at this site.) For now, all you need to know are the equations for error propagation for standard arithmetic operations.
Let's assume that you have two quantities, x and y, with a
standard (i.e. normally distributed, random) error \(E_x\) and \(E_y\). For
example, instead of knowing that \(x\) is exactly 22.1m, you estimate that
\(x\) is 22.1m plus or minus 0.05m. That is, \(x\)=22.1m and
\(E_x\)=0.05m. Again, how exactly you would calculate this standard
error we won't deal with here. We'll assume the errors are given.
Now you wish to find the standard error of the result of performing
some artimetic operation on \(x\) and \(y\). Call the result \(z\), and the
resulting error \(E_z\).
Here are the rules for error propagation, depending on how you use \(x\) and \(y\) to
get \(z\)
\[
\displaystyle
\begin{array}{ll}
\displaystyle
z = x + y & E_z = \sqrt{E_x^2 + E_y^2}\\
\ \\
z = x - y & E_z = \sqrt{E_x^2 + E_y^2}\\
\ \\
z = xy & \frac{E_z}{|z|} = \sqrt{\left(\frac{E_x}{x}\right)^2 + \left(\frac{E_y}{y}\right)^2 }\\
\ \\
z = \frac{x}{y} & \frac{E_z}{|z|} = \sqrt{ \left(\frac{E_x}{x}\right)^2 + \left(\frac{E_y}{y}\right)^2 }\\
\ \\
z = x^m & \frac{E_z}{|z|} = |m|\frac{E_x}{|x|}\\
\end{array}
\]
Error propagates the same way for addition and subtraction,
and the same way for multiplication and division. Note that the
error associated with \(x^2\) CANNOT be calculated by applying the
multiplication rule to \(x\times x\). This is because the multiplication
rule assumes that the two quantities (and their errors) are
independent. This is obviously not the case for \(x\) combined with
itself.
When implementing these formulas in Java, you will likely want to use the functions Math.sqrt(...) and Math.abs(...) and Math.pow(..., ...). Here is the documentation for the Math class.
Part 1: Quantity.java Basics (75 points) — Individual or Pair
An object of class Quantity should represent a numerical value having numerical value and uncertainty and attached units, e.g., “9.8 +/- 0.1 meters per second squared.” A Quantity object always has these three pieces of data, although the uncertainty might be 0 (if we know the number exactly) and the collection of units might be empty (for “dimensionless” numbers like π).
Start out by creating a file Quantity.java that contains a definition of a class Quantity.
The Quantity class must contain at least the following fields:
- The numerical value itself (as a double)
- The numerical uncertainty in that value (as a double)
- The attached units (as a Map<String,Integer>).
This last field requires a bit more explanation: the map gives us the (non-zero) exponent for each attached unit. For example, a quantity measured in “meter per second squared” would have a map that associates the key "meter" with the exponent 1 and the key "second" associated with the exponent -2. Similarly, a quantity measured in \(\mathrm{kg}\ \mathrm{m}^2\ /\ \mathrm{s}^3\) would have "kg" mapping to 1, and "m" mapping to 2, and "s" mapping to -3. Try to make sure this map never gives any unit name an exponent of 0. (Take that unit name out of the map, instead).
To start with, create and test the following public methods in the Quantity class:
- A constructor Quantity() of no arguments that creates a default quantity of value 1, uncertainty 0, and no units. (Note that even when there are no units; the object should still contain a map object; it will just be a map with no entries.)
- A constructor that takes a single Quantity argument, and creates a copy (i.e., copying the numeric parts and creating a new map with the same units and exponents).
- A constructor taking 4 arguments: a double (the numeric value), a double (the uncertainty), a List<String> of the units in the numerator (i.e., with positive exponents), and a List<String> of the units in the denominator (i.e., the units with positive exponents). For example, we can use this constructor to represent the quantity \(9.8 \pm 0.1 m/s^2\)
new Quantity(9.8, 0.0, Arrays.asList("m"), Arrays.asList("s", "s"))
You may assume that neither list argument will be null (lack of a list object), but either list might have size 0.
- A method multiply that takes a single Quantity argument, multiplies this by it, and returns the result.
The result should be a brand new Quantity object; neither this quantity nor the argument quantity should change!
- A method divide that takes a single Quantity argument, divides this by it, and returns the result.
The result should be a brand new Quantity object; neither this quantity nor the argument quantity should change!
- A method pow that takes a single int argument (positive, negative, or zero!), raises this to the given power, and returns the result.
The result should be a brand new Quantity object; neither this quantity nor the argument quantity should change!
- A method add that takes a single Quantity argument, adds this to it, and returns the result.
The result should be a brand new Quantity object; neither this quantity nor the argument quantity should change!
If the two quantities involved do not have the same units, your method should abort with an exception:
throw new IllegalArgumentException("...informative message goes here...");
When checking whether the quantities have the same units: the .equals(...) method for two maps should do what you want, as long as the maps don't associate any unit with the zero exponent.
- A method sub that takes a single Quantity argument, subtracts it from this, and returns the result.
The result should be a brand new Quantity object; neither this quantity nor the argument quantity should change!
As with add, you should throw an error if the two quantities involved do not have identical units.
- A method toString() that returns the quantity as a string. So that we can test your code, please put
import java.text.DecimalFormat;
at the top of your file, and paste the following code into your Quantity class, and update the first few lines to match the names of the fields in a Quantity:
public String toString()
{
// XXX You will need to fix these lines to match your fields!
double valueOfTheQuantity = this.NAME_OF_RELEVANT_FIELD;
double uncertaintyOfTheQuantity = this.NAME_OF_RELEVANT_FIELD;
Map<String,Integer> mapOfTheQuantity = this.NAME_OF_RELEVANT_FIELD;
// Ensure we get the units in order
TreeSet<String> orderedUnits =
new TreeSet<String>(mapOfTheQuantity.keySet());
StringBuffer unitsString = new StringBuffer();
for (String key : orderedUnits) {
int expt = mapOfTheQuantity.get(key);
unitsString.append(" " + key);
if (expt != 1)
unitsString.append("^" + expt);
}
// Used to convert doubles to a string with a
// fixed maximum number of decimal places.
DecimalFormat df = new DecimalFormat("0.0####");
// Put it all together and return.
return df.format(valueOfTheQuantity)
+ " ~ "
+ df.format(uncertaintyOfTheQuantity)
+ unitsString.toString();
}
- A boolean method equals that takes any single Object, and returns true if that object is a Quantity (you can test that with “if (... instanceof Quantity) ...”) and its toString() returns the same string as this.toString(). (Hint: don't forget to use .equals(...) instead of the =; they need to be strings with the same contents, not literally the same String object in memory.)
- A method hashCode() that returns an integer, such that equal Quantities always return the same integer. (Hint: this.toString().hashCode())
- A public static void main(String[] args) method that includes at least 2 tests for each method, e.g., creating quantities, printing them, doing arithematic and printing the results, etc. Just remember to use add and sub only when quantities have exactly identical units.
You should feel free to add additional constructors and additional (private) helper methods as you see fit. For example, I wrote a method adjustExponentBy(String unitName, int delta) that adds delta to the existing exponent in the units map. (It also adds the unit to the map, if it wasn't there already, and deletes it from the map, if the adjusted exponent is 0.) Then, the constructor that takes a list of units just calls adjustExponentBy(..., 1) for each element in the numerator, and calls adjustExponentBy(..., -1) for each element in the denominator.
Part 2: Extending Quantity.java (25 points) — Individual or Pair
The remaining methods we need in Quantity use a “database” telling us how to define some units in terms of others, e.g., a "day" is equivalent to exactly 24 hours. We represent this database using a Map<String,Quantity>.
Units that appear as keys in this map are said to be "defined" units; units that are not keys in this map are said to be "primitive" units. A quantity is said to be normalized if the units it its map are all primitive.
The definitions in the database might refer to other defined units (e.g., a day is 24 hours, an hour is 60 minutes, and a minute is 60 seconds).
Here is some code to create a very simple map for test purposes; feel free to include this (or an extension of this) in your main:
Map<String,Quantity> db = new HashMap<String,Quantity>();
List<String> emp = new ArrayList<String>();
// Sadly, Arrays.asList() gives an empty list of Objects
// instead of an empty list of Strings.
db.put("km", new Quantity(1000, 0, Arrays.asList("meter"), emp));
db.put("day", new Quantity(24, 0, Arrays.asList("hour"), emp));
db.put("hour", new Quantity(60, 0, Arrays.asList("minute"), emp));
db.put("minute", new Quantity(60, 0, Arrays.asList("second"), emp));
db.put("hertz", new Quantity(1, 0, emp, Arrays.asList("second")));
db.put("kph", new Quantity(1, 0, Arrays.asList("km"), Arrays.asList("hour")));
(You will also need import java.util.*; or import java.util.Arrays at the top of your file.)
Here are the new methods to add:
- A static method normalizedUnit that takes a String (the name of a unit) and a Map<String,Quantity> (a units database). It should create a brand-new normalized Quantity equivalent to 1 of the given unit. For example, if db is the database defined above, then Quantity.normalizedUnit("km", db) should return the quantity 1000 meter; Quantity.normalizedUnit("hour", db) should return the quantity 3600 second;
and
Quantity.normalizedUnit("kph",db) would return the quantity 0.27777... meters per second.
- A method normalize() that (takes in the database and) returns a copy of this but in normalized form (with all defined units expanded out into primitive units). For example, normalizing the quantity 60 kph, given the above database, should produce the quantity 16.6666... meters per second.
- Extend your main method with at least 3 tests for each of these methods.
The code for normalizeUnit and normalize is surprisingly simple, since each can use the other (in addition to mul, div, and pow).
For normalizedUnit, either the given unit is primitive (in which case the answer is easy to construct), or it has a definition in the database that could be normalized.
Similarly, to normalize a quantity like 30 km^2 / hour, it suffices to observe that
- The products, quotients, and power of normalized quantities are normalized, and
-
\[
\begin{array}{r@{\qquad}c@{\qquad}l}
30\ \mathrm{km}^2/\ \mathrm{hour} & = &
\displaystyle 30\ \mathrm{km}^2/\ \mathrm{hour}\quad \times \quad
\left(\frac{\mbox{1000 meter}}{\mathrm{km}}\right)^2 \quad \times \quad
\frac{\mathrm{hour}}{\mbox{3600 second}}\\
\ \\
&=& \displaystyle
\displaystyle 30\ \mathrm{km}^2\ \mathrm{hour}^{-1}\quad \times \quad
\left(\frac{\mbox{1000 meter}}{\mathrm{km}}\right)^2 \quad \times \quad
\left(\frac{\mbox{3600 second}}{\mathrm{hour}}\right)^{-1}\\
\end{array}
\]
- The quantities “1000 meter” and “3600 meter” should look familiar from the discussion of normalizeUnit.
Submission: Submit your code as Quantity.java.
Totally Optional Extra Credit (up to 10 points) — Pair or Individual
Add a method convert to your Quantity class, that returns the result of transforming the specified quantity into specified units.
For example, if we have a Quantity q representing 100 km/hour, then q.convert(...) with the right arguments
should create a brand-new Quantity representing 1666.66667 meters/minute, and with other arguments should create a brand-new Quantity representing 27.77778 meter-hertz.
If the given quantity would not make sense in the requested units, throw IllegalArgumentException (including a helpful string describing the specific error).
In main, add several tests demonstrating your convert method in action.