diff --git a/README.md b/README.md index 2b6ff3c..808c4de 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ The following instructions are currently supported by picometer: - `load` model from a cif file, given `filename` or mapping syntax: `{path: filename.cif, block: cif_block}`. - `write` table with all evaluations to a csv file. + - `read` table with previous evaluation (to append further). - **Selection instructions** - `select` atoms, groups, or shapes to be used; use raw element names or provide symmetry relation / recenter using mapping syntax, for example: diff --git a/picometer/instructions.py b/picometer/instructions.py index 850c2a4..6db7500 100644 --- a/picometer/instructions.py +++ b/picometer/instructions.py @@ -407,6 +407,32 @@ def handle_one(self, instruction: Instruction, ms_key: str, ms: ModelState) -> N logger.info(f'Evaluated dihedral {label}: {dihedral} for model state {ms_key}') +class ReadInstructionHandler(BaseInstructionHandler): + name = 'read' + kwargs = dict(path=Path) + + def handle(self, instruction: Instruction) -> None: + path = Path(instruction.kwargs['path']) + paths = [path] if path.is_file() else sorted(glob(str(path))) + for path in paths: + self._read_table(str(path)) + + def _read_table(self, path: str) -> None: + old = self.processor.evaluation_table + new = pd.read_csv(path, index_col=0) + + if old.empty: + self.processor.evaluation_table = new + else: + index = old.index.union(new.index) + columns = old.columns.union(new.columns) + old = old.reindex(index=index, columns=columns) + old.update(new) + self.processor.evaluation_table = old + + logger.info(f'Loaded evaluation table from {path}') + + class WriteInstructionHandler(BaseInstructionHandler): name = 'write' kwargs = dict(path=Path) diff --git a/tests/test_instructions.py b/tests/test_instructions.py index eaea075..336ac10 100644 --- a/tests/test_instructions.py +++ b/tests/test_instructions.py @@ -490,6 +490,46 @@ def test_write(self): assert_frame_equal(correct, results, check_exact=False, rtol=1e-13, atol=1e-12) + def test_read(self): + with importlib.resources.path('tests', 'test_ferrocene.yaml') as yaml_path: + tests_path = yaml_path.parent + routine_tmp = 'instructions:\n - read: {}/ferrocene_correct.csv\n' + routine_text = routine_tmp.format(tests_path) + p = process(Routine.from_string(routine_text)) + correct_path = tests_path / 'ferrocene_correct.csv' + correct = pd.read_csv(correct_path, index_col=0) + results = p.evaluation_table + results.index = correct.index # index is env-dependent so ignore it + assert_frame_equal(correct, results, check_exact=False, + rtol=1e-13, atol=1e-12) + + def test_read_twice(self): + with importlib.resources.path('tests', 'test_ferrocene.yaml') as yaml_path: + tests_path = yaml_path.parent + routine_tmp = 'instructions:\n - read: {}/ferrocene_correct.csv\n' + routine_tmp += ' - read: {}/ferrocene_correct.csv\n' + routine_text = routine_tmp.format(tests_path, tests_path) + p = process(Routine.from_string(routine_text)) + correct_path = tests_path / 'ferrocene_correct.csv' + correct = pd.read_csv(correct_path, index_col=0) + results = p.evaluation_table + results.index = correct.index # index is env-dependent so ignore it + assert_frame_equal(correct, results, check_exact=False, + rtol=1e-13, atol=1e-12) + + def test_read_many(self): + with importlib.resources.path('tests', 'test_ferrocene.yaml') as yaml_path: + tests_path = yaml_path.parent + routine_tmp = 'instructions:\n - read: {}/ferrocene_corr*.csv\n' + routine_text = routine_tmp.format(tests_path) + p = process(Routine.from_string(routine_text)) + correct_path = tests_path / 'ferrocene_correct.csv' + correct = pd.read_csv(correct_path, index_col=0) + results = p.evaluation_table + results.index = correct.index # index is env-dependent so ignore it + assert_frame_equal(correct, results, check_exact=False, + rtol=1e-13, atol=1e-12) + def test_document_history(self): routine_text = get_yaml('test_ferrocene.yaml') original_routine = Routine.from_string(routine_text)