diff --git a/adslab_parse_stats.py b/adslab_parse_stats.py new file mode 100644 index 0000000000..dbc4d60b43 --- /dev/null +++ b/adslab_parse_stats.py @@ -0,0 +1,120 @@ +import re +import json +from collections import defaultdict +from pathlib import Path + +import matplotlib.cm as cmx +import matplotlib.colors as colors +import matplotlib.pyplot as plt +import numpy as np +from minydra import resolved_args +from tqdm import tqdm + +if __name__ == "__main__": + + args = resolved_args() + + assert args.file is not None + assert Path(args.file).exists() + assert Path(args.file).is_file() + + with open(args.file, "r") as f: + lines = f.read() + + samples = [ + s + for s in lines.split( + "------------------------------\n------------------------------" + ) + if "Actions to Data" in s and "ABORTING" not in s + ] + + times = defaultdict(list) + metadatas = [] + time_regex = re.compile(r"(.*) \| Done! \((.*)s\)") + total_adsorbed_regex = re.compile(r"Total adsorbed_surfaces: (\d+)") + non_reasonable_regex = re.compile(r"Non reasonable configs: (\d+)/(\d+)") + + metadata_regexs = { + "adsorbate_id": re.compile( + r"args(?:\.actions|)\.adsorbate_id is None, choosing (\d+)" + ), + "adsorbate_desc": re.compile(r"# Selected adsorbate: (.+)"), + "bulk_id": re.compile(r"args\.actions\.bulk_id is None, choosing (\d+)"), + "bulk_desc": re.compile(r"# Selected bulk: (.+)"), + "surface_id": re.compile(r"args\.actions\.surface_id is None, choosing (\d+)"), + "surface_desc": re.compile(r"# Selected surface: (.+)"), + "bond_indices": re.compile(r"bond_indices: (.+)"), + } + + keys = [] + time_keys = [] + for s, sample in tqdm(enumerate(samples), total=len(samples)): + metadatas.append({}) + matches = time_regex.findall(sample) + if not time_keys: + time_keys = set([k.strip() for k, _ in matches] + ["Actions to Data"]) + matches += [ + ( + "Total adsorbed_surfaces", + int(total_adsorbed_regex.findall(sample)[0]), + ), + ( + "Proportion of non reasonable adsorbed_surfaces", + float(non_reasonable_regex.findall(sample)[0][0]) + / float(non_reasonable_regex.findall(sample)[0][1]), + ), + ] + + for k, v in matches: + k = k.strip() + if "Actions to Data" in k: + k = "Actions to Data" + times[k].append(float(v)) + metadatas[-1][k] = float(v) + if s == 0: + keys.append(k) + for name, reg in metadata_regexs.items(): + meta = reg.findall(sample)[0] + if "id" in name: + meta = int(meta) + metadatas[-1][name] = meta + + means = {k: m for k, v in times.items() if ((m := np.mean(v)) > 0.1)} + stds = {k: np.std(v) for k, v in times.items() if k in means} + keys = [k for k in keys if k in means] + + cmap = plt.get_cmap("viridis") + cnorm = colors.Normalize(vmin=0, vmax=len(samples)) + scalar_map = cmx.ScalarMappable(norm=cnorm, cmap=cmap) + + n_plots = len(means.keys()) + + ncols = args.plot_ncols or 3 + nrows = n_plots // ncols + if n_plots % ncols != 0: + nrows += 1 + + fig, axs = plt.subplots(nrows, ncols, figsize=(ncols * 5, nrows * 4)) + + for i, k in tqdm(enumerate(keys), total=len(keys)): + ax = axs.flat[i] + bars = ax.bar(range(len(times[k])), times[k]) + for b, bar in enumerate(bars): + bar.set_color(scalar_map.to_rgba(b)) + + title = k + if k in time_keys: + title += f" ({means[k]:.2f}s +/- {stds[k]:.2f}s)" + else: + title += " (count)" + + ax.set_title(title, fontsize=8) + ax.xaxis.set_tick_params(labelsize=6) + ax.yaxis.set_tick_params(labelsize=6) + + plt.suptitle(f"Time (s) for operations or printed counts ({len(samples)} samples)") + + plt.savefig(args.out_png or f"{Path(args.file).stem}.png", dpi=150) + with open(args.out_json or f"{Path(args.file).stem}.json", "w") as f: + json.dump(metadatas, f) diff --git a/configs/sample/defaults.yaml b/configs/sample/defaults.yaml new file mode 100644 index 0000000000..3f233fa73c --- /dev/null +++ b/configs/sample/defaults.yaml @@ -0,0 +1,115 @@ +# -------------------------------------------------------------- +# ----- minydra default args values for sample_adslab.py ----- +# -------------------------------------------------------------- + +# ------------------ +# ----- Data ----- +# ------------------ +paths: + # path to the bulk_db_flat pickle file + bulk_db_flat: /network/projects/_groups/ocp/oc20/dataset-creation/bulk_db_flat_2021sep20.pkl + + # path to the adsorbate_db pickle file + adsorbate_db: /network/projects/_groups/ocp/oc20/dataset-creation/adsorbate_db_2021apr28.pkl + + # path to the precomputed_structures pickle file with all surfaces + precomputed_structures: /network/projects/_groups/ocp/oc20/dataset-creation/precomputed_surfaces_2021Sep20 +# ------------------------------------ +# ----- Adslab parametrization ----- +# ------------------------------------ + +# random seed +seed: 123 + +# number of runs +nruns: 1 + +actions: + # adsorbate smiles representation (see end of this file for reference) + # null -> sample uniformly + adsorbate_smiles: null # H2O + + # index of the bulk in bulk_db_flat. + # null -> sample uniformly + bulk_id: null + + # index of the surface to select for a given bulk + # null -> sample uniformly + surface_id: null + + # index of the adsorption site to select for a given surface + # can be -1 (=all), a list of ints or a single int + binding_site_index: -1 + + +# whether or not to use pre-computed surfaces. +# if not they will be computed on the fly but it takes +use_precomputed_surfaces: true + +# Loader animation +animate: false +# Ignore loader prints +no_loader: false + +# prints +verbose: 0 + +# avaliable adsorbates smiles (82): +# {chemical_formula: smiles} +# { 'O': ['*O'], +# 'H': ['*H'], +# 'HO': ['*OH'], +# 'H2O': ['*OH2'], +# 'C': ['*C'], +# 'CO': ['*CO'], +# 'CH': ['*CH'], +# 'CHO': ['*CHO', '*COH'], +# 'CH2': ['*CH2'], +# 'CH2O': ['*CH2*O', '*CHOH'], +# 'CH3': ['*CH3'], +# 'CH3O': ['*OCH3', '*CH2OH'], +# 'CH4': ['*CH4'], +# 'CH4O': ['*OHCH3'], +# 'C2': ['*C*C'], +# 'C2O': ['*CCO'], +# 'C2H': ['*CCH'], +# 'C2HO': ['*CHCO', '*CCHO'], +# 'C2HO2': ['*COCHO'], +# 'C2H2O': ['*CCHOH', 'CH2*CO', '*CHCHO', 'CH*COH'], +# 'C2H2': ['*CCH2', '*CH*CH'], +# 'C2H2O2': ['*COCH2O', '*CHO*CHO', '*COHCHO', '*COHCOH'], +# 'C2H3': ['*CCH3', '*CHCH2'], +# 'C2H3O': ['*COCH3', '*OCHCH2', '*COHCH2', '*CHCHOH', '*CCH2OH'], +# 'C2H3O2': ['*CHOCHOH', '*COCH2OH', '*COHCHOH'], +# 'C2H4': ['*CH2*CH2'], +# 'C2H4O': ['*OCHCH3', '*COHCH3', '*CHOHCH2', '*CHCH2OH'], +# 'C2H4O2': ['*OCH2CHOH', '*CHOCH2OH', '*COHCH2OH', '*CHOHCHOH'], +# 'C2H5': ['*CH2CH3'], +# 'C2H5O': ['*OCH2CH3', '*CHOHCH3', '*CH2CH2OH'], +# 'C2H5O2': ['*CHOHCH2OH'], +# 'C2H6O': ['*OHCH2CH3'], +# 'C2H8N2': ['*NH2N(CH3)2'], +# 'C2H6N2O': ['*ONN(CH3)2'], +# 'CH4N2O': ['*OHNNCH3'], +# 'CH3N2': ['*NNCH3'], +# 'HNO': ['*ONH'], +# 'H2N2': ['*NHNH'], +# 'H4N2': ['*NHN2'], +# 'HN2': ['*N*NH'], +# 'N2O3': ['*ONNO2'], +# 'N2O4': ['*NO2NO2'], +# 'N2O': ['*N*NO'], +# 'N2': ['*N2'], +# 'H2N2O': ['*ONNH2'], +# 'H2N': ['*NH2'], +# 'H3N': ['*NH3'], +# 'HN2O': ['*NONH'], +# 'HN': ['*NH'], +# 'NO2': ['*NO2'], +# 'NO': ['*NO'], +# 'N': ['*N'], +# 'NO3': ['*NO3'], +# 'H3NO': ['*OHNH2'], +# 'HNO2': ['*ONOH'], +# 'CN': ['*CN'] +# } diff --git a/flat_slab.py b/flat_slab.py new file mode 100644 index 0000000000..963347a2a4 --- /dev/null +++ b/flat_slab.py @@ -0,0 +1,339 @@ +import catkit + +from ocdata.constants import COVALENT_MATERIALS_MPIDS, MAX_MILLER +from ocdata.loader import Loader + +with Loader("Imports"): + import pickle + from collections import defaultdict + from pathlib import Path + + import numpy as np + from minydra import resolved_args + from pymatgen.core.surface import ( + SlabGenerator, + get_symmetrically_distinct_miller_indices, + ) + + from ocdata.adsorbates import Adsorbate + from ocdata.bulk_obj import Bulk + from ocdata.combined import Combined + from ocdata.surfaces import Surface + from ocpmodels.preprocessing.atoms_to_graphs import AtomsToGraphs + + +# ---------------------------- +# ----- UTILS (ignore) ----- +# ---------------------------- + + +def print_header(i, nruns): + """ + Prints + ------------------- + ---- Run i ---- + ------------------- + """ + box_char = "#" + border_width = 4 + border = box_char * border_width + box_width = 40 + + runs_len = len(str(nruns)) + title_str = f"Run {str(i + 1).zfill(runs_len)}/{nruns}" + + n_space = box_width - 2 * len(border) - len(title_str) + n_left = n_space // 2 + n_right = n_space // 2 + (n_space % 2) + + print("\n" + box_char * box_width) + print(border + " " * n_left + title_str + " " * n_right + border) + print(box_char * box_width) + + +def print_out_times(out_times, fpath=None, prec=3): + """ + Prints a summary of the out_time dictionnary + + Args: + out_times (dict[list]): dictionnary of times + fpath (Union[str, pathlib.Path], optional): path to write the + string summary to. Defaults to None (= no writing) + prec (int, optional): print decimals. Defaults to 3. + + Returns: + str: stringsummary + """ + max_k_len = max([len(k) for k in out_times]) + strs = [f"{'Operation':{max_k_len}} -> Time (s)"] + + all_keys = sorted(out_times.keys()) + single_keys = [] + if not all([len(k) == 1 for k in out_times]): + single_keys = [k for k, v in out_times.items() if len(v) == 1] + all_keys = single_keys + [k for k in out_times if k not in set(single_keys)] + + single_key_sep = None + + for i, k in enumerate(all_keys): + if single_keys and k not in single_keys and single_key_sep is None: + single_key_sep = i + 1 + times = out_times[k] + s = f"{k:{max_k_len}} -> " + if len(times) > 1: + q1, med, q3 = np.percentile(times, [25, 50, 75]) + mean, std = np.mean(times), np.std(times) + s += f"[{q1:.{prec}f} | {med:.{prec}f} | {q3:.{prec}f}]" + s += f" ~ {mean:.{prec}f} +/- {std:.{prec}f}" + else: + s += f"{times[0]:.{prec}f}" + strs.append(s) + + max_s_len = max(len(s) for s in strs) + border = "-" * max_s_len + strs.append(border) + + if single_key_sep is not None: + strs = ( + strs[:single_key_sep] + + [ + border, + f"{'Operation':{max_k_len}} -> [q1 | med | q3] ~ mean +/- std", + border, + ] + + strs[single_key_sep:] + ) + + out_str = "\n".join([border] + strs[:1] + [border] + strs[1:]) + + if fpath is not None: + with open(fpath, "w") as f: + f.write(out_str) + + print(out_str) + + +def get_ads_db(args): + """ + Util to load the adsorbates pre-computed dict from the args + + Args: + args (Union[dict, minydra.MinyDict]): Command-line args + + Returns: + dict: adsorbates dictionnary + """ + with open(args.paths.adsorbate_db, "rb") as f: + return pickle.load(f) + + +# ------------------------------------ +# ----- Action Space Functions ----- +# ------------------------------------ + + +def select_adsorbate(ads_dict, smiles): + """ + Function to parameterize the choice of an adsorbate. + Curent parameterization relies on its chemical formula. + + Args: + db_path (Union[str, pathlib.Path]): path to the pickle file holding adsorbates + smiles (str): The smiles string description for the adsorbate + + Returns: + Optional[ase.Atom]: The selected adsorbate. None if the formula does not exist + """ + + if smiles is None: + smiles = np.random.choice([a[1] for a in ads_dict.values()]) + print( + "No adsorbate smiles has been provided. Selecting {} at random.".format( + smiles + ) + ) + + adsorbates = [(str(k), *a) for k, a in ads_dict.items() if a[1] == smiles] + + if len(adsorbates) == 0: + raise ValueError(f"No adsorbate exists with smiles {smiles}") + if len(adsorbates) > 1: + raise ValueError( + f"More than 1 adsorbate exists with smiles {smiles}:\n" + + ", ".join([a[2] for a in adsorbates]) + ) + + return adsorbates[0] + + +if __name__ == "__main__": + root = Path(__file__).resolve().parent + args = resolved_args(defaults=root / "configs" / "sample" / "defaults.yaml") + if isinstance( + args.actions.binding_site_index, str + ) and args.actions.binding_site_index.lower() in {"null", "none"}: + args.actions.binding_site_index = None + + out_times = defaultdict(list) + seed = args.seed or 0 + + with open(args.paths.bulk_db_flat, "rb") as f: + bulk_db_list = pickle.load(f) + + ads_dict = get_ads_db(args) + + for i in range(args.nruns): + np.random.seed(seed + i) + run_loader = Loader( + f"Actions to Data {i+1}/{args.nruns}", animate=False, out=out_times + ) + run_loader.start() + print_header(i, args.nruns) + + print("\n1. Adsorbate\n") + adsorbate_atoms = select_adsorbate(ads_dict, args.actions.adsorbate_formula) + adsorbate_obj = Adsorbate(adsorbate_atoms=adsorbate_atoms) # <<<< IMPORTANT + print("-> Selected adsorbate:", adsorbate_obj.atoms.get_chemical_formula()) + + # ------------------ + # ----- Bulk ----- + # ------------------ + + print("\n2. Bulk\n") + + # select bulk_id if None + if args.actions.bulk_id is None: + bulk_id = np.random.choice(len(bulk_db_list)) + print(f"args.actions.bulk_id is None, choosing {bulk_id}") + else: + bulk_id = args.actions.bulk_id + bulk = Bulk( # <<<< IMPORTANT + bulk_db_list, + bulk_index=bulk_id, + precomputed_structures=args.paths.precomputed_structures + if args.use_precomputed_surfaces + else None, + ) + print( + "-> Selected bulk:", + bulk.bulk_atoms.get_chemical_formula(), + f"({bulk.mpid})", + ) + + # possible_surfaces = bulk.get_possible_surfaces() + # -> surfaces_info = self.enumerate_surfaces( + # miller_indices=None, sample_miller_indices=False, max_miller=MAX_MILLER + # ) + bulk_struct = bulk.standardize_bulk(bulk.bulk_atoms) + all_millers = get_symmetrically_distinct_miller_indices(bulk_struct, MAX_MILLER) + np.random.shuffle(all_millers) + + all_slabs_info = [] + site_found = False # at the begining, no binding site has been found + + for millers in all_millers: + # 1. Select one set of miller indices + + if site_found: + break + + # 2. Generate all possible slabs for this set of miller indices + slab_gen = SlabGenerator( + initial_structure=bulk_struct, + miller_index=millers, + min_slab_size=7.0, + min_vacuum_size=20.0, + lll_reduce=False, + center_slab=True, + primitive=True, + max_normal_search=1, + ) + slabs = slab_gen.get_slabs( + tol=0.3, bonds=None, max_broken_bonds=0, symmetrize=False + ) + # If the bottoms of the slabs are different than the tops, then we want + # to consider them, too + if len(slabs) != 0: + flipped_slabs_info = [ + (bulk.flip_struct(slab), millers, slab.shift, False) + for slab in slabs + if bulk.is_structure_invertible(slab) is False + ] + + # Concatenate all the results together + slabs_info = [(slab, millers, slab.shift, True) for slab in slabs] + all_slabs_info.extend(slabs_info + flipped_slabs_info) + + if len(all_slabs_info) == 0: + print("No surface found. Next Miller indices") + continue + else: + print( + f"{len(all_slabs_info)} surfaces found for Miller indices {millers}" + ) + + possible_surfaces = all_slabs_info + np.random.shuffle(possible_surfaces) + + # Try the current (miller indices, surface) combination and look for binding sites + for surface in possible_surfaces: + # 3. Select one surface + + surface_obj = Surface( + bulk, + surface, + 0, # dummy + 0, # dummy + no_loader=args.no_loader, + ) + print( + "-> Selected surface:", + surface_obj.surface_atoms.get_chemical_formula(), + ) + + adslab = Combined( + adsorbate_obj, + surface_obj, + enumerate_all_configs=False, + no_loader=args.no_loader, + index=args.actions.binding_site_index, + early_init=True, + ) + surface_gratoms = catkit.Gratoms(surface_obj.surface_atoms) + surface_atom_indices = [ + i + for i, atom in enumerate(surface_obj.surface_atoms) + if atom.tag == 1 + ] + surface_gratoms.set_surface_atoms(surface_atom_indices) + surface_gratoms.pbc = np.array([True, True, False]) + + adsorbate_gratoms = adslab.convert_adsorbate_atoms_to_gratoms( + adsorbate_obj.atoms, adsorbate_obj.bond_indices + ) + builder = catkit.gen.adsorption.Builder(surface_gratoms) + + # Try adding the adsorbate onto the first binding site of the + # current surface + + for site_index in range(len(surface_atom_indices)): + adsorbed_surface = builder.add_adsorbate( + adsorbate_gratoms, + bonds=adsorbate_obj.bond_indices, + index=site_index, + ) + is_reasonable = adslab.is_config_reasonable(adsorbed_surface) + if is_reasonable: + # use this binding site + site_found = True + break + + if site_found: + print("-> Site found") + break + + # all binding sites have been tried + # go to next surface + print("No more sites available. Next surface") + run_loader.stop() + print_out_times(out_times) diff --git a/get_data_sample.py b/get_data_sample.py new file mode 100644 index 0000000000..5f06856d56 --- /dev/null +++ b/get_data_sample.py @@ -0,0 +1,71 @@ +""" +Exploration file to get a `batch` in-memory and play around with it. +Use it in notebooks or ipython console + +$ ipython +... +In [1]: run get_data_sample.py +Out[1]: ... + +In [2]: print(batch) + +""" +import sys + +import torch # noqa: F401 +from minydra import resolved_args +from tqdm import tqdm + +from ocpmodels.common.flags import flags +from ocpmodels.common.registry import registry +from ocpmodels.common.utils import build_config, setup_imports, setup_logging + + +if __name__ == "__main__": + + opts = resolved_args() + + sys.argv[1:] = ["--mode=train", "--config=configs/is2re/10k/schnet/schnet.yml"] + setup_logging() + + parser = flags.get_parser() + args, override_args = parser.parse_known_args() + config = build_config(args, override_args) + + config["optim"]["num_workers"] = 4 + config["optim"]["batch_size"] = 4 + config["logger"] = "dummy" + + if opts.victor_local: + config["dataset"][0]["src"] = "data/is2re/10k/train/data.lmdb" + config["dataset"] = config["dataset"][:1] + config["optim"]["num_workers"] = 0 + config["optim"]["batch_size"] = opts.bs or config["optim"]["batch_size"] + + setup_imports() + trainer = registry.get_trainer_class(config.get("trainer", "energy"))( + task=config["task"], + model_attributes=config["model"], + dataset=config["dataset"], + optimizer=config["optim"], + identifier=config["identifier"], + timestamp_id=config.get("timestamp_id", None), + run_dir=config.get("run_dir", "./"), + is_debug=config.get("is_debug", False), + print_every=config.get("print_every", 100), + seed=config.get("seed", 0), + logger=config.get("logger", "wandb"), + local_rank=config["local_rank"], + amp=config.get("amp", False), + cpu=config.get("cpu", False), + slurm=config.get("slurm", {}), + new_gnn=config.get("new_gnn", True), + data_split=config.get("data_split", None), + note=config.get("note", ""), + ) + + task = registry.get_task_class(config["mode"])(config) + task.setup(trainer) + for batch in trainer.train_loader: + b = batch[0] + break diff --git a/mila/launch_exp.py b/mila/launch_exp.py index 6a71fb42ef..dec6bde850 100644 --- a/mila/launch_exp.py +++ b/mila/launch_exp.py @@ -1,3 +1,4 @@ +import copy import os import re import subprocess @@ -5,10 +6,8 @@ from pathlib import Path from minydra import resolved_args -from yaml import safe_load, dump - from sbatch import now -import copy +from yaml import dump, safe_load ROOT = Path(__file__).resolve().parent.parent @@ -143,14 +142,16 @@ def cli_arg(args, key=""): s += cli_arg(v, key=f"{parent}{k}") else: if " " in str(v) or "," in str(v) or isinstance(v, str): - if "'" in str(v) and '"' in str(v): - v = str(v).replace("'", "\\'") + if '"' in str(v): + v = str(v).replace('"', '\\"') v = f"'{v}'" elif "'" in str(v): - v = f'"{v}"' + v = f'\\"{v}\\"' else: v = f"'{v}'" s += f" --{parent}{k}={v}" + if "ads" in k: + print(s.split(" --")[-1]) return s diff --git a/notebooks/explore.ipynb b/notebooks/explore.ipynb index 4657b93ad9..537cb6a218 100644 --- a/notebooks/explore.ipynb +++ b/notebooks/explore.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -112,17 +112,17 @@ " max_epochs : 30\n", "\n", "ip : 127.0.0.1\n", - "stdin : 9003\n", - "control : 9001\n", - "hb : 9000\n", + "stdin : 9008\n", + "control : 9006\n", + "hb : 9005\n", "Session : \n", " signature_scheme : hmac-sha256\n", - " key : b'ddee2dc9-1e42-4c1d-9273-3d560e0c3b57'\n", + " key : b'7061cb97-2cdc-4707-ab19-299aa9b73d74'\n", "\n", - "shell : 9002\n", + "shell : 9007\n", "transport : tcp\n", - "iopub : 9004\n", - "f : /tmp/tmp-77133q5BLwvV7pFSy.json\n", + "iopub : 9009\n", + "f : /tmp/tmp-87853nxUVdPftdky8.json\n", "mode : train\n", "identifier : \n", "timestamp_id : None\n", @@ -185,14 +185,14 @@ "text": [ "amp: false\n", "cmd:\n", - " checkpoint_dir: ./checkpoints/2022-04-05-14-54-56\n", + " checkpoint_dir: ./checkpoints/2022-04-12-09-56-16\n", " commit: bd247bc\n", " identifier: ''\n", - " logs_dir: ./logs/tensorboard/2022-04-05-14-54-56\n", + " logs_dir: ./logs/tensorboard/2022-04-12-09-56-16\n", " print_every: 10\n", - " results_dir: ./results/2022-04-05-14-54-56\n", + " results_dir: ./results/2022-04-12-09-56-16\n", " seed: 0\n", - " timestamp_id: 2022-04-05-14-54-56\n", + " timestamp_id: 2022-04-12-09-56-16\n", "dataset:\n", " normalize_labels: true\n", " src: /network/projects/_groups/ocp/oc20/is2re/10k/train/data.lmdb\n", @@ -233,18 +233,30 @@ "val_dataset:\n", " src: /network/projects/_groups/ocp/oc20/is2re/all/val_id/data.lmdb\n", "\n", - "2022-04-05 14:55:03 (INFO): Loading dataset: single_point_lmdb\n", - "2022-04-05 14:55:03 (INFO): Loading model: schnet\n", - "2022-04-05 14:55:03 (INFO): Loaded SchNetWrap with 541697 parameters.\n" + "2022-04-12 09:56:10 (INFO): Loading dataset: single_point_lmdb\n", + "2022-04-12 09:56:10 (INFO): Loading model: schnet\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/mila/s/schmidtv/.conda/envs/ocp-env/lib/python3.8/site-packages/torch/utils/data/dataloader.py:487: UserWarning: This DataLoader will create 4 worker processes in total. Our suggested max number of worker in current system is 2, which is smaller than what this DataLoader is going to create. Please be aware that excessive worker creation might get DataLoader running slow or even freeze, lower the worker number to avoid potential slowness/freeze if necessary.\n", + " warnings.warn(_create_warning_msg(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2022-04-12 09:56:11 (INFO): Loaded SchNetWrap with 541697 parameters.\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "/home/mila/a/alexandre.duval/.conda/envs/ocp/lib/python3.8/site-packages/torch/utils/data/dataloader.py:487: UserWarning: This DataLoader will create 4 worker processes in total. Our suggested max number of worker in current system is 1, which is smaller than what this DataLoader is going to create. Please be aware that excessive worker creation might get DataLoader running slow or even freeze, lower the worker number to avoid potential slowness/freeze if necessary.\n", - " warnings.warn(_create_warning_msg(\n", - "2022-04-05 14:55:03 (WARNING): Model gradient logging to tensorboard not yet supported.\n" + "2022-04-12 09:56:11 (WARNING): Model gradient logging to tensorboard not yet supported.\n" ] } ], @@ -296,7 +308,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -327,14 +339,18 @@ }, { "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], "source": [ + "# get 1 batch\n", "for batch in trainer.train_loader:\n", " break" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -359,7 +375,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 21, "metadata": {}, "outputs": [ { @@ -368,7 +384,7 @@ "DataBatch(edge_index=[2, 1372], pos=[40, 3], cell=[1, 3, 3], atomic_numbers=[40], natoms=[1], cell_offsets=[1372, 3], force=[40, 3], distances=[1372], fixed=[40], sid=[1], tags=[40], y_init=[1], y_relaxed=[1], pos_relaxed=[40, 3], batch=[40], ptr=[2], neighbors=[1])" ] }, - "execution_count": 12, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -380,29 +396,7 @@ }, { "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([[10.8727, -0.0000, -1.5002],\n", - " [-3.6242, 7.3342, -2.2150],\n", - " [ 0.0000, 0.0000, 32.5804]])" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "b.cell[0,:,:]" - ] - }, - { - "cell_type": "code", - "execution_count": 14, + "execution_count": 43, "metadata": {}, "outputs": [ { @@ -412,7 +406,7 @@ "`edge_index` contains the edges of all graphs in the batch:\n", "torch.Size([2, 170988])\n", "tensor([[ 19, 18, 14, ..., 4570, 4514, 4530],\n", - " [ 0, 0, 0, ..., 4584, 4584, 4584]])\n" + " [ 0, 0, 0, ..., 4584, 4584, 4584]], device='cuda:0')\n" ] } ], @@ -424,7 +418,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 44, "metadata": {}, "outputs": [ { @@ -433,7 +427,7 @@ "text": [ "`batch` contains the graph id of each atom in the batch:\n", "torch.Size([4585])\n", - "tensor([ 0, 0, 0, ..., 63, 63, 63])\n" + "tensor([ 0, 0, 0, ..., 63, 63, 63], device='cuda:0')\n" ] } ], @@ -445,7 +439,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 68, "metadata": {}, "outputs": [ { @@ -456,10 +450,10 @@ "torch.Size([4585, 3]) torch.Size([4585, 3])\n", "tensor([[ 6.3715, 1.4460, 16.5523],\n", " [ 9.9958, 1.4460, 18.7673],\n", - " [ 2.7473, 1.4460, 14.3374]])\n", + " [ 2.7473, 1.4460, 14.3374]], device='cuda:0')\n", "tensor([[ 6.3715, 1.4460, 16.5523],\n", " [ 9.9823, 1.3752, 18.7785],\n", - " [ 2.7473, 1.4460, 14.3374]])\n" + " [ 2.7473, 1.4460, 14.3374]], device='cuda:0')\n" ] } ], @@ -472,7 +466,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 48, "metadata": {}, "outputs": [ { @@ -487,7 +481,7 @@ "\n", " [[10.8287, 0.0000, 0.0000],\n", " [ 0.0000, 10.9216, 2.1019],\n", - " [ 0.0000, 0.0000, 29.4261]]])\n" + " [ 0.0000, 0.0000, 29.4261]]], device='cuda:0')\n" ] } ], @@ -499,7 +493,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 50, "metadata": {}, "outputs": [ { @@ -508,7 +502,7 @@ "text": [ "`atomic_numbers` contains the atomic number of each atom in the batch:\n", "torch.Size([4585])\n", - "tensor([39., 39., 39., ..., 1., 1., 8.])\n" + "tensor([39., 39., 39., ..., 1., 1., 8.], device='cuda:0')\n" ] } ], @@ -520,7 +514,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 53, "metadata": {}, "outputs": [ { @@ -533,8 +527,8 @@ " 40, 69, 77, 99, 52, 85, 100, 32, 50, 78, 67, 66, 68, 79,\n", " 114, 53, 54, 79, 97, 183, 92, 67, 75, 50, 10, 100, 64, 51,\n", " 87, 54, 70, 87, 65, 103, 70, 70, 65, 69, 52, 98, 59, 49,\n", - " 75, 69, 40, 67, 71, 32, 85, 113])\n", - "tensor(4585)\n" + " 75, 69, 40, 67, 71, 32, 85, 113], device='cuda:0')\n", + "tensor(4585, device='cuda:0')\n" ] } ], @@ -547,7 +541,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 160, "metadata": {}, "outputs": [ { @@ -565,18 +559,18 @@ " ...,\n", " [0, 0, 0],\n", " [0, 0, 0],\n", - " [0, 0, 0]])\n", - "tensor([-1, 0, 1])\n", + " [0, 0, 0]], device='cuda:0')\n", + "tensor([-1, 0, 1], device='cuda:0')\n", "\n", "\n", "\n", - "tensor([ 0, -1, 0])\n", - "tensor([10, 0])\n", + "tensor([ 0, -1, 0], device='cuda:0')\n", + "tensor([10, 0], device='cuda:0')\n", "tensor([[10.8727, -0.0000, -1.5002],\n", " [-3.6242, 7.3342, -2.2150],\n", - " [ 0.0000, 0.0000, 32.5804]])\n", - "tensor(5.5423)\n", - "tensor(8.4754)\n" + " [ 0.0000, 0.0000, 32.5804]], device='cuda:0')\n", + "tensor(5.5423, device='cuda:0')\n", + "tensor(8.4754, device='cuda:0')\n" ] } ], @@ -600,7 +594,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 161, "metadata": {}, "outputs": [ { @@ -609,13 +603,7 @@ "text": [ "`force` contains the 3D forces 'experienced' by each atom in the batch:\n", "torch.Size([4585, 3])\n", - "tensor([[-0.0324, -0.0750, -0.0513],\n", - " [ 0.1162, 0.6802, 0.5540],\n", - " [ 0.1088, 0.1297, -0.0559],\n", - " ...,\n", - " [ 0.1467, 0.0696, -0.3660],\n", - " [-1.3134, -1.5871, 3.1853],\n", - " [-1.3643, 2.3140, -1.5113]])\n" + "tensor([ 0.2466, -0.0354, 0.4422], device='cuda:0')\n" ] } ], @@ -627,28 +615,28 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 58, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "`distances` contains the length (||x_i-x_j||) of each edge (i,j) in the batch\n", + "`distances` contains the length (A?) of each edge in the batch\n", "torch.Size([170988])\n", - "tensor([2.9223, 2.9223, 2.9319, ..., 5.9122, 5.9325, 5.9584])\n" + "tensor([2.9223, 2.9223, 2.9319, ..., 5.9122, 5.9325, 5.9584], device='cuda:0')\n" ] } ], "source": [ - "print(\"`distances` contains the length (||x_i-x_j||) of each edge (i,j) in the batch\")\n", + "print(\"`distances` contains the length (A?) of each edge in the batch\")\n", "print(b.distances.shape)\n", "print(b.distances)" ] }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 59, "metadata": {}, "outputs": [ { @@ -657,7 +645,7 @@ "text": [ "`fixed` contains the boolean flag for each atom being fixed or not\n", "torch.Size([4585])\n", - "tensor([1., 0., 1., ..., 0., 0., 0.])\n" + "tensor([1., 0., 1., ..., 0., 0., 0.], device='cuda:0')\n" ] } ], @@ -669,7 +657,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 62, "metadata": {}, "outputs": [ { @@ -678,7 +666,7 @@ "text": [ "`sid` contains the system id associated with each graph in the batch\n", "torch.Size([64])\n", - "tensor([1831766, 1974982, 589818, 2158881, 550912])\n" + "tensor([1831766, 1974982, 589818, 2158881, 550912], device='cuda:0')\n" ] } ], @@ -690,7 +678,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 63, "metadata": {}, "outputs": [ { @@ -699,7 +687,7 @@ "text": [ "`tags` contains the tag of each atom: 0 - Fixed, sub-surface atoms, 1 - Free, surface atoms 2 - Free, adsorbate atoms\n", "torch.Size([4585])\n", - "tensor([0, 1, 0, ..., 2, 2, 2])\n" + "tensor([0, 1, 0, ..., 2, 2, 2], device='cuda:0')\n" ] } ], @@ -711,7 +699,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 65, "metadata": {}, "outputs": [ { @@ -720,7 +708,7 @@ "text": [ "`y_init` and `y_relaxed` respectively contain the initial and relaxed energies of each graph:\n", "torch.Size([64]) torch.Size([64])\n", - "tensor([0.5247, 2.9358, 0.5761, 0.1011, 3.9838]) tensor([-2.6237, -0.4135, -2.4655, -3.4437, -0.8626])\n" + "tensor([0.5247, 2.9358, 0.5761, 0.1011, 3.9838], device='cuda:0') tensor([-2.6237, -0.4135, -2.4655, -3.4437, -0.8626], device='cuda:0')\n" ] } ], @@ -732,53 +720,53 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 69, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "`ptr` contains the number of atoms accumulated in the batch up to this graph.\n", + "`ptr` ??\n", "torch.Size([65])\n", "tensor([ 0, 40, 95, 138, 194, 272, 383, 453, 516, 583, 682, 782,\n", " 834, 894, 984, 1024, 1093, 1170, 1269, 1321, 1406, 1506, 1538, 1588,\n", " 1666, 1733, 1799, 1867, 1946, 2060, 2113, 2167, 2246, 2343, 2526, 2618,\n", " 2685, 2760, 2810, 2820, 2920, 2984, 3035, 3122, 3176, 3246, 3333, 3398,\n", " 3501, 3571, 3641, 3706, 3775, 3827, 3925, 3984, 4033, 4108, 4177, 4217,\n", - " 4284, 4355, 4387, 4472, 4585])\n" + " 4284, 4355, 4387, 4472, 4585], device='cuda:0')\n" ] } ], "source": [ - "print(\"`ptr` contains the number of atoms accumulated in the batch up to this graph.\")\n", + "print(\"`ptr` ??\")\n", "print(b.ptr.shape)\n", "print(b.ptr)" ] }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 162, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "`neighbors` describes basically the number of edges found in each graph\n", + "`neighbors` ??\n", "torch.Size([64])\n", "tensor([1372, 1634, 1813, 2502, 2300, 5449, 3109, 1976, 1640, 4159, 2952, 880,\n", " 2677, 2684, 1086, 2954, 3554, 4461, 2455, 3349, 2812, 894, 2024, 2734,\n", " 1760, 2691, 2576, 1466, 2760, 2254, 1604, 2504, 4218, 7825, 4034, 2290,\n", " 3070, 2100, 90, 4486, 2920, 1890, 2012, 2180, 2886, 3236, 2741, 2954,\n", " 2056, 3220, 1352, 3212, 2217, 4535, 2602, 1816, 3114, 3243, 1292, 2912,\n", - " 2396, 378, 3330, 5296])\n", - "tensor(170988)\n" + " 2396, 378, 3330, 5296], device='cuda:0')\n", + "tensor(170988, device='cuda:0')\n" ] } ], "source": [ - "print(\"`neighbors` describes basically the number of edges found in each graph\")\n", + "print(\"`neighbors` ??\")\n", "print(b.neighbors.shape)\n", "print(b.neighbors)\n", "print(b.neighbors.sum())" @@ -793,7 +781,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 164, "metadata": {}, "outputs": [ { @@ -802,7 +790,7 @@ "text": [ "tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,\n", " 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,\n", - " 36, 37, 38, 39])\n" + " 36, 37, 38, 39], device='cuda:0')\n" ] } ], @@ -811,16 +799,14 @@ "graph_id = 0\n", "# find the atom indices of that graph\n", "atoms = torch.argwhere(b.batch == graph_id).squeeze()\n", - "atoms_bis = (b.batch==graph_id).nonzero().T.squeeze()\n", "# the number of atoms selected above should match natoms\n", "assert len(atoms) == b.natoms[graph_id]\n", - "assert torch.all(torch.eq(atoms, atoms_bis)).item()\n", "print(atoms)" ] }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 92, "metadata": {}, "outputs": [ { @@ -828,10 +814,10 @@ "output_type": "stream", "text": [ "torch.Size([170988])\n", - "tensor([ True, True, True, ..., False, False, False])\n", + "tensor([ True, True, True, ..., False, False, False], device='cuda:0')\n", "torch.Size([2, 1372])\n", "tensor([[19, 18, 14, ..., 25, 19, 14],\n", - " [ 0, 0, 0, ..., 39, 39, 39]])\n" + " [ 0, 0, 0, ..., 39, 39, 39]], device='cuda:0')\n" ] } ], @@ -850,7 +836,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 87, "metadata": {}, "outputs": [ { @@ -869,7 +855,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 93, "metadata": {}, "outputs": [ { @@ -890,7 +876,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 104, "metadata": {}, "outputs": [ { @@ -898,7 +884,7 @@ "output_type": "stream", "text": [ "tensor([0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0,\n", - " 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 2, 2, 2, 2])\n", + " 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 2, 2, 2, 2], device='cuda:0')\n", "['fixed', 'surface', 'fixed', 'fixed', 'fixed', 'surface', 'surface', 'fixed', 'fixed', 'fixed', 'fixed', 'surface', 'surface', 'fixed', 'surface', 'fixed', 'surface', 'fixed', 'fixed', 'fixed', 'surface', 'surface', 'fixed', 'fixed', 'fixed', 'surface', 'fixed', 'fixed', 'fixed', 'surface', 'surface', 'fixed', 'fixed', 'fixed', 'fixed', 'surface', 'adsorbate', 'adsorbate', 'adsorbate', 'adsorbate']\n" ] } @@ -912,14 +898,14 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 108, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "tensor([6., 6., 1., 1.])\n" + "tensor([6., 6., 1., 1.], device='cuda:0')\n" ] } ], @@ -931,7 +917,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 124, "metadata": {}, "outputs": [], "source": [ @@ -942,7 +928,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 165, "metadata": {}, "outputs": [ { @@ -1004,6399 +990,26 @@ "print(dict2str(sym_counts, margin=10))" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Continue comprehension of batch" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Distances are equal to ||x_i-x_j||\n", - "tensor(2.9223)\n", - "tensor(2.9223)\n" - ] - } - ], - "source": [ - "print(\"Distances are equal to ||x_i-x_j||\")\n", - "print(b.distances[0])\n", - "print( torch.norm(b.pos[b.edge_index[0,0]] - b.pos[b.edge_index[1,0]] ))" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The proportion of fixed atoms is: \n", - "0.6604143947655398\n" - ] - } - ], - "source": [ - "print(\"The proportion of fixed atoms is: \")\n", - "print((b.fixed==1).nonzero().shape[0] / b.atomic_numbers.shape[0])" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "we observe the atomic numbers & tags of the second graph of the batch:\n", - "tensor([39., 39., 39., 39., 39., 39., 39., 39., 14., 14., 14., 14., 14., 14.,\n", - " 14., 14., 14., 14., 14., 14., 14., 14., 14., 14., 78., 78., 78., 78.,\n", - " 78., 78., 78., 78., 78., 78., 78., 78., 78., 78., 78., 78., 6., 6.,\n", - " 1.])\n", - "tensor([1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0,\n", - " 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2])\n" - ] - } - ], - "source": [ - "print(\"we observe the atomic numbers & tags of the second graph of the batch:\")\n", - "print(b.atomic_numbers[b.ptr[2]:b.ptr[3]])\n", - "print(b.tags[b.ptr[2]:b.ptr[3]])" - ] - }, { "cell_type": "code", - "execution_count": 40, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Is the edge_index matrix an ordered concatenation of individual graph edge_index matrix ? Meaning that we could take edge_index[b.natoms[0]:b.natoms[1]] and get exactly the second graph edges.\n", - "Answer: YES\n" - ] - } - ], - "source": [ - "print(\"Is the edge_index matrix an ordered concatenation of individual graph edge_index matrix ? Meaning that we could take edge_index[b.natoms[0]:b.natoms[1]] and get exactly the second graph edges.\")\n", - "print(\"Answer: YES\")\n", - "for ind in range(b.sid.shape[0]): \n", - " node_indices = (b.batch==ind).nonzero().T[0]\n", - " edges = b.edge_index[:,b.neighbors[:ind].sum():b.neighbors[:ind+1].sum()]\n", - " node_list_from_edges = edges.reshape(1,-1).squeeze().tolist()\n", - " if not set(node_list_from_edges).issubset(node_indices.tolist()): \n", - " print('Condition not satisfied')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Visualisation and explore cell offsets further" - ] - }, - { - "cell_type": "code", - "execution_count": 105, + "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Import libraries\n", - "from torch_geometric.utils.convert import to_networkx\n", - "import networkx as nx\n", - "import plotly.graph_objs as go\n", - "\n", - "# Graph\n", - "graph_id = 0\n", - "g = b[graph_id]\n", - "G = to_networkx(g)" - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "all nodes are/have offset neighbours\n", - "40\n" - ] - } - ], - "source": [ - "print('all nodes are/have offset neighbours')\n", - "prop_nonzero_cell_offsets = g.cell_offsets.nonzero().shape[0] / g.edge_index.shape[1]\n", - "edge_with_nonzero_cell_offsets = g.edge_index[:, g.cell_offsets.nonzero()[:,0]]\n", - "print(len(set(edge_with_nonzero_cell_offsets.reshape(-1).tolist()))) # all nodes have/are offset neighbours" - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Study offsets neighbours of node 0 in first graph of the batch\n", - "offset neighbours are a subset of its neighbors\n" - ] - } - ], - "source": [ - "print('Study offsets neighbours of node 0 in first graph of the batch')\n", - "offsets_neighbours_of_node0 = edge_with_nonzero_cell_offsets[:, :20][0,:].tolist()\n", - "set(offsets_neighbours_of_node0).issubset(list(G.neighbors(0)))\n", - "print('offset neighbours are a subset of its neighbors')" - ] - }, - { - "cell_type": "code", - "execution_count": 48, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Offset neighbours are always a subset of neighbours node, for the first graph of the batch\n" - ] - } - ], - "source": [ - "print(\"Offset neighbours are always a subset of neighbours node, for the first graph of the batch\")\n", - "\n", - "edge_with_nonzero_cell_offsets = g.edge_index[:, g.cell_offsets.nonzero()[:,0]]\n", - "for ind in range(b.natoms[0]): \n", - " neighbours = (edge_with_nonzero_cell_offsets[1,:]==ind).nonzero().T[0].tolist()\n", - " offset_neighbours = edge_with_nonzero_cell_offsets[:, neighbours][0,:].tolist()\n", - " if not set(offset_neighbours).issubset(list(G.neighbors(ind))):\n", - " print('Offset neighbours are not a subset of neighbours')" - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Visualise the graph with networkx\n", - "But this graph does not take into consideration node positions\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "print('Visualise the graph with networkx')\n", - "nx.draw(G, with_labels = True)\n", - "print('But this graph does not take into consideration node positions')" - ] - }, - { - "cell_type": "code", - "execution_count": 50, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Try 3D plots in networkx with atom positions\n" - ] - }, - { - "ename": "ValueError", - "evalue": "too many values to unpack (expected 2)", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m/home/mila/a/alexandre.duval/ocp/ocp/explore.ipynb Cell 56'\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 4\u001b[0m dico[atom\u001b[39m.\u001b[39mitem()] \u001b[39m=\u001b[39m b\u001b[39m.\u001b[39mpos[atom]\n\u001b[1;32m 5\u001b[0m pos \u001b[39m=\u001b[39m nx\u001b[39m.\u001b[39mspring_layout(G, dim\u001b[39m=\u001b[39m\u001b[39m3\u001b[39m)\n\u001b[0;32m----> 6\u001b[0m nx\u001b[39m.\u001b[39;49mdraw(G, with_labels \u001b[39m=\u001b[39;49m \u001b[39mTrue\u001b[39;49;00m, pos\u001b[39m=\u001b[39;49mpos)\n\u001b[1;32m 7\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39m'\u001b[39m\u001b[39mNetworkx does not allow 3D positions\u001b[39m\u001b[39m'\u001b[39m)\n", - "File \u001b[0;32m~/.conda/envs/ocp/lib/python3.8/site-packages/networkx/drawing/nx_pylab.py:120\u001b[0m, in \u001b[0;36mdraw\u001b[0;34m(G, pos, ax, **kwds)\u001b[0m\n\u001b[1;32m 117\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39m\"\u001b[39m\u001b[39mwith_labels\u001b[39m\u001b[39m\"\u001b[39m \u001b[39mnot\u001b[39;00m \u001b[39min\u001b[39;00m kwds:\n\u001b[1;32m 118\u001b[0m kwds[\u001b[39m\"\u001b[39m\u001b[39mwith_labels\u001b[39m\u001b[39m\"\u001b[39m] \u001b[39m=\u001b[39m \u001b[39m\"\u001b[39m\u001b[39mlabels\u001b[39m\u001b[39m\"\u001b[39m \u001b[39min\u001b[39;00m kwds\n\u001b[0;32m--> 120\u001b[0m draw_networkx(G, pos\u001b[39m=\u001b[39;49mpos, ax\u001b[39m=\u001b[39;49max, \u001b[39m*\u001b[39;49m\u001b[39m*\u001b[39;49mkwds)\n\u001b[1;32m 121\u001b[0m ax\u001b[39m.\u001b[39mset_axis_off()\n\u001b[1;32m 122\u001b[0m plt\u001b[39m.\u001b[39mdraw_if_interactive()\n", - "File \u001b[0;32m~/.conda/envs/ocp/lib/python3.8/site-packages/networkx/drawing/nx_pylab.py:334\u001b[0m, in \u001b[0;36mdraw_networkx\u001b[0;34m(G, pos, arrows, with_labels, **kwds)\u001b[0m\n\u001b[1;32m 331\u001b[0m pos \u001b[39m=\u001b[39m nx\u001b[39m.\u001b[39mdrawing\u001b[39m.\u001b[39mspring_layout(G) \u001b[39m# default to spring layout\u001b[39;00m\n\u001b[1;32m 333\u001b[0m draw_networkx_nodes(G, pos, \u001b[39m*\u001b[39m\u001b[39m*\u001b[39mnode_kwds)\n\u001b[0;32m--> 334\u001b[0m draw_networkx_edges(G, pos, arrows\u001b[39m=\u001b[39;49marrows, \u001b[39m*\u001b[39;49m\u001b[39m*\u001b[39;49medge_kwds)\n\u001b[1;32m 335\u001b[0m \u001b[39mif\u001b[39;00m with_labels:\n\u001b[1;32m 336\u001b[0m draw_networkx_labels(G, pos, \u001b[39m*\u001b[39m\u001b[39m*\u001b[39mlabel_kwds)\n", - "File \u001b[0;32m~/.conda/envs/ocp/lib/python3.8/site-packages/networkx/drawing/nx_pylab.py:889\u001b[0m, in \u001b[0;36mdraw_networkx_edges\u001b[0;34m(G, pos, edgelist, width, edge_color, style, alpha, arrowstyle, arrowsize, edge_cmap, edge_vmin, edge_vmax, ax, arrows, label, node_size, nodelist, node_shape, connectionstyle, min_source_margin, min_target_margin)\u001b[0m\n\u001b[1;32m 887\u001b[0m _draw_networkx_edges_fancy_arrow_patch()\n\u001b[1;32m 888\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[0;32m--> 889\u001b[0m edge_viz_obj \u001b[39m=\u001b[39m _draw_networkx_edges_fancy_arrow_patch()\n\u001b[1;32m 891\u001b[0m \u001b[39m# update view after drawing\u001b[39;00m\n\u001b[1;32m 892\u001b[0m padx, pady \u001b[39m=\u001b[39m \u001b[39m0.05\u001b[39m \u001b[39m*\u001b[39m w, \u001b[39m0.05\u001b[39m \u001b[39m*\u001b[39m h\n", - "File \u001b[0;32m~/.conda/envs/ocp/lib/python3.8/site-packages/networkx/drawing/nx_pylab.py:801\u001b[0m, in \u001b[0;36mdraw_networkx_edges.._draw_networkx_edges_fancy_arrow_patch\u001b[0;34m()\u001b[0m\n\u001b[1;32m 799\u001b[0m arrow_colors \u001b[39m=\u001b[39m mpl\u001b[39m.\u001b[39mcolors\u001b[39m.\u001b[39mcolorConverter\u001b[39m.\u001b[39mto_rgba_array(edge_color, alpha)\n\u001b[1;32m 800\u001b[0m \u001b[39mfor\u001b[39;00m i, (src, dst) \u001b[39min\u001b[39;00m \u001b[39menumerate\u001b[39m(edge_pos):\n\u001b[0;32m--> 801\u001b[0m x1, y1 \u001b[39m=\u001b[39m src\n\u001b[1;32m 802\u001b[0m x2, y2 \u001b[39m=\u001b[39m dst\n\u001b[1;32m 803\u001b[0m shrink_source \u001b[39m=\u001b[39m \u001b[39m0\u001b[39m \u001b[39m# space from source to tail\u001b[39;00m\n", - "\u001b[0;31mValueError\u001b[0m: too many values to unpack (expected 2)" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "print('Try 3D plots in networkx with atom positions')\n", - "# dico = {}\n", - "# for atom in atoms: \n", - "# dico[atom.item()] = b.pos[atom]\n", - "# pos = nx.spring_layout(G, dim=3)\n", - "# nx.draw(G, with_labels = True, pos=pos)\n", - "print('Networkx does not allow 3D positions')\n", - "\n", - "# See ressource https://www.westgrid.ca/files/3Dgraphs-WestGridWebinar-May24.2016.pdf" - ] - }, - { - "cell_type": "code", - "execution_count": 106, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "marker": { - "color": [ - 0, - 1, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 1, - 0, - 1, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 1, - 2, - 2, - 2, - 2 - ], - "colorscale": [ - [ - 0, - "#440154" - ], - [ - 0.1111111111111111, - "#482878" - ], - [ - 0.2222222222222222, - "#3e4989" - ], - [ - 0.3333333333333333, - "#31688e" - ], - [ - 0.4444444444444444, - "#26828e" - ], - [ - 0.5555555555555556, - "#1f9e89" - ], - [ - 0.6666666666666666, - "#35b779" - ], - [ - 0.7777777777777778, - "#6ece58" - ], - [ - 0.8888888888888888, - "#b5de2b" - ], - [ - 1, - "#fde725" - ] - ], - "size": 12 - }, - "mode": "markers+text", - "text": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39 - ], - "textfont": { - "size": 9 - }, - "type": "scatter3d", - "x": [ - 6.371547222137451, - 9.995777130126953, - 2.747317314147949, - 8.183662414550781, - 0.9352022409439087, - 4.559432506561279, - -2.747317314147949, - 0.8769128322601318, - 4.501142978668213, - 6.313258171081543, - -0.9352023005485535, - 2.689028024673462, - -1.4626051187515259, - 2.1616251468658447, - 5.785855293273926, - -3.2747199535369873, - 0.3495100140571594, - 3.9737401008605957, - 5.086834907531738, - 8.711065292358398, - 1.4626049995422363, - 6.898950099945068, - 10.52318000793457, - 3.2747201919555664, - 7.0749406814575195, - -0.17351983487606049, - 3.4507102966308594, - -1.9856349229812622, - 1.6385953426361084, - 5.2628254890441895, - 7.421979904174805, - 0.1735198050737381, - 3.7977499961853027, - 5.609865188598633, - 9.234095573425293, - 1.9856349229812622, - 8.298834800720215, - 8.543374061584473, - 8.665401458740234, - 8.593488693237305 - ], - "y": [ - 1.4460099935531616, - 1.4460099935531616, - 1.4460099935531616, - 2.221095323562622, - 2.221095323562622, - 2.221095323562622, - 5.888200759887695, - 5.888200759887695, - 5.888200759887695, - 5.113115310668945, - 5.113115310668945, - 5.113115310668945, - 4.154236316680908, - 4.154236316680908, - 4.154236316680908, - 6.847079753875732, - 6.847079753875732, - 6.847079753875732, - 3.1799745559692383, - 3.1799745559692383, - 3.1799745559692383, - 0.48713088035583496, - 0.48713088035583496, - 0.48713088035583496, - 4.409692287445068, - 4.409692287445068, - 4.409692287445068, - 6.591623783111572, - 6.591623783111572, - 6.591623783111572, - 2.924518585205078, - 2.924518585205078, - 2.924518585205078, - 0.7425868511199951, - 0.7425868511199951, - 0.7425868511199951, - 1.5586717128753662, - 2.2468039989471436, - 1.6635432243347168, - 3.314473867416382 - ], - "z": [ - 16.552318572998047, - 18.76727867126465, - 14.337358474731445, - 13.587244033813477, - 17.302433013916016, - 19.517393112182617, - 19.255401611328125, - 13.3252534866333, - 15.540214538574219, - 12.575139999389648, - 16.290328979492188, - 18.50528907775879, - 21.225845336914062, - 15.295697212219238, - 17.510658264160156, - 16.04581069946289, - 18.260770797729492, - 12.330622673034668, - 14.58187484741211, - 16.79683494567871, - 20.51202392578125, - 19.76190948486328, - 13.831761360168457, - 17.54694938659668, - 15.40139102935791, - 19.116579055786133, - 13.186430931091309, - 13.936544418334961, - 16.151504516601562, - 18.366464614868164, - 18.90610122680664, - 14.476181030273438, - 16.69114112854004, - 13.726067543029785, - 15.941027641296387, - 19.65621566772461, - 20.783090591430664, - 22.116159439086914, - 23.00487518310547, - 22.165855407714844 - ] - } - ], - "layout": { - "height": 700, - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "#2a3f5f" - }, - "error_y": { - "color": "#2a3f5f" - }, - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "baxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmap" - } - ], - "heatmapgl": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmapgl" - } - ], - "histogram": [ - { - "marker": { - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergl" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "#EBF0F8" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "#C8D4E3" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowcolor": "#2a3f5f", - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "#8e0152" - ], - [ - 0.1, - "#c51b7d" - ], - [ - 0.2, - "#de77ae" - ], - [ - 0.3, - "#f1b6da" - ], - [ - 0.4, - "#fde0ef" - ], - [ - 0.5, - "#f7f7f7" - ], - [ - 0.6, - "#e6f5d0" - ], - [ - 0.7, - "#b8e186" - ], - [ - 0.8, - "#7fbc41" - ], - [ - 0.9, - "#4d9221" - ], - [ - 1, - "#276419" - ] - ], - "sequential": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "sequentialminus": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ] - }, - "colorway": [ - "#636efa", - "#EF553B", - "#00cc96", - "#ab63fa", - "#FFA15A", - "#19d3f3", - "#FF6692", - "#B6E880", - "#FF97FF", - "#FECB52" - ], - "font": { - "color": "#2a3f5f" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "#E5ECF6", - "showlakes": true, - "showland": true, - "subunitcolor": "white" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "#E5ECF6", - "polar": { - "angularaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "radialaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "yaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "zaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - } - }, - "shapedefaults": { - "line": { - "color": "#2a3f5f" - } - }, - "ternary": { - "aaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "baxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "caxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - }, - "yaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - } - } - }, - "title": { - "text": "3D plot of one adslab" - }, - "width": 900 - } - }, - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# 3D plots with plotly \n", - "atoms = torch.argwhere(b.batch == graph_id).squeeze()\n", - "\n", - "# Separate atom coordinates\n", - "x = [b.pos[atom][0].item() for atom in atoms] \n", - "y = [b.pos[atom][1].item() for atom in atoms] \n", - "z = [b.pos[atom][2].item() for atom in atoms] \n", - "\n", - "trace = go.Scatter3d(\n", - " x = x, y = y, z = z,\n", - " mode = 'markers+text', \n", - " text = atoms,\n", - " textfont= dict(size=9),\n", - " marker = dict(\n", - " size = 12,\n", - " color = b.tags[atoms], # set color to an array/list of desired values\n", - " colorscale = 'Viridis'\n", - " )\n", - " )\n", - "layout = go.Layout(title = '3D plot of one adslab', width=900, height=700)\n", - "fig = go.Figure(data = [trace], layout = layout)\n", - "fig.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 107, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Offset neighbours are always a subset of neighbours node, for the first graph of the batch\n", - "neighbours: [0, 1, 5, 6, 8, 11, 12, 14, 15, 16, 19, 20, 21, 24, 25, 28, 29, 30, 34, 35, 37, 38, 39]\n", - "offset neighbours: [6, 8, 11, 12, 14, 15, 16, 20, 24, 25, 28, 29, 35]\n", - "non-offset neighbours: [0, 1, 5, 19, 21, 30, 34, 37, 38, 39]\n", - "not neighbours: [2, 3, 4, 7, 9, 10, 13, 17, 18, 22, 23, 26, 27, 31, 32, 33, 36]\n" - ] - } - ], - "source": [ - "print(\"Offset neighbours are always a subset of neighbours node, for the first graph of the batch\")\n", - "\n", - "ind = 36\n", - "edge_idx_with_nonzero_cell_offsets = g.edge_index[:, g.cell_offsets.nonzero()[:,0]]\n", - "offset_neighbours_idx = (edge_idx_with_nonzero_cell_offsets[1,:]==ind).nonzero().T[0].tolist()\n", - "offset_neighbours = edge_idx_with_nonzero_cell_offsets[:, offset_neighbours_idx][0,:].tolist()\n", - "neighbours = list(G.neighbors(ind))\n", - "non_offset_neighbours = sorted(list(set(neighbours) - set(offset_neighbours)))\n", - "not_neighbours = sorted(list( set(range(b.natoms[graph_id].item())) - set(neighbours)))\n", - "offset_neighbours = sorted(list(set(offset_neighbours)))\n", - "\n", - "# 3D plots with plotly \n", - "\n", - "# Define new colour scheme based on neighbourhood to see offset clearer\n", - "colour_list = []\n", - "for i, atom in enumerate(atoms):\n", - " if i in offset_neighbours: \n", - " colour_list.append(0)\n", - " elif i in non_offset_neighbours: \n", - " colour_list.append(1)\n", - " elif i in not_neighbours:\n", - " colour_list.append(2)\n", - "colour_list[ind]=3\n", - "\n", - "print('neighbours: ', neighbours)\n", - "print('offset neighbours: ', offset_neighbours)\n", - "print('non-offset neighbours: ', non_offset_neighbours)\n", - "print('not neighbours: ', not_neighbours)" - ] - }, - { - "cell_type": "code", - "execution_count": 108, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "marker": { - "color": [ - 1, - 1, - 2, - 2, - 2, - 1, - 0, - 2, - 0, - 2, - 2, - 0, - 0, - 2, - 0, - 0, - 0, - 2, - 2, - 1, - 0, - 1, - 2, - 2, - 0, - 0, - 2, - 2, - 0, - 0, - 1, - 2, - 2, - 2, - 1, - 0, - 3, - 1, - 1, - 1 - ], - "colorscale": [ - [ - 0, - "rgb(150,0,90)" - ], - [ - 0.125, - "rgb(0,0,200)" - ], - [ - 0.25, - "rgb(0,25,255)" - ], - [ - 0.375, - "rgb(0,152,255)" - ], - [ - 0.5, - "rgb(44,255,150)" - ], - [ - 0.625, - "rgb(151,255,0)" - ], - [ - 0.75, - "rgb(255,234,0)" - ], - [ - 0.875, - "rgb(255,111,0)" - ], - [ - 1, - "rgb(255,0,0)" - ] - ], - "size": 12 - }, - "mode": "markers+text", - "text": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39 - ], - "textfont": { - "size": 9 - }, - "type": "scatter3d", - "x": [ - 6.371547222137451, - 9.995777130126953, - 2.747317314147949, - 8.183662414550781, - 0.9352022409439087, - 4.559432506561279, - -2.747317314147949, - 0.8769128322601318, - 4.501142978668213, - 6.313258171081543, - -0.9352023005485535, - 2.689028024673462, - -1.4626051187515259, - 2.1616251468658447, - 5.785855293273926, - -3.2747199535369873, - 0.3495100140571594, - 3.9737401008605957, - 5.086834907531738, - 8.711065292358398, - 1.4626049995422363, - 6.898950099945068, - 10.52318000793457, - 3.2747201919555664, - 7.0749406814575195, - -0.17351983487606049, - 3.4507102966308594, - -1.9856349229812622, - 1.6385953426361084, - 5.2628254890441895, - 7.421979904174805, - 0.1735198050737381, - 3.7977499961853027, - 5.609865188598633, - 9.234095573425293, - 1.9856349229812622, - 8.298834800720215, - 8.543374061584473, - 8.665401458740234, - 8.593488693237305 - ], - "y": [ - 1.4460099935531616, - 1.4460099935531616, - 1.4460099935531616, - 2.221095323562622, - 2.221095323562622, - 2.221095323562622, - 5.888200759887695, - 5.888200759887695, - 5.888200759887695, - 5.113115310668945, - 5.113115310668945, - 5.113115310668945, - 4.154236316680908, - 4.154236316680908, - 4.154236316680908, - 6.847079753875732, - 6.847079753875732, - 6.847079753875732, - 3.1799745559692383, - 3.1799745559692383, - 3.1799745559692383, - 0.48713088035583496, - 0.48713088035583496, - 0.48713088035583496, - 4.409692287445068, - 4.409692287445068, - 4.409692287445068, - 6.591623783111572, - 6.591623783111572, - 6.591623783111572, - 2.924518585205078, - 2.924518585205078, - 2.924518585205078, - 0.7425868511199951, - 0.7425868511199951, - 0.7425868511199951, - 1.5586717128753662, - 2.2468039989471436, - 1.6635432243347168, - 3.314473867416382 - ], - "z": [ - 16.552318572998047, - 18.76727867126465, - 14.337358474731445, - 13.587244033813477, - 17.302433013916016, - 19.517393112182617, - 19.255401611328125, - 13.3252534866333, - 15.540214538574219, - 12.575139999389648, - 16.290328979492188, - 18.50528907775879, - 21.225845336914062, - 15.295697212219238, - 17.510658264160156, - 16.04581069946289, - 18.260770797729492, - 12.330622673034668, - 14.58187484741211, - 16.79683494567871, - 20.51202392578125, - 19.76190948486328, - 13.831761360168457, - 17.54694938659668, - 15.40139102935791, - 19.116579055786133, - 13.186430931091309, - 13.936544418334961, - 16.151504516601562, - 18.366464614868164, - 18.90610122680664, - 14.476181030273438, - 16.69114112854004, - 13.726067543029785, - 15.941027641296387, - 19.65621566772461, - 20.783090591430664, - 22.116159439086914, - 23.00487518310547, - 22.165855407714844 - ] - } - ], - "layout": { - "height": 700, - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "#2a3f5f" - }, - "error_y": { - "color": "#2a3f5f" - }, - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "baxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmap" - } - ], - "heatmapgl": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmapgl" - } - ], - "histogram": [ - { - "marker": { - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergl" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "#EBF0F8" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "#C8D4E3" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowcolor": "#2a3f5f", - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "#8e0152" - ], - [ - 0.1, - "#c51b7d" - ], - [ - 0.2, - "#de77ae" - ], - [ - 0.3, - "#f1b6da" - ], - [ - 0.4, - "#fde0ef" - ], - [ - 0.5, - "#f7f7f7" - ], - [ - 0.6, - "#e6f5d0" - ], - [ - 0.7, - "#b8e186" - ], - [ - 0.8, - "#7fbc41" - ], - [ - 0.9, - "#4d9221" - ], - [ - 1, - "#276419" - ] - ], - "sequential": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "sequentialminus": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ] - }, - "colorway": [ - "#636efa", - "#EF553B", - "#00cc96", - "#ab63fa", - "#FFA15A", - "#19d3f3", - "#FF6692", - "#B6E880", - "#FF97FF", - "#FECB52" - ], - "font": { - "color": "#2a3f5f" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "#E5ECF6", - "showlakes": true, - "showland": true, - "subunitcolor": "white" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "#E5ECF6", - "polar": { - "angularaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "radialaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "yaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "zaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - } - }, - "shapedefaults": { - "line": { - "color": "#2a3f5f" - } - }, - "ternary": { - "aaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "baxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "caxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - }, - "yaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - } - } - }, - "title": { - "text": "3D plot of one adslab" - }, - "width": 900 - } - }, - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# 3D plots with plotly \n", - "\n", - "# Define new colour scheme based on neighbourhood to see offset clearer\n", - "colour_list = []\n", - "for atom in atoms: \n", - " if atom.item() in offset_neighbours: \n", - " colour_list.append(0)\n", - " elif atom.item() in non_offset_neighbours: \n", - " colour_list.append(1)\n", - " elif atom.item() in not_neighbours:\n", - " colour_list.append(2)\n", - "colour_list[ind]=3\n", - "\n", - "\n", - "trace = go.Scatter3d(\n", - " x = x, y = y, z = z,\n", - " mode = 'markers+text', \n", - " text = atoms,\n", - " textfont= dict(size=9),\n", - " marker = dict(\n", - " size = 12,\n", - " color = colour_list, # set color to an array/list of desired values\n", - " colorscale = 'rainbow'\n", - " )\n", - " )\n", - "layout = go.Layout(title = '3D plot of one adslab', width=900, height=700)\n", - "fig = go.Figure(data = [trace], layout = layout)\n", - "fig.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Repeat with other node" - ] - }, - { - "cell_type": "code", - "execution_count": 109, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "marker": { - "color": [ - 3, - 1, - 1, - 1, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 1, - 1, - 2, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 1, - 2, - 1, - 1, - 1, - 1, - 1, - 2, - 2, - 2 - ], - "colorscale": [ - [ - 0, - "rgb(150,0,90)" - ], - [ - 0.125, - "rgb(0,0,200)" - ], - [ - 0.25, - "rgb(0,25,255)" - ], - [ - 0.375, - "rgb(0,152,255)" - ], - [ - 0.5, - "rgb(44,255,150)" - ], - [ - 0.625, - "rgb(151,255,0)" - ], - [ - 0.75, - "rgb(255,234,0)" - ], - [ - 0.875, - "rgb(255,111,0)" - ], - [ - 1, - "rgb(255,0,0)" - ] - ], - "size": 12 - }, - "mode": "markers+text", - "text": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39 - ], - "textfont": { - "size": 9 - }, - "type": "scatter3d", - "x": [ - 6.371547222137451, - 9.995777130126953, - 2.747317314147949, - 8.183662414550781, - 0.9352022409439087, - 4.559432506561279, - -2.747317314147949, - 0.8769128322601318, - 4.501142978668213, - 6.313258171081543, - -0.9352023005485535, - 2.689028024673462, - -1.4626051187515259, - 2.1616251468658447, - 5.785855293273926, - -3.2747199535369873, - 0.3495100140571594, - 3.9737401008605957, - 5.086834907531738, - 8.711065292358398, - 1.4626049995422363, - 6.898950099945068, - 10.52318000793457, - 3.2747201919555664, - 7.0749406814575195, - -0.17351983487606049, - 3.4507102966308594, - -1.9856349229812622, - 1.6385953426361084, - 5.2628254890441895, - 7.421979904174805, - 0.1735198050737381, - 3.7977499961853027, - 5.609865188598633, - 9.234095573425293, - 1.9856349229812622, - 8.298834800720215, - 8.543374061584473, - 8.665401458740234, - 8.593488693237305 - ], - "y": [ - 1.4460099935531616, - 1.4460099935531616, - 1.4460099935531616, - 2.221095323562622, - 2.221095323562622, - 2.221095323562622, - 5.888200759887695, - 5.888200759887695, - 5.888200759887695, - 5.113115310668945, - 5.113115310668945, - 5.113115310668945, - 4.154236316680908, - 4.154236316680908, - 4.154236316680908, - 6.847079753875732, - 6.847079753875732, - 6.847079753875732, - 3.1799745559692383, - 3.1799745559692383, - 3.1799745559692383, - 0.48713088035583496, - 0.48713088035583496, - 0.48713088035583496, - 4.409692287445068, - 4.409692287445068, - 4.409692287445068, - 6.591623783111572, - 6.591623783111572, - 6.591623783111572, - 2.924518585205078, - 2.924518585205078, - 2.924518585205078, - 0.7425868511199951, - 0.7425868511199951, - 0.7425868511199951, - 1.5586717128753662, - 2.2468039989471436, - 1.6635432243347168, - 3.314473867416382 - ], - "z": [ - 16.552318572998047, - 18.76727867126465, - 14.337358474731445, - 13.587244033813477, - 17.302433013916016, - 19.517393112182617, - 19.255401611328125, - 13.3252534866333, - 15.540214538574219, - 12.575139999389648, - 16.290328979492188, - 18.50528907775879, - 21.225845336914062, - 15.295697212219238, - 17.510658264160156, - 16.04581069946289, - 18.260770797729492, - 12.330622673034668, - 14.58187484741211, - 16.79683494567871, - 20.51202392578125, - 19.76190948486328, - 13.831761360168457, - 17.54694938659668, - 15.40139102935791, - 19.116579055786133, - 13.186430931091309, - 13.936544418334961, - 16.151504516601562, - 18.366464614868164, - 18.90610122680664, - 14.476181030273438, - 16.69114112854004, - 13.726067543029785, - 15.941027641296387, - 19.65621566772461, - 20.783090591430664, - 22.116159439086914, - 23.00487518310547, - 22.165855407714844 - ] - } - ], - "layout": { - "height": 700, - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "#2a3f5f" - }, - "error_y": { - "color": "#2a3f5f" - }, - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "baxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmap" - } - ], - "heatmapgl": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmapgl" - } - ], - "histogram": [ - { - "marker": { - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergl" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "#EBF0F8" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "#C8D4E3" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowcolor": "#2a3f5f", - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "#8e0152" - ], - [ - 0.1, - "#c51b7d" - ], - [ - 0.2, - "#de77ae" - ], - [ - 0.3, - "#f1b6da" - ], - [ - 0.4, - "#fde0ef" - ], - [ - 0.5, - "#f7f7f7" - ], - [ - 0.6, - "#e6f5d0" - ], - [ - 0.7, - "#b8e186" - ], - [ - 0.8, - "#7fbc41" - ], - [ - 0.9, - "#4d9221" - ], - [ - 1, - "#276419" - ] - ], - "sequential": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "sequentialminus": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ] - }, - "colorway": [ - "#636efa", - "#EF553B", - "#00cc96", - "#ab63fa", - "#FFA15A", - "#19d3f3", - "#FF6692", - "#B6E880", - "#FF97FF", - "#FECB52" - ], - "font": { - "color": "#2a3f5f" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "#E5ECF6", - "showlakes": true, - "showland": true, - "subunitcolor": "white" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "#E5ECF6", - "polar": { - "angularaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "radialaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "yaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "zaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - } - }, - "shapedefaults": { - "line": { - "color": "#2a3f5f" - } - }, - "ternary": { - "aaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "baxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "caxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - }, - "yaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - } - } - }, - "title": { - "text": "3D plot of one adslab - new node" - }, - "width": 900 - } - }, - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "ind = 0\n", - "edge_idx_with_nonzero_cell_offsets = g.edge_index[:, g.cell_offsets.nonzero()[:,0]]\n", - "offset_neighbours_idx = (edge_idx_with_nonzero_cell_offsets[1,:]==ind).nonzero().T[0].tolist()\n", - "offset_neighbours = edge_idx_with_nonzero_cell_offsets[:, offset_neighbours_idx][0,:].tolist()\n", - "neighbours = list(G.neighbors(ind))\n", - "non_offset_neighbours = sorted(list(set(neighbours) - set(offset_neighbours)))\n", - "not_neighbours = sorted(list( set(range(b.natoms[graph_id].item())) - set(neighbours)))\n", - "offset_neighbours = sorted(list(set(offset_neighbours)))\n", - "\n", - "\n", - "# 3D plots with plotly \n", - "\n", - "# Define new colour scheme based on neighbourhood to see offset clearer\n", - "colour_list = []\n", - "for atom in atoms: \n", - " if atom.item() in offset_neighbours: \n", - " colour_list.append(0)\n", - " elif atom.item() in non_offset_neighbours: \n", - " colour_list.append(1)\n", - " elif atom.item() in not_neighbours:\n", - " colour_list.append(2)\n", - "colour_list[ind]=3\n", - "\n", - "\n", - "trace = go.Scatter3d(\n", - " x = x, y = y, z = z,\n", - " mode = 'markers+text', \n", - " text = atoms,\n", - " textfont= dict(size=9),\n", - " marker = dict(\n", - " size = 12,\n", - " color = colour_list, # set color to an array/list of desired values\n", - " colorscale = 'rainbow'\n", - " )\n", - " )\n", - "layout = go.Layout(title = '3D plot of one adslab - new node', width=900, height=700)\n", - "fig = go.Figure(data = [trace], layout = layout)\n", - "fig.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Repeat with other graph" - ] - }, - { - "cell_type": "code", - "execution_count": 110, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "marker": { - "color": [ - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 2, - 2, - 2, - 2 - ], - "colorscale": [ - [ - 0, - "#440154" - ], - [ - 0.1111111111111111, - "#482878" - ], - [ - 0.2222222222222222, - "#3e4989" - ], - [ - 0.3333333333333333, - "#31688e" - ], - [ - 0.4444444444444444, - "#26828e" - ], - [ - 0.5555555555555556, - "#1f9e89" - ], - [ - 0.6666666666666666, - "#35b779" - ], - [ - 0.7777777777777778, - "#6ece58" - ], - [ - 0.8888888888888888, - "#b5de2b" - ], - [ - 1, - "#fde725" - ] - ], - "size": 12 - }, - "mode": "markers+text", - "text": [ - "0", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", - "13", - "14", - "15", - "16", - "17", - "18", - "19", - "20", - "21", - "22", - "23", - "24", - "25", - "26", - "27", - "28", - "29", - "30", - "31", - "32", - "33", - "34", - "35", - "36", - "37", - "38", - "39", - "40", - "41", - "42", - "43", - "44", - "45", - "46", - "47", - "48", - "49", - "50", - "51", - "52", - "53", - "54", - "55", - "56", - "57", - "58", - "59", - "60", - "61", - "62", - "63" - ], - "textfont": { - "size": 9 - }, - "type": "scatter3d", - "x": [ - 3.262456178665161, - 2.9711008071899414, - 2.211700677871704, - 1.920345425605774, - 1.1609452962875366, - 0.8695899844169617, - 8.597650527954102, - 8.306295394897461, - 7.5468950271606445, - 7.255539894104004, - 6.4961395263671875, - 6.204784393310547, - 0.5305982828140259, - 5.7843756675720215, - 4.815037250518799, - 4.7336201667785645, - 4.65220308303833, - 3.6828649044036865, - 5.865792751312256, - 11.119569778442383, - 10.15023136138916, - 10.068814277648926, - 9.987397193908691, - 9.018058776855469, - 6.054972171783447, - 5.513779163360596, - 4.141380310058594, - 1.2042553424835205, - 5.0960845947265625, - 2.188228130340576, - 5.00421667098999, - 4.463023662567139, - 3.978546142578125, - 0.15349985659122467, - 4.0453290939331055, - 1.1374726295471191, - 3.953461170196533, - 3.4122684001922607, - 2.927790880203247, - 5.325860500335693, - 2.9945735931396484, - 5.421911239624023, - 11.390166282653809, - 10.848973274230957, - 9.476574897766113, - 6.539449691772461, - 10.431279182434082, - 7.5234222412109375, - 10.339410781860352, - 9.7982177734375, - 9.313740730285645, - 5.488694190979004, - 9.380523681640625, - 6.4726667404174805, - 9.288655281066895, - 8.747462272644043, - 8.262985229492188, - 10.661054611206055, - 8.329768180847168, - 10.757105827331543, - 4.430228233337402, - 4.249081611633301, - 4.174334526062012, - 4.1963114738464355 - ], - "y": [ - 7.084805011749268, - 2.869880199432373, - 6.320659160614014, - 2.10573410987854, - 5.55651330947876, - 1.3415882587432861, - 7.084805011749268, - 2.869880199432373, - 6.320659160614014, - 2.10573410987854, - 5.55651330947876, - 1.3415882587432861, - 1.1566129922866821, - 4.97734260559082, - 0.39246702194213867, - 4.213196754455566, - 8.033926010131836, - 3.4490509033203125, - 1.1566129922866821, - 4.97734260559082, - 0.39246702194213867, - 4.213196754455566, - 8.033926010131836, - 3.4490509033203125, - 6.862978458404541, - 3.0917067527770996, - 0.5269917845726013, - 1.0220882892608643, - 5.798619747161865, - 4.9202117919921875, - 6.098832607269287, - 2.3275606632232666, - 8.168451309204102, - 0.2579423785209656, - 5.034473896026611, - 4.156065464019775, - 5.334686756134033, - 1.5634148120880127, - 7.4043049812316895, - 7.899401664733887, - 4.270327568054199, - 3.3919198513031006, - 6.862978458404541, - 3.0917067527770996, - 0.5269917845726013, - 1.0220882892608643, - 5.798619747161865, - 4.9202117919921875, - 6.098832607269287, - 2.3275606632232666, - 8.168451309204102, - 0.2579423785209656, - 5.034473896026611, - 4.156065464019775, - 5.334686756134033, - 1.5634148120880127, - 7.4043049812316895, - 7.899401664733887, - 4.270327568054199, - 3.3919198513031006, - 6.962096214294434, - 7.260545253753662, - 6.457048416137695, - 8.274133682250977 - ], - "z": [ - 11.828250885009766, - 12.184473991394043, - 14.528149604797363, - 14.88437271118164, - 17.228046417236328, - 17.584270477294922, - 12.2420654296875, - 12.598288536071777, - 14.941963195800781, - 15.298187255859375, - 17.641860961914062, - 17.998085021972656, - 10.74976921081543, - 12.21327018737793, - 13.863481521606445, - 14.913167953491211, - 15.962854385375977, - 17.613065719604492, - 11.163583755493164, - 12.627084732055664, - 14.27729606628418, - 15.326982498168945, - 16.37666893005371, - 18.026880264282227, - 12.852269172668457, - 11.574271202087402, - 11.976825714111328, - 12.636425971984863, - 10.515288352966309, - 10.797539710998535, - 15.552166938781738, - 14.274168968200684, - 14.076197624206543, - 15.336323738098145, - 13.21518611907959, - 13.497437477111816, - 18.252065658569336, - 16.97406768798828, - 16.77609634399414, - 17.849510192871094, - 15.915083885192871, - 16.61115074157715, - 13.266083717346191, - 11.988085746765137, - 12.390640258789062, - 13.050240516662598, - 10.929102897644043, - 11.21135425567627, - 15.965981483459473, - 14.687983512878418, - 14.490012168884277, - 15.750138282775879, - 13.629000663757324, - 13.91125202178955, - 18.66588020324707, - 17.387882232666016, - 17.189910888671875, - 18.263324737548828, - 16.328899383544922, - 17.024965286254883, - 19.761154174804688, - 21.240516662597656, - 21.943159103393555, - 21.579269409179688 - ] - } - ], - "layout": { - "height": 700, - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "#2a3f5f" - }, - "error_y": { - "color": "#2a3f5f" - }, - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "baxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmap" - } - ], - "heatmapgl": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmapgl" - } - ], - "histogram": [ - { - "marker": { - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergl" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "#EBF0F8" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "#C8D4E3" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowcolor": "#2a3f5f", - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "#8e0152" - ], - [ - 0.1, - "#c51b7d" - ], - [ - 0.2, - "#de77ae" - ], - [ - 0.3, - "#f1b6da" - ], - [ - 0.4, - "#fde0ef" - ], - [ - 0.5, - "#f7f7f7" - ], - [ - 0.6, - "#e6f5d0" - ], - [ - 0.7, - "#b8e186" - ], - [ - 0.8, - "#7fbc41" - ], - [ - 0.9, - "#4d9221" - ], - [ - 1, - "#276419" - ] - ], - "sequential": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "sequentialminus": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ] - }, - "colorway": [ - "#636efa", - "#EF553B", - "#00cc96", - "#ab63fa", - "#FFA15A", - "#19d3f3", - "#FF6692", - "#B6E880", - "#FF97FF", - "#FECB52" - ], - "font": { - "color": "#2a3f5f" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "#E5ECF6", - "showlakes": true, - "showland": true, - "subunitcolor": "white" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "#E5ECF6", - "polar": { - "angularaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "radialaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "yaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "zaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - } - }, - "shapedefaults": { - "line": { - "color": "#2a3f5f" - } - }, - "ternary": { - "aaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "baxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "caxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - }, - "yaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - } - } - }, - "title": { - "text": "3D plot of one adslab - new graph" - }, - "width": 900 - } - }, - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Graph\n", - "graph_id = 40\n", - "g = b[graph_id]\n", - "G = to_networkx(g)\n", - "atoms = torch.argwhere(b.batch == graph_id).squeeze()\n", - "\n", - "# 3D plots with plotly \n", - "\n", - "# Separate atom coordinates\n", - "x = [b.pos[atom][0].item() for atom in atoms] \n", - "y = [b.pos[atom][1].item() for atom in atoms] \n", - "z = [b.pos[atom][2].item() for atom in atoms] \n", - "\n", - "trace = go.Scatter3d(\n", - " x = x, y = y, z = z,\n", - " mode = 'markers+text', \n", - " text = list(range(atoms.shape[0])),\n", - " textfont= dict(size=9),\n", - " marker = dict(\n", - " size = 12,\n", - " color = b.tags[atoms], # set color to an array/list of desired values\n", - " colorscale = 'Viridis'\n", - " )\n", - " )\n", - "layout = go.Layout(title = '3D plot of one adslab - new graph', width=900, height=700)\n", - "fig = go.Figure(data = [trace], layout = layout)\n", - "fig.show()" - ] + "source": [] }, { "cell_type": "code", - "execution_count": 111, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "marker": { - "color": [ - 3, - 0, - 1, - 0, - 1, - 1, - 0, - 2, - 2, - 2, - 2, - 2, - 0, - 1, - 0, - 1, - 1, - 2, - 0, - 0, - 0, - 0, - 0, - 2, - 1, - 0, - 0, - 0, - 1, - 1, - 1, - 0, - 1, - 0, - 1, - 0, - 2, - 0, - 1, - 2, - 1, - 2, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 1, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 - ], - "colorscale": [ - [ - 0, - "rgb(150,0,90)" - ], - [ - 0.125, - "rgb(0,0,200)" - ], - [ - 0.25, - "rgb(0,25,255)" - ], - [ - 0.375, - "rgb(0,152,255)" - ], - [ - 0.5, - "rgb(44,255,150)" - ], - [ - 0.625, - "rgb(151,255,0)" - ], - [ - 0.75, - "rgb(255,234,0)" - ], - [ - 0.875, - "rgb(255,111,0)" - ], - [ - 1, - "rgb(255,0,0)" - ] - ], - "size": 12 - }, - "mode": "markers+text", - "text": [ - 2920, - 2921, - 2922, - 2923, - 2924, - 2925, - 2926, - 2927, - 2928, - 2929, - 2930, - 2931, - 2932, - 2933, - 2934, - 2935, - 2936, - 2937, - 2938, - 2939, - 2940, - 2941, - 2942, - 2943, - 2944, - 2945, - 2946, - 2947, - 2948, - 2949, - 2950, - 2951, - 2952, - 2953, - 2954, - 2955, - 2956, - 2957, - 2958, - 2959, - 2960, - 2961, - 2962, - 2963, - 2964, - 2965, - 2966, - 2967, - 2968, - 2969, - 2970, - 2971, - 2972, - 2973, - 2974, - 2975, - 2976, - 2977, - 2978, - 2979, - 2980, - 2981, - 2982, - 2983 - ], - "textfont": { - "size": 9 - }, - "type": "scatter3d", - "x": [ - 3.262456178665161, - 2.9711008071899414, - 2.211700677871704, - 1.920345425605774, - 1.1609452962875366, - 0.8695899844169617, - 8.597650527954102, - 8.306295394897461, - 7.5468950271606445, - 7.255539894104004, - 6.4961395263671875, - 6.204784393310547, - 0.5305982828140259, - 5.7843756675720215, - 4.815037250518799, - 4.7336201667785645, - 4.65220308303833, - 3.6828649044036865, - 5.865792751312256, - 11.119569778442383, - 10.15023136138916, - 10.068814277648926, - 9.987397193908691, - 9.018058776855469, - 6.054972171783447, - 5.513779163360596, - 4.141380310058594, - 1.2042553424835205, - 5.0960845947265625, - 2.188228130340576, - 5.00421667098999, - 4.463023662567139, - 3.978546142578125, - 0.15349985659122467, - 4.0453290939331055, - 1.1374726295471191, - 3.953461170196533, - 3.4122684001922607, - 2.927790880203247, - 5.325860500335693, - 2.9945735931396484, - 5.421911239624023, - 11.390166282653809, - 10.848973274230957, - 9.476574897766113, - 6.539449691772461, - 10.431279182434082, - 7.5234222412109375, - 10.339410781860352, - 9.7982177734375, - 9.313740730285645, - 5.488694190979004, - 9.380523681640625, - 6.4726667404174805, - 9.288655281066895, - 8.747462272644043, - 8.262985229492188, - 10.661054611206055, - 8.329768180847168, - 10.757105827331543, - 4.430228233337402, - 4.249081611633301, - 4.174334526062012, - 4.1963114738464355 - ], - "y": [ - 7.084805011749268, - 2.869880199432373, - 6.320659160614014, - 2.10573410987854, - 5.55651330947876, - 1.3415882587432861, - 7.084805011749268, - 2.869880199432373, - 6.320659160614014, - 2.10573410987854, - 5.55651330947876, - 1.3415882587432861, - 1.1566129922866821, - 4.97734260559082, - 0.39246702194213867, - 4.213196754455566, - 8.033926010131836, - 3.4490509033203125, - 1.1566129922866821, - 4.97734260559082, - 0.39246702194213867, - 4.213196754455566, - 8.033926010131836, - 3.4490509033203125, - 6.862978458404541, - 3.0917067527770996, - 0.5269917845726013, - 1.0220882892608643, - 5.798619747161865, - 4.9202117919921875, - 6.098832607269287, - 2.3275606632232666, - 8.168451309204102, - 0.2579423785209656, - 5.034473896026611, - 4.156065464019775, - 5.334686756134033, - 1.5634148120880127, - 7.4043049812316895, - 7.899401664733887, - 4.270327568054199, - 3.3919198513031006, - 6.862978458404541, - 3.0917067527770996, - 0.5269917845726013, - 1.0220882892608643, - 5.798619747161865, - 4.9202117919921875, - 6.098832607269287, - 2.3275606632232666, - 8.168451309204102, - 0.2579423785209656, - 5.034473896026611, - 4.156065464019775, - 5.334686756134033, - 1.5634148120880127, - 7.4043049812316895, - 7.899401664733887, - 4.270327568054199, - 3.3919198513031006, - 6.962096214294434, - 7.260545253753662, - 6.457048416137695, - 8.274133682250977 - ], - "z": [ - 11.828250885009766, - 12.184473991394043, - 14.528149604797363, - 14.88437271118164, - 17.228046417236328, - 17.584270477294922, - 12.2420654296875, - 12.598288536071777, - 14.941963195800781, - 15.298187255859375, - 17.641860961914062, - 17.998085021972656, - 10.74976921081543, - 12.21327018737793, - 13.863481521606445, - 14.913167953491211, - 15.962854385375977, - 17.613065719604492, - 11.163583755493164, - 12.627084732055664, - 14.27729606628418, - 15.326982498168945, - 16.37666893005371, - 18.026880264282227, - 12.852269172668457, - 11.574271202087402, - 11.976825714111328, - 12.636425971984863, - 10.515288352966309, - 10.797539710998535, - 15.552166938781738, - 14.274168968200684, - 14.076197624206543, - 15.336323738098145, - 13.21518611907959, - 13.497437477111816, - 18.252065658569336, - 16.97406768798828, - 16.77609634399414, - 17.849510192871094, - 15.915083885192871, - 16.61115074157715, - 13.266083717346191, - 11.988085746765137, - 12.390640258789062, - 13.050240516662598, - 10.929102897644043, - 11.21135425567627, - 15.965981483459473, - 14.687983512878418, - 14.490012168884277, - 15.750138282775879, - 13.629000663757324, - 13.91125202178955, - 18.66588020324707, - 17.387882232666016, - 17.189910888671875, - 18.263324737548828, - 16.328899383544922, - 17.024965286254883, - 19.761154174804688, - 21.240516662597656, - 21.943159103393555, - 21.579269409179688 - ] - } - ], - "layout": { - "height": 700, - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "#2a3f5f" - }, - "error_y": { - "color": "#2a3f5f" - }, - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "baxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmap" - } - ], - "heatmapgl": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmapgl" - } - ], - "histogram": [ - { - "marker": { - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergl" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "#EBF0F8" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "#C8D4E3" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowcolor": "#2a3f5f", - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "#8e0152" - ], - [ - 0.1, - "#c51b7d" - ], - [ - 0.2, - "#de77ae" - ], - [ - 0.3, - "#f1b6da" - ], - [ - 0.4, - "#fde0ef" - ], - [ - 0.5, - "#f7f7f7" - ], - [ - 0.6, - "#e6f5d0" - ], - [ - 0.7, - "#b8e186" - ], - [ - 0.8, - "#7fbc41" - ], - [ - 0.9, - "#4d9221" - ], - [ - 1, - "#276419" - ] - ], - "sequential": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "sequentialminus": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ] - }, - "colorway": [ - "#636efa", - "#EF553B", - "#00cc96", - "#ab63fa", - "#FFA15A", - "#19d3f3", - "#FF6692", - "#B6E880", - "#FF97FF", - "#FECB52" - ], - "font": { - "color": "#2a3f5f" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "#E5ECF6", - "showlakes": true, - "showland": true, - "subunitcolor": "white" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "#E5ECF6", - "polar": { - "angularaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "radialaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "yaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "zaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - } - }, - "shapedefaults": { - "line": { - "color": "#2a3f5f" - } - }, - "ternary": { - "aaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "baxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "caxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - }, - "yaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - } - } - }, - "title": { - "text": "3D plot of one adslab" - }, - "width": 900 - } - }, - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "ind = 0\n", - "edge_idx_with_nonzero_cell_offsets = g.edge_index[:, g.cell_offsets.nonzero()[:,0]]\n", - "offset_neighbours_idx = (edge_idx_with_nonzero_cell_offsets[1,:]==ind).nonzero().T[0].tolist()\n", - "offset_neighbours = edge_idx_with_nonzero_cell_offsets[:, offset_neighbours_idx][0,:].tolist()\n", - "neighbours = list(G.neighbors(ind))\n", - "non_offset_neighbours = sorted(list(set(neighbours) - set(offset_neighbours)))\n", - "not_neighbours = sorted(list( set(range(b.natoms[graph_id].item())) - set(neighbours)))\n", - "offset_neighbours = sorted(list(set(offset_neighbours)))\n", - "\n", - "\n", - "# 3D plots with plotly \n", - "\n", - "# Define new colour scheme based on neighbourhood to see offset clearer\n", - "colour_list = []\n", - "for i, atom in enumerate(atoms):\n", - " if i in offset_neighbours: \n", - " colour_list.append(0)\n", - " elif i in non_offset_neighbours: \n", - " colour_list.append(1)\n", - " elif i in not_neighbours:\n", - " colour_list.append(2)\n", - "colour_list[ind]=3\n", - "\n", - "\n", - "trace = go.Scatter3d(\n", - " x = x, y = y, z = z,\n", - " mode = 'markers+text', \n", - " text = atoms,\n", - " textfont= dict(size=9),\n", - " marker = dict(\n", - " size = 12,\n", - " color = colour_list, # set color to an array/list of desired values\n", - " colorscale = 'rainbow'\n", - " )\n", - " )\n", - "layout = go.Layout(title = '3D plot of one adslab', width=900, height=700)\n", - "fig = go.Figure(data = [trace], layout = layout)\n", - "fig.show()" - ] + "outputs": [], + "source": [] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Change colours " - ] + "source": [] }, { "cell_type": "code", @@ -7668,7 +1281,7 @@ ], "metadata": { "interpreter": { - "hash": "8f46ba4cd7afc55211ef30a8756878c586436ef3efb2502902a1a7762250021d" + "hash": "4f2c5bf54521b1a83ada8aefbaac3b30bb3595b13a57b33ff84db64c4738a09f" }, "kernelspec": { "display_name": "Python 3 (ocp-env)", @@ -7686,8 +1299,9 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.13" - } + }, + "orig_nbformat": 4 }, "nbformat": 4, - "nbformat_minor": 4 + "nbformat_minor": 2 } \ No newline at end of file diff --git a/notebooks/is2re_stats.ipynb b/notebooks/is2re_stats.ipynb new file mode 100644 index 0000000000..2b0b9af1bb --- /dev/null +++ b/notebooks/is2re_stats.ipynb @@ -0,0 +1,551 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "from pathlib import Path\n", + "\n", + "sys.path.append(str(Path().resolve().parent))" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from ocpmodels.common.utils import make_trainer_from_conf_str\n", + "import numpy as np\n", + "import torch\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns\n", + "from collections import defaultdict, Counter\n", + "import pandas as pd\n", + "import os\n", + "from tqdm.notebook import tqdm\n", + "\n", + "from pymatgen.core.periodic_table import Element\n", + "from pymatgen.core.composition import Composition\n", + "\n", + "torch.set_grad_enabled(False)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def to_reduced_formula(list_of_z):\n", + " return Composition.from_dict(\n", + " Counter([Element.from_Z(i).symbol for i in list_of_z])\n", + " ).reduced_formula" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🏭 Overriding num_workers from 4 to 23 to match the machine's CPUs. Use --no_cpus_to_workers=true to disable this behavior.\n", + "Setting max_steps to 21578 from max_epochs (12), dataset length (460328), and batch_size (256)\n", + "\n", + "🗑️ Setting dropout_lin for output block to 0.0\n", + "⛄️ No layer to freeze\n", + "\n", + "Using max_steps for scheduler -> 21578\n" + ] + } + ], + "source": [ + "trainer = make_trainer_from_conf_str(\n", + " \"faenet-is2re-all\",\n", + " overrides={\n", + " \"is_debug\": True,\n", + " \"graph_rewiring\": \"\",\n", + " \"optim\": {\n", + " \"batch_size\": 256,\n", + " },\n", + " \"task\": {\n", + " \"dataset\": \"stats_lmdb\",\n", + " }\n", + " },\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "batch = next(iter(trainer.loaders[\"train\"]))[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Data(\n", + " edge_index=[2, 2985],\n", + " pos=[65, 3],\n", + " cell=[1, 3, 3],\n", + " atomic_numbers=[65],\n", + " natoms=[1],\n", + " cell_offsets=[2985, 3],\n", + " force=[65, 3],\n", + " distances=[2985],\n", + " fixed=[65],\n", + " sid=[1],\n", + " tags=[65],\n", + " y_init=[1],\n", + " y_relaxed=[1],\n", + " pos_relaxed=[65, 3],\n", + " id='0_256684',\n", + " load_time=[1],\n", + " transform_time=[1],\n", + " total_get_time=[1],\n", + " idx_in_dataset=[1],\n", + " stats={\n", + " atomic_numbers_bulk=[64],\n", + " atomic_numbers_ads=[1],\n", + " composition_bulk='ZnSnN2',\n", + " composition_ads='H2',\n", + " idx_in_dataset=[1],\n", + " sid=[1],\n", + " y_relaxed=[1],\n", + " y_init=[1]\n", + " }\n", + ")\n", + "2077917\n" + ] + }, + { + "data": { + "text/plain": [ + "tensor([0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1,\n", + " 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0,\n", + " 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 2])" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sample = batch.to_data_list()[0]\n", + "print(sample)\n", + "print(sample.stats[\"sid\"][0])\n", + "sample[\"tags\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# %timeit to_reduced_formula(sample.atomic_numbers.int())\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# from multiprocessing import Pool\n", + "\n", + "\n", + "# def make_entry(batch_list):\n", + "# batch = batch_list[0]\n", + "# entries = []\n", + "# for sample in batch.to_data_list():\n", + "# entries.append(\n", + "# {\n", + "# \"atomic_numbers\": sample.atomic_numbers.int().tolist(),\n", + "# \"composition\": to_reduced_formula(sample.atomic_numbers.int()),\n", + "# \"idx_in_dataset\": sample.idx_in_dataset.item(),\n", + "# \"sid\": sample.sid.item(),\n", + "# \"y_relaxed\": sample.y_relaxed.item(),\n", + "# \"y_init\": sample.y_init.item(),\n", + "# }\n", + "# )\n", + "# return entries\n", + "\n", + "\n", + "# num_workers = trainer.loaders[\"train\"].num_workers * 2\n", + "# # iterate over batches by chunks of n_workers\n", + "# iterator = iter(trainer.loaders[\"train\"])\n", + "# n_iters = len(trainer.loaders[\"train\"]) // num_workers\n", + "# if len(trainer.loaders[\"train\"]) % num_workers != 0:\n", + "# n_iters += 1\n", + "\n", + "\n", + "# entries = []\n", + "# for _ in tqdm(range(n_iters)):\n", + "# batch_list = []\n", + "# for _ in tqdm(range(num_workers), leave=False):\n", + "# try:\n", + "# batch_list.append(next(iterator))\n", + "# except StopIteration:\n", + "# break\n", + "# with Pool(num_workers) as p:\n", + "# entries += sum(p.map(make_entry, batch_list), [])" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "342cec9a74884e2b86183bc9dbfd42e5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/1799 [00:00\n", + "RangeIndex: 460328 entries, 0 to 460327\n", + "Data columns (total 8 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 atomic_numbers_bulk 460328 non-null object \n", + " 1 atomic_numbers_ads 460328 non-null object \n", + " 2 composition_bulk 460328 non-null object \n", + " 3 composition_ads 460328 non-null object \n", + " 4 idx_in_dataset 460328 non-null int64 \n", + " 5 sid 460328 non-null int64 \n", + " 6 y_relaxed 460328 non-null float64\n", + " 7 y_init 460328 non-null float64\n", + "dtypes: float64(2), int64(2), object(4)\n", + "memory usage: 28.1+ MB\n" + ] + } + ], + "source": [ + "df = pd.DataFrame(flat_entries) # df = pd.read_json(\"/network/scratch/s/schmidtv/crystals-proxys/data/is2re/comp.json\")\n", + "desc = df.describe()\n", + "df.info()\n", + "df.to_json(\"/network/scratch/s/schmidtv/crystals-proxys/data/is2re/comp.json\")\n", + "desc.to_json(\"/network/scratch/s/schmidtv/crystals-proxys/data/is2re/description.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.read_json(\"/network/scratch/s/schmidtv/crystals-proxys/data/is2re/comp.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "a5213b2652df4f78b594f0aaf551d83a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/460328 [00:00 1\u001b[0m \u001b[43mtrainer\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mloaders\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mtrain\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m]\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: 'DataLoader' object is not subscriptable" + ] + } + ], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ce" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.16" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/ocdata/LiFePO4.cif b/ocdata/LiFePO4.cif new file mode 100644 index 0000000000..2b01776980 --- /dev/null +++ b/ocdata/LiFePO4.cif @@ -0,0 +1,54 @@ +# generated using pymatgen +data_LiFePO4 +_symmetry_space_group_name_H-M 'P 1' +_cell_length_a 4.74644100 +_cell_length_b 10.44373000 +_cell_length_c 6.09022600 +_cell_angle_alpha 89.99726981 +_cell_angle_beta 90.00071024 +_cell_angle_gamma 90.00075935 +_symmetry_Int_Tables_number 1 +_chemical_formula_structural LiFePO4 +_chemical_formula_sum 'Li4 Fe4 P4 O16' +_cell_volume 301.89584168 +_cell_formula_units_Z 4 +loop_ + _symmetry_equiv_pos_site_id + _symmetry_equiv_pos_as_xyz + 1 'x, y, z' +loop_ + _atom_site_type_symbol + _atom_site_label + _atom_site_symmetry_multiplicity + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_occupancy + Li Li0 1 0.00000100 0.00001200 0.00003300 1 + Li Li1 1 0.50000600 0.50000900 0.00003100 1 + Li Li2 1 0.50000300 0.50001200 0.49996900 1 + Li Li3 1 0.00000300 0.00001400 0.49996700 1 + Fe Fe4 1 0.47524900 0.21803400 0.75000400 1 + Fe Fe5 1 0.02475600 0.71803500 0.75000300 1 + Fe Fe6 1 0.97510000 0.28190400 0.25000000 1 + Fe Fe7 1 0.52491600 0.78190200 0.25000000 1 + P P8 1 0.41781800 0.09476500 0.24999700 1 + P P9 1 0.91789000 0.40522300 0.75000500 1 + P P10 1 0.08218100 0.59476800 0.24999700 1 + P P11 1 0.58210900 0.90522200 0.75000500 1 + O O12 1 0.74186800 0.09673300 0.25000400 1 + O O13 1 0.24192700 0.40323600 0.74999600 1 + O O14 1 0.75813200 0.59672800 0.25000200 1 + O O15 1 0.25807400 0.90323500 0.74999400 1 + O O16 1 0.20694800 0.45707900 0.25000000 1 + O O17 1 0.70682400 0.04293300 0.74999900 1 + O O18 1 0.29304600 0.95707800 0.25000400 1 + O O19 1 0.79318000 0.54293300 0.75000400 1 + O O20 1 0.28451400 0.16549800 0.04703500 1 + O O21 1 0.78450500 0.33453800 0.95297200 1 + O O22 1 0.78447400 0.33453000 0.54706100 1 + O O23 1 0.28449100 0.16550500 0.45292700 1 + O O24 1 0.21550200 0.66551200 0.45292200 1 + O O25 1 0.71552100 0.83452400 0.54706500 1 + O O26 1 0.71548600 0.83453300 0.95297000 1 + O O27 1 0.21547700 0.66550400 0.04703700 1 diff --git a/ocdata/__init__.py b/ocdata/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ocdata/adsorbates.py b/ocdata/adsorbates.py new file mode 100644 index 0000000000..316ab2e739 --- /dev/null +++ b/ocdata/adsorbates.py @@ -0,0 +1,64 @@ +import numpy as np +import pickle +import os + + +class Adsorbate: + """ + This class handles all things with the adsorbate. + Selects one (either specified or random), and stores info as an object + + Attributes + ---------- + atoms : Atoms + actual atoms of the adsorbate + smiles : str + SMILES representation of the adsorbate + bond_indices : list + indices of the atoms meant to be bonded to the surface + adsorbate_sampling_str : str + string capturing the adsorbate index and total possible adsorbates + """ + + def __init__( + self, adsorbate_database=None, specified_index=None, adsorbate_atoms=None + ): + if adsorbate_atoms is None: + assert adsorbate_database is not None + self.choose_adsorbate_pkl(adsorbate_database, specified_index) + else: + ( + self.adsorbate_sampling_str, + self.atoms, + self.smiles, + self.bond_indices, + ) = adsorbate_atoms + + def choose_adsorbate_pkl(self, adsorbate_database, specified_index=None): + """ + Chooses an adsorbate from our pkl based inverted index at random. + + Args: + adsorbate_database: A string pointing to the a pkl file that contains + an inverted index over different adsorbates. + specified_index: adsorbate index to choose instead of choosing a random one + Sets: + atoms `ase.Atoms` object of the adsorbate + smiles SMILES-formatted representation of the adsorbate + bond_indices list of integers indicating the indices of the atoms in + the adsorbate that are meant to be bonded to the surface + adsorbate_sampling_str Enum string specifying the sample, [index] + adsorbate_db_fname filename denoting which version was used to sample + """ + with open(adsorbate_database, "rb") as f: + inv_index = pickle.load(f) + + if specified_index is not None: + element = specified_index + else: + element = np.random.choice(len(inv_index)) + print(f"args.actions.adsorbate_id is None, choosing {element}") + + self.adsorbate_sampling_str = str(element) + self.atoms, self.smiles, self.bond_indices = inv_index[element] + self.adsorbate_db_fname = os.path.basename(adsorbate_database) diff --git a/ocdata/base_atoms/__init__.py b/ocdata/base_atoms/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ocdata/base_atoms/ase_dbs/__init__.py b/ocdata/base_atoms/ase_dbs/__init__.py new file mode 100644 index 0000000000..525320ee30 --- /dev/null +++ b/ocdata/base_atoms/ase_dbs/__init__.py @@ -0,0 +1,5 @@ +import os + + +BULK_DB = os.path.join(__path__[0], 'bulks.db') +ADSORBATE_DB = os.path.join(__path__[0], 'adsorbates.db') \ No newline at end of file diff --git a/ocdata/base_atoms/ase_dbs/adsorbates.db b/ocdata/base_atoms/ase_dbs/adsorbates.db new file mode 100644 index 0000000000..6ebf41a736 Binary files /dev/null and b/ocdata/base_atoms/ase_dbs/adsorbates.db differ diff --git a/ocdata/base_atoms/ase_dbs/bulks.db b/ocdata/base_atoms/ase_dbs/bulks.db new file mode 100644 index 0000000000..bc13ef5011 Binary files /dev/null and b/ocdata/base_atoms/ase_dbs/bulks.db differ diff --git a/ocdata/base_atoms/pkls/__init__.py b/ocdata/base_atoms/pkls/__init__.py new file mode 100644 index 0000000000..fdf425a977 --- /dev/null +++ b/ocdata/base_atoms/pkls/__init__.py @@ -0,0 +1,7 @@ +import os + + +BULK_PKL = os.path.join(__path__[0], 'bulks.pkl') +MAY12_BULK_PKL = os.path.join(__path__[0], 'bulks_may12.pkl') +ADSORBATE_PKL = os.path.join(__path__[0], 'adsorbates.pkl') +MAY12_SURFACE_ENUM_PKL = os.path.join(__path__[0], 'for_surface_enumeration_bulk_may12.pkl') diff --git a/ocdata/base_atoms/pkls/adsorbates.pkl b/ocdata/base_atoms/pkls/adsorbates.pkl new file mode 100644 index 0000000000..c4775b0d44 Binary files /dev/null and b/ocdata/base_atoms/pkls/adsorbates.pkl differ diff --git a/ocdata/base_atoms/pkls/bulks.pkl b/ocdata/base_atoms/pkls/bulks.pkl new file mode 100644 index 0000000000..c8a49f551a Binary files /dev/null and b/ocdata/base_atoms/pkls/bulks.pkl differ diff --git a/ocdata/base_atoms/pkls/convert_db_to_pkl.py b/ocdata/base_atoms/pkls/convert_db_to_pkl.py new file mode 100644 index 0000000000..9f7fcc3d39 --- /dev/null +++ b/ocdata/base_atoms/pkls/convert_db_to_pkl.py @@ -0,0 +1,130 @@ +''' +Helper script convert db files to pkl files. +''' + +__author__ = 'Siddharth Goyal' + +import ase +import ase.db +import pickle + + +def get_bulk_inverted_index_1(input_bulk_database, max_num_elements): + ''' + Converts an input ASE.db to an inverted index to efficiently sample bulks + ''' + assert max_num_elements > 0 + db = ase.db.connect(input_bulk_database) + + index = {} + total_entries = 0 + for i in range(1, max_num_elements + 1): + index[i] = [] + rows = list(db.select(n_elements=i)) + print(len(rows)) + for r in range(len(rows)): + index[i].append((rows[r].toatoms(), rows[r].mpid)) + total_entries += 1 + + return index, total_entries + +def get_bulk_inverted_index_2(input_bulk_database, max_num_elements): + ''' + Converts an input ASE.db to an inverted index to efficiently sample bulks + ''' + assert max_num_elements > 0 + db = ase.db.connect(input_bulk_database) + rows = list(db.select()) + + index = {} + total_entries = 0 + for r in range(len(rows)): + bulk = rows[r].toatoms() + mpid = rows[r].mpid + formula_str = str(bulk.symbols) + num_ele = sum(1 for c in formula_str if c.isupper()) + if num_ele > max_num_elements: + continue + if num_ele not in index: + index[num_ele] = [] + index[num_ele].append((bulk, mpid)) + total_entries += 1 + + return index, total_entries + + +# handling 2 dbs +def convert_bulk(bulk_path1, bulk_path2, max_num_elements, output_pkl, precompute_pkl_for_surface_enumeration): + + index1, total_entries1 = get_bulk_inverted_index_1(bulk_path1, max_num_elements) + index2, total_entries2 = get_bulk_inverted_index_2(bulk_path2, max_num_elements) + + # As of bulk.db file from Kevin on 01 May 2020 + assert total_entries1 == 11010 + assert total_entries2 == 491 + + combined_total_entries = total_entries1 + total_entries2 + lst_for_surface_enumeration = [] + combined_index = {} + all_index_counter = 0 + + # Handle first db elements + for i in range(1, max_num_elements + 1): + combined_index[i] = [] + + for j in range(len(index1[i])): + sampling_str = str(j) + "/" + str(len(index1[i])) + "_" + str(all_index_counter) + "/11010" + bulk, mpid = index1[i][j] + current_obj = (bulk, mpid, sampling_str, all_index_counter) + print(current_obj) + combined_index[i].append(current_obj) + all_index_counter += 1 + lst_for_surface_enumeration.append(current_obj) + + # Handle second db elements + for i in range(1, max_num_elements + 1): + for j in range(len(index2[i])): + sampling_str = str(j + len(index1[i])) + "/" + str(len(index1[i]) + len(index2[i])) + "_" + str(all_index_counter) + "/" + str(combined_total_entries) + bulk, mpid = index2[i][j] + current_obj = (bulk, mpid, sampling_str, all_index_counter) + print(current_obj) + combined_index[i].append(current_obj) + all_index_counter += 1 + lst_for_surface_enumeration.append(current_obj) + + with open(output_pkl, 'wb') as f: + pickle.dump(combined_index, f) + + with open(precompute_pkl_for_surface_enumeration, 'wb') as g: + pickle.dump(lst_for_surface_enumeration, g) + + +def convert_adsorbate(input_adsorbate_database, output_pkl): + ''' + Converts an input ASE.db to an inverted index to efficiently sample adsorbates + ''' + db = ase.db.connect(input_adsorbate_database) + + index = {} + + for i, row in enumerate(db.select()): + atoms = row.toatoms() + data = row.data + smiles = data['SMILE'] + bond_indices = data['bond_idx'] + index[i] = (atoms, smiles, bond_indices) + + with open(output_pkl, 'wb') as f: + pickle.dump(index, f) + + # As of adsorbates.db file in master on April 28 2020 + assert len(index) == 82 + + +def main(): + convert_bulk("../ase_dbs/bulks.db", "../ase_dbs/new_bulks.db", 3, "bulks_may12.pkl", "for_surface_enumeration_bulk_may12.pkl") +# convert_adsorbate("../ase_dbs/adsorbates.db", "adsorbates.pkl") + + +if __name__ == "__main__": + main() diff --git a/ocdata/bulk_obj.py b/ocdata/bulk_obj.py new file mode 100644 index 0000000000..a1974f25f4 --- /dev/null +++ b/ocdata/bulk_obj.py @@ -0,0 +1,343 @@ +import math +import os +import pickle + +import numpy as np +from pymatgen.core.surface import ( + SlabGenerator, + get_symmetrically_distinct_miller_indices, +) +from pymatgen.io.ase import AseAtomsAdaptor +from pymatgen.symmetry.analyzer import SpacegroupAnalyzer + +from .constants import COVALENT_MATERIALS_MPIDS, MAX_MILLER + + +class Bulk: + """ + This class handles all things with the bulk. + It also provides possible surfaces, later used to create a Surface object. + + Attributes + ---------- + precomputed_structures : str + root dir of precomputed structures + bulk_atoms : Atoms + actual atoms of the bulk + mpid : str + mpid of the bulk + bulk_sampling_str : str + string capturing the bulk index and number of possible bulks + index_of_bulk_atoms : int + index of bulk in the db + n_elems : int + number of elements of the bulk + elem_sampling_str : str + string capturing n_elems and the max possible elements + + Public methods + -------------- + get_possible_surfaces() + returns a list of possible surfaces for this bulk instance + """ + + def __init__( + self, bulk_database=None, precomputed_structures=None, bulk_index=None, max_elems=3 + ): + """ + Initializes the object by choosing or sampling from the bulk database + + Args: + bulk_database: either a list of dict of bulks + precomputed_structures: Root directory of precomputed structures for + surface enumeration + bulk_index: index of bulk to select if not doing a random sample + max_elems: max number of elements for any bulk + """ + self.precomputed_structures = precomputed_structures + if bulk_database is not None: + self.choose_bulk_pkl(bulk_database, bulk_index, max_elems) + + def choose_bulk_pkl(self, bulk_db, bulk_index, max_elems): + """ + Chooses a bulk from our pkl file at random as long as the bulk contains + the specified number of elements in any composition. + + Args: + bulk_db Unpickled dict or list of bulks + bulk_index Index of which bulk to select. If None, randomly sample one. + max_elems Max elems for any bulk structure. Currently it is 3 by default. + + Sets as class attributes: + bulk_atoms `ase.Atoms` of the chosen bulk structure. + mpid A string indicating which MPID the bulk is + bulk_sampling_str A string to enumerate the sampled structure + index_of_bulk_atoms Index of the chosen bulk in the array (should match + bulk_index if provided) + """ + + try: + if bulk_index is not None: + assert ( + len(bulk_db) > max_elems + ), f"Bulk db only has {len(bulk_db)} entries. Did you pass in the correct bulk database?" + assert isinstance(bulk_db[bulk_index], tuple) + + ( + self.bulk_atoms, + self.mpid, + self.bulk_sampling_str, + self.index_of_bulk_atoms, + ) = bulk_db[bulk_index] + self.bulk_sampling_str = f"{self.index_of_bulk_atoms}" + self.n_elems = len(set(self.bulk_atoms.symbols)) # 1, 2, or 3 + self.elem_sampling_str = f"{self.n_elems}" + + else: + self.sample_n_elems() + assert isinstance( + bulk_db, dict + ), "Did you pass in the correct bulk database?" + assert ( + self.n_elems in bulk_db.keys() + ), f"Bulk db does not have bulks of {self.n_elems} elements" + assert isinstance( + bulk_db[self.n_elems], list + ), "Did you pass in the correct bulk database?" + + total_elements_for_key = len(bulk_db[self.n_elems]) + row_bulk_index = np.random.choice(total_elements_for_key) + ( + self.bulk_atoms, + self.mpid, + self.bulk_sampling_str, + self.index_of_bulk_atoms, + ) = bulk_db[self.n_elems][row_bulk_index] + + except IndexError: + raise ValueError( + "Randomly chose to look for a %i-component material, " + "but no such materials exist. Please add one " + "to the database or change the weights to exclude " + "this number of components." % self.n_elems + ) + + def sample_n_elems(self, n_cat_elems_weights={1: 0.05, 2: 0.65, 3: 0.3}): + """ + Chooses the number of species we should look for in this sample. + + Arg: + n_cat_elems_weights A dictionary whose keys are integers containing the + number of species you want to consider and whose + values are the probabilities of selecting this + number. The probabilities must sum to 1. + Sets: + n_elems An integer showing how many species have been chosen. + elem_sampling_str Enum string of [chosen n_elems]/[total number of choices] + """ + + possible_n_elems = list(n_cat_elems_weights.keys()) + weights = list(n_cat_elems_weights.values()) + assert math.isclose(sum(weights), 1) + + self.n_elems = np.random.choice(possible_n_elems, p=weights) + self.elem_sampling_str = str(self.n_elems) + "/" + str(len(possible_n_elems)) + + def get_possible_surfaces(self): + """ + Returns a list of possible surfaces for this bulk instance. + This can be later used to iterate through all surfaces, + or select one at random, to make a Surface object. + """ + if self.precomputed_structures: + surfaces_info = self.read_from_precomputed_enumerations( + self.index_of_bulk_atoms + ) + else: + surfaces_info = self.enumerate_surfaces() + return surfaces_info + + def read_from_precomputed_enumerations(self, index): + """ + Loads relevant pickle of precomputed surfaces. + + Args: + index: bulk index + Returns: + surfaces_info: a list of surface_info tuples (atoms, miller, shift, top) + """ + with open( + os.path.join(self.precomputed_structures, str(index) + ".pkl"), "rb" + ) as f: + surfaces_info = pickle.load(f) + return surfaces_info + + def enumerate_surfaces( + self, miller_indices=None, sample_miller_indices=False, max_miller=MAX_MILLER + ): + """ + Enumerate all the symmetrically distinct surfaces of a bulk structure. It + will not enumerate surfaces with Miller indices above the `max_miller` + argument. Note that we also look at the bottoms of surfaces if they are + distinct from the top. If they are distinct, we flip the surface so the bottom + is pointing upwards. + + Args: + bulk_atoms `ase.Atoms` object of the bulk you want to enumerate + surfaces from. + miller_indices: A tuple of Miller indices as tuples you want to enumerate. (victor) + sample_miller_indices: whether to select a Miller indices tuple from + get_symmetrically_distinct_miller_indices (victor) + max_miller An integer indicating the maximum Miller index of the surfaces + you are willing to enumerate. Increasing this argument will + increase the number of surfaces, but the surfaces will + generally become larger. + Returns: + all_slabs_info A list of 4-tuples containing: `pymatgen.Structure` + objects for surfaces we have enumerated, the Miller + indices, floats for the shifts, and Booleans for "top". + """ + bulk_struct = self.standardize_bulk(self.bulk_atoms) + + all_millers = [] + + if miller_indices: + assert isinstance(miller_indices, tuple) + assert len(miller_indices) == 3 + if sample_miller_indices: + print( + "Warning: sample_miller_indices is True, but miller_indices is not None. Ignoring sample_miller_indices." + ) + all_millers = [miller_indices] + else: + all_millers = get_symmetrically_distinct_miller_indices( + bulk_struct, MAX_MILLER + ) + if sample_miller_indices: + all_millers = [np.random.choice(all_millers)] + + all_slabs_info = [] + for millers in all_millers: + slab_gen = SlabGenerator( + initial_structure=bulk_struct, + miller_index=millers, + min_slab_size=7.0, + min_vacuum_size=20.0, + lll_reduce=False, + center_slab=True, + primitive=True, + max_normal_search=1, + ) + slabs = slab_gen.get_slabs( + tol=0.3, bonds=None, max_broken_bonds=0, symmetrize=False + ) + + # Additional filtering for the 2D materials' slabs + if self.mpid in COVALENT_MATERIALS_MPIDS: + slabs = [ + slab for slab in slabs if self.is_2D_slab_reasonsable(slab) is True + ] + + # If the bottoms of the slabs are different than the tops, then we want + # to consider them, too + if len(slabs) != 0: + flipped_slabs_info = [ + (self.flip_struct(slab), millers, slab.shift, False) + for slab in slabs + if self.is_structure_invertible(slab) is False + ] + + # Concatenate all the results together + slabs_info = [(slab, millers, slab.shift, True) for slab in slabs] + all_slabs_info.extend(slabs_info + flipped_slabs_info) + return all_slabs_info + + def is_2D_slab_reasonsable(self, struct): + """ + There are 400+ 2D bulk materials whose slabs generated by pymaten require + additional filtering: some slabs are cleaved where one or more surface atoms + have no bonds with other atoms on the slab. + + Arg: + struct `pymatgen.Structure` object of a slab + Returns: + A boolean indicating whether or not the slab is + reasonable. + """ + for site in struct: + if len(struct.get_neighbors(site, 3)) == 0: + return False + return True + + def standardize_bulk(self, atoms): + """ + There are many ways to define a bulk unit cell. If you change the unit cell + itself but also change the locations of the atoms within the unit cell, you + can get effectively the same bulk structure. To address this, there is a + standardization method used to reduce the degrees of freedom such that each + unit cell only has one "true" configuration. This function will align a + unit cell you give it to fit within this standardization. + + Args: + atoms: `ase.Atoms` object of the bulk you want to standardize + Returns: + standardized_struct: `pymatgen.Structure` of the standardized bulk + """ + struct = AseAtomsAdaptor.get_structure(atoms) + sga = SpacegroupAnalyzer(struct, symprec=0.1) + standardized_struct = sga.get_conventional_standard_structure() + return standardized_struct + + def flip_struct(self, struct): + """ + Flips an atoms object upside down. Normally used to flip surfaces. + + Arg: + struct `pymatgen.Structure` object + Returns: + flipped_struct: The same `ase.Atoms` object that was fed as an + argument, but flipped upside down. + """ + atoms = AseAtomsAdaptor.get_atoms(struct) + + # This is black magic wizardry to me. Good look figuring it out. + atoms.wrap() + atoms.rotate(180, "x", rotate_cell=True, center="COM") + if atoms.cell[2][2] < 0.0: + atoms.cell[2] = -atoms.cell[2] + if np.cross(atoms.cell[0], atoms.cell[1])[2] < 0.0: + atoms.cell[1] = -atoms.cell[1] + atoms.center() + atoms.wrap() + + flipped_struct = AseAtomsAdaptor.get_structure(atoms) + return flipped_struct + + def is_structure_invertible(self, structure): + """ + This function figures out whether or not an `pymatgen.Structure` object has + symmetricity. In this function, the affine matrix is a rotation matrix that + is multiplied with the XYZ positions of the crystal. If the z,z component + of that is negative, it means symmetry operation exist, it could be a + mirror operation, or one that involves multiple rotations/etc. Regardless, + it means that the top becomes the bottom and vice-versa, and the structure + is the symmetric. i.e. structure_XYZ = structure_XYZ*M. + + In short: If this function returns `False`, then the input structure can + be flipped in the z-direction to create a new structure. + + Arg: + structure: A `pymatgen.Structure` object. + Returns + A boolean indicating whether or not your `ase.Atoms` object is + symmetric in z-direction (i.e. symmetric with respect to x-y plane). + """ + # If any of the operations involve a transformation in the z-direction, + # then the structure is invertible. + sga = SpacegroupAnalyzer(structure, symprec=0.1) + for operation in sga.get_symmetry_operations(): + xform_matrix = operation.affine_matrix + z_xform = xform_matrix[2, 2] + if z_xform == -1: + return True + return False diff --git a/ocdata/bulks.py b/ocdata/bulks.py new file mode 100644 index 0000000000..01b15af3e2 --- /dev/null +++ b/ocdata/bulks.py @@ -0,0 +1,40 @@ +''' +This submodule contains the scripts that the Ulissi group used to pull the +relaxed bulk structures from our database. +''' + +__author__ = 'Kevin Tran' +__email__ = 'ktran@andrew.cmu.edu' + +import warnings +from tqdm import tqdm +import ase.db +from gaspy.gasdb import get_mongo_collection +from gaspy.mongo import make_atoms_from_doc + + +with get_mongo_collection('atoms') as collection: + docs = list(tqdm(collection.find({'fwname.calculation_type': 'unit cell optimization', + 'fwname.vasp_settings.gga': 'RP', + 'fwname.vasp_settings.pp': 'PBE', + 'fwname.vasp_settings.xc': {'$exists': False}, + 'fwname.vasp_settings.pp_version': '5.4', + 'fwname.vasp_settings.encut': 500, + 'fwname.vasp_settings.isym': 0}), + desc='pulling from FireWorks')) + +mpids = set() +db = ase.db.connect('bulks.db') +for doc in tqdm(docs, desc='writing to database'): + atoms = make_atoms_from_doc(doc) + n_elements = len(set(atoms.symbols)) + + if n_elements <= 3: + mpid = doc['fwname']['mpid'] + if mpid not in mpids: + mpids.add(mpid) + _ = db.write(atoms, mpid=doc['fwname']['mpid'], n_elements=n_elements) + + else: + warnings.warn('Found a duplicate MPID: %s; adding the first one' % mpid, + RuntimeWarning) diff --git a/ocdata/combined.py b/ocdata/combined.py new file mode 100644 index 0000000000..58b7cbb2b5 --- /dev/null +++ b/ocdata/combined.py @@ -0,0 +1,366 @@ +import warnings + +import catkit +import numpy as np +from ase import neighborlist +from ase.neighborlist import natural_cutoffs +from pymatgen.analysis.local_env import VoronoiNN +from pymatgen.io.ase import AseAtomsAdaptor + +from .constants import COVALENT_RADIUS +from .loader import Loader +from .surfaces import constrain_surface + + +class Combined: + """ + This class handles all things with the adsorbate placed on a surface + Needs one adsorbate and one surface to create this class. + + Attributes + ---------- + adsorbate : Adsorbate + object representing the adsorbate + surface : Surface + object representing the surface + enumerate_all_configs : boolean + whether to enumerate all adslab placements instead of choosing one random + adsorbed_surface_atoms : list + `Atoms` objects containing both the adsorbate and surface for all desired placements + adsorbed_surface_sampling_strs : list + list of strings capturing the config index for each adslab placement + constrained_adsorbed_surfaces : list + list of all constrained adslab atoms + all_sites : list + list of binding coordinates for all the adslab configs + + Public methods + -------------- + get_adsorbed_bulk_dict(ind) + returns a dict of info for the adsorbate+surface of the specified config index + """ + + def __init__( + self, + adsorbate, + surface, + enumerate_all_configs, + animate=False, + no_loader=False, + index=-1, + early_init=False, + ): + """ + Adds adsorbate to surface, does the constraining, and aggregates all data necessary to write out. + Can either pick a random configuration or store all possible ones. + + Args: + adsorbate: the `Adsorbate` object + surface: the `Surface` object + enumerate_all_configs: whether to enumerate all adslab placements instead of choosing one random + index: list of adsorbstion site indices (-1, list or int) (victor) + early_init: whether to skip the actual work and just initialize the class + """ + self.adsorbate = adsorbate + self.surface = surface + self.animate = animate + self.no_loader = no_loader + self.enumerate_all_configs = enumerate_all_configs + self.index = index + self.early_init = early_init + + if early_init: + return + + self.add_adsorbate_onto_surface( + self.adsorbate.atoms, + self.surface.surface_atoms, + self.adsorbate.bond_indices, + self.index, + ) + + self.constrained_adsorbed_surfaces = [] + self.all_sites = [] + for a, atoms in enumerate(self.adsorbed_surface_atoms): + # Add appropriate constraints + self.constrained_adsorbed_surfaces.append(constrain_surface(atoms)) + + # Do the hashing + self.all_sites.append( + self.find_sites( + self.surface.constrained_surface, + self.constrained_adsorbed_surfaces[-1], + self.adsorbate.bond_indices, + ) + ) + + def add_adsorbate_onto_surface(self, adsorbate, surface, bond_indices, index=-1): + """ + There are a lot of small details that need to be considered when adding an + adsorbate onto a surface. This function will take care of those details for + you. + + Args: + adsorbate: An `ase.Atoms` object of the adsorbate + surface: An `ase.Atoms` object of the surface + bond_indices: A list of integers indicating the indices of the + binding atoms of the adsorbate + index: list of adsorbstion site indices (-1, list or int) (victor) ; + if None, choose the first "reasonable" binding site according to + `is_config_reasonable` + Sets these values: + adsorbed_surface_atoms: An `ase graphic Atoms` object containing the adsorbate and + surface. The bulk atoms will be tagged with `0`; the + surface atoms will be tagged with `1`, and the the + adsorbate atoms will be tagged with `2` or above. + adsorbed_surface_sampling_strs: String specifying the sample, [index]/[total] + of reasonable adsorbed surfaces + """ + iterate_index = False + if index is None: + index = 0 + iterate_index = True + + with Loader( + " [AAOS] make adsorbate_gratoms", + animate=self.animate, + ignore=self.no_loader, + ): + # convert surface atoms into graphic atoms object + surface_gratoms = catkit.Gratoms(surface) + surface_atom_indices = [ + i for i, atom in enumerate(surface) if atom.tag == 1 + ] + surface_gratoms.set_surface_atoms(surface_atom_indices) + surface_gratoms.pbc = np.array([True, True, False]) + + # set up the adsorbate into graphic atoms object + # with its connectivity matrix + adsorbate_gratoms = self.convert_adsorbate_atoms_to_gratoms( + adsorbate, bond_indices + ) + + # generate all possible adsorption configurations on that surface. + # The "bonds" argument automatically take care of mono vs. + # bidentate adsorption configuration. + with Loader( + " [AAOS] make all adsorbed_surfaces", + animate=self.animate, + ignore=self.no_loader, + ): + builder = catkit.gen.adsorption.Builder(surface_gratoms) + with warnings.catch_warnings(): # suppress potential square root warnings + warnings.simplefilter("ignore") + adsorbed_surfaces = builder.add_adsorbate( + adsorbate_gratoms, bonds=bond_indices, index=index + ) + if not isinstance(adsorbed_surfaces, list): + # account for the case where an int index is given + adsorbed_surfaces = [adsorbed_surfaces] + + print(">>> Total adsorbed_surfaces:", len(adsorbed_surfaces)) + + with Loader( + " [AAOS] filter reasonable_adsorbed_surfaces", + animate=self.animate, + ignore=self.no_loader, + ): + # Filter out unreasonable structures. + # Then pick one from the reasonable configurations list as an output. + reasonable_adsorbed_surfaces = [ + surface + for surface in adsorbed_surfaces + if self.is_config_reasonable(surface) + ] + + # if index was None and index 0 is not a valid adsorption site, iterate until a valid site is found + while not reasonable_adsorbed_surfaces and iterate_index: + index += 1 + adsorbed_surfaces = [ + builder.add_adsorbate( + adsorbate_gratoms, bonds=bond_indices, index=index + ) + ] + reasonable_adsorbed_surfaces = [ + surface + for surface in adsorbed_surfaces + if self.is_config_reasonable(surface) + ] + if iterate_index: + print(f">>> Chosen adsorption site: {index}") + print( + f">>> Reasonable configs: {len(reasonable_adsorbed_surfaces)}/{len(adsorbed_surfaces)}" + ) + + self.adsorbed_surface_atoms = [] + self.adsorbed_surface_sampling_strs = [] + if self.enumerate_all_configs: + self.num_configs = len(reasonable_adsorbed_surfaces) + for ind, reasonable_config in enumerate(reasonable_adsorbed_surfaces): + self.adsorbed_surface_atoms.append(reasonable_config) + self.adsorbed_surface_sampling_strs.append( + str(ind) + "/" + str(len(reasonable_adsorbed_surfaces)) + ) + else: + self.num_configs = 1 + reasonable_adsorbed_surface_index = np.random.choice( + len(reasonable_adsorbed_surfaces) + ) + self.adsorbed_surface_atoms.append( + reasonable_adsorbed_surfaces[reasonable_adsorbed_surface_index] + ) + self.adsorbed_surface_sampling_strs.append( + str(reasonable_adsorbed_surface_index) + + "/" + + str(len(reasonable_adsorbed_surfaces)) + ) + + def convert_adsorbate_atoms_to_gratoms(self, adsorbate, bond_indices): + """ + Convert adsorbate atoms object into graphic atoms object, + so the adsorbate can be placed onto the surface with optimal + configuration. Set tags for adsorbate atoms to 2, to distinguish + them from surface atoms. + + Args: + adsorbate An `ase.Atoms` object of the adsorbate + bond_indices A list of integers indicating the indices of the + binding atoms of the adsorbate + + Returns: + adsorbate_gratoms An graphic atoms object of the adsorbate. + """ + connectivity = self.get_connectivity(adsorbate) + adsorbate_gratoms = catkit.Gratoms(adsorbate, edges=connectivity) + # tag adsorbate atoms: non-binding atoms as 2, the binding atom(s) as 3 for now to + # track adsorption site for analyzing if adslab configuration is reasonable. + adsorbate_gratoms.set_tags( + [3 if idx in bond_indices else 2 for idx in range(len(adsorbate_gratoms))] + ) + return adsorbate_gratoms + + def get_connectivity(self, adsorbate): + """ + Generate the connectivity of an adsorbate atoms obj. + + Args: + adsorbate An `ase.Atoms` object of the adsorbate + + Returns: + matrix The connectivity matrix of the adsorbate. + """ + cutoff = natural_cutoffs(adsorbate) + neighborList = neighborlist.NeighborList( + cutoff, self_interaction=False, bothways=True + ) + neighborList.update(adsorbate) + matrix = neighborlist.get_connectivity_matrix(neighborList.nl).toarray() + return matrix + + def is_config_reasonable(self, adslab): + """ + Function that check whether the adsorbate placement is reasonable. + Two criteria are: 1. The adsorbate should be placed on the slab: + the fractional coordinates of the adsorption site is bounded by the unit cell. + 2. The adsorbate should not be buried into the surface: for any atom + in the adsorbate, if the distance between the atom and slab atoms + are closer than 80% of their expected covalent bond, we reject that placement. + + Args: + adslab An `ase.Atoms` object of the adsorbate+slab complex. + + Returns: + A boolean indicating whether or not the adsorbate placement is + reasonable. + """ + vnn = VoronoiNN(allow_pathological=True, tol=0.2, cutoff=10) + adsorbate_indices = [atom.index for atom in adslab if atom.tag >= 2] + adsorbate_bond_indices = [atom.index for atom in adslab if atom.tag == 3] + structure = AseAtomsAdaptor.get_structure(adslab) + slab_lattice = structure.lattice + + # Check to see if the fractional coordinates of the adsorption site is bounded + # by the slab unit cell. We loosen the threshold to -0.01 and 1.01 + # to not wrongly exclude reasonable edge adsorption site. + for idx in adsorbate_bond_indices: + coord = slab_lattice.get_fractional_coords(structure[idx].coords) + if np.any((coord < -0.01) | (coord > 1.01)): + return False + + # Then, check the covalent radius between each adsorbate atoms + # and its nearest neighbors that are slab atoms + # to make sure adsorbate is not buried into the surface + for idx in adsorbate_indices: + try: + nearneighbors = vnn.get_nn_info(structure, n=idx) + except ValueError: + return False + + slab_nn = [ + nn for nn in nearneighbors if nn["site_index"] not in adsorbate_indices + ] + for nn in slab_nn: + ads_elem = structure[idx].species_string + nn_elem = structure[nn["site_index"]].species_string + cov_bond_thres = ( + 0.8 * (COVALENT_RADIUS[ads_elem] + COVALENT_RADIUS[nn_elem]) / 100 + ) + actual_dist = adslab.get_distance(idx, nn["site_index"], mic=True) + if actual_dist < cov_bond_thres: + return False + + # If the structure is reasonable, change tags of adsorbate atoms from 2 and 3 to 2 only + # for ML model compatibility and data cleanliness of the output adslab configurations + old_tags = adslab.get_tags() + adslab.set_tags(np.where(old_tags == 3, 2, old_tags)) + return True + + def find_sites(self, surface, adsorbed_surface, bond_indices): + """ + Finds the Cartesian coordinates of the bonding atoms of the adsorbate. + + Args: + surface `ase.Atoms` of the chosen surface + adsorbed_surface An `ase graphic Atoms` object containing the + adsorbate and surface. + bond_indices A list of integers indicating the indices of the + binding atoms of the adsorbate + Returns: + sites A tuple of 3-tuples containing the Cartesian coordinates of + each of the binding atoms + """ + sites = [] + for idx in bond_indices: + binding_atom_index = len(surface) + idx + atom = adsorbed_surface[binding_atom_index] + positions = tuple(round(coord, 2) for coord in atom.position) + sites.append(positions) + + return tuple(sites) + + def get_adsorbed_bulk_dict(self, ind): + """ + Returns an organized dict for writing to files. + All info is already processed and stored in class variables. + """ + ads_sampling_str = ( + self.adsorbate.adsorbate_sampling_str + + "_" + + self.adsorbed_surface_sampling_strs[ind] + ) + + return { + "adsorbed_bulk_atomsobject": self.constrained_adsorbed_surfaces[ind], + "adsorbed_bulk_metadata": ( + self.surface.bulk_object.mpid, + self.surface.millers, + round(self.surface.shift, 3), + self.surface.top, + self.adsorbate.smiles, + self.all_sites[ind], + ), + "adsorbed_bulk_samplingstr": self.surface.overall_sampling_str + + "_" + + ads_sampling_str, + "adsorbed_db_version": self.adsorbate.adsorbate_db_fname, + } diff --git a/ocdata/constants.py b/ocdata/constants.py new file mode 100644 index 0000000000..990b0481e4 --- /dev/null +++ b/ocdata/constants.py @@ -0,0 +1,113 @@ + +# unused? +ELEMENTS = {1: 'H', 2: 'He', 3: 'Li', 4: 'Be', 5: 'B', 6: 'C', 7: 'N', 8: 'O', + 9: 'F', 10: 'Ne', 11: 'Na', 12: 'Mg', 13: 'Al', 14: 'Si', 15: 'P', + 16: 'S', 17: 'Cl', 18: 'Ar', 19: 'K', 20: 'Ca', 21: 'Sc', 22: 'Ti', + 23: 'V', 24: 'Cr', 25: 'Mn', 26: 'Fe', 27: 'Co', 28: 'Ni', 29: + 'Cu', 30: 'Zn', 31: 'Ga', 32: 'Ge', 33: 'As', 34: 'Se', 35: 'Br', + 36: 'Kr', 37: 'Rb', 38: 'Sr', 39: 'Y', 40: 'Zr', 41: 'Nb', 42: + 'Mo', 43: 'Tc', 44: 'Ru', 45: 'Rh', 46: 'Pd', 47: 'Ag', 48: 'Cd', + 49: 'In', 50: 'Sn', 51: 'Sb', 52: 'Te', 53: 'I', 54: 'Xe', 55: + 'Cs', 56: 'Ba', 57: 'La', 58: 'Ce', 59: 'Pr', 60: 'Nd', 61: 'Pm', + 62: 'Sm', 63: 'Eu', 64: 'Gd', 65: 'Tb', 66: 'Dy', 67: 'Ho', 68: + 'Er', 69: 'Tm', 70: 'Yb', 71: 'Lu', 72: 'Hf', 73: 'Ta', 74: 'W', + 75: 'Re', 76: 'Os', 77: 'Ir', 78: 'Pt', 79: 'Au', 80: 'Hg', 81: + 'Tl', 82: 'Pb', 83: 'Bi', 84: 'Po', 85: 'At', 86: 'Rn', 87: 'Fr', + 88: 'Ra', 89: 'Ac', 90: 'Th', 91: 'Pa', 92: 'U', 93: 'Np', 94: + 'Pu', 95: 'Am', 96: 'Cm', 97: 'Bk', 98: 'Cf', 99: 'Es', 100: 'Fm', + 101: 'Md', 102: 'No', 103: 'Lr', 104: 'Rf', 105: 'Db', 106: 'Sg', + 107: 'Bh', 108: 'Hs', 109: 'Mt', 110: 'Ds', 111: 'Rg', 112: 'Cn', + 113: 'Nh', 114: 'Fl', 115: 'Mc', 116: 'Lv', 117: 'Ts', 118: 'Og'} + +# Covalent radius of elements (unit is pm, 1pm=0.01 angstrom) +# Value are taken from https://github.com/lmmentel/mendeleev +COVALENT_RADIUS = {'H': 32.0, 'He': 46.0, 'O': 63.0, 'F': 64.0, 'Ne': 67.0, 'N': 71.0, + 'C': 75.0, 'B': 85.0, 'Ar': 96.0, 'Cl': 99.0, 'Be': 102.0, 'S': 103.0, + 'Ni': 110.0, 'P': 111.0, 'Co': 111.0, 'Cu': 112.0, 'Br': 114.0, + 'Si': 116.0, 'Fe': 116.0, 'Se': 116.0, 'Kr': 117.0, 'Zn': 118.0, + 'Mn': 119.0, 'Pd': 120.0, 'Ge': 121.0, 'As': 121.0, 'Rg': 121.0, + 'Cr': 122.0, 'Ir': 122.0, 'Cn': 122.0, 'Pt': 123.0, 'Ga': 124.0, + 'Au': 124.0, 'Ru': 125.0, 'Rh': 125.0, 'Al': 126.0, 'Tc': 128.0, + 'Ag': 128.0, 'Ds': 128.0, 'Os': 129.0, 'Mt': 129.0, 'Xe': 131.0, 'Re': 131.0, + 'Li': 133.0, 'I': 133.0, 'Hg': 133.0, 'V': 134.0, 'Hs': 134.0, 'Ti': 136.0, + 'Cd': 136.0, 'Te': 136.0, 'Nh': 136.0, 'W': 137.0, 'Mo': 138.0, 'Mg': 139.0, + 'Sn': 140.0, 'Sb': 140.0, 'Bh': 141.0, 'In': 142.0, 'Rn': 142.0, 'Sg': 143.0, + 'Fl': 143.0, 'Tl': 144.0, 'Pb': 144.0, 'Po': 145.0, 'Ta': 146.0, 'Nb': 147.0, + 'At': 147.0, 'Sc': 148.0, 'Db': 149.0, 'Bi': 151.0, 'Hf': 152.0, 'Zr': 154.0, + 'Na': 155.0, 'Rf': 157.0, 'Og': 157.0, 'Lr': 161.0, 'Lu': 162.0, 'Mc': 162.0, + 'Y': 163.0, 'Ce': 163.0, 'Tm': 164.0, 'Er': 165.0, 'Es': 165.0, 'Ts': 165.0, + 'Ho': 166.0, 'Am': 166.0, 'Cm': 166.0, 'Dy': 167.0, 'Fm': 167.0, 'Eu': 168.0, + 'Tb': 168.0, 'Bk': 168.0, 'Cf': 168.0, 'Gd': 169.0, 'Pa': 169.0, 'Yb': 170.0, + 'U': 170.0, 'Ca': 171.0, 'Np': 171.0, 'Sm': 172.0, 'Pu': 172.0, 'Pm': 173.0, + 'Md': 173.0, 'Nd': 174.0, 'Th': 175.0, 'Lv': 175.0, 'Pr': 176.0, 'No': 176.0, + 'La': 180.0, 'Sr': 185.0, 'Ac': 186.0, 'K': 196.0, 'Ba': 196.0, 'Ra': 201.0, + 'Rb': 210.0, 'Fr': 223.0, 'Cs': 232.0} + +# We will enumerate surfaces with Miller indices <= MAX_MILLER +MAX_MILLER = 2 + +# We will create surfaces that are at least MIN_XY Angstroms wide. GASpy uses +# 4.5, but our larger adsorbates here can be up to 3.6 Angstroms long. So 4.5 + +# 3.6 ~= 8 Angstroms +MIN_XY = 8. + +COVALENT_MATERIALS_MPIDS = ['mp-104', 'mp-79', 'mp-94', 'mp-11', 'mp-48', 'mp-23152', 'mp-1014111', 'mp-567409', + 'mp-157', 'mp-10021', 'mp-140', 'mp-1094075', 'mp-571550', 'mp-1067758', 'mp-570875', + 'mp-1371', 'mp-2128', 'mp-995193', 'mp-2294', 'mp-31053', 'mp-556225', 'mp-568971', + 'mp-1009834', 'mp-909', 'mp-9548', 'mp-1863', 'mp-570325', 'mp-17524', 'mp-978553', + 'mp-2793', 'mp-998972', 'mp-641', 'mp-15700', 'mp-1009581', 'mp-580226', 'mp-604910', + 'mp-542640', 'mp-20311', 'mp-505531', 'mp-1634', 'mp-700', 'mp-2242', 'mp-628773', + 'mp-21405', 'mp-1379', 'mp-2194', 'mp-1115', 'mp-762', 'mp-572758', 'mp-630528', + 'mp-850131', 'mp-1943', 'mp-2418', 'mp-9889', 'mp-2160', 'mp-2231', 'mp-2156', + 'mp-541582', 'mp-2815', 'mp-604914', 'mp-22691', 'mp-1017565', 'mp-665', 'mp-13682', + 'mp-22375', 'mp-21296', 'mp-1821', 'mp-9920', 'mp-691', 'mp-694', 'mp-1013525', 'mp-1170', + 'mp-2809', 'mp-755263', 'mp-224', 'mp-571033', 'mp-19932', 'mp-2507', 'mp-30485', 'mp-2330', + 'mp-2686', 'mp-525', 'mp-562100', 'mp-7597', 'mp-850083', 'mp-570356', 'mp-684898', + 'mp-10033', 'mp-604908', 'mp-1023900', 'mp-672372', 'mp-1070580', 'mp-1057015', 'mp-30500', + 'mp-1080586', 'mp-1095294', 'mp-1071032', 'mp-1096986', 'mp-1078500', 'mp-1091375', 'mp-28919', + 'mp-7459', 'mp-27666', 'mp-1025459', 'mp-29652', 'mp-9922', 'mp-25469', 'mp-20050', 'mp-1018150', + 'mp-11693', 'mp-570122', 'mp-27507', 'mp-567279', 'mp-1018891', 'mp-1907', 'mp-985829', 'mp-9983', + 'mp-782', 'mp-684690', 'mp-676241', 'mp-22693', 'mp-28233', 'mp-28116', 'mp-1984', 'mp-27455', + 'mp-542449', 'mp-541885', 'mp-1245', 'mp-9996', 'mp-568328', 'mp-10009', 'mp-27164', + 'mp-11675', 'mp-2285', 'mp-13683', 'mp-2798', 'mp-11687', 'mp-21273', 'mp-2430', 'mp-1168', + 'mp-945077', 'mp-1063670', 'mp-1008626', 'mp-22853', 'mp-27628', 'mp-632403', 'mp-500', + 'mp-20826', 'mp-601823', 'mp-1079574', 'mp-27770', 'mp-985831', 'mp-1078443', 'mp-28117', + 'mp-1009641', 'mp-1017540', 'mp-542634', 'mp-23240', 'mp-938', 'mp-1100795', 'mp-9481', + 'mp-9897', 'mp-27513', 'mp-1683', 'mp-1007758', 'mp-1078708', 'mp-23162', 'mp-1186', 'mp-570858', + 'mp-1662', 'mp-602', 'mp-1080459', 'mp-1068510', 'mp-228', 'mp-1025402', 'mp-1967', 'mp-582549', + 'mp-22881', 'mp-9254', 'mp-22877', 'mp-542495', 'mp-605', 'mp-7541', 'mp-542812', 'mp-540922', + 'mp-540884', 'mp-865373', 'mp-570451', 'mp-23309', 'mp-23229', 'mp-484', 'mp-29772', 'mp-9921', + 'mp-2089', 'mp-1019322', 'mp-399', 'mp-972889', 'mp-568746', 'mp-541837', 'mp-22856', + 'mp-570197', 'mp-569581', 'mp-542615', 'mp-1078313', 'mp-27902', 'mp-486', 'mp-34202', + 'mp-23174', 'mp-27396', 'mp-23164', 'mp-2578', 'mp-27411', 'mp-556516', 'mp-10264', + 'mp-1018020', 'mp-35835', 'mp-3532', 'mp-20757', 'mp-29249', 'mp-990091', 'mp-555269', + 'mp-624190', 'mp-1025340', 'mp-8976', 'mp-20331', 'mp-20793', 'mp-560370', 'mp-5045', + 'mp-6959', 'mp-4468', 'mp-19885', 'mp-10412', 'mp-22253', 'mp-27532', 'mp-560262', + 'mp-627601', 'mp-21365', 'mp-504564', 'mp-560806', 'mp-541937', 'mp-542644', 'mp-985304', + 'mp-674328', 'mp-569662', 'mp-637614', 'mp-5807', 'mp-675326', 'mp-34289', 'mp-10232', + 'mp-866941', 'mp-28019', 'mp-20612', 'mp-675290', 'mp-675367', 'mp-675066', 'mp-1024076', + 'mp-12433', 'mp-9378', 'mp-7263', 'mp-23918', 'mp-21096', 'mp-1018658', 'mp-22152', 'mp-1071623', + 'mp-22035', 'mp-8147', 'mp-1029479', 'mp-1029779', 'mp-4384', 'mp-1068653', 'mp-1080466', + 'mp-1078896', 'mp-24428', 'mp-1094008', 'mp-19727', 'mp-1077470', 'mp-27749', 'mp-8435', 'mp-8848', + 'mp-20422', 'mp-13963', 'mp-540997', 'mp-1029316', 'mp-1029309', 'mp-14790', 'mp-571471', + 'mp-989651', 'mp-29300', 'mp-28846', 'mp-9379', 'mp-28557', 'mp-989586', 'mp-13962', 'mp-28580', + 'mp-1078140', 'mp-998560', 'mp-13542', 'mp-7505', 'mp-3208', 'mp-998512', 'mp-542096', + 'mp-605028', 'mp-14815', 'mp-574169', 'mp-3006', 'mp-28866', 'mp-8211', 'mp-8695', 'mp-628726', + 'mp-16765', 'mp-19917', 'mp-3779', 'mp-14791', 'mp-3342', 'mp-36381', 'mp-1095516', + 'mp-24081', 'mp-27178', 'mp-1079752', 'mp-568592', 'mp-674984', 'mp-27656', 'mp-13923', + 'mp-676437', 'mp-1094079', 'mp-27449', 'mp-541312', 'mp-28220', 'mp-620190', 'mp-567931', + 'mp-28487', 'mp-28480', 'mp-30971', 'mp-29072', 'mp-18279', 'mp-29607', 'mp-505164', 'mp-570340', + 'mp-31220', 'mp-27171', 'mp-17287', 'mp-1024958', 'mp-1078645', 'mp-30979', 'mp-567817', + 'mp-29022', 'mp-675163', 'mp-28224', 'mp-9797', 'mp-4628', 'mp-8436', 'mp-8768', 'mp-998233', + 'mp-2977', 'mp-28189', 'mp-18625', 'mp-541449', 'mp-7280', 'mp-29419', 'mp-675801', + 'mp-22945', 'mp-28178', 'mp-27361', 'mp-1079559', 'mp-541911', 'mp-17801', 'mp-12743', + 'mp-8613', 'mp-8677', 'mp-768680', 'mp-504630', 'mp-12527', 'mp-3123', 'mp-23396', 'mp-1013900', + 'mp-28361', 'mp-23434', 'mp-616481', 'mp-14474', 'mp-28126', 'mp-977371', 'mp-571661', + 'mp-29073', 'mp-505206', 'mp-541149', 'mp-672273', 'mp-22982', 'mp-20235', 'mp-27850', + 'mp-675543', 'mp-554921', 'mp-977592', 'mp-570930', 'mp-9622', 'mp-29666', 'mp-3534', + 'mp-9010', 'mp-23472', 'mp-38605', 'mp-8190', 'mp-20242', 'mp-3525', 'mp-9251', 'mp-15121', + 'mp-14242', 'mp-27947', 'mp-540818', 'mp-1025457', 'mp-504957', 'mp-27948', 'mp-19810', 'mp-580748', + 'mp-8612', 'mp-27195', 'mp-769218', 'mp-9272', 'mp-9391', 'mp-31406', 'mp-4988', 'mp-7277', 'mp-3849', + 'mp-14241', 'mp-31507', 'mp-12307', 'mp-1079754', 'mp-30183', 'mp-20506', 'mp-7038', 'mp-540687', + 'mp-569044', 'mp-541487', 'mp-17945', 'mp-1201', 'mp-130', 'mp-611219', 'mp-1057273', 'mp-158', + 'mp-9798', 'mp-676250', 'mp-998787', 'mp-21413', 'mp-27910'] \ No newline at end of file diff --git a/ocdata/loader.py b/ocdata/loader.py new file mode 100644 index 0000000000..db7c0f2442 --- /dev/null +++ b/ocdata/loader.py @@ -0,0 +1,81 @@ +from itertools import cycle +from shutil import get_terminal_size +from threading import Thread +from time import sleep, time + + +class Loader: + def __init__( + self, + desc="Loading...", + end="Done!", + timeout=0.1, + timer=True, + erase=False, + animate=True, + ignore=False, + out=None, + ): + """ + A loader-like context manager + + Args: + desc (str, optional): The loader's description. Defaults to "Loading...". + end (str, optional): Final print. Defaults to "Done!". + timeout (float, optional): Sleep time between prints. Defaults to 0.1. + """ + self.desc = desc + self.end = end + self.timeout = timeout + self.timer = timer + self.erase = erase + self.out = out + self.duration = None + + self._thread = Thread(target=self._animate, daemon=True) + self.steps = ["⢿", "⣻", "⣽", "⣾", "⣷", "⣯", "⣟", "⡿"] + self.done = (not animate) or ignore + self.ignore = ignore + + def start(self): + self._start_time = time() + self._thread.start() + return self + + def _animate(self): + for c in cycle(self.steps): + if self.done: + break + print(f"\r{self.desc} {c}", flush=True, end="") + sleep(self.timeout) + + def __enter__(self): + self.start() + return self + + def stop(self): + if self.ignore: + return + self.done = True + cols = get_terminal_size((80, 20)).columns + + if self.erase: + end = f"\r{self.end}" + else: + end = self.desc + " | " + self.end + + if self.timer: + end_time = time() + self.duration = end_time - self._start_time + if isinstance(self.out, list): + self.out.append(self.duration) + elif isinstance(self.out, dict): + self.out[self.desc].append(self.duration) + end += f" ({self.duration:.2f}s)" + + print("\r" + " " * cols, end="\r", flush=True) + print(end, flush=True) + + def __exit__(self, exc_type, exc_value, tb): + # handle exceptions with those variables ^ + self.stop() diff --git a/ocdata/precompute_sample_structures.py b/ocdata/precompute_sample_structures.py new file mode 100644 index 0000000000..507b64c61f --- /dev/null +++ b/ocdata/precompute_sample_structures.py @@ -0,0 +1,174 @@ +''' +This submodule contains the scripts that the we used to sample the adsorption +structures. + +Note that some of these scripts were taken from +[GASpy](https://github.com/ulissigroup/GASpy) with permission of author. +''' + +__authors__ = ['Kevin Tran', 'Aini Palizhati', 'Siddharth Goyal', 'Zachary Ulissi'] +__email__ = ['ktran@andrew.cmu.edu'] + +import math +from collections import defaultdict +import random +import pickle +import numpy as np +import catkit +import ase +import ase.db +from ase import neighborlist +from ase.constraints import FixAtoms +from ase.neighborlist import natural_cutoffs +from pymatgen.io.ase import AseAtomsAdaptor +from pymatgen.core.surface import SlabGenerator, get_symmetrically_distinct_miller_indices +from pymatgen.symmetry.analyzer import SpacegroupAnalyzer +from pymatgen.analysis.local_env import VoronoiNN +from .base_atoms.pkls import BULK_PKL, ADSORBATE_PKL +from .constants import MAX_MILLER +import sys +import time + +def enumerate_surfaces_for_saving(bulk_atoms, max_miller=MAX_MILLER): + ''' + Enumerate all the symmetrically distinct surfaces of a bulk structure. It + will not enumerate surfaces with Miller indices above the `max_miller` + argument. Note that we also look at the bottoms of surfaces if they are + distinct from the top. If they are distinct, we flip the surface so the bottom + is pointing upwards. + + Args: + bulk_atoms `ase.Atoms` object of the bulk you want to enumerate + surfaces from. + max_miller An integer indicating the maximum Miller index of the surfaces + you are willing to enumerate. Increasing this argument will + increase the number of surfaces, but the surfaces will + generally become larger. + Returns: + all_slabs_info A list of 4-tuples containing: `pymatgen.Structure` + objects for surfaces we have enumerated, the Miller + indices, floats for the shifts, and Booleans for "top". + ''' + bulk_struct = standardize_bulk(bulk_atoms) + + all_slabs_info = [] + for millers in get_symmetrically_distinct_miller_indices(bulk_struct, MAX_MILLER): + slab_gen = SlabGenerator(initial_structure=bulk_struct, + miller_index=millers, + min_slab_size=7., + min_vacuum_size=20., + lll_reduce=False, + center_slab=True, + primitive=True, + max_normal_search=1) + slabs = slab_gen.get_slabs(tol=0.3, + bonds=None, + max_broken_bonds=0, + symmetrize=False) + + # If the bottoms of the slabs are different than the tops, then we want + # to consider them, too + flipped_slabs_info = [(flip_struct(slab), millers, slab.shift, False) + for slab in slabs if is_structure_invertible(slab) is False] + + # Concatenate all the results together + slabs_info = [(slab, millers, slab.shift, True) for slab in slabs] + all_slabs_info.extend(slabs_info + flipped_slabs_info) + return all_slabs_info + + +def standardize_bulk(atoms): + ''' + There are many ways to define a bulk unit cell. If you change the unit cell + itself but also change the locations of the atoms within the unit cell, you + can get effectively the same bulk structure. To address this, there is a + standardization method used to reduce the degrees of freedom such that each + unit cell only has one "true" configuration. This function will align a + unit cell you give it to fit within this standardization. + + Arg: + atoms `ase.Atoms` object of the bulk you want to standardize + Returns: + standardized_struct `pymatgen.Structure` of the standardized bulk + ''' + struct = AseAtomsAdaptor.get_structure(atoms) + sga = SpacegroupAnalyzer(struct, symprec=0.1) + standardized_struct = sga.get_conventional_standard_structure() + return standardized_struct + + +def is_structure_invertible(structure): + ''' + This function figures out whether or not an `pymatgen.Structure` object has + symmetricity. In this function, the affine matrix is a rotation matrix that + is multiplied with the XYZ positions of the crystal. If the z,z component + of that is negative, it means symmetry operation exist, it could be a + mirror operation, or one that involves multiple rotations/etc. Regardless, + it means that the top becomes the bottom and vice-versa, and the structure + is the symmetric. i.e. structure_XYZ = structure_XYZ*M. + + In short: If this function returns `False`, then the input structure can + be flipped in the z-direction to create a new structure. + + Arg: + structure A `pymatgen.Structure` object. + Returns + A boolean indicating whether or not your `ase.Atoms` object is + symmetric in z-direction (i.e. symmetric with respect to x-y plane). + ''' + # If any of the operations involve a transformation in the z-direction, + # then the structure is invertible. + sga = SpacegroupAnalyzer(structure, symprec=0.1) + for operation in sga.get_symmetry_operations(): + xform_matrix = operation.affine_matrix + z_xform = xform_matrix[2, 2] + if z_xform == -1: + return True + return False + + +def flip_struct(struct): + ''' + Flips an atoms object upside down. Normally used to flip surfaces. + + Arg: + atoms `pymatgen.Structure` object + Returns: + flipped_struct The same `ase.Atoms` object that was fed as an + argument, but flipped upside down. + ''' + atoms = AseAtomsAdaptor.get_atoms(struct) + + # This is black magic wizardry to me. Good look figuring it out. + atoms.wrap() + atoms.rotate(180, 'x', rotate_cell=True, center='COM') + if atoms.cell[2][2] < 0.: + atoms.cell[2] = -atoms.cell[2] + if np.cross(atoms.cell[0], atoms.cell[1])[2] < 0.0: + atoms.cell[1] = -atoms.cell[1] + atoms.wrap() + + flipped_struct = AseAtomsAdaptor.get_structure(atoms) + return flipped_struct + + +def precompute_enumerate_surface(bulk_database, bulk_index, opfile): + + with open(bulk_database, 'rb') as f: + inv_index = pickle.load(f) + flatten = inv_index[1] + inv_index[2] + inv_index[3] + assert bulk_index < len(flatten) + + bulk, mpid = flatten[bulk_index] + + print(bulk, mpid) + surfaces_info = enumerate_surfaces_for_saving(bulk) + + with open(opfile, 'wb') as g: + pickle.dump(surfaces_info, g) + +if __name__ == "__main__": + s = time.time() + precompute_enumerate_surface(BULK_PKL, int(sys.argv[1]), sys.argv[2]) + e = time.time() + print(sys.argv[1], "Done in", e - s ) diff --git a/ocdata/structure_sampler.py b/ocdata/structure_sampler.py new file mode 100644 index 0000000000..4b0e3893e6 --- /dev/null +++ b/ocdata/structure_sampler.py @@ -0,0 +1,187 @@ + +from ocdata.vasp import write_vasp_input_files +from ocdata.adsorbates import Adsorbate +from ocdata.bulk_obj import Bulk +from ocdata.surfaces import Surface +from ocdata.combined import Combined + +import logging +import numpy as np +import os +import pickle +import time + +class StructureSampler(): + ''' + A class that creates adsorbate/bulk/surface objects and + writes vasp input files for one of the following options: + - one random adsorbate/bulk/surface/config, based on a specified random seed + - one specified adsorbate, n specified bulks, and all possible surfaces and configs + - one specified adsorbate, n specified bulks, one specified surface, and all possible configs + + The output directory structure will look like the following: + - For sampling a random structure, the directories will be `random{seed}/surface` and + `random{seed}/adslab` for the surface alone and the adsorbate+surface, respectively. + - For enumerating all structures, the directories will be `{adsorbate}_{bulk}_{surface}/surface` + and `{adsorbate}_{bulk}_{surface}/adslab{config}`, where everything in braces are the + respective indices. + + Attributes + ---------- + args : argparse.Namespace + contains all command line args + logger : logging.RootLogger + logging class to print info + adsorbate : Adsorbate + the selected adsorbate object + all_bulks : list + list of `Bulk` objects + bulk_indices_list : list + list of specified bulk indices (ints) that we want to select + + Public methods + -------------- + run() + selects the appropriate materials and writes to files + ''' + + def __init__(self, args): + ''' + Set up args from argparse, random seed, and logging. + ''' + self.args = args + + self.logger = logging.getLogger() + logging.basicConfig(format='[%(asctime)s] %(levelname)s: %(message)s', + datefmt='%H:%M:%S') + self.logger.setLevel(logging.INFO if self.args.verbose else logging.WARNING) + + if self.args.enumerate_all_structures: + self.bulk_indices_list = [int(ind) for ind in args.bulk_indices.split(',')] + self.logger.info(f'Enumerating all surfaces/configs for adsorbate {self.args.adsorbate_index} and bulks {self.bulk_indices_list}') + else: + self.logger.info('Sampling one random structure') + np.random.seed(self.args.seed) + + def run(self): + ''' + Runs the entire job: generates adsorbate/bulk/surface objects and writes to files. + ''' + start = time.time() + + if self.args.enumerate_all_structures: + self.adsorbate = Adsorbate(self.args.adsorbate_db, self.args.adsorbate_index) + self._load_bulks() + self._load_and_write_surfaces() + + end = time.time() + self.logger.info(f'Done! ({round(end - start, 2)}s)') + + def _load_bulks(self): + ''' + Loads bulk structures (one random or a list of specified ones) + and stores them in self.all_bulks + ''' + self.all_bulks = [] + with open(self.args.bulk_db, 'rb') as f: + bulk_db_lookup = pickle.load(f) + + if self.args.enumerate_all_structures: + for ind in self.bulk_indices_list: + self.all_bulks.append(Bulk(bulk_db_lookup, self.args.precomputed_structures, ind)) + else: + self.all_bulks.append(Bulk(bulk_db_lookup, self.args.precomputed_structures)) + + def _load_and_write_surfaces(self): + ''' + Loops through all bulks and chooses one random or all possible surfaces; + writes info for that surface and combined surface+adsorbate + ''' + for bulk_ind, bulk in enumerate(self.all_bulks): + possible_surfaces = bulk.get_possible_surfaces() + if self.args.enumerate_all_structures: + if self.args.surface_index is not None: + assert 0 <= self.args.surface_index < len(possible_surfaces), 'Invalid surface index provided' + self.logger.info(f'Loading only surface {self.args.surface_index} for bulk {self.bulk_indices_list[bulk_ind]}') + included_surface_indices = [self.args.surface_index] + else: + self.logger.info(f'Enumerating all {len(possible_surfaces)} surfaces for bulk {self.bulk_indices_list[bulk_ind]}') + included_surface_indices = range(len(possible_surfaces)) + + for cur_surface_ind in included_surface_indices: + surface_info = possible_surfaces[cur_surface_ind] + surface = Surface(bulk, surface_info, cur_surface_ind, len(possible_surfaces)) + self._combine_and_write(surface, self.bulk_indices_list[bulk_ind], cur_surface_ind) + else: + surface_info_index = np.random.choice(len(possible_surfaces)) + surface = Surface(bulk, possible_surfaces[surface_info_index], surface_info_index, len(possible_surfaces)) + self.adsorbate = Adsorbate(self.args.adsorbate_db) + self._combine_and_write(surface) + + + def _combine_and_write(self, surface, cur_bulk_index=None, cur_surface_index=None): + ''' + Add the adsorbate onto a given surface in a Combined object. + Writes output files for the surface itself and the combined surface+adsorbate + + Args: + surface: a Surface object to combine with self.adsorbate + cur_bulk_index: current bulk index from self.bulk_indices_list + cur_surface_index: current surface index if enumerating all + ''' + if self.args.enumerate_all_structures: + output_name_template = f'{self.args.adsorbate_index}_{cur_bulk_index}_{cur_surface_index}' + else: + output_name_template = f'random{self.args.seed}' + + self._write_surface(surface, output_name_template) + + combined = Combined(self.adsorbate, surface, self.args.enumerate_all_structures) + self._write_adsorbed_surface(combined, output_name_template) + + def _write_surface(self, surface, output_name_template): + ''' + Write VASP input files and metadata for the surface alone. + + Args: + surface: the Surface object to write info for + output_name_template: parent directory name for output files + ''' + bulk_dict = surface.get_bulk_dict() + bulk_dir = os.path.join(self.args.output_dir, output_name_template, 'surface') + write_vasp_input_files(bulk_dict['bulk_atomsobject'], bulk_dir) + self._write_metadata_pkl(bulk_dict, os.path.join(bulk_dir, 'metadata.pkl')) + self.logger.info(f"wrote surface ({bulk_dict['bulk_samplingstr']}) to {bulk_dir}") + + def _write_adsorbed_surface(self, combined, output_name_template): + ''' + Write VASP input files and metadata for the adsorbate placed on surface. + + Args: + combined: the Combined object to write info for, containing any number of adslabs + output_name_template: parent directory name for output files + ''' + self.logger.info(f'Writing {combined.num_configs} adslab configs') + for config_ind in range(combined.num_configs): + if self.args.enumerate_all_structures: + adsorbed_bulk_dir = os.path.join(self.args.output_dir, output_name_template, f'adslab{config_ind}') + else: + adsorbed_bulk_dir = os.path.join(self.args.output_dir, output_name_template, 'adslab') + adsorbed_bulk_dict = combined.get_adsorbed_bulk_dict(config_ind) + write_vasp_input_files(adsorbed_bulk_dict['adsorbed_bulk_atomsobject'], adsorbed_bulk_dir) + self._write_metadata_pkl(adsorbed_bulk_dict, os.path.join(adsorbed_bulk_dir, 'metadata.pkl')) + if config_ind == 0: + self.logger.info(f"wrote adsorbed surface ({adsorbed_bulk_dict['adsorbed_bulk_samplingstr']}) to {adsorbed_bulk_dir}") + + def _write_metadata_pkl(self, dict_to_write, path): + ''' + Writes a dict as a metadata pickle + + Args: + dict_to_write: dict containing all info to dump as file + path: output file path + ''' + file_path = os.path.join(path, 'metadata.pkl') + with open(path, 'wb') as f: + pickle.dump(dict_to_write, f) + diff --git a/ocdata/surfaces.py b/ocdata/surfaces.py new file mode 100644 index 0000000000..b96f571e81 --- /dev/null +++ b/ocdata/surfaces.py @@ -0,0 +1,348 @@ +import math +import os +import pickle +from collections import defaultdict + +import numpy as np +from ase import neighborlist +from ase.constraints import FixAtoms +from pymatgen.analysis.local_env import VoronoiNN +from pymatgen.core import Composition +from pymatgen.io.ase import AseAtomsAdaptor +from pymatgen.symmetry.analyzer import SpacegroupAnalyzer + +from .constants import MIN_XY +from .loader import Loader + + +def constrain_surface(atoms): + """ + This function fixes sub-surface atoms of a surface. Also works on systems + that have surface + adsorbate(s), as long as the bulk atoms are tagged with + `0`, surface atoms are tagged with `1`, and the adsorbate atoms are tagged + with `2` or above. + + This function is used for both surface atoms and the combined surface+adsorbate + + Inputs: + atoms `ase.Atoms` class of the surface system. The tags of + these atoms must be set such that any bulk/surface + atoms are tagged with `0` or `1`, resectively, and any + adsorbate atom is tagged with a 2 or above. + Returns: + atoms A deep copy of the `atoms` argument, but where the appropriate + atoms are constrained. + """ + # Work on a copy so that we don't modify the original + atoms = atoms.copy() + + # We'll be making a `mask` list to feed to the `FixAtoms` class. This list + # should contain a `True` if we want an atom to be constrained, and `False` + # otherwise + mask = [True if atom.tag == 0 else False for atom in atoms] + atoms.constraints += [FixAtoms(mask=mask)] + return atoms + + +class Surface: + """ + This class handles all things with a surface. + Create one with a bulk and one of its selected surfaces + + Attributes + ---------- + bulk_object : Bulk + bulk object that the surface comes from + surface_sampling_str : str + string capturing the surface index and total possible surfaces + surface_atoms : Atoms + actual atoms of the surface + constrained_surface : Atoms + constrained version of surface_atoms + millers : tuple + miller indices of the surface + shift : float + shift applied in the c-direction of bulk unit cell to get a termination + top : boolean + indicates the top or bottom termination of the pymatgen generated slab + + Public methods + -------------- + get_bulk_dict() + returns a dict containing info about the surface + """ + + def __init__( + self, + bulk_object, + surface_info, + surface_index, + total_surfaces_possible, + no_loader=True, + ): + """ + Initialize the surface object, tag atoms, and constrain the surface. + + Args: + bulk_object: `Bulk()` object of the corresponding bulk + surface_info: tuple containing atoms, millers, shift, top + surface_index: index of surface out of all possible ones for the bulk + total_surfaces_possible: number of possible surfaces from this bulk + """ + self.bulk_object = bulk_object + self.no_loader = no_loader + surface_struct, self.millers, self.shift, self.top = surface_info + self.surface_sampling_str = ( + str(surface_index) + "/" + str(total_surfaces_possible) + ) + + unit_surface_atoms = AseAtomsAdaptor.get_atoms(surface_struct) + self.surface_atoms = self.tile_atoms(unit_surface_atoms) + + # verify that the bulk and surface elements and stoichiometry match: + assert ( + Composition(self.surface_atoms.get_chemical_formula()).reduced_formula + == Composition( + bulk_object.bulk_atoms.get_chemical_formula() + ).reduced_formula + ), "Mismatched bulk and surface" + + self.tag_surface_atoms(self.bulk_object.bulk_atoms, self.surface_atoms) + self.constrained_surface = constrain_surface(self.surface_atoms) + + def tile_atoms(self, atoms): + """ + This function will repeat an atoms structure in the x and y direction until + the x and y dimensions are at least as wide as the MIN_XY constant. + + Args: + atoms `ase.Atoms` object of the structure that you want to tile + Returns: + atoms_tiled An `ase.Atoms` object that's just a tiled version of + the `atoms` argument. + """ + x_length = np.linalg.norm(atoms.cell[0]) + y_length = np.linalg.norm(atoms.cell[1]) + nx = int(math.ceil(MIN_XY / x_length)) + ny = int(math.ceil(MIN_XY / y_length)) + n_xyz = (nx, ny, 1) + atoms_tiled = atoms.repeat(n_xyz) + return atoms_tiled + + def tag_surface_atoms(self, bulk_atoms, surface_atoms): + """ + Sets the tags of an `ase.Atoms` object. Any atom that we consider a "bulk" + atom will have a tag of 0, and any atom that we consider a "surface" atom + will have a tag of 1. We use a combination of Voronoi neighbor algorithms + (adapted from from `pymatgen.core.surface.Slab.get_surface_sites`; see + https://pymatgen.org/pymatgen.core.surface.html) and a distance cutoff. + + Arg: + bulk_atoms `ase.Atoms` format of the respective bulk structure + surface_atoms The surface where you are trying to find surface sites in + `ase.Atoms` format + """ + with Loader( + " [surface][tag_surface_atoms] _find_surface_atoms_with_voronoi", + animate=False, + ignore=self.no_loader, + ) as loader: + voronoi_tags = self._find_surface_atoms_with_voronoi( + bulk_atoms, surface_atoms + ) + + height_tags = self._find_surface_atoms_by_height(surface_atoms) + # If either of the methods consider an atom a "surface atom", then tag it as such. + tags = [max(v_tag, h_tag) for v_tag, h_tag in zip(voronoi_tags, height_tags)] + surface_atoms.set_tags(tags) + + def _find_surface_atoms_with_voronoi(self, bulk_atoms, surface_atoms): + """ + Labels atoms as surface or bulk atoms according to their coordination + relative to their bulk structure. If an atom's coordination is less than it + normally is in a bulk, then we consider it a surface atom. We calculate the + coordination using pymatgen's Voronoi algorithms. + + Note that if a single element has different sites within a bulk and these + sites have different coordinations, then we consider slab atoms + "under-coordinated" only if they are less coordinated than the most under + undercoordinated bulk atom. For example: Say we have a bulk with two Cu + sites. One site has a coordination of 12 and another a coordination of 9. + If a slab atom has a coordination of 10, we will consider it a bulk atom. + + Args: + bulk_atoms `ase.Atoms` of the bulk structure the surface was cut + from. + surface_atoms `ase.Atoms` of the surface + Returns: + tags A list of 0's and 1's whose indices align with the atoms in + `surface_atoms`. 0's indicate a bulk atom and 1 indicates a + surface atom. + """ + # Initializations + surface_struct = AseAtomsAdaptor.get_structure(surface_atoms) + center_of_mass = self.calculate_center_of_mass(surface_struct) + bulk_cn_dict = self.calculate_coordination_of_bulk_atoms(bulk_atoms) + voronoi_nn = VoronoiNN(tol=0.1) # 0.1 chosen for better detection + default_cutoff = voronoi_nn.cutoff + + tags = [] + for idx, site in enumerate(surface_struct): + # Tag as surface atom only if it's above the center of mass + if site.frac_coords[2] > center_of_mass[2]: + # Run the voronoi tesselation with increasing cutoffs until it's + # possible to compute the coordination number + cutoff = default_cutoff + max_cutoff = ( + surface_struct.lattice.a**2 + + surface_struct.lattice.b**2 + + surface_struct.lattice.c**2 + ) ** 0.5 + while True: + try: + # Tag as surface if atom is under-coordinated + voronoi_nn.cutoff = cutoff + cn = voronoi_nn.get_cn(surface_struct, idx, use_weights=True) + cn = round(cn, 5) + if cn < min(bulk_cn_dict[site.species_string]): + tags.append(1) + else: + tags.append(0) + break + + # Tag as surface if we get a pathological error + except RuntimeError: + tags.append(1) + break + + # A ValueError can occur if the cutoff is too small. + except ValueError: + # Increase cutoff if max_cutoff has not been reached. Tag atom + # at surface otherwise + if cutoff < max_cutoff: + cutoff = min(cutoff * 2, max_cutoff) + else: + tags.append(1) + break + + # Tag as bulk otherwise + else: + tags.append(0) + return tags + + def calculate_center_of_mass(self, struct): + """ + Determine the surface atoms indices from here + """ + weights = [site.species.weight for site in struct] + center_of_mass = np.average(struct.frac_coords, weights=weights, axis=0) + return center_of_mass + + def calculate_coordination_of_bulk_atoms(self, bulk_atoms): + """ + Finds all unique atoms in a bulk structure and then determines their + coordination number. Then parses these coordination numbers into a + dictionary whose keys are the elements of the atoms and whose values are + their possible coordination numbers. + For example: `bulk_cns = {'Pt': {3., 12.}, 'Pd': {12.}}` + + Arg: + bulk_atoms An `ase.Atoms` object of the bulk structure. + Returns: + bulk_cn_dict A defaultdict whose keys are the elements within + `bulk_atoms` and whose values are a set of integers of the + coordination numbers of that element. + """ + voronoi_nn = VoronoiNN(tol=0.1) # 0.1 chosen for better detection + default_cutoff = voronoi_nn.cutoff + + # Object type conversion so we can use Voronoi + bulk_struct = AseAtomsAdaptor.get_structure(bulk_atoms) + sga = SpacegroupAnalyzer(bulk_struct) + sym_struct = sga.get_symmetrized_structure() + + # We'll only loop over the symmetrically distinct sites for speed's sake + bulk_cn_dict = defaultdict(set) + for idx in sym_struct.equivalent_indices: + site = sym_struct[idx[0]] + + # Run the voronoi tesselation with increasing cutoffs until it's + # possible to compute the coordination number + cutoff = default_cutoff + max_cutoff = ( + bulk_struct.lattice.a**2 + + bulk_struct.lattice.b**2 + + bulk_struct.lattice.c**2 + ) ** 0.5 + while True: + try: + voronoi_nn.cutoff = cutoff + cn = voronoi_nn.get_cn(bulk_struct, idx[0], use_weights=True) + cn = round(cn, 5) + break + + # A ValueError can occur if the cutoff is too small. + except ValueError: + # Increase cutoff if max_cutoff has not been reached. + if cutoff < max_cutoff: + cutoff = min(cutoff * 2, max_cutoff) + else: + raise RuntimeError("No neighbor found even with max cutoff.") + + bulk_cn_dict[site.species_string].add(cn) + return bulk_cn_dict + + def _find_surface_atoms_by_height(self, surface_atoms): + """ + As discussed in the docstring for `_find_surface_atoms_with_voronoi`, + sometimes we might accidentally tag a surface atom as a bulk atom if there + are multiple coordination environments for that atom type within the bulk. + One heuristic that we use to address this is to simply figure out if an + atom is close to the surface. This function will figure that out. + + Specifically: We consider an atom a surface atom if it is within 2 + Angstroms of the heighest atom in the z-direction (or more accurately, the + direction of the 3rd unit cell vector). + + Arg: + surface_atoms The surface where you are trying to find surface sites in + `ase.Atoms` format + Returns: + tags A list that contains the indices of + the surface atoms + """ + unit_cell_height = np.linalg.norm(surface_atoms.cell[2]) + scaled_positions = surface_atoms.get_scaled_positions() + scaled_max_height = max( + scaled_position[2] for scaled_position in scaled_positions + ) + scaled_threshold = scaled_max_height - 2.0 / unit_cell_height + + tags = [ + 0 if scaled_position[2] < scaled_threshold else 1 + for scaled_position in scaled_positions + ] + return tags + + def get_bulk_dict(self): + """ + Returns an organized dict for writing to files. + All info is already processed and stored in class variables. + """ + self.overall_sampling_str = ( + self.bulk_object.elem_sampling_str + + "_" + + self.bulk_object.bulk_sampling_str + + "_" + + self.surface_sampling_str + ) + return { + "bulk_atomsobject": self.constrained_surface, + "bulk_metadata": ( + self.bulk_object.mpid, + self.millers, + round(self.shift, 3), + self.top, + ), + "bulk_samplingstr": self.overall_sampling_str, + } diff --git a/ocdata/vasp.py b/ocdata/vasp.py new file mode 100644 index 0000000000..59fba9097c --- /dev/null +++ b/ocdata/vasp.py @@ -0,0 +1,230 @@ +''' +This submodule contains the scripts that the we used to run VASP. + +Note that some of these scripts were taken and modified from +[GASpy](https://github.com/ulissigroup/GASpy) with permission of authors. +''' + +__author__ = 'Kevin Tran' +__email__ = 'ktran@andrew.cmu.edu' + +import os +import numpy as np +import ase.io +from ase.io.trajectory import TrajectoryWriter +from ase.calculators.vasp import Vasp2 +from ase.calculators.singlepoint import SinglePointCalculator as SPC + +# NOTE: this is the setting for slab and adslab +VASP_FLAGS = {'ibrion': 2, + 'nsw': 2000, + 'isif': 0, + 'isym': 0, + 'lreal': 'Auto', + 'ediffg': -0.03, + 'symprec': 1e-10, + 'encut': 350., + 'laechg': True, + 'lwave': False, + 'ncore': 4, + 'gga': 'RP', + 'pp': 'PBE', + 'xc': 'PBE'} + +# This is the setting for bulk optmization. +# Only use when expanding the bulk_db with other crystal structures. +BULK_VASP_FLAGS = {'ibrion': 1, + 'nsw': 100, + 'isif': 7, + 'isym':0, + 'ediffg': 1e-08, + 'encut': 500., + 'kpts': (10, 10, 10), + 'prec':'Accurate', + 'gga': 'RP', + 'pp': 'PBE', + 'lwave':False, + 'lcharg':False} + +def run_vasp(atoms, vasp_flags=None): + ''' + Will relax the input atoms given the VASP flag inputs. + + Args: + atoms `ase.Atoms` object that we want to relax. + vasp_flags A dictionary of settings we want to pass to the `Vasp2` + calculator. Defaults to a standerd set of values if `None` + Returns: + trajectory A list of `ase.Atoms` objects where each element represents + each step during the relaxation. + ''' + if vasp_flags is None: # Immutable default + vasp_flags = VASP_FLAGS.copy() + + atoms, vasp_flags = _clean_up_inputs(atoms, vasp_flags) + vasp_flags = _set_vasp_command(vasp_flags) + trajectory = relax_atoms(atoms, vasp_flags) + return trajectory + + +def _clean_up_inputs(atoms, vasp_flags): + ''' + Parses the inputs and makes sure some things are straightened out. + + Arg: + atoms `ase.Atoms` object of the structure we want to relax + vasp_flags A dictionary of settings we want to pass to the `Vasp2` + calculator + Returns: + atoms `ase.Atoms` object of the structure we want to relax, but + with the unit vectors fixed (if needed) + vasp_flags A modified version of the 'vasp_flags' argument + ''' + # Check that the unit vectors obey the right-hand rule, (X x Y points in + # Z). If not, then flip the order of X and Y to enforce this so that VASP + # is happy. + if np.dot(np.cross(atoms.cell[0], atoms.cell[1]), atoms.cell[2]) < 0: + atoms.set_cell(atoms.cell[[1, 0, 2], :]) + + # Calculate and set the k points + if 'kpts' not in vasp_flags.keys(): + k_pts = calculate_surface_k_points(atoms) + vasp_flags['kpts'] = k_pts + + return atoms, vasp_flags + + +def calculate_surface_k_points(atoms): + ''' + For surface calculations, it's a good practice to calculate the k-point + mesh given the unit cell size. We do that on-the-spot here. + + Arg: + atoms `ase.Atoms` object of the structure we want to relax + Returns: + k_pts A 3-tuple of integers indicating the k-point mesh to use + ''' + cell = atoms.get_cell() + order = np.inf + a0 = np.linalg.norm(cell[0], ord=order) + b0 = np.linalg.norm(cell[1], ord=order) + multiplier = 40 + k_pts = (max(1, int(round(multiplier/a0))), + max(1, int(round(multiplier/b0))), + 1) + return k_pts + + +def _set_vasp_command(n_processors=16, vasp_executable='vasp_std'): + ''' + This function assigns the appropriate call to VASP to the `$VASP_COMMAND` + variable. + ''' + # TODO: Sid and/or Caleb to figure out what exactly to put here to make + # things work. Here are some examples: + # https://github.com/ulissigroup/GASpy/blob/master/gaspy/vasp_functions.py#L167 + # https://github.com/ulissigroup/GASpy/blob/master/gaspy/vasp_functions.py#L200 + command = 'srun -n %d %s' % (n_processors, vasp_executable) + os.environ['VASP_COMMAND'] = command + raise NotImplementedError + + +def relax_atoms(atoms, vasp_flags): + ''' + Perform a DFT relaxation with VASP and then write the trajectory to the + 'relaxation.traj' file. + + Args: + atoms `ase.Atoms` object of the structure we want to relax + vasp_flags A dictionary of settings we want to pass to the `Vasp2` + calculator + Returns: + images A list of `ase.Atoms` that comprise the relaxation + trajectory + ''' + # Run the calculation + calc = Vasp2(**vasp_flags) + atoms.set_calculator(calc) + atoms.get_potential_energy() + + # Read the trajectory from the output file + images = [] + for atoms in ase.io.read('vasprun.xml', ':'): + image = atoms.copy() + image = image[calc.resort] + image.set_calculator(SPC(image, + energy=atoms.get_potential_energy(), + forces=atoms.get_forces()[calc.resort])) + images += [image] + + # Write the trajectory + with TrajectoryWriter('relaxation.traj', 'a') as writer: + for atoms in images: + writer.write(atoms) + return images + + +def write_vasp_input_files(atoms, outdir='.', vasp_flags=None): + ''' + Effectively goes through the same motions as the `run_vasp` function, + except it only writes the input files instead of running. + + Args: + atoms `ase.Atoms` object that we want to relax. + outdir A string indicating where you want to save the input files. + Defaults to '.' + vasp_flags A dictionary of settings we want to pass to the `Vasp2` + calculator. Defaults to a standerd set of values if `None` + ''' + if vasp_flags is None: # Immutable default + vasp_flags = VASP_FLAGS.copy() + + atoms, vasp_flags = _clean_up_inputs(atoms, vasp_flags) + calc = Vasp2(directory=outdir, **vasp_flags) + calc.write_input(atoms) + + +def xml_to_tuples(xml='vasprun.xml'): + ''' + Converts an XML file into both a trajectory file while also returning the + trajectory as a list of `ase.Atoms` objects + + Args: + xml String indicating the XML file to read from + Returns: + images A list of 5-tuples for each images in the trajectory. The + tuples include a list of symbols for each atom; the positions + of the atoms; the forces each atom sees; the unit cell + dimensions; and the potential energy of the whole system. + ''' + traj = xml_to_traj(xml) + + images = [] + for atoms in traj: + symbols = atoms.get_chemical_symbols() + positions = atoms.get_positions() + forces = atoms.get_forces() + cell = np.array(atoms.get_cell()) + energy = atoms.get_potential_energy() + atoms_tuple = (symbols, positions, forces, cell, energy) + images.append(atoms_tuple) + + return images + + +def xml_to_traj(xml='vasprun.xml'): + ''' + Converts an XML file into both a trajectory file while also returning the + trajectory as a list of `ase.Atoms` objects + + Args: + xml String indicating the XML file to read from + Returns: + traj A list of `ase.Atoms` objects + ''' + traj = ase.io.read(xml, ':') + for atoms in traj: + atoms.set_calculator(SPC(atoms, + energy=atoms.get_potential_energy(), + forces=atoms.get_forces())) + return traj diff --git a/ocpmodels/common/logger.py b/ocpmodels/common/logger.py index 084a32f7e9..4400267730 100644 --- a/ocpmodels/common/logger.py +++ b/ocpmodels/common/logger.py @@ -9,9 +9,9 @@ from pathlib import Path import torch +import wandb from torch.utils.tensorboard import SummaryWriter -import wandb from ocpmodels.common.registry import registry from ocpmodels.common.utils import CLUSTER, JOB_ID diff --git a/ocpmodels/common/utils.py b/ocpmodels/common/utils.py index d0e3c15b48..af7cddc22d 100644 --- a/ocpmodels/common/utils.py +++ b/ocpmodels/common/utils.py @@ -33,12 +33,12 @@ from matplotlib.figure import Figure from torch_geometric.data import Data from torch_geometric.utils import remove_self_loops -from torch_scatter import segment_coo, segment_csr, scatter +from torch_scatter import scatter, segment_coo, segment_csr import ocpmodels -from ocpmodels.common.flags import flags, Flags -from ocpmodels.common.registry import registry import ocpmodels.common.dist_utils as dist_utils +from ocpmodels.common.flags import Flags, flags +from ocpmodels.common.registry import registry class Cluster: @@ -948,6 +948,37 @@ def set_cpus_to_workers(config, silent=None): return config +def set_dataset_split(config): + """ + Set the split for all datasets in the config to the one specified in the + config's name. + + Resulting dict: + { + "dataset": { + "train": { + "split": "all" + ... + }, + ... + } + } + + Args: + config (dict): The full trainer config dict + + Returns: + dict: The updated config dict + """ + split = config["config"].split("-")[-1] + for d, dataset in config["dataset"].items(): + if d == "default_val": + continue + assert isinstance(dataset, dict) + config["dataset"][d]["split"] = split + return config + + def check_regress_forces(config): if "regress_forces" in config["model"]: if config["model"]["regress_forces"] == "": @@ -1023,7 +1054,7 @@ def load_config(config_str): return config -def build_config(args, args_override=[], silent=None): +def build_config(args, args_override=[], dict_overrides={}, silent=None): config, overrides, loaded_config = {}, {}, {} if hasattr(args, "config_yml") and args.config_yml: @@ -1034,6 +1065,7 @@ def build_config(args, args_override=[], silent=None): args_dict_with_defaults = {k: v for k, v in vars(args).items() if v is not None} if args_override != []: overrides = create_dict_from_args(args_override) + overrides = merge_dicts(overrides, dict_overrides) if args.continue_from_dir or args.restart_from_dir: # make sure it's either continue xor restart @@ -1230,6 +1262,7 @@ def build_config(args, args_override=[], silent=None): config = override_drac_paths(config) config = continue_from_slurm_job_id(config) config = read_slurm_env(config) + config = set_dataset_split(config) config["optim"]["eval_batch_size"] = config["optim"]["batch_size"] dist_utils.setup(config) @@ -1825,17 +1858,17 @@ def make_trainer_from_dir(path, mode, overrides={}, silent=None): return registry.get_trainer_class(config["trainer"])(**config) -def make_trainer_from_conf_str(conf_str, overrides={}): +def make_trainer_from_conf_str(conf_str, overrides={}, silent=None): assert isinstance( overrides, dict ), f"Overrides must be a dict. Received {overrides}" - config = make_config_from_conf_str(conf_str) + config = make_config_from_conf_str(conf_str, overrides, silent) config = merge_dicts(config, overrides) return registry.get_trainer_class(config["trainer"])(**config) -def make_config_from_conf_str(conf_str): +def make_config_from_conf_str(conf_str, overrides={}, silent=None): argv = deepcopy(sys.argv) sys.argv[1:] = [] default_args = Flags().get_parser().parse_args() @@ -1843,7 +1876,7 @@ def make_config_from_conf_str(conf_str): default_args.config = conf_str - config = build_config(default_args) + config = build_config(default_args, dict_overrides=overrides, silent=silent) setup_imports() return config diff --git a/ocpmodels/datasets/lmdb_dataset.py b/ocpmodels/datasets/lmdb_dataset.py index 0a7abaea85..e4ea6bd7bc 100644 --- a/ocpmodels/datasets/lmdb_dataset.py +++ b/ocpmodels/datasets/lmdb_dataset.py @@ -53,11 +53,13 @@ def __init__( lmdb_glob=None, adsorbates=None, adsorbates_ref_dir=None, + silent=False, ): super().__init__() self.config = config self.adsorbates = adsorbates self.adsorbates_ref_dir = adsorbates_ref_dir + self.silent = silent self.path = Path(self.config["src"]) if not self.path.is_file(): @@ -105,7 +107,7 @@ def filter_per_adsorbates(self): return # val_ood_ads and val_ood_both don't have targeted adsorbates - if self.config["src"].split("/")[-2] in {"val_ood_ads", "val_ood_both"}: + if Path(self.config["src"]).parts[-1] in {"val_ood_ads", "val_ood_both"}: return # make set of adsorbates from a list or a string. If a string, split on comma. @@ -128,10 +130,23 @@ def filter_per_adsorbates(self): if not ref_path.is_dir(): print(f"Adsorbate reference directory {ref_path} does not exist.") return - pattern = "-".join(self.path.parts[-3:]) + pattern = f"{self.config['split']}-{self.path.parts[-1]}" candidates = list(ref_path.glob(f"*{pattern}*.json")) if not candidates: - print(f"No adsorbate reference files found for {self.path.name}.") + print( + f"No adsorbate reference files found for {self.path.name}.:" + + "\n".join( + [ + str(p) + for p in [ + ref_path, + pattern, + list(ref_path.glob(f"*{pattern}*.json")), + list(ref_path.glob("*")), + ] + ] + ) + ) return if len(candidates) > 1: print( @@ -147,6 +162,8 @@ def filter_per_adsorbates(self): if a in ads ) + previous_samples = self.num_samples + # filter the dataset indices if isinstance(self._keys[0], bytes): self._keys = [i for i in self._keys if i in allowed_idxs] @@ -158,6 +175,12 @@ def filter_per_adsorbates(self): self._keylen_cumulative = np.cumsum(keylens).tolist() self.num_samples = sum(keylens) + if not self.silent: + print( + f"Filtered dataset {pattern} from {previous_samples} to", + f"{self.num_samples} samples. (adsorbates: {ads})", + ) + assert self.num_samples > 0, f"No samples found for adsorbates {ads}." def __len__(self): @@ -229,14 +252,17 @@ def close_db(self): @registry.register_dataset("deup_lmdb") class DeupDataset(LmdbDataset): - def __init__(self, all_datasets_configs, deup_split, transform=None): + def __init__(self, all_datasets_configs, deup_split, transform=None, silent=False): + # ! WARNING: this does not (yet?) handle adsorbate filtering super().__init__( all_datasets_configs[deup_split], lmdb_glob=deup_split.replace("deup-", "").split("-"), + silent=silent, ) ocp_splits = deup_split.split("-")[1:] self.ocp_datasets = { - d: LmdbDataset(all_datasets_configs[d], transform) for d in ocp_splits + d: LmdbDataset(all_datasets_configs[d], transform, silent=silent) + for d in ocp_splits } def __getitem__(self, idx): @@ -248,6 +274,41 @@ def __getitem__(self, idx): return ocp_sample +@registry.register_dataset("stats_lmdb") +class StatsDataset(LmdbDataset): + def to_reduced_formula(self, list_of_z): + from collections import Counter + + from pymatgen.core.composition import Composition + from pymatgen.core.periodic_table import Element + + return Composition.from_dict( + Counter([Element.from_Z(i).symbol for i in list_of_z]) + ).reduced_formula + + def __getitem__(self, idx): + data_object = super().__getitem__(idx) + data_object.stats = { + "atomic_numbers_bulk": data_object.atomic_numbers[data_object["tags"] < 2] + .int() + .tolist(), + "atomic_numbers_ads": data_object.atomic_numbers[data_object["tags"] == 2] + .int() + .tolist(), + "composition_bulk": self.to_reduced_formula( + data_object.atomic_numbers[data_object["tags"] < 2].int() + ), + "composition_ads": self.to_reduced_formula( + data_object.atomic_numbers[data_object["tags"] == 2].int() + ), + "idx_in_dataset": [data_object.idx_in_dataset], + "sid": [data_object.sid], + "y_relaxed": [data_object.y_relaxed], + "y_init": [data_object.y_init], + } + return data_object + + class SinglePointLmdbDataset(LmdbDataset): def __init__(self, config, transform=None): super(SinglePointLmdbDataset, self).__init__(config, transform) diff --git a/ocpmodels/trainers/base_trainer.py b/ocpmodels/trainers/base_trainer.py index 824331210a..ea1537737d 100644 --- a/ocpmodels/trainers/base_trainer.py +++ b/ocpmodels/trainers/base_trainer.py @@ -14,6 +14,8 @@ from collections import defaultdict from copy import deepcopy from pathlib import Path +from uuid import uuid4 + import numpy as np import torch import torch.nn as nn @@ -25,7 +27,7 @@ from torch.utils.data import DataLoader from torch_geometric.data import Batch from tqdm import tqdm -from uuid import uuid4 + from ocpmodels.common import dist_utils from ocpmodels.common.data_parallel import ( BalancedBatchSampler, @@ -35,7 +37,12 @@ from ocpmodels.common.graph_transforms import RandomReflect, RandomRotate from ocpmodels.common.registry import registry from ocpmodels.common.timer import Times -from ocpmodels.common.utils import JOB_ID, get_commit_hash, save_checkpoint, resolve +from ocpmodels.common.utils import ( + JOB_ID, + get_commit_hash, + resolve, + save_checkpoint, +) from ocpmodels.datasets.data_transforms import FrameAveraging, get_transforms from ocpmodels.modules.evaluator import Evaluator from ocpmodels.modules.exponential_moving_average import ( @@ -258,7 +265,10 @@ def load_datasets(self): if "deup" in split: self.datasets[split] = registry.get_dataset_class("deup_lmdb")( - self.config["dataset"], split, transform=transform + self.config["dataset"], + split, + transform=transform, + silent=self.silent, ) else: self.datasets[split] = registry.get_dataset_class( @@ -268,6 +278,7 @@ def load_datasets(self): transform=transform, adsorbates=self.config.get("adsorbates"), adsorbates_ref_dir=self.config.get("adsorbates_ref_dir"), + silent=self.silent, ) shuffle = False diff --git a/sample_adslab.py b/sample_adslab.py new file mode 100644 index 0000000000..1edf53eeec --- /dev/null +++ b/sample_adslab.py @@ -0,0 +1,393 @@ +""" +Procedure to sample and construct an adslab graph. + + +Nota Benes: + +- The atoms in the slab will have tags set to the layer number: First layer atoms will have tag=1, second layer atoms will have tag=2, and so on. Adsorbates get tag=0: + https://wiki.fysik.dtu.dk/ase/ase/build/surface.html + + +- GFN miller indices action space should be constrained by get_symmetrically_distinct_miller_indices + (cf bulk_obj.enumerate_surfaces()) + +""" + +from ocdata.loader import Loader + +with Loader("Imports"): + import pickle + from collections import defaultdict + from pathlib import Path + + import numpy as np + from minydra import resolved_args + + from ocdata.adsorbates import Adsorbate + from ocdata.bulk_obj import Bulk + from ocdata.combined import Combined + from ocdata.surfaces import Surface + from ocpmodels.preprocessing.atoms_to_graphs import AtomsToGraphs + +# ---------------------------- +# ----- UTILS (ignore) ----- +# ---------------------------- + + +def print_header(i, nruns): + """ + Prints + ------------------- + ---- Run i ---- + ------------------- + """ + box_char = "#" + border_width = 4 + border = box_char * border_width + box_width = 40 + + runs_len = len(str(nruns)) + title_str = f"Run {str(i + 1).zfill(runs_len)}/{nruns}" + + n_space = box_width - 2 * len(border) - len(title_str) + n_left = n_space // 2 + n_right = n_space // 2 + (n_space % 2) + + print("\n" + box_char * box_width) + print(border + " " * n_left + title_str + " " * n_right + border) + print(box_char * box_width) + + +def print_out_times(out_times, fpath=None, prec=3): + """ + Prints a summary of the out_time dictionnary + + Args: + out_times (dict[list]): dictionnary of times + fpath (Union[str, pathlib.Path], optional): path to write the + string summary to. Defaults to None (= no writing) + prec (int, optional): print decimals. Defaults to 3. + + Returns: + str: stringsummary + """ + max_k_len = max([len(k) for k in out_times]) + strs = [f"{'Operation':{max_k_len}} -> Time (s)"] + + all_keys = sorted(out_times.keys()) + single_keys = [] + if not all([len(k) == 1 for k in out_times]): + single_keys = [k for k, v in out_times.items() if len(v) == 1] + all_keys = single_keys + [k for k in out_times if k not in set(single_keys)] + + single_key_sep = None + + for i, k in enumerate(all_keys): + if single_keys and k not in single_keys and single_key_sep is None: + single_key_sep = i + 1 + times = out_times[k] + s = f"{k:{max_k_len}} -> " + if len(times) > 1: + q1, med, q3 = np.percentile(times, [25, 50, 75]) + mean, std = np.mean(times), np.std(times) + s += f"[{q1:.{prec}f} | {med:.{prec}f} | {q3:.{prec}f}]" + s += f" ~ {mean:.{prec}f} +/- {std:.{prec}f}" + else: + s += f"{times[0]:.{prec}f}" + strs.append(s) + + max_s_len = max(len(s) for s in strs) + border = "-" * max_s_len + strs.append(border) + + if single_key_sep is not None: + strs = ( + strs[:single_key_sep] + + [ + border, + f"{'Operation':{max_k_len}} -> [q1 | med | q3] ~ mean +/- std", + border, + ] + + strs[single_key_sep:] + ) + + out_str = "\n".join([border] + strs[:1] + [border] + strs[1:]) + + if fpath is not None: + with open(fpath, "w") as f: + f.write(out_str) + + print(out_str) + + +def get_ads_db(args): + """ + Util to load the adsorbates pre-computed dict from the args + + Args: + args (Union[dict, minydra.MinyDict]): Command-line args + + Returns: + dict: adsorbates dictionnary + """ + with open(args.paths.adsorbate_db, "rb") as f: + return pickle.load(f) + + +# ------------------------------------ +# ----- Action Space Functions ----- +# ------------------------------------ + + +def select_adsorbate(ads_dict, smiles): + """ + Function to parameterize the choice of an adsorbate. + Curent parameterization relies on its chemical formula. + + Args: + db_path (Union[str, pathlib.Path]): path to the pickle file holding adsorbates + smiles (str): The smiles string description for the adsorbate + + Returns: + Optional[ase.Atom]: The selected adsorbate. None if the formula does not exist + """ + + if smiles is None: + smiles = np.random.choice([a[1] for a in ads_dict.values()]) + print( + "No adsorbate smiles has been provided. Selecting {} at random.".format( + smiles + ) + ) + + adsorbates = [(str(k), *a) for k, a in ads_dict.items() if a[1] == smiles] + + if len(adsorbates) == 0: + raise ValueError(f"No adsorbate exists with smiles {smiles}") + if len(adsorbates) > 1: + raise ValueError( + f"More than 1 adsorbate exists with smiles {smiles}:\n" + + ", ".join([a[2] for a in adsorbates]) + ) + + return adsorbates[0] + + +if __name__ == "__main__": + with Loader("Full procedure", animate=False): + # ------------------- + # ----- Setup ----- + # ------------------- + + # path to directory's root + root = Path(__file__).resolve().parent + # load default args then overwrite from command-line + args = resolved_args(defaults=root / "configs" / "sample" / "defaults.yaml") + + if isinstance( + args.actions.binding_site_index, str + ) and args.actions.binding_site_index.lower() in {"null", "none"}: + args.actions.binding_site_index = None + + # print parsed arguments + if args.verbose > 0: + args.pretty_print() + + out_times = defaultdict(list) + + # set seed + seed = args.seed or 0 + + with Loader( + "Reading bulk_db_flat", + animate=args.animate, + ignore=args.no_loader, + out=out_times, + ): + # load flat bulk db + with open(args.paths.bulk_db_flat, "rb") as f: + bulk_db_list = pickle.load(f) + + with Loader( + "Reading adsorbates dict", + animate=args.animate, + ignore=args.no_loader, + out=out_times, + ): + ads_dict = get_ads_db(args) + + print( + "Surface sampling string:", + "surface_idx / total_possible_surfaces_for_bulk", + ) + + # for sampling purposes and debugging we can run multiple sampling procedure + # by specifying nruns=N in the command-line + + # ------------------ + # ----- Runs ----- + # ------------------ + + for i in range(args.nruns): + np.random.seed(seed + i) + print_header(i, args.nruns) + + with Loader( + f"Actions to Data {i+1}/{args.nruns}", animate=False, out=out_times + ): + # ----------------------- + # ----- Adsorbate ----- + # ----------------------- + + print("\n1. Adsorbate\n") + + with Loader( + "Make Adsorbate object", + animate=args.animate, + ignore=args.no_loader, + out=out_times, + ): + adsorbate_atoms = select_adsorbate( + ads_dict, args.actions.adsorbate_formula + ) + # make Adsorbate object + # (adsorbate selection is done in the class if adsorbate_id is None) + adsorbate_obj = Adsorbate( # <<<< IMPORTANT + adsorbate_atoms=adsorbate_atoms + ) + print( + "# Selected adsorbate:", + adsorbate_obj.atoms.get_chemical_formula(), + ) + + # ------------------ + # ----- Bulk ----- + # ------------------ + + print("\n2. Bulk\n") + + with Loader( + "Make Bulk object", + animate=args.animate, + ignore=args.no_loader, + out=out_times, + ): + # select bulk_id if None + if args.actions.bulk_id is None: + bulk_id = np.random.choice(len(bulk_db_list)) + print(f"args.actions.bulk_id is None, choosing {bulk_id}") + else: + bulk_id = args.actions.bulk_id + + # make Bulk object + bulk = Bulk( # <<<< IMPORTANT + bulk_db_list, + bulk_index=bulk_id, + precomputed_structures=args.paths.precomputed_structures + if args.use_precomputed_surfaces + else None, + ) + print( + "# Selected bulk:", + bulk.bulk_atoms.get_chemical_formula(), + f"({bulk.mpid})", + ) + + # --------------------- + # ----- Surface ----- + # --------------------- + + print("\n3. Surface\n") + + with Loader( + "bulk.get_possible_surfaces()", + animate=args.animate, + ignore=args.no_loader, + out=out_times, + ): + possible_surfaces = bulk.get_possible_surfaces() + + if len(possible_surfaces) == 0: + print("No surface found. ABORTING") + continue + + with Loader( + "Make Surface object", + animate=args.animate, + ignore=args.no_loader, + out=out_times, + ): + # select surface_id if it is None + if args.actions.surface_id is None: + surface_id = np.random.choice(len(possible_surfaces)) + print(f"args.actions.surface_id is None, choosing {surface_id}") + else: + assert args.actions.surface_id < len(possible_surfaces) + surface_id = args.actions.surface_id + + # make Surface object + surface_obj = Surface( # <<<< IMPORTANT + bulk, + possible_surfaces[surface_id], + surface_id, + len(possible_surfaces), + no_loader=args.no_loader, + ) + print( + "# Selected surface:", + surface_obj.surface_atoms.get_chemical_formula(), + f"({surface_obj.surface_sampling_str})", + ) + + # ---------------------- + # ----- Combined ----- + # ---------------------- + + print("\n4. Combined\n") + + with Loader( + "Make Combined object", + animate=args.animate, + ignore=args.no_loader, + out=out_times, + ): + # combine adsorbate + bulk + try: + adslab = Combined( # <<<< IMPORTANT + adsorbate_obj, + surface_obj, + enumerate_all_configs=False, + no_loader=args.no_loader, + index=args.actions.binding_site_index, + ) + except Exception as e: + import traceback + + traceback.print_exc() + print("\n\nABORTING") + continue + atoms_object = adslab.constrained_adsorbed_surfaces[0] + + # ------------------ + # ----- Data ----- + # ------------------ + + print("\n5. Data\n") + + with Loader( + "Make torch_geometric data", + animate=args.animate, + ignore=args.no_loader, + out=out_times, + ): + converter = AtomsToGraphs( + r_energy=False, + r_forces=False, + r_distances=True, + r_edges=True, + r_fixed=True, + ) + # Convert ase.Atoms into torch_geometric.Data + data = converter.convert(atoms_object) + + print_out_times(out_times) diff --git a/scripts/cut_a_surface.py b/scripts/cut_a_surface.py new file mode 100644 index 0000000000..09fc795440 --- /dev/null +++ b/scripts/cut_a_surface.py @@ -0,0 +1,22 @@ +from pymatgen.core.structure import Structure +from pymatgen.core.surface import SlabGenerator + +# Import the LiFePO4 structure +LiFePO4 = Structure.from_file("ocdata/LiFePO4.cif") +print(LiFePO4) + +# Let's add some oxidation states to LiFePO4 +LiFePO4.add_oxidation_state_by_element({"Fe": 2, "Li": 1, "P": 5, "O": -2}) + +# Generate the slab = cut through the surface using Miller indices +slabgen = SlabGenerator( + LiFePO4, + miller_index=(0, 0, 1), + min_slab_size=10, + min_vacuum_size=10, + center_slab=True, +) +# Get all the unique terminations along the normal to the Miller plane we are interested in +slabs = slabgen.get_slabs() +print(f"There are {len(slabs)} slabs") +print(slabs[0]) diff --git a/setup.py b/setup.py index bf2daae37e..f6fa99bdcb 100644 --- a/setup.py +++ b/setup.py @@ -5,13 +5,31 @@ LICENSE file in the root directory of this source tree. """ -from setuptools import find_packages, setup +from distutils.util import convert_path +from pathlib import Path + +from setuptools import setup + + +def make_ocpmodels_package_dict(): + dirs = [ + convert_path(str(p)) + for p in Path("./ocpmodels/").glob("**") + if (p / "__init__.py").exists() + ] + pkgs = [d.replace("/", ".") for d in dirs] + return {p: d for p, d in zip(pkgs, dirs)} + + +pkg_dict = make_ocpmodels_package_dict() +pkg_dict["ocdata"] = convert_path("ocdata") setup( - name="ocp-models", - version="0.0.3", + name="ocpmodels", + version="0.1.0", description="Machine learning models for use in catalysis as part of the Open Catalyst Project", url="https://github.com/Open-Catalyst-Project/ocp", - packages=find_packages(), + packages=list(pkg_dict.keys()), + package_dir=pkg_dict, include_package_data=True, )