diff --git a/skills/dpgen-run/SKILL.md b/skills/dpgen-run/SKILL.md new file mode 100644 index 000000000..29723930a --- /dev/null +++ b/skills/dpgen-run/SKILL.md @@ -0,0 +1,40 @@ +--- +name: dpgen-run +description: Prepare, explain, validate, and run DP-GEN concurrent-learning workflows using param.json and machine.json. +license: LGPL-3.0-or-later +metadata: + author: MatMaster + version: 0.3.0 + repository: https://github.com/deepmodeling/dpgen +--- + +# DP-GEN Run + +Use this skill for the iterative train, explore, label, and retrain workflow. + +## Contract + +- Keep scientific settings in `param.json` and execution settings in `machine.json`. +- Run exactly `dpgen run param.json machine.json`. +- Prepare and validate by default; execute only after explicit confirmation. + +## Workflow + +1. Inspect existing data and configurations before asking for inputs. +1. Patch working files and preserve scientific/site-specific choices. +1. Load only the references needed for the task. +1. Validate schemas, generated task inputs, stage state, and monitoring signals. +1. Report paths, command, per-system evidence, risks, and next checks. + +## References + +- [Workflow guidance](references/workflow.md): scope, layout, policy, and reporting. +- [Configuration, validation, and execution](references/validation-and-run.md): both JSON files, generated-input checks, and safe launch/restart checks. +- [Monitoring and troubleshooting](references/monitoring.md): accuracy trends, stage progress, and evidence-based diagnosis. + +## Guardrails + +- Never merge the JSON files or invent paths, queues, modules, or commands. +- Preserve descriptor, backend, thresholds, schedules, ensemble, and `type_map` choices. +- Check `se_atten_v2` against the installed backend; `fp_style: "none"` is invalid. +- Stop and ask when required scientific or execution inputs are undiscoverable. diff --git a/skills/dpgen-run/references/monitoring.md b/skills/dpgen-run/references/monitoring.md new file mode 100644 index 000000000..2ebb2a630 --- /dev/null +++ b/skills/dpgen-run/references/monitoring.md @@ -0,0 +1,54 @@ +# DP-GEN Monitoring and Troubleshooting + +Load this reference after each iteration or whenever accuracy or stage progress +stalls. Here "accuracy" means energy/force train and validation RMSE; model +deviation and FP failure ratio are separate signals. + +## Evidence loop + +For each iteration, record the parameter/configuration identity, systems and +temperature/pressure conditions, train/validation RMSE, model-deviation +accurate/candidate/failed counts, FP success/failure counts, current stage, and +whether new labels reached the next training input. Report per-system and +per-condition results before any global aggregate. + +## Diagnose in this order + +| Symptom | First checks | Focused adjustment | +| ---------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| Train and validation RMSE both stay high | Units, labels, type map, coverage, descriptor capacity | Fix data/schema first; then review fitting net, learning rate, and steps | +| Train RMSE falls but validation RMSE stalls or rises | Split leakage, duplicates, outliers, validation coverage | Re-split or deduplicate; add representative labels before changing capacity | +| RMSE is good but model deviation is high | Unseen systems/geometries or conditions, initial structures | Broaden targeted sampling and labeling the candidate window | +| Candidate count is zero | Trust thresholds, MD stability, trajectory frequency, model diversity | Check thresholds and sampling settings before changing training | +| Candidate count is excessive or FP failures rise | Thresholds, unstable MD, malformed structures, FP convergence | Repair inputs/sampling; do not add failed labels | +| Stage does not advance or task count is wrong | record state, schedule length, sys_idx, generated input, submission metadata | Reconcile state and actual tasks; do not restart blindly | +| Effective steps differ from requested steps | Reuse/iteration override in generated input | Compare root and task-level values; fix the override deliberately | + +## Data and label checks + +Check units and signs, atom ordering, type maps, duplicates, abnormal +magnitudes, and train/validation coverage. Every training frame must have +finite, parseable energy and force; include virial/stress only when configured +and verified. Reject incomplete, duplicated, or unconverged labels before +merging them into training data. + +## Training checks + +Compare train and validation curves, loss components, learning-rate decay, step +budget, batch size, model capacity, ensemble seeds, and backend/version +compatibility. A good global metric must not hide a failing system or condition. +Change one major factor at a time and retain the previous evidence. + +## Sampling and decision checks + +Inspect MD stability, temperatures, pressures, ensembles, `nsteps`, `trj_freq`, +trust-window placement, and per-condition distributions. Distinguish a model +problem from missing coverage: good RMSE with high deviation usually calls for +targeted sampling, while high RMSE on covered data calls for data or training +review. + +Monitoring is an evidence-based decision stage, not only status reporting. +Before extending sampling, changing thresholds, increasing steps, or restarting, +record the evidence, exact parameter delta, expected effect, and controller +state. Afterward record the new task identity and outcome; never retune or +restart silently. diff --git a/skills/dpgen-run/references/validation-and-run.md b/skills/dpgen-run/references/validation-and-run.md new file mode 100644 index 000000000..7b3afe4d7 --- /dev/null +++ b/skills/dpgen-run/references/validation-and-run.md @@ -0,0 +1,100 @@ +# DP-GEN Configuration, Validation, and Execution + +Load this reference when creating, reviewing, validating, launching, or +recovering a `dpgen run` configuration. + +## 1. `param.json` essentials + +Preserve or collect: + +- `type_map` (ordered symbols), optional `mass_map`, `init_data_prefix`, and `init_data_sys`; +- `sys_configs_prefix` and `sys_configs` (a list of systems, each a list of structures); +- `numb_models` and complete `default_training_param.model`, `learning_rate`, `loss`, and `training`; +- `model_devi_dt`, `model_devi_skip`, trust thresholds, cleanup flag, and `model_devi_jobs`; +- `fp_style`, `fp_task_min`, `fp_task_max`, and backend-specific FP inputs. + +DeepMD-kit 1.x uses `training.systems`; 2.x/3.x use +`training.training_data.systems`. Leave the version-appropriate value empty so +DP-GEN can fill it from `init_data_sys`. Verify `deepmd_version` and the +installed stack before using version-specific descriptors or features. + +`train_backend` accepts `tensorflow` and `pytorch`, but compatibility still +depends on the installed DeePMD-kit and descriptor. Verify descriptor spelling +against that backend; current releases register `se_atten_v2`. +Ordinary LAMMPS exploration uses the default +`model_devi_engine`; set an alternative only when verified. Current `dpgen run` +does not accept `fp_style: "none"`. + +Each `init_data_sys` entry may be a DeepMD NumPy system directory, a parent +directory containing multiple systems, or an HDF5 file. Resolve entries with +DP-GEN's `init_data_prefix` and discovery rules, then validate each NumPy leaf: +it needs `type.raw` and one loadable `set.*` directory. `type_map.raw` is +optional; when present, its symbols must be included in `param.json.type_map`. +Subsets and different orders are valid because DeePMD remaps by element name; +without it, `type.raw` must already use the configured model order. + +## 2. `machine.json` essentials + +Keep separate `train`, `model_devi`, and `fp` blocks. Each block supplies a +`command`, `machine` (`batch_type`, `context_type`, `local_root`, and any +required `remote_root`/profile), and `resources` (`number_node`, CPU/GPU counts, +`group_size`, queue/partition flags, and scheduler `source_list`). See the +[repository machine schema walkthrough](../../../doc/run/example-of-machine.md) +for generic local, scheduler, and SSH shapes. + +The launcher shell and dispatched tasks are different environments. Scheduler +jobs must activate their own runtime through `source_list`; outer activation is +not inherited reliably. Keep the training command as `dp`; DP-GEN appends +backend flags such as `--pt` or `--jax` from `train_backend`. Validate +commands, context types, batch aliases, roots, and resources against installed +DPDispatcher rather than normalizing by assumption. + +## 3. Validate generated inputs before launch + +```bash +dpgen -h +python -m json.tool param.json +python -m json.tool machine.json +``` + +Normalize `param.json` with `run_jdata_arginfo()` and convert `machine.json` with +`convert_mdata()`. Resolve all data/structure paths, compare type maps, check +executables and scheduler access, and verify FP inputs and cost limits. + +Before submission, inspect each stage's generated backend artifact. For training, +check task `input.json` for model count, seeds, checkpoints, data systems, and +final steps; for LAMMPS exploration, check `input.lammps` and `job.json` for +`sys_idx`, temperatures/pressures, and effective MD settings; for first-principles +labeling, inspect backend inputs such as VASP `INCAR`. When reuse or iteration +overrides exist, assert both default and effective values. + +Cross-check the intended iteration against `record.dpgen`, stage directories, +DPDispatcher work base/submission metadata, and logs. A stage is not complete +because a directory or job exists. + +## 4. Restart and recovery + +Treat a restart from `record.dpgen` as cost-bearing. Keep one controller for a +work directory. Before editing state, stop it, back up the state file, record +stage/job IDs and old/new settings, and query the scheduler. + +A changed active-stage command, context, remote root, queue, resources, +`group_size`, flags, activation, task list, or common files can create a new +DPDispatcher submission identity. It will not import completion state from the +old identity. Distinguish recovery of the old submission from an intentional +rerun with a new remote root; never resubmit solely because a connection timed +out. + +Positive recovery evidence is the old submission hash, unchanged job IDs, and a +recovery message; absence of a new-submission message is required. +`record.dpgen` is a workflow pointer, not a scheduler completion record. +Apply later-iteration machine changes only after the current cost-bearing stage +advances. + +## 5. Confirm and inspect + +Show the exact files, validation summary, unresolved risks, and +`dpgen run param.json machine.json`. Execute only after explicit confirmation. +After launch, inspect `iter.*`, stage logs, failure/pending states, selected and +labeled counts, and [monitoring outputs](monitoring.md). +External references: https://docs.deepmodeling.com/projects/dpgen/en/latest/run/index.html diff --git a/skills/dpgen-run/references/workflow.md b/skills/dpgen-run/references/workflow.md new file mode 100644 index 000000000..d0aab464a --- /dev/null +++ b/skills/dpgen-run/references/workflow.md @@ -0,0 +1,73 @@ +# DP-GEN Run Workflow Guidance + +Load this reference for scope, preparation policy, project layout, or reporting. + +## Loop + +`dpgen run` repeats five activities: + +1. train an ensemble of Deep Potential models; +1. explore configurations, normally with LAMMPS MD; +1. select structures in the model-deviation candidate window; +1. label selected structures with the configured first-principles backend; +1. add labels to the next training iteration. + +Use `param.json` for scientific/workflow settings, `machine.json` for commands, +contexts, and resources, and `dpgen run param.json machine.json` as the launcher. +Preparation needs Python and DP-GEN. Execution also needs DeePMD-kit, the +exploration engine, the selected FP software, and any scheduler runtime. + +## Monitoring stage + +After each iteration, record RMSE, model-deviation, and FP outcomes by system +and condition. Use [monitoring guidance](monitoring.md) before changing settings +or restarting a stalled iteration. + +## Preparation policy + +Inspect existing configuration files, training inputs, dataset metadata, and +machine templates first. Ask only for values that cannot be discovered. Patch +working files instead of rebuilding them. + +Preserve descriptor and fitting settings, training and FP backends, thresholds, +`type_map` ordering, `model_devi_jobs`, ensemble size, temperatures, pressures, +and MD ensembles. Explain concerns and request direction before changing them. + +Reuse known activation commands, executables, queues, partitions, accounts, +paths, and scheduler flags exactly. Never guess site policy or conda/module setup. + +## Generic layout + +```text +project/ +|-- param.json +|-- machine.json +|-- init_data/ +| `-- raw_xxx/ # DeepMD NumPy system(s) +|-- inputs/ # optional structures and backend inputs +`-- iter.*/ # created by DP-GEN +``` + +Keep repeated experiments in separate, clearly named directories derived from a +reviewed base configuration. Backend-specific files belong under `inputs/` or +the path explicitly required by the selected backend. + +## Reporting contract + +Before execution, report absolute JSON paths, the exact command, validation +results, unresolved inputs, and cost-bearing stages. Execute only after the user +confirms that exact validated command. + +After execution, report the current iteration and stage status, failed/pending +tasks, main logs and outputs, candidate and labeled counts, accuracy trends, and +the next files to inspect. Use [monitoring guidance](monitoring.md) when an +iteration stalls or accuracy fails to improve. + +## Guardrails + +- Never run before both JSON files exist and pass validation. +- Keep `type_map` consistent through data, training, and labeling. +- Do not overwrite working templates blindly or assume outer activation reaches jobs. +- Stop rather than guess a missing scientific or site-specific value. + +Official overview: https://docs.deepmodeling.com/projects/dpgen/en/latest/run/index.html diff --git a/tests/test_dpgen_run_skill.py b/tests/test_dpgen_run_skill.py new file mode 100644 index 000000000..d1dc9df55 --- /dev/null +++ b/tests/test_dpgen_run_skill.py @@ -0,0 +1,298 @@ +import copy +import json +import re +import unittest +from pathlib import Path + +from dargs.dargs import ( + ArgumentKeyError, + ArgumentTypeError, + ArgumentValueError, +) + +from dpgen.generator.arginfo import run_jdata_arginfo +from dpgen.remote.decide_machine import convert_mdata +from dpgen.util import normalize + +LINK_PATTERN = re.compile(r"\[[^]]+\]\(([^)]+)\)") +SKILL_RELATIVE_PATH = Path("skills") / "dpgen-run" / "SKILL.md" + + +def skill_markdown_files(repository_root): + skill_root = repository_root / SKILL_RELATIVE_PATH.parent + return sorted(skill_root.rglob("*.md")) + + +def linked_json_examples(repository_root, examples_root): + examples = set() + for markdown_path in skill_markdown_files(repository_root): + for target in LINK_PATTERN.findall(markdown_path.read_text()): + target_path = target.split("#", 1)[0] + if not target_path.endswith(".json"): + continue + resolved_target = (markdown_path.parent / target_path).resolve() + try: + resolved_target.relative_to(examples_root) + except ValueError: + continue + examples.add(resolved_target) + return examples + + +class TestDPGenRunSkill(unittest.TestCase): + def test_main_skill_is_small_progressive_router(self): + repository_root = Path(__file__).resolve().parents[1] + skill_path = repository_root / SKILL_RELATIVE_PATH + skill_text = skill_path.read_text() + + self.assertLessEqual(len(skill_text.splitlines()), 40) + reference_targets = { + target + for target in LINK_PATTERN.findall(skill_text) + if target.startswith("references/") + } + self.assertEqual(len(reference_targets), 3) + + def test_reference_files_are_small_and_monitoring_is_actionable(self): + repository_root = Path(__file__).resolve().parents[1] + for markdown_path in skill_markdown_files(repository_root): + with self.subTest(markdown_path=markdown_path): + self.assertLessEqual(len(markdown_path.read_text().splitlines()), 100) + + monitoring = ( + (repository_root / "skills" / "dpgen-run" / "references" / "monitoring.md") + .read_text() + .lower() + ) + for term in ( + "data", + "training", + "sampling", + "labeling", + "rmse", + "model deviation", + "labeling", + "fp failure", + "system", + "condition", + "override", + ): + self.assertIn(term, monitoring) + + validation = ( + ( + repository_root + / "skills" + / "dpgen-run" + / "references" + / "validation-and-run.md" + ) + .read_text() + .lower() + ) + for term in ( + "record.dpgen", + "input.json", + "submission identity", + "sys_idx", + "generated", + ): + self.assertIn(term, validation) + + references = repository_root / "skills" / "dpgen-run" / "references" + self.assertFalse((references / "param-json.md").exists()) + self.assertFalse((references / "machine-json.md").exists()) + + def test_skill_docs_are_project_agnostic(self): + repository_root = Path(__file__).resolve().parents[1] + documents = [ + path.read_text() + for path in skill_markdown_files(repository_root) + if path.name != "test_dpgen_run_skill.py" + ] + text = "\n".join(documents) + self.assertNotRegex(text, r"(?m)(?:^|\s)/(?:[A-Za-z0-9_.-]+/)+") + self.assertNotRegex(text, r"\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b") + + def test_all_repository_relative_links_exist(self): + repository_root = Path(__file__).resolve().parents[1] + missing = [] + for markdown_path in skill_markdown_files(repository_root): + for target in LINK_PATTERN.findall(markdown_path.read_text()): + if "://" in target or target.startswith("#"): + continue + resolved_target = ( + markdown_path.parent / target.split("#", 1)[0] + ).resolve() + try: + resolved_target.relative_to(repository_root) + except ValueError: + missing.append(f"{markdown_path.name}: {target}") + else: + if not resolved_target.exists(): + missing.append(f"{markdown_path.name}: {target}") + + self.assertEqual( + missing, + [], + "Missing repository-relative links in dpgen-run skill: " + + ", ".join(missing), + ) + + def test_parameter_examples_use_current_schema(self): + repository_root = Path(__file__).resolve().parents[1] + examples = [ + repository_root + / "examples" + / "run" + / "dp2.x-lammps-cp2k" + / "param_CH4_deepmd-kit-2.0.1.json", + repository_root + / "examples" + / "run" + / "dp2.x-lammps-vasp" + / "CH4" + / "param_CH4_deepmd-kit-2.x.json", + ] + + for example in examples: + with self.subTest(example=example): + parameter_data = json.loads(example.read_text()) + normalized = normalize( + run_jdata_arginfo(), + parameter_data, + strict_check=False, + ) + self.assertIsInstance(normalized, dict) + self.assertTrue(normalized) + + def test_documented_parameter_schema_invariants(self): + repository_root = Path(__file__).resolve().parents[1] + example = ( + repository_root + / "examples" + / "run" + / "dp2.x-lammps-cp2k" + / "param_CH4_deepmd-kit-2.0.1.json" + ) + parameter_data = json.loads(example.read_text()) + normalized = normalize( + run_jdata_arginfo(), + copy.deepcopy(parameter_data), + strict_check=False, + ) + + self.assertEqual(normalized["model_devi_engine"], "lammps") + self.assertEqual(normalized["train_backend"], "tensorflow") + + without_mass_map = copy.deepcopy(parameter_data) + without_mass_map.pop("mass_map", None) + normalized_without_mass_map = normalize( + run_jdata_arginfo(), + without_mass_map, + strict_check=False, + ) + self.assertEqual(normalized_without_mass_map["mass_map"], "auto") + + invalid_fp_style = copy.deepcopy(parameter_data) + invalid_fp_style["fp_style"] = "none" + with self.assertRaises(ArgumentValueError): + normalize( + run_jdata_arginfo(), + invalid_fp_style, + strict_check=False, + ) + + missing_model_devi_skip = copy.deepcopy(parameter_data) + missing_model_devi_skip.pop("model_devi_skip") + with self.assertRaises(ArgumentKeyError): + normalize( + run_jdata_arginfo(), + missing_model_devi_skip, + strict_check=False, + ) + + flat_sys_configs = copy.deepcopy(parameter_data) + flat_sys_configs["sys_configs"] = ["POSCAR"] + with self.assertRaises(ArgumentTypeError): + normalize( + run_jdata_arginfo(), + flat_sys_configs, + strict_check=False, + ) + + for backend in ("tensorflow", "pytorch"): + with self.subTest(backend=backend): + backend_data = copy.deepcopy(parameter_data) + backend_data["train_backend"] = backend + normalized_backend = normalize( + run_jdata_arginfo(), + backend_data, + strict_check=False, + ) + self.assertEqual(normalized_backend["train_backend"], backend) + + def test_current_machine_example_uses_current_schema(self): + repository_root = Path(__file__).resolve().parents[1] + examples = [ + repository_root + / "examples" + / "run" + / "dp2.x-lammps-gaussian" + / "machine.json", + ] + + for example in examples: + with self.subTest(example=example): + machine_data = json.loads(example.read_text()) + stage_values = {} + for stage in ("train", "model_devi", "fp"): + self.assertIn(stage, machine_data) + stage_data = machine_data[stage] + if isinstance(stage_data, list): + self.assertTrue(stage_data) + stage_data = stage_data[0] + self.assertIsInstance(stage_data, dict) + stage_values[stage] = stage_data + + machine = stage_data.get("machine") + resources = stage_data.get("resources") + command = stage_data.get("command") + self.assertIsInstance(machine, dict) + self.assertTrue(machine) + self.assertIsInstance(resources, dict) + self.assertTrue(resources) + self.assertIsInstance(command, str) + self.assertTrue(command.strip()) + for field in ("batch_type", "context_type", "local_root"): + self.assertIn(field, machine) + self.assertTrue(machine[field]) + for field in ( + "number_node", + "cpu_per_node", + "gpu_per_node", + "group_size", + ): + self.assertIn(field, resources) + + converted = convert_mdata(machine_data) + for stage in ("train", "model_devi", "fp"): + self.assertIn(f"{stage}_machine", converted) + self.assertIn(f"{stage}_resources", converted) + self.assertIn(f"{stage}_command", converted) + self.assertEqual( + converted[f"{stage}_machine"], + stage_values[stage]["machine"], + ) + self.assertEqual( + converted[f"{stage}_resources"], + stage_values[stage]["resources"], + ) + self.assertEqual( + converted[f"{stage}_command"], + stage_values[stage]["command"], + ) + + +if __name__ == "__main__": + unittest.main()