diff --git a/aiida_flexpart/calculations/flexpart_post.py b/aiida_flexpart/calculations/flexpart_post.py new file mode 100644 index 0000000..8d8fe55 --- /dev/null +++ b/aiida_flexpart/calculations/flexpart_post.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +""" +Calculations provided by aiida_flexpart. +Register calculations via the "aiida.calculations" entry point in setup.json. +""" +from aiida import orm, common, engine + + +class PostProcessingCalculation(engine.CalcJob): + """AiiDA calculation plugin for post processing.""" + @classmethod + def define(cls, spec): + """Define inputs and outputs of the calculation.""" + # yapf: disable + super().define(spec) + + # set default values for AiiDA options + spec.inputs['metadata']['options']['resources'].default = { + 'num_machines': 1, + 'num_mpiprocs_per_machine': 1, + } + + #INPUTS + spec.input('metadata.options.parser_name', valid_type=str, default='flexpart.post') + spec.input('input_dir', valid_type = orm.RemoteData, required=True, + help = 'main FLEXPART output dir') + spec.input('input_offline_dir', valid_type = orm.RemoteData, required=False, + help = 'offline-nested FLEXPART output dir') + spec.input('metadata.options.output_filename', valid_type=str, default='aiida.out', required=True) + #exit codes + spec.outputs.dynamic = True + spec.exit_code(300, 'ERROR_MISSING_OUTPUT_FILES', message='Calculation did not produce all expected output files.') + + def prepare_for_submission(self, folder): + + params = ['-m',self.inputs.input_dir.get_remote_path(), + '-r','./' + ] + if 'input_offline_dir' in self.inputs: + params += ['-n',self.inputs.input_offline_dir.get_remote_path()] + + codeinfo = common.CodeInfo() + codeinfo.cmdline_params = params + codeinfo.code_uuid = self.inputs.code.uuid + codeinfo.stdout_name = self.metadata.options.output_filename + codeinfo.withmpi = self.inputs.metadata.options.withmpi + + # Prepare a `CalcInfo` to be returned to the engine + calcinfo = common.CalcInfo() + calcinfo.codes_info = [codeinfo] + calcinfo.retrieve_list = ['grid_time_*.nc', 'boundary_sensitivity_*.nc', 'aiida.out'] + + return calcinfo diff --git a/aiida_flexpart/parsers/flexpart_post.py b/aiida_flexpart/parsers/flexpart_post.py new file mode 100644 index 0000000..5ca2168 --- /dev/null +++ b/aiida_flexpart/parsers/flexpart_post.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +""" +Parsers provided by aiida_flexpart. + +Register parsers via the "aiida.parsers" entry point in setup.json. +""" +from aiida import engine, parsers, plugins, common, orm + +FlexpartCalculation = plugins.CalculationFactory('flexpart.post') + + +class FlexpartPostParser(parsers.Parser): + """ + Parser class for parsing output of calculation. + """ + def __init__(self, node): + """ + Initialize Parser instance + + Checks that the ProcessNode being passed was produced by a FlexpartCalculation. + + :param node: ProcessNode of calculation + :param type node: :class:`aiida.orm.ProcessNode` + """ + super().__init__(node) + if not issubclass(node.process_class, FlexpartCalculation): + raise common.ParsingError('Can only parse FlexpartCalculation') + + def parse(self, **kwargs): + """ + Parse outputs, store results in database. + + :returns: an exit code, if parsing fails (or nothing if parsing succeeds) + """ + output_filename = self.node.get_option('output_filename') + + # Check that folder content is as expected + files_retrieved = self.retrieved.list_object_names() + files_expected = [output_filename] + # Note: set(A) <= set(B) checks whether A is a subset of B + if not set(files_expected) <= set(files_retrieved): + self.logger.error( + f"Found files '{files_retrieved}', expected to find '{files_expected}'" + ) + return self.exit_codes.ERROR_MISSING_OUTPUT_FILES + + # add output file + self.logger.info(f"Parsing '{output_filename}'") + with self.retrieved.open(output_filename, 'rb') as handle: + output_node = orm.SinglefileData(file=handle) + self.out('output_file', output_node) + + return engine.ExitCode(0)