Batch Elution Chromatography#

tags: batch elution, langmuir

A basic chromatographic batch-elution setup is comprised of feed and eluent reservoirs, a pump that can deliver the necessary flow rate against the pressure drop of the packed column, a valve to select whether feed or eluent are pumped into the column, the column itself, and one or more valves for the collection of fractions.

In CADET-Process, this can be modelled by connecting two Inlets, a column model (e.g. LumpedRateModelWithoutPores), and an Outlet.

../../_images/flow_sheet.svg

Flow sheet for batch elution process.#

For this example, consider a two-component system with a Langmuir isotherm.

Component System#

from CADETProcess.processModel import ComponentSystem

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

Binding Model#

from CADETProcess.processModel import Langmuir

binding_model = Langmuir(component_system, name='langmuir')
binding_model.is_kinetic = False
binding_model.adsorption_rate = [0.02, 0.03]
binding_model.desorption_rate = [1, 1]
binding_model.capacity = [100, 100]

Unit Operations#

from CADETProcess.processModel import (
    Inlet, LumpedRateModelWithoutPores, Outlet
)
feed = Inlet(component_system, name='feed')
feed.c = [10, 10]

eluent = Inlet(component_system, name='eluent')
eluent.c = [0, 0]

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

column.length = 0.6
column.diameter = 0.024
column.axial_dispersion = 4.7e-7
column.total_porosity = 0.7

column.solution_recorder.write_solution_bulk = True

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

Flow Sheet#

from CADETProcess.processModel import FlowSheet

flow_sheet = FlowSheet(component_system)

flow_sheet.add_unit(feed, feed_inlet=True)
flow_sheet.add_unit(eluent, eluent_inlet=True)
flow_sheet.add_unit(column)
flow_sheet.add_unit(outlet, product_outlet=True)

flow_sheet.add_connection(feed, column)
flow_sheet.add_connection(eluent, column)
flow_sheet.add_connection(column, outlet)

Process#

To model the injection valve, Events are introduced that modify the flow_rate attribute of the Inlet unit operations.

../../_images/events.svg

Events of batch elution process.#

from CADETProcess.processModel import Process
process = Process(flow_sheet, 'batch elution')

Q = 60/(60*1e6)
process.add_event('feed_on', 'flow_sheet.feed.flow_rate', Q)
process.add_event('feed_off', 'flow_sheet.feed.flow_rate', 0.0)

process.add_event('eluent_on', 'flow_sheet.eluent.flow_rate', Q)
process.add_event('eluent_off', 'flow_sheet.eluent.flow_rate', 0.0)
Event(name=eluent_off, parameter_path=flow_sheet.eluent.flow_rate, state=0.0, time=0.0, indices=[(slice(None, None, None),)])

Event Dependencies#

To reduce the number of event times that need to be specified, event dependencies are specified which enforce that always either feed or eluent are being pumped through the column.

../../_images/event_dependencies.svg

Events of batch elution process with event dependencies.#

process.add_duration('feed_duration')

process.add_event_dependency('eluent_on', ['feed_off'])
process.add_event_dependency('eluent_off', ['feed_on'])
process.add_event_dependency('feed_off', ['feed_on', 'feed_duration'], [1, 1])

Event Times#

Now, the cycle time is set to \(10~min\) and the feed_duration to \(1~min\).

process.cycle_time = 600
process.feed_duration.time = 60

Simulate Process#

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

    simulation_results = process_simulator.simulate(process)

Optimize Fractionation Times#

After simulation, the SimulationResults can be analyzed to determine optimal fractionation times using the fractionation module.

if __name__ == '__main__':
    from CADETProcess.fractionation import FractionationOptimizer
    fractionation_optimizer = FractionationOptimizer()

    fractionator = fractionation_optimizer.optimize_fractionation(
        simulation_results, purity_required=[0.95, 0.95]
    )

    print(fractionator.performance)
    _ = fractionator.plot_fraction_signal()
Performance(mass=array([0.00049938, 0.00043264]), concentration=array([6.10878074, 2.1254641 ]), purity=array([0.94999998, 0.94967134]), recovery=array([0.83230706, 0.72107007]), productivity=array([0.01022113, 0.00885508]), eluent_consumption=array([0.92478562, 0.80118897]) mass_balance_difference=array([-7.82813818e-10,  9.89672933e-10]))

By selecting appropriate operating conditions, such as injected amount and flow rate, an efficient operating scenario can be achieved in which the stationary phase is utilized very efficiently. The highest product recovery is achieved by “baseline separation,” where the component peaks of the same injection do not overlap when leaving the column. Moreover, by minimizing the time between two injections, productivity can be improved. By allowing for waste fractions collected between product fractions or between peaks of consecutive injections, productivity and eluent consumption may be further improved at the cost of lower recovery.

These operating conditions can be adjusted using model based design. For this purpose, an OptimizationProblem is set up where to maximize process performance. This can be achieved by combining multiple parameters into a single objective (see Optimize Batch Elution Process (Single Objective)) or by setting up a multi-objective problem (see Optimize Batch Elution Process (Multi-Objective)).