Maths Parser
When an extension is intended for a new release, the temporary custom extensions are not appropriate. In these cases, it is possible to permanently add functions and constants to EPOCH’s maths parser. Although adding new operators is possible, it is sufficiently likely to cause problems with the operation of the maths parser that it is formally not recommended by the author of the program, and hence is not documented here.
Adding the new tokenizer handle
When adding a new function or constant to the maths parser using the temporary
routines, there are two calls (register_function
and
register_constant
) which give a numerical handle. This is the
token used to represent that function or constant after the text has been parsed
(remember that EPOCH’s maths parser tokenizes before evaluation!). When
permanently adding objects to the maths parser, the tokenizer handles have to
be set up manually. This takes place in src/shared_data.F90
in
the module shared_parser_data
. There are several lines which
look like:
INTEGER, PARAMETER :: c_const_ix = 40
INTEGER, PARAMETER :: c_const_iy = 41
.
.
.
INTEGER, PARAMETER :: c_func_interpolate = 22
INTEGER, PARAMETER :: c_func_tanh = 23
Constants beginning with c_const_
are tokenizer handles for
constants, and those beginning with c_func_
are tokenizer handles
for functions. Each number must be unique and has to be less than
the lower bound of values reserved for temporary or deck
specified values. This means that any tokenizer handle for a function has to be
less than the value of the variable c_func_custom_lowbound
and
any handle for a constant must be less than
c_const_deck_lowbound
. It is acceptable to simply increase the
value of c_func_custom_lowbound
and
c_const_deck_lowbound
to
allow the use of more values for internal constants and functions, although
care should be taken since this could lead to performance issues.
If c_const_deck_lowbound
is increased then the constant
c_constant_custom_lowbound
should be increased by the same
amount (the values between c_const_deck_lowbound
and
c_constant_custom_lowbound-1
are used for constants specified
inside the input deck while values greater than or equal to
c_constant_custom_lowbound
are used for constants specified
by register_constant
.
Once the tokenizer handle is specified in shared_parser_data
, it
is now possible to extend the main areas of the maths parser. Note that from
here on, you MUST always use the constant named handle, NEVER the numerical
value that you specified for the value of the handle. If this is not done
then combining functions and constants from several sources becomes much harder.
Adding the new function or constant to the tokenizer
The next stage is to add the string representation of your constant or function
to the tokenizer routines in
src/parser/tokenizer_blocks.f90
. This is very simple to do, just
find either the function as_constant
or as_function
and look at the existing code. These functions are just long lists of
str_cmp
commands followed by code to deal with custom
functions/constants. To add the new code, create an additional line such as:
IF (str_cmp(name, "my_const")) as_constant = c_const_my_const
.
.
.
IF (str_cmp(name, "my_func")) as_function = c_func_my_func
Note that neither routine returns immediately after recognising the name of the
function/constant. This allows users to override built in constants or
functions with custom versions using register_constant}
and register_function
. This is not significant, since tokenizing
should never be used in a speed critical part of the code.
Implementing the function or constant in the evaluator
The evaluator is the part of the code that actually takes the streams of tokens
produced by the tokenizer and evaluates them into a number. The relevant parts
of the evaluator for adding new constants or functions are in
src/parser/evaluator_blocks.f90
and the functions which may need
changing are do_constant
and do_functions
. These are
both passed up to five parameters:
INTEGER :: opcode
- The operation code, this is the tokenizer handle which was defined inshared_parser_data
.INTEGER :: ix, iy, iz
- The position of the current evaluation in the domain. If your function or constant behaves differently at different points in space then you should use these parameters to reference the correct point of an array.INTEGER :: errcode
- This should be set to an error code, usuallyc_err_bad_value
if for some reason it is not possible to evaluate your constant or function.
The rest of the routine to set a constant is as simple as testing for the
tokenizer handle already set up in shared_parser_data
and then
calling the subroutine push_on_eval
. This pushes the final
constant onto the evaluation stack which is used by the RPN parser. The basic
sequence for functions is similar except for the addition of a code to read
the values that the function takes. This is again the subroutine
get_values
which is also used in custom_function. The calling
sequence in do_function
looks like:
IF (opcode .EQ. c_func_gauss) THEN
CALL get_values(3, values)
CALL push_on_eval(EXP(-((values(1)-values(2))/values(3))**2))
RETURN
ENDIF
Simply call the get_values
subroutine, passing the number of
required parameters and an array of type REAL(num)
which is at
least as long as the number of required parameters. The array is populated
by the values passed into the function. Constants and maths expressions are
already evaluated by the time that this section of code is reached, so there is
no need to deal with further parsing. Next, simply call
push_on_eval
to push the result of your function onto the
evaluation stack.