Download model/nn/embedding/_edge.py from OneScience-Group/NequIP: direct link, hf CLI and curl.
- Browser
- Download file 8.35 kB
-
https://huggingface.co/OneScience-Group/NequIP/resolve/main/model/nn/embedding/_edge.py
- Command line
-
hf download hf://OneScience-Group/NequIP/model/nn/embedding/_edge.py
-
curl -L -o _edge.py https://huggingface.co/OneScience-Group/NequIP/resolve/main/model/nn/embedding/_edge.py
8.35 kB
| # This file is a part of the `nequip` package. Please see LICENSE and README at the root for information on using it. | |
| import torch | |
| from e3nn.o3._irreps import Irreps | |
| from e3nn.o3._spherical_harmonics import SphericalHarmonics | |
| from e3nn.util.jit import compile_mode | |
| from onescience.utils.nequip.internal.global_dtype import _GLOBAL_DTYPE | |
| from onescience.utils.nequip.internal.compile import conditional_torchscript_jit | |
| from onescience.datapipes.materials.nequip import AtomicDataDict | |
| from .._graph_mixin import GraphModuleMixin | |
| from ..utils import with_edge_vectors_, with_edge_type_ | |
| from .utils import cutoff_partialdict_to_tensor | |
| from typing import Optional, List, Dict, Union | |
| class EdgeLengthNormalizer(GraphModuleMixin, torch.nn.Module): | |
| num_types: int | |
| r_max: float | |
| _per_edge_type: bool | |
| def __init__( | |
| self, | |
| r_max: float, | |
| type_names: List[str], | |
| per_edge_type_cutoff: Optional[ | |
| Dict[str, Union[float, Dict[str, float]]] | |
| ] = None, | |
| # bookkeeping | |
| edge_type_field: str = AtomicDataDict.EDGE_TYPE_KEY, | |
| norm_length_field: str = AtomicDataDict.NORM_LENGTH_KEY, | |
| irreps_in=None, | |
| ): | |
| super().__init__() | |
| self.r_max = float(r_max) | |
| self.num_types = len(type_names) | |
| self.edge_type_field = edge_type_field | |
| self.norm_length_field = norm_length_field | |
| self._per_edge_type = False | |
| if per_edge_type_cutoff is not None: | |
| # process per_edge_type_cutoff | |
| self._per_edge_type = True | |
| per_edge_type_cutoff = cutoff_partialdict_to_tensor( | |
| per_edge_type_cutoff, type_names, self.r_max | |
| ) | |
| # compute 1/rmax and flatten for how they're used in forward, i.e. (n_type, n_type) -> (n_type^2,) | |
| rmax_recip = per_edge_type_cutoff.reciprocal().view(-1) | |
| else: | |
| rmax_recip = torch.as_tensor(1.0 / self.r_max, dtype=_GLOBAL_DTYPE) | |
| self.register_buffer("_rmax_recip", rmax_recip) | |
| irreps_out = {self.norm_length_field: Irreps([(1, (0, 1))])} | |
| if self._per_edge_type: | |
| irreps_out.update({self.edge_type_field: None}) | |
| self._init_irreps( | |
| irreps_in=irreps_in, | |
| irreps_out=irreps_out, | |
| ) | |
| def forward(self, data: AtomicDataDict.Type) -> AtomicDataDict.Type: | |
| # == get lengths with shape (num_edges, 1) == | |
| data = with_edge_vectors_(data, with_lengths=True) | |
| r = data[AtomicDataDict.EDGE_LENGTH_KEY].view(-1, 1) | |
| # == get norm == | |
| rmax_recip = self._rmax_recip | |
| if self._per_edge_type: | |
| # use helper to get edge types | |
| data = with_edge_type_(data, self.edge_type_field) | |
| edge_type = data[self.edge_type_field] | |
| # convert to row-major NxN matrix index with shape (num_edges,) | |
| edge_type_flat = edge_type[0] * self.num_types + edge_type[1] | |
| # (num_type^2,), (num_edges,) -> (num_edges, 1) | |
| rmax_recip = torch.index_select(rmax_recip, 0, edge_type_flat).unsqueeze(-1) | |
| data[self.norm_length_field] = r * rmax_recip | |
| return data | |
| class BesselEdgeLengthEncoding(GraphModuleMixin, torch.nn.Module): | |
| r"""Bessel edge length encoding. | |
| Args: | |
| num_bessels (int): number of Bessel basis functions | |
| trainable (bool): whether the :math:`n \pi` coefficients are trainable | |
| cutoff (torch.nn.Module): ``torch.nn.Module`` to apply a cutoff function that smoothly goes to zero at the cutoff radius | |
| """ | |
| def __init__( | |
| self, | |
| cutoff: torch.nn.Module, | |
| num_bessels: int = 8, | |
| trainable: bool = False, | |
| # bookkeeping | |
| edge_invariant_field: str = AtomicDataDict.EDGE_EMBEDDING_KEY, | |
| norm_length_field: str = AtomicDataDict.NORM_LENGTH_KEY, | |
| irreps_in=None, | |
| ): | |
| super().__init__() | |
| # === process inputs === | |
| self.cutoff = conditional_torchscript_jit(cutoff) | |
| self.num_bessels = num_bessels | |
| self.trainable = trainable | |
| self.edge_invariant_field = edge_invariant_field | |
| self.norm_length_field = norm_length_field | |
| # === bessel weights === | |
| bessel_weights = torch.linspace( | |
| start=1.0, | |
| end=self.num_bessels, | |
| steps=self.num_bessels, | |
| dtype=_GLOBAL_DTYPE, | |
| ).unsqueeze(0) # (1, num_bessel) | |
| if self.trainable: | |
| self.bessel_weights = torch.nn.Parameter(bessel_weights) | |
| else: | |
| self.register_buffer("bessel_weights", bessel_weights) | |
| self._init_irreps( | |
| irreps_in=irreps_in, | |
| irreps_out={ | |
| self.edge_invariant_field: Irreps([(self.num_bessels, (0, 1))]), | |
| AtomicDataDict.EDGE_CUTOFF_KEY: "0e", | |
| }, | |
| ) | |
| # i.e. `model_dtype` | |
| self._output_dtype = torch.get_default_dtype() | |
| def extra_repr(self) -> str: | |
| return f"num_bessels={self.num_bessels}" | |
| def forward(self, data: AtomicDataDict.Type) -> AtomicDataDict.Type: | |
| # == Bessel basis == | |
| x = data[self.norm_length_field] # (num_edges, 1) | |
| # (num_edges, 1), (1, num_bessel) -> (num_edges, num_bessel) | |
| bessel = (torch.sinc(x * self.bessel_weights) * self.bessel_weights).to( | |
| self._output_dtype | |
| ) | |
| # == polynomial cutoff == | |
| cutoff = self.cutoff(x).to(self._output_dtype) | |
| data[AtomicDataDict.EDGE_CUTOFF_KEY] = cutoff | |
| # == save product == | |
| data[self.edge_invariant_field] = bessel * cutoff | |
| return data | |
| class SphericalHarmonicEdgeAttrs(GraphModuleMixin, torch.nn.Module): | |
| """Construct edge attrs as spherical harmonic projections of edge vectors. | |
| Parameters follow ``e3nn.o3.spherical_harmonics``. | |
| Args: | |
| irreps_edge_sh (int, str, or o3.Irreps): if int, will be treated as lmax for o3.Irreps.spherical_harmonics(lmax) | |
| edge_sh_normalization (str): the normalization scheme to use | |
| edge_sh_normalize (bool, default: True): whether to normalize the spherical harmonics | |
| out_field (str, default: AtomicDataDict.EDGE_ATTRS_KEY: data/irreps field | |
| """ | |
| out_field: str | |
| def __init__( | |
| self, | |
| irreps_edge_sh: Union[int, str, Irreps], | |
| edge_sh_normalization: str = "component", | |
| edge_sh_normalize: bool = True, | |
| irreps_in=None, | |
| out_field: str = AtomicDataDict.EDGE_ATTRS_KEY, | |
| ): | |
| super().__init__() | |
| self.out_field = out_field | |
| if isinstance(irreps_edge_sh, int): | |
| self.irreps_edge_sh = Irreps.spherical_harmonics(irreps_edge_sh) | |
| else: | |
| self.irreps_edge_sh = Irreps(irreps_edge_sh) | |
| self._init_irreps( | |
| irreps_in=irreps_in, | |
| irreps_out={out_field: self.irreps_edge_sh}, | |
| ) | |
| self.sh = SphericalHarmonics( | |
| self.irreps_edge_sh, edge_sh_normalize, edge_sh_normalization | |
| ) | |
| # i.e. `model_dtype` | |
| self._output_dtype = torch.get_default_dtype() | |
| def forward(self, data: AtomicDataDict.Type) -> AtomicDataDict.Type: | |
| data = with_edge_vectors_(data, with_lengths=False) | |
| edge_vec = data[AtomicDataDict.EDGE_VECTORS_KEY] | |
| edge_sh = self.sh(edge_vec) | |
| data[self.out_field] = edge_sh.to(self._output_dtype) | |
| return data | |
| class AddRadialCutoffToData(GraphModuleMixin, torch.nn.Module): | |
| def __init__( | |
| self, | |
| cutoff: torch.nn.Module, | |
| norm_length_field: str = AtomicDataDict.NORM_LENGTH_KEY, | |
| irreps_in=None, | |
| ): | |
| super().__init__() | |
| self.cutoff = conditional_torchscript_jit(cutoff) | |
| self.norm_length_field = norm_length_field | |
| self._init_irreps( | |
| irreps_in=irreps_in, irreps_out={AtomicDataDict.EDGE_CUTOFF_KEY: "0e"} | |
| ) | |
| # i.e. `model_dtype` | |
| self._output_dtype = torch.get_default_dtype() | |
| def forward(self, data: AtomicDataDict.Type) -> AtomicDataDict.Type: | |
| if AtomicDataDict.EDGE_CUTOFF_KEY not in data: | |
| x = data[self.norm_length_field] | |
| cutoff = self.cutoff(x).to(self._output_dtype) | |
| data[AtomicDataDict.EDGE_CUTOFF_KEY] = cutoff | |
| return data | |