Spaces:
Running
Running
GitHub Actions commited on
Commit ·
2c76b35
1
Parent(s): 9e4203c
Sync from screenase@a9b2f49a40611ed7a10f2844bd6deca51a968047
Browse files- .gitattributes +0 -35
- .streamlit/config.toml +23 -6
- app.py +189 -87
.gitattributes
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.streamlit/config.toml
CHANGED
|
@@ -1,14 +1,31 @@
|
|
| 1 |
-
# Benchling-inspired
|
| 2 |
-
#
|
| 3 |
-
#
|
| 4 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
[theme]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
primaryColor = "#3d65fb"
|
| 7 |
backgroundColor = "#ffffff"
|
| 8 |
secondaryBackgroundColor = "#f5f6f8"
|
| 9 |
textColor = "#1f2937"
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
[server]
|
| 14 |
headless = true
|
|
|
|
| 1 |
+
# Benchling-inspired light theme (white-dominant, CTA-blue as the only
|
| 2 |
+
# accent) paired with a pure-black dark theme. Streamlit matches the user's
|
| 3 |
+
# browser preference on first load and exposes a light/dark switch in the
|
| 4 |
+
# settings menu (three-dot → Settings → Choose app theme).
|
| 5 |
+
#
|
| 6 |
+
# `dataframeHeaderBackgroundColor` is what finally makes st.dataframe /
|
| 7 |
+
# st.data_editor render with dark headers — glide-data-grid reads Streamlit's
|
| 8 |
+
# theme config at page load and won't honor runtime CSS variable flips.
|
| 9 |
+
|
| 10 |
[theme]
|
| 11 |
+
font = "sans serif"
|
| 12 |
+
base = "light"
|
| 13 |
+
|
| 14 |
+
[theme.light]
|
| 15 |
primaryColor = "#3d65fb"
|
| 16 |
backgroundColor = "#ffffff"
|
| 17 |
secondaryBackgroundColor = "#f5f6f8"
|
| 18 |
textColor = "#1f2937"
|
| 19 |
+
borderColor = "#e5e7eb"
|
| 20 |
+
|
| 21 |
+
[theme.dark]
|
| 22 |
+
primaryColor = "#3d65fb"
|
| 23 |
+
backgroundColor = "#000000"
|
| 24 |
+
secondaryBackgroundColor = "#0a0a0a"
|
| 25 |
+
textColor = "#ffffff"
|
| 26 |
+
borderColor = "#2a2a2a"
|
| 27 |
+
linkColor = "#7aa0ff"
|
| 28 |
+
dataframeHeaderBackgroundColor = "#0a0a0a"
|
| 29 |
|
| 30 |
[server]
|
| 31 |
headless = true
|
app.py
CHANGED
|
@@ -38,6 +38,75 @@ DEMO_RESULTS_PATH = Path(__file__).parent / "examples" / "results_simulated.csv"
|
|
| 38 |
POS_COLOR = "#4a6fa5"
|
| 39 |
NEG_COLOR = "#c05454"
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
def _config_from_url() -> ReactionConfig | None:
|
| 43 |
"""If ?cfg=… is present in the URL, decode it into a ReactionConfig."""
|
|
@@ -85,7 +154,7 @@ def build_default_config() -> ReactionConfig:
|
|
| 85 |
|
| 86 |
def generate_from_ui(
|
| 87 |
cfg: ReactionConfig,
|
| 88 |
-
min_pipet_uL: float = 0.
|
| 89 |
*,
|
| 90 |
design_kind: str = "full",
|
| 91 |
alpha: str = "face",
|
|
@@ -151,8 +220,11 @@ def generate_from_ui(
|
|
| 151 |
# ---------- sidebar ----------
|
| 152 |
|
| 153 |
def _sidebar(default_cfg: ReactionConfig) -> tuple[ReactionConfig, float, dict]:
|
|
|
|
|
|
|
|
|
|
| 154 |
with st.sidebar:
|
| 155 |
-
st.markdown("### Reaction")
|
| 156 |
c1, c2 = st.columns(2)
|
| 157 |
vol = c1.number_input(
|
| 158 |
"Volume (µL)", value=float(default_cfg.reaction_volume_uL),
|
|
@@ -167,76 +239,104 @@ def _sidebar(default_cfg: ReactionConfig) -> tuple[ReactionConfig, float, dict]:
|
|
| 167 |
"Center pts", min_value=0, max_value=10,
|
| 168 |
value=int(default_cfg.center_points), step=1, key="cps",
|
| 169 |
)
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
"
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
alpha = st.select_slider(
|
| 190 |
-
"Axial α",
|
| 191 |
-
options=["face", "rotatable"],
|
| 192 |
-
value="face",
|
| 193 |
-
help=(
|
| 194 |
-
"`face` (α=1) stays within low/high; `rotatable` extends "
|
| 195 |
-
"axial setpoints beyond the range — use only if your "
|
| 196 |
-
"stocks allow the wider span."
|
| 197 |
-
),
|
| 198 |
-
key="ccd_alpha",
|
| 199 |
)
|
| 200 |
|
| 201 |
-
st.
|
| 202 |
-
|
| 203 |
-
"
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
|
|
|
| 214 |
index=0,
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
-
st.
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
"
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
new_factors: list[Factor] = []
|
| 241 |
for orig, row in zip(default_cfg.factors, edited.itertuples(index=False),
|
| 242 |
strict=True):
|
|
@@ -244,7 +344,7 @@ def _sidebar(default_cfg: ReactionConfig) -> tuple[ReactionConfig, float, dict]:
|
|
| 244 |
"low": float(row.low), "high": float(row.high),
|
| 245 |
}))
|
| 246 |
|
| 247 |
-
with st.expander("Stock concentrations"):
|
| 248 |
stock_rows = [
|
| 249 |
{
|
| 250 |
"key": k,
|
|
@@ -273,7 +373,7 @@ def _sidebar(default_cfg: ReactionConfig) -> tuple[ReactionConfig, float, dict]:
|
|
| 273 |
for row in stock_edit.itertuples(index=False)
|
| 274 |
}
|
| 275 |
|
| 276 |
-
with st.expander("Fixed reagents"):
|
| 277 |
st.caption("Reagents with a constant volume per run (not swept).")
|
| 278 |
fixed_rows = [
|
| 279 |
{"reagent": k, "volume_uL": float(v)}
|
|
@@ -296,10 +396,10 @@ def _sidebar(default_cfg: ReactionConfig) -> tuple[ReactionConfig, float, dict]:
|
|
| 296 |
for row in fixed_edit.itertuples(index=False)
|
| 297 |
}
|
| 298 |
|
| 299 |
-
with st.expander("Advanced"):
|
| 300 |
min_pipet = st.number_input(
|
| 301 |
"Min pipetting volume (µL)",
|
| 302 |
-
value=0.
|
| 303 |
help="Volumes below this threshold emit a warning on the bench sheet.",
|
| 304 |
)
|
| 305 |
|
|
@@ -387,22 +487,23 @@ def _render_generate_tab(
|
|
| 387 |
"sees them at the bench."
|
| 388 |
)
|
| 389 |
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
column_config
|
| 398 |
-
"
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
with
|
| 404 |
-
|
| 405 |
-
|
|
|
|
| 406 |
|
| 407 |
if art.get("plate_df") is not None:
|
| 408 |
st.markdown("#### Plate layout")
|
|
@@ -706,6 +807,7 @@ def main() -> None:
|
|
| 706 |
"About": "Screenase — DoE planner for in-vitro transcription.",
|
| 707 |
},
|
| 708 |
)
|
|
|
|
| 709 |
|
| 710 |
header_l, header_r = st.columns([3, 1])
|
| 711 |
with header_l:
|
|
|
|
| 38 |
POS_COLOR = "#4a6fa5"
|
| 39 |
NEG_COLOR = "#c05454"
|
| 40 |
|
| 41 |
+
# Streamlit's native dual-theme support (`.streamlit/config.toml` with
|
| 42 |
+
# [theme.light] + [theme.dark]) drives dark mode — the user toggles via the
|
| 43 |
+
# three-dot menu → Settings → Choose app theme. glide-data-grid reads that
|
| 44 |
+
# config at page load, which is the only way to get dark dataframes.
|
| 45 |
+
#
|
| 46 |
+
# The only CSS we still inject ourselves is a white backing under iframes:
|
| 47 |
+
# the plate-map HTML has no body bg of its own, so on a dark Streamlit page
|
| 48 |
+
# its light-gray cell borders would render against a dark frame.
|
| 49 |
+
_IFRAME_SAFETY_CSS = """
|
| 50 |
+
<style>
|
| 51 |
+
.stApp iframe { background: #ffffff !important; }
|
| 52 |
+
[data-testid="stDeployButton"] { display: none !important; }
|
| 53 |
+
.stApp [data-testid="stMainBlockContainer"] { padding-top: 2rem; }
|
| 54 |
+
|
| 55 |
+
/* Swap Streamlit's default sidebar toggle (a Material Symbols ligature
|
| 56 |
+
`keyboard_double_arrow_left/right` that renders as << / >>) for
|
| 57 |
+
sidekickicons' sidebar-left outline icon — https://sidekickicons.com/.
|
| 58 |
+
We hide the ligature text via font-size:0 on the icon span, then paint
|
| 59 |
+
the sidekickicons glyph on a ::before using currentColor so it inherits
|
| 60 |
+
the span's theme-driven color. */
|
| 61 |
+
[data-testid="stSidebarCollapseButton"] [data-testid="stIconMaterial"],
|
| 62 |
+
[data-testid="stExpandSidebarButton"] [data-testid="stIconMaterial"] {
|
| 63 |
+
font-size: 0 !important;
|
| 64 |
+
line-height: 0 !important;
|
| 65 |
+
width: 1.25rem;
|
| 66 |
+
height: 1.25rem;
|
| 67 |
+
display: inline-block;
|
| 68 |
+
position: relative;
|
| 69 |
+
}
|
| 70 |
+
[data-testid="stSidebarCollapseButton"] [data-testid="stIconMaterial"]::before,
|
| 71 |
+
[data-testid="stExpandSidebarButton"] [data-testid="stIconMaterial"]::before {
|
| 72 |
+
content: "";
|
| 73 |
+
position: absolute;
|
| 74 |
+
inset: 0;
|
| 75 |
+
background-color: currentColor;
|
| 76 |
+
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><path d='M9 4.5v15M4.125 19.5h15.75c.621 0 1.125-.504 1.125-1.125V5.625c0-.621-.504-1.125-1.125-1.125H4.125C3.504 4.5 3 5.004 3 5.625v12.75c0 .621.504 1.125 1.125 1.125z'/></svg>") no-repeat center / contain;
|
| 77 |
+
mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><path d='M9 4.5v15M4.125 19.5h15.75c.621 0 1.125-.504 1.125-1.125V5.625c0-.621-.504-1.125-1.125-1.125H4.125C3.504 4.5 3 5.004 3 5.625v12.75c0 .621.504 1.125 1.125 1.125z'/></svg>") no-repeat center / contain;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.sn-label-with-info {
|
| 81 |
+
display: flex; align-items: center; gap: 0.3rem;
|
| 82 |
+
font-size: 0.875rem; line-height: 1.6; margin-bottom: 0.25rem;
|
| 83 |
+
}
|
| 84 |
+
.sn-info-icon {
|
| 85 |
+
position: relative; cursor: help; color: #888;
|
| 86 |
+
display: inline-flex; align-items: center; line-height: 1;
|
| 87 |
+
}
|
| 88 |
+
.sn-info-icon::after {
|
| 89 |
+
content: attr(data-tooltip);
|
| 90 |
+
position: absolute; bottom: calc(100% + 6px);
|
| 91 |
+
left: 50%; transform: translateX(-50%);
|
| 92 |
+
background: #262730; color: #fafafa;
|
| 93 |
+
padding: 0.4rem 0.6rem; border-radius: 0.3rem;
|
| 94 |
+
font-size: 0.78rem; font-weight: 400;
|
| 95 |
+
white-space: normal; width: max-content; max-width: 14rem;
|
| 96 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
|
| 97 |
+
opacity: 0; pointer-events: none; transition: opacity 120ms;
|
| 98 |
+
z-index: 1000;
|
| 99 |
+
}
|
| 100 |
+
.sn-info-icon:hover::after,
|
| 101 |
+
.sn-info-icon:focus::after { opacity: 1; }
|
| 102 |
+
</style>
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _inject_safety_css() -> None:
|
| 107 |
+
"""Inject the minimal CSS that's not covered by Streamlit's theme config."""
|
| 108 |
+
st.markdown(_IFRAME_SAFETY_CSS, unsafe_allow_html=True)
|
| 109 |
+
|
| 110 |
|
| 111 |
def _config_from_url() -> ReactionConfig | None:
|
| 112 |
"""If ?cfg=… is present in the URL, decode it into a ReactionConfig."""
|
|
|
|
| 154 |
|
| 155 |
def generate_from_ui(
|
| 156 |
cfg: ReactionConfig,
|
| 157 |
+
min_pipet_uL: float = 0.1,
|
| 158 |
*,
|
| 159 |
design_kind: str = "full",
|
| 160 |
alpha: str = "face",
|
|
|
|
| 220 |
# ---------- sidebar ----------
|
| 221 |
|
| 222 |
def _sidebar(default_cfg: ReactionConfig) -> tuple[ReactionConfig, float, dict]:
|
| 223 |
+
if "expand_all" not in st.session_state:
|
| 224 |
+
st.session_state["expand_all"] = False
|
| 225 |
+
|
| 226 |
with st.sidebar:
|
| 227 |
+
st.markdown("### Reaction Parameters")
|
| 228 |
c1, c2 = st.columns(2)
|
| 229 |
vol = c1.number_input(
|
| 230 |
"Volume (µL)", value=float(default_cfg.reaction_volume_uL),
|
|
|
|
| 239 |
"Center pts", min_value=0, max_value=10,
|
| 240 |
value=int(default_cfg.center_points), step=1, key="cps",
|
| 241 |
)
|
| 242 |
+
with c2:
|
| 243 |
+
st.markdown(
|
| 244 |
+
'<div class="sn-label-with-info">'
|
| 245 |
+
'<span>Seed</span>'
|
| 246 |
+
'<span class="sn-info-icon" tabindex="0" '
|
| 247 |
+
'data-tooltip="Randomizes run order. Same seed yields identical output.">'
|
| 248 |
+
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" '
|
| 249 |
+
'viewBox="0 0 24 24" fill="none" stroke="currentColor" '
|
| 250 |
+
'stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'
|
| 251 |
+
'<circle cx="12" cy="12" r="10"/>'
|
| 252 |
+
'<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>'
|
| 253 |
+
'<line x1="12" y1="17" x2="12.01" y2="17"/>'
|
| 254 |
+
'</svg></span>'
|
| 255 |
+
'</div>',
|
| 256 |
+
unsafe_allow_html=True,
|
| 257 |
+
)
|
| 258 |
+
seed = st.number_input(
|
| 259 |
+
"Seed", value=int(default_cfg.seed), step=1, key="seed",
|
| 260 |
+
label_visibility="collapsed",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
)
|
| 262 |
|
| 263 |
+
ec1, ec2 = st.columns(2)
|
| 264 |
+
if ec1.button("Expand all", width="stretch", key="expand_all_btn"):
|
| 265 |
+
st.session_state["expand_all"] = True
|
| 266 |
+
st.rerun()
|
| 267 |
+
if ec2.button("Collapse all", width="stretch", key="collapse_all_btn"):
|
| 268 |
+
st.session_state["expand_all"] = False
|
| 269 |
+
st.rerun()
|
| 270 |
+
|
| 271 |
+
expanded = st.session_state["expand_all"]
|
| 272 |
+
|
| 273 |
+
with st.expander("Design Type", expanded=expanded):
|
| 274 |
+
design_kind = st.radio(
|
| 275 |
+
"Design type",
|
| 276 |
+
options=["full", "ccd", "pb"],
|
| 277 |
index=0,
|
| 278 |
+
format_func=lambda k: {
|
| 279 |
+
"full": "Full factorial (2ᵏ + centers)",
|
| 280 |
+
"ccd": "Central-composite (CCD follow-up)",
|
| 281 |
+
"pb": "Plackett-Burman (screening, k > 5)",
|
| 282 |
+
}[k],
|
| 283 |
+
key="design_kind",
|
| 284 |
+
horizontal=False,
|
| 285 |
+
label_visibility="collapsed",
|
| 286 |
)
|
| 287 |
+
alpha = "face"
|
| 288 |
+
if design_kind == "ccd":
|
| 289 |
+
alpha = st.select_slider(
|
| 290 |
+
"Axial α",
|
| 291 |
+
options=["face", "rotatable"],
|
| 292 |
+
value="face",
|
| 293 |
+
help=(
|
| 294 |
+
"`face` (α=1) stays within low/high; `rotatable` extends "
|
| 295 |
+
"axial setpoints beyond the range — use only if your "
|
| 296 |
+
"stocks allow the wider span."
|
| 297 |
+
),
|
| 298 |
+
key="ccd_alpha",
|
| 299 |
+
)
|
| 300 |
|
| 301 |
+
with st.expander("Plate Layout", expanded=expanded):
|
| 302 |
+
plate_choice = st.radio(
|
| 303 |
+
"Plate",
|
| 304 |
+
options=["none", "96", "384"],
|
| 305 |
+
horizontal=True,
|
| 306 |
+
index=0,
|
| 307 |
+
key="plate_choice",
|
| 308 |
+
label_visibility="collapsed",
|
| 309 |
+
)
|
| 310 |
+
plate_layout = "column-major"
|
| 311 |
+
if plate_choice != "none":
|
| 312 |
+
plate_layout = st.radio(
|
| 313 |
+
"Fill order",
|
| 314 |
+
options=["column-major", "row-major", "randomized"],
|
| 315 |
+
index=0,
|
| 316 |
+
horizontal=True,
|
| 317 |
+
key="plate_layout",
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
with st.expander("High / Low Setpoints", expanded=expanded):
|
| 321 |
+
factor_rows = [
|
| 322 |
+
{
|
| 323 |
+
"factor": f.display or f.name,
|
| 324 |
+
"low": float(f.low),
|
| 325 |
+
"high": float(f.high),
|
| 326 |
+
}
|
| 327 |
+
for f in default_cfg.factors
|
| 328 |
+
]
|
| 329 |
+
edited = st.data_editor(
|
| 330 |
+
pd.DataFrame(factor_rows),
|
| 331 |
+
hide_index=True,
|
| 332 |
+
disabled=["factor"],
|
| 333 |
+
key="factors_editor",
|
| 334 |
+
column_config={
|
| 335 |
+
"factor": st.column_config.TextColumn("Factor", width="medium"),
|
| 336 |
+
"low": st.column_config.NumberColumn("Low", format="%.3g"),
|
| 337 |
+
"high": st.column_config.NumberColumn("High", format="%.3g"),
|
| 338 |
+
},
|
| 339 |
+
)
|
| 340 |
new_factors: list[Factor] = []
|
| 341 |
for orig, row in zip(default_cfg.factors, edited.itertuples(index=False),
|
| 342 |
strict=True):
|
|
|
|
| 344 |
"low": float(row.low), "high": float(row.high),
|
| 345 |
}))
|
| 346 |
|
| 347 |
+
with st.expander("Stock concentrations", expanded=expanded):
|
| 348 |
stock_rows = [
|
| 349 |
{
|
| 350 |
"key": k,
|
|
|
|
| 373 |
for row in stock_edit.itertuples(index=False)
|
| 374 |
}
|
| 375 |
|
| 376 |
+
with st.expander("Fixed reagents", expanded=expanded):
|
| 377 |
st.caption("Reagents with a constant volume per run (not swept).")
|
| 378 |
fixed_rows = [
|
| 379 |
{"reagent": k, "volume_uL": float(v)}
|
|
|
|
| 396 |
for row in fixed_edit.itertuples(index=False)
|
| 397 |
}
|
| 398 |
|
| 399 |
+
with st.expander("Advanced", expanded=expanded):
|
| 400 |
min_pipet = st.number_input(
|
| 401 |
"Min pipetting volume (µL)",
|
| 402 |
+
value=0.1, min_value=0.0, step=0.1, key="min_pipet",
|
| 403 |
help="Volumes below this threshold emit a warning on the bench sheet.",
|
| 404 |
)
|
| 405 |
|
|
|
|
| 487 |
"sees them at the bench."
|
| 488 |
)
|
| 489 |
|
| 490 |
+
st.markdown("#### Design")
|
| 491 |
+
display_cols = [f.name for f in cfg.factors] + ["is_center"]
|
| 492 |
+
st.dataframe(
|
| 493 |
+
design[display_cols],
|
| 494 |
+
height=540,
|
| 495 |
+
width="stretch",
|
| 496 |
+
column_config={
|
| 497 |
+
"is_center": st.column_config.CheckboxColumn(
|
| 498 |
+
"center?", help="Center-point replicate",
|
| 499 |
+
),
|
| 500 |
+
},
|
| 501 |
+
)
|
| 502 |
+
|
| 503 |
+
with st.expander("Bench sheet preview", expanded=False):
|
| 504 |
+
# Render inside a white-background iframe so the template's black
|
| 505 |
+
# text stays legible regardless of the surrounding theme.
|
| 506 |
+
st.components.v1.html(art["html"], height=540, scrolling=True)
|
| 507 |
|
| 508 |
if art.get("plate_df") is not None:
|
| 509 |
st.markdown("#### Plate layout")
|
|
|
|
| 807 |
"About": "Screenase — DoE planner for in-vitro transcription.",
|
| 808 |
},
|
| 809 |
)
|
| 810 |
+
_inject_safety_css()
|
| 811 |
|
| 812 |
header_l, header_r = st.columns([3, 1])
|
| 813 |
with header_l:
|