fix HXN pinch analysis: energy-consistent problem table - #254
fix HXN pinch analysis: energy-consistent problem table#254sarangbhagwat wants to merge 5 commits into
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ut], point loads for isothermal streams temperature_interval_pinch_analysis built the heat cascade from interval enthalpies that were (1) evaluated by flashing hot streams at the *shifted* temperature T - T_min_app, so a stream entering within T_min_app of its dew point was already liquid and lost its latent heat, (2) never clipped to the stream's actual [H_in, H_out], so non-equilibrium column outlets inflated interval duties, and (3) assigned with the wrong sign to streams whose outlet temperature moves against their duty. Targets were impossible (hot target above the un-integrated heating load on the class doctest; hot - cold != net demand on sugarcane). The cascade is now built by problem_table(): each monotone stream is walked once down the shifted grid with a warm-started copy flashed at the real temperature, exact at its own end points and clipped in between, so its contributions telescope exactly to its duty; isothermal and non-monotone streams are point loads at their outlet temperature. Hence hot_util_load - cold_util_load == sum(unit_duty) and both targets are non-negative by construction. Signature and return value of temperature_interval_pinch_analysis are unchanged. Validated by tests/test_hxn.py: per-stream and net energy identities on the doctest system and a 4-stream case at dT = 5/10/20 K, two-stream closed forms, point-load handling, and the synthetic network reaching MER. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… warning, clean restart after failed flash
Three review fixes to the problem_table helpers added on this branch:
1. _stream_H_at_boundaries compared grid temperatures to T_hi/T_lo with
float equality to detect a stream's own end points; this only worked
because Ts was built from the same floats. Assign Hs[0]/Hs[-1] by
position instead (the caller always passes T_hi first, T_lo last, and
monotone streams have T_hi > T_lo strictly so the slice has >= 2
entries, asserted), and loop only over the interior boundaries.
2. The VLE-failure warning printed {stream!r} for an anonymous inlet
copy, giving no way to trace which stream failed. Pass the inlet
stream's own ID (set by the wrapper to s_<i>__Util_<i>) into the
helper and name it in the warning.
3. On a failed flash, the warm-started copy was left in whatever state
the failed VLE call put it in and reused for the next boundary. Re-copy
stream_in in the except branch so the next boundary restarts clean.
Also rewrote the comment above the pinch_T_arr loop in
temperature_interval_pinch_analysis, which claimed non-monotone streams
get pinch = T_in "as before this fix" -- not true in general (e.g.
T_out < T_in <= pinch_cold_stream_T for a cold stream gave T_out/pinch
under the old rule). The comment now states the actual rule (streams
already on one side of the pinch, including non-monotone ones, are not
split; load_duties assigns their whole duty to one side) without
claiming equivalence with prior behavior.
Covering tests unchanged (tests/test_hxn.py + hxn doctests):
12 passed in 13.36s.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…re its point loads The cascade residual[k] = sum(point_H[:, :k+1]) + sum(interval_H[:, :k]) is the heat *leaving* boundary Ts[k], after that boundary's point loads. Testing only this for non-negativity lets a hot point load at Ts[k] (an isothermal condenser, a phase-changing HXutility) mask a deficit in the cold interval (Ts[k-1], Ts[k]) directly above it - but a source at Ts[k] cannot serve a sink above Ts[k]. Example: a condensing stream at 400 K (shifted 395 K) against a cold stream heated 392 -> 398 K returned a hot utility target of zero; the correct target is the cold 395-398 K segment. The target is now the minimum over both the arriving flow (residual - point_total) and the leaving flow at every boundary, which is the standard problem-table treatment of point loads. Also: cold_util_load is clamped at zero in the threshold branch (residual[-1] could be negative by a rounding-level amount, and the tests assert non-negativity); problem_table/ProblemTable are exported in __all__ with an Examples doctest (two-stream threshold case). Note on synthesis behaviour (unchanged by this commit, introduced with the wrapper rewrite): non-monotone streams (outlet temperature moving against the duty) get pinch_T = T_in in pinch_T_arr, so load_duties places their whole duty on the hot side. The old code gave a meaningless negative dH2 for such streams when T_in was on the wrong side of the pinch. The table counts them as point loads at T_out; reconciling that with the synthesis heuristic is a separate item. Validation: regression test test_problem_table_point_load_cannot_heat_above_itself; tests/test_hxn.py + hxn doctests 14 passed (class doctest unchanged); canonical suite 71 failed / 476 passed / 62 skipped with the same pre-existing failure set as the baseline; tests/test_biorefineries.py 1 failed (test_cornstover, baseline) / 4 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… utility negative The threshold branch could leave cold_util_load = residual[-1] negative by up to 1e-9 * scale; clamping it to zero alone would break hot_util_load - cold_util_load == sum(unit_duty) in relative terms (the identity the tests assert with rtol=1e-9). Absorb the rounding into hot_util_load instead, so both loads are non-negative and the identity is exact. Also round the problem_table doctest's cold utility to 10 kJ/hr so it does not demand ten significant digits from a VLE enthalpy across Python versions. tests/test_hxn.py + hxn doctests: 14 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Effect on heat recovery (old vs. new problem table)Same systems simulated with the old and new
The sugarcane/lipidcane results look like they worsened, but the old networks there actually "beat" the corrected MER, so they weren't actually feasible. This is an issue in With the corrected table: sugarcane recovers 43.99 of the 44.07 MJ/hr the MER allows (at MER) and lipidcane 41.3 of 43.9 (94 %), with no infeasible exchangers. In feasible heat, recovery improves on every case (sugarcane ≈ +14 %, lipidcane ≈ +6 %, synthetic +39 %, doctest unchanged). Downstream effect (all-streams configurations): sugarcane utility cost +5.5 %, TCI −0.7 %; lipidcane +1.6 % / −0.2 %. The default configurations, which integrate a hand-picked subset, are unchanged. Follow-ups this surfaced (out of scope here): |
Problem
temperature_interval_pinch_analysis(biosteam/facilities/hxn/hxn_synthesis.py) built the heat cascade from interval enthalpies that wereT − T_min_app, so a stream entering withinT_min_appof its dew point was already liquid and lost its latent heat;[H_in, H_out], so non-equilibrium column outlets inflated interval duties;The resulting utility targets were physically impossible — the hot target exceeded the un-integrated heating load on the class doctest system, and
hot − cold ≠ Σ unit_dutyon sugarcane — so the pinch temperatures handed tosynthesize_networkwere wrong (304 K vs 359 K on sugarcane).Fix
The cascade is now built by a pure function
problem_table(streams_inlet, streams_quenched, is_hot, T_min_app) -> ProblemTable:T + shift. Its enthalpies are exact at its own end points (assigned by index) and clipped to[H_in, H_out]in between, so its interval contributions telescope exactly tosign · |H_out − H_in|.Tcannot serve a sink aboveT, and checking only the leaving flow let such loads mask a deficit in the interval directly above them.hot_util_load − cold_util_load == Σ unit_dutyand both targets are non-negative by construction (rounding in the threshold branch is absorbed into the hot target to keep the identity exact).temperature_interval_pinch_analysisis now a thin wrapper; its signature and 12-tuple return are unchanged, andsynthesize_network/HeatExchangerNetworkare untouched.problem_table/ProblemTableare exported with anExamplesdoctest.Synthesis-side note: non-monotone streams get
pinch_T = T_ininpinch_T_arr, soload_dutiesplaces their whole duty on the hot side (the old code produced a meaningless negativedH2for them whenT_insat on the wrong side of the pinch). Reconciling that with the point-load treatment in the table is left to the planned synthesis-heuristic work.Validation
tests/test_hxn.py(new tests):per-stream and net energy identities on the doctest system and a 4-stream synthetic case at
ΔT_min= 5/10/20 K (also: a target can never exceed the un-integrated load; the public wrapper reports the same targets);two-stream closed forms (threshold problem; pinch at the cold inlet);
isothermal / non-monotone streams handled as point loads;
regression for the point-load masking case (condensing stream at 400 K vs. a cold stream 392 → 398 K: correct hot target = the 395–398 K segment, pinch 395 K);
the synthetic 4-stream network reaches the corrected MER.
HeatExchangerNetworkclass doctest unchanged (same pinch on that system; ratio still 0.82).Full suite (
pytest . --disable-numba=1 -m "not slow"): identical failure set tomaster— stack drift, missing optional deps, tutorial notebooks — nothing new.tests/test_biorefineries.py::test_sugarcane(full HXN synthesis) passes;test_cornstoverfails with the same pre-existing numbers as onmaster.🤖 Generated with Claude Code