Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 42 additions & 12 deletions dpgen2/exploration/render/traj_render_lammps.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

import dpdata
import numpy as np
from dflow.python import (
FatalError,
)
from dflow.python.opio import (
HDF5Dataset,
)
Expand Down Expand Up @@ -60,20 +63,47 @@ def get_model_devi(

def _load_one_model_devi(self, fname, model_devi):
if isinstance(fname, HDF5Dataset):
dd = fname.get_data()
dd = np.asarray(fname.get_data())
else:
dd = np.loadtxt(fname)
if (
len(np.shape(dd)) == 1 # type: ignore
): # In case model-devi.out is 1-dimensional
dd = dd.reshape((1, len(dd))) # type: ignore

model_devi.add(DeviManager.MAX_DEVI_V, dd[:, 1]) # type: ignore
model_devi.add(DeviManager.MIN_DEVI_V, dd[:, 2]) # type: ignore
model_devi.add(DeviManager.AVG_DEVI_V, dd[:, 3]) # type: ignore
model_devi.add(DeviManager.MAX_DEVI_F, dd[:, 4]) # type: ignore
model_devi.add(DeviManager.MIN_DEVI_F, dd[:, 5]) # type: ignore
model_devi.add(DeviManager.AVG_DEVI_F, dd[:, 6]) # type: ignore
if dd.ndim == 1: # In case model-devi.out is 1-dimensional
dd = dd.reshape((1, len(dd)))

# A NaN or infinity would otherwise reach the report and scheduler, where
# comparisons silently produce invalid trust levels. Fail at the artifact
# boundary so users can inspect the corresponding LAMMPS task directly.
# Validate all six quantities exported by this loader, including virials
# that may be used by reports with virial trust levels configured.
deviation_names = (
DeviManager.MAX_DEVI_V,
DeviManager.MIN_DEVI_V,
DeviManager.AVG_DEVI_V,
DeviManager.MAX_DEVI_F,
DeviManager.MIN_DEVI_F,
DeviManager.AVG_DEVI_F,
)
invalid = np.argwhere(~np.isfinite(dd[:, 1:7]))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is one of the two that turn the pyright job red:

traj_render_lammps.py:82:44 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)

dd can be None because it comes from HDF5Dataset.get_data(), which is why the reshape above and all six model_devi.add(dd[:, N]) calls below carry # type: ignore. This new access is the only one without it. Line 85's for row, column in invalid draws the second error ("intp" is not iterable). The Type checker workflow is green on master, so both are new here.

Separately on scope: this slice covers the three virial columns, and MIN_DEVI_V and AVG_DEVI_V are read by no report class at all, while MAX_DEVI_V is only compared when virial trust levels are set, which they are not by default. I am not asking you to change it, but it is worth deciding deliberately: dd[:, 4] alone would still have closed #276, since that file's max_devi_f is -nan too.

@njzjz-bot njzjz-bot Oct 7, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4726860 and ea782dc without adding suppressions: normalize HDF5 data to an ndarray, then index the capped diagnostic coordinates explicitly. Actual CI exposed a NumPy 2.2.6 typing difference, so the follow-up was checked with that version too: full-repository pyright 1.1.318 now reports zero errors locally. I deliberately kept validation of all six quantities exported by this loader, including virial metrics, and documented that choice; all six are covered on both input paths. Large finite sentinel values remain accepted.

Agent: dot

if invalid.size:
locations = ", ".join(
f"row {int(invalid[index, 0]) + 1} "
f"({deviation_names[int(invalid[index, 1])]})"
for index in range(min(len(invalid), 10))
)
if len(invalid) > 10:
locations += f", ... ({len(invalid)} non-finite values in total)"
source = fname.key if isinstance(fname, HDF5Dataset) else fname
raise FatalError(
f"Non-finite model-deviation value in {source}: {locations}. "
"Inspect the LAMMPS/DeePMD task output before scheduling the "
"next exploration iteration."
)

model_devi.add(DeviManager.MAX_DEVI_V, dd[:, 1])
model_devi.add(DeviManager.MIN_DEVI_V, dd[:, 2])
model_devi.add(DeviManager.AVG_DEVI_V, dd[:, 3])
model_devi.add(DeviManager.MAX_DEVI_F, dd[:, 4])
model_devi.add(DeviManager.MIN_DEVI_F, dd[:, 5])
model_devi.add(DeviManager.AVG_DEVI_F, dd[:, 6])

def get_ele_temp(self, optional_outputs):
ele_temp = []
Expand Down
94 changes: 92 additions & 2 deletions tests/exploration/test_traj_render_lammps.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,109 @@
import json
import os
import unittest
from pathlib import (
Path,
)

import dpdata
import h5py
import numpy as np
from dflow.python import (
FatalError,
)
from dflow.python.opio import (
HDF5Dataset,
)

# isort: off
from .context import (
dpgen2,
)
from dpgen2.exploration.render import TrajRenderLammps
from dpgen2.exploration.deviation import DeviManager

# isort: on


class TestTrajRenderLammps(unittest.TestCase):
deviation_names = (
DeviManager.MAX_DEVI_V,
DeviManager.MIN_DEVI_V,
DeviManager.AVG_DEVI_V,
DeviManager.MAX_DEVI_F,
DeviManager.MIN_DEVI_F,
DeviManager.AVG_DEVI_F,
)

def test_rejects_non_finite_model_deviation(self):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not blocking, but this pins less than the PR claims. The description says all six columns and NaN and infinity; this covers one NaN in one column. Both of these mutations of the source survive the suite:

~np.isfinite(dd[:, 1:7])  ->  np.isnan(dd[:, 1:7])   Ran 3 tests ... OK
dd[:, 1:7]                ->  dd[:, 1:5]             Ran 3 tests ... OK

I checked the rest of tests/ too: no test anywhere puts an inf in any column, and none puts a non-finite value in any column other than max_devi_f. An inf case and one non-force column would close both gaps.

Credit where due -- the test is not vacuous. Removing the guard entirely does make it fail.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded in 4726860 to test NaN, +inf and -inf in each of the six columns, on both text and real HDF5 datasets. Added finite single/multi-row and DBL_MAX coverage. Both proposed mutations (isnan only and 1:5) now fail. Local expanded regression: 114 tests passed across exploration and SelectConfs; the focused module has 6 tests with 40 subtests. Pinned formatting/import checks and changed-file pyright pass.

Agent: dot

"""Reject NaN and either infinity in every exported column."""
model_devi = Path("model_devi.out")
for column, name in enumerate(self.deviation_names, start=1):
for value in (np.nan, np.inf, -np.inf):
with self.subTest(column=name, value=value):
data = np.zeros((2, 7))
data[1, column] = value
np.savetxt(model_devi, data)
with self.assertRaisesRegex(
FatalError,
rf"Non-finite model-deviation value in model_devi.out: row 2 \({name}\)",
):
TrajRenderLammps().get_model_devi([model_devi])

def test_rejects_non_finite_hdf5_model_deviation(self):
"""An HDF5 error names the dataset, not its Python object address."""
key = "task.000000/model_devi"
for column, name in enumerate(self.deviation_names, start=1):
for value in (np.nan, np.inf, -np.inf):
with self.subTest(column=name, value=value):
data = np.zeros((2, 7))
data[1, column] = value
with h5py.File("model_devi.h5", "w") as h5:
h5.create_dataset(key, data=data)
dataset = HDF5Dataset(h5, key)
with self.assertRaises(FatalError) as error:
TrajRenderLammps().get_model_devi([dataset])
message = str(error.exception)
self.assertIn(key, message)
self.assertIn(f"row 2 ({name})", message)
self.assertNotIn("object at", message)

def test_limits_non_finite_diagnostics(self):
"""Large failed trajectories give a bounded message and total count."""
model_devi = Path("model_devi.out")
data = np.full((20000, 7), np.nan)
data[:, 0] = np.arange(len(data))
np.savetxt(model_devi, data)
with self.assertRaises(FatalError) as error:
TrajRenderLammps().get_model_devi([model_devi])
message = str(error.exception)
self.assertEqual(message.count("row "), 10)
self.assertIn("120000 non-finite values in total", message)
self.assertLess(len(message), 1000)

def test_loads_finite_model_deviation(self):
"""Keep single/multiple rows and large finite sentinel values valid."""
for nrows in (1, 2):
for use_hdf5 in (False, True):
with self.subTest(nrows=nrows, use_hdf5=use_hdf5):
data = np.arange(nrows * 7, dtype=float).reshape(nrows, 7)
data[0, 4] = np.finfo(float).max
if use_hdf5:
with h5py.File("model_devi.h5", "w") as h5:
h5.create_dataset(
"model_devi", data=data[0] if nrows == 1 else data
)
source = HDF5Dataset(h5, "model_devi")
deviations = TrajRenderLammps().get_model_devi([source])
else:
source = Path("model_devi.out")
np.savetxt(source, data)
deviations = TrajRenderLammps().get_model_devi([source])
for column, name in enumerate(self.deviation_names, start=1):
np.testing.assert_array_equal(
deviations.get(name)[0], data[:, column]
)

def test_use_ele_temp_1(self):
with open("job.json", "w") as f:
json.dump({"ele_temp": 6.6}, f)
Expand Down Expand Up @@ -58,5 +147,6 @@ def test_use_ele_temp_2(self):
np.testing.assert_array_almost_equal(system.data["aparam"], np.array([[[6.6]]]))

def tearDown(self):
if os.path.exists("job.json"):
os.remove("job.json")
for filename in ("job.json", "model_devi.out", "model_devi.h5"):
if os.path.exists(filename):
os.remove(filename)
Loading