| |
|
|
| import torch |
|
|
| from e3nn.o3._irreps import Irreps |
| from e3nn.util.jit import compile_mode |
|
|
| from onescience.datapipes.materials.nequip import AtomicDataDict |
| from ._graph_mixin import GraphModuleMixin |
| from .model_modifier_utils import model_modifier, replace_submodules |
|
|
|
|
| @compile_mode("unsupported") |
| class PartialForceOutput(GraphModuleMixin, torch.nn.Module): |
| r"""Generate partial and total forces from an energy model. |
| |
| Args: |
| func: the energy model |
| vectorize: the vectorize option to ``torch.autograd.functional.jacobian``, |
| false by default since it doesn't work well. |
| """ |
|
|
| vectorize: bool |
|
|
| def __init__( |
| self, |
| func: GraphModuleMixin, |
| vectorize: bool = False, |
| vectorize_warnings: bool = False, |
| ): |
| super().__init__() |
| self.func = func |
| self.vectorize = vectorize |
| if vectorize_warnings: |
| |
| torch._C._debug_only_display_vmap_fallback_warnings(True) |
|
|
| |
| self._init_irreps( |
| irreps_in=func.irreps_in, |
| my_irreps_in={AtomicDataDict.PER_ATOM_ENERGY_KEY: Irreps("0e")}, |
| irreps_out=func.irreps_out, |
| ) |
| self.irreps_out[AtomicDataDict.PARTIAL_FORCE_KEY] = Irreps("1o") |
| self.irreps_out[AtomicDataDict.FORCE_KEY] = Irreps("1o") |
|
|
| def forward(self, data: AtomicDataDict.Type) -> AtomicDataDict.Type: |
| data = data.copy() |
| out_data = {} |
|
|
| def wrapper(pos: torch.Tensor) -> torch.Tensor: |
| """Wrapper from pos to atomic energy""" |
| nonlocal data, out_data |
| data[AtomicDataDict.POSITIONS_KEY] = pos |
| out_data = self.func(data) |
| return out_data[AtomicDataDict.PER_ATOM_ENERGY_KEY].squeeze(-1) |
|
|
| pos = data[AtomicDataDict.POSITIONS_KEY] |
|
|
| partial_forces = torch.autograd.functional.jacobian( |
| func=wrapper, |
| inputs=pos, |
| create_graph=self.training, |
| vectorize=self.vectorize, |
| ) |
| partial_forces = partial_forces.negative() |
| |
|
|
| out_data[AtomicDataDict.PARTIAL_FORCE_KEY] = partial_forces |
| out_data[AtomicDataDict.FORCE_KEY] = partial_forces.sum(dim=0) |
|
|
| return out_data |
|
|
|
|
| @compile_mode("script") |
| class ForceStressOutput(GraphModuleMixin, torch.nn.Module): |
| r"""Compute forces (and stress if cell is provided) using autograd of an energy model. |
| |
| See: |
| Knuth et. al. Comput. Phys. Commun 190, 33-50, 2015 |
| https://pure.mpg.de/rest/items/item_2085135_9/component/file_2156800/content |
| |
| Args: |
| func: the energy model to wrap |
| """ |
|
|
| do_derivatives: bool |
|
|
| def __init__(self, func: GraphModuleMixin, do_derivatives: bool = True): |
| super().__init__() |
| self.func = func |
| self.do_derivatives = do_derivatives |
|
|
| |
| self._init_irreps( |
| irreps_in=self.func.irreps_in.copy(), |
| irreps_out=self.func.irreps_out.copy(), |
| ) |
| self.irreps_out[AtomicDataDict.FORCE_KEY] = "1o" |
| self.irreps_out[AtomicDataDict.STRESS_KEY] = "1o" |
| self.irreps_out[AtomicDataDict.VIRIAL_KEY] = "1o" |
| self.irreps_out[AtomicDataDict.EDGE_FORCE_KEY] = "1o" |
|
|
| |
| self.register_buffer("_empty", torch.Tensor()) |
|
|
| def forward(self, data: AtomicDataDict.Type) -> AtomicDataDict.Type: |
| |
| if not self.do_derivatives: |
| return self.func(data) |
|
|
| |
| |
| |
| |
|
|
| |
| |
| if AtomicDataDict.EDGE_VECTORS_KEY not in data: |
| if AtomicDataDict.BATCH_KEY in data: |
| batch = data[AtomicDataDict.BATCH_KEY] |
| num_batch: int = AtomicDataDict.num_frames(data) |
| else: |
| |
| batch = self._empty |
| num_batch: int = 1 |
|
|
| pos = data[AtomicDataDict.POSITIONS_KEY] |
| has_cell: bool = AtomicDataDict.CELL_KEY in data |
|
|
| if has_cell: |
| orig_cell = data[AtomicDataDict.CELL_KEY] |
| |
| cell = orig_cell.view(-1, 3, 3).expand(num_batch, 3, 3) |
| data[AtomicDataDict.CELL_KEY] = cell |
| else: |
| |
| orig_cell = self._empty |
| cell = self._empty |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| if num_batch > 1: |
| displacement = torch.zeros( |
| (num_batch, 3, 3), |
| dtype=pos.dtype, |
| device=pos.device, |
| ) |
| else: |
| displacement = torch.zeros( |
| (3, 3), |
| dtype=pos.dtype, |
| device=pos.device, |
| ) |
| displacement.requires_grad_(True) |
| data["_displacement"] = displacement |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| symmetric_displacement = 0.5 * ( |
| displacement + displacement.transpose(-1, -2) |
| ) |
| did_pos_req_grad: bool = pos.requires_grad |
| pos.requires_grad_(True) |
| if num_batch > 1: |
| |
| |
| data[AtomicDataDict.POSITIONS_KEY] = pos + torch.bmm( |
| pos.unsqueeze(-2), |
| torch.index_select(symmetric_displacement, 0, batch), |
| ).squeeze(-2) |
| else: |
| |
| data[AtomicDataDict.POSITIONS_KEY] = pos + torch.sum( |
| pos.view(-1, 3, 1) * symmetric_displacement, 1 |
| ) |
| |
| |
| if has_cell: |
| |
| |
| |
| |
| |
| |
| |
| if num_batch > 1: |
| |
| data[AtomicDataDict.CELL_KEY] = cell + torch.bmm( |
| cell, symmetric_displacement |
| ) |
| else: |
| |
| data[AtomicDataDict.CELL_KEY] = ( |
| cell.view(3, 3) |
| + torch.sum(cell.view(3, 3, 1) * symmetric_displacement, 1) |
| ).view(1, 3, 3) |
|
|
| |
| data = self.func(data) |
|
|
| grads = torch.autograd.grad( |
| [data[AtomicDataDict.TOTAL_ENERGY_KEY].sum()], |
| [pos, data["_displacement"]], |
| create_graph=self.training, |
| ) |
|
|
| |
| forces = grads[0] |
| if forces is None: |
| |
| assert False, "failed to compute forces autograd" |
| forces = torch.neg(forces) |
| data[AtomicDataDict.FORCE_KEY] = forces |
|
|
| |
| virial = grads[1] |
| if virial is None: |
| |
| assert False, "failed to compute virial autograd" |
| virial = virial.view(num_batch, 3, 3) |
|
|
| |
| if has_cell: |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| volume = torch.linalg.det(cell).abs().unsqueeze(-1) |
|
|
| |
| |
| |
| |
|
|
| stress = virial / volume.view(num_batch, 1, 1) |
| data[AtomicDataDict.CELL_KEY] = orig_cell |
| else: |
| stress = self._empty |
| data[AtomicDataDict.STRESS_KEY] = stress |
|
|
| |
| |
| |
| |
| |
| virial = torch.neg(virial) |
| data[AtomicDataDict.VIRIAL_KEY] = virial |
|
|
| |
| del data["_displacement"] |
| if not did_pos_req_grad: |
| |
| pos.requires_grad_(False) |
|
|
| else: |
| |
| |
| |
|
|
| |
| edge_vectors = data[AtomicDataDict.EDGE_VECTORS_KEY] |
| edge_vectors.requires_grad_(True) |
| data[AtomicDataDict.EDGE_VECTORS_KEY] = edge_vectors |
|
|
| |
| data = self.func(data) |
| edge_forces = torch.autograd.grad( |
| [data[AtomicDataDict.TOTAL_ENERGY_KEY].sum()], |
| [edge_vectors], |
| |
| )[0] |
| |
| assert edge_forces is not None |
| |
| data[AtomicDataDict.EDGE_FORCE_KEY] = edge_forces |
|
|
| return data |
|
|
| @model_modifier(persistent=True, private=False) |
| @classmethod |
| def enable_ForceStressOutput(cls, model): |
| """Enable force and stress computation.""" |
|
|
| def factory(old): |
| new = cls(func=old.func, do_derivatives=True) |
| return new |
|
|
| return replace_submodules(model, cls, factory) |
|
|
| @model_modifier(persistent=True, private=False) |
| @classmethod |
| def disable_ForceStressOutput(cls, model): |
| """Disable force and stress computation.""" |
|
|
| def factory(old): |
| new = cls(func=old.func, do_derivatives=False) |
| return new |
|
|
| return replace_submodules(model, cls, factory) |
|
|