From 7d0462e066cb3d6ebdb0c0df151934d45a3ea6eb Mon Sep 17 00:00:00 2001 From: SchrodingersCattt Date: Fri, 9 Oct 2026 13:36:09 +0000 Subject: [PATCH 1/3] feat(system): control registered post-processing --- dpdata/bond_order_system.py | 7 +++--- dpdata/system.py | 38 ++++++++++++++++++++++++----- tests/test_vasp_poscar_to_system.py | 15 ++++++++++++ 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/dpdata/bond_order_system.py b/dpdata/bond_order_system.py index db29d6b65..28f6bfb17 100644 --- a/dpdata/bond_order_system.py +++ b/dpdata/bond_order_system.py @@ -95,11 +95,12 @@ def __init__( self.check_data() def from_fmt_obj(self, fmtobj, file_name, **kwargs): + post_func_controls = self._get_post_func_controls( + fmtobj.from_bond_order_system, kwargs + ) mol = fmtobj.from_bond_order_system(file_name, **kwargs) self.from_rdkit_mol(mol) - if hasattr(fmtobj.from_bond_order_system, "post_func"): - for post_f in fmtobj.from_bond_order_system.post_func: # type: ignore - self.post_funcs.get_plugin(post_f)(self) + self._apply_post_funcs(fmtobj.from_bond_order_system, post_func_controls) return self def to_fmt_obj(self, fmtobj, *args, **kwargs): diff --git a/dpdata/system.py b/dpdata/system.py index a7607bd9b..32b429429 100644 --- a/dpdata/system.py +++ b/dpdata/system.py @@ -108,6 +108,7 @@ def __init__( step: int = 1, data: dict[str, Any] | None = None, convergence_check: bool = True, + rot_lower_triangular: bool = True, **kwargs, ): """Constructor. @@ -180,6 +181,9 @@ def __init__( The raw data of System class. convergence_check : boolean Whether to request a convergence check. + rot_lower_triangular : boolean + Whether to apply the registered ``rot_lower_triangular`` post + function after loading. The default is ``True``. **kwargs : dict other parameters """ @@ -204,6 +208,7 @@ def __init__( begin=begin, step=step, convergence_check=convergence_check, + rot_lower_triangular=rot_lower_triangular, **kwargs, ) @@ -236,7 +241,29 @@ def from_fmt(self, file_name: Any, fmt: str = "auto", **kwargs: Any): fmt = os.path.basename(file_name).split(".")[-1].lower() return self.from_fmt_obj(load_format(fmt), file_name, **kwargs) + def _get_post_func_controls( + self, from_method: Any, kwargs: dict[str, Any] + ) -> dict[str, Any]: + """Remove registered post-function controls from format arguments.""" + controls = {} + for post_f in getattr(from_method, "post_func", ()): + if post_f in kwargs: + controls[post_f] = kwargs.pop(post_f) + return controls + + def _apply_post_funcs(self, from_method: Any, controls: dict[str, Any]): + """Run registered post functions unless explicitly disabled. + + A post function can be disabled by passing a false value under its + registered name. This keeps format-specific post-processing opt-out + while retaining the existing default behavior. + """ + for post_f in getattr(from_method, "post_func", ()): + if controls.get(post_f, True): + self.post_funcs.get_plugin(post_f)(self) + def from_fmt_obj(self, fmtobj: Format, file_name: Any, **kwargs: Any): + post_func_controls = self._get_post_func_controls(fmtobj.from_system, kwargs) data = fmtobj.from_system(file_name, **kwargs) if data: if isinstance(data, (list, tuple)): @@ -245,9 +272,7 @@ def from_fmt_obj(self, fmtobj: Format, file_name: Any, **kwargs: Any): else: self.data = {**self.data, **data} self.check_data() - if hasattr(fmtobj.from_system, "post_func"): - for post_f in fmtobj.from_system.post_func: # type: ignore - self.post_funcs.get_plugin(post_f)(self) + self._apply_post_funcs(fmtobj.from_system, post_func_controls) return self def to(self, fmt: str, *args: Any, **kwargs: Any) -> System: @@ -1257,6 +1282,9 @@ class LabeledSystem(System): post_funcs = Plugin() + System.post_funcs def from_fmt_obj(self, fmtobj, file_name, **kwargs): + post_func_controls = self._get_post_func_controls( + fmtobj.from_labeled_system, kwargs + ) data = fmtobj.from_labeled_system(file_name, **kwargs) if data: if isinstance(data, (list, tuple)): @@ -1265,9 +1293,7 @@ def from_fmt_obj(self, fmtobj, file_name, **kwargs): else: self.data = {**self.data, **data} self.check_data() - if hasattr(fmtobj.from_labeled_system, "post_func"): - for post_f in fmtobj.from_labeled_system.post_func: # type: ignore - self.post_funcs.get_plugin(post_f)(self) + self._apply_post_funcs(fmtobj.from_labeled_system, post_func_controls) return self def to_fmt_obj(self, fmtobj, *args, **kwargs): diff --git a/tests/test_vasp_poscar_to_system.py b/tests/test_vasp_poscar_to_system.py index 51c541030..df752c6ad 100644 --- a/tests/test_vasp_poscar_to_system.py +++ b/tests/test_vasp_poscar_to_system.py @@ -21,6 +21,21 @@ def test_move_flags(self): self.assertTrue(np.array_equal(self.system["move"], expected)) +class TestPOSCARPostFunctionControl(unittest.TestCase): + def test_disable_rot_lower_triangular(self): + system = dpdata.System( + os.path.join("poscars", "POSCAR.oh.c"), + fmt="vasp/poscar", + rot_lower_triangular=False, + ) + expected_cell = np.array( + [[0.0, 1.785, 1.785], [1.6065, 0.0, 1.785], [1.9635, 1.8207, 0.0]] + ) + expected_coords = np.array([[0.0, 0.0, 0.0], [0.8925, 0.8925, 0.8925]]) + np.testing.assert_allclose(system["cells"][0], expected_cell) + np.testing.assert_allclose(system["coords"][0], expected_coords) + + class TestPOSCARMoveFlags(unittest.TestCase): def setUp(self): self.tmp_file = "POSCAR.tmp.1" From fc2416ba37f6be243b944106798004ed81a2a3f9 Mon Sep 17 00:00:00 2001 From: SchrodingersCattt Date: Fri, 9 Oct 2026 14:06:42 +0000 Subject: [PATCH 2/3] docs(system): clarify cell orientation option --- dpdata/system.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dpdata/system.py b/dpdata/system.py index 32b429429..d463f13a5 100644 --- a/dpdata/system.py +++ b/dpdata/system.py @@ -74,7 +74,10 @@ class System: Restrictions: - `d_example['orig']` is always [0, 0, 0] - - `d_example['cells'][ii]` is always lower triangular (lammps cell tensor convention) + - `d_example['cells'][ii]` is lower triangular (the LAMMPS cell tensor + convention) when `rot_lower_triangular` is enabled. Set + `rot_lower_triangular=False` while loading to preserve the source + cell orientation. Attributes ---------- From 22d521a65e41a18e11c1308e04be284238f47bee Mon Sep 17 00:00:00 2001 From: SchrodingersCattt Date: Fri, 9 Oct 2026 14:46:11 +0000 Subject: [PATCH 3/3] fix(system): avoid passing default post controls --- dpdata/system.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/dpdata/system.py b/dpdata/system.py index d463f13a5..f77677efc 100644 --- a/dpdata/system.py +++ b/dpdata/system.py @@ -204,16 +204,19 @@ def __init__( return if file_name is None: return - self.from_fmt( - file_name, - fmt, - type_map=type_map, - begin=begin, - step=step, - convergence_check=convergence_check, - rot_lower_triangular=rot_lower_triangular, + load_kwargs = { + "type_map": type_map, + "begin": begin, + "step": step, + "convergence_check": convergence_check, **kwargs, - ) + } + # The default keeps the historical post-processing behavior. Only + # pass an explicit opt-out to format readers; this avoids adding a + # control-only keyword to third-party readers that do not accept it. + if not rot_lower_triangular: + load_kwargs["rot_lower_triangular"] = False + self.from_fmt(file_name, fmt, **load_kwargs) if type_map is not None: self.apply_type_map(type_map)