Targeting and supertargeting (hensmith.targeting)#
hensmith.targeting says what a heat exchanger network of a set of
process streams can achieve, before (or instead of) synthesizing one: the
energy targets bound every network, the others are estimates.
PinchTargets holds the problem table of the streams and derives from
it the composite and grand composite curves, the targets of several utilities
priced by biosteam (UtilityTarget), the balanced
composite curves, and the targets of area, units, capital cost and total
annualized cost. supertarget() sweeps the minimum approach temperature
for the least total annualized cost and returns Supertargets;
network_report() compares a synthesized network with its targets; and
plot_composite_curves(), plot_grand_composite_curve() and
plot_supertargets() draw them. A facility’s targets come from
HeatExchangerNetwork.targets(). Nothing here changes the synthesis.
Targeting and supertargeting shows every function on the quickstart system,
and Key Concepts explains the methods and their limits.
- class hensmith.PinchTargets(streams_inlet, streams_quenched, is_hot, T_min_app=5.0, approach_contributions=None, labels=None, curves=None)[source]#
Bases:
objectPinch analysis targets of a set of process streams: the problem table (hensmith.hxn_synthesis.problem_table) and what follows from it, the composite and grand composite curves, the targets of several utilities (utility_targets) priced by biosteam, the balanced composite curves, and the targets of area, units, capital cost and total annualized cost (summary). Build it from heat utilities with from_heat_utilities (as hensmith.hxn_synthesis.synthesize_network prepares them), from a facility with HeatExchangerNetwork.targets, or from streams as problem_table takes them.
- Parameters:
streams_inlet (list[Stream]) – Inlet of each process stream.
streams_quenched (list[Stream]) – Corresponding outlets, re-flashed at their enthalpy.
is_hot (Sequence[bool]) – True where the stream is cooled, False where it is heated (a ValueError is raised for a stream with duty marked otherwise).
T_min_app (float, optional) – Minimum approach temperature [K], a finite number >= 0 (> 0 with approach contributions), or a ValueError is raised. Defaults to 5.
approach_contributions (Sequence[float or None], optional) – Approach temperature contribution of each stream [K], in stream order, as problem_table takes them: None or nan (unspecified:
T_min_app / 2), or a finite number > 0 (T_min_app must then be finite and > 0 too). All unspecified is the same as None.labels (list[str], optional) – Display name of each stream, which the curves built here also take (to name their streams in their warnings). Defaults to the IDs of streams_inlet.
curves (list[StreamCurve], optional) – Prebuilt temperature-enthalpy curves of the streams (built here if not given).
- approach_contributions#
Approach temperature contribution of each stream [K] in stream order (None:
T_min_app / 2), or None if no stream has one.
- is_hot#
(N,) bool, True for hot (cooled) streams.
- Type:
- streams_inlet, streams_quenched
The process streams.
- Type:
list[Stream]
- curves#
Temperature-enthalpy curve of each stream (independent of T_min_app).
- Type:
list[StreamCurve]
- table#
The problem table.
- Type:
- grid#
The table’s grid: ‘Ts’, its shifted temperatures [K] (descending, n); ‘shift’, as shift; ‘contributions’, each stream’s approach temperature contribution [K] (N); ‘Hl’ and ‘Hr’, (N, n) each stream’s enthalpy [kJ/hr] at every grid temperature, the left (low enthalpy) and right (high enthalpy) limit, which differ only at a point load of the stream; ‘k_hi’ and ‘k_lo’, (N,) the grid indices of each stream’s own upper and lower temperature.
- Type:
- shift#
(N,) shift of each stream [K]: its real temperature at shifted temperature T* is
T* + shift[j].- Type:
- Q_hot, Q_cold
Minimum hot and cold utility [kJ/hr], process side (the table’s hot_util_load and cold_util_load).
- Type:
- cut#
‘below’ or ‘above’: the side of the pinch to which the point loads at the pinch temperature belong. It is ‘below’ when the zero heat flow of the cascade is the flow arriving at pinch_T (before those point loads), ‘above’ when it is the flow leaving it (after them), and always ‘below’ for a threshold problem with no hot utility.
- Type:
- heat_utilities#
The heat utilities the streams were taken from, in stream order; None when built from streams. For HeatExchangerNetwork.targets after a simulation they are the facility’s original_heat_utils, which with replace_unit_heat_utilities hold the network’s utilities (the streams are read from the units).
- Type:
list[HeatUtility] or None
- units#
The unit of each stream (
hu.unit), in stream order; None when built from streams.- Type:
list[Unit] or None
- key_units#
Further units that keys of h may name (with their owners) without a stream of theirs, a key that names only such units then having no effect: for HeatExchangerNetwork.targets, every unit available to the network, its nested auxiliary units and the exchangers they hold through their heat utilities only, which approach contribution keys may name; for from_heat_utilities, its key_units and theirs; else None (settable). An auxiliary that one of them, or a unit of units, holds without an owner when the targets are made (for HeatExchangerNetwork.targets, at the network’s synthesis) stays that unit’s for keys and labels, as the facility takes it (see Notes of HeatExchangerNetwork), even once the unit has rebuilt its auxiliaries.
- Type:
list[Unit] or None
- force_ideal_thermo#
Whether the stream copies use ideal thermodynamics (False when built from streams).
- Type:
- system#
The system of the facility of HeatExchangerNetwork.targets, else None (settable). If it has operating hours, summary (and so supertarget) uses them unless operating_hours or tea is given.
- Type:
System or None
Notes
Composite curves. composite_curves adds up the streams on the table’s own grid, whose knots hold every breakpoint of every stream, so the composites reproduce the table to rounding: the cold composite starts at the cold utility target and ends the hot utility target beyond the end of the hot one. On the shifted scale the composites touch at the pinch (if there is hot utility). On the real scale they are at least T_min_app apart, or, with approach contributions, at least the smallest hot and the smallest cold contribution apart (unspecified ones
T_min_app / 2); the shifted composites then carry the pinch.Grand composite curve. grand_composite_curve is the heat cascaded through each shifted grid temperature with the minimum hot utility, both as it arrives (before the point loads at that temperature) and as it leaves (after them). It is non-negative and zero at the pinch.
Multiple utilities. utility_targets places biosteam’s agents on the grand composite curve, heating and cooling separately. biosteam lets a heating agent serve real temperatures below
R = agent.T - agent.dT - dTand a cooling agent real temperatures aboveR = agent.T + agent.dT + dT(dT = bst.HeatUtility.dT, read at call time). On the shifted scale a stream with shift s may take an agent’s heat only up toR - s(down to it, for cooling); the agent’s level is the highest single level, up to the highest of these limits, that serves no stream above its own limit (the lowest, down to the lowest limit, for cooling):Rfor heating andR - T_min_appfor cooling without approach contributions, which is biosteam’s rule. A level serves only what lies below it (above it, for cooling) and above the highest zero of the curve balanced with the agents already placed (below the lowest one): the pinch, a utility pinch or another process pinch, which its heat never passes. So a stream reaching beyond its limit bounds the level at that limit, one lying wholly beyond it only at its own end (just short of it, if it has a point load there), and one with no heat on the level’s side of that zero not at all. With contributions the level can be conservative for streams with slack. Non-fuel agents are placed in list order (biosteam’s first feasible agent: for the default lists the lowest steam and the warmest coolant first), each as much as the curve above its level (below it, for cooling) allows given the agents already placed; what remains goes to the first fuel agent, or is left unserved with a RuntimeWarning (nan cost). The order is biosteam’s preference, not price: the utility cost target is the cost of the agents 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, which serves each stream with one agent at its outlet, can cost less than the target as well as more (see Notes of network_report). A level equal to a grid temperature serves that temperature’s point loads (biosteam’s own rule is strict; they differ only at exact equality), except where a sensible agent’s line would have no driving force left: at a point load atagent.T - dT(agent.T + dTfor cooling), a stream’s limit whenagent.dTis 0 (UtilityAgent’s default), the agent would leave at its 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 a stream that ends at that temperature bounds the level of such an agent at that end, as one reaching beyond its limit does: were another stream (of another approach contribution) to set the level beyond that end, the agent would have given heat above it (taken it below, for cooling) and its line would reach its inlet temperature there; at the level it has given none.Each agent is priced as one biosteam heat utility over the stretch it serves, from its level (or the end of the grid, if that is nearer) to the next zero of the balanced curve (the pinch, or a utility pinch). Its T_in and T_out are the real temperatures of the streams with heat in that stretch (the cold streams for heating, the hot ones for cooling): 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, as the balanced curve does: a boiler at the pinch with the cut ‘above’, or a condenser at it with the cut ‘below’, is served at its saturation temperature. They are the stretch’s ends without approach contributions (where a stream reaches them), and T_out is within the agent’s limit. biosteam prices a latent agent and fuel from them (the flue leaves at least
dTabove T_in), but checks a sensible agent’s outlet only against T_in, while its line must keepdTfrom the process over the whole stretch (cooling water serving a condenser at the cold end of its stretch must leavedTbelow the condensing temperature). At each position p of the stretch, the agent takes at leastx = q - B(p)of its duty q below p (gives it above p, for heating), B being the balanced heat flow at p. Its outlet is therefore at mostagent.T + min_p (T(p) - dT - agent.T) * q / xfor cooling and at leastagent.T - min_p (agent.T - dT - T(p)) * q / xfor heating, T(p) being the lowest real temperature of the served hot streams at p (the highest of the cold ones): its line stays inside the grand composite curve, as a non-isothermal utility’s does in pinch analysis. biosteam is given the inlet that gives this outlet, and divides the duty by the agent’s heat transfer efficiency. Pricing uses fresh utility streams in a temporary flowsheet: costs are deterministic, and the flowsheet’s registry and the agents’ stream dumps are untouched.Balanced composite curves. balanced_composite_curves adds the utilities to the real composites (heating ones to the hot composite, cooling ones to the cold one), so both end at the same heat: a latent agent as a flat at its temperature, a sensible one as a line between its inlet and outlet temperatures, and fuel as a furnace at infinite temperature. An unserved remainder is left out: unserved cooling, the coldest member of the cold composite, as a gap at its start (which keeps the curves aligned), unserved heating as one at the end of the hot composite. balanced_grand_composite_curve is the cascade net of the utilities, zero at the process pinch and at every utility pinch.
Area target. area_target is Bath’s formula [Townsend84] on the balanced composite curves: vertical heat transfer, counter-current (a correction factor of 1), each stretch between breakpoints of either curve at its log-mean temperature difference,
A = sum_i (1 / LMTD_i) sum_j q_ij / h_j, over the real temperatures (with approach contributions too) and with a film coefficient h per stream and per utility (1 kW/m²/K by default, so a pair has biosteam’s heuristic overall coefficient of 0.5 kW/m²/K; condensing against boiling, where biosteam takes 1, is not reproduced). The sum is exact on the piecewise-linear curves, a stream’s share of a stretch being linear in the heat. Fuel takes no area, and the area is inf (with a RuntimeWarning) at a zero or negative driving force, as atT_min_app = 0.Units target. units_target applies Euler’s N = S - 1 to each region of the balanced grand composite curve between its zeros (the process pinch and the utility pinches), S counting the streams with heat in the region and the utilities acting in it. Where the curve stays zero, each piece between grid temperatures is balanced on its own: on each side of the pinch such pieces count S - 1 per group of them linked by common streams, so a stream across several counts once. It is an estimate, not a bound: it assumes one component and no loop per region, so a balanced subset of streams needs fewer units and each loop of matches one more.
Capital and total annualized cost. capital_cost_target takes as many shells as the units target (less one furnace for all fuel) and costs each with biosteam’s correlations at one pressure (biosteam takes each exchanger’s highest inlet pressure: its pressure factor is up to about 12 % higher for high pressure steam heaters): a double pipe below 150 ft² (for carbon steel at 1 atm and biosteam’s default CE of 567.5, the purchase cost jumps from about 5,486 USD just below 150 ft² to 20,138 USD for a floating head at 150 ft², and the installed cost, the one used here, from about 8,055 to 64,658 USD; the floating head correlation, not monotone below about 126 ft², is not used there), a shell and tube of any area above. Linnhoff and Ahmad divide the area target equally over the shells [Linnhoff90]: with a cost law of economies of scale, such as a + bA^c, that is the costliest way to divide it, and each added shell adds cost. biosteam’s jump breaks this: the equal division can bring every shell below 150 ft² although the area would fill some shell and tubes, so that a utility of a few watts that adds units would cut the target by most of it, where a network just adds a small exchanger and keeps its large ones. So when the equal division puts the N shells below 150 ft² and the area fills k = A // 150 ft² >= 1 shells of at least 150 ft², the target takes the costlier to install 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,305 USD installed). This is the least change to the equal division that never falls when a unit is added (or some area, except where biosteam’s correlations do: for shells whose cost rises faster than their area, beyond about 35,000 ft² (3,300 m²) for the default floating head of carbon steel and from about 15,000 ft² by type and material, and for the ‘Double pipe’ heat_exchanger_type, cheaper at 150 ft² than below). Where it departs from the equal division it tends to be high, as networks keep only part of their area in shell and tubes (the equal division tends to be low there): an estimate, not a bound. The furnace is costed on the fuel’s utility duty at biosteam’s default furnace pressure of 500 psi (P sets the pressure of the shells only). summary adds the total annualized cost, the installed (bare module) cost times the capital recovery factor plus the utility cost times the operating hours: no Lang factor and no working capital.
Examples
One hot stream cooled 400 -> 300 K and one cold stream heated 330 -> 440 K, both with a heat capacity flow rate of 1 kW/K (1000 kmol/hr of a pseudo-component with a molar heat capacity of 3.6 J/mol/K):
>>> import biosteam as bst, thermosteam as tmo >>> from hensmith import PinchTargets >>> fluid = tmo.Chemical('Fluid', search_db=False, phase='l', MW=1., ... Cn=3.6, default=True) >>> bst.settings.set_thermo([fluid]) >>> bst.main_flowsheet.set_flowsheet('targeting_example') >>> H1 = bst.HXutility('H1', ins=bst.Stream('H1_in', Fluid=1000., T=400.), ... T=300., rigorous=False) >>> C1 = bst.HXutility('C1', ins=bst.Stream('C1_in', Fluid=1000., T=330.), ... T=440., rigorous=False) >>> H1.simulate(); C1.simulate() >>> hus = [H1.heat_utilities[0], C1.heat_utilities[0]] >>> targets = PinchTargets.from_heat_utilities(hus, T_min_app=10.) >>> targets <PinchTargets: 2 streams, T_min_app = 10 K, Q_hot = 1.800e+05 kJ/hr, Q_cold = 1.440e+05 kJ/hr> >>> targets.labels, targets.pinch_T (['C1', 'H1'], 390.0)
The composite curves [K, kW], on the knots of the table’s grid (340 K is the cold inlet and 390 K the hot inlet, each shifted to the other side): the cold one starts at the cold utility target, 40 kW, and ends 50 kW (the hot utility target) to the right of the hot one.
>>> hot_T, hot_H, cold_T, cold_H = targets.composite_curves() >>> hot_T, hot_H / 3600 (array([300., 340., 400.]), array([ 0., 40., 100.])) >>> cold_T, cold_H / 3600 (array([330., 390., 440.]), array([ 40., 100., 150.]))
The utility targets with biosteam’s default agents [kW]: low pressure steam serves the cold stream up to its level, 407.189 K, and medium pressure steam the rest; cooling water serves the hot stream down to its level (302.372 K shifted) and chilled water the rest.
>>> utilities = targets.utility_targets() >>> [(u.ID, round(u.duty / 3600, 3)) for u in utilities] [('low_pressure_steam', 17.189), ('medium_pressure_steam', 32.811), ('cooling_water', 27.628), ('chilled_water', 12.372)] >>> [(round(u.T_in, 3), round(u.T_out, 3)) for u in utilities] [(390.0, 407.189), (407.189, 440.0), (340.0, 312.372), (312.372, 300.0)] >>> round(sum(u.cost for u in utilities), 4) # USD/hr 1.656
The balanced composite curves end at the same heat:
>>> hot_T, hot_H, cold_T, cold_H = targets.balanced_composite_curves(utilities) >>> round(hot_H[-1] / 3600, 6), round(cold_H[-1] / 3600, 6) (150.0, 150.0)
The area target [m²] with film coefficients of 1 kW/m²/K, the units target (the balanced grand composite curve is zero at the utility pinches of low pressure steam and cooling water and from the pinch down to 330 K, which leaves five regions of two streams or utilities each), and the capital cost target [USD]. The 24.14 m² (260 ft²) fill one floating head, which with four shells of no area costs more to install than five equal double pipes of 52 ft² (about 34,000 USD), so the target takes it:
>>> round(targets.area_target(utilities), 2) 24.14 >>> targets.units_target(utilities) {'above': 2, 'below': 3, 'total': 5} >>> capital = targets.capital_cost_target(utilities) >>> capital['N_shells'], capital['N_area_shells'], capital['type'] (5, 1, 'Floating head') >>> round(capital['installed_cost']) 72819
All targets, with the total annualized cost [USD/yr] at 10 % interest over 10 years and 8000 operating hours a year:
>>> summary = targets.summary() >>> summary['shell_type'] 'Floating head' >>> {key: round(value, 2) for key, value in summary.items() ... if key != 'shell_type'} {'T_min_app': 10.0, 'Q_hot': 180000.0, 'Q_cold': 144000.0, 'utility_cost': 1.66, 'area': 24.14, 'N_units': 5, 'N_shells': 5, 'N_area_shells': 1, 'shell_area': 24.14, 'purchase_cost': 21054.85, 'installed_cost': 72819.1, 'annualized_capital_cost': 11850.97, 'operating_cost': 13247.77, 'TAC': 25098.74}
The same streams at another minimum approach temperature (the curves are reused):
>>> targets.at(20.) <PinchTargets: 2 streams, T_min_app = 20 K, Q_hot = 2.160e+05 kJ/hr, Q_cold = 1.800e+05 kJ/hr>
References
[Townsend84]Townsend, D. W., & Linnhoff, B. (1984). Surface area targets for heat exchanger networks. IChemE 11th Annual Research Meeting, Bath, UK.
[Linnhoff90]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.
- classmethod PinchTargets.from_heat_utilities(hus, T_min_app=5.0, approach_contributions=None, force_ideal_thermo=False, sort_hus_by_T=False, key_units=None)[source]#
Targets of the process streams behind heat utilities, prepared exactly as hensmith.hxn_synthesis.synthesize_network prepares them (so the targets equal the synthesis’s, bit for bit).
- Parameters:
hus (Iterable[HeatUtility]) – Heat utilities of heat exchangers (
hu.unit, whose first inlet and outlet are the process stream), such asbst.process_tools.heat_exchanger_utilities_from_units(sys.units)(those a heat exchanger network may take,hu.hxn_ok, as HeatExchangerNetwork selects them), notsys.heat_utilities(sums by agent, of no unit). A selection is valid until its units are simulated again: biosteam then gives each unit new heat utilities, emptying the former ones, so select them again after any simulation. Zero-duty ones are dropped. Each counts once per occurrence, a stream each, as in the synthesis: biosteam lists the heat utility of a unit in parallel (Unit.parallel, as the recirculation heat exchanger of a biosteam.AbstractStirredTankReactor, such as a fermentor, withparallel['self']reactors) once per unit in its owner’sheat_utilities, and so in this selection. After a simulation of a HeatExchangerNetwork withreplace_unit_heat_utilities=Truethey hold the network’s utilities, not the process duties, and those of the streams it serves completely are empty (left out of biosteam’s selection): take HeatExchangerNetwork.targets then, or build the targets before that simulation.T_min_app (float, optional) – Minimum approach temperature [K], finite and >= 0 (> 0 with approach contributions). Defaults to 5.
approach_contributions (Mapping, Sequence or callable, optional) – Approach temperature contributions [K] in any form synthesize_network takes (a sequence is aligned with hus as given, zero-duty heat utilities included). Defaults to None.
force_ideal_thermo (bool, optional) – Copy the streams with ideal thermodynamics. Defaults to False.
sort_hus_by_T (bool, optional) – Order the streams by inlet temperature, as the synthesis’s option. Defaults to False.
key_units (Iterable[Unit], optional) – Further units at hand, such as
sys.units: the keys of approach_contributions and of h (see area_target) may name them, their nested auxiliary units and the exchangers they hold through their heat utilities only (the targets’ key_units), whether or not they hold a heat utility of hus, as the keys of a HeatExchangerNetwork with these units may. A biosteam.MultistageCompressor K1 among them owns the stages it holds without an owner, so'K1'covers its intercoolers and('K1', 'K1_H1')names one (as a unit owns an exchanger it holds through its heat utilities only). Without them, only the units of hus and their owners are at hand, and'K1_H1'is that intercooler’s key. Defaults to None.
- Returns:
Streams in stream order (heating utilities, i.e. cold streams, first), labeled by unit (
'<owner> - <unit>'for an auxiliary unit, with its top owner as keys take it).- Return type:
- Raises:
ValueError – If a heat utility belongs to no unit, or is not (or no longer) among its unit’s
heat_utilities(of a selection made before its unit was simulated again), or one with duty is not a heat exchanger’s (hu.hxn_okFalse, as for the heat utilities that reactors, mixers or compressors add withUnit.add_heat_utility: their unit’s first inlet and outlet need not be the stream they heat or cool). If the heat utilities with duty of a unit are given more often than itsheat_utilitiesor those of any of its owners list them, as when selections from overlapping unit lists (such as a unit and an auxiliary unit of it) give the same heat utility twice: its stream would count once more per extra one. If the stream of a heat utility, from its unit’s inlet now to its outlet as last simulated, runs against the heat utility (cooled for a heating one, or heated for a cooling one): its inlet has changed since its unit was simulated (as the second warning below, given before this error, may say too).- Warns:
RuntimeWarning – If a heat utility is one that a HeatExchangerNetwork simulated with
replace_unit_heat_utilities=Truehas written its utilities over (and not given back yet, as its next simulation does), with a duty (hu.unit_duty) other than its process stream’s (outs[0].H - ins[0].Hof its unit, which the targets use) beyond rounding: the warning names that network, whose HeatExchangerNetwork.targets are the process’s. A duty that differs from its stream’s for another reason warns of nothing, the targets being the streams’ (the total condenser of a biosteam.MESHDistillation withfull_condenser=Truereports a duty about 0.1 % off the enthalpy change of its own streams, for instance).RuntimeWarning – If the first inlet and outlet of a heat utility’s unit differ in flow (
ins[0].molandouts[0].mol) by more than 1 % of the larger: its inlet has changed since its unit was simulated (a feed changed, its unit not simulated since), and the targets take its stream from that new inlet to the outlet of the former state, a state of no process: simulate the system (or the units) first, then select the heat utilities again. A change of temperature alone keeps the flows equal and warns of nothing: the targets are then of the new inlet and of the outlet as last simulated, which is the outlet’s state now if the unit sets it by temperature (as a biosteam.HXutility withTdoes).
- PinchTargets.at(T_min_app, scale_contributions=False)[source]#
The targets of the same streams at another minimum approach temperature, reusing their curves (no flash).
- Parameters:
T_min_app (float) – Minimum approach temperature [K], finite and >= 0 (> 0 with approach contributions).
scale_contributions (bool, optional) – Scale the specified approach contributions by
T_min_app / self.T_min_app(the contribution scaling of contribution-based supertargeting, Ahmad, Linnhoff & Smith 1990); by default they stay as they are. Unspecified ones areT_min_app / 2either way. Defaults to False.
- Returns:
With the same streams, curves, labels, units, key units, heat utilities and system.
- Return type:
- PinchTargets.composite_curves(shifted=False)[source]#
The hot and cold composite curves.
- Parameters:
shifted (bool, optional) – Return shifted temperatures (each stream’s real temperature minus its shift; the scale of the problem table) instead of real ones. Defaults to False.
- Returns:
hot_T, hot_H, cold_T, cold_H – Temperatures [K] and cumulative heat [kJ/hr] of each composite, ascending; the hot one starts at 0 and the cold one at the cold utility target. Empty if there is no stream of that kind.
- Return type:
- PinchTargets.grand_composite_curve()[source]#
The grand composite curve.
- Returns:
T (numpy.ndarray) – Shifted grid temperatures [K], descending, each twice.
H (numpy.ndarray) – Heat cascaded with the hot utility target [kJ/hr]: at each grid temperature the heat arriving (before its point loads) and then the heat leaving it (after them). It starts at the hot utility target and ends at the cold one, to rounding, and is zero at the pinch.
- PinchTargets.utility_targets(heating_agents=None, cooling_agents=None)[source]#
Multiple-utility targets: the agents placed on the grand composite curve and priced by biosteam (see Notes of the class). They are placed in biosteam’s order of preference, not by price, so their total cost is not the least utility cost of a network.
- Parameters:
heating_agents (Sequence[UtilityAgent or str], optional) – Heating agents (
isheating_agent) in order of preference, or the IDs of agents ofbst.HeatUtility.heating_agents; fuel agents (isfuel) take the remainder, the first one only. Defaults tobst.HeatUtility.heating_agents(read at call time).cooling_agents (Sequence[UtilityAgent or str], optional) – Cooling agents (
iscooling_agent, not fuels) in order of preference, or the IDs of agents ofbst.HeatUtility.cooling_agents. Defaults tobst.HeatUtility.cooling_agents.
- Returns:
The heating utilities in allocation order (fuel, or an unserved remainder, last), then the cooling ones. Agents with no duty are left out.
- Return type:
- Raises:
ValueError – If an ID names no agent of biosteam’s list, or an agent is not of the kind of its list.
- Warns:
RuntimeWarning – If no agent can serve part of the hot or cold utility target.
- PinchTargets.balanced_composite_curves(utilities=None)[source]#
The balanced composite curves (see Notes of the class).
- Parameters:
utilities (list[UtilityTarget], optional) – utility_targets of these targets, with any agents: a ValueError is raised unless the heating duties (unserved ones included) add up to Q_hot and the cooling ones to Q_cold, and the shift of each is None or these targets’ shift (the utilities of other targets, e.g. of another T_min_app, or a part of them do not balance the curves). Defaults to utility_targets with the default agents.
- Returns:
hot_T, hot_H, cold_T, cold_H – Real temperatures [K] and cumulative heat [kJ/hr] of each composite, ascending: the hot one from 0, with fuel as a last stretch at
T = inf, and the cold one from the unserved cooling (0 if none), the coldest of its members, which is left out. The curves are thus aligned for vertical heat transfer, and end at the same heat unless part of the heating target is unserved (the hot one then ends short of the cold one by it).- Return type:
- PinchTargets.balanced_grand_composite_curve(utilities=None)[source]#
The balanced grand composite curve: the grand composite curve net of the utilities.
- Parameters:
utilities (list[UtilityTarget], optional) – utility_targets of these targets, with any agents: a ValueError is raised unless the heating duties (unserved ones included) add up to Q_hot and the cooling ones to Q_cold, and the shift of each is None or these targets’ shift (the utilities of other targets, e.g. of another T_min_app, or a part of them do not balance the curves). Defaults to utility_targets with the default agents.
- Returns:
T (numpy.ndarray) – Shifted temperatures [K], descending: the grid temperatures (twice each, arriving and leaving heat) and the utility levels between them (twice each, just above and just below).
B (numpy.ndarray) – Heat cascaded net of the utilities [kJ/hr], non-negative: zero at the pinch and at every utility pinch, at the top the duty of the fuel and of levels above the grid, at the bottom that of cooling levels below it.
- PinchTargets.area_target(utilities=None, h=None, h_utilities=None, agents=None)[source]#
The area target [m²] of vertical heat transfer between the balanced composite curves: Bath’s formula (see Notes of the class).
- Parameters:
utilities (list[UtilityTarget], optional) – utility_targets of these targets, with any agents: a ValueError is raised unless the heating duties (unserved ones included) add up to Q_hot and the cooling ones to Q_cold, and the shift of each is None or these targets’ shift (the utilities of other targets, e.g. of another T_min_app, or a part of them do not balance the curves). Defaults to utility_targets with the default agents.
h (float, Sequence[float] or Mapping, optional) – Film coefficient of the streams [kW/m²/K]: one for all, one per stream in stream order, or a mapping from stream indices (int) or, for targets of heat utilities, keys that name units as approach contribution keys do (a unit, the ID of a unit without an owner, or
(owner.ID, unit.ID); an auxiliary unit takes the value of its owner’s key unless a key names it). The nearest key wins, a stream index first; streams left out take 1. Labels are not keys. A key that names no stream raises a ValueError, unless it names one of key_units (for HeatExchangerNetwork.targets, any unit available to the network, as for its approach contribution keys) or an auxiliary that a unit may build but does not hold now: it then has no effect, so that a key of a unit idle in a Monte Carlo sample does not fail. Defaults to 1 for every stream.h_utilities (float or Mapping, optional) – Film coefficient of the utilities [kW/m²/K]: one for all, or a mapping from agent IDs or agents (the others take 1). An ID must name an agent of utilities, of agents or of biosteam’s lists, or a ValueError is raised: key another agent by the UtilityAgent itself, or pass it in agents. Defaults to 1.
agents (Sequence[UtilityAgent or str], optional) – Agents whose IDs h_utilities may key as well, or the IDs of agents of biosteam’s lists (which it may key anyway): the heating_agents and cooling_agents that utilities were priced with, so that one h_utilities serves at every T_min_app, an agent that serves nothing at one included, as for summary. They change nothing else. Defaults to none.
- Returns:
The area [m²]: inf (with a RuntimeWarning) at a zero or negative driving force, nan if part of a utility target is unserved.
- Return type:
- PinchTargets.units_target(utilities=None)[source]#
An estimate of the number of units: Euler’s S - 1 per region of the balanced grand composite curve between its zeros (see Notes of the class).
- Parameters:
utilities (list[UtilityTarget], optional) – utility_targets of these targets, with any agents: a ValueError is raised unless the heating duties (unserved ones included) add up to Q_hot and the cooling ones to Q_cold, and the shift of each is None or these targets’ shift (the utilities of other targets, e.g. of another T_min_app, or a part of them do not balance the curves). Defaults to utility_targets with the default agents.
- Returns:
{'above': int, 'below': int, 'total': int}: the units of the regions above the process pinch, below it, and both.- Return type:
- PinchTargets.capital_cost_target(utilities=None, h=None, h_utilities=None, P=101325.0, heat_exchanger_type='Floating head', material='Carbon steel/carbon steel', agents=None)[source]#
The capital cost target: the area target in as many shells as the units target (less a furnace), split equally over all of them or over fewer if that costs more (the others then have no area), costed with biosteam’s correlations, plus a furnace for the fuel (see Notes of the class).
- Parameters:
utilities (list[UtilityTarget], optional) – utility_targets of these targets, with any agents: a ValueError is raised unless the heating duties (unserved ones included) add up to Q_hot and the cooling ones to Q_cold, and the shift of each is None or these targets’ shift (the utilities of other targets, e.g. of another T_min_app, or a part of them do not balance the curves). Defaults to utility_targets with the default agents.
h (optional) – Film coefficients, as area_target takes them.
h_utilities (optional) – Film coefficients, as area_target takes them.
P (float, optional) – Pressure of every shell [Pa], > 0. Defaults to 101325.
heat_exchanger_type (str, optional) – biosteam’s type of the shell and tube exchangers (‘Floating head’, ‘Fixed head’, ‘U tube’, ‘Kettle vaporizer’ or ‘Double pipe’). Defaults to ‘Floating head’.
material (str, optional) – biosteam’s shell and tube material. Defaults to ‘Carbon steel/carbon steel’.
agents (Sequence[UtilityAgent or str], optional) – Agents whose IDs h_utilities may key as well, as area_target takes them.
- Returns:
‘area’ [m²]; ‘N_shells’, the number of shells; ‘N_area_shells’, how many of them share the area equally (the others have none); ‘shell_area’, the area of each of those [m²]; ‘type’, biosteam’s type of each (‘Double pipe’ below 150 ft², None without shells or with an infinite or nan area); ‘furnace_duty’, the utility duty of the fuel [kJ/hr]; ‘purchase_cost’ and ‘installed_cost’ (bare module) [USD]. inf with an infinite area, nan with an unserved utility.
- Return type:
- PinchTargets.summary(**options)[source]#
The targets of energy, area, units, capital and total annualized cost (TAC), pricing the utilities once.
- Parameters:
**options – heating_agents and cooling_agents (as utility_targets takes them); h and h_utilities (as area_target takes them); P, heat_exchanger_type and material (as capital_cost_target takes them); interest (default 0.10), years (default 10) and operating_hours (per year, default 8000), each taken from tea when not given and tea is (its IRR,
duration[1] - duration[0]and operating_hours unless None). Without operating_hours and tea, the operating hours are those of system if it has them, as for targets of HeatExchangerNetwork.targets in a system with operating hours (a TEA’s, for instance), so that they match network_report.- Returns:
‘T_min_app’ [K]; ‘Q_hot’ and ‘Q_cold’ [kJ/hr], process side; ‘utility_cost’ [USD/hr]; ‘area’ [m²]; ‘N_units’, the units target; ‘N_shells’, ‘N_area_shells’, ‘shell_area’ [m²] and ‘shell_type’, the ‘type’ of capital_cost_target, as it gives them; ‘purchase_cost’ and ‘installed_cost’ [USD]; ‘annualized_capital_cost’, the installed cost times the capital recovery factor, ‘operating_cost’, the utility cost times the operating hours, and their sum ‘TAC’ [USD/yr].
- Return type:
- class hensmith.targeting.UtilityTarget(ID, agent, heating, level, duty, utility_duty, flow, cost, T_in, T_out, T_utility_in, T_utility_out, shift=None)#
Bases:
tupleOne utility of PinchTargets.utility_targets: an agent placed on the grand composite curve and priced by biosteam as a single heat utility over the temperatures it serves.
Unlike biosteam’s signed HeatUtility.duty, duty is a magnitude: whether the utility heats or cools the process is the heating flag.
- agent#
The agent; None for an unserved remainder.
- Type:
UtilityAgent or None
- level#
Shifted temperature level of the utility on the grand composite curve [K]; +inf for fuel and for unserved heating, -inf for unserved cooling.
- Type:
- utility_duty#
Utility-side heat,
dutydivided by the agent’s heat transfer efficiency [kJ/hr], from biosteam; nan when unserved.- Type:
- T_in, T_out
Process temperatures of the stretch it serves [K]: the real temperatures of the streams it serves there, the coldest and the hottest (heating) or the hottest and the coldest (cooling). biosteam prices the utility from them, a sensible agent from the inlet that keeps its line
dTfrom the process (see Notes of PinchTargets).- Type:
- T_utility_in, T_utility_out
Inlet and outlet temperature of the agent [K], from the priced HeatUtility; nan when unserved.
- Type:
- shift#
PinchTargets.shift of the targets that placed it [K], in stream order: level is on their shifted temperatures, and only targets with the same shifts take it (their balanced curves and their area, units and capital targets raise a ValueError for any other). None, the default, for a utility built by hand, which is not checked.
- hensmith.supertarget(targets, T_min_apps=None, refine=3, n_refine=8, scale_contributions=False, **options)[source]#
Sweep the minimum approach temperature for the least total annualized cost (TAC) target: supertargeting [Ahmad90].
- Parameters:
targets (PinchTargets) – The streams (their curves are reused: no flash).
T_min_apps (Sequence[float], optional) – Minimum approach temperatures to evaluate [K], finite and >= 0 (> 0 with approach contributions); sorted and made unique. Defaults to 1, 2, …, 40 K.
refine (int, optional) – Rounds of refinement around the least TAC. Defaults to 3.
n_refine (int, optional) – New temperatures evaluated per round of refinement. Defaults to 8.
scale_contributions (bool, optional) – Scale the specified approach contributions of targets with the minimum approach temperature (see PinchTargets.at). Defaults to False.
**options – Options of PinchTargets.summary.
- Return type:
- Warns:
RuntimeWarning – Once for the sweep, if some minimum approach temperatures leave part of a utility target unserved (no agent given can serve it: their TAC is nan), instead of the warning of PinchTargets.utility_targets at each.
RuntimeWarning – If no minimum approach temperature of the sweep has a finite TAC (each has an unserved utility or an infinite area): the sweep is then not refined, and its optimum is the smallest one, meaningless.
Notes
Each minimum approach temperature is evaluated with
targets.at(T_min_app, scale_contributions).summary(**options). Only the streams without an approach contribution follow the minimum approach temperature (T_min_app / 2each), unless scale_contributions scales the specified ones byT_min_app / targets.T_min_apptoo, the contribution scaling of [Ahmad90]; with every stream specified, the sweep needs it. The streams’ curves are reused, but each point builds its own problem table (about that of a synthesis, which reads every stream’s enthalpy on its grid), so on a system of many streams with rigorous thermodynamics the default sweep (40 temperatures and three refinements) can take longer than a synthesis: a coarser T_min_apps or a smaller refine keeps it short.The TAC is not smooth (the units target jumps, so do biosteam’s correlations at 150 ft², and the pinch can move), so the sweep is a grid search: each round of refinement evaluates n_refine new temperatures between the neighbors of the least TAC so far, never on it: with it, they cut that bracket into
n_refine + 2gaps, shared by its two sides in proportion to their widths, each side evenly spaced (all on its one side at a bound of the sweep). On an evenly spaced grid, after r rounds the spacing near the optimum is then about the grid’s times(2 / (n_refine + 2))**r, (1/5)**r for the defaults. A least TAC at the smallest or largest temperature evaluated is flagged (Supertargets.at_bound). A sweep whose TACs are all finite and the same (to a relative 1e-12) is not refined: its targets do not depend on the minimum approach temperature over the sweep (for streams that recover no heat, only heaters, say), its optimum is merely the smallest temperature, and Supertargets.constant flags it. Nor is one whose finite TACs (at least two) are the same, the others not (an unserved utility or an infinite area): its optimum is merely the smallest temperature of that flat stretch, so its repr says ‘TAC constant where finite’ rather than ‘at the bound’.The capital target drops by about a shell and tube wherever the shrinking area fills one fewer of 150 ft² (see Notes of PinchTargets), so the least TAC often lies just past such a step, at the first temperature evaluated beyond it, rather than at a smooth trade-off of energy and capital. The records’ ‘N_area_shells’ and ‘shell_type’ show the steps (Supertargets.to_frame). A network has such steps too, but where its own exchangers cross 150 ft², which the target, with its equal shells, places only roughly.
Where the targets do not depend on the minimum approach temperature (below the threshold of a threshold problem, where neither the utilities nor the balanced composite curves change), the TACs differ only by the discretization of the streams’ curves on the problem table’s grid, by a tiny relative amount, and 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 of the tutorial, the warmest point evaluated, just short of the threshold): compare the TACs of its neighbors before reading anything into its position.
Unless operating_hours or tea is given, the operating hours are those of
targets.systemif it has them (see PinchTargets.summary).Examples
One hot stream cooled 400 -> 300 K and one cold stream heated 290 -> 390 K, both 1 kW/K. Up to 10 K they balance with no utility in a single exchanger of 20 m² (215 ft², a shell and tube); above it the utilities add two units, but the area still fills one shell and tube until it falls below 150 ft² (13.94 m²) just below 15 K, where three double pipes take it. So on a coarse grid the least total annualized cost (TAC) is at 15 K:
>>> import biosteam as bst, thermosteam as tmo >>> from hensmith import PinchTargets, supertarget >>> fluid = tmo.Chemical('Fluid', search_db=False, phase='l', MW=1., ... Cn=3.6, default=True) >>> bst.settings.set_thermo([fluid]) >>> bst.main_flowsheet.set_flowsheet('supertargeting_example') >>> H1 = bst.HXutility('H1', ins=bst.Stream('H1_in', Fluid=1000., T=400.), ... T=300., rigorous=False) >>> C1 = bst.HXutility('C1', ins=bst.Stream('C1_in', Fluid=1000., T=290.), ... T=390., rigorous=False) >>> H1.simulate(); C1.simulate() >>> targets = PinchTargets.from_heat_utilities( ... [H1.heat_utilities[0], C1.heat_utilities[0]], T_min_app=10.) >>> grid = [5., 10., 15., 20., 25., 30.] >>> sweep = supertarget(targets, grid, refine=0) >>> sweep <Supertargets: 6 points, T_min_app 5-30 K, optimum 15 K (TAC 4.949e+03 USD/yr)> >>> sweep['N_units'], sweep['area'].round(2) (array([1, 1, 3, 3, 4, 4]), array([20. , 20. , 13.88, 11.07, 9.73, 9.17])) >>> sweep['N_area_shells'], [r['shell_type'] for r in sweep.records[1:3]] (array([1, 1, 3, 3, 4, 4]), ['Floating head', 'Double pipe'])
Refining around it finds the least TAC at the first temperature evaluated past that step, where the TAC falls to under half that of its neighbor below: the step decides the optimum, not a trade-off of energy and capital.
>>> sweep = supertarget(targets, grid) >>> sweep <Supertargets: 30 points, T_min_app 5-30 K, optimum 14.96 K (TAC 4.936e+03 USD/yr)> >>> i = sweep.records.index(sweep.optimum) >>> [(r['shell_type'], round(r['area'], 2), round(r['TAC'])) ... for r in sweep.records[i - 1:i + 1]] [('Floating head', 13.95, 12573), ('Double pipe', 13.91, 4936)]
References
- class hensmith.Supertargets(targets, records, options=None, scale_contributions=False)[source]#
Bases:
objectThe targets of a sweep of the minimum approach temperature, as supertarget returns them.
- Parameters:
targets (PinchTargets) – The streams swept.
records (list[dict]) – PinchTargets.summary at each minimum approach temperature.
options (dict, optional) – Options of PinchTargets.summary of the records.
scale_contributions (bool, optional) – Whether the approach contributions were scaled (see PinchTargets.at). Defaults to False.
- targets#
The streams swept.
- Type:
- at_bound#
Whether optimum is at the smallest or largest temperature evaluated: the least TAC may lie beyond the sweep.
- Type:
- constant#
Whether the TAC is the same at every temperature evaluated (at least two, each finite, to a relative 1e-12): the targets do not depend on the minimum approach temperature over the sweep, and optimum is merely the first record.
- Type:
Notes
sweep[key]is the array of a key of the records over the sweep (for instancesweep['TAC']), and to_frame a pandas DataFrame of them indexed by ‘T_min_app’.
- hensmith.network_report(HXN, targets=None, **options)[source]#
Compare the heat exchanger network that a HeatExchangerNetwork facility synthesized with the targets of its process streams: energy, utilities, number of units, area, capital cost and total annualized cost (TAC) of both, side by side.
- Parameters:
HXN (HeatExchangerNetwork) – A simulated facility, whose network is of the units’ current states (simulate the system again after changing a unit or a stream).
targets (PinchTargets, optional) – Targets of the facility’s process streams, as HeatExchangerNetwork.targets makes them (the same units in the same order, the same thermodynamic basis and the units’ current states). Defaults to
HXN.targets().**options – Options of PinchTargets.summary. The film coefficients h and h_utilities and the economics interest, years, operating_hours and tea apply to the network and the targets alike (the operating hours default to the system’s, if it has them and neither operating_hours nor tea is given); the agents, P, heat_exchanger_type and material to the targets only (the network keeps biosteam’s agents and costs).
- Returns:
‘T_min_app’ and ‘targets_T_min_app’ [K], of the facility (its attribute as it is now: a reused cached network keeps the one it was synthesized at) and of the targets; ‘status’ (
HXN.synthesis_info['status']); ‘applied’, whether the network has a process exchanger; ‘Q_hot’ and ‘Q_cold’, the heat of the network’s heating and cooling utilities [kJ/hr], process side (HXN.actual_heat_util_load is utility side); ‘utilities’,{agent ID: {'duty', 'utility_duty', 'cost'}}[kJ/hr, kJ/hr, USD/hr] (duty process side, a magnitude); ‘utility_cost’ [USD/hr]; ‘N_units’, the exchangers with duty (a furnace included), of which ‘N_process_units’ and ‘N_utility_units’; ‘area’, the network’s area like for like with the area target [m²], inf if an exchanger’s is (see Notes); ‘area_biosteam’, the area biosteam sized on terminal temperature differences [m²]; ‘exchanger_areas’,{ID: (area, area_biosteam)}of each exchanger with duty [m²]; ‘purchase_cost’ and ‘installed_cost’ of all the network’s exchangers [USD]; ‘annualized_capital_cost’, ‘operating_cost’ and ‘TAC’ [USD/yr], as PinchTargets.summary computes them; ‘targets’, PinchTargets.summary of the targets.- Return type:
- Raises:
RuntimeError – If the facility was not simulated.
ValueError – If targets are not those of the facility’s process streams in their current states (enthalpies at both ends, and pressure and flows at the inlet), if the network’s streams do not enter and leave it at those states, are of exchangers no longer among the facility’s units and their auxiliaries or are not as many as the units list their heat utilities now (a unit or a stream changed since the facility’s last simulation: a biosteam.MultistageCompressor simulated alone after a change of pr, say, rebuilds its intercoolers, and a unit in parallel simulated alone may change its number of units,
Unit.parallel, whose heat utilities biosteam lists once per unit), or for an invalid option.TypeError – For an unknown option.
Notes
The network’s ‘area’ is sized as the area target is: each exchanger with the film coefficients of its two sides (1 kW/m²/K by default), counter-current (a correction factor of 1), and integrated against the process duty along the temperature profiles of its streams as their curves in the targets model them, like for like with the area target: a point-load stream at its outlet temperature and a non-equilibrium end state (such as a vapour entering below its dew point) as its jump, where the stream itself may glide a little in the exchanger. A utility’s temperature is linear in the duty between its inlet and outlet, and a furnace takes no area (as fuel in the targets). ‘area_biosteam’ is biosteam’s own sizing: heuristic overall coefficients (0.5 kW/m²/K, but 1 for condensing against boiling, which no film coefficients reproduce), a correction factor for two shells, the log-mean of the terminal temperature differences (which overstates the mean difference along a glide) and the utility duty (the process duty divided by the heat transfer efficiency).
An exchanger’s ‘area’ is inf, and so the network’s ‘area’, with a RuntimeWarning naming the exchanger, where its two temperature profiles meet or cross. biosteam specifies a utility from the temperatures at the ends of its exchanger only: it takes a sensible agent to its limit whenever the process inlet allows it (by
bst.HeatUtility.dT), as cooling water to 324.82 K and chilled water to 300.37 K, so against a stream that condenses a little below that limit (a vacuum condenser of steam condensing at 320 K, say) the utility’s line crosses the condensing stretch inside the exchanger: as specified, the exchanger cannot be built. Its ‘area_biosteam’ stays finite, since biosteam sizes on the terminal temperature differences, and so do the costs and the TAC, which are biosteam’s. The targets keep a sensible agent’s line inside the grand composite curve, so the area target of such streams is finite. The report keeps biosteam’s specification rather than size the exchanger for more of the utility, leaving where its line stays inside the stream’s profile: that would be another network at another utility cost. ‘exchanger_areas’ gives the areas of the other exchangers.The capital and utility costs are those of the whole network (HXN.new_HXs and HXN.new_HX_utils), not the facility’s costs, which are the differences from the original exchangers (and zero for a network without process exchangers: the facility then keeps the original exchangers, to which the utility exchangers of the network, reported here, are equivalent).
Only the energy targets (‘Q_hot’ and ‘Q_cold’) bound the network; the others are estimates, and the network may beat some of them:
the network serves each stream with one agent at its outlet (biosteam’s rule: the first agent of the list that can), while the targets place the agents on the grand composite curve in list order (fuel last), so the network may use other agents than the targets, costlier or cheaper ones: with Linnhoff & Hindmarsh’s problem at 10 K, medium pressure steam and chilled water where the targets need only low pressure steam and cooling water, 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 targets place 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: 3.14 USD/hr at MER 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: neither it nor the network’s utility cost bounds the other;
the area target is a minimum only for equal film coefficients and the same utilities: that problem’s network takes 54.06 m² against a target of 59.68 m², with hotter steam and colder coolant;
the units target counts S - 1 per region between zeros of the balanced grand composite curve, as if each region’s matches formed no loop: a network needs fewer units where a subset of streams balances (6 against 7 in that problem) or where one agent serves a stream the targets split between levels, and one more for each loop of its matches, such as 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.
Examples
The liquids of the second example of HeatExchangerNetwork at 10 K, with stream splitting, so that the network reaches the energy targets:
>>> import biosteam as bst >>> from hensmith import network_report >>> bst.main_flowsheet.set_flowsheet('network_report_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., ... stream_splitting=True) >>> sys = bst.System.from_units('sys', units=[*units, HXN]) >>> sys.simulate() >>> report = network_report(HXN) >>> targets = report['targets'] >>> report['status'], report['applied'] ('mer', True)
The network reaches the hot utility target [kW], but serves each stream with one agent at its outlet: medium pressure steam and chilled water, where the targets need mostly low pressure steam and cooling water [USD/hr]:
>>> round(report['Q_hot'] / 3600, 3), round(targets['Q_hot'] / 3600, 3) (102.821, 102.821) >>> sorted(report['utilities']) ['chilled_water', 'cooling_water', 'medium_pressure_steam'] >>> round(report['utility_cost'], 3), round(targets['utility_cost'], 3) (5.496, 3.58)
With hotter steam and colder coolant, its area [m²] is below the area target (biosteam’s own sizing in between), and it needs fewer units than the estimate:
>>> round(report['area'], 2), round(report['area_biosteam'], 2), round(targets['area'], 2) (50.62, 66.33, 70.3) >>> report['N_units'], targets['N_units'] (7, 9)
Its total annualized cost [USD/yr] is below the target’s, which puts the area target in five shell and tubes, where the network has one:
>>> round(report['TAC']), round(targets['TAC']) (62041, 82120) >>> targets['N_area_shells'], targets['shell_type'] (5, 'Floating head')
- hensmith.plot_composite_curves(targets, kind='real', utilities=None, ax=None, file=None, dpi=300)[source]#
Draw the composite curves of targets: real, shifted, or balanced with the utilities.
- Parameters:
targets (PinchTargets)
kind (str, optional) – ‘real’ (the default) or ‘shifted’ for PinchTargets.composite_curves with the heat recovered shaded and the utility targets labeled (shifted ones with the pinch marked), or ‘balanced’ for PinchTargets.balanced_composite_curves with each utility labeled.
utilities (list[UtilityTarget], optional) – Utilities of the balanced curves, as PinchTargets.balanced_composite_curves takes them (those of targets), with kind ‘balanced’ only. Defaults to PinchTargets.utility_targets with the default agents.
ax (matplotlib.axes.Axes, optional) – Axes to draw on; a new figure (with a constrained layout, which keeps the axis labels and a note under the x axis in it) is created if not given.
file (str, optional) – If given, the figure is saved to this path.
dpi (int, optional) – Resolution used when saving. Defaults to 300.
- Returns:
fig (matplotlib.figure.Figure)
ax (matplotlib.axes.Axes)
- Raises:
ValueError – For another kind, for utilities with kind ‘real’ or ‘shifted’ (whose curves take none), or for
utilities=False(the balanced curves are those of their utilities: kind ‘real’ draws the composites without them).
Notes
Temperatures are shown in degC. Artists carry stable gids: ‘Hot composite’ and ‘Cold composite’ (or ‘Hot balanced composite’ and ‘Cold balanced composite’), ‘Heat recovered’, ‘Hot utility’, ‘Cold utility’, ‘Pinch’, ‘Fuel’, ‘Utility:<agent ID>’ and ‘Leader:<agent ID>’. The pinch is marked where the shifted composites touch: not for a threshold problem whose composites end apart, nor without streams on one side (whose empty composite stays out of the legend). Fuel, at infinite temperature on the balanced curves, is labeled rather than drawn, above the hot end of the cold balanced composite (at the top of both), whose top its heat closes, and so is an unserved remainder, in a note under the x axis (‘Utility:unserved’, clear of the curves, of their labels and of the legend): the cold balanced composite starts at the unserved cooling, and the hot one ends short of the cold one by the unserved heating. A label of a latent agent sits by its flat, above left of the warm end of a heating one and below right of the cold end of a cooling one, and a sensible agent’s off the middle of its line, above left for heating and below right for cooling. When drawn (at the final layout of the figure, all of them in one pass), a label that would run past the left or right edge of the axes is moved just inside them, and off the curves. The labels of each side keep the order of the levels: each one that would run into the label of a lower heating (higher cooling) agent is moved on past it, up for heating and down for cooling (the fuel’s past those of the heating agents), in a column for which the limits of the y axis leave room (as the axes are laid out when drawn on). Where the column would still run past the top (bottom) of the axes, the labels go in rows instead, each beside its flat and moved sideways past the labels of the levels beyond its own; or, if those would not keep the order inside the axes, those that would run past the edge are moved sideways past the label in their way. Labels can then still overlap, on axes too small for them all (many agents on small axes without a layout engine, say, or on short axes that a legend above them leaves shorter): a larger figure, or one with
layout='constrained', gives them room. A label left more than 1.5 times its height away from its anchor gets a thin leader from it (‘Leader:<agent ID>’, an empty path otherwise), and each label a halo, so that a leader passing behind it strikes none of its letters. The legend, of any kind, covers none of the labels: when drawn, it goes to the spot of matplotlib’s ‘best’ location in the axes that covers the least of the curves, the leaders and the band among those clear of the labels (matplotlib’s own spot wherever that is clear of them: no fixed corner is clear of the composites of every problem); or, where each of those spots would cover a label (on small axes, say), above the axes and any title of them, left-aligned with them, where a constrained layout or a tight bounding box makes room for it.Examples
The streams of PinchTargets:
>>> import biosteam as bst, thermosteam as tmo >>> from hensmith import PinchTargets, plot_composite_curves >>> fluid = tmo.Chemical('Fluid', search_db=False, phase='l', MW=1., ... Cn=3.6, default=True) >>> bst.settings.set_thermo([fluid]) >>> bst.main_flowsheet.set_flowsheet('targeting_example') >>> H1 = bst.HXutility('H1', ins=bst.Stream('H1_in', Fluid=1000., T=400.), ... T=300., rigorous=False) >>> C1 = bst.HXutility('C1', ins=bst.Stream('C1_in', Fluid=1000., T=330.), ... T=440., rigorous=False) >>> H1.simulate(); C1.simulate() >>> targets = PinchTargets.from_heat_utilities( ... [H1.heat_utilities[0], C1.heat_utilities[0]], T_min_app=10.) >>> fig, ax = plot_composite_curves(targets, kind='shifted') >>> [line.get_gid() for line in ax.get_lines()] ['Hot composite', 'Cold composite', 'Pinch'] >>> import matplotlib.pyplot as plt >>> plt.close(fig)
- hensmith.plot_grand_composite_curve(targets, utilities=None, ax=None, file=None, dpi=300)[source]#
Draw the grand composite curve of targets with the utility targets as a staircase of levels.
- Parameters:
targets (PinchTargets)
utilities (list[UtilityTarget] or False, optional) – Utilities to draw. Defaults to PinchTargets.utility_targets with the default agents; False draws the curve only.
ax (matplotlib.axes.Axes, optional) – Axes to draw on; a new figure (with a constrained layout, which keeps the axis labels in it) is created if not given.
file (str, optional) – If given, the figure is saved to this path.
dpi (int, optional) – Resolution used when saving. Defaults to 300.
- Returns:
fig (matplotlib.figure.Figure)
ax (matplotlib.axes.Axes)
Notes
Shifted temperatures are shown in degC. Heating utilities are drawn by ascending level from the temperature axis, each a horizontal segment at its level as long as its duty, joined by dotted verticals; cooling utilities likewise by descending level. Fuel is drawn dashed at the top of the grid and an unserved remainder in gray. Each label sits beside the end of its segment, placed when drawn: on the side with more room in the axes, right of the end toward the pinch (below the level for heating, above it for cooling) or left of it beyond the level (above it for heating, below it for cooling, where the heat cascaded is at least the end of the step), if that keeps it inside the axes and off the curve, the steps and the labels placed before it; else at the first other side of the end and of the level that does (a pocket of the curve, or a curve that leaves the end at a shallow angle, can run toward the pinch); else on along its level, away from the end, where it first does. Artists carry stable gids: ‘GCC’, ‘Pinch’, ‘Utility:<agent ID>’ (the segments, ‘Utility:unserved’ for a remainder) and ‘Label:<agent ID>’.
Examples
The streams of PinchTargets:
>>> import biosteam as bst, thermosteam as tmo >>> from hensmith import PinchTargets, plot_grand_composite_curve >>> fluid = tmo.Chemical('Fluid', search_db=False, phase='l', MW=1., ... Cn=3.6, default=True) >>> bst.settings.set_thermo([fluid]) >>> bst.main_flowsheet.set_flowsheet('targeting_example') >>> H1 = bst.HXutility('H1', ins=bst.Stream('H1_in', Fluid=1000., T=400.), ... T=300., rigorous=False) >>> C1 = bst.HXutility('C1', ins=bst.Stream('C1_in', Fluid=1000., T=330.), ... T=440., rigorous=False) >>> H1.simulate(); C1.simulate() >>> targets = PinchTargets.from_heat_utilities( ... [H1.heat_utilities[0], C1.heat_utilities[0]], T_min_app=10.) >>> fig, ax = plot_grand_composite_curve(targets) >>> sorted(line.get_gid() for line in ax.get_lines() ... if (line.get_gid() or '').startswith('Utility:')) ['Utility:chilled_water', 'Utility:cooling_water', 'Utility:low_pressure_steam', 'Utility:medium_pressure_steam'] >>> import matplotlib.pyplot as plt >>> plt.close(fig)
- hensmith.plot_supertargets(sweep, ax=None, file=None, dpi=300)[source]#
Draw the total annualized cost (TAC) of a sweep of the minimum approach temperature, its annualized capital and operating parts, and the least TAC.
- Parameters:
sweep (Supertargets)
ax (matplotlib.axes.Axes, optional) – Axes to draw on; a new figure (with a constrained layout, which keeps the axis labels in it) is created if not given.
file (str, optional) – If given, the figure is saved to this path.
dpi (int, optional) – Resolution used when saving. Defaults to 300.
- Returns:
fig (matplotlib.figure.Figure)
ax (matplotlib.axes.Axes)
Notes
Artists carry stable gids: ‘TAC’, ‘Capital’ (the annualized capital cost), ‘Operating’, ‘Optimum’ and ‘Nonfinite’. The optimum is labeled ‘(TAC constant)’ if the TAC is the same over the sweep (Supertargets.constant), ‘(TAC constant where finite)’ if it is the same at each temperature with a finite TAC (at least two), else ‘(at the bound)’ if it is at an end of the sweep (Supertargets.at_bound), and is not marked without a finite TAC. The lines leave out the temperatures without a finite TAC (an unserved utility or an infinite area): each stretch of them is shaded (‘Nonfinite’, halfway to its finite neighbors), so the axis keeps the whole sweep.
Examples
The streams of PinchTargets:
>>> import biosteam as bst, thermosteam as tmo >>> from hensmith import PinchTargets, supertarget, plot_supertargets >>> fluid = tmo.Chemical('Fluid', search_db=False, phase='l', MW=1., ... Cn=3.6, default=True) >>> bst.settings.set_thermo([fluid]) >>> bst.main_flowsheet.set_flowsheet('targeting_example') >>> H1 = bst.HXutility('H1', ins=bst.Stream('H1_in', Fluid=1000., T=400.), ... T=300., rigorous=False) >>> C1 = bst.HXutility('C1', ins=bst.Stream('C1_in', Fluid=1000., T=330.), ... T=440., rigorous=False) >>> H1.simulate(); C1.simulate() >>> targets = PinchTargets.from_heat_utilities( ... [H1.heat_utilities[0], C1.heat_utilities[0]], T_min_app=10.) >>> sweep = supertarget(targets) >>> sweep <Supertargets: 64 points, T_min_app 1-40 K, optimum 7.816 K (TAC 2.490e+04 USD/yr)> >>> fig, ax = plot_supertargets(sweep) >>> [line.get_gid() for line in ax.get_lines() if line.get_gid()] ['TAC', 'Capital', 'Operating', 'Optimum'] >>> import matplotlib.pyplot as plt >>> plt.close(fig)
Note
UtilityTarget is not exported by
hensmith; it is documented because
PinchTargets.utility_targets() returns it. The private helpers of
hensmith.targeting (those whose names start with an underscore) are not
part of the supported API.