From 65bf483140e54af63c9f35194960a6a4c093fd50 Mon Sep 17 00:00:00 2001 From: njzjz-bot Date: Mon, 24 Aug 2026 03:14:58 +0800 Subject: [PATCH 1/2] fix: cover CALYPSO element distance radii Fall back to ASE covalent radii when CALYPSO trajectories contain elements absent from the legacy safe-distance table. Closes #353 Coding-Agent: Codex Codex-Version: codex-cli 0.149.1 Model: gpt-5.6-sol Reasoning-Effort: xhigh --- dpgen2/op/run_caly_model_devi.py | 14 +++++++++++++- tests/op/test_run_caly_model_devi.py | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/dpgen2/op/run_caly_model_devi.py b/dpgen2/op/run_caly_model_devi.py index 9e191cf2..14f1ecd7 100644 --- a/dpgen2/op/run_caly_model_devi.py +++ b/dpgen2/op/run_caly_model_devi.py @@ -232,6 +232,10 @@ def parse_traj(traj_file): from ase.build import ( # type: ignore make_supercell, ) + from ase.data import ( # type: ignore + atomic_numbers, + covalent_radii, + ) from ase.io import ( # type: ignore read, ) @@ -266,6 +270,14 @@ def parse_traj(traj_file): "H": 0.813, } + def safe_radius(symbol): + """Return the legacy radius or an ASE covalent-radius fallback.""" + if symbol in safe_dist_dict: + return safe_dist_dict[symbol] + # The legacy table is expressed in Bohr before the 0.529 conversion + # below. Convert ASE's maintained Angstrom values to the same unit. + return covalent_radii[atomic_numbers[symbol]] / 0.529 + trajs: List[Atoms] = read(traj_file, index=":", format="traj") # type: ignore dthresh = 0.72 numb_traj = len(trajs) @@ -308,7 +320,7 @@ def parse_traj(traj_file): for b in range(a + 1, len(atype)): dd = dist_dict[a][b] dr = ( - (safe_dist_dict[atype[a]] + safe_dist_dict[atype[b]]) + (safe_radius(atype[a]) + safe_radius(atype[b])) * 0.529 / 1.2 ) diff --git a/tests/op/test_run_caly_model_devi.py b/tests/op/test_run_caly_model_devi.py index 647604a8..5de31662 100644 --- a/tests/op/test_run_caly_model_devi.py +++ b/tests/op/test_run_caly_model_devi.py @@ -271,6 +271,21 @@ def test_00_parse_traj(self): atoms_list_10 = parse_traj(self.traj_file_10) self.assertTrue(atoms_list_10 is None), self.atoms_abnormal + def test_parse_traj_supports_elements_outside_legacy_table(self): + """Use ASE covalent radii for common elements such as oxygen.""" + oxygen_traj = self.work_dir / "oxygen.traj" + oxygen = Atoms( + symbols=["O", "O"], + scaled_positions=[[0, 0, 0], [0.5, 0.5, 0.5]], + cell=np.eye(3) * 10, + ) + write(oxygen_traj, oxygen, format="traj") + + selected = parse_traj(oxygen_traj) + + self.assertIsNotNone(selected) + self.assertEqual(len(selected), 1) + def test_01_atoms2lmpdump(self): dump_str = atoms2lmpdump(self.atoms_normal_2, 1, self.type_map) self.assertEqual(dump_str, self.ref_dump_str) From 523cd861de5f55bfc5403db8def6e9646c071c99 Mon Sep 17 00:00:00 2001 From: "A bot of @njzjz" Date: Sat, 29 Aug 2026 18:59:56 +0800 Subject: [PATCH 2/2] fix: reuse CALYPSO safe-distance table Fall back to the package-wide CALYPSO safe-distance data, reject unknown elements explicitly, and warn when every trajectory frame is filtered. Coding-Agent: Codex Codex-Version: codex-cli 0.150.1 Model: gpt-5.6-sol Reasoning-Effort: xhigh --- dpgen2/op/run_caly_model_devi.py | 31 ++++++++++++++++----------- tests/op/test_run_caly_model_devi.py | 32 +++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/dpgen2/op/run_caly_model_devi.py b/dpgen2/op/run_caly_model_devi.py index 14f1ecd7..9258afa5 100644 --- a/dpgen2/op/run_caly_model_devi.py +++ b/dpgen2/op/run_caly_model_devi.py @@ -1,3 +1,4 @@ +import logging from collections import ( defaultdict, ) @@ -15,10 +16,14 @@ OPIO, Artifact, BigParameter, + FatalError, OPIOSign, Parameter, ) +from dpgen2.exploration.selector.distance_conf_filter import ( + safe_dist_dict as full_safe_dist_dict, +) from dpgen2.utils import ( set_directory, ) @@ -232,10 +237,6 @@ def parse_traj(traj_file): from ase.build import ( # type: ignore make_supercell, ) - from ase.data import ( # type: ignore - atomic_numbers, - covalent_radii, - ) from ase.io import ( # type: ignore read, ) @@ -271,12 +272,13 @@ def parse_traj(traj_file): } def safe_radius(symbol): - """Return the legacy radius or an ASE covalent-radius fallback.""" + """Return a CALYPSO safe radius, preferring legacy tuned values.""" if symbol in safe_dist_dict: return safe_dist_dict[symbol] - # The legacy table is expressed in Bohr before the 0.529 conversion - # below. Convert ASE's maintained Angstrom values to the same unit. - return covalent_radii[atomic_numbers[symbol]] / 0.529 + try: + return full_safe_dist_dict[symbol] + except KeyError: + raise FatalError(f"no safe distance known for element {symbol!r}") from None trajs: List[Atoms] = read(traj_file, index=":", format="traj") # type: ignore dthresh = 0.72 @@ -319,11 +321,7 @@ def safe_radius(symbol): for a in range(len(atype)): for b in range(a + 1, len(atype)): dd = dist_dict[a][b] - dr = ( - (safe_radius(atype[a]) + safe_radius(atype[b])) - * 0.529 - / 1.2 - ) + dr = (safe_radius(atype[a]) + safe_radius(atype[b])) * 0.529 / 1.2 if dd < dr: frame_is_reasonable = False @@ -333,6 +331,13 @@ def safe_radius(symbol): else: selected_traj = None + if selected_traj == []: + logging.warning( + "All frames in CALYPSO trajectory %s were rejected by " + "safe-distance filtering.", + traj_file, + ) + return selected_traj diff --git a/tests/op/test_run_caly_model_devi.py b/tests/op/test_run_caly_model_devi.py index 5de31662..bcc686ad 100644 --- a/tests/op/test_run_caly_model_devi.py +++ b/tests/op/test_run_caly_model_devi.py @@ -272,7 +272,7 @@ def test_00_parse_traj(self): self.assertTrue(atoms_list_10 is None), self.atoms_abnormal def test_parse_traj_supports_elements_outside_legacy_table(self): - """Use ASE covalent radii for common elements such as oxygen.""" + """Use the shared safe-distance table for elements such as oxygen.""" oxygen_traj = self.work_dir / "oxygen.traj" oxygen = Atoms( symbols=["O", "O"], @@ -286,6 +286,36 @@ def test_parse_traj_supports_elements_outside_legacy_table(self): self.assertIsNotNone(selected) self.assertEqual(len(selected), 1) + def test_parse_traj_applies_shared_nitrogen_safe_distance(self): + """Keep and reject nitrogen frames on opposite sides of the limit.""" + # The shared table gives an N-N threshold of 1.1638 Angstrom. These + # distances also distinguish it from the previous ASE fallback + # threshold of 1.1833 Angstrom. + accepted_traj = self.work_dir / "nitrogen-accepted.traj" + accepted = Atoms( + symbols=["N", "N"], + positions=[[0, 0, 0], [0, 0, 1.17]], + cell=np.eye(3) * 12, + pbc=True, + ) + write(accepted_traj, accepted, format="traj") + + selected = parse_traj(accepted_traj) + + self.assertIsNotNone(selected) + self.assertEqual(len(selected), 1) + + rejected_traj = self.work_dir / "nitrogen-rejected.traj" + rejected = accepted.copy() + rejected.positions[1, 2] = 1.15 + write(rejected_traj, rejected, format="traj") + + with self.assertLogs(level="WARNING") as captured: + selected = parse_traj(rejected_traj) + + self.assertEqual(selected, []) + self.assertIn("All frames in CALYPSO trajectory", captured.output[0]) + def test_01_atoms2lmpdump(self): dump_str = atoms2lmpdump(self.atoms_normal_2, 1, self.type_map) self.assertEqual(dump_str, self.ref_dump_str)