fluid-bed-coating-twin / app_plotly.py
SamdGuizani
fix: app.py and app_plotly.py updated (added a help to DMC and coating level sliders to clarify mapping to r_spraying and r_drying correlation variables)
6f2bbe3
Raw
History Blame Contribute Delete
7.18 kB
"""
Fluid Bed Coating Digital Twin β€” Streamlit app (Plotly rendering variant).
Functionally identical to app.py, but charts are rendered with interactive
Plotly figures instead of Matplotlib. The simulation itself (constants,
correlations, PH→SP→DR chaining, dissolution model) is reused unchanged from the
fluid_bed package; only the rendering layer differs and lives in plotly_figures.py.
Run with: streamlit run app_plotly.py
"""
import sys
import os
# Make the fluid_bed package importable whether or not it is pip-installed.
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
import numpy as np
import streamlit as st
from fluid_bed.simulate import run_full_process
from fluid_bed.models.dissolution import dissolution_curve
from plotly_figures import make_process_figure, make_dissolution_figure
# ── Core simulation (results cached per unique parameter set) ─────────────────
@st.cache_data(show_spinner="Running ODE simulation…")
def run_simulation(
d_mm, ssa_cm2g, T0_C, batch_kg, humidity_g_kg, dmc_pct, coating_level,
ph_T_C, ph_flow, ph_dur_min,
sp_T_C, sp_flow, sp_rate_g_min,
dr_T_C, dr_flow, dr_dur_min,
):
return run_full_process(
d_mm, ssa_cm2g, T0_C, batch_kg, humidity_g_kg, dmc_pct, coating_level,
ph_T_C, ph_flow, ph_dur_min,
sp_T_C, sp_flow, sp_rate_g_min,
dr_T_C, dr_flow, dr_dur_min,
)
# ── Dissolution panel (fragment: t_sample reruns only this section) ───────────
@st.fragment
def dissolution_panel(sim, ssa_cm2g):
st.markdown("### Dissolution β€” Virtual Sample Withdrawal")
st.caption(
"Move the slider to any process time. "
"The dissolution curve updates instantly (simulation result is cached)."
)
t_max = float(sim.t_end)
if "t_sample" not in st.session_state:
st.session_state["t_sample"] = min(float(sim.sp_end), t_max)
elif st.session_state["t_sample"] > t_max:
st.session_state["t_sample"] = t_max
t_proc_min = st.slider(
"Sample time (min)",
min_value=0.0,
max_value=t_max,
step=0.5,
key="t_sample",
)
fig_diss, wg_at_t, wg_nl_at_t, k, stage_name, F_diss, t_diss = (
make_dissolution_figure(sim, t_proc_min, ssa_cm2g, dissolution_curve)
)
# theme=None β†’ use the figure's own (white) template instead of Streamlit's
# dark theme, which would otherwise force title/tick fonts to white.
st.plotly_chart(fig_diss, use_container_width=True, theme=None)
# Dissolution metrics
t50 = float(np.interp(50, F_diss, t_diss)) if F_diss[-1] >= 50 else float("nan")
t80 = float(np.interp(80, F_diss, t_diss)) if F_diss[-1] >= 80 else float("nan")
d1, d2, d3, d4 = st.columns(4)
d1.metric("Stage", stage_name)
d2.metric("tβ‚…β‚€ (min)", f"{t50:.0f}" if not np.isnan(t50) else "> 240")
d3.metric("tβ‚ˆβ‚€ (min)", f"{t80:.0f}" if not np.isnan(t80) else "> 240")
d4.metric("k (s⁻¹)", f"{k:.4e}")
# ── Page layout ────────────────────────────────────────────────────────────────
st.set_page_config(
page_title="Fluid Bed Coating Digital Twin (Plotly)",
page_icon="πŸ§ͺ",
layout="wide",
)
st.title("Fluid Bed Coating Digital Twin")
st.caption(
"**Pre-heating β†’ Spraying β†’ Drying β†’ Dissolution prediction.** "
"r_spray and r_dry are derived from the AICc-selected empirical correlations "
"(r_spray RΒ²=0.844, r_dry RΒ²=0.851; Bosch 2018 DoE, 19 runs)."
)
st.caption(
"**Full model development**: [GitHub repo](https://github.com/SamdGuizani/FB_Coating_Digital_Twin)"
)
# ── Sidebar sliders ───────────────────────────────────────────────────────────
with st.sidebar:
st.markdown("## Parameters")
st.markdown("**Particle / Batch**")
d_mm = st.slider("Diameter (mm)", 0.80, 1.50, 1.00, 0.05)
ssa_cm2g = st.slider("SSA (cmΒ²/g)", 55.0, 75.0, 65.0, 1.0)
T0_C = st.slider("Tβ‚€ particle (Β°C)", 15.0, 30.0, 20.0, 1.0)
batch_kg = st.slider("Batch size (kg)", 3.0, 6.0, 4.6, 0.1)
humidity_g_kg = st.slider("Humidity (g/kg)", 0.0, 25.0, 13.4, 0.5)
dmc_pct = st.slider("DMC (% m/m)", 1.0, 2.0, 1.5, 0.1,
help="Coating solution dry-matter concentration. Feeds the "
"correlations as CC (coating concentration).")
coating_level = st.slider("Coating level", -1.0, 1.0, 0.5, 0.05,
help="Coded DoE coating level (βˆ’1…+1). Converted to the dry-matter "
"ratio DM = 1.7Β·level + 6.4 g/kg, which feeds the correlations.")
st.markdown("**Pre-heating**")
ph_T_C = st.slider("[PH] T inlet (Β°C)", 40.0, 60.0, 50.0, 2.0)
ph_flow = st.slider("[PH] Flow (mΒ³/h)", 200, 400, 250, 10)
ph_dur_min = st.slider("[PH] Duration (min)", 10.0, 30.0, 20.0, 1.0)
st.markdown("**Spraying** *(r_spray from correlation)*")
sp_T_C = st.slider("[SP] T inlet (Β°C)", 30.0, 60.0, 40.0, 2.0)
sp_flow = st.slider("[SP] Flow (mΒ³/h)", 200, 400, 250, 10)
sp_rate_g_min = st.slider("[SP] Spray rate (g/min)", 95, 150, 120, 5)
st.markdown("**Drying** *(r_dry from correlation)*")
dr_T_C = st.slider("[DR] T inlet (Β°C)", 30.0, 60.0, 40.0, 2.0)
dr_flow = st.slider("[DR] Flow (mΒ³/h)", 200, 400, 250, 10)
dr_dur_min = st.slider("[DR] Duration (min)", 3.0, 15.0, 9.0, 1.0)
# ── Run simulation (result cached per unique slider combination) ──────────────
sim = run_simulation(
d_mm, ssa_cm2g, T0_C, batch_kg, humidity_g_kg, dmc_pct, coating_level,
ph_T_C, ph_flow, ph_dur_min,
sp_T_C, sp_flow, sp_rate_g_min,
dr_T_C, dr_flow, dr_dur_min,
)
# ── Summary metrics row ────────────────────────────────────────────────────────
c1, c2, c3, c4, c5 = st.columns(5)
c1.metric("r_spray (Γ—10⁻⁢ kg/s)", f"{sim.r_spraying*1e6:.2f}")
c2.metric("r_dry (Γ—10⁻³ kg/s)", f"{sim.r_drying*1e3:.3f}")
c3.metric("DM ratio (g/kg)", f"{sim.dm_ratio_g_kg:.2f}")
c4.metric("WG end-spray", f"{sim.wg_end_spray:.3f}%")
c5.metric("WG final", f"{sim.wg_final:.3f}%")
# ── Process plots ─────────────────────────────────────────────────────────────
fig_proc = make_process_figure(sim, ph_T_C, sp_T_C, dr_T_C)
st.plotly_chart(fig_proc, use_container_width=True, theme=None)
# ── Dissolution panel ─────────────────────────────────────────────────────────
st.divider()
dissolution_panel(sim, ssa_cm2g)