Repository navigation
Add per-stream approach contributions, a targeting toolkit and supertargeting - #3
Open
sarangbhagwat wants to merge 52 commits into
Open
sarangbhagwat wants to merge 52 commits into
sarangbhagwat wants to merge 52 commits into
Conversation
Each process stream can carry its own approach temperature contribution c_j [K]; a match between hot stream h and cold stream c must then keep c_h + c_c instead of T_min_app (Kemp 2007). A stream without a contribution contributes T_min_app / 2. Stream j is shifted down by s_j = T_min_app/2 + c_j if hot and T_min_app/2 - c_j if cold, so T*_h >= T*_c iff T_h - T_c >= c_h + c_c. The problem table, the planner and the splitting work unchanged on that one shifted scale, so the targets, the pinch rules and Theorem M carry over. Every real-temperature step takes its own pair's requirement s_h - s_c: the exact check, the repair, the HXprocess dT guard and the reported margin. Each stream's pinch temperature is pinch_T + s_j. Utilities are unaffected. Values are None or nan (unspecified), or finite and > 0, which keeps every pair requirement and the dT guard positive. This is the numerics work package (WP1 of the F1 spec). - _planner: _contribution_value, _approach_contributions, _approach_shifts (the default is still the literal np.where(is_hot, T_min_app, 0.)). _stream_curves takes a shift. plan_network(approach_contributions=...) adds plan.info 'shift' and 'min_approach_margin' only when contributions are given, and reports min_approach per pair. - hxn_synthesis: problem_table / _problem_table(approach_contributions=...) with grid['contributions']. - hxn_synthesis: _pinch_analysis and temperature_interval_pinch_analysis resolve contributions before any copy or flash, and give per-stream pinch temperatures. - hxn_synthesis: _exact_approach, _repair and _realize take `required` (a scalar, or the shift vector, length-checked); helpers _required_approach and _approach_margin. - hxn_synthesis: synthesize_network(approach_contributions=...) takes a sequence aligned with hus for now (mapping and callable keys follow in WP2). It always passes grid['shift'] to the exact steps. It adds info['min_approach_margin'] and info['approach_contributions'] (stream order). - Docstrings updated wherever they said every match keeps T_min_app. problem_table has a new doctest. Default-off results are bit for bit unchanged. With no contributions the shifts are the old expression, and every pair's requirement float(s_h - s_c) is T_min_app as the same double. Evidence: - the out-of-tree oracle (29 facility syntheses, every number as float.hex) gives sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf before and after (byte-identical JSON); - the four golden plan digests ffff603a8687fd88, 9a2b7cbfa36b0791, 07eeacf5af328127 and 57b9addff180e07e are unchanged; - the new tests compare None, [None]*N, [nan]*N and [T/2]*N with the default in-process with float.hex and tobytes. New tests/test_hxn_contributions.py (23 tests, 18 s serial): - default bit-for-bit identity of plans and problem tables; - uniform contributions against a doubled T_min_app (planner) and against uniform real-thermo runs, plus monotone bounds; - targets and pinch against an independent closed-form cascade with classic symmetric shifts (40 random problems, KEMP, a corpus case); - the 24 constant-CP NO_SPLIT cases (walked balances and per-pair end approaches); - five split cases at MER with splits; - the per-pair exact check, repair and realization; - value and sequence validation. Full suite: 981 passed (958 existing + 23 new) in 202 s with -n 4. The module doctests pass. ast.parse(feature_version=(3, 12)) is OK on both modules and the test file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
Approach temperature contributions can now be given by unit, which is
how they are meant to be set on a flowsheet, and through the
HeatExchangerNetwork facility. This is WP2 of the F1 spec; WP1 took only
a sequence aligned with hus.
- hxn_synthesis: synthesize_network(approach_contributions=...) accepts a
mapping, a sequence or a callable, checked in that order; anything else
raises a TypeError.
- Keys name units in three ways: the unit itself (or one with the same
(owner.ID, ID), so a stale auxiliary that its owner rebuilt still
matches), the tuple (owner.ID, unit.ID), or the ID of a unit without an
owner.
- A heat utility takes the key nearest to its unit along the _owner chain:
'D1' covers a column's condenser and reboiler, and ('D1', 'condenser')
overrides it for the condenser. None and nan are an explicit default at
their level.
- Rejected keys and values:
- heat utility keys raise a TypeError with a hint, because biosteam
replaces heat utilities at every simulation;
- other key types raise a TypeError;
- keys that name no unit raise a ValueError ("name no unit");
- two keys at the same level with different values raise a ValueError;
- bad values raise a ValueError naming the key.
All of this runs before any stream is copied or flashed. A callable is
called once per heat utility, in order.
- New private helpers: _names_unit, _contribution_key_level,
_check_contribution_key, _owner_chains, _mapping_contributions and
_unit_name. _stream_contributions dispatches to them.
- HeatExchangerNetwork(approach_contributions=None) takes a mapping or a
callable; a sequence raises a TypeError because the facility's stream
order is internal. The attribute can be reassigned between simulations,
and a class attribute covers older instances.
- The facility checks keys against every candidate unit (the units and
their nested auxiliary units, new _get_units and _candidate_units)
before it filters the heat utilities. A bad key, value or conflict
therefore raises even while its unit is idle. A valid key for an idle
or ignored unit has no effect.
- _cost resolves the contributions once per simulation and passes them to
synthesize_network. A cached network is reused only while the
contributions resolve to the same values per stream
(_resolved_contributions, None by default). Without explicit
contributions every reuse decision is unchanged, and T_min_app is still
not part of the cache key.
- Docstrings: the synthesize_network parameter (all forms) and the r002
mapping doctest ('mer', 74/444 kW, every dT 6.999999). In
HeatExchangerNetwork: the T_min_app line, the new parameter, the Notes
wording, a Notes paragraph "Approach temperature contributions", the
cache paragraph, and a doctest with per-pair dT guards (7 and 4 K). The
doctest outputs are pasted from real runs.
Default-off results are bit for bit unchanged:
- the out-of-tree oracle (29 facility syntheses, every number as
float.hex) gives sha256
dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf,
unchanged;
- the golden plan digests ffff603a8687fd88, 9a2b7cbfa36b0791,
07eeacf5af328127 and 57b9addff180e07e are unchanged;
- new in-process tests compare the facility with None, {}, {u: nan},
lambda hu: None and {u: T/2} against the default with float.hex and
tobytes.
tests/test_hxn_contributions.py gains 15 tests (38 in all, about 25 s
serial):
- default networks bit for bit (r002, case_05, split smith2005_ex18_2);
- uniform contributions in the facility against a doubled T_min_app;
- a contribution that reproduces the T=7 shifts gives that network bit
for bit;
- facility split and unsplit cases against the contribution-aware problem
table, with per-pair exact approaches and dT guards;
- per-stream pinch temperatures (with and without sort_hus_by_T);
- key resolution on auxiliary units, stale HXutilities keys and the
quickstart facility simulated twice;
- validation, before any flash and for idle units;
- the cache rule.
Full suite: 996 passed in 261 s with -n 4 (981 before, plus 15).
Sphinx -W html build in a scratch venv: exit 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
Document the approach_contributions option of the two previous commits. - README: one sentence after the stream-splitting sentence. - concepts: a section "Approach temperature contributions" between the problem table and the targets. It covers the convention and the shifts, the value domain and why it is > 0, and the bit-for-bit default. It says why the planner and splitting guarantees carry over (one shifted scale; a branch keeps its parent's shift), and what is per pair: the exact check, the repair, HXprocess dT and min_approach_margin. It also covers the per-stream pinch temperatures, utilities following BioSTEAM's rule, the facility keys (levels, robust ID keys, no heat-utility keys, the skip rule after validation, a pure callable), the cache rule, and additive requirements only. - concepts, elsewhere: the wording that every match keeps T_min_app now says the pair's required approach. The cache paragraph, min_approach_margin, a validation paragraph on tests/test_hxn_contributions.py, and the limitation now titled "Additive approach temperatures only". - API page: an approach_contributions row; T_min_app as the default pair requirement; the cache_network, synthesis_info and pinch_Ts rows. - Tutorial chapter 4: a section on the quickstart system, with 'D1' and F1 at 1 K and the coolers at the default 2.5 K. Its numbers come from the new capture ch04_contributions.txt and are compared with the 5 K and 2 K networks of the sweep. Also the intro, a forward reference, "Other options" and the cache paragraph. - Contributing: an entry for the new test module. The _demo_src outputs table lists the new capture. The demo block follows the spec, plus one line that prints the network's heating and cooling loads, which the comparison with the sweep needs. Validation: - Only ch04_configuring.py was run. ch04_sweep.txt, ch04_ignored.txt, ch04_splitting.txt and ch04_ten_streams.txt are byte-identical, and ch04_contributions.txt is the only new asset. The two re-rendered chapter 4 PNGs differed by byte noise only (same data, different renderer) and were restored. - SPHINX_OFFLINE=1 sphinx-build -W -b html docs/source <scratch>/html (sphinx 9.1.0 from docs/requirements.txt, --system-site-packages venv): build succeeded, 0 warnings. - Full suite: 996 passed in 242 s (-n 4), as on the parent. Documentation only; no library or test code changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
hensmith.targeting tells what any network of a set of process streams can achieve before (or instead of) synthesizing one. This is commit A (F2 core) of the targeting spec; area, units, capital and the supertargeting sweep follow in B, the facility method and the network report in C, the docs in D. - PinchTargets holds the problem table of the streams. from_heat_utilities prepares them exactly as synthesize_network does, so Q_hot and Q_cold equal the synthesis's targets bit for bit; it takes approach contributions in every form synthesize_network takes, resolved with F1's helpers on the heat utilities as given. The constructor takes streams as problem_table does (contributions as a per-stream sequence). at() re-targets at another T_min_app on the same curves (no flash); unspecified contributions follow T_min_app/2, specified ones stay or, with scale_contributions, scale with T_min_app. - composite_curves (real or shifted) are built on the table's own grid knots by a vectorized composite builder, so they reproduce the table to rounding; grand_composite_curve gives the arriving and leaving heat at every grid temperature. - utility_targets places biosteam's agents on the grand composite curve: one level per agent from biosteam's own limit (agent.T -+ agent.dT -+ HeatUtility.dT, the exact rule without contributions, conservative with them), greedy allocation in list order on the constraint positions of the curve, the remainder to the first fuel agent or unserved with a RuntimeWarning. Each agent is priced as one biosteam heat utility over the stretch it serves (its level down to the next zero of the balanced curve), with fresh utility streams in a temporary flowsheet: costs are deterministic and the registry and the agents' stream dumps are untouched. - balanced_composite_curves and balanced_grand_composite_curve add the utilities (latent, sensible, fuel as a furnace at infinite T). - plot_composite_curves (real, shifted, balanced) and plot_grand_composite_curve (with the utility staircase) follow plot_pinch_diagram, with stable gids. - hxn_synthesis: _arrange_heat_utilities and _process_streams are factored out of _pinch_analysis and shared with targeting, so the two preparations cannot drift apart. No behavior change. - hensmith exports PinchTargets, plot_composite_curves and plot_grand_composite_curve (9 names). Validation: tests/test_targeting.py (15 tests, about 8 s serial): KEMP composites in closed form; consistency with the table on four real-thermo cases; contributions against the independent classic cascade, levels recomputed from the agents' attributes; reference A's allocation (17.189/32.811/27.628/12.372 kW), served stretches and balanced knots; list order, biosteam's dT, unserved remainders; KEMP's single LP against the facility's MP/chilled-water network; the furnace case (natural gas 16.009 kW, flue 508.991 K) and pricing isolation; gids of both plots. Three new doctests; hensmith.targeting joins the docstring citation test. Bit-identity oracle dcd15864... and the four golden digests are unchanged. Full suite: 1014 passed (996 + 18) in 245 s with -n 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…g sweep Commit B (F3 numerics) of the targeting spec: PinchTargets now targets what the energy targets cost, and supertarget finds the T_min_app of least targeted total annualized cost. - area_target: Bath's formula on the balanced composite curves, exact on the stretches between breakpoints of both curves (vectorized), real temperatures (approach contributions included), film coefficients per stream (number, stream-order sequence, or a mapping from stream indices or units resolved with F1's key levels; heat utility keys rejected like F1) and per agent. Fuel takes no area; a zero driving force gives inf with a RuntimeWarning, an unserved utility nan. - units_target: Euler's S - 1 per region between zeros of the balanced grand composite curve. Where the curve stays zero, the pieces on one side of the pinch count S - 1 per group linked by common streams (otherwise a zero-duty stream's grid knot alone added a unit, against the spec's "counts nowhere"); every spec reference is unchanged. - capital_cost_target: N shells of equal area with biosteam's own correlations (_exchanger_costs replicates HX._cost and _load_costs; inf and overflow give inf), one furnace on the fuel's utility duty. - summary prices once and adds the TAC (CRF from keywords, a tea, or 10 %/10 yr/8000 h); options are validated before any pricing. - supertarget sweeps at() on the reused curves (no flash) with grid refinement around the least TAC (spacing x 2/9 per round); Supertargets holds the records (to_frame, sweep[key], at_bound); plot_supertargets draws TAC, capital and operating cost with stable gids. - hensmith exports 12 names. Validation: tests/test_targeting.py 24 tests (about 12 s serial), 5 doctests in targeting.py. Bath 20.0/25.0 m2 on a balanced pair (indices, unit IDs, tuples, units, sequences), reference A 24.14438 m2, KEMP with film coefficients 56.139 and with contributions 64.2819 m2, all against an independent midpoint integration (agreement 2e-12, 5e-8, 1e-12); quickstart 5946.1 m2 and (5, 1) units; the eight units references plus zero-stretch cases; exchanger costs equal biosteam's on every KEMP exchanger (double pipe and floating head); furnace capital, TAC inf at 0 K, nan when unserved; CRF equal to solve_payment, tea precedence; the closed-form optimum 21.595 K found within 0.02 K; a 64-point scaled sweep with contributions; plots. Only targeting.py and its tests change, so the synthesis path is untouched. Full suite: 1025 passed (1014 + 9 tests + 2 doctests) in 197 s with -n 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
Commit C (facility and report) of the targeting spec: a simulated HeatExchangerNetwork can be targeted directly, and its synthesized network compared with those targets like for like. - HeatExchangerNetwork.targets(T_min_app=None): after a simulation, the PinchTargets of original_heat_exchangers in the network's stream order, with the approach contributions resolved against original_heat_utils (so replaced unit heat utilities do not matter); before one, those of the heat utilities the next simulation would take, sorted and prepared as the synthesis does. Bit for bit equal to synthesis_info's Q_hot_target/Q_cold_target; nothing is stored, the synthesis path, the cache key and _synthesis_options are unchanged. PinchTargets is imported lazily. - network_report(HXN, targets=None, **options): energy (process side), utilities by agent, unit counts, the network's area integrated along the exact stream profiles with the targets' film model (counter-current, process duty; a furnace takes none) beside biosteam's own sizing, the costs of the whole new network, TAC with summary's economics (operating hours default to the system's), and the targets' summary. Given targets must be of the same units in the same order, thermodynamic basis and current states. The utilities are grouped by hand rather than with HeatUtility.sum_by_agent, which loads agents (registry and stream dump side effects). hensmith exports 13 names. Validation: tests/test_targeting.py 27 tests (+3: facility targets bitwise before/after simulation, with contributions and with replaced heat utilities; the report on KEMP, 54.06 m2 equal to terminal-LMTD areas with the process duty of the medium pressure steam heater, 6 units against the estimate of 7, a no-match network, stale or mismatched targets; no side effects on the registry, the agents' dumps, a later furnace cost or a later synthesis's float(hx.Q).hex()), about 15 s with the doctests; 2 new doctests. Quickstart at 5 K: area 7462 m2 against a 5946 m2 target and biosteam's 5536 m2, utility cost 1839.61 USD/hr for both. Oracle digest dcd15864... and the four golden digests unchanged. Full suite: 1030 passed (1025 + 3 tests + 2 doctests) in 241 s with -n 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
Document hensmith.targeting, added by the three previous commits. - Tutorial chapter 5 (05_targeting.rst, script examples/ch05_targeting.py): the facility's targets (HXN.targets(), equal to synthesis_info's bit for bit; process side 2.828e+08 against the utility side 2.977e+08 kJ/hr of chapter 4, divided by eta 0.95), the utility targets with biosteam's agents, the composite, balanced composite and grand composite curves drawn by the library, the area, units, capital and TAC targets, the T_min_app sweep, and the network report beside its targets with the caveats. Every number comes from a capture. The script checks that the library's composites are chapter 2's as knot lists and that the grand composite arrays are equal. - The quickstart sweep's optimum is inside the sweep (1.848 K), but it sits at the threshold edge, not at a trade-off: the cold utility is zero up to it, and operating_hours=2000 does not move it. A short trade_off region therefore adds the spec's fallback, the closed-form pair of test_optimum_T_min_app_closed_form, with its optimum at 21.59 K and a second sweep figure. - API: a new targeting page, with PinchTargets and Supertargets :no-members: plus one automethod per public method, UtilityTarget, and the four functions. api.rst now says thirteen names and lists the seven new ones. The facility page gains the targets automethod and a sentence. - concepts: the curves are library functions; a subsection "Multiple utilities on the grand composite curve" (levels, list order, fuel last, the closed rule, levels with contributions, served intervals, biosteam pricing over eta); a section "Supertargeting: area, units, capital and total cost" (balanced composites, the Bath formula, units per region of the balanced GCC as an estimate, N C(A/N) with biosteam's correlations, TAC and CRF, the sweep and contribution scaling, the network report and its limits); a Validation paragraph on tests/test_targeting.py; the no-cost-optimization limitation now points to targeting, plus a new "Targets are estimates" bullet; three references. - Prose that became false: chapter 2 no longer says the curves are not library functions, chapter 4's sweep caption and the concepts caption point to chapter 5. - Wiring: build_all.py, the _demo_src README outputs table, the tutorial toctree and intro, "Where to next" of chapters 1-4, README.md, the landing page, contributing.rst (three public modules, targeting.py and test_targeting.py). One library change, cosmetic: plot_composite_curves(kind='balanced') drew a latent heating agent's label left of its flat, which runs off the axes over the tick labels when the flat starts near H = 0, as on the quickstart. The label now sits above the flat, ending at its warm end (a cooling label below its flat, from its cold end), where neither monotone curve can pass, with some headroom. Gids and data are unchanged. Validation: - Only ch05_targeting.py was run (13 s). git status shows only the new ch05 captures and figures; the captures are identical across two runs. - SPHINX_OFFLINE=1 sphinx-build -T -E -W --keep-going -b html (sphinx 9.1.0): build succeeded with 0 warnings; no unresolved cross-reference on the changed pages. - tests/test_targeting.py and the targeting doctests: 33 passed in 12 s. - Full suite (-n 4): 1030 passed in 221 s, as after the rebase onto dev (1030 passed in 207 s); no test changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…at any T_min_app F1-1 (fixed): the planner's contribution validation is now one helper, _given_contributions, which checks the length, then every value, then, only when a value is given, that T_min_app is finite and > 0; an all-unspecified vector (None or nan) returns None. _approach_contributions and _approach_shifts use it, and _approach_shifts then returns the literal default shifts (no x/2 + x/2 round trip, so subnormal or non-finite T_min_app cannot differ from the default). problem_table and plan_network therefore accept [None]*N / [nan]*N at T_min_app = 0, as their docstrings, the spec and concepts.rst say, and as synthesize_network, the facility and PinchTargets already did. PinchTargets now uses the same helper, so an all-unspecified vector of the wrong length raises instead of being ignored. New test test_unspecified_contributions_need_no_positive_T_min_app (T_min_app 0 and the smallest subnormal: plan and table bit for bit equal to the default, shifts literal); length checks for all-unspecified vectors in test_invalid_contributions and test_targeting::test_contributions. F1-2 (fixed): concepts.rst no longer claims the '> 0' rule keeps the HXprocess dT guard positive; it says zero is excluded for a positive driving force and that a pair whose contributions sum to 1e-6 K or less still gets a guard of zero or less, as T_min_app <= 1e-6 K does by default. F1-3 (fixed): test_split_cases_with_contributions_reach_mer checks info['min_approach'] and info['min_approach_margin'] against the walked match ends (within 1e-9), covering split-mode reporting with non-uniform contributions. F1-4 (fixed): test_exact_check_and_repair_per_pair takes each exchanger's expected dT guard from its pair (hot stream j), not from whether it was flagged. F1-5 (fixed, partly): test_planner_targets_match_classic_cascade also runs pair_ends_ok on the 40 random problems, so the validation paragraph of concepts.rst holds as written. The optional pinch_Ts case (a stream moved to one side of its own pinch by its own shift) is not added: it needs a new hand-built facility case with a computed pinch, more than the nit buys. Tests: test_hxn_contributions.py + test_targeting.py 67 passed; test_hxn_planner.py 96 passed; doctests of hensmith/_planner.py and hensmith/targeting.py 8 passed; the new test fails on the previous code. Oracle: 29 cases, sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf (unchanged). sphinx is not installed in this environment; the docs change is plain prose with inline literals only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…ons callable after a simulation F1F-1 (fixed): after a simulation, HeatExchangerNetwork.targets() called the approach_contributions callable on original_heat_utils, which replace_unit_heat_utilities had already overwritten (copy_like of the network's utility, or empty() for a stream the process exchangers serve). A duty-based callable gave other targets than the synthesis (15.5/55.5 kW instead of 6.5/46.5 kW on KEMP), an agent-based one raised AttributeError, and network_report contradicted itself. targets() now takes a callable's values from _resolved_contributions (the values the last simulation resolved, in original_heat_exchangers order) and never calls it again (F1 spec section 7). A Mapping, which depends on hu.unit only, is still resolved again, so a mapping changed after the run applies. The targets() Notes, the class docstring and concepts.rst say so. New test test_facility_targets_replaced_callable covers replace_unit_heat_utilities with a duty-based and an agent-based callable: targets() equal the synthesis targets bit for bit, the callable is not called, the report is consistent, a callable set since is ignored until the next simulation, and a mapping set since applies. Before the fix the test fails. F1F-5 (fixed): the HeatUtility-key TypeError no longer offers a sequence to facility users. A sequence is now named only for use outside a HeatExchangerNetwork. F1F-7 (fixed): the concepts.rst cache paragraph now says that T_min_app is never part of the reuse test, with or without contributions. A reused network keeps its unspecified streams at the T_min_app/2 it was synthesized with, as does synthesis_info['approach_contributions']. F1F-6 (not applied, optional nit): key resolution is quadratic only with about one key per unit (0.18 s per simulation at 400 keys and 400 units), which is small next to synthesis. Indexing keys by normalized name would also have to keep the error-reporting order and the str-key rule (top-level units only), and that is more risk than it buys here. Tests: pytest tests/test_targeting.py tests/test_hxn_contributions.py: 68 passed. Doctests of hensmith/_heat_exchanger_network.py and hensmith/hxn_synthesis.py: 5 passed. Default-off oracle: sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf (unchanged). SPHINX_OFFLINE=1 sphinx-build -W -b html: clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
TGC-1 (fixed): a sensible agent was priced by biosteam from the warm end of its stretch alone, so cooling water serving a condenser near the cold end of its stretch left above the condensing temperature (impossible flow, utility cost about 3x too low, balanced composites crossing, area and TAC inf). utility_targets now bounds each sensible non-fuel agent's outlet by its line against the process: at each position p of its stretch the agent takes at least x = q - B(p) below p (gives it above p, heating), so its outlet is at most agent.T + min_p (T(p) - dT - agent.T) q / x (at least agent.T - min_p (agent.T - dT - T(p)) q / x), T(p) the lowest real temperature of the served hot streams present at p (highest of the cold ones). biosteam is called with the inlet that reproduces the bound, so its own rule still applies. Exact without approach contributions (the line stays inside the grand composite curve), conservative with them. Reference A, test 9 and every documented number except one are unchanged; the vacuum condenser at 316 K now costs 1.98 USD/hr (the series-flow minimum, outlet 311.15 K) with a finite area. The Bath warning says "non-positive driving force"; concepts.rst, the class Notes, the UtilityTarget fields and tutorial ch05 describe the rule. New tests: test_sensible_line_condenser (316 K binds, 330 K keeps the limit; finite area at 2, 10, 20 K) and test_sensible_line_constant_cp (closed forms for cooling water and for a hot oil against a concave cold composite). TGC-2 (fixed): T_in/T_out are now the real temperatures of the served streams (cold streams with heat above the pinch, hot streams with heat below it) overlapping the stretch: coldest and hottest for heating, hottest and coldest for cooling. T_out is within the agent's limit by construction (10 of 24 random contribution cases violated it before, 0 now), the uniform case gives the stretch's ends where streams reach them (440 K, not 445 K, for medium pressure steam on H1 455->300, C1 330->440). I used the coldest cold-end temperature for heating rather than the suggested max over the streams' bottoms: the max would raise uniform fuel costs whenever a cold stream starts inside the furnace's stretch. With contributions the fuel flue is therefore priced from the coldest served stream (lower than the old largest-shift offset). Latent costs are unchanged. New test: test_pricing_temperatures. TGC-4 (nit, fixed): _isolated_pricing restores biosteam's flowsheet registry, so no '_hensmith_targets' flowsheet lingers (asserted in test_pricing_is_isolated). TGC-5 (nit, fixed): test_composites_consistent_with_table also checks the utility targets of the real-thermo cases: duties add up to Q_hot/Q_cold, the balanced GCC is non-negative and zero at the pinch, the balanced curves end at the same heat, the area is finite, with no RuntimeWarning. Docs: ch05 regenerated alone; only ch05_sweep.txt (15 K: area 5168 -> 5158 m2, TAC 15690339 -> 15694680 USD/yr, cooling water bound by a cold stream starting at 298.15 K) and tutorial_05_supertargets.png changed. Tests: pytest tests/test_targeting.py hensmith/targeting.py: 38 passed. On the previous code test_sensible_line_condenser[316], test_sensible_line_constant_cp, test_pricing_temperatures and test_pricing_is_isolated fail. Oracle unchanged (sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf). Sphinx -W (offline) clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…te tac, test sensible lines across process streams TGS-1 (fixed): the root cause, a sensible agent priced from the end of its stretch alone so that its line crossed the grand composite curve, was already removed by the line limit of 48f0447 (tg-core TGC-1): each sensible agent's outlet is bounded by the duty it must have taken at each position of its stretch. On the reported two-stream problem (H1 518.9 -> 272.3 K, 4.99 kW/K; C1 297 -> 450.3 K, 4.85 kW/K) chilled water now leaves at 293.48 K at 1 K, and the area is finite: 162.2, 128.9, 115.1 and 102.9 m2 at 1, 2, 3 and 5 K (was inf, 190.7, 143.3 and 113.6). The balanced curves' smallest driving force is at least min(T_min_app, dT) there, on the cooling water cases of the verifier and on 720 random 2-6 stream evaluations from 0.5 to 20 K. What was missing is the regression test: the new test_sensible_line_process_in_stretch sweeps 1-10 K on that problem and on its heating mirror (a custom hot oil serving C1 290 -> 470 K across H1 up to 450 K): no RuntimeWarning, a finite area that falls with T_min_app, the smallest driving force at least min(T_min_app, dT), and the agent's outlet and cost against closed forms (where the opposite process stream starts). Both cases give inf on df45d85. TGS-3 (fixed): P is validated by a new _pressure (a finite real number, not a bool, > 0; a ValueError naming P) in _check_options, so supertarget, summary and network_report fail before pricing anything, and in capital_cost_target and _exchanger_costs. P=nan no longer yields a nan optimum, P=-1e6 no longer costs less, P=True and P='2 bar' are rejected with a message naming P. The capital_cost_target docstring says P > 0. Cases added to the test_sweep validation loop and to test_capital_target_formula (capital_cost_target, summary and _exchanger_costs). TGS-4 (nit, fixed): a new _lmtd, used by _bath and _profile_area, takes the log of the ratio as log1p((d1 - d2) / d2) for a ratio between 0.5 and 1.5 (where the difference is exact) and d1 at equality: the relative error at d2 = d1 (1 + 3e-12) drops from 1.5e-5 to 0. test_lmtd_nearly_equal checks it against a 60-digit Decimal reference to 4e-16. No documented number changes (ch05 regenerated alone: no capture or figure differs). TGS-5 (nit, fixed): test_exchanger_costs_match_biosteam_design compares _exchanger_costs with a biosteam HXprocess costed from the same design results at 8 points: A = 1 (F_l < 1), 2.1718, 149.999, 150, 2000, 5000 and 1e4 ft2, at 14.7, 150 and 600 psi, every shell and tube type and five materials. TGS-6 (nit, fixed): supertarget warns (RuntimeWarning) when no point of the sweep has a finite TAC, as its optimum is then the smallest temperature and meaningless; documented in its Warns section and in concepts.rst, and tested with the cooling unserved (and its absence on a normal sweep). TGS-7 (nit, fixed): the class Notes quote the jump at 150 ft2 as biosteam gives it at its default CE of 567.5 and 1 atm: a base purchase cost of about 3,211 USD just below 150 ft2 to 20,516 USD for a floating head, an installed cost of about 8,055 to 64,658 USD. Tests: pytest tests/test_targeting.py hensmith/targeting.py: 48 passed; hensmith/_heat_exchanger_network.py doctests and test_docstring_references_are_cited pass. On df45d85, test_sensible_line_process_in_stretch fails (RuntimeWarning, area inf). Oracle unchanged (sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf). Sphinx -W (offline) clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…readable TGR-1 (no change needed): already fixed by df45d85 (F1F-1). After a simulation, HeatExchangerNetwork.targets() takes a callable's values from _resolved_contributions and never calls it on the heat utilities that replace_unit_heat_utilities overwrote. The reviewer's case6 (a duty-based callable with replacement) now gives (2, 2, 6, 6) and Q_hot 39600, equal to synthesis_info, and test_facility_targets_replaced_callable covers duty-based and agent-based callables together with network_report. TGR-2 (fixed): network_report now calls a new _check_network on both the default and the explicit path. It checks that every stream of the network enters the network (its copy of the real inlet at the entry port) and leaves its last stage at the end states of its curve in the targets. The network's copies share the curves' thermodynamic basis, so this also works under force_ideal_thermo. A stale network raises a ValueError ("... not of the current states of C1 ...: simulate the system"). This covers a stream or a unit changed without simulating the system. _check_targets no longer returns early under force_ideal_thermo: it compares the curves with ideal copies of the exchangers' streams. A shared _same_ends also compares the inlet pressure and flows, so a flow change at the 298.15 K enthalpy reference (H = 0) is caught. On the reviewer's corpus, the network-vs-curve mismatch in fresh states (KEMP, replace, cache after a feed change, the docstring liquids with and without splitting, ideal thermo and cache, the quickstart real and ideal with cache, 8 SPLIT and 8 NO_SPLIT cases) is at most 4e-11 of a stream's duty, against tolerances of 1e-9 (inlet) and 1e-6 (outlet). The docstring's Raises section and the 'T_min_app' entry (the attribute as it is now) say so, and so does concepts.rst. TGR-5 (fixed, all four): (a) On the balanced plot, the fuel label stacks above the top flat's label when both anchor at the end of the finite hot curve, with more headroom. (b) kind='shifted' marks the pinch only where the composites touch (both reach pinch_T). There is no marker for a threshold problem whose composites end apart or for streams of one side. An empty composite stays out of the legend. (c) An unserved remainder is labeled in gray in the upper left corner ('Utility:unserved': "unserved cooling 4.45E4 kJ/hr (not drawn)"). (d) The heat axis of both plots uses scientific tick labels from 1e4 on (a plain ScalarFormatter only). The GCC labels sit just below a heating step's end and just above a cooling step's end, off the curve. docs/_demo_src/examples/ch05_targeting.py was run alone to regenerate the docs. Only tutorial_05_grand_composite.png changed (the label offsets): the other figures and captures are byte-identical. TGR-6 (fixed): new tests in tests/test_targeting.py: - test_network_report_independent_areas integrates each exchanger independently: flashes of copies of its own streams at 41 duty points, log-mean in between. It runs on the split liquids network (branches 0.52/0.48, rtol 1e-4) and on the quickstart network, where process and utility exchangers boil and condense (rtol 1e-3; the curves' glide tolerance accounts for about 3e-4). The quickstart system becomes a module fixture shared with `quickstart`. - test_network_report_current_states covers a cached network re-simulated after a feed change (areas against terminal LMTD, targets equal to HXN.targets()) and stale T, flow and unit-alone states on the default path. It also covers a flow change at the enthalpy reference (explicit and default) and stale targets and a stale network under force_ideal_thermo. - test_plot_readability covers the four plot fixes, through text bounding boxes, legend entries and the GCC polyline in display coordinates. Replacement with a callable, plus the report, was already covered by test_facility_targets_replaced_callable. Before the fix, test_network_report_current_states and test_plot_readability fail. Tests: pytest hensmith/targeting.py tests/test_targeting.py: 51 passed (18 s). Doctests of hensmith/_heat_exchanger_network.py: passed. The default-off oracle sha256 is unchanged at dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf (the synthesis path is untouched). SPHINX_OFFLINE=1 sphinx-build -W -b html: clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…costs the code gives DOCS-2 (minor, fixed): ch05 now defines a heating agent's level as its temperature less biosteam's approach (the agent's dT plus HeatUtility.dT), 412.19 - 0 - 5 = 407.19 K for low pressure steam, and a coolant's level as its temperature plus that approach, moved onto the shifted scale by subtracting T_min_app, 280.37 + 2 + 5 - 5 = 282.37 K for chilled water, matching _levels, concepts.rst and the ch05_utilities capture. DOCS-3 (minor, fixed): the PinchTargets capital note now quotes the purchase cost the code returns at the 150 ft2 jump (about 5,486 to 20,138 USD for carbon steel at 1 atm, CE 567.5) and the installed cost the target uses (about 8,055 to 64,658 USD), instead of the correlations' base costs. DOCS-4 (nit, fixed): "as the end of this page does" now reads "as the trade-off example below does". DOCS-5 (nit, fixed): "ends the hot utility target ... beyond the end of the hot one" in the ch05 composite-curves caption and the PinchTargets Notes. DOCS-6 (nit, fixed): concepts.rst, the PinchTargets Notes and the code comment say each agent's priced stretch starts at its level or at the end of the grid, whichever is nearer, as the code clips it. DOCS-7 (nit, fixed): the dpi parameter of plot_composite_curves, plot_grand_composite_curve and plot_supertargets documents "Defaults to 300." Text only: no computed result, capture or figure changes, and the synthesis path is untouched. Tests: pytest hensmith/targeting.py tests/test_targeting.py: 51 passed. sphinx-build (docs/requirements.txt) of docs/source: no warnings other than the unreachable intersphinx inventories (no network in this environment). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…er the system's hours, explain the flat threshold E2E-1 (fixed): PinchTargets.__init__, from_heat_utilities and _from_exchangers (and so at() and HeatExchangerNetwork.targets) now check T_min_app with supertarget's rule before any target is computed: a finite number >= 0, not a bool (> 0 with approach contributions, as before). nan, inf, -10, '5' and True raise a ValueError naming the value, where they used to return wrong energy targets or fail later. The docstrings say so. E2E-2 (fixed at the root): PinchTargets has a new settable attribute `system`, None by default. HeatExchangerNetwork.targets sets it to the facility's system, and at() carries it over. When neither operating_hours nor tea is given, summary (and so supertarget, through _check_options) reads the system's operating hours at call time, as network_report already did. A system with a TEA is priced at the TEA's hours everywhere, and supertarget(HXN.targets()) no longer optimizes for 8000 h. Without a system, or with a system that has no operating hours, nothing changes (the quickstart captures are unchanged). The tutorial (TAC bullet and report paragraph), concepts.rst, the summary, supertarget and HXN.targets docstrings, and the ch05 script comment now say this. E2E-3 (fixed): rewrote the tutorial paragraph and the figure caption. Below about 1.85 K the quickstart system is a threshold problem whose targets are flat. The difference of under 7 USD/yr (4e-7 of the TAC) between 1 and 1.848 K comes from where the problem table's grid falls on the curved profiles, not from physics, and any point of the stretch is an optimum. The real step is at 1.853 K, where the cold utility and a sixth unit add about 12000 USD/yr. The ch05 sweep table now prints that next point (regenerated ch05_sweep.txt by running ch05_targeting.py alone; no figure changed). supertarget's Notes and concepts.rst state that a flat stretch gives an arbitrary optimum. No tie tolerance was added, so the sweep's results stay as they are. E2E-4 (no change needed): already fixed by 1729e59 (tg-report). The fuel label stacks above the top flat's label when both are anchored at the end of the finite hot curve. Re-running the verifier's v_e4.py gives a fuel box (y 926-967 px) above the high pressure steam box (y 876-917 px), and test_plot_readability covers this case. Nits: E2E-7 (fixed): utility_targets, summary, supertarget and network_report accept agent IDs. Each ID is resolved among bst.HeatUtility.heating_agents (or cooling_agents). An unknown ID raises a ValueError that lists the known ones. A bare string or a single agent in place of a sequence raises a TypeError. E2E-8 (fixed): from_heat_utilities raises a ValueError if a heat utility has no unit (System.heat_utilities) and names the unit heat utilities to pass instead. E2E-9 (fixed): a sweep with no finite TAC is not refined, so it evaluates the given grid only and does not repeat warnings on nan. The existing RuntimeWarning (and its docs) says so. Supertargets keeps an optimum (the smallest point), because the warning already marks it as meaningless. E2E-10 (no change needed): already fixed by 48f0447 (tg-core). _isolated_pricing removes the temporary flowsheet from biosteam's registry on exit, and the reviewer's side.py now reports no change to the flowsheets. Tests: new test_T_min_app_validated, test_agents_by_ID and test_facility_targets_operating_hours, plus a refinement check in test_sweep. pytest tests/test_targeting.py hensmith/targeting.py hensmith/_heat_exchanger_network.py: 56 passed. Default-off oracle: sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf (unchanged). sphinx -W (docs/requirements.txt, SPHINX_OFFLINE=1) of docs/source: clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
… units option once F1F2-1 (fixed): a unit key named every unit with its (top owner ID, ID), so a key on one auxiliary of an auxiliary also named its namesakes under other intermediate owners (the heaters of two auxiliary flash drums), and two such keys conflicted. A unit key now names that very unit; only a key that is no longer one of the units at hand (a stale auxiliary its owner rebuilt) names the unit of the same ID path (its ID and those of all its owners), so a rebuilt HXutilities exchanger still matches, also nested. The same rule applies to the film coefficient keys of targets (h). Conflict and value messages name a nested unit with its intermediate owners. The docstrings, concepts.rst and the API page say that a tuple key, with the top owner, names every auxiliary of that ID under it, and that the heater objects or the drums tell them apart. New test test_contribution_keys_on_nested_auxiliary_units (two-flash unit, keys and conflicts, h keys of targets, the facility, a stale auxiliary of an auxiliary). F1F2-2 (fixed): _cost and targets evaluate the units option once and pass it to both the heat utilities and the candidate units, so a callable is called once per simulation and a one-shot iterable is not exhausted before the keys are checked. F1F2-3 (fixed): new test test_contribution_keys_with_the_units_option (list, callable and one-shot iterable units=: a key on the idle D1.pump simulates, a key on D1_H2 outside units= raises 'name no unit', the callable is called once). Tests: test_hxn_contributions.py 42 passed; test_targeting.py -k "bath or film or h_ or facility or targets" 20 passed; doctests of hxn_synthesis, _heat_exchanger_network and targeting 11 passed; oracle sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf (unchanged); sphinx -W (SPHINX_OFFLINE=1) clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…vels only by streams in reach TGC2-1 (fixed). _ends decided which streams a utility serves with the strict test (top > lo) & (bottom < hi). That dropped a stream whose only heat in the stretch is a point load at its end, such as a boiler at the pinch with the cut 'above' or a condenser at it with the cut 'below'. The same mask filtered the streams given to _line_limit. With no stream left, _ends returned the stretch's shifted ends as real temperatures. In case_two_stream low pressure steam reported T_out = 395 K, H1's shifted inlet; C1 leaves at 373.124 K. With contributions a sensible agent was also priced inside dT of the stream it serves (the cooling water of a condenser at 320 K left at 319 K, 0.154 instead of 0.218 USD/hr). Membership now comes from each stream's heat in the stretch. The new _position_H gives each stream's enthalpy at the positions of the grand composite curve; it is factored out of units_target, which is unchanged bit for bit. A point load at a grid temperature counts where the stretch holds both its arriving and its leaving position, as the balanced curve does. The streams with heat above the tolerance are kept, or those with any heat if none has more. If there are none, a RuntimeError is raised; shifted ends are never returned. The served streams' arrays go to _line_limit unchanged. The no-zero fallback, unreachable for an allocated agent, now ends the stretch at the pinch's position, so that lo and hi are always positions (_pinch_position, shared with units_target). TGC2-2 (fixed). _levels capped a level at R - s_j for every stream whose top passes R - s_j (heating), even one lying wholly beyond its limit, which no level short of it serves. Its contribution then moved duty to a dearer agent (LP steam 36 -> 33.2 kW, cooling water 27 -> 23.6 kW). Now such a stream caps the level only at its own bottom (top, for cooling), 2e-9 K short of it (_T_SHORT, beyond the snapping distance) if it has a point load there, since a level at a grid temperature serves that temperature's point loads. The level stays within the highest of the served streams' limits (the lowest, for cooling). Without contributions the level is still R (R - T_min_app for cooling), bit for bit. The class Notes, the _levels docstring and concepts.rst describe this rule. test_contributions recomputes the levels independently with it. TGC2-3 (nit, applied). _agents raises a ValueError for an agent of the other kind: a heating agent must be isheating_agent or isfuel, a cooling one iscooling_agent and not a fuel. Steam passed as a coolant used to be priced as a negative cost. Documented under Parameters and Raises of utility_targets. TGC2-4 (nit, applied). The tests' min_driving_force skips stretches no wider than 1e-9 of the heat, as _bath skips slivers. New tests: - test_point_loads_at_stretch_ends: case_two_stream's LP steam at C1's outlet temperature. A boiler at the pinch with a hot oil, and a condenser at the pinch with cooling water, each with no, one and two contributions: the served temperatures, the agent outlet dT away, the cost against an independent pricing, a driving force of the balanced composites >= min(process gap, dT), and the RuntimeError. - test_levels_of_streams_beyond_reach: B wholly above LP steam's limit, then wholly below cooling water's, at three contributions, gives the same levels, duties and costs as without them. A boiler wholly beyond LP steam's limit keeps the level just short of it. - test_agents_by_ID: covers the kind checks. The new tests and the kind checks fail on the previous code. Validation: pytest tests/test_targeting.py together with test_hxn_contributions.py::test_contribution_keys_on_nested_auxiliary_units passed (51 tests, 20.7 s). pytest hensmith/targeting.py ran 6 doctests, all passing. 150 random constant-CP cases with contributions, run with biosteam's agents and with a hot oil, showed no balanced driving force below min(process gap, dT) and no error. docs/_demo_src/examples/ ch05_targeting.py was rerun alone and changed no capture or figure. Python 3.12 syntax (ast.parse) checks out. The synthesis path is untouched: the oracle gives sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf, which matches. sphinx is not installed in this environment, so sphinx -W was not run; the docs changes are prose and a numpydoc Raises section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…in the capital target
TGS2-1 (fixed): the capital target divided the area equally over the N
shells of the units target, N*C(A/N). With biosteam's jump at 150 ft²
(about 8,100 USD installed for a double pipe just below, 64,700 for a
floating head at 150 ft²) an added unit could push every shell below
150 ft² and cut the target by most of it, so the sweep's optimum sat at
that switch (KEMP: 5.565 K, the TAC 4.7 times lower than 0.01 K below).
New _shell_split: 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 is the costlier to install of the N double pipes and of k equal
shell and tubes with N - k shells of no area (each a vanishing double
pipe, 1,305 USD installed). It is the equal division wherever the shells
stay above 150 ft² or the whole area is below it (so the tutorial's
large-stream sweeps are unchanged), and the least change to it that never
falls when a unit is added (checked on a grid for every type and
material; the only exceptions are biosteam's own: shells beyond about
20,000 ft² and the 'Double pipe' type). Results:
- KEMP 5.554 -> 5.560 K: 263,121 -> 326,790 USD (was -> 54,051), so it
ranks the networks (124,788 at 5.5 K, 239,130 at 5.565 K) the right way
round; the default sweep's optimum is 11.74 K, past a step of one
floating head (lower neighbor 1.25 times the TAC).
- supertarget's example pair at 10.05 K: 68,749 USD (one floating head and
two empty shells) against a network of 68,940 (was 21,458).
- 10 random problems: optima past one-shell steps of 1.06-1.12 times the
TAC (were 2.5-3.9 times at the switch).
Where it departs from the equal division it is high against networks
(they keep only part of their area in shell and tubes), as the equal
division was low; the Notes say so. I did not use the suggested min over
k >= N of k*C(A/k): it would replace shell and tubes by double pipes up to
about 2,900 ft² per shell (the tutorial's 3 x 2,583 ft² pair would become
52 double pipes), break the 9.9 K agreement of the example pair (15,300
against a 66,171 network) and put the KEMP optimum at the 1 K bound.
capital_cost_target returns the new 'N_area_shells' (the shells that share
the area; 'shell_area' is theirs). Declined: a flag or warning for an
optimum next to a step; the remaining steps are one shell and tube, which
networks have too, so it would fire on most small-stream sweeps; the
records now show them (TGS2-3) and the Notes of supertarget, concepts and
tutorial ch05 explain them. Docs: PinchTargets Notes and example,
capital_cost_target, summary, supertarget (Notes, example rewritten: the
optimum is the step where the area first fits in double pipes),
network_report and plot_supertargets examples, concepts.rst (capital,
sweep, estimates), tutorial ch05. New tests test_capital_target_split
(the rule and monotonicity on a grid) and test_capital_target_keeps_shells
(KEMP across 5.554/5.56 K, its sweep, the example pair at 9.9/10.05 K);
test_capital_target_formula updated (reference A: one floating head;
furnace case: two).
TGS2-2 (fixed): supertarget checks each entry of T_min_apps with _real, the
rule of PinchTargets.at: True, '10' and None raise ValueError ('each of
T_min_apps must be a finite number, not True'); any iterable of numbers is
accepted. Tested in test_sweep.
TGS2-3 (fixed): summary (so the sweep's records and to_frame) adds
'N_area_shells', 'shell_area' and 'shell_type' after 'N_shells'; the other
keys are unchanged. Tested in test_sweep.
Regenerated docs/source/_generated/ch05_summary.txt by running
docs/_demo_src/examples/ch05_targeting.py alone (it now prints the string
key as is); its other captures and figures came out byte-identical.
Tests: pytest tests/test_targeting.py hensmith/targeting.py
hensmith/_heat_exchanger_network.py: 60 passed. Default-off oracle: sha256
dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf
(unchanged; the synthesis path is untouched). sphinx-build -W
(SPHINX_OFFLINE=1) of docs/source: clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…ensible labels off the line, target reversed streams TGR2-1 (fixed): balanced_composite_curves now starts the cold composite at the unserved cooling (its coldest member, at level -inf, which is left out), so the curves stay aligned and the plot no longer draws a cross (REF_A at 10 K with cooling water only: min drawn hot - cold goes from -7.37 K to +5.0 K, the same as with every utility served). Unserved heating stays a gap at the hot end. Served-only results and the area (nan when unserved) are unchanged. Docstrings, class Notes and concepts.rst describe the gap. test_unserved_warns checks the new origin and the dT gap; test_plot_readability checks the drawn curves for LP + CW and default heating + CW. TGR2-2 (fixed): a sensible agent's label now sits below right of the rising cold balanced composite for cooling (offset (6, -4), va='top') and above left of the hot composite for heating (offset (-6, 4), va='bottom'). test_plot_readability checks that no 'Utility:' or 'Fuel' label box holds points of either balanced composite for the threshold, furnace, hot oil and liquids cases (it fails with the old placement). tutorial_05_balanced_composites.png was regenerated with ch05_targeting.py; it is the only asset that changed. TGR2-3 (fixed): after a simulation, HXN.targets() takes each stream as hot or cold from its current direction (outs[0].H - ins[0].H; the heat utility may have been replaced). A stream with no duty keeps its synthesis side. Right after a synthesis this agrees with cold_indices, so the targets still equal synthesis_info bit for bit. The new test_facility_targets_reversed_stream covers C1 turned into a cooler, simulated alone and through a reused cached network; the targets now match from_heat_utilities (Q_cold 1.116e6 kJ/hr). TGR2-4 (nit, documented): the PinchTargets.heat_utilities attribute and the HXN.targets Notes say that, after a simulation with replace_unit_heat_utilities, these are original_heat_utils and hold the network's utilities. TGR2-5 (nit, fixed): plot_supertargets adds ' (at the bound)' to the optimum label when sweep.at_bound is set, and leaves out the marker and dashed line when the optimum TAC is not finite. Tested. TGR2-6 (nit, fixed): a zero duty in a composite plot label now reads '0' (new _format_duty in targeting; _format_H and the grid diagrams are unchanged). Tested on the threshold real and shifted plots. Tests: pytest tests/test_targeting.py tests/test_hxn_targets.py hensmith/targeting.py hensmith/_heat_exchanger_network.py: 103 passed. Oracle sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf (29 cases) unchanged. ch05_targeting.py rerun alone: only the balanced composites PNG changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…ps as the cause of exceeding the units target DOCS-R2-1 (fixed): the Units bullet of tutorial ch05 put the quickstart network's 7 exchangers against a target of 6 down to per-stream utility exchangers. It now names the real cause: above the pinch D1_H1 heats both the reboiler (HX_0_2_hs) and the flash's feed heater (HX_1_2_hs), and each also has a steam heater (Util_0_hs, Util_1_hs), so the four close a loop that Euler's S - 1 does not count (it counts the steam as one stream of any degree). The lists of causes in ch05, concepts.rst (the units paragraph and the network-report limits) and the units-target and network_report Notes in targeting.py now say that S - 1 assumes one component and no loop per region, and that each loop of matches adds one unit: a repeated match (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. A split alone is no longer listed as a cause, since it adds no edge between distinct streams by itself. The concepts wording of the "fewer units" case now matches network_report's (one agent serving a stream whose heat the target splits between two utility levels), which removes the contradiction with ch05. DOCS-R2-2 (fixed): the untraceable "2e-5" area variation in ch05 is replaced by what the capture shows (the area is 6836 m2 at both 1 and 1.848 K); no capture or figure changes. DOCS-R2-3 (fixed): the API targeting page's colon is now a full stop. DOCS-R2-4 (fixed): rewrapped the overlong prose lines of the HeatExchangerNetwork Notes, the network_report Returns entry and concepts.rst's test-suite (iii)-(v) sentence (the PinchTargets Notes line was already wrapped; the remaining long lines are doctest output). Tests: pytest hensmith/targeting.py hensmith/_heat_exchanger_network.py (doctests): 8 passed. sphinx -W (SPHINX_OFFLINE=1) of docs/source: clean. Only docstrings and rst changed, so the synthesis path and the default-off oracle are untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…k is_hot, keep dT-free sensible agents off point loads at their limit TGC3-1 (fixed): PinchTargets.from_heat_utilities now raises a ValueError, naming the units, for any heat utility with duty and hxn_ok False. Units add these with Unit.add_heat_utility (reactors, mixers, compressors, ...), and their unit's ins[0] -> outs[0] need not be the stream being heated or cooled. The docstring, the hu.unit-is-None error message and tutorial ch05 now recommend bst.process_tools.heat_exchanger_utilities_from_units( sys.units), the selection HeatExchangerNetwork makes. Zero-duty utilities are still dropped. Not done: the suggested check of hu.unit_duty against outs[0].H - ins[0].H. With replace_unit_heat_utilities the facility writes the network's utility over an original exchanger's heat utility, so unit_duty can rightly differ from the streams, which are what the targets use. hxn_ok is biosteam's own flag, and in biosteam only HXutility sets it. New test test_from_heat_utilities_takes_exchangers: a heated two-feed mixer next to two HXutilities is refused in either order, and the recommended selection gives the facility's targets (HXN.targets()). TGC3-2 (nit, fixed): a sensible agent's level now stays _T_SHORT short of a served point load that would leave its line no driving force, at agent.T - dT (agent.T + dT for cooling). That is a stream's own limit when agent.dT is 0. The next agent serves the point load, as biosteam's strict rule does. Before, the agent's outlet equalled its inlet, so its flow was unbounded (6e10 kmol/hr) or biosteam raised FloatingPointError. This cannot trigger for agents with agent.dT > 0 or for latent agents, so default results are unchanged. Documented in the class Notes and concepts.rst. New test test_sensible_agent_without_driving_force, for heating and cooling; it fails without the fix. TGC3-3 (nit, fixed): PinchTargets raises a ValueError when is_hot contradicts a stream's direction (sign of H_out - H_in beyond the table's own tolerance). A stream without duty may be either. Documented under is_hot. New test test_is_hot_validated. Tests: pytest tests/test_targeting.py hensmith/targeting.py (with doctests): 62 passed. tests/test_hxn_contributions.py -k nested_auxiliary_units: 1 passed. sphinx -W (SPHINX_OFFLINE): clean. oracle sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf (unchanged; synthesis path untouched). No docs numbers change, so no captures were regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…ut new figures TGR3-1 (fixed): plot_grand_composite_curve put every step label right of the step's end, so the last steps (which end at the largest heats) ran past the axes and the figure. Each label now goes on the side of its end with more room in the axes: right of it toward the pinch as before, 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, so the curve is never there. Keeping the label below a heating level left of the end, as suggested, would cover the curve where a level meets it at its end (the fuel flat, for one). A y margin of 0.08 makes room for a label beyond the outermost level. The figures created by all three plot functions now use layout='constrained', so axis labels are not cut without a tight bounding box. This also moves KEMP's cooling_water label off the curve. test_plot_readability now asserts that every label is inside the axes and off the curve, on the documented side, and that the axis labels are inside the figure, for REF_A, FURNACE, liquids, KEMP, threshold, hot-only and quickstart. The balanced labels get the same inside-axes check, and the supertargets test checks its axis labels. The ch05 figures were regenerated. TGR3-2 (fixed): the 'hot utility' label of the real and shifted composites is now right-aligned at the right one of the hot ends, 4 pt above the top of the curves. The 'cold utility' label is likewise offset in points, not kelvin. Both are tested inside the axes and off the curves on KEMP, hot-only, threshold and quickstart. TGR3-3 (fixed): the network_report Notes now say it integrates along the streams' profiles as the targets' curves model them (a point-load stream at its outlet temperature, a non-equilibrium end state as its jump), like for like with the area target. concepts.rst limits the independent-area match to equilibrium streams. Tests: pytest hensmith/targeting.py tests/test_targeting.py: 62 passed. The synthesis path is untouched, so the oracle was not run. sphinx is not installed in this environment; the rst change is plain prose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…mum, drop fixture and private-helper references DOCS-R3-1 (fixed): ch05 no longer says the refinement "happens to report the last one it evaluated". The new text says the area, and with it the TAC, falls steadily across the threshold stretch as the grid moves. The least TAC is therefore always the warmest point evaluated below the threshold, here 1.848 K, 0.005 K short of the 1.853 K step. concepts.rst and the supertarget Notes no longer call the optimum "an arbitrary point". They say its position has no physical meaning, and that when the discretization trend is steady it lies at an end of the stretch (the warmest point evaluated, for the quickstart system). No numbers changed, so no captures or figures were regenerated. DOCS-R3-2 (fixed): concepts.rst now says "the four-stream problem of Linnhoff & Hindmarsh at 5.56 K" in place of the test fixture name KEMP. DOCS-R3-3 (fixed): the PinchTargets attributes 'grid' and 'cut' are now described inline (the grid keys Ts, shift, contributions, Hl/Hr, k_hi/k_lo, and the meaning of 'below'/'above'). They no longer point to the private _problem_table and _pinch_cut. Only prose and docstrings changed; the synthesis path is untouched, so the oracle was not rerun. Tests: pytest hensmith/targeting.py (doctests) 6 passed; tests/test_hxn.py -k "docstring_references or supertarget or pinch_targets" 1 passed; tests/test_targeting.py -k "supertarget or grid or cut" 1 passed. sphinx is not installed in this environment, so the -W build was not run here (the edits add no new markup). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
… keys, check utilities against the targets
E2E-R3-1 (fixed): a contribution key that names an auxiliary its unit may
build but does not hold in a simulation is now valid and has no effect,
instead of raising from sys.simulate(). This covers an old auxiliary object
whose top owner is at hand, and (owner.ID, name) or (owner.ID, 'name[k]')
for a name of the auxiliary_unit_names of that owner or of one of its
auxiliaries (new hxn_synthesis._names_unbuilt_auxiliary and _unknown_keys).
Typos still raise (e.g. ('X1', 'heat_exchanger[2]'), ('X9', ...)).
Updated the docs to match: the facility Notes, synthesize_network,
concepts.rst and the API page. New test: an HXutilities outlet T changes
480 -> 440 -> 400 -> 480 K between simulations, with the tuple key and the
old-object key.
E2E-R3-2 (fixed): PinchTargets gains a key_units attribute (None by default,
copied by at()). HXN.targets sets it to the facility's candidate units, and
_film_mapping checks h keys against them and against unbuilt auxiliaries,
as the facility checks contribution keys. So an h key of a unit that is
idle or ignored in a sample has no effect in area_target, summary,
network_report and supertarget, while unknown keys still raise. HXN.targets
now reads the units option once in both branches. Documented this in the h
parameter docs and the targets Returns. New test with a unit that falls
idle, with and without ignored.
E2E-R3-4 (fixed): new PinchTargets._check_utilities, used by
_balanced_segments, _balanced_positions and _area (so by area/units/capital
targets, the balanced curves and the balanced composite plot). It raises a
ValueError unless the heating duties (unserved included) add up to Q_hot
and the cooling ones to Q_cold, within 2*_tol + 1e-9*|Q|.
plot_grand_composite_curve is left unchecked, since it may draw a subset.
Documented the requirement in each utilities parameter and in tutorial
ch05. New test: utilities of another T_min_app, an empty list, subsets and
a duplicate all raise; the targets' own utilities pass, including
partially unserved ones.
E2E-R3-5 (nit, applied): the supertarget Notes, concepts.rst and tutorial
ch05 now say that each point builds its own problem table, and suggest a
coarser T_min_apps or a smaller refine for large systems.
E2E-R3-6 (nit, applied): supertarget silences the per-point "no ... agent
can serve" warnings and emits a single "N of the M minimum approach
temperatures evaluated leave part of a utility target unserved" warning.
Tested.
No numbers in the docs change, so no captures or figures were regenerated.
Tests: tests/test_targeting.py + tests/test_hxn_contributions.py 101 passed;
doctests of hensmith/targeting.py, hxn_synthesis.py and
_heat_exchanger_network.py 11 passed; the 4 new tests fail without the
source changes. Oracle oracle_r3.json sha256
dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf (matches).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…ids as keys of unbuilt auxiliaries
F1F4-1 (fixed): _names_unbuilt_auxiliary accepted both 'name' and
'name[k]' for every name of auxiliary_unit_names, so ('X1',
'heat_exchangers'), ('D1', 'condenser[0]'), ('F1', 'heat_exchanger[3]'),
('D1', 'reboiler[7]') and ('D1', 'pump[0]') were silently valid no-ops
(as approach_contributions keys and as PinchTargets h keys, which share
_unknown_keys). It now follows biosteam's naming rule (Unit.auxiliary):
'name[k]' only while the unit's attribute `name` holds a list, the bare
'name' only while it does not, and either while it holds nothing (None
or missing). Class Notes, concepts.rst and the helper docstring state
which form applies to which attribute. Tests: ('X1', 'heat_exchangers')
moved from the accepted keys to the raising keys (contributions and h),
and indexed IDs of the quickstart column's and flash's single
auxiliaries now must raise.
F1F4-4 (nit, applied): test_contribution_keys_with_the_units_option no
longer presents a one-shot iterator as a supported units option (it is
exhausted on a second simulation, as at 5ef9a35); the case and its
comment are dropped.
Tests: tests/test_hxn_contributions.py + tests/test_targeting.py 101
passed; doctests of hxn_synthesis.py, _heat_exchanger_network.py and
targeting.py 11 passed; oracle sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf
(unchanged); sphinx -W (offline) clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…at can reach, warn on replaced heat utilities TGC4-1 (fixed): utility_targets now sets each non-fuel agent's level inside the greedy allocation, after the agents before it are placed. A stream caps the level only if it is in reach: it has heat above the highest zero of the current balanced curve above the pinch temperature (below the lowest zero below it, for cooling). That zero is a utility pinch left by an earlier agent or another process pinch. A level at or past that zero gets no duty, and heat from a level beyond it never passes it. The upper bound of the level is still the highest (lowest) limit of all served streams, so levels with no such zero are unchanged. Without contributions every level is still R (R - T_min_app for cooling), so default results are unchanged. _levels was split into _level and _in_reach, and _allocate now takes the agents. The class Notes and concepts.rst state the rule. New test test_levels_of_streams_past_a_zero, which fails without the fix. Heating, Ck at c = 0.5: medium pressure steam now serves Ctop's 20 kW (447 -> 449 K, biosteam's own choice), not high pressure steam. Cooling, second process pinch at shifted 300 K: cooling water serves 16.28 kW of H2 down to 312.372 K, the same whether H1 ends at 308 or 313 K. TGC4-2 (fixed): PinchTargets.from_heat_utilities emits a RuntimeWarning naming the units whose heat-exchanger heat utilities have a unit_duty that differs from their stream's outs[0].H - ins[0].H by more than 1e-6 relative. These are the heat utilities that a HeatExchangerNetwork with replace_unit_heat_utilities=True has written over or emptied. On the quickstart, column and KEMP systems without replacement the largest mismatch is 1e-13; replaced ones are off by 8 % or more. HXN.targets() never takes this path after a simulation, so it is unaffected. The hus docstring (plus a Warns section), both ValueError messages and tutorial ch05 now say that after such a simulation the selection holds the network's utilities: use HXN.targets(), or build the targets before that simulation. New test test_from_heat_utilities_after_replaced_heat_utilities (KEMP at 10 K): the selection warns and gives only C1 and H4, every unit's own heat utility warns with all four named, and without replacement there is no warning and the targets match the facility's. Tests: pytest tests/test_targeting.py hensmith/targeting.py hensmith/_heat_exchanger_network.py (with doctests): 68 passed. tests/test_hxn_contributions.py -k nested_auxiliary_units: 1 passed. oracle sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf (unchanged). docs/_demo_src/examples/ch05_targeting.py rerun alone, with hensmith.targeting RuntimeWarnings as errors: no generated capture or figure changed. sphinx is not installed in this environment, so the -W build was not run; the docs edits are prose and a numpydoc Warns section of the kind utility_targets already has. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…ssed utility TGR4-1 (fixed, documented): network_report keeps biosteam's utility specification, so an exchanger whose utility line crosses its stream's profile (cooling water heated to its 324.82 K limit against steam that condenses just below it) has an inf 'area', and so has the network. That is the honest answer for the network as built: sizing it for more of the utility would be another network at another utility cost than the one the report prices, and leaving it out of the total would understate the area. Now: - the RuntimeWarning says why: "<ID> transfers heat at a driving force of -4.35 K: its cooling_water from 305.37 to 324.82 K, as biosteam specifies it, meets or crosses the profile of H1"; - the network_report Returns say 'area' is inf if an exchanger's is and that 'area_biosteam' is sized on terminal differences, and a Notes paragraph gives the cause, the agents' limits, that area_biosteam, costs, TAC and the area target stay finite, and why the report keeps biosteam's specification; - concepts.rst and ch05 say the same in their report paragraphs; - test_network_report_crossed_utility builds the vacuum condenser on a real network, checks independently (flashes of the outlet) that the water crosses the stream inside the exchanger with positive terminal differences, and checks the warning, the inf areas, the finite biosteam area, costs and targets, and the targets' water line below the dew point. TGR4-2 (nit, applied): concepts.rst and ch05 now say the report integrates along the streams' profiles as the targets' curves model them, not along the exact stream profiles. No number shown in the docs changes, so no capture was regenerated; the synthesis path is untouched (report code only). Tests: pytest tests/test_targeting.py 61 passed; pytest hensmith/targeting.py (doctests) 6 passed; sphinx-build -W clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…ult pair requirement F1N-R5-1 (fixed): synthesize_network passed the shift vector grid['shift'] as the pair requirement even with no contribution given, so _required_approach's float(shift[h] - shift[c]) promoted a numpy float32 T_min_app to float64 in the exactness limit, the repair and every HXprocess dT guard, changing default results relative to 5ef9a35. The default path (values is None) now passes T_min_app itself, as given (`shift = T_min_app if values is None else grid['shift']`); _exact_approach, _repair and _realize already accept a scalar, and the contribution path is unchanged. The comment there now says why. New regression test test_default_float32_T_min_app_keeps_its_own_arithmetic (r002, case_05_boiling_cold, smith2005_ex18_2_split) checks every exchanger's dT against float32 T_min_app - _APPROACH_TOL by float.hex (and that this differs from the float64 guard), and that contributions unspecified in any form ({}, all nan, a callable returning None, None) give the same network bit for bit; it fails on the previous code for all three cases. The reviewer's f32.py script now prints output identical to the 5ef9a35 tree. Tests: oracle sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf (29 records, unchanged); tests/test_hxn_contributions.py plus the hensmith/hxn_synthesis.py doctests: 49 passed; tests/test_hxn_regression.py: 20 passed. No docs numbers change (float and int T_min_app were already bit-identical). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…loads at the pinch
TGC5-1 (fixed): the duties check let utilities of another T_min_app
through wherever the duties do not depend on it (streams of one kind,
threshold problems below their threshold), and their cooling levels were
then read on the wrong shifted temperatures (negative balanced GCC,
wrong units). UtilityTarget gets a trailing field `shift` (default None,
additive): the PinchTargets.shift of the targets that placed it, as a
tuple. _check_utilities now also raises a ValueError when a utility's
shift is not None and is not the targets' shift, which covers the
balanced composite curves, the balanced GCC and the area, units and
capital targets alike. Recording the shift rather than T_min_app and
pinch_T compares what the levels are actually placed on: it catches
other approach contributions, and it accepts targets with the same
shifts (at() of the same T_min_app, or all-cold problems, whose shifts
are 0 at every T_min_app and whose results are the same). A check of
B.min() alone would not have caught utilities of a larger T_min_app
(their levels are too low, so B stays non-negative but the units are 3
instead of 4), and it would not have covered the area. A utility built
by hand (shift None) is still checked by its duties only. The
UtilityTarget, method parameter and tutorial ch05 docs say so. New test:
test_utilities_of_another_shift (one-sided and threshold problems with
the utilities of at(0., 5., 20.) raise for every method; at(10.)'s and
hand-built ones give the targets' own results). It fails if the shift
check is bypassed.
TGC5-2 (fixed): new test_point_loads_at_the_pinch_follow_the_cut. The
p8 condenser at the pinch (cut 'below') gives no cooling water, chilled
water serves all of Q_cold, and every cooling T_out is above agent.T +
agent.dT + HeatUtility.dT. case_two_stream (cut 'above') gives a units
target of {'above': 2, 'below': 2}. Fails under the reviewer's
MUT=sidecut and MUT=pinchpos mutations.
TGC5-3 (nit, fixed): new test_level_at_a_point_load_up_to_rounding,
parametrized over steam limits 1e-11 K below, at and above a boiler's
grid temperature: the steam serves all of Q_hot. Fails under MUT=snap
at -1e-11 K.
No docs numbers change, so no captures or figures were regenerated. The
synthesis path is untouched. Oracle sha256 is still
dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf.
Tests: pytest -n 8 tests/test_targeting.py tests/test_hxn_targets.py:
108 passed. pytest hensmith/targeting.py (doctests): 6 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…ever on it TGS5-1 (fixed): supertarget evaluated np.linspace(lo, hi, n_refine + 2) inside the bracket of the least TAC and skipped the candidate that fell on the optimum itself, so on a symmetric bracket (any evenly spaced grid) an odd n_refine lost a point per round and n_refine=1 refined nothing. Each round now puts its n_refine new temperatures on both sides of the least TAC, never on it: with it they cut the bracket into n_refine + 2 gaps, shared by its two sides in proportion to their widths, each side evenly spaced (all on the one side at a bound of the sweep, as before). The docstring says so, with the spacing after r rounds now about (2 / (n_refine + 2))**r of the grid's, (1/5)**r for the defaults. New test test_sweep_refinement: on the 1..40 K grid each n_refine in (1, 2, 3, 8, 9) adds exactly n_refine points per round (refine=1 and 3), none on the optimum, evenly spaced per side; n_refine=1 adds one record; a lopsided bracket and an at-bound sweep place theirs as described. Regenerated outputs: the supertarget doctest (30 points, optimum 14.96 K), the plot_supertargets doctest (64 points, 7.816 K), docs/_demo_src ch05 run alone (ch05_sweep: optimum 1.849 K; ch05_trade_off: 21.59 K, capital 71606.7, operating 20031 USD/yr; their two figures) and the matching numbers of tutorial ch05. TGS5-2 (nit, applied): concepts.rst now gives the exception to "never falls when a unit is added": shells beyond about 35,000 ft² (3,300 m²) for the default floating head of carbon steel, from about 15,000 ft² by type and material, whose cost rises faster than their area. The class Notes of PinchTargets say the same in place of "about 20,000 ft²" (thresholds measured with _shell_split over every type and material: 14,091 to 45,940 ft², 33,031 to 45,940 for the default). TGS5-3 (nit, applied): capital_cost_target documents 'type' None with an infinite or nan area too. Tests: pytest tests/test_targeting.py 67 passed; pytest hensmith/targeting.py (doctests) 6 passed; docs/_demo_src/examples/ ch05_targeting.py ran alone with its asserts; oracle sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf (unchanged, synthesis path untouched). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…n, place the legend where it is clear
TGR5-1 (fixed): _same_ends compared the network's outlet with the curve
to 1e-6 of the duty only, which a rigorous utility exchanger's flash
cannot reach on a large stream that changes temperature by micro-kelvins
(duty still above Qmin), so network_report rejected a freshly simulated
network and sent the user to re-simulate. The outlet now passes within
the larger of 1e-6 of the duty and the heat of a micro-kelvin of the
outlet stream (1e-6 * |C|, evaluated only when the relative check
fails); a stale outlet still moves H by far more. New
test_network_report_micro_kelvin_stream (20000 kmol/hr of water cooled
by 1e-6 K, duty -1.5 kJ/hr): the report succeeds right after the
simulation and again after a re-simulation, and a set point moved by
0.1 mK is still detected as a change of states (fails before the fix).
TGR5-2 (fixed): plot_composite_curves places its legend at loc='best'
instead of the fixed lower right corner, which covered 29-67 % of the
cold composite of threshold problems that need only cooling. On every
other case checked (REF_A, FURNACE, PAIR, KEMP, liquids, hot-only,
cold-only, threshold) 'best' leaves the legend off the curves, the band
and the labels; the docs' quickstart keeps the lower right, so
docs/_demo_src/examples/ch05_targeting.py, re-run alone, regenerates its
figures byte-identical. Documented in the Notes. test_plot_readability
now checks that the legend lies inside the axes, off both composites and
off the utility labels, also on the cooling-only threshold problem
[('H1', 420, 330, 3), ('C1', 300, 340, 1)] (fails with the old corner).
TGR5-3 (nit, applied): new parametrized
test_network_report_cached_split_network on liquids with
stream_splitting, cache_network and replace_unit_heat_utilities, with
and without approach contributions: the report's targets equal the
facility's and synthesis_info's at the synthesis, and after a 5 % feed
change and re-simulation of the reused network each area agrees with
independent_area to 1e-4.
Tests: pytest tests/test_targeting.py hensmith/targeting.py -> 76 passed.
Synthesis path untouched (oracle not needed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
… flat sweeps, shade non-finite stretches E2E-R5-1 (fixed): the utility cost target is the cost of biosteam's placement of its agents (list order, fuel last), not a minimum. With biosteam's default prices natural gas is cheaper per kJ than any steam for a furnace heating from below about 565 K, so a network at MER that heats a stream ending above the hottest steam with fuel over its whole stretch costs less than the target. The docs said only that the network can use costlier agents. Reworded the concepts.rst comparison limits bullet (both directions, with the cp case: 3.14 against 4.56 USD/hr), its "List order and fuel" paragraph and its "Targets are estimates" bullet, the Notes of network_report (only the energy targets bound the network) and of PinchTargets, the utility_targets summary, the tutorial ch05 remark after the report, and "any network" in the module docstring, README, API page and ch05 intro. New test test_network_report_cheaper_than_utility_target pins the case (status mer, equal Q_hot, natural gas over the whole stream, the per-kJ prices, 3.14 vs 4.56 USD/hr, TAC below its target, the 565 K crossover). E2E-R5-2 (nit, applied): plot_supertargets shades each stretch without a finite TAC (gid 'Nonfinite', halfway to its finite neighbours, one legend entry), so the axis keeps the whole sweep. Nothing is added when every TAC is finite, so the docs figures are unchanged. E2E-R5-3 (nit, applied): a sweep whose TACs are all finite and the same to a relative 1e-12 (at least two points) is not refined, and Supertargets.constant flags it; its repr says "TAC constant over the sweep" and the plot "(TAC constant)" in place of "at the bound" (at_bound itself is unchanged). New test test_sweep_constant. The synthesis path is untouched (no oracle run). Tests: pytest hensmith/targeting.py: 6 passed; pytest tests/test_targeting.py: 72 passed; sphinx -W (SPHINX_OFFLINE=1): clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…lace, take holders as owners
F1F6-1 (fixed): a tuple key (owner.ID, name) or (owner.ID, 'name[k]') of
the ID that biosteam.Unit.auxiliary gives an auxiliary is now valid only
where the attribute holds no unit (None, missing, a non-unit object, or a
list shorter than k + 1) or holds the unit of that very ID. Where it holds
a unit of another ID (EnzymeTreatment's heater is ('T1', ''); the high-rate
wastewater units use '<ID>_hx'), the key raises 'name no unit' / 'h keys
that name no stream' instead of being silently ignored, and the message
names the held unit's key ("T1.heat_exchanger is ('T1', '')"). Keys of
held units with biosteam's own IDs outside the facility (('D1', 'pump'),
('D1', 'reflux_drum')) stay valid. The k of 'name[k]' must be a plain
decimal (no leading zeros). New helpers in hxn_synthesis:
_auxiliary_slots, _unknown_key_hints. Class Notes of
HeatExchangerNetwork and concepts.rst no longer say that (owner.ID, name)
is the ID BioSTEAM gives every auxiliary; they say where the key raises.
F1F6-2 (fixed): key resolution now derives owners from the units at hand
without mutating user objects (_held_owners, _parent, _top,
_key_universe): an auxiliary without an _owner among the auxiliary_units
of a unit at hand (the facility's candidate units; outside the facility,
the units of the heat utilities and their owner chains) takes that unit
as its owner in _unit_name, _id_path, _names_unit,
_contribution_key_level, _owner_chains and the unbuilt-auxiliary check,
for approach contributions and PinchTargets h keys alike. So for a
MultistageCompressor K1, 'K1' covers its intercoolers and ('K1', 'K1_H1')
names one in every sample, whether or not K1 was simulated before
System.from_units and however often pr changes, and the bare 'K1_H1'
always raises (now with the hint "'K1_H1' is ('K1', 'K1_H1')"; str keys
of auxiliaries get that hint in general). Documented in the class Notes
and concepts.rst, including that an old intercooler object that never had
an owner names nothing and raises (ID keys are the robust form).
Tests: new test_keys_of_auxiliaries_under_other_IDs (EnzymeTreatment and a
ListedHeaters unit with '<ID>_hx<k>' list entries; contributions, h keys,
in and outside the facility) and test_keys_of_auxiliaries_held_without_an_owner
(MultistageCompressor, both build orders, pr 2 -> 2.2 -> 2, h keys,
owners left unchanged).
- pytest hensmith/hxn_synthesis.py hensmith/_heat_exchanger_network.py
hensmith/targeting.py tests/test_hxn_contributions.py: 59 passed
- pytest tests/test_targeting.py: 72 passed
- oracle (default-off): sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf (unchanged)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
… line, test the cooling short level TGC6-1 (fixed): new test_sensible_line_streams_of_other_shifts pins the hottest cold (coldest hot) stream rule of _line_limit with approach contributions: a hot oil serving C1 (shift 4 K) and C2 (shift -4 K), and its cooling mirror with a coolant serving H1 (shift 6 K) and H2 (shift 14 K). Each asserts the closed-form outlet (bound where the low-shift stream ends, x = 13 of 155 kW), the isolated price, and, from an independent cascade (new helper line_gaps), that the line keeps exactly dT from each served stream at its own shift. New test_sensible_line_point_load_alone pins the _T_EQ tolerance of the presence mask: a pure saturated condenser alone at the cold end of cooling water's stretch bounds the outlet at the condensing knot (CW.T + (T_c - dT - CW.T) q / Q_cond), and the balanced composites keep dT. Mutants (max/min swapped in either branch, strict mask) now fail. TGC6-2 (fixed): test_levels_of_streams_beyond_reach gains the cooling mirror of the boiler case: a condenser at 311 K (contribution 1 K) with H 340 -> 300 K (9 K), between CW.T + dT and cooling water's limit. Cooling water's level lies strictly within 1e-8 K above the condenser's shifted temperature, its T_out - dT > CW.T + CW.dT, it serves H above its outlet only, and chilled water serves the condenser and H below. Dropping the cooling `+ _T_SHORT * point` now fails the test. TGC6-3 (nit, applied): dropped the no-op threshold branch of _side_heat (a threshold problem already has k_pinch = 0 and the cut 'below'); its docstring says why all heat is below. Tests: tests/test_targeting.py, tests/test_hxn_targets.py and the hensmith/targeting.py doctests: 122 passed. Each of the five mutants (heating and cooling max/min swap, strict mask, cooling and heating short-level offsets) fails at least one targeted test. Oracle sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf (unchanged). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…xes, label the furnace at the top of the cold curve TGR6-1 (fixed): the labels of the balanced composite curves are placed when drawn, at the figure's final layout, by a picklable textcoords (_Inside): a label that would run past the edge of the axes is moved just inside them and kept off the composite it labels (raised over the hot curve under its new span for heating, lowered under the cold curve for cooling); a label that would run into the label of a lower heating (higher cooling) agent is moved on past it. The fuel label now sits above the hot end of the cold balanced composite (at the top of both curves), where the furnace heat closes the curve, as the 'hot utility' label of the real/shifted plots does, stacked over any heating label it would overlap; headroom 0.16 when a heating label is near the top. Labels that fit are placed exactly as before (the ch05 figures regenerate byte-identical). test_plot_readability: the FURNACE fuel anchor and stacking, plus the 'pyro' (short LP flat near the left edge) and 'fuel_only' cases among the balanced readable() cases, with no two labels overlapping, and the shifted LP label 2 points inside the axes. TGR6-2 (fixed, nit): a sweep whose finite TACs (at least two) are the same, the others nan, is no longer refined, and its repr and plot say 'TAC constant where finite' instead of 'at the bound'. Supertargets.constant is unchanged (every TAC finite). Tests: pytest tests/test_targeting.py hensmith/targeting.py: 80 passed. The synthesis path is untouched (oracle not needed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
… at unowned stages outside the facility
F1F7-1 (fixed): a key (top.ID, f'{K.ID}_{ticket}{n}') of the ID that a
biosteam.MultistageCompressor K at hand gives its stage n (ticket K or H,
or that of the stages it holds) is now valid for any n >= 1, and names
nothing while K has fewer stages (new _names_unbuilt_stage, used by
_names_unbuilt_auxiliary, so the facility's approach_contributions and the
film coefficient keys of its targets both accept it). A sweep over
n_stages 3 -> 2 -> 3 -> 4 with ('K1', 'K1_H3') no longer raises at 2
stages. concepts.rst, the class Notes and the API row say so. New test
test_keys_of_stages_a_compressor_does_not_build (sweeps from 3 and from 2
stages, checks bad stage IDs still raise).
F1F7-2 (fixed): _unknown_key_hints now hints that a tuple key (o, x) of a
unit x without an owner is x, and that the stages a MultistageCompressor
of a str key's ID builds have no owner there (their IDs are their keys).
synthesize_network's mapping bullet and concepts.rst state that outside
the facility such an intercooler has no owner, so 'K1_H1' is its key.
F1F7-3 (fixed): the tutorial says a pair key names the auxiliary units of
that ID under the top owner; the class Notes and concepts.rst call a
unit's owner its direct owner (Unit.owner gives the top one).
F1F7-4 (fixed): new test_held_owners_skip_loops_and_take_the_first_holder
with two toy units holding each other and a third without owners, in both
orders: the first holder owns, the loop is skipped, key resolution ends.
Tests: tests/test_hxn_contributions.py 50 passed; tests/test_targeting.py
-k "bath_balanced_pair or keys_of_idle_units or facility" 6 passed;
doctests of hensmith/hxn_synthesis.py and _heat_exchanger_network.py 5
passed; oracle sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf
(matches); sphinx -W shows only the unreachable intersphinx inventories.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…, inside the axes and off the curves TGR7-1 (fixed): the labels of the balanced composite curves are placed by one _Stacked placer per axes, in one pass for all of them, kept for the renderer, the axes box and the view limits (with the texts). Each label's textcoords (_At) reads its spot from that pass instead of measuring the other labels' get_window_extent, which re-ran their own placement and made a draw exponential in the number of labels. Fifteen steam levels now take at most a few passes per draw (0.3 s; 182 s before). New test_plot_labels_placed_once bounds the passes per draw (<= 4) with 15 agents and checks order, overlap and the axes. TGR7-2 (fixed): labels are stacked in a column per side, and the stack is bounded by the axes. At plot time (when the canvas has a renderer) make_room runs the columns unbounded and extends the y data limits so that they fit. When drawn, a column that would still run past the top or bottom is set in rows instead, each label at its own level, moved sideways past the labels of the levels beyond its own and their leaders. If that fails, the labels are moved sideways past the label in their way, and as a last resort clamped inside the axes. The two crowded-refrigerant cases are added to test_plot_readability: labels inside the axes, off the curves and tick labels, apart and in order. TGR7-3 (fixed): on each side the labels keep the order of the levels (each heating label no lower than the one before it, each cooling label no higher). A label left more than 1.5 label heights from its anchor gets a thin leader (a _Leader patch, gid 'Leader:<agent ID>') from the anchor to the nearest point of its box. Balanced labels get a halo in the axes face colour, so that a leader passing behind one strikes none of its letters. The curves the labels are kept off are now both balanced curves. Tests: pyro on 4.5 and 5 in axes and in a 1x2 grid (readable, in order, leaders from anchor to box), and no leaders at the default size for pyro, furnace and liquid. TGR7-4 (fixed): grand composite curve labels are placed when drawn by _Beside. It tries the four places beside the step end in order: the side with more room first (right toward the pinch or left beyond the level, then the other vertical), then the other side. It takes the first that is inside the axes and crosses neither the curve's display path (Path.intersects_bbox), the steps, nor an earlier label. If none is clear, it takes the nearest clear place further along the level, away from the end. The Notes are corrected. The reviewer's pocket, shallow and twin cases and a 'nose' case (1 of the reviewer's 40 random GCCs, where no place beside the end is clear) are added to the GCC readability checks, which now measure the boxes, not xyann. Docs: the Notes of plot_composite_curves and plot_grand_composite_curve describe the placement and the 'Leader:<agent ID>' gid. tutorial_05_balanced_composites.png was regenerated by running docs/_demo_src/examples/ch05_targeting.py alone. Only the label halos changed in it, and no other asset or capture changed. The synthesis path is untouched, so the oracle was not needed. Tests: tests/test_targeting.py -k plot: 5 passed; pytest hensmith/targeting.py (doctests): 6 passed. Sphinx and docutils are not installed in the local venvs, so sphinx -W was not run here; the docstring changes are prose only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…ets and at synthesis, label by them, take key_units in from_heat_utilities
E2E-R7-1 (fixed): the holder relation of auxiliaries held without an
owner (MultistageCompressor stages) is now captured with the streams.
_held_owners/_key_universe/_mapping_contributions/_stream_contributions/
_explicit_contributions take an optional snapshot `held` that comes before
the live relation (default None: unchanged). The facility stores
_held_owners(hu units + candidate units) at each fresh synthesis next to
original_heat_exchangers (_holders); HXN.targets() resolves a mapping of
approach contributions with it and hands the targets
_held_owners(hxs + key_units, held). PinchTargets keep it in _holders
(set by _from_exchangers/from_heat_utilities, copied by at()), and
_film_mapping resolves h keys with it. Stored targets now keep
summary(h={'K1': x}) after K1 rebuilds its stages (repro: 1185.1 m2
before and after, was 395.03 after), and HXN.targets() after K1.simulate()
alone gives the synthesis's contributions (was None). Docs: concepts.rst,
HXN.targets Notes, PinchTargets.key_units.
E2E-R7-2 (fixed earlier): stage keys of any number were already accepted
by fd7a7d4 (nst7.py: n_stages 3 -> 2 simulates); stored targets now also
keep ('K1', 'K1_H3') on their old intercooler via the snapshot.
E2E-R7-3 (fixed): _unit_label takes the top owner along _parent with the
targets' holders, so intercoolers are 'K1 - K1_H1' in the facility's
targets (and in from_heat_utilities given key_units), and an auxiliary of
an empty ID is labeled by its owner's ID alone ('T1', not 'T1 - '). No
generated docs capture shows such a label.
E2E-R7-4 (fixed): PinchTargets.from_heat_utilities takes key_units=None
(e.g. sys.units, expanded with nested auxiliaries as the facility's
_candidate_units, now the shared helper _with_auxiliaries): approach
contribution and h keys may name them, so {'K1': x}, the K1 object and
('K1', 'K1_H1') work there as in the facility. HXN.targets() before a
simulation passes its units this way. The str-key hint also names
key_units; tutorial ch05 mentions it.
Tests: new test_targets_keep_the_holders_of_stages_rebuilt_since;
labels asserted in test_keys_of_auxiliaries_under_other_IDs; key_units in
test_facility_targets_keys_of_idle_units.
test_hxn_targets.py + test_hxn_contributions.py: 93 passed;
test_targeting.py: 75 passed; doctests of targeting.py, hxn_synthesis.py,
_heat_exchanger_network.py: 11 passed. Oracle sha256
dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf
(unchanged). Sphinx is not installed in either venv, so the docs build was
not run here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…eam's nested_auxiliary_units, label callable values by their facility keys
F1F8-1 (fixed): the facility's candidate units (every fresh synthesis,
including the default path, which takes the holders of owner-less
stages for HXN.targets() labels), HXN.targets() and
PinchTargets.from_heat_utilities(key_units=...) went through
biosteam.Unit.nested_auxiliary_units, which fails on an auxiliary of an
auxiliary that is not a unit (the VacuumSystem of a flash drum under
vacuum held by a custom unit) and recurses without end on units that
hold each other. hxn_synthesis._with_auxiliaries now walks the
auxiliaries itself (_nested_auxiliaries): it reads the auxiliaries of
biosteam.Unit objects only, lists each unit once and goes into each
unit at most once, in the order of nested_auxiliary_units (checked
identical, unit by unit, on BinaryDistillation and ShortcutColumn under
vacuum, MultistageCompressor, MultiEffectEvaporator, vacuum Flash and
the test units TwoFlash, NestedHeaters and ListedHeaters). The holders
stay computed on the default path (the walk can no longer fail there):
without them, HXN.targets() after a compressor rebuilt its stages would
label them 'K1_H1' instead of 'K1 - K1_H1'. New
test_auxiliaries_that_hold_auxiliaries_that_are_not_units: a VacuumTrain
unit in the facility gives, by default, the network (network_record) and
targets of the same streams with the drum as a unit of its own, takes
contributions by unit, tuple, heater and callable, and gives targets
before (units option) and after a synthesis and from
from_heat_utilities(key_units=...); test_held_owners_skip_loops_and_
take_the_first_holder checks _with_auxiliaries ends on mutual holders.
Both fail without the fix.
F1F8-2 (fixed, nit): a bad value of a callable now names its unit with
the owners a mapping's keys take (_held_owners of the heat utilities'
units, the given units and held), so a facility intercooler is
('K1', 'K1_H2') as in the mapping path; tested in the facility and in
_stream_contributions with and without units.
Tests: tests/test_hxn_contributions.py with the doctests of
hensmith/hxn_synthesis.py and hensmith/_heat_exchanger_network.py: 57
passed; tests/test_targeting.py with hensmith/targeting.py doctests: 81
passed. Oracle: 29 cases, sha256
dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf
(unchanged).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…ending at its zero-margin temperature
TGC8-1 (fixed): with approach contributions, a stream could end exactly at
agent.T - dT real (agent.T + dT for cooling) of a sensible agent with
agent.dT = 0 while another stream set the level higher (lower). That end
then lay inside the agent's stretch with heat already given above it, so
_line_limit bound the outlet at agent.T and biosteam divided by a zero
enthalpy change (FloatingPointError under the errstate biosteam sets; nan
cost and infinite flow otherwise), taking down utility_targets, summary,
the balanced curves, area/units/capital targets, supertarget,
network_report and the plots. PinchTargets._level now treats such a
stream in reach as capping the level at that end for a sensible agent,
as a stream reaching beyond its limit already does (the same zero-margin
test _short_of_stuck_loads applies to point loads): the contact is then at
the level, where the agent has given nothing and keeps dT. The test is a
no-op for agents with agent.dT > 0 (biosteam's defaults) and without
contributions (every stream of a kind then shares the limit, which is
already the level), so no documented number changes. Class Notes, the
_level docstring and concepts.rst describe the rule. The suggested
backstop in _line_limit/_utility is not added: after the fix a
zero-margin contact can only lie at the level (x = 0), and choosing
another level or agent at pricing time would duplicate the allocation;
the fuzzing below found no case left.
New test test_sensible_line_stream_ending_without_driving_force covers
both mirrors (hot oil at 480 K, coolant at 300 K, dT 0): level at the
stream's end, the rest to the next agent, finite flow and cost equal to
an independent pricing, line gaps {9, dT}, finite summary utility cost.
It raises FloatingPointError on the previous code.
Tests: pytest hensmith/targeting.py tests/test_targeting.py: 82 passed.
Synthesis untouched; the oracle still prints 29 dcd15864a700a58d...691bf.
Constant-CP fuzzing with an oracle updated for the rule: 1,200 random
cases (seeds 30-37, including the reviewer's seed 32 case 14) and 720
round-valued cases aimed at dT-0 agents' zero-margin temperatures: no
exception, no non-finite cost or flow, no line gap below dT (the round
set raised FloatingPointError in 71 of 240 cases before the fix). sphinx
is not installed here; the doc changes are prose with literals only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
… raise on stages rebuilt since the last simulation
TGR8-1 (fixed): plot_composite_curves(kind='balanced') wrote the note on an
unserved remainder ('Utility:unserved') at the upper left corner of the
axes, where the label placer, unaware of it, could stack or lift labels
(and where 'best' then pushed the legend elsewhere). The note is not a label
of anything drawn, so it now leaves the label area: an annotation under the
x axis, left-aligned with the axes, placed at draw time below the axes and
the visible x axis decorations (tick labels, label, offset text) by a
picklable `_Under` coordinate class. A constrained layout (the default
figure) and bbox_inches='tight' keep it in the figure. Docstring of
plot_composite_curves (Notes and `ax`) updated. test_plot_readability now
asserts the note is inside the figure, under the axes and the x axis
decorations, and clear of every label and of the legend: REF_A with low
pressure steam and with cooling water only at the default size, REF_A with
cooling water only at (4.5, 4.2), and the reviewer's three-stream case with
LP/HP steam at the default size (it fails on the old code). The legend may
still cover a label at (4.5, 4.2) or smaller in the REF_A case: there every
one of matplotlib's 'best' candidates overlaps a label or crosses a curve,
as before this change; that is the docstring's "covers the least", not the
note. test_plot_labels_placed_once required at least one placement pass on
a repeated draw, which held only through a 1-ulp change of the axes height
between draws; it now requires 1-4 passes at the first draw and at most 4
at the next (none when the layout is the same to the bit, as it now is).
The served case is untouched, so no docs figure changes (the ch05 balanced
figure has no unserved utility).
TGR8-2 (fixed): network_report checked the network only against
HXN.original_heat_exchangers, whose detached former stages (a
MultistageCompressor simulated alone after a change of pr rebuilds its
intercoolers) keep consistent former states, so it reported stale streams
without error. _check_network (both the default and the explicit targets
paths) now raises the "changed since the facility's last simulation?:
simulate the system" kind of ValueError, naming the stream by its label,
when an original exchanger is no longer among the facility's units and
their nested auxiliaries (HXN._candidate_units()). Checked free of false
positives after a system simulation for Flash, BinaryDistillation,
MultiEffectEvaporator, MultistageCompressor and plain units. Raises section
of network_report and the Notes of HXN.targets (which keeps the former
stages until the system is simulated, as test_targets_keep_the_holders_of_
stages_rebuilt_since requires) updated. New test
test_network_report_rebuilt_stages (fails on the old code with "DID NOT
RAISE").
Tests: tests/test_targeting.py (all 77) plus
tests/test_hxn_contributions.py::test_targets_keep_the_holders_of_stages_rebuilt_since:
78 passed; doctests of hensmith/targeting.py and
hensmith/_heat_exchanger_network.py: 8 passed; oracle sha256
dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf (29 cases)
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…since, validate the utilities of plot_composite_curves E2E-R8-1 (fixed): the network_report half was fixed by TGR8-2 in the previous commit (it now raises when an original exchanger is no longer among the facility's units and their nested auxiliaries). HXN.targets() still returned the targets of the detached former stages silently (only its Notes said so). It now warns (RuntimeWarning, at the caller's line) naming the first such stream by its label, "the targets of HXN are of the former state of K1 - K1_H2, which is no longer one of the facility's units nor an auxiliary of one (rebuilt since the facility's last simulation?): simulate the system for those of the process now", and still returns those targets (test_targets_keep_the_holders_of_stages_rebuilt_since keeps its contributions). The check is shared with network_report's (targeting._rebuilt_label, over the same candidate units). network_report takes its default targets through the new private HeatExchangerNetwork._pinch_targets, so it raises its ValueError without also emitting the warning. Docstring of HXN.targets: new Warns section, Notes updated. Tests: test_network_report_rebuilt_stages now expects the warning from HXN.targets() after K1.simulate() alone, checks that those targets equal the report's before the change, that network_report (by default and with explicit targets) raises without the warning, and that neither warns after the system is simulated again; test_targets_keep_the_holders_of_stages_rebuilt_since expects the warning. No false positive in tests/test_targeting.py or in the contribution tests that call HXN.targets() with the warning made an error. E2E-R8-3 (nit, fixed): plot_composite_curves raised "TypeError: 'bool' object is not iterable" for kind='balanced', utilities=False, and silently ignored utilities with kind 'real' or 'shifted'. It now raises a clear ValueError in both cases before making a figure; parameter doc and a new Raises section updated; covered in test_plot_composite_curves_gids. E2E-R8-2 (nit, documented): the Notes of plot_composite_curves now say that labels can still overlap on axes too small for them all (many agents on small axes without a layout engine) and that a larger figure or a constrained layout gives them room. Shrinking fonts or dropping duties was not done: it would change the documented label contents for a rare case. No docs figure or capture changes (the default path and the demo scripts are untouched; ch05 calls HXN.targets() only after a system simulation). Tests: tests/test_targeting.py (77) plus tests/test_hxn_contributions.py::test_targets_keep_the_holders_of_stages_rebuilt_since plus doctests of hensmith/targeting.py and hensmith/_heat_exchanger_network.py: 86 passed; tests/test_targeting.py with -W "error:the targets of:RuntimeWarning": 77 passed; the 6 tests of tests/test_hxn_contributions.py calling HXN.targets() with the same filter: 6 passed; oracle sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf (29 cases) unchanged. sphinx is not installed in this environment, so the docs build was not run (the new Warns and Raises sections follow those already in targeting.py). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…lities as at hand, hint the other form of auxiliary list keys
F1F9-1 (fixed): a unit may keep an exchanger outside its
auxiliary_unit_names and take its heat utilities in _design
(self.heat_utilities = self.hx.heat_utilities). The facility takes those
heat utilities, so the exchanger becomes an original heat exchanger, but
_with_auxiliaries (and so HXN._candidate_units and the key_units of
from_heat_utilities) never listed it. As a result HXN.targets() warned
"rebuilt since" and network_report raised right after a simulation.
- hxn_synthesis._heat_utility_units is new: it gives the units of the
heat utilities of a group of units (hu.unit), other than the group's
own.
- _with_auxiliaries now lists, after each unit and its nested
auxiliaries, the exchangers they hold through their heat utilities
only. This makes the "at hand" set of _rebuilt_label/_check_network
include them. A unit that rebuilds such an exchanger and is simulated
alone is still detected, as is a rebuilt MultistageCompressor stage.
- _held_owners gives such an owner-less exchanger, held as no unit's
auxiliary, the lowest unit on the owner chains whose heat_utilities
hold its heat utility as its holder (an owner lists its auxiliaries'
heat utilities too). This runs after the auxiliary holders and never
closes a loop. So 'U' covers the exchanger, ('U', 'U_hx') names it,
the bare 'U_hx' raises with the pair as a hint, and labels read
'U - U_hx' (the owner's ID alone for an unnamed exchanger, where the
message read "of , which is" before). Outside the facility without
key_units, the exchanger has no owner and 'U_hx' is its key, as for
K1's stages.
- Docs: the HXN Notes, the targeting docstrings (key_units, targets
attribute, _rebuilt_label, _check_network) and concepts.rst now say
this.
- Tests: test_keys_of_exchangers_held_through_heat_utilities and
test_held_owners_through_heat_utilities (lowest holder, an existing
owner kept, mutual holders without a loop) in
tests/test_hxn_contributions.py, and
test_network_report_of_exchangers_held_through_heat_utilities[replace
False/True] in tests/test_targeting.py (clean right after a
simulation, warn/raise after a rebuilding unit is simulated alone,
clean again after the system is simulated).
F1F9-2 (nit, applied): _unknown_key_hints now also uses the new
_other_form_hints. A tuple key giving a list attribute's bare name gets
"; E1.evaporators is a list: ('E1', 'evaporators[0]'), ..." (at most
three keys, then ...). An indexed key into a single auxiliary gets
"; D1.condenser is ('D1', 'condenser')". Assertions were added to
test_contribution_keys_on_auxiliary_units and
test_keys_of_auxiliaries_a_unit_does_not_build (facility and h keys).
Default-off results are unchanged: the oracle gives sha256
dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf. The
review's Monte Carlo probe (mc.py) gives byte-identical records to HEAD
in default mode, and in contrib mode against a HEAD worktree.
Tests (NUMBA_DISABLE_JIT=1): tests/test_hxn_contributions.py,
tests/test_targeting.py, tests/test_hxn_targets.py and the doctests of
hensmith/hxn_synthesis.py, hensmith/_heat_exchanger_network.py and
hensmith/targeting.py: 186 passed. Sphinx is not installed in either
venv, so the docs build was not run (the edits are plain RST literals
and docstring prose with no new cross-references).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…vel and three tolerance branches TGC9-1 (fixed): new tests pin the two rules of the level that only the coarse mutants were tested on; no code change, the shipped code is the correct side in each case. - test_levels_of_streams_between_zeros: the zero that bounds a level's reach is the furthest from the pinch. Cooling: process zeros at 450 (pinch), 445, 400 and 360 K shifted with S between the last two; boiling water (latent, 370 K) serves HL's 20 kW at level 330 K (catches cooling taking z[0]). Heating: steam at 415 K serves 5 kW at 420 K and leaves zeros at 420 and 440 K with S between them; steam at 431 K serves CL's 20 kW at level 461 K (catches heating taking z[-1]). Levels, duties, pricing temperatures and the GCC zeros are asserted in closed form, and each cost against an independent fresh_price. - test_levels_of_streams_across_the_pinch: a stream with no heat on the agent's side of the pinch is not served and does not cap the level. Steam at 407 K serves 2 kW at 402 K and natural gas 8 kW (CB lies below the pinch); its mirror, boiling water at 403 K serves 2 kW at 398 K and cooling water 8 kW (HB lies above it). Costs checked with fresh_price. TGC9-2 (nit, applied): - test_utilities_must_be_of_the_targets: a heating or a cooling duty off by +-1e-6 relative raises in _check_utilities, one off by 1e-12 does not. - test_from_heat_utilities_after_replaced_heat_utilities: a unit_duty off its stream's by +-1e-4 warns, one off by 1e-9 does not. - test_pricing_is_isolated: a '_hensmith_targets' flowsheet already registered is the same object after pricing and gets none of its streams. Mutation check (a copy of the package, the selected tests): the four mutants of the finding (M1c, M1h, M24, M25), the coarse all-in-reach ones, and M23, M29 (and the same threshold lowered to 1e-12) and M4 of the nit are all killed now. Tests: pytest tests/test_targeting.py hensmith/targeting.py: 87 passed. The synthesis path is untouched (test-only change). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…ents h_utilities may key TGS9-1 (fixed): area_target and capital_cost_target rejected an h_utilities key that is the ID of a custom agent whenever that agent served nothing at the targets' T_min_app (utility_targets left it out), although summary, supertarget and network_report accept the same mapping because they pass their heating_agents and cooling_agents as known agents. - PinchTargets.area_target and capital_cost_target take a new optional keyword `agents` (last, default None: no change otherwise): agents whose IDs h_utilities may key as well, or IDs of agents of biosteam's lists, i.e. the heating_agents and cooling_agents the utilities were priced with. A new _key_agents validates it (TypeError for a bare str/agent or a non-agent item, ValueError for an ID not in biosteam's lists, with the same hint as _agents). It changes nothing but the known keys. - The h_utilities documentation of area_target now states that an ID must name an agent of `utilities`, of `agents` or of biosteam's lists, and how to key another agent (by the UtilityAgent, or via `agents`). - The error now says where the key was looked for and hints at the object key: "h_utilities keys that name no agent of the utilities, of agents, or of biosteam's lists: 'x' (key another agent by the UtilityAgent itself)"; summary, supertarget and network_report name "heating_agents or cooling_agents" instead of "agents". - New test test_h_utilities_keys_of_agents_serving_nothing: a custom agent served at T_min_app = 20 K and idle at 5 K; with agents the ID key matches the object key and summary at both points, in area and capital (and agents alone change nothing); without agents the idle point raises the new message; summary's message; validation of agents. Tests: tests/test_targeting.py plus the doctests of hensmith/targeting.py: 88 passed. Default-off oracle unchanged (29 cases, sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
… off their labels TGR9-1 (fixed): plot_composite_curves left the legend to matplotlib's 'best' location, which counts an overlapped label like one curve vertex, so on smaller axes (KEMP, REF_A, FURNACE and the liquids network at 4x3 in., or the middle panel of a constrained 1x3 grid at 10x4 in.) it took a spot over one or two agent labels. The legend is now a _ClearLegend (a matplotlib Legend added as Axes.legend adds one) whose 'best' hook, run when drawn as the labels are placed, treats the texts of the axes as obstacles: of matplotlib's spots in the axes, in its order, it takes the one clear of every text with matplotlib's least count of curve points and crossings, leaders (where drawn now, via the new _Leader.path) and band, which is matplotlib's own spot wherever that is clear of the labels; where every spot covers a text, it goes above the axes (and any title), left-aligned, where constrained layout and bbox_inches='tight' make room. It is used for every kind (real and shifted labels were covered the same way on 3x3 in. axes). The Notes of plot_composite_curves say so, and the label-overlap caveat now names short axes that a legend above them leaves shorter. Sweeps (11 problems, kinds real/shifted/balanced, single axes and constrained grids): at 7.5x4.2 (default), 6x4, 5x3.5, 4.5x4.2, 4x3 and 1x2/1x3 grids, legend-over-label cases go from 14 to 0, every other legend box and ylim is unchanged, and no labels newly overlap each other. On 2.5 to 3.5 in. tall axes, the cases go from 143 to 0. In 9 configurations with 5 or 6 agents on 2.5 in. tall axes, no spot in the axes is clear, so the legend goes above them. The shorter axes then let some labels overlap each other, as the caveat says, where before the legend hid them. The docs/_demo_src ch05 script, run alone, reproduces every asset byte for byte. Tests: test_plot_readability adds clear_legend(), which checks the legend is off every text and either inside the axes or above them and the title. It covers these cases: FURNACE and liquids at 4x3 and as the middle panel of a constrained 1x3 at 10x4 (labels readable, none overlapping, legend inside); liquids real/shifted at 3x3; and a user's texts on all nine spots (legend above the axes, then above the title). At the default size it also checks the location equals plain ax.legend(loc='best'). The new checks fail on the old code (FURNACE 4x3: legend over high_pressure_steam). pytest hensmith/targeting.py tests/test_targeting.py: 88 passed; pytest hensmith/_heat_exchanger_network.py: 2 passed; sphinx-build -W clean. Synthesis path untouched (oracle not needed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
… a network wrote them, refuse repeated ones, name curves by label E2E-R9-1 (fixed): PinchTargets.from_heat_utilities warned whenever a heat utility's unit_duty differed from its stream's outs[0].H - ins[0].H by more than 1e-6, and blamed a HeatExchangerNetwork simulated with replace_unit_heat_utilities=True. biosteam's MESHDistillation total condenser (full_condenser=True) reports a duty 0.1 % off its own streams' with no network anywhere, so the selection and a facility's targets() before any simulation warned of a network that never ran, and told the caller of targets() to call targets(). The facility module now keeps a weak set of the facilities holding heat utilities written over by _replace_unit_heat_utilities (added there, discarded by _restore_unit_heat_utilities), and _replacing_networks(hus) maps each such heat utility to its facility. The warning fires only for an off-stream heat utility that a facility holds written over, names that facility, and advises '<ID>.targets()' (never the facility being asked, whose pre-simulation path implies it has replaced nothing). Any other off-stream duty warns of nothing: the targets are the streams'. Docstring Warns section, _duty_off_stream note. Tests: the replaced-KEMP test matches the new text (network 'HXN', HXN.targets()); its threshold part now runs on a written-over heat utility (1e-4 warns, 1e-9 does not, value restored) and checks that an unreplaced 1e-4 offset is silent; new test_from_heat_utilities_of_a_total_condenser (MESHDistillation with full_condenser=True: no warning from the selection or from HeatExchangerNetwork(units=sys.units).targets(), equal targets). E2E-R9-2 (fixed): PinchTargets.__init__ builds the curves it is not given with the targets' labels, so curve warnings raised by HXN.targets(), network_report and supertarget name 'C6', not a private copy's 's9' (numbers unchanged: labels only name curves in warnings and reprs). labels docstring; the replaced-KEMP test checks the curve labels. E2E-R9-3 (fixed): from_heat_utilities raises a ValueError naming the unit when two heat utilities with duty are of the same unit (the same one given twice, as by overlapping selections such as a unit and its auxiliary), whose stream would count once per heat utility. Raises docstring, tutorial ch05 sentence; test in test_from_heat_utilities_takes_exchangers. Tests: pytest hensmith/targeting.py hensmith/_heat_exchanger_network.py tests/test_targeting.py tests/test_hxn_targets.py: 133 passed; tests/test_hxn.py -k replace: 2 passed. Oracle (default-off synthesis): sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf, unchanged. No docs captures change (none show curve warnings); sphinx is not installed here, the rst change is plain prose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
Round 8's blocker showed that a facility without approach contributions
had come to depend on the key machinery: every fresh synthesis in
HeatExchangerNetwork._cost recorded the holders of auxiliaries held
without an owner (_held_owners over _candidate_units, i.e. the
_with_auxiliaries / _nested_auxiliaries walk), for the labels of
targets() only, so an unusual unit structure could make a plain
simulation raise. The default path is now the base path plus nothing it
needs from that code.
What:
- New HeatExchangerNetwork._synthesis_holders, called where _cost took
the holders. With approach_contributions, they are taken as before
(the keys were just resolved by the same walk). Without (the default
path) they serve the labels of targets() and network_report only: they
are taken only if an original exchanger's _owner chain ends outside
the facility's units (plain attribute reads; e.g. a
MultistageCompressor's intercoolers), and are None on any error, so a
default simulation never raises from them and labels then go by the
live relation. A system whose exchangers its units own (columns,
flashes, plain exchangers) runs none of the key machinery at all;
_network_contributions already returned None first.
- New test_default_facility_needs_none_of_the_key_machinery
(tests/test_hxn_contributions.py): every key-machinery helper, in
every hensmith module that imports it, and HXN._candidate_units, is
monkeypatched to raise. The quickstart system (ShortcutColumn with
condenser and reboiler, product coolers, flash), plain, with
cache_network (reused after a feed change) and with
replace_unit_heat_utilities, gives the same process and utility
exchanger duties (float.hex) and unit heat utility duties as unpatched,
over two simulations, and calls none of the helpers. A
MultistageCompressor system (owner-less intercoolers) does the same
with the best-effort holders failing to None; afterwards targets()
has the same labels ('K1 - K1_H1', ...) and summary as unpatched. It
fails on the previous commit for all four cases.
- Simplified key resolution, behaviour unchanged (messages identical):
_check_key_levels and _nearest_key_value are now shared by
_mapping_contributions and PinchTargets._film_mapping, which each had
their own copy of the same-level conflict check and of the
nearest-key loop (h's stream-index keys are taken first, as level -1
made them win before); _auxiliary_attributes is shared by
_auxiliary_slots and _other_form_hints, which each parsed the
'name[k]' key and looked up the attribute themselves; targets()'
callable test drops a redundant None check.
- Plot label placement reviewed and left as is: each placer (_Stacked
with its rows fallback and leaders, _Beside, _Under, _ClearLegend)
answers a confirmed finding that its tests pin, and the remaining
heuristics (the margins next to make_room) shape the generated
figures, so trimming them would change the docs assets.
Validation: the four new cases fail on 0bd3bf8 and pass here;
test_hxn_contributions, test_targeting, test_hxn_targets and the
modules' doctests pass (194); full suite 1106 passed; default-path
oracle sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf
(unchanged). No docs changed, so no sphinx run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…re given TGS10-1 (fixed): PinchTargets._film_mapping evaluated units[j] for every stream without an index key, so targets built with the PinchTargets constructor (units None) raised IndexError for any h mapping that left a stream out (an empty one included), in area_target, capital_cost_target, summary and supertarget (regression from e3e2c1f). The nearest unit key is now looked up only when there are unit keys; streams left out take 1 again, matching 0bd3bf8. The direct-targets block of test_bath_balanced_pair now checks h={0: .5} against [.5, 1.], h={} against the default and the heat-utility targets, and that summary, supertarget and capital_cost_target run with h={1: 2.} and give the results of the sequence form. TGS10-2 (nit, applied): the Notes of PinchTargets and the capital paragraph of concepts.rst now say the furnace is costed at biosteam's default furnace pressure of 500 psi (P sets the shells' pressure only). Tests: tests/test_targeting.py plus the doctests of hensmith/targeting.py, 89 passed; the reviewer's reproduction (r10/c10_hmap.py) now gives 15.0, 8.333 and 20.0 as on 0bd3bf8. The synthesis path is untouched (no oracle run needed). sphinx is not installed in this environment; the doc changes are prose only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
… former outlet
E2E-R11-1 (fixed): HXN.targets() and PinchTargets.from_heat_utilities now
warn (RuntimeWarning) when an exchanger's ins[0].mol and outs[0].mol
differ by more than 1 % of the larger (targeting._stale_labels,
_STALE_FLOW = 1e-2): its inlet has changed since its unit was simulated,
and the targets would take the stream from the new inlet to the former
outlet. The message names the streams and says to simulate the system (or
its units) first. The tolerance stays well above the about 0.1 % of a
MESHDistillation total condenser, and test_from_heat_utilities_of_a_total_condenser
still passes under simplefilter('error'). No state is stored, so units
simulated alone give correct targets without a warning. network_report
still raises its own ValueError and does not warn first (_pinch_targets
now also returns the stale labels, and only HXN.targets warns of them).
Before the facility's first simulation, from_heat_utilities warns, once.
It warns rather than raises: a unit whose exchanger legitimately changes
its flows between those ends (a reaction on a condenser stage, a custom
auxiliary) would otherwise be unable to target at all, while its synthesis
works. Documented in the Warns/Notes of both methods, in tutorial ch05 and
in concepts.rst. New test test_facility_targets_changed_inlet covers one
stream and two streams, the pre-simulation path, the report, units
simulated alone, a change of temperature only, and where the warning
points.
E2E-R11-4 (nit, applied): targeting's warnings go through _warn, which
uses warnings.warn(skip_file_prefixes=<package dir>) (Python >= 3.12, as
setup.py requires). They now point at the caller's line, not at
targeting.py: unserved heating/cooling, replaced heat utilities, the
supertarget sweep warnings, and the area driving-force warnings. Two tests
now assert the filename.
E2E-R11-3 (nit, not applied): pre-existing (5ef9a35 crashes the same way)
and out of scope. A fallback would turn a crash on the cache path into a
re-synthesis.
Tests: tests/test_targeting.py + doctests of hensmith/targeting.py and
hensmith/_heat_exchanger_network.py: 92 passed (doctests rerun after the
last docstring edit: 8 passed); tests/test_hxn_contributions.py: 58
passed. Synthesis path untouched; oracle sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf
(matches).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…l once per unit, say a reversed stream in terms of its heat utility
E2E-R12-1 (fixed): PinchTargets.from_heat_utilities refused any two heat
utilities with duty of the same unit, but biosteam lists the heat utility
of a unit in parallel once per unit in its owner's heat_utilities
(Unit._load_costs; an AbstractStirredTankReactor, such as biosteam's
fermentors, sets parallel['self'] = N for its recirculation heat
exchanger). So the facility's selection,
heat_exchanger_utilities_from_units(sys.units), and a facility's targets()
before any simulation raised on such a process, and the advice to give
each heat utility once dropped (N-1)/N of the reactor's cooling. A new
_overcounted_units(hus, owners) refuses only the units whose heat
utilities with duty are given more often than the unit or any owner on
its chain (along _parent, with the held-owner relation) lists them, so a
heat utility counts as a stream once per occurrence, as in the synthesis,
and an overlapping selection (a unit and its auxiliary) still raises,
now naming the counts ("'R1 - heat_exchanger' (6 given, 5 listed)") and
advising heat_exchanger_utilities_from_units of units none of which owns
another. key_units and the holders are now resolved before the check.
Docstring (hus parameter, Raises) and tutorial ch05 wording fixed. Tests:
new test_from_heat_utilities_of_units_in_parallel (a stirred-tank
fermentor with parallel['self'] > 1: the selection, the preview facility's
targets() before any simulation and the facility's targets() and
synthesis targets after its simulation are equal, N reactor streams, the
whole reactor cooling counted; the unit plus its auxiliary raises; the
auxiliary alone is one stream); test_from_heat_utilities_takes_exchangers
matches the new message.
E2E-R12-2 (nit, fixed): from_heat_utilities now warns of stale streams
(_stale_labels/_warn_stale) before it builds the targets, and turns the
constructor's direction error (which named is_hot, a parameter the
caller never gave) into one about the heat utility: "the heat utility of
'H1' heats (hu.duty = ...), but its stream, from its unit's inlet now to
its outlet as last simulated, is cooled (...): its inlet has changed
since its unit was simulated (a feed changed, or a recycle converged
loosely?); simulate the system (or its units) first, converging any
recycle more tightly". PinchTargets.__init__ still raises a plain
ValueError with the same text, marked with the stream's index and change
for this. Test: new test_from_heat_utilities_reversed_stream (feed made
hotter than the heater's outlet: the new message, no is_hot, no stale
warning; with a flow change too: the stale warning comes first).
Tests: pytest hensmith/targeting.py tests/test_targeting.py
tests/test_hxn_targets.py: 134 passed; hensmith/_heat_exchanger_network.py
tests/test_hxn.py -k "replace or targets or doctest": 9 passed. The new
tests fail on the previous code (the parallel refusal; the is_hot
message). The synthesis path is untouched (targeting.py only), so the
oracle was not rerun. No docs captures show these messages; sphinx is not
installed here, the rst change is plain prose.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…lel in the cache rule and the targets
F1F13-1 (fixed):
- `_cost`: with cache_network, a network is reused only while the multiset
of heat exchangers is the same (collections.Counter of the units behind
the heat utilities against `original_heat_exchangers`), not the set: a
unit in parallel whose Unit.parallel changed (a fermentor sized by its
volume) changes the number of streams, and the network is synthesized
again instead of being reused for the former number. Only the
cache_network=True path changes, and only where the former set
comparison reused a network of the wrong number of streams;
cache_network=False (the default) never runs the comparison.
- `HXN.targets()` warns (RuntimeWarning), and `network_report` raises a
ValueError, when an exchanger of the network gives more or fewer streams
than the facility's units list its heat utility now (a unit in parallel
simulated alone whose number changed): new `targeting._miscounted` /
`_miscounted_names`, returned by `_pinch_targets` and checked in
`_check_network` before the end-state check.
- Docs: the class Notes, the cache_network API row, concepts.rst
("Reusing a network"), tutorial ch04 describe the multiset; the
targets() Warns/Notes, network_report Raises and tutorial ch05 qualify
"the units alone give the targets of the process now".
- Test: test_facility_units_in_parallel_whose_number_changes (cache kept
at the same N, synthesized again at a new N with the loads and targets
of a facility without the cache; units simulated alone warn and the
report raises until the system is simulated). It fails with the former
set comparison.
Tests: tests/test_targeting.py tests/test_hxn_targets.py 129 passed;
tests/test_hxn.py tests/test_hxn_contributions.py -k cache 12 passed;
doctests of hensmith/_heat_exchanger_network.py hensmith/targeting.py
8 passed; oracle sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf
(29 cases, unchanged).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
…ger hold TG14-1 (fixed): biosteam gives a unit new heat utilities each time it is simulated, emptying the former ones (duty 0), so a selection kept across a simulation lost those streams silently in PinchTargets.from_heat_utilities (dropped as zero-duty ones), or, after a system simulation, failed with "needs at least one process stream". from_heat_utilities now raises a ValueError naming the units of any heat utility not (or no longer) among its unit's heat_utilities (an identity scan, as _restore_unit_heat_utilities already does), saying a selection is valid until its units are simulated again and to select them again. The changed-inlet warning (_warn_stale, new reselect flag, used by from_heat_utilities only; the facility's HXN.targets message is unchanged) and the reversed-stream error now add "then select the heat utilities again". The hus parameter, Raises and Warns docs and tutorial ch05 say the selection is valid until its units are simulated again. New test test_from_heat_utilities_of_a_former_selection (simulate one unit, then the system: ValueError naming the units; a fresh selection is right); test_from_heat_utilities_takes_exchangers now adds its idle heat utility to its unit before expecting it dropped (an unheld one raises); the changed-inlet and reversed-stream tests check the new advice. Synthesis and facility paths unchanged. Tests: tests/test_targeting.py, tests/test_hxn_targets.py, tests/test_hxn_contributions.py and the hensmith/targeting.py doctests: 194 passed. Oracle sha256 dcd15864a700a58d4edb6273f8ebaee729f7a821ba166e5de7ce5764aa1691bf (unchanged). sphinx is not installed here; the docs changes are plain prose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhJoUiNNefNdnCJQEZ3q9
Member
Author
|
CI status:
I'll keep watching the PR and re-run CI once upstream HEAD is fixed. Generated by Claude Code |
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
Three new features. All of them are additive: with the new options left off, results are bit-for-bit identical to
5ef9a35.1. Per-stream approach temperature contributions
HeatExchangerNetwork(..., approach_contributions={unit: dT})(also onsynthesize_network,problem_tableand the planner).T_min_app. A stream you don't specify contributesT_min_app / 2, so nothing changes by default.T* = T - (T_min_app/2 ± c_j). The planner and_splittingalready work on one shared shifted scale, so the MER guarantees carry over, including the splitting construction._shrink/_repair, eachHXprocess.dTand the per-stream pinch temperatures.synthesis_infokeys:approach_contributionsandmin_approach_margin.(owner.ID, aux.ID)pairs for auxiliaries such as a column's condenser or reboiler. Nearer keys win, and a callable form is also accepted. Units can be idle or in parallel.2. Targeting toolkit (new public module
hensmith.targeting)PinchTargets, built from heat utilities, from streams, or withHXN.targets(). It provides:plot_composite_curves(real, shifted or balanced) andplot_grand_composite_curve, with the utility staircase.3. Supertargeting
tea=).supertarget(...)sweepsT_min_appreusing the stream curves (no new flashes) and refines around the minimum TAC.plot_supertargetsdraws the result.network_report(HXN)compares a synthesized network against its targets: area on a like-for-like basis and as BioSTEAM sizes it, units, utility cost by agent, capital and TAC.Docs
concepts.rst, an API page forhensmith.targeting(hensmithnow exports 13 names), and README and contributing updates.docs/_demo_srcscripts.Validation
NUMBA_DISABLE_JIT=1.tests/test_hxn_contributions.pyandtests/test_targeting.py. They check against independent references: closed-form cascades with per-stream shifts, an independent Bath integrator, BioSTEAM's own cost functions and an exact-approach check on independent states.float.hexregressions pass;5ef9a35and on this branch;sphinx-build -Wbuilds with no warnings.Notes for reviewers
build.ymlinstalls biosteam from GitHub HEAD. With that build, 235 existing tests fail in our environment: numba raises aTypingErrorwhen the JIT is enabled, and the natural-gas agent raisesFloatingPointErrorwhen it is disabled. With biosteam 2.54.0 from PyPI, everything passes.cache_networkreuses a network afterT_min_apporQminchanges. This is documented behaviour today, so it was left as is.replace_unit_heat_utilitiesgives wrong utility costs for units in parallel, and a vacuum flash's heater can make the synthesis raiseAssertionError.Generated by Claude Code