Recycle Streams#
Prerequisites: 00h_connecting_units, 00i_parallel_bypass
Learning Objectives:
Understand why recycles are essential in chemical processes
See how recycles create circular dependencies
Use optimistix to solve recycle loops via fixed-point iteration
Learn about tear streams and convergence
Why Recycle?#
Many reactions don’t go to completion in a single pass. Without recycle:
Ammonia synthesis: ~15% conversion per pass → 85% of feed wasted!
Methanol synthesis: ~10-15% per pass
Most equilibrium-limited reactions
Solution: Separate unreacted material and recycle it back.
┌─────────────────────────────────┐
│ Recycle │
▼ │
Feed ──►Mixer──►Reactor──►Separator──►Product
│
└──► (recycle)
This dramatically improves overall conversion!
The Circular Dependency Problem#
The recycle stream creates a circular dependency:
Mixer output depends on recycle (unknown)
Reactor output depends on mixer output
Separator output depends on reactor output
Recycle depends on separator output → back to step 1!
We can’t solve this in one forward pass. We need iteration.
# Setup
import jax.numpy as jnp
import jax
jax.config.update("jax_enable_x64", True)
import matplotlib.pyplot as plt
import optimistix as optx
from difflow import (CSTR, CSTRParams, Flash, FlashParams,
make_stream, get_flows, combine_streams, IdealThermo, SpeciesData)
from difflow.units.flash import Mixer
WARNING:2026-01-10 21:11:02,396:jax._src.xla_bridge:852: An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu.
# Define process: A (heavy) -> B (light)
species_data = {
'A': SpeciesData(name='A', MW=100.0, Cp_coeffs=(100.0, 0, 0, 0),
Hvap_coeffs=(40000.0, 0.38, 500.0),
antoine_coeffs=(10.0, 2200.0, -40.0), Hf=0.0),
'B': SpeciesData(name='B', MW=80.0, Cp_coeffs=(80.0, 0, 0, 0),
Hvap_coeffs=(32000.0, 0.38, 450.0),
antoine_coeffs=(10.0, 1600.0, -40.0), Hf=-50000.0),
}
thermo = IdealThermo(species_data)
species_order = ['A', 'B']
# Rate function
def rate_fn(C, T, params):
return jnp.array([params['k'] * C['A']])
stoich = jnp.array([[-1.0], [1.0]])
# Create units
cstr_params = CSTRParams(
V=jnp.array(2.0),
rate_fn=rate_fn,
stoich=stoich,
rate_params={'k': jnp.array(0.3)},
species_order=species_order,
)
reactor = CSTR(cstr_params, thermo=thermo, mode='isothermal')
flash_params = FlashParams(species_order=species_order)
flash = Flash(flash_params, thermo=thermo)
mixer = Mixer(species_order, thermo=thermo)
print("Process units created")
Process units created
Fixed-Point Iteration#
To solve the recycle loop:
Guess initial recycle: \(R^{(0)}\)
Calculate flowsheet with this guess → new recycle \(R^{(1)}\)
Check convergence: Is \(R^{(1)} \approx R^{(0)}\)?
If not, update guess and repeat
We’re looking for a fixed point: \(R^* = f(R^*)\)
Using optimistix for Fixed-Point Iteration#
The optimistix library provides robust, JAX-native fixed-point solvers with automatic differentiation support.
# Define the flowsheet iteration function
def one_iteration(recycle, fresh_feed, T_reactor=350.0, T_flash=350.0, P_flash=50000.0, Q_vol=0.1):
"""
One pass through the flowsheet.
Returns the NEW recycle stream (liquid from flash) and product (vapor).
"""
# Mix fresh feed with recycle
reactor_inlet, _ = mixer(fresh_feed, recycle)
# React
reactor_out, _ = reactor(reactor_inlet, T_spec=T_reactor, volumetric_flow=Q_vol)
# Separate at lower pressure for good vapor/liquid separation
liquid, vapor, _ = flash(reactor_out, T=T_flash, P=P_flash)
# Liquid is the new recycle, vapor is product
return liquid, vapor
# Fresh feed
fresh_feed = make_stream({'A': 10.0, 'B': 0.0}, T=300.0, P=101325.0)
def flowsheet_iteration(recycle_arr, args):
"""Flowsheet as a function of recycle array for optimistix."""
fresh_feed = args
recycle = make_stream(
{'A': recycle_arr[0], 'B': recycle_arr[1]},
T=350.0, P=101325.0
)
new_recycle, _ = one_iteration(recycle, fresh_feed)
return jnp.array([new_recycle['F_A'], new_recycle['F_B']])
# Solve using optimistix fixed-point solver
initial_guess = jnp.array([1.0, 0.1])
solver = optx.FixedPointIteration(rtol=1e-8, atol=1e-8)
solution = optx.fixed_point(flowsheet_iteration, solver, initial_guess, args=fresh_feed, max_steps=100)
converged = solution.value
# Final evaluation
final_recycle = make_stream({'A': converged[0], 'B': converged[1]}, T=350.0, P=101325.0)
final_liquid, final_product = one_iteration(final_recycle, fresh_feed)
print("Recycle Loop Solution (optimistix)")
print("=" * 50)
print(f"Converged recycle: A = {float(converged[0]):.4f}, B = {float(converged[1]):.4f} mol/s")
print(f"Product B: {float(get_flows(final_product)['B']):.4f} mol/s")
print(f"")
print(f"Process Performance:")
print(f" Feed A: 10.0 mol/s")
print(f" Product B: {float(get_flows(final_product)['B']):.4f} mol/s")
print(f" Overall yield: {float(get_flows(final_product)['B'])/10.0*100:.1f}%")
Recycle Loop Solution (optimistix)
==================================================
Converged recycle: A = 1.6147, B = 4.1861 mol/s
Product B: 9.9554 mol/s
Process Performance:
Feed A: 10.0 mol/s
Product B: 9.9554 mol/s
Overall yield: 99.6%
Impact of Recycle on Performance#
Let’s compare with and without recycle.
# Without recycle (single pass)
reactor_out_single, info_single = reactor(fresh_feed, T_spec=350.0, volumetric_flow=0.1)
liquid_single, vapor_single, _ = flash(reactor_out_single, T=350.0, P=50000.0)
print("Comparison: With vs Without Recycle")
print("=" * 50)
print(f"")
print(f"{'Metric':<25} {'Single Pass':<15} {'With Recycle':<15}")
print("-" * 55)
print(f"{'Reactor conversion':<25} {float(info_single['conversion']['A'])*100:<15.1f} {'(per-pass)':<15}")
print(f"{'Product B (mol/s)':<25} {float(get_flows(vapor_single)['B']):<15.4f} {float(get_flows(final_product)['B']):<15.4f}")
print(f"{'Waste A (mol/s)':<25} {float(get_flows(liquid_single)['A']):<15.4f} {float(get_flows(final_liquid)['A']):<15.4f}")
print(f"{'Overall yield (%)':<25} {float(get_flows(vapor_single)['B'])/10*100:<15.1f} {float(get_flows(final_product)['B'])/10*100:<15.1f}")
print(f"")
print(f"Per-pass conversion is lower with recycle because inlet is diluted,")
print(f"but overall yield is MUCH higher due to multiple passes!")
Comparison: With vs Without Recycle
==================================================
Metric Single Pass With Recycle
-------------------------------------------------------
Reactor conversion 85.7 (per-pass)
Product B (mol/s) 4.9245 9.9554
Waste A (mol/s) 1.4066 1.6147
Overall yield (%) 49.2 99.6
Per-pass conversion is lower with recycle because inlet is diluted,
but overall yield is MUCH higher due to multiple passes!
Try It Yourself!#
Exercise 1: Purge Stream#
Add a purge stream that removes 5% of the recycle to prevent accumulation of inerts. How does this affect yield?
Exercise 2: Multiple Recycles#
Add a second flash drum that further separates the liquid stream. Can you improve yield?
Exercise 3: Recycle Ratio#
Define recycle ratio = (recycle flow)/(fresh feed flow). Plot yield vs recycle ratio.
Key Takeaways#
Recycles improve yield by giving unreacted material more chances to react
Circular dependencies require iterative solution
Fixed-point iteration: Find \(R^* = f(R^*)\) where the recycle equals itself
optimistix provides robust, differentiable fixed-point solvers
Tear streams: The streams we “guess” to break the cycle
Next Steps#
In the final notebook (00k: Why Differentiable Flowsheets?), we’ll discover:
Why gradients through flowsheets are powerful
How difflow differentiates through recycle iterations
Applications: optimization, sensitivity, uncertainty