From 56d71fada6cb9ce80ac1fc50cba249df0595568e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Tue, 25 Aug 2026 14:55:02 +0200 Subject: [PATCH 1/2] :sparkles: Add instruction: `read` previous evaluation tables --- README.md | 1 + picometer/instructions.py | 21 +++++++++++++++++++++ tests/test_instructions.py | 27 +++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) 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..b3b6e0b 100644 --- a/picometer/instructions.py +++ b/picometer/instructions.py @@ -407,6 +407,27 @@ 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 = instruction.kwargs['path'] + 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..81a4bec 100644 --- a/tests/test_instructions.py +++ b/tests/test_instructions.py @@ -490,6 +490,33 @@ 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_document_history(self): routine_text = get_yaml('test_ferrocene.yaml') original_routine = Routine.from_string(routine_text) From 09c4b2cb0e7bf30bd13bb94f40a602798eadbe90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Tcho=C5=84?= Date: Tue, 25 Aug 2026 15:59:58 +0200 Subject: [PATCH 2/2] :sparkles: Load multiple evaluation files using glob, add tests --- picometer/instructions.py | 7 ++++++- tests/test_instructions.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/picometer/instructions.py b/picometer/instructions.py index b3b6e0b..6db7500 100644 --- a/picometer/instructions.py +++ b/picometer/instructions.py @@ -412,7 +412,12 @@ class ReadInstructionHandler(BaseInstructionHandler): kwargs = dict(path=Path) def handle(self, instruction: Instruction) -> None: - path = instruction.kwargs['path'] + 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) diff --git a/tests/test_instructions.py b/tests/test_instructions.py index 81a4bec..336ac10 100644 --- a/tests/test_instructions.py +++ b/tests/test_instructions.py @@ -517,6 +517,19 @@ def test_read_twice(self): 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)