Example #1
0
def test_basegen_start_parent_loop_omp_end_dbg(capsys):
    '''Check the debug option to the start_parent_loop method when we have
    an OpenMP end directive'''
    module = ModuleGen(name="testmodule")
    sub = SubroutineGen(module, name="testsubroutine")
    module.add(sub)
    dgen = DirectiveGen(sub, "omp", "end", "do", "")
    sub.add(dgen)
    loop = DoGen(sub, "it", "1", "10")
    sub.add(loop)
    call = CallGen(loop, "testcall")
    loop.add(call)
    call.start_parent_loop(debug=True)
    out, _ = capsys.readouterr()
    print out
    expected = ("Parent is a do loop so moving to the parent\n"
                "The type of the current node is now <class "
                "'fparser.one.block_statements.Do'>\n"
                "The type of parent is <class "
                "'fparser.one.block_statements.Subroutine'>\n"
                "Finding the loops position in its parent ...\n"
                "The loop's index is  1\n"
                "The type of the object at the index is <class "
                "'fparser.one.block_statements.Do'>\n"
                "If preceding node is a directive then move back one\n"
                "preceding node is a directive so find out what type ...\n")

    assert expected in out
Example #2
0
def test_basegen_start_parent_loop_no_loop_dbg():
    '''Check the debug option to the start_parent_loop method when we have
    no loop'''
    module = ModuleGen(name="testmodule")
    sub = SubroutineGen(module, name="testsubroutine")
    module.add(sub)
    dgen = DirectiveGen(sub, "omp", "end", "do", "")
    sub.add(dgen)
    call = CallGen(sub, name="testcall", args=["a", "b"])
    sub.add(call)
    with pytest.raises(RuntimeError) as err:
        call.start_parent_loop(debug=True)
    assert "This node has no enclosing Do loop" in str(err)
Example #3
0
    def gen_code(self, parent):
        # pylint: disable=arguments-differ
        '''Creates the profile start and end calls, surrounding the children
        of this node.

        :param parent: the parent of this node.
        :type parent: :py:class:`psyclone.psyGen.Node`

        '''
        if self._module_name is None or self._region_name is None:
            # Find the first kernel and use its name. In an untransformed
            # Schedule there should be only one kernel, but if Profile is
            # invoked after e.g. a loop merge more kernels might be there.
            region_name = "unknown-kernel"
            module_name = "unknown-module"
            for kernel in self.walk(Kern):
                region_name = kernel.name
                module_name = kernel.module_name
                break
            if self._region_name is None:
                self._region_name = Profiler.create_unique_region(region_name)
            if self._module_name is None:
                self._module_name = module_name

        # Note that adding a use statement makes sure it is only
        # added once, so we don't need to test this here!
        use = UseGen(parent,
                     self.fortran_module,
                     only=True,
                     funcnames=["ProfileData, ProfileStart, ProfileEnd"])
        parent.add(use)
        prof_var_decl = TypeDeclGen(parent,
                                    datatype="ProfileData",
                                    entity_decls=[self._var_name],
                                    save=True)
        parent.add(prof_var_decl)

        prof_start = CallGen(parent, "ProfileStart", [
            "\"{0}\"".format(self._module_name), "\"{0}\"".format(
                self._region_name), self._var_name
        ])
        parent.add(prof_start)

        for child in self.children:
            child.gen_code(parent)

        prof_end = CallGen(parent, "ProfileEnd", [self._var_name])
        parent.add(prof_end)
Example #4
0
def test_basegen_start_parent_loop_dbg(capsys):
    '''Check the debug option to the start_parent_loop method'''
    module = ModuleGen(name="testmodule")
    sub = SubroutineGen(module, name="testsubroutine")
    module.add(sub)
    loop = DoGen(sub, "it", "1", "10")
    sub.add(loop)
    call = CallGen(loop, "testcall")
    loop.add(call)
    call.start_parent_loop(debug=True)
    out, _ = capsys.readouterr()
    print out
    expected = ("Parent is a do loop so moving to the parent\n"
                "The type of the current node is now <class "
                "'fparser.one.block_statements.Do'>\n"
                "The type of parent is <class "
                "'fparser.one.block_statements.Subroutine'>\n"
                "Finding the loops position in its parent ...\n"
                "The loop's index is  0\n")
    assert expected in out
Example #5
0
def test_adduse_default_funcnames():
    ''' Test that the adduse module method works correctly when we do
    not specify a list of funcnames '''
    module = ModuleGen(name="testmodule")
    sub = SubroutineGen(module, name="testsubroutine")
    module.add(sub)
    call = CallGen(sub, name="testcall", args=["a", "b"])
    sub.add(call)
    from psyclone.f2pygen import adduse
    adduse("fred", call.root)
    gen = str(sub.root)
    expected = ("    SUBROUTINE testsubroutine()\n" "      USE fred\n")
    assert expected in gen
Example #6
0
    def _add_call(self, name, parent, arguments=None):
        '''This function adds a call to the specified (type-bound) method of
        self._var_name to the parent.

        :param str name: name of the method to call.
        :param parent: parent node into which to insert the calls.
        :type parent: :py:class:`psyclone.f2pygen.BaseGen`
        :param arguments: optional arguments for the method call.
        :type arguments: list of str or None
        '''
        call = CallGen(parent, "{0}%{1}".format(self._var_name, name),
                       arguments)
        parent.add(call)
Example #7
0
def test_adduse():
    ''' Test that the adduse module method works correctly when we use a
    call object as our starting point '''
    module = ModuleGen(name="testmodule")
    sub = SubroutineGen(module, name="testsubroutine")
    module.add(sub)
    call = CallGen(sub, name="testcall", args=["a", "b"])
    sub.add(call)
    from psyclone.f2pygen import adduse
    adduse("fred", call.root, only=True, funcnames=["astaire"])
    gen = str(sub.root)
    expected = ("    SUBROUTINE testsubroutine()\n"
                "      USE fred, ONLY: astaire\n")
    assert expected in gen
Example #8
0
 def gen_code(self, parent):
     ''' Generates GOcean specific psy code for a call to the dynamo
         kernel instance. '''
     from psyclone.f2pygen import CallGen, UseGen
     arguments = ["i", "j"]
     for arg in self._arguments.args:
         if arg.space.lower() == "r":
             arguments.append(arg.name + "%data")
         else:
             arguments.append(arg.name)
     parent.add(CallGen(parent, self._name, arguments))
     if not self.module_inline:
         parent.add(UseGen(parent, name=self._module_name, only=True,
                           funcnames=[self._name]))
Example #9
0
def test_add_before():
    ''' add the new code before a particular object '''
    module = ModuleGen(name="testmodule")
    subroutine = SubroutineGen(module, name="testsubroutine")
    module.add(subroutine)
    loop = DoGen(subroutine, "it", "1", "10")
    subroutine.add(loop)
    call = CallGen(subroutine, "testcall")
    subroutine.add(call, position=["before", loop.root])
    lines = str(module.root).splitlines()
    # the call should be inserted before the loop
    print lines
    assert "SUBROUTINE testsubroutine" in lines[3]
    assert "CALL testcall" in lines[4]
    assert "DO it=1,10" in lines[5]
Example #10
0
    def gen_code(self, parent):
        ''' Generates GOcean v1.0 specific psy code for a call to the dynamo
            kernel instance. '''
        from psyclone.f2pygen import CallGen, UseGen

        # Before we do anything else, go through the arguments and
        # determine the best one from which to obtain the grid properties.
        grid_arg = self._find_grid_access()

        # A GOcean 1.0 kernel always requires the [i,j] indices of the
        # grid-point that is to be updated
        arguments = ["i", "j"]
        for arg in self._arguments.args:

            if arg.type == "scalar":
                # Scalar arguments require no de-referencing
                arguments.append(arg.name)
            elif arg.type == "field":
                # Field objects are Fortran derived-types
                arguments.append(arg.name + "%data")
            elif arg.type == "grid_property":
                # Argument is a property of the grid which we can access via
                # the grid member of any field object.
                # We use the most suitable field as chosen above.
                if grid_arg is None:
                    raise GenerationError(
                        "Error: kernel {0} requires grid property {1} but "
                        "does not have any arguments that are fields".format(
                            self._name, arg.name))
                else:
                    arguments.append(grid_arg.name + "%grid%" + arg.name)
            else:
                raise GenerationError("Kernel {0}, argument {1} has "
                                      "unrecognised type: {2}".format(
                                          self._name, arg.name, arg.type))

        parent.add(CallGen(parent, self._name, arguments))
        if not self.module_inline:
            parent.add(
                UseGen(parent,
                       name=self._module_name,
                       only=True,
                       funcnames=[self._name]))
Example #11
0
    def create_driver(self, input_list, output_list):
        # pylint: disable=too-many-locals, too-many-statements
        '''This function creates a driver that can read the
        output created by the extraction code. This is a stand-alone
        program that will read the input data, calls the kernels/
        instrumented region, and then compares the results with the
        stored results in the file.

        TODO: #644: we need type information here.

        :param input_list: list of variables that are input parameters.
        :type input_list: list of str
        :param output_list: list of variables that are output parameters.
        :type output_list: list or str
        '''

        from psyclone.f2pygen import AllocateGen, AssignGen, CallGen,\
            CommentGen, DeclGen, ModuleGen, SubroutineGen, UseGen, \
            TypeDeclGen
        from psyclone.gocean1p0 import GOSymbolTable
        from psyclone.psyir.symbols import Symbol

        all_vars = list(set(input_list).union(set(output_list)))
        all_vars.sort()

        module_name, region_name = self.region_identifier
        module = ModuleGen(name=module_name)
        prog = SubroutineGen(parent=module, name=module_name+"_code",
                             implicitnone=True)
        module.add(prog)
        use = UseGen(prog, self.add_psydata_class_prefix("psy_data_mod"),
                     only=True,
                     funcnames=[self.add_psydata_class_prefix("PSyDataType")])
        prog.add(use)

        # Use a symbol table to make sure all variable names are unique
        sym_table = GOSymbolTable()
        sym = Symbol("PSyDataType")
        sym_table.add(sym)

        psy_data = sym_table.new_symbol_name(self.add_psydata_class_prefix
                                             ("psy_data"))
        sym_table.add(Symbol(psy_data))
        var_decl = TypeDeclGen(prog, datatype=self.add_psydata_class_prefix
                               ("PSyDataType"),
                               entity_decls=[psy_data])
        prog.add(var_decl)

        call = CallGen(prog,
                       "{0}%OpenRead(\"{1}\", \"{2}\")"
                       .format(psy_data, module_name, region_name))
        prog.add(call)

        post_suffix = self._post_name

        # Variables might need to be renamed in order to guarantee unique
        # variable names in the driver: An example of this would be if the
        # user code contains a variable 'dx', and the kernel takes a
        # property 'dx' as well. In the original code that is no problem,
        # since the property is used via field%grid%dx. But the stand-alone
        # driver renames field%grid%dx to dx, which can cause a name clash.
        # Similar problems can exist with any user defined type, since all
        # user defined types are rewritten to just use the field name.
        # We use a mapping to support renaming of variables: it takes as
        # key the variable as used in the original program (e.g. 'dx' from
        # an expression like field%grid%dx), and maps it to a unique local
        # name (e.g. dx_0).

        rename_variable = {}
        for var_name in all_vars:
            # TODO #644: we need to identify arrays!!
            # Support GOcean properties, which are accessed via a
            # derived type (e.g. 'fld%grid%dx'). In this stand-alone
            # driver we don't have the derived type, instead we create
            # variable based on the field in the derived type ('dx'
            # in the example above), and pass this variable to the
            # instrumented code.
            last_percent = var_name.rfind("%")
            if last_percent > -1:
                # Strip off the derived type, and only leave the last
                # field, which is used as the local variable name.
                local_name = var_name[last_percent+1:]
            else:
                # No derived type, so we can just use the
                # variable name directly in the driver
                local_name = var_name
            unique_local_name = sym_table.new_symbol_name(local_name)
            rename_variable[local_name] = unique_local_name
            sym_table.add(Symbol(unique_local_name))
            local_name = unique_local_name

            # TODO: #644 - we need to identify arrays!!
            # Any variable used needs to be defined. We also need
            # to handle the kind property better and not rely on
            # a hard-coded value.
            decl = DeclGen(prog, "real", [local_name], kind="8",
                           dimension=":,:", allocatable=True)
            prog.add(decl)
            is_input = var_name in input_list
            is_output = var_name in output_list

            if is_input and not is_output:
                # We only need the pre-variable, and we can read
                # it from the file (this call also allocates space for it).
                call = CallGen(prog,
                               "{0}%ReadVariable(\"{1}\", {2})"
                               .format(psy_data, var_name, local_name))
                prog.add(call)
            elif is_input:
                # Now must be input and output:
                # First read the pre-variable (which also allocates it):
                call = CallGen(prog,
                               "{0}%ReadVariable(\"{1}\", {2})"
                               .format(psy_data, var_name, local_name))
                prog.add(call)
                # Then declare the post variable, and and read its values
                # (ReadVariable will also allocate it)
                sym = Symbol(local_name+post_suffix)
                sym_table.add(sym)
                decl = DeclGen(prog, "real", [local_name+post_suffix],
                               dimension=":,:", kind="8", allocatable=True)
                prog.add(decl)
                call = CallGen(prog,
                               "{0}%ReadVariable(\"{1}{3}\", {2}{3})"
                               .format(psy_data, var_name, local_name,
                                       post_suffix))
                prog.add(call)
            else:
                # Now the variable is output only. We need to read the
                # post variable in, and create and allocate a pre variable
                # with the same size as the post
                sym = Symbol(local_name+post_suffix)
                sym_table.add(sym)
                decl = DeclGen(prog, "real", [local_name+post_suffix],
                               dimension=":,:", kind="8", allocatable=True)
                prog.add(decl)
                call = CallGen(prog,
                               "{0}%ReadVariable(\"{1}{3}\", {2}{3})"
                               .format(psy_data, var_name, local_name,
                                       post_suffix))
                prog.add(call)
                decl = DeclGen(prog, "real", [local_name], kind="8",
                               dimension=":,:", allocatable=True)
                prog.add(decl)
                alloc = AllocateGen(prog, [var_name],
                                    mold="{0}".format(local_name +
                                                      post_suffix))
                prog.add(alloc)
                # Initialise the variable with 0, since it might contain
                # values that are not set at all (halo regions, or a
                # kernel might not set all values). This way the array
                # comparison with the post value works as expected
                # TODO #644 - create the right "0.0" type here (e.g.
                # 0.0d0, ...)
                assign = AssignGen(prog, local_name, "0.0d0")
                prog.add(assign)

        # Now add the region that was extracted here:
        prog.add(CommentGen(prog, ""))
        prog.add(CommentGen(prog, " RegionStart"))

        # For the driver we have to re-create the code of the
        # instrumented region, but in this stand-alone driver the
        # arguments are not dl_esm_inf fields anymore, but simple arrays.
        # Similarly, for properties we cannot use e.g. 'fld%grid%dx'
        # anymore, we have to use e.g. a local variable 'dx' that has
        # been created. Since we are using the existing way of creating
        # the code for the instrumented region, we need to modify how
        # these variables are created. We do this by temporarily
        # modifying the properties in the config file.
        api_config = Config.get().api_conf("gocean1.0")
        all_props = api_config.grid_properties
        # Keep a copy of the original values, so we can restore
        # them later
        orig_props = dict(all_props)

        # 1) A grid property is defined like "{0}%grid%dx". This is
        #    changed to be just 'dx', i.e. the final component of
        #    the current value (but we also take renaming into account,
        #    so 'dx' might become 'dx_0').
        #    If a property is not used, it doesn't matter if we modify
        #    its definition, so we just change all properties.
        for name, prop in all_props.items():
            last_percent = prop.fortran.rfind("%")
            if last_percent > -1:
                # Get the last field name, which will be the
                # local variable name
                local_name = prop.fortran[last_percent+1:]
                unique_name = rename_variable.get(local_name, local_name)
                all_props[name] = GOceanConfig.make_property(
                    unique_name, prop.type, prop.intrinsic_type)

        # 2) The property 'grid_data' is a reference to the data on the
        #    grid (i.e. the actual field) , and it is defined as "{0}%data".
        #    This just becomes {0} ('a_fld%data' in the original program
        #    becomes just 'a_fld', and 'a_fld' is declared to be a plain
        #    Fortran 2d-array)
        all_props["go_grid_data"] = GOceanConfig.make_property(
            "{0}", "array", "real")

        # Each kernel caches the argument code, so we also
        # need to clear this cached data to make sure the new
        # value for "go_grid_data" is actually used.
        from psyclone.psyGen import CodedKern
        for kernel in self.psy_data_body.walk(CodedKern):
            kernel.clear_cached_data()

        # Recreate the instrumented region. Due to the changes in the
        # config files, fields and properties will now become local
        # plain arrays and variables:
        for child in self.psy_data_body:
            child.gen_code(prog)

        # Now reset all properties back to the original values:
        for name in all_props.keys():
            all_props[name] = orig_props[name]

        prog.add(CommentGen(prog, " RegionEnd"))
        prog.add(CommentGen(prog, ""))

        for var_name in output_list:
            prog.add(CommentGen(prog, " Check {0}".format(var_name)))

        code = str(module.root)

        with open("driver-{0}-{1}.f90".
                  format(module_name, region_name), "w") as out:
            out.write(code)
Example #12
0
    def gen_code(self, parent):
        ''' Generates dynamo version 0.1 specific psy code for a call to
            the dynamo kernel instance. '''
        from psyclone.f2pygen import CallGen, DeclGen, AssignGen, UseGen

        # TODO: we simply choose the first field as the lookup for the moment
        field_name = self.arguments.args[0].name

        # add a dofmap lookup using first field.
        # TODO: This needs to be generalised to work for multiple dofmaps
        parent.add(
            CallGen(parent, field_name + "%vspace%get_cell_dofmap",
                    ["cell", "map"]))
        parent.add(DeclGen(parent, datatype="integer", entity_decls=["cell"]))
        parent.add(
            DeclGen(parent,
                    datatype="integer",
                    pointer=True,
                    entity_decls=["map(:)"]))

        # create the argument list on the fly so we can also create
        # appropriate variables and lookups
        arglist = []
        arglist.append("nlayers")
        arglist.append("ndf")
        arglist.append("map")

        found_gauss_quad = False
        gauss_quad_arg = None
        for arg in self._arguments.args:
            if arg.requires_basis:
                basis_name = arg.function_space + "_basis_" + arg.name
                arglist.append(basis_name)
                new_parent, position = parent.start_parent_loop()
                new_parent.add(CallGen(new_parent,
                                       field_name + "%vspace%get_basis",
                                       [basis_name]),
                               position=["before", position])
                parent.add(
                    DeclGen(parent,
                            datatype="real",
                            kind="dp",
                            pointer=True,
                            entity_decls=[basis_name + "(:,:,:,:,:)"]))
            if arg.requires_diff_basis:
                raise GenerationError("differential basis has not yet "
                                      "been coded")
            if arg.requires_gauss_quad:
                if found_gauss_quad:
                    raise GenerationError("found more than one gaussian "
                                          "quadrature in this kernel")
                found_gauss_quad = True
                gauss_quad_arg = arg
            dataref = "%data"
            arglist.append(arg.name + dataref)

        if found_gauss_quad:
            gq_name = "gaussian_quadrature"
            arglist.append(gauss_quad_arg.name + "%" + gq_name)

        # generate the kernel call and associated use statement
        parent.add(CallGen(parent, self._name, arglist))
        if not self.module_inline:
            parent.add(
                UseGen(parent,
                       name=self._module_name,
                       only=True,
                       funcnames=[self._name]))

        # declare and initialise the number of layers and the number
        # of degrees of freedom. Needs to be generalised.
        parent.add(
            DeclGen(parent,
                    datatype="integer",
                    entity_decls=["nlayers", "ndf"]))
        new_parent, position = parent.start_parent_loop()
        new_parent.add(AssignGen(new_parent,
                                 lhs="nlayers",
                                 rhs=field_name + "%get_nlayers()"),
                       position=["before", position])
        new_parent.add(AssignGen(new_parent,
                                 lhs="ndf",
                                 rhs=field_name + "%vspace%get_ndf()"),
                       position=["before", position])