GraphAtoms 2.3.1__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 (92) hide show
  1. {graphatoms-2.3.1 → graphatoms-2.3.3}/PKG-INFO +2 -2
  2. {graphatoms-2.3.1 → graphatoms-2.3.3}/pyproject.toml +28 -28
  3. {graphatoms-2.3.1 → graphatoms-2.3.3}/pyproject.toml.orig +28 -28
  4. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/dataclasses/_pydanticMixin.py +38 -20
  5. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/dataclasses/_pydanticModel.py +1 -1
  6. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/network/_clsnetwork.py +15 -7
  7. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/network/_metadata.py +43 -3
  8. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/network/_scheduler.py +14 -8
  9. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/parallel/__init__.py +22 -6
  10. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/parallel/_base.py +24 -5
  11. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/parallel/_ray.py +35 -12
  12. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/__init__.py +10 -14
  13. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/common/_base.py +6 -3
  14. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/common/_helper.py +7 -5
  15. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/general/otfkmc.py +5 -1
  16. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/general/rxngen.py +24 -14
  17. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/geometry/_bond_list.py +1 -2
  18. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/geometry/_neighbor_list.py +6 -3
  19. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/geometry/sample.py +7 -1
  20. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/system/database/abc.py +8 -5
  21. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/system/graph.py +18 -4
  22. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/utils/adsorption.py +6 -2
  23. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/utils/bytestool.py +52 -1
  24. {graphatoms-2.3.1 → graphatoms-2.3.3}/README.md +0 -0
  25. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/LICENSE +0 -0
  26. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/__init__.py +0 -0
  27. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/arrayapi/__init__.py +0 -0
  28. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/arrayapi/compat.py +0 -0
  29. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/arrayapi/typing.py +0 -0
  30. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/dataclasses/__init__.py +0 -0
  31. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/dataclasses/_numpydantic.py +0 -0
  32. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/dataclasses/_pydantic2pyarrow.py +0 -0
  33. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/dataclasses/graph.py +0 -0
  34. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/__init__.py +0 -0
  35. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/config/__init__.py +0 -0
  36. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/config/atoms/__init__.py +0 -0
  37. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/config/atoms/aseread.yaml +0 -0
  38. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/config/atoms/octahedron.yaml +0 -0
  39. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/config/bonds/__init__.py +0 -0
  40. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/config/bonds/raw.yaml +0 -0
  41. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/config/calculator/__init__.py +0 -0
  42. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/config/calculator/emt.yaml +0 -0
  43. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/config/calculator/nequip.yaml +0 -0
  44. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/config/example.yaml +0 -0
  45. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/config/run.yaml +0 -0
  46. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/network/__init__.py +0 -0
  47. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/network/_commands.py +0 -0
  48. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/network/_recorder.py +0 -0
  49. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/parallel/_dask.py +0 -0
  50. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/parallel/_executorlib.py +0 -0
  51. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/common/__init__.py +0 -0
  52. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/common/_errors.py +0 -0
  53. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/general/__init__.py +0 -0
  54. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/general/_expl.py +0 -0
  55. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/v2/__init__.py +0 -0
  56. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/v2/_helperv2.py +0 -0
  57. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/v2/general/__init__.py +0 -0
  58. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/v2/general/_expl.py +0 -0
  59. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/v2/general/otfkmc.py +0 -0
  60. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/v2/general/rxngen.py +0 -0
  61. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/v2/ray/__init__.py +0 -0
  62. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/v2/ray/_expl.py +0 -0
  63. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/v2/ray/_helper.py +0 -0
  64. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/v2/ray/otfkmc.py +0 -0
  65. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/enterpoint/runner/v2/ray/rxngen.py +0 -0
  66. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/geometry/__init__.py +0 -0
  67. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/geometry/_distance_pairs.py +0 -0
  68. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/geometry/_inner_outer.py +0 -0
  69. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/geometry/mic.py +0 -0
  70. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/geometry/rotation.py +0 -0
  71. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/reaction/__init__.py +0 -0
  72. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/reaction/_event.py +0 -0
  73. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/reaction/reaction.py +0 -0
  74. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/reaction/xxsorption.py +0 -0
  75. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/system/__init__.py +0 -0
  76. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/system/atoms.py +0 -0
  77. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/system/bonds.py +0 -0
  78. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/system/database/__init__.py +0 -0
  79. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/system/database/dict.py +0 -0
  80. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/system/database/dir.py +0 -0
  81. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/system/database/hdf5.py +0 -0
  82. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/system/database/sqliteASE.py +0 -0
  83. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/system/sysCluster.py +0 -0
  84. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/system/sysGas.py +0 -0
  85. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/system/system.py +0 -0
  86. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/utils/__init__.py +0 -0
  87. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/utils/aseqhull.py +0 -0
  88. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/utils/asetools.py +0 -0
  89. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/utils/logger.py +0 -0
  90. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/utils/parser.py +0 -0
  91. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/utils/rdutils.py +0 -0
  92. {graphatoms-2.3.1 → graphatoms-2.3.3}/src/graphatoms/utils/subgraph.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphatoms
3
- Version: 2.3.1
3
+ Version: 2.3.3
4
4
  Summary: The Chemical Core Class for Graph Theory Analysis.
5
5
  Keywords: chemistry,graph theory,reaction,event,KMC
6
6
  Author: LiuGaoyong
@@ -14,12 +14,12 @@ Requires-Dist: ase
14
14
  Requires-Dist: pymatgen>2023.6
15
15
  Requires-Dist: rdkit>=2025
16
16
  Requires-Dist: numpy>=2,<3
17
- Requires-Dist: pandas>=2,<4
18
17
  Requires-Dist: scipy>=1.10,<2
19
18
  Requires-Dist: scikit-learn>=1.5
20
19
  Requires-Dist: array-api-compat>=1.10
21
20
  Requires-Dist: array-api-extra>=0.10
22
21
  Requires-Dist: pyarrow
22
+ Requires-Dist: pandas
23
23
  Requires-Dist: igraph>=0.11
24
24
  Requires-Dist: h5py>=3.16
25
25
  Requires-Dist: numpydantic
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "graphatoms"
7
- version = "2.3.1"
7
+ version = "2.3.3"
8
8
  readme = "README.md"
9
9
  description = "The Chemical Core Class for Graph Theory Analysis."
10
10
  license-files = ["src/LICENSE"]
@@ -27,12 +27,12 @@ dependencies = [
27
27
  "pymatgen>2023.6",
28
28
  "rdkit>=2025",
29
29
  "numpy>=2,<3",
30
- "pandas>=2,<4",
31
30
  "scipy>=1.10,<2",
32
31
  "scikit-learn>=1.5",
33
32
  "array-api-compat>=1.10",
34
33
  "array_api-extra>=0.10",
35
34
  "pyarrow",
35
+ "pandas",
36
36
  "igraph>=0.11",
37
37
  "h5py>=3.16",
38
38
  "numpydantic",
@@ -146,29 +146,29 @@ benchmark = "pytest -s src/tests-benchmark/*.py"
146
146
 
147
147
  [tool.pixi.dependencies]
148
148
  python = ">=3.12"
149
- pytest = ">=9.0.3,<10"
150
- array-api-compat = ">=1.15.0,<2"
151
- array-api-extra = ">=0.11.0,<0.12"
152
- array-api-strict = ">=2.5,<3"
153
- ase = ">=3.28.0,<4"
154
- pymatgen = ">=2026.5.4,<2027"
155
- rdkit = ">=2025.9.5,<2027"
156
- openbabel = ">=3.1.1,<4"
157
- pyarrow = ">=21.0.0,<25"
158
- scikit-learn = ">=1.8.0,<2"
159
- python-igraph = ">=0.11.9,<0.12"
160
- numpy = ">=2.0.0,<3"
161
- pydantic = ">=2.10.0,<3"
162
- numpydantic = ">=1.8.1,<2"
163
- python-snappy = ">=0.7.3,<0.8"
164
- h5py = ">=3.16.0,<4"
165
- hydra-core = ">=1.3.2,<2"
166
- tomli-w = ">=1.2.0,<2"
167
- loguru = ">=0.7.3,<0.8"
168
- jupyterlab = ">=4.5.8,<5"
169
- joblib = ">=1.1.0,<2"
170
- ovld = ">=0.5.15,<0.6"
171
- pytest-xdist = ">=3.8.0,<4"
172
- executorlib = ">=1.8,<2"
173
- typer = ">=0.20,<1"
174
- grpcio = ">=1.52.0,<2"
149
+ pytest = ">=8"
150
+ array-api-compat = ">=1.15.0"
151
+ array-api-extra = ">=0.11.0"
152
+ array-api-strict = ">=2.5"
153
+ ase = ">=3.28.0"
154
+ pymatgen = ">=2026.5.4"
155
+ rdkit = ">=2025.9.5"
156
+ openbabel = ">=3.1.1"
157
+ pyarrow = ">=21.0.0"
158
+ scikit-learn = ">=1.8.0"
159
+ python-igraph = ">=0.11.9"
160
+ numpy = ">=2.0.0"
161
+ pydantic = ">=2.10.0"
162
+ numpydantic = ">=1.8.1"
163
+ python-snappy = ">=0.7.3"
164
+ h5py = ">=3.16"
165
+ hydra-core = ">=1.3.0"
166
+ tomli-w = ">=1.2.0"
167
+ loguru = ">=0.7.3"
168
+ jupyterlab = ">=4.5.8"
169
+ joblib = ">=1.1.0"
170
+ ovld = ">=0.5.15"
171
+ pytest-xdist = ">=3.8.0"
172
+ executorlib = ">=1.8"
173
+ typer = ">=0.20"
174
+ grpcio = ">=1.52.0"
@@ -12,7 +12,7 @@ graphatoms-network = "graphatoms.enterpoint.network:main"
12
12
  graphatoms-run = "graphatoms.enterpoint.runner:run"
13
13
  [project]
14
14
  name = "graphatoms"
15
- version = "2.3.1"
15
+ version = "2.3.3"
16
16
  readme = "README.md"
17
17
  description = "The Chemical Core Class for Graph Theory Analysis."
18
18
  authors = [{ name = "LiuGaoyong", email = "liugaoyong_88@163.com" }]
@@ -39,12 +39,12 @@ dependencies = [
39
39
 
40
40
  # Scientific Computing
41
41
  "numpy>=2,<3",
42
- "pandas>=2,<4",
43
42
  "scipy>=1.10,<2",
44
43
  "scikit-learn>=1.5",
45
44
  "array-api-compat>=1.10",
46
45
  "array_api-extra>=0.10",
47
46
  "pyarrow",
47
+ "pandas",
48
48
 
49
49
  # Graph Theory
50
50
  "igraph>=0.11", # high performance graph library based on C
@@ -145,29 +145,29 @@ benchmark = "pytest -s src/tests-benchmark/*.py"
145
145
 
146
146
  [tool.pixi.dependencies]
147
147
  python = ">=3.12"
148
- pytest = ">=9.0.3,<10"
149
- array-api-compat = ">=1.15.0,<2"
150
- array-api-extra = ">=0.11.0,<0.12"
151
- array-api-strict = ">=2.5,<3"
152
- ase = ">=3.28.0,<4"
153
- pymatgen = ">=2026.5.4,<2027"
154
- rdkit = ">=2025.9.5,<2027"
155
- openbabel = ">=3.1.1,<4"
156
- pyarrow = ">=21.0.0,<25"
157
- scikit-learn = ">=1.8.0,<2"
158
- python-igraph = ">=0.11.9,<0.12"
159
- numpy = ">=2.0.0,<3"
160
- pydantic = ">=2.10.0,<3"
161
- numpydantic = ">=1.8.1,<2"
162
- python-snappy = ">=0.7.3,<0.8"
163
- h5py = ">=3.16.0,<4"
164
- hydra-core = ">=1.3.2,<2"
165
- tomli-w = ">=1.2.0,<2"
166
- loguru = ">=0.7.3,<0.8"
167
- jupyterlab = ">=4.5.8,<5"
168
- joblib = ">=1.1.0,<2"
169
- ovld = ">=0.5.15,<0.6"
170
- pytest-xdist = ">=3.8.0,<4"
171
- executorlib = ">=1.8,<2"
172
- typer = ">=0.20,<1"
173
- grpcio = ">=1.52.0,<2"
148
+ pytest = ">=8"
149
+ array-api-compat = ">=1.15.0"
150
+ array-api-extra = ">=0.11.0"
151
+ array-api-strict = ">=2.5"
152
+ ase = ">=3.28.0"
153
+ pymatgen = ">=2026.5.4"
154
+ rdkit = ">=2025.9.5"
155
+ openbabel = ">=3.1.1"
156
+ pyarrow = ">=21.0.0"
157
+ scikit-learn = ">=1.8.0"
158
+ python-igraph = ">=0.11.9"
159
+ numpy = ">=2.0.0"
160
+ pydantic = ">=2.10.0"
161
+ numpydantic = ">=1.8.1"
162
+ python-snappy = ">=0.7.3"
163
+ h5py = ">=3.16"
164
+ hydra-core = ">=1.3.0"
165
+ tomli-w = ">=1.2.0"
166
+ loguru = ">=0.7.3"
167
+ jupyterlab = ">=4.5.8"
168
+ joblib = ">=1.1.0"
169
+ ovld = ">=0.5.15"
170
+ pytest-xdist = ">=3.8.0"
171
+ executorlib = ">=1.8"
172
+ typer = ">=0.20"
173
+ grpcio = ">=1.52.0"
@@ -229,7 +229,11 @@ class __Json(PydanticConvertFactoryMixin):
229
229
  indent=indent,
230
230
  )
231
231
  )
232
- filename.write_text(self.to_str(**kwargs), "utf-8")
232
+ bytesutils.atomic_write_text(
233
+ target=filename,
234
+ text=self.to_str(**kwargs),
235
+ encoding="utf-8",
236
+ )
233
237
  return filename
234
238
 
235
239
  @classmethod
@@ -260,13 +264,15 @@ class __Toml(PydanticConvertFactoryMixin):
260
264
  numpy_ndarray_compatible=True,
261
265
  )
262
266
  )
263
- with filename.open("wb") as f:
264
- toml_w.dump(
267
+ bytesutils.atomic_write_text(
268
+ target=filename,
269
+ text=toml_w.dumps(
265
270
  self.to_dict(**kwargs),
266
- f,
267
271
  indent=indent,
268
272
  multiline_strings=True,
269
- )
273
+ ),
274
+ encoding="utf-8",
275
+ )
270
276
  return filename
271
277
 
272
278
  @classmethod
@@ -301,16 +307,17 @@ class __Yaml(PydanticConvertFactoryMixin):
301
307
  numpy_ndarray_compatible=True,
302
308
  )
303
309
  )
304
- with filename.open("w", encoding="utf-8") as f:
305
- f.write(
306
- OmegaConf.to_yaml(
307
- self.to_dict(**kwargs),
308
- # f,
309
- # encoding="utf-8",
310
- # # default_flow_style=False,
311
- # indent=4,
312
- )
313
- )
310
+ bytesutils.atomic_write_text(
311
+ target=filename,
312
+ text=OmegaConf.to_yaml(
313
+ self.to_dict(**kwargs),
314
+ # f,
315
+ # encoding="utf-8",
316
+ # # default_flow_style=False,
317
+ # indent=4,
318
+ ),
319
+ encoding="utf-8",
320
+ )
314
321
  return filename
315
322
 
316
323
  @classmethod
@@ -345,10 +352,14 @@ class __Npz(PydanticConvertFactoryMixin):
345
352
  numpy_ndarray_compatible=True,
346
353
  )
347
354
  )
348
- (np.savez_compressed if compress else np.savez)(
349
- filename,
350
- allow_pickle=False,
351
- **self.to_dict(**kwargs),
355
+ func = np.savez_compressed if compress else np.savez
356
+ bytesutils.atomic_write(
357
+ target=filename,
358
+ writer=lambda p: func(
359
+ p,
360
+ allow_pickle=False,
361
+ **self.to_dict(**kwargs),
362
+ ),
352
363
  )
353
364
  return filename
354
365
 
@@ -411,7 +422,14 @@ class __Pickle(PydanticConvertFactoryMixin):
411
422
  numpy_ndarray_compatible=True,
412
423
  )
413
424
  )
414
- dump(self.to_dict(**kwargs), filename, compress=compress)
425
+ bytesutils.atomic_write(
426
+ target=filename,
427
+ writer=lambda p: dump(
428
+ value=self.to_dict(**kwargs),
429
+ filename=p,
430
+ compress=compress, # type: ignore
431
+ ),
432
+ )
415
433
  return filename
416
434
 
417
435
  @classmethod
@@ -126,7 +126,7 @@ class OurBaseModel(PydanticFactoryMixin, _IndexMaskMixin):
126
126
  if v0.shape != v1.shape:
127
127
  return False
128
128
  if np.all(v0 == v1):
129
- return True
129
+ continue
130
130
  elif not np.allclose(v0, v1):
131
131
  return False
132
132
  else:
@@ -27,6 +27,7 @@ from graphatoms.reaction import (
27
27
  )
28
28
  from graphatoms.system import Cluster, Gas, SysGraph, System
29
29
  from graphatoms.system.database import DatabaseABC, get_db
30
+ from graphatoms.utils.bytestool import atomic_write, atomic_write_text
30
31
 
31
32
  __all__ = [
32
33
  "MetaData",
@@ -88,8 +89,9 @@ class ReactionNetwork(Mapping[str, tuple[EventInfo, EventBase]]):
88
89
  + " Please delete it or use `restart=True`."
89
90
  )
90
91
  self.__path.mkdir(parents=True, exist_ok=True)
91
- self.__path_exploration_recorder.write_text(
92
- RecorderInfo.get_csv_title() + "\n"
92
+ atomic_write_text(
93
+ self.__path_exploration_recorder,
94
+ RecorderInfo.get_csv_title() + "\n",
93
95
  )
94
96
  self.__recorder = Recorder()
95
97
  self.__scheduler = Scheduler()
@@ -130,6 +132,7 @@ class ReactionNetwork(Mapping[str, tuple[EventInfo, EventBase]]):
130
132
  config: Config,
131
133
  *args,
132
134
  restart: bool | None = None,
135
+ remote_retry_times: float = 2.0,
133
136
  remote_mode: bool = False,
134
137
  **kwargs,
135
138
  ) -> Self: # type: ignore
@@ -152,7 +155,7 @@ class ReactionNetwork(Mapping[str, tuple[EventInfo, EventBase]]):
152
155
  )
153
156
  break
154
157
  except Exception as e:
155
- time.sleep(0.5)
158
+ time.sleep(abs(remote_retry_times))
156
159
  err = e
157
160
  if err is not None:
158
161
  raise RuntimeError(
@@ -535,8 +538,11 @@ class ReactionNetwork(Mapping[str, tuple[EventInfo, EventBase]]):
535
538
  msg = f"Event {event._string()} failed "
536
539
  msg += f"to simplify. because of {e}"
537
540
  fname = self.__path / "event-simplify-fail.pkl"
538
- fname.with_suffix(".err").write_text(msg)
539
- fname.write_bytes(pickle.dumps(event))
541
+ atomic_write_text(fname.with_suffix(".err"), msg)
542
+ atomic_write(
543
+ fname,
544
+ writer=lambda p: p.write_bytes(pickle.dumps(event)),
545
+ )
540
546
  event.R.write_npz(fname.with_suffix(".R.npz"))
541
547
  event.P.write_npz(fname.with_suffix(".P.npz"))
542
548
  if event.G is not None:
@@ -789,8 +795,10 @@ if __name__ == "__main__":
789
795
  if "merge" not in zzzz_path.name:
790
796
  p = zzzz_path / "event"
791
797
  (p / "recorder.json").unlink(missing_ok=True)
792
- with open(p / "recorder.json", "w") as f:
793
- json.dump({}, f, indent=4)
798
+ atomic_write_text(
799
+ p / "recorder.json",
800
+ json.dumps({}, indent=4),
801
+ )
794
802
  network_lst.append(
795
803
  ReactionNetwork(
796
804
  zzzz_path / "event",
@@ -1,6 +1,7 @@
1
1
  import dataclasses as dc
2
+ import types
2
3
  from pathlib import Path
3
- from typing import Any, Self, override
4
+ from typing import Any, Self, Union, get_args, get_origin, override
4
5
 
5
6
  import numpy as np
6
7
  import pandas as pd
@@ -10,10 +11,30 @@ from graphatoms.dataclasses import OurBaseModel
10
11
  from graphatoms.enterpoint.config import Config, EventConfig
11
12
  from graphatoms.reaction import EventBase, EventInfo
12
13
  from graphatoms.system.bonds import matchmode2nmatch
14
+ from graphatoms.utils.bytestool import atomic_write
13
15
 
14
16
  __all__ = ["MetaData"]
15
17
 
16
18
 
19
+ def _is_list_of_optional_str(annotation: Any) -> bool:
20
+ """Whether the annotation is ``list[str | None]``."""
21
+ if get_origin(annotation) is not list:
22
+ return False
23
+ element = get_args(annotation)[0]
24
+ if get_origin(element) not in (Union, types.UnionType):
25
+ return False
26
+ return set(get_args(element)) == {str, type(None)}
27
+
28
+
29
+ def _missing_to_none(value: Any) -> Any:
30
+ """Convert a missing value marker of pandas to ``None``."""
31
+ if value is None or value is pd.NA:
32
+ return None
33
+ if isinstance(value, float) and np.isnan(value):
34
+ return None
35
+ return value
36
+
37
+
17
38
  class _MetaDataBasic(OurBaseModel, EventConfig):
18
39
  gas_sticking: dict[str, NonNegativeFloat] = {}
19
40
 
@@ -108,7 +129,23 @@ class _MetaDataTable(BaseModel):
108
129
 
109
130
  @classmethod
110
131
  def from_dataframe(cls, df: pd.DataFrame) -> Self:
111
- return cls(**{k: df[k].to_list() for k in df.columns})
132
+ """Create the table from a DataFrame.
133
+
134
+ Note:
135
+ Since pandas 3.0, a string column containing missing values is
136
+ read back as the new default string dtype (``str``), whose
137
+ missing value marker is ``numpy.nan`` instead of ``None``.
138
+ Such ``nan`` values are rejected by the ``str | None`` fields,
139
+ so they are converted back to ``None`` here.
140
+ """
141
+ data: dict[str, list[Any]] = {}
142
+ for k in df.columns:
143
+ values: list[Any] = df[k].to_list()
144
+ field = cls.__pydantic_fields__.get(k)
145
+ if field is not None and _is_list_of_optional_str(field.annotation):
146
+ values = [_missing_to_none(v) for v in values]
147
+ data[k] = values
148
+ return cls(**data)
112
149
 
113
150
  def merge(self, other: Self, idxs: list[int] | None = None) -> None:
114
151
  if idxs is None:
@@ -187,7 +224,10 @@ class MetaData(BaseModel):
187
224
 
188
225
  def persistence(self, path: Path | str) -> None:
189
226
  self.basic.write_json(Path(path) / "metadata-basic.json")
190
- self.table.dataframe.to_feather(Path(path) / "metadata-table.feather")
227
+ atomic_write(
228
+ Path(path) / "metadata-table.feather",
229
+ writer=lambda p: self.table.dataframe.to_feather(p),
230
+ )
191
231
 
192
232
  @classmethod
193
233
  def from_storage(cls, path: Path | str) -> Self:
@@ -9,6 +9,7 @@ from sklearn.metrics.pairwise import cosine_similarity
9
9
 
10
10
  from graphatoms.dataclasses._numpydantic import NDArray
11
11
  from graphatoms.system import SysGraph # type: ignore
12
+ from graphatoms.utils.bytestool import atomic_write
12
13
 
13
14
 
14
15
  class Scheduler(BaseModel):
@@ -35,17 +36,21 @@ class Scheduler(BaseModel):
35
36
  save: Callable = np.savez_compressed if compress else np.savez
36
37
  dct = {f"{k}_is_fixed": v for k, v in self.is_fixed.items()}
37
38
  dct = {f"{k}_dR": v for k, v in self.dR.items()} | dct
38
- save(filename, allow_pickle=False, **dct)
39
+ atomic_write(
40
+ target=filename,
41
+ writer=lambda p: save(p, allow_pickle=False, **dct),
42
+ )
39
43
  return filename
40
44
 
41
45
  @classmethod
42
46
  @validate_call
43
47
  def read_npz(cls, filename: FilePath, *args, **kwargs) -> Self:
44
48
  data: dict[str, dict[str, NDArray]] = defaultdict(dict)
45
- dct: Mapping[str, NDArray] = np.load(filename)
46
- for k, v in dct.items():
47
- k2, k1 = k.split("_")
48
- data[k1][k2] = v
49
+ with np.load(filename) as npz:
50
+ dct: Mapping[str, NDArray] = npz
51
+ for k, v in dct.items():
52
+ k2, k1 = k.rsplit("_", 1)
53
+ data[k1][k2] = v
49
54
  return cls(**data)
50
55
 
51
56
  def can_be_skip(
@@ -58,7 +63,6 @@ class Scheduler(BaseModel):
58
63
  **kwargs,
59
64
  ) -> tuple[bool, float]:
60
65
  diffpositions = np.asarray(diffpositions, dtype=float)
61
- print(diffpositions)
62
66
  assert 0 <= thetacutoff <= 180, "thetacutoff must be in 0-180"
63
67
  if isinstance(graph, str):
64
68
  if is_fixed is None:
@@ -106,7 +110,6 @@ class Scheduler(BaseModel):
106
110
  cos: np.ndarray = cosine_similarity(dR, dR_new)
107
111
  if np.max(cos) <= np.cos(np.deg2rad(thetacutoff)):
108
112
  self.dR[key] = np.vstack((dR, dR_new)) # row_stack
109
- print(self.dR[key])
110
113
  return False, np.max(cos)
111
114
  return True, np.max(cos)
112
115
 
@@ -151,5 +154,8 @@ if __name__ == "__main__":
151
154
  plt.title("Blue: Keep; Red: Skip")
152
155
 
153
156
  p = Path(__file__).with_suffix(".png")
154
- plt.savefig(p)
157
+ atomic_write(
158
+ p,
159
+ writer=lambda p: plt.savefig(p),
160
+ )
155
161
  obj.write_npz(p.with_suffix(".npz"))
@@ -10,7 +10,7 @@ from graphatoms.enterpoint.parallel._base import (
10
10
  )
11
11
  from graphatoms.enterpoint.parallel._dask import DaskExecutor
12
12
  from graphatoms.enterpoint.parallel._executorlib import ExecutorLibExecutor
13
- from graphatoms.enterpoint.parallel._ray import _RAY_AVAILABLE, RayExecutor, ray
13
+ from graphatoms.enterpoint.parallel._ray import RayExecutor, ray
14
14
 
15
15
  __all__ = [
16
16
  "SerialExecutor",
@@ -18,6 +18,14 @@ __all__ = [
18
18
  "ParallelExecutorABC",
19
19
  "get_executor",
20
20
  ]
21
+ BACKENDS = [
22
+ "serial",
23
+ "multiprocessing",
24
+ "ray",
25
+ "dask",
26
+ "executorlib",
27
+ "joblib",
28
+ ]
21
29
 
22
30
 
23
31
  def get_executor(
@@ -40,7 +48,11 @@ def get_executor(
40
48
  backend = str(backend).lower()
41
49
  if backend in ("ray",):
42
50
  return RayExecutor(nworkers=nworkers, **kwargs) # type: ignore[arg-type]
43
- elif backend in ("dask",):
51
+ # `ncpus_for_tasks` only applies to the ray backend; `RunnerABC` passes
52
+ # it unconditionally, so drop it for the other backends instead of
53
+ # forwarding it to `ProcessPoolExecutor`.
54
+ kwargs.pop("ncpus_for_tasks", None)
55
+ if backend in ("dask",):
44
56
  return DaskExecutor(nworkers=nworkers, **kwargs) # type: ignore[arg-type]
45
57
  elif backend in ("executorlib",):
46
58
  return ExecutorLibExecutor( # type: ignore[arg-type]
@@ -53,10 +65,7 @@ def get_executor(
53
65
  nworkers = None
54
66
  return MultiprocessingExecutor(nworkers=nworkers, **kwargs) # type: ignore[call-arg]
55
67
  else:
56
- raise ValueError(
57
- f"Unknown backend: {backend} Available: "
58
- f"{['serial', 'multiprocessing', 'ray', 'dask', 'executorlib']}"
59
- )
68
+ raise ValueError(f"Unknown backend: {backend} Available: {BACKENDS}")
60
69
 
61
70
 
62
71
  def executor_map(
@@ -69,12 +78,19 @@ def executor_map(
69
78
  ) -> Sequence[Any]:
70
79
  if isinstance(executor, RayExecutor):
71
80
  dct: dict[int, ray.ObjectRef] = {}
81
+ # Hold a strong reference to every deduplicated object. Keying the
82
+ # cache on `id(v)` alone is only safe while the objects stay alive;
83
+ # if an intermediate object were collected, CPython could hand its
84
+ # `id` to a *different* object and the wrong value would be
85
+ # substituted into a task.
86
+ keepalive: list[Any] = []
72
87
  remote_params: list[dict[str, ray.ObjectRef]] = []
73
88
  for params in fn_params:
74
89
  item: dict[str, ray.ObjectRef] = {}
75
90
  for k, v in params.items():
76
91
  if id(v) not in dct:
77
92
  dct[id(v)] = ray.put(v)
93
+ keepalive.append(v)
78
94
  item[k] = dct[id(v)]
79
95
  remote_params.append(item)
80
96
  fn_params = remote_params
@@ -111,12 +111,21 @@ class MultiprocessingExecutor(ParallelExecutorABC):
111
111
  return_when=cf.FIRST_COMPLETED,
112
112
  timeout=timeout,
113
113
  )
114
- self.__executor.map
115
- return self.get(list(done)[0]), undone # type: ignore[override]
114
+ if len(done) == 0:
115
+ raise TimeoutError(
116
+ "concurrent.futures.wait() returned no finished task "
117
+ f"within timeout={timeout!r}."
118
+ )
119
+ # `FIRST_COMPLETED` may return *several* futures at once. Only one
120
+ # result can be returned by this API, so the other already-finished
121
+ # futures must be pushed back into the remaining list; otherwise their
122
+ # results are silently dropped by callers that loop until the list is
123
+ # empty.
124
+ done = list(done)
125
+ return self.get(done[0]), list(undone) + done[1:] # type: ignore[override]
116
126
 
117
127
  @override
118
128
  def map(self, fn, *iterables, timeout=None, chunksize=1) -> list[Any]:
119
- print(iterables)
120
129
  return list(
121
130
  self.__executor.map(
122
131
  fn,
@@ -128,6 +137,16 @@ class MultiprocessingExecutor(ParallelExecutorABC):
128
137
 
129
138
 
130
139
  class SerialExecutor(MultiprocessingExecutor):
140
+ """Run tasks one at a time.
141
+
142
+ .. warning::
143
+ This is *not* an in-process executor: it delegates to a
144
+ single-worker :class:`~concurrent.futures.ProcessPoolExecutor`, so
145
+ every task and its arguments must be picklable and each call runs in a
146
+ forked child process. See the project bug report for the implications
147
+ under Slurm.
148
+ """
149
+
131
150
  @override
132
- def __init__(self, *, nworkers=None, **kwargs) -> None:
133
- super().__init__(nworkers=1, **kwargs)
151
+ def __init__(self, *a, **kw) -> None:
152
+ super().__init__(nworkers=1)
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
  import weakref
3
5
  from collections.abc import Callable
@@ -18,6 +20,17 @@ if TYPE_CHECKING:
18
20
  import ray # type: ignore
19
21
 
20
22
 
23
+ ###############################################################################
24
+ # NOTE: `from __future__ import annotations` above is required, *not* optional.
25
+ # `ray` is an optional dependency (it is only a dev dependency in
26
+ # pyproject.toml), so when it is absent this module sets `ray = None`. Without
27
+ # postponed evaluation the `-> ray.ObjectRef` annotations below would be
28
+ # evaluated eagerly at class-definition time and raise
29
+ # `AttributeError: 'NoneType' object has no attribute 'ObjectRef'`, which makes
30
+ # `import graphatoms.enterpoint.parallel` (and therefore `graphatoms-run`)
31
+ # fail on any installation that does not happen to have ray installed.
32
+
33
+
21
34
  class RayExecutor(ParallelExecutorABC):
22
35
  """An executor backed by :mod:`ray`."""
23
36
 
@@ -35,15 +48,12 @@ class RayExecutor(ParallelExecutorABC):
35
48
  raise ImportError(
36
49
  "ray is not installed. Install it with: pip install ray"
37
50
  )
38
- try:
39
- if not ray.is_initialized(): # type: ignore
40
- ray.init( # type: ignore
41
- ignore_reinit_error=True,
42
- num_cpus=nworkers,
43
- **kwargs,
44
- )
45
- except Exception:
46
- pass
51
+ if not ray.is_initialized(): # type: ignore
52
+ ray.init( # type: ignore
53
+ ignore_reinit_error=True,
54
+ num_cpus=nworkers,
55
+ **kwargs,
56
+ )
47
57
 
48
58
  @override
49
59
  def submit(
@@ -68,7 +78,7 @@ class RayExecutor(ParallelExecutorABC):
68
78
  func_ref = weakref.ref(func) # type: ignore
69
79
  except TypeError:
70
80
 
71
- def func_ref(): # type: ignore
81
+ def func_ref() -> Callable[..., Any]: # type: ignore
72
82
  return func
73
83
 
74
84
  self._remote_fn_cache[key] = (func_ref, remote_fn) # type: ignore
@@ -90,8 +100,21 @@ class RayExecutor(ParallelExecutorABC):
90
100
  timeout: float | None = None,
91
101
  **kwargs,
92
102
  ) -> tuple[Any, list[ray.ObjectRef]]: # type: ignore
93
- done, unfinished = ray.wait(futures, timeout=timeout) # type: ignore
94
- return ray.get(done)[0], unfinished # type: ignore
103
+ done, unfinished = ray.wait( # type: ignore
104
+ futures,
105
+ timeout=timeout,
106
+ num_returns=1,
107
+ **kwargs,
108
+ )
109
+ if len(done) == 0:
110
+ raise TimeoutError(
111
+ "ray.wait() returned no finished task "
112
+ f"within timeout={timeout!r}."
113
+ )
114
+ # `ray.wait` defaults to `num_returns=1`, but a caller may ask for
115
+ # more. Keep every finished ref accounted for so that no completed
116
+ # result is silently dropped from the caller's work queue.
117
+ return ray.get(done[0]), list(unfinished) + list(done[1:]) # type: ignore
95
118
 
96
119
  @override
97
120
  def map(self, fn, *iterables, timeout=None, chunksize=1) -> list[Any]: