Saeed commited on
Commit
d723d5e
·
1 Parent(s): 3cc5d7f

Update MLATE model selection and optimization app

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +2 -0
  2. app.py +608 -39
  3. catboost_printability.cbm → models/cell response/Cell_Response_1D_CNN_model.pth +2 -2
  4. models/cell response/Cell_Response_AdaBoost.pkl +3 -0
  5. models/cell response/Cell_Response_Bagging.pkl +3 -0
  6. cell_line_encoder.joblib → models/cell response/Cell_Response_BernoulliNB.pkl +2 -2
  7. models/cell response/Cell_Response_CalibratedClassifierCV.pkl +3 -0
  8. models/cell response/Cell_Response_ComplementNB.pkl +3 -0
  9. scaler_printability.joblib → models/cell response/Cell_Response_DecisionTree.pkl +2 -2
  10. scaler_cell_response.joblib → models/cell response/Cell_Response_DummyClassifier.pkl +2 -2
  11. models/cell response/Cell_Response_ExtraTree.pkl +3 -0
  12. models/cell response/Cell_Response_ExtraTrees.pkl +3 -0
  13. catboost_cell_response.cbm → models/cell response/Cell_Response_FT_Transformer_model.pth +2 -2
  14. models/cell response/Cell_Response_GaussianNB.pkl +3 -0
  15. models/cell response/Cell_Response_GradientBoosting.pkl +3 -0
  16. models/cell response/Cell_Response_HistGradientBoosting.pkl +3 -0
  17. models/cell response/Cell_Response_KNeighbors.pkl +3 -0
  18. models/cell response/Cell_Response_LDA.pkl +3 -0
  19. models/cell response/Cell_Response_LabelPropagation.pkl +3 -0
  20. models/cell response/Cell_Response_LabelSpreading.pkl +3 -0
  21. models/cell response/Cell_Response_LightGBM.pkl +3 -0
  22. models/cell response/Cell_Response_LinearSVC.pkl +3 -0
  23. models/cell response/Cell_Response_LogisticRegression.pkl +3 -0
  24. models/cell response/Cell_Response_MLP.pkl +3 -0
  25. models/cell response/Cell_Response_MLP_model.pth +3 -0
  26. models/cell response/Cell_Response_MultinomialNB.pkl +3 -0
  27. models/cell response/Cell_Response_NODE_Lite_model.pth +3 -0
  28. models/cell response/Cell_Response_NuSVC.pkl +3 -0
  29. models/cell response/Cell_Response_PassiveAggressive.pkl +3 -0
  30. models/cell response/Cell_Response_Perceptron.pkl +3 -0
  31. models/cell response/Cell_Response_QDA.pkl +3 -0
  32. models/cell response/Cell_Response_RadiusNeighbors.pkl +3 -0
  33. models/cell response/Cell_Response_RandomForest.pkl +3 -0
  34. models/cell response/Cell_Response_ResNet_model.pth +3 -0
  35. models/cell response/Cell_Response_RidgeClassifier.pkl +3 -0
  36. models/cell response/Cell_Response_SGD.pkl +3 -0
  37. models/cell response/Cell_Response_TabICL_v2.joblib +3 -0
  38. models/cell response/Cell_Response_TabNet_Lite_model.pth +3 -0
  39. models/cell response/Cell_Response_TabPFN_2.6.joblib +3 -0
  40. models/cell response/Cell_Response_XGBoost.pkl +3 -0
  41. models/preprocessors/feature_cols.pkl +3 -0
  42. models/preprocessors/label_encoder_cell_response.pkl +3 -0
  43. models/preprocessors/label_encoder_printability.pkl +3 -0
  44. models/preprocessors/preprocessor.pkl +3 -0
  45. models/printability/Printability_1D_CNN_model.pth +3 -0
  46. models/printability/Printability_AdaBoost.pkl +3 -0
  47. models/printability/Printability_Bagging.pkl +3 -0
  48. models/printability/Printability_BernoulliNB.pkl +3 -0
  49. models/printability/Printability_CalibratedClassifierCV.pkl +3 -0
  50. models/printability/Printability_ComplementNB.pkl +3 -0
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ __pycache__/
2
+ .venv-test/
app.py CHANGED
@@ -1,10 +1,10 @@
1
  import os
 
2
  import streamlit as st
3
  import pandas as pd
4
  import joblib
5
  import optuna
6
  import numpy as np
7
- from catboost import CatBoostClassifier
8
 
9
  from google import genai
10
  from google.genai import types
@@ -33,12 +33,197 @@ def safe_number_input(label, **kwargs):
33
 
34
  st.number_input = safe_number_input
35
 
36
- gemini_key = os.getenv("GEMINI_API_KEY")
37
- if not gemini_key:
38
- st.error("Gemini API key not found. Please set GEMINI_API_KEY in your Space settings.")
39
- st.stop()
40
-
41
- client = genai.Client(api_key=gemini_key)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
  def scaffold_quality_combined(printability, cell_response,
44
  weight_printability=0.3, weight_cell_response=0.7):
@@ -79,28 +264,342 @@ PRINT_PARAM_NAMES = [
79
  ]
80
 
81
  @st.cache_resource
82
- def load_encoder():
83
- return joblib.load('cell_line_encoder.joblib')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
  @st.cache_resource
86
- def load_scalers():
87
- return (
88
- joblib.load('scaler_printability.joblib'),
89
- joblib.load('scaler_cell_response.joblib')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  )
91
 
92
- @st.cache_resource
93
- def load_models():
94
- m_p = CatBoostClassifier(); m_p.load_model('catboost_printability.cbm')
95
- m_c = CatBoostClassifier(); m_c.load_model('catboost_cell_response.cbm')
96
- return m_p, m_c
 
 
 
 
 
 
 
97
 
98
- encoder = load_encoder()
99
- scaler_print, scaler_cell = load_scalers()
100
- model_print, model_cell = load_models()
101
 
102
- feature_order_print = list(scaler_print.feature_names_in_)
103
- feature_order_cell = list(scaler_cell.feature_names_in_)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
  if 'bio_rows' not in st.session_state:
106
  st.session_state.bio_rows = [{
@@ -129,11 +628,60 @@ w_cell_pct = st.sidebar.slider("Cell Response Weight (%)", min_value=0, max_valu
129
  # Printability is dynamically calculated and cannot be changed manually
130
  w_print_pct = 100 - w_cell_pct
131
  st.sidebar.number_input("Printability Weight (%)", value=w_print_pct, disabled=True, help="Auto-calculated to ensure sum is 100%")
 
 
132
 
133
  # Convert back to 0.0 - 1.0 for the mathematical formula
134
  w_cell = w_cell_pct / 100.0
135
  w_print = w_print_pct / 100.0
136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  st.title("MLATE: Machine Learning Applications in Tissue Engineering")
138
  st.markdown(
139
  "<p style='font-size:1.2em; color:grey;'>"
@@ -162,9 +710,10 @@ for i, row in enumerate(st.session_state.bio_rows):
162
 
163
  c1, c2, c3, c4, c5 = st.columns([2, 1, 1, 1, 0.3])
164
  mat = c1.selectbox(
165
- "", options,
166
  index=options.index(row['mat']) if row['mat'] in options else 0,
167
- key=f"bio_mat_{i}"
 
168
  )
169
  st.session_state.bio_rows[i]['mat'] = mat
170
 
@@ -299,6 +848,28 @@ st.markdown("---")
299
 
300
  if st.button("Optimize WSSQ"):
301
  with st.spinner("Running Optuna…"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  def objective(trial):
303
  bi_vals = {
304
  r['mat']: trial.suggest_float(
@@ -325,19 +896,12 @@ if st.button("Optimize WSSQ"):
325
 
326
  feat = {**bi_vals, **pp_vals}
327
  feat["Cell Density (cells/mL)"] = cd
328
- feat.update(
329
- encoder.transform(pd.DataFrame({"Cell Line":[cell_line]}))
330
- .iloc[0].to_dict()
331
- )
332
 
333
- X = pd.DataFrame([feat])
334
- Xp = X.reindex(columns=feature_order_print, fill_value=0.0)
335
- Xc = X.reindex(columns=feature_order_cell, fill_value=0.0)
336
 
337
- p_proba = model_print.predict_proba(scaler_print.transform(Xp))[0]
338
- c_proba = model_cell .predict_proba(scaler_cell .transform(Xc))[0]
339
- exp_p = float(np.dot(p_proba, model_print.classes_.astype(float)))
340
- exp_c = float(np.dot(c_proba, model_cell .classes_.astype(float)))
341
 
342
  np.random.seed(42)
343
  # Use dynamic weights from the sidebar sliders
@@ -361,7 +925,7 @@ if st.button("Optimize WSSQ"):
361
  sampler=sampler,
362
  pruner=optuna.pruners.MedianPruner()
363
  )
364
- study.optimize(objective, n_trials=1000)
365
 
366
  # Store results in session state to persist after rerun
367
  st.session_state.best_params = study.best_trial.params
@@ -384,7 +948,12 @@ if 'best_params' in st.session_state:
384
  )
385
 
386
  if st.button("Generate Fabrication Procedure"):
 
 
 
 
387
  with st.spinner("Generating rigorous fabrication procedure…"):
 
388
 
389
  formatted_params = "\n".join([
390
  f"- {k.replace('bio__', 'Biomaterial: ').replace('pp__', 'Print Setting: ')}: {v:.2f}"
@@ -422,7 +991,7 @@ if 'best_params' in st.session_state:
422
  final_prompt += f"\n**Additional User Constraints & Inquiries (Integrate these into the protocol):**\n{user_inquiry}"
423
 
424
  resp = client.models.generate_content(
425
- model="gemini-2.5-flash-lite",
426
  contents=final_prompt,
427
  config=types.GenerateContentConfig(
428
  system_instruction=(
@@ -440,4 +1009,4 @@ if 'best_params' in st.session_state:
440
  )
441
 
442
  st.markdown("## Fabrication Procedure")
443
- st.markdown(resp.text)
 
1
  import os
2
+ from pathlib import Path
3
  import streamlit as st
4
  import pandas as pd
5
  import joblib
6
  import optuna
7
  import numpy as np
 
8
 
9
  from google import genai
10
  from google.genai import types
 
33
 
34
  st.number_input = safe_number_input
35
 
36
+ APP_DIR = Path(__file__).resolve().parent
37
+ MODEL_ROOT = APP_DIR / "models"
38
+ PREPROCESSOR_DIR = MODEL_ROOT / "preprocessors"
39
+
40
+ MODEL_TASKS = {
41
+ "printability": {
42
+ "folder": MODEL_ROOT / "printability",
43
+ "prefix": "Printability_",
44
+ "label_encoder": "label_encoder_printability.pkl",
45
+ },
46
+ "cell_response": {
47
+ "folder": MODEL_ROOT / "cell response",
48
+ "prefix": "Cell_Response_",
49
+ "label_encoder": "label_encoder_cell_response.pkl",
50
+ },
51
+ }
52
+
53
+ DL_MODEL_CONFIGS = {
54
+ ("printability", "ResNet"): {"n_layers": 6, "hidden_dim": 302, "dropout": 0.190106, "activation_func": "relu"},
55
+ ("printability", "MLP"): {"n_layers": 3, "hidden_dim": 367, "dropout": 0.169472, "activation_func": "tanh"},
56
+ ("printability", "1D_CNN"): {"n_layers": 6, "hidden_dim": 287, "dropout": 0.233072, "activation_func": "relu"},
57
+ ("printability", "FT_Transformer"): {"n_layers": 3, "hidden_dim": 437, "dropout": 0.324095, "activation_func": "GELU"},
58
+ ("printability", "TabNet_Lite"): {"n_layers": 4, "hidden_dim": 283, "dropout": 0.309445, "activation_func": "relu"},
59
+ ("printability", "NODE_Lite"): {"n_layers": 5, "hidden_dim": 289, "dropout": 0.145481, "activation_func": "SELU"},
60
+ ("cell_response", "ResNet"): {"n_layers": 4, "hidden_dim": 269, "dropout": 0.229224, "activation_func": "tanh"},
61
+ ("cell_response", "MLP"): {"n_layers": 5, "hidden_dim": 238, "dropout": 0.256294, "activation_func": "ELU"},
62
+ ("cell_response", "1D_CNN"): {"n_layers": 6, "hidden_dim": 134, "dropout": 0.158794, "activation_func": "SiLU"},
63
+ ("cell_response", "FT_Transformer"): {"n_layers": 5, "hidden_dim": 395, "dropout": 0.185155, "activation_func": "SiLU"},
64
+ ("cell_response", "TabNet_Lite"): {"n_layers": 5, "hidden_dim": 342, "dropout": 0.103486, "activation_func": "SiLU"},
65
+ ("cell_response", "NODE_Lite"): {"n_layers": 2, "hidden_dim": 127, "dropout": 0.328308, "activation_func": "SiLU"},
66
+ }
67
+
68
+ MODEL_RANKINGS = {
69
+ "printability": [
70
+ "HistGradientBoosting", "TabPFN 2.6", "Bagging", "GradientBoosting", "TabICL v2",
71
+ "XGBoost", "LightGBM", "KNeighbors", "LabelPropagation", "ExtraTrees", "MLP",
72
+ "LabelSpreading", "TabNet Lite", "MLP DL", "ResNet", "1D CNN", "DecisionTree",
73
+ "NODE Lite", "LinearSVC", "LDA", "PassiveAggressive", "CalibratedClassifierCV",
74
+ "LogisticRegression", "RidgeClassifier", "FT Transformer", "Perceptron",
75
+ "MultinomialNB", "RadiusNeighbors", "ComplementNB", "NuSVC", "AdaBoost",
76
+ "SGD", "BernoulliNB", "GaussianNB", "QDA", "RandomForest", "ExtraTreeClassifier",
77
+ "DummyClassifier",
78
+ ],
79
+ "cell_response": [
80
+ "HistGradientBoosting", "TabICL v2", "TabPFN 2.6", "XGBoost", "Bagging",
81
+ "KNeighbors", "LabelPropagation", "LabelSpreading", "LightGBM", "FT Transformer",
82
+ "CalibratedClassifierCV", "ResNet", "NODE Lite", "1D CNN", "MLP DL", "LDA",
83
+ "NuSVC", "MultinomialNB", "LogisticRegression", "LinearSVC", "Perceptron",
84
+ "QDA", "RidgeClassifier", "ExtraTrees", "ExtraTreeClassifier", "AdaBoost",
85
+ "DecisionTree", "RandomForest", "GradientBoosting", "SGD", "MLP",
86
+ "PassiveAggressive", "ComplementNB", "TabNet Lite", "BernoulliNB",
87
+ "RadiusNeighbors", "DummyClassifier", "GaussianNB",
88
+ ],
89
+ }
90
+
91
+ PERFORMANCE_GUIDE = {
92
+ "printability": """
93
+ | Rank | Model | Framework | Accuracy | F1 | AUC | MCC |
94
+ |---:|---|---|---:|---:|---:|---:|
95
+ | 1 | HistGradientBoosting | Machine Learning | 0.80 | 0.80 | 0.94 | 0.69 |
96
+ | 2 | TabPFN 2.6 | Deep Learning / Transformer | 0.80 | 0.80 | 0.94 | 0.69 |
97
+ | 3 | Bagging | Machine Learning | 0.79 | 0.79 | 0.93 | 0.67 |
98
+ | 4 | GradientBoosting | Machine Learning | 0.79 | 0.79 | 0.93 | 0.66 |
99
+ | 5 | TabICL v2 | Deep Learning / Transformer | 0.79 | 0.79 | 0.94 | 0.68 |
100
+ | 6 | XGBoost | Machine Learning | 0.78 | 0.78 | 0.93 | 0.66 |
101
+ | 7 | LightGBM | Machine Learning | 0.77 | 0.77 | 0.93 | 0.64 |
102
+ | 8 | KNeighbors | Machine Learning | 0.77 | 0.77 | 0.90 | 0.64 |
103
+ | 9 | LabelPropagation | Machine Learning | 0.77 | 0.77 | 0.82 | 0.64 |
104
+ | 10 | ExtraTrees | Machine Learning | 0.77 | 0.77 | 0.92 | 0.63 |
105
+ """,
106
+ "cell_response": """
107
+ | Rank | Model | Framework | Accuracy | F1 | AUC | MCC |
108
+ |---:|---|---|---:|---:|---:|---:|
109
+ | 1 | HistGradientBoosting | Machine Learning | 0.81 | 0.81 | 0.96 | 0.67 |
110
+ | 2 | TabICL v2 | Deep Learning / Transformer | 0.79 | 0.79 | 0.97 | 0.65 |
111
+ | 3 | TabPFN 2.6 | Deep Learning / Transformer | 0.78 | 0.78 | 0.96 | 0.63 |
112
+ | 4 | XGBoost | Machine Learning | 0.79 | 0.77 | 0.96 | 0.65 |
113
+ | 5 | Bagging | Machine Learning | 0.77 | 0.77 | 0.95 | 0.62 |
114
+ | 6 | KNeighbors | Machine Learning | 0.77 | 0.77 | 0.93 | 0.61 |
115
+ | 7 | LabelPropagation | Machine Learning | 0.77 | 0.77 | 0.93 | 0.61 |
116
+ | 8 | LabelSpreading | Machine Learning | 0.78 | 0.76 | 0.93 | 0.61 |
117
+ | 9 | LightGBM | Machine Learning | 0.78 | 0.76 | 0.96 | 0.64 |
118
+ | 10 | FT Transformer | Deep Learning / Transformer | 0.76 | 0.76 | 0.94 | 0.61 |
119
+ """,
120
+ }
121
+
122
+ PERFORMANCE_GUIDE_FULL = {
123
+ "printability": """
124
+ # Printability - Merged Performance Summary
125
+
126
+ 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**.
127
+
128
+ | Rank | Model | Framework | Accuracy | Precision | Recall | F1 | AUC | MCC | Kappa |
129
+ |---:|---|---|---:|---:|---:|---:|---:|---:|---:|
130
+ | 1 | HistGradientBoosting | Machine Learning | 0.80 | 0.80 | 0.80 | 0.80 | 0.94 | 0.69 | 0.69 |
131
+ | 2 | TabPFN 2.6 | Deep Learning / Transformer | 0.80 | 0.81 | 0.80 | 0.80 | 0.94 | 0.69 | 0.68 |
132
+ | 3 | Bagging | Machine Learning | 0.79 | 0.79 | 0.79 | 0.79 | 0.93 | 0.67 | 0.67 |
133
+ | 4 | GradientBoosting | Machine Learning | 0.79 | 0.79 | 0.79 | 0.79 | 0.93 | 0.66 | 0.66 |
134
+ | 5 | TabICL v2 | Deep Learning / Transformer | 0.79 | 0.81 | 0.79 | 0.79 | 0.94 | 0.68 | 0.67 |
135
+ | 6 | XGBoost | Machine Learning | 0.78 | 0.79 | 0.78 | 0.78 | 0.93 | 0.66 | 0.65 |
136
+ | 7 | LightGBM | Machine Learning | 0.77 | 0.78 | 0.77 | 0.77 | 0.93 | 0.64 | 0.64 |
137
+ | 8 | KNeighbors | Machine Learning | 0.77 | 0.78 | 0.77 | 0.77 | 0.90 | 0.64 | 0.64 |
138
+ | 9 | LabelPropagation | Machine Learning | 0.77 | 0.77 | 0.77 | 0.77 | 0.82 | 0.64 | 0.63 |
139
+ | 10 | ExtraTrees | Machine Learning | 0.77 | 0.77 | 0.77 | 0.77 | 0.92 | 0.63 | 0.63 |
140
+ | 11 | MLP | Machine Learning | 0.76 | 0.76 | 0.76 | 0.76 | 0.90 | 0.63 | 0.63 |
141
+ | 12 | LabelSpreading | Machine Learning | 0.76 | 0.76 | 0.76 | 0.76 | 0.90 | 0.62 | 0.62 |
142
+ | 13 | TabNet Lite | Deep Learning / Transformer | 0.74 | 0.74 | 0.74 | 0.74 | 0.91 | 0.59 | 0.59 |
143
+ | 14 | MLP (DL) | Deep Learning / Transformer | 0.73 | 0.73 | 0.73 | 0.73 | 0.91 | 0.58 | 0.58 |
144
+ | 15 | ResNet | Deep Learning / Transformer | 0.73 | 0.73 | 0.73 | 0.73 | 0.91 | 0.57 | 0.57 |
145
+ | 16 | 1D CNN | Deep Learning / Transformer | 0.71 | 0.73 | 0.71 | 0.72 | 0.89 | 0.56 | 0.56 |
146
+ | 17 | DecisionTree | Machine Learning | 0.71 | 0.72 | 0.71 | 0.71 | 0.89 | 0.54 | 0.54 |
147
+ | 18 | NODE Lite | Deep Learning / Transformer | 0.69 | 0.70 | 0.69 | 0.70 | 0.90 | 0.52 | 0.52 |
148
+ | 19 | LinearSVC | Machine Learning | 0.69 | 0.68 | 0.69 | 0.68 | 0.85 | 0.49 | 0.49 |
149
+ | 20 | LDA | Machine Learning | 0.68 | 0.68 | 0.68 | 0.68 | 0.84 | 0.49 | 0.48 |
150
+ | 21 | PassiveAggressive | Machine Learning | 0.69 | 0.68 | 0.69 | 0.67 | 0.85 | 0.48 | 0.48 |
151
+ | 22 | CalibratedClassifierCV | Machine Learning | 0.68 | 0.68 | 0.68 | 0.67 | 0.85 | 0.48 | 0.48 |
152
+ | 23 | LogisticRegression | Machine Learning | 0.68 | 0.67 | 0.68 | 0.67 | 0.85 | 0.47 | 0.47 |
153
+ | 24 | RidgeClassifier | Machine Learning | 0.68 | 0.67 | 0.68 | 0.66 | 0.84 | 0.46 | 0.46 |
154
+ | 25 | FT Transformer | Deep Learning / Transformer | 0.66 | 0.65 | 0.66 | 0.65 | 0.87 | 0.45 | 0.45 |
155
+ | 26 | Perceptron | Machine Learning | 0.67 | 0.68 | 0.67 | 0.64 | 0.84 | 0.45 | 0.42 |
156
+ | 27 | MultinomialNB | Machine Learning | 0.64 | 0.69 | 0.64 | 0.64 | 0.83 | 0.45 | 0.43 |
157
+ | 28 | RadiusNeighbors | Machine Learning | 0.66 | 0.70 | 0.66 | 0.63 | 0.87 | 0.43 | 0.39 |
158
+ | 29 | ComplementNB | Machine Learning | 0.63 | 0.67 | 0.63 | 0.63 | 0.82 | 0.44 | 0.43 |
159
+ | 30 | NuSVC | Machine Learning | 0.60 | 0.69 | 0.60 | 0.61 | 0.84 | 0.45 | 0.42 |
160
+ | 31 | AdaBoost | Machine Learning | 0.62 | 0.59 | 0.62 | 0.58 | 0.80 | 0.38 | 0.37 |
161
+ | 32 | SGD | Machine Learning | 0.63 | 0.59 | 0.63 | 0.57 | 0.82 | 0.36 | 0.35 |
162
+ | 33 | BernoulliNB | Machine Learning | 0.59 | 0.62 | 0.59 | 0.55 | 0.77 | 0.32 | 0.31 |
163
+ | 34 | GaussianNB | Machine Learning | 0.33 | 0.70 | 0.33 | 0.36 | 0.74 | 0.25 | 0.19 |
164
+ | 35 | QDA | Machine Learning | 0.52 | 0.27 | 0.52 | 0.35 | 0.80 | 0.00 | 0.00 |
165
+ | 36 | RandomForest | Machine Learning | 0.52 | 0.27 | 0.52 | 0.35 | 0.78 | 0.00 | 0.00 |
166
+ | 37 | ExtraTreeClassifier | Machine Learning | 0.52 | 0.27 | 0.52 | 0.35 | 0.50 | 0.00 | 0.00 |
167
+ | 38 | DummyClassifier | Machine Learning | 0.52 | 0.27 | 0.52 | 0.35 | 0.50 | 0.00 | 0.00 |
168
+ """,
169
+ "cell_response": """
170
+ # Cell Response - Merged Performance Summary
171
+
172
+ 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**.
173
+
174
+ | Rank | Model | Framework | Accuracy | Precision | Recall | F1 | AUC | MCC | Kappa |
175
+ |---:|---|---|---:|---:|---:|---:|---:|---:|---:|
176
+ | 1 | HistGradientBoosting | Machine Learning | 0.81 | 0.81 | 0.81 | 0.81 | 0.96 | 0.67 | 0.67 |
177
+ | 2 | TabICL v2 | Deep Learning / Transformer | 0.79 | 0.79 | 0.79 | 0.79 | 0.97 | 0.65 | 0.65 |
178
+ | 3 | TabPFN 2.6 | Deep Learning / Transformer | 0.78 | 0.78 | 0.78 | 0.78 | 0.96 | 0.63 | 0.63 |
179
+ | 4 | XGBoost | Machine Learning | 0.79 | 0.76 | 0.79 | 0.77 | 0.96 | 0.65 | 0.64 |
180
+ | 5 | Bagging | Machine Learning | 0.77 | 0.78 | 0.77 | 0.77 | 0.95 | 0.62 | 0.62 |
181
+ | 6 | KNeighbors | Machine Learning | 0.77 | 0.77 | 0.77 | 0.77 | 0.93 | 0.61 | 0.61 |
182
+ | 7 | LabelPropagation | Machine Learning | 0.77 | 0.77 | 0.77 | 0.77 | 0.93 | 0.61 | 0.61 |
183
+ | 8 | LabelSpreading | Machine Learning | 0.78 | 0.76 | 0.78 | 0.76 | 0.93 | 0.61 | 0.61 |
184
+ | 9 | LightGBM | Machine Learning | 0.78 | 0.79 | 0.78 | 0.76 | 0.96 | 0.64 | 0.63 |
185
+ | 10 | FT Transformer | Deep Learning / Transformer | 0.76 | 0.79 | 0.76 | 0.76 | 0.94 | 0.61 | 0.60 |
186
+ | 11 | CalibratedClassifierCV | Machine Learning | 0.76 | 0.76 | 0.76 | 0.75 | 0.94 | 0.59 | 0.59 |
187
+ | 12 | ResNet | Deep Learning / Transformer | 0.76 | 0.76 | 0.76 | 0.75 | 0.94 | 0.59 | 0.59 |
188
+ | 13 | NODE Lite | Deep Learning / Transformer | 0.76 | 0.75 | 0.76 | 0.75 | 0.95 | 0.59 | 0.59 |
189
+ | 14 | 1D CNN | Deep Learning / Transformer | 0.75 | 0.76 | 0.75 | 0.74 | 0.95 | 0.59 | 0.59 |
190
+ | 15 | MLP (DL) | Deep Learning / Transformer | 0.75 | 0.75 | 0.75 | 0.74 | 0.95 | 0.57 | 0.57 |
191
+ | 16 | LDA | Machine Learning | 0.75 | 0.72 | 0.75 | 0.73 | 0.94 | 0.55 | 0.55 |
192
+ | 17 | NuSVC | Machine Learning | 0.74 | 0.73 | 0.74 | 0.73 | 0.95 | 0.55 | 0.55 |
193
+ | 18 | MultinomialNB | Machine Learning | 0.74 | 0.72 | 0.74 | 0.73 | 0.95 | 0.55 | 0.55 |
194
+ | 19 | LogisticRegression | Machine Learning | 0.74 | 0.70 | 0.74 | 0.71 | 0.95 | 0.54 | 0.54 |
195
+ | 20 | LinearSVC | Machine Learning | 0.74 | 0.71 | 0.74 | 0.70 | 0.94 | 0.53 | 0.52 |
196
+ | 21 | Perceptron | Machine Learning | 0.74 | 0.68 | 0.74 | 0.70 | 0.94 | 0.53 | 0.52 |
197
+ | 22 | QDA | Machine Learning | 0.74 | 0.68 | 0.74 | 0.70 | 0.94 | 0.57 | 0.55 |
198
+ | 23 | RidgeClassifier | Machine Learning | 0.73 | 0.70 | 0.73 | 0.70 | 0.94 | 0.52 | 0.51 |
199
+ | 24 | ExtraTrees | Machine Learning | 0.74 | 0.71 | 0.74 | 0.68 | 0.95 | 0.55 | 0.52 |
200
+ | 25 | ExtraTreeClassifier | Machine Learning | 0.73 | 0.67 | 0.73 | 0.68 | 0.91 | 0.54 | 0.52 |
201
+ | 26 | AdaBoost | Machine Learning | 0.74 | 0.65 | 0.74 | 0.67 | 0.91 | 0.59 | 0.55 |
202
+ | 27 | DecisionTree | Machine Learning | 0.74 | 0.65 | 0.74 | 0.67 | 0.92 | 0.59 | 0.55 |
203
+ | 28 | RandomForest | Machine Learning | 0.74 | 0.65 | 0.74 | 0.67 | 0.92 | 0.59 | 0.55 |
204
+ | 29 | GradientBoosting | Machine Learning | 0.74 | 0.65 | 0.74 | 0.67 | 0.92 | 0.59 | 0.55 |
205
+ | 30 | SGD | Machine Learning | 0.71 | 0.65 | 0.71 | 0.66 | 0.92 | 0.49 | 0.47 |
206
+ | 31 | MLP | Machine Learning | 0.73 | 0.61 | 0.73 | 0.65 | 0.93 | 0.55 | 0.52 |
207
+ | 32 | PassiveAggressive | Machine Learning | 0.73 | 0.65 | 0.73 | 0.65 | 0.92 | 0.52 | 0.49 |
208
+ | 33 | ComplementNB | Machine Learning | 0.72 | 0.65 | 0.72 | 0.65 | 0.93 | 0.49 | 0.47 |
209
+ | 34 | TabNet Lite | Deep Learning / Transformer | 0.70 | 0.67 | 0.70 | 0.65 | 0.93 | 0.48 | 0.46 |
210
+ | 35 | BernoulliNB | Machine Learning | 0.71 | 0.60 | 0.71 | 0.63 | 0.93 | 0.48 | 0.45 |
211
+ | 36 | RadiusNeighbors | Machine Learning | 0.60 | 0.36 | 0.60 | 0.45 | 0.50 | 0.00 | 0.00 |
212
+ | 37 | DummyClassifier | Machine Learning | 0.60 | 0.36 | 0.60 | 0.45 | 0.50 | 0.00 | 0.00 |
213
+ | 38 | GaussianNB | Machine Learning | 0.28 | 0.69 | 0.28 | 0.34 | 0.84 | 0.18 | 0.14 |
214
+ """,
215
+ }
216
+
217
+ GEMINI_MODELS = [
218
+ "gemini-3.5-flash",
219
+ "gemini-3.1-flash-lite",
220
+ "gemini-3.1-pro-preview",
221
+ "gemini-3.1-flash-lite-preview",
222
+ "gemini-3-flash-preview",
223
+ "gemini-2.5-pro",
224
+ "gemini-2.5-flash",
225
+ "gemini-2.5-flash-lite",
226
+ ]
227
 
228
  def scaffold_quality_combined(printability, cell_response,
229
  weight_printability=0.3, weight_cell_response=0.7):
 
264
  ]
265
 
266
  @st.cache_resource
267
+ def load_prediction_preprocessors():
268
+ return {
269
+ "preprocessor": joblib.load(PREPROCESSOR_DIR / "preprocessor.pkl"),
270
+ "feature_cols": joblib.load(PREPROCESSOR_DIR / "feature_cols.pkl"),
271
+ "printability_encoder": joblib.load(PREPROCESSOR_DIR / MODEL_TASKS["printability"]["label_encoder"]),
272
+ "cell_response_encoder": joblib.load(PREPROCESSOR_DIR / MODEL_TASKS["cell_response"]["label_encoder"]),
273
+ }
274
+
275
+ def model_display_name(path, prefix):
276
+ name = path.stem.replace("_model", "")
277
+ if name.startswith(prefix):
278
+ name = name[len(prefix):]
279
+ return f"{name} ({path.suffix.lstrip('.')})"
280
+
281
+ def model_rank_key(display_name, task_key):
282
+ base_name = display_name.rsplit(" (", 1)[0]
283
+ rank_aliases = {
284
+ "1D_CNN": "1D CNN",
285
+ "FT_Transformer": "FT Transformer",
286
+ "NODE_Lite": "NODE Lite",
287
+ "TabNet_Lite": "TabNet Lite",
288
+ "TabPFN_2.6": "TabPFN 2.6",
289
+ "TabICL_v2": "TabICL v2",
290
+ "ExtraTree": "ExtraTreeClassifier",
291
+ }
292
+ rank_name = rank_aliases.get(base_name, base_name)
293
+ if base_name == "MLP" and display_name.endswith("(pth)"):
294
+ rank_name = "MLP DL"
295
+ ranking = MODEL_RANKINGS[task_key]
296
+ rank = ranking.index(rank_name) if rank_name in ranking else len(ranking)
297
+ return rank, base_name.lower(), display_name
298
+
299
+ def discover_model_options(task_key):
300
+ task = MODEL_TASKS[task_key]
301
+ files = []
302
+ for suffix in ("*.pkl", "*.joblib", "*.pth"):
303
+ files.extend(task["folder"].glob(suffix))
304
+ options = {model_display_name(path, task["prefix"]): str(path) for path in files}
305
+ return dict(sorted(options.items(), key=lambda item: model_rank_key(item[0], task_key)))
306
+
307
+ def parse_architecture(path, task_key):
308
+ stem = Path(path).stem.replace("_model", "")
309
+ prefix = MODEL_TASKS[task_key]["prefix"]
310
+ return stem[len(prefix):] if stem.startswith(prefix) else stem
311
+
312
+ def build_torch_model(architecture, input_dim, out_dim, cfg):
313
+ import torch
314
+ import torch.nn as nn
315
+
316
+ activation_funcs = {
317
+ "relu": nn.ReLU, "tanh": nn.Tanh, "GELU": nn.GELU,
318
+ "SELU": nn.SELU, "ELU": nn.ELU, "SiLU": nn.SiLU,
319
+ }
320
+ act = activation_funcs[cfg["activation_func"]]
321
+
322
+ class ResidualBlock(nn.Module):
323
+ def __init__(self):
324
+ super().__init__()
325
+ self.linear = nn.Linear(cfg["hidden_dim"], cfg["hidden_dim"])
326
+ self.bn = nn.BatchNorm1d(cfg["hidden_dim"])
327
+ self.act = act()
328
+ self.dropout = nn.Dropout(cfg["dropout"])
329
+ def forward(self, x):
330
+ return x + self.dropout(self.act(self.bn(self.linear(x))))
331
+
332
+ class TissueResNet(nn.Module):
333
+ def __init__(self):
334
+ super().__init__()
335
+ self.input_layer = nn.Sequential(
336
+ nn.Linear(input_dim, cfg["hidden_dim"]),
337
+ nn.BatchNorm1d(cfg["hidden_dim"]),
338
+ act(),
339
+ )
340
+ self.blocks = nn.ModuleList([ResidualBlock() for _ in range(cfg["n_layers"])])
341
+ self.output_layer = nn.Linear(cfg["hidden_dim"], out_dim)
342
+ def forward(self, x):
343
+ x = self.input_layer(x)
344
+ for block in self.blocks:
345
+ x = block(x)
346
+ return self.output_layer(x)
347
+
348
+ class StandardMLP(nn.Module):
349
+ def __init__(self):
350
+ super().__init__()
351
+ layers = []
352
+ in_dim = input_dim
353
+ for _ in range(cfg["n_layers"]):
354
+ layers.extend([
355
+ nn.Linear(in_dim, cfg["hidden_dim"]),
356
+ nn.BatchNorm1d(cfg["hidden_dim"]),
357
+ act(),
358
+ nn.Dropout(cfg["dropout"]),
359
+ ])
360
+ in_dim = cfg["hidden_dim"]
361
+ layers.append(nn.Linear(cfg["hidden_dim"], out_dim))
362
+ self.network = nn.Sequential(*layers)
363
+ def forward(self, x):
364
+ return self.network(x)
365
+
366
+ class Tabular1DCNN(nn.Module):
367
+ def __init__(self):
368
+ super().__init__()
369
+ layers = []
370
+ in_channels = 1
371
+ for _ in range(cfg["n_layers"]):
372
+ layers.extend([
373
+ nn.Conv1d(in_channels, cfg["hidden_dim"], kernel_size=3, padding=1),
374
+ nn.BatchNorm1d(cfg["hidden_dim"]),
375
+ act(),
376
+ nn.Dropout(cfg["dropout"]),
377
+ ])
378
+ in_channels = cfg["hidden_dim"]
379
+ self.conv_net = nn.Sequential(*layers)
380
+ self.pool = nn.AdaptiveAvgPool1d(1)
381
+ self.fc = nn.Linear(cfg["hidden_dim"], out_dim)
382
+ def forward(self, x):
383
+ x = self.conv_net(x.unsqueeze(1))
384
+ return self.fc(self.pool(x).squeeze(2))
385
+
386
+ class FTTransformer(nn.Module):
387
+ def __init__(self):
388
+ super().__init__()
389
+ self.d_token = max(4, (cfg["hidden_dim"] // 4) * 4)
390
+ self.feature_embeddings = nn.ModuleList([nn.Linear(1, self.d_token) for _ in range(input_dim)])
391
+ self.cls_token = nn.Parameter(torch.randn(1, 1, self.d_token))
392
+ encoder_layer = nn.TransformerEncoderLayer(
393
+ d_model=self.d_token, nhead=4, dropout=cfg["dropout"], batch_first=True
394
+ )
395
+ self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=cfg["n_layers"])
396
+ self.fc = nn.Linear(self.d_token, out_dim)
397
+ def forward(self, x):
398
+ batch_size = x.size(0)
399
+ tokens = [self.feature_embeddings[i](x[:, i:i+1]).unsqueeze(1) for i in range(x.size(1))]
400
+ x_emb = torch.cat([self.cls_token.expand(batch_size, -1, -1)] + tokens, dim=1)
401
+ return self.fc(self.transformer(x_emb)[:, 0, :])
402
+
403
+ class TabNetLite(nn.Module):
404
+ def __init__(self):
405
+ super().__init__()
406
+ self.n_steps = max(1, cfg["n_layers"])
407
+ self.initial_bn = nn.BatchNorm1d(input_dim)
408
+ self.transformers = nn.ModuleList([
409
+ nn.Sequential(
410
+ nn.Linear(input_dim, cfg["hidden_dim"]),
411
+ nn.BatchNorm1d(cfg["hidden_dim"]),
412
+ act(),
413
+ nn.Dropout(cfg["dropout"]),
414
+ )
415
+ for _ in range(self.n_steps)
416
+ ])
417
+ self.attentions = nn.ModuleList([
418
+ nn.Sequential(
419
+ nn.Linear(cfg["hidden_dim"], input_dim),
420
+ nn.BatchNorm1d(input_dim),
421
+ nn.Softmax(dim=-1),
422
+ )
423
+ for _ in range(self.n_steps)
424
+ ])
425
+ self.fc_out = nn.Linear(cfg["hidden_dim"], out_dim)
426
+ def forward(self, x):
427
+ x = self.initial_bn(x)
428
+ out_agg, prior = 0, torch.ones_like(x)
429
+ feat_rep = self.transformers[0](x)
430
+ for step in range(self.n_steps):
431
+ mask = self.attentions[step](feat_rep) * prior
432
+ prior = prior * (1.0 - mask)
433
+ feat_rep = self.transformers[step](x * mask)
434
+ out_agg += feat_rep
435
+ return self.fc_out(out_agg)
436
+
437
+ class NeuralDecisionForest(nn.Module):
438
+ def __init__(self):
439
+ super().__init__()
440
+ self.n_trees = max(1, cfg["hidden_dim"] // 16)
441
+ self.depth = max(2, cfg["n_layers"] + 1)
442
+ self.n_leaves = 2 ** self.depth
443
+ self.trees = nn.ModuleList([
444
+ nn.Sequential(
445
+ nn.Linear(input_dim, self.n_leaves),
446
+ nn.Dropout(cfg["dropout"]),
447
+ nn.Softmax(dim=-1),
448
+ )
449
+ for _ in range(self.n_trees)
450
+ ])
451
+ self.leaf_weights = nn.Parameter(torch.randn(self.n_trees, self.n_leaves, out_dim))
452
+ def forward(self, x):
453
+ out = 0
454
+ for i, tree in enumerate(self.trees):
455
+ out += torch.matmul(tree(x), self.leaf_weights[i])
456
+ return out / self.n_trees
457
+
458
+ builders = {
459
+ "ResNet": TissueResNet,
460
+ "MLP": StandardMLP,
461
+ "1D_CNN": Tabular1DCNN,
462
+ "FT_Transformer": FTTransformer,
463
+ "TabNet_Lite": TabNetLite,
464
+ "NODE_Lite": NeuralDecisionForest,
465
+ }
466
+ return builders[architecture]()
467
 
468
  @st.cache_resource
469
+ def load_prediction_model(path, task_key, input_dim, out_dim):
470
+ path = Path(path)
471
+ if path.suffix in {".pkl", ".joblib"}:
472
+ return {"kind": "sklearn", "model": joblib.load(path)}
473
+
474
+ if path.suffix == ".pth":
475
+ import torch
476
+ architecture = parse_architecture(path, task_key)
477
+ cfg = DL_MODEL_CONFIGS.get((task_key, architecture))
478
+ if cfg is None:
479
+ raise ValueError(f"No architecture configuration found for {path.name}.")
480
+ model = build_torch_model(architecture, input_dim, out_dim, cfg)
481
+ state_dict = torch.load(path, map_location="cpu")
482
+ model.load_state_dict(state_dict)
483
+ model.eval()
484
+ return {"kind": "torch", "model": model}
485
+
486
+ raise ValueError(f"Unsupported model file: {path.name}")
487
+
488
+ def softmax(values):
489
+ values = np.asarray(values, dtype=float)
490
+ values = values - np.max(values)
491
+ exp_values = np.exp(values)
492
+ return exp_values / exp_values.sum()
493
+
494
+ def expected_class_value(model_bundle, x_raw, preprocessor, label_encoder):
495
+ x_model = preprocessor.transform(x_raw).astype(np.float32)
496
+
497
+ if model_bundle["kind"] == "torch":
498
+ import torch
499
+ with torch.no_grad():
500
+ logits = model_bundle["model"](torch.tensor(x_model, dtype=torch.float32)).numpy()[0]
501
+ probs = softmax(logits)
502
+ labels = label_encoder.classes_.astype(float)
503
+ return float(np.dot(probs, labels))
504
+
505
+ model = model_bundle["model"]
506
+ if hasattr(model, "predict_proba"):
507
+ probs = model.predict_proba(x_model)[0]
508
+ classes = np.asarray(model.classes_, dtype=int)
509
+ labels = label_encoder.inverse_transform(classes).astype(float)
510
+ return float(np.dot(probs, labels))
511
+
512
+ if hasattr(model, "decision_function"):
513
+ scores = np.asarray(model.decision_function(x_model)[0])
514
+ if scores.ndim == 0:
515
+ p_high = 1.0 / (1.0 + np.exp(-scores))
516
+ probs = np.array([1.0 - p_high, p_high])
517
+ else:
518
+ probs = softmax(scores)
519
+ classes = np.asarray(model.classes_, dtype=int)
520
+ labels = label_encoder.inverse_transform(classes).astype(float)
521
+ return float(np.dot(probs, labels))
522
+
523
+ pred = np.asarray(model.predict(x_model), dtype=int)
524
+ return float(label_encoder.inverse_transform(pred)[0])
525
+
526
+ prediction_assets = load_prediction_preprocessors()
527
+ preprocessor = prediction_assets["preprocessor"]
528
+ feature_cols = prediction_assets["feature_cols"]
529
+ label_encoder_print = prediction_assets["printability_encoder"]
530
+ label_encoder_cell = prediction_assets["cell_response_encoder"]
531
+
532
+ sample_for_shape = {col: 0.0 for col in feature_cols}
533
+ sample_for_shape["Cell Line"] = CELL_LINE_OPTIONS[0]
534
+ preprocessed_input_dim = preprocessor.transform(pd.DataFrame([sample_for_shape])[feature_cols]).shape[1]
535
+
536
+ @st.dialog("Optimization Trials")
537
+ def show_trial_guidance():
538
+ st.markdown(
539
+ """
540
+ The trial count controls how many candidate scaffold settings Optuna tests before choosing the best WSSQ.
541
+
542
+ **Recommended range:** 100-1000 trials.
543
+
544
+ **Default:** 300 trials, which is a balanced choice for normal use.
545
+
546
+ **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.
547
+ """
548
  )
549
 
550
+ @st.dialog("Weighted Synergistic Scaffold Quality (WSSQ)")
551
+ def show_wssq_guidance():
552
+ st.markdown(
553
+ """
554
+ WSSQ is the optimization score used in MLATE to combine **printability** and **cell response** into one scaffold-quality objective.
555
+
556
+ In the manuscript, 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. Cell response is weighted strongly because it reflects explicit biological outcomes and also indirectly captures scaffold physical and mechanical properties such as pore structure, interconnectivity, elasticity, and compressive behavior.
557
+
558
+ The app normalizes printability and cell response, then combines them using two components:
559
+
560
+ - **Weighted harmonic mean:** rewards balanced high values and penalizes weak performance in either target.
561
+ - **Weighted multiplicative component:** captures synergy between printability and cell response.
562
 
563
+ 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.
 
 
564
 
565
+ 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.
566
+ """
567
+ )
568
+
569
+ @st.dialog("Model Selection Guide", width="large")
570
+ def show_model_selection_guidance():
571
+ st.markdown(
572
+ """
573
+ Models are sorted by the merged benchmark summaries you provided, using F1 score and accuracy as the main ranking criteria. The highest-ranked available model appears first in each dropdown.
574
+
575
+ 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.
576
+ """
577
+ )
578
+ tab_print, tab_cell = st.tabs(["Printability", "Cell Response"])
579
+ with tab_print:
580
+ st.markdown(PERFORMANCE_GUIDE["printability"])
581
+ with st.expander("Show more"):
582
+ st.markdown(PERFORMANCE_GUIDE_FULL["printability"])
583
+ with tab_cell:
584
+ st.markdown(PERFORMANCE_GUIDE["cell_response"])
585
+ with st.expander("Show more"):
586
+ st.markdown(PERFORMANCE_GUIDE_FULL["cell_response"])
587
+
588
+ @st.dialog("How to Get a Gemini API Key")
589
+ def show_gemini_api_key_guidance():
590
+ st.markdown(
591
+ """
592
+ 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.
593
+
594
+ 1. Open [Google AI Studio API Keys](https://aistudio.google.com/app/apikey) and sign in with your Google/Gmail account if prompted.
595
+ 2. If this is your first visit, accept the terms of service and continue.
596
+ 3. Click **Get API key** or **Create API key**.
597
+ 4. Choose an existing Google Cloud project, or select **Create API key in new project**.
598
+ 5. Copy the generated key, return to this app, and paste it into the **Gemini API Key** box.
599
+
600
+ **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.
601
+ """
602
+ )
603
 
604
  if 'bio_rows' not in st.session_state:
605
  st.session_state.bio_rows = [{
 
628
  # Printability is dynamically calculated and cannot be changed manually
629
  w_print_pct = 100 - w_cell_pct
630
  st.sidebar.number_input("Printability Weight (%)", value=w_print_pct, disabled=True, help="Auto-calculated to ensure sum is 100%")
631
+ if st.sidebar.button("What is WSSQ?", use_container_width=True):
632
+ show_wssq_guidance()
633
 
634
  # Convert back to 0.0 - 1.0 for the mathematical formula
635
  w_cell = w_cell_pct / 100.0
636
  w_print = w_print_pct / 100.0
637
 
638
+ print_model_options = discover_model_options("printability")
639
+ cell_model_options = discover_model_options("cell_response")
640
+
641
+ if not print_model_options or not cell_model_options:
642
+ st.error("No selectable prediction models were found in the models folder.")
643
+ st.stop()
644
+
645
+ selected_print_model_name = st.sidebar.selectbox(
646
+ "Printability Model",
647
+ list(print_model_options.keys()),
648
+ key="printability_model_select",
649
+ )
650
+ selected_cell_model_name = st.sidebar.selectbox(
651
+ "Cell Response Model",
652
+ list(cell_model_options.keys()),
653
+ key="cell_response_model_select",
654
+ )
655
+ if st.sidebar.button("Model Selection Guide", use_container_width=True):
656
+ show_model_selection_guidance()
657
+
658
+ n_trials = st.sidebar.number_input(
659
+ "Optimization Trials",
660
+ min_value=10,
661
+ max_value=10000,
662
+ value=300,
663
+ step=50,
664
+ help="Number of Optuna trials used when you click Optimize WSSQ.",
665
+ )
666
+ if st.sidebar.button("Trial Count Help", use_container_width=True):
667
+ show_trial_guidance()
668
+
669
+ gemini_key = st.sidebar.text_input(
670
+ "Gemini API Key",
671
+ value=os.getenv("GEMINI_API_KEY", ""),
672
+ type="password",
673
+ help="Used only when generating the LLM-based fabrication procedure.",
674
+ )
675
+ if st.sidebar.button("How to Get API Key", use_container_width=True):
676
+ show_gemini_api_key_guidance()
677
+
678
+ gemini_model = st.sidebar.selectbox(
679
+ "Gemini Model",
680
+ GEMINI_MODELS,
681
+ index=0,
682
+ key="gemini_model_select",
683
+ )
684
+
685
  st.title("MLATE: Machine Learning Applications in Tissue Engineering")
686
  st.markdown(
687
  "<p style='font-size:1.2em; color:grey;'>"
 
710
 
711
  c1, c2, c3, c4, c5 = st.columns([2, 1, 1, 1, 0.3])
712
  mat = c1.selectbox(
713
+ "Biomaterial", options,
714
  index=options.index(row['mat']) if row['mat'] in options else 0,
715
+ key=f"bio_mat_{i}",
716
+ label_visibility="collapsed",
717
  )
718
  st.session_state.bio_rows[i]['mat'] = mat
719
 
 
848
 
849
  if st.button("Optimize WSSQ"):
850
  with st.spinner("Running Optuna…"):
851
+ try:
852
+ model_print = load_prediction_model(
853
+ print_model_options[selected_print_model_name],
854
+ "printability",
855
+ preprocessed_input_dim,
856
+ len(label_encoder_print.classes_),
857
+ )
858
+ model_cell = load_prediction_model(
859
+ cell_model_options[selected_cell_model_name],
860
+ "cell_response",
861
+ preprocessed_input_dim,
862
+ len(label_encoder_cell.classes_),
863
+ )
864
+ except Exception as exc:
865
+ st.error(
866
+ "Could not load the selected prediction model. This model will only work when the "
867
+ "running environment matches the saved artifact dependencies. For HistGradientBoosting, "
868
+ "use numpy>=2.0 in the Python environment that runs Streamlit.\n\n"
869
+ f"Details:\n{exc}"
870
+ )
871
+ st.stop()
872
+
873
  def objective(trial):
874
  bi_vals = {
875
  r['mat']: trial.suggest_float(
 
896
 
897
  feat = {**bi_vals, **pp_vals}
898
  feat["Cell Density (cells/mL)"] = cd
899
+ feat["Cell Line"] = cell_line
 
 
 
900
 
901
+ X = pd.DataFrame([feat]).reindex(columns=feature_cols, fill_value=0.0)
 
 
902
 
903
+ exp_p = expected_class_value(model_print, X, preprocessor, label_encoder_print)
904
+ exp_c = expected_class_value(model_cell, X, preprocessor, label_encoder_cell)
 
 
905
 
906
  np.random.seed(42)
907
  # Use dynamic weights from the sidebar sliders
 
925
  sampler=sampler,
926
  pruner=optuna.pruners.MedianPruner()
927
  )
928
+ study.optimize(objective, n_trials=int(n_trials))
929
 
930
  # Store results in session state to persist after rerun
931
  st.session_state.best_params = study.best_trial.params
 
948
  )
949
 
950
  if st.button("Generate Fabrication Procedure"):
951
+ if not gemini_key:
952
+ st.error("Please enter your Gemini API key in the sidebar before generating a fabrication procedure.")
953
+ st.stop()
954
+
955
  with st.spinner("Generating rigorous fabrication procedure…"):
956
+ client = genai.Client(api_key=gemini_key)
957
 
958
  formatted_params = "\n".join([
959
  f"- {k.replace('bio__', 'Biomaterial: ').replace('pp__', 'Print Setting: ')}: {v:.2f}"
 
991
  final_prompt += f"\n**Additional User Constraints & Inquiries (Integrate these into the protocol):**\n{user_inquiry}"
992
 
993
  resp = client.models.generate_content(
994
+ model=gemini_model,
995
  contents=final_prompt,
996
  config=types.GenerateContentConfig(
997
  system_instruction=(
 
1009
  )
1010
 
1011
  st.markdown("## Fabrication Procedure")
1012
+ st.markdown(resp.text)
catboost_printability.cbm → models/cell response/Cell_Response_1D_CNN_model.pth RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:3c0a21e8657aa8d990f62750f9a8342227ba51ec7f02136a8882069ba7a64952
3
- size 81574688
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f70717cc4444f97c2e20465a4c8b9f845bdc2daf9c202940f93eead164cff4b0
3
+ size 1113045
models/cell response/Cell_Response_AdaBoost.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b48ae05ebd1292beb4fa80c174e604dcc59ec1d110ef6b10e58e52e014af34eb
3
+ size 209772
models/cell response/Cell_Response_Bagging.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a675e99d706e3a0d067f763561e07d47070784c176ee8d62aef57dc6fcce1b2b
3
+ size 1589916
cell_line_encoder.joblib → models/cell response/Cell_Response_BernoulliNB.pkl RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:8c62b2bc4682c3e67f364511201060dea195f93ed9ff90bd930a1443cc3aa73d
3
- size 25655
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d255aa6a97b9eb85ba154f09059fef319931e3a68ef5f05cac63d9b96cde8304
3
+ size 11919
models/cell response/Cell_Response_CalibratedClassifierCV.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:34ef9c42687fa42a0f81b24823e4325e4b1ee8422422317492400ae6d07336ed
3
+ size 10412
models/cell response/Cell_Response_ComplementNB.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c4fc2165c356f1c0c1cfdcf9ad01dad9a552cef4706b359f49d9ccc70d4402b4
3
+ size 13071
scaler_printability.joblib → models/cell response/Cell_Response_DecisionTree.pkl RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:0f5c37a1cb5bd29c1a4fb868808e305545c4930b31a22e25e8e7271e861fe1a4
3
- size 7703
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:22469b5cc3c669706f3bd5b96379f8d26fecf478f7867f2df2cada910c1beb87
3
+ size 1649
scaler_cell_response.joblib → models/cell response/Cell_Response_DummyClassifier.pkl RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:0e8ad571c0a19952513271331ff8140a64eff4325c0b5d11f702ffca2c38a8c9
3
- size 7703
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5c2d13a951982709550ec71eebb9e0585ed02165280b8e08587b8229ca683094
3
+ size 639
models/cell response/Cell_Response_ExtraTree.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ecf6cfe975b9bae1fd67796f9754f24837f7691e99cb976bf38f3a4db9b56592
3
+ size 2705
models/cell response/Cell_Response_ExtraTrees.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9a7989a7733d5013777e20972c8d233f1719d93b23bd33c490a36e49a1dbd7a1
3
+ size 793841
catboost_cell_response.cbm → models/cell response/Cell_Response_FT_Transformer_model.pth RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:d8eeb02258c9780147ea7e4030757f3ba7d4d80b7530f74cff79cd3e0481c23e
3
- size 98406080
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e9f9c7750f1594daaf0c36f722814b030b23bd5a12b899e2b4d912954f3761c9
3
+ size 45076412
models/cell response/Cell_Response_GaussianNB.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:835125ad7ffce8a24e8a89ac6af4e94108e69e262301e40f2aac9e0d0c872d48
3
+ size 11935
models/cell response/Cell_Response_GradientBoosting.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:06c095734b4c2ab779f369987cd8fce7dc0ae0fed22413a0879d0aa021f9f933
3
+ size 481962
models/cell response/Cell_Response_HistGradientBoosting.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d53b8bdb8a5137461c4be6f5bb7b9c60e8c3ad9376c1fda18a8edd52a7dc7c57
3
+ size 1010734
models/cell response/Cell_Response_KNeighbors.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:60c934e5877f0524bf1d13683e3ae339182a212b71f0b2017233d0ebcc4bb876
3
+ size 3548502
models/cell response/Cell_Response_LDA.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:596c41b26b585f7eb520520eec27589a990e7f0c8a6d7623423cb8d872b2eef4
3
+ size 9443
models/cell response/Cell_Response_LabelPropagation.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:136a5ca830e55a3fae9b097afc121b276294a43822f2c50c78ca755c0fbab98a
3
+ size 2347800
models/cell response/Cell_Response_LabelSpreading.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:248ea1f34e286c939f3d81b7e0a983db48b7ee88f3c08bede2729f2650143892
3
+ size 1223023
models/cell response/Cell_Response_LightGBM.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fe1a7ac9c25508dcf5639cf64d3286e1b9f752debf16bd1875b44b5091cc0491
3
+ size 1552684
models/cell response/Cell_Response_LinearSVC.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4dbdfc34b36b5385c39dd31bb5fdde824616bc1357661ced6704b34d802d11db
3
+ size 37964
models/cell response/Cell_Response_LogisticRegression.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e609d345c492bb68e9c9fbda8ef9b0acb73b8beebd212d515b7c23be01502f35
3
+ size 6447
models/cell response/Cell_Response_MLP.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a17c1d1e68a25074f87f836ff8726943b146ca62be9f683b6cfc487332792a9f
3
+ size 1244052
models/cell response/Cell_Response_MLP_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:51d34e040f5681704b3119c04b9b08881cb72b1e6c9799c39b99e7158016d4bf
3
+ size 1079652
models/cell response/Cell_Response_MultinomialNB.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c770fa8292e9027fd90cd81196c746e294143dc0c6a2256185d640ac6a740264
3
+ size 11903
models/cell response/Cell_Response_NODE_Lite_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dfea4e05557355a92e031f2329be19b5e90f19f913a30d6a5a3f33f0c879f073
3
+ size 38724
models/cell response/Cell_Response_NuSVC.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:17fe9887f6e6b3406b79b83dc20fbb9e04049de61d360c3c7c5c5492f509a233
3
+ size 932687
models/cell response/Cell_Response_PassiveAggressive.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b8113b6af5e28e93e3ce83f0a85ecac45f4dde7d543c002377493786734e47f5
3
+ size 22764
models/cell response/Cell_Response_Perceptron.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d1c0ce5c5425f9974624eeb2821dd6981a945b4d7dbd1521175ce4d2445aa14a
3
+ size 22748
models/cell response/Cell_Response_QDA.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d3d0300d01053acde77c9f0c79aa5ae385349b9f5e6666609a78a3d463f4b415
3
+ size 367160
models/cell response/Cell_Response_RadiusNeighbors.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1bfda1acdc5fb36f1b40dc04c955c652fccc5a66f32027534dcd69e4c6635341
3
+ size 3479055
models/cell response/Cell_Response_RandomForest.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2f3053956b46b8d0c74a52b6ac0f13aeb8ac74b56b7c2c0dd5bb872256ce883a
3
+ size 332673
models/cell response/Cell_Response_ResNet_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:944e1b741ee964877bab40fd94e54b9be1164677c99d4132516fad76f2a2a30f
3
+ size 1352549
models/cell response/Cell_Response_RidgeClassifier.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:37db18fde6d0e4cc43d048d12457cf16c505b81fe2e4f8e88a8a3fdb35704f12
3
+ size 21148
models/cell response/Cell_Response_SGD.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:15d2ee2711131ff81259d756689f3b2c4eed4d4aa612df08d8dfca8dfb6aa908
3
+ size 3987
models/cell response/Cell_Response_TabICL_v2.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:34f5f68afdd324c18901a6aec64662fa3049992b46f0bfafd0c4e7f33a0ecb43
3
+ size 3595929
models/cell response/Cell_Response_TabNet_Lite_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a5390d355028d8c9dc10810517e8622ae24a6ec568a706b01ab63cf2d71da7e5
3
+ size 1973188
models/cell response/Cell_Response_TabPFN_2.6.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e94ae36afa55bf2d92253ab3a41fe75967e741a40c20af4cfc4f0d5be1be69e4
3
+ size 1212783
models/cell response/Cell_Response_XGBoost.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:483ecf0b6d7e3cf90ad1e31859ad5d7de77c67379c5574df79d9da823a5e96c3
3
+ size 1735817
models/preprocessors/feature_cols.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f3a045e40d698347d8b260e980d4271c50bbe5c7dd0e14363f5d3ec734b17e8a
3
+ size 2841
models/preprocessors/label_encoder_cell_response.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8dee86699c0433019442cd223b7bbe83c27dc5097d3942aa128c5b1eec17da59
3
+ size 367
models/preprocessors/label_encoder_printability.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ee6eddf5091c4429a1ad40759cb7cc49ff5a3acb4450f529359c058eefc0771c
3
+ size 359
models/preprocessors/preprocessor.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d9d91db4db191c3eb411f99a5f82a12f1f044c85f4a0e7096010b1cf70a27a02
3
+ size 38481
models/printability/Printability_1D_CNN_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b96f399e3e548ce7f0b815f2092ede680ecbdbe96617b81f2335b0d548b0632c
3
+ size 5000675
models/printability/Printability_AdaBoost.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6283ac39e40e3234c153a7241367781316c31c1aefa2e0501edcc17679a40efb
3
+ size 168308
models/printability/Printability_Bagging.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:58a1b12b467887c97da2d278778bce5504d203d1746b492feaa6ed0bd3c845e4
3
+ size 11463188
models/printability/Printability_BernoulliNB.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:518379522054a43fc6fe7140901e6944728b50833069a57a3ce32a1f7a3b36ab
3
+ size 9687
models/printability/Printability_CalibratedClassifierCV.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:229166fe376a8809f5618f56d85b08212c8be0d4d8047db22aeaeaec3905beeb
3
+ size 41268
models/printability/Printability_ComplementNB.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ca825ba4811afa56f0bc8e7a71230897ae16a5e562fb1cdf582d94875b69f334
3
+ size 10839