SymPy
PSyclone uses the symbolic maths package “SymPy” for comparing
expressions symbolically, e.g. a comparison like i+j > i+j-1
will be evaluated to be true.
The SymPy package is wrapped in the PSyclone class SymbolicMaths
:
- class psyclone.core.SymbolicMaths[source]
A wrapper around the symbolic maths package ‘sympy’. It provides convenience functions for PSyclone. It has a Singleton access, e.g.:
>>> from psyclone.psyir.backend.fortran import FortranWriter >>> from psyclone.core import SymbolicMaths >>> sympy = SymbolicMaths.get() >>> # Assume lhs is the PSyIR of 'i+j', and rhs is 'j+i' >>> if sympy.equal(lhs, rhs): ... writer = FortranWriter() ... print("'{0}' and '{1}' are equal." ... .format(writer(lhs), writer(rhs))) 'i + j' and 'j + i' are equal.
- static equal(exp1, exp2)[source]
Test if the two PSyIR expressions are symbolically equivalent.
- Parameters
exp1 (Optional[
psyclone.psyir.nodes.Node
]) – the first expression to be compared.exp2 (Optional[
psyclone.psyir.nodes.Node
]) – the first expression to be compared.
- Returns
whether the two expressions are mathematically identical.
- Return type
- static expand(expr)[source]
Expand a PSyIR expression. This is done by converting the PSyIR expression to a sympy expression, applying the expansion operation and then converting the resultant output back into PSyIR.
Currently does not work if the PSyIR expression contains Range nodes, see issue #1655.
- Parameters
expr (py:class:psyclone.psyir.nodes.Node) – the expression to be expanded.
- static get()[source]
Static function that creates (if necessary) and returns the singleton SymbolicMaths instance.
- Returns
the instance of the symbolic maths class.
- Return type
psyclone.core.SymbolicMaths.
- static never_equal(exp1, exp2)[source]
Returns if the given SymPy expressions are guaranteed to be different regardless of the values of symbolic variables. E.g. n-1 and n are always different, but 5 and n are not always different.
- Parameters
exp1 (py:class:psyclone.psyir.nodes.Node) – the first expression to be compared.
exp2 (py:class:psyclone.psyir.nodes.Node) – the first expression to be compared.
- Returns
whether or not the expressions are never equal.
- Return type
- static solve_equal_for(exp1, exp2, symbol)[source]
Returns all solutions of exp1==exp2, solved for the specified symbol. It restricts the solution domain to integer values. If there is an infinite number of solutions, it returns the string ‘independent’, indicating that the solution of exp1==exp2 does not depend on the specified symbol. This is done to avoid that the SymPy instance representing an infinite set is used elsewhere in PSyclone (i.e. creating a dependency in other modules to SymPy). Otherwise a standard Python set is returned that stores the solutions.
This can be used for tests of nodes in the PSyIR. For example, the NEMO loop fuse transformation checks that the loops to be fused have the same loop boundaries using code like this:
from psyclone.core import SymbolicMaths
from psyclone.psyir.backend.fortran import FortranWriter
# Assume loop1 is ``do i=1, k`` and loop2 ``do i=5+k-4-k, 2*k-k-1``.
writer = FortranWriter()
sym_maths = SymbolicMaths.get()
if sym_maths.equal(loop1.start_expr, loop2.start_expr):
print("'{0}' equals '{1}'".format(writer(loop1.start_expr),
writer(loop2.start_expr)))
if not sym_maths.equal(loop1.stop_expr, loop2.stop_expr):
print("'{0}' does not equal '{1}'".format(writer(loop1.stop_expr),
writer(loop2.stop_expr)))
'1' equals '5 + k - 4 - k'
'k' does not equal '2 * k - k - 1'
Handling of PSyIR Structures and Arrays
SymPy has no concept of structure references or array syntax like
a(i)%b
in Fortran. But this case is not handled especially, the
PSyIR is converted to Fortran syntax and is provided unmodified to SymPy.
SymPy interprets the %
symbol
as modulo function, so the expression above is read as Mod(a(i), b)
.
This interpretation achieves the expected outcome when comparing structures
and array references.
For example, a(i+2*j-1)%b(k-i)
and a(j*2-1+i)%b(-i+k)
will be
considered to be equal:
Converting the two expressions to SymPy internally results in
Mod(a(i+2*j-1), b(k-i))
andMod(a(j*2-1+i, b(-i+k))
.Since nothing is known about the arguments of any of the
Mod
functions, SymPy will first detect that the same function is called in both expression, and then continue to compare the arguments of this function.The first arguments are
a(i+2*j-1)
anda(j*2-1+i)
. The namea
is considered an unknown function. SymPy detects that both expressions appear to call the same function, and it will therefore compare the arguments.SymPy compares
i+2*j-1
andj*2-1+i
symbolically, and evaluate these expressions to be identical. Therefore, the two expressionsa(...)
are identical, so the first arguments of theMod
function are identical.Similarly, it will then continue to evaluate the second argument of the
Mod
function (b(...)
), and evaluate them to be identical.Since all arguments of the
Mod
function are identical, SymPy will report these two functions to be the same, which is the expected outcome.
Converting PSyIR to Sympy - SymPyWriter
The method equal
of the SymbolicMaths class expects two PSyIR
nodes. It converts these expression first into strings before parsing
them as SymPy expressions. The conversion is done with the SymPyWriter
class. As described in the previous section, a member of a structure
in Fortran becomes a stand alone symbol or function in sympy. The SymPy
writer will rename members to better indicate that they are members:
an expression like a%b%c
will be written as a%a_b%a_b_c
, which
SymPy then parses as MOD(a, MOD(a_b, a_b_c))
. This convention
makes it easier to identify what the various expressions in SymPy are.
This handling of member variables can result in name clashes. Consider
the expression a%b + a_b + b
. The structure access will be using
two symbols a
and a_b
- but now there are two different symbols
with the same name. Note that the renaming of the member from b
to
a_b
is not the reason for this - without renaming the same clash would
happen with the symbol b
.
The SymPy writer uses a symbol table to make sure it creates unique symbols.
It first adds all References in the expression to the symbol table, which
guarantees that no Reference to an existing symbol is renamed. The writer
then renames all members and makes sure it uses a unique name. In the case of
a%b + a_b + b
, it would create a%a_b_1 + a_b + b
, using the name
a_b_1
for the member to avoid the name clash with the reference
a_b
- so an existing Symbol Reference will not be renamed, only
members.
The SymPy writer mostly uses the Fortran writer, but implements the following, SymPy specific features:
It will declare any array access as a SymPy unknown function, and any scalar access as a SymPy symbol. These declarations are stored in a dictionary, which can be queried. This dictionary is parsed into the SymPy writer to ensure the correct interpretation of any names found in the expression. Declaring arrays as functions results in the correct behaviour of SymPy: in case of an unknown function SymPy will compare all arguments, which are the array indices.
It renames members as described above. So a structure reference like
a%b
(in Fortran syntax) will create two SymPy symbols:a
anda_b
(or a similar name if a name clash was detected).No precision or kind information is added to a constant (e.g. a Fortran value like
2_4
will be written just as2
).The intrinsic functions
Max
,Min
,Mod
are returned with a capitalised first letter. The Fortran writer would write them asMAX
etc., which SymPy does not recognise and would then handle as unknown functions.
- class psyclone.psyir.backend.sympy_writer.SymPyWriter(type_map=None)[source]
Implements a PSyIR-to-sympy writer, which is used to create a representation of the PSyIR tree that can be understood by SymPy. Most Fortran expressions work as expected without modification. This class implements special handling for constants (which can have a precision attached, e.g. 2_4) and some intrinsic functions (e.g. MAX, which SymPy expects to be Max). It additionally supports accesses to structure types. A full description can be found in the manual: https://psyclone-dev.readthedocs.io/en/latest/sympy.html#sympy
- Parameters
type_map (dict of str:Sympy-data-type values) – Optional initial mapping that contains the SymPy data type of each reference in the expressions. This is the result of the static function
psyclone.core.sympy_writer.create_type_map()
.
- static convert_to_sympy_expressions(list_of_expressions)[source]
This function takes a list of PSyIR expressions, and converts them all into Sympy expressions using the SymPy parser. It takes care of all Fortran specific conversion required (e.g. constants with kind specification, …), including the renaming of member accesses, as described in https://psyclone-dev.readthedocs.io/en/latest/sympy.html#sympy
- Parameters
list_of_expressions (list of
psyclone.psyir.nodes.Node
) – the list of expressions which are to be converted into SymPy-parsable strings.- Returns
the converted PSyIR expressions.
- Return type
list of SymPy expressions
- static create_type_map(list_of_expressions)[source]
This function creates a dictionary mapping each Reference in any of the expressions to either a Sympy Function (if the reference is an array reference) or a Symbol (if the reference is not an array reference).
- Parameters
list_of_expressions (list of
psyclone.psyir.nodes.Node
) – the list of expressions from which all references are taken and added to the a symbol table to avoid renaming any symbols (so that only member names will be renamed).- Returns
the dictionary mapping each reference name to a Sympy data type (Function of Symbol).
- Return type
dictionary of string:Sympy-data-type values
- get_operator(operator)[source]
Determine the operator that is equivalent to the provided PSyIR operator. This implementation checks for certain functions that SymPy supports: Max, Min, Mod. These functions must be spelled with a capital first letter, otherwise SymPy will handle them as unknown functions. If none of these special operators are given, the base implementation is called (which will return the Fortran syntax).
- static get_sympy_expressions_and_symbol_map(list_of_expressions)[source]
This function takes a list of PSyIR expressions, and converts them all into Sympy expressions using the SymPy parser. It takes care of all Fortran specific conversion required (e.g. constants with kind specification, …), including the renaming of member accesses, as described in https://psyclone-dev.readthedocs.io/en/latest/sympy.html#sympy It also returns the symbol map, i.e. the mapping of Fortran symbol names to SymPy Symbols.
- Parameters
list_of_expressions (list of
psyclone.psyir.nodes.Node
) – the list of expressions which are to be converted into SymPy-parsable strings.- Returns
a 2-tuple consisting of the the converted PSyIR expressions, followed by a dictionary mapping the symbol names to SymPy Symbols.
- Return type
Tuple[List[
sympy.core.basic.Basic
], Dict[str,sympy.core.symbol.Symbol
]]- Raises
VisitorError – if an invalid SymPy expression is found.
- is_intrinsic(operator)[source]
Determine whether the supplied operator is an intrinsic function (i.e. needs to be used as f(a,b)) or not (i.e. used as a + b). This tests for known SymPy names of these functions (e.g. Max), and otherwise calls the function in the base class.
- Parameters
operator (str) – the supplied operator.
- Returns
true if the supplied operator is an intrinsic and false otherwise.
- literal_node(node)[source]
This method is called when a Literal instance is found in the PSyIR tree. For SymPy we need to handle booleans (which are expected to be capitalised: True). Real values work by just ignoring any precision information (e.g. 2_4, 3.1_wp). Character constants are not supported and will raise an exception.
- member_node(node)[source]
In SymPy an access to a member ‘b’ of a structure ‘a’ (i.e. a%b in Fortran) is handled as the ‘MOD’ function MOD(a, b). We must therefore make sure that a member access is unique (e.g. b could already be a scalar variable). This is done by creating a new name, which replaces the % with an _. So a%b becomes MOD(a, a_b). This makes it easier to see where the function names come from. Additionally, we still need to avoid a name clash, e.g. there could already be a variable a_b. This is done by using a symbol table, which was prefilled with all references (a in the example above) in the constructor. We use the string containing the ‘%’ as a unique tag and get a new, unique symbol from the symbol table based on the new name using _. For example, the access to member b in a(i)%b would result in a new symbol with tag a%b and a name like a_b, a_b_1, …
- Parameters
node (
psyclone.psyir.nodes.Member
) – a Member PSyIR node.- Returns
the SymPy representation of this member access.
- Return type
Note
The SymPyWriter class provides the static function
convert_to_sympy_expressions
which hides the complexities of the
conversion from PSyIR expressions to SymPy expressions. It is
strongly recommended to only use this function when this functionality
is needed.