import os from pathlib import Path import streamlit as st import pandas as pd import joblib import optuna import numpy as np from google import genai from google.genai import types from biomaterials import BIOMATERIAL_OPTIONS from cell_lines import CELL_LINE_OPTIONS _original_number_input = st.number_input def safe_number_input(label, **kwargs): """ Clamp `value` into [min_value, max_value] and warn if we had to adjust. Then call the real st.number_input with the clamped default. """ min_value = kwargs.get("min_value", float("-inf")) max_value = kwargs.get("max_value", float("inf")) value = kwargs.get("value", min_value) clamped = min(max(value, min_value), max_value) if clamped != value: st.warning( f"⚠️ Default for “{label}” ({value}) was outside " f"[{min_value}, {max_value}]; using {clamped} instead." ) kwargs["value"] = clamped return _original_number_input(label, **kwargs) st.number_input = safe_number_input APP_DIR = Path(__file__).resolve().parent MODEL_ROOT = APP_DIR / "models" PREPROCESSOR_DIR = MODEL_ROOT / "preprocessors" MODEL_TASKS = { "printability": { "folder": MODEL_ROOT / "printability", "prefix": "Printability_", "label_encoder": "label_encoder_printability.pkl", }, "cell_response": { "folder": MODEL_ROOT / "cell response", "prefix": "Cell_Response_", "label_encoder": "label_encoder_cell_response.pkl", }, } DL_MODEL_CONFIGS = { ("printability", "ResNet"): {"n_layers": 6, "hidden_dim": 302, "dropout": 0.190106, "activation_func": "relu"}, ("printability", "MLP"): {"n_layers": 3, "hidden_dim": 367, "dropout": 0.169472, "activation_func": "tanh"}, ("printability", "1D_CNN"): {"n_layers": 6, "hidden_dim": 287, "dropout": 0.233072, "activation_func": "relu"}, ("printability", "FT_Transformer"): {"n_layers": 3, "hidden_dim": 437, "dropout": 0.324095, "activation_func": "GELU"}, ("printability", "TabNet_Lite"): {"n_layers": 4, "hidden_dim": 283, "dropout": 0.309445, "activation_func": "relu"}, ("printability", "NODE_Lite"): {"n_layers": 5, "hidden_dim": 289, "dropout": 0.145481, "activation_func": "SELU"}, ("cell_response", "ResNet"): {"n_layers": 4, "hidden_dim": 269, "dropout": 0.229224, "activation_func": "tanh"}, ("cell_response", "MLP"): {"n_layers": 5, "hidden_dim": 238, "dropout": 0.256294, "activation_func": "ELU"}, ("cell_response", "1D_CNN"): {"n_layers": 6, "hidden_dim": 134, "dropout": 0.158794, "activation_func": "SiLU"}, ("cell_response", "FT_Transformer"): {"n_layers": 5, "hidden_dim": 395, "dropout": 0.185155, "activation_func": "SiLU"}, ("cell_response", "TabNet_Lite"): {"n_layers": 5, "hidden_dim": 342, "dropout": 0.103486, "activation_func": "SiLU"}, ("cell_response", "NODE_Lite"): {"n_layers": 2, "hidden_dim": 127, "dropout": 0.328308, "activation_func": "SiLU"}, } MODEL_RANKINGS = { "printability": [ "HistGradientBoosting", "TabPFN 2.6", "Bagging", "GradientBoosting", "TabICL v2", "XGBoost", "LightGBM", "KNeighbors", "LabelPropagation", "ExtraTrees", "MLP", "LabelSpreading", "TabNet Lite", "MLP DL", "ResNet", "1D CNN", "DecisionTree", "NODE Lite", "LinearSVC", "LDA", "PassiveAggressive", "CalibratedClassifierCV", "LogisticRegression", "RidgeClassifier", "FT Transformer", "Perceptron", "MultinomialNB", "RadiusNeighbors", "ComplementNB", "NuSVC", "AdaBoost", "SGD", "BernoulliNB", "GaussianNB", "QDA", "RandomForest", "ExtraTreeClassifier", "DummyClassifier", ], "cell_response": [ "HistGradientBoosting", "TabICL v2", "TabPFN 2.6", "XGBoost", "Bagging", "KNeighbors", "LabelPropagation", "LabelSpreading", "LightGBM", "FT Transformer", "CalibratedClassifierCV", "ResNet", "NODE Lite", "1D CNN", "MLP DL", "LDA", "NuSVC", "MultinomialNB", "LogisticRegression", "LinearSVC", "Perceptron", "QDA", "RidgeClassifier", "ExtraTrees", "ExtraTreeClassifier", "AdaBoost", "DecisionTree", "RandomForest", "GradientBoosting", "SGD", "MLP", "PassiveAggressive", "ComplementNB", "TabNet Lite", "BernoulliNB", "RadiusNeighbors", "DummyClassifier", "GaussianNB", ], } PERFORMANCE_GUIDE = { "printability": """ | Rank | Model | Framework | Accuracy | F1 | AUC | MCC | |---:|---|---|---:|---:|---:|---:| | 1 | HistGradientBoosting | Machine Learning | 0.80 | 0.80 | 0.94 | 0.69 | | 2 | TabPFN 2.6 | Deep Learning / Transformer | 0.80 | 0.80 | 0.94 | 0.69 | | 3 | Bagging | Machine Learning | 0.79 | 0.79 | 0.93 | 0.67 | | 4 | GradientBoosting | Machine Learning | 0.79 | 0.79 | 0.93 | 0.66 | | 5 | TabICL v2 | Deep Learning / Transformer | 0.79 | 0.79 | 0.94 | 0.68 | | 6 | XGBoost | Machine Learning | 0.78 | 0.78 | 0.93 | 0.66 | | 7 | LightGBM | Machine Learning | 0.77 | 0.77 | 0.93 | 0.64 | | 8 | KNeighbors | Machine Learning | 0.77 | 0.77 | 0.90 | 0.64 | | 9 | LabelPropagation | Machine Learning | 0.77 | 0.77 | 0.82 | 0.64 | | 10 | ExtraTrees | Machine Learning | 0.77 | 0.77 | 0.92 | 0.63 | """, "cell_response": """ | Rank | Model | Framework | Accuracy | F1 | AUC | MCC | |---:|---|---|---:|---:|---:|---:| | 1 | HistGradientBoosting | Machine Learning | 0.81 | 0.81 | 0.96 | 0.67 | | 2 | TabICL v2 | Deep Learning / Transformer | 0.79 | 0.79 | 0.97 | 0.65 | | 3 | TabPFN 2.6 | Deep Learning / Transformer | 0.78 | 0.78 | 0.96 | 0.63 | | 4 | XGBoost | Machine Learning | 0.79 | 0.77 | 0.96 | 0.65 | | 5 | Bagging | Machine Learning | 0.77 | 0.77 | 0.95 | 0.62 | | 6 | KNeighbors | Machine Learning | 0.77 | 0.77 | 0.93 | 0.61 | | 7 | LabelPropagation | Machine Learning | 0.77 | 0.77 | 0.93 | 0.61 | | 8 | LabelSpreading | Machine Learning | 0.78 | 0.76 | 0.93 | 0.61 | | 9 | LightGBM | Machine Learning | 0.78 | 0.76 | 0.96 | 0.64 | | 10 | FT Transformer | Deep Learning / Transformer | 0.76 | 0.76 | 0.94 | 0.61 | """, } PERFORMANCE_GUIDE_FULL = { "printability": """ # Printability - Merged Performance Summary This file contains the aggregated and benchmarked results of traditional Machine Learning (ML) and Deep Learning / Transformer architectures for predicting **Printability**, sorted hierarchically by **F1 Score** and **Accuracy**. | Rank | Model | Framework | Accuracy | Precision | Recall | F1 | AUC | MCC | Kappa | |---:|---|---|---:|---:|---:|---:|---:|---:|---:| | 1 | HistGradientBoosting | Machine Learning | 0.80 | 0.80 | 0.80 | 0.80 | 0.94 | 0.69 | 0.69 | | 2 | TabPFN 2.6 | Deep Learning / Transformer | 0.80 | 0.81 | 0.80 | 0.80 | 0.94 | 0.69 | 0.68 | | 3 | Bagging | Machine Learning | 0.79 | 0.79 | 0.79 | 0.79 | 0.93 | 0.67 | 0.67 | | 4 | GradientBoosting | Machine Learning | 0.79 | 0.79 | 0.79 | 0.79 | 0.93 | 0.66 | 0.66 | | 5 | TabICL v2 | Deep Learning / Transformer | 0.79 | 0.81 | 0.79 | 0.79 | 0.94 | 0.68 | 0.67 | | 6 | XGBoost | Machine Learning | 0.78 | 0.79 | 0.78 | 0.78 | 0.93 | 0.66 | 0.65 | | 7 | LightGBM | Machine Learning | 0.77 | 0.78 | 0.77 | 0.77 | 0.93 | 0.64 | 0.64 | | 8 | KNeighbors | Machine Learning | 0.77 | 0.78 | 0.77 | 0.77 | 0.90 | 0.64 | 0.64 | | 9 | LabelPropagation | Machine Learning | 0.77 | 0.77 | 0.77 | 0.77 | 0.82 | 0.64 | 0.63 | | 10 | ExtraTrees | Machine Learning | 0.77 | 0.77 | 0.77 | 0.77 | 0.92 | 0.63 | 0.63 | | 11 | MLP | Machine Learning | 0.76 | 0.76 | 0.76 | 0.76 | 0.90 | 0.63 | 0.63 | | 12 | LabelSpreading | Machine Learning | 0.76 | 0.76 | 0.76 | 0.76 | 0.90 | 0.62 | 0.62 | | 13 | TabNet Lite | Deep Learning / Transformer | 0.74 | 0.74 | 0.74 | 0.74 | 0.91 | 0.59 | 0.59 | | 14 | MLP (DL) | Deep Learning / Transformer | 0.73 | 0.73 | 0.73 | 0.73 | 0.91 | 0.58 | 0.58 | | 15 | ResNet | Deep Learning / Transformer | 0.73 | 0.73 | 0.73 | 0.73 | 0.91 | 0.57 | 0.57 | | 16 | 1D CNN | Deep Learning / Transformer | 0.71 | 0.73 | 0.71 | 0.72 | 0.89 | 0.56 | 0.56 | | 17 | DecisionTree | Machine Learning | 0.71 | 0.72 | 0.71 | 0.71 | 0.89 | 0.54 | 0.54 | | 18 | NODE Lite | Deep Learning / Transformer | 0.69 | 0.70 | 0.69 | 0.70 | 0.90 | 0.52 | 0.52 | | 19 | LinearSVC | Machine Learning | 0.69 | 0.68 | 0.69 | 0.68 | 0.85 | 0.49 | 0.49 | | 20 | LDA | Machine Learning | 0.68 | 0.68 | 0.68 | 0.68 | 0.84 | 0.49 | 0.48 | | 21 | PassiveAggressive | Machine Learning | 0.69 | 0.68 | 0.69 | 0.67 | 0.85 | 0.48 | 0.48 | | 22 | CalibratedClassifierCV | Machine Learning | 0.68 | 0.68 | 0.68 | 0.67 | 0.85 | 0.48 | 0.48 | | 23 | LogisticRegression | Machine Learning | 0.68 | 0.67 | 0.68 | 0.67 | 0.85 | 0.47 | 0.47 | | 24 | RidgeClassifier | Machine Learning | 0.68 | 0.67 | 0.68 | 0.66 | 0.84 | 0.46 | 0.46 | | 25 | FT Transformer | Deep Learning / Transformer | 0.66 | 0.65 | 0.66 | 0.65 | 0.87 | 0.45 | 0.45 | | 26 | Perceptron | Machine Learning | 0.67 | 0.68 | 0.67 | 0.64 | 0.84 | 0.45 | 0.42 | | 27 | MultinomialNB | Machine Learning | 0.64 | 0.69 | 0.64 | 0.64 | 0.83 | 0.45 | 0.43 | | 28 | RadiusNeighbors | Machine Learning | 0.66 | 0.70 | 0.66 | 0.63 | 0.87 | 0.43 | 0.39 | | 29 | ComplementNB | Machine Learning | 0.63 | 0.67 | 0.63 | 0.63 | 0.82 | 0.44 | 0.43 | | 30 | NuSVC | Machine Learning | 0.60 | 0.69 | 0.60 | 0.61 | 0.84 | 0.45 | 0.42 | | 31 | AdaBoost | Machine Learning | 0.62 | 0.59 | 0.62 | 0.58 | 0.80 | 0.38 | 0.37 | | 32 | SGD | Machine Learning | 0.63 | 0.59 | 0.63 | 0.57 | 0.82 | 0.36 | 0.35 | | 33 | BernoulliNB | Machine Learning | 0.59 | 0.62 | 0.59 | 0.55 | 0.77 | 0.32 | 0.31 | | 34 | GaussianNB | Machine Learning | 0.33 | 0.70 | 0.33 | 0.36 | 0.74 | 0.25 | 0.19 | | 35 | QDA | Machine Learning | 0.52 | 0.27 | 0.52 | 0.35 | 0.80 | 0.00 | 0.00 | | 36 | RandomForest | Machine Learning | 0.52 | 0.27 | 0.52 | 0.35 | 0.78 | 0.00 | 0.00 | | 37 | ExtraTreeClassifier | Machine Learning | 0.52 | 0.27 | 0.52 | 0.35 | 0.50 | 0.00 | 0.00 | | 38 | DummyClassifier | Machine Learning | 0.52 | 0.27 | 0.52 | 0.35 | 0.50 | 0.00 | 0.00 | """, "cell_response": """ # Cell Response - Merged Performance Summary This file contains the aggregated and benchmarked results of traditional Machine Learning (ML) and Deep Learning / Transformer architectures for predicting **Cell Response**, sorted hierarchically by **F1 Score** and **Accuracy**. | Rank | Model | Framework | Accuracy | Precision | Recall | F1 | AUC | MCC | Kappa | |---:|---|---|---:|---:|---:|---:|---:|---:|---:| | 1 | HistGradientBoosting | Machine Learning | 0.81 | 0.81 | 0.81 | 0.81 | 0.96 | 0.67 | 0.67 | | 2 | TabICL v2 | Deep Learning / Transformer | 0.79 | 0.79 | 0.79 | 0.79 | 0.97 | 0.65 | 0.65 | | 3 | TabPFN 2.6 | Deep Learning / Transformer | 0.78 | 0.78 | 0.78 | 0.78 | 0.96 | 0.63 | 0.63 | | 4 | XGBoost | Machine Learning | 0.79 | 0.76 | 0.79 | 0.77 | 0.96 | 0.65 | 0.64 | | 5 | Bagging | Machine Learning | 0.77 | 0.78 | 0.77 | 0.77 | 0.95 | 0.62 | 0.62 | | 6 | KNeighbors | Machine Learning | 0.77 | 0.77 | 0.77 | 0.77 | 0.93 | 0.61 | 0.61 | | 7 | LabelPropagation | Machine Learning | 0.77 | 0.77 | 0.77 | 0.77 | 0.93 | 0.61 | 0.61 | | 8 | LabelSpreading | Machine Learning | 0.78 | 0.76 | 0.78 | 0.76 | 0.93 | 0.61 | 0.61 | | 9 | LightGBM | Machine Learning | 0.78 | 0.79 | 0.78 | 0.76 | 0.96 | 0.64 | 0.63 | | 10 | FT Transformer | Deep Learning / Transformer | 0.76 | 0.79 | 0.76 | 0.76 | 0.94 | 0.61 | 0.60 | | 11 | CalibratedClassifierCV | Machine Learning | 0.76 | 0.76 | 0.76 | 0.75 | 0.94 | 0.59 | 0.59 | | 12 | ResNet | Deep Learning / Transformer | 0.76 | 0.76 | 0.76 | 0.75 | 0.94 | 0.59 | 0.59 | | 13 | NODE Lite | Deep Learning / Transformer | 0.76 | 0.75 | 0.76 | 0.75 | 0.95 | 0.59 | 0.59 | | 14 | 1D CNN | Deep Learning / Transformer | 0.75 | 0.76 | 0.75 | 0.74 | 0.95 | 0.59 | 0.59 | | 15 | MLP (DL) | Deep Learning / Transformer | 0.75 | 0.75 | 0.75 | 0.74 | 0.95 | 0.57 | 0.57 | | 16 | LDA | Machine Learning | 0.75 | 0.72 | 0.75 | 0.73 | 0.94 | 0.55 | 0.55 | | 17 | NuSVC | Machine Learning | 0.74 | 0.73 | 0.74 | 0.73 | 0.95 | 0.55 | 0.55 | | 18 | MultinomialNB | Machine Learning | 0.74 | 0.72 | 0.74 | 0.73 | 0.95 | 0.55 | 0.55 | | 19 | LogisticRegression | Machine Learning | 0.74 | 0.70 | 0.74 | 0.71 | 0.95 | 0.54 | 0.54 | | 20 | LinearSVC | Machine Learning | 0.74 | 0.71 | 0.74 | 0.70 | 0.94 | 0.53 | 0.52 | | 21 | Perceptron | Machine Learning | 0.74 | 0.68 | 0.74 | 0.70 | 0.94 | 0.53 | 0.52 | | 22 | QDA | Machine Learning | 0.74 | 0.68 | 0.74 | 0.70 | 0.94 | 0.57 | 0.55 | | 23 | RidgeClassifier | Machine Learning | 0.73 | 0.70 | 0.73 | 0.70 | 0.94 | 0.52 | 0.51 | | 24 | ExtraTrees | Machine Learning | 0.74 | 0.71 | 0.74 | 0.68 | 0.95 | 0.55 | 0.52 | | 25 | ExtraTreeClassifier | Machine Learning | 0.73 | 0.67 | 0.73 | 0.68 | 0.91 | 0.54 | 0.52 | | 26 | AdaBoost | Machine Learning | 0.74 | 0.65 | 0.74 | 0.67 | 0.91 | 0.59 | 0.55 | | 27 | DecisionTree | Machine Learning | 0.74 | 0.65 | 0.74 | 0.67 | 0.92 | 0.59 | 0.55 | | 28 | RandomForest | Machine Learning | 0.74 | 0.65 | 0.74 | 0.67 | 0.92 | 0.59 | 0.55 | | 29 | GradientBoosting | Machine Learning | 0.74 | 0.65 | 0.74 | 0.67 | 0.92 | 0.59 | 0.55 | | 30 | SGD | Machine Learning | 0.71 | 0.65 | 0.71 | 0.66 | 0.92 | 0.49 | 0.47 | | 31 | MLP | Machine Learning | 0.73 | 0.61 | 0.73 | 0.65 | 0.93 | 0.55 | 0.52 | | 32 | PassiveAggressive | Machine Learning | 0.73 | 0.65 | 0.73 | 0.65 | 0.92 | 0.52 | 0.49 | | 33 | ComplementNB | Machine Learning | 0.72 | 0.65 | 0.72 | 0.65 | 0.93 | 0.49 | 0.47 | | 34 | TabNet Lite | Deep Learning / Transformer | 0.70 | 0.67 | 0.70 | 0.65 | 0.93 | 0.48 | 0.46 | | 35 | BernoulliNB | Machine Learning | 0.71 | 0.60 | 0.71 | 0.63 | 0.93 | 0.48 | 0.45 | | 36 | RadiusNeighbors | Machine Learning | 0.60 | 0.36 | 0.60 | 0.45 | 0.50 | 0.00 | 0.00 | | 37 | DummyClassifier | Machine Learning | 0.60 | 0.36 | 0.60 | 0.45 | 0.50 | 0.00 | 0.00 | | 38 | GaussianNB | Machine Learning | 0.28 | 0.69 | 0.28 | 0.34 | 0.84 | 0.18 | 0.14 | """, } GEMINI_MODELS = [ "gemini-3.5-flash", "gemini-3.1-flash-lite", "gemini-3.1-pro-preview", "gemini-3.1-flash-lite-preview", "gemini-3-flash-preview", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite", ] def scaffold_quality_combined(printability, cell_response, weight_printability=0.3, weight_cell_response=0.7): """ Calculates the Weighted Scaffold Synthesis Quality (WSSQ). """ if printability == 0: return 0.0 # Normalization norm_p = printability / 3.0 # If cell_response is 1 (minimum), avoid division by zero in harmonic mean if cell_response <= 1: return 100 * norm_p norm_c = (cell_response - 1) / 4.0 # Weighted Harmonic Mean hm = (weight_printability + weight_cell_response) / ( (weight_printability / norm_p) + (weight_cell_response / norm_c) ) # Weighted Multiplicative Component mc = (norm_p**weight_printability) * (norm_c**weight_cell_response) return 100 * ((hm + mc) / 2.0) PRINT_PARAM_NAMES = [ "Physical Crosslinking Duration (s)", "Photo Crosslinking Duration (s)", "Extrusion Pressure (kPa)", "Nozzle Movement Speed (mm/s)", "Nozzle Diameter (µm)", "Syringe Temperature (°C)", "Substrate Temperature (°C)", ] @st.cache_resource def load_prediction_preprocessors(): return { "preprocessor": joblib.load(PREPROCESSOR_DIR / "preprocessor.pkl"), "feature_cols": joblib.load(PREPROCESSOR_DIR / "feature_cols.pkl"), "printability_encoder": joblib.load(PREPROCESSOR_DIR / MODEL_TASKS["printability"]["label_encoder"]), "cell_response_encoder": joblib.load(PREPROCESSOR_DIR / MODEL_TASKS["cell_response"]["label_encoder"]), } def model_display_name(path, prefix): name = path.stem.replace("_model", "") if name.startswith(prefix): name = name[len(prefix):] return f"{name} ({path.suffix.lstrip('.')})" def model_rank_key(display_name, task_key): base_name = display_name.rsplit(" (", 1)[0] rank_aliases = { "1D_CNN": "1D CNN", "FT_Transformer": "FT Transformer", "NODE_Lite": "NODE Lite", "TabNet_Lite": "TabNet Lite", "TabPFN_2.6": "TabPFN 2.6", "TabICL_v2": "TabICL v2", "ExtraTree": "ExtraTreeClassifier", } rank_name = rank_aliases.get(base_name, base_name) if base_name == "MLP" and display_name.endswith("(pth)"): rank_name = "MLP DL" ranking = MODEL_RANKINGS[task_key] rank = ranking.index(rank_name) if rank_name in ranking else len(ranking) return rank, base_name.lower(), display_name def discover_model_options(task_key): task = MODEL_TASKS[task_key] files = [] for suffix in ("*.pkl", "*.joblib", "*.pth"): files.extend(task["folder"].glob(suffix)) options = {model_display_name(path, task["prefix"]): str(path) for path in files} return dict(sorted(options.items(), key=lambda item: model_rank_key(item[0], task_key))) def parse_architecture(path, task_key): stem = Path(path).stem.replace("_model", "") prefix = MODEL_TASKS[task_key]["prefix"] return stem[len(prefix):] if stem.startswith(prefix) else stem def build_torch_model(architecture, input_dim, out_dim, cfg): import torch import torch.nn as nn activation_funcs = { "relu": nn.ReLU, "tanh": nn.Tanh, "GELU": nn.GELU, "SELU": nn.SELU, "ELU": nn.ELU, "SiLU": nn.SiLU, } act = activation_funcs[cfg["activation_func"]] class ResidualBlock(nn.Module): def __init__(self): super().__init__() self.linear = nn.Linear(cfg["hidden_dim"], cfg["hidden_dim"]) self.bn = nn.BatchNorm1d(cfg["hidden_dim"]) self.act = act() self.dropout = nn.Dropout(cfg["dropout"]) def forward(self, x): return x + self.dropout(self.act(self.bn(self.linear(x)))) class TissueResNet(nn.Module): def __init__(self): super().__init__() self.input_layer = nn.Sequential( nn.Linear(input_dim, cfg["hidden_dim"]), nn.BatchNorm1d(cfg["hidden_dim"]), act(), ) self.blocks = nn.ModuleList([ResidualBlock() for _ in range(cfg["n_layers"])]) self.output_layer = nn.Linear(cfg["hidden_dim"], out_dim) def forward(self, x): x = self.input_layer(x) for block in self.blocks: x = block(x) return self.output_layer(x) class StandardMLP(nn.Module): def __init__(self): super().__init__() layers = [] in_dim = input_dim for _ in range(cfg["n_layers"]): layers.extend([ nn.Linear(in_dim, cfg["hidden_dim"]), nn.BatchNorm1d(cfg["hidden_dim"]), act(), nn.Dropout(cfg["dropout"]), ]) in_dim = cfg["hidden_dim"] layers.append(nn.Linear(cfg["hidden_dim"], out_dim)) self.network = nn.Sequential(*layers) def forward(self, x): return self.network(x) class Tabular1DCNN(nn.Module): def __init__(self): super().__init__() layers = [] in_channels = 1 for _ in range(cfg["n_layers"]): layers.extend([ nn.Conv1d(in_channels, cfg["hidden_dim"], kernel_size=3, padding=1), nn.BatchNorm1d(cfg["hidden_dim"]), act(), nn.Dropout(cfg["dropout"]), ]) in_channels = cfg["hidden_dim"] self.conv_net = nn.Sequential(*layers) self.pool = nn.AdaptiveAvgPool1d(1) self.fc = nn.Linear(cfg["hidden_dim"], out_dim) def forward(self, x): x = self.conv_net(x.unsqueeze(1)) return self.fc(self.pool(x).squeeze(2)) class FTTransformer(nn.Module): def __init__(self): super().__init__() self.d_token = max(4, (cfg["hidden_dim"] // 4) * 4) self.feature_embeddings = nn.ModuleList([nn.Linear(1, self.d_token) for _ in range(input_dim)]) self.cls_token = nn.Parameter(torch.randn(1, 1, self.d_token)) encoder_layer = nn.TransformerEncoderLayer( d_model=self.d_token, nhead=4, dropout=cfg["dropout"], batch_first=True ) self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=cfg["n_layers"]) self.fc = nn.Linear(self.d_token, out_dim) def forward(self, x): batch_size = x.size(0) tokens = [self.feature_embeddings[i](x[:, i:i+1]).unsqueeze(1) for i in range(x.size(1))] x_emb = torch.cat([self.cls_token.expand(batch_size, -1, -1)] + tokens, dim=1) return self.fc(self.transformer(x_emb)[:, 0, :]) class TabNetLite(nn.Module): def __init__(self): super().__init__() self.n_steps = max(1, cfg["n_layers"]) self.initial_bn = nn.BatchNorm1d(input_dim) self.transformers = nn.ModuleList([ nn.Sequential( nn.Linear(input_dim, cfg["hidden_dim"]), nn.BatchNorm1d(cfg["hidden_dim"]), act(), nn.Dropout(cfg["dropout"]), ) for _ in range(self.n_steps) ]) self.attentions = nn.ModuleList([ nn.Sequential( nn.Linear(cfg["hidden_dim"], input_dim), nn.BatchNorm1d(input_dim), nn.Softmax(dim=-1), ) for _ in range(self.n_steps) ]) self.fc_out = nn.Linear(cfg["hidden_dim"], out_dim) def forward(self, x): x = self.initial_bn(x) out_agg, prior = 0, torch.ones_like(x) feat_rep = self.transformers[0](x) for step in range(self.n_steps): mask = self.attentions[step](feat_rep) * prior prior = prior * (1.0 - mask) feat_rep = self.transformers[step](x * mask) out_agg += feat_rep return self.fc_out(out_agg) class NeuralDecisionForest(nn.Module): def __init__(self): super().__init__() self.n_trees = max(1, cfg["hidden_dim"] // 16) self.depth = max(2, cfg["n_layers"] + 1) self.n_leaves = 2 ** self.depth self.trees = nn.ModuleList([ nn.Sequential( nn.Linear(input_dim, self.n_leaves), nn.Dropout(cfg["dropout"]), nn.Softmax(dim=-1), ) for _ in range(self.n_trees) ]) self.leaf_weights = nn.Parameter(torch.randn(self.n_trees, self.n_leaves, out_dim)) def forward(self, x): out = 0 for i, tree in enumerate(self.trees): out += torch.matmul(tree(x), self.leaf_weights[i]) return out / self.n_trees builders = { "ResNet": TissueResNet, "MLP": StandardMLP, "1D_CNN": Tabular1DCNN, "FT_Transformer": FTTransformer, "TabNet_Lite": TabNetLite, "NODE_Lite": NeuralDecisionForest, } return builders[architecture]() @st.cache_resource def load_prediction_model(path, task_key, input_dim, out_dim): path = Path(path) if path.suffix in {".pkl", ".joblib"}: return {"kind": "sklearn", "model": joblib.load(path)} if path.suffix == ".pth": import torch architecture = parse_architecture(path, task_key) cfg = DL_MODEL_CONFIGS.get((task_key, architecture)) if cfg is None: raise ValueError(f"No architecture configuration found for {path.name}.") model = build_torch_model(architecture, input_dim, out_dim, cfg) state_dict = torch.load(path, map_location="cpu") model.load_state_dict(state_dict) model.eval() return {"kind": "torch", "model": model} raise ValueError(f"Unsupported model file: {path.name}") def softmax(values): values = np.asarray(values, dtype=float) values = values - np.max(values) exp_values = np.exp(values) return exp_values / exp_values.sum() def expected_class_value(model_bundle, x_raw, preprocessor, label_encoder): x_model = preprocessor.transform(x_raw).astype(np.float32) if model_bundle["kind"] == "torch": import torch with torch.no_grad(): logits = model_bundle["model"](torch.tensor(x_model, dtype=torch.float32)).numpy()[0] probs = softmax(logits) labels = label_encoder.classes_.astype(float) return float(np.dot(probs, labels)) model = model_bundle["model"] if hasattr(model, "predict_proba"): probs = model.predict_proba(x_model)[0] classes = np.asarray(model.classes_, dtype=int) labels = label_encoder.inverse_transform(classes).astype(float) return float(np.dot(probs, labels)) if hasattr(model, "decision_function"): scores = np.asarray(model.decision_function(x_model)[0]) if scores.ndim == 0: p_high = 1.0 / (1.0 + np.exp(-scores)) probs = np.array([1.0 - p_high, p_high]) else: probs = softmax(scores) classes = np.asarray(model.classes_, dtype=int) labels = label_encoder.inverse_transform(classes).astype(float) return float(np.dot(probs, labels)) pred = np.asarray(model.predict(x_model), dtype=int) return float(label_encoder.inverse_transform(pred)[0]) prediction_assets = load_prediction_preprocessors() preprocessor = prediction_assets["preprocessor"] feature_cols = prediction_assets["feature_cols"] label_encoder_print = prediction_assets["printability_encoder"] label_encoder_cell = prediction_assets["cell_response_encoder"] sample_for_shape = {col: 0.0 for col in feature_cols} sample_for_shape["Cell Line"] = CELL_LINE_OPTIONS[0] preprocessed_input_dim = preprocessor.transform(pd.DataFrame([sample_for_shape])[feature_cols]).shape[1] @st.dialog("Optimization Trials") def show_trial_guidance(): st.markdown( """ The trial count controls how many candidate scaffold settings Optuna tests before choosing the best WSSQ. **Recommended range:** 100-1000 trials. **Default:** 300 trials, which is a balanced choice for normal use. **Runtime impact:** running time grows roughly in proportion to the number of trials. Use 50-100 for a quick test, 300 for a balanced run, and 500-1000 when you want a more thorough search and can wait longer. """ ) @st.dialog("Weighted Synergistic Scaffold Quality (WSSQ)", width="large") def show_wssq_guidance(): st.markdown( """ WSSQ is the optimization score used in MLATE to combine **printability** and **cell response** into one scaffold-quality objective. WSSQ was introduced to handle two practical needs: acellular 3D-printed scaffolds, where cell response is not applicable in the same way, and bioprinted scaffolds, where biological response is central to scaffold quality. The app normalizes printability and cell response, then combines them using two components: - **Weighted harmonic mean:** rewards balanced high values and penalizes weak performance in either target. - **Weighted multiplicative component:** captures synergy between printability and cell response. The final WSSQ is scaled from 0 to 100%. If printability is 0, WSSQ is 0. If cell response is at the minimum biological-response level, the score falls back to normalized printability. Otherwise, both targets contribute according to the sidebar weights. Practically, a scaffold with excellent cell response but poor printability can score lower than a scaffold with slightly lower cell response but better balance, because WSSQ is designed to favor experimentally useful, well-balanced scaffold candidates. """ ) @st.dialog("Model Selection Guide", width="large") def show_model_selection_guidance(): st.markdown( """ Models are ranked by F1 score and accuracy. The highest-ranked available model appears first in each dropdown. Use the top-ranked models when you want the strongest benchmarked predictive performance. If a selected model fails to load because of local package-version incompatibility, choose the next ranked model in the same task until the environment is aligned with the model artifacts. """ ) tab_print, tab_cell = st.tabs(["Printability", "Cell Response"]) with tab_print: st.markdown(PERFORMANCE_GUIDE["printability"]) with st.expander("Show more"): st.markdown(PERFORMANCE_GUIDE_FULL["printability"]) with tab_cell: st.markdown(PERFORMANCE_GUIDE["cell_response"]) with st.expander("Show more"): st.markdown(PERFORMANCE_GUIDE_FULL["cell_response"]) @st.dialog("How to Get a Gemini API Key") def show_gemini_api_key_guidance(): st.markdown( """ To generate a fabrication procedure, you need a Gemini API key from Google. Creating a key only takes a minute, and Google provides a free tier. 1. Open [Google AI Studio API Keys](https://aistudio.google.com/app/apikey) and sign in with your Google/Gmail account if prompted. 2. If this is your first visit, accept the terms of service and continue. 3. Click **Get API key** or **Create API key**. 4. Choose an existing Google Cloud project, or select **Create API key in new project**. 5. Copy the generated key, return to this app, and paste it into the **Gemini API Key** box. **Important:** Treat your API key like a password. Do not share it publicly or paste it into files that will be uploaded online. This app uses your key only for the current protocol generation request and does not save it. """ ) if 'bio_rows' not in st.session_state: st.session_state.bio_rows = [{ 'mat': BIOMATERIAL_OPTIONS[0], 'min': 0.0, 'max': 10.0, 'step': 0.1 }] if 'density_range' not in st.session_state: st.session_state.density_range = {'min': 0.0, 'max': 10.0, 'step': 0.1} if 'pp_ranges' not in st.session_state: st.session_state.pp_ranges = { "Physical Crosslinking Duration (s)": {'min': 0.0, 'max': 300.0, 'step': 5.0}, "Photo Crosslinking Duration (s)": {'min': 0.0, 'max': 180.0, 'step': 5.0}, "Extrusion Pressure (kPa)": {'min': 5.0, 'max': 200.0, 'step': 5.0}, "Nozzle Movement Speed (mm/s)": {'min': 1.0, 'max': 20.0, 'step': 0.5}, "Nozzle Diameter (µm)": {'min': 100.0,'max': 1000.0, 'step': 50.0}, "Syringe Temperature (°C)": {'min': 4.0, 'max': 40.0, 'step': 1.0}, "Substrate Temperature (°C)": {'min': 4.0, 'max': 37.0, 'step': 1.0}, } # --- Sidebar UI for Weights --- st.sidebar.header("Optimization Weights") # User only controls Cell Response (0 to 100) w_cell_pct = st.sidebar.slider("Cell Response Weight (%)", min_value=0, max_value=100, value=70, step=5) # Printability is dynamically calculated and cannot be changed manually w_print_pct = 100 - w_cell_pct st.sidebar.number_input("Printability Weight (%)", value=w_print_pct, disabled=True, help="Auto-calculated to ensure sum is 100%") if st.sidebar.button("What is WSSQ?", use_container_width=True): show_wssq_guidance() # Convert back to 0.0 - 1.0 for the mathematical formula w_cell = w_cell_pct / 100.0 w_print = w_print_pct / 100.0 print_model_options = discover_model_options("printability") cell_model_options = discover_model_options("cell_response") if not print_model_options or not cell_model_options: st.error("No selectable prediction models were found in the models folder.") st.stop() selected_print_model_name = st.sidebar.selectbox( "Printability Model", list(print_model_options.keys()), key="printability_model_select", ) selected_cell_model_name = st.sidebar.selectbox( "Cell Response Model", list(cell_model_options.keys()), key="cell_response_model_select", ) if st.sidebar.button("Model Selection Guide", use_container_width=True): show_model_selection_guidance() n_trials = st.sidebar.number_input( "Optimization Trials", min_value=10, max_value=10000, value=300, step=50, help="Number of Optuna trials used when you click Optimize WSSQ.", ) if st.sidebar.button("Trial Count Help", use_container_width=True): show_trial_guidance() gemini_key = st.sidebar.text_input( "Gemini API Key", value=os.getenv("GEMINI_API_KEY", ""), type="password", help="Used only when generating the LLM-based fabrication procedure.", ) if st.sidebar.button("How to Get API Key", use_container_width=True): show_gemini_api_key_guidance() gemini_model = st.sidebar.selectbox( "Gemini Model", GEMINI_MODELS, index=0, key="gemini_model_select", ) st.title("MLATE: Machine Learning Applications in Tissue Engineering") st.markdown( "

" "A Data-driven Cross-tissue Machine Learning Framework for Inverse design of 3D (Bio)printing Scaffolds " "For more details, please refer to and cite our paper: " "https://doi.org/xxx" "

", unsafe_allow_html=True ) st.subheader("Biomaterials (enter range for each)") if st.button("➕ Add Biomaterial"): used = {r['mat'] for r in st.session_state.bio_rows} available = [m for m in BIOMATERIAL_OPTIONS if m not in used] if available: st.session_state.bio_rows.append({ 'mat': available[0], 'min': 0.0, 'max': 10.0, 'step': 0.1 }) st.rerun() for i, row in enumerate(st.session_state.bio_rows): used_except_current = { r['mat'] for idx, r in enumerate(st.session_state.bio_rows) if idx != i } options = [m for m in BIOMATERIAL_OPTIONS if m not in used_except_current] c1, c2, c3, c4, c5 = st.columns([2, 1, 1, 1, 0.3]) mat = c1.selectbox( "Biomaterial", options, index=options.index(row['mat']) if row['mat'] in options else 0, key=f"bio_mat_{i}", label_visibility="collapsed", ) st.session_state.bio_rows[i]['mat'] = mat mn = c2.number_input( "Min", min_value=0.0, max_value=row['max'], value=row['min'], step=row['step'], key=f"bio_min_{i}" ) mx = c3.number_input( "Max", min_value=row['step'], max_value=100.0, value=max(row['max'], row['step']), step=row['step'], key=f"bio_max_{i}" ) st.session_state.bio_rows[i].update(min=mn, max=mx) st.session_state.bio_rows[i]['step'] = c4.number_input( "Step", min_value=0.0, max_value=(mx - mn) if mx > mn else 0.1, value=row['step'], step=0.1, key=f"bio_step_{i}" ) if c5.button("❌", key=f"rem_{i}"): st.session_state.bio_rows.pop(i) st.rerun() st.markdown("---") st.subheader("Cell Line & Density (10^6 cells/ml)") col1, col2, col3, col4 = st.columns([2,1,1,1]) cell_line = col1.selectbox("Cell Line", CELL_LINE_OPTIONS, key="cell_line_select") if cell_line == "NoCellCultured": st.info("🧪 **Acellular 3D printing mode** – No cells will be included. Cell density is forced to 0.") st.session_state.density_range.update({'min': 0.0, 'max': 0.0, 'step': 0.0}) col2.number_input("Min Density", value=0.0, disabled=True, key="cd_min") col3.number_input("Max Density", value=0.0, disabled=True, key="cd_max") col4.number_input("Step", value=0.0, disabled=True, key="cd_step") else: if st.session_state.density_range.get('max', 0) <= 0.1: st.session_state.density_range.update({'min': 1.0, 'max': 20.0, 'step': 0.5}) dr = st.session_state.density_range dmin = col2.number_input( "Min Density", min_value=0.0, max_value=dr['max'], value=dr['min'], step=dr['step'], key="cd_min" ) dmax = col3.number_input( "Max Density", min_value=dr['step'], max_value=1000.0, value=max(dr['max'], dr['step']), step=dr['step'], key="cd_max" ) dstep = col4.number_input( "Step", min_value=0.0, max_value=(dmax - dmin) if dmax > dmin else 0.1, value=dr['step'], step=0.1, key="cd_step" ) st.session_state.density_range.update({'min': dmin, 'max': dmax, 'step': dstep}) st.markdown("---") st.subheader("Crosslinking Settings") col_cross1, col_cross2 = st.columns(2) disable_physical = col_cross1.checkbox( "Disable Physical Crosslinking", value=False, help="Check if you do not want physical/ionic crosslinking (e.g. CaCl₂ bath, temperature-induced)" ) disable_photo = col_cross2.checkbox( "Disable Photo Crosslinking", value=False, help="Check if you do not want UV/visible light crosslinking" ) st.subheader("Printing Parameters (enter range)") for name in PRINT_PARAM_NAMES: if name == "Physical Crosslinking Duration (s)" and disable_physical: st.session_state.pp_ranges[name].update({'min': 0.0, 'max': 0.0, 'step': 0.0}) c1, c2, c3, c4 = st.columns([2,1,1,1]) c1.write(name + " (DISABLED)") c2.number_input("Min", value=0.0, disabled=True, key=f"pp_min_{name}") c3.number_input("Max", value=0.0, disabled=True, key=f"pp_max_{name}") c4.number_input("Step", value=0.0, disabled=True, key=f"pp_step_{name}") continue elif name == "Photo Crosslinking Duration (s)" and disable_photo: st.session_state.pp_ranges[name].update({'min': 0.0, 'max': 0.0, 'step': 0.0}) c1, c2, c3, c4 = st.columns([2,1,1,1]) c1.write(name + " (DISABLED)") c2.number_input("Min", value=0.0, disabled=True, key=f"pp_min_{name}") c3.number_input("Max", value=0.0, disabled=True, key=f"pp_max_{name}") c4.number_input("Step", value=0.0, disabled=True, key=f"pp_step_{name}") continue pmin = st.session_state.pp_ranges[name]['min'] pmax = st.session_state.pp_ranges[name]['max'] pstep = st.session_state.pp_ranges[name]['step'] c1, c2, c3, c4 = st.columns([2,1,1,1]) c1.write(name) pmin = c2.number_input( "Min", min_value=0.0, max_value=pmax, value=pmin, step=pstep, key=f"pp_min_{name}" ) pmax = c3.number_input( "Max", min_value=pstep, max_value=10000.0, value=max(pmax, pstep), step=pstep, key=f"pp_max_{name}" ) pstep = c4.number_input( "Step", min_value=0.0, max_value=(pmax - pmin) if pmax > pmin else 1.0, value=pstep, step=max(1e-3, pstep/10), key=f"pp_step_{name}" ) st.session_state.pp_ranges[name].update(min=pmin, max=pmax, step=pstep) st.markdown("---") if st.button("Optimize WSSQ"): with st.spinner("Running Optuna…"): try: model_print = load_prediction_model( print_model_options[selected_print_model_name], "printability", preprocessed_input_dim, len(label_encoder_print.classes_), ) model_cell = load_prediction_model( cell_model_options[selected_cell_model_name], "cell_response", preprocessed_input_dim, len(label_encoder_cell.classes_), ) except Exception as exc: st.error( "Could not load the selected prediction model. This model will only work when the " "running environment matches the saved artifact dependencies. For HistGradientBoosting, " "use numpy>=2.0 in the Python environment that runs Streamlit.\n\n" f"Details:\n{exc}" ) st.stop() def objective(trial): bi_vals = { r['mat']: trial.suggest_float( f"bio__{r['mat']}", r['min'], r['max'], step=r['step'] ) for r in st.session_state.bio_rows } for m in BIOMATERIAL_OPTIONS: bi_vals.setdefault(m, 0.0) cd = 0.0 if cell_line=="NoCellCultured" else trial.suggest_float( "cell_density", dr['min'], dr['max'], step=dr['step'] ) pp_vals = { name: trial.suggest_float( f"pp__{name}", st.session_state.pp_ranges[name]['min'], st.session_state.pp_ranges[name]['max'], step=st.session_state.pp_ranges[name]['step'] ) for name in PRINT_PARAM_NAMES } feat = {**bi_vals, **pp_vals} feat["Cell Density (cells/mL)"] = cd feat["Cell Line"] = cell_line X = pd.DataFrame([feat]).reindex(columns=feature_cols, fill_value=0.0) exp_p = expected_class_value(model_print, X, preprocessor, label_encoder_print) exp_c = expected_class_value(model_cell, X, preprocessor, label_encoder_cell) np.random.seed(42) # Use dynamic weights from the sidebar sliders return scaffold_quality_combined( exp_p, exp_c, weight_printability=w_print, weight_cell_response=w_cell ) sampler = optuna.samplers.TPESampler( seed=42, n_startup_trials=30, multivariate=True, group=True, consider_prior=True ) study = optuna.create_study( direction="maximize", sampler=sampler, pruner=optuna.pruners.MedianPruner() ) study.optimize(objective, n_trials=int(n_trials)) # Store results in session state to persist after rerun st.session_state.best_params = study.best_trial.params st.session_state.best_value = study.best_trial.value st.session_state.optimized_cell_line = cell_line if 'best_params' in st.session_state: st.success(f"Best WSSQ: **{st.session_state.best_value:.3f}**") best_df = pd.Series(st.session_state.best_params, name="value") \ .rename_axis("parameter") \ .to_frame() st.table(best_df) st.markdown("---") st.subheader("Customize Fabrication Protocol") user_inquiry = st.text_area( "Add specific limitations, equipment, or extra requirements:", placeholder="e.g., I only have a 25G nozzle available, or I need to use a specific UV intensity of 10mW/cm²...", key="user_inquiry" ) if st.button("Generate Fabrication Procedure"): if not gemini_key: st.error("Please enter your Gemini API key in the sidebar before generating a fabrication procedure.") st.stop() with st.spinner("Generating rigorous fabrication procedure…"): client = genai.Client(api_key=gemini_key) formatted_params = "\n".join([ f"- {k.replace('bio__', 'Biomaterial: ').replace('pp__', 'Print Setting: ')}: {v:.2f}" for k, v in st.session_state.best_params.items() ]) # Base prompt (remains unchanged) base_prompt = ( f"Please act as a senior tissue engineer with 15+ years of hands-on experience in 3D bioprinting for regenerative medicine. " f"Write a **highly practical, bench-ready laboratory fabrication protocol** for fabricating a scaffold. " f"Assume the reader is an experienced experimentalist who routinely works in a tissue engineering lab.\n\n" f"**Use exactly these inputs to tailor every step:**\n" f"Target Cell Line: {st.session_state.optimized_cell_line}\n" f"Parameters:\n{formatted_params}\n\n" f"**Critical requirements for the protocol (you MUST follow all of them):**\n" f"• If Target Cell Line is 'NoCellCultured', this is **acellular 3D printing** (not bioprinting). Remove all references to cells, cell viability, cell density, and cell culturing. The final scaffold is cell-free. Change section 6 title to 'Post-Printing Incubation & Storage Instructions' and adapt its content accordingly.\n" f"• If any suggested parameter is physically unrealistic (e.g. nozzle diameter 9 µm or syringe temp 2°C) or the nozzle diameter is very small relative to the cell diameter of the target cell line (when cells are used), adjust it slightly in the protocol and explicitly note the adjustment with justification.\n" f"• Every quantity must be given in precise, measurable lab units (e.g., 2.5 mL, 1.2 % w/v, 10 mg/mL, 37 °C, 5 min, 150 rpm).\n" f"• Include exact timings, temperatures, and workflow order to protect structural fidelity (and cell viability >85 % post-print when cells are used).\n" f"• Anticipate and explicitly address common bioprinting pitfalls relevant to the given parameters (nozzle clogging, shear-induced cell death, premature gelation, filament fusion, air bubbles, etc.) and give precise mitigation steps.\n" f"• Use only reagents and equipment that are standard in tissue engineering labs; if a specific brand/model is implied by the parameters, note a common equivalent.\n" f"• Include simple quality-control checkpoints (visual inspection, live/dead staining timing when cells are used, etc.).\n\n" f"Your response must be structured **exactly** with the following sections (no extra sections, no introductory text, no summary, no conclusions):\n" f"1. Required Materials & Equipment\n" f"2. Sterilization & Safety Precautions\n" f"3. Bioink Preparation\n" f"4. 3D Bioprinting Settings & Execution\n" f"5. Post-processing & Crosslinking\n" f"6. Cell Culturing & Incubation Instructions\n" ) # Append user inquiry if provided final_prompt = base_prompt if user_inquiry: final_prompt += f"\n**Additional User Constraints & Inquiries (Integrate these into the protocol):**\n{user_inquiry}" resp = client.models.generate_content( model=gemini_model, contents=final_prompt, config=types.GenerateContentConfig( system_instruction=( "You are a senior tissue engineer and expert experimentalist specializing in translating optimized bioprinting parameters into reproducible, high-viability laboratory protocols. " "Your protocols are used daily by PhD students and post-docs in regenerative medicine labs. " "You always prioritize: (1) maximum cell viability and function, (2) structural fidelity of the printed construct, (3) workflow efficiency under sterile conditions, and (4) safety. " "Write in clear, imperative, step-by-step language with numbered or bulleted sub-steps. " "Never be vague — give exact volumes, times, temperatures, speeds, and concentrations. " "Never add disclaimers or theoretical background unless explicitly asked." ), temperature=0.1, top_p=0.85, max_output_tokens=6144 ) ) st.markdown("## Fabrication Procedure") st.markdown(resp.text)