Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a5fdd31
add stream-splitting primitives: branch curves, cells, vertical coupl…
sarangbhagwat Sep 24, 2026
3b2e0ce
add the vertical core of stream splitting: blocks, pre-leak, driver, …
sarangbhagwat Sep 24, 2026
8da98f7
address inspection of T2: carry round-off slivers, raise on a lost tr…
sarangbhagwat Sep 24, 2026
798e1cb
add split plan records and the stream_splitting switch to the planner
sarangbhagwat Sep 24, 2026
8531240
add the stage s cut transports and the branched side to splitting
sarangbhagwat Sep 24, 2026
45a6a39
add stage s to stream splitting: pinch splits planned by the dfs
sarangbhagwat Sep 24, 2026
38e86a8
add core pinch blocks and dfs tails to stream splitting
sarangbhagwat Sep 24, 2026
de5000b
address review A: exact core residuals, strict split searches, diagno…
sarangbhagwat Sep 24, 2026
6dbb33b
add split-aware exact checks, walk and branch ports to hxn_synthesis
sarangbhagwat Sep 24, 2026
457d759
realize stream splits and add the split-aware refine loop to synthesi…
sarangbhagwat Sep 24, 2026
bb818d0
address review B: drift-proof split identity, split first-node rule, …
sarangbhagwat Sep 24, 2026
4138a58
add split-aware stream life cycles, connections and pinch-diagram col…
sarangbhagwat Sep 24, 2026
fbb4081
add the strict split-network checker and the corpus harness to test_h…
sarangbhagwat Sep 24, 2026
3dc81af
let the facility split streams: stream_splitting, wiring from life cy…
sarangbhagwat Sep 24, 2026
701203a
let the cached network serve split networks: entry, branch limits, flows
sarangbhagwat Sep 24, 2026
9b152d3
address review C: old caches resynthesize, csv splitter row, stronger…
sarangbhagwat Sep 24, 2026
ac646e0
test that splitting reaches mer on the constant-cp split corpus
sarangbhagwat Sep 24, 2026
f366124
test that splitting reaches mer on the real-thermo corpus and regression
sarangbhagwat Sep 24, 2026
a5cfa0b
address review D: hold regression split cases to the corpus checks, d…
sarangbhagwat Sep 25, 2026
48531fe
document stream splitting in docstrings, docs pages and the readme
sarangbhagwat Sep 25, 2026
4acb8db
address review E: raise tiny capacity branches, entry-free life cycles
sarangbhagwat Sep 25, 2026
6d4776f
raise tiny demand branches in stage s, check the planned minimum frac…
sarangbhagwat Sep 25, 2026
7891192
fix duplicate autodoc entries for StreamLifeCycle.entry and splits
sarangbhagwat Oct 8, 2026
5ef9a35
keep the phase split in the cached network's pre-copy without a flash
sarangbhagwat Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ systems. Targets come from a problem table on each stream's temperature-enthalpy
curve (phase changes included), and a pinch-outward planner synthesizes a
network without stream splits that reaches those minimum energy requirement
(MER) targets whenever it finds one, keeping the minimum approach temperature
everywhere inside every exchanger; where MER provably needs a split, the
network is a best-effort one close to the targets.
everywhere inside every exchanger; by default, where MER provably needs a
split, the network is a best-effort one close to the targets. With
`HeatExchangerNetwork(..., stream_splitting=True)`, streams are instead
split into parallel branches there (BioSTEAM splitters and rigorous mixers),
and the network reaches the MER targets (not guaranteed with `avoid_recycle`;
see the documentation for the limits with real thermodynamics).

```python
import biosteam as bst # hensmith units plug into BioSTEAM systems
Expand Down
2 changes: 1 addition & 1 deletion docs/_demo_src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ directories themselves.
| `examples/ch01_quickstart.py` | `_static/images/examples/tutorial_01_quickstart_flowsheet_light.png`, `…_flowsheet_dark.png`, `_static/images/examples/tutorial_01_quickstart_pinch_diagram.png`; `_generated/ch01_results.txt`, `ch01_loads.txt`, `ch01_life_cycles.txt`, `ch01_summary.txt` |
| `examples/ch02_pinch_analysis.py` | `_static/images/examples/tutorial_02_composite_curves.png`, `tutorial_02_grand_composite.png`; `_generated/ch02_threshold.txt`, `ch02_table.txt`, `ch02_compare.txt` |
| `examples/ch03_network_anatomy.py` | `_static/images/examples/tutorial_03_hxn_flowsheet_light.png`, `…_hxn_flowsheet_dark.png`, `tutorial_03_pinch_diagram_minimal.png`; `_generated/ch03_flowsheet.txt`, `ch03_life_cycles.txt`, `ch03_stage.txt`, `ch03_pinch_Ts.txt`, `ch03_accounting.txt` |
| `examples/ch04_configuring.py` | `_static/images/examples/tutorial_04_T_min_app_sweep.png`, `tutorial_04_ten_streams_pinch_diagram.png`; `_generated/ch04_sweep.txt`, `ch04_ignored.txt`, `ch04_ten_streams.txt` |
| `examples/ch04_configuring.py` | `_static/images/examples/tutorial_04_T_min_app_sweep.png`, `tutorial_04_ten_streams_pinch_diagram.png`; `_generated/ch04_sweep.txt`, `ch04_splitting.txt`, `ch04_ignored.txt`, `ch04_ten_streams.txt` |
| `make_hero_gif.py` | `_static/images/demo/hero_light.gif`, `hero_dark.gif` (8 s loop, 20 fps, 2000 × 720), `hero_light_still.png`, `hero_dark_still.png` (the frame-0 stills served under `prefers-reduced-motion`) |
| `build_demo.py` | `_static/quickstart_demo.html` — the interactive quickstart demo, filled in from `quickstart_demo_template.html` |
| `make_poster.py` | `_static/images/examples/quickstart_demo_poster.png` — the README poster that links to the demo (2400 × 1260) |
Expand Down
21 changes: 20 additions & 1 deletion docs/_demo_src/examples/ch04_configuring.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
# for license details.
"""Tutorial chapter 04 (docs/source/tutorial/04_configuring.rst): configuring a
HeatExchangerNetwork -- sweeping the minimum approach temperature to expose the
utility/capital trade-off, scoping the analysis with ``ignored=``, and
utility/capital trade-off, splitting streams where the targets need it
(``stream_splitting=True``), scoping the analysis with ``ignored=``, and
synthesizing a larger ten-stream system (the ten-stream case of the regression
suite, inlined here rather than imported from ``tests``). Regions between
``# [start:x]`` / ``# [end:x]`` are literalinclude'd by the page; everything
Expand Down Expand Up @@ -109,6 +110,24 @@ def main():
print(f'cooling utility: {HXN.original_cool_util_load:.4g} -> {HXN.actual_cool_util_load:.4g} kJ/hr')
HXN.ignored = None
# [end:ignored]
with capturing('ch04_splitting'):
# [start:splitting]
HXN.T_min_app = 15. # the sweep's first best-effort network
for stream_splitting in (False, True):
HXN.stream_splitting = stream_splitting
sys.simulate()
info = HXN.synthesis_info
print(f'stream_splitting={stream_splitting}: {info["status"]}, '
f'{len(HXN.new_HXs)} process exchangers, '
f'{HXN.installed_costs["Heat exchangers"]:.4g} USD added installed cost')
print('splitters:', [u.ID for u in HXN.new_splitters])
print('mixers: ', [u.ID for u in HXN.new_mixers])
print('process exchangers:', [hx.ID for hx in HXN.new_HXs])
print(info['splits'])
print(HXN.stream_life_cycles[3])
HXN.stream_splitting = False
HXN.T_min_app = 5.
# [end:splitting]
with capturing('ch04_ten_streams'):
# [start:ten_streams]
bst.settings.set_thermo(['Water', 'Ethanol'], cache=True)
Expand Down
22 changes: 16 additions & 6 deletions docs/source/API/heat_exchanger_network.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ HeatExchangerNetwork
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 -- and
(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
Expand Down Expand Up @@ -45,7 +46,7 @@ shows what each of them changes.
- 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 set of units contributing heat utilities is 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, 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.
- Reuse the network configuration of the previous simulation when the set of units contributing heat utilities and ``stream_splitting`` 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.
Expand All @@ -58,6 +59,9 @@ shows what each of them changes.
* - ``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.

Class attributes
----------------
Expand Down Expand Up @@ -119,10 +123,10 @@ the cached network.
- 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 :func:`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, and the smallest approach inside any process exchanger. Kept from the synthesis that produced a cached network.
- The synthesis report (see the ``info`` keyword of :func:`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, and the smallest approach inside any process exchanger. With ``stream_splitting``, also ``'stream_splitting'``, ``'splits'`` (the realized splits, :class:`~hensmith.hxn_synthesis.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``.
- 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.
Expand All @@ -135,6 +139,12 @@ the cached network.
* - ``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.
Expand All @@ -143,7 +153,7 @@ the cached network.
- 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, named ``<sys>_HXN`` and registered in ``HXN_flowsheet``; converged and summarized during costing.
- 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.
Expand All @@ -161,7 +171,7 @@ the cached network.
- 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.
- 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.
Expand Down
19 changes: 15 additions & 4 deletions docs/source/API/hxn_synthesis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ temperature-interval heat cascade of a set of process streams on their
temperature-enthalpy curves and locates the pinch, :func:`synthesize_network`
plans an unsplit network from the pinch outward on the same curves -- one that
reaches the minimum energy requirement (MER) targets whenever its search finds
one -- and realizes it as BioSTEAM exchangers, :class:`StreamLifeCycle`
one, or, with ``stream_splitting=True``, a network with stream splits where a
side of the pinch needs them -- and realizes it as BioSTEAM exchangers (and
the splits as :class:`~hensmith.hxn_synthesis.StreamSplit` splitter chains and
mixers), :class:`StreamLifeCycle`
records the exchangers each stream ends up passing through, and
:func:`plot_pinch_diagram` draws the result. All four are usable on their own,
without a :class:`HeatExchangerNetwork` instance; :doc:`../concepts` explains
Expand All @@ -28,6 +31,9 @@ the method.
.. autoclass:: hensmith.hxn_synthesis.LifeStage
:no-members:

.. autoclass:: hensmith.hxn_synthesis.StreamSplit
:no-members:

.. autofunction:: plot_pinch_diagram

.. note::
Expand All @@ -39,6 +45,11 @@ the method.
stream at a pinch temperature; the synthesis itself plans on the stream
curves and does not use them) are public in name only: they are not exported
by ``hensmith``, and are not part of the supported API. Neither are the
private modules ``hensmith._curves`` (the stream temperature-enthalpy curves)
and ``hensmith._planner`` (the MER planner). Their signatures and behavior
may change without notice.
private modules ``hensmith._curves`` (the stream temperature-enthalpy curves),
``hensmith._planner`` (the MER planner) and ``hensmith._splitting`` (stream
splitting: the split candidates and their theory, in its module docstring).
Their signatures and behavior may change without notice.
:class:`~hensmith.hxn_synthesis.StreamSplit` and
:class:`~hensmith.hxn_synthesis.LifeStage` are not exported by ``hensmith``
either; they are documented because synthesis results and life cycles hold
them.
14 changes: 14 additions & 0 deletions docs/source/_generated/ch04_splitting.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
stream_splitting=False: best_effort, 8 process exchangers, 6.684e+05 USD added installed cost
stream_splitting=True: mer, 5 process exchangers, 5.893e+05 USD added installed cost
splitters: ['Split_3_cs']
mixers: ['Mix_3_cs']
process exchangers: ['HX_0_2_hs', 'HX_3_0_cs', 'HX_3_1_cs', 'HX_2_1_cs', 'HX_3_0_cs_2']
[<StreamSplit Split_3_cs: stream 3 below, 2 branches (0.5624, 0.4376)>]
<StreamLifeCycle: Stream_3, hot
life_cycle = [
<LifeStage: <HXprocess: HX_3_0_cs>, branch (0, 0), fraction 0.5624, H_in = 1.15e+07 kJ/hr, H_out = 1.39e+06 kJ/hr>
<LifeStage: <HXprocess: HX_3_1_cs>, branch (0, 1), fraction 0.4376, H_in = 8.93e+06 kJ/hr, H_out = 2.23e+06 kJ/hr>
<LifeStage: <HXprocess: HX_3_0_cs_2>, branch (0, 1), fraction 0.4376, H_in = 2.23e+06 kJ/hr, H_out = 1.08e+06 kJ/hr>
<LifeStage: <HXutility: Util_3_cs>, H_in = 2.47e+06 kJ/hr, H_out = 2.47e+06 kJ/hr>
]
split 0: 2 branches (0.5624, 0.4376)>
Loading
Loading