Custom Maths Parser
Sometimes the complexity in changing the input.deck file is due to the fact that a function which must be used is fairly complex in form and is not supplied with the core code. It must therefore be represented in the input deck maths parser. This can be a significant cause of complexity for some problems, and in this case, there are three options:
- Put up with it and implement in the deck
- Use the internal initial conditions rather than the deck
- Extend the maths parser to include your function
Extending the maths parser can either
be permanent (described in extensions) or temporary (described
here). All of the routines used in extending the maths parser are in the file
user_interaction/custom_parser.f90
. Temporarily adding elements to the parser
is much easier than a permanent addition. It is
possible to add new constants and functions to the maths parser. It is hoped
that in a future release of EPOCH this will be extended to allow custom
operators as well.
As an example, lets look at adding a new function (lorentz) for a Lorentzian distribution, and adding a new constant, phi.
Registering your new constant/function
Before a new constant or function
can be defined it must be registered. In the registration phase the text
representation of the function or constant is given to the parser subroutines
and the user is returned an integer handle for the registered object. The
numerical handle must be stored so that that all of the functions in this
module can access it, so they should be placed after the IMPLICIT NONE
statement at the top of the file and defined as:
INTEGER :: c_func_lorentz
INTEGER :: c_const_phi
Note that the names given to the constants is obviously at the developers
discretion, but these names comply with the EPOCH style guide.
Actually registering the objects is done in the register_objects
subroutine which should include lines to register functions and constants.
An example is given below.
SUBROUTINE register_objects
c_func_lorentz = register_function("lorentz")
c_const_phi = register_constant("phi")
END SUBROUTINE register_objects
Note that the input deck parser is case sensitive, so the strings which are
given to register_function
and register_constant
should be in the case that they will appear in the input deck. To follow the
EPOCH style guide this should be all lowercase. At this point, the maths
parser would start to recognise the new function/constant, but would still
give error messages since they haven’t yet been implemented.
Setting up new constants
Once a new constant has been registered it must be described using the
custom_constant
function. In 2D this function looks like:
FUNCTION custom_constant(opcode, ix, iy, errcode)
INTEGER, INTENT(IN) :: opcode, ix, iy
INTEGER, INTENT(INOUT) :: errcode
REAL(num) :: custom_constant
! Leave these lines in place. They cause the code to throw an error if
! The opcode is unknown
custom_constant = 0.0_num
errcode = IOR(errcode, c_err_unknown_element)
END FUNCTION custom_constant
The parameters are
opcode
- The operator code of the constant requested. This will be the integer handle returned fromregister_constant
.ix
,iy
,iz
- Some constants are actually evaluated at specific points in space and ix, iy, iz are the gridpoint number of the location currently being evaluated. If you are specifying a simple constant then just ignore these. If your constant does depend upon space then directly subscript your array with ix, iy, iz as needed to read the correct location.errcode
- The error code which should be passed back to the parser. If for some reason you cannot evaluate your constant then you shouldIOR
errcode with the appropriate error code (all the error codes are listed in appendix A). Note that errcode should never be SET to any specific error code when extending the parser, since this might overwrite errors put in place earlier in the parsing sequence. This is different to extending the input deck where the error code is set.
The function should just return the evaluated value of the constant requested
by opcode
. This might look like:
FUNCTION custom_constant(opcode, ix, iy, errcode)
INTEGER, INTENT(IN) :: opcode, ix, iy
INTEGER, INTENT(INOUT) :: errcode
REAL(num) :: custom_constant
IF (opcode .EQ. c_const_phi) THEN
custom_constant = pi
RETURN
ENDIF
! Leave these lines in place. They cause the code to throw an error if
! The opcode is unknown
custom_constant = 0.0_num
errcode = IOR(errcode, c_err_unknown_element)
END FUNCTION custom_constant
Note that when opcode
is successfully recognised, the code sets
the return value and returns straight away. This is how all constants should
work, since the last line forces the function to return an error code. This
last line is in place to trap people registering constants but never defining
them. Without this line, it is possible to define a constant which is
never specified and have the code complete OK with a random value for that
constant.
The constant “phi” should now work fine when used anywhere in the input deck and will return a value of $\pi$.
Setting up new functions
Setting up the new function lorentz
is very similar to setting up
the new constant. The relevant function is custom_function
and
when empty looks like:
FUNCTION custom_function(opcode, ix, iy, errcode)
INTEGER, INTENT(IN) :: opcode, ix, iy
INTEGER, INTENT(INOUT) :: errcode
REAL(num) :: custom_function
REAL(num) :: values(5)
! Leave these lines in place. They cause the code to throw an error if
! The opcode is unknown
custom_function = 0.0_num
errcode = IOR(errcode, c_err_unknown_element)
END FUNCTION custom_function
The parameters are
opcode
- The operator code of the constant requested. This will be the integer handle returned from \inlinecode {register_function}.ix
,iy
,iz
- Some functions are evaluated differently at specific points in space and ix, iy, iz are the gridpoint number of the location currently being evaluated. If you are specifying a simple function then just ignore these. If your function does depend upon space then directly subscript your array with ix, iy, iz as needed to read the correct location.errcode
- The error code which should be passed back to the parser. If for some reason you cannot evaluate your function then you shouldIOR
errcode with the appropriate error code. Note that errcode should never be SET to any specific error code, since this might overwrite errors put in place earlier in the parsing sequence.
The function should return the value of your evaluated constant. The
parameters which are passed to the function can be retrieved by the function
get_values(n, values)
, where n
is the number of
parameters to be returned and values
is a REAL(num)
array of length n
which will hold the returned values . In this
implementation of the Lorentzian function there are three parameters: The
dependent variable, the location parameter and the scale parameter. The code
to implement the function therefore looks like:
FUNCTION custom_function(opcode, ix, iy, errcode)
INTEGER, INTENT(IN) :: opcode, ix, iy
INTEGER, INTENT(INOUT) :: errcode
REAL(num) :: custom_function
REAL(num) :: values(5)
IF (opcode .EQ. c_func_lorentz) THEN
CALL get_values(3, values(1:3))
! values(1) - Dependent variable
! values(2) - location parameter
! values(3) - scale parameter
custom_function = values(3)**2/((values(1)-values(2))**2+values(3)**2)
RETURN
ENDIF
! Leave these lines in place. They cause the code to throw an error if
! The opcode is unknown
custom_function = 0.0_num
errcode = IOR(errcode, c_err_unknown_element)
END FUNCTION custom_function
This function is then available at any point in the input deck and if I return to the previous example ic.deck file, it would be used as follows:
begin:constant
particle_density = 100.0 # Particle number density
profile_x = lorentz(x,0.0,1.0)
profile_y = lorentz(y,0.0,1.0)
end:constant
begin:species
name = s1
# multiply density by real particle density
density = particle_density * profile_x * profile_y
# Set the temperature to be zero
temp_x = 0.0
temp_y = temp_x(s1)
# Set the minimum density for this species
density_min = 0.3*particle_density
end:species
begin:species
name = s2
# Just copy the density for species s1
density = density(s1)
# Just copy the temperature from species s1
temp_x = temp_x(s1)
temp_y = temp_y(s1)
# Set the minimum density for this species
density_min = 0.3*particle_density
end:species
It is therefore clear that the new lorentz function is essentially the same as
the built in gauss function. Note that due to the way that the parser works,
the end user is not required to deal with parameters which are themselves
maths expressions. They have been fully evaluated by the time they are
returned by get_values
. Note that the parser is not guaranteed to
be bulletproof. If a user calls get_values
requesting more
parameters than have been passed to the function then it will scramble the
stack which is used by the parser and cause the code to fail. Note that
calling get_values(2, values)
is not the same as calling
get_values(1, values)
twice, in fact calling
get_values(1, values)
multiple times will return the parameters in
reverse order. This is normal and is a feature of how the maths parser
operates. It is possible to use this property to write functions which have a
variable number of parameters, but this is not recommended.