| |
| import torch |
|
|
| from onescience.datapipes.materials.nequip import AtomicDataDict |
| from onescience.utils.nequip.internal.aoti_metadata import NEQUIP_CUSTOM_OPS_LIBS_KEY |
| from ._graph_mixin import GraphModuleMixin |
|
|
| from typing import List, Dict, Any, Optional, Final |
|
|
|
|
| R_MAX_KEY: Final[str] = "r_max" |
| PER_EDGE_TYPE_CUTOFF_KEY: Final[str] = "per_edge_type_cutoff" |
| TYPE_NAMES_KEY: Final[str] = "type_names" |
| NUM_TYPES_KEY: Final[str] = "num_types" |
| MODEL_DTYPE_KEY: Final[str] = "model_dtype" |
|
|
|
|
| def _model_metadata_from_config(model_config: Dict[str, str]) -> Dict[str, str]: |
| model_metadata_dict = {} |
| |
| model_metadata_dict[MODEL_DTYPE_KEY] = model_config[MODEL_DTYPE_KEY] |
| model_metadata_dict[TYPE_NAMES_KEY] = " ".join(model_config[TYPE_NAMES_KEY]) |
| model_metadata_dict[NUM_TYPES_KEY] = str(len(model_config[TYPE_NAMES_KEY])) |
| model_metadata_dict[R_MAX_KEY] = str(model_config[R_MAX_KEY]) |
|
|
| if model_config.get(PER_EDGE_TYPE_CUTOFF_KEY, None) is not None: |
| from .embedding.utils import cutoff_partialdict_to_str |
|
|
| model_metadata_dict[PER_EDGE_TYPE_CUTOFF_KEY] = cutoff_partialdict_to_str( |
| model_config[PER_EDGE_TYPE_CUTOFF_KEY], |
| model_config[TYPE_NAMES_KEY], |
| model_config[R_MAX_KEY], |
| ) |
| return model_metadata_dict |
|
|
|
|
| class GraphModel(GraphModuleMixin, torch.nn.Module): |
| """Top-level module for any complete `nequip` model. |
| |
| Manages top-level rescaling, dtypes, and more. |
| |
| Args: |
| model (GraphModuleMixin): model to wrap |
| model_input_fields (Dict[str, Any]): input fields and their irreps |
| """ |
|
|
| model_input_fields: List[str] |
| is_graph_model: Final[bool] = True |
| is_compile_graph_model: Final[bool] = False |
| |
|
|
| _metadata: Dict[str, str] |
|
|
| def __init__( |
| self, |
| model: GraphModuleMixin, |
| model_config: Optional[Dict[str, str]] = None, |
| model_input_fields: Dict[str, Any] = {}, |
| ) -> None: |
| super().__init__() |
| irreps_in = { |
| |
| AtomicDataDict.POSITIONS_KEY: "1o", |
| AtomicDataDict.EDGE_INDEX_KEY: None, |
| AtomicDataDict.EDGE_TRANSPOSE_PERM_KEY: None, |
| AtomicDataDict.EDGE_CELL_SHIFT_KEY: None, |
| AtomicDataDict.EDGE_VECTORS_KEY: "1o", |
| AtomicDataDict.CELL_KEY: "1o", |
| AtomicDataDict.BATCH_KEY: None, |
| AtomicDataDict.NUM_NODES_KEY: None, |
| AtomicDataDict.ATOM_TYPE_KEY: None, |
| |
| AtomicDataDict.LMP_MLIAP_DATA_KEY: None, |
| AtomicDataDict.NUM_LOCAL_GHOST_NODES_KEY: None, |
| } |
| model_input_fields = AtomicDataDict._fix_irreps_dict(model_input_fields) |
| irreps_in.update(model_input_fields) |
| self._init_irreps(irreps_in=irreps_in, irreps_out=model.irreps_out) |
| for k, irreps in model.irreps_in.items(): |
| if self.irreps_in.get(k, None) != irreps: |
| raise RuntimeError( |
| f"Model has `{k}` in its irreps_in with irreps `{irreps}`, but `{k}` is missing from/has inconsistent irreps in model_input_fields of `{self.irreps_in.get(k, 'missing')}`" |
| ) |
| self.model = model |
| self.model_input_fields = list(self.irreps_in.keys()) |
|
|
| |
| self.model_dtype = torch.get_default_dtype() |
| self._metadata = {} |
| self.type_names = [] |
| if model_config is not None: |
| self._metadata = _model_metadata_from_config(model_config) |
| self.type_names = self._metadata[TYPE_NAMES_KEY].split(" ") |
| model_dtype = {"float32": torch.float32, "float64": torch.float64}[ |
| self._metadata[MODEL_DTYPE_KEY] |
| ] |
| assert self.model_dtype == model_dtype |
|
|
| @property |
| @torch.jit.unused |
| def metadata(self) -> Dict[str, str]: |
| """Get model metadata, including dynamic contributions from modules. |
| |
| Collects metadata from all modules that override ``_get_metadata_contributions()``. |
| Dynamic contributions can override static config values. |
| Expected to be queried for inference workflows (but not for ``nequip-package``). |
| """ |
| out = self._metadata.copy() |
|
|
| |
| contributed_keys = {} |
|
|
| for name, module in self.model.named_modules(): |
| if ( |
| hasattr(module, "_is_graph_module_mixin") |
| and module._is_graph_module_mixin |
| ): |
| contributions = module._get_metadata_contributions() |
| if not contributions: |
| continue |
|
|
| |
| for key in contributions: |
| if key in contributed_keys: |
| raise ValueError( |
| f"Metadata conflict: modules '{contributed_keys[key]}' " |
| f"and '{name}' both contribute key '{key}'" |
| ) |
| contributed_keys[key] = name |
|
|
| |
| out.update(contributions) |
|
|
| |
| if PER_EDGE_TYPE_CUTOFF_KEY in contributed_keys: |
| cutoff_values = [float(x) for x in out[PER_EDGE_TYPE_CUTOFF_KEY].split()] |
| out[R_MAX_KEY] = str(max(cutoff_values)) |
|
|
| |
| custom_ops_libs: set = set() |
| for m in self.model.modules(): |
| custom_ops_libs.update(getattr(m, "_nequip_custom_ops_libs", ())) |
| if custom_ops_libs: |
| out[NEQUIP_CUSTOM_OPS_LIBS_KEY] = " ".join(sorted(custom_ops_libs)) |
|
|
| return out |
|
|
| def forward(self, data: AtomicDataDict.Type) -> AtomicDataDict.Type: |
| |
| |
| new_data: AtomicDataDict.Type = {} |
| for k in self.model_input_fields: |
| if k in data: |
| new_data[k] = data[k] |
| return self.model(new_data) |
|
|