RSGC ACES: The Calculus Project |
12. Function.integrate(). As the inverse operation of the well-defined differentiation operation, one would think integration is just as clear cut. However, as you've discovered, if differentiation is a science, integration is an art. In recognition for how advanced your Calculus project is, I'm giving you a lot of latitude with this final segment. Here's what I suggest.
Add methods to your driver to test your new features. Remember to add -1 to your 'f method (see Step 4) so that clients can evaluate the integral of your function.
11. Function.compose. Your Function class preserves the user's definition of f(x), but what about String representations of f'(x) and f"(x)? For these results we require a method that performs the inverse of your parse() method: returning an explicit String from an ETree. This is a challenging but highly rewarding method to write!
Develop the recursive method,
private String compose (ENode root)
that should be invoked on dyTree and d2yTree to enable your Function class to present String representations of f'(x) and f"(x) to users.
10. Function.analysis. (New for 2015). With the roots of the system of functions available, a reasonable analysis of a function can be undertaken. This task can be broken down into two sections, the analysis of discrete behaviour (`x`-intercepts, extrema, points of inflection) and the analysis of interval behaviour (positive and negative range, increase and decrease, and concavity).
Required Task (Discrete)
For each of the functions in RootHarness.txt you are to present an analysis of the `x`-intercepts, extrema, and points of inflection. Using the driver Calculus.java (slightly enhanced from the previous stage) will produce this output.
Elective Task (Interval)
As an elective task that can be used as a replacement for an earlier Calculus stage, have your analysis include interval behaviour for positive and negative range, increase and decrease, and concavity.
9. Function.roots. The key to the analysis of functions is the ability to determine the roots of the functions within the system that includes, `f(x), f'(x), and f''(x)`. Two standard Numerical Methods for Root Finding are known by the names,
Task.
Considerations.
8. Function.prune. You'll notice in the differentiate segment below that statements involving calls to prune and compose methods were commented out. It's now time to implement these methods.
A side effect of the differentiation activity results in subtrees that must be simplified. Typical patterns include,
`0+?, 1*?, ?^1, ?^0 (?\ne 0)`, COC, and FC
to name a few. In situations like these, expression trees require 'pruning'. Develop the recursive method,private ENode prune (ENode root)
that should be invoked on dyTree and d2yTree at the correct moment (ie. differentiating an ETree prior to pruning incurs future performance and efficiency penalties).
7. Function.ErrorCode. Division by zero, a negative radicand and a domain error are situations that can bring your evaluate() method to a dead stop. In this final segment you are to add defensive code to warn of such occurrences, everywhere you can think of.
Exception-handling would be the preferred technique (if you have the courage- go for it) but a lighter approach would simply be to anticipate a possible illegal evaluation beforehand and set the value of a global variable appropriately, prior to returning a value of, say, 1.0. Your own code and that of clients would be responsible for accessing your Function class's public int getErrorCode() method to determine whether the result of the evaluation is to be trusted. An error code of 0 means a successful evaluation. Other codes can be assigned as you deem appropriate. Don't forget to reset the ErrorCode to 0 prior to any evaluation.
Task. Add the methods suggested by the UML diagram. With the exception of the last method, there should be self-explanatory. In the case of the recursive duplicate() method, deep copies of Expression Trees are required in a number of derivative contexts and I've found that it's quite handy to call on a single method to deliver the copy of the ETree. Once developed, temporarily add the following code to your Function's constructor to test the strength of your methods.
dyTree = differentiate(yTree);
//dyTree = prune(dyTree);
System.out.println("dyTree Structure: " + reportStructure(1));
//yp = compose(dyTree);
d2yTree = differentiate(dyTree);
//d2yTree = prune(d2yTree);
System.out.println("d2yTree Structure: " + reportStructure(2));
//ydp = compose(d2yTree);
5. Function.parse(). All the necessary infrastructure is in place (expression grammar, preProcess(), ENode hierarchy, reportStructure() and evaluate()) for software to assume the responsibility of creating the Expression Tree from a function defined as a String.
Add the following statement to your Function constructor,
yTree = parse()
and a sequence of mutually recursive methods starting with,
private ENode expression()
that are used to support and enforce the grammar you created at the beginning of this Project. This interdependent suite of methods forms what is referred to as a Recursive Descent Parser. The RDP manipulates a class variable (int c for cursor, works) that acts as an index into the preprocessed string definition of the function. I would recommend the use of a healthy suite of predicate helper methods (to make your code read well) either built-in or developed by you. For example, the Character class offer isDigit(char ch) and isLetter(char ch) to name a couple of methods.
After the parsing is complete, an Expression Tree has been assembled corresponding to your function. Be sure to have your driver call reportStructure() to confirm the expected tree structure, followed by a call to f(0,x) to lock in your delight :)
System.out.printf("f(%3.1f) = %3.1f\n",1.5,f.f(0,1.5));
The intent of the call is to obtain the value of `f(1.5)`. If the Function f is defined as `f(x)=2x+1`, and your code is functioning properly, the output should be f(1.5) = 4.0. To support the call, add the following methods to your Function class.
public double f(int which, double x)and,
private double evaluate(ENode node)
For the f method, the value of the parameter which is 0 for `y`, 1 for `dy/dx` and 2 for `{d^2y}/{dx^2}`. The f method sets the value of the class variable x and then calls the recursive evaluate method to determine corresponding range element, passing it a reference to the root of the requested function's expression tree.
To test your code's ability to evaluate FNode's correctly as well, comment out the manual assembly of the Expression Tree for `f(x)=2x + 1` and replace it with the function `f(x)=log(100sin(x))` and test it with the driver call,
System.out.printf("f(%3.1f) = %3.1f\n",Math.PI/2,f.f(0,Math.PI/2));
If your code is functioning correctly, the console should display a value close to `2.0`.
3. Function.ENode. The role of the parser (which we have yet to write) will be to construct an Expression Tree (yTree). Just as our earlier studies incorporated classes such as ListNode and TreeNode, we need to develop and implement an ENode class hierarchy whose objects can be used as the fundamental building blocks of our yTree. For example, consider the resultant yTree at the right generated from parsing the expression, 2x+1. This simple example suggests ENode subclasses such as ONode, CNode, and VNode. Throw in an FNode subclass and we should have everything we need for secondary school functions.
Task.
public String reportStructure(int which)that, with the support of a recursive helper method, returns a coded String identifying the infix structure of the Expression Tree. This example should return: COVOC
2. Function.preProcess(). Create a project called Calculus and drop in this driver. Add a class called Function whose constructor receives a String definition of a mathematical expression. Add public String toString() and private String preProcess() methods to your Function class. The latter method modifies the expression in manner that makes it more compatible for the later stages of your analysis. Before you begin it would be wise to refresh your memory of the methods offered by the String Class. Alternatively, the underlying array structure of the StringBuffer class and the methods it offers might be justified. Methods you might find particularly useful include charAt(), indexOf(), and insert().
Considerations should include,
NOTE: From the outset you are encouraged to heavily document your work. It's amazing how quickly you will forget why you coded the way you did. The output should appear as follows (e stands for e, and p stands for π),
1. Syntax Diagrams for Expression Grammar. Of all the assignments you've been asked to complete over the past couple of years, you'll likely remember this as one of the most satisfying. As could be expected of a comprehensive project, it attempts to tie together some of the most important concepts within the co-disciplines of mathematics and computer science. For years to come, you will marvel at what you are about to accomplish. Finally, since your exam will expect that you extend this work in some new direction, you are encouraged to develop efficient and well-documented code.
The first step in writing IMAGE-Algebra&Geometry many years ago was a three-month investment in developing a grammar for the language before an interpreter could be undertken. Similarly your first step in developing application software to manipulate mathematical expressions is to develop an unambiguous grammar for the expressions you hope to interpret.
Using appropriate knowledge and vocabulary, complete a sequence of syntax diagrams for the broadest range of mathematical expressions you have encountered to this point in your development. Here's how the set of rules will start out. You should consider including most (if not all!) of these definitions in your grammar.