diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..400824b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +api_key.txt +boxylmer/tests/__pycache__/* +boxylmer/tests/test_output/* +boxylmer/MaterialPropertyPredictor/__pycache__/* + diff --git a/Flowchart-Designer.md b/Flowchart-Designer.md index 76a923d0..12d69e62 100644 --- a/Flowchart-Designer.md +++ b/Flowchart-Designer.md @@ -35,4 +35,10 @@ We leave exact timing to the candidate. Must fit Within 5 days total. # Examples -To view examples or discuss this task more in details, please [contact us](README.md). +We recommend reviewing the following: + +- Introduction to Flowcharts: https://www.geeksforgeeks.org/an-introduction-to-flowcharts/ +- An example convergence workflow: https://docs.mat3ra.com/models/auxiliary-concepts/reciprocal-space/convergence/ +- ReactFlow library examples: https://reactflow.dev/docs/examples/overview/ and https://pro.reactflow.dev/pro-examples + +To discuss this task more in details, please [contact us](README.md). diff --git a/Materials-Designer.md b/Materials-Designer.md index 611f7904..3af1c5d8 100644 --- a/Materials-Designer.md +++ b/Materials-Designer.md @@ -4,9 +4,9 @@ # Overview -Create a skeleton IDE (integrated development environment) for materials design. Close to Adobe Dreamweaver (or any other IDE) - when you can change html-markup and simultaneously see the result in another tab. We edit material in XYZ format and view result in 3D. +Create a skeleton IDE (integrated development environment) for materials design, where one can change text and simultaneously see the visual result in another tab. We edit material in XYZ format and view result in 3D. -Front-end developers: use Meteor and React.js and minimalistic UX/UI. +Front-end developers: use React.js and minimalistic UX/UI. Pure UI/UX designers: create high fidelity mockups.  diff --git a/README.md b/README.md index 414438d3..6c154cfb 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Each file represents an assignment similar to what one would get when hired. | Front-End / UX | [Materials Designer](Materials-Designer.md) | ReactJS / UX Design, ThreeJS | | Front-End / UX | [Flowchart Designer](Flowchart-Designer.md) | ReactJS / UX Design, DAG | | Back-End / Ops | [Parallel Uploader](Parallel-File-Uploader.md) | Python, OOD, Threading, Objectstore | -| CI/CD, DevOps | [End-to-End Tests](End-To-End-Tests.md) | BDD tests, CI/CD workflows, Cypress | +| CI/CD, DevOps | [End-to-End Tests](End-to-End-Tests.md) | BDD tests, CI/CD workflows, Cypress | | HPC, Cloud Inf | [Cloud HPC Bench.](Cloud-Infrastructure.md) | HPC Cluster, Linpack, Benchmarks | | HPC, Containers| [Containerized HPC](Containerization-HPC.md) | HPC Cluster, Containers, Benchmarks | diff --git a/boxylmer/ML-Band-Gaps.md b/boxylmer/ML-Band-Gaps.md new file mode 100644 index 00000000..2ccb5adc --- /dev/null +++ b/boxylmer/ML-Band-Gaps.md @@ -0,0 +1,31 @@ +# ML Band Gaps (Materials) + +> Ideal candidate: skilled ML data scientist with solid knowledge of materials science. + +# Overview + +The aim of this task is to create a python package that implements automatic prediction of electronic band gaps for a set of materials based on training data. + +# User story + +As a user of this software I can predict the value of an electronic band gap after passing training data and structural information about the target material. + +# Requirements + +- suggest the bandgap values for a set of materials designated by their crystallographic and stoichiometric properties +- the code shall be written in a way that can facilitate easy addition of other characteristics extracted from simulations (forces, pressures, phonon frequencies etc) + +# Expectations + +- the code shall be able to suggest realistic values for slightly modified geometry sets - eg. trained on Si and Ge it should suggest the value of bandgap for Si49Ge51 to be between those of Si and Ge +- modular and object-oriented implementation +- commit early and often - at least once per 24 hours + +# Timeline + +We leave exact timing to the candidate. Must fit Within 5 days total. + +# Notes + +- use a designated github repository for version control +- suggested source of training data: materialsproject.org diff --git a/boxylmer/MaterialPropertyPredictor/MaterialDataLoader.py b/boxylmer/MaterialPropertyPredictor/MaterialDataLoader.py new file mode 100644 index 00000000..39c87773 --- /dev/null +++ b/boxylmer/MaterialPropertyPredictor/MaterialDataLoader.py @@ -0,0 +1,541 @@ +from mp_api.client import MPRester +from abc import ABC, abstractmethod +import numpy as np +from sklearn.model_selection import train_test_split +from pymatgen.core import Element + + +RSEED = 1996 + +class AbstractDataLoader(ABC): + test_size = 0.2 + def __init__(self): + """ + Abstract class for data loaders that fetch and preprocess data from various sources (really just MPRester right now). + + Defines an interface for data loaders, implementations should provide methods to load and preprocess + data (see below), get model inputs, get model outputs, etc. + + Required methods (more details in each abstract function) + ------- + load_data(*args, **kwargs): + Fetches the data from a specific source. + + _process_parsed_data(*args, **kwargs): + Processes the fetched data to generate features and labels. Must be called once load_data is complete. + + _validate(): + Validates that the data loaded and processed are consistent and ready for modeling. Must be called once _process_parsed_data is complete. + + get_model_inputs(): + Return the preprocessed features for the model using it. Caching may be necessary. + + get_model_outputs(): + Returns the expected outputs for the input data. + + In addition to this, _process_parsed_data must set self.n_features and self.n_samples. (see below) + """ + self.n_features = None + self.n_samples = None + + # + # Required abstractions + # + + @abstractmethod + def load_data(self): + """ + This method fetches materials data / parses it. Implementation arguments will change. + """ + pass + + @abstractmethod + def _process_parsed_data(self): + """ + This should be called at the end of load_data(). + Take the loading data and perform post-loading analysis and featurization. + Should prepare and cache data as needed. + + Returns: None + """ + + @abstractmethod + def _validate(self): + """ + This should be called at the end of _process_parsed_data(). + Determine if the resulting loaded data is valid and will not cause errors. + + Returns: None + """ + pass + + @abstractmethod + def calculate_max_input_size(self): + """Calculate the expected total size of the input feature vector for each sample.""" + pass + + @abstractmethod + def get_model_inputs(self, padding_value=0): + """ + Acquire the model input data as a procured ndarray of values in a standard (Sample, Feature) format. + Standard practice for this abstraction is to calculate the inputs and cache them in self.input_cache if the calculations are heavy. + + Parameters: + - padding_value (float, optional): Padding. Default is 0. + + Returns: + - padded_feature_vectors (np.array): Padded inputs of shape (Sample, Feature) + """ + pass + + @abstractmethod + def get_model_outputs(self): + """ + Acquire the model ouptut data as a procured single dimensional ndarray. + + Returns: + - outputs (np.array): Outputs of shape (Float32) + """ + pass + + + # + # General-ish helper functions used to featurize or make life easier for implemented dataloaders. + # + + @classmethod + def _calculate_structure_atom_distances_fast(cls, structure): + coords = np.array([site.coords for site in structure.sites]) + n = len(coords) + distances = np.zeros((n, n)) + + for i in range(n): + for j in range(i+1, n): + dist = np.linalg.norm(coords[i] - coords[j]) + distances[i, j] = dist + distances[j, i] = dist + + return distances + + @classmethod + def _calculate_structure_atom_distances_accurate(cls, structure): + n = len(structure) + distances = np.zeros((n, n)) + for i in range(n): + for j in range(i+1, n): + dist = structure.get_distance(i, j) + distances[i, j] = dist + distances[j, i] = dist + + return distances + + # coulomb matrix implemented from https://singroup.github.io/dscribe/0.3.x/tutorials/coulomb_matrix.html + @classmethod + def calculate_structure_coulomb_matrix(cls, structure, distance_method): + """ + Calculates the Coulomb matrix for a given atomic structure using either a 'fast' + or 'accurate' distance calculation method. + + The Coulomb matrix is a representation of an atomic structure, capturing the + pairwise Coulombic interactions between atoms in a matrix form. + Implementation is based on: https://singroup.github.io/dscribe/0.3.x/tutorials/coulomb_matrix.html + + Parameters: + structure: A pymatgen Structure object representing the atomic structure. + distance_method: The method for distance calculation between atoms. Supply 'fast' or 'accurate'. + - Fast will use the immediate cartesian distance of the structure, which will lose important + information about the periodic nature of the lattice. Use this for debugging. + - Accurate will use pymatgens internal distance calculation, but will take around 1000 times + longer to process when loading. Use this for final model calculations. + Returns: + np.ndarray: The Coulomb matrix of shape (n, n), where n is the number of atoms in the structure. + + Raises: + ValueError: If an invalid distance calculation method is provided. + """ + n = len(structure) + mat = np.zeros((n, n)) + z = np.array([atom.specie.Z for atom in structure]) # docs: https://pymatgen.org/pymatgen.core.html#pymatgen.core.periodic_table.Element + if distance_method == 'fast': + distances = AbstractDataLoader._calculate_structure_atom_distances_fast(structure) + elif distance_method == 'accurate': + distances = AbstractDataLoader._calculate_structure_atom_distances_accurate(structure) + else: + raise ValueError("Invalid distance calculation method. Use \'fast\' or \'accurate\'.") + + z_product = z[:, np.newaxis] * z[np.newaxis, :] + z_product = z_product.astype('float64') + mat = np.divide(z_product, distances, out=np.zeros_like(z_product), where=distances!=0) + + np.fill_diagonal(mat, 0.5 * z ** 2.4) # faster than using for loop directly (cProfile -> 0.181) + return mat + + @classmethod + def calculate_eigenvalues(cls, coulomb_matrix): + "Calculate the eigenvalues of a coulomb matrix." + eigenvalues, _ = np.linalg.eigh(coulomb_matrix) + sorted_evs = np.sort(eigenvalues)[::-1] + return sorted_evs + + @classmethod + def calculate_coordination_stats(cls, structure, radius): + """ + Calculate the average and standard deviation of + the coordination numbers in a given radius for all + of the atoms in a pymatgen structure object. + + Parameters: + structure (pymatgen.Structure): structure to analyze. + radius (float): The radius (in angstroms) to search for coordinating atoms. + + Returns: + tuple: (average, standard deviation) of coordination numbers. + + I have not been able to back this strategy from the literature yet: Eventually, we should search for better ways to encode variable-size -> constant-size structural info. + """ + total_coordination = 0 + coordination_sqr = 0 + + n = len(structure) + for site in structure: + neighbors = structure.get_neighbors(site, r=radius) + + coordination_number = len(neighbors) + total_coordination += coordination_number + coordination_sqr += coordination_number ** 2 + + avg_coordination = total_coordination / n + variance = (coordination_sqr / n) - (avg_coordination ** 2) + std_deviation = np.sqrt(variance) + + return avg_coordination, std_deviation + + @classmethod + def create_element_position_map(cls, structures): + """ + Given all structures in a dataset + + Parameters: + structures (list): A list of pymatgen Structures. + + Returns: + dict: A dictionary where (Element string -> integer position). + + Example: + >>> position_map = create_element_position_map([structure1, structure2]) + >>> print(position_map) + {'H': 0, 'O': 1, 'Fe': 2, ...} + + """ + unique_elements = set() + for structure in structures: + for site in structure: + e = site.specie.symbol + unique_elements.add(e) + + sorted_elements = sorted(list(unique_elements), key=lambda x: Element(x).Z) + element_position_map = {element: index for index, element in enumerate(sorted_elements)} + + return element_position_map + + @classmethod + def create_element_fraction_vector(cls, structure, element_position_map): + """ + Generate a vector whose indices are mapped by the element_position_map (element -> position). + + Parameters: + structure (pymatgen.Structure): The structure of the material. + position_map (dict): A dictionary mapping element strings to positions in the feature vector (see `create_element_position_map`). + + Returns: + np.array: ndarray holding the fractional occurrence of each element in the structure, reflecting the mapping. + + Example: + >>> # assume you have 20 of one atom, 80 of another, and nothing else. + >>> fractional_vector = create_fractional_occurrence_vector(structure, element_position_map) + >>> print(fractional_vector) + [0.2, 0.8, 0, ...] + """ + total_atoms = len(structure) + element_vector = np.zeros(len(element_position_map)) + + for site in structure: + e = site.specie.symbol + position = element_position_map[e] + element_vector[position] += 1 + + element_vector /= total_atoms + return element_vector + + @classmethod + def resize_eigenvals(cls, arr, new_length): + "Pad the eigenvalue vector with zeros to a total length." + current_length = len(arr) + if current_length > new_length: + return arr[:new_length] + elif current_length < new_length: + return np.pad(arr, (0, new_length - current_length), 'constant', constant_values=0) + else: + return arr + + # + # Directly implemented functions + # + + def get_train_data(self, train_size=1-test_size, seed=RSEED): + """ + Get data used in training the model. + + Parameters: + train_size (float): Amount of the dataset to include in the training split. Ranges from 0 to 1. + Defaults to 1 - test_size. (defined in this class) + seed (int): Random seed used for reproducibility. Defaults to RSEED, a constant defined in this module. + + + Returns: + tuple: (train_x, train_y) where train_x is the feature matrix (samples, features) for training and + train_y is the target band gaps for training. + """ + # these data sets are typically small, calling the function like this is not likely to bottleneck + inputs = self.get_model_inputs() + outputs = self.get_model_outputs() + train_x, _, train_y, _ = train_test_split(inputs, outputs, train_size=train_size, random_state=seed) + return train_x, train_y + + def get_test_data(self, test_size=test_size, seed=RSEED): + """ + Get data used in testing the model. + + Parameters: + test_size (float): Amount of the dataset to include in the testing split. Ranges from 0 to 1. + Defaults to test_size. (defined in this class) + seed (int): Random seed used for reproducibility. Defaults to RSEED, a constant defined in this module. + + Returns: + tuple: (train_x, train_y) where train_x is the feature matrix (samples, features) for training and + train_y is the target band gaps for training. + """ + inputs = self.get_model_inputs() + outputs = self.get_model_outputs() + _, test_x, _, test_y = train_test_split(inputs, outputs, test_size=test_size, random_state=seed) + return test_x, test_y + + def get_input_length(self): + return self.n_features + + def __len__(self): + return len(self.structures) + +class MPRLoader(AbstractDataLoader): + def __init__(self, n_eigenvals=None, **kwargs): + """ + Initialize the MPRLoader class for loading and pre-processing materials data. + + Parameters: + n_eigenvals (int, optional): The number of eigenvalues to consider when calculating the + Coulomb Matrix Eigenvalues. If None, all eigenvalues are used. + **kwargs: Additional keyword arguments to be passed to the superclass constructor. + Attributes contain all data, data is randomized with the same seed (by default) at every call. + Thus, accessing attributes directly for the purpose of testing or model validation is unadvised. + Attributes: + structures (list): Pymatgen Structure objects from loaded data. + lattice_vectors (ndarray): Lattice vectors of the pymatgen Structures. + lattice_angles (ndarray): Lattice angles of the pymatgen Structures. + volumes (ndarray): Lattice cell volumes. + densities (ndarray): Mass densities of the materials. + atomic_densities (ndarray): Atom densities of the materials. + is_metal (ndarray): Whether the material is metallic (1-> True, 0 -> False). + natoms (ndarray): Number of atoms in each sample. + energies_per_atom (ndarray): Energy per atom for the cell per sample. + coulomb_matrices (list): Calculated coulomb matrices for each sample. + coulomb_matrix_eigenvalues (list): Solved eigenvalues for each sample's coulomb matrix, in descending order. + n_eigenvals (int, optional): The number of eigenvalues to be considered. + None (default) -> Pad zeros up to the largest sample in the dataset and use this many values in the feature vector for eigenvalues. + input_cache (ndarray): Cached model inputs. + """ + + self.structures = None + # cached data in self.structures. It's possible to lump this all into a dict or just to use it on the fly, + # but for now, explicitly referencing them will assist when we're unsure what the best descriptors are for the target property. + self.lattice_vectors = None + self.lattice_angles = None + self.volumes = None + self.densities = None + self.atomic_densities = None + self.is_metal = None + self.natoms = None + self.energies_per_atom = None + # hmm... https://journals.aps.org/prb/abstract/10.1103/PhysRevB.89.205118 + # suggests there are better ways to represent a crystal lattice than coulomb matrices. + # This could be a good idea to explore in the future for more complex properties + # This paper ALSO tells me that coulomb matrices are a previous industry standard, + # So it stands to reason there should be some libraries to speed this up out there. + # For now, though, this method is slow when doing lattice-consistent distances and needs optimization. + self.coulomb_matrices = None + + # https://par.nsf.gov/servlets/purl/10187380#:~:text=Coulomb%20Matrix%20Eigenvalues%20(CME)%20are,searches%2C%20and%20interpret%20rotational%20spectra. + # Very good primer on why the sorted eigenvalues are both useful and computationally efficient. + # Still not as good as a graph representation, but this is much faster to implement and I still have papers to grade :(. + self.coulomb_matrix_eigenvalues = None + self.n_eigenvals = n_eigenvals + + self.input_cache = None # will be filled when get_model_inputs is called. + super().__init__(**kwargs) + + def load_data(self, api_key, distance_method='fast', **kwargs): + """ + Implements load_data from AbstractDataLoader. + + Parameters: + api_key (str): The API key for accessing MPR. + distance_method (str, optional): The method used for distance calculations. Defaults to 'fast'. + Using 'accurate' instead will cause a progress meter to be printed. Expect very slow parse times for datasets > 500 items in size. + **kwargs: Search criteria in MPR (Forwarded to MPRester). + + Returns: None + """ + with MPRester(api_key) as mpr: + data = mpr.materials.summary.search( + fields=[ + "material_id", + "band_gap", + "formula_pretty", + "volume", + "density", + "density_atomic", + "structure", + "is_metal", + "energy_per_atom"], + **kwargs + ) + + self.structures = [d.structure for d in data] + self.lattice_vectors = np.array([(s.lattice.a, s.lattice.b, s.lattice.c) for s in self.structures], dtype=np.float32) + self.lattice_angles = np.array([s.lattice.angles for s in self.structures], dtype=np.float32) + self.volumes = np.array([s.volume for s in self.structures], dtype=np.float32) + self.densities = np.array([d.density for d in data], dtype=np.float32) + self.atomic_densities = np.array([d.density_atomic for d in data], dtype=np.float32) + self.is_metal = np.array([d.is_metal for d in data], dtype=np.float32) # implicit bool -> float for ndarray, I think + self.natoms = np.array([len(s) for s in self.structures], dtype=np.float32) + self.energies_per_atom = np.array([d.energy_per_atom for d in data], dtype=np.float32) + + + self.formulas = [d.formula_pretty for d in data] + + + self.band_gaps = np.array([d.band_gap for d in data], dtype=np.float32) + + + # when pulling directly from MPR, extra data doesn't make sense as you don't know how much data will be found ahead of time + # thus for this concrete class, extras are moot (but we could fill them out later, or just lump all our parsed data into a dict) + self.extra_data = {} + + self._process_parsed_data(distance_method=distance_method) + + return None + + def _process_parsed_data(self, distance_method): + """Implementation for AbstractDataLoader._process_parsed_data. See aforementioned function and MPRloader.load_data.""" + if distance_method == 'accurate': + print("Distance method is accurate rather than fast, this may take some time.") + self.coulomb_matrices = [] + total_iterations = len(self.structures) + percent_complete = 0 + for i, s in enumerate(self.structures): + if distance_method == 'accurate': + new_percent_complete = (i * 100) // total_iterations + if new_percent_complete > percent_complete: + percent_complete = new_percent_complete + print(f"Coulomb Matrix Progress: {percent_complete}%", end='\r') + self.coulomb_matrices.append( + self.calculate_structure_coulomb_matrix(s, distance_method) + ) + if distance_method == 'accurate': + print("Coulomb Matrix Progress: Complete") + + self.coulomb_matrix_eigenvalues = [self.calculate_eigenvalues(cmat) for cmat in self.coulomb_matrices] + self.elemental_composition_map = self.create_element_position_map(self.structures) + self.elemental_fraction_vectors = [self.create_element_fraction_vector(s, self.elemental_composition_map) for s in self.structures] + + self.n_features = self.calculate_max_input_size() + self.n_samples = len(self.structures) + self._validate() + + def _validate(self): + ndata = self.n_samples + for key in self.extra_data: + assert len(self.extra_data[key] == ndata) + # given more constraints later on, this may be expanded if the types of input data structures widen + + def get_model_inputs(self, padding_value=0): + """See AbstractDataLoader.get_model_inputs""" + if self.input_cache is None: + num_structures = len(self.structures) + padded_feature_vectors = np.full((num_structures, self.n_features), padding_value, dtype=np.float32) + + for i in range(num_structures): + + if self.n_eigenvals is None: + coulomb_matrix_eigenvalues = self.coulomb_matrix_eigenvalues[i] + else: + coulomb_matrix_eigenvalues = self.resize_eigenvals(self.coulomb_matrix_eigenvalues[i], self.n_eigenvals) + + + feature_vector = np.hstack( + ( + self.volumes[i], + self.lattice_vectors[i], + self.lattice_angles[i], + self.densities[i], + self.atomic_densities[i], + self.is_metal[i], + self.natoms[i], + self.energies_per_atom[i], + self.calculate_coordination_stats(self.structures[i], 3), + self.calculate_coordination_stats(self.structures[i], 2), + self.calculate_coordination_stats(self.structures[i], 1), + self.elemental_fraction_vectors[i], + coulomb_matrix_eigenvalues, + ), + dtype=np.float32 + ) + + padding_size = self.n_features - len(feature_vector) + + if padding_size >= 0: + padded_feature_vectors[i, :len(feature_vector)] = feature_vector + else: + raise ValueError("Input feature vector size exceeded the available input size.") + self.input_cache = padded_feature_vectors + + return self.input_cache + + def get_model_outputs(self): + """See AbstractDataLoader.get_model_outputs.""" + return self.band_gaps + + def calculate_max_input_size(self): + # later we may modify these descriptors, so lets tabulate them. We also have extras! + vol = 1 + abc = 3 + angles = 3 + density = 1 + atomic_density = 1 + is_metal = 1 + natoms = 1 + energy_per_atom = 1 + coordination_stats = 2 * 3 + if self.n_eigenvals is None: + eigenvals_len = max([len(s) for s in self.structures]) + else: + eigenvals_len = self.n_eigenvals + elemental_composition_length = len(self.elemental_composition_map) + + extras_len = len(self.extra_data) + + + n = vol + abc + angles + density + atomic_density + is_metal + natoms + energy_per_atom + coordination_stats + elemental_composition_length + eigenvals_len + extras_len + + return n + + diff --git a/boxylmer/MaterialPropertyPredictor/PredictorModel.py b/boxylmer/MaterialPropertyPredictor/PredictorModel.py new file mode 100644 index 00000000..ecba193d --- /dev/null +++ b/boxylmer/MaterialPropertyPredictor/PredictorModel.py @@ -0,0 +1,189 @@ +from abc import ABC, abstractmethod +import numpy as np +from sklearn.ensemble import RandomForestRegressor +from sklearn.ensemble import GradientBoostingRegressor +from sklearn.model_selection import RandomizedSearchCV + + +class AbstractBandGapModel(ABC): + + def __init__(self): + """ + Abstract band gap prediction model. + + Defines the interface and some common functionality for Band Gap prediction. + + Accepts a generic MaterialDataLoader, which will predict using however many features that data loader provides. + """ + self.model = None + pass + + @abstractmethod + def fit(self, material_data_loader): + """ + Fit the model to the training data in the data loader. + Optionally, a printed progress bar may be supplied. + + Args: + material_data_loader (AbstractDataLoader): Data loader object containing training and testing data. + """ + pass + + def predict(self, material_data_loader, test_data_only=True): + """ + Predict band gap based on existing test data within the loader. Optionally predict using ALL data if test_data_only is false. + - By default, a seed is used, so data should be the same at every call. + + Args: + material_data_loader (AbstractDataLoader): Data loader object containing material data. + test_data_only (bool): If True (default), use only test data for predictions. + + Returns: + numpy.ndarray: Predicted band gap values. + """ + if test_data_only: + x, _ = material_data_loader.get_test_data() + else: + x = material_data_loader.get_model_inputs() + y = self._predict(x) + return y + + @abstractmethod + def _predict(self, x): + pass + + def parity(self, material_data_loader, test_data_only=True): + """ + Get predicted and actual band gap values for the data loader's test data or all data. + + Args: + material_data_loader (AbstractDataLoader): data loader object. + test_data_only (bool): If True, use only previously unseen test data. + + Returns: + tuple: (Actual band gap values, predicted band gap values) + """ + if test_data_only: + x, y = material_data_loader.get_test_data() + else: + x = material_data_loader.get_model_inputs() + y = material_data_loader.get_model_outputs() + + pred = self._predict(x) + + return y, pred + + @abstractmethod + def fit_hyperparameters(self, material_data_loader): + """ + Fit hyperparameters of the model using training data, then train the model on the optimized hyperparameters, which are also printed to console. + + Args: + material_data_loader (AbstractDataLoader): Data loader object. + + Returns: + None + """ + pass + +class RandomForestBandGapModel(AbstractBandGapModel): + """Predict band gap using random forest regression.""" + def __init__(self, **kwargs): + super().__init__() + self.model = RandomForestRegressor( + n_estimators=160, + min_samples_split=4, + min_samples_leaf=1, + max_depth=None, + bootstrap=True, + **kwargs + ) + + + def fit(self, material_data_loader): + train_x, train_y = material_data_loader.get_train_data() + self.model.fit(train_x, train_y) + return None + + def _predict(self, x): + return self.model.predict(x) + + + def fit_hyperparameters(self, material_data_loader, n_iter=50): + param_dist = { + 'n_estimators': np.arange(10, 200, 10), + 'max_depth': [None] + list(np.arange(5, 50, 5)), + 'min_samples_split': np.arange(2, 10, 1), + 'min_samples_leaf': np.arange(1, 10, 1), + 'bootstrap': [True, False] + } + + random_search = RandomizedSearchCV( + estimator=self.model, + param_distributions=param_dist, + n_iter=n_iter, + scoring='neg_mean_squared_error', # really annoying convention, I'm going to forget this later and be upset when I do it again and it moves **away** from local minima + verbose=1, + n_jobs=-1 + ) + + train_x, train_y = material_data_loader.get_train_data() + + random_search.fit(train_x, train_y) # very confused initially but the docs say it does a validation split internally, so we're OK + + self.model = random_search.best_estimator_ + + print(f"Best hyperparameters found: {random_search.best_params_} at RMSE = {-random_search.best_score_}") + +# if this performs well, we would want to replace it with the more performant XGBoost model, +# which is fundamentally the same but can be parallelized easily and may even have better optimization methods +# more info: https://stats.stackexchange.com/questions/282459/xgboost-vs-python-sklearn-gradient-boosted-trees +class GradientBoostingBandGapModel(AbstractBandGapModel): + """Predict band gap using gradient boosted trees.""" + def __init__(self, **kwargs): + super().__init__() + self.model = GradientBoostingRegressor( + n_estimators=180, + min_samples_split=3, + min_samples_leaf=8, + max_depth=3, + learning_rate=0.1, + **kwargs, + ) + + + def fit(self, material_data_loader): + train_x, train_y = material_data_loader.get_train_data() + self.model.fit(train_x, train_y) + return None + + def _predict(self, x): + return self.model.predict(x) + + def fit_hyperparameters(self, material_data_loader, n_iter=50): + param_dist = { + 'n_estimators': np.arange(10, 200, 10), + 'max_depth': [None] + list(np.arange(3, 16, 1)), + 'min_samples_split': np.arange(2, 10, 1), + 'min_samples_leaf': np.arange(1, 10, 1), + 'learning_rate': [0.01, 0.05, 0.1, 0.2, 0.5] + } + + random_search = RandomizedSearchCV( + estimator=self.model, + param_distributions=param_dist, + n_iter=n_iter, + scoring='neg_mean_squared_error', + verbose=1, + n_jobs=-1 + ) + + train_x, train_y = material_data_loader.get_train_data() + + random_search.fit(train_x, train_y) + + self.model = random_search.best_estimator_ + + print(f"Best hyperparameters found: {random_search.best_params_} at RMSE = {-random_search.best_score_}") + + return None \ No newline at end of file diff --git a/boxylmer/MaterialPropertyPredictor/__init__.py b/boxylmer/MaterialPropertyPredictor/__init__.py new file mode 100644 index 00000000..feca4276 --- /dev/null +++ b/boxylmer/MaterialPropertyPredictor/__init__.py @@ -0,0 +1,3 @@ +from .MaterialDataLoader import MPRLoader +from .PredictorModel import RandomForestBandGapModel +from .PredictorModel import GradientBoostingBandGapModel diff --git a/boxylmer/README.md b/boxylmer/README.md new file mode 100644 index 00000000..9dc22f3c --- /dev/null +++ b/boxylmer/README.md @@ -0,0 +1,33 @@ +## Project results +I would say this project was moderately successful given the constraints of a 5 day time limit and the concurrent management of graduate obligations. I had a lot of fun working on this and learned a bit along the way too! + +Two tree based models, that is, random forest and gradient boosting, were chosen due to their ease-of-implementation in scikit-learn and their reputation as good initial-choice models. Additionally, these models facilitated the 5 day time constraint as they are tolerant to lack of feature regularization. A graph convolution approach would likely be far superior in performance, however, a number of caveats to these kinds of models would have put this project out of a 5 day timeline: +- Regularization of features is much more important, requiring additional time to test and validate. +- Training can be excessively slow, requiring either a proxy model or ample resources. +- Predictions may require much larger datasets to generalize, and memorization is a larger concern that would have risked losing time to. +- There are *far* more hyperparameters to worry about. +- My previous work regarding graph-based approaches were implemented in Julia, not Python, and there would have been about a day or so lost to finding the relevant libraries (which I later found were NetworkX, DGL, Pytorch Geometric, and some others anyway) and learning how to use them appropriately. + +The resulting RMSE when using datasets with a smaller (3-9) number of atoms (which is a limitation of the featurization choices used in the data preparation) ranges from 0.25 to 0.3, which I'm happy with considering SOTA literature models report around 0.14 to 0.2 as [the current best (2018)](https://pubs.acs.org/doi/10.1021/acs.chemmater.8b00686). + +see the [plan, notes, and progress](./plan,%20notes,%20and%20progress.md) for a more detailed log of what I struggled with, found success in, and what I would pursue given more time. + + + + +# Running this package + +## Setup +No GPU-acceleration is used. Simply install requirements.txt + +An API key is needed to use MPRester. The demo and tests will search for the key in a file called "api_key.txt" in the root git folder (i.e., alongside `./boxylmer`). Before running, create that file and put the key in there. + +## Running tests +A very minimal testfile is included using Pytest that should indicate if the environment is set up correctly and basic functionality is working, but a complete set of unit tests is not yet implemented. + +Navigate to ./rewotes and run `./rewotes> pytest`. Output will be sent to `./test_output` + + +## Running the Demo +The demo demonstrates usage and hyperparameter tuning (which is commented out by default) of two proof-of-concept models. +By default, the demo assumes you're running Python from the git folder root, where the api_key.txt is stored. To run the demo, navigate to the root (asummed to be "rewotes") and run `./rewotes> python .\boxylmer\demo\demo.py`. An importance matrix for the random forest model will be printed. \ No newline at end of file diff --git a/boxylmer/demo/.ipynb_checkpoints/demo-checkpoint.ipynb b/boxylmer/demo/.ipynb_checkpoints/demo-checkpoint.ipynb new file mode 100644 index 00000000..9e03058f --- /dev/null +++ b/boxylmer/demo/.ipynb_checkpoints/demo-checkpoint.ipynb @@ -0,0 +1,251 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "15f8503c-30b6-4746-b172-4de55d448c23", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\git\\rewotes\\.venv\\lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "import sys\n", + "import os\n", + "demo_dir = os.path.abspath(\".\")\n", + "parent_dir = os.path.dirname(demo_dir) # feels very hacky just as a way to avoid pip install -e. \n", + "sys.path.append(parent_dir)\n", + "\n", + "from MaterialPropertyPredictor import MPRLoader, RandomForestBandGapModel, GradientBoostingBandGapModel\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from sklearn import metrics" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "4ddffb51-b7b7-4b62-bee9-d5fdf3710912", + "metadata": {}, + "outputs": [], + "source": [ + "def generate_parity_plot(y, pred, title):\n", + " rmse = metrics.mean_absolute_error(y, pred) \n", + " rmse = round(rmse, 2)\n", + " plt.figure(figsize=(6, 6)) \n", + " plt.plot(y, pred, 'ro', markersize=8, markerfacecolor='red')\n", + " max_value = max(max(y), max(pred))\n", + " plt.plot([0, max_value], [0, max_value], 'k-', lw=2)\n", + "\n", + " plt.xlabel('y')\n", + " plt.ylabel('pred')\n", + " plt.xlim(0, max_value)\n", + " plt.ylim(0, max_value)\n", + " plt.gca().set_aspect('equal', adjustable='box') \n", + " plt.box(True)\n", + " plt.grid(False) \n", + " plt.title(title + \" RMSE: \" + str(rmse))\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6d46626a-41cf-4dd6-be74-c3d9344f4319", + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "API key: yKDyZrX3LGe09wvygtdnQwTnNwrF36JR\n" + ] + } + ], + "source": [ + "api_key = input(\"API key: \")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "5c91c377-5907-4544-9dda-fa9428368217", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\git\\rewotes\\.venv\\lib\\site-packages\\mp_api\\client\\mprester.py:193: UserWarning: mpcontribs-client not installed. Install the package to query MPContribs data, or construct pourbaix diagrams: 'pip install mpcontribs-client'\n", + " warnings.warn(\n", + "Retrieving SummaryDoc documents: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 610/610 [00:00" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "y, pred = randomforest_model.parity(loader, test_data_only=False)\n", + "generate_parity_plot(y, pred, \"Random Forest Training\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2b016204-cd8c-4872-ad6a-575b4ce82233", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAggAAAIjCAYAAABml+OWAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/OQEPoAAAACXBIWXMAAA9hAAAPYQGoP6dpAABRMklEQVR4nO3de3yT5f3/8XdCoaVAW1pAYBw8oQhMUQ7qQIdfUGRYKIPOCQyKzilSQZ3fbfzcTzxMYe63Decc6nAIKNMVaKkHBmwquimeEJWhIggDKSqUHqDQCs39++MmoWmS5tCk953k9Xw8+gi5eyW5elebd677uj6XwzAMQwAAAA04re4AAACwHwICAADwQUAAAAA+CAgAAMAHAQEAAPggIAAAAB8EBAAA4IOAAAAAfBAQAACADwICklZBQYFOP/10r2MOh0P33HOPJf1JRJxPIH4RENDidu3apcLCQp1zzjlKT09Xenq6+vXrp1mzZunDDz+0unsxt2LFCi1cuDDk9qeffrocDofnKy0tTX369NH//u//6tChQ7HraIheeukl24WA3bt3e50zp9Op7OxsjRkzRm+++aZP+3vuucfTbu/evT7fr66uVtu2beVwOFRYWOj1vQMHDmjOnDnq27ev2rZtqy5dumjo0KH6+c9/riNHjnjaFRQUePWp8e80Ui6XSw899JDOOOMMpaWl6fzzz9df//rXkB772muvady4cerZs6fS0tLUtWtXXX311fr3v//d5OMqKyvVpUsXORwOrVy5MuK+w95SrO4AkssLL7yga6+9VikpKZoyZYouuOACOZ1OffLJJ1q9erUWLVqkXbt2qXfv3pb079ixY0pJie3/FitWrNDWrVt12223hfyYgQMH6qc//akkqba2Vu+9954WLlyojRs36u23345RT0Pz0ksv6dFHH/UbElrifDbluuuu0/e+9z3V19dr+/bt+tOf/qQrrrhC77zzjr797W/7tE9NTdVf//pX/exnP/M6vnr1ar/Pf+jQIQ0ePFjV1dW6/vrr1bdvX5WXl+vDDz/UokWLNHPmTLVv397r+RcvXuzzPK1atYr4Z7zrrru0YMEC3XjjjRoyZIjWrFmjyZMny+Fw6Ic//GGTj92+fbucTqduvvlmde3aVRUVFXr66ad1+eWX68UXX9TVV1/t93F33323jh49GnGfEScMoIXs2LHDaNeunXHeeecZZWVlPt8/fvy48fDDDxt79uxp8nmOHDkSlf5Mnz7d6N27d1SeKxxjx44N63V79+5tjB071uf4nXfeaUgytm/fHsXehW/WrFmG3f6U7Nq1y5Bk/OY3v/E6vnbtWkOSMXPmTK/j8+bNMyQZ3//+942BAwf6PN+VV15pTJw40ZBkzJo1y3P8oYceMiQZ//73v30eU1VVZRw7dsxzf/r06Ua7du2a+6N5+eKLL4zWrVt79cnlchmXXXaZ0aNHD+PEiRNhP2dNTY1x2mmnGaNHj/b7/Y8++shISUkx7rvvPkOSUVRUFHH/YW9cYkCLeeihh1RTU6MlS5aoW7duPt9PSUnR7Nmz1bNnT8+xgoICtW/fXjt37tT3vvc9dejQQVOmTJEkvf7668rPz1evXr2Umpqqnj176vbbb9exY8d8nrukpEQDBgxQWlqaBgwYoOLiYr999HfNfN++fbr++ut12mmnKTU1Vf3799df/vIXrzavvvqqHA6H/va3v+mBBx5Qjx49lJaWppEjR2rHjh2ediNGjNCLL76o//73v57h5cbzIELVtWtXSfL5hP7yyy/rsssuU7t27ZSVlaXx48fr448/9nn8+++/rzFjxigjI0Pt27fXyJEjtWnTJq82x48f17333qs+ffooLS1NOTk5Gj58uDZs2CDJ/P08+uijnnPn/nJrfD7dQ/k7duxQQUGBsrKylJmZqRkzZvh8Ij127Jhmz56tTp06qUOHDho3bpz27dvXrHkNl112mSRp586dfr8/efJkbdmyRZ988onn2JdffqmXX35ZkydP9mm/c+dOtWrVSpdcconP9zIyMiK+dLBz586AfWxozZo1On78uG655RbPMYfDoZkzZ+qLL77wezklmPT0dHXu3FmVlZV+vz9nzhxNmDDBcy6RuLjEgBbzwgsv6Oyzz9bFF18c1uNOnDih0aNHa/jw4fp//+//KT09XZJUVFSko0ePaubMmcrJydHbb7+tRx55RF988YWKioo8j1+/fr0mTpyofv36af78+SovL9eMGTPUo0ePoK/91Vdf6ZJLLvFce+7cubPWrl2rG264QdXV1T6XCRYsWCCn06k777xTVVVVeuihhzRlyhS99dZbkszh4KqqKn3xxRf6/e9/L0leQ9CBHD9+XAcPHpRkXmJ4//339bvf/U6XX365zjjjDE+7f/zjHxozZozOPPNM3XPPPTp27JgeeeQRDRs2TJs3b/aEkf/85z+67LLLlJGRoZ/97Gdq3bq1Hn/8cY0YMUIbN270/I7uuecezZ8/Xz/+8Y81dOhQVVdX691339XmzZt15ZVX6qabblJZWZk2bNig5cuXB/053H7wgx/ojDPO0Pz587V582YtXrxYXbp00a9//WtPm4KCAv3tb3/Tj370I11yySXauHGjxo4dG/Jr+LN7925JUseOHf1+//LLL1ePHj20YsUK3XfffZKk5557Tu3bt/f72r1791Z9fb2WL1+u6dOnh9QH9++xoTZt2igjI8Nzf+TIkV79DeT9999Xu3btdN5553kdHzp0qOf7w4cPD9qn6upqffPNNzp48KCWLVumrVu36v/8n//j066oqEhvvPGGPv7446B9QwKweggDyaGqqsqQZOTl5fl8r6Kiwjhw4IDn6+jRo57vTZ8+3ZBk/OIXv/B5XMN2bvPnzzccDofx3//+13Ns4MCBRrdu3YzKykrPsfXr1xuSfIb6JRnz5s3z3L/hhhuMbt26GQcPHvRq98Mf/tDIzMz09OGVV14xJBnnnXeeUVdX52n38MMPG5KMjz76yHMskksMkny+hg0b5tOvgQMHGl26dDHKy8s9xz744APD6XQa06ZN8xzLy8sz2rRpY+zcudNzrKyszOjQoYNx+eWXe45dcMEFfi9vNNTUJYbG59M9lH/99dd7tZswYYKRk5Pjuf/ee+8ZkozbbrvNq11BQYHPc/rjvsRw7733GgcOHDC+/PJL4/XXXzeGDBnid1jc3a8DBw4Yd955p3H22Wd7vjdkyBBjxowZnp+n4XD+l19+aXTu3NmQZPTt29e4+eabjRUrVnj9t+bm/m/Z31fj4fzevXuH9N/I2LFjjTPPPNPneE1NTcD/b/wZPXq0py9t2rQxbrrpJq/LI4Zh/v/Wq1cvY+7cuYZhnPpvnksMiYtLDGgR1dXVkvx/Wh4xYoQ6d+7s+XIPWTc0c+ZMn2Nt27b1/LumpkYHDx7Ud77zHRmGoffff1+StH//fm3ZskXTp09XZmamp/2VV16pfv36NdlnwzC0atUq5ebmyjAMHTx40PM1evRoVVVVafPmzV6PmTFjhtq0aeO57x6G/fzzz5t8rWAuvvhibdiwQRs2bNALL7ygBx54QP/5z380btw4zyUV989aUFCg7Oxsz2PPP/98XXnllXrppZckSfX19Vq/fr3y8vJ05plnetp169ZNkydP1r/+9S/P7ysrK0v/+c9/9NlnnzWr/43dfPPNXvcvu+wylZeXe17373//uyR5DZ1L0q233hrW68ybN0+dO3dW165dddlll+njjz/Wb3/7W02aNCngYyZPnqwdO3bonXfe8dz6u7wgSaeddpo++OAD3XzzzaqoqNBjjz2myZMnq0uXLrr//vtlGIZX+7S0NM/vseHXggULvNrt3r07pE/ox44dU2pqqs9x96UNf5fb/FmwYIHWr1+vJ598Updccom++eYbnThxwqfN8ePH/Y4sIDFxiQEtokOHDpLktezL7fHHH9fhw4f11VdfaerUqT7fT0lJ8Xs5YM+ePbr77rtVWlqqiooKr+9VVVVJkv773/9Kkvr06ePz+HPPPdfnDb6hAwcOqLKyUk888YSeeOIJv22+/vprr/u9evXyuu8eym7cv3B16tRJo0aN8twfO3aszj33XE2aNEmLFy/Wrbfe6vlZzz33XJ/Hn3feeVq3bp1qamp0+PBhHT16NGA7l8ulvXv3qn///rrvvvs0fvx4nXPOORowYICuvvpq/ehHP9L555/frJ+nqfOUkZGh//73v3I6nV6XTyTp7LPPDut1fvKTnyg/P1+1tbV6+eWX9Yc//EH19fVNPubCCy9U3759tWLFCmVlZalr1676n//5n4Dtu3XrpkWLFulPf/qTPvvsM61bt06//vWvdffdd6tbt2768Y9/7GnbqlUrr99jc7Vt21Z1dXU+x2traz3fD8XAgQM9/546daouuugiFRQUeJYw7t69W7/5zW/06KOPhnRJDImBgIAWkZmZqW7dumnr1q0+33Nf7w70iSk1NVVOp/dgV319va688kodOnRIP//5z9W3b1+1a9dO+/btU0FBgVwuV7P77H6OqVOnBry+3PiNMtBytcafJKPBfZ36tddeC/uTdaguv/xy7dy5U2vWrNH69eu1ePFi/f73v9djjz3m9cYXrpY6T3369PG8IV9zzTVq1aqVfvGLX+iKK67Q4MGDAz5u8uTJWrRokTp06KBrr73W578/fxwOh8455xydc845Gjt2rPr06aNnnnmmWecpmG7duumVV16RYRhek0P3798vSerevXvYz9mmTRuNGzdOCxYs0LFjx9S2bVvdfffd+ta3vqURI0Z4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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "gradientboosting_model = GradientBoostingBandGapModel()\n", + "gradientboosting_model.fit(loader)\n", + "y, pred = gradientboosting_model.parity(loader)\n", + "generate_parity_plot(y, pred, \"Gradient Boosting\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "bc4dff28-9994-4823-b593-268e64245fd2", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "y, pred = gradientboosting_model.parity(loader, test_data_only=False)\n", + "generate_parity_plot(y, pred, \"Gradient Boosting Training\")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "07b5b4c8-43f8-434f-8122-ea1db5a40a82", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "vol -> 0.2142503869933048\n", + "lattice vecs -> [0.16403965 0.22457721 0.14573862]\n", + "lattice angs -> [0.18249219 0.13755082 0.11528523]\n", + "mass density -> 0.8147220157386834\n", + "atom density -> 0.20131281279079555\n", + "is it metal? -> 2.8002421436647036\n", + "number atoms -> 0.09632179063843187\n", + "eng per atom -> 71.66258183324956\n", + "Coord 3Angst -> [0.23337498 0.48541303]\n", + "Coord 2Angst -> [16.82683622 0.42899729]\n", + "Coord 1Angst -> [0. 0.]\n", + "Compositions -> [1.15932754 1.47377265 1.31944836]\n", + "sum Eigenval -> 1.3137152237614458\n", + "Checksum 100 -> 100.00000000000001\n" + ] + } + ], + "source": [ + "fi = randomforest_model.model.feature_importances_\n", + "n = len(loader.elemental_fraction_vectors[1])\n", + "print(\"vol ->\", fi[0] * 100)\n", + "print(\"lattice vecs ->\", fi[1:4] * 100)\n", + "print(\"lattice angs ->\", fi[4:7] * 100)\n", + "print(\"mass density ->\", fi[7] * 100)\n", + "print(\"atom density ->\", fi[8] * 100)\n", + "print(\"is it metal? ->\", fi[9] * 100)\n", + "print(\"number atoms ->\", fi[10] * 100)\n", + "print(\"eng per atom ->\", fi[11] * 100)\n", + "print(\"Coord 3Angst ->\", fi[12:14] * 100)\n", + "print(\"Coord 2Angst ->\", fi[14:16] * 100)\n", + "print(\"Coord 1Angst ->\", fi[16:18] * 100)\n", + "print(\"Compositions ->\", fi[18:18+n] * 100)\n", + "print(\"sum Eigenval ->\", sum(fi[18+n:]) * 100)\n", + "print(\"Checksum 100 ->\", sum(fi * 100))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "rewote_venv", + "language": "python", + "name": "rewote_venv" + }, + "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.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/boxylmer/demo/demo output/gradient boosting parity - all data.png b/boxylmer/demo/demo output/gradient boosting parity - all data.png new file mode 100644 index 00000000..9555b23b Binary files /dev/null and b/boxylmer/demo/demo output/gradient boosting parity - all data.png differ diff --git a/boxylmer/demo/demo output/gradient boosting parity - tuned hyperparameters.png b/boxylmer/demo/demo output/gradient boosting parity - tuned hyperparameters.png new file mode 100644 index 00000000..f2d68c9d Binary files /dev/null and b/boxylmer/demo/demo output/gradient boosting parity - tuned hyperparameters.png differ diff --git a/boxylmer/demo/demo output/gradient boosting parity.png b/boxylmer/demo/demo output/gradient boosting parity.png new file mode 100644 index 00000000..68394114 Binary files /dev/null and b/boxylmer/demo/demo output/gradient boosting parity.png differ diff --git a/boxylmer/demo/demo output/random forest parity - all data.png b/boxylmer/demo/demo output/random forest parity - all data.png new file mode 100644 index 00000000..01711828 Binary files /dev/null and b/boxylmer/demo/demo output/random forest parity - all data.png differ diff --git a/boxylmer/demo/demo output/random forest parity - tuned hyperparameters.png b/boxylmer/demo/demo output/random forest parity - tuned hyperparameters.png new file mode 100644 index 00000000..7e508265 Binary files /dev/null and b/boxylmer/demo/demo output/random forest parity - tuned hyperparameters.png differ diff --git a/boxylmer/demo/demo output/random forest parity.png b/boxylmer/demo/demo output/random forest parity.png new file mode 100644 index 00000000..bc0dbcbb Binary files /dev/null and b/boxylmer/demo/demo output/random forest parity.png differ diff --git a/boxylmer/demo/demo.ipynb b/boxylmer/demo/demo.ipynb new file mode 100644 index 00000000..9e03058f --- /dev/null +++ b/boxylmer/demo/demo.ipynb @@ -0,0 +1,251 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "15f8503c-30b6-4746-b172-4de55d448c23", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\git\\rewotes\\.venv\\lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "import sys\n", + "import os\n", + "demo_dir = os.path.abspath(\".\")\n", + "parent_dir = os.path.dirname(demo_dir) # feels very hacky just as a way to avoid pip install -e. \n", + "sys.path.append(parent_dir)\n", + "\n", + "from MaterialPropertyPredictor import MPRLoader, RandomForestBandGapModel, GradientBoostingBandGapModel\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from sklearn import metrics" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "4ddffb51-b7b7-4b62-bee9-d5fdf3710912", + "metadata": {}, + "outputs": [], + "source": [ + "def generate_parity_plot(y, pred, title):\n", + " rmse = metrics.mean_absolute_error(y, pred) \n", + " rmse = round(rmse, 2)\n", + " plt.figure(figsize=(6, 6)) \n", + " plt.plot(y, pred, 'ro', markersize=8, markerfacecolor='red')\n", + " max_value = max(max(y), max(pred))\n", + " plt.plot([0, max_value], [0, max_value], 'k-', lw=2)\n", + "\n", + " plt.xlabel('y')\n", + " plt.ylabel('pred')\n", + " plt.xlim(0, max_value)\n", + " plt.ylim(0, max_value)\n", + " plt.gca().set_aspect('equal', adjustable='box') \n", + " plt.box(True)\n", + " plt.grid(False) \n", + " plt.title(title + \" RMSE: \" + str(rmse))\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6d46626a-41cf-4dd6-be74-c3d9344f4319", + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "API key: yKDyZrX3LGe09wvygtdnQwTnNwrF36JR\n" + ] + } + ], + "source": [ + "api_key = input(\"API key: \")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "5c91c377-5907-4544-9dda-fa9428368217", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\git\\rewotes\\.venv\\lib\\site-packages\\mp_api\\client\\mprester.py:193: UserWarning: mpcontribs-client not installed. Install the package to query MPContribs data, or construct pourbaix diagrams: 'pip install mpcontribs-client'\n", + " warnings.warn(\n", + "Retrieving SummaryDoc documents: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 610/610 [00:00" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "y, pred = randomforest_model.parity(loader, test_data_only=False)\n", + "generate_parity_plot(y, pred, \"Random Forest Training\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2b016204-cd8c-4872-ad6a-575b4ce82233", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAggAAAIjCAYAAABml+OWAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/OQEPoAAAACXBIWXMAAA9hAAAPYQGoP6dpAABRMklEQVR4nO3de3yT5f3/8XdCoaVAW1pAYBw8oQhMUQ7qQIdfUGRYKIPOCQyKzilSQZ3fbfzcTzxMYe63Decc6nAIKNMVaKkHBmwquimeEJWhIggDKSqUHqDQCs39++MmoWmS5tCk953k9Xw8+gi5eyW5elebd677uj6XwzAMQwAAAA04re4AAACwHwICAADwQUAAAAA+CAgAAMAHAQEAAPggIAAAAB8EBAAA4IOAAAAAfBAQAACADwICklZBQYFOP/10r2MOh0P33HOPJf1JRJxPIH4RENDidu3apcLCQp1zzjlKT09Xenq6+vXrp1mzZunDDz+0unsxt2LFCi1cuDDk9qeffrocDofnKy0tTX369NH//u//6tChQ7HraIheeukl24WA3bt3e50zp9Op7OxsjRkzRm+++aZP+3vuucfTbu/evT7fr66uVtu2beVwOFRYWOj1vQMHDmjOnDnq27ev2rZtqy5dumjo0KH6+c9/riNHjnjaFRQUePWp8e80Ui6XSw899JDOOOMMpaWl6fzzz9df//rXkB772muvady4cerZs6fS0tLUtWtXXX311fr3v//d5OMqKyvVpUsXORwOrVy5MuK+w95SrO4AkssLL7yga6+9VikpKZoyZYouuOACOZ1OffLJJ1q9erUWLVqkXbt2qXfv3pb079ixY0pJie3/FitWrNDWrVt12223hfyYgQMH6qc//akkqba2Vu+9954WLlyojRs36u23345RT0Pz0ksv6dFHH/UbElrifDbluuuu0/e+9z3V19dr+/bt+tOf/qQrrrhC77zzjr797W/7tE9NTdVf//pX/exnP/M6vnr1ar/Pf+jQIQ0ePFjV1dW6/vrr1bdvX5WXl+vDDz/UokWLNHPmTLVv397r+RcvXuzzPK1atYr4Z7zrrru0YMEC3XjjjRoyZIjWrFmjyZMny+Fw6Ic//GGTj92+fbucTqduvvlmde3aVRUVFXr66ad1+eWX68UXX9TVV1/t93F33323jh49GnGfEScMoIXs2LHDaNeunXHeeecZZWVlPt8/fvy48fDDDxt79uxp8nmOHDkSlf5Mnz7d6N27d1SeKxxjx44N63V79+5tjB071uf4nXfeaUgytm/fHsXehW/WrFmG3f6U7Nq1y5Bk/OY3v/E6vnbtWkOSMXPmTK/j8+bNMyQZ3//+942BAwf6PN+VV15pTJw40ZBkzJo1y3P8oYceMiQZ//73v30eU1VVZRw7dsxzf/r06Ua7du2a+6N5+eKLL4zWrVt79cnlchmXXXaZ0aNHD+PEiRNhP2dNTY1x2mmnGaNHj/b7/Y8++shISUkx7rvvPkOSUVRUFHH/YW9cYkCLeeihh1RTU6MlS5aoW7duPt9PSUnR7Nmz1bNnT8+xgoICtW/fXjt37tT3vvc9dejQQVOmTJEkvf7668rPz1evXr2Umpqqnj176vbbb9exY8d8nrukpEQDBgxQWlqaBgwYoOLiYr999HfNfN++fbr++ut12mmnKTU1Vf3799df/vIXrzavvvqqHA6H/va3v+mBBx5Qjx49lJaWppEjR2rHjh2ediNGjNCLL76o//73v57h5cbzIELVtWtXSfL5hP7yyy/rsssuU7t27ZSVlaXx48fr448/9nn8+++/rzFjxigjI0Pt27fXyJEjtWnTJq82x48f17333qs+ffooLS1NOTk5Gj58uDZs2CDJ/P08+uijnnPn/nJrfD7dQ/k7duxQQUGBsrKylJmZqRkzZvh8Ij127Jhmz56tTp06qUOHDho3bpz27dvXrHkNl112mSRp586dfr8/efJkbdmyRZ988onn2JdffqmXX35ZkydP9mm/c+dOtWrVSpdcconP9zIyMiK+dLBz586AfWxozZo1On78uG655RbPMYfDoZkzZ+qLL77wezklmPT0dHXu3FmVlZV+vz9nzhxNmDDBcy6RuLjEgBbzwgsv6Oyzz9bFF18c1uNOnDih0aNHa/jw4fp//+//KT09XZJUVFSko0ePaubMmcrJydHbb7+tRx55RF988YWKioo8j1+/fr0mTpyofv36af78+SovL9eMGTPUo0ePoK/91Vdf6ZJLLvFce+7cubPWrl2rG264QdXV1T6XCRYsWCCn06k777xTVVVVeuihhzRlyhS99dZbkszh4KqqKn3xxRf6/e9/L0leQ9CBHD9+XAcPHpRkXmJ4//339bvf/U6XX365zjjjDE+7f/zjHxozZozOPPNM3XPPPTp27JgeeeQRDRs2TJs3b/aEkf/85z+67LLLlJGRoZ/97Gdq3bq1Hn/8cY0YMUIbN270/I7uuecezZ8/Xz/+8Y81dOhQVVdX691339XmzZt15ZVX6qabblJZWZk2bNig5cuXB/053H7wgx/ojDPO0Pz587V582YtXrxYXbp00a9//WtPm4KCAv3tb3/Tj370I11yySXauHGjxo4dG/Jr+LN7925JUseOHf1+//LLL1ePHj20YsUK3XfffZKk5557Tu3bt/f72r1791Z9fb2WL1+u6dOnh9QH9++xoTZt2igjI8Nzf+TIkV79DeT9999Xu3btdN5553kdHzp0qOf7w4cPD9qn6upqffPNNzp48KCWLVumrVu36v/8n//j066oqEhvvPGGPv7446B9QwKweggDyaGqqsqQZOTl5fl8r6Kiwjhw4IDn6+jRo57vTZ8+3ZBk/OIXv/B5XMN2bvPnzzccDofx3//+13Ns4MCBRrdu3YzKykrPsfXr1xuSfIb6JRnz5s3z3L/hhhuMbt26GQcPHvRq98Mf/tDIzMz09OGVV14xJBnnnXeeUVdX52n38MMPG5KMjz76yHMskksMkny+hg0b5tOvgQMHGl26dDHKy8s9xz744APD6XQa06ZN8xzLy8sz2rRpY+zcudNzrKyszOjQoYNx+eWXe45dcMEFfi9vNNTUJYbG59M9lH/99dd7tZswYYKRk5Pjuf/ee+8ZkozbbrvNq11BQYHPc/rjvsRw7733GgcOHDC+/PJL4/XXXzeGDBnid1jc3a8DBw4Yd955p3H22Wd7vjdkyBBjxowZnp+n4XD+l19+aXTu3NmQZPTt29e4+eabjRUrVnj9t+bm/m/Z31fj4fzevXuH9N/I2LFjjTPPPNPneE1NTcD/b/wZPXq0py9t2rQxbrrpJq/LI4Zh/v/Wq1cvY+7cuYZhnPpvnksMiYtLDGgR1dXVkvx/Wh4xYoQ6d+7s+XIPWTc0c+ZMn2Nt27b1/LumpkYHDx7Ud77zHRmGoffff1+StH//fm3ZskXTp09XZmamp/2VV16pfv36NdlnwzC0atUq5ebmyjAMHTx40PM1evRoVVVVafPmzV6PmTFjhtq0aeO57x6G/fzzz5t8rWAuvvhibdiwQRs2bNALL7ygBx54QP/5z380btw4zyUV989aUFCg7Oxsz2PPP/98XXnllXrppZckSfX19Vq/fr3y8vJ05plnetp169ZNkydP1r/+9S/P7ysrK0v/+c9/9NlnnzWr/43dfPPNXvcvu+wylZeXe17373//uyR5DZ1L0q233hrW68ybN0+dO3dW165dddlll+njjz/Wb3/7W02aNCngYyZPnqwdO3bonXfe8dz6u7wgSaeddpo++OAD3XzzzaqoqNBjjz2myZMnq0uXLrr//vtlGIZX+7S0NM/vseHXggULvNrt3r07pE/ox44dU2pqqs9x96UNf5fb/FmwYIHWr1+vJ598Updccom++eYbnThxwqfN8ePH/Y4sIDFxiQEtokOHDpLktezL7fHHH9fhw4f11VdfaerUqT7fT0lJ8Xs5YM+ePbr77rtVWlqqiooKr+9VVVVJkv773/9Kkvr06ePz+HPPPdfnDb6hAwcOqLKyUk888YSeeOIJv22+/vprr/u9evXyuu8eym7cv3B16tRJo0aN8twfO3aszj33XE2aNEmLFy/Wrbfe6vlZzz33XJ/Hn3feeVq3bp1qamp0+PBhHT16NGA7l8ulvXv3qn///rrvvvs0fvx4nXPOORowYICuvvpq/ehHP9L555/frJ+nqfOUkZGh//73v3I6nV6XTyTp7LPPDut1fvKTnyg/P1+1tbV6+eWX9Yc//EH19fVNPubCCy9U3759tWLFCmVlZalr1676n//5n4Dtu3XrpkWLFulPf/qTPvvsM61bt06//vWvdffdd6tbt2768Y9/7GnbqlUrr99jc7Vt21Z1dXU+x2traz3fD8XAgQM9/546daouuugiFRQUeJYw7t69W7/5zW/06KOPhnRJDImBgIAWkZmZqW7dumnr1q0+33Nf7w70iSk1NVVOp/dgV319va688kodOnRIP//5z9W3b1+1a9dO+/btU0FBgVwuV7P77H6OqVOnBry+3PiNMtBytcafJKPBfZ36tddeC/uTdaguv/xy7dy5U2vWrNH69eu1ePFi/f73v9djjz3m9cYXrpY6T3369PG8IV9zzTVq1aqVfvGLX+iKK67Q4MGDAz5u8uTJWrRokTp06KBrr73W578/fxwOh8455xydc845Gjt2rPr06aNnnnmmWecpmG7duumVV16RYRhek0P3798vSerevXvYz9mmTRuNGzdOCxYs0LFjx9S2bVvdfffd+ta3vqURI0Z4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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "gradientboosting_model = GradientBoostingBandGapModel()\n", + "gradientboosting_model.fit(loader)\n", + "y, pred = gradientboosting_model.parity(loader)\n", + "generate_parity_plot(y, pred, \"Gradient Boosting\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "bc4dff28-9994-4823-b593-268e64245fd2", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "y, pred = gradientboosting_model.parity(loader, test_data_only=False)\n", + "generate_parity_plot(y, pred, \"Gradient Boosting Training\")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "07b5b4c8-43f8-434f-8122-ea1db5a40a82", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "vol -> 0.2142503869933048\n", + "lattice vecs -> [0.16403965 0.22457721 0.14573862]\n", + "lattice angs -> [0.18249219 0.13755082 0.11528523]\n", + "mass density -> 0.8147220157386834\n", + "atom density -> 0.20131281279079555\n", + "is it metal? -> 2.8002421436647036\n", + "number atoms -> 0.09632179063843187\n", + "eng per atom -> 71.66258183324956\n", + "Coord 3Angst -> [0.23337498 0.48541303]\n", + "Coord 2Angst -> [16.82683622 0.42899729]\n", + "Coord 1Angst -> [0. 0.]\n", + "Compositions -> [1.15932754 1.47377265 1.31944836]\n", + "sum Eigenval -> 1.3137152237614458\n", + "Checksum 100 -> 100.00000000000001\n" + ] + } + ], + "source": [ + "fi = randomforest_model.model.feature_importances_\n", + "n = len(loader.elemental_fraction_vectors[1])\n", + "print(\"vol ->\", fi[0] * 100)\n", + "print(\"lattice vecs ->\", fi[1:4] * 100)\n", + "print(\"lattice angs ->\", fi[4:7] * 100)\n", + "print(\"mass density ->\", fi[7] * 100)\n", + "print(\"atom density ->\", fi[8] * 100)\n", + "print(\"is it metal? ->\", fi[9] * 100)\n", + "print(\"number atoms ->\", fi[10] * 100)\n", + "print(\"eng per atom ->\", fi[11] * 100)\n", + "print(\"Coord 3Angst ->\", fi[12:14] * 100)\n", + "print(\"Coord 2Angst ->\", fi[14:16] * 100)\n", + "print(\"Coord 1Angst ->\", fi[16:18] * 100)\n", + "print(\"Compositions ->\", fi[18:18+n] * 100)\n", + "print(\"sum Eigenval ->\", sum(fi[18+n:]) * 100)\n", + "print(\"Checksum 100 ->\", sum(fi * 100))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "rewote_venv", + "language": "python", + "name": "rewote_venv" + }, + "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.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/boxylmer/demo/demo.py b/boxylmer/demo/demo.py new file mode 100644 index 00000000..39b0f236 --- /dev/null +++ b/boxylmer/demo/demo.py @@ -0,0 +1,87 @@ +import sys +import os +demo_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.dirname(demo_dir) # feels very hacky just as a way to avoid pip install -e. +sys.path.append(parent_dir) + +from MaterialPropertyPredictor import MPRLoader, RandomForestBandGapModel, GradientBoostingBandGapModel +import matplotlib.pyplot as plt + +from sklearn import metrics + +def output_directory(): + script_dir = os.path.dirname(os.path.abspath(__file__)) + output_dir = os.path.join(script_dir, "demo output") + + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + return output_dir + +def generate_parity_plot(y, pred, filename): + + rmse = metrics.mean_absolute_error(y, pred) + rmse = round(rmse, 2) + plt.figure(figsize=(6, 6)) + plt.plot(y, pred, 'ro', markersize=8, markerfacecolor='red') + max_value = max(max(y), max(pred)) + plt.plot([0, max_value], [0, max_value], 'k-', lw=2) + + plt.xlabel('y') + plt.ylabel('pred') + plt.xlim(0, max_value) + plt.ylim(0, max_value) + plt.gca().set_aspect('equal', adjustable='box') + plt.box(True) + plt.grid(False) + plt.title(filename + " RMSE: " + str(rmse)) + + plot_filename = os.path.join(output_directory(), filename + ".png") + plt.savefig(plot_filename) + + assert os.path.isfile(plot_filename) + +api_key_file = "api_key.txt" +with open(api_key_file, "r") as f: + api_key = f.read().strip() + +loader = MPRLoader( + n_eigenvals=10 +) +loader.load_data( + api_key, + distance_method='fast', + # elements=["Si", "Fe"], + # chemsys=["Si-Ge", "Si", "Ge"], + chemsys=["Fe", "O", "Si", "Fe-O", "Si-O", "Si-Fe", "Si-Fe-O"] +) + +randomforest_model = RandomForestBandGapModel() +randomforest_model.fit(loader) +print(randomforest_model.model.feature_importances_) +y, pred = randomforest_model.parity(loader) +generate_parity_plot(y, pred, "random forest parity") + +y, pred_train = randomforest_model.parity(loader, test_data_only=False) +generate_parity_plot(y, pred_train, "random forest parity - all data") + + + +gradientboosting_model = GradientBoostingBandGapModel() +gradientboosting_model.fit(loader) +y, pred = gradientboosting_model.parity(loader) +generate_parity_plot(y, pred, "gradient boosting parity") + +y, pred_train = gradientboosting_model.parity(loader, test_data_only=False) +generate_parity_plot(y, pred_train, "gradient boosting parity - all data") + + + + +# # Run this to identify better hyperparams +# randomforest_model.fit_hyperparameters(loader) +# y, pred = randomforest_model.parity(loader) +# generate_parity_plot(y, pred, "random forest parity - tuned hyperparameters") +# gradientboosting_model.fit_hyperparameters(loader) +# y, pred = gradientboosting_model.parity(loader) +# generate_parity_plot(y, pred, "gradient boosting parity - tuned hyperparameters") diff --git a/boxylmer/plan, notes, and progress.md b/boxylmer/plan, notes, and progress.md new file mode 100644 index 00000000..492591f3 --- /dev/null +++ b/boxylmer/plan, notes, and progress.md @@ -0,0 +1,108 @@ +# Minimum Goals +- Create a model which will automatically fit and predict material band-gaps given an input. + +# The Plan +## Monday (primarily planning, familiarize with literature and MPRester) +### Day plan +- Create package structure +- Outline classes and necessary abstractions + - Abstract class to load / curate data (DataLoader) + - Abstract class to wrap prediction (BandGapPredictor) +- Fill out the data loader and curator + - How would other kinds of data be incorporated? + - Probably a dict that holds extra vectors and a validation function to check that every entry is the right corresponding length. + - Depending on whether or not we can expect the inputs to be standardized in pymatgen's structure format, this may need to change a lot. + - How would data from other sources be parsed? -> Another concrete DataLoader + - Parsed data held in this way will be slightly inefficient, even for Python's standards, as np.arrays allocate. + - But I think the result will be much more organized and it looks like structure.get_distance() will be the bottleneck anyhow. + - Do we need to cache the data? -> Not within scope + - Leave reading/writing method spaces for future dev if necessary +- Will leave relevant and useful literature sources as comments for reference later. + +## Tuesday (Finish up data loader, begin model class) +### Day plan +- Identify appropriate structure for models and fill out the BandGapPredictor + - in this limited case the input will be flat, but a 3D structure such as graphs will be more complicated and likely require a different representation or accessor method + - The loader should hold onto the structure objects, so graph construction is still possible without weird methods / a class rewrite. +- Models: RandomForest (doable), GradientBoosting (doable), Graph Convolution (reach, likely won't have enough time this week) +- Padding -> Decide how to allocate it, how will the max size be determined and stored? +- Possibly, we could use an autoencoder to get over possible padding issues, if the tree based methods don't like the zero padding. + - Pros: Faster tree inputs, reduced input space, likely better performance + - Cons: Can't find the importance of each input (random forest), training the AE could be super expensive and lag out everything + - This might be a more realistic reach goal than GNNs, lets try this first if there's time. + + +### Notes and issues - Tuesday +- It's a simple design problem, but should we have the model be dependent on the dataloader directly, or some direct representation of its data? + - Making it dependent on abstract data loaders make it harder to get fine-grained control of what data we're using, so train-test-validate splits are less clear. + - The direct meaning of a flat input vector is basically useless to anyone trying to use this package, as the meaning of the vector changes depending on what features you're using for the model. + - The latter point is- probably -more important in an industry setting, though I don't have much experience here. I'm guessing this is the case: it's more important to have it be readable than to expose fine grain control over training and testing. Therefore: We will add train/test/validate accessors with rng seeds to the dataloader class and implement reasonable defaults. + +- It may be useful to implement predict_pymatgen(pymatgen_structure, extras={}) to facilitate easy prediction with custom structures, but this may be out of scope for now. **Future design will need to consider how exactly novel predictions are going to be made and where from / in what format that data will arrive in.** + +- Clearly, based on parity plots alone we are not predicting well. Though we technically satisfy the "reasonable value" property set forth by the original request (that is, values are in the right ballpark and will interpolate between pure systems), these techniques are a far stretch from being usable in a real environment. We have a number of options and conclusions as of today. + - Random forest slightly out-performs gradient boosting using these features for large datasets and likely all sizes as well. + - Foregoing coulomb matrix methods and attempting more direct- but less informative -features such as simply a list of atomic numbers might assist with these models. + - Time permitting, an autoencoder would be very interesting to pursue, as it would fix two issues at once: High dimensionality and variable input sizes (necessitating padding). + + - We can check the model for memorization by plotting training parity as well: Thus far memorization does not appear to be occurring with fewer than 100 estimators. This is consistent for smaller datasets. (see below) + + - Since we're padding with zeros and eigenvalues encode structural information, maybe we should ensure the sorting is *descending* and not *ascending*? + - Yep! This slightly improves the testing results in both models, almost certainly because larger EVs represent more prevalent informaiton. ] + + - I'll go ahead and implement something to find the best hyperparameters and move on from this, as it's a proof of concept. Fixing the descriptor with an autoencoder or another better featurization is likely the core issue, everything else is just slapping band-aids on the problem. + + - Optimizing hyperparameters definitely doesn't avoid memorization to any degree, and possibly worsens it. + +- Overall conclusion: **Switching from eigenvalues to atomic numbers improves both models RMSE by almost exaclty 0.02, suggesting that the models are struggling to learn to represent these features. As a result, autoencoders present the most viable way to improve these predictions, but implementing them may be out of scope within the time limits of this project** + +## Wednesday (~~Finish up model, test model for goals (realistic band gap values, parity, etc)~~ -> Document what exists now, begin understanding more about why the current set of features fail to adequately predict band gap, and begin improving the results.) +### Day plan +- We have a model that overfits/memorizes with larger sizes. Yesterday, I determined that the coulomb matrix eigenvalues, while good descriptors, are too noisy and too ubiquitous to provide the model with generalizable information. Instead, I've come up with a number of options that may avoid this. + - Truncating the eigenvalue descriptors to the top N values, possibly cutting out unimportant noise + - Switching to other (limited) structural information, such as scoring aggregates for the atoms in the lattice + some lattice parameters that are already there + - Autoencoder representation of the model input (reach, out of time) + - Graph representation of the model input (very much a reach, but it's here to be a future option) + + ___ + - The first option, truncating the eigenvalues, is the most likely to result in success. The overfitting in the context of the large number of descriptors, as well as the large amount of needed padding, likely happens as the descriptors become "fingerprints" rather than generalizable information. Reducing descriptors therefore is promising. + + +- The classes and function need documentation, namely each model / especially each object that will be exposed in a theoretical API +- The scripts that provide plots need to be separated from the (rather unorganized) tests, as they're a demonstration rather than tests +- A readme needs to be written to provide instructions on installing and running the PoC + + + +## Thursday (Visualization of results + deal with possible issues or hangups that may have happened Mon-Wed) +### Day plan +- Finish up changed needed to better reduce overfitting +- Document everything in MaterialDataLoader.py +- Implement some more ideas I had + - Coordination numbers for structural proxies, at various radius + - Element vectors + - Dimensionless element vectors + + +### Notes and issues +- The coulomb matrix is proving to provide no feature importance and is acting more as a fingerprint, as suspected. +- Instead, we can feed in a list of atomic numbers and some aggregate for connectivity in lieu of a graph +- I think right now we could just get get average coordination numbers using a variety of radii, and also the stdev to tell the model how "spread out" the packing is. +- Clearly more useful descriptors are needed. The model really cares about mass (not atomic) density. I expected the latter and have not yet found literature to back this. +- Supplying composition by elemental fractional composition would fix the padding issues + - I don't think it would make sense to supply elements that the dataset hasn't previoulsy seen, so we will need to bring attention to the limitation this introduces, lets see if it's worth including. Since chemsys searches are exclusive, they should work better as they have lower atom diversity. + - This also needs to be communicated, as if true, it be very important for this models correct usage. + +### Results thus far +- Wow! RMSE 0.44 -> 0.29 with the new descriptors and ways of including structure. This works, but the parity is still indicating overfitting. +- As expected, limiting the search to a smaller number of atoms (but similarly sized datasets) greatly reduces memorization and RMSE decreases to ~0.08. +- Coulomb eigenvalues significantly harm performance for small datasets as they act like fingerprints, encouraging memorization rather than generalization. + - CMEs still provide useful information, but more work will need to be done on finding the optimal number of top N eigenvalues to include (hyperparameter tuning problem) + - I expect the bigger the dataset, the more eigenvalues can be included. + +## Friday (last day: wrap up and write comments and instructions.) +### Day plan +- Document PredictorModel.py +- Write an installation readme and point to the demo file. +- Finish up conclusions in the writeup mdfile. + diff --git a/boxylmer/tests/__init__.py b/boxylmer/tests/__init__.py new file mode 100644 index 00000000..160f51b2 --- /dev/null +++ b/boxylmer/tests/__init__.py @@ -0,0 +1 @@ +# also frustrating and hacky, but the environment refuses to acknowledge this in pytest without an __init__. diff --git a/boxylmer/tests/main_test.py b/boxylmer/tests/main_test.py new file mode 100644 index 00000000..71917c44 --- /dev/null +++ b/boxylmer/tests/main_test.py @@ -0,0 +1,132 @@ +import warnings +from MaterialPropertyPredictor import MPRLoader +from MaterialPropertyPredictor import RandomForestBandGapModel +from MaterialPropertyPredictor import GradientBoostingBandGapModel +from sklearn import metrics + +import os +import matplotlib.pyplot as plt + +def output_directory(): + script_dir = os.path.dirname(os.path.abspath(__file__)) + output_dir = os.path.join(script_dir, "test_output") + + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + return output_dir + +def generate_parity_plot(y, pred, filename): + + rmse = metrics.mean_absolute_error(y, pred) + rmse = round(rmse, 2) + plt.figure(figsize=(6, 6)) + plt.plot(y, pred, 'ro', markersize=8, markerfacecolor='red') + max_value = max(max(y), max(pred)) + plt.plot([0, max_value], [0, max_value], 'k-', lw=2) + + plt.xlabel('y') + plt.ylabel('pred') + plt.xlim(0, max_value) + plt.ylim(0, max_value) + plt.gca().set_aspect('equal', adjustable='box') + plt.box(True) + plt.grid(False) + plt.title(filename + " RMSE: " + str(rmse)) + + plot_filename = os.path.join(output_directory(), filename + ".png") + plt.savefig(plot_filename) + + assert os.path.isfile(plot_filename) + + +api_key_file = "api_key.txt" +loader = None +def test_loading(): + global loader + warnings.filterwarnings("ignore", category=UserWarning) + with open(api_key_file, "r") as f: + api_key = f.read().strip() + + loader = MPRLoader() + loader.load_data( + api_key, + distance_method='fast', + elements=["Si", "Ge"], + chemsys=["Si-Ge"] + ) + assert len(loader) > 0 + assert len(loader) == len(loader.get_model_inputs()) + assert len(loader) == len(loader.formulas) + + truncated_eigenval_loader = MPRLoader(n_eigenvals=20) + truncated_eigenval_loader.load_data( + api_key, + distance_method='fast', + elements=["Si", "Ge"], + chemsys=["Si-Ge"] + ) + assert len(loader) == len(truncated_eigenval_loader) + inputs = truncated_eigenval_loader.get_model_inputs() + input_len = len(inputs[0]) + expected_len = truncated_eigenval_loader.calculate_max_input_size() + assert input_len == expected_len + assert all(len(item) == input_len for item in inputs) + +def test_loading_accurate_distances(): + with open(api_key_file, "r") as f: + api_key = f.read().strip() + accurate_loader = MPRLoader() + accurate_loader.load_data( + api_key, + distance_method='accurate', + chemsys=["Si-Ge"], # should pull little data, making tests faster + ) + assert len(accurate_loader) > 1 + +def test_input_and_output_len(): + assert len(loader) > 1 + assert len(loader.get_model_inputs()) == len(loader.get_model_outputs()) + +def test_training_and_testing_splits(): + # test_size = 0.3 + train_x, train_y = loader.get_train_data() + assert len(train_x) == len(train_y) + + test_x, test_y = loader.get_test_data() + assert len(test_x) == len(test_y) + + training_length = len(train_x) + testing_length = len(test_x) + assert training_length > testing_length + +randomforest_model = None +def test_random_forest_bandgap_model(): + global randomforest_model + randomforest_model = RandomForestBandGapModel() + randomforest_model.fit(loader) + randomforest_model.predict(loader) + y, pred = randomforest_model.parity(loader) + assert len(y) == len(pred) + generate_parity_plot(y, pred, "random forest parity") + + y, pred = randomforest_model.parity(loader, test_data_only=False) + assert len(y) == len(pred) + generate_parity_plot(y, pred, "random forest parity - all data") + + + +gradientboosting_model = None +def test_gradient_boosting_bandgap_model(): + global gradientboosting_model + gradientboosting_model = GradientBoostingBandGapModel() + gradientboosting_model.fit(loader) + gradientboosting_model.predict(loader) + y, pred = gradientboosting_model.parity(loader) + assert len(y) == len(pred) + generate_parity_plot(y, pred, "gradient boosting parity") + + y, pred = gradientboosting_model.parity(loader, test_data_only=False) + assert len(y) == len(pred) + generate_parity_plot(y, pred, "gradient boosting parity - all data") + diff --git a/previous diagnostic output/demo output - Thursday/gradient boosting parity - all data.png b/previous diagnostic output/demo output - Thursday/gradient boosting parity - all data.png new file mode 100644 index 00000000..52bfe9e0 Binary files /dev/null and b/previous diagnostic output/demo output - Thursday/gradient boosting parity - all data.png differ diff --git a/previous diagnostic output/demo output - Thursday/gradient boosting parity - tuned hyperparameters.png b/previous diagnostic output/demo output - Thursday/gradient boosting parity - tuned hyperparameters.png new file mode 100644 index 00000000..f2d68c9d Binary files /dev/null and b/previous diagnostic output/demo output - Thursday/gradient boosting parity - tuned hyperparameters.png differ diff --git a/previous diagnostic output/demo output - Thursday/gradient boosting parity.png b/previous diagnostic output/demo output - Thursday/gradient boosting parity.png new file mode 100644 index 00000000..4a64eabd Binary files /dev/null and b/previous diagnostic output/demo output - Thursday/gradient boosting parity.png differ diff --git a/previous diagnostic output/demo output - Thursday/random forest parity - all data.png b/previous diagnostic output/demo output - Thursday/random forest parity - all data.png new file mode 100644 index 00000000..afb07295 Binary files /dev/null and b/previous diagnostic output/demo output - Thursday/random forest parity - all data.png differ diff --git a/previous diagnostic output/demo output - Thursday/random forest parity - tuned hyperparameters.png b/previous diagnostic output/demo output - Thursday/random forest parity - tuned hyperparameters.png new file mode 100644 index 00000000..7e508265 Binary files /dev/null and b/previous diagnostic output/demo output - Thursday/random forest parity - tuned hyperparameters.png differ diff --git a/previous diagnostic output/demo output - Thursday/random forest parity.png b/previous diagnostic output/demo output - Thursday/random forest parity.png new file mode 100644 index 00000000..c6a89245 Binary files /dev/null and b/previous diagnostic output/demo output - Thursday/random forest parity.png differ diff --git a/previous diagnostic output/demo output - Wednesday/gradient boosting parity - all data.png b/previous diagnostic output/demo output - Wednesday/gradient boosting parity - all data.png new file mode 100644 index 00000000..564efa02 Binary files /dev/null and b/previous diagnostic output/demo output - Wednesday/gradient boosting parity - all data.png differ diff --git a/previous diagnostic output/demo output - Wednesday/gradient boosting parity - tuned hyperparameters.png b/previous diagnostic output/demo output - Wednesday/gradient boosting parity - tuned hyperparameters.png new file mode 100644 index 00000000..d8ab9182 Binary files /dev/null and b/previous diagnostic output/demo output - Wednesday/gradient boosting parity - tuned hyperparameters.png differ diff --git a/previous diagnostic output/demo output - Wednesday/gradient boosting parity.png b/previous diagnostic output/demo output - Wednesday/gradient boosting parity.png new file mode 100644 index 00000000..23a4db03 Binary files /dev/null and b/previous diagnostic output/demo output - Wednesday/gradient boosting parity.png differ diff --git a/previous diagnostic output/demo output - Wednesday/random forest parity - all data.png b/previous diagnostic output/demo output - Wednesday/random forest parity - all data.png new file mode 100644 index 00000000..154ba2b2 Binary files /dev/null and b/previous diagnostic output/demo output - Wednesday/random forest parity - all data.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..fb428cd0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,81 @@ +asttokens==2.4.0 +backcall==0.2.0 +certifi==2023.7.22 +charset-normalizer==3.2.0 +colorama==0.4.6 +comm==0.1.4 +contourpy==1.1.0 +cycler==0.11.0 +debugpy==1.8.0 +decorator==5.1.1 +emmet-core==0.67.5 +exceptiongroup==1.1.3 +executing==1.2.0 +fonttools==4.42.1 +future==0.18.3 +idna==3.4 +importlib-metadata==6.8.0 +importlib-resources==6.0.1 +iniconfig==2.0.0 +ipykernel==6.25.2 +ipython==8.12.2 +jedi==0.19.0 +joblib==1.3.2 +jupyter_client==8.3.1 +jupyter_core==5.3.1 +kiwisolver==1.4.5 +latexcodec==2.0.1 +matplotlib==3.7.3 +matplotlib-inline==0.1.6 +monty==2023.9.5 +mp-api==0.35.1 +mpmath==1.3.0 +msgpack==1.0.5 +nest-asyncio==1.5.7 +networkx==3.1 +numpy==1.24.4 +packaging==23.1 +palettable==3.3.3 +pandas==2.0.3 +parso==0.8.3 +pickleshare==0.7.5 +Pillow==10.0.0 +platformdirs==3.10.0 +plotly==5.16.1 +pluggy==1.3.0 +prompt-toolkit==3.0.39 +psutil==5.9.5 +pure-eval==0.2.2 +pybtex==0.24.0 +pydantic==1.10.12 +Pygments==2.16.1 +pymatgen==2023.8.10 +pyparsing==3.1.1 +pytest==7.4.2 +python-dateutil==2.8.2 +pytz==2023.3.post1 +pywin32==306 +PyYAML==6.0.1 +pyzmq==25.1.1 +requests==2.31.0 +ruamel.yaml==0.17.32 +ruamel.yaml.clib==0.2.7 +scikit-learn==1.3.0 +scipy==1.10.1 +six==1.16.0 +spglib==2.1.0 +stack-data==0.6.2 +sympy==1.12 +tabulate==0.9.0 +tenacity==8.2.3 +threadpoolctl==3.2.0 +tomli==2.0.1 +tornado==6.3.3 +tqdm==4.66.1 +traitlets==5.10.0 +typing_extensions==4.7.1 +tzdata==2023.3 +uncertainties==3.1.7 +urllib3==2.0.4 +wcwidth==0.2.6 +zipp==3.16.2