atst-tools 2.3.2__tar.gz → 2.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. {atst_tools-2.3.2/src/atst_tools.egg-info → atst_tools-2.3.3}/PKG-INFO +10 -8
  2. {atst_tools-2.3.2 → atst_tools-2.3.3}/README.md +7 -7
  3. {atst_tools-2.3.2 → atst_tools-2.3.3}/pyproject.toml +3 -2
  4. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/api/services.py +5 -1
  5. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/calculators/abacuslite_backend.py +24 -3
  6. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/calculators/constant_potential.py +38 -13
  7. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/calculators/factory.py +152 -4
  8. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/mep/autoneb.py +36 -5
  9. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/mep/ccqn.py +69 -15
  10. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/scripts/cli_impl.py +3 -4
  11. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/abacus_io.py +14 -8
  12. atst_tools-2.3.3/src/atst_tools/utils/analysis.py +367 -0
  13. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/config_schema.py +97 -16
  14. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/idpp.py +31 -2
  15. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/io.py +10 -1
  16. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/neb_endpoints.py +87 -34
  17. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/reactive_modes.py +109 -1
  18. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/reverse_config.py +4 -0
  19. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/workflows/constant_potential.py +2 -3
  20. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/workflows/d2s.py +23 -5
  21. atst_tools-2.3.3/src/atst_tools/workflows/endpoint_relax.py +62 -0
  22. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/workflows/execution.py +75 -100
  23. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/workflows/md.py +28 -1
  24. {atst_tools-2.3.2 → atst_tools-2.3.3/src/atst_tools.egg-info}/PKG-INFO +10 -8
  25. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools.egg-info/SOURCES.txt +1 -0
  26. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools.egg-info/requires.txt +2 -0
  27. atst_tools-2.3.2/src/atst_tools/utils/analysis.py +0 -57
  28. {atst_tools-2.3.2 → atst_tools-2.3.3}/COPYING +0 -0
  29. {atst_tools-2.3.2 → atst_tools-2.3.3}/LICENSE +0 -0
  30. {atst_tools-2.3.2 → atst_tools-2.3.3}/setup.cfg +0 -0
  31. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/__init__.py +0 -0
  32. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/api/__init__.py +0 -0
  33. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/api/models.py +0 -0
  34. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/api/runner.py +0 -0
  35. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/api/validation.py +0 -0
  36. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/bench/__init__.py +0 -0
  37. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/bench/batch_runner.py +0 -0
  38. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/bench/batch_worker.py +0 -0
  39. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/bench/record.py +0 -0
  40. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/bench/sweep.py +0 -0
  41. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/calculators/__init__.py +0 -0
  42. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/calculators/backend_identity.py +0 -0
  43. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/calculators/base.py +0 -0
  44. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/calculators/dp.py +0 -0
  45. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/ASE_interface/ABACUSLITE_SNAPSHOT.md +0 -0
  46. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/ASE_interface/PATCHES.md +0 -0
  47. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/ASE_interface/__init__.py +0 -0
  48. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/ASE_interface/abacuslite/__init__.py +0 -0
  49. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/ASE_interface/abacuslite/core.py +0 -0
  50. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/ASE_interface/abacuslite/io/__init__.py +0 -0
  51. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/ASE_interface/abacuslite/io/generalio.py +0 -0
  52. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/ASE_interface/abacuslite/io/latestio.py +0 -0
  53. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/ASE_interface/abacuslite/io/legacyio.py +0 -0
  54. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/ASE_interface/abacuslite/utils/__init__.py +0 -0
  55. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/ASE_interface/abacuslite/utils/ksampling.py +0 -0
  56. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/__init__.py +0 -0
  57. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/pydmf/LICENSE +0 -0
  58. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/pydmf/UPSTREAM.md +0 -0
  59. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/pydmf/__init__.py +0 -0
  60. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/pydmf/dmf/__init__.py +0 -0
  61. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/pydmf/dmf/dmf.py +0 -0
  62. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/pydmf/dmf/fbenm.py +0 -0
  63. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/pydmf/dmf/interpolate.py +0 -0
  64. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/pydmf/dmf/torch/__init__.py +0 -0
  65. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/pydmf/dmf/torch/_torch_config.py +0 -0
  66. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/pydmf/dmf/torch/dmf.py +0 -0
  67. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/pydmf/dmf/torch/fbenm.py +0 -0
  68. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/external/pydmf/dmf/torch/interpolate.py +0 -0
  69. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/mep/__init__.py +0 -0
  70. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/mep/dimer.py +0 -0
  71. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/mep/neb.py +0 -0
  72. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/mep/sella.py +0 -0
  73. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/runtime/__init__.py +0 -0
  74. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/runtime/cli_dispatch.py +0 -0
  75. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/runtime/counters.py +0 -0
  76. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/runtime/devices.py +0 -0
  77. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/runtime/errors.py +0 -0
  78. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/runtime/evidence.py +0 -0
  79. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/runtime/launch.py +0 -0
  80. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/scripts/__init__.py +0 -0
  81. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/scripts/cli.py +0 -0
  82. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/scripts/main.py +0 -0
  83. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/__init__.py +0 -0
  84. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/artifacts.py +0 -0
  85. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/banner.py +0 -0
  86. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/config.py +0 -0
  87. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/config_docs.py +0 -0
  88. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/convergence.py +0 -0
  89. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/dmf_validation.py +0 -0
  90. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/electrochemistry.py +0 -0
  91. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/gate_compensation.py +0 -0
  92. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/geometry.py +0 -0
  93. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/md_post.py +0 -0
  94. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/mpi.py +0 -0
  95. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/plot.py +0 -0
  96. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/post.py +0 -0
  97. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/restart_helpers.py +0 -0
  98. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/sella_event_summary.py +0 -0
  99. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/sella_events.py +0 -0
  100. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/summary.py +0 -0
  101. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/thermochemistry.py +0 -0
  102. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/utils/ts_validation.py +0 -0
  103. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/workflows/__init__.py +0 -0
  104. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/workflows/cp_md.py +0 -0
  105. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/workflows/dmf.py +0 -0
  106. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/workflows/irc.py +0 -0
  107. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/workflows/relax.py +0 -0
  108. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools/workflows/vibration.py +0 -0
  109. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools.egg-info/dependency_links.txt +0 -0
  110. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools.egg-info/entry_points.txt +0 -0
  111. {atst_tools-2.3.2 → atst_tools-2.3.3}/src/atst_tools.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atst-tools
3
- Version: 2.3.2
3
+ Version: 2.3.3
4
4
  Summary: Advanced ASE Transition State Tools for ABACUS and Deep-Potential
5
5
  License-Expression: LGPL-3.0-or-later
6
6
  Requires-Python: >=3.10
@@ -24,6 +24,7 @@ Requires-Dist: mpi4py>=4.1.2; extra == "parallel"
24
24
  Provides-Extra: test
25
25
  Requires-Dist: pytest<10,>=8.4; extra == "test"
26
26
  Requires-Dist: tomli<3,>=2.0; extra == "test"
27
+ Requires-Dist: pyyaml<7,>=6; extra == "test"
27
28
  Provides-Extra: release
28
29
  Requires-Dist: build<2,>=1.5; extra == "release"
29
30
  Requires-Dist: twine<7,>=6.2; extra == "release"
@@ -33,11 +34,12 @@ Requires-Dist: pytest<10,>=8.4; extra == "dev"
33
34
  Requires-Dist: build<2,>=1.5; extra == "dev"
34
35
  Requires-Dist: twine<7,>=6.2; extra == "dev"
35
36
  Requires-Dist: tomli<3,>=2.0; extra == "dev"
37
+ Requires-Dist: pyyaml<7,>=6; extra == "dev"
36
38
  Dynamic: license-file
37
39
 
38
40
  # ATST-Tools
39
41
 
40
- [![Version](https://img.shields.io/badge/version-2.3.2-blue)](pyproject.toml)
42
+ [![Version](https://img.shields.io/badge/version-2.3.3-blue)](pyproject.toml)
41
43
  [![Python](https://img.shields.io/badge/python-%3E%3D3.10-blue)](pyproject.toml)
42
44
  [![License](https://img.shields.io/badge/license-LGPL--v3-blue)](#license)
43
45
 
@@ -67,11 +69,11 @@ Examples: [GitHub](https://github.com/QuantumMisaka/atst-tools/tree/main/example
67
69
  | Intrinsic reaction coordinates | `irc` |
68
70
  | ASE-driven or ABACUS-native molecular dynamics | `md` |
69
71
  | Experimental Direct MaxFlux path search | `dmf` |
70
- | Experimental ABACUS constant-potential single points and serial scans | `constant_potential` |
72
+ | ABACUS constant-potential single points and serial scans | `constant_potential` |
71
73
 
72
- Experimental constant-potential fixed-cell relaxation, NEB and ASE-driven MD
74
+ Constant-potential fixed-cell relaxation, serial NEB and ASE-driven MD
73
75
  use the explicit `compensated_gate` boundary. See the
74
- [configuration reference](docs/user/CONFIG_REFERENCE.md#211-constant-potential-evaluation-development-candidate)
76
+ [configuration reference](docs/user/CONFIG_REFERENCE.md#211-constant-potential-evaluation)
75
77
  for setup and supported boundaries.
76
78
 
77
79
  Local commands prepare paths and ABACUS inputs, inspect trajectories, and
@@ -107,10 +109,10 @@ backend inputs.
107
109
 
108
110
  ## Quick Start
109
111
 
110
- Obtain examples at the tag matching your installed version (shown for 2.3.2):
112
+ Obtain examples at the tag matching your installed version (shown for 2.3.3):
111
113
 
112
114
  ```bash
113
- git clone --branch v2.3.2 https://github.com/QuantumMisaka/atst-tools.git
115
+ git clone --branch v2.3.3 https://github.com/QuantumMisaka/atst-tools.git
114
116
  cd atst-tools
115
117
  ```
116
118
 
@@ -149,7 +151,7 @@ methods or DP variants. A complete walkthrough is also available in the
149
151
  | Use CLI commands | [CLI reference](docs/user/CLI_REFERENCE.md) |
150
152
  | Configure ABACUS input/output and parallelism | [ABACUS wrapper guide](docs/user/ABACUSLITE_WRAPPER_GUIDE.md) |
151
153
  | Embed workflows or use the JSON process runner | [Python API](docs/user/PYTHON_API_REFERENCE.md) |
152
- | Check support and releases | [Feature matrix](docs/reports/FEATURE_STATUS_MATRIX.md) · [2.3.2 release notes](docs/releases/RELEASE_NOTES_2.3.2.md) |
154
+ | Check support and releases | [Feature matrix](docs/reports/FEATURE_STATUS_MATRIX.md) · [2.3.3 release notes](docs/releases/RELEASE_NOTES_2.3.3.md) |
153
155
  | Contribute | [Developer handover](docs/developer/HANDOVER.md) · [Source architecture](src/atst_tools/README.md) |
154
156
 
155
157
  The primary documentation is in English, with a [Chinese user guide](docs/user/USER_GUIDE_CN.md).
@@ -1,6 +1,6 @@
1
1
  # ATST-Tools
2
2
 
3
- [![Version](https://img.shields.io/badge/version-2.3.2-blue)](pyproject.toml)
3
+ [![Version](https://img.shields.io/badge/version-2.3.3-blue)](pyproject.toml)
4
4
  [![Python](https://img.shields.io/badge/python-%3E%3D3.10-blue)](pyproject.toml)
5
5
  [![License](https://img.shields.io/badge/license-LGPL--v3-blue)](#license)
6
6
 
@@ -30,11 +30,11 @@ Examples: [GitHub](https://github.com/QuantumMisaka/atst-tools/tree/main/example
30
30
  | Intrinsic reaction coordinates | `irc` |
31
31
  | ASE-driven or ABACUS-native molecular dynamics | `md` |
32
32
  | Experimental Direct MaxFlux path search | `dmf` |
33
- | Experimental ABACUS constant-potential single points and serial scans | `constant_potential` |
33
+ | ABACUS constant-potential single points and serial scans | `constant_potential` |
34
34
 
35
- Experimental constant-potential fixed-cell relaxation, NEB and ASE-driven MD
35
+ Constant-potential fixed-cell relaxation, serial NEB and ASE-driven MD
36
36
  use the explicit `compensated_gate` boundary. See the
37
- [configuration reference](docs/user/CONFIG_REFERENCE.md#211-constant-potential-evaluation-development-candidate)
37
+ [configuration reference](docs/user/CONFIG_REFERENCE.md#211-constant-potential-evaluation)
38
38
  for setup and supported boundaries.
39
39
 
40
40
  Local commands prepare paths and ABACUS inputs, inspect trajectories, and
@@ -70,10 +70,10 @@ backend inputs.
70
70
 
71
71
  ## Quick Start
72
72
 
73
- Obtain examples at the tag matching your installed version (shown for 2.3.2):
73
+ Obtain examples at the tag matching your installed version (shown for 2.3.3):
74
74
 
75
75
  ```bash
76
- git clone --branch v2.3.2 https://github.com/QuantumMisaka/atst-tools.git
76
+ git clone --branch v2.3.3 https://github.com/QuantumMisaka/atst-tools.git
77
77
  cd atst-tools
78
78
  ```
79
79
 
@@ -112,7 +112,7 @@ methods or DP variants. A complete walkthrough is also available in the
112
112
  | Use CLI commands | [CLI reference](docs/user/CLI_REFERENCE.md) |
113
113
  | Configure ABACUS input/output and parallelism | [ABACUS wrapper guide](docs/user/ABACUSLITE_WRAPPER_GUIDE.md) |
114
114
  | Embed workflows or use the JSON process runner | [Python API](docs/user/PYTHON_API_REFERENCE.md) |
115
- | Check support and releases | [Feature matrix](docs/reports/FEATURE_STATUS_MATRIX.md) · [2.3.2 release notes](docs/releases/RELEASE_NOTES_2.3.2.md) |
115
+ | Check support and releases | [Feature matrix](docs/reports/FEATURE_STATUS_MATRIX.md) · [2.3.3 release notes](docs/releases/RELEASE_NOTES_2.3.3.md) |
116
116
  | Contribute | [Developer handover](docs/developer/HANDOVER.md) · [Source architecture](src/atst_tools/README.md) |
117
117
 
118
118
  The primary documentation is in English, with a [Chinese user guide](docs/user/USER_GUIDE_CN.md).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "atst-tools"
7
- version = "2.3.2"
7
+ version = "2.3.3"
8
8
  description = "Advanced ASE Transition State Tools for ABACUS and Deep-Potential"
9
9
  readme = "README.md"
10
10
  license = "LGPL-3.0-or-later"
@@ -24,13 +24,14 @@ dependencies = [
24
24
  plot = ["matplotlib>=3.9,<4"]
25
25
  dp = ["deepmd-kit>=3.1.3"]
26
26
  parallel = ["mpi4py>=4.1.2"]
27
- test = ["pytest>=8.4,<10", "tomli>=2.0,<3"]
27
+ test = ["pytest>=8.4,<10", "tomli>=2.0,<3", "pyyaml>=6,<7"]
28
28
  release = ["build>=1.5,<2", "twine>=6.2,<7", "tomli>=2.0,<3"]
29
29
  dev = [
30
30
  "pytest>=8.4,<10",
31
31
  "build>=1.5,<2",
32
32
  "twine>=6.2,<7",
33
33
  "tomli>=2.0,<3",
34
+ "pyyaml>=6,<7",
34
35
  ]
35
36
 
36
37
  [project.scripts]
@@ -24,6 +24,7 @@ from atst_tools.api.models import (
24
24
  WorkflowResult,
25
25
  )
26
26
  from atst_tools.calculators.abacuslite_backend import BACKEND_SOURCE
27
+ from atst_tools.calculators.factory import socket_session
27
28
  from atst_tools.utils.artifacts import (
28
29
  SCHEMA_VERSION,
29
30
  add_backend_identity_to_manifest,
@@ -915,7 +916,10 @@ def _run_workflow(
915
916
  failure = None
916
917
  dispatch_started = time.monotonic()
917
918
  try:
918
- value = _dispatch_normalized(config, options)
919
+ # Persistent ABACUS socket clients built by this workflow must not
920
+ # outlive it; clients owned by other callers are left untouched.
921
+ with socket_session():
922
+ value = _dispatch_normalized(config, options)
919
923
  except ATSTAPIError as exc:
920
924
  failure = exc
921
925
  except Exception as exc:
@@ -20,12 +20,32 @@ def _load_abacuslite_backend():
20
20
  "abacuslite is required by ATST-Tools; install abacuslite>=1.2.1,<2",
21
21
  name="abacuslite",
22
22
  )
23
- from abacuslite import Abacus, AbacusProfile
23
+ from abacuslite import Abacus, AbacusProfile, AbacusSocketIO
24
24
 
25
- return Abacus, AbacusProfile, "external"
25
+ return Abacus, AbacusProfile, AbacusSocketIO, "external"
26
26
 
27
27
 
28
- Abacus, AbacusProfile, BACKEND_SOURCE = _load_abacuslite_backend()
28
+ Abacus, AbacusProfile, AbacusSocketIO, BACKEND_SOURCE = _load_abacuslite_backend()
29
+
30
+
31
+ def socket_input_parameters(parameters: dict) -> dict:
32
+ """Apply the backend's socket-mode INPUT rules to ABACUS parameters.
33
+
34
+ Delegates to ``AbacusSocketIO._socket_inp`` so prepared INPUT files match
35
+ what the socket calculator writes: ``socket_driver = 1``,
36
+ ``calculation = scf`` and normalized ``cal_force``/``cal_stress``.
37
+
38
+ Args:
39
+ parameters: ABACUS INPUT parameters.
40
+
41
+ Returns:
42
+ A new parameter mapping with socket-mode values applied.
43
+
44
+ Raises:
45
+ ValueError: If ``calculation`` is not ``scf`` or a ``cal_*`` flag is
46
+ not a boolean value.
47
+ """
48
+ return AbacusSocketIO._socket_inp(parameters)
29
49
 
30
50
 
31
51
  def import_generalio():
@@ -131,6 +151,7 @@ class ATSTAbacusProfile(AbacusProfile):
131
151
  __all__ = [
132
152
  "Abacus",
133
153
  "AbacusProfile",
154
+ "AbacusSocketIO",
134
155
  "ATSTAbacusProfile",
135
156
  "BACKEND_SOURCE",
136
157
  "BackendSource",
@@ -20,6 +20,7 @@ from typing import Any
20
20
  import numpy as np
21
21
  from ase.calculators.calculator import Calculator, all_changes
22
22
 
23
+ from atst_tools.utils.config_schema import ABACUS_CONTROL_KEYS
23
24
  from atst_tools.utils.electrochemistry import chebyshev_derivative, surface_area
24
25
 
25
26
 
@@ -61,7 +62,7 @@ _CURRENT_SCF_FAILURE_MARKER = re.compile(
61
62
  CP_FACTS_INFO_KEY = "atst_constant_potential_facts"
62
63
  CP_IDENTITY_INFO_KEY = "atst_constant_potential_identity"
63
64
  CP_FACTS_SCHEMA = "atst-constant-potential-facts-v1"
64
- _ABACUS_CONTROL_KEYS = {"command", "mpi", "omp", "directory", "parameters", "version_command"}
65
+ _ABACUS_CONTROL_KEYS = ABACUS_CONTROL_KEYS
65
66
  _FIXED_BOUNDARY_KEYS = (
66
67
  "gate_flag",
67
68
  "imp_sol",
@@ -359,7 +360,8 @@ def _same_finite_float(left: Any, right: Any) -> bool:
359
360
 
360
361
 
361
362
  def _constant_potential_facts_error(
362
- atoms: Any, payload: Any, *, require_duplicate_identity: bool = True
363
+ atoms: Any, payload: Any, *, require_duplicate_identity: bool = True,
364
+ check_geometry: bool = True,
363
365
  ) -> str | None:
364
366
  """Return a fail-closed validation error for one durable CP envelope."""
365
367
  if atoms is None or not hasattr(atoms, "info"):
@@ -370,12 +372,13 @@ def _constant_potential_facts_error(
370
372
  return "constant-potential facts schema or workflow is unsupported"
371
373
  if payload.get("status") != "complete" or payload.get("cp_converged") is not True:
372
374
  return "constant-potential facts are not a complete converged result"
373
- try:
374
- geometry_fingerprint = constant_potential_geometry_fingerprint(atoms)
375
- except (AttributeError, TypeError, ValueError):
376
- return "constant-potential facts geometry cannot be fingerprinted"
377
- if payload.get("geometry_fingerprint") != geometry_fingerprint:
378
- return "constant-potential facts geometry fingerprint does not match"
375
+ if check_geometry:
376
+ try:
377
+ geometry_fingerprint = constant_potential_geometry_fingerprint(atoms)
378
+ except (AttributeError, TypeError, ValueError):
379
+ return "constant-potential facts geometry cannot be fingerprinted"
380
+ if payload.get("geometry_fingerprint") != geometry_fingerprint:
381
+ return "constant-potential facts geometry fingerprint does not match"
379
382
 
380
383
  identity = payload.get("identity")
381
384
  facts = payload.get("facts")
@@ -612,12 +615,14 @@ def publish_constant_potential_facts(
612
615
  return copy.deepcopy(payload)
613
616
 
614
617
 
615
- def read_constant_potential_facts(atoms: Any) -> dict[str, Any] | None:
616
- """Read and validate CP facts persisted on an ASE atoms object."""
618
+ def read_constant_potential_facts(
619
+ atoms: Any, *, check_geometry: bool = True
620
+ ) -> dict[str, Any] | None:
621
+ """Read valid CP facts; restart checks geometry while NEB may use ASE state."""
617
622
  if atoms is None or not hasattr(atoms, "info"):
618
623
  return None
619
624
  payload = atoms.info.get(CP_FACTS_INFO_KEY)
620
- if _constant_potential_facts_error(atoms, payload) is not None:
625
+ if _constant_potential_facts_error(atoms, payload, check_geometry=check_geometry) is not None:
621
626
  return None
622
627
  return copy.deepcopy(dict(payload))
623
628
 
@@ -751,7 +756,14 @@ def _read_vacuum_from_logs(directory: str | Path, suffix: str | None = None) ->
751
756
 
752
757
 
753
758
  def _backend_log_paths(calculator: Any) -> tuple[Path, ...]:
754
- """Return only the current backend's SCF log candidates."""
759
+ """Return only the current backend's SCF log candidates.
760
+
761
+ An abacuslite ``AbacusSocketIO`` keeps its ABACUS working directory and
762
+ INPUT parameters on the wrapped FileIO calculator (``calculator.abacus``);
763
+ its own ``directory`` is ASE's default ``'.'``, which is not where ABACUS
764
+ writes.
765
+ """
766
+ calculator = getattr(calculator, "abacus", calculator)
755
767
  directory = getattr(calculator, "directory", None)
756
768
  if directory is None:
757
769
  return ()
@@ -765,7 +777,15 @@ def _backend_log_paths(calculator: Any) -> tuple[Path, ...]:
765
777
 
766
778
 
767
779
  def _backend_scf_converged(calculator: Any, results: Mapping[str, Any]) -> bool | None:
768
- """Read an explicit SCF-validity fact from results or the current log."""
780
+ """Read an explicit SCF-validity fact from results, the backend, or its log.
781
+
782
+ Args:
783
+ calculator: Backend calculator that produced ``results``.
784
+ results: Result mapping of the evaluation being validated.
785
+
786
+ Returns:
787
+ ``True``/``False`` for an explicit fact, ``None`` when no evidence exists.
788
+ """
769
789
  for key in ("scf_converged", "scf_convergence", "scf_status", "convergence"):
770
790
  if key not in results or results[key] is None:
771
791
  continue
@@ -782,6 +802,11 @@ def _backend_scf_converged(calculator: Any, results: Mapping[str, Any]) -> bool
782
802
  return bool(int(value))
783
803
  except (TypeError, ValueError):
784
804
  return None
805
+ # abacuslite's socket client reports the per-evaluation SCF state from the
806
+ # ABACUS i-PI metadata as an attribute; it is reset before every request.
807
+ socket_converged = getattr(calculator, "last_scf_converged", None)
808
+ if isinstance(socket_converged, (bool, np.bool_)):
809
+ return bool(socket_converged)
785
810
  for path in _backend_log_paths(calculator):
786
811
  if not path.is_file():
787
812
  continue
@@ -2,18 +2,21 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import itertools
5
6
  import os
6
7
  import re
7
8
  import shlex
8
9
  import logging
9
10
  import math
11
+ from contextlib import contextmanager
12
+ from contextvars import ContextVar
10
13
  from copy import deepcopy
11
- from typing import Any, Dict
14
+ from typing import Any, Dict, Iterator
12
15
 
13
16
  from ase.calculators.calculator import Calculator
14
17
 
15
18
  from atst_tools.calculators.abacuslite_backend import (
16
- Abacus, ATSTAbacusProfile, BACKEND_SOURCE, _default_version_command,
19
+ Abacus, AbacusSocketIO, ATSTAbacusProfile, BACKEND_SOURCE, _default_version_command,
17
20
  )
18
21
  from atst_tools.calculators.constant_potential import (
19
22
  ConstantPotentialCalculator,
@@ -23,13 +26,18 @@ from atst_tools.calculators.dp import DeepPotentialFactory
23
26
  from atst_tools.runtime import counters as runtime_counters
24
27
  from atst_tools.runtime import launch as runtime_launch
25
28
  from atst_tools.utils.config_schema import (
29
+ ABACUS_CONTROL_KEYS,
30
+ SOCKET_FIXED_CELL_MD_ENSEMBLES,
31
+ SOCKET_SUPPORTED_WORKFLOWS,
26
32
  ConstantPotentialConfig,
27
33
  validate_constant_potential_md_scope,
28
34
  )
29
35
  from atst_tools.utils.mpi import mpi_launcher_detected
30
36
 
31
37
 
32
- _ABACUS_CONTROL_KEYS = {"command", "mpi", "omp", "directory", "parameters", "version_command"}
38
+ _ABACUS_CONTROL_KEYS = ABACUS_CONTROL_KEYS
39
+ # INPUT keys that AbacusSocketIO validates only when passed through ``inp``.
40
+ _SOCKET_INP_KEYS = ("calculation", "cal_force", "cal_stress")
33
41
  _MPI_ENV_KEYS_TO_CLEAR = (
34
42
  "OMPI_COMM_WORLD_SIZE",
35
43
  "OMPI_COMM_WORLD_RANK",
@@ -46,6 +54,9 @@ LOGGER = logging.getLogger(__name__)
46
54
  _ABACUS_BACKEND_LOGGED = False
47
55
  _SHELL_ASSIGNMENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=.*$")
48
56
  _MPI_LAUNCHERS = {"mpirun", "mpiexec", "srun"}
57
+ _SOCKET_NAME_COUNTER = itertools.count()
58
+ #: Socket clients owned by the innermost active :func:`socket_session`.
59
+ _SOCKET_SESSION: ContextVar[list[Any] | None] = ContextVar("atst_abacus_socket_session", default=None)
49
60
 
50
61
 
51
62
  def _abacus_section(config: Dict[str, Any]) -> Dict[str, Any]:
@@ -133,6 +144,96 @@ def _validate_abacus_command(command: str) -> None:
133
144
  )
134
145
 
135
146
 
147
+ def _socket_options(raw: Any) -> Dict[str, Any] | None:
148
+ """Normalize ``calculator.abacus.socket`` into AbacusSocketIO keywords.
149
+
150
+ Args:
151
+ raw: ``None``/``False`` (disabled), ``True`` (enabled with defaults), or
152
+ a mapping with optional ``enabled``, ``unixsocket``, ``port`` and
153
+ ``timeout`` keys.
154
+
155
+ Returns:
156
+ Keyword arguments for ``AbacusSocketIO`` or ``None`` when disabled.
157
+
158
+ Raises:
159
+ ValueError: If the value has an unsupported shape or both ``port`` and
160
+ ``unixsocket`` are given.
161
+ """
162
+ if raw is None or raw is False:
163
+ return None
164
+ if raw is True:
165
+ raw = {}
166
+ if not isinstance(raw, dict):
167
+ raise ValueError("calculator.abacus.socket must be a boolean or a mapping")
168
+ options = dict(raw)
169
+ if not options.pop("enabled", True):
170
+ return None
171
+ unknown = set(options) - {"unixsocket", "port", "timeout"}
172
+ if unknown:
173
+ raise ValueError(f"Unknown calculator.abacus.socket keys: {sorted(unknown)}")
174
+ if options.get("port") is not None and options.get("unixsocket") is not None:
175
+ raise ValueError("calculator.abacus.socket accepts either port or unixsocket, not both")
176
+ if options.get("port") is None and options.get("unixsocket") is None:
177
+ # A per-process, per-calculator name keeps concurrent runs on one node
178
+ # from colliding on /tmp/ipi_<name>.
179
+ options["unixsocket"] = f"atst_{os.getpid()}_{next(_SOCKET_NAME_COUNTER)}"
180
+ options.setdefault("timeout", None)
181
+ return options
182
+
183
+
184
+ def _check_socket_workflow(config: Dict[str, Any]) -> None:
185
+ """Reject socket mode for workflows that are not single fixed-cell runs."""
186
+ calculation = config.get("calculation")
187
+ if not isinstance(calculation, dict):
188
+ return
189
+ workflow = calculation.get("type")
190
+ if workflow is not None and workflow not in SOCKET_SUPPORTED_WORKFLOWS:
191
+ raise ValueError(
192
+ "calculator.abacus.socket is supported only for calculation.type in "
193
+ f"{list(SOCKET_SUPPORTED_WORKFLOWS)}; got {workflow!r}"
194
+ )
195
+ if workflow == "md":
196
+ if calculation.get("driver", "ase") != "ase":
197
+ raise ValueError("calculator.abacus.socket requires calculation.driver=ase for md")
198
+ ensemble = calculation.get("ensemble", "nvt")
199
+ if ensemble not in SOCKET_FIXED_CELL_MD_ENSEMBLES:
200
+ raise ValueError(
201
+ "calculator.abacus.socket requires a fixed-cell md ensemble "
202
+ f"{list(SOCKET_FIXED_CELL_MD_ENSEMBLES)}; got {ensemble!r}"
203
+ )
204
+
205
+
206
+ def _close_socket_calculator(calculator: Any) -> None:
207
+ """Close one socket client; failures are logged, never raised."""
208
+ try:
209
+ calculator.close()
210
+ except Exception as exc: # pragma: no cover - best-effort cleanup
211
+ LOGGER.warning("Failed to close ABACUS socket calculator: %s", exc)
212
+
213
+
214
+ @contextmanager
215
+ def socket_session() -> Iterator[list[Any]]:
216
+ """Own the ABACUS socket clients built while this context is active.
217
+
218
+ Clients created by :class:`AbacusFactory` inside the context are closed
219
+ when it exits, including on error. Clients created outside any session
220
+ belong to the caller, who closes them. Sessions are context-local (thread
221
+ and asyncio safe) and may nest; each session closes only the clients it
222
+ created.
223
+
224
+ Yields:
225
+ The list of socket clients owned by this session.
226
+ """
227
+ owned: list[Any] = []
228
+ token = _SOCKET_SESSION.set(owned)
229
+ try:
230
+ yield owned
231
+ finally:
232
+ _SOCKET_SESSION.reset(token)
233
+ while owned:
234
+ _close_socket_calculator(owned.pop())
235
+
236
+
136
237
  def _resolve_directory(path: str | None) -> str | None:
137
238
  if path is None:
138
239
  return None
@@ -197,7 +298,13 @@ class AbacusFactory:
197
298
  omp_num_threads=omp,
198
299
  version_command=version_command,
199
300
  )
200
- calculator = Abacus(directory=directory, profile=profile, **parameters)
301
+ socket = _socket_options(abacus_config.get("socket"))
302
+ if socket is None:
303
+ calculator = Abacus(directory=directory, profile=profile, **parameters)
304
+ else:
305
+ calculator = AbacusFactory._socket_calculator(
306
+ config, directory, profile, parameters, socket
307
+ )
201
308
  return runtime_counters.instrument_calculator(
202
309
  calculator,
203
310
  build_key="abacus.calculator_built",
@@ -205,6 +312,42 @@ class AbacusFactory:
205
312
  )
206
313
 
207
314
 
315
+ @staticmethod
316
+ def _socket_calculator(
317
+ config: Dict[str, Any],
318
+ directory: str,
319
+ profile: ATSTAbacusProfile,
320
+ parameters: Dict[str, Any],
321
+ socket: Dict[str, Any],
322
+ ) -> Calculator:
323
+ """Build an ``AbacusSocketIO`` calculator owned by the active socket session.
324
+
325
+ Endpoint conflicts are left to the backend: binding an occupied Unix
326
+ socket or TCP port fails when the first calculation starts the server.
327
+ """
328
+ _check_socket_workflow(config)
329
+ endpoint = (
330
+ f"unix:{socket['unixsocket']}" if socket.get("unixsocket") is not None else f"port:{socket['port']}"
331
+ )
332
+ parameters = dict(parameters)
333
+ inp = dict(parameters.pop("inp", {}) or {})
334
+ for key in _SOCKET_INP_KEYS:
335
+ if key in parameters:
336
+ inp[key] = parameters.pop(key)
337
+ calculator = AbacusSocketIO(
338
+ profile=profile,
339
+ directory=directory,
340
+ inp=inp,
341
+ **socket,
342
+ **parameters,
343
+ )
344
+ session = _SOCKET_SESSION.get()
345
+ if session is not None:
346
+ session.append(calculator)
347
+ LOGGER.info("Using ABACUS i-PI socket client (%s).", endpoint)
348
+ return calculator
349
+
350
+
208
351
  class CalculatorFactory:
209
352
  """Unified factory for calculator construction."""
210
353
 
@@ -218,6 +361,11 @@ class CalculatorFactory:
218
361
  else None
219
362
  )
220
363
  if constant_potential is not None:
364
+ if _socket_options(_abacus_section(config).get("socket")) is not None:
365
+ raise ValueError(
366
+ "calculator.abacus.socket cannot be combined with "
367
+ "calculator.constant_potential"
368
+ )
221
369
  return _constant_potential_calculator(config, constant_potential, kwargs)
222
370
  return AbacusFactory.get_calculator(config, **kwargs)
223
371
  if name in {"dp", "deepmd"}:
@@ -22,6 +22,7 @@ from ase.optimize import FIRE, BFGS
22
22
 
23
23
  from atst_tools.mep.neb import AbacusNEB
24
24
  from atst_tools.calculators.factory import CalculatorFactory
25
+ from atst_tools.calculators.constant_potential import read_constant_potential_facts
25
26
  from atst_tools.calculators.dp import is_dp_calculator, should_share_calculator
26
27
  from atst_tools.utils.artifacts import write_artifact_manifest
27
28
  from atst_tools.utils.geometry import prepare_fixed_cell_images, fixed_cell_metadata
@@ -32,8 +33,13 @@ from atst_tools.utils.convergence import (
32
33
  as_step_count,
33
34
  emit_unconverged_advisory,
34
35
  )
35
- from atst_tools.utils.neb_endpoints import endpoint_policy, ensure_neb_endpoint_results
36
+ from atst_tools.utils.neb_endpoints import (
37
+ endpoint_policy,
38
+ ensure_neb_endpoint_results,
39
+ expected_cp_endpoint_identity,
40
+ )
36
41
  from atst_tools.utils.neb_endpoints import freeze_current_results, freeze_results, get_endpoint_results
42
+ from atst_tools.workflows.endpoint_relax import relax_neb_endpoints
37
43
  from atst_tools.utils.mpi import (
38
44
  get_ase_world,
39
45
  rank_owns_local_image,
@@ -813,6 +819,7 @@ class AutoNEBRunner:
813
819
  raise initial_chain_error
814
820
  self._image_index_by_id = {id(image): index for index, image in enumerate(self.init_chain)}
815
821
  self._active_autoneb = None
822
+ self._endpoint_stages = []
816
823
  self._apply_legacy_endpoint_conditions()
817
824
 
818
825
  def _apply_legacy_endpoint_conditions(self):
@@ -827,6 +834,7 @@ class AutoNEBRunner:
827
834
  results[0],
828
835
  results[1],
829
836
  self.init_chain[index].info.get("atst_endpoint_result"),
837
+ read_constant_potential_facts(self.init_chain[index], check_geometry=False),
830
838
  )
831
839
  reference = self.init_chain[len(self.init_chain) // 2]
832
840
  magmoms = reference.get_initial_magnetic_moments()
@@ -835,8 +843,8 @@ class AutoNEBRunner:
835
843
  endpoint.set_initial_magnetic_moments(magmoms)
836
844
  if constraints:
837
845
  endpoint.set_constraint(deepcopy(constraints))
838
- for index, (energy, forces, status) in endpoint_results.items():
839
- freeze_results(self.init_chain[index], energy, forces, status=status)
846
+ for index, (energy, forces, status, cp_facts) in endpoint_results.items():
847
+ freeze_results(self.init_chain[index], energy, forces, status=status, cp_facts=cp_facts)
840
848
 
841
849
  def _base_directory(self):
842
850
  base_dir = self.calc_config['directory']
@@ -910,17 +918,39 @@ class AutoNEBRunner:
910
918
  self.attach_calculators(images, synchronize=False)
911
919
 
912
920
  def _prepare_endpoint_results(self):
913
- """Prepare endpoint single-point results once and synchronize images."""
921
+ """Relax missing-result endpoints, then synchronize their results."""
914
922
  base_dir = self._base_directory()
915
923
  policy = endpoint_policy(self.calc_config, default="auto")
924
+ cp_identity = expected_cp_endpoint_identity(self.config)
925
+ endpoint_config = self.calc_config["endpoint_optimization"]
926
+ self._endpoint_stages = []
916
927
 
917
928
  def prepare():
929
+ if endpoint_config["enabled"]:
930
+ if policy == "never":
931
+ ensure_neb_endpoint_results(
932
+ self.init_chain,
933
+ lambda directory: None,
934
+ policy="never",
935
+ context="AutoNEB",
936
+ constant_potential_identity=cp_identity,
937
+ )
938
+ self._endpoint_stages = relax_neb_endpoints(
939
+ self.init_chain,
940
+ endpoint_config,
941
+ lambda label, atoms: self._get_calculator(
942
+ f"{base_dir}/endpoint_{label}_relax"
943
+ ),
944
+ self._get_optimizer(),
945
+ constant_potential_identity=cp_identity,
946
+ )
918
947
  ensure_neb_endpoint_results(
919
948
  self.init_chain,
920
949
  lambda directory: self._get_calculator(f"{base_dir}/{directory}"),
921
950
  policy=policy,
922
951
  directories=("endpoint_initial", "endpoint_final"),
923
952
  context="AutoNEB",
953
+ constant_potential_identity=cp_identity,
924
954
  )
925
955
 
926
956
  if not self.parallel:
@@ -1167,7 +1197,8 @@ class AutoNEBRunner:
1167
1197
  workflow="autoneb",
1168
1198
  world=self.world,
1169
1199
  )
1170
- stages = [record.to_manifest() for record in iteration_records]
1200
+ stages = [record.to_manifest() for record in self._endpoint_stages]
1201
+ stages.extend(record.to_manifest() for record in iteration_records)
1171
1202
  if final_record is not None:
1172
1203
  stages.append(final_record.to_manifest())
1173
1204