def test_all_nemo_loop_trans_base_validate(monkeypatch): ''' Check that all transformations that sub-class LoopTrans call the base validate() method. ''' # First get a valid Loop object that we can pass in. _, invoke = get_invoke("explicit_over_implicit.f90", api="nemo", idx=0) loop = invoke.schedule.walk(Loop)[0] # Get all transformations for the NEMO domain transmod = import_module("psyclone.domain.nemo.transformations") all_trans_classes = inspect.getmembers(transmod, inspect.isclass) # To ensure that we identify that the validate() method in the LoopTrans # base class has been called, we monkeypatch it to raise an exception. def fake_validate(_1, _2, options=None): raise NotImplementedError("validate test exception") monkeypatch.setattr(LoopTrans, "validate", fake_validate) for name, cls_type in all_trans_classes: trans = cls_type() if isinstance(trans, LoopTrans): # The Loop fuse validation function requires two # parameters (the two loops to fuse), so it needs # to be tested separately: if isinstance(trans, NemoLoopFuseTrans): with pytest.raises(NotImplementedError) as err: trans.validate(loop, node2=loop) else: with pytest.raises(NotImplementedError) as err: trans.validate(loop) assert "validate test exception" in str(err.value), \ "{0}.validate() does not call LoopTrans.validate()".format( name)
def test_invoke_function(): ''' Check that we successfully construct an Invoke if the program unit is a function. ''' psy, _ = get_invoke("afunction.f90", api=API, idx=0) assert len(psy.invokes.invoke_list) == 1 invoke = psy.invokes.invoke_list[0] assert invoke.name == "afunction"
def test_gocean_omp_parallel(): '''Test that an OMP PARALLEL directive in a 'classical' API (gocean here) is created correctly. ''' from psyclone.transformations import OMPParallelTrans _, invoke = get_invoke("single_invoke.f90", "gocean1.0", idx=0) omp = OMPParallelTrans() omp_sched, _ = omp.apply(invoke.schedule[0]) # Now remove the GOKern (since it's not yet supported in the # visitor pattern) and replace it with a simple assignment # TODO: #440 tracks this replace_child_with_assignment(omp_sched[0]) # omp_sched is a GOInvokeSchedule, which is not yet supported. # So only convert starting from the OMPParallelDirective fvisitor = FortranWriter() result = fvisitor(omp_sched[0]) correct = '''!$omp parallel a=b !$omp end parallel''' assert correct in result cvisitor = CWriter() # Remove newlines for easier RE matching result = cvisitor(omp_sched[0]) correct = '''#pragma omp parallel { a = b; }''' result = cvisitor(omp_sched[0]) assert correct in result
def test_apply_existing_names(): '''Check that the apply method uses existing iterators appropriately when their symbols are already defined. ''' _, invoke_info = get_invoke("implicit_do.f90", api=API, idx=0) schedule = invoke_info.schedule assignment = schedule[0] array_ref = assignment.lhs trans = NemoArrayRange2LoopTrans() symbol_table = schedule.symbol_table symbol_table.add(DataSymbol("ji", INTEGER_TYPE)) symbol_table.add(DataSymbol("jj", INTEGER_TYPE)) symbol_table.add(DataSymbol("jk", INTEGER_TYPE)) trans.apply(array_ref.children[2]) trans.apply(array_ref.children[1]) trans.apply(array_ref.children[0]) writer = FortranWriter() result = writer(schedule) assert ( "do jk = 1, jpk, 1\n" " do jj = 1, jpj, 1\n" " do ji = 1, jpi, 1\n" " umask(ji,jj,jk) = 0.0e0\n" " enddo\n" " enddo\n" "enddo" in result)
def test_omp_private_declaration(): ''' Check code generation and private/shared declaration when an assignment is parallelised. In this case the code is like: !$omp parallel default(shared), private() jpk = 100 do k=1, jpk ... enddo !$omp end parallel do k=1, jpk ... In this case jpk should not be declared private, since then it is not defined in the next loop.''' psy, invoke_info = get_invoke("explicit_do_two_loops.f90", api=API, idx=0) schedule = invoke_info.schedule omp_parallel = TransInfo().get_trans_name('OMPParallelTrans') # Apply "omp parallel" around one assignment to a scalar variable # and a loop using this variable as loop boundary. Parallelising an # assignment statement is not allowed by default, so we need to disable # the node type check in order to apply the omp parallel transform. omp_parallel.apply(schedule.children[0:2], {'node-type-check': False}) expected = "!$omp parallel default(shared), private(ji,jj,jk)" gen_code = str(psy.gen).lower() assert expected in gen_code
def test_2kern_trans(kernel_outputdir): ''' Check that we generate correct code when we transform two kernels within a single invoke. ''' psy, invoke = get_invoke("4.5.2_multikernel_invokes.f90", api="dynamo0.3", idx=0) sched = invoke.schedule kernels = sched.walk(Kern) assert len(kernels) == 5 ktrans = Dynamo0p3KernelConstTrans() _, _ = ktrans.apply(kernels[1], {"number_of_layers": 100}) _, _ = ktrans.apply(kernels[2], {"number_of_layers": 100}) # Generate the code (this triggers the generation of new kernels) code = str(psy.gen).lower() # Find the tags added to the kernel/module names for match in re.finditer('use testkern_any_space_2(.+?)_mod', code): tag = match.group(1) assert ("use testkern_any_space_2{0}_mod, only: " "testkern_any_space_2{0}_code".format(tag) in code) assert "call testkern_any_space_2{0}_code(".format(tag) in code filepath = os.path.join(str(kernel_outputdir), "testkern_any_space_2{0}_mod.f90".format(tag)) assert os.path.isfile(filepath) assert "nlayers = 100" in open(filepath).read() assert "use testkern_any_space_2_mod, only" not in code assert "call testkern_any_space_2_code(" not in code assert LFRicBuild(kernel_outputdir).code_compiles(psy)
def test_accroutine_err(monkeypatch): ''' Check that we raise the expected error if we can't find the source of the kernel subroutine. ''' import fparser _, invoke = get_invoke("1_single_invoke.f90", api="dynamo0.3", idx=0) sched = invoke.schedule kernels = sched.coded_kernels() kern = kernels[0] assert isinstance(kern, Kern) # Edit the fparser1 AST of the kernel so that it does not have a # subroutine of the correct name ast = kern._module_code mod = ast.content[0] # Find the subroutine statement for child in mod.content: if isinstance(child, fparser.one.block_statements.Subroutine): sub = child # Find the end subroutine statement for child in sub.content: if isinstance(child, fparser.one.block_statements.EndSubroutine): end = child monkeypatch.setattr(sub, "name", "some_other_name") monkeypatch.setattr(end, "name", "some_other_name") rtrans = ACCRoutineTrans() with pytest.raises(TransformationError) as err: _ = rtrans.apply(kern) assert ("Failed to create PSyIR version of kernel code for kernel " "'testkern_code'. Error reported is Generation Error: Unexpected " "kernel AST. Could not find subroutine: testkern_code." in str(err.value))
def test_change_prefix(tmpdir, monkeypatch): ''' This tests that the prefix of a gocean extract transformation can be changed, and that the new prefix is also used in the created driver. ''' # Use tmpdir so that the driver is created in tmp tmpdir.chdir() psy, invoke = get_invoke("single_invoke_scalar_float_arg.f90", GOCEAN_API, idx=0, dist_mem=False) # In order to use a different prefix, this prefix needs to be valid. # So monkeypatch the valid prefix names in the config object: config = Config.get() monkeypatch.setattr(config, "_valid_psy_data_prefixes", ["NEW"]) etrans = GOceanExtractTrans() etrans.apply(invoke.schedule.children[0], { 'create_driver': True, 'region_name': ("main", "update"), 'prefix': "NEW" }) # Test that the extraction code contains the new prefix: assert 'CALL NEW_psy_data%PreStart("main", "update", 4, 3)' \ in str(psy.gen) # Now test if the created driver has the right prefix: driver_name = tmpdir.join("driver-main-update.f90") with open(str(driver_name), "r") as driver_file: driver_code = driver_file.read() assert 'CALL NEW_psy_data%OpenRead("main", "update")' in driver_code
def test_goloop_field_accesses(): ''' Check that for a GOcean kernel appropriate field accesses (based on the meta data) are added to the dependency analysis. ''' _, invoke = get_invoke("large_stencil.f90", "gocean1.0", name="invoke_large_stencil", dist_mem=False) do_loop = invoke.schedule.children[0] assert isinstance(do_loop, Loop) var_accesses = VariablesAccessInfo(invoke.schedule) # cu_fld has a pointwise write access in the first loop: cu_fld = var_accesses["cu_fld"] assert len(cu_fld.all_accesses) == 1 assert cu_fld.all_accesses[0].access_type == AccessType.WRITE assert cu_fld.all_accesses[0].indices == ["i", "j"] # The stencil is defined to be GO_STENCIL(123,110,100)) for # p_fld. Make sure that these 9 accesses are indeed reported: p_fld = var_accesses["p_fld"] all_indices = [access.indices for access in p_fld.all_accesses] for test_index in [["i-1", "j+1"], ["i", "j+1"], ["i", "j+2"], ["i+1", "j+1"], ["i+2", "j+2"], ["i+3", "j+3"], ["i-1", "j"], ["i", "j"], ["i-1", "j-1"]]: assert test_index in all_indices # Since we have 9 different indices found (above), the following # test guarantees that we don't get any invalid accesses reported. assert len(p_fld.all_accesses) == 9
def test_driver_generation_flag(tmpdir, create_driver): '''Test that driver generation can be enabled and disabled, and that it is disabled by default. If create_driver is None, the default behaviour (don't create driver) is tested. ''' # Use tmpdir so that the driver is created in tmp tmpdir.chdir() etrans = GOceanExtractTrans() psy, invoke = get_invoke("driver_test.f90", GOCEAN_API, idx=0, dist_mem=False) schedule = invoke.schedule if create_driver is None: schedule, _ = etrans.apply(schedule.children[0:2]) else: schedule, _ = etrans.apply(schedule.children[0:2], {'create_driver': create_driver}) # We are only interested in the potentially triggered driver-creation. str(psy.gen) driver = tmpdir.join("driver-psy_extract_example_with_various_variable_" "access_patterns-invoke_0_compute_kernel:compute_" "kernel_code:r0.f90") # When create_driver is None, as a default no driver should be created. # Since "None or False" is "False", this simple test can be used in all # three cases. assert driver.isfile() == (create_driver or False)
def test_rename_region(tmpdir): ''' This tests that an extract region can be renamed, and that the created driver will use the new names. ''' # Use tmpdir so that the driver is created in tmp tmpdir.chdir() etrans = GOceanExtractTrans() psy, invoke = get_invoke("single_invoke_scalar_float_arg.f90", GOCEAN_API, idx=0, dist_mem=False) etrans.apply(invoke.schedule.children[0], { 'create_driver': True, 'region_name': ("main", "update") }) # Test that the extraction code contains the right names assert 'CALL extract_psy_data%PreStart("main", "update", 4, 3)' \ in str(psy.gen) # Now test if the created driver has the right name, and will open the # right file: driver_name = tmpdir.join("driver-main-update.f90") with open(str(driver_name), "r") as driver_file: driver_code = driver_file.read() assert 'CALL extract_psy_data%OpenRead("main", "update")' in driver_code
def test_extract_node_position(): ''' Test that Extract Transformation inserts the ExtractNode at the position of the first node in the Node list marked for extraction. ''' # Test GOcean1.0 API for extraction of a single Node gocetrans = GOceanExtractTrans() _, invoke = get_invoke("single_invoke_three_kernels.f90", GOCEAN_API, idx=0, dist_mem=False) schedule = invoke.schedule # Apply Extract transformation to the second Node and assert that # position and the absolute position of the ExtractNode are the same as # respective positions of the second Node before the transformation. pos = 1 child = schedule.children[pos] abspos = child.abs_position dpth = child.depth schedule, _ = gocetrans.apply(child) extract_node = schedule.walk(ExtractNode) # The result is only one ExtractNode in the list with position 1 assert extract_node[0].position == pos assert extract_node[0].abs_position == abspos assert extract_node[0].depth == dpth assert extract_node[0].dag_name == "gocean_extract_1"
def test_no_parent_accdirective(): ''' Test that applying Extract Transformation on an orphaned ACCLoopDirective without its ancestor ACCParallelDirective when optimisations are applied raises a TransformationError. ''' etrans = GOceanExtractTrans() acclpt = ACCLoopTrans() accpara = ACCParallelTrans() accdata = ACCEnterDataTrans() _, invoke = get_invoke("single_invoke_three_kernels.f90", GOCEAN_API, idx=0, dist_mem=False) schedule = invoke.schedule # Apply the OpenACC Loop transformation to every loop in the Schedule for child in schedule.children: if isinstance(child, Loop): schedule, _ = acclpt.apply(child) # Enclose all of these loops within a single ACC Parallel region schedule, _ = accpara.apply(schedule.children) # Add a mandatory ACC enter-data directive schedule, _ = accdata.apply(schedule) orphaned_directive = schedule.children[1].children[0] with pytest.raises(TransformationError) as excinfo: _, _ = etrans.apply(orphaned_directive) assert "Error in GOceanExtractTrans: Application to Nodes enclosed " \ "within a thread-parallel region is not allowed." \ in str(excinfo.value)
def test_openmp_region(): ''' Test the application of an OpenMP parallel region transformation to a single loop ''' psy, invoke = get_invoke("algorithm/1_single_function.f90", TEST_API, name="invoke_0_testkern_type") schedule = invoke.schedule rtrans = OMPParallelTrans() rtrans.apply(schedule.children[0]) gen = str(psy.gen) # Check that our list of private variables is correct assert "!$omp parallel default(shared), private(cell,map)" in gen for idx, line in enumerate(gen.split('\n')): if "!$omp parallel default(shared)" in line: startpara_idx = idx if "DO cell=1,f1%get_ncell()" in line: do_idx = idx if "CALL f1%vspace%get_cell_dofmap(cell, map)" in line: dmap_idx = idx if "CALL testkern_code(nlayers, ndf, map, f1%data, "\ "f2%data, m1%data)" in line: kcall_idx = idx if "END DO" in line: enddo_idx = idx if "!$omp end parallel" in line: endpara_idx = idx assert do_idx == startpara_idx + 1 assert dmap_idx == do_idx + 1 assert kcall_idx == dmap_idx + 1 assert enddo_idx == kcall_idx + 1 assert endpara_idx == enddo_idx + 1
def test_new_kern_single_error(kernel_outputdir, monkeypatch): ''' Check that we do not overwrite an existing, different kernel if there is a name clash and kernel-naming is 'single'. ''' # Ensure kernel-output directory is uninitialised config = Config.get() monkeypatch.setattr(config, "_kernel_naming", "single") _, invoke = get_invoke("1_single_invoke.f90", api="dynamo0.3", idx=0) sched = invoke.schedule kernels = sched.coded_kernels() kern = kernels[0] old_mod_name = kern.module_name[:].lower() if old_mod_name.endswith("_mod"): old_mod_name = old_mod_name[:-4] # Create a file with the same name as we would otherwise generate with open(os.path.join(str(kernel_outputdir), old_mod_name + "_0_mod.f90"), "w") as ffile: ffile.write("some code") rtrans = ACCRoutineTrans() new_kern, _ = rtrans.apply(kern) # Generate the code - this should raise an error as we get a name # clash and the content of the existing file is not the same as that # which we would generate with pytest.raises(GenerationError) as err: new_kern.rename_and_write() assert ("transformed version of this Kernel 'testkern_0_mod.f90' already " "exists in the kernel-output directory ({0}) but is not the same " "as the current, transformed kernel and the kernel-renaming " "scheme is set to 'single'".format(str(kernel_outputdir)) in str(err.value))
def test_lfric_acc(): '''Check variable usage detection when OpenACC is used. ''' # Use the OpenACC transforms to enclose the kernels # with OpenACC directives. acc_par_trans = ACCParallelTrans() acc_enter_trans = ACCEnterDataTrans() _, invoke = get_invoke("1_single_invoke.f90", "dynamo0.3", name="invoke_0_testkern_type", dist_mem=False) sched = invoke.schedule _ = acc_par_trans.apply(sched.children) _ = acc_enter_trans.apply(sched) # Find the first kernel: kern = invoke.schedule.walk(CodedKern)[0] create_acc_arg_list = KernCallAccArgList(kern) var_accesses = VariablesAccessInfo() create_acc_arg_list.generate(var_accesses=var_accesses) var_info = str(var_accesses) assert "f1: READ+WRITE" in var_info assert "f2: READ" in var_info assert "m1: READ" in var_info assert "m2: READ" in var_info assert "undf_w1: READ" in var_info assert "map_w1: READ" in var_info assert "undf_w2: READ" in var_info assert "map_w2: READ" in var_info assert "undf_w3: READ" in var_info assert "map_w3: READ" in var_info
def test_1kern_trans(kernel_outputdir): ''' Check that we generate the correct code when an invoke contains the same kernel more than once but only one of them is transformed. ''' psy, invoke = get_invoke("4_multikernel_invokes.f90", api="dynamo0.3", idx=0) sched = invoke.schedule kernels = sched.coded_kernels() # We will transform the second kernel but not the first kern = kernels[1] rtrans = ACCRoutineTrans() _, _ = rtrans.apply(kern) # Generate the code (this triggers the generation of a new kernel) code = str(psy.gen).lower() tag = re.search('use testkern(.+?)_mod', code).group(1) # We should have a USE for the original kernel and a USE for the new one assert "use testkern{0}_mod, only: testkern{0}_code".format(tag) in code assert "use testkern_mod, only: testkern_code" in code # Similarly, we should have calls to both the original and new kernels assert "call testkern_code(" in code assert "call testkern{0}_code(".format(tag) in code first = code.find("call testkern_code(") second = code.find("call testkern{0}_code(".format(tag)) assert first < second assert LFRicBuild(kernel_outputdir).code_compiles(psy)
def test_lfric_acc_operator(): '''Check variable usage detection when OpenACC is used with a kernel that uses an operator. ''' # Use the OpenACC transforms to enclose the kernels # with OpenACC directives. acc_par_trans = ACCParallelTrans() acc_enter_trans = ACCEnterDataTrans() _, invoke = get_invoke("20.0_cma_assembly.f90", "dynamo0.3", idx=0, dist_mem=False) sched = invoke.schedule _ = acc_par_trans.apply(sched.children) _ = acc_enter_trans.apply(sched) # Find the first kernel: kern = invoke.schedule.walk(CodedKern)[0] create_acc_arg_list = KernCallAccArgList(kern) var_accesses = VariablesAccessInfo() create_acc_arg_list.generate(var_accesses=var_accesses) var_info = str(var_accesses) assert "lma_op1_proxy%ncell_3d: READ" in var_info assert "lma_op1_proxy%local_stencil: WRITE" in var_info
def test_kernel_trans_validate(monkeypatch): '''Check that the validate method in the class KernelTrans raises an exception if the reference is not found in any of the symbol tables. KernelTrans can't be instantiated as it is abstract so use a the subclass. ''' from psyclone.transformations import KernelModuleInlineTrans kernel_trans = KernelModuleInlineTrans() _, invoke = get_invoke("single_invoke_kern_with_global.f90", api="gocean1.0", idx=0) sched = invoke.schedule kernels = sched.walk(Kern) kernel = kernels[0] def dummy_func(): '''Simple Dummy function that raises SymbolError.''' from psyclone.psyir.symbols import SymbolError raise SymbolError("error") monkeypatch.setattr(kernel, "get_kernel_schedule", dummy_func) with pytest.raises(TransformationError) as err: _, _ = kernel_trans.apply(kernel) assert ("'kernel_with_global_code' contains accesses to data that are " "not captured in the PSyIR Symbol Table(s) (error)." "" in str(err.value))
def test_lfric_stencil(): '''Check variable usage detection when OpenACC is used with a kernel that uses a stencil. ''' # Use the OpenACC transforms to create the required kernels acc_par_trans = ACCParallelTrans() acc_enter_trans = ACCEnterDataTrans() _, invoke = get_invoke("14.4_halo_vector.f90", "dynamo0.3", idx=0, dist_mem=False) sched = invoke.schedule _ = acc_par_trans.apply(sched.children) _ = acc_enter_trans.apply(sched) # Find the first kernel: kern = invoke.schedule.walk(CodedKern)[0] create_acc_arg_list = KernCallAccArgList(kern) var_accesses = VariablesAccessInfo() create_acc_arg_list.generate(var_accesses=var_accesses) var_info = str(var_accesses) assert "f1: READ+WRITE" in var_info assert "f2: READ" in var_info assert "f2_stencil_dofmap: READ" in var_info
def test_apply_reference_literal(): '''Check that the apply method add bounds appropriately when the config file specifies a lower bound as a reference and an upper bound as a literal. ''' _, invoke_info = get_invoke("implicit_many_dims.f90", api=API, idx=0) # Create a new config instance and load a test config file with # the bounds information set the way we want. config = Config.get(do_not_load_file=True) config.load(config_file=TEST_CONFIG) schedule = invoke_info.schedule assignment = schedule[0] array_ref = assignment.lhs trans = NemoArrayRange2LoopTrans() for index in range(4, -1, -1): range_node = array_ref.children[index] trans.apply(range_node) # Remove this config file so the next time the default one will be # loaded (in case we affect other tests) Config._instance = None writer = FortranWriter() result = writer(schedule) assert ( "do idx = LBOUND(umask, 5), UBOUND(umask, 5), 1\n" " do jt = 1, UBOUND(umask, 4), 1\n" " do jk = jpk, 1, 1\n" " do jj = 1, jpj, 1\n" " do ji = jpi, 1, 1\n" " umask(ji,jj,jk,jt,idx) = vmask(ji,jj,jk,jt,idx) + 1.0\n" " enddo\n" " enddo\n" " enddo\n" " enddo\n" "enddo" in result)
def test_transform_apply_mixed_implicit_do(): '''Check that the PSyIR is transformed as expected for a lat,lon,levs loop with some of its indices accessed using array notation and some using explicit loops. The resultant Fortran code is used to confirm the transformation has worked correctly. ''' _, invoke_info = get_invoke("explicit_over_implicit.f90", api=API, idx=0) schedule = invoke_info.schedule assignment = schedule[0].loop_body[0] trans = NemoOuterArrayRange2LoopTrans() # Apply transformation 2 times, once for each implicit # dimension trans.apply(assignment) writer = FortranWriter() result = writer(schedule) expected = ( "do jk = 1, jpk, 1\n" " do jj = 1, jpj, 1\n" " umask(:,jj,jk) = vmask(:,jj,jk) + 1.0\n" " enddo\n" "enddo") assert expected in result trans.apply(assignment) result = writer(schedule) expected = ( "do jk = 1, jpk, 1\n" " do jj = 1, jpj, 1\n" " do ji = 1, jpi, 1\n" " umask(ji,jj,jk) = vmask(ji,jj,jk) + 1.0\n" " enddo\n" " enddo\n" "enddo") assert expected in result
def test_omp_explicit_gen(): ''' Check code generation for a single explicit loop containing a kernel. ''' psy, invoke_info = get_invoke("explicit_do.f90", api=API, idx=0) schedule = invoke_info.schedule omp_trans = TransInfo().get_trans_name('OMPParallelLoopTrans') for loop in schedule.loops(): kernel = loop.kernel if kernel and loop.loop_type == "levels": schedule, _ = omp_trans.apply(loop) gen_code = str(psy.gen).lower() expected = ( "program explicit_do\n" " implicit none\n" " integer :: ji, jj, jk\n" " integer :: jpi, jpj, jpk\n" " real :: r\n" " real, dimension(jpi, jpj, jpk) :: umask\n" " !$omp parallel do default(shared), private(ji,jj,jk), " "schedule(static)\n" " do jk = 1, jpk\n" " do jj = 1, jpj\n" " do ji = 1, jpi\n" " umask(ji, jj, jk) = ji * jj * jk / r\n" " end do\n" " end do\n" " end do\n" " !$omp end parallel do\n" "end program explicit_do") assert expected in gen_code # Check that calling gen a second time gives the same code gen_code = str(psy.gen).lower() assert expected in gen_code
def test_goloop_grid_property_psyir_expression(): ''' Tests for the _grid_property_psyir_expression() method. ''' _, invoke = get_invoke("test11_different_iterates_over_one_invoke.f90", API, idx=0) schedule = invoke.schedule loop = schedule.walk(GOLoop)[0] # A simple name should result in a new symbol and a suitable reference assert "hello" not in schedule.symbol_table href = loop._grid_property_psyir_expression("hello") hsym = schedule.symbol_table.lookup("hello") assert isinstance(hsym, DataSymbol) assert href.parent is loop assert hsym.datatype == INTEGER_TYPE assert isinstance(href, Reference) # A derived-type reference must be in the form of a format string with # "{0}" at the start. with pytest.raises(NotImplementedError) as err: loop._grid_property_psyir_expression("wrong%one") assert ("Supplied grid property is a derived-type reference but does " "not begin with '{0}': 'wrong%one'" in str(err.value)) gref = loop._grid_property_psyir_expression("{0}%grid%xstart") assert isinstance(gref, StructureReference) assert gref.parent is None assert gref.symbol.name == "cv_fld"
def test_apply_multi_assignments(): '''Check that the apply() method can be called for multiple assigments with and without ranges. ''' _, invoke_info = get_invoke("array_syntax.f90", api=API, idx=0) schedule = invoke_info.schedule trans = NemoAllArrayRange2LoopTrans() for assignment in schedule.walk(Assignment): trans.apply(assignment) writer = FortranWriter() result = writer(schedule) expected = ("do jk = 1, jpk, 1\n" " do jj = 1, jpj, 1\n" " do ji = 1, jpi, 1\n" " zftv(ji,jj,jk) = 0.0e0\n" " enddo\n" " enddo\n" "enddo\n" "if (l_ptr) then\n" " call dia_ptr_hst(jn, ''ldf'', -zftv(:,:,:))\n" "end if\n" "call dia_ptr_hst(jn, ''ldf'', -zftv(:,:,:))\n" "do jj = 1, jpj, 1\n" " do ji = 1, jpi, 1\n" " zftu(ji,jj,1) = 1.0e0\n" " enddo\n" "enddo\n" "do jj = 1, jpj, 1\n" " do ji = 1, jpi, 1\n" " tmask(ji,jj) = jpi\n" " enddo\n" "enddo\n") assert expected in result
def test_lower_to_lang_level_multi_node(): ''' Test the lower_to_language_level() method when a Schedule contains multiple ProfileNodes. ''' # We use a GOcean example containing multiple kernel calls Profiler.set_options([Profiler.KERNELS]) _, invoke = get_invoke("single_invoke_two_kernels.f90", "gocean1.0", idx=0) sched = invoke.schedule table = sched.symbol_table Profiler.add_profile_nodes(sched, Loop) sched.lower_to_language_level() sym0 = table.lookup("profile_psy_data") assert isinstance(sym0, DataSymbol) sym1 = table.lookup("profile_psy_data_1") assert isinstance(sym1, DataSymbol) cblocks = sched.walk(CodeBlock) ptree = cblocks[0].get_ast_nodes code = str(ptree[0]).lower() assert "call profile_psy_data % prestart('invoke_0', 'r0'" in code assert cblocks[0].annotations == ["profile-start"] assert cblocks[1].annotations == [] ptree = cblocks[2].get_ast_nodes code = str(ptree[0]).lower() assert "call profile_psy_data_1 % prestart('invoke_0', 'r1'" in code assert cblocks[2].annotations == ["profile-start"] assert cblocks[3].annotations == []
def test_loop_trans_validate(monkeypatch): ''' Test the validation checks on the loop node provided to the transformation. ''' # We have to use sub-class of LoopTrans as it itself is abstract. trans = OMPParallelLoopTrans() _, invoke = get_invoke("test27_loop_swap.f90", "gocean1.0", idx=1, dist_mem=False) with pytest.raises(TransformationError) as err: trans.validate(invoke.schedule) assert ("Target of OMPParallelLoopTrans transformation must be a sub-" "class of Loop but got 'GOInvokeSchedule'" in str(err.value)) # Check that validate is OK with a valid loop loop = invoke.schedule.walk(Loop)[0] trans.validate(loop) # Pretend that the loop is of 'null' type monkeypatch.setattr(loop, "_loop_type", "null") with pytest.raises(TransformationError) as err: trans.validate(loop) assert ("Cannot apply a OMPParallelLoopTrans transformation to a " "'null' loop" in str(err.value)) monkeypatch.undo() # Break the contents of the loop loop.children = loop.pop_all_children()[0:1] with pytest.raises(TransformationError) as err: trans.validate(loop) assert ("Error in OMPParallelLoopTrans transformation. The target loop " "must have four children but found: ['Literal']" in str(err.value))
def test_accroutine(): ''' Test that we can transform a kernel by adding a "!$acc routine" directive to it. ''' from psyclone.gocean1p0 import GOKern from fparser.two import Fortran2003 _, invoke = get_invoke("nemolite2d_alg_mod.f90", api="gocean1.0", idx=0) sched = invoke.schedule kern = sched.coded_kernels()[0] assert isinstance(kern, GOKern) rtrans = ACCRoutineTrans() assert rtrans.name == "ACCRoutineTrans" new_kern, _ = rtrans.apply(kern) # The transformation should have populated the fparser2 AST of # the kernel... assert new_kern._fp2_ast assert isinstance(new_kern._fp2_ast, Fortran2003.Program) # Check AST contains directive comments = walk(new_kern._fp2_ast.content, Fortran2003.Comment) assert len(comments) == 1 assert str(comments[0]) == "!$acc routine" # Check that directive is in correct place (end of declarations) gen = str(new_kern._fp2_ast) assert ("REAL(KIND = go_wp), DIMENSION(:, :), INTENT(IN) :: sshn, sshn_u, " "sshn_v, hu, hv, un, vn\n" " !$acc routine\n" " ssha(ji, jj) = 0.0_go_wp\n" in gen)
def test_region(): ''' Tests that the profiling transform works correctly when a region of code is specified that does not cover the full invoke and also contains multiple kernels. ''' _, invoke = get_invoke("3.1_multi_functions_multi_invokes.f90", "dynamo0.3", name="invoke_0", dist_mem=True) schedule = invoke.schedule prt = ProfileTrans() # Just halo exchanges. _ = prt.apply(schedule[0:4]) # Two loops. _ = prt.apply(schedule[1:3]) result = str(invoke.gen()) assert ("CALL profile_psy_data%PreStart(\"multi_functions_multi_invokes_" "psy\", \"invoke_0:r0\", 0, 0)" in result) assert ("CALL profile_psy_data_1%PreStart(\"multi_functions_multi_" "invokes_psy\", \"invoke_0:r1\", 0, 0)" in result) # Make nested profiles. _ = prt.apply(schedule[1].profile_body[1]) _ = prt.apply(schedule) result = str(invoke.gen()) assert ("CALL profile_psy_data_3%PreStart(\"multi_functions_multi_" "invokes_psy\", \"invoke_0:r0\", 0, 0)" in result) assert ("CALL profile_psy_data%PreStart(\"multi_functions_multi_" "invokes_psy\", \"invoke_0:r1\", 0, 0)" in result) assert ("CALL profile_psy_data_1%PreStart(\"multi_functions_multi_" "invokes_psy\", \"invoke_0:r2\", 0, 0)" in result) assert ("CALL profile_psy_data_2%PreStart(\"multi_functions_multi_" "invokes_psy\", \"invoke_0:testkern_code:r3\", 0, 0)" in result)
def test_opencl_kernel_output_file(kernel_outputdir): '''Check that an OpenCL file named modulename_kernelname_0 is generated. ''' psy, _ = get_invoke("single_invoke.f90", API, idx=0) sched = psy.invokes.invoke_list[0].schedule # Create dummy boundary symbols for the "name" kernel sched.symbol_table.new_symbol("a", tag="xstart_name", symbol_type=DataSymbol, datatype=INTEGER_TYPE) sched.symbol_table.new_symbol("b", tag="xstop_name", symbol_type=DataSymbol, datatype=INTEGER_TYPE) sched.symbol_table.new_symbol("c", tag="ystart_name", symbol_type=DataSymbol, datatype=INTEGER_TYPE) sched.symbol_table.new_symbol("d", tag="ystop_name", symbol_type=DataSymbol, datatype=INTEGER_TYPE) otrans = OCLTrans() otrans.apply(sched) sched.kernels()[0].name = "name" _ = psy.gen # Generates the OpenCL kernels as a side-effect. assert os.path.exists( os.path.join(str(kernel_outputdir), "compute_cu_name_0.cl"))