% tubular_reactor_2rxn_dyn_sim.m % This MATLAB program computes the concentration profiles within a % tubular rector for the system of two elementary reactions % A + B --> C % C + B --> D % The model includes both convection and dispersion, and uses % upwind finite differences to discretize the system at a grid % of N uniformly-spaced points within the reactor. The program % is modified from the steady-state program to simulate the % dynamics of reactor start-up. % K. Beers. MIT ChE. 10/7/03 % ver. 11/17/2004 (contains all necessary files to perform simulation) function iflag_main = tubular_reactor_2rxn_dyn_sim(); iflag_main = 0; % set problem specifications Param.L = 10; % reactor length Param.k = [1; 1]; % rate constants for rxn. 1 and 2 Param.c_in = [1; 1; 0; 0]; % inlet conc. of A,B,C,D Param.v = 1; % axial velocity in reactor Param.D = 1e-4; % dispersion coefficient Param.N_f = 4; % total number of fields % Next, give name of user-supplied routine that computes the reaction rates % and the derivatives of the reaction rates with respect to each % concentration. Param.scheme_rxn = 'scheme_2rxn'; % set problem dimensions Grid.N = 100; % 100 grid points Grid.dz = Param.L/(Grid.N + 1); % uniform grid spacing Grid.z = linspace(Grid.dz,Param.L-Grid.dz,Grid.N)'; % set uniform grid Grid.N_var = Grid.N*Param.N_f; % total # of unknowns Grid.Pe_loc = Param.v*Grid.dz/Param.D; % local Peclet number of grid % as initial values, use uniform profile of [A] = 2, [B]=[C]=[D]=0 x0 = zeros(Grid.N_var,1); % allocate memory for k=1:Grid.N % for each grid point n = tubular_reactor_get_label(1,k,Param.N_f); % position of [A] x0(n) = 2; end % compute mean residence time, and generate vector of times at which to % compute concentration profile Param.t_res = Param.L/Param.v; % reactor residence time sim_period = Param.t_res / 5; t_sim_vect = [0: sim_period : 2*Param.t_res]; % call ode15s to perform dynamic simulation Options_ODE = odeset('BDF','on', ... 'Jacobian','tubular_reactor_2rxn_dyn_calc_Jac'); [t_traj,x_traj] = ode15s(@tubular_reactor_2rxn_dyn_calc_f, ... t_sim_vect,x0,Options_ODE,Param,Grid); % plot results figure; z_plot = [0; Grid.z; Param.L]; % extended to include inlet, outlet positions num_frames = length(t_traj); % number of frames in trajectory % extract and plot concentration profile of A (use BC to generate % values at the boundaries) subplot(2,2,1); for itime=1:num_frames % for each time in trajectory file field_val = extract_field(1,x_traj(itime,:)',Grid,Param); if(itime==1) plot(z_plot,field_val); hold on; elseif(itime==num_frames) plot(z_plot,field_val); else plot(z_plot,field_val,'-.'); end end xlabel('z'); ylabel('[A]'); title('Tubular reactor start-up dynamics'); % concentration profiles of B subplot(2,2,2); for itime=1:size(t_traj,1) field_val = extract_field(2,x_traj(itime,:)',Grid,Param); if(itime==1) plot(z_plot,field_val); hold on; elseif(itime==num_frames) plot(z_plot,field_val); else plot(z_plot,field_val,'-.'); end end xlabel('z'); ylabel('[B]'); % concentration profiles of C subplot(2,2,3); for itime=1:size(t_traj,1) field_val = extract_field(3,x_traj(itime,:)',Grid,Param); if(itime==1) plot(z_plot,field_val); hold on; elseif(itime==num_frames) plot(z_plot,field_val); else plot(z_plot,field_val,'-.'); end end xlabel('z'); ylabel('[C]'); % concentration profiles of D subplot(2,2,4); for itime=1:size(t_traj,1) field_val = extract_field(4,x_traj(itime,:)',Grid,Param); if(itime==1) plot(z_plot,field_val); hold on; elseif(itime==num_frames) plot(z_plot,field_val); else plot(z_plot,field_val,'-.'); end end xlabel('z'); ylabel('[D]'); iflag_main = 1; return; % ================================================================ % tubular_reactor_2rxn_dyn_calc_f.m % This routine returns the vector of time derivatives. It merely % serves as a wrapper and invokes the steady-state function to do the % actual calculation. function f = tubular_reactor_2rxn_dyn_calc_f(t,x,Grid,Param); f = tubular_reactor_2rxn_SS_calc_f(x,Grid,Param); return; % --------------------------------------------------------------------- % tubular_reactor_2rxn_dyn_calc_Jac.m % This routine returns the Jacobian matrix. It merely serves as a % wrapper and invokes the steady-state function to do the actual % calculation. The ODE solvers split the function and Jacobian % evaluations because they do not need to calculate new Jacobians % at every time step. function Jac = tubular_reactor_2rxn_dyn_calc_Jac(t,x,Grid,Param); [f,Jac] = tubular_reactor_2rxn_SS_calc_f(x,Grid,Param); return; %---------------------------------------------------------------------- % This routine computes the function vector and the Jacobian of the % tubular reactor model. It calls a user-supplied routine that % computes the rates of each reaction, the derivatives of the % reaction rates with respect to each component, and returns the % stoichiometric coefficients. From this information, the routine % computes the function vector and the Jacobian. function [f,Jac] = tubular_reactor_2rxn_SS_calc_f(x,Param,Grid); % allocate space in memory f = zeros(Grid.N_var,1); nnz_Jac_max = Grid.N_var * (Param.N_f + 2); Jac = spalloc(Grid.N_var,Grid.N_var,nnz_Jac_max); % set common coefficients for lower and upper grid points for discretizing % the transport terms A_lo = Param.v/Grid.dz + Param.D/(Grid.dz^2); A_mid = -Param.v/Grid.dz - 2*Param.D/(Grid.dz^2); A_hi = Param.D/(Grid.dz^2); % for each point for k=1:Grid.N % extract concentrations of each field variable at grid point % conc_loc = ( [A], [B], [C], [D] ) conc_loc = zeros(Param.N_f,1); for ifield=1:Param.N_f n = tubular_reactor_get_label(ifield,k,Param.N_f); conc_loc(ifield) = x(n); end % compute the reaction rates and the partial derivatives of each % rate with respect to each local concentration. This is done in % the routine supplied by the user whose name is stored in the % character string Param.rxn_scheme. [num_rxn,rxn_rate,stoich_coeff,rxn_rate_deriv] = ... feval(Param.scheme_rxn,conc_loc,Param); % compute local source term values of each species source = zeros(Param.N_f,1); for ifield=1:Param.N_f for irxn=1:num_rxn source(ifield) = source(ifield) + rxn_rate(irxn)*stoich_coeff(ifield,irxn); end end % compute source term Jacobianm matrix for which element (i,j) % is the partial derivative of the source term for field # i % with respect to the local value of field # j source_Jac = zeros(Param.N_f,Param.N_f); for ifield=1:Param.N_f for irxn=1:num_rxn for jfield=1:Param.N_f source_Jac(ifield,jfield) = source_Jac(ifield,jfield) + ... rxn_rate_deriv(jfield,irxn)*stoich_coeff(ifield,irxn); end end end % Now, initialize function vector and Jacobian elements for this grid % point to contain the source term and the source term Jacobian for ifield=1:Param.N_f n = tubular_reactor_get_label(ifield,k,Param.N_f); f(n) = source(ifield); for jfield=1:Param.N_f % for all fields m = tubular_reactor_get_label(jfield,k,Param.N_f); Jac(n,m) = source_Jac(ifield,jfield); end end % Now, get transport contribution to function and Jacobian values for ifield=1:Param.N_f n = tubular_reactor_get_label(ifield,k,Param.N_f); % dependence of transport on field value itself f(n) = f(n) + A_mid*x(n); Jac(n,n) = Jac(n,n) + A_mid; % dependence on value to the left if(k > 1) % if not the left-most point m = tubular_reactor_get_label(ifield,k-1,Param.N_f); % value of same field at left point f(n) = f(n) + A_lo*x(m); Jac(n,m) = Jac(n,m) + A_lo; else % use Danckwerts' BC to estimate conc. at inlet c_lo = (Grid.Pe_loc*Param.c_in(ifield) + x(n) ) / (1 + Grid.Pe_loc); f(n) = f(n) + A_lo*c_lo; Jac(n,n) = Jac(n,n) + A_lo/(1+Grid.Pe_loc); end % dependence on value to the right if(k < Grid.N) % if not the right-most point m = tubular_reactor_get_label(ifield,k+1,Param.N_f); % value of same field at right point f(n) = f(n) + A_hi*x(m); Jac(n,m) + Jac(n,m) + A_hi; else % use outlet BC to estimate conc. at outlet c_hi = conc_loc(ifield); % same as current value f(n) = f(n) + A_hi*c_hi; Jac(n,n) = Jac(n,n) + A_hi; end end end return; % ---------------------------------------------------------------------- % scheme_2rxn.m % This routine computes the rates of each reaction, sets the stoichiometric % coefficients, and computes the derivative of each reaction rate with % respect to the concentrations of each species. function [num_rxn,rxn_rate,stoich_coeff,rxn_rate_deriv] = ... scheme_2rxn(conc_loc,Param); % compute rates of each reaction and set stiochoimetric coefficients num_rxn = length(Param.k); rxn_rate = zeros(1,num_rxn); stoich_coeff = zeros(Param.N_f,num_rxn); % reaction 1, A + B --> C, (1) + (2) --> (3) rxn_rate(1) = Param.k(1)*conc_loc(1)*conc_loc(2); % local rate of rxn. 1 stoich_coeff(:,1) = [-1; -1; 1; 0]; % reaction 2, B + C --> D, (2) + (3) --> (4) rxn_rate(2) = Param.k(2)*conc_loc(2)*conc_loc(3); % local rate of rxn. 2 stoich_coeff(:,2) = [0; -1; -1; 1]; % compute partials of each rxn. rate with respect to concentrations. rxn_rate_deriv = zeros(Param.N_f,num_rxn); % reaction 1, A + B --> C, (1) + (2) --> (3) rxn_rate_deriv(1,1) = Param.k(1)*conc_loc(2); % deriv. of rate # 1 w.r.t. [A] rxn_rate_deriv(2,1) = Param.k(1)*conc_loc(1); % deriv. of rate # 1 w.r.t. [B] % reaction 2, B + C --> D, (2) + (3) --> (4) rxn_rate_deriv(2,2) = Param.k(2)*conc_loc(3); % deriv. or rate # 2 w.r.t. [B] rxn_rate_deriv(3,2) = Param.k(2)*conc_loc(2); % deriv. of rate # 2 w.r.t. [C] return; % ================================================================== % Save this routine to tubular_reactor_get_label.m % This routine returns the index that identifies the location in the % state vector of the concentation at a specific interior grid point function label = tubular_reactor_get_label(ifield,k,N_f); label = N_f * (k - 1) + ifield; return; % --------------------------------------------------------------------- % extract_field.m % This routine extracts from the state vector the concentration values % for a particular species from the master state vector. It also uses % the Danckwert's BC to compute the inlet and outlet concentrations. function field_val = extract_field(ifield,x,Grid,Param); field_val = zeros(Grid.N + 2, 1); % allocate space in memory for k=1:Grid.N n = tubular_reactor_get_label(ifield,k,Param.N_f); field_val(1+k) = x(n); end % add outlet value from BC field_val(Grid.N + 2) = field_val(Grid.N + 1); % add inlet value from BC field_val(1) = ( Grid.Pe_loc * Param.c_in(ifield) + field_val(2) ) / (1 + Grid.Pe_loc); return;