reach MER on unsplit-feasible problems and correct MER targets for phase change - #1
Merged
Merged
Conversation
…lems The synthesizer misses the minimum energy requirement (MER) on problems that need no stream splitting (e.g. the Linnhoff 4-stream problem: 27.5 / 67.5 kW instead of 20 / 60 kW). Both designs consume streams from their inlets instead of from the pinch, pinch partners are chosen by a local score, and tick-off duties alone are not enough: some no-split problems need a duty that stops short of tick-off and the same pair matched twice on one side (e.g. H1 220->50 CP4, C1 140->170, C2 100->120, C3 120->250, dTmin 10, whose only unsplit MER network is H1-C3 160, H1-C1 30, H1-C2 20, H1-C3 20). hensmith/_planner.py is a pure-numpy planner (no biosteam objects) that works on piecewise-linear T-H curves with flat (isothermal) segments. Each side of the pinch becomes the same problem on level curves: 'must' streams (hot above, cold below) are covered from the pinch outward by matches at the current frontiers, 'flex' streams take utility at their far end. A deterministic depth-first search tries exact event duties (tick-off, dT limit, flex saving, vertical switch, return, preemption, lead sharing) with repeated pairs, and prunes only on necessary conditions: the residual cascade (split-relaxed feasibility), dead must streams, and flat-aware pinch number/CP rules at every tight level. Sides with a pinch-rule proof, or an exhausted work budget, get a best-effort network (greedy dives, then per-must gap bisection) whose uncovered heat is moved to the pinch end. Budgets are counted in work units, never wall-clock time, so results are reproducible. The design came from a panel of three competing prototypes judged on a certified benchmark: 1,937 constant-CP problems (86 verified literature problems, 1,851 random) labelled by an independent oracle (stage-wise no-split MILP certificates checked by plain arithmetic, pinch-rule proofs). Validation of this module: all 2,855 no-split problems of the benchmark plus 1,680 fresh held-out problems (2-40 streams) reach MER and pass the certificate checker; none of 761 split-proven problems is reported as MER; every network is feasible; reruns are bit-identical. tests/test_hxn_planner.py pins the curve primitives against brute force, the flat-aware rules, the r002 network, repeated-pair literature cases, hard held-out cases, determinism and best-effort feasibility. The module is not wired into synthesize_network yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… MER planner Two defects kept the heat exchanger network from its minimum energy requirement (MER). Targets. problem_table evaluated the cascade only at stream end temperatures and flashed streams at vle(T=T_sat), where thermosteam keeps whatever phase split the previous flash left. A boiling point, a dew or bubble point, a mixture glide or a curved heat capacity inside a grid interval could therefore hide the pinch, always making the hot-utility target too low (by up to 100 %: e.g. cold water 300 K -> saturated vapour against hot water 400 -> 330 K at 5 bar, dT 5 K, gave 0 instead of 2,395,102 kJ/hr). The synthesizer then "met" those targets with exchangers whose approach dropped below T_min_app inside the unit, because HXprocess checks the approach only at its two terminals (regression case 04 relied on 4.63 K, case 10 on 2.07 K, against 5 K). The new private module hensmith/_curves.py builds one piecewise-linear T-H curve per stream from a handful of flashes: pure-component latent heat as a flat from saturated liquid/vapour enthalpies (V-specified flashes, never vle(T=T_sat)), binary glides traced along the bubble curve, curved single-phase stretches and glides sampled until the chord is within 0.002 K, clipped to the stream's own enthalpy range. problem_table's grid is the union of all curve breakpoints; its targets now agree with an independent dense calculator to <= 2.3e-5 on 22 phase-change stream sets (constant-Cp problems are bit-identical to before). Synthesis. synthesize_network is rewritten on the pinch-outward planner of hensmith/_planner.py: the curves are sampled onto the problem-table grid so the planner's cascade equals the table, each side is planned from the pinch outward (repeated pairs allowed; IDs get a _<n> suffix), each exchanger is realised as an HXprocess from exact inlet states with both enthalpy limits at the planned outlets and dT = T_min_app - 1e-6 K as a guard, every approach is re-verified on exact states (with local knot refinement), and each stream gets one utility exchanger at its far end. The old four passes, which consumed streams from their inlets and chose pinch partners by a local score (and inflated cold-stream CP by the steam efficiency through hx.Q), are removed. HeatExchangerNetwork orders its system by the stream stages, converges recycle loops to ~1e-10 % energy balance, rescales the stages of a cached network by their fractional position on each stream, and exposes synthesis_info (status 'mer' or 'best_effort', targets, penalty, per-side proofs). Additive keywords only: problem_table(curves=), pinch_state(side=, curve=), load_duties(curves=), synthesize_network(info=); the 13-tuple is unchanged (C_flow_vector now from process enthalpies). A stream already at its outlet passes through its utility exchanger unchanged, so flash noise no longer leaves phantom 1e-9 kJ/hr utility duties. Doctest outputs changed as a consequence (HeatExchangerNetwork life cycles; problem_table prints Ts[[0, -1]] because the grid now holds curvature breakpoints). Regression loads re-recorded deliberately (heating/cooling, kJ/hr): case 04 raised to 2.40548e6/3.601e6 and case 10 raised to its corrected MER 1.44713e7/8.46199e6, because the old values were only reachable through internal temperature crosses and lie below the corrected targets; case 05 lowered to MER 0/4.59225e6; cases 08 and 09 (split needed) lowered to 3.01517e6/7.35711e6 and 1.40002e7/9.56801e6. The regression harness now also checks the exact internal approach of every exchanger. Validated: full suite green (CI invocation); on real systems the doctest system and the isobutanol biorefinery now reach MER (old: +0.0068 MW and +0.77 MW), oilcane O1 (split needed) ends 2.0 MW above target (old: 9.7 MW), synthesis takes 0.05-1.1 s, energy balance <= 1e-10 %. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed problems tests/hxn_mer_cases.py holds two sets of heat exchanger network problems and tests/test_hxn_mer.py checks them through the public HeatExchangerNetwork facility. NO_SPLIT (40 cases, 11 with more than 10 streams): 24 verified literature problems (Linnhoff 4-stream; classic 4SP-23SP benchmarks; Smith, Kemp, Turton and NPTEL examples incl. ones whose MER network needs the same pair twice; the 12-37 stream Furman-Sahinidis, Sargent, nitric acid, Sorak- Kravanja and Bandar Imam aromatics problems) with constant heat capacity, and 16 real-thermodynamics problems (water, ethanol, methanol and safe binary glides; condensers and boilers at the pinch, desuperheat-condense- subcool, glides). Each constant-Cp case carries an unsplit MER network (found by an independent MILP oracle) that a short arithmetic verifier in the test module re-checks, which proves that no split is needed. The tests check that the MER targets are identified correctly (against an independent closed-form cascade and the published values, or against a dense-grid reference for real thermo), that the network reaches them (synthesis_info status 'mer'), and that every balance holds: energy balance, heat minus cool equal to the net duty, every exchanger's duty and mass balance, every stream's outlet, and the exact internal approach of every exchanger. SPLIT (38 cases, 13 with more than 10 streams): 25 literature problems and 13 real-thermo problems whose need for stream splitting is proven by the pinch number or CP rule, re-derived in the test module. The tests check the targets, that the network never beats them, that it reports 'best_effort', and the same balance and feasibility checks. Tolerances are derived from the realized accuracy (MER achieved within 1e-12 of total duty for constant Cp, 1e-10 for real thermo) and, for the real-thermo references, from the reference's own grid error at the pinch; the module docstring gives the reasoning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lace_unit_heat_utilities
Point loads. Under force_ideal_thermo a near-azeotropic condenser feed
(oilcane O1, D302: 91 % ethanol vapour at 364.02 K) is a vapour below its
ideal dew point whose quenched outlet (369.84 K) is hotter than the feed,
so its curve is a point load at 369.84 K. Its first exchanger still got
the real 364.02 K feed, because the equilibrium-inlet rule reused
_end_state, which rejects states outside [T_lo, T_hi], a single point for
a point load; only pure components passed. HXprocess then capped the
partner at the real feed temperature and delivered 1.38 of the planned
3.38 MW. _point_load_inlet now feeds a point-load stream its equilibrium
state at the feed enthalpy whenever that state lies on the plan's side of
T_out (a non-equilibrium feed relaxes there without exchanging heat), at
both call sites and in StreamCurve.state_at_H.
Registry warnings. Internal stream copies were made with a bare
stream.copy(), which names the copy from its source line ('-' when the
line assigns no plain variable), so every synthesis re-registered '-' and
thermosteam warned 128 times on the HeatExchangerNetwork doctest system
(282 on oilcane O1; 2-3 before this branch). Internal copies are now
anonymous ('.'-prefixed IDs); only the final units and streams are
registered. The test filters for these warnings are narrowed accordingly.
replace_unit_heat_utilities=True called a method biosteam no longer has
(unit.owner._load_utility_cost) and paired the original utilities (cold
streams first) with the new utility exchangers (hot streams first). Each
original utility now takes its own stream's utility exchanger (the last
stage of its life cycle), costs are reloaded with _load_operation_costs,
the replacement runs after the load totals and the energy-balance check,
and a repeated simulation first restores the original utility data.
Also from the final review: the legacy pinch helpers (pinch_state,
load_duties, temperature_interval_pinch_analysis) keep their public
signatures but no longer claim to be steps of synthesize_network; the
curves= keyword briefly added to load_duties is removed again (master's
signature); a duplicate exact-approach checker in _curves is removed in
favour of the synthesizer's own, which the target tests now exercise.
Full suite: 467 passed (CI invocation).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The concepts page and the tutorials described the old four matching passes, IDs matched at most once per side, a problem-table grid of stream end temperatures only, and a network that was "heuristic, not guaranteed to reach MER". They now describe the stream curves and the breakpoint grid (latent heat, non-equilibrium ends and point-load streams as point loads), the pinch-outward planner (must/flex streams, pinch rules, repeated pairs with _<n> IDs, best effort when splitting is needed, both enthalpy limits, exact approach verification, synthesis_info), cache fractions, and the guarantees as they are: MER is never beaten, and it is reached on every certified no-split problem tested, which is empirical, not proven. Every captured output, figure, hero GIF/still, the demo page and the poster were regenerated by running the docs/_demo_src generators on the new library (ch01/ch02 now print the synthesis status and grid size; ch04 adds pinch, exchanger-count and status columns). The quickstart now reaches MER with 4 exchangers; the ten-stream case reaches MER with 10. sphinx -W --keep-going builds with no warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_non_monotone_stream_is_a_point_load failed on the PR's CI (Linux, Python 3.12) with `assert 7366735.070962861 >= 7366735.07096287`: the network reaches MER there, so the achieved hot-utility load equals the problem-table target in exact arithmetic, but the two are sums over different terms (utility exchanger duties vs. the cascade), so they differ by a few ulps (1.2e-15 relative) in whichever direction the platform's arithmetic rounds. A bare `>=` on a bound that holds with equality is platform-dependent; this compares against the target less 1e-9 of the total duty, the tolerance the sibling phase-change test already uses. The other bare `>=` against a target (test_synthesis_info) is a best_effort case with a strictly positive penalty, so it keeps a real gap. Validated: full suite 467 passed locally against biosteam 3bacb0fe and thermosteam 3f943856 (the commits the PR's CI installs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
synthesize_networknow reaches the minimum-energy-requirement (MER) utility targets on every problem tested that can reach them without stream splitting. On split-needed problems it returns its best unsplit network, and that network never reports utilities below MER. The MER targets themselves are also corrected for phase change. Tests grow from 37 to 467, all green locally.What was wrong
1. The synthesizer missed MER on problems that don't need splitting.
T_min_app, so the cold stream never reached the pinch.2. MER targets were too low for streams that change phase.
vle(T=T_sat)is ambiguous at a phase boundary.3. Exchangers could cross internally without anything catching it.
HXprocesschecks the approach temperature only at its two ends.T_min_app, sometimes to a real temperature cross.The fix
hensmith/_curves.py(new, private): one temperature–enthalpy curve per stream.hensmith/_planner.py(new, private, pure numpy): a pinch-design-method planner (Linnhoff & Hindmarsh 1983).synthesize_networkis rebuilt on the planner.HXprocesswith both outlet limits set to its planned outlets.HXutilityat its far end.HeatExchangerNetwork(hensmith/_heat_exchanger_network.py):replace_unit_heat_utilities=Trueworks again. It was already broken before this PR, because it called a biosteam method that no longer exists.problem_table(..., curves=None)pinch_state(..., side=None, curve=None)synthesize_network(..., info=None)HeatExchangerNetwork.synthesis_info, whose'status'is'mer'or'best_effort'Validation
Full suite:
pytest . --disable-numba=1gives 467 passed (≈ 4 min locally; it was 37 tests in 17 s).New test sets (
tests/test_hxn_mer.py, case data intests/hxn_mer_cases.py):status == 'mer'status == 'best_effort'Both sets also check all balances: the network's energy balance, each exchanger's hot/cold duty and mass balance, each stream's outlet, and each exchanger's internal approach.
Other new tests:
tests/test_hxn_planner.pyandtests/test_hxn_targets.py.tests/test_hxn.pyandtests/test_hxn_regression.pygain integration tests and an internal-approach check.Outside the suite:
Real biorefineries:
Docs: rewritten for the new method. All generated outputs, figures, and the demo were regenerated, and
sphinx -Wbuilds clean.CI fix (
f914c4c):tests/test_hxn.py::test_non_monotone_stream_is_a_point_load, withassert 7366735.070962861 >= 7366735.07096287.>=on a bound attained with equality is platform-dependent.test_internal_pinch_of_condenser_against_boiling_mixtureand the regression suite (MER_RTOL = 1e-9) already use.>=against a target (test_synthesis_info) is abest_effortcase with a ~33% strict gap, so it is left as is.Against the upstream that CI installs (all local on Windows, Python 3.14):
3bacb0feand thermosteam3f943856(0.54.2). These are the exact commits this PR's CI installs from git HEAD. The baseline against the previous biosteam7ff69657/ thermosteamf768d381was also 467 passed.master: 37 passed against the same upstream.hensmith>=0.1.2(on PyPI), and hensmith'sbiosteam>=2.54.0floor still holds.Changes to review
tests/test_hxn_regression.py): cases 04 and 10 were raised. Their old documented loads could only be reached through the internal crosses above, and were below the corrected targets. Cases 05, 08 and 09 were lowered.Commits
454d71badd a pinch-outward planner that reaches MER on unsplit-feasible problemsce17233correct MER targets for phase change and synthesize networks with the MER planner1f1db9ftest MER identification and achievement on 78 no-split and split-needed problems25d0684feed point loads their equilibrium inlet, stop registry spam, fix replace_unit_heat_utilitiesf9931cadescribe the MER planner and the corrected targets in the docsf914c4callow round-off in the point-load test's MER lower bound🤖 Generated with Claude Code