|
66 | 66 | import ase.neighborlist |
67 | 67 |
|
68 | 68 |
|
| 69 | +# Public output keys emitted by the graph-form AOTI forward |
| 70 | +# (``forward_lower_graph_exportable``) keyed by the output-variable category that |
| 71 | +# ``request_defs`` carries. The graph path is LOCAL-only (``N == sum(n_node)`` |
| 72 | +# nodes, no ghosts), so its outputs are already at local-atom resolution -- no |
| 73 | +# ``communicate_extended_output`` fold-back is needed. |
| 74 | +_GRAPH_CATEGORY_TO_KEY = { |
| 75 | + OutputVariableCategory.OUT: "atom_energy", |
| 76 | + OutputVariableCategory.REDU: "energy", |
| 77 | + OutputVariableCategory.DERV_R: "force", |
| 78 | + OutputVariableCategory.DERV_C_REDU: "virial", |
| 79 | + OutputVariableCategory.DERV_C: "atom_virial", |
| 80 | +} |
| 81 | + |
| 82 | + |
69 | 83 | def _reshape_charge_spin( |
70 | 84 | charge_spin: np.ndarray, nframes: int, dim_chg_spin: int |
71 | 85 | ) -> np.ndarray: |
@@ -1423,6 +1437,10 @@ def _eval_model( |
1423 | 1437 | request_defs: list[OutputVariableDef], |
1424 | 1438 | charge_spin: np.ndarray | None = None, |
1425 | 1439 | ) -> tuple[np.ndarray, ...]: |
| 1440 | + if self.metadata.get("lower_input_kind") == "graph": |
| 1441 | + return self._eval_model_graph( |
| 1442 | + coords, cells, atom_types, fparam, aparam, request_defs, charge_spin |
| 1443 | + ) |
1426 | 1444 | model_inputs, mapping_t, nframes, natoms = self._prepare_inputs( |
1427 | 1445 | coords, cells, atom_types, fparam, aparam, charge_spin |
1428 | 1446 | ) |
@@ -1621,6 +1639,101 @@ def _eval_model_spin( |
1621 | 1639 | ) |
1622 | 1640 | return tuple(results) |
1623 | 1641 |
|
| 1642 | + def _eval_model_graph( |
| 1643 | + self, |
| 1644 | + coords: np.ndarray, |
| 1645 | + cells: np.ndarray | None, |
| 1646 | + atom_types: np.ndarray, |
| 1647 | + fparam: np.ndarray | None, |
| 1648 | + aparam: np.ndarray | None, |
| 1649 | + request_defs: list[OutputVariableDef], |
| 1650 | + charge_spin: np.ndarray | None = None, |
| 1651 | + ) -> tuple[np.ndarray, ...]: |
| 1652 | + """Evaluate a graph-form ``.pt2`` (``lower_input_kind == "graph"``). |
| 1653 | +
|
| 1654 | + Builds a carry-all :class:`~deepmd.dpmodel.utils.neighbor_graph.NeighborGraph` |
| 1655 | + from the eval system at its exact (tight) edge count and feeds the |
| 1656 | + positional schema |
| 1657 | + ``(atype, n_node, edge_index, edge_vec, edge_mask, fparam, aparam, |
| 1658 | + charge_spin)`` to the exported forward. The AOTI artifact's edge axis |
| 1659 | + is DYNAMIC (B2.0), so no ``edge_capacity`` padding is needed. The |
| 1660 | + forward returns the LOCAL public keys directly, so results are reshaped |
| 1661 | + without ``communicate_extended_output``. |
| 1662 | + """ |
| 1663 | + from deepmd.dpmodel.utils.neighbor_graph import ( |
| 1664 | + build_neighbor_graph, |
| 1665 | + ) |
| 1666 | + from deepmd.pt_expt.utils.env import ( |
| 1667 | + DEVICE, |
| 1668 | + ) |
| 1669 | + |
| 1670 | + nframes = coords.shape[0] |
| 1671 | + if len(atom_types.shape) == 1: |
| 1672 | + natoms = len(atom_types) |
| 1673 | + atom_types = np.tile(atom_types, nframes).reshape(nframes, -1) |
| 1674 | + else: |
| 1675 | + natoms = len(atom_types[0]) |
| 1676 | + |
| 1677 | + coord_input = coords.reshape(nframes, natoms, 3) |
| 1678 | + box_input = cells.reshape(nframes, 9) if cells is not None else None |
| 1679 | + # Dynamic edge axis (B2.0): build the carry-all graph at its exact edge |
| 1680 | + # count (no static padding); the AOTI artifact accepts any E. |
| 1681 | + graph = build_neighbor_graph( |
| 1682 | + coord_input, |
| 1683 | + atom_types, |
| 1684 | + box_input, |
| 1685 | + self._rcut, |
| 1686 | + ) |
| 1687 | + |
| 1688 | + atype_t = torch.tensor( |
| 1689 | + np.asarray(atom_types).reshape(-1), dtype=torch.int64, device=DEVICE |
| 1690 | + ) |
| 1691 | + n_node_t = torch.tensor( |
| 1692 | + np.asarray(graph.n_node), dtype=torch.int64, device=DEVICE |
| 1693 | + ) |
| 1694 | + edge_index_t = torch.tensor( |
| 1695 | + np.asarray(graph.edge_index), dtype=torch.int64, device=DEVICE |
| 1696 | + ) |
| 1697 | + edge_vec_t = torch.tensor( |
| 1698 | + np.asarray(graph.edge_vec), dtype=torch.float64, device=DEVICE |
| 1699 | + ) |
| 1700 | + edge_mask_t = torch.tensor( |
| 1701 | + np.asarray(graph.edge_mask), dtype=torch.bool, device=DEVICE |
| 1702 | + ) |
| 1703 | + |
| 1704 | + fparam_t, aparam_t = self._prepare_optional_lower_inputs( |
| 1705 | + fparam, aparam, nframes, natoms, DEVICE |
| 1706 | + ) |
| 1707 | + charge_spin_t = self._make_charge_spin_input(nframes, charge_spin) |
| 1708 | + |
| 1709 | + model_inputs = ( |
| 1710 | + atype_t, |
| 1711 | + n_node_t, |
| 1712 | + edge_index_t, |
| 1713 | + edge_vec_t, |
| 1714 | + edge_mask_t, |
| 1715 | + fparam_t, |
| 1716 | + aparam_t, |
| 1717 | + charge_spin_t, |
| 1718 | + ) |
| 1719 | + if self._is_pt2: |
| 1720 | + model_ret = self._pt2_runner(*model_inputs) |
| 1721 | + else: |
| 1722 | + model_ret = self.exported_module(*model_inputs) |
| 1723 | + |
| 1724 | + results = [] |
| 1725 | + for odef in request_defs: |
| 1726 | + shape = self._get_output_shape(odef, nframes, natoms) |
| 1727 | + gkey = _GRAPH_CATEGORY_TO_KEY.get(odef.category) |
| 1728 | + val = model_ret.get(gkey) if gkey is not None else None |
| 1729 | + if val is not None: |
| 1730 | + results.append(val.detach().cpu().numpy().reshape(shape)) |
| 1731 | + else: |
| 1732 | + results.append( |
| 1733 | + np.full(np.abs(shape), np.nan, dtype=GLOBAL_NP_FLOAT_PRECISION) |
| 1734 | + ) |
| 1735 | + return tuple(results) |
| 1736 | + |
1624 | 1737 | def _get_output_shape( |
1625 | 1738 | self, odef: OutputVariableDef, nframes: int, natoms: int |
1626 | 1739 | ) -> list[int]: |
|
0 commit comments