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(f"'{writer(lhs)}' and '{writer(rhs)}' are equal.") 'i + j' and 'j + i' are equal.
- class Fuzzy(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]
Enumeration used as a return value for situations where we need to support ‘true’, ‘false’ and ‘maybe’.
- static equal(exp1, exp2, identical_variables=None)[source]
Test if the two PSyIR expressions are symbolically equivalent. The optional identical_variables dictionary can contain information about variables which are known to be the same. For example, if identical_variables={‘i’: ‘j’}, then ‘i+1’ and ‘j+1’ will be considered equal.
- Parameters:
- 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 (
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:
- static greater_than(exp1, exp2, all_variables_positive=None)[source]
Determines whether exp1 is, or might be, numerically greater than exp2.
- Parameters:
exp1 (
psyclone.psyir.nodes.Node
) – the first expression for the comparison.exp1 – the second expression for the comparison.
all_variables_positive (Optional[bool]) – whether or not to assume that all variables appearing in either expression are positive definite. Default is not to make this assumption.
- Returns:
whether exp1 is, or might be, numerically greater than exp2.
- Return type:
psyclone.core.symbolic_maths.Fuzzy
- 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 (
psyclone.psyir.nodes.Node
) – the first expression to be compared.exp2 (
psyclone.psyir.nodes.Node
) – the second 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(f"'{writer(loop1.start_expr)}' equals "
f"'{writer(loop2.start_expr)}'")
if not sym_maths.equal(loop1.stop_expr, loop2.stop_expr):
print(f"'{writer(loop1.stop_expr)}' does not equal "
f"'{ writer(loop2.stop_expr)}'")
'1' equals '5 + k - 4 - k'
'k' does not equal '2 * k - k - 1'
SymPyWriter - Converting PSyIR to SymPy
The methods of the SymbolicMaths class expect to be passed PSyIR nodes.
They convert these expressions first into strings before parsing
them as SymPy expressions. The conversion is done with the SymPyWriter
class, and it is the task of the SymPyWriter
to convert the PSyIR
into a form that can be understood by SymPy. Several Fortran constructs
need to be converted in order to work with SymPy. The SymPy writer mostly
uses the Fortran writer for creating a string for the PSyIR, but implements
the following features to allow the parsing of the expressions by SymPy:
Array Accesses
Any array access is declared as a SymPy unknown function and any scalar
access as a SymPy symbol. These declarations are stored in a
dictionary, which is used by the parser of SymPy to ensure the correct
interpretation of any names found in the expression. Note that while
SymPy has the concept of Indexed
expressions, they do not work well
when solving equations that requires the comparison of indices (which is
frequently needed for the dependency analysis). For example,
M[x] - M[1] == 0
does not result in the solution x=1
when M
is an indexed SymPy type. Using an unknown function on the other hand
handles this as expected.
Array Expressions
Each array index will be converted into three arguments for the corresponding
unknown SymPy function that represents the array access. For example, an array
expression a(i:j:k)
will become
a(i, j, k)
, and to then maintain the same number of arguments for
each use of an array/function, a(i)
will become a(i,i,1)
, and
b(i,j)
becomes b(i,i,1,j,j,1)
etc. Array expressions like a(:)
will be using specific names for the lower and upper bound, defaulting
to sympy_lower
and sympy_upper
. So the previous expression
becomes a(sympy_lower, sympy_upper, 1)
. Note that in case of a name
clash SymPyWriter will change the names of the boundaries to be unique.
Fortran-specific Syntax
No precision or kind information is added to a constant (e.g. a Fortran
value like 2_4
will be written just as 2
). The intrinsic functions
Max
, Min
, Mod
are capitalised to be recognised by SymPy which
is case sensitive.
User-defined Types
SymPy has no concept of user-defined types like a(i)%b
in Fortran.
A structure reference like this is converted to a single new symbol
(scalar) or function (if an array index is involved). The default name
will be the name of the reference and members concatenated using _
,
e.g. a%b%c
becomes a_b_c
, which will be declared as a new SymPy
symbol or function (if it is an array access). 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. In the case of
a%b + a_b + b
, it would create a_b_1 + a_b + b
, using the name
a_b_1
for the structure reference to avoid the name clash with the
reference a_b
.
Any array indices are converted into arguments of this new function. So an
expression like a(i)%b%c(j,k)
becomes a_b_c(i,i,1,j,j,1,k,k,1)
(see Array Expressions). The SymPyWriter
creates a custom SymPy
function, which keeps a list of which reference/member contained how many
indices. In the example this would be [1, 0, 2]
, indicating that the
first reference had one index, the second one none (i.e. it is not an
array access), and the last reference had two indices. This allows the
function to properly re-create the Fortran string.
Documentation for SymPyWriter Functions
The SymPyWriter provides the following functions:
- class psyclone.psyir.backend.sympy_writer.SymPyWriter(*expressions)[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 beMax
). Array accesses are converted into functions (while SymPy supports indexed expression, they cannot be used as expected when solving, SymPy does not solve component-wise -M[x]-M[1]
would not result inx=1
, while it does for SymPy unknown functions). Array expressions are supported by the writer: it will convert any array expression likea(i:j:k)
by using three arguments:a(i, j, k)
. Then simple array accesses likeb(i,j)
are converted tob(i,i,1,j,j,1)
. Similarly, ifa
is known to be an array, then the writer will usea(sympy_lower,sympy_upper,1)
. This makes sure all SymPy unknown functions that represent an array use the same number of arguments.The simple use case of converting a (list of) PSyIR expressions to SymPy expressions is as follows:
symp_expr_list = SymPyWriter(exp1, exp2, ...)
If additional functionality is required (access to the type map or to convert a potentially modified SymPy expression back to PSyIR), an instance of SymPy writer must be created:
writer = SymPyWriter() symp_expr_list = writer([exp1, exp2, ...])
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
- static __new__(cls, *expressions)[source]
This function allows the SymPy writer to be used in two different ways: if only the SymPy expression of the PSyIR expressions are required, it can be called as:
sympy_expressions = SymPyWriter(exp1, exp2, ...)
But if additional information is needed (e.g. the SymPy type map, or to convert a SymPy expression back to PSyIR), an instance of the SymPyWriter must be kept, e.g.:
writer = SymPyWriter() sympy_expressions = writer([exp1, exp2, ...]) writer.type_map
- Parameters:
expressions (Tuple[
psyclone.psyir.nodes.Node
]) – a (potentially empty) tuple of PSyIR nodes to be converted to SymPy expressions.- Returns:
either an instance of SymPyWriter, if no parameter is specified, or a list of SymPy expressions.
- Return type:
Union[
psyclone.psyir.backend.SymPyWriter
, List[sympy.core.basic.Basic
]]
- arrayofstructuresreference_node(node)[source]
This handles ArrayOfStructureReferences (and also simple StructureReferences). An access like
a(i)%b(j)
is converted to the stringa_b(i,i,1,j,j,1)
(also handling name clashes in case that the user code already contains a symbola_b
). The SymPy function created for this new symbol will store the original signature and the number of indices for each member (so in the example above that would beSignature("a%b")
and(1,1)
. This information is sufficient to convert the SymPy symbol back to the correct Fortran representation- Parameters:
node (
psyclone.psyir.nodes.StructureReference
) – a StructureReference PSyIR node.- Returns:
the code as string.
- Return type:
- arrayreference_node(node)[source]
The implementation of the method handling a ArrayOfStructureReference is generic enough to also handle non-structure arrays. So just use it.
- Parameters:
node (
psyclone.psyir.nodes.ArrayReference
) – a ArrayReference PSyIR node.- Returns:
the code as string.
- Return type:
- gen_indices(indices, var_name=None)[source]
Given a list of PSyIR nodes representing the dimensions of an array, return a list of strings representing those array dimensions. This is used both for array references and array declarations. Note that ‘indices’ can also be a shape in case of Fortran. The implementation here overwrites the one in the base class to convert each array index into three parameters to support array expressions.
- Parameters:
indices (List[
psyclone.psyir.symbols.Node
]) – list of PSyIR nodes.var_name (str) – name of the variable for which the dimensions are created. Not used in this implementation.
- Returns:
the Fortran representation of the dimensions.
- Return type:
List[str]
- Raises:
NotImplementedError – if the format of the dimension is not supported.
- intrinsiccall_node(node)[source]
This method is called when an IntrinsicCall instance is found in the PSyIR tree. The Sympy backend will use the exact sympy name for some math intrinsics (listed in _intrinsic_to_str) and will remove named arguments.
- Parameters:
node (
psyclone.psyir.nodes.IntrinsicCall
) – an IntrinsicCall PSyIR node.- Returns:
the SymPy representation for the Intrinsic.
- Return type:
- 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.
- range_node(node)[source]
This method is called when a Range instance is found in the PSyIR tree. This implementation converts a range into three parameters for the corresponding SymPy function.
- Parameters:
node (
psyclone.psyir.nodes.Range
) – a Range PSyIR node.- Returns:
the Fortran code as a string.
- Return type:
- reference_node(node)[source]
This method is called when a Reference instance is found in the PSyIR tree. It handles the case that this normal reference might be an array expression, which in the SymPy writer needs to have indices added explicitly: it basically converts the array expression
a
toa(sympy_lower, sympy_upper, 1)
.- Parameters:
node (
psyclone.psyir.nodes.Reference
) – a Reference PSyIR node.- Returns:
the text representation of this reference.
- Return type:
- structurereference_node(node)[source]
The implementation of the method handling a ArrayOfStructureReference is generic enough to also handle non-arrays. So just use it.
- Parameters:
node (
psyclone.psyir.nodes.StructureReference
) – a StructureReference PSyIR node.- Returns:
the code as string.
- Return type:
- property type_map
- Returns:
the mapping of names to SymPy symbols or functions.
- Return type:
Dict[str, Union[
sympy.core.symbol.Symbol
,sympy.core.function.Function
]]
SymPyReader - Converting SymPy to PSyIr
The SymPyReader
converts a SymPy expression back to PSyIR. Together
with the SymPyWriter it allows to take a PSyIR expression, manipulate
it with SymPy, and creating a new PSyIR that can be used.
The SymPyReader is closely connected to the SymPyWriter: as explained
in Array Expressions the SymPyWriter creates special symbols
to indicate the lower- and upper-bound of an array expression. In order
to convert these arguments back to the corresponding Fortran representation,
the SymPyReader needs to know the actual names (which might have been
changed from their default because of a name clash with a user variable).
The SymPyReader constructor therefore takes a SymPyWriter as argument,
and it is the responsibility of the user to make sure the provided
SymPyWriter instance is indeed the one used to create the SymPy expressions
in the first place.
An example of converting an expression expr from PSyIR to SymPy and back:
from sympy import expand
from psyclone.psyir.backend.fortran import FortranWriter
from psyclone.psyir.backend.sympy_writer import SymPyWriter
from psyclone.psyir.frontend.sympy_reader import SymPyReader
sympy_writer = SymPyWriter()
sympy_expr = sympy_writer(expr)
# Use SymPy to modify the expression ...
new_expr = expand(sympy_expr)
# Find the required symbol table in the original PSyIR
symbol_table = expr.scope.symbol_table
sympy_reader = SymPyReader(sympy_writer)
# Convert the new sympy expression to PSyIR
new_psyir_expr = sympy_reader.psyir_from_expression(new_expr, symbol_table)
writer = FortranWriter()
print(f"{writer(expr)} = {writer(new_psyir_expr)}")
(j + 1) * k = j * k + k
Documentation for SymPyReader Functions
The SymPyReader provides the following functions:
- class psyclone.psyir.frontend.sympy_reader.SymPyReader(sympy_writer)[source]
This class converts a SymPy expression, that was created by the SymPyWriter, back to PSyIR. It basically allows to use SymPy to modify PSyIR expressions:
The SymPyWriter converts the Fortran expression to SymPy
SymPy is used to manipulate these mathematical expressions
The SymPyReader is used to convert these SymPy expressions back into PSyIR (from which source code can be recreated)
Most SymPy expressions can be parsed as Fortran expression immediately, but the big exception to this are Fortran Arrays which must be able to support array expressions like
a(1:5:2)
. As outlined in the SymPyWriter, each array is represented as an UndefinedFunction in SymPy, and each each dimension of the Fortran array will provide three arguments to this function: the start, stop, and step value. So the expression above will be converted toa(1,5,2)
, anda(7)
will be represented asa(7:7:1)
. If the start- or stop-expression is not specified, e.g.a(:)
, the SymPy writer will use two special symbolssympy_lower
andsympy_upper
, e.g. the above expression will becomea(sympy_lower:sympy_upper:1)
. The SymPyWriter will change the name if required in case of a name clash (i.e. if the user declared a variable calledsympy_lower
etc).The task of the SymPy reader is to convert these expressions back to the original PSyIR representation. For example, the SymPy expressions
a(i,j,k)
will be written asa(i:j:k)
. This conversion is done by the functionprint_fortran_array
by combining each 3-tuple of arguments back to the corresponding Fortran representation.The SymPyWriter sets the
_sympystr
attribute of the SymPy UndefinedFunction it creates toprint_fortran_array
. The_sympystr
method is automatically called by SymPy when converting an expression into a string.In order to achieve this, the SymPyReader must know the names used for the lower- and upper-bounds. The constructor takes a SymPyWriter as argument in order to get the name of these bounds. It is important that the SymPyWriter provided here is the one that was used to create the SymPy expressions in the first place.
- Parameters:
sympy_writer (
psyclone.psyir.backend.SymPyWriter
) – the SymPyWriter that was used to create the SymPy expressions.
- print_fortran_array(printer)[source]
A custom print function to convert a modified Fortran array access back to standard Fortran. This function is set as
_sympystr_
method of the SymPy functions created in the SymPyWriter (see_create_type_map
method of the SymPyWriter), so it will be called by SymPy to convert this function to a string, with the function to convert being the first argument! This function converts the three values that each index is converted to back into the Fortran array notation. It uses the class variablesSymPyReader._lower_bound
andSymPyReader._upper_bound
as the names that were used when the SymPy expressions were created in order to convert array expressions correctly back.- Parameters:
function (
sympy.core.function.Function
) – this function is called from a SymPy Function class, therefore the first argument is a SymPy Function instance (and NOT a SymPyReader) instance.printer (
sympy.printing.str.StrPrinter
) – the SymPy writer base class.
- Returns:
the string representation of this array access.
- Return type:
- psyir_from_expression(sympy_expr, symbol_table)[source]
This function converts a SymPy expression back into PSyIR. It converts the SymPy expression into a string, which is then parsed by the FortranReader. It relies on the
print_fortran_array
function to convert the function arguments back to Fortran array expressions. This function will be called by SymPy when converting the functions that represent array indices into a string, seepsyclone.psyir.backend.SymPyWriter._create_type_map
, where this function is defined to be called for string conversion.- Parameters:
sympy_expr (
sympy.core.basic.Basic
) – the original SymPy expression.symbol_table (
psyclone.psyir.symbols.SymbolTable
) – the symbol table required for parsing, it should be the table from which the original SymPy expression was created from (i.e. contain all the required symbols in the SymPy expression).
- Returns:
the PSyIR representation of the SymPy expression.
- Return type:
psyclone.psyir.nodes.Node