migrate heat exchanger network to the hensmith package - #257
migrate heat exchanger network to the hensmith package#257sarangbhagwat wants to merge 18 commits into
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ut], point loads for isothermal streams temperature_interval_pinch_analysis built the heat cascade from interval enthalpies that were (1) evaluated by flashing hot streams at the *shifted* temperature T - T_min_app, so a stream entering within T_min_app of its dew point was already liquid and lost its latent heat, (2) never clipped to the stream's actual [H_in, H_out], so non-equilibrium column outlets inflated interval duties, and (3) assigned with the wrong sign to streams whose outlet temperature moves against their duty. Targets were impossible (hot target above the un-integrated heating load on the class doctest; hot - cold != net demand on sugarcane). The cascade is now built by problem_table(): each monotone stream is walked once down the shifted grid with a warm-started copy flashed at the real temperature, exact at its own end points and clipped in between, so its contributions telescope exactly to its duty; isothermal and non-monotone streams are point loads at their outlet temperature. Hence hot_util_load - cold_util_load == sum(unit_duty) and both targets are non-negative by construction. Signature and return value of temperature_interval_pinch_analysis are unchanged. Validated by tests/test_hxn.py: per-stream and net energy identities on the doctest system and a 4-stream case at dT = 5/10/20 K, two-stream closed forms, point-load handling, and the synthetic network reaching MER. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… warning, clean restart after failed flash
Three review fixes to the problem_table helpers added on this branch:
1. _stream_H_at_boundaries compared grid temperatures to T_hi/T_lo with
float equality to detect a stream's own end points; this only worked
because Ts was built from the same floats. Assign Hs[0]/Hs[-1] by
position instead (the caller always passes T_hi first, T_lo last, and
monotone streams have T_hi > T_lo strictly so the slice has >= 2
entries, asserted), and loop only over the interior boundaries.
2. The VLE-failure warning printed {stream!r} for an anonymous inlet
copy, giving no way to trace which stream failed. Pass the inlet
stream's own ID (set by the wrapper to s_<i>__Util_<i>) into the
helper and name it in the warning.
3. On a failed flash, the warm-started copy was left in whatever state
the failed VLE call put it in and reused for the next boundary. Re-copy
stream_in in the except branch so the next boundary restarts clean.
Also rewrote the comment above the pinch_T_arr loop in
temperature_interval_pinch_analysis, which claimed non-monotone streams
get pinch = T_in "as before this fix" -- not true in general (e.g.
T_out < T_in <= pinch_cold_stream_T for a cold stream gave T_out/pinch
under the old rule). The comment now states the actual rule (streams
already on one side of the pinch, including non-monotone ones, are not
split; load_duties assigns their whole duty to one side) without
claiming equivalence with prior behavior.
Covering tests unchanged (tests/test_hxn.py + hxn doctests):
12 passed in 13.36s.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…re its point loads The cascade residual[k] = sum(point_H[:, :k+1]) + sum(interval_H[:, :k]) is the heat *leaving* boundary Ts[k], after that boundary's point loads. Testing only this for non-negativity lets a hot point load at Ts[k] (an isothermal condenser, a phase-changing HXutility) mask a deficit in the cold interval (Ts[k-1], Ts[k]) directly above it - but a source at Ts[k] cannot serve a sink above Ts[k]. Example: a condensing stream at 400 K (shifted 395 K) against a cold stream heated 392 -> 398 K returned a hot utility target of zero; the correct target is the cold 395-398 K segment. The target is now the minimum over both the arriving flow (residual - point_total) and the leaving flow at every boundary, which is the standard problem-table treatment of point loads. Also: cold_util_load is clamped at zero in the threshold branch (residual[-1] could be negative by a rounding-level amount, and the tests assert non-negativity); problem_table/ProblemTable are exported in __all__ with an Examples doctest (two-stream threshold case). Note on synthesis behaviour (unchanged by this commit, introduced with the wrapper rewrite): non-monotone streams (outlet temperature moving against the duty) get pinch_T = T_in in pinch_T_arr, so load_duties places their whole duty on the hot side. The old code gave a meaningless negative dH2 for such streams when T_in was on the wrong side of the pinch. The table counts them as point loads at T_out; reconciling that with the synthesis heuristic is a separate item. Validation: regression test test_problem_table_point_load_cannot_heat_above_itself; tests/test_hxn.py + hxn doctests 14 passed (class doctest unchanged); canonical suite 71 failed / 476 passed / 62 skipped with the same pre-existing failure set as the baseline; tests/test_biorefineries.py 1 failed (test_cornstover, baseline) / 4 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… utility negative The threshold branch could leave cold_util_load = residual[-1] negative by up to 1e-9 * scale; clamping it to zero alone would break hot_util_load - cold_util_load == sum(unit_duty) in relative terms (the identity the tests assert with rtol=1e-9). Absorb the rounding into hot_util_load instead, so both loads are non-negative and the identity is exact. Also round the problem_table doctest's cold utility to 10 kJ/hr so it does not demand ten significant digits from a VLE enthalpy across Python versions. tests/test_hxn.py + hxn doctests: 14 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HeatExchangerNetwork had no way to visualize the synthesized network. Add hxn_synthesis.plot_pinch_diagram (pure function, returns fig/ax) and a HeatExchangerNetwork.plot_pinch_diagram wrapper that draw the standard diagram: cold streams (blue, left to right) above hot streams (red, right to left) with inlet/outlet T [degC] and H [kJ/hr], one vertical connector with its duty per HXprocess, a dashed pinch line separating cold-side from hot-side exchangers, and circles marking utility exchangers whose duty exceeds Qmin. Design notes: - The hot and cold stream of each exchanger are found from the stream life cycles by identity, not by parsing HX_<i>_<j>_<side> IDs. - Which side of the pinch an exchanger belongs to comes from the new attributes new_HXs_hot_side / new_HXs_cold_side stored in _cost (only their concatenation new_HXs was kept before). - Columns on each side are ordered by a topological sort of the exchangers under the constraint that every stream meets them in its flow direction (hot streams reversed); contradictory constraints fall back to synthesis order. This keeps the diagram readable and deterministic. - Artists carry gids (HX:<ID>, Util:<ID>) so tests can check the drawing structurally. Validation: tests/test_hxn.py gains an ordering unit test (including the hot-stream reversal and the cyclic fallback) and a structural test on the class doctest system; docstring example added. tests/test_hxn.py + hxn doctests: 17 passed. Rendered the doctest system and the full sugarcane network (HXN.units=None) and checked them against the reference figure. Canonical suite: 74 failed / 476 passed / 62 skipped; the 3 failures beyond the previous run (test_oilcane_O6/O8/O9, test_tire_modeling) also fail on the parent commit bbea705 with this change stashed - they come from the sibling biorefineries/thermosteam clone state, not from biosteam. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…am ID Stream indices alone do not say which process stream a row of the pinch diagram is. Add a label next to each index, '<unit> - <auxiliary> (<ID>)', with one toggle per part on plot_pinch_diagram: show_units (the unit that owns the stream's original heat exchanger, i.e. the main unit for auxiliary exchangers), show_auxiliary_units (the exchanger's name within that unit, e.g. 'condenser' or a dotted path for nested auxiliaries), and show_stream_IDs (the original exchanger's inlet stream ID; unnamed inlets add nothing). All default to True. The owner comes from Unit.owner and the auxiliary name from a recursive search of get_auxiliary_units_with_names(), so no unit or stream IDs are parsed (unlike get_original_hxs_associated_with_streams, which scans for '.' in IDs and special-cases unit classes). plot_pinch_diagram takes the per-stream original exchangers as a new original_hxs argument (required when any label part is on); HeatExchangerNetwork.plot_pinch_diagram passes original_heat_exchangers. Labels carry gid 'Label:<index>'. Validation: new test_pinch_diagram_stream_labels (helper composition, figure labels, toggles off -> no labels); tests/test_hxn.py + hxn doctests 18 passed. Rendered the full sugarcane network to check nested auxiliary names. Canonical suite 74 failed / 477 passed / 62 skipped, same pre-existing failure set as the previous commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New show_legend toggle (default True) on plot_pinch_diagram draws a three-column legend below the axes with proxy handles for the six symbols: cold stream, hot stream, process heat exchange, hot utility, cold utility, and the pinch line. Placed with bbox_to_anchor below the axes so it never overlaps the stream rows; savefig already uses bbox_inches='tight'. Validation: new test_pinch_diagram_legend (entries and order; toggle off -> no legend); tests/test_hxn.py + hxn doctests 19 passed; sugarcane network re-rendered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code-review fixes for the pinch diagram: - Utility markers were colored by the stream they sit on, so hot utilities (on cold streams) were blue and cold utilities red - the opposite of the reference figure and of the diagram's own legend. Color them by utility type. test_pinch_diagram_legend now checks every Util marker's edge color against the legend handle for its type, so the legend and drawing cannot drift apart again. - Stream labels were drawn at the same zorder as the connectors and ran through the hot-side columns; draw them above with a white box. - Remove the dead empty-life-cycle branch (H = nan), which _format_H could not format anyway: every stream always has its utility stage. - HeatExchangerNetwork.plot_pinch_diagram raises a clear RuntimeError when called before simulation instead of an AttributeError from _get_stream_life_cycles; regression test added. - Close the figure in the docstring example; NumPy-style Returns section; comment documenting the gid contract used by the tests. tests/test_hxn.py + hxn doctests: 20 passed; sugarcane network re-rendered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…er circles The unit/stream label sat lower than the bold index (both were anchored with va='bottom' at different font sizes) and its white box could cover exchanger and utility circles on the stream line. Put index and label on a shared baseline (va='baseline') raised to yi + 0.25 so both sit in line and clear of the circles. tests/test_hxn.py + hxn doctests: 20 passed; sugarcane network re-rendered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With a shared baseline the smaller label still reads slightly low next to the larger bold index; raise its baseline by 0.08 so the two appear vertically centered on each other. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ts, and documented heat recovery
Ten synthetic systems of increasing complexity (2 to 10 process streams,
built from simulated HXutility units; every case from the third on has
phase-changing streams: total and partial condensers, boiling and
partially boiling cold streams, mixed pressures, a threshold problem and a
near-degenerate cascade). For each, the synthesized network must
(i) close its energy balance to |error| < 0.1 % with no numerical
RuntimeWarning escaping simulate();
(ii) never beat the problem-table MER targets computed on the same
streams (a network below MER is a bookkeeping error), and satisfy
actual_heat - actual_cool == net process duty;
(iii) use no more hot or cold utility than documented in CASES
(recorded at 1ab689f by running the file directly), so a future
change to hxn cannot silently make the synthesizer recover less heat.
The recorded baselines also document where the greedy heuristic currently
falls short of MER: cases 1-4 and 7 hit the target exactly; cases 8 and 9
are within 1 %; case 5 (boiling cold stream) uses 5.8e6 kJ/hr of hot
utility where MER is 0, and case 6 uses 7.1e6 against an MER of 2.1e6.
Case 10 is a strict xfail: synthesis emits a zero-duty exchanger
(HX_8_2_cs, hot 355 K against cold 365 K, Q = 0) whose cold inlet is hotter
than its hot inlet, and compute_LMTD then raises FloatingPointError on the
log of a negative ratio. Fixing that flips the xfail into a failure that
prompts recording the case's baseline.
Validation: tests/test_hxn_regression.py -> 9 passed, 1 xfailed (5.7 s,
not marked slow). Full CI-style suite -> 74 failed, 488 passed, 62 skipped,
1 xfailed; the 4 failures beyond the documented 70 (test_oilcane_O6/O8/O9:
AttributeError inside the sibling biorefineries clone; test_tire: value
drift, fails identically on unmodified master) are pre-existing stack drift
since the baseline was measured against the pip thermosteam wheel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… and H_lim at the bubble point Regression case 10 (tests/test_hxn_regression.py) crashed in compute_LMTD with a zero-duty exchanger whose cold inlet was hotter than its hot inlet. Tracing it back exposed three independent defects, each fixed at its cause: 1. Phantom pinch loads for non-equilibrium inlets (hxn_synthesis.py). load_duties() and the transient pinch-state setup in synthesize_network() flashed each stream at its pinch temperature and took that equilibrium enthalpy at face value. A non-rigorous HXutility can carry a superheated liquid (ethanol at 370 K, 1 atm, H = 6.4e6 kJ/hr); flashing it at the 355 K pinch yields vapor with H = 32.0e6, and abs(H_pinch - H_in) turned that into a 25.6e6 kJ/hr "cooling load" above the pinch for a stream whose whole duty is 5.45e6. Exchangers were then synthesized against a transient stream carrying latent heat the real stream does not have; rewired with the real inlet they behaved differently, overheating a cold stream and leaving a downstream match with crossed, zero-duty inlets. New pinch_state() flashes at the pinch T and, when the result lies outside [min(H_in, H_out), max(H_in, H_out)], re-flashes at the clipped enthalpy, so the hot-side and cold-side loads split |H_in - H_out| exactly by construction (the abs() is gone). This is the synthesizer's counterpart of the clipping the problem table got in 08e873c. 2. HXN_sys path in synthesis order (_heat_exchanger_network.py). _cost built bst.System(path=new_HXs + new_HX_utils). Hot-side exchangers are synthesized before the cold-side exchangers that feed them after the life-cycle rewiring, and with no recycle declared the system ran the path once, leaving those exchangers with stale inlets (an energy-balance residual equal to the upstream duty change). The path is now built with tmo.Network.from_units(all_units, interaction=False) and System._from_network, which orders units by their connections and detects the genuine loops in larger networks for the fixed-point convergence that was already configured. interaction=False is required: HXprocess is an interaction unit and the default strips those out and disconnects them. 3. heat_exchange_to_condition ignored H_lim at the bubble point (heat_transfer.py). When the temperature limit lands within 1e-3 K of the stream's bubble point (e.g. T_hot_in - dT = 373.124 K for water at 1 atm) the outlet was set to the saturated phase and the enthalpy limit was never applied, so a cold stream's apparent capacity became its full vaporization and it overshot its enthalpy target; its "heating" utility then had to cool it back. The limit is now applied in that branch too, by VLE since the clipped state is two-phase. Also in counter_current_heat_exchange, both no-exchange early exits did s1_in.copy_like(s1_out), overwriting an inlet with the already modified outlet (observed rewriting a superheated-liquid inlet to vapor); corrected to s1_out.copy_like(s1_in). Tests: tests/test_heat_exchange.py (new) covers H_lim at the bubble point for heat_exchange_to_condition and HXprocess, and inlet immutability; tests/test_hxn.py gains a load_duties/pinch_state energy-conservation test for a non-equilibrium inlet. Case 10 loses its xfail and records its baseline (hot 1.40742e7, cold 8.06488e6 kJ/hr, both above MER, energy balance error 0.0000 %); the H5 input is corrected to 1 atm (it was a subcooled vapor at 2 bar). Cases 1-9 are bit-identical to their baselines. Validation: targeted HX/HXN tests and doctests -> 37 passed. Full CI-style suite -> 74 failed, 493 passed, 62 skipped: exactly the same 74 pre-existing failures as the previous run, +5 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…path warning, tighter regression tolerances Follow-up to 75f1402 from a code review of the branch. pinch_state (hxn_synthesis.py) now takes the stream's real end states and, when the pinch temperature coincides with an end temperature or the flash at the pinch leaves the stream's enthalpy range, returns the *equilibrium state at that end enthalpy* (new helper _end_state). Two defects closed: - A non-equilibrium inlet with pinch_T == T_in still had its duty split across the pinch: flashing a superheated liquid at its own temperature does not reproduce H_in, and the result can lie inside the range so the clip never fired (superheated liquid water 380 -> 400 K put 98 % of the duty on the side the stream is "unavailable" on). Now H_pinch == H_in exactly, so the whole duty lands on one side as the pinch analysis promises. - A phase-mislabelled non-condensable (N2 as 'l' at 400 K) re-flashed at the clipped enthalpy came back as gas at ~2500 K and drove matching. _end_state falls back to the stream as given when the equilibrium state leaves the stream's own [T_out, T_in] range. The review suggested returning the stream as given at end points; that was tried and rejected: it lets the synthesizer offer a non-equilibrium inlet's heat at its fictitious temperature (liquid ethanol "at 370 K") and case 10 then beat the problem-table MER by 0.3 %. The equilibrium state at the end enthalpy keeps synthesis consistent with the table, which is the invariant the regression suite enforces. _heat_exchanger_network.py: thermosteam's Network.sort can warn 'network path could not be determined' when its ordering heuristic does not settle; since 75f1402 that could surface from HXN with no context. It is caught and re-issued as an HXN-specific RuntimeWarning (tested by monkeypatching Network.from_units). tests/test_hxn_regression.py: converged networks close their energy balance to ~1e-10 %, so the 0.1 % tolerance could not catch the stale-ordering defect (ablating it gives 0.049 %). EB_TOLERANCE is now 1e-6 % and the heat - cool == net duty identity uses rtol 1e-8. Case 5's documented baseline is lowered (hot 5.8433e6 -> 3.2224e6, cold 1.04356e7 -> 7.81466e6): its 420 K, 5 bar vapor feed is below water's boiling point there, a non-equilibrium inlet now handled consistently. Cases 1-4 and 6-10 are unchanged. Validation: HX/HXN tests and doctests -> 37 passed. Full CI-style suite -> 74 failed, 495 passed, 62 skipped; the same 74 pre-existing failures as the previous two runs, +2 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
biosteam/facilities/hxn and its tests now live in github.com/BioSTEAMDevelopmentGroup/hensmith, extracted with full git history (including the pre-March-2023 biosteam/units/facilities/hxn paths) at this branch state (1d882c4). biosteam gains hensmith as an install dependency and re-exports HeatExchangerNetwork lazily via PEP 562 module __getattr__ in both biosteam/__init__ and biosteam.facilities, so bst.HeatExchangerNetwork, biosteam.facilities.HeatExchangerNetwork, from-imports, and star-imports keep working. The laziness makes the biosteam <-> hensmith circular dependency import-safe in both orders: importing biosteam never initializes hensmith mid-import, and hensmith imports biosteam eagerly to subclass bst.Facility. The name is intentionally absent from facilities.__all__ because biosteam/__init__ star-imports facilities during initialization, which would resolve it eagerly and re-create the cycle. Covered by tests/test_hensmith_integration.py (subprocess tests of both import orders); biosteam also imports cleanly with hensmith absent. The synthesis helpers (synthesize_network, problem_table, plot_pinch_diagram, StreamLifeCycle, ProblemTable) are no longer exported from biosteam; no downstream biorefinery references them via biosteam (verified across Bioindustrial-Park). Docs page for HeatExchangerNetwork now documents hensmith.HeatExchangerNetwork and notes the move. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…0, wire CI/docs Implements the ten findings from the PR #257 code review of the HXN -> hensmith migration. Version (finding 1): bump biosteam 2.53.11 -> 2.54.0 and the setup.py install_requires so hensmith can pin a floor (biosteam>=2.54.0) that the pre-migration PyPI wheel does not satisfy. Otherwise `pip install hensmith` onto released biosteam 2.53.11 leaves two distinct HeatExchangerNetwork classes (the bundled biosteam.facilities.hxn copy winning bst.HeatExchangerNetwork), silently breaking downstream isinstance checks. Lazy re-export shim (findings 4, 8, 10): replace the two hand-copied single-name __getattr__ shims with one shared helper in facilities/__init__.py, delegated to from biosteam/__init__. It now - catches ImportError and re-raises AttributeError with a hensmith pointer, so hasattr()/getattr(default) no longer leak ModuleNotFoundError and create_all_facilities fails with a message that names the move; - caches resolved supported names into module globals (PEP 562 memoization) so bst.HeatExchangerNetwork stops re-entering the import machinery on every access, and adds the companion __dir__; - forwards the five moved synthesis helpers (StreamLifeCycle, ProblemTable, problem_table, synthesize_network, plot_pinch_diagram) with a DeprecationWarning for one release cycle, rather than a bare AttributeError. CI / docs (findings 2, 7): git-install hensmith from HEAD in build.yml (--no-deps, since the biosteam<->hensmith pins are circular by design) so CI validates the sibling repo instead of the frozen PyPI wheel; add ../hensmith to docs/conf.py's sys.path so autoclass resolves from a source checkout; qualify the docs note (star-import of biosteam.facilities no longer provides the name; all five helpers listed and marked deprecated). Tests (findings 5, 6, 9): rework tests/test_hensmith_integration.py to capture subprocess stderr, assert HeatExchangerNetwork stays out of facilities.__all__ (the cycle guard), exercise the hensmith-first and from-scratch star-import orderings, assert missing-hensmith degrades to AttributeError, assert the deprecated helpers warn, and add a create_all_facilities smoke test that constructs the network through the lazy re-export. Validation: full suite (CI invocation, local thermosteam+hensmith clones) 74 failed / 469 passed / 62 skipped -- identical documented pre-existing failure set, passed up by the 4 new integration tests. hensmith suite 33 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thank for putting this together! I see you added lazy loading to avoid the circular dependency. It is a good feature, but I would like to avoid lazy loading because it adds a layer of complexity which may not be necessary since hensmith is quick to load (not to mention lazyloading adds an overhead for every attribute call and import that can stack up). Instead, we can import HXN from hensmith in Also, I can directly add hensmith's objects (like HXN) to the biosteam docs if I set it up as a submodule (similar to thermosteam's integrated docs). Would this be something you are interested in? Otherwise, I can always add a link to hensmith's separate documentation later. Thank you! |
Upstream merged the HXN PR stack (#253-#256) into master and added two follow-up commits touching files this branch deleted when the heat exchanger network moved to hensmith: - bc06684 reworded the cache_network comments in biosteam/facilities/hxn/_heat_exchanger_network.py - 15f6c63 loosened the cache_network test tolerance for reproducibility in tests/test_hxn.py (rtol 1e-6 -> 2e-3, feed perturbation 1.01 -> 1.000001) Both modify/delete conflicts are resolved by keeping the deletions; the two changes are ported to hensmith (hensmith/_heat_exchanger_network.py and tests/test_hxn.py) on its port-upstream-hxn-review-tweaks branch.
…review The PR #257 review asked to drop the PEP 562 module __getattr__ shims that re-exported HeatExchangerNetwork from hensmith: they add a layer of indirection that is unnecessary because hensmith is quick to import. biosteam/__init__.py now ends with a plain `import hensmith`, after every biosteam subpackage hensmith needs (it subclasses Facility) is initialized, and lists 'HeatExchangerNetwork' in __all__ only when that import succeeds. Only a missing hensmith (ModuleNotFoundError naming hensmith) is tolerated, so biosteam still imports in environments without it; an error raised while hensmith itself initializes propagates instead of being masked. The binding of the name is done by hensmith, not here: in the hensmith-first import order biosteam's __init__ runs from hensmith's own `import biosteam` while hensmith is still initializing and HeatExchangerNetwork does not exist yet, so a `from hensmith import HeatExchangerNetwork` in biosteam would raise ImportError in that order. hensmith 0.1.2 therefore binds HeatExchangerNetwork into biosteam and biosteam.facilities at the end of its own __init__ -- whichever package finishes initializing second does the binding -- and setup.py pins hensmith>=0.1.2 since 0.1.1 (on PyPI) would leave the name unbound. The name stays out of facilities.__all__, which biosteam star-imports before hensmith can bind it. The __getattr__/__dir__ shims and the deprecated aliases for the synthesis helpers (StreamLifeCycle, ProblemTable, problem_table, synthesize_network, plot_pinch_diagram) are removed; those live in hensmith only (no downstream user in biorefineries). create_all_facilities already used bst.HeatExchangerNetwork with a module-scope import. tests/test_hensmith_integration.py is rewritten for the eager contract: both import orders, star-import from scratch, missing hensmith (name absent from the namespace and from __all__), broken hensmith propagating, the facilities.__all__ guard, and the create_all_facilities smoke test (7 passed). hensmith suite: 33 passed. Full suite (CI invocation, with thermosteam at f768d38 as master's submodule pointer requires): 82 failed, 462 passed, 62 skipped -- versus the recorded 74-failure baseline, 15 fewer notebook failures and 23 new doctest failures that are stack drift in the merged master (21 single-component Stream.show representation from thermosteam 68e89dc; BinaryDistillation and ShortcutColumn platform-and-ladders labels not regenerated after #252), none involving hensmith. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Thanks, @yoelcortes! Done, lazy loading removed. One issue I ran into without lazy loading: I fixed this issue by letting hensmith do the binding: its I merged your recent changes to master into this branch and retargeted the PR to master. BTW, #254–#256 were merged into each other's branches a few minutes after #253 had already gone into master, so the problem-table fixes, the pinch diagram, and the regression tests aren't on master yet — they're still on On docs API — yes, I'd love to have hensmith as a submodule like thermosteam (would be great if we could also add a link to the hensmith docs in the bst API and also credit hensmith appropriately elsewhere in the docs). Happy to set it up / feel free to set it up whenever you'd like! |
What
Migrates the heat exchanger network facility (
biosteam/facilities/hxn) and its tests out of biosteam into the new standalone hensmith package (Heat Exchanger Network Synthesis, Modeling, Integration, Thermodynamics, and Heuristics), which biosteam now depends on.Stacked on #256 (
hxn-regression-tests) — retarget tomasterafter the #253 → #254 → #255 → #256 chain merges. hensmith HEAD was extracted at exactly this base (1d882c4), so this PR's diff is purely the migration.History preservation
hensmith's
master(146 commits) was extracted withgit filter-repofrom biosteam at 1d882c4, keeping every commit that touchedbiosteam/units/facilities/hxn(the pre-March-2023 path) orbiosteam/facilities/hxn, back to biosteam's Oct-2020 root commit ("Reset commit history") — the earliest HXN history that exists in any repo. The migrated files are byte-identical to this branch's copies at the extraction point.Circular dependency design
biosteam requires hensmith (
setup.py), and hensmith requires biosteam (its classes subclassbst.Facility). The cycle is import-safe by direction-asymmetry:HeatExchangerNetworklazily via PEP 562 module__getattr__inbiosteam/__init__.pyandbiosteam/facilities/__init__.py, soimport biosteamnever initializes hensmith mid-import.bst.HeatExchangerNetwork,biosteam.facilities.HeatExchangerNetwork, from-imports, andfrom biosteam import *all keep working.facilities.__all__: biosteam's__init__star-importsfacilitiesduring initialization, which would resolve the name eagerly and re-create the cycle when hensmith is imported first.import biosteamalso works with hensmith absent — only accessing the name requires it.Covered by the new
tests/test_hensmith_integration.py(subprocess tests of both import orders, written to fail before the migration and pass after).The synthesis helpers (
synthesize_network,problem_table,plot_pinch_diagram,StreamLifeCycle,ProblemTable) are no longer exported from biosteam — no downstream biorefinery references them via biosteam (verified across Bioindustrial-Park). TheHeatExchangerNetworkRTD page now documentshensmith.HeatExchangerNetworkand notes the move.Validation
test_hxnfunctions, 12test_hxn_regressionitems, 3 doctests) and gained the 3 new integration tests. All 74 failures are pre-existing (notebooks, missing optional deps, stack drift).tests/test_biorefineries.py:test_sugarcane(HXN end-to-end through the lazy re-export) passes; only the pre-existing failures remain (oilcane O6/O8/O9, cornstover).Release ordering
setup.pynow requireshensmith>=0.1.0, so hensmith must be published to PyPI before the next biosteam release / CI run / RTD build that pip-installs biosteam's dependencies (or CI temporarily installs it from git). hensmith README and RTD docs are follow-up work in that repo.🤖 Generated with Claude Code