Spaces:
Sleeping
Sleeping
| """ | |
| Full-process orchestration: pre-heating β spraying β drying. | |
| `run_full_process` is the single implementation of the digital-twin chain that | |
| both front-ends (Streamlit app.py and notebook 05b) consume. It takes operating | |
| parameters in the units the UIs expose (mm, Β°C, mΒ³/h, g/min, %), converts to SI, | |
| derives the coating-loss rates from the DoE-fitted empirical correlations, runs | |
| the three stage ODEs with end-state chaining, and returns per-stage results | |
| plus the concatenated time series ready for plotting (time in min, temperatures | |
| in Β°C, solvent contents in wt %). | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| import numpy as np | |
| from .config import default_coater_params, wg_max_noloss | |
| from .models.preheating import run_preheating, PreheatingResult | |
| from .models.spraying import run_spraying, SprayingResult | |
| from .models.drying_stage import run_drying, DryingResult | |
| from .models.coating_correlations import calc_r_spraying, calc_r_drying_empirical | |
| class FullProcessResult: | |
| """Per-stage ODE results plus plot-ready concatenated series.""" | |
| # Stage results (SI units, as returned by the stage solvers) | |
| preheating: PreheatingResult | |
| spraying: SprayingResult | |
| drying: DryingResult | |
| # Concatenated series over the whole process (plot units) | |
| t_all: np.ndarray # min | |
| T_product: np.ndarray # Β°C | |
| T_gas: np.ndarray # Β°C (quasi-steady) | |
| Y_particle: np.ndarray # wt % acetone on particles | |
| Y_gas: np.ndarray # wt % acetone in gas | |
| WG: np.ndarray # % coating weight gain | |
| WG_noloss: np.ndarray # % theoretical no-loss reference | |
| # Stage boundaries [min] | |
| ph_end: float | |
| sp_end: float | |
| t_end: float | |
| # Derived scalars | |
| r_spraying: float # kg/s, from empirical correlation | |
| r_drying: float # kg/s, from empirical correlation | |
| dm_ratio_g_kg: float # g dry coating / kg particles | |
| qty_sol_kg: float # kg coating solution sprayed | |
| sp_dur_s: float # s, spraying duration | |
| wg_max_noloss: float # %, theoretical maximum WG | |
| wg_end_spray: float # %, WG at end of spraying | |
| wg_final: float # %, WG at discharge | |
| def run_full_process( | |
| d_mm: float, ssa_cm2g: float, T0_C: float, batch_kg: float, | |
| humidity_g_kg: float, dmc_pct: float, coating_level: float, | |
| ph_T_C: float, ph_flow_m3h: float, ph_dur_min: float, | |
| sp_T_C: float, sp_flow_m3h: float, sp_rate_g_min: float, | |
| dr_T_C: float, dr_flow_m3h: float, dr_dur_min: float, | |
| ) -> FullProcessResult: | |
| """ | |
| Run the full PH β SP β DR chain from UI-unit operating parameters. | |
| Parameters | |
| ---------- | |
| d_mm : equivalent particle diameter [mm] | |
| ssa_cm2g : particle specific surface area [cmΒ²/g] | |
| T0_C : initial particle temperature [Β°C] | |
| batch_kg : batch mass [kg] | |
| humidity_g_kg : measured inlet air absolute humidity [g/kg dry air] | |
| (enters only the r_drying correlation, not the ODEs) | |
| dmc_pct : coating solution dry-matter concentration [wt %] | |
| coating_level : coded DoE coating-level factor [-1 β¦ 1]; sets the | |
| solution quantity via the DoE recipe | |
| qty_sol = (0.0017Β·level + 0.0064)Β·batch/dmc_frac | |
| ph/sp/dr_T_C : stage inlet air temperatures [Β°C] | |
| ph/sp/dr_flow_m3h : stage air flows [mΒ³/h] | |
| ph_dur_min, dr_dur_min : stage durations [min] | |
| sp_rate_g_min : spray rate [g solution/min]; spraying duration follows | |
| from qty_sol / spray rate | |
| """ | |
| dmc_frac = dmc_pct / 100.0 | |
| qty_sol_kg = (0.0017 * coating_level + 0.0064) * batch_kg / dmc_frac | |
| sp_rate_kgs = sp_rate_g_min / 60_000.0 | |
| sp_dur_s = qty_sol_kg / sp_rate_kgs | |
| dm_ratio_g_kg = qty_sol_kg * dmc_frac / batch_kg * 1000.0 | |
| rho_air = default_coater_params().rho_air | |
| ph_m = ph_flow_m3h / 3600.0 * rho_air | |
| sp_m = sp_flow_m3h / 3600.0 * rho_air | |
| dr_m = dr_flow_m3h / 3600.0 * rho_air | |
| ph_K, sp_K, dr_K = ph_T_C + 273.15, sp_T_C + 273.15, dr_T_C + 273.15 | |
| # Empirical correlations for coating loss rates | |
| r_spray = calc_r_spraying(sp_rate_g_min, dmc_pct, dm_ratio_g_kg) | |
| r_dry = calc_r_drying_empirical(batch_kg, dm_ratio_g_kg, ssa_cm2g, humidity_g_kg) | |
| phys = dict(diameter_eq=d_mm * 1e-3, ssa_cm2_g=ssa_cm2g, batch_size=batch_kg) | |
| # Inlet air carries no acetone; humidity enters only via r_dry above. | |
| p_ph = default_coater_params( | |
| **phys, air_flow_rates=(ph_m,) * 3, | |
| air_temperatures=(ph_K,) * 3, air_inlet_moisture=(0.0, 0.0, 0.0)) | |
| res_ph = run_preheating(p_ph, duration=ph_dur_min * 60.0, | |
| T_particle_init=T0_C + 273.15) | |
| p_sp = default_coater_params( | |
| **phys, air_flow_rates=(sp_m,) * 3, | |
| air_temperatures=(sp_K,) * 3, air_inlet_moisture=(0.0, 0.0, 0.0), | |
| spray_rate=sp_rate_kgs, dry_matter_conc=dmc_frac, r_spraying=r_spray) | |
| res_sp = run_spraying(p_sp, duration=sp_dur_s, | |
| T_particle_init=res_ph.T_particle[-1]) | |
| p_dr = default_coater_params( | |
| **phys, air_flow_rates=(dr_m,) * 3, | |
| air_temperatures=(dr_K,) * 3, air_inlet_moisture=(0.0, 0.0, 0.0), | |
| r_drying=r_dry) | |
| res_dr = run_drying( | |
| p_dr, duration=dr_dur_min * 60.0, | |
| Y_particle_init=res_sp.Y_particle[-1], | |
| Y_gas_init=res_sp.Y_gas[-1], | |
| M_coating_init=res_sp.M_coating[-1], | |
| T_particle_init=res_sp.T_particle[-1], | |
| ) | |
| # ββ Concatenated plot series ββββββββββββββββββββββββββββββββββββββββββββββ | |
| t_ph = res_ph.t / 60.0 | |
| t_sp = (res_sp.t + res_ph.t[-1]) / 60.0 | |
| t_dr = (res_dr.t + res_ph.t[-1] + res_sp.t[-1]) / 60.0 | |
| t_all = np.concatenate([t_ph, t_sp, t_dr]) | |
| wg_max = wg_max_noloss(qty_sol_kg, dmc_frac, batch_kg) | |
| # No-loss WG reference: linear ramp during spray, flat plateau during dry | |
| WG_noloss = np.concatenate([ | |
| np.zeros_like(t_ph), | |
| np.linspace(0.0, wg_max, len(t_sp)), | |
| np.full(len(t_dr), wg_max), | |
| ]) | |
| WG = np.concatenate([ | |
| np.zeros_like(t_ph), | |
| res_sp.M_coating / batch_kg * 100, | |
| res_dr.M_coating / batch_kg * 100, | |
| ]) | |
| T_prod = np.concatenate([ | |
| res_ph.T_particle - 273.15, | |
| res_sp.T_particle - 273.15, | |
| res_dr.T_particle - 273.15, | |
| ]) | |
| T_gas = np.concatenate([ | |
| res_ph.T_gas - 273.15, | |
| res_sp.T_gas - 273.15, | |
| res_dr.T_gas - 273.15, | |
| ]) | |
| Y_part = np.concatenate([ | |
| np.zeros_like(t_ph), | |
| res_sp.Y_particle * 100, | |
| res_dr.Y_particle * 100, | |
| ]) | |
| Y_gas_ = np.concatenate([ | |
| np.zeros_like(t_ph), | |
| res_sp.Y_gas * 100, | |
| res_dr.Y_gas * 100, | |
| ]) | |
| return FullProcessResult( | |
| preheating=res_ph, spraying=res_sp, drying=res_dr, | |
| t_all=t_all, T_product=T_prod, T_gas=T_gas, | |
| Y_particle=Y_part, Y_gas=Y_gas_, WG=WG, WG_noloss=WG_noloss, | |
| ph_end=float(t_ph[-1]), sp_end=float(t_sp[-1]), t_end=float(t_dr[-1]), | |
| r_spraying=r_spray, r_drying=r_dry, | |
| dm_ratio_g_kg=dm_ratio_g_kg, qty_sol_kg=qty_sol_kg, sp_dur_s=sp_dur_s, | |
| wg_max_noloss=wg_max, | |
| wg_end_spray=float(res_sp.M_coating[-1] / batch_kg * 100), | |
| wg_final=float(res_dr.M_coating[-1] / batch_kg * 100), | |
| ) | |