Key Concepts#
This page collects the ideas hensmith implements: what a minimum approach
temperature is, how the problem table turns a set of process streams into
utility targets and a pinch, how those targets extend to several utilities,
area, units and cost, how synthesize_network() turns the
targets into a network of exchangers, and what the result is and is not
guaranteed to be. Everything below describes the behavior of the code in
hensmith/hxn_synthesis.py, hensmith/_heat_exchanger_network.py and
hensmith/targeting.py, and of two private helpers,
hensmith/_curves.py (the stream curves) and hensmith/_planner.py (the
network planner); the Tutorial shows the same concepts on a
running system.
Heat integration and the minimum approach temperature#
Every heating and cooling duty in a flowsheet is paid for with a utility: steam, cooling water, chilled water, refrigerant. Heat integration replaces part of that spending with process-to-process exchange – heat taken from a stream that must be cooled and given to a stream that must be heated. What is left after the process exchangers have done as much as they can is the irreducible utility demand.
Heat only flows down a temperature gradient, and a finite exchanger can only
transfer heat across a finite temperature difference: the closer the two
streams are in temperature, the more area is needed for the same duty, since
the area of a counter-current exchanger scales as
\(Q / (U \Delta T_{lm})\). hensmith expresses that limit as a single
number, the minimum approach temperature T_min_app (in K, default 5.),
used to shift hot streams in the problem table and required between the
streams everywhere inside every synthesized exchanger:
in
problem_table(), where every hot stream’s temperature is shifted down byT_min_app(with approach contributions, every stream by its own shift; see below) before the streams are compared, so that two streams which meet on the shifted scale are reallyT_min_appapart;in the planner of
synthesize_network(), which admits a match only if the two streams stay at leastT_min_appapart at every breakpoint of their temperature-enthalpy curves along the whole exchanger, not only at its two ends; andon every synthesized process exchanger, whose approach is verified on the exact states of its two streams, again along its whole length. The exchanger itself is a
biosteam.HXprocessconstructed withdTequal to its pair’s required approach minus 1e-6, a guard against rounding only.
Lowering T_min_app lowers the utility targets and raises exchanger area;
raising it does the reverse. It is the parameter in hensmith that sets the
trade between utilities and capital, and because
HeatExchangerNetwork reports its capital as added cost
and its utilities as differences (see below), both sides of that trade are
visible in the facility’s results.
Shifted temperatures and the problem table#
problem_table() builds the temperature-interval heat cascade
of a set of streams, given each stream’s inlet, its outlet quenched to
equilibrium at its own enthalpy, a flag saying whether it is cooled, and
T_min_app. Its result is a ProblemTable.
Stream curves. Each stream is first described by a piecewise-linear temperature-enthalpy curve over its own enthalpy range, built once from a handful of flashes and evaluated afterwards without flashing. Its breakpoints are the stream’s two end temperatures and every phase boundary inside its range (a pure component’s saturation temperature, a mixture’s bubble and dew points); a pure component’s latent heat is a flat (isothermal) segment between its saturated-liquid and saturated-vapor enthalpies; a mixture’s two-phase glide is sampled, and so is every curved single-phase stretch (temperature-dependent heat capacity), densely enough that linear interpolation stays within 0.002 K of the true curve. Inside its range the stream is taken at equilibrium with its enthalpy clipped to \([H_{lo}, H_{hi}]\), so a stream copy at an interior temperature can never carry more enthalpy than the real stream ever has (a non-equilibrium outlet, for instance); what a non-equilibrium end state departs from equilibrium at its own end temperature becomes a flat there.
The shifted grid. Hot streams are shifted down by T_min_app; cold
streams are not (with approach contributions, every stream by its own shift;
see below). The grid Ts is the sorted (descending) union of the
shifted breakpoints of all curves, temperatures closer than 1e-9 K being one
grid point. Every temperature at which any stream’s curve bends or jumps is
therefore a grid point, and between two consecutive grid points every stream
is linear to within 0.002 K. A grid of the stream end temperatures alone
would average a boiling point, a dew or bubble point, or the curvature of a
glide that falls strictly inside an interval over that interval, which can
hide a pinch and make the hot utility target too low.
Per-stream contributions. Write \(H_j^-(T)\) and \(H_j^+(T)\) for the low- and high-enthalpy limits of stream \(j\)’s curve at the real temperature \(T + \mathrm{shift}_j\); they differ only where the curve has a flat at that temperature, and they are \(H_{hi}\) above and \(H_{lo}\) below the stream’s range. The heat a stream contributes to the open interval between grid temperatures \(T_k\) and \(T_{k+1}\), and at the grid temperature \(T_k\) itself, are
with \(s_j = +1\) for a hot stream and \(-1\) for a cold one. The stream’s own end points are grid points selected by position rather than by a float comparison, so the contributions telescope exactly:
that is, to the stream’s duty.
Point loads. point_H is nonzero only where a curve is flat: at a pure
component’s (shifted) saturation temperature, which receives its latent heat;
at an end temperature where a non-equilibrium end state departs from
equilibrium; and at the shifted outlet temperature of a point-load stream
– an isothermal stream, or one whose outlet temperature moves against its
duty (a heated stream that leaves cooler than it entered, such as a reboiler
outlet quenched to equilibrium) – which has no interval to occupy and puts
its whole duty \(s_j |H_{out} - H_{in}|\) there.
The cascade. Starting from zero hot utility, the heat leaving grid boundary \(T_k\) (after that boundary’s point loads) is
Feasibility must hold for the heat arriving at each boundary as well, before its point loads are applied,
because a point source located at \(T_k\) cannot serve a sink above \(T_k\). hensmith therefore takes the elementwise minimum of the two flows; the most negative value is the deficit hot utility must make up,
its first location is the pinch, and the heat left at the bottom of the cascade is the cold utility, \(\mathrm{cold\_util\_load} = \mathrm{residual}[-1] + \mathrm{hot\_util\_load}\). Whether that minimum is the arriving or the leaving flow at the pinch fixes the side of the pinch that the point loads at the pinch temperature belong to, the pinch cut.
Because every stream is linear to within 0.002 K between grid points, the
grid minimum of the cascade is the true one to within 0.002 K times the sum of
the heat capacity flow rates; equivalently, the targets lie between the exact
targets at T_min_app minus and plus 0.004 K (approximately: the 0.002 K
tolerance is established by midpoint tests). For streams of constant heat
capacity the curves are exact.
Threshold problems. When that minimum is not negative – or negative by no
more than 1e-9 of the total stream duty – no hot utility is needed at all.
The table then reports zero hot utility and places the pinch at the top of the
grid, Ts[0]. A cold utility that comes out slightly negative through
rounding is absorbed back into the hot utility so that the identity
stays exact: the difference between the two targets is always the net heating demand of the whole set of streams. That identity is the table’s own consistency check – whatever the algorithm does with the cascade, it can neither invent nor destroy energy.
Approach temperature contributions#
A single T_min_app holds every match to the same driving-force floor,
whether its streams condense and boil with high film coefficients or are
gases with low ones. Each stream \(j\) may therefore carry its own
approach temperature contribution \(c_j\), in K, as described by Kemp
(2007): a process match between hot stream \(h\) and cold stream \(c\)
must keep \(c_h + c_c\) everywhere inside it, on the exact states, and a
stream without a contribution contributes T_min_app / 2, so that without
contributions every pair requires T_min_app exactly as before. A small
contribution suits a condensing or boiling stream, a large one a gas.
problem_table() takes the contributions as a sequence aligned
with its streams, synthesize_network() as a sequence aligned
with its heat utilities, a mapping or a callable, and
HeatExchangerNetwork as a mapping or a callable (see
below).
The shift. Writing \(\Delta T_{min}\) for T_min_app, every stream
is shifted by its own amount, \(T^* = T - \mathrm{shift}_j\), with
so that
Two streams that meet on the shifted scale are therefore exactly their pair’s
required approach apart, and the problem table above is built on that scale
with no other change. A contribution is None or nan (unspecified) or a finite
number greater than zero. Zero is excluded so that every pair keeps a
positive driving force: a match allowed to touch would have none at its pinch
and need an infinite area. (The HXprocess guard is the requirement less
1e-6 K, so a pair whose contributions sum to 1e-6 K or less still gets a
guard of zero or less, as T_min_app <= 1e-6 K does by default.) Whenever
a contribution is given, T_min_app must be finite and positive as well,
since it sets the default T_min_app / 2; without contributions, or with
every one unspecified, T_min_app = 0 is accepted as before.
The default is unchanged, bit for bit. An unspecified stream gets
exactly the default shifts, T_min_app if hot and 0 if cold
(\(x/2 + x/2 = x\) and \(x/2 - x/2 = 0\) hold exactly in floating
point), and with nothing specified the code takes the old expression
literally. A run without contributions, with every contribution unspecified
or with every one equal to T_min_app / 2 gives the same targets and the
same network, number for number.
One shifted scale for the planner. The planner’s search sees one curve per stream on the shifted scale, not the shifts themselves, and on that scale a match is feasible exactly when its real streams keep their pair’s requirement. The MER targets, the pinch design rules, the remaining problem analysis and the guarantee of Stream splitting below therefore carry over unchanged; a branch is its parent’s curve with every heat scaled by its fraction, so it keeps its parent’s shift.
Per pair on the real scale. What works on real temperatures uses the
pair’s own value. A pair’s required approach is
\(\mathrm{shift}_h - \mathrm{shift}_c\) – equal to \(c_h + c_c\) up
to round-off, and to T_min_app exactly without contributions. The exact
approach verification and the repair described below enforce it, each
HXprocess gets it less 1e-6 K as its dT, and
synthesis_info['min_approach_margin'] reports the smallest margin of any
exchanger’s exact approach over its own pair’s requirement (no less than
-1e-6 K, the check’s rounding guard), next to 'min_approach', the
smallest approach itself. Wherever this page says that a match keeps
T_min_app, read its pair’s required approach.
Per-stream pinch temperatures. The pinch of the problem table, and the
'pinch_T' of synthesis_info['plan_targets'], stay on the shifted
scale. Stream \(j\)’s own pinch temperature is that pinch plus
\(\mathrm{shift}_j\) – T_min_app above it for a hot stream and at
it for a cold one by default – and that is what the facility’s pinch_Ts
reports. The real composite curves no longer keep one fixed distance: they
can come as close as the smallest hot plus the smallest cold contribution,
and only the shifted curves carry the pinch.
Utilities are not affected. The contributions apply to the process
matches only: each stream’s utility exchanger gets its agent from BioSTEAM’s
own rule (HeatUtility.dT plus the agent’s dT), whatever the stream’s
contribution.
Keys in the facility. HeatExchangerNetwork takes a
mapping or a callable, since the order of its streams is internal. A mapping’s
keys name units: a unit object (matched by identity), the ID of a unit
without an owner ('D1'), or (owner.ID, unit.ID) for an auxiliary
unit (('D1', 'condenser'); a bare auxiliary ID such as 'condenser'
names nothing). The owner of a pair is the top owner, so a pair names
every auxiliary of that ID under it, at any depth: if a unit holds two
auxiliary flash drums, each with its own heat_exchanger, the pair
('U', 'heat_exchanger') names both heaters, and the heater objects
or the drums (('U', 'flash_a')) tell them apart. A heat utility takes
the value of the key nearest to its unit along the unit’s chain of owners,
so 'D1' covers a column’s condenser and reboiler and
('D1', 'condenser') overrides it for the condenser. A None or nan value
is an explicit default that wins at its level like any value, and two keys
of the same level for a unit must agree. A unit’s owner is its direct owner
(the unit it was made an auxiliary of; BioSTEAM’s Unit.owner gives the
top one), or, for an auxiliary that a unit available to the network holds
without setting its owner, or an exchanger without one whose heat utilities
a unit takes without holding it as an auxiliary (a custom unit’s
self.heat_utilities = self.hx.heat_utilities), that unit (the user’s
objects are left as they are): biosteam.MultistageCompressor builds its
compressors and intercoolers anew, without an owner, whenever pr or
n_stages changes, and 'K1' still covers the intercoolers of
compressor K1 and ('K1', 'K1_H1') names one in every sample, while
'K1_H1' names nothing. Targets keep this relation as it was when they
were made (those of HXN.targets() as it was at the network’s synthesis),
for their contributions, film coefficient keys and labels
('K1 - K1_H1'): 'K1'
still covers the intercoolers of targets stored in one sample after K1 has
rebuilt them in the next, or has been simulated alone. (Outside the
facility – synthesize_network, temperature_interval_pinch_analysis,
PinchTargets.from_heat_utilities without key_units=sys.units – only
the heat utilities’ units and their owners are at hand, so such an
intercooler has no owner there and 'K1_H1' is its key.) ID
keys are the robust form: a unit that rebuilds its auxiliary units at every
simulation (biosteam.HXutilities, for one) leaves a key holding an old
auxiliary object, which then names the auxiliary that took its place: the
one of the same ID under owners of the same IDs, if any (an old intercooler
of K1, which never had an owner, names none and raises). Heat utilities
cannot be keys,
because BioSTEAM replaces them at every simulation; a value per heat
utility is given by a callable
f(hu) instead, which is called once per heat utility at every simulation
and should therefore be pure (after a simulation, HXN.targets() takes the
values that simulation resolved instead of calling it again, since
replace_unit_heat_utilities overwrites the heat utilities by then). The
keys and values are checked against every unit available to the network
and its auxiliary units before any heat utility is filtered out: a key that
names none of them, a bad value or two
conflicting keys raise a ValueError even while their unit is idle, but a
valid key whose unit holds no heat utility in a simulation (no duty,
ignored, hxn_ok=False) has no effect, so that a Monte Carlo sample in
which a unit falls idle does not fail. The same holds for an auxiliary that a
unit may build but does not hold in a sample: biosteam.HXutilities, for
one, builds one exchanger per agent it needs, so a lower outlet temperature
drops ('X1', 'heat_exchangers[2]'), and a biosteam.MultistageCompressor
K1 with two stages drops ('K1', 'K1_H3'): a key ('K1',
'K1_<ticket><n>') of the ID that K1 gives its stage n (ticket K for a
compressor, H for an intercooler) is valid for any stage number n >= 1,
and has no effect while K1 has fewer stages. A key of the ID that BioSTEAM’s
Unit.auxiliary gives an auxiliary under a name of the
auxiliary_unit_names of the owner or of one of its auxiliaries is valid
whether or not that auxiliary is built, as is an old auxiliary object of a
unit at hand that none took the place of; while the auxiliary is missing,
the key has no effect. That ID is (owner.ID, 'name[k]') where the
attribute name holds a list of auxiliaries (as heat_exchangers of
HXutilities does) and (owner.ID, name) where it holds a single one,
so ('X1', 'heat_exchangers') or ('D1', 'condenser[0]') still raise.
Not every unit gives its auxiliaries that ID, and where the attribute holds
a unit of another ID, the key raises: the heater of a
biosteam.EnzymeTreatment T1 has the ID '', so ('T1',
'heat_exchanger') raises, with a message that gives the heater’s key,
('T1', '') ('T1' covers it too).
The cache. With cache_network=True, a cached network is reused only
while the explicit contributions resolve to the same value for every stream
(see Reusing a network below). T_min_app is never part of the reuse
test, with or without explicit contributions, so changing it alone keeps a
cached network, and a reused network keeps the streams without a
contribution at the T_min_app / 2 it was synthesized with (as does
synthesis_info['approach_contributions']).
Additive requirements only. A requirement that belongs to a pair but is not the sum of two stream terms cannot be written as one shift per stream, so it has no problem table and is not supported.
Targets: MER, pinch, composite and grand composite curves#
The two loads returned by the table are the minimum energy requirement
(MER) targets: the least hot and cold utility any network operating with this
T_min_app can use. They are available before a single exchanger has been
placed, which is what makes pinch analysis a targeting method – the target
comes first, and the network is judged against it.
The same information can be read graphically. The composite curves plot
cumulative enthalpy against temperature for all hot streams together and all
cold streams together, on the real temperature scale. Where the two curves
overlap horizontally, heat can pass from hot to cold: that overlap is the heat
recovered by process exchange. The overhang of the cold curve at its warm end
is the hot utility, and the overhang of the hot curve at its cold end is the
cold utility. The place where the curves come closest vertically is the pinch,
and by construction they approach no closer than T_min_app (with approach
contributions, no closer than the smallest hot plus the smallest cold
contribution; the shifted curves carry the pinch).
Composite curves of the quickstart system at T_min_app = 5 K. The
shaded band is the heat the two curves can exchange with each other; the
arrows are the two MER targets, a hot utility of 2.828e+08 kJ/hr and a cold
utility of 1.936e+06 kJ/hr. The curves come closest at the cold end, where
the pinch of this system lies – 298.15 K on the shifted scale.
Pinch analysis and targets builds this figure from a
ProblemTable; Targeting and supertargeting draws it
with the library.#
The grand composite curve plots the same cascade differently: the heat
carried through each shifted grid temperature once the minimum hot utility is
supplied, against that temperature. It touches zero exactly at the pinch for a
pinched problem; for a threshold problem, where no hot utility is needed, the
curve’s minimum may be strictly positive and the pinch is placed at the top of
the grid, Ts[0], by convention. Its shape shows where in the temperature
range utility has to be added or removed.
Both curves are library functions: PinchTargets returns
them (composite_curves(), on the real or the
shifted scale, and grand_composite_curve()), on
the knots of the table’s own grid, so they reproduce the table to rounding;
plot_composite_curves() and
plot_grand_composite_curve() draw them.
Both pictures express the three rules of pinch design: no heat may cross the pinch, no cold utility may be used above it, and no hot utility below it. Violating any one of them makes the network use more of both utilities than the targets require, by the amount transferred across the pinch.
Multiple utilities on the grand composite curve#
The MER targets say how much heat the utilities must supply and remove, not
which utilities. utility_targets() places
BioSTEAM’s heating and cooling agents on the grand composite curve, heating
and cooling separately, and prices each one with BioSTEAM.
Levels. BioSTEAM lets a heating agent serve a stream only below
agent.T - agent.dT - dT, and a cooling agent only above agent.T +
agent.dT + dT, where dT is bst.HeatUtility.dT, read at call time.
On the shifted scale that limit is the agent’s level: the limit itself for
a heating agent and the limit less T_min_app for a cooling agent, which is
BioSTEAM’s rule exactly. With approach contributions the streams have
different shifts, and an agent’s level is the highest single shifted
temperature (the lowest, for cooling), up to the highest of the streams’ own
limits, at which it serves no stream beyond that stream’s own real limit. A
level serves only what lies below it (above it, for cooling), and its heat
goes no further than the next zero of the curve balanced with the agents
placed before it (see below): the pinch, a utility pinch or another process
pinch. So a stream lying wholly beyond its limit bounds the level only at the
stream’s own end, and a stream lying wholly past that zero does not bound it;
streams with slack are then served conservatively.
List order and fuel. Non-fuel agents are placed in the order of
BioSTEAM’s lists, BioSTEAM’s own “first feasible agent” rule: for the default
lists, the lowest pressure steam first, and the warmest coolant first. Each
takes as much of the remaining duty as the curve above its level (below it,
for cooling) allows, given the agents already placed. What remains of the
heating goes to the first fuel agent, a furnace at the top of the curve; if
there is none, or if some cooling remains, it is left unserved, with a
RuntimeWarning and a nan cost. A level equal to a grid temperature
serves that temperature’s point loads; BioSTEAM’s own rule is strict, and the
two differ only at exact equality. The exception is a sensible agent whose
line would have no driving force left there: with agent.dT of 0
(UtilityAgent’s default), it could serve a point load at its limit only
at its own inlet temperature, at an unbounded flow, so its level stays just
short of that point load, which the next agent serves, as in BioSTEAM.
For the same reason, with approach contributions, a stream that ends at
such an agent’s limit bounds its level at that end, as a stream reaching
beyond its limit does: were another stream to set the level beyond that
end, the agent, having given heat above it (taken it below, for cooling),
would have to leave at its inlet temperature there.
The order is BioSTEAM’s preference, not price: the utility cost target is
what the agents cost placed this way, not the least utility cost, and an
agent later in the list can be cheaper per kJ (with BioSTEAM’s default
prices, natural gas is cheaper than any steam for a furnace heating from
below about 565 K), so a network can cost less than the target as well as
more (see the comparison of a network with its targets in Supertargeting:
area, units, capital and total cost below).
Served intervals and pricing. Each agent is priced as one BioSTEAM heat
utility over the stretch it serves: from its level, or from the end of the
grid if that is nearer, to the next zero of the balanced grand composite
curve (the cascade net of the utilities), which is the pinch or a utility
pinch where a lower agent is used to its limit.
A target’s T_in and T_out are the real temperatures of the streams it
serves in that stretch: the coldest and the hottest for heating, the hottest
and the coldest for cooling. A point load at an end of the stretch counts
where the stretch holds it: a boiler at the pinch, whose latent heat the cut
puts above it, is served at its boiling temperature. Without approach contributions
T_in and T_out are the stretch’s ends where streams reach them, and in
every case T_out is within the agent’s limit. BioSTEAM prices a latent
agent and fuel from them.
A sensible agent, such as cooling water, warms (or cools) along a line as
it takes its duty, and BioSTEAM checks that line only at the process inlet: a
coolant leaves at T_in - dT or at its limit, whichever is lower. Inside
the stretch the line must keep dT from the process too. Cooling water that
serves a condenser at the cold end of its stretch, say, has taken most of its
duty by the time it passes the condensing temperature, so it must leave well
below T_in - dT. At each point of the stretch the agent has taken at least
the duty less the balanced heat flowing there (on the grand composite curve:
its line stays inside the curve, as a non-isothermal utility’s does in pinch
analysis), and that bounds its outlet. hensmith prices the agent at the
highest outlet the bound allows (the lowest, for heating), by giving BioSTEAM
the matching inlet. Its flow, and therefore its cost, is then the least a
single stream of the agent needs. With approach contributions the bound takes,
at each point, the lowest real temperature of the hot streams there (the
highest of the cold ones), which is conservative.
BioSTEAM divides each duty by the agent’s heat transfer efficiency, so a
target’s utility_duty is utility side while its duty, like the MER
targets, is process side.
Each pricing uses fresh utility streams in a temporary flowsheet: the costs
are deterministic, and the user’s flowsheet, the agents and their later costs
are untouched.
Supertargeting: area, units, capital and total cost#
Energy is one part of the cost of a network; its exchangers are the other.
Supertargeting targets the exchangers too – their area, their number and
their capital cost – before design, like the MER targets, and with them the
total annualized cost, which a sweep of T_min_app then minimizes
(Linnhoff & Ahmad, 1990; Ahmad, Linnhoff & Smith, 1990).
summary() returns every target at one
T_min_app and supertarget() sweeps it.
Balanced composite curves. Adding the utility targets to the composite
curves – the heating utilities to the hot curve, the cooling ones to the cold
curve – gives two curves that span the same heat
(balanced_composite_curves()): a latent agent is a
flat at its temperature, a sensible one a line between its inlet and outlet
temperatures, and fuel a furnace that takes no area. If some utility is
unserved, the curves cannot balance, and the area, capital and total cost
targets are nan; the unserved cooling, the coldest part of the cold curve, is
left out as a gap at its start, which keeps the two curves aligned.
Area. The area target is Bath’s formula (Townsend & Linnhoff, 1984) on the balanced composite curves: heat is transferred vertically, from the hot curve to the cold curve below it, in counter-current (a correction factor of 1). Split the heat axis at every breakpoint of either curve into intervals \(i\); in each, stream or utility \(j\) carries heat \(q_{ij}\), and
where \(\Delta T_{LM,i}\) is the log-mean temperature difference of
interval \(i\) on the real scale and \(h_j\) the film coefficient of
\(j\). Every curve and every stream’s share of it is linear in the heat
inside an interval, so the sum is exact. hensmith takes \(h_j\) = 1
kW/m²/K for every stream and utility by default, which gives any pair
BioSTEAM’s heuristic overall coefficient of 0.5 kW/m²/K (condensing against
boiling, where BioSTEAM takes 1.0, is not reproduced); h= and
h_utilities= set others. A zero or negative driving force anywhere
makes the area infinite, with a RuntimeWarning, as at T_min_app = 0.
Units. A network of \(S\) streams and utilities that forms one connected component needs at least \(S - 1\) units (Euler’s rule). The units target applies it to each region between the zeros of the balanced grand composite curve – the process pinch and every utility pinch –, \(S\) counting the streams with heat in the region and the utilities acting in it. It is an estimate, S - 1 per region, assuming one component and no loop: a subset of streams that balances on its own needs fewer units, and each loop of matches one more.
Capital. The area is put in as many shells as the units target,
\(N\), less one furnace for all the fuel, and every shell is costed with
BioSTEAM’s own correlations; the furnace is costed on the fuel’s utility duty,
at BioSTEAM’s default furnace pressure of 500 psi.
Linnhoff & Ahmad (1990) divide the area equally, \(N \cdot C(A / N)\):
with economies of scale (\(C = a + b A^c\)) that is the costliest way to
divide it, and every unit added adds cost. BioSTEAM’s correlations are not of
that form: a double pipe exchanger below 150 ft² costs at most about 8,100
USD installed, a shell and tube at 150 ft² about 64,700 USD. An equal division
can then put every shell below 150 ft², so that a utility of a few watts that
adds units would cut the capital target by most of it (the four-stream problem
of Linnhoff & Hindmarsh at 5.56 K: seven double pipes instead of four floating
heads, a fifth of the cost), although a network just adds a small exchanger
and keeps its large ones. So when the
equal division puts the shells below 150 ft² and the area fills
\(k = \lfloor A / 150\ \mathrm{ft^2} \rfloor \geq 1\) shells of at least
150 ft², the target is the costlier of the \(N\) double pipes and of
\(k\) equal shell and tubes with \(N - k\) shells of no area (each
costed as BioSTEAM costs a vanishing double pipe, about 1,300 USD installed).
This is the least change to the equal division that never falls when a unit
is added, except for shells beyond about 35,000 ft² (3,300 m²) with the
default floating head of carbon steel (from about 15,000 ft² by type and
material), whose cost rises faster than their area; where it departs from it,
it tends to be high (networks keep only part of their area in shell and tubes)
as the equal division tends to be low.
The summary’s 'N_area_shells', 'shell_area' and 'shell_type' say
which shells carry the area. BioSTEAM’s other specifics carry over: the
floating head correlation is not monotone below about 126 ft², which the
double pipe hides, and there is no largest shell area. All shells are costed
at one pressure, P, whereas BioSTEAM takes each exchanger’s highest inlet
pressure; for high pressure steam heaters its pressure factor is up to about
12 % higher.
Total annualized cost. With a capital recovery factor over \(n\) years at interest \(i\),
with \(i\) = 10 %, \(n\) = 10 years and \(t_{op}\) = 8000 h/yr
by default, or taken from a BioSTEAM TEA (tea=). The targets of a facility
(HXN.targets()) take \(t_{op}\) from its system when the system has
operating hours (a TEA sets them), as the network report does. It counts the
bare module cost of the exchangers only: no Lang factor and no working capital.
The sweep. supertarget() evaluates the targets over a grid
of T_min_app values, reusing the streams’ curves (nothing is flashed
again, though each point builds its own problem table, about the table of a
synthesis), and then refines the grid around the least total annualized cost. The
cost is not smooth – the units target jumps, the double pipe switches to a
shell and tube, the pinch moves – so the refinement only narrows a bracket,
and at_bound flags an optimum at an end of the sweep. The capital target
drops by about a shell and tube wherever the shrinking area fills one fewer of
150 ft², so the least cost often lies just past such a step rather than at a
smooth trade-off of energy and capital: compare the 'N_area_shells' and
'shell_type' of its neighbors (sweep.to_frame()). A network has such
steps too, but where its own exchangers cross 150 ft², which the target’s
equal shells place only roughly. Where the targets do
not depend on T_min_app – below the threshold of a threshold problem, the
utilities and the balanced composite curves stay the same – the costs differ
only by the discretization of the curves, by a tiny relative amount (4e-7
of the total cost for the quickstart system), so where the optimum lies in
that flat stretch means nothing physically; where the discretization trend is
steady, it lies at an end of the stretch (for the quickstart system, the
warmest point evaluated, just short of the threshold). A sweep whose costs
are all finite and the same to round-off (streams that recover no heat, say)
is not refined, and constant flags it. A sweep with no finite
total cost (an unserved utility everywhere, say) is not refined and warns that
its optimum is meaningless; plot_supertargets() shades the
stretches of a sweep without a finite cost. With approach
contributions, only the unspecified streams follow T_min_app;
scale_contributions=True scales the specified ones with it, the
contribution scaling of Ahmad, Linnhoff & Smith (1990).
A network against its targets. network_report() compares a
synthesized network with its targets: utility cost, units, area, capital and
total annualized cost. Its 'area' re-sizes the network’s exchangers like
the target sizes them (the same film coefficients, a correction factor of 1,
the integrated driving force along the streams’ profiles as the targets’
curves model them and the process side duty), so it is comparable with the
target; 'area_biosteam' is BioSTEAM’s own sizing, which includes its
correction factor, the terminal log-mean temperature difference and the
utility side duty. An exchanger whose profiles meet or cross has an infinite
'area', and so has the network, with a RuntimeWarning naming it, while
its 'area_biosteam' stays finite: BioSTEAM specifies a utility from the
ends of its exchanger only, taking a sensible agent to its limit (cooling
water to 324.82 K) whenever the process inlet allows it, so against a stream
that condenses a little below that limit (a vacuum condenser, say) the
utility’s line crosses the condensing stretch inside the exchanger. The
report keeps BioSTEAM’s specification of the utility, whose cost it reports;
the target, which keeps a sensible agent’s line inside the grand composite
curve, stays finite. The report checks that its targets and the network’s
streams are both of the units’ current states: after a stream or a unit
changes, it raises a ValueError until the system is simulated again
(HXN.targets() warns meanwhile of a stream whose inlet has changed in flow
since its unit was simulated, whose targets would run from that new inlet to
its former outlet). The comparison has limits:
the Bath target bounds the area only with equal film coefficients and the same utilities;
the network gives each stream one agent at its outlet, BioSTEAM’s list rule, while the target allocates the agents on the grand composite curve in list order, fuel last, so the network can use other agents than the target, costlier or cheaper ones. On the four-stream problem of Linnhoff & Hindmarsh at 10 K, the target is low pressure steam and cooling water, the network medium pressure steam and chilled water, which cost more. But a stream heated above the hottest steam takes fuel over its whole utility stretch, and with BioSTEAM’s default prices natural gas costs less per kJ than the steam the target places below the fuel: a stream of 1 kW/K heated from 330 to 520 K against one cooled from 400 to 300 K, at 10 K, takes natural gas from 390 K at 5.2e-6 USD/kJ, where the target places low, medium and high pressure steam (6.5e-6 to 1.2e-5 USD/kJ) and fuel only above 504 K, so the network at MER costs 3.14 USD/hr against a target of 4.56 USD/hr. The utility cost target is the cost of BioSTEAM’s placement of its agents, not a minimum, and neither it nor the network’s utility cost bounds the other;
the units target is an estimate in both directions, since Euler’s S - 1 assumes one component and no loop per region: a balanced subset of streams, or one agent serving a stream whose heat the target splits between two utility levels, needs fewer units; each loop of matches needs one more, whether a repeated match between two streams (two branches of a split stream meeting the same stream make one too) or two streams linked both through process matches and through a shared utility (for such a loop, the quickstart network at 5 K has 7 units against 6, see Targeting and supertargeting).
From targets to a network: the pinch-outward MER planner#
synthesize_network() takes the heat utilities of the process,
runs the problem table above, plans a network without stream splits that
reaches the MER targets whenever its search finds one (by default; with
stream_splitting=True it splits streams where MER needs them, see Stream
splitting below), and realizes the plan
as BioSTEAM exchangers. Streams are numbered in a rearranged order – heated
streams first, then cooled streams – and every array, exchanger ID and life
cycle uses that index. The order only breaks ties in the planner’s search: the
facility hands the utilities over sorted by signed duty, and
sort_hus_by_T sorts them by inlet temperature instead.
The planner’s model. Each stream enters the planner as its knots on the problem-table grid: its enthalpy at every grid temperature inside its range, with two knots where its curve has a flat. Every breakpoint of every curve is a grid point and the table is linear between grid points, so the planner’s own cascade reproduces the table exactly – the same targets, the same pinch and the same pinch cut. Each stream is cut at the pinch into an above-pinch part and a below-pinch part (a stream lying wholly on one side has an empty part on the other), and the two sides are planned as two independent problems.
Must and flex streams. Above the pinch no cold utility may be used, so every hot stream there is a must: process matches have to cool it completely. The cold streams above the pinch are flex streams: whatever their matches leave over is supplied by one hot utility at their far (hot) end. Below the pinch the roles swap: no hot utility may be used, so every cold stream is a must, and the hot streams are flex streams finished by one cooler at their cold end. Putting a flex stream’s utility at its far end loses nothing: moving a stream’s later matches toward its inlet never reduces the approach of any match on it, because every stream’s curve is monotone.
Building each side from the pinch outward. A depth-first search builds
each side one match at a time, starting at the pinch, where the driving forces
are smallest, and moving outward. A match of duty \(x\) between a must and
a flex stream is feasible only if the two streams keep T_min_app at every
knot along it, so internal pinches – a condensing vapor against a boiling
mixture, say – are respected, not just the exchanger’s terminals. Every step
also keeps the problem table of the remaining problem feasible (remaining
problem analysis): the largest duty that does so has a closed form, so no step
can make MER unreachable by its own table. Wherever that remaining table is
tight – a pinch of the remaining problem – the pinch design rules of Linnhoff
and Hindmarsh must hold:
above the pinch, every hot stream at the pinch needs its own cold stream at the pinch, with \(C_{hot} \le C_{cold}\) (the number rule \(N_{hot} \le N_{cold}\) and the heat-capacity-flow rule);
below the pinch, every cold stream at the pinch needs its own hot stream at the pinch, with \(C_{hot} \ge C_{cold}\).
Both rules are generalized to isothermal segments: a flat – a pure component boiling or condensing exactly at the pinch – has unlimited series capacity and can serve several partners in turn. At the process pinch itself a violation of these rules proves that MER needs stream splitting.
The candidate duties of a match are the largest feasible one and a finite set of events: a stream is ticked off, a partner is saved for another stream, a stream switches partner or returns to an earlier one. The search is budgeted in deterministic work units rather than seconds, so its result does not depend on the speed of the machine. Once a MER plan is found, a branch and bound on the number of exchangers looks for a smaller one; consecutive pieces of the same match are merged into one exchanger.
Repeated pairs. The same hot and cold stream may be matched more than
once on the same side: alternating two partners in series emulates a split,
and some unsplit MER networks need it (a real split is optional, see Stream
splitting). Process exchangers are named
HX_<cold>_<hot>_hs above the pinch (the hot-side design) and
HX_<hot>_<cold>_cs below it (the cold-side design), the first number being
the stream at port 0; the n-th exchanger of the same pair on the same side,
counted in the order the hot stream meets them, gets the suffix _<n> for
\(n \ge 2\) (for example HX_3_2_cs_2). Utility exchangers are
Util_<index>_hs for a cold stream and Util_<index>_cs for a hot one.
avoid_recycle=True forbids matching any pair twice anywhere – on one side
or across the two – so that no two exchangers connect the same pair of
streams, at the cost of the MER networks that need a repeated pair.
Best effort when splitting is needed. By default, a side whose pinch rules
prove that MER needs a split, or whose search runs out of budget, gets a
best-effort plan instead (with stream_splitting=True it is planned with
splits, see Stream splitting). Heat that a must stream cannot place (a gap) is moved to the
stream’s pinch end, where it crosses the pinch at the cost of an equal amount
of extra hot and cold utility, the penalty; greedy dives and a bisection of
the gaps keep that penalty small, though not minimal in general. Such a
network reports 'best_effort'.
Small matches. A planned exchanger with a duty below Qmin (default
1e-3 kJ/hr) is dropped and its duty left to the utilities. Removing a
match never reduces the approach of another, so the rest of the plan stays
feasible; a large Qmin can, however, cost MER.
Realization. Each stream is walked in flow order from its inlet: a hot
stream through its above-pinch matches (from its inlet end), then its
below-pinch matches, then its cooler; a cold stream through its below-pinch
matches, then its above-pinch matches, then its heater. Each match becomes one
plain HXprocess whose two enthalpy limits, H_lim0 and H_lim1, are
the planned outlet enthalpies of its two streams, so that its duty reproduces
the plan. A planned outlet whose equilibrium state is not past the stream’s
state at the exchanger inlet cannot be a limit (HXprocess rejects it): one
strictly inside a non-equilibrium end jump, or a state of a point-load stream
on the wrong side of its inlet. That stream’s limit is left out, and the other
stream’s sets the duty. A stream’s first exchanger receives its real inlet –
except a point-load stream, which enters at equilibrium at its inlet enthalpy,
on the side of its outlet temperature where the plan put its duty – and every
later exchanger receives the stream’s exact state at the planned enthalpy.
Each exchanger is simulated once; one whose duty differs from the plan is
reported, and one that cannot be simulated at all is dropped, its duty going
to the utilities. Finally one rigorous HXutility per stream takes it to its outlet
enthalpy, and an AssertionError is raised if that does not reproduce the
quenched outlet enthalpy and temperature, so a network that would not deliver
the specified outlets fails loudly rather than silently.
Exact approach verification. The knots are exact at grid points but are
chords in between, up to 0.002 K off inside glides and curved single-phase
stretches. Every planned exchanger is therefore checked on the exact states of
its two streams wherever its planned approach is within that margin of
T_min_app: at its ends, at every breakpoint inside it and, where the exact
approach is not linear between two positions, by a search for a dip in
between. HXprocess itself checks only its two terminals, which would miss
an internal pinch at a phase change. Where a MER plan falls short by more than
1e-6 K, the exact states are inserted as knots and the network is planned
again, for at most three rounds. A best-effort plan, or a MER plan still short
after the last round, instead has each violating match shrunk to the largest
duty that keeps the approach, the rest going to the utilities: with its inlets
fixed, a smaller duty can only raise a match’s approach, and the later stages
of both streams move toward their inlets, which never reduces another match’s
approach. Streams of constant heat capacity never need either step.
The report. synthesize_network(..., info={}) fills the dictionary it is
given, and HeatExchangerNetwork keeps it as
synthesis_info. 'status' is 'mer' only if the utilities of the
realized network, computed from the simulated exchanger duties, equal the
targets (to 1e-6 of the total stream duty), and 'best_effort' otherwise.
Next to it are the targets ('Q_hot_target', 'Q_cold_target'), the
planned and the realized utilities, the 'penalty', per side of the pinch
('sides') the search method, its work, any proof that a split is needed
and the gaps, the planner’s own targets and pinch, the number of refinement
rounds, the smallest approach inside any process exchanger, the matches that
were shrunk ('repaired'), dropped ('qmin_dropped', 'dropped') or
deviated from their plan ('deviations'), and the point-load streams. With
stream_splitting=True it also holds the realized splits ('splits'), any
mixer whose outlet is off its planned state ('split_deviations') and, per
side, the split candidate chosen ('split'). The full list is under the
info keyword of synthesize_network().
Stream splitting#
By default hensmith does not split streams, so a side of the pinch whose
pinch design rules prove that MER needs a split gets a best-effort network.
HeatExchangerNetwork(..., stream_splitting=True) (or
synthesize_network(..., stream_splitting=True, info={})) lets the planner
split a process stream into parallel branches that re-join. Only a side that
no unsplit network serves at MER – one with a pinch-rule proof, or whose
unsplit search leaves a utility penalty – is planned with splits; a side that
an unsplit network serves is planned exactly as without the option, so a
problem that needs no split gets the same network either way.
Branches. A branch of flow fraction \(f\) carries the parent stream’s
material at the parent’s pressure, so at branch heat \(q\) it is in the
parent’s state at heat \(q/f\): its temperature-enthalpy curve is the
parent’s with every heat times \(f\), and its heat capacity flow rate is
\(f\) times the parent’s. Splitting a stream into branches whose fractions
sum to one leaves the problem table unchanged, so a split never changes the
targets; it changes only what the pinch design rules count (one stream per
branch, each with \(f\,C\)) and the approach inside each exchanger. The
planner builds a split side from a portfolio of candidates – splits at the
pinch that repair the number and heat-capacity-flow rules, completed by the
ordinary search, and a vertical construction that matches every stream’s
branches at the same position of the composite curves – verifies every
exchanger at every knot of both branch curves, and keeps the best candidate
by a key that first avoids tiny exchangers or branches and re-joins at
different temperatures ahead of another exchanger, then counts exchangers,
extra branches and split stages. The theory (the lemmas, the constructions
and their proofs) is in the module docstring of hensmith._splitting.
Realization. Each split becomes a chain of BioSTEAM Splitter units,
one fewer than its branches (IDs Split_<stream>_<hs|cs>, with _<n> for
a stream’s n-th split on that side and _b<c> for the chain’s element
\(c \ge 2\)), and a rigorous, adiabatic Mixer (Mix_<stream>_<hs|cs>)
where the branches re-join; neither is costed. A branch exchanger runs its
fraction of the flow in the parent’s states, with an enthalpy limit
\(f\) times the parent’s, and every mixer starts at its planned state. The
facility lists them in new_splitters and new_mixers and simulates them
in HXN_sys with the exchangers; synthesis_info['splits'] describes
every split (StreamSplit), and a split
stream’s life cycle marks each branch stage with its branch and fraction.
The guarantee and its limits. Without avoid_recycle, every side that
needs a split gets a split plan at MER on the planner’s knots: the vertical
construction always succeeds (up to a deficit of about 1e-9 of the total duty
that the problem table’s own tolerances already absorbed into the targets).
With constant heat capacities the knots are exact and the realized network
reaches the targets to round-off. With real thermodynamics the knots are
chords of the exact curves, so a split exchanger can fall short of
T_min_app on the exact states by up to the chords’ tolerance; the same
refinement rounds as for unsplit networks close that, plus up to two rounds
that try another candidate, and only an exchanger still short after them is
shrunk ('repaired', status 'best_effort'). With avoid_recycle, a
split that would repeat a stream pair is not used, and MER is then not
guaranteed. The number of exchangers is minimized only among the candidates a
side generates, not globally. The test suite reaches 'mer' with splits on
all 38 split-needing problems of its corpus and on the three regression
systems that need splits, each checked on its actual stream graph: splitters,
mixers at equilibrium, balances and the exact approach inside every exchanger.
Configuring the network and a larger system shows an example.
Rigor and phase change#
Process streams in a biorefinery boil, condense and change composition, so hensmith never assumes a constant heat capacity. Every enthalpy it uses comes from thermosteam:
Quenched outlets. Before any analysis, each stream’s outlet copy is re-flashed at its own enthalpy (
s.vle(H=s.H, P=s.P)). An upstreamHXutilitythat was not solved rigorously can leave an outlet in a non-equilibrium state; quenching puts that heat at the temperature the equilibrium model says it is available at.Curves instead of flashes on the grid. Flashing every stream at every grid temperature has three defects that the stream curves avoid. A phase boundary or a curvature inside a grid interval is averaged away (see The shifted grid above). A
vle(T=T_sat)of a single chemical keeps whatever phase split the stream had, so the enthalpy exactly at a saturation temperature depends on history; the curve takes a pure component’s latent heat as a flat between V-specified saturated states and evaluates single-phase stretches with their phases fixed, so a grid point on a saturation temperature is never ambiguous. And thermosteam’s two-phase flashes of some mixtures (water and ethanol with 20-50 % ethanol, for instance) silently return non-converged states; the curve traces a binary glide along its bubble-point curve and sanity-checks the flashes of other mixtures.Point loads. A pure component’s latent heat contributes at its saturation temperature as a point load, and so does the whole duty of an isothermal or non-monotone stream at its outlet temperature, instead of being smeared over an interval; that is what keeps the cascade – and the pinch it locates – correct for latent duties.
Pinch states.
pinch_statereturns the state of a stream at a pinch temperature from its curve, deterministically: a pinch on the stream’s own saturation temperature is resolved by the side of the pinch cut, never by whatever phase split a previous flash left, and the enthalpy is clipped to the stream’s real range, so the state never carries heat the real stream does not have. It is a standalone analysis helper; the synthesis plans on the curves themselves.Rigorous exchangers. Every synthesized process exchanger is an
HXprocesssimulated from exact inlet states with both enthalpy limits at its planned outlets and its approach verified on exact states, and every synthesized utility exchanger is anHXutilitywithrigorous=True, specified by enthalpy rather than by temperature.
The network as a BioSTEAM system#
HeatExchangerNetwork is a BioSTEAM Facility. Its
_run and _design do nothing; all of the work happens in _cost,
which runs only after every process unit of the system has converged, so the
duties it integrates are final ones.
A separate flowsheet. The synthesis happens inside a temporary flowsheet
named <sys>_HXN, where <sys> is the system’s ID. The original streams,
exchangers and units are never modified (unless replace_unit_heat_utilities
is set, described below): the network is built from copies, and
those copies and the new exchangers are what you inspect afterwards through
new_HXs, new_HX_utils and the stream_life_cycles
(StreamLifeCycle) that
plot_pinch_diagram() draws.
Which streams take part. The facility collects the heat utilities of its
units (all units of the system unless a list or a callable is given),
drops any utility flagged as not usable for HXN integration
(hxn_ok=False, set by some unit operations) or with non-positive flow,
anything listed in ignored, and anything with zero duty, and sorts what is
left by duty. Auxiliary exchangers – a column’s condenser and reboiler, a
flash’s feed heater – are included like any other.
Convergence. After synthesis each stream’s stages are rewired in series
(the branches of a split in parallel, from its splitter chain to its mixer),
and the new units are assembled into a System, HXN_sys, whose path
follows the streams: every stage links to the next stage of the same stream,
and the path is a topological order of that graph (Kahn’s algorithm,
ties broken by the order of the exchangers). Where the graph has a cycle –
a pair of streams matched both above and below the pinch, or repeated matches
in alternating order – the unit with the fewest unplaced predecessors comes
next and its inlets from later units become recycle streams. Every exchanger
starts at its planned state, so the loops are at their fixed point after one
pass; the system is converged by fixed-point iteration to tight tolerances
(a temperature change of 1e-8 K), which closes the energy balance to about
1e-10 %. Should convergence raise, every unit is run once and a
RuntimeWarning is issued.
Streams served by process exchange alone. A stream that its process exchangers bring to its outlet (to within 1e-9 of its duty, the residual of the enthalpy flashes) leaves its utility exchanger in exactly the state it enters it, so that exchanger has no duty and no cost – rather than a spurious duty of a few 1e-9 kJ/hr from re-flashing the stream, which biosteam would design and cost as a minimum-size exchanger.
Where it sits among the facilities. network_priority = -2 is lower
than that of any other standard BioSTEAM facility, and facilities are
simulated in increasing order of priority, so the network is integrated before
the chilled water package, cooling tower and boiler are sized: they see the
loads it has already reduced.
Costs and utilities are differences. The facility’s capital is the added
exchanger cost, max(0, new - original) for both installed and purchase
costs, so a network whose exchangers happen to be cheaper than the ones it
replaces is reported as adding nothing rather than as a credit. Its heat
utilities are the new utilities summed by agent with the reversed original
ones – new minus original – so a negative utility cost on the facility is a
saving. Setting replace_unit_heat_utilities=True instead overwrites each
original unit’s heat utility with that of its own stream’s utility exchanger,
reloads the utility costs of the unit and of its owner, and leaves the
facility itself carrying none; the original data are given back before the
network is costed again, so the next network is synthesized from the units’
own utilities. If no process match was made at all, the facility reports zero
capital and no utilities.
Reusing a network. With cache_network=True a later simulation checks
whether the units behind the heat utilities, each as often as it gives a
stream (biosteam lists the heat utility of a unit in parallel once per unit,
so a reactor whose parallel['self'] changes with its volume changes the
streams), and the explicit approach contributions they resolve to, are
unchanged; if they are, the same network configuration is reused instead of
being synthesized again. Each
life cycle’s first inlet is copied from the current stream, and each process
exchanger keeps, as the enthalpy limit of its must stream (the hot stream
above the pinch, the cold stream below it), the fraction of that stream’s
duty at which its limit sat at synthesis, so a changed feed rescales every
stage instead of letting the first one take the whole duty. Its partner, the
flex stream, transfers what that sets, but never past its own outlet, and
takes the rest to its utility exchanger; a port that had no limit at
synthesis gets none. The utility exchangers bring every stream to its new
outlet enthalpy. A reused network is not planned again, so it need not be at
MER for the new duties, and synthesis_info still describes the synthesis
that produced it. If the reused network then fails its outlet checks,
hensmith warns, discards the cache and re-synthesizes from scratch. If it fails
its energy balance instead, the cache is discarded and the network
re-synthesized silently; a warning is issued only if the freshly synthesized
network fails the same check.
Validation#
hensmith checks every network it synthesizes, and its test suite checks the synthesizer against independent references.
The energy balance. After convergence, every process exchanger duty is counted twice – it satisfies a cooling demand and a heating demand at once – and added to the new utility duties, and the total is compared with the original utility duties, each utility weighted by its agent’s heat transfer efficiency:
energy_balance_percent_error is \(100 (Q_{bal} - 1)\), reported for
diagnostics. The check itself compares the underlying fraction: when
\(|Q_{bal} - 1|\) exceeds acceptable_energy_balance_error (a class
attribute, 0.02, i.e. 2%, overridable per instance), hensmith warns – or
raises, if raise_energy_balance_error is set. A cached network that fails
this check is instead discarded and re-synthesized, as described above. A
converged network closes this balance to numerical precision.
Outlet reproduction. Each stream’s life cycle must end in the same state as the original exchanger’s outlet: composition, pressure and enthalpy are asserted stream by stream. The utility exchangers created during synthesis are checked the same way, against the quenched outlet enthalpy and temperature.
The approach inside every exchanger. The synthesis verifies every process
exchanger on the exact states of its streams (see Exact approach
verification above) and records the smallest approach it found in
synthesis_info['min_approach'], and the smallest margin of an
exchanger’s approach over its pair’s required approach in
synthesis_info['min_approach_margin']; the tests re-check both with
temperatures computed independently of the code under test.
MER identification and achievement. tests/test_hxn_mer.py synthesizes
a corpus of 78 problems through the public facility. For 40 of them – 24
constant heat capacity problems from the literature and 16
real-thermodynamics problems with condensers and boilers at the pinch,
desuperheating and subcooling, and binary glides; 11 with more than ten
streams – an unsplit MER network exists, proven inside the suite by a
certificate network that is re-checked there (by plain arithmetic for
constant heat capacity); the synthesized network must reach the targets and
report 'mer'. For the other 38 (25 from the literature and 13 with real
thermodynamics; 13 with more than ten streams) the pinch design rules prove
that MER needs stream splitting, a proof re-derived in the test module; by
default the network must never beat the targets and must report
'best_effort', and with stream_splitting=True it must reach them and
report 'mer', checked on its actual stream graph (splitters, mixers at
equilibrium, every stream’s closure). The 40 problems of the first set must
also plan exactly as without the option (18 of them are synthesized both
ways, and must give the same network). In both sets the
targets must equal an independent
reference – a closed-form constant heat capacity cascade and the published
values, or a dense-grid calculator for real thermodynamics – and every
material and energy balance and the exact internal approach of every
exchanger are checked.
Regression cases. tests/test_hxn_regression.py builds ten synthetic
systems of increasing complexity – all with phase-changing streams from the
third on – and for each one requires that the synthesized network (i) closes
its energy balance without raising RuntimeWarning, (ii) never uses less
hot or cold utility than the MER targets computed on the same streams, and
reports 'mer' exactly when it reaches them, (iii) keeps each pair’s
required approach inside every process exchanger on exact states, (iv) is
planned on the problem table’s own cascade, and (v) recovers at least as
much heat as a load recorded in the test file. A network that improves leaves slack in (v); those
recorded numbers are lowered deliberately by a maintainer, never raised to
make a failing test pass. Every case is synthesized again with
stream_splitting=True and held to the same checks, and the three cases
that need stream splits for MER must then reach their targets.
Approach contributions. tests/test_hxn_contributions.py checks the
contributions against references that do not use them. Without
contributions – and with every contribution unspecified or equal to
T_min_app / 2 – plans, problem tables and facility networks must equal
the default ones bit for bit, compared in-process. Every stream at
\(x\) must plan the network of T_min_app \(= 2x\), and on
real-thermodynamics cases all hot streams at \(a\) and all cold ones at
\(b\) must give the default targets at T_min_app \(= a + b\).
With mixed contributions, the targets of 40 random problems, of the 24
constant heat capacity no-split problems of the MER corpus and of five split
ones (which must reach MER with stream_splitting=True) must equal a
closed-form constant heat capacity cascade with the classic symmetric shifts,
and every planned match must keep its pair’s requirement on recomputed
temperatures. The facility, with and without splits, on constant heat
capacity and real-thermodynamics cases, must reach the contribution-aware
targets, keep each pair’s requirement on exact states recomputed
independently and give each exchanger its pair’s dT; the exact check and
the repair must act on each pair’s own requirement. The key rules, the
validation of keys and values (for idle units too, and before any stream is
flashed) and the cache_network rule are tested as well.
Targeting. tests/test_targeting.py checks hensmith.targeting
against references computed independently of it: the composite curves of
the four-stream problem of Linnhoff & Hindmarsh in closed form, and the
composite and grand composite curves of constant heat capacity and
real-thermodynamics cases against their problem tables; the multiple-utility
allocations and served intervals against duties computed from the agents’
own attributes, each priced again in isolation; the area target against a
midpoint integration of hand-built composite curves (with approach
contributions and per-stream film coefficients too) and against closed
forms; the units target on eight reference problems, utility pinches and
balanced subsets among them; the exchanger costs against BioSTEAM’s own; the
capital recovery factor against BioSTEAM’s; and the swept optimum against
the closed-form total annualized cost of a two-stream problem. The
facility’s targets must equal the synthesis’s bit for bit, with and without
approach contributions; the network report’s areas must equal independent
log-mean areas integrated along the network’s own streams where these are at
equilibrium (the report follows each stream as the targets’ curves model it,
like for like with the area target, so it may differ slightly for a
point-load stream, held at its outlet temperature, or a non-equilibrium end
state, such as a vapour entering below its dew point); and targeting must
leave the flowsheet’s streams, the agents, later utility costs and a later
synthesis unchanged.
Doctests. The examples in the docstrings are executed as part of the test suite, so the numbers printed in the API reference are numbers the code currently produces.
Guarantees and limitations#
What the synthesized network is guaranteed to be:
Never better than MER. Its utilities are never below the targets of the problem table, and
'mer'is reported only if the realized network reaches them.Feasible everywhere inside. Every process exchanger keeps its pair’s required approach (
T_min_app, orc_h + c_c) minus 1e-6 K on the exact stream states at its ends and at every checked position inside it, and the heat balance closes on every stream.Deterministic. The search is budgeted in work units, not seconds, so the same streams give the same network however fast the machine is.
What it is not:
MER is reached whenever the planner finds an unsplit network – which is an empirical, not a proven, property. Every pruning test of the search is a necessary condition, so a missed MER network can only come from the finite set of candidate duties, the caps on repeated pairs or the work budgets. The planner reached MER on every problem of a certified benchmark of about 1,700 problems with 2 to 40 streams for which an unsplit MER network exists, and it does so on all 40 no-split problems of the test suite, but no proof covers every problem.
By default, streams are not split. Every stream stays a single branch through the network. Where the pinch design rules prove that MER needs a split, the network is a best-effort one whose penalty is small but not minimal in general; repeated matches between the same two streams, alternating in series, can approach a split only in the limit. With
stream_splitting=Truesuch a side is split and reaches MER, within the limits given under Stream splitting: exact on constant heat capacity streams, closed by refinement on real thermodynamics, not guaranteed withavoid_recycle, and with the number of exchangers minimized only among the candidates the planner generates.Energy first, then units; no cost optimization. MER always takes precedence over the number of exchangers, and an unsplit MER network can need many of them. The branch and bound reduces the number of exchangers among MER plans, but the synthesis does not optimize area, capital or total cost;
hensmith.targetingtargets them, finds theT_min_appof the least targeted total annualized cost (see Supertargeting: area, units, capital and total cost), andnetwork_report()compares a network with the targets.Some networks cannot be represented. Networks whose match order is cyclic are outside the planner’s model. A side that needs a split without a pinch-rule proof spends its whole MER search budget before the best-effort step (or the split attempt), which costs time rather than quality.
Flash failures inside some glides. Thermosteam’s TP flashes fail silently inside the glides of some mixtures (water and ethanol with 20-50 % ethanol, for instance); an exchanger simulated there can deviate from its plan, and is then reported in
synthesis_info['deviations'](a split’s mixer, insynthesis_info['split_deviations']).Only streams behind existing utility exchangers are integrated. The facility sees a process stream only through a heat utility attached to a unit of the system. A duty carried some other way is invisible to it; utilities flagged as not usable for HXN integration (
hxn_ok=False) or with non-positive flow, and streams excluded withignored, are removed from the analysis entirely.Additive approach temperatures only. Per-stream approach temperature contributions are supported, so a match’s requirement is
T_min_appor the sum of its two streams’ contributions; a requirement that belongs to a pair and is not such a sum is not (see Approach temperature contributions). The utility exchangers use BioSTEAM’s approach rule whatever a stream’s contribution.Targets are estimates. Beyond the MER targets, the targets of
hensmith.targetingare estimates: the utility cost target is the cost of BioSTEAM’s placement of its agents, which a network at MER can exceed or undercut, the area target bounds a network’s area only with equal film coefficients and the same utilities, the units target can be wrong in both directions, the capital target assumes shells of equal area at one pressure (keeping the shell and tubes the area fills when an equal division would drop below 150 ft²), and with approach contributions the utility levels are conservative and their pricing temperatures approximate. The optimumT_min_appis the optimum of these estimates, and often sits just past a step of the capital target where the area fills one shell and tube fewer: a step a network shares, though not at the sameT_min_app.
References#
Linnhoff, B., & Hindmarsh, E. (1983). The pinch design method for heat exchanger networks. Chemical Engineering Science, 38(5), 745-763.
Smith, R. (2005). Chemical Process Design and Integration. Wiley.
Kemp, I. C. (2007). Pinch Analysis and Process Integration: A User Guide on Process Integration for the Efficient Use of Energy (2nd ed.). Butterworth-Heinemann.
Seider, W. D., Lewin, D. R., Seader, J. D., Widagdo, S., Gani, R., & Ng, M. K. (2017). Product and Process Design Principles. Wiley. Heat Exchanger Networks (Chapter 9).
Townsend, D. W., & Linnhoff, B. (1984). Surface area targets for heat exchanger networks. IChemE 11th Annual Research Meeting, Bath, UK.
Linnhoff, B., & Ahmad, S. (1990). Cost optimum heat exchanger networks - 1. Minimum energy and capital using simple models for capital cost. Computers & Chemical Engineering, 14(7), 729-750.
Ahmad, S., Linnhoff, B., & Smith, R. (1990). Cost optimum heat exchanger networks - 2. Targets and design for detailed capital cost models. Computers & Chemical Engineering, 14(7), 751-767.
Cortes-Pena, Y., Kumar, D., Singh, V., & Guest, J. S. (2020). BioSTEAM: A fast and flexible platform for the design, simulation, and techno-economic analysis of biorefineries under uncertainty. ACS Sustainable Chemistry & Engineering, 8(8), 3302-3310. https://doi.org/10.1021/acssuschemeng.9b07040
See also
Pinch analysis and targets applies the problem table to a real system; Anatomy of a synthesized network walks through a synthesized network exchanger by exchanger; API Reference documents every public name.