diff --git a/dpgen/generator/arginfo.py b/dpgen/generator/arginfo.py index 0f8abb865..efc4fc367 100644 --- a/dpgen/generator/arginfo.py +++ b/dpgen/generator/arginfo.py @@ -621,16 +621,299 @@ def model_devi_amber_args() -> list[Argument]: ] +def _is_scalar_or_singleton(value) -> bool: + """Accept legacy one-item lists while rejecting ambiguous CALYPSO values.""" + return not isinstance(value, list) or len(value) == 1 + + +def model_devi_calypso_args() -> list[Argument]: + """CALYPSO engine arguments.""" + doc_model_devi_jobs = ( + "Settings for CALYPSO structure generation and model deviation. " + "Each dict in the list describes the CALYPSO input used for one or more " + "iterations selected by `times`." + ) + doc_times = "List of iteration indices when this CALYPSO job should be executed." + doc_nameofatoms = "Element symbols of the chemical species." + doc_numberofatoms = "Number of atoms for each chemical species in one formula unit." + doc_numberofformula = "Range of formula units per cell as [min, max]." + doc_volume = "Volume per formula unit in angstrom^3. If not provided, CALYPSO determines it automatically." + doc_distanceofion = "Minimal distances between atom types in angstrom. Shape should match the number of species." + doc_psoratio = ( + "Proportion of structures generated by the PSO algorithm, between 0.0 and 1.0." + ) + doc_popsize = "Population size for structure generation." + doc_maxstep = "Maximum number of CALYPSO optimization steps." + doc_icode = "CALYPSO interface code for local optimization, such as 1 for VASP." + doc_split = "Whether to split calculations. Use 'T' or 'F'." + doc_vsc = "Variable stoichiometry control. Use 'T' to enable or 'F' to disable." + doc_maxnumatom = ( + "Maximum number of atoms in the unit cell. Required when VSC is 'T'." + ) + doc_ctrlrange = "Variation range for each atom type. Required when VSC is 'T'." + doc_pstress = "Target pressure list in GPa. One CALYPSO input directory is created for each pressure." + doc_fmax = "Force convergence criterion for local optimization, in eV/angstrom." + doc_calypso_input_path = ( + "Path to a directory containing pre-existing CALYPSO input.dat files. " + "When set, DP-GEN copies those files instead of generating input.dat from " + "the CALYPSO fields in model_devi_jobs." + ) + doc_model_devi_max_iter = "Maximum iteration index when using calypso_input_path." + doc_vsc_mode = ( + "Enable variable stoichiometry mode when using external CALYPSO input files." + ) + doc_model_devi_dt = ( + "Timestep retained for compatibility with existing CALYPSO parameter files." + ) + doc_model_devi_skip = ( + "Number of structures skipped during model deviation selection." + ) + doc_model_devi_f_trust_lo = "Lower bound of force model deviation for selection." + doc_model_devi_f_trust_hi = "Upper bound of force model deviation for selection." + doc_model_devi_v_trust_lo = "Lower bound of virial model deviation for selection." + doc_model_devi_v_trust_hi = "Upper bound of virial model deviation for selection." + doc_model_devi_e_trust_lo = "Lower bound of energy model deviation for selection." + doc_model_devi_e_trust_hi = "Upper bound of energy model deviation for selection." + doc_model_devi_clean_traj = ( + "Whether to clean large trajectory folders after model deviation." + ) + doc_model_devi_adapt_trust_lo = ( + "Adaptively determine the lower force and virial trust levels." + ) + doc_model_devi_numb_candi_f = "See model_devi_adapt_trust_lo." + doc_model_devi_numb_candi_v = "See model_devi_adapt_trust_lo." + doc_model_devi_perc_candi_f = "See model_devi_adapt_trust_lo." + doc_model_devi_perc_candi_v = "See model_devi_adapt_trust_lo." + doc_model_devi_f_avg_relative = ( + "Normalize force model deviations by the RMS force magnitude." + ) + doc_use_relative = "Calculate relative force model deviation." + doc_epsilon = "Level parameter for computing the relative force model deviation." + doc_use_relative_v = "Calculate relative virial model deviation." + doc_epsilon_v = "Level parameter for computing the relative virial model deviation." + + return [ + Argument( + "model_devi_jobs", + list, + optional=False, + repeat=True, + doc=doc_model_devi_jobs, + sub_fields=[ + Argument("times", list[int], optional=False, doc=doc_times), + Argument("NameOfAtoms", list[str], optional=False, doc=doc_nameofatoms), + Argument( + "NumberOfAtoms", list[int], optional=False, doc=doc_numberofatoms + ), + Argument( + "NumberOfFormula", + list[int], + optional=True, + default=[1, 1], + doc=doc_numberofformula, + ), + Argument( + "Volume", + [float, int, list[float], list[int]], + optional=True, + extra_check=_is_scalar_or_singleton, + extra_check_errmsg="Volume must be a scalar or a one-item list.", + doc=doc_volume, + ), + Argument( + "DistanceOfIon", + list[list[float]], + optional=False, + doc=doc_distanceofion, + ), + Argument( + "PsoRatio", + [float, int, list[float], list[int]], + optional=True, + default=0.6, + extra_check=_is_scalar_or_singleton, + extra_check_errmsg="PsoRatio must be a scalar or a one-item list.", + doc=doc_psoratio, + ), + Argument( + "PopSize", + [int, list[int]], + optional=True, + default=30, + extra_check=_is_scalar_or_singleton, + extra_check_errmsg="PopSize must be an integer or a one-item list.", + doc=doc_popsize, + ), + Argument( + "MaxStep", + [int, list[int]], + optional=True, + default=5, + extra_check=_is_scalar_or_singleton, + extra_check_errmsg="MaxStep must be an integer or a one-item list.", + doc=doc_maxstep, + ), + Argument( + "ICode", + [int, list[int]], + optional=True, + default=1, + extra_check=_is_scalar_or_singleton, + extra_check_errmsg="ICode must be an integer or a one-item list.", + doc=doc_icode, + ), + Argument("Split", str, optional=True, default="T", doc=doc_split), + Argument("VSC", str, optional=True, default="F", doc=doc_vsc), + Argument( + "MaxNumAtom", + [int, list[int]], + optional=True, + extra_check=_is_scalar_or_singleton, + extra_check_errmsg="MaxNumAtom must be an integer or a one-item list.", + doc=doc_maxnumatom, + ), + Argument( + "CtrlRange", list[list[int]], optional=True, doc=doc_ctrlrange + ), + Argument( + "PSTRESS", + list[float], + optional=True, + default=[0.001], + doc=doc_pstress, + ), + Argument( + "fmax", + [float, int, list[float], list[int]], + optional=True, + default=0.01, + extra_check=_is_scalar_or_singleton, + extra_check_errmsg="fmax must be a scalar or a one-item list.", + doc=doc_fmax, + ), + ], + ), + Argument("calypso_input_path", str, optional=True, doc=doc_calypso_input_path), + Argument( + "model_devi_max_iter", int, optional=True, doc=doc_model_devi_max_iter + ), + Argument("vsc", bool, optional=True, default=False, doc=doc_vsc_mode), + Argument("model_devi_dt", float, optional=True, doc=doc_model_devi_dt), + Argument( + "shuffle_poscar", + bool, + optional=True, + default=False, + doc="Shuffle atoms in generated configurations before downstream use.", + ), + Argument("model_devi_skip", int, optional=False, doc=doc_model_devi_skip), + Argument( + "model_devi_f_trust_lo", + [float, list[float], dict], + optional=False, + doc=doc_model_devi_f_trust_lo, + ), + Argument( + "model_devi_f_trust_hi", + [float, list[float], dict], + optional=False, + doc=doc_model_devi_f_trust_hi, + ), + Argument( + "model_devi_v_trust_lo", + [float, list[float], dict], + optional=True, + default=1e10, + doc=doc_model_devi_v_trust_lo, + ), + Argument( + "model_devi_v_trust_hi", + [float, list[float], dict], + optional=True, + default=1e10, + doc=doc_model_devi_v_trust_hi, + ), + Argument( + "model_devi_e_trust_lo", + [float, list[float], dict], + optional=True, + default=1e10, + doc=doc_model_devi_e_trust_lo, + ), + Argument( + "model_devi_e_trust_hi", + [float, list[float], dict], + optional=True, + default=1e10, + doc=doc_model_devi_e_trust_hi, + ), + Argument( + "model_devi_adapt_trust_lo", + bool, + optional=True, + doc=doc_model_devi_adapt_trust_lo, + ), + Argument( + "model_devi_numb_candi_f", + int, + optional=True, + doc=doc_model_devi_numb_candi_f, + ), + Argument( + "model_devi_numb_candi_v", + int, + optional=True, + doc=doc_model_devi_numb_candi_v, + ), + Argument( + "model_devi_perc_candi_f", + float, + optional=True, + doc=doc_model_devi_perc_candi_f, + ), + Argument( + "model_devi_perc_candi_v", + float, + optional=True, + doc=doc_model_devi_perc_candi_v, + ), + Argument( + "model_devi_f_avg_relative", + bool, + optional=True, + doc=doc_model_devi_f_avg_relative, + ), + Argument( + "model_devi_clean_traj", + [bool, int], + optional=True, + default=True, + doc=doc_model_devi_clean_traj, + ), + Argument( + "use_relative", bool, optional=True, default=False, doc=doc_use_relative + ), + Argument("epsilon", float, optional=True, doc=doc_epsilon), + Argument( + "use_relative_v", bool, optional=True, default=False, doc=doc_use_relative_v + ), + Argument("epsilon_v", float, optional=True, doc=doc_epsilon_v), + ] + + def model_devi_args() -> list[Variant]: doc_model_devi_engine = "Engine for the model deviation task." doc_amber = "Amber DPRc engine. The command argument in the machine file should be path to sander." + doc_calypso = ( + "CALYPSO structure generation engine for crystal structure prediction." + ) return [ Variant( "model_devi_engine", [ Argument("lammps", dict, model_devi_lmp_args(), doc="LAMMPS"), Argument("amber", dict, model_devi_amber_args(), doc=doc_amber), - Argument("calypso", dict, [], doc="TODO: add doc"), + Argument("calypso", dict, model_devi_calypso_args(), doc=doc_calypso), Argument("gromacs", dict, [], doc="TODO: add doc"), ], default_tag="lammps", diff --git a/dpgen/generator/lib/make_calypso.py b/dpgen/generator/lib/make_calypso.py index 42d5e7f69..b315c6057 100644 --- a/dpgen/generator/lib/make_calypso.py +++ b/dpgen/generator/lib/make_calypso.py @@ -162,6 +162,15 @@ def _make_model_devi_buffet(jdata, calypso_run_opt_path): raise FileNotFoundError("input.dat") +def _unwrap_calypso_scalar(value, name): + """Normalize a legacy one-item CALYPSO list to its scalar value.""" + if not isinstance(value, list): + return value + if len(value) != 1: + raise ValueError(f"{name} must be a scalar or a one-item list") + return value[0] + + def _make_model_devi_native_calypso(iter_index, model_devi_jobs, calypso_run_opt_path): for iiidx, jobbs in enumerate(model_devi_jobs): if iter_index in jobbs.get("times"): @@ -176,12 +185,12 @@ def _make_model_devi_native_calypso(iter_index, model_devi_jobs, calypso_run_opt nameofatoms = cur_job.get("NameOfAtoms") numberofatoms = cur_job.get("NumberOfAtoms") numberofformula = cur_job.get("NumberOfFormula", [1, 1]) - volume = cur_job.get("Volume") + volume = _unwrap_calypso_scalar(cur_job.get("Volume"), "Volume") distanceofion = cur_job.get("DistanceOfIon") - psoratio = cur_job.get("PsoRatio", 0.6) - popsize = cur_job.get("PopSize", 30) - maxstep = cur_job.get("MaxStep", 5) - icode = cur_job.get("ICode", 1) + psoratio = _unwrap_calypso_scalar(cur_job.get("PsoRatio", 0.6), "PsoRatio") + popsize = _unwrap_calypso_scalar(cur_job.get("PopSize", 30), "PopSize") + maxstep = _unwrap_calypso_scalar(cur_job.get("MaxStep", 5), "MaxStep") + icode = _unwrap_calypso_scalar(cur_job.get("ICode", 1), "ICode") split = cur_job.get("Split", "T") # Cluster @@ -192,10 +201,10 @@ def _make_model_devi_native_calypso(iter_index, model_devi_jobs, calypso_run_opt ctrlrange = None vsc = cur_job.get("VSC", "F") if vsc == "T": - maxnumatom = cur_job.get("MaxNumAtom") + maxnumatom = _unwrap_calypso_scalar(cur_job.get("MaxNumAtom"), "MaxNumAtom") ctrlrange = cur_job.get("CtrlRange") # Optimization - fmax = cur_job.get("fmax", 0.01) + fmax = _unwrap_calypso_scalar(cur_job.get("fmax", 0.01), "fmax") # pstress is a List which contains the target stress pstress = cur_job.get("PSTRESS", [0.001]) # pressures diff --git a/tests/test_calypso_arginfo.py b/tests/test_calypso_arginfo.py new file mode 100644 index 000000000..d652d873b --- /dev/null +++ b/tests/test_calypso_arginfo.py @@ -0,0 +1,78 @@ +import json +import unittest +from pathlib import Path + +from dargs import Argument + +from dpgen.generator.arginfo import model_devi_args +from dpgen.generator.lib.make_calypso import _unwrap_calypso_scalar + + +class TestCalypsoArginfo(unittest.TestCase): + def test_legacy_singleton_scalars(self): + """The checked-in CALYPSO list spelling remains schema-compatible.""" + arginfo = Argument("model_devi", dict, sub_variants=model_devi_args()) + data = { + "model_devi_engine": "calypso", + "model_devi_skip": 0, + "model_devi_f_trust_lo": 0.05, + "model_devi_f_trust_hi": 0.15, + "model_devi_jobs": [ + { + "times": [0], + "NameOfAtoms": ["Mg"], + "NumberOfAtoms": [1], + "Volume": [30], + "DistanceOfIon": [[1.4]], + "PsoRatio": [0.6], + "PopSize": [30], + "MaxStep": [5], + "ICode": [1], + "VSC": "T", + "MaxNumAtom": [20], + "CtrlRange": [[1, 20]], + "fmax": [0.01], + } + ], + } + + normalized = arginfo.normalize_value(data) + arginfo.check_value(normalized, strict=True) + + def test_runtime_unwraps_singleton_scalars(self): + self.assertEqual(_unwrap_calypso_scalar([30], "Volume"), 30) + self.assertEqual(_unwrap_calypso_scalar(0.6, "PsoRatio"), 0.6) + with self.assertRaisesRegex(ValueError, "one-item list"): + _unwrap_calypso_scalar([1, 2], "PopSize") + + def test_checked_in_example_is_schema_compatible(self): + """The maintained example's CALYPSO section passes strict validation.""" + param_file = ( + Path(__file__).parent.parent + / "examples" + / "run" + / "dp-calypso-vasp" + / "param.json" + ) + with open(param_file) as fp: + example = json.load(fp) + + model_devi_keys = { + "model_devi_engine", + "model_devi_jobs", + "model_devi_dt", + "model_devi_skip", + "model_devi_f_trust_lo", + "model_devi_f_trust_hi", + "model_devi_clean_traj", + "shuffle_poscar", + "vsc", + } + data = {key: example[key] for key in model_devi_keys} + arginfo = Argument("model_devi", dict, sub_variants=model_devi_args()) + normalized = arginfo.normalize_value(data) + arginfo.check_value(normalized, strict=True) + + +if __name__ == "__main__": + unittest.main()