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
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)
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)
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
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
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)
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
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]))
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]
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]))
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)
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])