Skip to content

Add heat exchanger performance and cost components - #418

Open
bayc wants to merge 28 commits into
NatLabRockies:developfrom
bayc:feature/heat_exchanger
Open

Add heat exchanger performance and cost components#418
bayc wants to merge 28 commits into
NatLabRockies:developfrom
bayc:feature/heat_exchanger

Conversation

@bayc

@bayc bayc commented Dec 18, 2025

Copy link
Copy Markdown
Collaborator

Add HX performance and cost components

This PR adds a simple heat exchanger model, along with a cost model. This model is meant to be used with other models that are being developed, such as the electric thermal energy storage model #810 and a yet to be opened electric/natural gas boiler model PR. As such, an explicit example using this technology has not yet been added, but will be as part of an integrated example of these components.

This model is meant to accept two fluids, a source fluid (that brings the heat) and a working fluid (that removes the heat). I would appreciate specific review on the implementation of these inputs/outputs and whether they should be changed in some way to reflect commodity streams, or some other form.

Also, review of the organization of the code would be helpful. I originally had the specific heat exchanger functions located in a sub-folder converters/heat/heat_exchanger_model, but the current version has those functions integrated directly into the model class. So feedback on which method will determine what remains and what gets removed.

Docs will be updated once reviewed to be informed by any required changes.

Section 1: Type of Contribution

  • Feature Enhancement
    • Framework
    • New Model
    • Updated Model
    • Tools/Utilities
    • Other (please describe):
  • Bug Fix
  • Documentation Update
  • CI Changes
  • Other (please describe):

Section 2: Draft PR Checklist

  • Open draft PR
  • Describe the feature that will be added
  • Fill out TODO list steps
  • Describe requested feedback from reviewers on draft PR
  • Complete Section 7: New Model Checklist (if applicable)

TODO:

  • Have TLDRD team review implementation of model.
  • Determine whether an example is needed now or later (once other heat components get added).
  • Ask for reviews from H2I team once above are complete.

Type of Reviewer Feedback Requested (on Draft PR)

Structural feedback:

  • Feedback on the addition of a heat folder to the converters directory.

Implementation feedback:

  • The specifications of the incoming and outgoing heat exchanging fluids as OpenMDAO inputs and outputs

Other feedback:

Section 3: General PR Checklist

  • PR description thoroughly describes the new feature, bug fix, etc.
  • Added tests for new functionality or bug fixes
  • Tests pass (If not, and this is expected, please elaborate in the Section 6: Test Results)
  • Documentation
    • Docstrings are up-to-date
    • Related docs/ files are up-to-date, or added when necessary
    • Documentation has been rebuilt successfully
    • Examples have been updated (if applicable)
  • CHANGELOG.md has been updated to describe the changes made in this PR

Section 3: Related Issues

Section 4: Impacted Areas of the Software

Section 4.1: New Files

  • converters/heat/shell_tube_hx.py
    • Contains the heat exchanger model.
  • converters/heat/shell_tube_hx_cost_model.py
    • Contains the heat exchanger cost model.
  • converters/heat/test/test_shell_tube_hx.py
    • Simple test for heat exchanger.
  • converters/heat/test/test_shell_tube_hx_cost_model.py
    • Simple test for heat exchanger cost model.

Section 4.2: Modified Files

Section 5: Additional Supporting Information

Section 6: Test Results, if applicable

Section 7 (Optional): New Model Checklist

  • Model Structure:
    • Follows established naming conventions outlined in docs/developer_guide/coding_guidelines.md
    • Used attrs class to define the Config to load in attributes for the model
      • If applicable: inherit from BaseConfig or CostModelBaseConfig
    • Added: initialize() method, setup() method, compute() method
      • If applicable: inherit from CostModelBaseClass
  • Integration: Model has been properly integrated into H2Integrate
    • Added to supported_models.py
    • If a new commodity_type is added, update create_financial_model in h2integrate_model.py
  • Tests: Unit tests have been added for the new model
    • Pytest-style unit tests
    • Unit tests are in a "test" folder within the folder a new model was added to
    • If applicable add integration tests
  • Example: If applicable, a working example demonstrating the new model has been created
    • Input file comments
    • Run file comments
    • Example has been tested and runs successfully in test_all_examples.py
  • Documentation:
    • Write docstrings using the Google style
    • Model added to the main models list in docs/user_guide/model_overview.md
      • Model documentation page added to the appropriate docs/ section
      • <model_name>.md is added to the _toc.yml

@bayc bayc self-assigned this Dec 18, 2025
@bayc bayc added enhancement New feature or request in progress labels Dec 18, 2025
@bayc
bayc marked this pull request as draft December 18, 2025 19:58
@bayc
bayc force-pushed the feature/heat_exchanger branch 5 times, most recently from 5cf34b2 to 8c1bcb7 Compare December 18, 2025 20:50
@bayc
bayc force-pushed the feature/heat_exchanger branch from 8c1bcb7 to a693f1c Compare December 18, 2025 20:58
Comment thread h2integrate/converters/heat/shell_tube_hx_cost_model.py Outdated
Comment thread h2integrate/converters/heat/test/test_shell_tube_hx.py Outdated
@elenya-grant
elenya-grant self-requested a review January 22, 2026 20:39
"""_summary_
This is a very simple placeholder cost model:

- Reference: 240,000 USD for a 1 MW HX with U ~ 1000 W/m²-K

@alidor85 alidor85 Jan 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know the source for this function, I would rather use something from a chemical plant design handbook.
From Sinott and Towler (2021):
U-tube shell and tube HX:
Total installed costs = [(a+b*S^n) * f_year * f_install * f_material]
a = 28000
b = 54
n = 1.2
S - heat transfer area in m^2
limits: 10 m^2 <= S <= 1000 m^2

This is in CEPCI index = 532.9 so need to be converted (it's for Jan. 2010)
To get it to 2022 USD f_year = 816/532.9

f_install = 1.61 (default, maybe let the user add their own?)

It also assums simple carbon steel, so we need to account for materials once temperatures are higher
f_material = 1 for carbon steel (should be a default value), if user specifies another more expensive material they should include f_material

For the OpEx I am not sure we want to include it in a cost function, this should be in the general economic calculation (like ProFAST)

exp_Q = inputs["exp_Q"][0]

scale_Q = (Q_total_W / Q_ref) ** exp_Q if Q_total_W > 0 else 0.0
capex = C_ref * scale_Q

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to add installation factor in here, or are those things usually accounted for elsewhere in H2I analysis?
I think in ther steel paper at least the cost functions are for totat installed costs

@elenya-grant elenya-grant left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Howdy Chris! I'm not sure whether this is ready for a full in-depth review yet but I left a few small comments and did a quick review (I will do another deeper-review once this is not longer a draft). Overall it seems like some very useful and cool functionality! Thanks for the work on this!

Some higher-level notes are:

  • heat_exchanger_model/hx_shell_tube_steady.py: more inline comments and docstrings would be helpful in the functions defined in here
  • Could you update the performance model to use the recently introduced PerformanceModelBaseClass? I'd be happy to help here if needed!
  • Don't forget to add the performance and cost model(s) to supported_models.py

D_o_m: float = field(validator=gt_zero)
t_wall_m: float = field(validator=gt_zero)
D_shell_m: float = field(validator=gt_zero)
cost_year: int = field(validator=gt_zero)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is cost year an input to the performance model config? Doesn't look like its used in this model

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied from other code, good catch!


# Setup OpenMDAO inputs
self.add_input(
"Th_in_C", val=self.config.Th_in_C, units="C", desc="Hot fluid inlet temperature"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

units="degC", "C" is Coulombs in OpenMDAO units

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we will ever model Coulombs...

)

# Setup OpenMDAO outputs
self.add_output("Th_out_C", val=0.0, units="C", desc="Hot fluid outlet temperature")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same note about units being degC instead of C

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated!

"D_shell": self.config.D_shell_m,
}

res = hx_shell_tube_steady(params)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of feel like whatever logic is done in the hx_shell_tube_steady() function should be replaced in ShellTubeHXPerformanceModel as a method

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could understand the reasoning there. The reason why I kept it in a separate file is because that method is ~600 lines of code. Plus the model comes with some printing and plotting utilities. I will move them into the class and have you take a look to see if you think it is better or if keeping them separate is preferred.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved! Let me know what you think.

"Tc_in_C", val=self.config.Tc_in_C, units="C", desc="Cold fluid inlet temperature"
)
self.add_input(
"m_dot_h_kg_s",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think these input and output names should have units in them - because it gets confusing if I did prob.model.get_val("hx.m_dot_h_kg_s", units="kg/h"). I do think units are appropriate for the config input names

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated!

Th_in_C: float = field(validator=gt_zero)
Tc_in_C: float = field(validator=gt_zero)
m_dot_h_kg_s: float = field(validator=gt_zero)
m_dot_c_kg_s: float = field(validator=gt_zero)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that the h and c in the variable names mean "hot" and "cold" but I think that it'd be nice to have the "hot" and "cold" within the variable names themselves, something like:

  • Temp_hot_C or Hot_temp_C
  • Temp_cold_C or Cold_temp_C
  • Hot_mass_flow_rate_kg_s

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried to update all the inputs and outputs to be spelled out.

cost_year (int): Year for cost estimation.
"""

Th_in_C: float = field(validator=gt_zero)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this relates to PR #480, but I think that this could be generalized to just define the two commodities coming into the heat exchanger. Then in ShellTubeHXPerformanceModel, the inputs would be something like: f"{commodity1}_in_temp", f"{commodity2}_in_temp", f"{commodity1}_in", f"{commodity2}_in".

Is the "hot" thing in just hotter than the "cold" thing in (if so - then the model could see which one is hotter rather than having the user define it). Or is the "hot" thing the thing getting heated?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if it ties to #480, to be honest. I could see the generalization happening as the commodities would be the working fluid and the source fluid, which the temperatures of which would be defined by other components. The "cold" thing (working fluid) is the thing getting heated by the "hot" thing (source fluid). For example, the working fluid could be a feedstock for a food/beverage process, while the source fluid could be water that is heated from an electric boiler.

Can you re-evaluate this with respect to #480 now that that is merged in and see if your thoughts/suggestions have changed at all? If not, I can work to implement the generalization as you suggested.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right - I don't think this is related to PR #480 (what a long time ago that was). I haven't re-looked into the code yet, and I understand your definition of the working fluid vs source fluid. But what if the food/beverage process feedstock had more thermal energy than the water - then the working fluid would be the water and the source fluid would be the food/beverage feedstock - right? I'll update this comment in a sec - I'm going to look back at the code real quick.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay - my above comment was more related to the openmdao inputs rather than the config inputs. I guess I was imaging this as almost a transport model instead of a converter. Like - where I could connect the temperature and "out" of two commodity streams to this heat exchanger and that it'd output the temperate after heat has been exchanged. If that's the intended usage - then I think that some of these config inputs would not be necessary because they'd be inputs from upstream techs. Anyway - I think that using this model as a transport model instead is probably out of scope for this PR. So - no changes necessary on this topic!

@bayc bayc closed this Apr 15, 2026
@bayc bayc reopened this Jul 22, 2026
@bayc
bayc force-pushed the feature/heat_exchanger branch from 445a159 to f9bfe3a Compare July 22, 2026 15:58
@bayc
bayc marked this pull request as ready for review August 6, 2026 17:57

@kbrunik kbrunik left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exciting stuff! It was fun getting a chance to dig into it a little. Overall I think it's coming along, I have a few bigger comments particularly regarding the use of a multivariable commodity that I'd love to address before this is ready to come in. Thanks for putting this together though, I think it's really pushing the boudaries of H2I as we know it :)

)

# Setup OpenMDAO inputs
self.add_input(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey I saw your discussion with @elenya-grant about PR #480 and I was thinking more about it. H2I currently focuses on passing commodities or physical mass/electrons around a system. @johnjasa added the notion of multivariable commodities where you can add things like temperature and pressure to the commodity. I think to maintain our structure and plan for a larger heat integration effort in the future we should use that paradigm when handling the heat exchanger so your commodities would be your working fluid and your process fluid and those would have associated temperatures and mass flows. Then you would be able to add a commodity that can be more easily tracked in h2i. This converter has a good example of how to add inputs and outputs that way: https://github.com/NatLabRockies/H2Integrate/blob/develop/h2integrate/converters/natural_gas/dummy_gas_components.py

It might not be fully necessary in this PR to move in this direction but I do think in the long run it's going to be easier on the user to appropriately track heat across their system (through things like combiners, splitters and transporters).

"temp_cold_out", val=0.0, units="degC", desc="Cold fluid outlet temperature"
)
self.add_output("Q_total", val=0.0, units="kW", desc="Total heat transfer rate")
self.add_output("epsilon", val=0.0, desc="Effectiveness of the heat exchanger")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On naming outputs it would be really helpful if the OpenMDAO outputs were more descriptive. Also, I've seen it a few places but be sure to keep the units out of the OpenMDAO name.

@@ -0,0 +1,916 @@
"""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file looks duplicative with what's in the body of the ShellTubeHXPerformanceModel, I'd suggest removing this.


@define(kw_only=True)
class ShellTubeHXPerformanceModelConfig(BaseConfig):
"""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest adding an optional argument to be able to plot using the plot_results() function

# ----------------------------------------------------------------------
# 4) Printing summary utility
# ----------------------------------------------------------------------
def print_summary(res, label="Python + CoolProp"):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggest removing or adding an option for a user set in the config to print the summary.

Tc_out0_max = Th_in - 1e-3
Tc_out0_guess = Tc_in + 0.7 * (Th_in - Tc_in)

def march_segments(Th_in_local, Tc_out0_local):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest adding at least a docstring to this function

S_gen_seg,
)

def residual_Tc_out(Tc_out0):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A docstring would be helpful

return Tc_at_L - Tc_in

# Simple bisection
def find_root_bisect(f, a, b, tol, maxit):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also a docstring here

# ----------------------------------------------------------------------
# 3) Main HX model
# ----------------------------------------------------------------------
def hx_shell_tube_steady(self, params=None):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any conditions that would cause this hx model to not work properly and are they captured by the attrs validators or are there some additional errors/warnings that should be added?


class ShellTubeHXPerformanceModel(om.ExplicitComponent):
"""
An OpenMDAO component that wraps the steady shell tube heat exchanger model.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

steady state?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request in progress

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants