Skip to content

Commit e0bc0f0

Browse files
authored
merge devel to master and release 0.2.25 (#857)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved handling and validation of custom data types, including enhanced equality checks and string representations. * Added robust enforcement of the right-hand rule for lattice vectors and coordinates, and introduced cell rotation to lower-triangular form for LAMMPS output. * Enhanced VASP OUTCAR and XML parsing to better extract atom names, types, and handle the NWRITE parameter. * Broadened support for loading virial data from ASE Atoms objects. * **Bug Fixes** * Corrected per-atom property indexing when writing ABACUS STRU files. * **Chores** * Updated dependencies and workflow configurations for improved compatibility and reproducibility. * Added comprehensive test cases for new features, edge cases, and custom data type handling. * Included new VASP test data files for improved test coverage. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents 08aefba + efa66a7 commit e0bc0f0

22 files changed

+11528
-101
lines changed

.github/workflows/benchmark.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
uses: actions/setup-python@v5
1414
with:
1515
python-version: 3.12
16-
- uses: astral-sh/setup-uv@v5
16+
- uses: astral-sh/setup-uv@v6
1717
with:
1818
enable-cache: true
1919
cache-dependency-glob: |

.github/workflows/pyright.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
with:
1414
python-version: '3.12'
1515
- run: pip install uv
16-
- run: uv pip install --system -e .[amber,ase,pymatgen] rdkit openbabel-wheel
16+
- run: uv pip install --system -e .[amber,ase,pymatgen] 'rdkit<2025.3.3' openbabel-wheel
1717
- uses: jakebailey/pyright-action@v2
1818
with:
1919
version: 1.1.363

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ jobs:
1818
uses: actions/setup-python@v5
1919
with:
2020
python-version: ${{ matrix.python-version }}
21-
- uses: astral-sh/setup-uv@v5
21+
- uses: astral-sh/setup-uv@v6
2222
with:
2323
enable-cache: true
2424
cache-dependency-glob: |
2525
**/requirements*.txt
2626
**/pyproject.toml
2727
cache-suffix: "py${{ matrix.python-version }}"
2828
- name: Install dependencies
29-
run: uv pip install --system .[test,amber,ase,pymatgen] coverage ./tests/plugin rdkit openbabel-wheel
29+
run: uv pip install --system .[test,amber,ase,pymatgen] coverage ./tests/plugin 'rdkit<2025.3.3' openbabel-wheel 'numpy<2.3'
3030
- name: Test
3131
run: cd tests && coverage run --source=../dpdata -m unittest && cd .. && coverage combine tests/.coverage && coverage report
3232
- name: Run codecov

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ repos:
2121
# Python
2222
- repo: https://github.com/astral-sh/ruff-pre-commit
2323
# Ruff version.
24-
rev: v0.11.0
24+
rev: v0.12.5
2525
hooks:
2626
- id: ruff
2727
args: ["--fix"]

docs/rtd_environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ channels:
33
- conda-forge
44
dependencies:
55
- python <3.13
6-
- mamba <2
6+
- jupyterlite-xeus
77
- pip:
88
- ..[docs]

dpdata/abacus/stru.py

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ def get_atom_mag_cartesian(atommag, angle1, angle2):
314314
]
315315

316316

317-
def get_carteisan_coords(coords, coord_type, celldm, cell):
317+
def get_cartesian_coords(coords, coord_type, celldm, cell):
318318
"""Transform the atomic coordinates to cartesian coordinates.
319319
320320
Args:
@@ -378,7 +378,7 @@ def parse_pos(coords_lines, atom_names, celldm, cell):
378378
parse_pos_oneline(coords_lines[line_idx])
379379
)
380380

381-
coords.append(get_carteisan_coords(np.array(pos), coord_type, celldm, cell))
381+
coords.append(get_cartesian_coords(np.array(pos), coord_type, celldm, cell))
382382

383383
move.append(imove)
384384
velocity.append(ivelocity)
@@ -422,6 +422,25 @@ def parse_pos(coords_lines, atom_names, celldm, cell):
422422
return atom_numbs, coords, move, mags, velocity, sc, lambda_
423423

424424

425+
def right_hand_rule(
426+
cell: np.ndarray, coord: np.ndarray
427+
) -> tuple[np.ndarray, np.ndarray]:
428+
"""Rotate the cell and coord to make the cell fit the right-hand rule.
429+
430+
Args:
431+
cell (np.ndarray): the cell vectors.
432+
coord (np.ndarray): the atomic coordinates in cartesian.
433+
434+
Returns
435+
-------
436+
tuple: the rotated cell and coord.
437+
"""
438+
if np.linalg.det(cell) < 0:
439+
cell = -cell
440+
coord = -coord
441+
return cell, coord
442+
443+
425444
def get_frame_from_stru(stru):
426445
"""Read the ABACUS STRU file and return the dpdata frame.
427446
@@ -473,6 +492,7 @@ def get_frame_from_stru(stru):
473492
blocks["ATOMIC_POSITIONS"], atom_names, celldm, cell
474493
)
475494

495+
cell, coords = right_hand_rule(cell, coords)
476496
data = {
477497
"atom_names": atom_names,
478498
"atom_numbs": atom_numbs,
@@ -731,66 +751,68 @@ def process_file_input(file_input, atom_names, input_name):
731751
out += "0.0\n"
732752
out += str(data["atom_numbs"][iele]) + "\n"
733753
for iatom in range(data["atom_numbs"][iele]):
734-
iatomtype = np.nonzero(data["atom_types"] == iele)[0][iatom]
754+
iatomtype = np.nonzero(data["atom_types"] == iele)[0][
755+
iatom
756+
] # it is the atom index
735757
iout = f"{data['coords'][frame_idx][iatomtype, 0]:.12f} {data['coords'][frame_idx][iatomtype, 1]:.12f} {data['coords'][frame_idx][iatomtype, 2]:.12f}"
736758
# add flags for move, velocity, mag, angle1, angle2, and sc
737759
if move is not None:
738760
if (
739-
isinstance(ndarray2list(move[natom_tot]), (list, tuple))
740-
and len(move[natom_tot]) == 3
761+
isinstance(ndarray2list(move[iatomtype]), (list, tuple))
762+
and len(move[iatomtype]) == 3
741763
):
742764
iout += " " + " ".join(
743-
["1" if ii else "0" for ii in move[natom_tot]]
765+
["1" if ii else "0" for ii in move[iatomtype]]
744766
)
745-
elif isinstance(ndarray2list(move[natom_tot]), (int, float, bool)):
746-
iout += " 1 1 1" if move[natom_tot] else " 0 0 0"
767+
elif isinstance(ndarray2list(move[iatomtype]), (int, float, bool)):
768+
iout += " 1 1 1" if move[iatomtype] else " 0 0 0"
747769
else:
748770
iout += " 1 1 1"
749771

750772
if (
751773
velocity is not None
752-
and isinstance(ndarray2list(velocity[natom_tot]), (list, tuple))
753-
and len(velocity[natom_tot]) == 3
774+
and isinstance(ndarray2list(velocity[iatomtype]), (list, tuple))
775+
and len(velocity[iatomtype]) == 3
754776
):
755-
iout += " v " + " ".join([f"{ii:.12f}" for ii in velocity[natom_tot]])
777+
iout += " v " + " ".join([f"{ii:.12f}" for ii in velocity[iatomtype]])
756778

757779
if mag is not None:
758-
if isinstance(ndarray2list(mag[natom_tot]), (list, tuple)) and len(
759-
mag[natom_tot]
780+
if isinstance(ndarray2list(mag[iatomtype]), (list, tuple)) and len(
781+
mag[iatomtype]
760782
) in [1, 3]:
761-
iout += " mag " + " ".join([f"{ii:.12f}" for ii in mag[natom_tot]])
762-
elif isinstance(ndarray2list(mag[natom_tot]), (int, float)):
763-
iout += " mag " + f"{mag[natom_tot]:.12f}"
783+
iout += " mag " + " ".join([f"{ii:.12f}" for ii in mag[iatomtype]])
784+
elif isinstance(ndarray2list(mag[iatomtype]), (int, float)):
785+
iout += " mag " + f"{mag[iatomtype]:.12f}"
764786

765787
if angle1 is not None and isinstance(
766-
ndarray2list(angle1[natom_tot]), (int, float)
788+
ndarray2list(angle1[iatomtype]), (int, float)
767789
):
768-
iout += " angle1 " + f"{angle1[natom_tot]:.12f}"
790+
iout += " angle1 " + f"{angle1[iatomtype]:.12f}"
769791

770792
if angle2 is not None and isinstance(
771-
ndarray2list(angle2[natom_tot]), (int, float)
793+
ndarray2list(angle2[iatomtype]), (int, float)
772794
):
773-
iout += " angle2 " + f"{angle2[natom_tot]:.12f}"
795+
iout += " angle2 " + f"{angle2[iatomtype]:.12f}"
774796

775797
if sc is not None:
776-
if isinstance(ndarray2list(sc[natom_tot]), (list, tuple)) and len(
777-
sc[natom_tot]
798+
if isinstance(ndarray2list(sc[iatomtype]), (list, tuple)) and len(
799+
sc[iatomtype]
778800
) in [1, 3]:
779801
iout += " sc " + " ".join(
780-
["1" if ii else "0" for ii in sc[natom_tot]]
802+
["1" if ii else "0" for ii in sc[iatomtype]]
781803
)
782-
elif isinstance(ndarray2list(sc[natom_tot]), (int, float, bool)):
783-
iout += " sc " + "1" if sc[natom_tot] else "0"
804+
elif isinstance(ndarray2list(sc[iatomtype]), (int, float, bool)):
805+
iout += " sc " + "1" if sc[iatomtype] else "0"
784806

785807
if lambda_ is not None:
786-
if isinstance(ndarray2list(lambda_[natom_tot]), (list, tuple)) and len(
787-
lambda_[natom_tot]
808+
if isinstance(ndarray2list(lambda_[iatomtype]), (list, tuple)) and len(
809+
lambda_[iatomtype]
788810
) in [1, 3]:
789811
iout += " lambda " + " ".join(
790-
[f"{ii:.12f}" for ii in lambda_[natom_tot]]
812+
[f"{ii:.12f}" for ii in lambda_[iatomtype]]
791813
)
792-
elif isinstance(ndarray2list(lambda_[natom_tot]), (int, float)):
793-
iout += " lambda " + f"{lambda_[natom_tot]:.12f}"
814+
elif isinstance(ndarray2list(lambda_[iatomtype]), (int, float)):
815+
iout += " lambda " + f"{lambda_[iatomtype]:.12f}"
794816

795817
out += iout + "\n"
796818
natom_tot += 1

dpdata/data_type.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,43 @@ def __init__(
6464
self.required = required
6565
self.deepmd_name = name if deepmd_name is None else deepmd_name
6666

67+
def __eq__(self, other) -> bool:
68+
"""Check if two DataType instances are equal.
69+
70+
Parameters
71+
----------
72+
other : object
73+
object to compare with
74+
75+
Returns
76+
-------
77+
bool
78+
True if equal, False otherwise
79+
"""
80+
if not isinstance(other, DataType):
81+
return False
82+
return (
83+
self.name == other.name
84+
and self.dtype == other.dtype
85+
and self.shape == other.shape
86+
and self.required == other.required
87+
and self.deepmd_name == other.deepmd_name
88+
)
89+
90+
def __repr__(self) -> str:
91+
"""Return string representation of DataType.
92+
93+
Returns
94+
-------
95+
str
96+
string representation
97+
"""
98+
return (
99+
f"DataType(name='{self.name}', dtype={self.dtype.__name__}, "
100+
f"shape={self.shape}, required={self.required}, "
101+
f"deepmd_name='{self.deepmd_name}')"
102+
)
103+
67104
def real_shape(self, system: System) -> tuple[int]:
68105
"""Returns expected real shape of a system."""
69106
assert self.shape is not None

dpdata/deepmd/mixed.py

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import numpy as np
66

7+
import dpdata
8+
79
from .comp import dump as comp_dump
810
from .comp import to_system_data as comp_to_system_data
911

@@ -27,10 +29,32 @@ def to_system_data(folder, type_map=None, labels=True):
2729
all_real_atom_types_concat = index_map[all_real_atom_types_concat]
2830
all_cells_concat = data["cells"]
2931
all_coords_concat = data["coords"]
32+
33+
# handle custom registered data types
3034
if labels:
31-
all_eners_concat = data.get("energies")
32-
all_forces_concat = data.get("forces")
33-
all_virs_concat = data.get("virials")
35+
dtypes = dpdata.system.LabeledSystem.DTYPES
36+
else:
37+
dtypes = dpdata.system.System.DTYPES
38+
reserved = {
39+
"atom_numbs",
40+
"atom_names",
41+
"atom_types",
42+
"real_atom_names",
43+
"real_atom_types",
44+
"cells",
45+
"coords",
46+
"orig",
47+
"nopbc",
48+
}
49+
extra_data = {}
50+
for dtype in dtypes:
51+
name = dtype.name
52+
if name in reserved:
53+
continue
54+
if not (len(dtype.shape) and dtype.shape[0] == dpdata.system.Axis.NFRAMES):
55+
continue
56+
if name in data:
57+
extra_data[name] = data.pop(name)
3458

3559
data_list = []
3660
while True:
@@ -56,16 +80,12 @@ def to_system_data(folder, type_map=None, labels=True):
5680
all_cells_concat = all_cells_concat[rest_idx]
5781
temp_data["coords"] = all_coords_concat[temp_idx]
5882
all_coords_concat = all_coords_concat[rest_idx]
59-
if labels:
60-
if all_eners_concat is not None and all_eners_concat.size > 0:
61-
temp_data["energies"] = all_eners_concat[temp_idx]
62-
all_eners_concat = all_eners_concat[rest_idx]
63-
if all_forces_concat is not None and all_forces_concat.size > 0:
64-
temp_data["forces"] = all_forces_concat[temp_idx]
65-
all_forces_concat = all_forces_concat[rest_idx]
66-
if all_virs_concat is not None and all_virs_concat.size > 0:
67-
temp_data["virials"] = all_virs_concat[temp_idx]
68-
all_virs_concat = all_virs_concat[rest_idx]
83+
84+
for name in extra_data:
85+
all_dtype_concat = extra_data[name]
86+
temp_data[name] = all_dtype_concat[temp_idx]
87+
extra_data[name] = all_dtype_concat[rest_idx]
88+
6989
data_list.append(temp_data)
7090
return data_list
7191

0 commit comments

Comments
 (0)