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

1D Membrane Model for CO2 Capture and Utilization #1378

Merged
merged 43 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2affa77
Add CCUS file structure
Morgan88888888 Mar 18, 2024
b0a336a
One-dimensional membrane model
Morgan88888888 Mar 18, 2024
775b27a
add the unit test file
Morgan88888888 Mar 18, 2024
dd437b7
reformatted by black
Morgan88888888 Mar 18, 2024
693fd68
reformat using black
Morgan88888888 Mar 18, 2024
5505b1f
fixed the path issue in testing
Morgan88888888 Mar 18, 2024
f91e825
added the missing heading
Morgan88888888 Mar 18, 2024
446951b
refined the workspace name
Morgan88888888 Mar 19, 2024
db77a65
formatting issue resolved for GitHub test
Morgan88888888 Mar 19, 2024
508f10c
fixed typo
Morgan88888888 Mar 19, 2024
d31a4cb
fixed the unit model importing issue
Morgan88888888 Mar 29, 2024
e228592
fixed linter warnings
Morgan88888888 Mar 29, 2024
feda25c
resolved the comments
Morgan88888888 Mar 29, 2024
054808f
fix linter issues
Morgan88888888 Mar 29, 2024
f6dc03b
formated
Morgan88888888 Mar 29, 2024
a6c9697
Merge branch 'IDAES:main' into co2_membrane
Morgan88888888 May 2, 2024
14d3856
remove the unit level property config
Morgan88888888 May 13, 2024
e8bc928
save changes
Morgan88888888 May 14, 2024
3d8e068
Merge branch 'IDAES:main' into co2_membrane
Morgan88888888 May 28, 2024
81c3fa3
Merge branch 'IDAES:main' into co2_membrane
Morgan88888888 Jun 4, 2024
35a8ee4
Merge branch 'IDAES:main' into co2_membrane
Morgan88888888 Jul 5, 2024
53a17b5
Merge branch 'co2_membrane' of https://github.com/Morgan88888888/idae…
Morgan88888888 Jul 11, 2024
97abbe7
Merge branch 'IDAES:main' into co2_membrane
Morgan88888888 Nov 5, 2024
b795061
Merge branch 'co2_membrane' of https://github.com/Morgan88888888/idae…
Morgan88888888 Nov 5, 2024
e721692
added test for different configs and added stream table display
Morgan88888888 Nov 5, 2024
2cd61a4
added more docs to explain the models and settings
Morgan88888888 Nov 5, 2024
3e3dda8
Addressed the comments to support different property packages
Morgan88888888 Nov 5, 2024
278345b
add linebreak
Morgan88888888 Nov 5, 2024
c30f6f9
corrected copyright info
Morgan88888888 Nov 5, 2024
0b8a57d
reformatted file to pass test
Morgan88888888 Nov 5, 2024
d826433
fixed copyright info
Morgan88888888 Nov 5, 2024
093de8e
added material conservation test
Morgan88888888 Nov 5, 2024
448394f
reformatted
Morgan88888888 Nov 5, 2024
d6a977c
address comments
Morgan88888888 Nov 5, 2024
75cfb24
added basic documentation
Morgan88888888 Nov 5, 2024
c6d3677
fix pylint test
Morgan88888888 Nov 5, 2024
83529cf
fix doc strings
Morgan88888888 Nov 5, 2024
80befee
fix the doc string
Morgan88888888 Nov 5, 2024
6f3f8a6
added what the inputs/degrees of freedom
Morgan88888888 Nov 5, 2024
93038d4
fix pytest
Morgan88888888 Nov 5, 2024
d44f95e
Merge branch 'main' into co2_membrane
ksbeattie Nov 14, 2024
410c036
Merge branch 'main' into co2_membrane
ksbeattie Nov 21, 2024
b1051bf
Merge branch 'main' into co2_membrane
lbianchi-lbl Nov 27, 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
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ Additional IDAES Model Libraries

phe
temperature_swing_adsorption/fixed_bed_tsa0d
membrane_model/1d_membrane

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
One-dimensional membrane class for CO2 gas separation
=============================================

This is a one-dimensional model for gas separation in CO₂ capture applications.
The model will be discretized in the flow direction, and it supports two flow patterns:
counter-current flow and co-current flow. The model was customized for gas-phase separation
in CO₂ capture with a single-layer design. If a multi-layer design is needed, multiple units
can be connected for this application. The two sides of the membrane are called the feed side
and sweep side. The sweep stream inlet is optional. The driving force across the membrane is the
partial pressure difference in this gas separation application. Additionally, the energy balance
assumes that temperature remains constant on each side of the membrane.
Copy link
Member

Choose a reason for hiding this comment

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

A few comments here:

  1. Reference guides should be entirely autogenerated using Sphinx. Any thing that is hand written really should be an explanation (they go else where in the docs structure).
  2. It would be good to have a short code example here, or a discussion of what the inputs/degrees of freedom are. This will help users understand what information they should be supplying.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks and more info about model inputs/degrees of freedom are added.




Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Framework (IDAES IP) was produced under the DOE Institute for the
# Design of Advanced Energy Systems (IDAES).
#
# Copyright (c) 2018-2023 by the software owners: The Regents of the
# Copyright (c) 2018-2024 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory,
# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
# University, West Virginia University Research Corporation, et al.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Framework (IDAES IP) was produced under the DOE Institute for the
# Design of Advanced Energy Systems (IDAES).
#
# Copyright (c) 2018-2023 by the software owners: The Regents of the
# Copyright (c) 2018-2024 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory,
# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
# University, West Virginia University Research Corporation, et al.
Expand All @@ -15,16 +15,16 @@
One-dimensional membrane class for CO2 gas separation
"""

# pylint: disable=unused-import

from enum import Enum
from pyomo.common.config import Bool, ConfigDict, ConfigValue, In
from pyomo.environ import (
Constraint,
Param,
Var,
units,
Expression,
)

Check warning on line 27 in idaes/models_extra/co2_capture_and_utilization/unit_models/membrane_1d.py

View workflow job for this annotation

GitHub Actions / Pylint

W0611 (unused-import)

Unused Constraint imported from pyomo.environ
from pyomo.network import Port

from idaes.core import (
Expand All @@ -37,6 +37,7 @@
from idaes.core.util.config import is_physical_parameter_block
from idaes.models.unit_models.mscontactor import MSContactor
from idaes.core.util.exceptions import ConfigurationError
from idaes.core.util.tables import create_stream_table_dataframe

__author__ = "Maojian Wang"

Expand All @@ -49,7 +50,6 @@

COUNTERCURRENT = 1
COCURRENT = 2
CROSSFLOW = 3


@declare_process_block_class("Membrane1D")
Expand Down Expand Up @@ -85,6 +85,7 @@
see property package for documentation.}""",
),
)

Stream_Config.declare(
"has_energy_balance",
ConfigValue(
Expand Down Expand Up @@ -134,31 +135,6 @@
sweep side flows from 1 to 0 (default)""",
),
)
CONFIG.declare(
"property_package",
ConfigValue(
default=None,
domain=is_physical_parameter_block,
description="Property package to use for control volume",
doc="""Property parameter object used to define property
calculations
(default = 'use_parent_value')
- 'use_parent_value' - get package from parent (default = None)
- a ParameterBlock object""",
),
)
CONFIG.declare(
"property_package_args",
ConfigValue(
default={},
description="Arguments for constructing property package",
doc="""A dict of arguments to be passed to the PropertyBlockData
and used when constructing these
(default = 'use_parent_value')
- 'use_parent_value' - get package from parent (default = None)
- a dict (see property package for documentation)""",
),
)

for side_name in ["feed", "sweep"]:
Morgan88888888 marked this conversation as resolved.
Show resolved Hide resolved
CONFIG.declare(
Expand All @@ -167,14 +143,19 @@
)

def build(self):
"""
This is a one-dimensional model for gas separation in CO₂ capture applications.
The model will be discretized in the flow direction, and it supports two flow patterns:
counter-current flow and co-current flow. The model was customized for gas-phase separation
in CO₂ capture with a single-layer design. If a multi-layer design is needed, multiple units
can be connected for this application. The two sides of the membrane are called the feed side
and sweep side. The sweep stream inlet is optional. The driving force across the membrane is the
partial pressure difference in this gas separation application. Additionally, the energy balance
assumes that temperature remains constant on each side of the membrane.

"""
super().build()

if self.config.property_package is not None:
if self.config.feed_side.property_package == useDefault:
self.config.feed_side.property_package = self.config.property_package
if self.config.sweep_side.property_package == useDefault:
self.config.sweep_side.property_package = self.config.property_package

feed_dict = dict(self.config.feed_side)
sweep_dict = dict(self.config.sweep_side)

Expand Down Expand Up @@ -210,14 +191,14 @@

def _make_geometry(self):

self.area = Var(initialize=100, units=units.cm**2, doc="The membrane area")
self.area = Var(
initialize=100, units=units.cm**2, doc="Area per cell (or finite element)"
)

self.length = Var(initialize=100, units=units.cm, doc="The membrane length")
self.cell_length = Expression(expr=self.length / self.config.finite_elements)

self.cell_area = Var(
initialize=100, units=units.cm**2, doc="The membrane area"
)
self.cell_area = Var(initialize=100, units=units.cm**2, doc="The membrane area")

@self.Constraint()
def area_per_cell(self):
Expand All @@ -227,50 +208,33 @@
feed_side_units = (
self.config.feed_side.property_package.get_metadata().derived_units
)
crossover_component_list = list(
set(self.mscontactor.feed_side.component_list)
& set(self.mscontactor.sweep_side.component_list)
)

self.permeance = Var(
self.flowsheet().time,
self.mscontactor.elements,
Copy link
Member

Choose a reason for hiding this comment

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

To check, does this need to be indexed by time and finite element? Whilst there are cases where you would want this degree of flexibility, is it something you want to support right now?

Also, I do not think this should be indexed by the feed side component list. Either you should have a single property package for the unit model, in which case you should use the uni level package, or it you want to support separate property packages you should use the intersection of the feed and sweep side component lists (i.e. only create a term for those species which appear in both property packages). Otherwise, you run the risk of trying to write constraints involving species which do not exist on the sweep side.

Copy link
Member Author

Choose a reason for hiding this comment

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

In the simple assumption, it isn't necessary. However, retaining it isn't particularly harmful either.
The example you gave about indexing could occur in some membrane separations but isn't common in this application. I'll keep this idea in mind for future improvements if the need arises.

Copy link
Member

Choose a reason for hiding this comment

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

It is better to fix these issues now rather than wait until later when you have forgotten how the model works.

You need to make the decision to either use a single property package for everything or not, and if you decide to allow for multiple property packages then you need to go the full way to supporting them.

Copy link
Member

Choose a reason for hiding this comment

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

What happens here if the two property packages have different components? Should this instead use the intersection of the two component lists (which is easy to do)?

self.mscontactor.feed_side.component_list,
crossover_component_list,
initialize=1,
doc="Values in Gas Permeance Unit(GPU)",
doc="Values in Gas Permeance Unit (GPU)",
units=units.dimensionless,
)

self.gpu_factor = Param(
default=10e-8 / 13333.2239,
units=units.m / units.s / units.Pa,
mutable=True,
# This is a coefficient that will convert the unit of permeability from GPU to SI units for further calculation"
)

self.selectivity = Var(
self.flowsheet().time,
self.mscontactor.elements,
self.mscontactor.feed_side.component_list,
self.mscontactor.feed_side.component_list,
initialize=1,
units=units.dimensionless,
)

@self.Constraint(
self.flowsheet().time,
self.mscontactor.elements,
self.mscontactor.feed_side.component_list,
self.mscontactor.feed_side.component_list,
doc="permeance calculation",
)
def permeance_calculation(self, t, e, a, b):
return (
self.permeance[t, e, a] * self.selectivity[t, e, a, b]
== self.permeance[t, e, b]
)

p_units = feed_side_units.PRESSURE

@self.Constraint(
self.flowsheet().time,
self.mscontactor.elements,
self.mscontactor.feed_side.component_list,
crossover_component_list,
doc="permeability calculation",
)
def permeability_calculation(self, t, s, m):
Expand All @@ -282,7 +246,9 @@
mb_units = feed_side_units.FLOW_MASS
rho = self.mscontactor.feed_side[t, s].dens_mass
else:
raise TypeError("Undefined flow basis, please define the flow basis")
raise TypeError(
"This model only supports MaterialFlowBasis equal to molar or mass"
)

return self.mscontactor.material_transfer_term[
t, s, "feed_side", "sweep_side", m
Expand All @@ -307,10 +273,31 @@
@self.Constraint(
self.flowsheet().time,
self.mscontactor.elements,
doc="Energy balance",
doc="isothermal constraint",
)
def energy_transfer(self, t, s):
Copy link
Member

Choose a reason for hiding this comment

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

For clarity, I would call this an isothermal constraint. It is not an energy balance.

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

Copy link
Member

Choose a reason for hiding this comment

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

I would also suggest changing the name of the constraint (i.e. the name of the rule/function).

Copy link
Member Author

Choose a reason for hiding this comment

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

changed

return (
self.mscontactor.feed_side[t, s].temperature
== self.mscontactor.sweep_side[t, s].temperature
)

def _get_stream_table_contents(self, time_point=0):
if self.config.sweep_flow:
return create_stream_table_dataframe(
{
"Feed Inlet": self.feed_side_inlet,
"Feed Outlet": self.feed_side_outlet,
"Permeate Inlet": self.sweep_side_inlet,
"Permeate Outlet": self.sweep_side_outlet,
},
time_point=time_point,
)
else:
return create_stream_table_dataframe(
{
"Feed Inlet": self.feed_side_inlet,
"Feed Outlet": self.feed_side_outlet,
"Permeate Outlet": self.sweep_side_outlet,
},
time_point=time_point,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Framework (IDAES IP) was produced under the DOE Institute for the
# Design of Advanced Energy Systems (IDAES).
#
# Copyright (c) 2018-2023 by the software owners: The Regents of the
# Copyright (c) 2018-2024 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory,
# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
# University, West Virginia University Research Corporation, et al.
Expand All @@ -20,6 +20,7 @@

from pyomo.environ import (
check_optimal_termination,
assert_optimal_termination,
ConcreteModel,
value,
)
Expand Down Expand Up @@ -50,9 +51,10 @@
# Get default solver for testing
solver = get_solver()


# -----------------------------------------------------------------------------
@pytest.mark.unit
def test_config():
def test_config_countercurrent():
m = ConcreteModel()
m.fs = FlowsheetBlock(dynamic=False)

Expand All @@ -66,17 +68,44 @@ def test_config():
dynamic=False,
sweep_flow=True,
flow_type=MembraneFlowPattern.COUNTERCURRENT,
property_package=m.fs.properties,
feed_side={"property_package": m.fs.properties},
sweep_side={"property_package": m.fs.properties},
)

# Check unit config arguments
print("=====================================================")
Copy link
Member

Choose a reason for hiding this comment

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

You should remove these print statements for the tests.

Copy link
Member Author

Choose a reason for hiding this comment

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

removed

print(len(m.fs.unit.config))
assert len(m.fs.unit.config) == 7
assert not m.fs.unit.config.dynamic
assert not m.fs.unit.config.has_holdup


@pytest.mark.unit
def test_congif_cocurrent():
m = ConcreteModel()
m.fs = FlowsheetBlock(dynamic=False)

m.fs.properties = GenericParameterBlock(
**get_prop(["CO2", "H2O", "N2"], ["Vap"], eos=EosType.IDEAL),
doc="Key flue gas property parameters",
)

m.fs.unit = Membrane1D(
finite_elements=3,
dynamic=False,
sweep_flow=True,
flow_type=MembraneFlowPattern.COCURRENT,
feed_side={"property_package": m.fs.properties},
sweep_side={"property_package": m.fs.properties},
)

# Check unit config arguments
assert len(m.fs.unit.config) == 9
assert len(m.fs.unit.config) == 7
assert not m.fs.unit.config.dynamic
assert not m.fs.unit.config.has_holdup
assert m.fs.unit.config.property_package is m.fs.properties


class TestMembrane(object):
class TestMembrane:
@pytest.fixture(scope="class")
def membrane(self):
m = ConcreteModel()
Expand All @@ -91,7 +120,8 @@ def membrane(self):
dynamic=False,
sweep_flow=True,
flow_type=MembraneFlowPattern.COUNTERCURRENT,
property_package=m.fs.properties,
feed_side={"property_package": m.fs.properties},
sweep_side={"property_package": m.fs.properties},
)

m.fs.unit.permeance[:, :, "CO2"].fix(1500)
Expand Down Expand Up @@ -151,8 +181,8 @@ def test_build(self, membrane):
assert hasattr(membrane.fs.unit, "permeability_calculation")
assert hasattr(membrane.fs.unit, "energy_transfer")

assert number_variables(membrane) == 184
assert number_total_constraints(membrane) == 116
assert number_variables(membrane) == 157
assert number_total_constraints(membrane) == 89
assert number_unused_variables(membrane) == 28

@pytest.mark.component
Expand All @@ -168,7 +198,7 @@ def test_solve(self, membrane):
initializer.initialize(membrane.fs.unit)
results = solver.solve(membrane)
# Check for optimal solution
assert check_optimal_termination(results)
assert_optimal_termination(results)

@pytest.mark.solver
@pytest.mark.skipif(solver is None, reason="Solver not available")
Expand Down Expand Up @@ -255,3 +285,22 @@ def test_enthalpy_balance(self, membrane):
)
<= 1e-6
)

@pytest.mark.solver
@pytest.mark.skipif(solver is None, reason="Solver not available")
@pytest.mark.component
def test_material_balance(self, membrane):

assert (
abs(
value(
(
membrane.fs.unit.feed_side_inlet.flow_mol[0]
+ membrane.fs.unit.sweep_side_inlet.flow_mol[0]
- membrane.fs.unit.feed_side_outlet.flow_mol[0]
- membrane.fs.unit.sweep_side_outlet.flow_mol[0]
)
)
)
<= 1e-3
)
Loading