#####################################################################################################
# “PrOMMiS” was produced under the DOE Process Optimization and Modeling for Minerals Sustainability
# (“PrOMMiS”) initiative, and is copyright (c) 2023-2026 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory, et al. All rights reserved.
# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license information.
#####################################################################################################
r"""
Multi-Component Diafiltration Unit Model
========================================
Author: Molly Dougher
This membrane unit model is for the multi-component diafiltration of a multi-salt system with a common anion. The model can be built with or without the assumption of a boundary layer. Currently, the model and property packages support one, two, and three salt systems; however, the model can be extended to :math:`n` salts by supplying the appropriate properties and arguments (see below). The membrane is designed for use in a diafiltration cascade, i.e., the model represents one spiral-wound membrane module piece within a cascade of several membranes.
Configuration Arguments
-----------------------
The Multi-Component Diafiltration unit model requires a property package that provides the valency (:math:`z_i`), diffusion coefficients (:math:`D_{i,bl}` and :math:`D_{i,m}`) within the boundary layer and membrane, respectively, in :math:`\mathrm{mm}^2 \, \mathrm{h}^{-1}`, thermodynamic reflection coefficient (:math:`\sigma_i`), partition coefficients (:math:`H_{i,r}` and :math:`H_{i,p}`) at the retentate-membrane and membrane-permeate interfaces, and number of dissolved species (:math:`n_i`) for each ion :math:`i` in solution. When used in a flowsheet, the user can provide separate property packages for the feed and product streams.
There are six configuration arguments to create an instance of the Multi-Component Diafiltration Unit Model:
#. ``cation_list``: list of cations present in the system
``default=["Li", "Co"]``
#. ``anion_list``: list of anions present in the system
``default=["Cl"]``
#. ``include_boundary_layer``: Boolean to specify if the model is to be built with a boundary layer
``default=True``
#. ``NFE_module_length``: the desired number of finite elements across the width of the membrane (i.e., the module length)
``default=10``
#. ``NFE_boundary_layer_thickness``: the desired number of finite elements across the thickness of the boundary layer
``default=5``
#. ``NFE_membrane_thickness``: the desired number of finite elements across the thickness of the membrane
``default=5``
Degrees of Freedom
------------------
The Multi-Component Diafiltration unit model has :math:`5+2n` degrees of freedom, where :math:`n` is the number of cations in the system:
#. the length of the membrane module (``total_module_length``)
#. the length of the membrane (``total_membrane_length``)
#. the pressure applied to the membrane system (``applied_pressure``)
#. the volumetric flow rate of the feed (``feed_flow_volume``)
#. the cation concentration in the feed (``feed_conc_mol_comp[t,k]``)
#. the volumetric flow rate of the diafiltrate (``diafiltrate_flow_volume``)
#. the cation concentration in the diafiltrate (``diafiltrate_conc_mol_comp[t,k]``)
Model Structure
---------------
There are (up to) four regions in the Multi-Component Diafiltration model: the retentate, the boundary layer, the membrane, and the permeate. The retentate and the permeate are only discretized with respect to module length (:math:`x`-direction), while the boundary layer and membrane are discretized with respect to both module length (:math:`x`-direction) and thickness (:math:`z_{bl}`-direction and :math:`z_{m}`-direction, respectively). The resulting system of partial differential algebraic equations is solved by discretizing with the backward finite difference method.
A schematic of the Multi-Component Diafiltration model's geometry can be found `here <https://github.com/prommis/prommis/blob/main/src/prommis/nanofiltration/membrane_schematic.png>`_.
Assumptions
-----------
* The membrane module dimensions, maximum applied pressure, and inlet flow rates assume that one tube (one instance of this model) consists of 4 NF270-440 membranes in series.
* The partitioning relationships, which describe how the solutes transition (partition) across the solution-membrane interfaces, are derived assuming Donnan equilibrium. The partitioning coefficients incorporate both steric and Donnan effects.
* The default value for the membrane's surface charge (:math:`-44 \, \mathrm{mM}`), was calculated using zeta potential measurements for NF270 membranes. (See `this reference <https://doi.org/10.1021/acs.iecr.4c04763>`_). Currently, the default property package only supports negatively charged membranes.
* The boundary layer thickness is assumed to be :math:`20 \, \mathrm{\mu m}` and the membrane thickness is assumed to be :math:`100 \, \mathrm{nm}`.
* The default value for the membrane permeability (:math:`0.01 \, \mathrm{m} \, \mathrm{h}^{-1} \, \mathrm{bar}^{-1}`) is based off of parameter estimation results from `this reference <https://doi.org/10.1021/acs.iecr.4c04763>`_ for NF270 membranes.
* The dominating transport mechanism within the bulk/retentate and permeate solutions is convection.
* The transport mechanisms modeled within the both the boundary layer and the membrane are convection, diffusion, and electromigration.
* The system is uniform with respect to the wound-dimension of the membrane.
* Diffusion that occurs normal to the direction of flux is assumed to be negligible, meaning the concentration gradient of ion :math:`i` only has a :math:`z_{bl}`- or :math:`z_m`-component (perpendicular to the membrane surface).
Sets
----
The Multi-Component Diafiltration model defines the following discrete sets for solutes (:math:`\mathcal{I}`) and cations (:math:`\mathcal{K}`) in the system:
.. math:: \mathcal{I}=\{\mathrm{cation_1, cation_2, ..., cation_n, anion}\}
.. math:: \mathcal{K}=\{\mathrm{cation_1, cation_2, ..., cation_n}\}
where :math:`n` is the desired number of cations.
There are 3 continuous sets for each length dimension: ``dimensionless_module_length`` (in the :math:`x`-direction parallel to the membrane surface), ``dimensionless_boundary_layer_thickness`` (in the :math:`z_{bl}`-direction perpendicular to the membrane surface), and ``dimensionless_membrane_thickness`` (in the :math:`z_m`-direction perpendicular to the membrane surface). The length dimensions, :math:`x`, :math:`z_{bl}`, and :math:`z_m`, are non-dimensionalized as :math:`\bar{x}`, :math:`\bar{z}_{bl}`, and :math:`\bar{z}_m`, respectively, using the module length (:math:`w`), boundary layer thickness (:math:`\delta`), and membrane thickness (:math:`l`), respectively, to improve numerical stability.
.. math:: \bar{x} \in \mathbb{R} \| 0 \leq \bar{x} \leq 1
.. math:: \bar{z}_{bl} \in \mathbb{R} \| 0 \leq \bar{z}_{bl} \leq 1
.. math:: \bar{z}_m \in \mathbb{R} \| 0 \leq \bar{z}_m \leq 1
*Note:* :math:`z_{bl}` *and* :math:`z_m` *point in the same direction (perpendicular to the membrane surface), but are defined as separate length scales to simplify the implementation of the model. The appropriate boundary conditions between* :math:`z_{bl}` *and* :math:`z_m` *are enforced within the model.*
Though this is not implemented as a dynamic model, a set is defined for time.
.. math:: t \in [0]
Default Model Parameters
------------------------
The Multi-Component Diafiltration model has the following parameters.
================ =============================================== ================================== ============= ==========================================================
Parameter Description Name Default Value Units
================ =============================================== ================================== ============= ==========================================================
:math:`\epsilon` numerical tolerance for zero values ``numerical_zero_tolerance`` :math:`1e-10`
:math:`\delta` boundary layer thickness ``total_boundary_layer_thickness`` :math:`2e-05` :math:`\mathrm{m}`
:math:`l` membrane thickness ``total_membrane_thickness`` :math:`1e-07` :math:`\mathrm{m}`
:math:`L_p` hydraulic permeability of the membrane ``membrane_permeability`` :math:`0.01` :math:`\mathrm{m} \, \mathrm{h}^{-1} \, \mathrm{bar}^{-1}`
:math:`T` temperature of the system ``temperature`` :math:`298` :math:`\mathrm{K}`
:math:`\chi` concentration of surface charge on the membrane ``membrane_fixed_charge`` :math:`-44` :math:`\mathrm{mol} \, \mathrm{m}^{-3}`
================ =============================================== ================================== ============= ==========================================================
Variables
---------
The Multi-Component Diafiltration model adds the following variables.
=============================== ==================================================================== ======================================================= =========================================================================== ==========================================================================================================
Variable Description Name Units Indexed over
=============================== ==================================================================== ======================================================= =========================================================================== ==========================================================================================================
:math:`c_{i,bl}` ion concentration in the boundary layer ``boundary_layer_conc_mol_comp`` :math:`\mathrm{mol} \, \mathrm{m}^{-3}` :math:`t`, :math:`\bar{x}`, :math:`\bar{z}_{bl}`, and :math:`i \in \mathcal{I}`
:math:`c_{i,d}` ion concentration in the diafiltrate ``diafiltrate_conc_mol_comp`` :math:`\mathrm{mol} \, \mathrm{m}^{-3}` :math:`t` and :math:`i \in \mathcal{I}`
:math:`c_{i,f}` ion concentration in the feed ``feed_conc_mol_comp`` :math:`\mathrm{mol} \, \mathrm{m}^{-3}` :math:`t` and :math:`i \in \mathcal{I}`
:math:`c_{i,m}` ion concentration in the membrane ``membrane_conc_mol_comp`` :math:`\mathrm{mol} \, \mathrm{m}^{-3}` :math:`t`, :math:`\bar{x}`, :math:`\bar{z}_m`, and :math:`i \in \mathcal{I}`
:math:`c_{i,p}` ion concentration in the permeate ``permeate_conc_mol_comp`` :math:`\mathrm{mol} \, \mathrm{m}^{-3}` :math:`t`, :math:`\bar{x}`, and :math:`i \in \mathcal{I}`
:math:`c_{i,r}` ion concentration in the retentate ``retentate_conc_mol_comp`` :math:`\mathrm{mol} \, \mathrm{m}^{-3}` :math:`t`, :math:`\bar{x}`, and :math:`i \in \mathcal{I}`
:math:`\tilde{D}_{bl}` cross-diffusion coefficient denominator in the boundary layer ``boundary_layer_D_tilde`` :math:`\mathrm{mm}^2 \, \mathrm{h}^{-1} \, \mathrm{mol} \, \mathrm{m}^{-3}` :math:`t`, :math:`\bar{x}`, and :math:`\bar{z}_{bl}`
:math:`D_{kj,bl}^{bilinear}` bilinear cross-diffusion coefficient in the membrane ``boundary_layer_cross_diffusion_coefficient_bilinear`` :math:`\mathrm{mm}^4 \, \mathrm{h}^{-2} \, \mathrm{mol} \, \mathrm{m}^{-3}` :math:`t`, :math:`\bar{x}`, :math:`\bar{z}_{bl}`, :math:`k \in \mathcal{K}`, and :math:`j \in \mathcal{K}`
:math:`D_{kj,bl}` cross-diffusion coefficient in the membrane ``boundary_layer_cross_diffusion_coefficient`` :math:`\mathrm{mm}^2 \, \mathrm{h}^{-1}` :math:`t`, :math:`\bar{x}`, :math:`\bar{z}_{bl}`, :math:`k \in \mathcal{K}`, and :math:`j \in \mathcal{K}`
:math:`\tilde{D}_m` cross-diffusion & convection coefficient denominator in the membrane ``membrane_D_tilde`` :math:`\mathrm{mm}^2 \, \mathrm{h}^{-1} \, \mathrm{mol} \, \mathrm{m}^{-3}` :math:`t`, :math:`\bar{x}`, and :math:`\bar{z}_m`
:math:`D_{kj,m}^{bilinear}` bilinear cross-diffusion coefficient in the membrane ``membrane_cross_diffusion_coefficient_bilinear`` :math:`\mathrm{mm}^4 \, \mathrm{h}^{-2} \, \mathrm{mol} \, \mathrm{m}^{-3}` :math:`t`, :math:`\bar{x}`, :math:`\bar{z}_m`, :math:`k \in \mathcal{K}`, and :math:`j \in \mathcal{K}`
:math:`\alpha_{k,m}^{bilinear}` bilinear convection coefficient in the membrane ``membrane_convection_coefficient_bilinear`` :math:`\mathrm{mm}^2 \, \mathrm{h}^{-1} \, \mathrm{mol} \, \mathrm{m}^{-3}` :math:`t`, :math:`\bar{x}`, :math:`\bar{z}_m`, and :math:`k \in \mathcal{K}`
:math:`D_{kj,m}` cross-diffusion coefficient in the membrane ``membrane_cross_diffusion_coefficient`` :math:`\mathrm{mm}^2 \, \mathrm{h}^{-1}` :math:`t`, :math:`\bar{x}`, :math:`\bar{z}_m`, :math:`k \in \mathcal{K}`, and :math:`j \in \mathcal{K}`
:math:`\alpha_{k,m}` convection coefficient in the membrane ``membrnane_convection_coefficient`` :math:`\mathrm{dimensionless}` :math:`t`, :math:`\bar{x}`, :math:`\bar{z}_m`, and :math:`k \in \mathcal{K}`
:math:`j_i` molar flux of ions through the membrane ``molar_ion_flux`` :math:`\mathrm{mol} \, \mathrm{m}^{-2} \, \mathrm{h}^{-1}` :math:`t`, :math:`\bar{x}`, and :math:`i \in \mathcal{I}`
:math:`J_w` water flux across the membrane ``volume_flux_water`` :math:`\mathrm{m}^3 \, \mathrm{m}^{-2} \, \mathrm{h}^{-1}` :math:`t` and :math:`\bar{x}`
:math:`L` length of the membrane ``total_membrane_length`` :math:`\mathrm{m}`
:math:`\Delta \pi` osmotic pressure of feed-side fluid ``osmotic_pressure`` :math:`\mathrm{bar}` :math:`t` and :math:`\bar{x}`
:math:`\Delta P` applied pressure to the membrane ``applied_pressure`` :math:`\mathrm{bar}` :math:`t`
:math:`q_d` volumetric flow rate of the diafiltrate ``diafiltrate_flow_volume`` :math:`\mathrm{m}^3 \, \mathrm{h}^{-1}` :math:`t`
:math:`q_f` volumetric flow rate of the feed ``feed_flow_volume`` :math:`\mathrm{m}^3 \, \mathrm{h}^{-1}` :math:`t`
:math:`q_p` volumetric flow rate of the permeate ``permeate_flow_volume`` :math:`\mathrm{m}^3 \, \mathrm{h}^{-1}` :math:`t` and :math:`\bar{x}`
:math:`q_r` volumetric flow rate of the retentate ``retentate_flow_volume`` :math:`\mathrm{m}^3 \, \mathrm{h}^{-1}` :math:`t` and :math:`\bar{x}`
:math:`w` length of the membrane module ``total_module_length`` :math:`\mathrm{m}`
=============================== ==================================================================== ======================================================= =========================================================================== ==========================================================================================================
Derivative Variables
--------------------
The Multi-Component Diafiltration model adds the following derivative variables.
======================================================= ================================================ ===================================== ======================================= ===============================================================================
Variable Description Name Units Indexed over
======================================================= ================================================ ===================================== ======================================= ===============================================================================
:math:`\frac{\mathrm{d}c_{k,r}}{\mathrm{d}\bar{x}}` ion concentration gradient in the retentate ``d_retentate_conc_mol_comp_dx`` :math:`\mathrm{mol} \, \mathrm{m}^{-3}` :math:`t`, :math:`\bar{x}`, and :math:`k \in \mathcal{K}`
:math:`\frac{\mathrm{d}q_r}{\mathrm{d}\bar{x}}` retentate flow rate gradient ``d_retentate_flow_volume_dx`` :math:`\mathrm{m}^3 \, \mathrm{h}^{-1}` :math:`t` and :math:`\bar{x}`
:math:`\frac{\partial c_{k,bl}}{\partial \bar{z}_{bl}}` ion concentration gradient in the boundary layer ``d_boundary_layer_conc_mol_comp_dz`` :math:`\mathrm{mol} \, \mathrm{m}^{-3}` :math:`t`, :math:`\bar{x}`, :math:`\bar{z}_{bl}`, and :math:`k \in \mathcal{K}`
:math:`\frac{\partial c_{k,m}}{\partial \bar{z}_m}` ion concentration gradient in the membrane ``d_membrane_conc_mol_comp_dz`` :math:`\mathrm{mol} \, \mathrm{m}^{-3}` :math:`t`, :math:`\bar{x}`, :math:`\bar{z}_m`, and :math:`k \in \mathcal{K}`
======================================================= ================================================ ===================================== ======================================= ===============================================================================
Constraints
-----------
**Differential mole balances:**
.. math:: \frac{\mathrm{d}q_r(\bar{x})}{\mathrm{d}\bar{x}} = - J_w(\bar{x}) wL \qquad \forall \, \bar{x} \in (0, 1]
.. math:: q_r(\bar{x}) \frac{\mathrm{d}c_{k,r}(\bar{x})}{\mathrm{d}\bar{x}} = wL (J_w(\bar{x}) c_{k,r}(\bar{x}) - j_{k}(\bar{x})) \qquad \forall \, \bar{x} \in (0, 1], \, k \in \mathcal{K}
**Bulk flux balances:**
.. math:: q_p(\bar{x}) = \bar{x} wL J_w(\bar{x}) \qquad \forall \, \bar{x} \in (0, 1]
.. math:: j_{k}(\bar{x}) = c_{k,p}(\bar{x}) J_w(\bar{x}) \qquad \forall \, \bar{x} \in (0, 1], \, k \in \mathcal{K}
**Overall water flux through the membrane:**
.. math:: J_w (\bar{x}) = L_p (\Delta P - \Delta \pi (\bar{x})) \qquad \forall \, \bar{x} \in (0, 1]
*without a boundary layer:*
.. math:: \Delta \pi (\bar{x}) = \mathrm{R} \mathrm{T} \sum_{i \in \mathcal{I}} n_i \sigma_i (c_{i,r}(\bar{x})-c_{i,p}(\bar{x})) \qquad \forall \, \bar{x} \in (0, 1]
*with a boundary layer:*
.. math:: \Delta \pi (\bar{x}) = \mathrm{R} \mathrm{T} \sum_{i \in \mathcal{I}} n_i \sigma_i (c_{i,bl}(\bar{x}, \bar{z}_{bl}=1)-c_{i,p}(\bar{x})) \qquad \forall \, \bar{x} \in (0, 1]
**Cation flux through the boundary layer and membrane:**
*Derived from the extended Nernst-Planck equation*
.. math:: j_k(\bar{x}) = c_{k,bl}(\bar{x},\bar{z}_{bl}) J_w(\bar{x}) + \frac{1}{\delta} \sum_{j \in \mathcal{K}} \left(D_{kj,bl} (\bar{x},\bar{z}_{bl}) \nabla c_{j,bl} (\bar{x},\bar{z}_{bl}) \right) \qquad \forall \, \bar{x} \in (0, 1], \, k \in \mathcal{K}
.. math:: j_k(\bar{x}) = \alpha_{k,m}(\bar{x},\bar{z}_m) c_{k,m}(\bar{x},\bar{z}_m) J_w(\bar{x}) + \frac{1}{l} \sum_{j \in \mathcal{K}} \left(D_{kj,m} (\bar{x},\bar{z}_m) \nabla c_{j,m} (\bar{x},\bar{z}_m) \right) \qquad \forall \, \bar{x} \in (0, 1], \, k \in \mathcal{K}
where
.. math::
\alpha_{k,h}(\bar{x},\bar{z}_h) =
\begin{cases}
1,& \text{if } h = bl \\
1 + \dfrac{z_k D_{k,h} \chi}{\tilde{D}_h (\bar{x},\bar{z}_h)},& \text{if } h = m
\end{cases}
.. math::
D_{kj,h}(\bar{x},\bar{z}_h) =
\begin{cases}
\dfrac{(z_k z_j D_{k,h} D_{j,h} - z_k z_j D_{k,h} D_{a,h})c_{k,h} (\bar{x},\bar{z}_h)}{\tilde{D}_h (\bar{x},\bar{z}_h)},& \text{if } k \neq j, h \in \{bl, m\} \\
\dfrac{\sum_{t \in \mathcal{C}} \left((z_t z_a D_{k,h} D_{a,h} - \beta_{kt,h})c_{t,h} (\bar{x},\bar{z}_h) \right)}{\tilde{D}_h (\bar{x},\bar{z}_h)} ,& \text{if } k=j, h=bl \\
\dfrac{\sum_{t \in \mathcal{C}} \left((z_t z_a D_{k,h} D_{a,h} - \beta_{kt,h})c_{t,h} (\bar{x},\bar{z}_h) \right) + z_a D_{k,h} D_{a,h} \chi}{\tilde{D}_h (\bar{x},\bar{z}_h)} ,& \text{if } k=j, h=m \\
\end{cases}
.. math::
\beta_{kt,h} =
\begin{cases}
z_t^2 D_{t,h} D_{k,h} ,& \text{if } k\neq t \\
z_t^2 D_{t,h} D_{a,h} ,& \text{if } k=t \\
\end{cases}
.. math::
\tilde{D}_h (\bar{x},\bar{z}_h) =
\begin{cases}
\sum_{j \in \mathcal{K}} \left((z_j^2 D_{j,h} - z_j z_a D_{a,h})c_{j,h} (\bar{x},\bar{z}_h) \right),& \text{if } h = bl \\
\sum_{j \in \mathcal{K}} \left((z_j^2 D_{j,h} - z_j z_a D_{a,h})c_{j,h} (\bar{x},\bar{z}_h) \right) - z_a D_{a,h} \chi,& \text{if } h = m \\
\end{cases}
.. math:: \nabla c_{k,h} (\bar{x},\bar{z}_h)= \dfrac{\partial c_{k,h}(\bar{x},\bar{z}_h)}{\partial \bar{z}_h}
and the subscript :math:`a` represents the anion in solution.
The diffusion and convection coefficients are reformulated to bilinear constraints:
.. math:: \alpha_{k,m}^{bilinear}(\bar{x},\bar{z}_m) = \alpha_{k,m}(\bar{x},\bar{z}_m) \tilde{D}_m(\bar{x},\bar{z}_m) = \tilde{D}_m(\bar{x},\bar{z}_m) + z_k D_{k,m} \chi \qquad \forall \, \bar{x} \in (0, 1]
.. math:: D_{kj,h}^{bilinear}(\bar{x},\bar{z}_h) = D_{kj,h}(\bar{x},\bar{z}_h) \tilde{D}_h(\bar{x},\bar{z}_h) \qquad \forall \, h \in \{bl, m\}, \, \bar{x} \in (0, 1]
*Note that the single solute diffusion coefficients are provided in* :math:`\mathrm{mm}^2\ \, \mathrm{h}^{-1}` *to improve numerical stability. When used in the Nernst-Planck equations, the diffusion coefficients are converted to* :math:`\mathrm{m}^2\ \, \mathrm{h}^{-1}`.
**No applied potential on the system:**
.. math:: 0 = \sum_{i \in \mathcal{I}} z_i j_i(\bar{x}) \qquad \forall \, \bar{x} \in (0, 1]
**Electroneutrality:**
.. math:: 0 = \sum_{i \in \mathcal{I}} z_i c_{i,r}(\bar{x})
.. math:: 0 = \sum_{i \in \mathcal{I}} z_i c_{i,bl}(\bar{x},\bar{z}_{bl}) \qquad \forall \, \bar{x} \in (0, 1]
.. math:: 0 = \chi + \sum_{i \in \mathcal{I}} z_i c_{i,m}(\bar{x},\bar{z}_m) \qquad \forall \, \bar{x} \in (0, 1]
.. math:: 0 = \sum_{i \in \mathcal{I}} z_i c_{i,p}(\bar{x}) \qquad \forall \, \bar{x} \in (0, 1]
**Partitioning:**
At the feed-side solution-membrane interface:
*without a boundary layer:*
.. math:: H_{k,r}^{-z_a} H_{a,r}^{z_k} = \left(\frac{c_{k,m} (\bar{x},\bar{z}=0)}{c_{k,r} (\bar{x})}\right)^{-z_a} \left(\frac{c_{a,m} (\bar{x},\bar{z}=0)}{c_{a,r}(\bar{x})}\right)^{z_k} \qquad \forall \, \bar{x} \in (0, 1], \, k \in \mathcal{K}
*with a boundary layer:*
.. math:: c_{k,r} (\bar{x}) = c_{k,bl} (\bar{x},\bar{z}_{bl}=0) \qquad \forall \, \bar{x} \in (0, 1], \, k \in \mathcal{K}
.. math:: H_{k,r}^{-z_a} H_{a,r}^{z_k} = \left(\frac{c_{k,m} (\bar{x},\bar{z}_m=0)} {c_{k,bl} (\bar{x},\bar{z}_{bl}=1)}\right)^{-z_a} \left(\frac{c_{a,m} (\bar{x},\bar{z}_m=0)}{c_{a,bl}(\bar{x},\bar{z}_{bl}=1)}\right)^{z_k} \qquad \forall \, \bar{x} \in (0, 1], \, k \in \mathcal{K}
At the membrane-permeate interface:
.. math:: H_{k,p}^{-z_a} H_{a,p}^{z_k} = \left(\frac{c_{k,m} (\bar{x},\bar{z}=1)}{c_{k,p} (\bar{x})}\right)^{-z_a} \left(\frac{c_{a,m} (\bar{x},\bar{z}=1)}{c_{a,p}(\bar{x})}\right)^{z_k} \qquad \forall \, \bar{x} \in (0, 1], \, k \in \mathcal{K}
**Boundary conditions:**
.. math:: q_r(\bar{x}=0) = q_f + q_d
.. math:: c_{k,r}(\bar{x}=0) = \frac{q_f c_{k,f} + q_d c_{k,d}}{q_f + q_d} \qquad \forall \, k \in \mathcal{K}
.. math:: c_{k,bl} (\bar{x}=0,\bar{z}_{bl}) = \epsilon \qquad \forall \, \bar{z}_{bl}, \, k \in \mathcal{K}
.. math:: c_{k,m} (\bar{x}=0,\bar{z}_m) = \epsilon \qquad \forall \, \bar{z}_m, \, k \in \mathcal{K}
The following constraints (which are expected to be zero) are enforced to improve numerical stability (with the appropriate constraints deactivated as described above):
.. math:: q_p(\bar{x}=0) = \epsilon
.. math:: c_{i,p}(\bar{x}=0) = \epsilon \qquad \forall \, i \in \mathcal{I}
.. math:: \frac{\mathrm{d}q_r(\bar{x})}{\mathrm{d}\bar{x}}(\bar{x}=0)=\epsilon
.. math:: \frac{\mathrm{d}c_{k,r}(\bar{x})}{\mathrm{d}\bar{x}}(\bar{x}=0)=\epsilon \qquad \forall \, k \in \mathcal{K}
.. math:: J_w(\bar{x}=0) = \epsilon
.. math:: j_i(\bar{x}=0) = \epsilon \qquad \forall \, i \in \mathcal{I}
"""
from pyomo.common.config import ConfigBlock, ConfigValue, ListOf
from pyomo.dae import ContinuousSet, DerivativeVar
from pyomo.environ import (
Constraint,
Expression,
Param,
Reference,
Set,
Suffix,
TransformationFactory,
Var,
units,
value,
)
from pyomo.network import Port
from pyomo.util.calc_var_value import calculate_variable_from_constraint
from idaes.core import UnitModelBlockData, declare_process_block_class, useDefault
from idaes.core.initialization import BlockTriangularizationInitializer
from idaes.core.util.config import is_physical_parameter_block
from idaes.core.util.constants import Constants
from idaes.core.util.exceptions import ConfigurationError
[docs]
class MultiComponentDiafiltrationInitializer(BlockTriangularizationInitializer):
"""
Multi-Component Diafiltration Initializer Class.
"""
[docs]
def initialization_routine(self, model):
"""
Initializes the retentate and permeate streams, membrane and boundary
layer concentrations, and un-initialized derivative variables.
Method then calls the block triangularization initializer method.
"""
for t in model.time:
for x in model.dimensionless_module_length:
model.retentate_flow_volume[t, x].set_value(
value(model.feed_flow_volume[t]) * 1 / 3
)
model.d_retentate_flow_volume_dx[t, x].set_value(-10)
model.permeate_flow_volume[t, x].set_value(
value(model.feed_flow_volume[t]) * 2 / 3
)
for j in model.solutes:
model.retentate_conc_mol_comp[t, x, j].set_value(
value(model.feed_conc_mol_comp[t, j]) * 0.95
)
if len(model.config.cation_list) == 1:
model.d_retentate_conc_mol_comp_dx[t, x, j].set_value(1)
else:
model.d_retentate_conc_mol_comp_dx[t, x, j].set_value(10)
model.permeate_conc_mol_comp[t, x, j].set_value(
value(model.feed_conc_mol_comp[t, j]) * 0.8
)
if model.config.include_boundary_layer:
for z in model.dimensionless_boundary_layer_thickness:
for j in model.solutes:
model.boundary_layer_conc_mol_comp[t, x, z, j].set_value(
value(model.feed_conc_mol_comp[t, j]) * 0.75
)
model.d_boundary_layer_conc_mol_comp_dz[
t, x, z, j
].set_value(10)
# update diffusion coefficients
if x != 0:
calculate_variable_from_constraint(
model.boundary_layer_D_tilde[t, x, z],
model.boundary_layer_D_tilde_calculation[t, x, z],
)
for k in model.cations:
for j in model.cations:
calculate_variable_from_constraint(
model.boundary_layer_cross_diffusion_coefficient_bilinear[
t, x, z, k, j
],
model.boundary_layer_cross_diffusion_coefficient_calculation[
t, x, z, k, j
],
)
calculate_variable_from_constraint(
model.boundary_layer_cross_diffusion_coefficient[
t, x, z, k, j
],
model.boundary_layer_cross_diffusion_coefficient_bilinear_calculation[
t, x, z, k, j
],
)
for z in model.dimensionless_membrane_thickness:
for j in model.solutes:
# adjust membrane concentration based on charge for 3 salt system
if len(model.config.cation_list) == 1:
model.membrane_conc_mol_comp[t, x, z, j].set_value(
value(model.feed_conc_mol_comp[t, j]) * 0.1
)
else:
if value(model.config.property_package.charge[j]) == 1:
model.membrane_conc_mol_comp[t, x, z, j].set_value(
value(model.feed_conc_mol_comp[t, j]) * 0.2
)
elif value(model.config.property_package.charge[j]) >= 2:
model.membrane_conc_mol_comp[t, x, z, j].set_value(
value(model.feed_conc_mol_comp[t, j]) * 1e-2
)
# update anion concentration to consider fixed membrane charge
if x != 0:
calculate_variable_from_constraint(
model.membrane_conc_mol_comp[
t, x, z, model.config.anion_list[0]
],
model.electroneutrality_membrane[t, x, z],
)
# Note: this threshold is not rigorously tested
if value(model.feed_ionic_strength[t]) < 800:
model.d_membrane_conc_mol_comp_dz[t, x, z, j].set_value(1)
else:
model.d_membrane_conc_mol_comp_dz[t, x, z, j].set_value(0.1)
# update diffusion and convection coefficients
# improves numerics for multi-salt systems
if len(model.config.cation_list) >= 3:
if x != 0:
calculate_variable_from_constraint(
model.membrane_D_tilde[t, x, z],
model.membrane_D_tilde_calculation[t, x, z],
)
for k in model.cations:
calculate_variable_from_constraint(
model.membrane_convection_coefficient_bilinear[
t, x, z, k
],
model.membrane_convection_coefficient_calculation[
t, x, z, k
],
)
calculate_variable_from_constraint(
model.membrane_convection_coefficient[t, x, z, k],
model.membrane_convection_coefficient_bilinear_calculation[
t, x, z, k
],
)
for j in model.cations:
calculate_variable_from_constraint(
model.membrane_cross_diffusion_coefficient_bilinear[
t, x, z, k, j
],
model.membrane_cross_diffusion_coefficient_calculation[
t, x, z, k, j
],
)
calculate_variable_from_constraint(
model.membrane_cross_diffusion_coefficient[
t, x, z, k, j
],
model.membrane_cross_diffusion_coefficient_bilinear_calculation[
t, x, z, k, j
],
)
super().initialization_routine(model)
[docs]
@declare_process_block_class("MultiComponentDiafiltration")
class MultiComponentDiafiltrationData(UnitModelBlockData):
"""
Multi-Component Diafiltration Unit Model Class.
"""
# Set default initializer
default_initializer = MultiComponentDiafiltrationInitializer
CONFIG = UnitModelBlockData.CONFIG()
CONFIG.declare(
"property_package",
ConfigValue(
default=useDefault,
domain=is_physical_parameter_block,
description="Property package to use for membrane system",
doc="""Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PhysicalParameterObject** - a PhysicalParameterBlock object.}
""",
),
)
CONFIG.declare(
"property_package_args",
ConfigBlock(
implicit=True,
description="Arguments to use for constructing property packages",
doc="""A ConfigBlock with arguments to be passed to a property block(s)
and used when constructing these,
**default** - None.
**Valid values:** {see property package for documentation}
""",
),
)
CONFIG.declare(
"cation_list",
ConfigValue(
domain=ListOf(str),
default=["Li", "Co"],
doc="List of cations present in the system",
),
)
CONFIG.declare(
"anion_list",
ConfigValue(
domain=ListOf(str),
default=["Cl"],
doc="List of anions present in the system",
),
)
CONFIG.declare(
"include_boundary_layer",
ConfigValue(
default=True,
doc="Boolean to specify if the model is to be built with a boundary layer",
),
)
CONFIG.declare(
"NFE_module_length",
ConfigValue(
default=10,
doc="Number of finite elements across module length (in the x-direction)",
),
)
CONFIG.declare(
"NFE_boundary_layer_thickness",
ConfigValue(
default=5,
doc="Number of finite elements across the boundary layer (in the z-direction)",
),
)
CONFIG.declare(
"NFE_membrane_thickness",
ConfigValue(
default=5,
doc="Number of finite elements across the membrane thickness (in the z-direction)",
),
)
[docs]
def build(self):
"""
Build method for the multi-component diafiltration unit model.
"""
super().build()
if len(self.config.anion_list) > 1:
raise ConfigurationError(
"The multi-component diafiltration unit model only supports systems with a common anion"
)
self.add_mutable_parameters()
self.add_variables()
self.add_constraints()
self.discretize_model()
self.deactivate_unnecessary_objects()
self.add_scaling_factors()
self.add_ports()
self.add_helpful_expressions()
[docs]
def add_mutable_parameters(self):
"""
Adds default parameters for the multi-component diafiltration unit model.
Values can be changed by the user during implementation.
Assumes membrane thickness of 100 nm.
Membrane permeability and fixed charged are estimated from:
Liu, Xinhong, et al. (2025) https://doi.org/10.1021/acs.iecr.4c04763
"""
self.numerical_zero_tolerance = Param(
initialize=1e-10,
mutable=True,
doc="Numerical tolerance for zero values in the model",
)
if self.config.include_boundary_layer:
self.total_boundary_layer_thickness = Param(
initialize=2e-5, # Baker, Chapter 4, page 176
mutable=True,
units=units.m,
doc="Thickness of boundary layer (z-direction)",
)
self.total_membrane_thickness = Param(
initialize=1e-7,
mutable=True,
units=units.m,
doc="Thickness of membrane (z-direction)",
)
self.membrane_fixed_charge = Param(
initialize=-44,
mutable=True,
units=units.mol / units.m**3, # mM
doc="Fixed charge on the membrane",
)
self.membrane_permeability = Param(
initialize=0.01,
mutable=True,
units=units.m / units.h / units.bar,
doc="Hydraulic permeability coefficient",
)
self.temperature = Param(
initialize=298,
mutable=True,
units=units.K,
doc="System temperature",
)
[docs]
def add_variables(self):
"""
Adds variables for the multi-component diafiltration unit model.
Membrane module dimensions and maximum flowrate (17 m3/h) are
estimated from NF270-440 modules.
Assumes 4 modules in series.
"""
# define length scales
self.dimensionless_module_length = ContinuousSet(bounds=(0, 1))
if self.config.include_boundary_layer:
self.dimensionless_boundary_layer_thickness = ContinuousSet(bounds=(0, 1))
self.dimensionless_membrane_thickness = ContinuousSet(bounds=(0, 1))
# add a time index since the property package variables are indexed over time
self.time = Set(initialize=[0])
# add components
self.solutes = Set(initialize=self.config.cation_list + self.config.anion_list)
self.cations = Set(initialize=self.config.cation_list)
# add global variables
self.total_module_length = Var(
initialize=4, # 4 tubes that are ~1m long each (NF270-440)
units=units.m,
bounds=[1e-11, None],
doc="Width of the membrane (x-direction)",
)
self.total_membrane_length = Var(
initialize=41, # 41 m of length in each tube (NF270-440)
units=units.m,
bounds=[1e-11, None],
doc="Length of the membrane, wound radially",
)
self.applied_pressure = Var(
self.time,
initialize=10,
units=units.bar,
bounds=[1e-11, 41], # maximum operating presssure (NF270-440)
doc="Pressure applied to membrane",
)
self.feed_flow_volume = Var(
self.time,
initialize=12.5,
units=units.m**3 / units.h,
bounds=[1e-11, None],
doc="Volumetric flow rate of the feed",
)
def initialize_feed_conc_mol_comp(m, t, j):
vals = {k: 200 for k in self.config.cation_list}
vals.update({self.config.anion_list[0]: 600})
return vals[j]
self.feed_conc_mol_comp = Var(
self.time,
self.solutes,
initialize=initialize_feed_conc_mol_comp,
units=units.mol / units.m**3, # mM
bounds=[1e-11, None],
doc="Mole concentration of solutes in the feed",
)
self.diafiltrate_flow_volume = Var(
self.time,
initialize=3.75,
units=units.m**3 / units.h,
bounds=[1e-11, None],
doc="Volumetric flow rate of the diafiltrate",
)
def initialize_diafiltrate_conc_mol_comp(m, t, j):
vals = {k: 10 for k in self.config.cation_list}
vals.update({self.config.anion_list[0]: 30})
return vals[j]
self.diafiltrate_conc_mol_comp = Var(
self.time,
self.solutes,
initialize=initialize_diafiltrate_conc_mol_comp,
units=units.mol / units.m**3, # mM
bounds=[1e-11, None],
doc="Mole concentration of solutes in the diafiltrate",
)
# add variables dependent on dimensionless_module_length
self.volume_flux_water = Var(
self.time,
self.dimensionless_module_length,
initialize=0.06,
units=units.m**3 / units.m**2 / units.h,
bounds=[1e-11, None],
doc="Volumetric water flux of water across the membrane",
)
def initialize_molar_ion_flux(m, t, w, j):
vals = {k: 10 for k in self.config.cation_list}
vals.update({self.config.anion_list[0]: 30})
return vals[j]
self.molar_ion_flux = Var(
self.time,
self.dimensionless_module_length,
self.solutes,
initialize=initialize_molar_ion_flux,
units=units.mol / units.m**2 / units.h,
bounds=[1e-11, None],
doc="Mole flux of solutes across the membrane (z-direction, x-dependent)",
)
self.retentate_flow_volume = Var(
self.time,
self.dimensionless_module_length,
initialize=6.75,
units=units.m**3 / units.h,
bounds=[1e-11, None],
doc="Volumetric flow rate of the retentate, x-dependent",
)
def initialize_retentate_conc_mol_comp(m, t, w, j):
vals = {
i: 0.95 * initialize_feed_conc_mol_comp(m, t, i) for i in self.solutes
}
return vals[j]
self.retentate_conc_mol_comp = Var(
self.time,
self.dimensionless_module_length,
self.solutes,
initialize=initialize_retentate_conc_mol_comp,
units=units.mol / units.m**3, # mM
bounds=[1e-11, None],
doc="Mole concentration of solutes in the retentate, x-dependent",
)
self.permeate_flow_volume = Var(
self.time,
self.dimensionless_module_length,
initialize=10,
units=units.m**3 / units.h,
bounds=[1e-11, None],
doc="Volumetric flow rate of the permeate, x-dependent",
)
def initialize_permeate_conc_mol_comp(m, t, w, j):
vals = {
i: 0.8 * initialize_feed_conc_mol_comp(m, t, i) for i in self.solutes
}
return vals[j]
self.permeate_conc_mol_comp = Var(
self.time,
self.dimensionless_module_length,
self.solutes,
initialize=initialize_permeate_conc_mol_comp,
units=units.mol / units.m**3, # mM
bounds=[1e-11, None],
doc="Mole concentration of solutes in the permeate, x-dependent",
)
self.osmotic_pressure = Var(
self.time,
self.dimensionless_module_length,
initialize=4,
units=units.bar,
bounds=[1e-11, None],
doc="Osmostic pressure difference across the membrane",
)
# add variables dependent on dimensionless_module_length and dimensionless_membrane_thickness
if self.config.include_boundary_layer:
def initialize_boundary_layer_conc_mol_comp(m, t, w, l, j):
vals = {
i: 0.5 * initialize_feed_conc_mol_comp(m, t, i)
for i in self.solutes
}
return vals[j]
self.boundary_layer_conc_mol_comp = Var(
self.time,
self.dimensionless_module_length,
self.dimensionless_boundary_layer_thickness,
self.solutes,
initialize=initialize_boundary_layer_conc_mol_comp,
units=units.mol / units.m**3, # mM
bounds=[1e-11, None],
doc="Mole concentration of solutes in the boundary layer, x- and z-dependent",
)
self.boundary_layer_D_tilde = Var(
self.time,
self.dimensionless_module_length,
self.dimensionless_boundary_layer_thickness,
initialize=1000,
units=(units.mm**2 / units.hr) * (units.mol / units.m**3), # D * c
doc="Denominator of diffusion and convection coefficients in boundary layer",
)
def initialize_boundary_layer_cross_diffusion_coefficient_bilinear(
m, t, w, l, j, k
):
vals = {
k: {j: -3000 for j in self.config.cation_list}
for k in self.config.cation_list
}
return vals[j][k]
self.boundary_layer_cross_diffusion_coefficient_bilinear = Var(
self.time,
self.dimensionless_module_length,
self.dimensionless_boundary_layer_thickness,
self.cations,
self.cations,
initialize=initialize_boundary_layer_cross_diffusion_coefficient_bilinear,
units=(units.mm**2 / units.h)
* (units.mm**2 / units.h * units.mol / units.m**3), # D * D,tilde
doc="Bi-linear cross diffusion coefficient for cations in boundary layer",
)
def initialize_boundary_layer_cross_diffusion_coefficient(m, t, w, l, j, k):
vals = {
k: {j: -5 for j in self.config.cation_list}
for k in self.config.cation_list
}
return vals[j][k]
self.boundary_layer_cross_diffusion_coefficient = Var(
self.time,
self.dimensionless_module_length,
self.dimensionless_boundary_layer_thickness,
self.cations,
self.cations,
initialize=initialize_boundary_layer_cross_diffusion_coefficient,
units=units.mm**2 / units.h,
doc="Cross diffusion coefficient for cations in boundary layer",
)
def initialize_membrane_conc_mol_comp(m, t, w, l, j):
vals = {
i: 0.1 * initialize_feed_conc_mol_comp(m, t, i) for i in self.solutes
}
return vals[j]
self.membrane_conc_mol_comp = Var(
self.time,
self.dimensionless_module_length,
self.dimensionless_membrane_thickness,
self.solutes,
initialize=initialize_membrane_conc_mol_comp,
units=units.mol / units.m**3, # mM
bounds=[1e-11, None],
doc="Mole concentration of solutes in the membrane, x- and z-dependent",
)
self.membrane_D_tilde = Var(
self.time,
self.dimensionless_module_length,
self.dimensionless_membrane_thickness,
initialize=620,
units=(units.mm**2 / units.hr) * (units.mol / units.m**3), # D * c
doc="Denominator of diffusion and convection coefficients in membrane",
)
def initialize_membrane_cross_diffusion_coefficient_bilinear(m, t, w, l, j, k):
vals = {
k: {j: -3000 for j in self.config.cation_list}
for k in self.config.cation_list
}
return vals[j][k]
self.membrane_cross_diffusion_coefficient_bilinear = Var(
self.time,
self.dimensionless_module_length,
self.dimensionless_membrane_thickness,
self.cations,
self.cations,
initialize=initialize_membrane_cross_diffusion_coefficient_bilinear,
units=(units.mm**2 / units.h)
* (units.mm**2 / units.h * units.mol / units.m**3), # D * D,tilde
doc="Bi-linear cross diffusion coefficient for cations in membrane",
)
def initialize_membrane_convection_coefficient_bilinear(m, t, w, l, j):
vals = {k: 100 for k in self.config.cation_list}
return vals[j]
self.membrane_convection_coefficient_bilinear = Var(
self.time,
self.dimensionless_module_length,
self.dimensionless_membrane_thickness,
self.cations,
initialize=initialize_membrane_convection_coefficient_bilinear,
units=(units.mm**2 / units.hr) * (units.mol / units.m**3), # D,tilde
doc="Convection coefficient for cations in membrane",
)
def initialize_membrane_cross_diffusion_coefficient(m, t, w, l, j, k):
vals = {
k: {j: -5 for j in self.config.cation_list}
for k in self.config.cation_list
}
return vals[j][k]
self.membrane_cross_diffusion_coefficient = Var(
self.time,
self.dimensionless_module_length,
self.dimensionless_membrane_thickness,
self.cations,
self.cations,
initialize=initialize_membrane_cross_diffusion_coefficient,
units=units.mm**2 / units.h,
doc="Cross diffusion coefficient for cations in membrane",
)
def initialize_membrane_convection_coefficient(m, t, w, l, j):
vals = {k: 0.2 for k in self.config.cation_list}
return vals[j]
self.membrane_convection_coefficient = Var(
self.time,
self.dimensionless_module_length,
self.dimensionless_membrane_thickness,
self.cations,
initialize=initialize_membrane_convection_coefficient,
units=units.dimensionless,
doc="Convection coefficient for cations in membrane",
)
# define the (partial) derivative variables
self.d_retentate_conc_mol_comp_dx = DerivativeVar(
self.retentate_conc_mol_comp,
wrt=self.dimensionless_module_length,
units=units.mol / units.m**3, # mM
doc="Solute concentration gradient in the retentate",
)
self.d_retentate_flow_volume_dx = DerivativeVar(
self.retentate_flow_volume,
wrt=self.dimensionless_module_length,
units=units.m**3 / units.h,
doc="Volume flow gradient in the retentate",
)
if self.config.include_boundary_layer:
self.d_boundary_layer_conc_mol_comp_dz = DerivativeVar(
self.boundary_layer_conc_mol_comp,
wrt=self.dimensionless_boundary_layer_thickness,
units=units.mol / units.m**3, # mM
doc="Solute concentration gradient wrt z in the boundary layer",
)
self.d_membrane_conc_mol_comp_dz = DerivativeVar(
self.membrane_conc_mol_comp,
wrt=self.dimensionless_membrane_thickness,
units=units.mol / units.m**3, # mM
doc="Solute concentration gradient wrt membrane thickness",
)
[docs]
def add_constraints(self):
"""
Adds model constraints for the multi-component diafiltration unit model.
"""
# mol balance constraints
def _overall_mol_balance(blk, t, x):
if x == 0:
return Constraint.Skip
return blk.d_retentate_flow_volume_dx[t, x] == (
-blk.volume_flux_water[t, x]
* blk.total_membrane_length
* blk.total_module_length
)
self.overall_mol_balance = Constraint(
self.time, self.dimensionless_module_length, rule=_overall_mol_balance
)
def _cation_mol_balance(blk, t, x, k):
if x == 0:
return Constraint.Skip
return (
blk.retentate_flow_volume[t, x]
* blk.d_retentate_conc_mol_comp_dx[t, x, k]
) == (
(
blk.volume_flux_water[t, x] * blk.retentate_conc_mol_comp[t, x, k]
- blk.molar_ion_flux[t, x, k]
)
* blk.total_membrane_length
* blk.total_module_length
)
self.cation_mol_balance = Constraint(
self.time,
self.dimensionless_module_length,
self.cations,
rule=_cation_mol_balance,
)
# bulk flux balance constraints
def _overall_bulk_flux_equation(blk, t, x):
if x == 0:
return Constraint.Skip
return (
blk.permeate_flow_volume[t, x]
== blk.volume_flux_water[t, x]
* x
* blk.total_membrane_length
* blk.total_module_length
)
self.overall_bulk_flux_equation = Constraint(
self.time,
self.dimensionless_module_length,
rule=_overall_bulk_flux_equation,
)
def _cation_bulk_flux_equation(blk, t, x, k):
if x == 0:
return Constraint.Skip
return blk.molar_ion_flux[t, x, k] == (
blk.permeate_conc_mol_comp[t, x, k] * blk.volume_flux_water[t, x]
)
self.cation_bulk_flux_equation = Constraint(
self.time,
self.dimensionless_module_length,
self.cations,
rule=_cation_bulk_flux_equation,
)
# transport constraints (first principles)
def _lumped_water_flux(blk, t, x):
if x == 0:
return Constraint.Skip
return blk.volume_flux_water[t, x] == (
blk.membrane_permeability
* (blk.applied_pressure[t] - blk.osmotic_pressure[t, x])
)
self.lumped_water_flux = Constraint(
self.time, self.dimensionless_module_length, rule=_lumped_water_flux
)
if self.config.include_boundary_layer:
def _boundary_layer_D_tilde_calculation(blk, t, x, z):
if x == 0:
return Constraint.Skip
a0 = self.config.anion_list[0]
charge = blk.config.property_package.charge
conc_bl = blk.boundary_layer_conc_mol_comp
D_bl = blk.config.property_package.boundary_layer_diffusion_coefficient
return blk.boundary_layer_D_tilde[t, x, z] == sum(
(
(
((charge[k] ** 2) * D_bl[k])
- (charge[k] * charge[a0] * D_bl[a0])
)
* conc_bl[t, x, z, k]
)
for k in self.cations
)
self.boundary_layer_D_tilde_calculation = Constraint(
self.time,
self.dimensionless_module_length,
self.dimensionless_boundary_layer_thickness,
rule=_boundary_layer_D_tilde_calculation,
)
def _boundary_layer_cross_diffusion_coefficient_bilinear_calculation(
blk, t, x, z, k, j
):
if x == 0:
return Constraint.Skip
return (
blk.boundary_layer_cross_diffusion_coefficient_bilinear[
t, x, z, k, j
]
== blk.boundary_layer_cross_diffusion_coefficient[t, x, z, k, j]
* blk.boundary_layer_D_tilde[t, x, z]
)
self.boundary_layer_cross_diffusion_coefficient_bilinear_calculation = Constraint(
self.time,
self.dimensionless_module_length,
self.dimensionless_boundary_layer_thickness,
self.cations,
self.cations,
rule=_boundary_layer_cross_diffusion_coefficient_bilinear_calculation,
)
def _boundary_layer_cross_diffusion_coefficient_calculation(
blk, t, x, z, k, j
):
if x == 0:
return Constraint.Skip
a0 = self.config.anion_list[0]
charge = blk.config.property_package.charge
conc_bl = blk.boundary_layer_conc_mol_comp
D_bl = blk.config.property_package.boundary_layer_diffusion_coefficient
# off-diagonal
if k != j:
return blk.boundary_layer_cross_diffusion_coefficient_bilinear[
t, x, z, k, j
] == (
(
(charge[k] * charge[j] * D_bl[k] * D_bl[j])
- (charge[k] * charge[j] * D_bl[k] * D_bl[a0])
)
* conc_bl[t, x, z, k]
)
# diagonal
if k == j:
return blk.boundary_layer_cross_diffusion_coefficient_bilinear[
t, x, z, k, j
] == (
sum(
(
(
(charge[i] * charge[a0] * D_bl[k] * D_bl[a0])
- (charge[i] ** 2 * D_bl[i] * D_bl[k])
)
* conc_bl[t, x, z, i]
)
for i in blk.cations
if k != i
)
+ sum(
(
(
(charge[i] * charge[a0] * D_bl[k] * D_bl[a0])
- (charge[i] ** 2 * D_bl[i] * D_bl[a0])
)
* conc_bl[t, x, z, i]
)
for i in blk.cations
if k == i
)
)
self.boundary_layer_cross_diffusion_coefficient_calculation = Constraint(
self.time,
self.dimensionless_module_length,
self.dimensionless_boundary_layer_thickness,
self.cations,
self.cations,
rule=_boundary_layer_cross_diffusion_coefficient_calculation,
)
def _membrane_D_tilde_calculation(blk, t, x, z):
if x == 0:
return Constraint.Skip
a0 = self.config.anion_list[0]
charge = blk.config.property_package.charge
chi = blk.membrane_fixed_charge
conc_mem = blk.membrane_conc_mol_comp
D_mem = blk.config.property_package.membrane_diffusion_coefficient
return blk.membrane_D_tilde[t, x, z] == (
sum(
(
(
((charge[k] ** 2) * D_mem[k])
- (charge[k] * charge[a0] * D_mem[a0])
)
* conc_mem[t, x, z, k]
)
for k in blk.cations
)
- (charge[a0] * D_mem[a0] * chi)
)
self.membrane_D_tilde_calculation = Constraint(
self.time,
self.dimensionless_module_length,
self.dimensionless_membrane_thickness,
rule=_membrane_D_tilde_calculation,
)
def _membrane_cross_diffusion_coefficient_bilinear_calculation(
blk, t, x, z, k, j
):
if x == 0:
return Constraint.Skip
return (
blk.membrane_cross_diffusion_coefficient_bilinear[t, x, z, k, j]
== blk.membrane_cross_diffusion_coefficient[t, x, z, k, j]
* blk.membrane_D_tilde[t, x, z]
)
self.membrane_cross_diffusion_coefficient_bilinear_calculation = Constraint(
self.time,
self.dimensionless_module_length,
self.dimensionless_membrane_thickness,
self.cations,
self.cations,
rule=_membrane_cross_diffusion_coefficient_bilinear_calculation,
)
def _membrane_convection_coefficient_bilinear_calculation(blk, t, x, z, k):
if x == 0:
return Constraint.Skip
return (
blk.membrane_convection_coefficient_bilinear[t, x, z, k]
== blk.membrane_convection_coefficient[t, x, z, k]
* blk.membrane_D_tilde[t, x, z]
)
self.membrane_convection_coefficient_bilinear_calculation = Constraint(
self.time,
self.dimensionless_module_length,
self.dimensionless_membrane_thickness,
self.cations,
rule=_membrane_convection_coefficient_bilinear_calculation,
)
def _membrane_cross_diffusion_coefficient_calculation(blk, t, x, z, k, j):
if x == 0:
return Constraint.Skip
a0 = self.config.anion_list[0]
charge = blk.config.property_package.charge
chi = blk.membrane_fixed_charge
conc_mem = blk.membrane_conc_mol_comp
D_mem = blk.config.property_package.membrane_diffusion_coefficient
# off-diagonal
if k != j:
return blk.membrane_cross_diffusion_coefficient_bilinear[
t, x, z, k, j
] == (
(
(charge[k] * charge[j] * D_mem[k] * D_mem[j])
- (charge[k] * charge[j] * D_mem[k] * D_mem[a0])
)
* conc_mem[t, x, z, k]
)
# diagonal
if k == j:
return blk.membrane_cross_diffusion_coefficient_bilinear[
t, x, z, k, j
] == (
sum(
(
(
(charge[i] * charge[a0] * D_mem[k] * D_mem[a0])
- (charge[i] ** 2 * D_mem[i] * D_mem[k])
)
* conc_mem[t, x, z, i]
)
for i in blk.cations
if k != i
)
+ sum(
(
(
(charge[i] * charge[a0] * D_mem[k] * D_mem[a0])
- (charge[i] ** 2 * D_mem[i] * D_mem[a0])
)
* conc_mem[t, x, z, i]
)
for i in blk.cations
if k == i
)
+ charge[a0] * D_mem[k] * D_mem[a0] * chi
)
self.membrane_cross_diffusion_coefficient_calculation = Constraint(
self.time,
self.dimensionless_module_length,
self.dimensionless_membrane_thickness,
self.cations,
self.cations,
rule=_membrane_cross_diffusion_coefficient_calculation,
)
def _membrane_convection_coefficient_calculation(blk, t, x, z, k):
if x == 0:
return Constraint.Skip
charge = blk.config.property_package.charge
chi = blk.membrane_fixed_charge
D_mem = blk.config.property_package.membrane_diffusion_coefficient
return blk.membrane_convection_coefficient_bilinear[t, x, z, k] == (
blk.membrane_D_tilde[t, x, z] + (charge[k] * D_mem[k] * chi)
)
self.membrane_convection_coefficient_calculation = Constraint(
self.time,
self.dimensionless_module_length,
self.dimensionless_membrane_thickness,
self.cations,
rule=_membrane_convection_coefficient_calculation,
)
if self.config.include_boundary_layer:
def _cation_flux_boundary_layer(blk, t, x, z, k):
if x == 0 or z == 0:
return Constraint.Skip
return blk.molar_ion_flux[t, x, k] == (
(
blk.boundary_layer_conc_mol_comp[t, x, z, k]
* blk.volume_flux_water[t, x]
)
+ sum(
(
units.convert(
blk.boundary_layer_cross_diffusion_coefficient[
t, x, z, k, i
],
to_units=units.m**2 / units.h,
)
/ (blk.total_boundary_layer_thickness)
* blk.d_boundary_layer_conc_mol_comp_dz[t, x, z, i]
)
for i in self.cations
)
)
self.cation_flux_boundary_layer = Constraint(
self.time,
self.dimensionless_module_length,
self.dimensionless_boundary_layer_thickness,
self.cations,
rule=_cation_flux_boundary_layer,
)
def _cation_flux_membrane(blk, t, x, z, k):
if x == 0:
return Constraint.Skip
return blk.molar_ion_flux[t, x, k] == (
(
blk.membrane_convection_coefficient[t, x, z, k]
* blk.membrane_conc_mol_comp[t, x, z, k]
* blk.volume_flux_water[t, x]
)
+ sum(
(
units.convert(
blk.membrane_cross_diffusion_coefficient[t, x, z, k, i],
to_units=units.m**2 / units.h,
)
/ blk.total_membrane_thickness
* blk.d_membrane_conc_mol_comp_dz[t, x, z, i]
)
for i in blk.cations
)
)
self.cation_flux_membrane = Constraint(
self.time,
self.dimensionless_module_length,
self.dimensionless_membrane_thickness,
self.cations,
rule=_cation_flux_membrane,
)
def _anion_flux_membrane(blk, t, x):
if x == 0:
return Constraint.Skip
charge = blk.config.property_package.charge
return 0 == sum(
charge[j] * blk.molar_ion_flux[t, x, j] for j in blk.solutes
)
self.anion_flux_membrane = Constraint(
self.time, self.dimensionless_module_length, rule=_anion_flux_membrane
)
# other physical constraints
def _osmotic_pressure_calculation(blk, t, x):
if x == 0:
return Constraint.Skip
conc_p = blk.permeate_conc_mol_comp
conc_r = blk.retentate_conc_mol_comp
n = blk.config.property_package.num_solutes
R = Constants.gas_constant # J / mol / K
sigma = blk.config.property_package.sigma
T = blk.temperature
if self.config.include_boundary_layer:
conc_bl = blk.boundary_layer_conc_mol_comp
return blk.osmotic_pressure[t, x] == units.convert(
(
R
* T
* sum(
(n[j] * sigma[j] * (conc_bl[t, x, 1, j] - conc_p[t, x, j]))
for j in blk.solutes
)
),
to_units=units.bar,
)
else:
return blk.osmotic_pressure[t, x] == units.convert(
(
R
* T
* sum(
(n[j] * sigma[j] * (conc_r[t, x, j] - conc_p[t, x, j]))
for j in blk.solutes
)
),
to_units=units.bar,
)
self.osmotic_pressure_calculation = Constraint(
self.time,
self.dimensionless_module_length,
rule=_osmotic_pressure_calculation,
)
def _electroneutrality_retentate(blk, t, x):
charge = blk.config.property_package.charge
conc_r = blk.retentate_conc_mol_comp
return 0 == sum(charge[j] * conc_r[t, x, j] for j in blk.solutes)
self.electroneutrality_retentate = Constraint(
self.time,
self.dimensionless_module_length,
rule=_electroneutrality_retentate,
)
if self.config.include_boundary_layer:
def _electroneutrality_boundary_layer(blk, t, x, z):
if x == 0:
return Constraint.Skip
charge = blk.config.property_package.charge
conc_bl = blk.boundary_layer_conc_mol_comp
return 0 == sum(charge[j] * conc_bl[t, x, z, j] for j in blk.solutes)
self.electroneutrality_boundary_layer = Constraint(
self.time,
self.dimensionless_module_length,
self.dimensionless_boundary_layer_thickness,
rule=_electroneutrality_boundary_layer,
)
def _electroneutrality_membrane(blk, t, x, z):
if x == 0:
return Constraint.Skip
charge = blk.config.property_package.charge
chi = blk.membrane_fixed_charge
conc_mem = blk.membrane_conc_mol_comp
return 0 == (
sum(charge[j] * conc_mem[t, x, z, j] for j in blk.solutes) + chi
)
self.electroneutrality_membrane = Constraint(
self.time,
self.dimensionless_module_length,
self.dimensionless_membrane_thickness,
rule=_electroneutrality_membrane,
)
def _electroneutrality_permeate(blk, t, x):
if x == 0:
return Constraint.Skip
charge = blk.config.property_package.charge
conc_p = blk.permeate_conc_mol_comp
return 0 == sum(charge[j] * conc_p[t, x, j] for j in blk.solutes)
self.electroneutrality_permeate = Constraint(
self.time,
self.dimensionless_module_length,
rule=_electroneutrality_permeate,
)
# partitioning equations
if self.config.include_boundary_layer:
def _retentate_boundary_layer_interface(blk, t, x, k):
if x == 0:
return Constraint.Skip
return (
blk.retentate_conc_mol_comp[t, x, k]
== blk.boundary_layer_conc_mol_comp[t, x, 0, k]
)
self.retentate_boundary_layer_interface = Constraint(
self.time,
self.dimensionless_module_length,
self.cations,
rule=_retentate_boundary_layer_interface,
)
def _cation_equilibrium_boundary_layer_membrane_interface(blk, t, x, k):
if x == 0:
return Constraint.Skip
a0 = self.config.anion_list[0]
charge = blk.config.property_package.charge
conc_bl = blk.boundary_layer_conc_mol_comp
conc_mem = blk.membrane_conc_mol_comp
H_r = blk.config.property_package.partition_coefficient_retentate
return (
(H_r[k] ** (-charge[a0]))
* (H_r[a0] ** charge[k])
* (conc_bl[t, x, 1, k] ** (-charge[a0]))
* (conc_bl[t, x, 1, a0] ** charge[k])
) == (
(conc_mem[t, x, 0, k] ** (-charge[a0]))
* (conc_mem[t, x, 0, a0] ** charge[k])
)
self.cation_equilibrium_boundary_layer_membrane_interface = Constraint(
self.time,
self.dimensionless_module_length,
self.cations,
rule=_cation_equilibrium_boundary_layer_membrane_interface,
)
else:
def _cation_equilibrium_retentate_membrane_interface(blk, t, x, k):
if x == 0:
return Constraint.Skip
a0 = self.config.anion_list[0]
charge = blk.config.property_package.charge
conc_mem = blk.membrane_conc_mol_comp
conc_r = blk.retentate_conc_mol_comp
H_r = blk.config.property_package.partition_coefficient_retentate
return (
(H_r[k] ** (-charge[a0]))
* (H_r[a0] ** charge[k])
* (conc_r[t, x, k] ** (-charge[a0]))
* (conc_r[t, x, a0] ** charge[k])
) == (
(conc_mem[t, x, 0, k] ** (-charge[a0]))
* (conc_mem[t, x, 0, a0] ** charge[k])
)
self.cation_equilibrium_retentate_membrane_interface = Constraint(
self.time,
self.dimensionless_module_length,
self.cations,
rule=_cation_equilibrium_retentate_membrane_interface,
)
def _cation_equilibrium_membrane_permeate_interface(blk, t, x, k):
if x == 0:
return Constraint.Skip
a0 = self.config.anion_list[0]
charge = blk.config.property_package.charge
conc_mem = blk.membrane_conc_mol_comp
conc_p = blk.permeate_conc_mol_comp
H_p = blk.config.property_package.partition_coefficient_permeate
return (
(H_p[k] ** (-charge[a0]))
* (H_p[a0] ** charge[k])
* (conc_p[t, x, k] ** (-charge[a0]))
* (conc_p[t, x, a0] ** charge[k])
) == (
(conc_mem[t, x, 1, k] ** (-charge[a0]))
* (conc_mem[t, x, 1, a0] ** charge[k])
)
self.cation_equilibrium_membrane_permeate_interface = Constraint(
self.time,
self.dimensionless_module_length,
self.cations,
rule=_cation_equilibrium_membrane_permeate_interface,
)
# boundary conditions
def _retentate_flow_volume_boundary_condition(blk, t):
return (
blk.retentate_flow_volume[t, 0]
== blk.feed_flow_volume[t] + blk.diafiltrate_flow_volume[t]
)
self.retentate_flow_volume_boundary_condition = Constraint(
self.time, rule=_retentate_flow_volume_boundary_condition
)
def _retentate_conc_mol_comp_boundary_condition(blk, t, k):
return blk.retentate_conc_mol_comp[t, 0, k] == (
(
blk.feed_flow_volume[t] * blk.feed_conc_mol_comp[t, k]
+ blk.diafiltrate_flow_volume[t]
* blk.diafiltrate_conc_mol_comp[t, k]
)
/ (blk.feed_flow_volume[t] + blk.diafiltrate_flow_volume[t])
)
self.retentate_conc_mol_comp_boundary_condition = Constraint(
self.time, self.cations, rule=_retentate_conc_mol_comp_boundary_condition
)
if self.config.include_boundary_layer:
def _boundary_layer_conc_mol_comp_boundary_condition(blk, t, z, k):
return (
blk.boundary_layer_conc_mol_comp[t, 0, z, k]
== self.numerical_zero_tolerance * units.mol / units.m**3
)
self.boundary_layer_conc_mol_comp_boundary_condition = Constraint(
self.time,
self.dimensionless_boundary_layer_thickness,
self.cations,
rule=_boundary_layer_conc_mol_comp_boundary_condition,
)
def _membrane_conc_mol_comp_boundary_condition(blk, t, z, k):
return (
blk.membrane_conc_mol_comp[t, 0, z, k]
== self.numerical_zero_tolerance * units.mol / units.m**3
)
self.membrane_conc_mol_comp_boundary_condition = Constraint(
self.time,
self.dimensionless_membrane_thickness,
self.cations,
rule=_membrane_conc_mol_comp_boundary_condition,
)
# constraints to improve numerical stability
def _permeate_flow_volume_boundary_condition(blk, t):
return (
blk.permeate_flow_volume[t, 0]
== self.numerical_zero_tolerance * units.m**3 / units.h
)
self.permeate_flow_volume_boundary_condition = Constraint(
self.time, rule=_permeate_flow_volume_boundary_condition
)
def _permeate_conc_mol_comp_boundary_condition(blk, t, j):
return (
blk.permeate_conc_mol_comp[t, 0, j]
== self.numerical_zero_tolerance * units.mol / units.m**3
)
self.permeate_conc_mol_comp_boundary_condition = Constraint(
self.time, self.solutes, rule=_permeate_conc_mol_comp_boundary_condition
)
def _d_retentate_flow_volume_dx_boundary_condition(blk, t):
return (
blk.d_retentate_flow_volume_dx[t, 0]
== self.numerical_zero_tolerance * units.m**3 / units.h
)
self.d_retentate_flow_volume_dx_boundary_condition = Constraint(
self.time, rule=_d_retentate_flow_volume_dx_boundary_condition
)
def _d_retentate_conc_mol_comp_dx_boundary_condition(blk, t, k):
return (
blk.d_retentate_conc_mol_comp_dx[t, 0, k]
== self.numerical_zero_tolerance * units.mol / units.m**3
)
self.d_retentate_conc_mol_comp_dx_boundary_condition = Constraint(
self.time,
self.cations,
rule=_d_retentate_conc_mol_comp_dx_boundary_condition,
)
def _volume_flux_water_boundary_condition(blk, t):
return (
blk.volume_flux_water[t, 0]
== self.numerical_zero_tolerance * units.m / units.h
)
self.volume_flux_water_boundary_condition = Constraint(
self.time, rule=_volume_flux_water_boundary_condition
)
def _molar_ion_flux_boundary_condition(blk, t, j):
return (
blk.molar_ion_flux[t, 0, j]
== self.numerical_zero_tolerance * units.mol / units.m**2 / units.h
)
self.molar_ion_flux_boundary_condition = Constraint(
self.time, self.solutes, rule=_molar_ion_flux_boundary_condition
)
def discretize_model(self):
discretizer = TransformationFactory("dae.finite_difference")
discretizer.apply_to(
self,
wrt=self.dimensionless_module_length,
nfe=self.config.NFE_module_length,
scheme="BACKWARD",
)
if self.config.include_boundary_layer:
discretizer.apply_to(
self,
wrt=self.dimensionless_boundary_layer_thickness,
nfe=self.config.NFE_boundary_layer_thickness,
scheme="BACKWARD",
)
discretizer.apply_to(
self,
wrt=self.dimensionless_membrane_thickness,
nfe=self.config.NFE_membrane_thickness,
scheme="BACKWARD",
)
[docs]
def deactivate_unnecessary_objects(self):
"""
Deactivates variables and constraints not needed in the multi-component
diafiltration unit model.
"""
a0 = self.config.anion_list[0]
for t in self.time:
for x in self.dimensionless_module_length:
# anion concentration gradient in retentate variable is created by default but
# is not needed in model; fix to reduce number of variables
self.d_retentate_conc_mol_comp_dx[t, x, a0].fix(
value(self.numerical_zero_tolerance)
)
# associated discretization equation not needed in model
if x != 0:
self.d_retentate_conc_mol_comp_dx_disc_eq[t, x, a0].deactivate()
if self.config.include_boundary_layer:
for z in self.dimensionless_boundary_layer_thickness:
# anion concentration gradient in boundary layer variable is created by default but
# is not needed in model; fix to reduce number of variables
self.d_boundary_layer_conc_mol_comp_dz[t, x, z, a0].fix(
value(self.numerical_zero_tolerance)
)
# associated discretization equation not needed in model
if z != 0:
self.d_boundary_layer_conc_mol_comp_dz_disc_eq[
t, x, z, a0
].deactivate()
for z in self.dimensionless_membrane_thickness:
# anion concentration gradient in membrane variable is created by default but
# is not needed in model; fix to reduce number of variables
self.d_membrane_conc_mol_comp_dz[t, x, z, a0].fix(
value(self.numerical_zero_tolerance)
)
# associated discretization equation not needed in model
if z != 0:
self.d_membrane_conc_mol_comp_dz_disc_eq[
t, x, z, a0
].deactivate()
[docs]
def add_scaling_factors(self):
"""
Assigns scaling factors to certain variables and constraints to
improve solver performance.
"""
self.scaling_factor = Suffix(direction=Suffix.EXPORT)
self.scaling_factor[self.volume_flux_water] = 1e2
if self.config.include_boundary_layer:
self.scaling_factor[self.boundary_layer_D_tilde] = 1e-3
self.scaling_factor[
self.boundary_layer_cross_diffusion_coefficient_bilinear
] = 1e-4
self.scaling_factor[self.boundary_layer_cross_diffusion_coefficient] = 1e1
self.scaling_factor[self.membrane_D_tilde] = 1e-1
self.scaling_factor[self.membrane_cross_diffusion_coefficient_bilinear] = 1e-2
self.scaling_factor[self.membrane_convection_coefficient_bilinear] = 1e-1
self.scaling_factor[self.membrane_cross_diffusion_coefficient] = 1e1
self.scaling_factor[self.membrane_convection_coefficient] = 1e1
if len(self.config.cation_list) >= 2:
for t in self.time:
for x in self.dimensionless_module_length:
if x != 0:
self.scaling_factor[self.lumped_water_flux[t, x]] = 1e3
def add_ports(self):
self.feed_inlet = Port(doc="Feed Inlet Port")
self._feed_flow_volume_ref = Reference(self.feed_flow_volume)
self.feed_inlet.add(self._feed_flow_volume_ref, "flow_vol")
self._feed_conc_mol_comp_ref = Reference(self.feed_conc_mol_comp)
self.feed_inlet.add(self._feed_conc_mol_comp_ref, "conc_mol_comp")
self.diafiltrate_inlet = Port(doc="Diafiltrate Inlet Port")
self._diafiltrate_flow_volume_ref = Reference(self.diafiltrate_flow_volume)
self.diafiltrate_inlet.add(self._diafiltrate_flow_volume_ref, "flow_vol")
self._diafiltrate_conc_mol_comp_ref = Reference(self.diafiltrate_conc_mol_comp)
self.diafiltrate_inlet.add(self._diafiltrate_conc_mol_comp_ref, "conc_mol_comp")
self.retentate_outlet = Port(doc="Retentate Outlet Port")
self._retentate_flow_volume_ref = Reference(
self.retentate_flow_volume[:, self.dimensionless_module_length.last()]
)
self.retentate_outlet.add(self._retentate_flow_volume_ref, "flow_vol")
self._retentate_conc_mol_comp_ref = Reference(
self.retentate_conc_mol_comp[:, self.dimensionless_module_length.last(), :]
)
self.retentate_outlet.add(self._retentate_conc_mol_comp_ref, "conc_mol_comp")
self.permeate_outlet = Port(doc="Permeate Outlet Port")
self._permeate_flow_volume_ref = Reference(
self.permeate_flow_volume[:, self.dimensionless_module_length.last()]
)
self.permeate_outlet.add(self._permeate_flow_volume_ref, "flow_vol")
self._permeate_conc_mol_comp_ref = Reference(
self.permeate_conc_mol_comp[:, self.dimensionless_module_length.last(), :]
)
self.permeate_outlet.add(self._permeate_conc_mol_comp_ref, "conc_mol_comp")
def add_helpful_expressions(self):
def _feed_ionic_strength(
blk,
t,
):
charge = blk.config.property_package.charge
return 0.5 * sum(
(
(
(
blk.feed_flow_volume[t] * blk.feed_conc_mol_comp[t, j]
+ blk.diafiltrate_flow_volume[t]
* blk.diafiltrate_conc_mol_comp[t, j]
)
/ (blk.feed_flow_volume[t] + blk.diafiltrate_flow_volume[t])
)
* charge[j] ** 2
)
for j in blk.solutes
)
self.feed_ionic_strength = Expression(self.time, rule=_feed_ionic_strength)