| import itertools |
| import numpy as np |
| import torch |
| import hydra |
|
|
| from scipy.spatial.distance import pdist |
| from scipy.spatial.distance import cdist |
| from hydra.experimental import compose |
| from hydra import initialize_config_dir |
| from pathlib import Path |
|
|
| import smact |
| from smact.screening import pauling_test |
|
|
| from cdvae.common.constants import CompScalerMeans, CompScalerStds |
| from cdvae.common.data_utils import StandardScaler, chemical_symbols |
| from cdvae.pl_data.dataset import TensorCrystDataset |
| from cdvae.pl_data.datamodule import worker_init_fn |
|
|
| from torch_geometric.data import DataLoader |
|
|
| CompScaler = StandardScaler( |
| means=np.array(CompScalerMeans), |
| stds=np.array(CompScalerStds), |
| replace_nan_token=0.) |
|
|
|
|
| def load_data(file_path): |
| if file_path[-3:] == 'npy': |
| data = np.load(file_path, allow_pickle=True).item() |
| for k, v in data.items(): |
| if k == 'input_data_batch': |
| for k1, v1 in data[k].items(): |
| data[k][k1] = torch.from_numpy(v1) |
| else: |
| data[k] = torch.from_numpy(v).unsqueeze(0) |
| else: |
| data = torch.load(file_path) |
| return data |
|
|
|
|
| def get_model_path(eval_model_name): |
| import cdvae |
| model_path = ( |
| Path(cdvae.__file__).parent / 'prop_models' / eval_model_name) |
| return model_path |
|
|
|
|
| def load_config(model_path): |
| with initialize_config_dir(str(model_path)): |
| cfg = compose(config_name='hparams') |
| return cfg |
|
|
|
|
| def load_model(model_path, load_data=False, testing=True, batch_size=None): |
| with initialize_config_dir(str(model_path)): |
| cfg = compose(config_name='hparams') |
| if batch_size is not None: |
| cfg.data.datamodule.batch_size.test = batch_size |
| model = hydra.utils.instantiate( |
| cfg.model, |
| optim=cfg.optim, |
| data=cfg.data, |
| logging=cfg.logging, |
| _recursive_=False, |
| ) |
| ckpts = list(model_path.glob('*.ckpt')) |
| if len(ckpts) > 0: |
| ckpt_epochs = np.array( |
| [int(ckpt.parts[-1].split('-')[0].split('=')[1]) for ckpt in ckpts]) |
| ckpt = str(ckpts[ckpt_epochs.argsort()[-1]]) |
| model = type(model).load_from_checkpoint(ckpt) |
| model.lattice_scaler = torch.load(model_path / 'lattice_scaler.pt') |
| |
|
|
| if load_data: |
| datamodule = hydra.utils.instantiate( |
| cfg.data.datamodule, _recursive_=False, scaler_path=model_path |
| ) |
| if testing: |
| datamodule.setup('test') |
| test_loader = datamodule.test_dataloader()[0] |
| else: |
| datamodule.setup() |
| test_loader = datamodule.val_dataloader()[0] |
| else: |
| test_loader = None |
|
|
| return model, test_loader, cfg |
|
|
|
|
| def get_crystals_list( |
| frac_coords, atom_types, lengths, angles, num_atoms): |
| """ |
| args: |
| frac_coords: (num_atoms, 3) |
| atom_types: (num_atoms) |
| lengths: (num_crystals) |
| angles: (num_crystals) |
| num_atoms: (num_crystals) |
| """ |
| assert frac_coords.size(0) == atom_types.size(0) == num_atoms.sum() |
| assert lengths.size(0) == angles.size(0) == num_atoms.size(0) |
|
|
| start_idx = 0 |
| crystal_array_list = [] |
| for batch_idx, num_atom in enumerate(num_atoms.tolist()): |
| cur_frac_coords = frac_coords.narrow(0, start_idx, num_atom) |
| cur_atom_types = atom_types.narrow(0, start_idx, num_atom) |
| cur_lengths = lengths[batch_idx] |
| cur_angles = angles[batch_idx] |
|
|
| crystal_array_list.append({ |
| 'frac_coords': cur_frac_coords.detach().cpu().numpy(), |
| 'atom_types': cur_atom_types.detach().cpu().numpy(), |
| 'lengths': cur_lengths.detach().cpu().numpy(), |
| 'angles': cur_angles.detach().cpu().numpy(), |
| }) |
| start_idx = start_idx + num_atom |
| return crystal_array_list |
|
|
|
|
| def smact_validity(comp, count, |
| use_pauling_test=True, |
| include_alloys=True): |
| elem_symbols = tuple([chemical_symbols[elem] for elem in comp]) |
| space = smact.element_dictionary(elem_symbols) |
| smact_elems = [e[1] for e in space.items()] |
| electronegs = [e.pauling_eneg for e in smact_elems] |
| ox_combos = [e.oxidation_states for e in smact_elems] |
| if len(set(elem_symbols)) == 1: |
| return True |
| if include_alloys: |
| is_metal_list = [elem_s in smact.metals for elem_s in elem_symbols] |
| if all(is_metal_list): |
| return True |
|
|
| threshold = np.max(count) |
| compositions = [] |
| for ox_states in itertools.product(*ox_combos): |
| stoichs = [(c,) for c in count] |
| |
| cn_e, cn_r = smact.neutral_ratios( |
| ox_states, stoichs=stoichs, threshold=threshold) |
| |
| if cn_e: |
| if use_pauling_test: |
| try: |
| electroneg_OK = pauling_test(ox_states, electronegs) |
| except TypeError: |
| |
| electroneg_OK = True |
| else: |
| electroneg_OK = True |
| if electroneg_OK: |
| for ratio in cn_r: |
| compositions.append( |
| tuple([elem_symbols, ox_states, ratio])) |
| compositions = [(i[0], i[2]) for i in compositions] |
| compositions = list(set(compositions)) |
| if len(compositions) > 0: |
| return True |
| else: |
| return False |
|
|
|
|
| def structure_validity(crystal, cutoff=0.5): |
| dist_mat = crystal.distance_matrix |
| |
| dist_mat = dist_mat + np.diag( |
| np.ones(dist_mat.shape[0]) * (cutoff + 10.)) |
| if dist_mat.min() < cutoff or crystal.volume < 0.1: |
| return False |
| else: |
| return True |
|
|
|
|
| def get_fp_pdist(fp_array): |
| if isinstance(fp_array, list): |
| fp_array = np.array(fp_array) |
| fp_pdists = pdist(fp_array) |
| return fp_pdists.mean() |
|
|
|
|
| def prop_model_eval(eval_model_name, crystal_array_list): |
|
|
| model_path = get_model_path(eval_model_name) |
|
|
| model, _, _ = load_model(model_path) |
| cfg = load_config(model_path) |
|
|
| dataset = TensorCrystDataset( |
| crystal_array_list, cfg.data.niggli, cfg.data.primitive, |
| cfg.data.graph_method, cfg.data.preprocess_workers, |
| cfg.data.lattice_scale_method) |
|
|
| |
|
|
| loader = DataLoader( |
| dataset, |
| shuffle=False, |
| batch_size=256, |
| num_workers=0, |
| worker_init_fn=worker_init_fn) |
|
|
| model.eval() |
| model = model.cuda() |
|
|
| all_preds = [] |
|
|
| for batch in loader: |
| preds = model(batch.cuda()) |
| |
| |
| scaled_preds = preds |
| all_preds.append(scaled_preds.detach().cpu().numpy()) |
|
|
| all_preds = np.concatenate(all_preds, axis=0).squeeze(1) |
| return all_preds.tolist() |
|
|
|
|
| def filter_fps(struc_fps, comp_fps): |
| assert len(struc_fps) == len(comp_fps) |
|
|
| filtered_struc_fps, filtered_comp_fps = [], [] |
|
|
| for struc_fp, comp_fp in zip(struc_fps, comp_fps): |
| if struc_fp is not None and comp_fp is not None: |
| filtered_struc_fps.append(struc_fp) |
| filtered_comp_fps.append(comp_fp) |
| return filtered_struc_fps, filtered_comp_fps |
|
|
|
|
| def compute_cov(crys, gt_crys, |
| struc_cutoff, comp_cutoff, num_gen_crystals=None): |
| struc_fps = [c.struct_fp for c in crys] |
| comp_fps = [c.comp_fp for c in crys] |
| gt_struc_fps = [c.struct_fp for c in gt_crys] |
| gt_comp_fps = [c.comp_fp for c in gt_crys] |
|
|
| assert len(struc_fps) == len(comp_fps) |
| assert len(gt_struc_fps) == len(gt_comp_fps) |
|
|
| |
| if num_gen_crystals is None: |
| num_gen_crystals = len(struc_fps) |
|
|
| struc_fps, comp_fps = filter_fps(struc_fps, comp_fps) |
|
|
| comp_fps = CompScaler.transform(comp_fps) |
| gt_comp_fps = CompScaler.transform(gt_comp_fps) |
|
|
| struc_fps = np.array(struc_fps) |
| gt_struc_fps = np.array(gt_struc_fps) |
| comp_fps = np.array(comp_fps) |
| gt_comp_fps = np.array(gt_comp_fps) |
|
|
| struc_pdist = cdist(struc_fps, gt_struc_fps) |
| comp_pdist = cdist(comp_fps, gt_comp_fps) |
|
|
| struc_recall_dist = struc_pdist.min(axis=0) |
| struc_precision_dist = struc_pdist.min(axis=1) |
| comp_recall_dist = comp_pdist.min(axis=0) |
| comp_precision_dist = comp_pdist.min(axis=1) |
|
|
| cov_recall = np.mean(np.logical_and( |
| struc_recall_dist <= struc_cutoff, |
| comp_recall_dist <= comp_cutoff)) |
| cov_precision = np.sum(np.logical_and( |
| struc_precision_dist <= struc_cutoff, |
| comp_precision_dist <= comp_cutoff)) / num_gen_crystals |
|
|
| metrics_dict = { |
| 'cov_recall': cov_recall, |
| 'cov_precision': cov_precision, |
| 'amsd_recall': np.mean(struc_recall_dist), |
| 'amsd_precision': np.mean(struc_precision_dist), |
| 'amcd_recall': np.mean(comp_recall_dist), |
| 'amcd_precision': np.mean(comp_precision_dist), |
| } |
|
|
| combined_dist_dict = { |
| 'struc_recall_dist': struc_recall_dist.tolist(), |
| 'struc_precision_dist': struc_precision_dist.tolist(), |
| 'comp_recall_dist': comp_recall_dist.tolist(), |
| 'comp_precision_dist': comp_precision_dist.tolist(), |
| } |
|
|
| return metrics_dict, combined_dist_dict |
|
|