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:

  1. Mixer output depends on recycle (unknown)

  2. Reactor output depends on mixer output

  3. Separator output depends on reactor output

  4. 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:

  1. Guess initial recycle: \(R^{(0)}\)

  2. Calculate flowsheet with this guess → new recycle \(R^{(1)}\)

  3. Check convergence: Is \(R^{(1)} \approx R^{(0)}\)?

  4. 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#

  1. Recycles improve yield by giving unreacted material more chances to react

  2. Circular dependencies require iterative solution

  3. Fixed-point iteration: Find \(R^* = f(R^*)\) where the recycle equals itself

  4. optimistix provides robust, differentiable fixed-point solvers

  5. 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