Difference in colloidal isotherm behavior between v4.4 and v5.0

Hi all,

We’ve observed a difference in the behavior of our model with the colloidal isotherm when switching from cadet v4.4 to 5.0. To try to make sure it wasn’t coming from anything else, I set up the same model with the multicomponent Langmuir model and some dummy parameters, but the difference in the output chromatograms only showed up with the colloidal isotherm.

The script I used to generate the figures is copied below. The only thing I’ve changed is whether the Langmuir or colloidal isotherm is used and whether the cadet_bin_path references the environment where I have cadet 4.4.0 or 5.0.0 installed.

I realize the colloidal isotherm wasn’t official before, but does this mean that the previous version was in some way incorrect?

Code below:

# -*- coding: utf-8 -*-
"""
Inlet > column > outlet with GRM and colloidal isotherm.
"""
import platform
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
from cadet import Cadet


def cadet_config(cadet_bin_path, binding):
    
    print('\nCADET simulation running!\n')
    
    set_CADET_path(cadet_bin_path)
    
    # Create model object
    model = Cadet()

    # Number of unit operations
    model.root.input.model.nunits = 3
    
    n_comp = 2
    init_c = [1e3*10**(-7.4), 0]
    
    # Inlet ####################################################################
    model.root.input.model.unit_000.unit_type = 'INLET'
    model.root.input.model.unit_000.inlet_type = 'PIECEWISE_CUBIC_POLY'
    model.root.input.model.unit_000.ncomp = n_comp
    
    model.root.input.model.unit_000.sec_000.const_coeff = [1e3*10**(-7.4), 0.0338]
    model.root.input.model.unit_000.sec_001.const_coeff = [1e3*10**(-7.4), 0]
    model.root.input.model.unit_000.sec_002.const_coeff = [1e3*10**(-3.5), 0] 
    
    # General Rate Model #######################################################
    model.root.input.model.unit_001.unit_type = 'GENERAL_RATE_MODEL'
    model.root.input.model.unit_001.ncomp = n_comp
    model.root.input.model.unit_001.init_c = init_c
    model.root.input.model.unit_001.init_cp = init_c
    model.root.input.model.unit_001.init_q = [0, 0]

    ## Geometry
    model.root.input.model.unit_001.col_length = 0.018
    model.root.input.model.unit_001.cross_section_area = 1.963e-05
    model.root.input.model.unit_001.col_porosity = 0.4
    model.root.input.model.unit_001.par_porosity = 0.92
    model.root.input.model.unit_001.par_radius = 0.5*5.41e-05
                                                                                         
    ## Transport
    model.root.input.model.unit_001.col_dispersion = [1.80e-08, 1.80e-08, 1.80e-08, 1.80e-08]
    model.root.input.model.unit_001.film_diffusion = [[0.00011, 0.00011, 0.00011, 0.00011], [1.32e-05, 1.32e-05, 1.32e-05, 1.32e-05]]

    model.root.input.model.unit_001.par_diffusion = [8.85e-10, 1.54e-12]
    
    model.root.input.model.unit_001.adsorption.is_kinetic = True

    model.root.input.model.unit_001.discretization.ncol = 100
    model.root.input.model.unit_001.discretization.npar = 40
    
    ## Adsorption
    if binding=='Langmuir':
        model.root.input.model.unit_001.adsorption_model = 'MULTI_COMPONENT_LANGMUIR'
        model.root.input.model.unit_001.adsorption.is_kinetic = True    # Kinetic binding
        model.root.input.model.unit_001.adsorption.mcl_ka = [0, 0.2]      # m^3 / (mol * s)   (mobile phase)
        model.root.input.model.unit_001.adsorption.mcl_kd = [0, 1.0]      # 1 / s (desorption)
        model.root.input.model.unit_001.adsorption.mcl_qmax = [0, 300.0,]  # mol / m^3   (solid phase)
    
    #### Colloidal
    elif binding=='colloidal':
        model.root.input.model.unit_001.adsorption_model = 'MULTI_COMPONENT_COLLOIDAL'
        model.root.input.model.unit_001.adsorption.col_phi = 591000000.0
        model.root.input.model.unit_001.adsorption.col_kappa_exp = 0
        model.root.input.model.unit_001.adsorption.col_kappa_fact = 0
        model.root.input.model.unit_001.adsorption.col_kappa_const = 4
        model.root.input.model.unit_001.adsorption.col_cordnum = 6  
    
        model.root.input.model.unit_001.adsorption.col_logkeq_ph_exp = [0] * n_comp
        model.root.input.model.unit_001.adsorption.col_logkeq_salt_powexp = [0] * n_comp
        model.root.input.model.unit_001.adsorption.col_logkeq_salt_powfact = [0] * n_comp
        model.root.input.model.unit_001.adsorption.col_logkeq_salt_expfact = [0, 30.6]
        model.root.input.model.unit_001.adsorption.col_logkeq_salt_expargmult = [0, -20.9]
        model.root.input.model.unit_001.adsorption.COL_BPP_PH_EXP = [0] * n_comp
        model.root.input.model.unit_001.adsorption.COL_BPP_SALT_POWEXP = [0, -0.152]
        model.root.input.model.unit_001.adsorption.COL_BPP_SALT_POWFACT = [0, 5.73]
        model.root.input.model.unit_001.adsorption.COL_BPP_SALT_EXPFACT = [0] * n_comp
        model.root.input.model.unit_001.adsorption.COL_BPP_SALT_EXPARGMULT = [0] * n_comp
        
        model.root.input.model.unit_001.adsorption.col_radius = [1.78e-10, 4.488e-09]
        model.root.input.model.unit_001.adsorption.col_kKin = [1e8] * n_comp
        
        model.root.input.model.unit_001.adsorption.COL_LINEAR_THRESHOLD = 1e-8
        model.root.input.model.unit_001.adsorption.COL_USE_PH = False      
        
    model.root.input.model.unit_001.discretization.nbound = [0, 1]       
    ###########################################################################
    
    model.root.input.model.unit_001.discretization.par_disc_type = 'EQUIDISTANT_PAR'     
    model.root.input.model.unit_001.discretization.use_analytic_jacobian = 0
    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 = 1e-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 = 3

    ## Outlet Model ############################################################
    model.root.input.model.unit_002.unit_type = 'OUTLET'
    model.root.input.model.unit_002.ncomp = n_comp
 
   ############################################################################
    # Sections 
    model.root.input.solver.sections.nsec = 3
    model.root.input.solver.sections.section_times = [0.0, 1440.4, 2040.4, 2640.4]   # s
    model.root.input.solver.sections.section_continuity = [0, 0, 0]
   
    # units: inlet > Column > Outlet

    # Switches
    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.95e-09,  # [unit_000, unit_002, all components, all components, Q/ m^3*s^-1 
       1, 2, -1, -1, 2.95e-09,  # [unit_002, unit_004, all components, all components, Q/ m^3*s^-1
       ]

    # Solver settings
    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 = 4

    # Tolerances for the time integrator
    model.root.input.solver.time_integrator.abstol = 1e-8
    model.root.input.solver.time_integrator.algtol = 1e-12
    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
    # dont return data except for outlet unit
    model.root.input['return'].split_components_data = 0
    model.root.input['return'].split_ports_data = 0
    model.root.input['return'].unit_000.write_solution_bulk = 0
    model.root.input['return'].unit_000.write_solution_inlet = 1
    model.root.input['return'].unit_000.write_solution_outlet = 0
    
    model.root.input['return'].unit_001.write_solution_bulk = 0
    model.root.input['return'].unit_001.write_solution_inlet = 0
    model.root.input['return'].unit_001.write_solution_outlet = 0

    # return outlet data for outlet unit
    model.root.input['return'].unit_002.write_solution_bulk = 0
    model.root.input['return'].unit_002.write_solution_inlet = 0
    model.root.input['return'].unit_002.write_solution_outlet = 1
    
    # Solution times
    model.root.input.solver.user_solution_times = np.linspace(0, 2640.4, 5000)

    # Save and run simulation
    model.filename = 'cadet_version_check.h5'
    model.save()

    data = model.run()

    if data.returncode == 0:
         model.load()    
    else:
         print(data)
         raise Exception('Simulation failed')
     
    sim_solution = model.root.output.solution
        
    return sim_solution


def set_CADET_path(cadet_bin_path):    
    if platform.system() == 'Windows':
        cadet_path = cadet_bin_path / 'cadet-cli.exe'
        lwe_path = cadet_bin_path / 'createLWE.exe'
        
    else:
         cadet_path = cadet_bin_path / 'cadet-cli'
         lwe_path = cadet_bin_path / 'createLWE'
      
    if cadet_path.exists() and lwe_path.exists():
        Cadet.cadet_path = cadet_path.as_posix()
    elif cadet_path.exists() and not lwe_path.exists():
        print('CADET was found but createLWE.exe was not found. Please make sure that none of the files have been moved.')
    else:
        print('CADET could not be found. Please check the bin path')
        
        
def compare(cadet_bin_path, binding):
    CADET_solution = cadet_config(cadet_bin_path, binding)
    
    CADET_time = CADET_solution.solution_times
    inlet_array = CADET_solution.unit_000.solution_inlet
    CADET_solution_array = CADET_solution.unit_002.solution_outlet
    CADET_protein = CADET_solution_array.T[1]
    
    fig, ax = plt.subplots()

    ax.plot(CADET_time/60, CADET_solution_array, linewidth=1, color='k')
    plt.show()
    
    # ax.plot(CADET_time/60, CADET_protein, linewidth=1, color='k')
    

if __name__=='__main__':
    cadet_bin_path = Path('/Users/angelamoser/miniconda3/envs/cadet4-env/bin')       
    compare(cadet_bin_path, binding='colloidal')

Good catch, thanks for the report. I’ll look into it.

Ah, yes, now I remember :smiley:

We did change it and didn’t list the change because the isotherm was a “new” addition in v5. Sorry about that. I hope it didn’t eat too much of your time.

Here are the changes, you can see them in the src/libcadet/model/binding/ColloidalBinding.cpp file in this PullRequest

  1. Renamed COL_RADIUS to COL_PROTEIN_RADIUS
  2. Fixed kappa factors not adhering to SI units by changing it from \kappa = \frac{10^9}{\kappa_f c_0^{\kappa_e} + \kappa_c} to \kappa = \frac{1}{\kappa_f c_0^{\kappa_e} + \kappa_c}. So you’ll need to adapt your COL_KAPPA_FACT and COL_KAPPA_CONST by 10^9.
  3. Fixed a bug in the analytical Jacobian, so it works now and can speed up the simulation time if enabled.
3 Likes

Good to know, thanks. Got the expected results after changing the kappa units. :+1:

1 Like