import numpy as np import gradio as gr import pandas as pd from sklearn.preprocessing import MinMaxScaler from surrogate import CrabNetSurrogateModel, PARAM_BOUNDS from pydantic import ( BaseModel, ValidationError, ValidationInfo, field_validator, model_validator, ) model = CrabNetSurrogateModel() # Define the input parameters example_parameterization = { "N": 3, "alpha": 0.5, "d_model": 512, "dim_feedforward": 2048, "dropout": 0.1, "emb_scaler": 0.5, "epochs_step": 10, "eps": 0.000001, "fudge": 0.02, "heads": 4, "k": 6, "lr": 0.001, "pe_resolution": 5000, "ple_resolution": 5000, "pos_scaler": 0.5, "weight_decay": 0, "batch_size": 32, "out_hidden4": 128, "betas1": 0.9, "betas2": 0.999, "bias": False, "criterion": "RobustL1", "elem_prop": "mat2vec", "train_frac": 0.5, } example_results = model.surrogate_evaluate([example_parameterization]) example_result = example_results[0] # Initialize and fit scalers for each parameter scalers = {} for param_info in PARAM_BOUNDS: if param_info["type"] == "range": scaler = MinMaxScaler() # Fit the scaler using the parameter bounds scaler.fit([[bound] for bound in param_info["bounds"]]) scalers[param_info["name"]] = scaler # HACK: Hardcoded BLINDED_PARAM_BOUNDS = [ {"name": "x1", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x2", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x3", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x4", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x5", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x6", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x7", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x8", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x9", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x10", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x11", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x12", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x13", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x14", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x15", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x16", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x17", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x18", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x19", "type": "range", "bounds": [0.0, 1.0]}, {"name": "x20", "type": "range", "bounds": [0.0, 1.0]}, {"name": "c1", "type": "choice", "values": ["c1_0", "c1_1"]}, {"name": "c2", "type": "choice", "values": ["c2_0", "c2_1"]}, {"name": "c3", "type": "choice", "values": ["c3_0", "c3_1", "c3_2"]}, {"name": "fidelity1", "type": "range", "bounds": [0.0, 1.0]}, ] class BlindedParameterization(BaseModel): x1: float # int x2: float x3: float # int x4: float # int x5: float x6: float x7: float # int x8: float x9: float x10: float # int x11: float # int x12: float x13: float # int x14: float # int x15: float x16: float # int x17: float # int x18: float # int x19: float x20: float c1: str # bool c2: str c3: str fidelity1: float @field_validator("*") def check_bounds(cls, v: int, info: ValidationInfo) -> int: param = next( (item for item in BLINDED_PARAM_BOUNDS if item["name"] == info.field_name), None, ) if param is None: return v if param["type"] == "range": min_val, max_val = param["bounds"] if not min_val <= v <= max_val: raise ValueError( f"{info.field_name} must be between {min_val} and {max_val}" ) elif param["type"] == "choice": if v not in param["values"]: raise ValueError(f"{info.field_name} must be one of {param['values']}") return v @model_validator(mode="after") def check_constraints(self) -> "BlindedParameterization": if self.x19 > self.x20: raise ValueError( f"Received x19={self.x19} which should be less than x20={self.x20}" ) if self.x6 + self.x15 > 1.0: raise ValueError( f"Received x6={self.x6} and x15={self.x15} which should sum to less than or equal to 1.0" # noqa: E501 ) # Conversion from original to blinded representation def convert_to_blinded(params): blinded_params = {} numeric_index = 1 choice_index = 1 for param in PARAM_BOUNDS: if param["type"] == "range": key = f"x{numeric_index}" if param["name"] != "train_frac" else "fidelity1" blinded_params[key] = scalers[param["name"]].transform( [[params[param["name"]]]] )[0][0] numeric_index += 1 if param["name"] != "train_frac" else 0 elif param["type"] == "choice": key = f"c{choice_index}" choice_index = param["values"].index(params[param["name"]]) blinded_params[key] = f"{key}_{choice_index}" choice_index += 1 return blinded_params # Conversion from blinded to original representation def convert_from_blinded(blinded_params): original_params = {} numeric_index = 1 choice_index = 1 for param in PARAM_BOUNDS: if param["type"] == "range": key = f"x{numeric_index}" if param["name"] != "train_frac" else "fidelity1" original_params[param["name"]] = scalers[param["name"]].inverse_transform( [[blinded_params[key]]] )[0][0] numeric_index += 1 if param["name"] != "train_frac" else 0 elif param["type"] == "choice": key = f"c{choice_index}" choice_value = blinded_params[key].split("_")[-1] original_params[param["name"]] = param["values"][int(choice_value)] choice_index += 1 return original_params def evaluate(*args): # Assume args are in the order of BLINDED_PARAM_BOUNDS blinded_params = dict(zip([param["name"] for param in BLINDED_PARAM_BOUNDS], args)) original_params = convert_from_blinded(blinded_params) BlindedParameterization(**blinded_params) # Validation params_list = [original_params] results = model.surrogate_evaluate(params_list) results_list = [list(result.values()) for result in results] return results_list def get_interface(param_info, numeric_index, choice_index): key = param_info["name"] default_value = example_parameterization[key] if param_info["type"] == "range": # Rescale the parameter to be between 0 and 1 scaler = scalers[key] scaler.fit([[bound] for bound in param_info["bounds"]]) scaled_value = scaler.transform([[default_value]])[0][0] scaled_bounds = scaler.transform([[bound] for bound in param_info["bounds"]]) label = f"fidelity1" if key == "train_frac" else f"x{numeric_index}" return ( gr.Slider( # Change this line value=scaled_value, minimum=scaled_bounds[0][0], maximum=scaled_bounds[1][0], label=label, step=(scaled_bounds[1][0] - scaled_bounds[0][0]) / 100, ), numeric_index + 1, choice_index, ) elif param_info["type"] == "choice": return ( gr.Radio( choices=[ f"c{choice_index}_{i}" for i in range(len(param_info["values"])) ], label=f"c{choice_index}", value=f"c{choice_index}_{param_info['values'].index(default_value)}", ), numeric_index, choice_index + 1, ) # test the evaluate function blinded_results = evaluate(*[0.5] * 20, "c1_0", "c2_0", "c3_0", 0.5) numeric_index = 1 choice_index = 1 inputs = [] for param in PARAM_BOUNDS: input, numeric_index, choice_index = get_interface( param, numeric_index, choice_index ) inputs.append(input) iface = gr.Interface( title="Advanced Optimization", fn=evaluate, inputs=inputs, outputs=gr.Numpy( value=np.array([list(example_result.values())]), headers=[f"y{i+1}" for i in range(len(example_result))], col_count=(len(example_result), "fixed"), datatype=["number"] * len(example_result), ), description=""" ## Objectives **Minimize `y1`, `y2`, `y3`, and `y4`** ### Correlations - `y1` and `y2` are correlated - `y1` is anticorrelated with `y3` - `y2` is anticorrelated with `y3` ### Noise `y1`, `y2`, and `y3` are stochastic with heteroskedastic, parameter-free noise, whereas `y4` is deterministic, but still considered 'black-box'. In other words, repeat calls with the same input arguments will result in different values for `y1`, `y2`, and `y3`, but the same value for `y4`. ### Objective thresholds If `y1` is greater than 0.2, the result is considered "bad" no matter how good the other values are. If `y2` is greater than 0.7, the result is considered "bad" no matter how good the other values are. If `y3` is greater than 1800, the result is considered "bad" no matter how good the other values are. If `y4` is greater than 40e6, the result is considered "bad" no matter how good the other values are. ## Search Space ### Fidelity `fidelity1` is a fidelity parameter. The lowest fidelity is 0, and the highest fidelity is 1. The higher the fidelity, the more expensive the evaluation, and the higher the quality. NOTE: `fidelity1` and `y3` are correlated. ### Constraints - x19 < x20 - x6 + x15 ≤ 1.0 ### Parameter bounds - 0 ≤ xi ≤ 1 for i ∈ {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20} - c1 ∈ {c1_0, c1_1} - c2 ∈ {c2_0, c2_1} - c3 ∈ {c3_0, c3_1, c3_2} - 0 ≤ fidelity1 ≤ 1 ## Notion of best Thresholded Pareto front hypervolume vs. running cost for three different budgets, and averaged over 10 search campaigns. ## References: 1. Baird, S. G.; Liu, M.; Sparks, T. D. High-Dimensional Bayesian Optimization of 23 Hyperparameters over 100 Iterations for an Attention-Based Network to Predict Materials Property: A Case Study on CrabNet Using Ax Platform and SAASBO. Computational Materials Science 2022, 211, 111505. https://doi.org/10.1016/j.commatsci.2022.111505. 2. Baird, S. G.; Parikh, J. N.; Sparks, T. D. Materials Science Optimization Benchmark Dataset for High-Dimensional, Multi-Objective, Multi-Fidelity Optimization of CrabNet Hyperparameters. ChemRxiv March 7, 2023. https://doi.org/10.26434/chemrxiv-2023-9s6r7. """, ) iface.launch(show_error=True)