The MultiStreamHeatExchanger2 class models heat exchangers with multiple hot and cold streams exchanging heat simultaneously. This is essential for:
- LNG/Cryogenic Plants: Main cryogenic heat exchangers (MCHE)
- Refinery Heat Integration: Crude preheat trains
- Gas Processing: Turbo-expander cold boxes
- Chemical Plants: Multi-effect evaporators
Table of Contents
- Overview
- Mathematical Foundations
- Solver Modes
- Usage Examples
- Python Examples
- API Reference
- Best Practices
- Troubleshooting
Overview
| Property | Value |
|---|---|
| Class | neqsim.process.equipment.heatexchanger.MultiStreamHeatExchanger2 |
| Package | neqsim.process.equipment.heatexchanger |
| Max Streams | Unlimited (practical limit ~10) |
| Unknown Outlets | 1, 2, or 3 temperatures |
| Solver | Newton-Raphson with numerical Jacobian |
┌─────────────────────────────────────────┐
Hot 1 ───▶│ │───▶ Hot 1 Out
Hot 2 ───▶│ MULTI-STREAM HEAT EXCHANGER │───▶ Hot 2 Out
Hot 3 ───▶│ │───▶ Hot 3 Out
│ Q_hot = Q_cold │
Cold 1 ──▶│ (Energy Balance) │───▶ Cold 1 Out
Cold 2 ──▶│ │───▶ Cold 2 Out
Cold 3 ──▶│ ΔT_min ≥ Approach Temperature │───▶ Cold 3 Out
└─────────────────────────────────────────┘
Mathematical Foundations
Energy Balance
The fundamental constraint is that total heat released by hot streams equals total heat absorbed by cold streams:
\[\sum_{i \in \text{hot}} \dot{m}_i \cdot (h_{in,i} - h_{out,i}) = \sum_{j \in \text{cold}} \dot{m}_j \cdot (h_{out,j} - h_{in,j})\]Where:
- $\dot{m}_i$ = mass flow rate of stream $i$ (kg/s)
- $h_{in,i}$ = specific enthalpy at inlet (kJ/kg)
- $h_{out,i}$ = specific enthalpy at outlet (kJ/kg)
Enthalpy Calculation: NeqSim performs a TP flash at each temperature to calculate enthalpy, accounting for:
- Phase changes (condensation/vaporization)
- Non-ideal mixing
- Real gas effects
The energy imbalance residual function:
\[f_E = Q_{hot} + Q_{cold} = \sum_i \dot{m}_i (h_{out,i} - h_{in,i})\]| At convergence: $ | f_E | < \epsilon$ (default $\epsilon = 10^{-3}$ kW) |
Composite Curves
Composite curves combine all hot streams into a single “hot composite” and all cold streams into a “cold composite”. This enables visualization of heat transfer feasibility and pinch point identification.
Construction Algorithm:
- Collect temperature breakpoints: All inlet and outlet temperatures on each curve
- Sort temperatures in ascending order
- Calculate interval loads: For each temperature interval $[T_k, T_{k+1}]$:
Where “active” streams are those spanning the interval.
- Accumulate loads: Starting from $Q = 0$ at the coldest point:
Result: Temperature vs. cumulative heat duty for each composite curve.
Pinch Analysis
The pinch point is the location of minimum temperature approach between hot and cold composites:
\[\Delta T_{min} = \min_k \left( T_{hot,k} - T_{cold,k} \right)\]Where temperatures are evaluated at the same cumulative heat load.
Feasibility constraint: For thermodynamically feasible heat exchange:
\[\Delta T_{min} \geq \Delta T_{approach}\]The pinch residual function:
\[f_P = \Delta T_{min} - \Delta T_{approach}\]UA Calculation (LMTD Method)
The overall heat transfer coefficient-area product (UA) is calculated by summing contributions from each interval:
\[UA = \sum_{k=1}^{N} \frac{Q_k}{\text{LMTD}_k}\]Where the Log Mean Temperature Difference for interval $k$ is:
\[\text{LMTD}_k = \frac{(T_{h,in} - T_{c,in}) - (T_{h,out} - T_{c,out})}{\ln\left(\frac{T_{h,in} - T_{c,in}}{T_{h,out} - T_{c,out}}\right)}\]| For small temperature differences ($ | \Delta T_1 - \Delta T_2 | < 10^{-4}$): |
Units: UA is returned in W/K (Watts per Kelvin).
Solver Modes
The solver automatically selects the solution method based on the number of unknown outlet temperatures:
| Unknown Count | Equations | Variables | Method |
|---|---|---|---|
| 0 | - | - | Direct calculation (no iteration) |
| 1 | Energy balance | 1 temperature | 1×1 Newton-Raphson |
| 2 | Energy + Pinch | 2 temperatures | 2×2 Newton-Raphson |
| 3 | Energy + Pinch + UA | 3 temperatures | 3×3 Newton-Raphson |
Newton-Raphson Iteration
For $n$ unknowns, the system solves:
\[\mathbf{J} \cdot \Delta \mathbf{T} = \mathbf{f}\]Where:
- $\mathbf{f}$ = residual vector (energy, pinch, UA)
- $\mathbf{J}$ = Jacobian matrix (numerical derivatives)
- $\Delta \mathbf{T}$ = temperature corrections
Jacobian calculation (numerical, with $\delta = 10^{-4}$°C):
\[J_{ij} = \frac{f_i(T_j + \delta) - f_i(T_j)}{\delta}\]Damping: Updates are damped to improve stability:
\[T_j^{new} = T_j^{old} - \alpha \cdot \Delta T_j\]Default damping factor $\alpha = 1.0$.
Usage Examples
Basic Usage
import neqsim.process.equipment.heatexchanger.MultiStreamHeatExchanger2;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
// Create fluid
SystemSrkEos fluid = new SystemSrkEos(273.15 + 50.0, 50.0);
fluid.addComponent("methane", 0.90);
fluid.addComponent("ethane", 0.05);
fluid.addComponent("propane", 0.03);
fluid.addComponent("n-butane", 0.02);
fluid.setMixingRule("classic");
// Create hot streams
Stream hot1 = new Stream("Hot-1", fluid.clone());
hot1.setTemperature(100.0, "C");
hot1.setFlowRate(20000.0, "kg/hr");
Stream hot2 = new Stream("Hot-2", fluid.clone());
hot2.setTemperature(80.0, "C");
hot2.setFlowRate(15000.0, "kg/hr");
// Create cold streams
Stream cold1 = new Stream("Cold-1", fluid.clone());
cold1.setTemperature(10.0, "C");
cold1.setFlowRate(25000.0, "kg/hr");
Stream cold2 = new Stream("Cold-2", fluid.clone());
cold2.setTemperature(20.0, "C");
cold2.setFlowRate(10000.0, "kg/hr");
// Create multi-stream heat exchanger
MultiStreamHeatExchanger2 mshx = new MultiStreamHeatExchanger2("MCHE-100");
// Add streams: (stream, type, outletTemp)
// outletTemp = null means "solve for this"
mshx.addInStreamMSHE(hot1, "hot", 40.0); // Known outlet: 40°C
mshx.addInStreamMSHE(hot2, "hot", 35.0); // Known outlet: 35°C
mshx.addInStreamMSHE(cold1, "cold", null); // Unknown - solve
mshx.addInStreamMSHE(cold2, "cold", 60.0); // Known outlet: 60°C
// Set approach temperature
mshx.setTemperatureApproach(5.0); // Minimum ΔT = 5°C
// Build process and run
ProcessSystem process = new ProcessSystem();
process.add(hot1);
process.add(hot2);
process.add(cold1);
process.add(cold2);
process.add(mshx);
process.run();
// Get results
System.out.println("Cold-1 outlet: " + mshx.getOutStream(2).getTemperature("C") + " °C");
System.out.println("UA value: " + mshx.getUA() + " W/K");
System.out.println("Pinch ΔT: " + mshx.getTemperatureApproach() + " °C");
One Unknown Temperature
When only one outlet is unknown, the solver uses energy balance alone:
MultiStreamHeatExchanger2 mshx = new MultiStreamHeatExchanger2("HX-1");
// All outlets known except one
mshx.addInStreamMSHE(hot1, "hot", 50.0); // Known
mshx.addInStreamMSHE(hot2, "hot", 45.0); // Known
mshx.addInStreamMSHE(cold1, "cold", null); // Unknown - energy balance determines this
mshx.addInStreamMSHE(cold2, "cold", 70.0); // Known
// No need to set approach or UA - only energy balance is solved
mshx.run();
Two Unknown Temperatures
With two unknowns, the solver uses energy balance + pinch constraint:
MultiStreamHeatExchanger2 mshx = new MultiStreamHeatExchanger2("HX-2");
mshx.addInStreamMSHE(hot1, "hot", null); // Unknown
mshx.addInStreamMSHE(hot2, "hot", 80.0);
mshx.addInStreamMSHE(hot3, "hot", 60.0);
mshx.addInStreamMSHE(cold1, "cold", 10.0);
mshx.addInStreamMSHE(cold2, "cold", null); // Unknown
mshx.addInStreamMSHE(cold3, "cold", 30.0);
// REQUIRED: Set approach temperature
mshx.setTemperatureApproach(5.0);
mshx.run();
// Solver finds temperatures that satisfy:
// 1. Energy balance: Q_hot = Q_cold
// 2. Pinch: ΔT_min = 5.0°C
Three Unknown Temperatures
With three unknowns, the solver needs energy + pinch + UA:
MultiStreamHeatExchanger2 mshx = new MultiStreamHeatExchanger2("HX-3");
mshx.addInStreamMSHE(hot1, "hot", null); // Unknown
mshx.addInStreamMSHE(hot2, "hot", 80.0);
mshx.addInStreamMSHE(hot3, "hot", 60.0);
mshx.addInStreamMSHE(cold1, "cold", null); // Unknown
mshx.addInStreamMSHE(cold2, "cold", null); // Unknown
mshx.addInStreamMSHE(cold3, "cold", 30.0);
// REQUIRED for 3 unknowns: Both approach AND UA
mshx.setTemperatureApproach(5.0);
mshx.setUAvalue(70000.0); // W/K
mshx.run();
// Solver finds temperatures satisfying:
// 1. Energy balance: Q_hot = Q_cold
// 2. Pinch: ΔT_min = 5.0°C
// 3. UA: calculated UA = 70000 W/K
Python Examples
Basic Multi-Stream Heat Exchanger
from neqsim import jneqsim
# Import classes
SystemSrkEos = jneqsim.thermo.system.SystemSrkEos
Stream = jneqsim.process.equipment.stream.Stream
MultiStreamHeatExchanger2 = jneqsim.process.equipment.heatexchanger.MultiStreamHeatExchanger2
ProcessSystem = jneqsim.process.processmodel.ProcessSystem
# Create fluid
fluid = SystemSrkEos(273.15 + 50.0, 50.0)
fluid.addComponent("methane", 0.85)
fluid.addComponent("ethane", 0.10)
fluid.addComponent("propane", 0.05)
fluid.setMixingRule("classic")
# Create streams
hot1 = Stream("Hot-1", fluid.clone())
hot1.setTemperature(100.0, "C")
hot1.setFlowRate(20000.0, "kg/hr")
hot2 = Stream("Hot-2", fluid.clone())
hot2.setTemperature(90.0, "C")
hot2.setFlowRate(20000.0, "kg/hr")
cold1 = Stream("Cold-1", fluid.clone())
cold1.setTemperature(0.0, "C")
cold1.setFlowRate(20000.0, "kg/hr")
cold2 = Stream("Cold-2", fluid.clone())
cold2.setTemperature(10.0, "C")
cold2.setFlowRate(10000.0, "kg/hr")
# Create MSHE
mshx = MultiStreamHeatExchanger2("LNG-MCHE")
# Add streams (stream, type, outlet_temp)
# Use None for unknown outlet temperatures
mshx.addInStreamMSHE(hot1, "hot", None) # Unknown
mshx.addInStreamMSHE(hot2, "hot", 80.0) # Known: 80°C
mshx.addInStreamMSHE(cold1, "cold", None) # Unknown
mshx.addInStreamMSHE(cold2, "cold", 85.0) # Known: 85°C
# Set constraints
mshx.setTemperatureApproach(5.0) # 5°C minimum approach
# Build and run process
process = ProcessSystem()
process.add(hot1)
process.add(hot2)
process.add(cold1)
process.add(cold2)
process.add(mshx)
process.run()
# Results
print(f"Hot-1 outlet: {mshx.getOutStream(0).getTemperature('C'):.1f} °C")
print(f"Cold-1 outlet: {mshx.getOutStream(2).getTemperature('C'):.1f} °C")
print(f"UA value: {mshx.getUA():.0f} W/K")
Extracting Composite Curves for Plotting
import matplotlib.pyplot as plt
# After running the heat exchanger...
composite_data = mshx.getCompositeCurve()
# Extract hot and cold curves
hot_curve = composite_data.get("hot")
cold_curve = composite_data.get("cold")
# Convert to lists for plotting
hot_loads = [float(p.get("load")) for p in hot_curve]
hot_temps = [float(p.get("temperature")) for p in hot_curve]
cold_loads = [float(p.get("load")) for p in cold_curve]
cold_temps = [float(p.get("temperature")) for p in cold_curve]
# Plot composite curves
plt.figure(figsize=(10, 6))
plt.plot(hot_loads, hot_temps, 'r-o', label='Hot Composite', linewidth=2)
plt.plot(cold_loads, cold_temps, 'b-o', label='Cold Composite', linewidth=2)
plt.xlabel('Heat Load (kW)')
plt.ylabel('Temperature (°C)')
plt.title('Composite Curves')
plt.legend()
plt.grid(True)
plt.show()
API Reference
Constructor
MultiStreamHeatExchanger2(String name)
Key Methods
| Method | Description |
|---|---|
addInStreamMSHE(StreamInterface stream, String type, Double outletTemp) |
Add a stream. type = “hot” or “cold”. outletTemp = null for unknown. |
setTemperatureApproach(double approach) |
Set minimum approach temperature (°C) |
setUAvalue(double ua) |
Set UA constraint (W/K) - required for 3 unknowns |
run() |
Execute the solver |
getOutStream(int index) |
Get outlet stream by index (order of addition) |
getUA() |
Get calculated UA value (W/K) |
getTemperatureApproach() |
Get the achieved minimum approach (pinch) of the last solution (°C) |
getSpecifiedTemperatureApproach() |
Get the specified approach temperature (°C) |
getSolverStatus() |
Outcome of the last run: CONVERGED, DEGENERATE, INFEASIBLE, FALLBACK, FAILED or NOT_RUN |
getSolverMessage() |
Explanation of the last outcome; empty when converged normally |
isSpecificationMet() |
True when all active equations are met (CONVERGED or DEGENERATE); see the mode table below |
getMaximumFeasibleApproach() |
Approach upper bound from fixed outlets and inlets (°C); not a feasibility guarantee; NaN with fewer than 2 unknowns |
setThrowOnUnmetSpecification(boolean) |
Strict mode: publish the best-effort outlets, then throw IllegalStateException if an active specification is unmet |
getCompositeCurve() |
Get composite curve data for plotting |
Solver Configuration
| Method | Description | Default |
|---|---|---|
setTolerance(double tol) |
Convergence tolerance | 1e-3 |
setMaxIterations(int max) |
Maximum iterations | 1000 |
setJacobiDelta(double delta) |
Numerical derivative step | 1e-4 |
setDamping(double factor) |
Newton step damping | 1.0 |
Best Practices
1. Stream Ordering
Add streams in a logical order (all hot first, then all cold) for easier result interpretation:
// Recommended ordering
mshx.addInStreamMSHE(hot1, "hot", ...); // Index 0
mshx.addInStreamMSHE(hot2, "hot", ...); // Index 1
mshx.addInStreamMSHE(cold1, "cold", ...); // Index 2
mshx.addInStreamMSHE(cold2, "cold", ...); // Index 3
2. Reasonable Initial Guesses
The solver initializes unknown temperatures based on the approach temperature. For difficult cases, provide more known temperatures to improve convergence.
3. Physical Feasibility
Ensure your specification is physically feasible:
- Hot outlets should be cooler than inlets
- Cold outlets should be warmer than inlets
- Sufficient temperature driving force exists
4. UA Estimation
For 3-unknown cases, estimate UA from:
\[UA \approx \frac{Q}{\Delta T_{lm}}\]Where $Q$ is the expected duty and $\Delta T_{lm}$ is an estimated LMTD.
5. Check Composite Curves
Always visualize composite curves to verify:
- No temperature crossover (hot always above cold)
- Pinch location makes physical sense
- Heat loads are balanced
Troubleshooting
Approach temperature cannot be met with fixed outlets
With energy balance the hot and cold composite curves end at the same load. A fixed cold outlet $T_{c,out}^{fix}$ therefore limits the warm-end difference, and a fixed hot outlet $T_{h,out}^{fix}$ limits the cold-end difference:
\[\Delta T_{min} \le \min\left(T_{h,in}^{max} - T_{c,out}^{fix},\; T_{h,out}^{fix} - T_{c,in}^{min}\right)\]No choice of the unknown outlets can raise the approach above this bound. Despite its API name,
getMaximumFeasibleApproach() is a necessary upper bound, not the calculated maximum achievable
pinch: internal pinches, the energy balance and a specified UA may impose tighter limits.
- Above the bound, the two-unknown mode reports
INFEASIBLE, skips Newton refinement and attempts a best-effort result. Its message reports the achieved approach and energy residual in kW; check both before using the result. Energy balance is not guaranteed for every infeasible input. - At the bound, the two-unknown mode reports
DEGENERATEonly if a nearby, distinct, energy-balanced solution preserves the pinch imposed by fixed outlets. This is a local numerical check of non-uniqueness, not a proof for every possible outlet state. - The three-unknown mode also enforces UA. If its approach already exceeds the bound, it throws
immediately with status
INFEASIBLE, in either strict or permissive mode. No three-unknown best-effort solution is provided.
isSpecificationMet() checks the equations active in the selected mode:
| Unknown outlets | Equations checked |
|---|---|
| 0 | Energy balance of the fixed outlet temperatures |
| 1 | Energy balance; the approach setting is not an enforced constraint |
| 2 | Energy balance and minimum approach |
| 3 | Energy balance, minimum approach and specified UA |
Residuals must be finite and smaller in magnitude than the solver tolerance. A returned result
that misses an active equation is FALLBACK. Numerical failures before an outlet result is
published are FAILED and continue to throw. NOT_RUN is the initial state. Every run resets
the diagnostic fields so that a corrected specification can recover from an earlier failure.
Check the status after run(), or enable strict mode to get an exception:
mshx.setThrowOnUnmetSpecification(true); // optional: throw after publishing an unmet best-effort result
mshx.run();
if (!mshx.isSpecificationMet() || mshx.getSolverStatus() == MultiStreamHeatExchanger2.SolverStatus.DEGENERATE) {
logger.warn("{} approach upper bound: {}", mshx.getSolverMessage(), mshx.getMaximumFeasibleApproach());
}
For a returned best-effort result, strict mode publishes the outlet streams and calculation
identifier before throwing, so the diagnostic message and outlet state refer to the same run.
If the calculation fails before publication, previous outlet streams must not be treated as a
new result. The example’s strict-mode and degenerate branches are exercised by
MultiStreamHeatExchanger2Test.testSolverStatusDegenerateAtFeasibleApproachLimit.
toJson() includes solverStatus, solverMessage, specificationMet,
specifiedTemperatureApproach and the finite maximumFeasibleApproach when evaluated.
Approach temperature inputs must be finite and non-negative. Three-unknown solves require
a finite, positive UA in W/K.
“Failed to converge after maxIterations”
Causes:
- Infeasible specification (temperatures physically impossible)
- Poor initial guess
- Stiff problem near pinch
Solutions:
- Reduce number of unknowns (specify more outlets)
- Check that hot inlets > cold inlets
- Increase
setMaxIterations() - Adjust
setDamping()to 0.5-0.8
“Jacobian is singular”
Causes:
- Two unknowns have identical effect on residuals
- System at or near degenerate point
Solutions:
- Reduce number of unknowns
- Add small UA regularization
- Check stream specifications
Negative ΔT in Composite Curves
Cause: Temperature crossover (cold above hot) - thermodynamically infeasible.
Solutions:
- Increase cold stream flow rates
- Decrease hot stream flow rates
- Reduce specified cold outlet temperatures
- Increase specified hot outlet temperatures
Very Large UA Values
Cause: Small LMTD in one or more intervals.
Solutions:
- Check for temperature pinch
- Increase approach temperature specification
- Review flow rates for imbalance
Related Documentation
- Heat Exchangers Overview - Two-stream exchangers
- Streams - Stream creation
- Process System - Building flowsheets
- Dynamic Simulation - Transient heat exchangers