Section dependent isotherms and alternative approaches

I wonder if a time section dependent isotherm is possible in CADET? I wanted to simulate a situation where either the isotherm parameters change or the isotherm model changes during one single simulation (experiment).

Time section dependent isotherm

After looking at the CADET document, I found the inlet unit and transport parameters can be section dependent, but not adsorption models. This is not supported now.

Run simulations sequentially, use the last state of a previous simulation to initialize the next

This should be possible. Relevant fields to check are: write_solution_last_unit (or write_solution_last). This will write the state vectors at the last time point to last_state_y in the output group and time derivatives to last_state_ydot. Appending last_state_ydot to last_state_y gives the final vector that can be passed to the next simulation through the init_state in the input group.

Using external functions

A successful example is posted below. This can change isotherm parameters but not the model itself.

Unfortunately the second approach did not work properly. See my test below.

original simulation

GMR + Multicomponent Langmuir. Single component, breakthrough.

Break up this simulation to two parts: t \in (0,8000) and t \in (8000, 16000).

simulation 1

set up init_state for simulation 2

Append last_state_y and last_state_ydot and pass it to init_state.

simulation 2

The same script in simulation 1 is used. Nothing changes except for the init_state. The results are:

In theory I should get a horizontal line. This result would indicate something is wrong.


The last state of simulation 1:

This plots q and c_p as well as c as a function of particle radius r at an axial position at a fixed time. Note that c is a constant at an axial position and is not dependent on r. To facilitate comparison I plotted it as a straight horizontal line here.

The initial state of simulation 2 (at the same axial position):

The magnitude is already different. Upon a closer look at the outputted last_state_y vector in simulation 1:
It already contains these large values that are not seen in the simulation 1 which I think caused the problem.

Is there any conversion between the output last_state_y and the input init_state to properly set up the simulations?


1 Like

In your particular example (breaking up the simulation in parts with same isotherm), I suggest to disable consistent initialization for the second part:

model.root.input.solver.consistent_init_mode = 0 # or 3

My theory is that consistent initialization somehow fails and produces those unreasonable values.

Another way to accomplish your goal (change isotherm parameters during simulation) are external functions. Here, the isotherm parameters are taken as

k_{a,i} = k_{a,i}(T) = k_{a,i,0} + k_{a,i,1} T + k_{a,i,2} T^2 + k_{a,i,3} T^3,

where T = T(t) is some profile provided to the simulator. So each isotherm parameter is a third-degree polynomial of a given profile. You could set the constant coefficients of the isotherm parameters to your first configuration and the linear coefficients to

second configuration - first configuration.

For the first part of the simulation, we need to set T = 0 and for the second, we change to T=1. Here are the docs for specifying a profile, where you’d set VELOCITY to inf: A profile is usually moved along the column over time. In your case, we want the profile to instantly apply to all spatial positions (solely depend on time).

We have some posts in the forum about external functions.


Thanks for the reply!

  1. Disabling the consistent initialization unfortunately did not work. IDAS would fail at t=0, which is kind of strange since the last_state_y and last_state_ydot from simulation 1 should already be consistent? I think the problem may have something to do with the last_state_y vector from simulation 1, it already contained some unreal values which I was not able to find at any t, r and z.

  2. Using external functions seems to be working. My test case is simple: GRM, Multi-component Langmuir

    t \in (0, 4000): q_{max}=21, \; k_a=100
    t \in (4000, 8000): q_{max}=23, \; k_a=110.

    A runnable Python code is below:

from cadet import Cadet
import numpy as np
import matplotlib.pyplot as plt

# replace the path
Cadet.cadet_path = r"C:\Users\zwend\CADET\cadetL2\bin\cadet-cli.exe" 

## set up
c_feed = 0.0113                                            

def create_model():
    model = Cadet()

    model.root.input.model.nunits = 3

    model.root.input.model.unit_000.unit_type = 'INLET'
    model.root.input.model.unit_000.ncomp = 1
    model.root.input.model.unit_000.inlet_type = 'PIECEWISE_CUBIC_POLY'

    model.root.input.model.unit_001.unit_type = 'GENERAL_RATE_MODEL'
    model.root.input.model.unit_001.ncomp = 1

    ## Geometry
    model.root.input.model.unit_001.col_length = 0.05                         # m              
    model.root.input.model.unit_001.cross_section_area = 5.025e-6/0.05      # m^2
    model.root.input.model.unit_001.col_porosity = 0.37                     # 1      
    model.root.input.model.unit_001.par_porosity = 0.97                     # 1      
    model.root.input.model.unit_001.par_radius = 0.045e-3                              # m     

    ## Transport
    model.root.input.model.unit_001.col_dispersion = 2.061e-8                          # m^2/s       
    model.root.input.model.unit_001.film_diffusion = [1.18e-5]                         # m/s 
    model.root.input.model.unit_001.par_diffusion = [0.8e-11,]                              # m^2/s  [0.55e-11]
    model.root.input.model.unit_001.par_surfdiffusion = [0.0, ]    

    ## external adsorption isotherm
    model.root.input.model.unit_001.adsorption_model = 'EXT_MULTI_COMPONENT_LANGMUIR'
    model.root.input.model.unit_001.adsorption.extfun = 0
    model.root.input.model.unit_001.adsorption.is_kinetic = 1
    model.root.input.model.unit_001.adsorption.ext_mcl_ka =     100.0         
    model.root.input.model.unit_001.adsorption.ext_mcl_ka_t =   10.0
    model.root.input.model.unit_001.adsorption.ext_mcl_ka_tt =  0
    model.root.input.model.unit_001.adsorption.ext_mcl_ka_ttt = 0
    model.root.input.model.unit_001.adsorption.ext_mcl_kd =     1.0                                           ## s^-1
    model.root.input.model.unit_001.adsorption.ext_mcl_kd_t =   0
    model.root.input.model.unit_001.adsorption.ext_mcl_kd_tt =  0
    model.root.input.model.unit_001.adsorption.ext_mcl_kd_ttt = 0
    model.root.input.model.unit_001.adsorption.ext_mcl_qmax =     21.0        
    model.root.input.model.unit_001.adsorption.ext_mcl_qmax_t =   2.0
    model.root.input.model.unit_001.adsorption.ext_mcl_qmax_tt =  0
    model.root.input.model.unit_001.adsorption.ext_mcl_qmax_ttt = 0
    ## external function
    model.root.input.model.external.source_000.extfun_type = 'PIECEWISE_CUBIC_POLY'
    model.root.input.model.external.source_000.section_times = [0.0, 4000.0, 8000.0]
    model.root.input.model.external.source_000.velocity = 1e5                                                 # set it to a large number
    model.root.input.model.external.source_000.const_coeff = [0.0, 1.0]
    model.root.input.model.external.source_000.lin_coeff =   [0.0, 0.0]
    model.root.input.model.external.source_000.quad_coeff =  [0.0, 0.0]
    model.root.input.model.external.source_000.cube_coeff =  [0.0, 0.0]
    ## init condition
    model.root.input.model.unit_001.init_c = [0.0, ]
    model.root.input.model.unit_001.init_q = [0.0, ] 

    ### Grid cells
    model.root.input.model.unit_001.discretization.ncol = 50                    ## increase as needed
    model.root.input.model.unit_001.discretization.npar = 12                    ## same as above

    ### Bound states
    model.root.input.model.unit_001.discretization.nbound = [1, ]

    ### Other options
    model.root.input.model.unit_001.discretization.par_disc_type = 'EQUIDISTANT_PAR'    
    model.root.input.model.unit_001.discretization.use_analytic_jacobian = 1
    model.root.input.model.unit_001.discretization.reconstruction = 'WENO'
    model.root.input.model.unit_001.discretization.gs_type = 1
    model.root.input.model.unit_001.discretization.max_krylov = 0
    model.root.input.model.unit_001.discretization.max_restarts = 10
    model.root.input.model.unit_001.discretization.schur_safety = 1.0e-8

    model.root.input.model.unit_001.discretization.weno.boundary_model = 0
    model.root.input.model.unit_001.discretization.weno.weno_eps = 1e-10
    model.root.input.model.unit_001.discretization.weno.weno_order = 2

    model.root.input.model.unit_002.unit_type = 'OUTLET'
    model.root.input.model.unit_002.ncomp = 1

    model.root.input.solver.sections.nsec = 1
    model.root.input.solver.sections.section_times = [0.0, 8000]   # s
    model.root.input.solver.sections.section_continuity = [0,0]

    model.root.input.model.unit_000.sec_000.const_coeff = [c_feed]                                                        # mol / m^3;  mg/ml = kg/m^3;  1 kda = 1 kg/mol 1.25/150
    model.root.input.model.unit_000.sec_000.lin_coeff = [0.0]
    model.root.input.model.unit_000.sec_000.quad_coeff = [0.0]
    model.root.input.model.unit_000.sec_000.cube_coeff = [0.0]

    model.root.input.model.connections.nswitches = 1
    model.root.input.model.connections.switch_000.section = 0
    model.root.input.model.connections.switch_000.connections = [
        0, 1, -1, -1, 2.5 /(6e7),
        1, 2, -1, -1, 2.5 /(6e7)]

    model.root.input.model.solver.gs_type = 1
    model.root.input.model.solver.max_krylov = 0
    model.root.input.model.solver.max_restarts = 10
    model.root.input.model.solver.schur_safety = 1e-8

    # Number of cores for parallel simulation
    model.root.input.solver.nthreads = 1

    # Tolerances for the time integrator
    model.root.input.solver.time_integrator.abstol = 1e-6
    model.root.input.solver.time_integrator.algtol = 1e-10
    model.root.input.solver.time_integrator.reltol = 1e-6
    model.root.input.solver.time_integrator.init_step_size = 1e-6
    model.root.input.solver.time_integrator.max_steps = 1000000

    # Return data
    model.root.input['return'].split_components_data = 1
    model.root.input['return'].split_ports_data = 0
    model.root.input['return'].unit_000.write_solution_outlet = 1

    # Copy settings to the other unit operations
    model.root.input['return'].unit_001 = model.root.input['return'].unit_000
    model.root.input['return'].unit_002 = model.root.input['return'].unit_000

    # Solution times
    model.root.input.solver.user_solution_times = np.linspace(0, 8000, 1000+1)
    return model

model = create_model()
model.filename = 'model.h5'
data =

time = model.root.output.solution.solution_times
c = model.root.output.solution.unit_001.solution_outlet_comp_000                                        ## 1 mol/m^3 * 150 kg/mol=150 kg/m^3 = 150 mg/mL
fig = plt.figure()
ax = fig.add_subplot()
ax.plot(time, c*150, c="orange") 
ax.set_ylabel(r'conc./ mg/mL')

The warning would still appear: Warning: setExternalFunctions::113] Index 0 exceeds number of passed external functions (0), external dependence is ignored\r\n, but it should be fine. The following plot would appear:

If there is no change in the isotherm parameters:

The sharp decrease at t=4000 made sense because q_{max} and k_a increased, the column continued to absorb the feed so the outlet concentration decreased.


Please ignore this warning. This is a known “problem”.

Thanks for sharing the solution!

This is actually something @j.breuer and I were discussing recently. I need to follow this exact approach to solve a dilemma described in the earlier post (Protein at inlet not showing up at outlet - #10 by Flynn). We were not able to figure out a solution (Jan mentioned there might be a problem with consistent initialization). I still have those hdf5 files lying around, so we could look into actually solving the problem again. To be honest, it was a highly atypical application of CADET and using a workaround with running simulations sequentially is totally fine. The ultimate outlet solution already requires three separate simulations, before we implement the workaround.

I have another application on the docket that also requires using section-dependent isotherm parameters, so I am interested in the outcome of your investigation @Flynn.

1 Like