ColumnForge is a PySide6 desktop application containing an open-source toolkit for distillation column synthesis and rigorous equilibrium-stage simulation. Unlike most commercial simulators, it includes initialization and synthesis tools such as Boundary Value Methods (BVM), Rectification Bodies (RBM), Residue Curve Mapping (RCM), and other features for designing difficult columns and sending to a rigorous simulation in one integrated workflow.
This application is intended to be an educational tool for Chemical Engineers.
Commercial simulators ship powerful column solvers, but their design workflows and initialization methods are proprietary and hard to inspect or extend. ColumnForge is the open version of that workflow, with the synthesis tools (RCM, BVM, RBM, FUG, Txy/Pxy) feeding the rigorous solver directly.
| Commercial Simulator | ColumnForge | |
|---|---|---|
| solver internals | opaque | readable Python, one self-check per module |
| initialization | proprietary | BVM/FUG warm start, every intermediate visible |
| preliminary design | separate tools, manual bridge | RCM/BVM/RBM/FUG in-app, sharing the session's thermo |
| when it fails to converge | "no solution found" | named section, pinch, or spec that caused it |
pip install -r requirements.txt # PySide6, numpy, scipy, matplotlib
python launch.py # run the GUI (canonical entry point)launch.py puts src/python (for core/gui) and src (for
side_features.*) on the path and calls gui.main_window.main(). To run a
module directly, replicate those paths:
PYTHONPATH=src:src/python python -m gui.main_window.
Note
Solver backend. Python is the backend that ships today, and it needs no
compiler. A compiled C/Fortran backend selectable from the UI is in progress;
the sources live in src/native/.
Tip
The RCM (residue-curve map) module has two engines. side_features/rcm/pyrcm.py is pure
NumPy/SciPy and always works; RCM_solv.c is a port of it into C (Fortran thermo, called through
ctypes) that returns the same numbers 7-30x faster. Prebuilt libraries ship in
src/side_features/rcm/lib/, but they were built on a machine that may or may not share your
architecture — if they do not load, the module says so in its status line and draws the map
anyway, just slowly. Building it yourself is worth it:
make -C src/side_features/rcm # needs gfortran + a C compilerAdditional requirements for this include GSL and MINPACK. The rest of the app is unaffected either way.
The GUI stores parameters and hands it to separate solver/module layers for calculation.
flowchart TD
subgraph GUI["gui/ Qt layer"]
WS["<b>WindowState</b><br/>single source of truth"]
TABS["tabs/ Initialization → Specifications →<br/>Simulation → Results → Modules<br/>panels/ state/"]
WS --- TABS
end
subgraph CORE["core/ Qt-free solver library"]
SI["<b>SolverInput</b><br/>canonical column, per-stage NumPy arrays"]
TH["thermodynamics.py<br/>Antoine/PLXANT, K-values, activity, EOS"]
CS["column_solvers.py<br/>Bubble-Point · Inside-Out"]
DOF["dof.py / operating_specs.py<br/>spec ledger → (R, D)"]
BAL["material_balance.py · enthalpy.py<br/>shortcut.py · flash.py"]
SI --> CS
TH --> CS
DOF --> SI
BAL --> CS
end
subgraph SIDE["side_features/ Modules tab"]
BVM["<b>bvm/</b><br/>difference-point-chain<br/>boundary-value sizing"]
RBM["<b>rbm/</b><br/>rectification bodies<br/>feasibility, R_min / R_max"]
RCM["<b>core/rcm.py</b><br/>residue-curve maps<br/>(RCM_solv.c → nifco2.f90,<br/>or the pyrcm.py port)"]
RBM -->|"operating point"| BVM
end
GUI -->|"build_solver_input()"| CORE
BVM -->|"warm start: stages + profiles"| CORE
CORE -.->|"species, thermo model"| SIDE
style WS fill:#1f6feb,stroke:#1f6feb,color:#fff
style SI fill:#1f6feb,stroke:#1f6feb,color:#fff
style CS fill:#238636,stroke:#238636,color:#fff
style BVM fill:#8957e5,stroke:#8957e5,color:#fff
style RBM fill:#8957e5,stroke:#8957e5,color:#fff
style RCM fill:#8957e5,stroke:#8957e5,color:#fff
Tip
core never imports gui; it is a standalone library you can drive from a
script or notebook with no Qt installed. Every core module carries a runnable
self-check (python -m core.column_solvers and friends), so each file vouches
for itself independently of the pytest suite.
| method | answers | scope | |
|---|---|---|---|
| Bubble-Point | Wang-Henke MESH | rigorous tray-by-tray profile | CMO, total condenser |
| Inside-Out | Boston-Sullivan two-tier | same, faster on stiff systems | CMO or full energy balance |
| BVM | difference-point-chain boundary-value method | Stages/section, |
sizing/feasibility, energy balance, reactive stages |
| RBM | rectification bodies (Bausa/Marquardt) | feasibility, Rmin and Rmax, (E/F)min — no stage count | screening/feasibility, simple + extractive columns |
| Shortcut (FUG) | Fenske-Underwood-Gilliland-Kirkbride | back-of-envelope N, Rmin, feed stage | screening tool, constant α |
Material balance, Equilibrium, Summation, Heat balance, solved stage-by-stage until temperatures stop moving. The per-component material balance is a tridiagonal system in liquid composition, solved with the Thomas algorithm: one linear solve per component, per outer iteration.
with
Outer loop:
- assemble the tridiagonal system at the current
Tprofile - Thomas-solve for
x - bubble-point each stage for a new
T - repeat until
max|ΔT| < tol
Note
Constant molar overflow (CMO) by default. An optional flows_hook seam lets an
energy balance replace the CMO flow assumption without touching the assembly
code.
Boston and Sullivan method with an outer loop that refreshes rigorous K-values and freezes them into per-stage relative volatilities, and a cheap inner loop that iterates only those frozen ratios against the material balance.
Outer (expensive thermo, once per pass), freeze a per-stage base K and relative volatilities:
Inner (no thermo calls), hold
Refresh
Answers whether a split is reachable and how many stages each section needs.
Given an operating point (R, S, E/F), BVM builds one difference point
Each section marches stage by stage: an equilibrium step, then an operating-line
step anchored on
Profiles march inward from each product end until adjacent profiles meet. Two
profiles are connected when they come within tolerance in full
After Levy, Van Dongen & Doherty (1985) and Doherty & Malone, Conceptual Design of Distillation Systems (2001); reactive columns follow Ung & Doherty (1995).
Tip
A sized BVM column goes straight to Bubble-Point as a warm start
(api.to_solver). Module-by-module writeup:
src/side_features/bvm/README.md.
BVM marches profiles and asks whether the curves meet. RBM never marches. A
pinch is a stage where nothing changes any more: the equilibrium map and the
section's operating line
Solving that algebraic system on every branch gives a section's pinch points; spanning them gives a rectification body, a linearised stand-in for the set its profiles can reach. Feasibility is then geometry: adjacent sections' bodies intersect (gap zero) exactly when one continuous profile can run the whole column.
That buys three things marching does not:
- The pinch equations are algebraic, so a component with a very small K costs nothing. Marching amplifies it by ~1/K per stage.
- Bodies are up to (C−1)-dimensional and intersect generically at any C; two 1-D marched profiles generically miss for C ≥ 4.
- An extractive column has a maximum reflux as well as a minimum — too much reflux washes the entrainer out of the extractive section. The same test gives both bounds, and both move with entrainer flow.
Important
RBM gives no stage count — a body approximates the reachable set, not the
profile. Use RBM to locate a feasible operating point, BVM to size the column
there. It also wants sharp product specs: exact zeros in
After Bausa, von Watzdorf & Marquardt, AIChE J. 44(10) 2181 (1998),
extended to extractive columns by Brüggemann & Marquardt (2002).
Module-by-module writeup:
src/side_features/rbm/README.md.
Good starting point for columns with more ideal behavior and CMO validity.
Fenske, minimum stages at total reflux:
Underwood, minimum reflux (root
Gilliland/Molokanov, actual stages at operating reflux
Kirkbride, feed-stage location (
Every solver uses some form of modified Raoult's law, formulated
with the equilibrium ratio
| layer | models |
|---|---|
| vapor pressure | Antoine |
|
activity |
NRTL, Wilson, UNIQUAC, two-suffix Margules, UNIFAC group contribution (no binary parameters needed, built from the bundled group-interaction database) |
| equation of state | SRK vapor-phase fugacity, for pressure effects on relative volatility (validated on a 4-atm depropanizer) |
| enthalpy | constant liquid |
Full equations for every model: docs/thermodynamics.md.
I used AI to scrape some parameters for components and test them, and came up with:
- 78 curated components (
core/data/components.json), searchable by name, alias, CAS, or formula, one click to load into a column. Every record is gated by a physical-consistency test: Antoine reproduces Tb within 1 K, ΔHvap matches Clausius-Clapeyron within 12%. - 7 NRTL binary pairs ship with it, gated against known azeotropes (ethanol/water, 2-propanol/water, acetone/chloroform, acetone/methanol).
- UNIFAC groups for 54 of the 78 components, loaded with the component, so UNIFAC needs no binary parameters and no typing — the way around a missing NRTL pair. Each assignment is gated on adding up to the molecular formula, and the ester/alcohol groups on reproducing two literature azeotropes (methyl acetate/methanol 0.657 at 53.8 °C, ethyl acetate/ethanol 0.539 at 71.8 °C). The other 24 are left blank on purpose: the curated group table cannot express them (CH₄, ethers, CHCl₃, formic acid, pyridine, …) and UNIFAC then refuses that species rather than running ideal.
- The enthalpy seam is shared by the Inside-Out energy balance, enthalpy-based feed quality, and condenser subcooling.
Warning
No silent fallback. A model with missing parameters raises a user-facing error instead of quietly reverting to ideal.
Standalone tools that share the session's species and thermo but need no full column defined.
| module | what it does |
|---|---|
| RCM | residue-curve maps for a ternary: auto-generated or one curve per click, the azeotrope/node/saddle table, and the two-phase (liquid-liquid) region shaded with tie lines (writeup) |
| BVM | size at one R, sweep a design map, send a warm start to the rigorous solver, or size a reactive column |
| RBM | pinches + rectification bodies at one point, feasibility, and the r_min/r_max band |
| Shortcut (FUG) | Fenske/Underwood/Gilliland/Kirkbride report + stages-vs-reflux curve |
| Txy/Pxy | binary bubble/dew loci at fixed P or T, plus an azeotrope table (singular-point classification) |
| Pure Components | browse/search the 78-species database, plot Psat(T), load straight into the column |
| Phase EQ | isothermal / vapor-fraction flash on the loaded species, a live test bench for whichever thermo model is selected |
Tick Reaction in the BVM module, type the stoichiometry (products +, reactants
−), pick the reference component and give Keq = exp(A + B/T[K]), and the sizing
runs in Ung–Doherty transformed compositions: same difference-point geometry,
one fewer component, chemical equilibrium solved inside every stage. The results
table shows the transformed profile alongside the physical compositions and the
reaction extent per stage.
Limits, enforced rather than assumed:
- One equilibrium reaction, ideal stages, every stage catalytic (condenser and reboiler included, so products sit on the reaction-equilibrium surface). Efficiency, entrainer and Send to Rigorous Solver grey out with the reason — the MESH solvers carry no reaction terms, so a reactive warm start would converge a different column.
- The transform must stay inside the composition simplex, which holds for a
one-product reaction with the product as reference (etherification,
hydration, hydrogenation). A two-product reaction — any esterification, ester
plus water — has no such reference, and the sizing says so
(
leaves_simplex) instead of quietly returning a wrong column. - Dropping the reference has to leave at least three components, so one reaction needs a four-component system (MTBE synthesis has its inert n-butane). A two-component transformed problem puts every profile on one line, where closest-approach connection is degenerate; that case is refused, not guessed.
Details and the upgrade path:
src/side_features/bvm/README.md.
- Species & thermodynamics: VLE/activity/EOS model choice, editable binary interaction tables, searchable or hand-entered component properties.
- Live degrees-of-freedom status (
core/dof.py): how many more specs the column needs, and which kinds are valid under the active flow model, before you hit run. - Interactive column sandbox (Specifications → Column Overview): stages, feeds, draws, interreboiler/intercooler modules on one canvas.
- Complex topology: interreboilers and intercoolers carry a signed duty the energy balance consumes as a real per-stage term.
- Threaded solves: every solver runs on a QThread with live iteration/residual progress and a real Abort.
- Results: composition, temperature, pressure, flow, K-value, and enthalpy profile plots; a McCabe-Thiele diagram for binary columns; a component-aware data table; product-stream summary with mass-balance closure; CSV export. Display units (°C/K/°F, kmol/h, kg/h, kW/MW/kJ/h) are independent of solver-internal units.
- Save/Load: the full session persists to
.colx, versioned JSON.
columnForge/
├── src/
│ ├── python/
│ │ ├── core/ # thermodynamics, solvers, dof, material/energy balance
│ │ │ └── data/ # components.json, unifac_groups.json
│ │ ├── gui/
│ │ │ ├── tabs/ # Initialization / Specifications / Simulation / Results / Modules
│ │ │ ├── panels/ # reusable config panels (species, streams, condenser, ...)
│ │ │ ├── modules/ # BVM, RBM, FUG, Txy/Pxy, Pure Components, Phase EQ widgets
│ │ │ ├── state/ # WindowState (single source of truth) + .colx persistence
│ │ │ └── theme/ # Qt stylesheet
│ │ └── tests/ # headless pytest suite (+ tests/validation/)
│ ├── side_features/
│ │ ├── bvm/ # difference-point-chain BVM solver (own README + tests/)
│ │ ├── rbm/ # rectification-body feasibility / R_min (own tests/)
│ │ └── rcm/ # residue-curve integrator: RCM_solv.c + the pyrcm.py port
│ └── native/ # Fortran thermo kernels (nifco2.f90) + Makefile
├── docs/ # thermodynamics.md (equation reference), img/
└── launch.py # GUI entry point
The solver and state layers are Qt-free and self-checking. 278 tests, headless:
QT_QPA_PLATFORM=offscreen python -m pytest src/python/tests/ src/side_features/bvm/tests/ src/side_features/rbm/tests/docs/validation.md is the short answer to "is any of this right?" — 33 reference-vs-computed comparisons, 16 against published data (NIST boiling points, DDBST/Gmehling azeotropes, literature VLE) and 17 between ColumnForge's own methods, which should agree and are checked to say so. It is generated, never hand-written, so it cannot drift from the code:
python tools/validation_report.py # runs the suite, rewrites the tablesrc/python/tests/validation/ is the acceptance gate for solver changes:
| file | what it pins |
|---|---|
test_validation_v1.py |
the six named cases — BTX ideal, depropanizer through PLXANT, ethanol/water vs its azeotrope, methanol/water vs Perry's, cross-model NRTL/Wilson/UNIQUAC, SRK |
test_literature_data.py |
published boiling points and azeotropes, each with a URL that resolves |
test_limits.py |
the identities any MESH solver must obey — Fenske at total reflux, Underwood's Rmin wall, and monotonicity in both stages and reflux |
test_agreement.py |
BVM's sized column re-solved in MESH, FUG's design built rigorously, Rmin three independent ways |
RBM's own tests cross-check its Rmin against Underwood on a near-ideal
split and its pinch structure against the published extractive case. Every core
module is also runnable standalone as a self-check
(PYTHONPATH=src/python python -m core.column_solvers).
Important
Gate solver results on converged, not found — found only reports that
the run was not cancelled. See solve_bubble_point's docstring for what
residual does and does not bound.
Note
Much of the testing suite is AI-generated and hasn't had a line-by-line review pass
yet; it is committed because a green gate you can run beats a private one you
can't. CI (.github/workflows/ci.yml) runs it on 3.11 and 3.12 with pyflakes.
Important
- Stage 0 = distillate (top) everywhere in the GUI and result profiles. Solvers may use a different internal ordering, converted at the boundary.
- Nothing is silently ignored. Every value you can enter is either consumed by the active solver or visibly greyed out with a "not consumed yet" tooltip.
.colxis versioned JSON, never pickle, so an old save stays readable by a newer build and vice versa.
- Selectable compiled backend. Very fast solves, can run on a potato, SciPy and for loops can definitely be a bottleneck in Python.
- Full energy balance in the core solvers is opt-in for Inside-Out. Bubble-Point is CMO-only (BVM already has its own energy balance).
- Full N-section BVM. Extractive works well column profiles match MESH solutions well. Great for starting parameters as is, would be cool to extend the idea to optimal stage count and feed location(s).
- RBM → BVM handoff in one click. RBM finds the feasible operating point and
BVM sizes the column there, but
randE/Fare copied across by hand today. A dedicated RBM README is still to be written.
MIT, see LICENSE.









