Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add steam heater and condenser #1358

Merged
merged 31 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
fcdd657
add steam heater
ElmiraShamlou Apr 12, 2024
ada04b6
modification
ElmiraShamlou Apr 12, 2024
673ebb8
add steam flow cost to heat exchanger costing method
ElmiraShamlou Apr 15, 2024
41a2518
renames files, updates tests
ElmiraShamlou Apr 16, 2024
5a40980
removes files with old names
ElmiraShamlou Apr 16, 2024
e503723
Merge branch 'main' into steam-heater
ElmiraShamlou Apr 16, 2024
f102095
add docs, revise initialization, revise tests
ElmiraShamlou Apr 16, 2024
c2caa2e
Merge branch 'steam-heater' of https://github.com/ElmiraShamlou/water…
ElmiraShamlou Apr 16, 2024
1a6bba6
add config option for outlet saturation pressure
ElmiraShamlou Apr 16, 2024
515a540
modifies config options
ElmiraShamlou Apr 16, 2024
2928af4
code linting
ElmiraShamlou Apr 16, 2024
8ae8fe9
update test
ElmiraShamlou Apr 16, 2024
2d97f6a
update tests
ElmiraShamlou Apr 16, 2024
ca962d8
add iter limit
ElmiraShamlou Apr 17, 2024
36e4a1f
applies unit test harness
ElmiraShamlou Apr 17, 2024
62ef175
fixes pressure
ElmiraShamlou Apr 18, 2024
96347b9
Merge branch 'main' into steam-heater
ElmiraShamlou Apr 22, 2024
8a56c8a
watertap get_solver
ElmiraShamlou Apr 22, 2024
2d6d274
Merge branch 'main' into steam-heater
adam-a-a May 15, 2024
5c42ba6
Merge branch 'main' into steam-heater
ElmiraShamlou Jun 6, 2024
6c26717
Merge branch 'main' into steam-heater
ElmiraShamlou Jun 25, 2024
97ea972
update model to include condenser
ElmiraShamlou Jun 27, 2024
9f057b5
add test for the condenser cooling water flow estimation
ElmiraShamlou Jul 1, 2024
9e0390d
Merge branch 'main' into steam-heater
ElmiraShamlou Jul 1, 2024
6608eeb
add conservation check
ElmiraShamlou Jul 1, 2024
a7c890b
add steam cost to the hx doc
ElmiraShamlou Jul 1, 2024
d075185
add steam cost to hx doc
ElmiraShamlou Jul 1, 2024
ca9c656
Merge branch 'main' into steam-heater
ElmiraShamlou Jul 1, 2024
bf9c705
Merge branch 'main' into steam-heater
ElmiraShamlou Jul 1, 2024
1bbde8c
Merge branch 'main' into steam-heater
ElmiraShamlou Jul 2, 2024
b63d120
modifies hx doc and test
ElmiraShamlou Jul 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion watertap/costing/unit_models/heat_exchanger.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,20 @@
units=pyo.units.dimensionless,
)

blk.steam_cost = pyo.Var(
initialize=0.008,
units=pyo.units.USD_2018 / (pyo.units.kg),
doc="steam cost per kg",
)
MarcusHolly marked this conversation as resolved.
Show resolved Hide resolved

blk.parent_block().register_flow_type("steam", blk.steam_cost)


@register_costing_parameter_block(
build_rule=build_heat_exchanger_cost_param_block,
parameter_block_name="heat_exchanger",
)
def cost_heat_exchanger(blk):
def cost_heat_exchanger(blk, cost_steam_flow=False):
"""
Heat Exchanger Costing Method

Expand All @@ -54,3 +62,12 @@
to_units=blk.costing_package.base_currency,
)
)

if cost_steam_flow:
blk.costing_package.cost_flow(

Check warning on line 67 in watertap/costing/unit_models/heat_exchanger.py

View check run for this annotation

Codecov / codecov/patch

watertap/costing/unit_models/heat_exchanger.py#L67

Added line #L67 was not covered by tests
pyo.units.convert(
(blk.unit_model.hot_side_inlet.flow_mass_phase_comp[0, "Vap", "H2O"]),
to_units=pyo.units.kg / pyo.units.s,
),
"steam",
)
195 changes: 195 additions & 0 deletions watertap/unit_models/steam_heater_0D.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#################################################################################
# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California,
# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory,
# National Renewable Energy Laboratory, and National Energy Technology
# Laboratory (subject to receipt of any required approvals from the U.S. Dept.
# of Energy). All rights reserved.
#
# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license
# information, respectively. These files are also available online at the URL
# "https://github.com/watertap-org/watertap/"
#################################################################################


from idaes.core import (
declare_process_block_class,
)
from idaes.models.unit_models.heat_exchanger import HeatExchangerData
from watertap.core.solvers import get_solver
import idaes.logger as idaeslog
from watertap.costing.unit_models.heat_exchanger import (
cost_heat_exchanger,
)
from enum import Enum, auto
from pyomo.common.config import ConfigValue


_log = idaeslog.getLogger(__name__)


__author__ = "Elmira Shamlou"

"""
This unit model uses is based on the IDAES `feedwater_heater_0D` model.
However, the constraints and properties defined in the IDAES unit model do not align with those in the available WaterTAP property packages.
To address this, alternative constraints have been replaced to ensure full condensation based on the WaterTAP properties.
In addition, a condenser option and its corresponding initialization routine have been added, allowing the user to switch between the condenser and steam heater models.
Note that additional components like desuperheaters, drain mixers, and coolers are not included. If necessary, these can be modeled separately by adding heat exchangers and mixers to the flowsheet.
To do: Consider incorporating as a modification to IDAES' FeedWaterHeater model directly in the IDAES repo"
"""


class Mode(Enum):
HEATER = auto()
CONDENSER = auto()


@declare_process_block_class(
"SteamHeater0D",
doc="""Feedwater Heater Condensing Section
The feedwater heater condensing section model is a normal 0D heat exchanger
model with an added constraint to calculate the steam flow such that the outlet
of shell is a saturated liquid.""",
)
class SteamHeater0DData(HeatExchangerData):
CONFIG = HeatExchangerData.CONFIG()
CONFIG.declare(
"mode",
ConfigValue(
default=Mode.HEATER,
domain=Mode,
description="Mode of operation: heater or condenser",
),
)
CONFIG.declare(
"estimate_cooling_water",
ConfigValue(
default=False,
domain=bool,
description="Estimate cooling water flow rate for condenser mode",
),
)

def build(self):
super().build()

@self.Constraint(
self.flowsheet().time,
self.hot_side.config.property_package.component_list,
doc="Mass balance",
)
def outlet_liquid_mass_balance(b, t, j):
lb = b.hot_side.properties_out[t].flow_mass_phase_comp["Vap", j].lb
b.hot_side.properties_out[t].flow_mass_phase_comp["Vap", j].fix(lb)
return (
b.hot_side.properties_in[t].flow_mass_phase_comp["Vap", j]
+ b.hot_side.properties_in[t].flow_mass_phase_comp["Liq", j]
== b.hot_side.properties_out[t].flow_mass_phase_comp["Liq", j]
)
ElmiraShamlou marked this conversation as resolved.
Show resolved Hide resolved

@self.Constraint(self.flowsheet().time, doc="Saturation pressure constraint")
def outlet_pressure_sat(b, t):
return (
b.hot_side.properties_out[t].pressure
>= b.hot_side.properties_out[t].pressure_sat
)

def initialize_build(self, *args, **kwargs):
"""
Initialization routine for both heater and condenser modes. For condenser mode with cooling water estimation, the initialization is performed based on a specified design temperature rise on the cold side.
"""
solver = kwargs.get("solver", None)
optarg = kwargs.get("optarg", {})
outlvl = kwargs.get("outlvl", idaeslog.NOTSET)
init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit")
solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit")

if self.config.mode == Mode.HEATER:
self.hot_side_inlet.fix()
self.cold_side_inlet.fix()
self.hot_side_outlet.unfix()
self.cold_side_outlet.unfix()
self.area.fix()

self.outlet_liquid_mass_balance.deactivate()
self.outlet_pressure_sat.deactivate()

# regular heat exchanger initialization
super().initialize_build(*args, **kwargs)

for j in self.hot_side.config.property_package.component_list:
self.hot_side.properties_out[0].flow_mass_phase_comp["Vap", j].fix(0)

self.outlet_liquid_mass_balance.activate()
self.outlet_pressure_sat.activate()

for j in self.hot_side.config.property_package.component_list:
self.hot_side_inlet.flow_mass_phase_comp[0, "Vap", j].unfix()

opt = get_solver(solver, optarg)

with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
res = opt.solve(self, tee=slc.tee)
init_log.info(
"Initialization Complete (w/ extraction calc): {}".format(
idaeslog.condition(res)
)
)
elif (
self.config.mode == Mode.CONDENSER
and not self.config.estimate_cooling_water
):
# condenser mode without cooling water estimation
super().initialize_build(*args, **kwargs)
opt = get_solver(solver, optarg)

with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
res = opt.solve(self, tee=slc.tee)
init_log.info("Initialization Complete {}".format(idaeslog.condition(res)))

elif self.config.mode == Mode.CONDENSER and self.config.estimate_cooling_water:

# condenser mode with cooling water estimation
cold_side_outlet_temperature = self.cold_side_outlet.temperature[0].value
self.hot_side_inlet.fix()
self.cold_side_inlet.fix()
self.cold_side_outlet.unfix()

super().initialize_build(*args, **kwargs)
self.area.unfix()
self.cold_side_outlet.temperature[0].fix(cold_side_outlet_temperature)
self.cold_side.properties_in[0].mass_frac_phase_comp["Liq", "TDS"]
self.cold_side.properties_in[0].mass_frac_phase_comp["Liq", "TDS"].fix()

for j in self.cold_side.config.property_package.component_list:
self.cold_side.properties_in[0].flow_mass_phase_comp["Liq", j].unfix()

self.outlet_liquid_mass_balance.activate()
self.outlet_pressure_sat.activate()

opt = get_solver(solver, optarg)

with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
res = opt.solve(self, tee=slc.tee)
init_log.info(
"Initialization Complete (w/ cooling water estimation): {}".format(
idaeslog.condition(res)
)
)

self.cold_side.properties_in[0].mass_frac_phase_comp["Liq", "TDS"].unfix()
self.cold_side.properties_in[0].flow_mass_phase_comp["Liq", "TDS"].fix()

opt = get_solver(solver, optarg)

with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
res = opt.solve(self, tee=slc.tee)
init_log.info(
"Initialization Complete (w/ cooling water estimation): {}".format(
idaeslog.condition(res)
)
)

@property
def default_costing_method(self):
return cost_heat_exchanger

Check warning on line 195 in watertap/unit_models/steam_heater_0D.py

View check run for this annotation

Codecov / codecov/patch

watertap/unit_models/steam_heater_0D.py#L195

Added line #L195 was not covered by tests
Loading
Loading