EO vs SM Solver Benchmarks#
This notebook compares the equation-oriented (EO) and sequential modular (SM) solvers on various flowsheet problems.
import jax
import jax.numpy as jnp
import time
jax.config.update("jax_enable_x64", True)
from difflow import (
CSTR, CSTRParams,
Mixer, Splitter,
Flowsheet, Unit,
EOSolver,
IdealThermo, SpeciesData,
make_stream, get_flows,
)
from difflow.benchmarks import compare_solvers
# Thermo setup
species_data = {
"A": SpeciesData("A", MW=100.0, Cp_coeffs=(75.0, 0.0, 0.0, 0.0),
Hvap_coeffs=(35000.0, 0.38, 500.0),
antoine_coeffs=(10.0, 3000.0, -50.0)),
"B": SpeciesData("B", MW=100.0, Cp_coeffs=(75.0, 0.0, 0.0, 0.0),
Hvap_coeffs=(30000.0, 0.38, 450.0),
antoine_coeffs=(10.0, 2800.0, -40.0)),
}
thermo = IdealThermo(species_data)
def rate_fn(C, T, params):
k = params["A"] * jnp.exp(-params["Ea"] / (8.314 * T))
return jnp.array([k * C["A"]])
stoich = jnp.array([[-1.0], [+1.0]])
rate_params = {"A": jnp.array(1e6), "Ea": jnp.array(50000.0)}
print("Setup complete.")
Setup complete.
Benchmark 1: CSTR with Recycle#
Compare SM and EO on a CSTR with 30% recycle.
cstr_params = CSTRParams(
V=jnp.array(1.0), rate_fn=rate_fn, stoich=stoich,
rate_params=rate_params, species_order=["A", "B"],
)
cstr = CSTR(cstr_params, thermo=thermo, mode="isothermal")
mixer = Mixer(species_order=["A", "B"])
splitter = Splitter(species_order=["A", "B"])
fs = Flowsheet(species_order=["A", "B"])
feed = make_stream({"A": 10.0, "B": 0.0}, T=300.0, P=101325.0)
fs.add_feed("feed", feed)
fs.add_unit(Unit("mixer", mixer, ["feed", "recycle"], ["mixed"]))
fs.add_unit(Unit("reactor", cstr, ["mixed"], ["reactor_out"],
params={"T_spec": 350.0}))
fs.add_unit(Unit("splitter", splitter, ["reactor_out"],
["product", "recycle"], params={"split_frac": 0.7}))
fs.add_recycle("recycle", "recycle")
comparison = compare_solvers(fs, tol=1e-8)
print(f"SM time: {comparison.sm_time:.4f} s")
print(f"EO time: {comparison.eo_time:.4f} s")
print(f"EO iterations: {comparison.eo_iterations}")
print(f"Max stream difference: {comparison.max_stream_difference:.2e}")
print(f"EO converged: {comparison.eo_converged}")
SM time: 10.1030 s
EO time: 1.1670 s
EO iterations: 3
Max stream difference: 1.53e-09
EO converged: True
Benchmark 2: EO from SM Init#
When initialized from the SM solution, EO should converge in very few iterations.
solver = EOSolver(fs)
# Cold start
result_cold = solver.solve(use_sm_init=False, tol=1e-8)
print(f"Cold start: {result_cold.n_iterations} iterations, "
f"residual = {result_cold.residual_norm:.2e}, "
f"time = {result_cold.wall_time:.4f}s")
# Warm start from SM
result_warm = solver.solve(use_sm_init=True, tol=1e-8)
print(f"SM init: {result_warm.n_iterations} iterations, "
f"residual = {result_warm.residual_norm:.2e}, "
f"time = {result_warm.wall_time:.4f}s")
Cold start: 3 iterations, residual = 5.68e-14, time = 0.5897s
SM init: 2 iterations, residual = 4.44e-16, time = 1.5202s
Summary#
SM and EO converge to the same solution (within tolerance)
EO with SM initialization converges in few Newton iterations
Both approaches support automatic differentiation through the solution