- Batch time to 90 % conversion
- CSTR versus PFR volume for 95 % conversion
- Adiabatic temperature rise and runaway check
- Yield model from a factorial design
- Shortcut column design: Fenske, Underwood, Gilliland
- Residence time distribution and tanks-in-series model
- Process capability of an API assay
- Production campaign planning
- Mass balance of a two-column distillation train
- Agitation scale-up at constant power per volume
Batch time to 90 % conversion
An isothermal batch reactor runs A + B -> C (k = 0.012 L/mol.min, CA0 = 1.5 and CB0 = 1.8 mol/L). The script integrates the mole balances and stops exactly when 90 % of A is converted (event), then checks the batch time against the analytic formula.
#1. A + B to C in a batch reactor, second order: k = 0.012 L/mol.min, CA0 = 1.5, CB0 = 1.8 mol/L; #2. Integrate until 90 % conversion of A: event A - 0.15 = 0; json load "rx_ch_br" (num ode "{\"A\": \"-0.012*A*B\", \"B\": \"-0.012*A*B\", \"C\": \"0.012*A*B\"}" "{\"A\": 1.5, \"B\": 1.8, \"C\": 0}" 0 1000 "{\"event\": \"A - 0.15\", \"direction\": -1}"); -> "[t90]" (json select "rx_ch_br" /event/t); #3. Check with the analytic batch time for A + B with CA0 different from CB0; -> "[t_an]" (math calc "1/(0.012*(1.8-1.5))*ln((0.45/1.8)/(0.15/1.5))"); concat "t(X = 90 %) = " (math round [t90] 2) " min (analytic " (math round [t_an] 2) " min), C formed = " (math round (json select "rx_ch_br" /event/C) 3) " mol/L"
t(X = 90 %) = 254.53 min (analytic 254.53 min), C formed = 1.35 mol/L
- Add the cycle time (filling, heating, emptying) to size the reactor per year.
- Draw A, B, C with
plot ode.
CSTR versus PFR volume for 95 % conversion
A first order liquid reaction (A = 4.0e7 1/min, Ea = 60 kJ/mol) runs at 350 K with a feed of 2.5 m3/h. The script computes k by Arrhenius, the residence times tau = X/(k(1-X)) for a CSTR and -ln(1-X)/k for a PFR, then the reactor volumes in m3.
#1. First order liquid reaction at 350 K: A = 4.0e7 1/min, Ea = 60 kJ/mol; -> "[k]" (math calc "4.0e7*exp(-60000/(_R*350))"); #2. Residence times for 95 % conversion: CSTR X/k.1-X, PFR -ln 1-X / k; -> "[tc]" (math calc "0.95/([k]*(1-0.95))"); -> "[tp]" (math calc "-ln(1-0.95)/[k]"); #3. Reactor volumes for a feed of 2.5 m3/h; -> "[vc]" (unit calc (concat "2.5 m3/h * " [tc] " min") "m3"); -> "[vp]" (unit calc (concat "2.5 m3/h * " [tp] " min") "m3"); concat "k = " (math round [k] 4) " 1/min; CSTR: tau = " (math round [tc] 0) " min, V = " (math round [vc] 1) " m3; PFR: tau = " (math round [tp] 0) " min, V = " (math round [vp] 1) " m3"
k = 0.0444 1/min; CSTR: tau = 428 min, V = 17.8 m3; PFR: tau = 67 min, V = 2.8 m3
- Find the number of CSTRs in series that reach the PFR volume with
num sum. - Optimize the temperature with
num minimizeon a cost function.
Adiabatic temperature rise and runaway check
An exothermic first order reaction (dHr = -85 kJ/mol, CA0 = 2.0 mol/L) is run in a batch without cooling, starting at 320 K. The script gives the adiabatic temperature rise dT_ad = (-dHr).CA0/(rho.cp), then integrates the coupled conversion and energy balances to find when 99 % conversion is reached and the maximum temperature.
#1. Adiabatic temperature rise: dHr = -85 kJ/mol, CA0 = 2.0 mol/L, rho = 1.05 kg/L, cp = 3.9 kJ/kg.K; -> "[dtad]" (unit calc "85 kJ/mol * 2.0 mol/L / (1.05 kg/L * 3.9 kJ/(kg*K))" "K"); #2. Adiabatic batch, first order, k = 1e13.exp of -11000/T in 1/min, T0 = 320 K; json load "rx_ch_ad" (num ode (concat "{\"X\": \"1e13*exp(-11000/T)*(1-X)\", \"T\": \"" [dtad] "*1e13*exp(-11000/T)*(1-X)\"}") "{\"X\": 0, \"T\": 320}" 0 600 "{\"event\": \"X - 0.99\", \"direction\": 1}"); #3. Time to 99 % conversion and final temperature; concat "dT_ad = " (math round [dtad] 1) " K, X = 99 % after " (math round (json select "rx_ch_ad" /event/t) 1) " min, T max = " (math round (stat max (json select "rx_ch_ad" /T)) 1) " K"
dT_ad = 41.5 K, X = 99 % after 33 min, T max = 361.1 K
- Compute the time to maximum rate (TMRad) for the safety study.
- Add a cooling term UA(T - Tc) and find the critical coolant temperature.
Yield model from a factorial design
A 2^2 design with centre points gives the yield against temperature and residence time. The script fits yield = b0 + b1 T + b2 t by multiple regression, gives the standard errors, R2 and adjusted R2, and predicts the yield at 105 degC and 3.5 h.
#1. Factorial design: yield in % against temperature in degC and residence time in h; -> "[X]" "[[80, 2], [80, 4], [90, 2], [90, 4], [100, 2], [100, 4], [85, 3], [95, 3]]"; -> "[Y]" "[62.1, 70.3, 68.9, 77.2, 74.8, 83.5, 70.0, 76.4]"; pa rm load_from_json "rx_yield" [X] [Y]; #2. Model yield = b0 + b1 T + b2 t, with the standard errors of b; -> "[b]" (pa rm estimate_regression_parameters "rx_yield"); -> "[se]" (pa rm estimate_regression_parameters_standard_errors "rx_yield"); #3. Quality of the model; -> "[r2]" (pa rm calculate_r_squared "rx_yield"); -> "[r2a]" (pa rm calculate_adjusted_r_squared "rx_yield"); -> "[s]" (pa rm estimate_regression_standard_error "rx_yield"); #4. Predicted yield at 105 degC and 3.5 h, then close the model; -> "[pred]" (pa rm predict "rx_yield" "[105, 3.5]"); pa rm close "rx_yield"; concat "{\"b\": " [b] ", \"std_err\": " [se] ", \"r2\": " (math round [r2] 4) ", \"r2_adj\": " (math round [r2a] 4) ", \"s\": " (math round [s] 3) ", \"yield_105C_3.5h\": " (math round [pred] 1) "}"
{"b": [
2.099999999999971,
0.646666666666667,
4.199999999999999
], "std_err": [
1.4008925726122052,
0.014907119849998762,
0.129099444873582
], "r2": 0.9983, "r2_adj": 0.9976, "s": 0.316, "yield_105C_3.5h": 84.7}- Look at the residuals with
pa rm estimate_residualsbefore extrapolating. - Add an interaction column T*t to the X rows and compare the adjusted R2.
Shortcut column design: Fenske, Underwood, Gilliland
Size a benzene-toluene column: minimum stages at total reflux (Fenske), minimum reflux ratio from the Underwood root, operating reflux 1.3 Rmin, theoretical stages by the Gilliland correlation and real trays for a 70 % efficiency.
#1. Benzene-toluene column: relative volatility 2.45, feed 50 % saturated liquid q = 1, distillate 95 %, bottoms 5 % of benzene; -> "[al]" 2.45; -> "[xD]" 0.95; -> "[xB]" 0.05; -> "[zF]" 0.5; #2. Fenske: minimum number of theoretical stages at total reflux; -> "[Nmin]" (math calc "ln(([xD]/(1-[xD]))*((1-[xB])/[xB]))/ln([al])"); #3. Underwood: root theta between 1 and alpha of sum alpha.z over alpha - theta = 1 - q = 0, then Rmin; -> "[th]" (num solve (concat [al] "*" [zF] "/(" [al] "-t)+(1-" [zF] ")/(1-t)") "t" 1.0001 2.4499); -> "[Rmin]" (math calc "[al]*[xD]/([al]-[th])+(1-[xD])/(1-[th])-1"); #4. Operating reflux 1.3 Rmin, Gilliland correlation by Eduljee, real stages with a 70 % tray efficiency; -> "[R]" (math calc "1.3*[Rmin]"); -> "[X]" (math calc "([R]-[Rmin])/([R]+1)"); -> "[Y]" (math calc "0.75*(1-[X]^0.5668)"); -> "[Nth]" (math calc "([Y]+[Nmin])/(1-[Y])"); concat "Nmin = " (math round [Nmin] 2) ", theta = " (math round [th] 4) ", Rmin = " (math round [Rmin] 3) ", R = " (math round [R] 3) ", X = " (math round [X] 3) ", Y = " (math round [Y] 3) ", N = " (math round [Nth] 2) " theoretical stages including the reboiler, " (math ceiling (math calc "([Nth]-1)/0.7")) " real trays"
Nmin = 6.57, theta = 1.4203, Rmin = 1.141, R = 1.484, X = 0.138, Y = 0.506, N = 14.33 theoretical stages including the reboiler, 20 real trays
- Locate the feed tray with the Kirkbride equation
- Find the reflux ratio that minimizes the total cost (trays + reboiler duty) with
num minimize
Residence time distribution and tanks-in-series model
From a pulse tracer test, compute the mean residence time and variance of the RTD, the equivalent number of tanks in series and the peak of E(t), then the conversion of a first-order reaction compared with an ideal PFR and CSTR.
#1. Pulse tracer test on a continuous reactor: outlet concentration in mg/L every 2.5 min; -> "[c]" "[0.0, 20.43, 36.95, 40.81, 33.43, 24.67, 17.51, 10.88, 6.69, 4.06, 2.39, 1.33, 0.76, 0.41, 0.23, 0.12, 0.06]"; -> "[tab]" (num table "t^2" "t" 0 40 2.5); #2. Moments of the RTD: mean residence time and variance, weights = C on a regular grid; -> "[tm]" (stat weighted_mean (matrix col [tab] 1) [c]); -> "[s2]" (math calc (concat (stat weighted_mean (matrix col [tab] 2) [c]) " - [tm]^2")); #3. Tanks-in-series model: N = tm^2 / sigma^2, E of t at the peak t = tm.N-1/N with Gamma of N; -> "[n]" (math calc "[tm]^2/[s2]"); -> "[tp]" (math calc "[tm]*([n]-1)/[n]"); -> "[ep]" (math calc (concat "[n]^[n]*[tp]^([n]-1)/([tm]^[n]*" (math gamma [n]) ")*exp(-[n]*[tp]/[tm])")); #4. First-order reaction k = 0.25 1/min: conversion with N tanks vs ideal PFR and CSTR; -> "[x]" (math calc (concat "1 - " (math power (math calc "1+0.25*[tm]/[n]") (math calc "-[n]")))); concat "tm = " (math round [tm] 2) " min, sigma2 = " (math round [s2] 2) " min2, N = " (math round [n] 2) " tanks, E max = " (math round [ep] 4) " 1/min at " (math round [tp] 2) " min; X = " (math round (math calc "100*[x]") 1) " % vs PFR " (math round (math calc "100*(1-exp(-0.25*[tm]))") 1) " % and CSTR " (math round (math calc "100*0.25*[tm]/(1+0.25*[tm])") 1) " %"
tm = 10.01 min, sigma2 = 32.78 min2, N = 3.06 tanks, E max = 0.0816 1/min at 6.74 min; X = 83.9 % vs PFR 91.8 % and CSTR 71.5 %
- Fit N directly on the normalized E(t) curve with
stat fit_model. - Use the segregation model with the measured E(t) for a second-order reaction.
Process capability of an API assay
For 20 consecutive batches of an active ingredient, compute Cp and Cpk against the 98.0-102.0 % specification and the expected fraction of out-of-specification batches from the normal tails.
#1. Assay of 20 consecutive API batches in % of label claim, specification 98.0 - 102.0; -> "[a]" "[100.4, 99.8, 100.9, 100.2, 99.5, 100.7, 101.1, 100.0, 99.9, 100.6, 100.3, 101.4, 99.7, 100.5, 100.8, 100.1, 99.6, 100.9, 100.4, 101.0]"; -> "[lsl]" 98.0; -> "[usl]" 102.0; #2. Process mean and standard deviation; -> "[m]" (stat mean [a]); -> "[s]" (stat stddev [a]); #3. Capability indices Cp and Cpk; -> "[cp]" (math calc "([usl]-[lsl])/(6*[s])"); -> "[cpk]" (math calc "min([usl]-[m], [m]-[lsl])/(3*[s])"); #4. Expected fraction out of specification: normal tails = 1/2.erfc of z/sqrt 2; -> "[pu]" (math calc (concat "0.5*" (math erfc (math calc "([usl]-[m])/([s]*sqrt(2))")))); -> "[pl]" (math calc (concat "0.5*" (math erfc (math calc "([m]-[lsl])/([s]*sqrt(2))")))); concat "mean = " (math round [m] 2) " %, s = " (math round [s] 3) " %, Cp = " (math round [cp] 2) ", Cpk = " (math round [cpk] 2) ", expected out of spec = " (math round (math calc "1e6*([pu]+[pl])") 1) " ppm of the batches"
mean = 100.39 %, s = 0.538 %, Cp = 1.24, Cpk = 1, expected out of spec = 1385.9 ppm of the batches
- Check normality with
stat shapirobefore trusting the ppm. - Build an I-MR control chart with
plot chart.
Production campaign planning
Plan the yearly campaign of an intermediate: overall yield of a 4-step route, number of full batches and size of the last one, campaign length with cleaning, and starting material to order.
#1. Annual demand 18.5 t of an intermediate, reactor batch 1.35 t of product, 4 steps with these yields; -> "[dem]" 18.5; -> "[bat]" 1.35; -> "[y]" "[0.94, 0.88, 0.97, 0.91]"; #2. Overall yield = product of the step yields, i.e. geometric mean ^ n; -> "[yo]" (math power (stat geo_mean [y]) 4); #3. Full batches and the size of the last partial batch; -> "[nb]" (math calc "[dem]/[bat]"); -> "[full]" (math trunc [nb]); -> "[last]" (math calc (concat (math frac_part [nb]) "*[bat]")); #4. Campaign length with a 38 h cycle and 3 cleaning days, starting material needed per t of product: 0.92 t at 100 % yield; -> "[days]" (math calc (concat "(" (math ceiling [nb]) "*38)/24 + 3")); concat "overall yield = " (math round (math calc "100*[yo]") 1) " %, " [full] " full batches + 1 batch of " (math round [last] 3) " t, campaign = " (math round [days] 1) " days, starting material = " (math round (math calc "[dem]*0.92/[yo]") 2) " t"
overall yield = 73 %, 13 full batches + 1 batch of 0.95 t, campaign = 25.2 days, starting material = 23.31 t
- Add a safety stock and the lead time of the starting material.
- Compare campaigns in one or two reactors with
num minimizeon the total cost.
Mass balance of a two-column distillation train
Solve the component balances of a benzene/toluene/xylene train with fixed product specifications to get the three product flows, check the conditioning of the system and that no flow is negative.
#1. Two columns in series, feed 100 kmol/h of benzene / toluene / xylene = 0.40 / 0.35 / 0.25; #2. Specs: D1 = 0.98 / 0.02 / 0, D2 = 0.01 / 0.97 / 0.02, B2 = 0 / 0.03 / 0.97; -> "[m]" "[[0.98, 0.01, 0], [0.02, 0.97, 0.03], [0, 0.02, 0.97]]"; -> "[f]" "[40, 35, 25]"; #3. Component balances M.flows = feed: solve, and check the conditioning of the system; -> "[q]" (matrix solve [m] [f]); -> "[k]" (matrix cond [m]); #4. All flows must be positive: sign of the smallest one; -> "[ok]" (math sign (stat min [q])); concat "D1 = " (math round (matrix get (concat "[" [q] "]") 1 1) 2) ", D2 = " (math round (matrix get (concat "[" [q] "]") 1 2) 2) ", B2 = " (math round (matrix get (concat "[" [q] "]") 1 3) 2) " kmol/h, sum = " (math round (stat sum [q]) 2) ", condition number = " (math round [k] 2) ", sign of the smallest flow = " [ok]
D1 = 40.46, D2 = 34.47, B2 = 25.06 kmol/h, sum = 100, condition number = 1.06, sign of the smallest flow = 1
- Add the energy balance of each reboiler with the latent heats.
- Make the specs uncertain and propagate with
num uncertainty.
Agitation scale-up at constant power per volume
Scale a Rushton-agitated reaction from a 2 L lab reactor to 6.3 m3 at constant P/V: vessel and impeller diameters, plant speed, Reynolds number, power draw and tip speed.
#1. Lab reactor 2 L, plant reactor 6.3 m3, H = T, Rushton turbine D = T/3, Np = 5, lab speed 450 rpm; -> "[v1]" 0.002; -> "[v2]" 6.3; -> "[n1]" 450; -> "[rho]" 1050; -> "[mu]" 0.004; #2. Vessel diameters from V = pi.T^3/4 by the cube root, impeller diameters; -> "[d1]" (math calc (concat (math cbrt (math calc "4*[v1]/pi")) "/3")); -> "[d2]" (math calc (concat (math cbrt (math calc "4*[v2]/pi")) "/3")); #3. Scale-up at constant power per volume: N2 = N1.D1/D2 ^2/3; -> "[n2]" (math calc (concat "[n1]*" (math power (math calc "[d1]/[d2]") (math calc "2/3")))); #4. Plant Reynolds number, power and tip speed, N in rev/s; -> "[re]" (math calc "[rho]*([n2]/60)*[d2]^2/[mu]"); -> "[p]" (math calc "5*[rho]*([n2]/60)^3*[d2]^5"); concat "D lab = " (math round (math calc "[d1]*1000") 1) " mm, D plant = " (math round [d2] 3) " m, N plant = " (math round [n2] 1) " rpm, Re = " (math round [re] 0) " turbulent, P = " (math round (math calc "[p]/1000") 2) " kW = " (math round (math calc "[p]/[v2]") 0) " W/m3, tip speed = " (math round (math calc "pi*[d2]*[n2]/60") 2) " m/s"
D lab = 45.5 mm, D plant = 0.667 m, N plant = 75.1 rpm, Re = 146346 turbulent, P = 1.36 kW = 216 W/m3, tip speed = 2.62 m/s
- Compare with constant tip speed (shear-sensitive crystals) or constant mixing time.
- Check the heat removal: U.A grows like V^2/3 only.
