If the difference table algorithm finds at least 4 contiguous changes
of sign in a difference level it is signalled as the presence of
numerical error. Each level at which this occurs is displayed so
that you can decide for yourself the quality of the data. The error
is the estimated variance of the input data based on the variance
found in the difference data at that level.
Approximate Derivatives
A simple finite difference can be used to approximate the derivative of
a sample function, f(x), if the steps size, h, is small enough. If we
denote the difference operator by D, the second difference by D2, ect,
then
and the derivative is approximated by
Differences can be calculated with the ndiff_fndf function,
> dfval = ndiff_fndf(hcb, x, 1, 0.01, 'FORWARD');
|
where
-
hcb = the handle to the callback function
-
x = the x value at which to calculate the difference
-
1 = the order of the difference
-
0.01 = the step size, h
-
'FORWARD' = type specifier for a forward difference
The HMD function for calculating the raw finite difference approximation
of a derivative, as shown above, is nderivative_fndf. For example,
> dfval = nderivative_fndf(hcb, x, 1, 0.01, 'FORWARD');
|
where
-
hcb = the handle to the callback function
-
x = the x value at which to calculate the derivative
-
1 = the order of the derivative
-
0.01 = the step size, h
-
'FORWARD' = type specifier for a forward difference
The sample function would be defined, for example, with
function f_x { double x } {
|
double ret;
ret = sin(x);
return ret;
|
}
|
and registered as a callback function with
> hcb = callback_double(f_x, 1, $1);
|
The problem with a raw finite difference approximation is the contention
between truncation error (accuracy) and roundoff error (or bad sample
function error). In order to get an accurate answer the step size, h,
must be very small. But derivatives of the 3rd order and higher begin
to show numerical divergence for reasons discussed above in the section
on difference tables. For that reason the raw finite difference
approximation has limited practical use unless you can generate samples
with very high precision.
The work-around for getting a numerical derivative approximation is
to create an interpolation polynomial of sufficiently high order in the
interval of interest and calculate its derivative. This is what the
pderivative_fndf does. An example of this function call would be
> dfval = pderivative_fndf(hcb, x, 1, 0, 2, 10);
|
where
-
hcb = the handle to the callback function
-
x = the x value at which to calculate the derivative
-
1 = the order of the derivative
-
0 = the lower limit of the interpolation interval
-
2 = the upper limit of the interpolation interval
-
10 = the number of samples to take
Integration
Introduction
The standard method for the numerical integration of some function f(x)
over an interval a <= x <= b is
For example, the Trapezoid Rule is
and Simpson's Rule is
where x1 = a, x2 = a + h, x3 = a + 2h = b.
The remainder term, R(z), gives an indication of the truncation error
of the integration, and is usually represented as
The numerical integration library provides methods for integrating
functions, f(x), using predefined formulas such as the Trapezoid Rule and
Simpson's Rule. The user may also define integration formulas,
inserting the necessary weights and error term factors.
An integration must be set up with three basic steps. First, a function
f(x) must be defined and declared as a callback function. Second, an
integration rule must be declared, which establishes a handle to be used
to access the integration object. Third, the integration is performed
with specified limits of integration.
The two basic function calls for implimenting an integration are
defined_rule_intg
and
integrate
the former specifies the integration formula. The latter does the integration
on the limits.
Defining a Simple Integration Rule
A simple integration rule is one which defined as a weighted sum of equally
spaced function samples. There are different ways of representing the
formulas for numerical integration. In the following tutorial, the simple
formulas conform to the representations in Moursund and Duris, "Elementary
Theory and Application of Numerical Analysis."
For the Trapezod Rule,
the weights are c0 = 1/2 and c1 = 1/2.
The remainder gives an error estimate, and is composed of three factors:
a truncation error factor, errFac, the mth derivative of the sample function
evaluated at an unknown location, z, in the integration interval, and
the step size raised to the m+1 power. For the Trapezoid Rule, the errFac
is -1/12.
The rule definition is given by the code
> c0 = 1/2;
|
> c1 = 1/2;
|
> errFac = -1/12;
|
> m = 2;
|
> hnd_trap = defined_rule_intg(1, 2, c0, c1, 0, 0, errFac, m, hfunc);
|
The first argument in the call is the number of coordinate dimensions of
the problem, which is 1. The rest of the arguments supply the formula
parameters. After the dimension are given the integration weights,
c0 and c1, preceded by the number of weights that follow. The next
two arguments are not used for simple formulas. Then the magnitude of
the truncation error is given, the order of the remainder derivative,
and a handle to the sample function to be integrated.
The essential ingredients of the simple formula are the weights, the
error factor, and the order of the remainder derivative.
The sample function, f(x), must be defined and declared as a callback.
For example, the following code gives a function definition.
function f_x { double x } {
|
double ret;
ret = sin(x);
return ret;
|
}
|
Then this function must be declare for use as a callback function by
the integrator,
> hfunc = callback_double(f_x, 1, $1);
|
This provides a handle to the function that is passed as a parameter
to the defined_rule_intg method. In practice, these sample function
declarations must come before the call to defined_rule_intg.
Optionally, another argument can be given, which is a handle to a
user-provided function that gives the remainder derivative of the
sample function. This is needed for the computation of an error
estimate. If a remainder derivative is not provided an attempt will
be made to calculate a numerical approximation by finite differences.
In the special case of the Trapezoid Rule a predefined method is also
provided,
> hnd_trap = trapezoid_intg(1, hfunc);
|
which is used for simple testing.
Finally, the integration is performed. For example, with integration
over the 0 <= x <= 1,
> a = 0;
|
> b = 1;
|
> value = integrate(hnd_trap, a, b, 1);
|
The integration formula given in mathematical tables will be in
terms of multiple function samples and a step size, h, not just the
two endpoints shown above:
The use of multiple function samples is equivalent to subdividing the
integration interval into smaller intervals, h, and summing the results
of each of the smaller integrations. The integrate method does the
subdivision for you. You simply give the number of smaller intervals as
the argument that follows the limits of integration. The step size is
automatically calculated. For example, for 4 subintervals the function
call looks like
> value = integrate(hnd_trap, a, b, 4);
|
The error bounds on the calculated integral are automatically printed
to the log file. But they can be acquired programmatically by calling
get_error_intg, as in the following example.
> errterm = get_error_intg(hnd_trap);
|
> print errterm;
|
where the handle to the rule definition is again used. The value returned
by get_error_intg is an array of two elements: the lower estimate and
the upper estimate. These are calculated by evaluating the remainder
at x = a and x = b.
There is also a predefined Simpson's Rule formula,
> hnd_simp = simpson_intg(1, hfunc);
|
The rule definition for Simpson's Rule could be done with the
defined_rule_intg method as follows,
> c0 = 1/3;
|
> c1 = 4/3;
|
> c2 = 1/3;
|
> errFac = -1/90;
|
> m = 4;
|
> hnd_simp = defined_rule_intg(1, 3, c0, c1, c2, 0, 0, errFac, m, hfunc);
|
Defining a Gaussian Integration Rule
A Gaussian integration rule makes use of the of the sample point locations
as parameters in addition to the weights. Therefore, there are twice as
many coefficients in the integration and twice the order of approximation.
An easy example of a Gaussian integration is taken from Hamming, ch 19.2.
and let
w1 = 5/9, w2 = 8/9, w3 = 5/9
x1 = -sqrt(3/5), x2 = 0, x3 = sqrt(3/5)
For the error term, see Hamming, ch 19.5.
For N = 3 (three weights),
then
The Gaussian integration rule is then defined with the
defined_rule_intg command like
> c1 = 5/9;
|
c2 = 8/9;
|
c3 = 5/9;
|
x1 = -sqrt(3/5);
|
x2 = 0;
|
x3 = sqrt(3/5);
|
errFac = 1 / 31500;
|
> hnd_gauss = defined_rule_intg(1, 3, c1, c2, c3,
3, x1, x2, x3, 0, errFac, 6, hfunc, hf6);
|
Here was assume that hfunc is the handle to a callback function,
f(x), and hf6 is the callback handle to a literally defined 6th-order
derivative of f(x). The Gaussian rule as given above may only be used
on the integration limits from which the formula was derived: -1 to +1.
We may therefore perform the integration on those limits like this
dval = integrate(hnd_gauss, -1, 1, 1);
|
msgprint dval;
|
errterm = get_error_intg(hnd_gauss);
|
print errterm;
|
which is an interval from -1 to +1, not subdivided.
As long as we still use those integration limits we can also subdivide
the integration interval, thereby achieving greater accuracy. Below we
increase the subintervals from 1 to 4,
dval = integrate(hnd_gauss, -1, 1, 4);
|
msgprint dval;
|
errterm = get_error_intg(hnd_gauss);
|
print errterm;
|
To be useful the integration rule must be capabale of integrating over
arbitrary intervals. In order to achieve this with Gaussian rules we
must call defined_rule_intg with extra parameters. After the list of
x coordinates, and before the error factor, we must insert the number of
integration limits and the limits themselves for which the formula was
derived. For the Gaussian rule we
are using for an example, the formula was derived with limits -1 to +1, so
we need the three parameters 2, -1, 1 in the call to defined_rule_intg,
hnd_gauss4 = defined_rule_intg(1, 3, c1, c2, c3,
3, x1, x2, x3, 2, -1, 1,
errFac, 6, hfunc4);
|
The Error Calculation
The default behaviour of the integrate function is to recalculate the
estimate for the integration if the limits of integration are changed
or the number of subintervals is changed. If you don't need the error
estimate, you can turn off that automatic computation using set_intg,
> set_intg(hnd, 'errflag', 0);
|
where "hnd" is the handle to the integration rule. To turn it back on
again, use "1" for the argument,
> set_intg(hnd, 'errflag', 1);
|
As was shown above, the error estimate for a simple rule is usually
given by the formula
which complies with the representation from Moursund and Duris,
"Elementary Theory and Application of Numerical Analysis," section
6-5.
The formula for the Gaussian error estimate, as shown above, is
and is derived in Hamming, "Numerical Methods for Scientists and
Engineers", section 19.5.
This is essentially the truncated remainder from the Taylor series
approximation for the integral, and is commonly called the truncation
error. The mth-order derivative of the integrated function, f(x), must be
evaluated at the unknown value of x = z, but we do not know the value of z
(or else we would already know the exact answer). The error computaion is
performed at each of the lower and upper limits of integration. So the
error computation provides a good guess for the lower and upper bounds
of the error correction to the result of your integration calculation.
But the error computation requires a function be given for the mth-order
derivative of the integrated function, f(x). This can be defined by the
user, and its callback handle passed to the defined_rule_intg function,
or set later with the set_intg function. This is an optional parameter
because often we do not need the error computation. But if the user does
not provide an explicit function for the mth-order derivative, a finite
difference approximation to it will automatically be calculated, unless
you set "errflag" to 0 as discussed above.
It is often inconvenient to actually work out the mth-order derivative for
a function, f(x). So the HMD integration function estimates one for you.
This is done by creating an interpolation polynomial that approximates
the function, f(x), within the given limits of integration. Then its
mth-order derivative is evaluated. In order to obtain a reasonable
degree of acuarcy for this estimate a polynomial with twice the order
as the derivative is created. The default method for creating the
interpolation polynomial is by construction of the Vandermonde matrix.
The most important consideration for the error calculation is that you
must provide the error factor, Em, and the order, m, of the remainder
derivative. For all of the well-known formulas these parameters have
already been derived. For example, for the Trapezoid rule Em = -1/12
and m = 2, and for the Simpson rule Em = -1/90 and m = 4. The derivations
of these parameters can be found in Moursund and Duris, Hamming, and
other sources.
Ordinary Differential Equations (ODE)
Introduction
ODE means Ordinary Differential Equation, more specifically a first-order,
differential equation in one independent variable. A first-order ODE is
usually represented, for numerical work, as
and the solution is written formally as the indefinite integral
Over a sufficiently small interval in x, x1 - x0 = h, the derivative f(x,y)
is assumed to be nearly constant, and therefore the indefinite integral
can be approximated by the simplest quadrature formula
Given known values of x = x0 and y = y0 at the lower limit of integration
the value of the integral at a small distance h from the starting point
can be approximated by the formula
which is called Euler's method. After the value y = y1 is calculated at
the point x = x1, the point x1 is treated as the new starting point. The
procedure is repeated to obtain y = y2 at x = x2.
Example
The ordinary differential equation y' + y = 0 is known to have the solution
y = exp(-x). Using initial conditions x0 = 0, y0 = 1, and h = 0.1, the
value of y = y1 is calculated:
y | = | y0 | + | f(x0, y0) * h |
| = | 1 | + | (-y0) * 0.1 |
| = | 1 | + | (-1) * 0.1 |
| = | 0.9 | | |
|
In the HMD interactive interpreter (hmd -i) you can type in the following
statements.
> y = 1;
|
> h = 0.1;
|
> y = y + h*(-y);
|
> y
|
Type y again without a semicolon and its value will be printed. Now
press the up arrow key to repeat the last command,
You can step forward in the integration by pressing the up arrow and
return keys.
The result of performing the Euler method with this equation and these
initial conditions is shown in the table below, along with the accurate
values in the column under Y.
x | y | Y | err = 100 (y-Y)/Y |
0 | 1 | 1 | 0 |
0.1 | 0.9 | 0.9048 | -0.53050 |
0.2 | 0.81 | 0.8187 | -1.0627 |
0.3 | 0.729 | 0.7408 | -1.5929 |
0.4 | 0.6561 | 0.6703 | -2.1185 |
0.5 | 0.59049 | 0.6065 | -2.6397 |
0.6 | 0.531441 | 0.5488 | -3.1631 |
0.7 | 0.478297 | 0.4966 | -3.6857 |
0.8 | 0.430467 | 0.4493 | -4.1916 |
0.9 | 0.38742 | 0.4066 | -4.7172 |
1.0 | 0.348678 | 0.3679 | -5.2248 |
The HMD library contains a function for performing the Euler integration
called "ode_euler".
> output = ode_euler(hx, x0, y0, N, fxy);
|
In the function arguments, hx is the step size, x0 the initial x position,
and y0 the initial y position. N is the number of steps to perform in the
integration. The arguments that follow are used to specify how the ODE
function y' = f(x,y) is given to ode_euler. A function f(x,y) will be
defined in the interpreter workspace, and it will be used as a callback
function invoked by ode_euler at each integration step. The fifth argument
is the name of the previously-defined callback function. This callback
function must always be defined to take 2 double-type arguments, where the
first will be given the x coordinate, and the second will be give the
y coordinate.
For example, the function f(x,y) as given previously, y' = -y, would be
defined in the HMD workspace as
function my_fxy { double x, double y } {
|
double ret;
ret = -1.0 * y;
return ret;
|
}
|
When working in an interactive session (hmd -i) it will not be possible
to enter the above function definition line by line. It can be written
into a file by itself, say my_fxy.hmd, and then source'd into the
interactive session with the source command.
In the particular version of f(x,y) we have chosen the x variable is not
used, but the ode_euler callback functions must be defined in this way.
So a complete sequence of commands in the interactive session would look
like
> hx = 0.1;
|
> x0 = 0.0;
|
> y0 = 1.0;
|
> N = 10;
|
> source 'my_fxy';
|
> output = ode_euler(hx, x0, y0, N, fxy);
|
Data
The data from the integration is returned in the "output" variable. This
variable is a "structure", similar to the struct type in C. It has at
least three members: data, rows, and columns. The members are accessed
by following the variable name, "output", by a dot and then the member name:
output.rows
output.columns
output.data
In the example above the data member will be an array of double-precision
numbers. The array will have 3 rows and 10 columns. The columns contain
the integration steps, and the rows contain x, y, and y'. So, if you print
the value of output.rows, it should be 3:
The values in the data array can be printed to the screen using the
"dump_block" function.
> dump_block(output.data, 1, 'euler.dat');
|
Plots
The output data can be plotted using the HMD plot command. The command
> plot(output.data, output.data, 'row 2');
|
will plot the x-y values in the data. You can also plot the y derivative
values that were computed at each point with
> plot(output.data, output.data, 'row 3');
|
The first argument to plot is the data set to be used for the x data set.
Since there is no option specifying the row number it is assumed to be
row 1 (the rows are counted 1, 2, 3). The second data argument is assumed
to be the first y-axis data set, as there can be multiple y data sets.
Because the data argument has more than one row it is necessary to specify
the row number with an option string.
The HMD plot command is a front end to the Gnuplot program, which must
installed already on your computer.
ODE Solvers in HMD
The HMD program provides several ODE solvers as single function calls
(as opposed to building blocks, discussed later):
A trivial Euler model,
> output = ode_euler(hx, x0, y0, N, fxy);
|
The 2nd order predictor/corrector, that is, "Heun" model,
> output = ode_heun(hx, x0, y0, N, Ni, fxy);
|
A 4th order Adams predictor/corrector model,
> output = ode_adams(hx, x0, y0, N, Ni, fxy);
|
The common 4th order Runga-Kutta model,
> output = ode_rk45(hx, x0, y0, N, fxy);
|
You need only supply the function y' = f(x,y) and the initialization
arguments,
hx = step size
x0 = initial x value
y0 = initial y value
N = the number of steps to calculate
and for the prdictor/corrector models an additional argument, Ni, which
is the number of times to iterate the corrector equation.
The return value is a block of data that can be view or saved using the
dump_block function (see the Data discussion above). The block of data
is a matrix with as many columns as there are steps in the calculation (and
the initial values). There are rows for each order of derivative used
in the model plus one for the x values. The first row are the x values.
The second row contains the y values, and the third row contains the first
derivative values.
Building Blocks for ODE
The ODE problem can be assembled in steps of elementary building blocks.
The following functions are used for that purpose.
init_ode
set_function_ode
compute_derivatives_ode
step_ode
add_datatable_ode
get_ode_data
The elementary ODE functions are used for creating a specific ODE model
not provided in the HMD library. To illustrate their use, consider the
Euler model presented above.
Define the initial constants for the ODE as before.
> hx = 0.1;
|
> x0 = 0.0;
|
> y0 = 1.0;
|
> N = 10;
|
Then "open" a handle to a new model with the init_ode function,
> h_odemodel = init_ode(hx, np, nO, x0, y0, [1.0; 1.0]);
|
The init_ode function requires some extra parameters that were not introduced
before. These new parameters allow ODE models to be assembled as either
single-step or as multistep methods. This will be explained in the following
sections. The Euler ODE method is a single-step, first-order method, and
so we set the number of (past) steps to 1,
and we set the highest derivative order to 1,
The array argument at the end provides the "weights coefficients" for
previous step values and derivatives. For the Euler method this is trivially
1, and 1. That means the coefficient of the last y value is 1, and the
coefficient of the last y' value is 1.
The integrator must be given the name of the ODE function to call, and this
is done with the set_function_ode function,
> set_function_ode(h_odemodel, 1, 1, fxy, 2, $1, $2);
|
We must use, for the first argument, the handle returned from the init_ode
function call to identify the model. The second argument specifies which
stage of the computation will use the callback function. A "stage" is one
of a sequence of equations computed for one step of the integration. For
example, in a predictor-corrector model the predictor is stage 1, and the
corrector is stage 2. For this example there is only one stage, so the
second argument is trivially 1, meaning stage 1. The third argument specifies
what derivative order to be assigned to the callback function. The Euler
method has only a first-order derivative, so this is set to 1.
The set_function_ode function requires (at least) 2 parameters to specify the
callback function. The last 4 arguments shown above for a standard set of
callback paramaters: the callback function name, the number of arguments
that will be passed to the callback, and a list of symbolic representations
for the callback function parameters. $1 means x, $2 means y, $3 means y',
$4 means the second derivative, etc.
The work of integration of the ODE is performed by the step_ode function.
This function does all the computations in the step from position x[n], y[n],
to position x[n+1], y[n+1]. The step_ode function may be called for each
step, or it may be called once with a repetition parameter which will cause
it to calculate N steps forward.
> step_ode(h_odemodel, 'repeat', 10);
|
At the end of each step the step_ode function performs the calculation
of y' = f(x,y) for the y value just computed, and its value will be used for
the next step in the integration. That means that before the first step the
derivative y' = f(x,y) must be manually calculated for the initial position
x0, and y0. This is done with the compute_derivatives_ode function.
It must be called before the first call to step_ode.
> compute_derivatives_ode(h_odemodel);
|
The data values from each step in the integration can be printed to the
console by setting a global variable, ode_step_print, before beginning
the steps.
And the output prints can be turned off again by setting it to 0.
But the step_ode function can automatically write the output data into
a data table with a given number of steps. To setup a data table you
must call the add_datatable_ode function before any computations are
performed.
> add_datatable_ode(h_odemodel, N);
|
The number, N, of columns of the data table must match the number of steps
to be calculated with step_ode. The number of rows of the table will be
automatically assigned to match the highest-order derivative in the
integration. That is, the data table will have in its first row the x
values. The second row will contain the y values. The third row will
contain the y' values. For higher-order calculations, such as a Taylor
method, there will be more rows to contain higher derivative values.
To obtain the data in the data table as an array (matrix) in the HMD
workspace, use the get_ode_data function.
> output = get_ode_data(h_odemodel);
|
The return value from get_ode_data will be an output struct with the
same format as shown above for the ode_euler function, that is, it will
have at least the three members
output.rows
output.columns
output.data
This data can be saved or viewed with the dump_block or
plot functions.
Multistep ODE Building Blocks
A multistep ODE integration uses y values from past steps in the integration.
With a choice of appropriate coefficients as factors on these past step
values the order of the integration will increase. For example, the Euler
integrator shown above can be rewritten as
y[n+1] = y[n] + h * y'[n]
and is a single step integration. It is single step because the indices
on the right hand side select only the values from the last integration step.
An integrator that uses the last two steps might look, for example, like
y[n+1] = y[n] + A * y[n-1] + h * (y'[n] + B * y'[n-1])
The integration uses the steps n and n-1 as shown on the right
hand side. Thus, the second argument to the init_ode
function is used. The value of np specifies the number
of (past) steps to be used. The Euler model uses only one past
step value, so the value of np would be 1.
Most often a multistep model will also be a multistage model, which means
an integration step will be performed as two (or more) stages. One type
of multistage ODE model is a predictor-corrector model. Each equation in
a multistage model is constrained to be a difference equation in the past
y and y' values with a choice of coefficients. So the specification of an
equation to be used as a stage in the integration step will require
-
np, the number of past step values to be used in the stage
-
nO, the order of the highest derivative (usually 1) to be used
-
the coefficients of the past y and y' values
The init_ode function automatically creates the first stage in
a multistage model. More stages can be added to the model using
the add_branch_ode function. The equation used for a stage of
the integration is also called a "branch". The add_branch_ode
function requires two arguments: the handle to the ODE model
that was returned from the init_ode function, and a matrix of
coefficients, called "weights".
As an example, consider the simplest multistage model using only the last
step in the integration. It is variously called the Heun model, or the
implicit Euler model, or a Backward Difference Formula (BDF).
y[n+1] = y[n] + (h/2) * (y'[n] + y'[n+1])
The right hand side contains, implicitly, the value of the derivative at
the n+1 step. But the n+1 step has not been calculated yet. It is therefore
called an implicit method. A starting value for y'[n+1] must be obtained
first. The stage that calculates the starting value is called the predictor,
and the final, implicit, calculation stage is called the corrector.
The first stage of the integration is performed using the formula
y[n+1] = y[n] + h * y'[n]
which is just the euler calculation. And the trial value of y'[n+1] is
calulated with f(x+h, y[n+1]). The init_ode function sets up the first
branch
> h_odemodel = init_ode(hx, np, nO, x0, y0, [1.0; 1.0]);
|
The coefficients for the first branch are given by the 2-row, 1-column
matrix
1.0
1.0
Row 1 of the weights matrix gives the coefficients on the past y values,
and row 2 gives the coefficients on the past y' values.
Each branch of the ODE model is given its own handle number to be referenced
later. The init_ode function always assigns a handle equal to 1 to the
first ODE branch.
The corrector branch of the ODE
y[n+1] = y[n] + (h/2) * (y'[n] + y'[n+1])
is added using the add_branch_ode function.
> h_ode2 = add_branch_ode(h_odemodel, [1.0; 0.5], [0.5, 0.0], [1,0]);
|
Here the coefficients assigned to this branch are given by the matrix
1.0
0.5
because the coefficient on y[n] is 1, and the coefficient on y'[n] is 1/2.
The coefficient on the implicit value, y'[n+1], is provided in the next
argument, the row array
The second entry in this array, as well as the last
parameter [1,0], will be explained later. For two-stage predictor/corrector
models the last two arguments of add_branch_ode will always look like
where "B" is the implicit coefficient on y'[n+1].
The size of the weights matrix must correspond to the ODE parameters given
in the init_ode function call. The matrix will have np
columns and nO+1 rows. The format of the matrix is shown below,
| n | n-1 | n-2 | ... |
y | A1 | A2 | A3 | ... |
y' | B1 | B2 | B3 | ... |
y'' | C1 | C2 | C3 | ... |
. | . | . | . | ... |
Note that each branch, or "stage", of the model must be provided with a
different weight table as shown above. The weights for the first branch
are always provided as the last argument to the init_ode function,
and for each successive branch in the second argument of add_branch_ode.
The following example shows the code for building the Heun, or "implicit
Euler" model.
> hx = 0.1;
|
> x0 = 0.0;
|
> y0 = 1.0;
|
> N = 10;
|
> np = 1;
|
> nO = 1;
|
> source 'my_fxy.hmd';
|
> h_odemodel = init_ode(hx, np, nO, x0, y0, [1.0; 1.0]);
|
> set_function_ode(h_odemodel, 1, 1, fxy, 2, $1, $2);
|
> h_ode2 = add_branch_ode(h_odemodel, [1.0; 0.5], [0.5, 0.0], [1,0]);
|
> set_ode(h_odemodel, h_ode2, 'bwmcnt', 3);
|
> add_datatable_ode(h_odemodel, N);
|
> compute_derivatives_ode(h_odemodel);
|
> step_ode(h_odemodel, 'repeat', N);
|
> output = get_ode_data(h_odemodel);
|
> dump_block(output.data, 1, 'heunA.dat');
|
Notice the extra function
> set_ode(h_odemodel, h_ode2, 'bwmcnt', 3);
|
in the above example. By setting the 'bwmcnt' parameter to 3 the y'[n+1]
value will be reiterated 3 times to improve the accuracy.
There are some details you should observe. Notice that the
size of the data table created with add_datatable_ode is equal to the
number of steps given in step_ode. You must include the call to
compute_derivatives_ode before the call to step_ode so that the table
of past steps will be initialized. The call to set_ode with 'bwmcnt' is
not necessary, but it is known that reiterating the corrector will
slightly reduce the truncation error of the step. The accuracy does not
improve after about 3 or 4 iterations. Lastly, observe that the single
call to step_ode could be separated into repeated single steps with
the call
> step_ode(h_odemodel, 'repeat', 1);
|
Runga-Kutta Building Blocks
Runga-Kutta is the name for an approach to solving ODEs. It is usually
called a "single step" approach, as opposed to the multistep approach
of predictor/corrector models. That means that a Runga-Kutta model does
not need to include the results from past step calculations to calculate
the next step. The most well-known Runga-Kutta calculation is the 4th
order method, given by the following algorithm:
y(n+1) | = | y(n) | + | (1/6) * (k1 + 2*k2 + 2*k3 + k4) |
|
k1 | = | h * f(xn, y(n)) |
k2 | = | h * f(xn + h/2, y(n) + k1/2) |
k3 | = | h * f(xn + h/2, y(n) + k2/2) |
k4 | = | h * f(xn + h, y(n) + k3) |
|
It is not a multistep calculation, but it does use multiple stages. These
stages (k1, k2, k3, and k4) are obviously of a different nature than the
two stages of predictor corrector models. But there is a similarity from
the computer programming point of view (see The General Linear approach,
below).
The building block approach to Runga-Kutta models follows the same pattern
of assembly as for the predictor/corrector models. A "branch" of the ODE
will be created for each of the calculation stages k1, k2, k3, k4, but with
a minor difference that a special fifth stage is supplied. Also, because
the Runga-Kutta models calculate a weighted sum of the k's, an extra
building block function called a "quadrature" must be provided.
An example set of building blocks that computes the 4th order Runga-Kutta
models is shown here.
> hx = 0.1;
|
> x0 = 0.0;
|
> y0 = 1.0;
|
> N = 10;
|
> np = 1;
|
> nO = 1;
|
> source 'my_fxy.hmd';
|
> h_odemodel = init_ode(hx, np, nO, x0, y0, [1; 0]);
|
> set_function_ode(h_odemodel, 1, 1, fxy, 2, $1, $2);
|
> h_ode2 = add_branch_ode(h_odemodel, [1;0], [1/2,0,0,0], [0,0,0,0]);
|
> h_ode3 = add_branch_ode(h_odemodel, [1;0], [0,1/2,0,0], [0,1/2,0,0]);
|
> h_ode4 = add_branch_ode(h_odemodel, [1;0], [0,0,1,0], [0,0,1/2,0]);
|
> h_ode5 = add_branch_ode_implicit(h_odemodel, [0,0,0,1], [0,0,0,1]);
|
> h_odequad = add_quadrature_ode(h_odemodel, h_ode4,
|
[1/6, 1/3, 1/3, 1/6], [1;0]);
|
> add_datatable_ode(h_odemodel, N);
|
> compute_derivatives_ode(h_odemodel);
|
> step_ode(h_odemodel, 'repeat', 10);
|
> output = get_ode_data(h_odemodel);
|
> dump_block(output.data, 1, 'rkex45A.dat');
|
The correspondence of the k stage calculations to the weight arrays given
as arguments to the branches will be preesented here. But a more complete
picture of this algorithm must wait for the discussion on The General Linear
ODE model below.
The first branch of the ODE is created by calling the init_ode function,
but for a Runga-Kutta model the first branch does not really start the
calculation. It just provides y0. The calculation of the k's starts in the
second branch and finishes with the special fifth branch. Think of it this
way: whereas in the predictor/corrector model the result of a branch
is the value y[n+1], a branch of the Runga-Kutta model produces the "y"
input to f(x,y) used in the next k calculation. Thus we have
y(n) | output from branch 1 |
y(n) + k1 / 2 | output from branch 2 |
y(n) + k2 / 2 | output from branch 3 |
y(n) + k3 | output from branch 4 |
|
and k4 itself needs the special fifth branch to be calculated. The
calculation of each k(n)/h is performed as the f(x,y) using the output of
the previous branch for the y parameter. The quirk of this algorithm
is that the k(n) values are logically matched according to the "stage"
of the calculation, i.e., k1 to stage 1, k2 to stage 2, etc. But k1
is actually computed at the beginning of branch 2, k2 at the beginning
of branch 3, etc.
The arrays [1; 0] supplied as the second argument to add_branch_ode
are coefficients of 1 to y(n) and 0 to y'(n). This is what produces the
y(n) part of the branch outputs shown above.
The third argument to add_branch_ode are the "implicity" weights with
the following correspondence
branch 2: | [1/2, 0, 0, 0] | --> | coefficient of k1 |
branch 3: | [0, 1/2, 0, 0] | --> | coefficient of k2 |
branch 4: | [0, 0, 1, 0] | --> | coefficient of k3 |
|
The last argument to add_branch_ode gives the weights that produce
a fractional step size in the variable x as the first argument to f(x,y).
The inclusion of add_quadrature_ode after all the branches have been
created is what adds the final calculation of the Runga-Kutta step,
y(n+1) = y(n) + (1/6) * (k1 + 2*k2 + 2*k3 + k4)
where obviously the weight array gives the coefficients of the k(n)
in the weighted sum.
The General Linear ODE Approach
John C. Butcher in his book, "Numerical Methods for Ordinary Differential
Equations", Second Edition, presents the General Linear Method for
representing ordinary differential equations. The most significant
result of the General Linear Form is to show a correspondence between
the computations of Predictor/Corrector models and Runga-Kutta models.
The General Linear Form is a matrix M given as
The submatrix U contains the coefficients of past step values, the submatrix
A contains coefficients of h*f(x,y) that will be used to calculate either
y'(n+1) for a predictor/corrector, or the k's for a runga-kutta model. The
submatrices B and V contain coefficients for the final, combining sums that
result in y(n+1).
The solution process is represented as a matrix equation
G = M * X
or
This can also be written
The size of the input vector is determined by the "s" stages
of the calculation and the number "r" of past step values used.
This complicated- looking set of equations can be made easier to see
by comparison to the known ODE methods. The first set of equations,
Y_i, represents the two equations in the predictor/corrector method.
Or it represents the calculation of k1, k2, k3, k4 in the 4th order
Runga-Kutta method. The second set of equations, W_i, represents
the weighted sum that finishes the Runga-Kutta step.
To see this better, consider the 2nd order predictor/corrector step
calculation,
The first calculation contains only the second sum of Y_i, that is, all
the coefficients a_ij are zero. The second calculation of the
predictor/corrector has the input, F_1, as the f(x,y) value using as y
the Y_1 from the previous stage. This could also be shown as
In this example the submatrix A would be
and the submatrix U would be
In this case, the single equation for W_1 is just a repetition of the
Y_2 equation. This is just saying that the predictor/corrector needs
no final weighted sum. But formally, to satisfy the matrix equation we
could write
From the above example it should be obvious that the General Linear procedure
is not as simple as a linear algebra calculation. The input vector must be
updated after each row computation, as can be seen with the F_i values.
But there is another piece of information missing in the representation:
the step position in x where f(x,y) is to be evaluated for the value of F_i.
As another example, consider the 4th order Runga-Kutta step calculation,
This would be represented in terms of the General Linear matrix as
In terms of the linear equations for Y_i and W_i this would be written as
It will be seen that k1 = h * F_1, k2 = h * F_2, etc. As mentioned above,
the F_i are updated after each row calculation. So the k1, k2, k3, k4 are
calculated between stages. The k4 is calculated after stage 4. Also, observe
that Y_1 is the input to F_1, Y_2 is the input to F_2, and so on, as required
for the calculation of the k values.
In the W_i calculation the F_i are NOT updated after each row. Now the F_i
are all the values computed for the Y_i equations.
The HMD Implimentation of the General Linear Form
The HMD implimentation of the General Linear ODE approach obeys the following
rules,
-
Each row of the upper partition of the General Linear matrix is
represented by an ODE "branch" and is roughly equivalent to a "stage"
of a multistage model. So the Y_1 calculation is set up with the
init_ode
function, the Y_2 calculation is set up with the
add_branch_ode
function for the second branch, and so on.
-
Each row of the lower partition is represented by a "quadrature"
calculation that is set up by calling the add_quadrature_ode
function.
-
The result of the last quadrature calculation will be the output
for the ODE step. If no quadrature calculation is defined then
the output of the ODE step will be the result of the last branch
calculation.
-
The coefficients in the U submatrix correspond to the "weight
table" that is given as a parameter to each of the
add_branch_ode function calls. Each row of U corresponds
to one branch weight table as a concatenation of its columns.
For example, if the weight table, w_ij, has 2 rows and 3 columns
then that row of U would correspond to w_11, w_21, w_21, w_22,
w_31, w_32. The coefficients of the A submatrix are given by
the "implicit weights" that are passed to add_branch_ode. The
weights arrays (matrices) given on each call to *add_branch_ode*
constitute a single row of the A or U submatrix of the General
Linear matrix.
-
The 4th parameter to the add_branch_ode function call is an
array that has no correspondence with the General Linear matrix.
It provides the fractional positions within an ODE step where the
F_i = f(x,y) will be calculated. For a predictor/corrector model
this is trivially 0 or 1 (x0 or x0 + h). For a Runga-Kutta model
it is 0, 1/2 and 1 (x0, x0 + h/2, x0 + h).
The core of the HMD integrator for ODE is the internal Ode_Step function,
which first loops through the ODE branch objects (stages) then loops through
the ODE quadrature objects (weighted sums).
Stopping ODE Integration with a Trap
The need might arise where the integration should be stopped if the
integration state, y, takes values that are out of bounds with
respect to the derivative calculation. For example, the value of
y becomes less than zero or becomes greater than some upper
bound. For this circumstance you can set up a "trap" with the
function call trap_ode_model.
> trap_ode_model(h_model, 0, 'LT', 0.0);
|
The function syntax requires four parameters: the model handle,an
integer specifier of the order of derivative of the ODE state to
trap, a string specifier for the type of test condition, and the
value that it should be compared to. The integer specifier for the
order of derivative starts at 0 for y, 1 means y',
2 means y'', etc. The string that specifies the comparison
type is a two letter, upper case identifier similar to the Fortran
test condition:
-
LT, less than
-
LTE, less than or equal
-
EQ, equal
-
GT, greater than
-
GTE, greater than or equal
Systems of Ordinary Differential Equations
Set up a System of Equations
Systems of ordinary differential equations are integrated by setting
up each equation as an individual model, as above. Then
some extra functions are called to "glue" the separate models into
one system.
You initialize the system of equations by providing a list of the
model handles that will become part of the system. This is done with
the init_ode_system call, which returns a handle to the new
system of equations. An example with two equations would look like
> h_system = init_ode_system(h_model1, h_model2);
|
With a system of equations there must be an extra configuration of
the callback function arguments for those functions that define the
first derivatives. This is already done for each model setup with
the call to set_function_ode, so it seems reduntant to have
to do it again when combining the equations into a system. The reason
for this is that by default for a single ODE the list of function
arguments in set_function_ode is assumed to define x,
then y, then y', y'', etc. That is, the argument
placeholders of $3, $4, and above are considered to
specify higher derivatives as arguments. But when defining a system
of equations some of these arguments must be state variables for
the cointegrated equations. For example, for a system of two
equations, with states y1 and y2, the argument list
for any one of the derivative functions could be x, y1,
y2.
The second argument list specification for each of the ODE models
is performed with the set_function_args_ode_system call. An
example of using this function for a system of two equations in
which the first model (equation) takes x, y1, and
y2 as parameters would be
> set_function_args_ode_system(h_system, h_model1, 1, 1, '$, $1_0, $2_0');
|
Notice that the last parameter to set_function_args_ode_system is
a string in single quotes containing $, $1_0, and $2_0.
The $ symbols are use in a slightly different format than in
set_function_ode. The state variables are numbered 1, 2, 3, ... for
y1, y2, y3, etc. The number following the underscore
specifies the order of the derivative. So $ means x,
$1_0 means y1, and $2_0 means y2.
The first 4 arguments to set_function_args_ode_system are
-
ODE system handle
-
ODE model handle
-
ODE branch handle (usually just "1")
-
The order of the derivative function (usually just "1")
For a system of ODE you do not call step_ode on individual
ODE models (individual equations). Rather, you must call
step_ode_system.
> step_ode_system(h_system, 'repeat', N);
|
After the integration is complete you may retreive the data from
the ODE models individually, as if they were single ODE models.
You can also get the data in the form of a system output by calling
get_ode_system_data. For example,
> outstates = get_ode_system_data(h_system);
|
In the above example the variable outstates will be a struct
with the same format as an output struct for a single ODE model. It
will have a rows member, a columns member, and a
data member. The first row of the data member will be
the x data; the next row will be the y1 data, the next
the y2 data, and so on.
Starting a System of Equations with Another
A multistep, predictor/corrector model that is greater than second-order
needs to be started with another lower-order predictor/corrector model
or a Runga-Kutta model.
It is a simple matter to join the two models so that the main
model starts where the starter model stops. The two models must
share one "step table." Each model has a step table, which
is an internal matrix of past ODE step calculations and their
derivative calculations. The step table has as many columns
as needed to store the past values. It will have as many rows
as are needed to save the highest order of derivative needed
by the model (these are specified in the init_ode call).
The starter model needs a smaller step table than the main model.
So the main model keeps its step table, and the starter model
will be set to use the main model's step table.
First perform the setup of both models, but do not call the
step_ode integrator yet. Then call share_step_table_ode.
> share_step_table_ode(h_model1, 0, h_model2, 0);
|
The first argument, h_model1, in the above code example is the
handle to the starter model. The third argument, h_model2, is
the handle to the main model. Arguments two and four should
be zero (These are actually meant to be handles for specific
ODE branches within each model, but that usage would entail a
somewhat complicated integrator).
When integrating a system of equations we must call
share_step_table_ode for each model (equation) in the
system. For example, if the system is a set of three equations
we must call share_step_table_ode three times. Model 1
in system 1 will be matched with model 1 in system2; model 2 in
system 1 will be matched with model 2 in system 2, etc.
But a system of equations, as opposed to a single model, will
require an additional function call for gluing the starter system
with the main system. We must call share_states_table_ode.
> share_states_table_ode(h_system1, h_system2);
|
In the above function call the first argument specifies the starter
system, and the second argument specifies the main system.
Finite Elements (FE)
Introduction
The finite element method is an engineering method for solving two
kinds of equations
where the first equation represents static forces on a system,
and the second equation represents vibrational modes. In a nutshell,
the first equation is the starting point for defining static problems,
and the second for defining dynamic problems. The mathematics for
diverse areas of engineering, such as fluid mechanics, can be
shoe-horned into forms that resemble the above equations.
The solution of these partial differential equations is a quadrature
(definite integral) as opposed to an iteration (indefinite integral)
as is performed on ODE problems. This is possible because the finite
element method is always applied to boundary-value problems as opposed
to initial-value problems. You approximate the funtion phi over
a small region as an interpolation function in two or three dimensions.
Rather than working with polynomial coefficients the interpolation is
transformed into node values and influence functions.
Then apply the Laplacian operation (two differentiations), and perform
the quadrature.
The result is a numerical solution over a tiny region, i.e., an
element. For a single element the node values and the
boundary values are equivalent. In other words, nothing is solved.
The usefulness of the method is achieved by combining many elements
into a set of equations. The outer nodes are known boundary values,
and the inner nodes are simultaneous solutions of the problem.
This simultaneous solution results in a stiffness matrix,
S, and the forces end up in a vector, f.
The simultaneous solution of the second equation results in an
inertia matrix, M,
and the second equation solves for the mode frequencies and the
mode shapes of a vibrational problem.
FE Model Components
The finite element model is constructed from a mesh file
and an HMD script file.
The mesh file contains the partition
of some geometric region into elements that are defined by vertices
and their locations. Nodes are assigned to the vertices and are shared
by neighboring elements. The mesh file is created with the GMSH
program, which is freely available from the GMSH web site.
Documentation on how to use GMSH is found there.
The HMD script reads the mesh file and creates internal data structures
for the nodes and elements. During the creation of the mesh file with
GMSH every element is designated as belonging to a physical
region and given a region number. In the HMD script the region
numbers are assigned material coefficients, boundary values, and forces
(that is, applied influences). The HMD program assembles the matrix
equations from the nodes and elements and solves the matrix equations.
An output file is generated for each finite element model. Although
the output data can be viewed and manipulated with any program you
like, it is expedient to use the GMSH program for working with the
output. For that you need the HMD data file and the GMSH mesh file.
There are HMD functions that can produce data files in the GMSH
post processing format. You can then use the GMSH
display to examine your model.
The Simple FE Function
HMD provides a simple, one-function call approach to the static
finite element equation defined above.
> output = finelm(meshfile, keyword1, region_number1, region_value1,
|
keyword2, region_number2, region_value2,
|
. .
|
. .
|
keywordN, region_numberN, region_valueN);
|
where
output = a struct with (at least) three members: rows, columns,
data. The data member will be a block of double-precision
numbers giving the nodal values.
keyword = a two-letter text string: 'bc', 'gr', 'ms', 'fc'.
-
'bc' means boundary condition value
-
'gr' means the stiffness value
-
'ms' means the inertia or mass value
-
'fc' means the value of the forcing term.
region_number = an integer identifier of the region to be given
a value. Regions are created during the creation
of the mesh file.
region_value = a double-precision number to be assigned to the
region.
Example,
Given that you have created a mesh file called triGreen.msh, call
> triGreen = finelm('triGreen.msh',
|
'bc', 1, 0.0,
|
'gr', 2, 1,
|
'gr', 3, 1,
|
'fc', 2, 1);
|
> print 'rows = ', triGreen.rows;
|
> print 'columns = ', triGreen.columns;
|
> dump_block(triGreen.data, 1, 'triGreen.dat');
|
where we used the dump_block function to write the output
data to the file triGreen.dat.
The output from an HMD finite element model can be displayed using GMSH.
An AWK script provided in the HMD share directory
(/usr/local/share/hmd/scripts) called gmsh2post.awk which can
create a GMSH post-processing file. For a model based on the following
mesh
The output displayed with GMSH looks like
Interface with GMSH
Documentation and tutorials for using GMSH are available at the
GMSH web site. The user must first create a geometry file, building
points, lines, surfaces, and volumes. It has a graphical user
interface for building the geometry visually. Or the geometry file
can be created from a text file. The following text fiel was used
to create the geometry for the above model.
// -----------------------------------------------------------------------
|
// Title: tri_green.geo
|
//
|
// GMSH geometry example, 1/10/2014, A. Steffens Jr.
|
//
|
// GMSH is software written by Christophe Geuzaine and Jean-François Remacle,
|
// available at www.geuz.org
|
// -----------------------------------------------------------------------
|
//
|
// -----------------------------------------------------------------------
|
// Description
|
//
|
// The potenial in a triangle with a rectangular charge distribution.
|
//
|
// Usage:
|
// gmsh -2 -format msh1 tri_green.geo
|
// -----------------------------------------------------------------------
|
//
|
|
// Vertices of the triangle
|
Xa = 1.0;
|
Ya = 1.0;
|
Xb = 10.0;
|
Yb = 2.0;
|
Xc = 7.0;
|
Yc = 9.0;
|
|
// Corners of the charge rectangle
|
xp1 = 4;
|
yp1 = 3;
|
xp2 = 8;
|
yp2 = 3;
|
xp3 = 8;
|
yp3 = 4;
|
xp4 = 4;
|
yp4 = 4;
|
|
scale = 0.2;
|
|
// Points for the triangle
|
Point(1) = {Xa, Ya, 0.0, scale};
|
Point(2) = {Xb, Yb, 0.0, scale};
|
Point(3) = {Xc, Yc, 0.0, scale};
|
|
// Points for the rectangle
|
Point(4) = {xp1, yp1, 0.0, scale};
|
Point(5) = {xp2, yp2, 0.0, scale};
|
Point(6) = {xp3, yp3, 0.0, scale};
|
Point(7) = {xp4, yp4, 0.0, scale};
|
|
// Lines for the triangle
|
Line(1) = {1,2};
|
Line(2) = {2,3};
|
Line(3) = {3,1};
|
Line Loop(100001) = {1, 2, 3};
|
|
// Lines for the Rectangle
|
Line(4) = {4,5};
|
Line(5) = {5,6};
|
Line(6) = {6,7};
|
Line(7) = {7,4};
|
Line Loop(100002) = {-4, -5, -6, -7};
|
|
// Surface for the triangle
|
Plane Surface (10) = {100001, 100002};
|
|
// Surface for the rectangle
|
Plane Surface (11) = {100002};
|
|
// The boundary of the triangle (Region 1)
|
Physical Line (1) = {1, 2, 3};
|
|
// The charge region (Region 2)
|
Physical Surface(2) = {11};
|
|
// The remaining area of the triangle (Region 3)
|
Physical Surface(3) = {10};
|
If you use the graphical interface the result will still be a
text file as above. Note, however, the comment in the file
gmsh -2 -format msh1 tri_green.geo
The mesh must be created with format "msh1", that is, with the
legacy GMSH mesh format. The HMD software development began
when GMSH was still a relatively new program. All HMD mesh interfaces
use the legacy GMSH formats.
The HMD program can send native GMSH commands to the GMSH program over
a socket connection. The GMSH program must be started already in
interactive mode using the "listen" option:
When running HMD you must tell it what socket name to use when
establishing a connection to GMSH. This is done with the HMD -g option.
The default socket name used by GMSH will be .gmshsock located in the
home directory for your user account. So HMD would be involked as
The simplest way to send commands to GMSH is with the
> gmsh_command('string');
|
function call. If a socket connection is not already established an
attempt will be made to make the connection before sending the GMSH
command. Then the socket is left open for the remainder of the HMD
session.
If the socket connection to GMSH is not explicitly closed during the
HMD session the HMD program will attempt to close it when it exits.
The GMSH socket can be opened manually in the HMD script with the
function
> err = gmsh_open('/home/.gmshsock');
|
Or, when opening and closing the socket multiple times the socket
name argument may be omitted on subsequent open calls,
in which the last socket name used will be used as the socket name.
Or, it the first open call, and a socket name was given on the command
line, that socket name will be used.
Closing the socket connection is performed using the function call
Note that no argument is needed--no socket handle is needed. That is
because there can be only one socket connection to GMSH open at any
one time. For example, if you try to call gmsh_open with a different
socket name when a socket session is already open, the open call will
be ignored until the current session is closed.
Low-Level FE Commands
For creating more specialized finite element models than can be
performed with the simple FE function, there are low-level finite
element commands available. Documentation of these commands is a
work in progress. The following steps outline the creation of a
static finite element model:
-
Declare the mesh
-
Set the type of mesh
Notice that the name of the mesh is now treated like a struct
or class object.
-
Set the filename of the mesh to read
> smesh.filename = 'tri_green.msh';
|
-
Declare the model.
The first argument is the name of the model. The second argument
is the name of the mesh that was already declared.
-
Set the type of model
> tri.modeltype = FINITE_ELEMENT_E;
|
The model types are FINITE_ELEMENT and FINITE_ELEMENT_E. The
difference is that FINITE_ELEMENT is the old algorithm.
-
Set the equation type
> tri.equation = POISSON;
|
-
Read the mesh file and load into memory
-
Set up special model flags
> createsparsity 'tri', 0x00;
|
-
Set the boundary conditions. Use the "setregionbc" and
"setregionbcn" commands for any region that should be set.
Arguments are model name, region number, and boundary value.
> setregionbc 'tri', 1, 0.0;
|
which sets the boundary value of region 1 to 0.0. The difference
between commands is that "setregionbc" is for Dirichtlet and
"setregionbcn" is for Neumann BC's.
-
Set the stiffness coefficients in any region. Arguments are
model name, region number, and stiffness value.
> setregiongrad 'tri', 2, 1;
|
which sets the stiffness value in region 2 to 1.0.
-
Set the "forcing", or "driving" coefficients. For any region
required use the command "setregionforce". Arguments are model
name, region number, and forcing value.
> setregionforce 'tri', 2, 1;
|
which sets the forcing cefficient of 1 in region 2 of model named
'tri'.
-
Assemble the FE matrix
-
Assemble the vector (the "right-hand side")
> buildglobalvector 'tri';
|
-
Assign the matrix just assembled to be the stiffness matrix for
the model,
> setdivergence 'tri', 'tri-grad';
|
The second argument is the default name given to the matrix when
it was created by "buildglobalgrad".
-
Assign the vector just created to be the forcing vector for the
model,
> setsource 'tri', 'tri-force';
|
The second argument is the default name given to the vector when
it was created by "buildglobalvector".
-
Insert the boundary values into the matrix and the vector,
-
Solve the matrix equation--that is, solve for the scalar values
at each of the nodes,
> solve_div 'tri', 'tri_green.dat', 0x00, 'all';
|
which will write the output to the file 'tri_green.dat'.
-
Use the EXIT command to finish the script.
Integer Arithmetic
Introduction to Gnumbers
A "Gnumber" is a number type in HMD for performing integer-only, rational
number arithmetic of arbitrary-length precision. A Gnumber looks like
123 / 7
whereas a normal, standard HMD number is a floating-point number and looks
like
17.5714
A Gnumber can represent a whole number of arbitrary length. A machine number
will always be limited in length. For instance, a 32-bit integer can have at
most 10 digits. A Gnumber integer can have any number of digits up to the
limits of available computer memory. But to create an integer of more than
machine digit length you must enclose it in single quotes.
You have to create a Gnumber in a special way, that is, you cannot just enter
an integer or divide two whole numbers like you can in Lisp or Maxima. You
execute the gnumber function call which creates a variable containing a
Gnumber.
A calculation performed with Gnumbers will never be reduced to a floating-point
number, so in some sense the result of a calculation remains "exact."
Divisions result in a rational number, composed of integers, reduced to their
lowest factors.
Computation with Gnumbers is many times slower than with ordinary computer
types such as double-precision floating point numbers. Gnumbers are software
objects and fundamental arithmetic is performed in software, whereas a
floating-point number is a machine object, and computations are performed by
the processor. Therefore, Gnumbers are not of much interest for numerical
modeling. Gnumbers would of interest in number theory, or for testing the
limits of truncation error for a certain type of calculation, or other special
purposes.
Another feature of a Gnumber is that it can be defined with any desired radix
up to a maximum value. But computatiuons between any two Gnumbers must be
performed with the same radix. Internally, a Gnumber is a list of numerical
digits, where each digit is a natural number between 0 and radix-1. For
example, in base 10 a digit can be between 0 and 9. Each higher-order digit
represents another higher power in the radix. A radix can be chosen as any
natural number between 2 and 32768, with the radix 0 having a special
significance. When a Gnumber is printed any digit with value greater than 9
will be represented by more than one decimal digit and separated from others by
vertical bars.
Defining a Gnumber
A Gnumber is created with a function call (like a constructor) and lives in the
form of a software variable only. There is no literal form of a Gnumber. To
create the number 2 as a base 10 variable you would type
or to create the number 2 in base 7 you would type
Although it will be printed in human-readable form, there is no literal
representation for creating a Gnumber. In some other computer languages, say
Lisp or Maxima, you could just enter numbers without decimal points, like 10
and 7, and get the rational number 10/7. Such computer langauges decide
whether the number should be integer or floating point by whether it finds a
fractional part after the decimal point. In HMD all literal numbers are
basically floating-point double-precision numbers whether or not there is a
decimal point. To get a pure rational number you must define it with the
gnumber() function call. So, the number 3/2 in base 10 is created with
After the radix, each position in the gnumber() function call places its
integer into a specific location of the resulting Gnumber:
(1) | | | (3) |
----- | + | i | ----- |
(2) | | | (4) |
So, creating a Gnumber with
g1 = gnumber(10, 3, 2, 5, 4);
|
results in
There may be a total of five arguments to the gnumber function call:
gnumber( radix,
numerator,
denominator,
imaginary numerator,
imaginary denominator );
The first two arguments are required, the last three are optional.
Rationalization
The act of rationalizing a floating-point number mean converting it to a
ratio of whole numbers. For example,
Complex Integer Numbers
A Gnumber can have a "complex" part. Of course, this is a stretch of the
definition of complex numbers. True complex numbers are a generalization of
the set of real numbers, and Gnumbers are only rational numbers. But an
approximation of complex numbers using rational numbers only can be created
by entering numbers into the last places of the gnumber function:
gcm = gnumber(0, 1, 2, 7, 5);
|
print gcm;
|
Also, a floating-point version of a complex number can be entered into the
gnumber function to create a complex Gnumber:
c1 = <0.21 %i 1.414>
|
print c1;
|
gc1 = gnumber(0, c1);
|
print gc1;
|
Arithmetic with Gnumbers
There are 5 basic operations defined for Gnumber symbols: addition,
subtraction, multiplication, division and exponentiation. These are performed
with the usual arithmetic operators +, -, *, /, and ^. For example,
g1 = gnumber(0, 2, 3); // 2/3
|
g2 = gnumber(0, 1, 8); // 1/8
|
g3 = g1 + g2;
|
print g3;
|
A Gnumber may be raised to an integer power (possibly negative) by another
Gnumber. For example,
g1 = gnumber(0, 2, 7); | // 2 / 7 |
g2 = gnumber(0, 3); | // 3 |
g3 = g1 ^ g2; | // (2/7) ^ 3 |
print g3;
|
g2 = gnumber(0, -3);
|
g3 = g1 ^ g2; // (2/7) ^ -3
|
print g3;
|
The exponent operator will raise an error if you try to raise a Gnumber to a
fractional power. The reason for this is that a non-integer exponent on a
number is a hybrid concept that partakes of both mulltiplication and root
finding. You can raise x to the p/q power by first raising x to the p power,
then taking the qth root of the result, or in the opposite order. The result
in either case is usually the same, but the decision is yours, the
mathematician. The nth root of a Gnumber can be calculated with the
nth_root_number function call,
nth_root_number(number, nth, iterations);
|
which uses a recursive Newton-Raphson algorithm.
Change of Radix
The first argument to the gnumber creation function is the desired radix, or
base of representation. If you have made computations that result in one
Gnumber of a given radix, you might want to see the same number converted to
a different radix. This can easily be done by generating another Gnumber using
the first as the input argument. Beginning with one Gnumber, say, g1, it would
be converted to g2 with
g2 = gnumber(new_radix, g1);
|
The use of an arbitrary radix greater than 9, while of academic interest only,
is included merely because it was an unexpected outcome of the Gnumber
algorithm design. Gnumber arithmetic operations work digit by digit, like you
would work it out by hand, independent of the value of the radix. The only
consideration that makes a difference is how to represent it when it is printed
to the screen. In a radix greater than 9 some "digits" require 2 or more
placeholders (hexidecimal gets around this by extending the symbol set with A
THROUGH F). In order to insure that the digits are properly separated HMD will
print multi-symbol "digits" surrounded by vertical bars (the "|" symbol). For
example, the number 37812 in base 60 would be created and printed as shown
below,
A special radix value of 0 signifies the maximum radix, 32768. Any Gnumber
having this maximum radix will automatically be converted to a base 10 number
before being printed.
Using a Gnumber where each "digit" represents a higher power of 32768 is a more
efficient way to carry out calculations than those using, say, base 10. The
process of doing arithmetic by stepping, recursively, through each of the
digits of two numbers requires software overhead hundreds or thousands of time
greater than would be needed by the computer processor. A huge radix like
32768 allows many, if not most, numbers of interest to be represented by only a
few "digits" of the Gnumber. A computation that operates on only one or two
digits will be much faster than those operating on many. This is basically the
internal design for how HMD accomplishes its arbitrary-precision integer
arithmetic.
Limitations
Entry of numbers into the gnumber() function are limited in size to that
allowed by the IEEE double-precision number size. In order to get a huge
number at gnumber creation time you will have to enclose the number in
single quotes. For example, entering 142857142857 will result in a malformed
number (which will probably show up negative). But entering '142857142857'
will result in a valid Gnumber. Any number enclosed in quotes is not allowed
to have a fractional part or an imaginary part.