Instruments#

The instruments module provides ready-made LC system flow sheet templates and experiment process classes. Rather than manually assembling a FlowSheet and adding events by hand, you pick a pre-built template or compose phases declaratively.

The design follows the same two-step pattern as plain CADET-Process objects: construct the flow sheet first, then pass it to the process.

LC system topology#

All templates share the same physical topology, modelled by LCFlowSheet. The topology and valve nomenclature follow typical preparative LC instruments such as the Äkta (Cytiva) and Knauer Azura series.

LC system flow sheet

Physical topology of the modeled LC system. Internally, each tubing segment, the mixer, and the sample loop are represented as explicit unit operations with configurable dimensions; see the units table below. Pumps and inline detectors are not modeled as separate units: flow rates are set directly on the inlet units.#

Inlets

Name

Purpose

buffer_a to buffer_d

Eluent reservoirs; flow through the mixer for gradient formation

feed_inlet

Feed inlet; connects directly to the sample loop

Internal units

Name

Model

Purpose

mixer

Cstr

Mixes buffers A to D

tubing_pre_injection

TubularReactor

Dead volume between mixer and injection point

sample_loop

TubularReactor

Injection loop; content set as initial condition

tubing_pre_column

TubularReactor

Pre-column dead volume

column

configurable

Chromatographic column

tubing_post_column

TubularReactor

Post-column dead volume

tubing_detectors

TubularReactor

Detector cell dead volume

Outlets: outlet (product) and waste (for loop loading and pump waste routing).

from CADETProcess.processModel import ComponentSystem, LumpedRateModelWithPores, Linear
from CADETProcess.instruments import LCFlowSheet

cs = ComponentSystem(["Salt", "Protein"])
Q = 1.0e-8  # m³/s

fs = LCFlowSheet(
    cs,
    sample_loop_volume=50e-9,
    sample_loop_diameter=0.75e-3,
    ColumnModel=LumpedRateModelWithPores,
    BindingModel=Linear,
)
print("units:", [u.name for u in fs.units])
units: ['buffer_a', 'buffer_b', 'buffer_c', 'buffer_d', 'feed_inlet', 'mixer', 'tubing_pre_injection', 'sample_loop', 'tubing_pre_column', 'column', 'tubing_post_column', 'tubing_detectors', 'outlet', 'waste']

Units can be excluded from the flow path to characterize the system sequentially, starting from the simplest configuration and adding components one at a time:

fs_no_col = LCFlowSheet(
    cs,
    sample_loop_volume=50e-9,
    sample_loop_diameter=0.75e-3,
    bypass_units=["tubing_pre_column", "column", "tubing_post_column", "tubing_detectors"],
)
print("units:", [u.name for u in fs_no_col.units])
units: ['buffer_a', 'buffer_b', 'buffer_c', 'buffer_d', 'feed_inlet', 'mixer', 'tubing_pre_injection', 'sample_loop', 'outlet', 'waste']

Process templates#

Each template takes a pre-constructed LCFlowSheet as its second argument.

PulseInjection#

The system is pre-equilibrated with buffer A. The sample loop content is injected at t=0 and washed through with buffer A.

from CADETProcess.instruments import PulseInjection

pulse = PulseInjection(
    "pulse", fs_no_col,
    c_buffer_a=[0.0, 0.0],
    c_sample=[0.0, 1.0],
    cycle_time=600.0,
    flow_rate=Q,
)
print("cycle_time:", pulse.cycle_time, "s")
print("buffer_a flow rate:", pulse.flow_sheet.buffer_a.flow_rate[0], "m³/s")
pulse.plot_events();
cycle_time: 600.0 s
buffer_a flow rate: 1e-08 m³/s
../../_images/fb6a527893dd4703eb01a4aa914377c0103d82e69f00310164dd88164fb40986.png

Step#

Switch from buffer A to buffer B at t=0. Useful for measuring system dead volumes and mixing dynamics.

from CADETProcess.instruments import Step

step = Step(
    "step", fs_no_col,
    c_buffer_a=[0.0, 0.0],
    c_buffer_b=[1000.0, 0.0],
    cycle_time=800.0,
    flow_rate=Q,
)
step.plot_events();
../../_images/44c7b8a231ef090662b4b88e064fd85d4fbdc2355d3103592e089467762c72b9.png

LWE#

Load-wash-elute with a linear salt gradient. After the wash phase, buffer B ramps up linearly while buffer A ramps down.

from CADETProcess.instruments import LWE

lwe = LWE(
    "lwe", fs,
    c_buffer_a=[20.0, 0.0],
    c_buffer_b=[1000.0, 0.0],
    c_sample=[20.0, 1.0],
    delta_t_wash=200.0,
    delta_t_elute=400.0,
    delta_t_final_wash=100.0,
    flow_rate_wash=Q,
)
print("cycle_time:", lwe.cycle_time, "s")
lwe.plot_events();
cycle_time: 700.0 s
../../_images/9563f71009de4bd2eadb39348209d12d37c4deda6ba157d82e3be4ff6f053d41.png

StepElution#

Like LWE but with an instantaneous step to buffer B instead of a gradient.

from CADETProcess.instruments import StepElution

se = StepElution(
    "se", fs,
    c_buffer_a=[20.0, 0.0],
    c_buffer_b=[1000.0, 0.0],
    c_sample=[20.0, 1.0],
    delta_t_wash=200.0,
    delta_t_elute=400.0,
    delta_t_final_wash=100.0,
    flow_rate_wash=Q,
)
print("cycle_time:", se.cycle_time, "s")
se.plot_events();
cycle_time: 700.0 s
../../_images/6cb9ab0a653b2ad175dc0c571c7b8bcd9c402afb951181243217048329cb629a.png

Breakthrough#

Sample flows continuously from t=0 through the column. By default the sample is delivered via the feed inlet (feed_inlet). Pass sample_buffer="B" (or any key A to D) to use a main buffer instead.

from CADETProcess.instruments import Breakthrough

bt = Breakthrough(
    "bt", fs,
    c_sample=[20.0, 1.0],
    flow_rate=Q,
    cycle_time=600.0,
)
print("feed flow rate:", bt.flow_sheet.feed_inlet.flow_rate[0], "m³/s")
bt.plot_events();
feed flow rate: 1e-08 m³/s
../../_images/941edb1893ef68ab4925aa843355ce7098a92130e66ca734a583be6fd50460d7.png

PhasedProcess#

PhasedProcess lets you compose arbitrary phase sequences. Each Phase specifies a duration, flow rate, and buffer fractions at the start (and optionally end) of the phase.

from CADETProcess.instruments import Phase, ValveEvent, PhasedProcess

# Three-phase wash / gradient / final-wash
steps = [
    Phase(200.0, Q, {"A": 1.0}),                    # wash: 100 % A
    Phase(400.0, Q, {"A": 1.0}, {"B": 1.0}),        # gradient: A to 0, B to 1
    Phase(100.0, Q, {"A": 1.0}),                     # final wash
]
proc = PhasedProcess("custom", fs_no_col, steps)
print("cycle_time:", proc.cycle_time, "s")
print("events:", [e.name for e in proc.events])
proc.plot_events();
cycle_time: 700.0 s
events: ['phase_0_B', 'phase_0_A', 'phase_1_B', 'phase_1_A', 'phase_2_B', 'phase_2_A']
../../_images/f34cbc81e303df6ea7a1c75dfcf83ddb3e2c12dc861328fb12deb51555bbd2ae.png

A step is a phase whose composition does not change (composition_end=None):

step_phases = [
    Phase(200.0, Q, {"A": 1.0}),   # 100 % A
    Phase(400.0, Q, {"B": 1.0}),   # step to 100 % B
    Phase(100.0, Q, {"A": 1.0}),   # step back to A
]

The feed inlet can appear as key "F" in a phase composition, but cannot be mixed with buffers A to D in the same phase:

# Deliver sample via feed inlet for 60 s, then switch to running buffer
feed_phases = [
    Phase(60.0,  Q, {"F": 1.0}),    # sample via feed
    Phase(540.0, Q, {"A": 1.0}),    # running buffer
]

Valve events#

ValveEvent is an instantaneous valve position change. Its time is determined by the cumulative duration of all preceding phases in the step sequence. The default valve state before the first step is "run".

Valve events and phases are passed together as a single steps list to PhasedProcess. This example injects at t=0 and returns to run after 30 s:

fs_pulse = LCFlowSheet(
    cs,
    sample_loop_volume=50e-9,
    sample_loop_diameter=0.75e-3,
    bypass_units=["tubing_pre_column", "column", "tubing_post_column", "tubing_detectors"],
)
pulse_steps = [
    ValveEvent("inject"),
    Phase(30.0,  Q, {"A": 1.0}),
    ValveEvent("run"),
    Phase(570.0, Q, {"A": 1.0}),
]
proc_pulse = PhasedProcess("pulse", fs_pulse, pulse_steps)
proc_pulse.plot_events();
../../_images/5c1a9e309a7a7440d05e324dfb9e6ba427be0af57715a407f871c6185670a520.png

The four named positions, with two aliases:

Valve positions#

SyP (system pump) is the buffer line: buffers A to D flow through the mixer into tubing_pre_injection. SaP (sample pump) is the feed line: feed_inlet connects directly to the sample loop (or first_unit when no loop is present).

Four named positions are available, with two aliases:

Position

Alias

SyP (tubing_pre_injection)

SaP / loop

Requires loop

"run"

first_unit

→ waste

no

"load"

first_unit

→ loop → waste

yes (degrades to "run")

"inject"

"sample_pump_waste"

→ loop → first_unit

→ waste

yes (degrades to "run")

"direct_inject"

"system_pump_waste"

→ waste

first_unit

no

Single injection then run (using LCProcess directly with add_valve_event()):

from CADETProcess.instruments import LCProcess

fs_valve = LCFlowSheet(
    cs,
    sample_loop_volume=50e-9,
    sample_loop_diameter=0.75e-3,
    bypass_units=["tubing_pre_column", "column", "tubing_post_column", "tubing_detectors"],
)
proc = LCProcess("single_inj", fs_valve)
proc.cycle_time = 600.0
proc.add_valve_event("inject", t=0.0)   # push loop contents through column
proc.add_valve_event("run",    t=30.0)  # return to normal run after loop is cleared
proc.plot_events();
../../_images/66b25650b0dcf0a7151d16d592809f0b1538b7722f719b211cd1343a8198ebfc.png

Successive injections: reload the loop during the run:

fs_multi = LCFlowSheet(
    cs,
    sample_loop_volume=50e-9,
    sample_loop_diameter=0.75e-3,
    bypass_units=["tubing_pre_column", "column", "tubing_post_column", "tubing_detectors"],
)
proc2 = LCProcess("multi_inj", fs_multi)
proc2.cycle_time = 1400.0

proc2.add_valve_event("inject", t=0.0)    # first injection: loop → column
proc2.add_valve_event("load",   t=30.0)   # feed_inlet fills loop; column on direct path
proc2.add_valve_event("inject", t=700.0)  # second injection
proc2.add_valve_event("load",   t=730.0)  # reload again

proc2.plot_events();
../../_images/8a1a0572cb867ddb5acf47279ef56941b9333f867562bd78dba7cc60ea8c7ae3.png

System equilibration: waste system pump output before the column:

fs_equil = LCFlowSheet(
    cs,
    sample_loop_volume=50e-9,
    sample_loop_diameter=0.75e-3,
    bypass_units=["tubing_pre_column", "column", "tubing_post_column", "tubing_detectors"],
)
proc3 = LCProcess("equil", fs_equil)
proc3.cycle_time = 600.0

proc3.add_valve_event("system_pump_waste", t=0.0)   # system pump to waste while equilibrating
proc3.add_valve_event("run",               t=60.0)  # switch to column path
proc3.plot_events();
../../_images/215dd744cf8b7ab8f5f3ab032f703ac8d4cd14f2b81d041b15f61aa47a143a32.png