Reference Simulation for Multi-State SMA

Hi There,

I am currently trying to generate a reference simulation for the Multi-State SMA isotherm with 2 components ‘salt’, and ‘A’ in CADET-Process.

I have set the number of bound states for component ‘A’ to 2 and for salt to 1 as required by CADET. I notice that for adsorption and desorption rates, 3 inputs are therefore required to account for one ‘salt = 0’ and the two states of component ‘A’. However when defining ‘conversion_rate’ between bound states CADET asks for 4 inputs. I would have thought a ‘0’ input would be required for salt then two values for the forward and backward conversion between states giving 3 again. Where does the extra number come from and what is the structure of this input?

I think I must be misunderstanding something as when running the below code I get the following error of:

CADETProcessError: CADET Error: Simulation failed with b’ERROR: Not enough elements in dataset MSSMA_RATES (expected at least 5 but got only 4)\r\n’

Many Thanks for your help.

import numpy as np
from CADETProcess.processModel import ComponentSystem
from CADETProcess.processModel import MultistateStericMassAction
from CADETProcess.processModel import Inlet, GeneralRateModel, Outlet
from CADETProcess.processModel import FlowSheet
from CADETProcess.processModel import Process

component_system = ComponentSystem()
component_system.add_component('Salt')
component_system.add_component('A')

binding_model = MultistateStericMassAction(component_system,bound_states=[1,2], name='MSSMA')
binding_model.is_kinetic = True
binding_model.adsorption_rate = [0.0, 35.5,60]
binding_model.desorption_rate = [0.0, 1000,1000]
binding_model.conversion_rate = [0,0,20,70]
binding_model.characteristic_charge = [0.0, 4.7,10]
binding_model.steric_factor = [0.0, 11.83,20]
binding_model.capacity = 1200.0

inlet = Inlet(component_system, name='inlet')
inlet.flow_rate = 6.683738370512285e-8

column = GeneralRateModel(component_system, name='column')
column.binding_model = binding_model

column.length = 0.014
column.diameter = 0.02
column.bed_porosity = 0.37
column.particle_radius = 4.5e-5
column.particle_porosity = 0.75
column.axial_dispersion = 5.75e-8
column.film_diffusion = column.n_comp*[6.9e-6]
column.pore_diffusion = [7e-10, 6.07e-11]
column.surface_diffusion = column.n_bound_states*[0.0]

column.c = [50, 0]
column.cp = [50, 0]
column.q = [binding_model.capacity, 0,0]

outlet = Outlet(component_system, name='outlet')

flow_sheet = FlowSheet(component_system)

flow_sheet.add_unit(inlet)
flow_sheet.add_unit(column)
flow_sheet.add_unit(outlet, product_outlet=True)

flow_sheet.add_connection(inlet, column)
flow_sheet.add_connection(column, outlet)

process = Process(flow_sheet, 'lwe')
process.cycle_time = 2000.0

load_duration = 9
t_gradient_start = 90.0
gradient_duration = process.cycle_time - t_gradient_start

c_load = np.array([50.0, 1.0])
c_wash = np.array([50.0, 0.0])
c_elute = np.array([500.0, 0.0])
gradient_slope = (c_elute - c_wash)/gradient_duration
c_gradient_poly = np.array(list(zip(c_wash, gradient_slope)))

process.add_event('load', 'flow_sheet.inlet.c', c_load)
process.add_event('wash', 'flow_sheet.inlet.c',  c_wash, load_duration)
process.add_event('grad_start', 'flow_sheet.inlet.c', c_gradient_poly, t_gradient_start)

if __name__ == '__main__':
    from CADETProcess.simulator import Cadet
    process_simulator = Cadet()

    simulation_results = process_simulator.simulate(process)

    from CADETProcess.plotting import SecondaryAxis
    sec = SecondaryAxis()
    sec.components = ['Salt']
    sec.y_label = '$c_{salt}$'

    simulation_results.solution.column.outlet.plot(secondary_axis=sec)

I think it is a simple typo: “0,0” instead of “0.0”.

Unfortunately, this is not it. Since I’ve never used the Multi-State SMA, there might still be some issues with the adapter. @George, please review the interface specification and then check the corresponding isotherm model in CADET-Process, as well as the adapter.

1 Like

Hi both ,
Thanks for getting back to me so quickly !

Reviewing the links you sent seems to suggest its some error with the conversion_rates (MSSMA_RATES). In the isotherm model link you provided the length of “conversion_rates” is given by “_conversion_entries” defined as the sum square of all bound_states but ignoring the first element which is salt . Hence for my example below where bound_states= [1,2] this returns 4. I’m not sure why 4 is required as there are only 2 conversion rates as per the diagram (i.e. k12 and k22). I thought this was perhaps due to the use of a matrix e.g. [k11,k12,k21,k22] where k11 and k22 are just set to zero. But trying this I still get my error.

This “MSSMA_RATES” input also appears not to require a “0” input for salt in the binding model but the error I’m getting seems to suggest 5 elements are required which implies the salt input is needed somewhere? I have attached the full error if this helps.

thanks in advance!

Simulation of lwe with parameters {'parameters': {'load': {'time': 0.0, 'state': array([50.,  1.])}, 'wash': {'time': 9.0, 'state': array([50.,  0.])}, 'grad_start': {'time': 90.0, 'state': array([[50.        ,  0.23560209],
       [ 0.        ,  0.        ]])}, 'cycle_time': 2000.0, 'flow_sheet': {'inlet': {'c': array([[50.        ,  0.23560209,  0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ]]), 'flow_rate': array([6.68373837e-08, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00])}, 'output_states': {'inlet': [1], 'column': [1], 'outlet': []}, 'column': {'bed_porosity': 0.37, 'particle_porosity': 0.75, 'particle_radius': 4.5e-05, 'film_diffusion': [6.9e-06, 6.9e-06], 'pore_accessibility': [1, 1], 'pore_diffusion': [7e-10, 6.07e-11], 'surface_diffusion': None, 'cp': [50, 0], 'q': [1200.0, 0, 0], 'length': 0.014, 'diameter': 0.02, 'axial_dispersion': 5.75e-08, 'flow_direction': 1, 'binding_model': {'adsorption_rate': [0.0, 35.5, 60], 'desorption_rate': [0.0, 1000, 1000], 'characteristic_charge': [0.0, 4.7, 10], 'steric_factor': [0.0, 11.83, 20], 'conversion_rate': [0, 10, 0, 10], 'capacity': 1200.0, 'reference_liquid_phase_conc': 500.0, 'reference_solid_phase_conc': 1200.0, 'is_kinetic': False}, 'discretization': {'ncol': 100, 'npar': 5, 'par_geom': 'SPHERE', 'par_disc_type': 'EQUIDISTANT_PAR', 'par_boundary_order': 2, 'use_analytic_jacobian': True, 'reconstruction': 'WENO', 'gs_type': True, 'max_krylov': 0, 'max_restarts': 10, 'schur_safety': 1e-08, 'fix_zero_surface_diffusion': False, 'weno': {'boundary_model': 0, 'weno_eps': 1e-10, 'weno_order': 3}, 'consistency_solver': {'solver_name': 'LEVMAR', 'init_damping': 0.01, 'min_damping': 0.0001, 'max_iterations': 50, 'subsolvers': 'LEVMAR'}, 'nbound': [1, 2]}, '_surface_diffusion': [0.0, 0.0, 0.0]}, 'outlet': {}}}, 'initial_state': {'system_state': None, 'system_state_derivative': None, 'flow_sheet': {'inlet': {}, 'column': {'cp': [50, 0], 'q': [1200.0, 0, 0]}, 'outlet': {}}}} failed.
There was an exception in simulate_n_cycles
Traceback (most recent call last):
  File "C:\Users\user\Anaconda3\envs\cadetenv\lib\site-packages\CADETProcess\log.py", line 172, in wrapper
    return function(*args, **kwargs)
  File "C:\Users\user\Anaconda3\envs\cadetenv\lib\site-packages\CADETProcess\simulator\simulator.py", line 306, in simulate_n_cycles
    return self.run(process, **kwargs)
  File "C:\Users\user\Anaconda3\envs\cadetenv\lib\site-packages\CADETProcess\simulator\cadetAdapter.py", line 136, in wrapper
    results = func(self, process, *args, **kwargs)
  File "C:\Users\user\Anaconda3\envs\cadetenv\lib\site-packages\CADETProcess\simulator\cadetAdapter.py", line 444, in run
    raise CADETProcessError(
CADETProcess.CADETProcessError.CADETProcessError: CADET Error: Simulation failed with b'ERROR: Not enough elements in dataset MSSMA_RATES (expected at least 5 but got only 4)\r\n'
---------------------------------------------------------------------------
CADETProcessError                         Traceback (most recent call last)
Cell In[8], line 82
     79 from CADETProcess.simulator import Cadet
     80 process_simulator = Cadet()
---> 82 simulation_results = process_simulator.simulate(process)
     84 from CADETProcess.plotting import SecondaryAxis
     85 sec = SecondaryAxis()

File ~\Anaconda3\envs\cadetenv\lib\site-packages\CADETProcess\simulator\simulator.py:250, in SimulatorBase.simulate(self, process, previous_results, **kwargs)
    247     raise CADETProcessError("Process is not configured correctly.")
    249 if not self.evaluate_stationarity:
--> 250     results = self.simulate_n_cycles(
    251         process, self.n_cycles, previous_results, **kwargs
    252     )
    253 else:
    254     results = self.simulate_to_stationarity(
    255         process, previous_results, **kwargs
    256     )

File ~\Anaconda3\envs\cadetenv\lib\site-packages\CADETProcess\log.py:150, in log_time.<locals>.log_time_decorator.<locals>.wrapper(*args, **kwargs)
    147 @wraps(function)
    148 def wrapper(*args, **kwargs):
    149     start = time.time()
--> 150     result = function(*args, **kwargs)
    151     elapsed = time.time() - start
    152     logger = get_logger(logger_name, level=None)

File ~\Anaconda3\envs\cadetenv\lib\site-packages\CADETProcess\log.py:201, in log_results.<locals>.log_results_decorator.<locals>.wrapper(*args, **kwargs)
    197 logger = get_logger(logger_name, level=None)
    199 logger.debug('{} was called with {}, {}'.format(
    200         function, *args, **kwargs))
--> 201 results = function(*args, **kwargs)
    202 logger.debug(f'Results: {results}')
    204 return results

File ~\Anaconda3\envs\cadetenv\lib\site-packages\CADETProcess\log.py:180, in log_exceptions.<locals>.log_exception_decorator.<locals>.wrapper(*args, **kwargs)
    177 logger.exception(err)
    179 # re-raise the exception
--> 180 raise e

File ~\Anaconda3\envs\cadetenv\lib\site-packages\CADETProcess\log.py:172, in log_exceptions.<locals>.log_exception_decorator.<locals>.wrapper(*args, **kwargs)
    170 logger = get_logger(logger_name, level=None)
    171 try:
--> 172     return function(*args, **kwargs)
    173 except Exception as e:
    174     # log the exception
    175     err = "There was an exception in "

File ~\Anaconda3\envs\cadetenv\lib\site-packages\CADETProcess\simulator\simulator.py:306, in SimulatorBase.simulate_n_cycles(self, process, n_cyc, previous_results, **kwargs)
    303 if previous_results is not None:
    304     self.set_state_from_results(process, previous_results)
--> 306 return self.run(process, **kwargs)
    308 self.n_cycles = n_cyc_orig

File ~\Anaconda3\envs\cadetenv\lib\site-packages\CADETProcess\simulator\cadetAdapter.py:136, in Cadet.locks_process.<locals>.wrapper(self, process, *args, **kwargs)
    133     process.lock = True
    134     locked_process = True
--> 136 results = func(self, process, *args, **kwargs)
    138 if locked_process:
    139     process.lock = False

File ~\Anaconda3\envs\cadetenv\lib\site-packages\CADETProcess\simulator\cadetAdapter.py:444, in Cadet.run(self, process, cadet, file_path)
    439 if return_information.returncode != 0:
    440     self.logger.error(
    441         f'Simulation of {process.name} '
    442         f'with parameters {process.config} failed.'
    443     )
--> 444     raise CADETProcessError(
    445         f'CADET Error: Simulation failed with {return_information.stderr}'
    446     ) from None
    448 try:
    449     results = self.get_simulation_results(
    450         process, cadet, elapsed, return_information
    451     )

CADETProcessError: CADET Error: Simulation failed with b'ERROR: Not enough elements in dataset MSSMA_RATES (expected at least 5 but got only 4)\r\n'

Ok, probably I didn’t fully understand the MSSMA_RATES parameter. If do the following:

    @property
    def _conversion_entries(self):
        n = 0
        for state in self.bound_states:
            n += state**2

        return n

it seems to run.

This returns 1² + 2² = 5 as the number of entries. What those 5 entries mean in terms of the model equation? :person_shrugging: I assume we need some input from people who better understand the model. @s.leweke?

1 Like

Thank Johannes, this makes more sense with 5 now.

Where exactly in the code are you inputting those lines to get it to run ?

As for the entries I’m guessing they are a matrix with all possible conversions in the bound state (i.e. if there were 3 bound states you could transition 1 ->2 then 2->3 but also 1->3) but there would still be entries in this that were effectively meaningless (i.e. 1->1) which I assume are set to zero?

Hopefully someone with more experience can weigh in :slight_smile:

In the MSSMA model.

Let’s consider a system with one salt component (only one bound state) and two protein components that have 2 and 3 bound states, respectively.
The ordering of the rates would then be

comp0fromBnd0toBnd0,
comp1fromBnd0toBnd0, comp1fromBnd0toBnd1,
comp1fromBnd1toBnd0, comp1fromBnd1toBnd1,
comp2fromBnd0toBnd0, comp2fromBnd0toBnd1, comp2fromBnd0toBnd2,
comp2fromBnd1toBnd0, comp2fromBnd1toBnd1, comp2fromBnd1toBnd2,
comp2fromBnd2toBnd0, comp2fromBnd2toBnd1, comp2fromBnd2toBnd2

Note that the rates compXfromBndYtoBndY (conversion from a bound state to itself) are ignored in the MSSMA model. In addition, the rates of the salt component 0 (i.e., comp0fromBnd0toBnd0) are ignored.

The address of the parameters (in terms of SENS_COMP and SENS_BOUNDPHASE is given by

comp0Bnd0,
comp1Bnd0, comp1Bnd1,
comp1Bnd2, comp1Bnd3,
comp2Bnd0, comp2Bnd1, comp2Bnd2,
comp2Bnd3, comp2Bnd4, comp2Bnd5,
comp2Bnd6, comp2Bnd7, comp2Bnd8

That is, the elements are enumerated in row-major ordering inside each component.

In summary, we have a component-row-major ordering.

2 Likes

Thanks for your input @s.leweke!

We had a call yesterday with George and the way we interpreted it for us is a list of flattened square matrices (with shape NBND_i \times NBND_i) where the diagonal (i.e. the self-transition rate) is always ignored and should consequently be set to 0.

To make this a bit more readable, I suggest, setting up the system this way (values are arbitrary):

import numpy as np

n_bound_states = [1, 2, 3]

conversion_rates_0 = np.zeros((n_bound_states[0], n_bound_states[0]))
conversion_rates_1 = np.zeros((n_bound_states[1], n_bound_states[1]))
conversion_rates_1[0, 1] = 1
conversion_rates_1[1, 0] = 0.5
conversion_rates_2 = np.zeros((n_bound_states[2], n_bound_states[2]))
conversion_rates_2[0, 1] = 1
conversion_rates_2[0, 2] = 0.5
conversion_rates_2[1, 0] = 1
conversion_rates_2[2, 1] = 0.5
# [...]

conversion_rates = [conversion_rates_0, conversion_rates_1, conversion_rates_2]

# To visualize:
for rate in conversion_rates:
    print(rate)

>>> [0.]
>>> [[0.  1. ]
     [0.5 0. ]]
>>> [[0.  1.  0.5]
     [1.  0.  0. ]
     [0.  0.5 0. ]]

for rate in conversion_rates:
    print(rate.flatten())
>>> [0.]
>>> [0.  1.  0.5 0. ]
>>> [0.  1.  0.5 1.  0.  0.  0.  0.5 0. ]

# To set in the binding model:
conversion_rate = []
for rate in conversion_rates:
    conversion_rate += rate.flatten().tolist()

binding_model.conversion_rate = conversion_rate

Maybe at some point we could also include this in the conversion_rates setter in CADET-Process but for now, a little bit of manual pre-processing has never harmed anyone, right? :nerd_face:

1 Like

Fix is in this pull request.