GraphAtoms 0.0.1__tar.gz → 0.0.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.
- graphatoms-0.0.3/GraphAtoms/__init__.py +8 -0
- graphatoms-0.0.3/GraphAtoms/common/__init__.py +4 -0
- graphatoms-0.0.3/GraphAtoms/common/abc.py +392 -0
- graphatoms-0.0.3/GraphAtoms/common/error.py +16 -0
- graphatoms-0.0.3/GraphAtoms/common/geometry.py +376 -0
- graphatoms-0.0.3/GraphAtoms/common/ndarray.py +3 -0
- graphatoms-0.0.3/GraphAtoms/common/ndarray.pyi +5 -0
- graphatoms-0.0.3/GraphAtoms/common/rotation.py +101 -0
- graphatoms-0.0.3/GraphAtoms/common/string.py +138 -0
- graphatoms-0.0.3/GraphAtoms/containner/__init__.py +20 -0
- graphatoms-0.0.3/GraphAtoms/containner/_atomic.py +366 -0
- graphatoms-0.0.3/GraphAtoms/containner/_bonded.py +90 -0
- graphatoms-0.0.3/GraphAtoms/containner/_graph.py +704 -0
- graphatoms-0.0.3/GraphAtoms/containner/_mConnComp.py +45 -0
- graphatoms-0.0.3/GraphAtoms/containner/_mDataPyG.py +87 -0
- graphatoms-0.0.3/GraphAtoms/containner/_mFreeE.py +117 -0
- graphatoms-0.0.3/GraphAtoms/containner/_mIGraph.py +269 -0
- graphatoms-0.0.3/GraphAtoms/containner/_mNetworKit.py +12 -0
- graphatoms-0.0.3/GraphAtoms/containner/_mNetworkX.py +12 -0
- graphatoms-0.0.3/GraphAtoms/containner/_mRDKit.py +337 -0
- graphatoms-0.0.3/GraphAtoms/containner/_mRustworkX.py +12 -0
- graphatoms-0.0.3/GraphAtoms/containner/_sysCluster.py +121 -0
- graphatoms-0.0.3/GraphAtoms/containner/_sysGas.py +81 -0
- graphatoms-0.0.3/GraphAtoms/containner/_system.py +15 -0
- {graphatoms-0.0.1 → graphatoms-0.0.3}/GraphAtoms.egg-info/PKG-INFO +20 -12
- graphatoms-0.0.3/GraphAtoms.egg-info/SOURCES.txt +37 -0
- {graphatoms-0.0.1 → graphatoms-0.0.3}/GraphAtoms.egg-info/requires.txt +22 -10
- {graphatoms-0.0.1 → graphatoms-0.0.3}/PKG-INFO +20 -12
- {graphatoms-0.0.1 → graphatoms-0.0.3}/pyproject.toml +29 -17
- {graphatoms-0.0.1 → graphatoms-0.0.3}/tests/test_abc.py +35 -2
- graphatoms-0.0.3/tests/test_rotation.py +44 -0
- graphatoms-0.0.1/GraphAtoms.egg-info/SOURCES.txt +0 -12
- {graphatoms-0.0.1 → graphatoms-0.0.3}/GraphAtoms.egg-info/dependency_links.txt +0 -0
- {graphatoms-0.0.1 → graphatoms-0.0.3}/GraphAtoms.egg-info/top_level.txt +0 -0
- {graphatoms-0.0.1 → graphatoms-0.0.3}/LICENSE +0 -0
- {graphatoms-0.0.1 → graphatoms-0.0.3}/README.md +0 -0
- {graphatoms-0.0.1 → graphatoms-0.0.3}/setup.cfg +0 -0
- {graphatoms-0.0.1 → graphatoms-0.0.3}/tests/test_container.py +0 -0
- {graphatoms-0.0.1 → graphatoms-0.0.3}/tests/test_mRDKit.py +0 -0
- {graphatoms-0.0.1 → graphatoms-0.0.3}/tests/test_utils.py +0 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# ruff: noqa: D100 D102
|
|
2
|
+
import sys
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from pickle import dumps, loads
|
|
5
|
+
from typing import Annotated, override
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pydantic
|
|
9
|
+
import tomli_w as toml_w
|
|
10
|
+
import yaml
|
|
11
|
+
from joblib import dump, load
|
|
12
|
+
from numpy.typing import ArrayLike
|
|
13
|
+
from typing_extensions import Self
|
|
14
|
+
|
|
15
|
+
from GraphAtoms.common.string import (
|
|
16
|
+
SUPPORTED_COMPRESS_FORMATS,
|
|
17
|
+
compress,
|
|
18
|
+
decompress,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if sys.version_info < (3, 11):
|
|
22
|
+
import tomli as tomllib
|
|
23
|
+
else:
|
|
24
|
+
import tomllib
|
|
25
|
+
__all__ = [
|
|
26
|
+
"BaseModel",
|
|
27
|
+
"NpzPklBaseModel",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BaseMixin:
|
|
32
|
+
"""The base mixin class which provides some useful classmethods."""
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def get_mask_or_index(k: ArrayLike, n: int) -> np.ndarray:
|
|
36
|
+
if np.isscalar(k):
|
|
37
|
+
if isinstance(k, bool):
|
|
38
|
+
k = [k] * n
|
|
39
|
+
elif isinstance(k, int):
|
|
40
|
+
k = [k]
|
|
41
|
+
else:
|
|
42
|
+
raise TypeError(f"Unsupported type({type(k)}): {k}.")
|
|
43
|
+
subset = np.asarray(k)
|
|
44
|
+
if subset.dtype == bool:
|
|
45
|
+
if subset.size != n:
|
|
46
|
+
raise KeyError(
|
|
47
|
+
f"Except {n} boolean array, but {subset.size} got."
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
subset = np.unique(subset.astype(int))
|
|
51
|
+
if not 0 <= max(subset) < n:
|
|
52
|
+
raise KeyError(
|
|
53
|
+
f"Except 0-{n - 1} integer array, but the "
|
|
54
|
+
f"max({max(subset)}),min({min(subset)}) got."
|
|
55
|
+
)
|
|
56
|
+
return subset.flatten()
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def get_index(cls, k: ArrayLike, n: int) -> np.ndarray:
|
|
60
|
+
result = cls.get_mask_or_index(k=k, n=n)
|
|
61
|
+
if result.dtype == bool:
|
|
62
|
+
result = np.arange(n)[result]
|
|
63
|
+
return result.astype(int, copy=False)
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def get_mask(cls, k: ArrayLike, n: int) -> np.ndarray:
|
|
67
|
+
result = cls.get_mask_or_index(k=k, n=n)
|
|
68
|
+
if result.dtype != bool:
|
|
69
|
+
result = np.isin(np.arange(n), result)
|
|
70
|
+
return result.astype(bool, copy=False)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class BaseModel(pydantic.BaseModel):
|
|
74
|
+
"""A base class for creating Pydantic models.
|
|
75
|
+
|
|
76
|
+
This class support many file formats:
|
|
77
|
+
json by `pydantic`
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
model_config = pydantic.ConfigDict(frozen=True)
|
|
81
|
+
|
|
82
|
+
@override
|
|
83
|
+
def __str__(self) -> str:
|
|
84
|
+
return f"{self.__class__.__name__}({self._string()})"
|
|
85
|
+
|
|
86
|
+
def _string(self) -> str:
|
|
87
|
+
"""The string representation of this class.
|
|
88
|
+
|
|
89
|
+
The expression like `str(obj)` will output
|
|
90
|
+
`f"{self.__class__.__name__}({this_value})"`.
|
|
91
|
+
"""
|
|
92
|
+
return "object"
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
@pydantic.validate_call
|
|
96
|
+
def __get_format(fname: pydantic.FilePath | pydantic.NewPath) -> str:
|
|
97
|
+
format: str = fname.name.split(".")[-1].lower()
|
|
98
|
+
if format == "yml":
|
|
99
|
+
format = "yaml"
|
|
100
|
+
elif format == "pkl":
|
|
101
|
+
format = "pickle"
|
|
102
|
+
return format
|
|
103
|
+
|
|
104
|
+
def write(self, fname: pydantic.FilePath, **kwargs) -> pydantic.FilePath:
|
|
105
|
+
f = getattr(self, f"write_{self.__get_format(fname)}")
|
|
106
|
+
return f(fname, **kwargs)
|
|
107
|
+
|
|
108
|
+
@pydantic.validate_call
|
|
109
|
+
def as_bytes(
|
|
110
|
+
self,
|
|
111
|
+
compressformat: SUPPORTED_COMPRESS_FORMATS = "snappy",
|
|
112
|
+
compresslevel: Annotated[int, pydantic.Field(ge=0, le=9)] = 0,
|
|
113
|
+
) -> bytes:
|
|
114
|
+
"""Return the json bytes of this object."""
|
|
115
|
+
return compress(
|
|
116
|
+
dumps(self.model_dump(exclude_none=True)),
|
|
117
|
+
format=compressformat, # type: ignore
|
|
118
|
+
compresslevel=compresslevel,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
@pydantic.validate_call
|
|
123
|
+
def from_bytes(
|
|
124
|
+
cls,
|
|
125
|
+
data: bytes,
|
|
126
|
+
compressformat: SUPPORTED_COMPRESS_FORMATS = "snappy",
|
|
127
|
+
) -> Self:
|
|
128
|
+
return cls.model_validate(
|
|
129
|
+
loads(
|
|
130
|
+
decompress(
|
|
131
|
+
data,
|
|
132
|
+
format=compressformat, # type: ignore
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@pydantic.validate_call
|
|
138
|
+
def write_json(
|
|
139
|
+
self,
|
|
140
|
+
filename: pydantic.FilePath | pydantic.NewPath,
|
|
141
|
+
indent: int | None = 4,
|
|
142
|
+
**kwargs,
|
|
143
|
+
) -> pydantic.FilePath:
|
|
144
|
+
filename.write_text(
|
|
145
|
+
self.model_dump_json(
|
|
146
|
+
indent=indent,
|
|
147
|
+
exclude_none=True,
|
|
148
|
+
**kwargs,
|
|
149
|
+
),
|
|
150
|
+
encoding="utf-8",
|
|
151
|
+
)
|
|
152
|
+
return filename
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
@pydantic.validate_call
|
|
156
|
+
def read_json(cls, filename: pydantic.FilePath, **kwargs) -> Self:
|
|
157
|
+
return cls.model_validate_json(filename.read_bytes(), **kwargs)
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def read(cls, fname: pydantic.FilePath, **kwargs) -> Self:
|
|
161
|
+
f = getattr(cls, f"read_{cls.__get_format(fname)}")
|
|
162
|
+
return f(fname, **kwargs)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ExtendedBaseModel(BaseModel):
|
|
166
|
+
"""A extended base class for creating Pydantic models.
|
|
167
|
+
|
|
168
|
+
This class support many file formats:
|
|
169
|
+
json by `pydantic`
|
|
170
|
+
yaml/yml by `pyyaml`
|
|
171
|
+
toml by `tomli_w`, `tomli`, & `toml`
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
@pydantic.validate_call
|
|
175
|
+
def write_toml(
|
|
176
|
+
self,
|
|
177
|
+
filename: pydantic.FilePath | pydantic.NewPath,
|
|
178
|
+
indent: int = 4,
|
|
179
|
+
**kwargs,
|
|
180
|
+
) -> pydantic.FilePath:
|
|
181
|
+
kwargs["exclude_none"] = True
|
|
182
|
+
with filename.open("wb") as f:
|
|
183
|
+
toml_w.dump(
|
|
184
|
+
self.model_dump(mode="python", **kwargs),
|
|
185
|
+
f,
|
|
186
|
+
indent=indent,
|
|
187
|
+
multiline_strings=True,
|
|
188
|
+
)
|
|
189
|
+
return filename
|
|
190
|
+
|
|
191
|
+
@pydantic.validate_call
|
|
192
|
+
def write_yaml(
|
|
193
|
+
self,
|
|
194
|
+
filename: pydantic.FilePath | pydantic.NewPath,
|
|
195
|
+
indent: int | None = 2,
|
|
196
|
+
**kwargs,
|
|
197
|
+
) -> pydantic.FilePath:
|
|
198
|
+
with filename.open("w", encoding="utf-8") as f:
|
|
199
|
+
yaml.safe_dump(
|
|
200
|
+
self.model_dump(mode="python", **kwargs),
|
|
201
|
+
f,
|
|
202
|
+
encoding="utf-8",
|
|
203
|
+
default_flow_style=False,
|
|
204
|
+
indent=indent,
|
|
205
|
+
)
|
|
206
|
+
return filename
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
@pydantic.validate_call
|
|
210
|
+
def read_yaml(cls, filename: pydantic.FilePath, **kwargs) -> Self:
|
|
211
|
+
with filename.open(encoding="utf-8") as f:
|
|
212
|
+
data = yaml.safe_load(f)
|
|
213
|
+
return cls.model_validate(data, **kwargs)
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
@pydantic.validate_call
|
|
217
|
+
def read_toml(cls, filename: pydantic.FilePath, **kwargs) -> Self:
|
|
218
|
+
with filename.open("rb") as f:
|
|
219
|
+
data = tomllib.load(f)
|
|
220
|
+
return cls.model_validate(data, **kwargs)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class NpzPklBaseModel(BaseModel):
|
|
224
|
+
"""A extended base class for creating Pydantic models.
|
|
225
|
+
|
|
226
|
+
This class support many file formats:
|
|
227
|
+
json by `pydantic`
|
|
228
|
+
pickle by `joblib`
|
|
229
|
+
npz by `numpy`
|
|
230
|
+
|
|
231
|
+
Note: only numpy ndarray & numpy-compatible scalar value supported.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
@classmethod
|
|
235
|
+
def _convert(cls) -> dict[str, tuple[tuple, str]]:
|
|
236
|
+
"""Override if needed to specify the dtype and shape of attributes.
|
|
237
|
+
|
|
238
|
+
Note: must be implemented by children class.
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
return dict(a=((None, 3), "uint8"))
|
|
242
|
+
This `None` indicates it can take any shape along the first axis.
|
|
243
|
+
"""
|
|
244
|
+
return {}
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def _npz_available(cls) -> bool:
|
|
248
|
+
return True
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def __validate_ndarray_and_convert( # noqa: D103
|
|
252
|
+
data: np.ndarray | Sequence,
|
|
253
|
+
shape: Sequence[int | None],
|
|
254
|
+
dtype: str,
|
|
255
|
+
) -> np.ndarray:
|
|
256
|
+
assert shape.count(None) <= 1, (type(shape), shape)
|
|
257
|
+
shape = tuple(int(i) if i is not None else -1 for i in shape)
|
|
258
|
+
if not isinstance(data, np.ndarray):
|
|
259
|
+
data = np.asarray(data)
|
|
260
|
+
return np.asarray(data, dtype).reshape(shape)
|
|
261
|
+
|
|
262
|
+
@pydantic.model_validator(mode="before")
|
|
263
|
+
@classmethod
|
|
264
|
+
def __convert(cls, data):
|
|
265
|
+
if isinstance(data, dict):
|
|
266
|
+
for key, (shape, dtype) in cls._convert().items():
|
|
267
|
+
if key in data and data[key] is not None:
|
|
268
|
+
kw = dict(data=data[key], shape=shape, dtype=dtype)
|
|
269
|
+
data[key] = cls.__validate_ndarray_and_convert(**kw) # type: ignore
|
|
270
|
+
return data
|
|
271
|
+
|
|
272
|
+
@pydantic.model_validator(mode="after")
|
|
273
|
+
def __check_shape(self) -> Self:
|
|
274
|
+
for k in self.__pydantic_fields__:
|
|
275
|
+
v = getattr(self, k, None)
|
|
276
|
+
if v is None or np.isscalar(v) or isinstance(v, np.ndarray):
|
|
277
|
+
pass
|
|
278
|
+
else:
|
|
279
|
+
raise ValueError(f"Invalid value: {v}")
|
|
280
|
+
return self
|
|
281
|
+
|
|
282
|
+
@override
|
|
283
|
+
def __eq__(self, other) -> bool:
|
|
284
|
+
if not isinstance(other, self.__class__):
|
|
285
|
+
return False
|
|
286
|
+
for k in self.__pydantic_fields__:
|
|
287
|
+
v0 = getattr(self, k, None)
|
|
288
|
+
v1 = getattr(other, k, None)
|
|
289
|
+
if v0 is None or np.isscalar(v0):
|
|
290
|
+
if v0 is None or np.isscalar(v0):
|
|
291
|
+
if v0 != v1:
|
|
292
|
+
return False
|
|
293
|
+
else:
|
|
294
|
+
return False
|
|
295
|
+
elif isinstance(v0, np.ndarray):
|
|
296
|
+
if isinstance(v1, np.ndarray):
|
|
297
|
+
if v0.shape != v1.shape:
|
|
298
|
+
return False
|
|
299
|
+
elif np.any(v0 != v1):
|
|
300
|
+
return False
|
|
301
|
+
else:
|
|
302
|
+
return False
|
|
303
|
+
else:
|
|
304
|
+
raise NotImplementedError(f"Field({k}): {type(v0)}")
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
@pydantic.validate_call
|
|
308
|
+
def write_pickle(
|
|
309
|
+
self,
|
|
310
|
+
filename: pydantic.FilePath | pydantic.NewPath,
|
|
311
|
+
compress: bool | int | tuple[str, int] = 3,
|
|
312
|
+
**kwargs,
|
|
313
|
+
) -> pydantic.FilePath:
|
|
314
|
+
"""Persist the dictionary of this object into one file.
|
|
315
|
+
|
|
316
|
+
Read more in the reference:
|
|
317
|
+
https://joblib.readthedocs.io/en/latest/generated/joblib.dump.html
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
filename (str | Path | TextIOWrapper): The file object or path
|
|
321
|
+
of the file in which it is to be stored. The compression
|
|
322
|
+
method corresponding to one of the supported filename
|
|
323
|
+
extensions ('.z', '.gz', '.bz2', '.xz' or '.lzma')
|
|
324
|
+
will be used automatically.
|
|
325
|
+
compress (bool | int | tuple[str, int], optional):
|
|
326
|
+
Optional compression level for the data.
|
|
327
|
+
0 or False is no compression.
|
|
328
|
+
Higher value means more compression, but also
|
|
329
|
+
slower read and write times.
|
|
330
|
+
Using a value of 3 is often a good compromise.
|
|
331
|
+
Defaults to 3.
|
|
332
|
+
exclude_defaults (bool): ...
|
|
333
|
+
exclude_none (bool): ...
|
|
334
|
+
**kwargs: ...
|
|
335
|
+
|
|
336
|
+
Note:
|
|
337
|
+
If compress is True, the compression level used is 3.
|
|
338
|
+
If compress is a 2-tuple, the first element must correspond to
|
|
339
|
+
a string between supported compressors (e.g 'zlib', 'gzip',
|
|
340
|
+
'bz2', 'lzma' 'xz'), the second element must be an integer
|
|
341
|
+
from 0 to 9, corresponding to the compression level.
|
|
342
|
+
"""
|
|
343
|
+
kwargs["exclude_none"] = True
|
|
344
|
+
kwargs["exclude_defaults"] = False
|
|
345
|
+
dump(
|
|
346
|
+
self.model_dump(mode="python", **kwargs),
|
|
347
|
+
filename,
|
|
348
|
+
compress=compress, # type: ignore
|
|
349
|
+
)
|
|
350
|
+
return filename
|
|
351
|
+
|
|
352
|
+
@pydantic.validate_call
|
|
353
|
+
def write_npz(
|
|
354
|
+
self,
|
|
355
|
+
filename: pydantic.FilePath | pydantic.NewPath,
|
|
356
|
+
compress: bool = True,
|
|
357
|
+
**kwargs,
|
|
358
|
+
) -> pydantic.FilePath:
|
|
359
|
+
if not self._npz_available():
|
|
360
|
+
raise RuntimeError(
|
|
361
|
+
"Subclass must have class method named"
|
|
362
|
+
" `_npz_available()` to be True."
|
|
363
|
+
)
|
|
364
|
+
kwargs["exclude_none"] = True
|
|
365
|
+
kwargs["exclude_defaults"] = False
|
|
366
|
+
f_savez = np.savez_compressed if compress else np.savez
|
|
367
|
+
data = self.model_dump(mode="python", **kwargs)
|
|
368
|
+
dct: dict[str, ArrayLike] = {}
|
|
369
|
+
for k, v in data.items():
|
|
370
|
+
if not isinstance(v, dict):
|
|
371
|
+
dct[k] = v
|
|
372
|
+
f_savez(
|
|
373
|
+
filename,
|
|
374
|
+
allow_pickle=False,
|
|
375
|
+
**self.model_dump(mode="python", **kwargs),
|
|
376
|
+
)
|
|
377
|
+
return filename
|
|
378
|
+
|
|
379
|
+
@classmethod
|
|
380
|
+
@pydantic.validate_call
|
|
381
|
+
def read_npz(cls, filename: pydantic.FilePath, **kwargs) -> Self:
|
|
382
|
+
if not cls._npz_available():
|
|
383
|
+
raise RuntimeError(
|
|
384
|
+
"Subclass must have class method named"
|
|
385
|
+
" `_npz_available()` to be True."
|
|
386
|
+
)
|
|
387
|
+
return cls.model_validate(np.load(filename), **kwargs)
|
|
388
|
+
|
|
389
|
+
@classmethod
|
|
390
|
+
@pydantic.validate_call
|
|
391
|
+
def read_pickle(cls, filename: pydantic.FilePath, **kwargs) -> Self:
|
|
392
|
+
return cls.model_validate(load(filename), **kwargs)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# ruff: noqa: D100 D101 D102 D103
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NotSupportPeriodicSystem(KeyError):
|
|
5
|
+
"""The periodic system is not supported."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NotSupportNonOrthorhombicLattice(KeyError):
|
|
9
|
+
"""The non-orthorhombic lattice is not supported."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def not_support_type(data, required_type: str) -> TypeError:
|
|
13
|
+
return TypeError(
|
|
14
|
+
f"Only support type({required_type:s}). But"
|
|
15
|
+
f" the type of input is {type(data)} now."
|
|
16
|
+
)
|