File size: 7,181 Bytes
38db72f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f2bbe3
 
 
 
 
 
38db72f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
"""
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)