HeatExchangerNetwork#

HeatExchangerNetwork is a BioSTEAM facility that runs a pinch analysis over the heating and cooling utilities of a whole system, synthesizes a network of process heat exchangers that meets part of those duties by stream-to-stream exchange – at the minimum energy requirement (MER) targets whenever it finds such a network without stream splits, or, with stream_splitting=True, with stream splits where MER needs them – and reports the utility loads and capital cost that result. The original units, streams and heat exchangers are left untouched: the stream copies and synthesized exchangers live in a separate flowsheet named <sys>_HXN. See Tutorial for a worked example and Key Concepts for the method. targets() returns the targets of the facility’s streams (a PinchTargets: utilities, area, units, capital and total annualized cost) without synthesizing anything, and network_report() compares the synthesized network with them (Targeting and supertargeting (hensmith.targeting)).

class hensmith.HeatExchangerNetwork(ID='', T_min_app=5.0, units=None, ignored=None, Qmin=0.001, force_ideal_thermo=False, cache_network=False, avoid_recycle=False, acceptable_energy_balance_error=None, replace_unit_heat_utilities=False, sort_hus_by_T=False, stream_splitting=False, approach_contributions=None)[source]#

Bases: Facility

Create a HeatExchangerNetwork object that will perform a pinch analysis on the entire system’s heating and cooling utility objects. The heat exchanger network reduces the heating and cooling utility requirements of the system and may add additional capital cost.

Parameters:
  • ID (str) – Unique name for the facility.

  • T_min_app (float) – Minimum approach temperature observed during synthesis of heat exchanger network: the default approach temperature, required of every pair of streams unless approach_contributions are given.

  • units (Iterable[Unit], optional) – All unit operations available to the heat exchanger network. Defaults to all unit operations in the system.

  • stream_splitting (bool, optional) – Allow a process stream to be split into parallel branches that re-join. A side of the pinch that no unsplit network serves at the minimum energy requirement (a pinch-rule proof, or an unsplit search that leaves a utility penalty) is planned with splits instead, and then reaches the MER targets exactly on the planner’s knots (see Notes, “Stream splitting”). Sides that an unsplit network serves are never split, so a problem that needs no split gets the same network as with the default. The branches are realized with biosteam.Splitter chains and rigorous biosteam.Mixer units, which are adiabatic and cost nothing; their structure is reported in info['splits'] (synthesize_network) and synthesis_info['splits'] (HeatExchangerNetwork). With avoid_recycle, a split that would repeat a stream pair is not used, and MER is then not guaranteed. Defaults to False.

  • approach_contributions (Mapping or callable, optional) – Approach temperature contribution of each process stream [K]: a match between hot stream h and cold stream c must keep c_h + c_c instead of T_min_app, and a stream without one contributes T_min_app / 2 (see Notes, “Approach temperature contributions”). A mapping {key: value} whose keys name units (a unit, its ID if it has no owner, or (owner.ID, unit.ID); the nearest key along a unit’s chain of owners applies), or a callable f(hu) returning the value of a heat utility. A value is None or nan (unspecified) or a finite number > 0. A sequence raises a TypeError: the order of the facility’s streams is internal. May be changed between simulations. Defaults to None (every stream contributes T_min_app / 2).

Notes

Unless stream_splitting, the network is synthesized without stream splits by synthesize_network(): a problem table on the streams’ temperature-enthalpy curves gives the minimum energy requirement (MER) targets, and a planner builds each side of the pinch from the pinch outward [1] [2], keeping each pair’s required approach (T_min_app by default) everywhere inside every exchanger on the exact stream states. It reaches the targets whenever its search finds an unsplit network that does; the same pair of streams may then be matched more than once (IDs with a suffix _<n>, e.g. HX_3_2_cs_2), since series alternation can replace a split. Where the pinch design rules prove that MER needs stream splitting, the network is a best-effort one close to the targets, or, with stream_splitting, one that splits streams to reach them. The outcome is recorded in synthesis_info (a dict; see the info keyword of synthesize_network): ‘status’ is ‘mer’ when the network’s utilities equal the targets and ‘best_effort’ otherwise, with the targets, the planned utilities and, per side of the pinch, any proof that a split is needed.

Stream splitting. With stream_splitting, a side of the pinch that no unsplit network serves at MER is planned with stream splits and reaches the targets exactly on the planner’s knots (see synthesize_network, Notes, for the guarantee and its limits). Each split is realized as a chain of biosteam.Splitter units, IDs Split_<stream>_<hs|cs> (with _<n> for the n-th split of the stream on that side, n >= 2, and _b<c> for the chain’s element c >= 2), and a rigorous biosteam.Mixer, ID Mix_<stream>_<hs|cs>[_<n>], listed in new_splitters and new_mixers (both empty without a split) and simulated in HXN_sys with the exchangers. They are adiabatic and add no cost. synthesis_info['splits'] holds the splits (StreamSplit: the branch fractions and the exchangers of every branch), and a split stream’s life cycle lists its branch stages with their branch and fraction.

Approach temperature contributions. With approach_contributions, each process stream contributes its own approach temperature c_j (T_min_app / 2 unless given), and every process exchanger keeps the sum of its two streams’ contributions instead of T_min_app (see synthesize_network, Notes); each stream’s utility exchanger gets its agent as usual. A key names a unit: the unit itself, the ID of a unit without an owner (e.g. 'D1'), or (owner.ID, unit.ID) for an auxiliary unit (e.g. ('D1', 'condenser'); a bare auxiliary ID names nothing). owner is the top owner, so the pair names every auxiliary of that ID under it: the heaters of two auxiliary flash drums of one unit, say, are told apart by keying the heater objects or the drums. A heat utility takes the value of the key nearest to its unit along the unit’s chain of owners, so 'D1' covers the condenser and the reboiler of column D1 and ('D1', 'condenser') overrides it for the condenser; a None or nan value is an explicit default that wins at its level like any other, 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.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 (self.heat_utilities = self.hx.heat_utilities), that unit: a biosteam.MultistageCompressor K1 builds its compressors and intercoolers anew whenever pr or n_stages changes, without an owner, and 'K1' still covers the intercoolers and ('K1', 'K1_H1') names one in every simulation (the bare 'K1_H1' names nothing, though it is the key of that intercooler outside the facility, see synthesize_network; user objects are not changed). ID keys are the robust form: a unit that rebuilds its auxiliary units at every simulation (e.g. biosteam.HXutilities) leaves a key holding an old auxiliary, 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 auxiliary that never had an owner (an old intercooler of K1) names none, and raises (heat utility keys are rejected, since BioSTEAM replaces them at every simulation). The keys and values are checked against every unit available to the network, its nested auxiliary units and the exchangers they hold through their heat utilities only, before the heat utilities are taken: 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, or not available to a heat exchanger network) has no effect, so that, e.g., Monte Carlo samples do not fail. A key of an auxiliary that a unit may build but does not hold in a simulation is valid too, and has no effect: an old auxiliary object of the unit that none took the place of, or the ID that biosteam.Unit.auxiliary gives an auxiliary under a name of the auxiliary_unit_names of the owner or of one of its auxiliaries, unless that place holds a unit of another ID: (owner.ID, 'name[k]') where the attribute name holds a list of auxiliaries (e.g. ('X1', 'heat_exchangers[2]') while a biosteam.HXutilities X1, which builds one exchanger per agent it needs, needs fewer), and (owner.ID, name) where it holds a single one (so ('X1', 'heat_exchangers') raises); or the ID that a biosteam.MultistageCompressor K1 gives its stage n, for any n >= 1 (('K1', 'K1_H3') while K1 has 2 stages). Not every unit gives its auxiliaries that ID: 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', ''). A callable is called once per heat utility at every simulation (also to check a cached network, and again if a failed cache synthesizes anew), in the order of the facility’s heat utilities: it should be pure. After a simulation, targets takes the values that simulation resolved instead of calling it again. synthesis_info reports the contributions as used by stream (‘approach_contributions’, in the order of original_heat_exchangers) and the smallest margin over each exchanger’s required approach (‘min_approach_margin’), and pinch_Ts holds each stream’s own pinch temperature.

Original system stream and heat exchanger objects are preserved. All stream copies and new HX objects can be found in a newly created flowsheet ‘<sys>_HXN’ where <sys> is the name of the system associated to the HeatExchangerNetwork object. Each stream passes its exchangers in series (where it splits, its branches run in parallel from a splitter chain to a mixer); the network is simulated as a System (HXN_sys) whose path follows the streams, with the loops that repeated matches can form torn and converged to a tight tolerance (every exchanger starts at its planned state, so the loops are at their fixed point after one pass).

With cache_network, a network is reused while the multiset of heat exchangers (each as often as it gives a stream: biosteam lists the heat utility of a unit in parallel once per unit, by Unit.parallel, which a reactor may set by its volume), stream_splitting and the explicit approach contributions (resolved per stream) are the same: each process exchanger keeps, as its enthalpy limit, the share of the stream’s duty it had at synthesis (on the stream that the plan serves completely on that side of the pinch; its partner transfers that share, but never past its own outlet, and takes the rest to its utility), and the utility exchangers bring every stream to its new outlet. The splitters keep their fractions, so a branch exchanger’s limit is its branch’s fraction of the whole stream’s. If the cached network does not reproduce the outlets, or its energy balance is off, the network is synthesized again.

Every utility exchanger is designed and costed by biosteam as usual. A stream that its process exchangers bring to its outlet (within 1e-9 of its duty: the residual of the enthalpy flashes) leaves its utility exchanger in the state it enters it, so that exchanger has exactly no duty and no cost instead of a spurious duty from re-flashing the stream.

The facility’s heat utilities are the new utilities less the original ones, summed by agent (a negative utility cost is a saving). With replace_unit_heat_utilities, each original heat utility takes the heat utility of its own stream’s utility exchanger instead, the utility costs of its unit and of that unit’s owner are reloaded, and the facility carries no heat utilities. The original data are given back before the network is costed again, so that the network is synthesized from the units’ own utilities whether or not the units were simulated again.

References

Examples

>>> import biosteam as bst
>>> bst.settings.set_thermo(['Water', 'Methanol', 'Glycerol'])
>>> feed1 = bst.Stream('feed1', flow=(8000, 100, 25))
>>> feed2 = bst.Stream('feed2', flow=(10000, 1000, 10))
>>> D1 = bst.ShortcutColumn('D1', ins=feed1,
...                     outs=('distillate', 'bottoms_product'),
...                     LHK=('Methanol', 'Water'),
...                     y_top=0.99, x_bot=0.01, k=2,
...                     is_divided=True)
>>> D1_H1 = bst.HXutility('D1_H1', ins = D1.outs[1], T = 300)
>>> D1_H2 = bst.HXutility('D1_H2', ins = D1.outs[0], T = 300)
>>> F1 = bst.Flash('F1', ins=feed2,
...                outs=('vapor', 'liquid'), V = 0.9, P = 101325)
>>> HXN = bst.HeatExchangerNetwork('HXN', T_min_app = 5.)
>>> sys = bst.System.from_units('sys', units=[D1, D1_H1, D1_H2, F1, HXN])
>>> sys.simulate()
>>> # See all results
>>> round(HXN.actual_heat_util_load/HXN.original_heat_util_load, 2)
0.82
>>> abs(HXN.energy_balance_percent_error) < 0.01
True
>>> HXN.synthesis_info['status']  # the utilities equal the MER targets
'mer'
>>> HXN.stream_life_cycles
[<StreamLifeCycle: Stream_0, cold
    life_cycle = [
            <LifeStage: <HXprocess: HX_0_2_hs>, H_in = 5.38e+06 kJ/hr, H_out = 4.24e+07 kJ/hr>
            <LifeStage: <HXutility: Util_0_hs>, H_in = 4.24e+07 kJ/hr, H_out = 6.92e+07 kJ/hr>
    ]>, <StreamLifeCycle: Stream_1, cold
    life_cycle = [
            <LifeStage: <HXprocess: HX_1_2_hs>, H_in = 0 kJ/hr, H_out = 5.05e+06 kJ/hr>
            <LifeStage: <HXprocess: HX_1_4_hs>, H_in = 5.05e+06 kJ/hr, H_out = 5.08e+06 kJ/hr>
            <LifeStage: <HXprocess: HX_1_3_hs>, H_in = 5.08e+06 kJ/hr, H_out = 2.3e+07 kJ/hr>
            <LifeStage: <HXutility: Util_1_hs>, H_in = 2.3e+07 kJ/hr, H_out = 2.79e+08 kJ/hr>
    ]>, <StreamLifeCycle: Stream_2, hot
    life_cycle = [
            <LifeStage: <HXprocess: HX_0_2_hs>, H_in = 4.52e+07 kJ/hr, H_out = 8.12e+06 kJ/hr>
            <LifeStage: <HXprocess: HX_1_2_hs>, H_in = 8.12e+06 kJ/hr, H_out = 3.07e+06 kJ/hr>
            <LifeStage: <HXutility: Util_2_cs>, H_in = 3.07e+06 kJ/hr, H_out = 1.14e+06 kJ/hr>
    ]>, <StreamLifeCycle: Stream_3, hot
    life_cycle = [
            <LifeStage: <HXprocess: HX_1_3_hs>, H_in = 2.04e+07 kJ/hr, H_out = 2.47e+06 kJ/hr>
            <LifeStage: <HXutility: Util_3_cs>, H_in = 2.47e+06 kJ/hr, H_out = 2.47e+06 kJ/hr>
    ]>, <StreamLifeCycle: Stream_4, hot
    life_cycle = [
            <LifeStage: <HXprocess: HX_1_4_hs>, H_in = 7.51e+05 kJ/hr, H_out = 7.18e+05 kJ/hr>
            <LifeStage: <HXutility: Util_4_cs>, H_in = 7.18e+05 kJ/hr, H_out = 7.18e+05 kJ/hr>
    ]>]

Two hot liquids that reach the pinch against one cold liquid break the number rule above it (case rtB05 of the test suite), so no unsplit network reaches the MER targets; with stream_splitting, the cold stream is split into two branches, one per hot stream:

>>> bst.main_flowsheet.set_flowsheet('two_hot_one_cold')
>>> bst.settings.set_thermo(['Water', 'Ethanol'], cache=True)
>>> def process_stream(ID, T_in, T_out, P, **kmol_hr):
...     inlet = bst.Stream(ID + '_in', T=T_in, P=P, phase='l',
...                        units='kmol/hr', **kmol_hr)
...     return bst.HXutility(ID, ins=inlet, T=T_out, rigorous=False)
>>> units = [process_stream('H1', 400., 320., 5e5, Water=60.),
...          process_stream('H2', 390., 330., 5e5, Ethanol=30.),
...          process_stream('C1', 310., 410., 5e5, Water=150.),
...          process_stream('H3', 360., 300., 101325., Water=200.)]
>>> HXN = bst.HeatExchangerNetwork('HXN', T_min_app=10.)
>>> sys = bst.System.from_units('sys', units=[*units, HXN])
>>> sys.simulate()
>>> HXN.synthesis_info['status']
'best_effort'
>>> HXN.stream_splitting = True
>>> sys.simulate()
>>> HXN.synthesis_info['status']
'mer'
>>> [u.ID for u in HXN.new_splitters], [u.ID for u in HXN.new_mixers]
(['Split_0_hs'], ['Mix_0_hs'])
>>> [hx.ID for hx in HXN.new_HXs]
['HX_0_2_hs', 'HX_0_3_hs', 'HX_1_0_cs']
>>> HXN.synthesis_info['splits']
[<StreamSplit Split_0_hs: stream 0 above, 2 branches (0.5201, 0.4799)>]

With approach temperature contributions of 2 K for C1 and H3 (the other streams keep T_min_app / 2 = 5 K), C1 needs 7 K against H1 and H2 but only 4 K against H3, and each exchanger’s dT guard is its pair’s requirement less 1e-6 K:

>>> HXN.stream_splitting = False
>>> HXN.approach_contributions = {'C1': 2., 'H3': 2.}
>>> sys.simulate()
>>> HXN.synthesis_info['status']
'best_effort'
>>> [round(hx.dT, 6) for hx in HXN.new_HXs]
[6.999999, 6.999999, 6.999999, 6.999999, 6.999999, 3.999999]
HeatExchangerNetwork.plot_pinch_diagram(file=None, **kwargs)[source]#

Draw the pinch diagram of the synthesized network; see plot_pinch_diagram() for the keyword arguments. Returns the matplotlib figure and axes.

HeatExchangerNetwork.targets(T_min_app=None)[source]#

The pinch analysis targets of the facility’s process streams (see hensmith.targeting.PinchTargets): energy, utilities, area, units, capital and total annualized cost.

Parameters:

T_min_app (float, optional) – Minimum approach temperature [K], finite and >= 0 (> 0 with approach contributions). Defaults to the facility’s T_min_app.

Returns:

The targets, with the facility’s approach_contributions and force_ideal_thermo, and its system, whose operating hours (if it has them, a TEA’s for instance) PinchTargets.summary and hensmith.targeting.supertarget then use by default, as hensmith.targeting.network_report does. Their key_units are the units that the facility’s approach contribution keys may name, so that a film coefficient key (h=) of a unit without a stream in a simulation has no effect, as such an approach contribution key has none.

Return type:

PinchTargets

Raises:

RuntimeError – If the facility has neither units nor a system (it learns its system when the system is first simulated).

Warns:
  • RuntimeWarning – After a simulation, if a stream of the network is that of an exchanger no longer among the facility’s units and their nested auxiliaries (rebuilt since the facility’s last simulation, see Notes): the targets are of its former state.

  • RuntimeWarning – After a simulation, if an exchanger gives more or fewer streams than the facility’s units list its heat utility now: a unit in parallel whose number (Unit.parallel, which a reactor may set by its volume) has changed since, simulated alone (see Notes). The targets keep a stream per unit of the facility’s last simulation.

  • RuntimeWarning – If the first inlet and outlet of a stream’s exchanger differ in flow (ins[0].mol and outs[0].mol) by more than 1 % of the larger: its inlet has changed since its unit was simulated (see Notes), and the targets take the stream from that new inlet to the outlet of the former state, a state of no process.

Notes

After a simulation, the streams are those of original_heat_exchangers, in the network’s stream order, at the units’ current states, each hot or cold by its current direction (a stream that has reversed since the synthesis, its unit simulated alone or the network reused by cache_network, changes sides; one without duty keeps its side): right after a synthesis, the energy targets at the facility’s T_min_app equal synthesis_info['Q_hot_target'] and synthesis_info['Q_cold_target'] bit for bit. The targets’ heat_utilities are original_heat_utils, which with replace_unit_heat_utilities hold the network’s utilities, not the streams’ (see hensmith.targeting.PinchTargets). A mapping of approach contributions is resolved again by the units of original_heat_utils, so a mapping changed since applies (an auxiliary that a unit held without an owner at the synthesis, such as an intercooler of a biosteam.MultistageCompressor K1, is still that unit’s, as are the targets’ film coefficient keys, even after K1 has rebuilt its stages: K1 simulated alone, or a later sample of stored targets; the targets keep that relation); a callable is not called again (with replace_unit_heat_utilities, the heat utilities it would read hold the network’s utilities by then): its values are those the last simulation resolved, and a callable set since takes effect at the next simulation. Before any simulation, the streams are those of the heat utilities the next simulation would take, prepared as the synthesis prepares them, and a callable is called once per heat utility. Nothing is stored: with cache_network, the targets follow the units’ current states, not those of the network’s synthesis. A unit that rebuilds its auxiliaries when simulated alone (K1 above, after a change of its pr) leaves the streams on its former stages, which are no longer in the process and keep their former states, until the system is simulated again: the targets are then of those former states, not of the process now, with a RuntimeWarning (hensmith.targeting.network_report raises meanwhile). Each stream runs from its exchanger’s inlet now to its outlet as its unit last left it: after a feed or another stream changes, simulate the system before targeting (or the units alone, which gives the targets of the process now, unless a unit in parallel changes its number of units: biosteam lists its heat utilities once per unit, and the streams stay one per unit at the facility’s last simulation, with a RuntimeWarning; hensmith.targeting.network_report raises). A stream whose inlet has changed in flow meanwhile warns (see Warns); a change of temperature alone keeps the flows equal and warns of nothing, its targets being right only if its unit sets its outlet by temperature (as a biosteam.HXutility with T does).

Examples

The liquids of the second example of the class at 10 K (whose unsplit network falls short of the energy targets):

>>> import biosteam as bst
>>> bst.main_flowsheet.set_flowsheet('HXN_targets_example')
>>> bst.settings.set_thermo(['Water', 'Ethanol'], cache=True)
>>> def process_stream(ID, T_in, T_out, P, **kmol_hr):
...     inlet = bst.Stream(ID + '_in', T=T_in, P=P, phase='l',
...                        units='kmol/hr', **kmol_hr)
...     return bst.HXutility(ID, ins=inlet, T=T_out, rigorous=False)
>>> units = [process_stream('H1', 400., 320., 5e5, Water=60.),
...          process_stream('H2', 390., 330., 5e5, Ethanol=30.),
...          process_stream('C1', 310., 410., 5e5, Water=150.),
...          process_stream('H3', 360., 300., 101325., Water=200.)]
>>> HXN = bst.HeatExchangerNetwork('HXN', T_min_app=10.)
>>> sys = bst.System.from_units('sys', units=[*units, HXN])
>>> sys.simulate()
>>> HXN.synthesis_info['status']
'best_effort'

The targets [kW] are the synthesis’s own, bit for bit:

>>> targets = HXN.targets()
>>> targets.labels
['C1', 'H3', 'H1', 'H2']
>>> round(targets.Q_hot / 3600, 3), round(targets.Q_cold / 3600, 3)
(102.821, 209.035)
>>> info = HXN.synthesis_info
>>> targets.Q_hot == info['Q_hot_target'], targets.Q_cold == info['Q_cold_target']
(True, True)

At another minimum approach temperature:

>>> HXN.targets(20.)
<PinchTargets: 4 streams, T_min_app = 20 K, Q_hot = 4.834e+05 kJ/hr, Q_cold = 8.658e+05 kJ/hr>

Constructor options#

In addition to ID, T_min_app (the default pair requirement: each stream contributes T_min_app / 2 unless given its own contribution with approach_contributions) and units documented above, the constructor accepts the following options; Configuring the network and a larger system shows what each of them changes.

Option

Type

Meaning

ignored

Iterable[Unit] or callable

Units whose heat utilities are excluded from the analysis; a callable is evaluated at simulation time. Defaults to None.

Qmin

float, kJ/hr

Planned exchangers with a duty below this are dropped and their duty left to the utilities (a large value can cost MER), and utility exchangers at or below it are not marked on the pinch diagram. Defaults to 1e-3.

force_ideal_thermo

bool

Run the analysis on stream copies with ideal thermodynamics; the synthesized exchangers inherit that thermo. Defaults to False.

cache_network

bool

Reuse the network configuration of the previous simulation when the units contributing heat utilities (each as often as it gives a stream, as a unit in parallel does once per unit), stream_splitting and the explicit approach contributions (resolved per stream) are unchanged, updating only stream states and exchanger specifications: each process exchanger keeps the fraction of its stream’s duty at which its enthalpy limit sat at synthesis (the splitters keep their branch fractions), and the utility exchangers bring every stream to its new outlet. The reused network is not planned again, so it need not be at MER for the new duties. Defaults to False.

avoid_recycle

bool

Never match the same hot/cold stream pair twice anywhere (on one side of the pinch or across the two), so that no two exchangers connect the same pair and form a recycle loop; this forbids the repeated matches some unsplit MER networks need. Defaults to False.

acceptable_energy_balance_error

float

When given, sets an instance attribute that overrides the class default of 0.02 (see below). Defaults to None, i.e. the class value is used.

replace_unit_heat_utilities

bool

Copy each synthesized utility exchanger’s heat utility onto the corresponding original heat utility and reload that unit’s utility cost, instead of reporting the net utilities on the facility itself. Applies only when at least one process exchanger was synthesized. Defaults to False.

sort_hus_by_T

bool

Sort the heating utilities by inlet temperature descending and the cooling utilities ascending before the analysis, so that inlet temperature rather than signed duty (the default: smallest heating duty first, largest cooling duty first) sets the stream indices, which break ties in the planner’s search. Defaults to False.

stream_splitting

bool

Allow a process stream to be split into parallel branches that re-join. A side of the pinch that no unsplit network serves at MER is planned with splits and reaches the targets exactly on the planner’s knots; sides that an unsplit network serves are never split, so a problem that needs no split gets the same network as with the default. Each split is a chain of Splitter units and a rigorous Mixer (adiabatic, no cost), listed in new_splitters and new_mixers. With avoid_recycle, a split that would repeat a stream pair is not used, and MER is then not guaranteed. Stored as an attribute of the same name, and part of the cache_network key. Defaults to False.

approach_contributions

Mapping or callable

Approach temperature contribution of each process stream, in K: a match between hot stream h and cold stream c must keep \(c_h + c_c\) instead of T_min_app everywhere inside it, and a stream without a contribution contributes T_min_app / 2; the utility exchangers keep BioSTEAM’s own approach rule. A mapping’s keys name units – a unit, the ID of a unit without an owner ('D1'), or (owner.ID, unit.ID) for an auxiliary unit (('D1', 'condenser'); with the top owner, so it names every auxiliary of that ID under it) – and a heat utility takes the value of the key nearest to its unit along the unit’s chain of owners (two keys of the same level must agree); ID keys stay valid when a unit rebuilds its auxiliaries, and heat utility keys are rejected (TypeError). A callable f(hu) is called once per heat utility at every simulation and should be pure; a sequence raises a TypeError. A value is None or nan (unspecified, an explicit default at its level) or a finite number > 0. Keys and values are checked against every available unit and its auxiliaries before any filtering, so an unknown key, a bad value or a conflict raises a ValueError even while its unit is idle, but a valid key whose unit holds no heat utility in a simulation has no effect, as does a key of an auxiliary that its unit may build but does not build in that simulation (('X1', 'heat_exchangers[2]') of a biosteam.HXutilities that needs fewer exchangers, say, or ('K1', 'K1_H3') of a biosteam.MultistageCompressor K1 with 2 stages). The resolved values are part of the cache_network key. May be changed between simulations. Defaults to None (every stream contributes T_min_app / 2, so every pair requires T_min_app).

Class attributes#

Defaults shared by every instance; assigning to an instance overrides the value for that instance only.

Attribute

Type

Meaning

ticket_name = 'HXN'

str

Default ID of an unnamed instance ('HXN'; facilities are not auto-numbered).

acceptable_energy_balance_error = 0.02

float (fraction)

Fraction (0.02 = 2 %) absolute energy balance error above which the simulation warns (or raises); a cached network exceeding it is discarded and resynthesized.

raise_energy_balance_error = False

bool

Raise a RuntimeError instead of warning when the energy balance error exceeds the tolerance above.

network_priority = -2

int

Facility ordering key; facilities are simulated in ascending order of this value, so the network runs before the utility facilities.

Attributes set by simulation#

The following attributes are set at the end of every simulation. They carry no docstrings of their own, so autodoc cannot list them; each per-stream list or array is indexed by the stream index of the rearranged utility list (see synthesize_network()). With cache_network=True the attributes that describe the synthesized topology are kept from the synthesis that produced the cached network.

Attribute

Type

Meaning

original_heat_util_load

float, kJ/hr

Total duty of the original heating utilities, before integration.

actual_heat_util_load

float, kJ/hr

Total heating duty of the synthesized utility exchangers, after integration.

original_cool_util_load

float, kJ/hr

Total magnitude of the duty of the original cooling utilities, before integration.

actual_cool_util_load

float, kJ/hr

Total magnitude of the cooling duty of the synthesized utility exchangers, after integration.

energy_balance_percent_error

float, %

Percent deviation from one of the ratio (twice the duty of each process exchanger, plus the new utility duties weighted by their agents’ heat-transfer efficiency) / (the original utility duties weighted the same way), as computed in _cost.

synthesis_info

dict

The synthesis report (see the info keyword of synthesize_network()): 'status' is 'mer' when the network’s utilities equal the MER targets and 'best_effort' otherwise; next to it the targets, the planned and realized utilities, the penalty, per side of the pinch any proof that a split is needed, the smallest approach inside any process exchanger ('min_approach') and the smallest margin of an exchanger’s approach over its pair’s required approach ('min_approach_margin', no less than -1e-6 K, the exact check’s rounding guard), and each stream’s approach contribution as used ('approach_contributions', T_min_app / 2 where none was given, in stream order). With stream_splitting, also 'stream_splitting', 'splits' (the realized splits, StreamSplit), 'split_deviations' and, per side, the split candidate chosen ('split'). Kept from the synthesis that produced a cached network.

stream_life_cycles

list[StreamLifeCycle]

Ordered sequence of exchangers each stream passes through, aligned with original_heat_exchangers; a split stream’s life cycle also lists its splits and marks each branch stage with its branch and flow fraction.

new_HXs

list[HXprocess]

All synthesized process exchangers, the hot-side ones followed by the cold-side ones.

new_HXs_hot_side

list[HXprocess]

Process exchangers of the hot-side (above-pinch) design, in plan order (from the pinch outward), IDs HX_<cold>_<hot>_hs.

new_HXs_cold_side

list[HXprocess]

Process exchangers of the cold-side (below-pinch) design, in plan order, IDs HX_<hot>_<cold>_cs; on either side the n-th exchanger of a repeated pair gets the suffix _<n>.

new_HX_utils

list[HXutility]

One rigorous utility exchanger per stream, bringing it from its last process exchanger (or its inlet, if it was not matched) to its outlet enthalpy.

new_splitters

list[Splitter]

The splitters of the network’s stream splits, every split’s chain in order (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 >= 2); empty without a split, and always without stream_splitting.

new_mixers

list[Mixer]

One rigorous, adiabatic mixer per stream split, where its branches re-join (IDs Mix_<stream>_<hs|cs>, with _<n> as for the splitters); empty without a split.

original_heat_exchangers

list[Unit]

The original heat exchangers behind the analyzed heat utilities, in stream order.

original_heat_utils

list[HeatUtility]

The original heat utilities rearranged into stream order, so that they align with stream_life_cycles.

HXN_sys

System

The system built from the synthesized exchangers (and the splitters and mixers of any stream split), named <sys>_HXN and registered in HXN_flowsheet; converged and summarized during costing.

HXN_flowsheet

Flowsheet

The flowsheet <sys>_HXN holding the network’s stream copies and exchangers.

pinch_Ts

ndarray, K

Per-stream pinch temperature (informational): the shifted pinch plus the stream’s shift (T_min_app for a hot stream and 0 for a cold one by default; T_min_app / 2 + c_j for a hot and T_min_app / 2 - c_j for a cold stream with approach contributions) when the stream crosses it, else its inlet temperature (inlet already past the pinch, or an isothermal or non-monotone stream) or its outlet temperature (stream ending before the pinch).

inlet_Ts

ndarray, K

Inlet temperature of each stream.

outlet_Ts

ndarray, K

Outlet temperature of each stream, after quenching the outlet to equilibrium at its own enthalpy.

streams_inlet

list[Stream]

One copy of each stream’s inlet, in stream order, as prepared for the analysis; the synthesis works on further copies, so these keep their inlet state.

stream_HXs_dict

dict[int, list[Unit]]

Exchangers that each stream index passes through: its process exchangers in flow order, then its utility exchanger. Where the stream splits, the order is topological: the exchangers before the split, those of its branches (branch by branch, each in flow order), then those after it.

cold_indices

list[int]

Stream indices of the heated (cold) streams.

original_purchase_costs

list[float], USD

Purchase cost of each original heat exchanger.

new_purchase_costs_HXp

list[float], USD

Purchase cost of each synthesized process exchanger, indexed like new_HXs.

new_purchase_costs_HXu

list[float], USD

Purchase cost of each synthesized utility exchanger, indexed like new_HX_utils.

original_utility_costs

list[HeatUtility]

The original heat utilities summed by agent, with their duties reversed in sign so that they net against the new ones.

new_utility_costs

list[HeatUtility]

The heat utilities of the synthesized utility exchangers, summed by agent.