From 77b00ffafd12434aa3116698c236a27c47c18141 Mon Sep 17 00:00:00 2001 From: Sofiia Chorna Date: Tue, 13 Aug 2024 13:54:12 +0200 Subject: [PATCH] Create metatensor_featurizer function This allows taking a metatensor model that compute "features" and use it in chemiscope.explore --- README.md | 6 + docs/src/python/reference.rst | 2 + pyproject.toml | 5 + python/chemiscope/__init__.py | 2 +- .../{explore.py => explore/__init__.py} | 101 +------- python/chemiscope/explore/_metatensor.py | 148 ++++++++++++ python/chemiscope/explore/_soap_pca.py | 90 +++++++ python/examples/7-explore-advanced.py | 1 + python/examples/8-explore-with-metatensor.py | 223 ++++++++++++++++++ python/examples/model.pt | Bin 0 -> 35984 bytes tox.ini | 5 +- 11 files changed, 488 insertions(+), 95 deletions(-) rename python/chemiscope/{explore.py => explore/__init__.py} (60%) create mode 100644 python/chemiscope/explore/_metatensor.py create mode 100644 python/chemiscope/explore/_soap_pca.py create mode 100644 python/examples/8-explore-with-metatensor.py create mode 100644 python/examples/model.pt diff --git a/README.md b/README.md index 60d0bd28d..37272db17 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,12 @@ dependencies with: pip install chemiscope[explore] ``` +To use `chemiscope.metatensor_featurizer` for providing your trained model +to get the features for `chemiscope.explore`, install the dependencies with: +```bash +pip install chemiscope[metatensor] +``` + ## sphinx and sphinx-gallery integration Chemiscope provides also extensions for `sphinx` and `sphinx-gallery` to diff --git a/docs/src/python/reference.rst b/docs/src/python/reference.rst index 896bb2596..a65851ea3 100644 --- a/docs/src/python/reference.rst +++ b/docs/src/python/reference.rst @@ -26,3 +26,5 @@ .. autofunction:: chemiscope.ase_tensors_to_ellipsoids .. autofunction:: chemiscope.explore + +.. autofunction:: chemiscope.metatensor_featurizer diff --git a/pyproject.toml b/pyproject.toml index 4273534c7..ad3633332 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,3 +75,8 @@ explore = [ "dscribe", "scikit-learn", ] + +metatensor = [ + "metatensor", + "metatensor[torch]" +] diff --git a/python/chemiscope/__init__.py b/python/chemiscope/__init__.py index ddefa6805..9723959c1 100644 --- a/python/chemiscope/__init__.py +++ b/python/chemiscope/__init__.py @@ -11,7 +11,7 @@ extract_properties, librascal_atomic_environments, ) -from .explore import explore # noqa: F401 +from .explore import explore, metatensor_featurizer # noqa: F401 from .version import __version__ # noqa: F401 from .jupyter import show, show_input # noqa diff --git a/python/chemiscope/explore.py b/python/chemiscope/explore/__init__.py similarity index 60% rename from python/chemiscope/explore.py rename to python/chemiscope/explore/__init__.py index 5394fa934..e83612821 100644 --- a/python/chemiscope/explore.py +++ b/python/chemiscope/explore/__init__.py @@ -1,6 +1,8 @@ -import os +from ..jupyter import show +from ._soap_pca import soap_pca_featurize +from ._metatensor import metatensor_featurizer -from .jupyter import show +__all__ = ["explore", "metatensor_featurizer"] def explore(frames, featurize=None, properties=None, environments=None, mode="default"): @@ -116,96 +118,9 @@ def soap_kpca_featurize(frames, environments): # Add dimensionality reduction results to properties properties["features"] = X_reduced - # Return chemiscope widget return show( - frames=frames, properties=properties, mode=mode, environments=environments + frames=frames, + properties=properties, + environments=environments, + mode=mode, ) - - -def soap_pca_featurize(frames, environments=None): - """ - Computes SOAP features for a given set of atomic structures and performs - dimensionality reduction using PCA. Custom featurize functions should - have the same signature. - - Note: - - The SOAP descriptor parameters are pre-defined. - - We use all available CPU cores for parallel computation of SOAP descriptors. - """ - - # Check if dependencies were installed - try: - from dscribe.descriptors import SOAP - from sklearn.decomposition import PCA - except ImportError as e: - raise ImportError( - f"Required package not found: {str(e)}. Please install dependency " - + "using 'pip install chemiscope[explore]'." - ) - centers = None - - # Get the atom indexes from the environments and pick related frames - if environments is not None: - centers = _extract_environment_indices(environments) - - # Pick frames and properties related to the environments if provided - if environments is not None: - # Sort environments by structure id and atom id - environments = sorted(environments, key=lambda x: (x[0], x[1])) - - # Check structure indexes - unique_structures = list({env[0] for env in environments}) - if any(index >= len(frames) for index in unique_structures): - raise IndexError( - "Some structure indices in 'environments' are larger than the number of" - "frames" - ) - - if len(unique_structures) != len(frames): - # only include frames that are present in the user-provided environments - frames = [frames[index] for index in unique_structures] - - # Get global species - species = set() - for frame in frames: - species.update(frame.get_chemical_symbols()) - species = list(species) - - # Check if periodic - is_periodic = all(all(frame.get_pbc()) for frame in frames) - - # Initialize calculator - soap = SOAP( - species=species, - r_cut=4.5, - n_max=8, - l_max=6, - sigma=0.2, - rbf="gto", - average="outer", - periodic=is_periodic, - weighting={"function": "pow", "c": 1, "m": 5, "d": 1, "r0": 3.5}, - compression={"mode": "mu1nu1"}, - ) - - # Calculate descriptors - n_jobs = min(len(frames), os.cpu_count()) - feats = soap.create(frames, centers=centers, n_jobs=n_jobs) - - # Compute pca - pca = PCA(n_components=2) - return pca.fit_transform(feats) - - -def _extract_environment_indices(envs): - """ - Convert from chemiscope's environements to DScribe's centers selection - - :param: list envs: each element is a list of [env_index, atom_index, cutoff] - """ - grouped_envs = {} - for [env_index, atom_index, _cutoff] in envs: - if env_index not in grouped_envs: - grouped_envs[env_index] = [] - grouped_envs[env_index].append(atom_index) - return list(grouped_envs.values()) diff --git a/python/chemiscope/explore/_metatensor.py b/python/chemiscope/explore/_metatensor.py new file mode 100644 index 000000000..cf7d2ce17 --- /dev/null +++ b/python/chemiscope/explore/_metatensor.py @@ -0,0 +1,148 @@ +import numpy as np + + +def metatensor_featurizer( + model, + extensions_directory=None, + check_consistency=False, + device=None, +): + """ + Create a featurizer function using a `metatensor`_ model to obtain the features from + structures. The model must be able to create a ``"feature"`` output. + + :param model: model to use for the calculation. It can be a file path, a Python + instance of :py:class:`metatensor.torch.atomistic.MetatensorAtomisticModel`, or + the output of :py:func:`torch.jit.script` on + :py:class:`metatensor.torch.atomistic.MetatensorAtomisticModel`. + :param extensions_directory: a directory where model extensions are located + :param check_consistency: should we check the model for consistency when running, + defaults to False. + :param device: a torch device to use for the calculation. If ``None``, the function + will use the options in model's ``supported_device`` attribute. + + :returns: a function that takes a list of frames and returns the features. + + To use this function, additional dependencies are required. They can be installed + with the following command: + + .. code:: bash + + pip install chemiscope[metatensor] + + Here is an example using a pre-trained `metatensor`_ model, stored as a ``model.pt`` + file with the compiled extensions stored in the ``extensions/`` directory. To obtain + the details on how to get it, see metatensor `tutorial + `_. The + frames are obtained by reading structures from a file that `ase `_ can + read. + + .. code-block:: python + + import chemiscope + import ase.io + + # Read the structures from the dataset frames = + ase.io.read("data/explore_c-gap-20u.xyz", ":") + + # Provide model file ("model.pt") to `metatensor_featurizer` + featurizer = chemiscope.metatensor_featurizer( + "model.pt", extensions_directory="extensions" + ) + + chemiscope.explore(frames, featurize=featurizer) + + For more examples, see the related :ref:`documentation + `. + + .. _metatensor: https://docs.metatensor.org/latest/index.html + .. _chemiscope-explore-metatensor: + https://chemiscope.org/docs/examples/7-explore-advanced.html#example-with-metatensor-model + """ + + # Check if dependencies were installed + try: + from metatensor.torch.atomistic import ModelOutput + from metatensor.torch.atomistic.ase_calculator import MetatensorCalculator + except ImportError as e: + raise ImportError( + f"Required package not found: {e}. Please install the dependency using " + "'pip install chemiscope[metatensor]'." + ) + + # Initialize metatensor calculator + CALCULATOR = MetatensorCalculator( + model=model, + extensions_directory=extensions_directory, + check_consistency=check_consistency, + device=device, + ) + + def get_features(atoms, environments): + """Run the model on a single atomic structure and extract the features""" + outputs = {"features": ModelOutput(per_atom=environments is not None)} + selected_atoms = _create_selected_atoms(environments) + output = CALCULATOR.run_model(atoms, outputs, selected_atoms) + + return output["features"].block().values.detach().cpu().numpy() + + def featurize(frames, environments): + if isinstance(frames, list): + envs_per_frame = _get_environments_per_frame(environments, len(frames)) + + outputs = [ + get_features(frame, envs) for frame, envs in zip(frames, envs_per_frame) + ] + return np.vstack(outputs) + else: + return get_features(frames, environments) + + return featurize + + +def _get_environments_per_frame(environments, num_frames): + """ + Organize the environments for each frame + + :param list environments: a list of atomic environments + :param int num_frames: total number of frames + """ + envs_per_frame = [None] * num_frames + + if environments is None: + return envs_per_frame + + frames_dict = {} + + # Group environments by structure_id + for env in environments: + structure_id = env[0] + if structure_id not in frames_dict: + frames_dict[structure_id] = [] + frames_dict[structure_id].append(env) + + # Insert environments to the frame indices + for structure_id, envs in frames_dict.items(): + if structure_id < num_frames: + envs_per_frame[structure_id] = envs + + return envs_per_frame + + +def _create_selected_atoms(environments): + """ + Convert environments into ``Labels`` object, to be used as ``selected_atoms`` + + :param environments: a list of atom-centered environments + """ + import torch + from metatensor.torch import Labels + + if environments is None: + return None + + # Extract system and atom indices from environments, overriding the structure id to + # be 0 (since we will only give a single frame to the calculator at the same time). + values = torch.tensor([(0, atom_id) for _, atom_id, _ in environments]) + + return Labels(names=["system", "atom"], values=values) diff --git a/python/chemiscope/explore/_soap_pca.py b/python/chemiscope/explore/_soap_pca.py new file mode 100644 index 000000000..6411c8732 --- /dev/null +++ b/python/chemiscope/explore/_soap_pca.py @@ -0,0 +1,90 @@ +import os + + +def soap_pca_featurize(frames, environments=None): + """ + Computes SOAP features for a given set of atomic structures and performs + dimensionality reduction using PCA. Custom featurize functions should + have the same signature. + + Note: + - The SOAP descriptor parameters are pre-defined. + - We use all available CPU cores for parallel computation of SOAP descriptors. + """ + + # Check if dependencies were installed + try: + from dscribe.descriptors import SOAP + from sklearn.decomposition import PCA + except ImportError as e: + raise ImportError( + f"Required package not found: {str(e)}. Please install dependency " + + "using 'pip install chemiscope[explore]'." + ) + centers = None + + # Get the atom indexes from the environments and pick related frames + if environments is not None: + centers = _extract_environment_indices(environments) + + # Pick frames and properties related to the environments if provided + if environments is not None: + # Sort environments by structure id and atom id + environments = sorted(environments, key=lambda x: (x[0], x[1])) + + # Check structure indexes + unique_structures = list({env[0] for env in environments}) + if any(index >= len(frames) for index in unique_structures): + raise IndexError( + "Some structure indices in 'environments' are larger than the number of" + "frames" + ) + + if len(unique_structures) != len(frames): + # only include frames that are present in the user-provided environments + frames = [frames[index] for index in unique_structures] + + # Get global species + species = set() + for frame in frames: + species.update(frame.get_chemical_symbols()) + species = list(species) + + # Check if periodic + is_periodic = all(all(frame.get_pbc()) for frame in frames) + + # Initialize calculator + soap = SOAP( + species=species, + r_cut=4.5, + n_max=8, + l_max=6, + sigma=0.2, + rbf="gto", + average="outer", + periodic=is_periodic, + weighting={"function": "pow", "c": 1, "m": 5, "d": 1, "r0": 3.5}, + compression={"mode": "mu1nu1"}, + ) + + # Calculate descriptors + n_jobs = min(len(frames), os.cpu_count()) + feats = soap.create(frames, centers=centers, n_jobs=n_jobs) + + # Compute pca + pca = PCA(n_components=2) + return pca.fit_transform(feats) + + +def _extract_environment_indices(environments): + """ + Convert from chemiscope's environments to DScribe's centers selection + + :param: list environments: each element is a list of [env_index, atom_index, cutoff] + """ + grouped_envs = {} + for [env_index, atom_index, _cutoff] in environments: + if env_index not in grouped_envs: + grouped_envs[env_index] = [] + grouped_envs[env_index].append(atom_index) + return list(grouped_envs.values()) diff --git a/python/examples/7-explore-advanced.py b/python/examples/7-explore-advanced.py index a57250c58..4d77360f9 100644 --- a/python/examples/7-explore-advanced.py +++ b/python/examples/7-explore-advanced.py @@ -186,6 +186,7 @@ def mace_mp0_tsne(frames, environments): fetch_dataset("mace-mp-tsne-m3cd.json.gz") chemiscope.show_input("data/mace-mp-tsne-m3cd.json.gz") + # %% # # Example with SOAP, t-SNE and environments diff --git a/python/examples/8-explore-with-metatensor.py b/python/examples/8-explore-with-metatensor.py new file mode 100644 index 000000000..135744110 --- /dev/null +++ b/python/examples/8-explore-with-metatensor.py @@ -0,0 +1,223 @@ +""" +.. _chemiscope-explore-metatensor: + +Using `metatensor`_ models for dataset exploration +================================================== + +In this example, we demonstrate how to create and use a `metatensor`_ model with +:py:func:`chemiscope.metatensor_featurizer` to extract features from the model, which +are then displayed using a chemiscope widget. To use this function, some additional +dependencies are required. You can install them with the following command: + +.. code:: bash + + pip install chemiscope[metatensor] + +.. _metatensor: https://docs.metatensor.org/latest/index.html +""" + +# %% +# +# Firstly, we import necessary packages and read structures from the dataset. + +from typing import Dict, List, Optional + +import ase.io +import torch +from metatensor.torch import Labels, TensorBlock, TensorMap +from metatensor.torch.atomistic import ( + MetatensorAtomisticModel, + ModelCapabilities, + ModelMetadata, + ModelOutput, + NeighborListOptions, + System, +) + +import chemiscope + +frames = ase.io.read("data/explore_c-gap-20u.xyz", ":") + +# %% +# +# Using pre-trained models +# ------------------------ +# +# Most commonly, you will have an already existing model in metatensor format that +# you'll want to use for dataset exploration. In this case, you'll have to create a +# ``featurizer`` function using :py:func:`chemiscope.metatensor_featurizer`. +# +# ``metatensor_featurizer`` takes an existing model as input. It can be either a +# ``MetatensorAtomisticModel`` instance or a path to a pre-trained model file (here +# ``"model.pt"``) + +featurizer = chemiscope.metatensor_featurizer(model="model.pt") + +# %% +# +# From here, you can use :py:func:`chemiscope.explore` to visualize the features +# computed from the structures. For this, we are passing the frames, the ``featurizer`` +# function, and — as the model computes per-atom properties — environments. + +chemiscope.explore( + frames=frames, + featurize=featurizer, + environments=chemiscope.all_atomic_environments(frames), +) + +# %% +# +# Defining a custom model +# ----------------------- +# +# Let's now move on and see how one can define a fully custom model to use through the +# metatensor interface. +# +# Here we will use an atom-centered representation, where each atomic environment is +# represented with the moments of the positions of the neighbors up to a maximal order. +# +# The model computes moments up to a specified maximum order :math:`k_{\text{max}}`, +# computing a representation :math:`F_i^k` +# +# .. math:: +# +# F_i^k = \sum_{j} \frac{r_{ij}}{r_c}^k +# +# where :math:`r_{ij}` is the distance between atom :math:`i` and its neighbor +# :math:`j`, :math:`k` is the moment order and :math:`r_c` is the cutoff radius. +# +# And then, the model will take a PCA of the above features to extract the three most +# relevant dimensions. + + +class FeatureModel(torch.nn.Module): + def __init__(self, cutoff: float, max_k: int): + super().__init__() + self.cutoff = cutoff + self.max_k = max_k + + self._neighbors_options = NeighborListOptions(cutoff=cutoff, full_list=True) + + def requested_neighbor_lists(self) -> List[NeighborListOptions]: + # our model requires a neighbor list, that will be computed and provided to it + # automatically. + return [self._neighbors_options] + + def forward( + self, + systems: List[System], + outputs: Dict[str, ModelOutput], + selected_atoms: Optional[Labels] = None, + ) -> Dict[str, TensorMap]: + if list(outputs.keys()) != ["features"]: + raise ValueError( + "this model can only compute 'features', but outputs contains other " + f"keys: {', '.join(outputs.keys())}" + ) + + if not outputs["features"].per_atom: + raise NotImplementedError("per structure features are not implemented") + + all_features = [] + all_samples = [] + + for system_i, system in enumerate(systems): + dtype = system.positions.dtype + device = system.positions.device + n_atoms = len(system.positions) + + # Initialize a tensor to store features for each atom + features = torch.zeros((n_atoms, self.max_k), dtype=dtype, device=device) + + # get the neighbor list for this system + neighbors = system.get_neighbor_list(self._neighbors_options) + i = neighbors.samples.column("first_atom") + + r_ij = torch.linalg.vector_norm(neighbors.values.reshape(-1, 3), dim=1) + r_ij /= self.cutoff + + for k in range(self.max_k): + features[i, k] += torch.pow(r_ij, k) + + all_features.append(features) + + # Create labels for each atom in the system + system_atom_labels = torch.tensor( + [[system_i, atom_i] for atom_i in range(n_atoms)] + ) + all_samples.append(system_atom_labels) + + # Concatenate features and labels across all systems + features_tensor = torch.cat(all_features, dim=0) + samples_tensor = torch.cat(all_samples, dim=0) + + # Take the PCA of the features + _, _, V = torch.linalg.svd(features_tensor - features_tensor.mean()) + features_pca = features_tensor @ V[:3].T + + # Add metadata to the output + block = TensorBlock( + values=features_pca, + samples=Labels(names=["system", "atom"], values=samples_tensor), + components=[], + properties=Labels( + names=["feature"], + values=torch.tensor([[0], [1], [2]]), + ), + ) + return { + "features": TensorMap( + keys=Labels(names=["_"], values=torch.tensor([[0]])), blocks=[block] + ) + } + + +# %% +# +# With the class defined, we can now create an instance of the model, giving ``cutoff`` +# and ``max_k`` as a maximal moment to compute. We don’t need to train this model since +# there are no trainable parameters inside. + +model = FeatureModel(cutoff=4.5, max_k=6) + + +# %% +# +# Next, we set up the model metadata and capabilities: + +metadata = ModelMetadata( + name="Example moment model", + description=( + "A model that computes atom-centered features based on the distances of " + "neighboring atoms" + ), +) + +capabilities = ModelCapabilities( + outputs={ + "features": ModelOutput(per_atom=True), + }, + atomic_types=[6], + interaction_range=0.0, + supported_devices=["cpu"], + dtype="float64", +) + +model = MetatensorAtomisticModel(model.eval(), metadata, capabilities) + +# %% +# +# For a more detailed example of exporting a model, please check the related +# documentation `page +# `_ +# in `metatensor`_. +# +# Once the model is fully defined, we can use it with +# :py:func:`chemiscope.metatensor_featurizer`: + +featurizer = chemiscope.metatensor_featurizer(model, check_consistency=True) +chemiscope.explore( + frames=frames, + featurize=featurizer, + environments=chemiscope.all_atomic_environments(frames), +) diff --git a/python/examples/model.pt b/python/examples/model.pt new file mode 100644 index 0000000000000000000000000000000000000000..8739fb63c56ee2d37094d2fefcfed628fb278bf6 GIT binary patch literal 35984 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 literal 0 HcmV?d00001 diff --git a/tox.ini b/tox.ini index eab9f2354..86749e9e1 100644 --- a/tox.ini +++ b/tox.ini @@ -64,6 +64,10 @@ commands = [testenv:docs] description = Build chemiscope documentation +extras = + explore + metatensor + deps = -r docs/requirements.txt @@ -72,7 +76,6 @@ allowlist_externals = commands = npm run api-docs sphinx-build --doctree-dir docs/build/doctrees -W --builder html docs/src docs/build/html -extras = explore [testenv:generate-standalone] description = Generate standalone HTML for chemiscope