From 2e5e903b8adfe5488ff40f8fe231473088254ba1 Mon Sep 17 00:00:00 2001 From: jjd Date: Tue, 1 Oct 2024 20:18:37 -0400 Subject: [PATCH 01/17] Added bipolar membrane unit model and costing --- docs/_static/unit_models/BPEDdiagram.png | Bin 0 -> 28577 bytes .../Biploar_electrodialysis_0D.rst | 338 +++ .../unit_models/bipolar_electrodialysis.py | 183 ++ .../unit_models/Bipolar_Electrodialysis_0D.py | 2427 +++++++++++++++++ .../tests/test_biploar_electrodialysis_0D.py | 756 +++++ 5 files changed, 3704 insertions(+) create mode 100644 docs/_static/unit_models/BPEDdiagram.png create mode 100644 docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst create mode 100644 watertap/costing/unit_models/bipolar_electrodialysis.py create mode 100644 watertap/unit_models/Bipolar_Electrodialysis_0D.py create mode 100644 watertap/unit_models/tests/test_biploar_electrodialysis_0D.py diff --git a/docs/_static/unit_models/BPEDdiagram.png b/docs/_static/unit_models/BPEDdiagram.png new file mode 100644 index 0000000000000000000000000000000000000000..e8442055e973716531bab3c520f4ea67108b127e GIT binary patch literal 28577 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 literal 0 HcmV?d00001 diff --git a/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst b/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst new file mode 100644 index 0000000000..fad18875b3 --- /dev/null +++ b/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst @@ -0,0 +1,338 @@ +Bipolar Electrodialysis (0D) +==================== + +Introduction +------------ + +Bipolar electrodialysis, an electrochemical separation technology, has primarily been used to generate acids and bases +from waste salts produced in water purification. By providing in-situ access to valuable raw materials bipolar membranes can +significantly reduce the total cost of the operation. This cell stack is shown in Figure 1 with **basate** and **acidate** channels, that produce base and acid +respectively. More overview of the bipolar +electrodialysis technology can be found in the *References*. + +.. figure:: ../../_static/unit_models/BPEDdiagram.png + :width: 600 + :align: center + + Figure 1. Schematic representation of a bipolar electrodialysis unit + + +One bipolar membrane along with the **Acidate** and **Basate** channels can thus be treated as a modelling unit that can +multiply to larger-scale systems. The presented bipolar electrodialysis model establishes mathematical descriptions of +ion and water transport across the membrane along with water splitting. Modelled transfer mechanisms include +electrical migration, diffusion of ions, osmosis, electroosmosis, and water splitting. The following are the key +assumptions made: + +* The **Acidate** and **Basate** side channels have identical geometry. +* For each channel, component fluxes are uniform in the bulk solutions (the 0-dimensional assumption) and are set as the average of inlet and outlet of each channel. +* Steady state: all variables are independent on time. +* Co-current flow operation. +* Ideality assumptions: activity, osmotic, and van't Hoff coefficients are set at one. +* All ion-exchange membrane properties (ion and water transport number, resistance, permeability) are constant. +* Detailed concentration gradient effect at membrane-water interfaces is neglected. +* Constant pressure and temperature through each channel. + +Control Volumes +--------------- + +This model has two control volumes for the acidate and basate channels. + +* **Acidate** channel +* **Basate** side channel + +Ports +----- + +On the two control volumes, this model provides four ports (Pyomo notation in parenthesis): + +* inlet_acidate (inlet) +* outlet_acidate (outlet) +* inlet_basate (inlet) +* outlet_basate (outlet) + +Sets +---- +This model can simulate the water splitting and the transport of multiple species. All solution components +( H\ :sub:`2`\ O, neutral solutes, and ions, including Proton and Hydroxide ion) form a Pyomo set in the model. +For a clear model demonstration, this document uses a NaCl water solution along with the products of water splitting, H\ :sup:`+` and OH\ :sup:`-`, hereafter. + +This model can mathematically take a multi-component (i.e., one salt molecule to be treated) as an input; nevertheless +a multi-component solution creates unknown or difficult-to-specify parameters, e.g., the electrical transport numbers through membranes, +the multi-ion diffusivity, etc., and physical relationships, which may result in ill-posed or ill-conditioned problems challenging the models' +numerical solutions. While we continuously work on advancing our models to absorb new principles revealed by progressing +research, we advise the users be very **cautious** with simulating multi-component systems by this programmed model for aspects stated above. + +.. csv-table:: **Table 1.** List of Set + :header: "Description", "Symbol", "Indices" + + + "Time", ":math:`t`", "[t] ([0])\ :sup:`1`" + "Phase", ":math:`p`", "['Liq']" + "Component", ":math:`j`", "['H\ :sub:`2` \O', 'Na\ :sup:`+`', 'Cl\ :sup:`-`', 'H\ :sup:`+`', 'OH\ :sup:`-`']" + "Ion", ":math:`j`", "['Na\ :sup:`+`', 'Cl\ :sup:`-`', 'H\ :sup:`+`', 'OH\ :sup:`-`'] \ :sup:`2`" + "Membrane", "n/a", "['bpem']" + +**Notes** + :sup:`1` The time set index is set as [0] in this steady-state model and is reserved majorly for the future extension + to a dynamic model. + + :sup:`2` "Ion" is a subset of "Component" and uses the same symbol j. + + +Degrees of Freedom +------------------ +The bipolar membrane model has multiple degrees of freedom, among which temperature, pressure, and component molar flow +rate are state variables that are fixed as initial conditions. The rest are parameters that should be provided in order +to fully solve the model. The exact degrees of freedom depend on the mode of operation. For the simplest case where no water +splitting occurs and the bipolar membrane acts like a simple electrodialysis memrbane these are: + +.. csv-table:: **Table 2.** List of Degree of Freedom (DOF) + :header: "Description", "Symbol", "Variable Name", "Index", "Units", "DOF Number \ :sup:`1`" + + "Temperature, inlet_acidate", ":math:`T^acidate`", "temperature", "None", ":math:`K`", 1 + "Temperature, inlet_basate", ":math:`T^basate`", "temperature", "None", ":math:`K`", 1 + "Pressure, inlet_acidate",":math:`p^acidate`", "temperature", "None", ":math:`Pa`", 1 + "Pressure, inlet_basate",":math:`p^basate`", "temperature", "None", ":math:`Pa`", 1 + "Component molar flow rate, inlet_acidate", ":math:`N_{j,in}^{acidate}`", "flow_mol_phase_comp", "[t], ['Liq'], ['H\ :sub:`2`\O', 'Na\ :sup:`+`', '\Cl\ :sup:`-`', 'H\ :sup:`+`', 'OH\ :sup:`-`']", ":math:`mol \, s^{-1}`", 5 + "Component molar flow rate, inlet_basate", ":math:`N_{j, in}^{basate}`", "flow_mol_phase_comp", "[t], ['Liq'], ['H\ :sub:`2`\O', 'Na\ :sup:`+`', '\Cl\ :sup:`-`', 'H\ :sup:`+`', 'OH\ :sup:`-`']", ":math:`mol \, s^{-1}`", 5 + "Water transport number", ":math:`t_w`", "water_trans_number_membrane", "['bpem']", "dimensionless", 1 + "Water permeability", ":math:`L`", "water_permeability_membrane", "['bpem']", ":math:`m^{-1}s^{-1}Pa^{-1}`", 1 + "Voltage or Current \ :sup:`2`", ":math:`U` or :math:`I`", "voltage or current", "[t]", ":math:`\text{V}` or :math:`A`", 1 + "Electrode areal resistance", ":math:`r_{el}`", "electrodes_resistance", "[t]", ":math:`\Omega m^2`", 1 + "Cell pair number", ":math:`n`", "cell_pair_num", "None", "dimensionless", 1 + "Current utilization coefficient", ":math:`\xi`", "current_utilization", "None", "dimensionless", 1 + "Shadow factor", ":math:`\xi`", "shadow_factor", "None", "dimensionless", 1 + "Spacer thickness", ":math:`s`", "spacer_thickness", "none", ":math:`m` ", 1 + "Membrane areal resistance", ":math:`r`", "membrane_surface_resistance", "['acidate', 'basate']", ":math:`\Omega m^2`", 2 + "Cell width", ":math:`b`", "cell_width", "None", ":math:`\text{m}`", 1 + "Cell length", ":math:`l`", "cell_length", "None", ":math:`\text{m}`", 1 + "Thickness of ion exchange membranes", ":math:`\delta`", "membrane_thickness", "['bpem']", ":math:`m`", 1 + "transport number of ions in the membrane phase", ":math:`t_j`", "ion_trans_number_membrane", "['bpem'], ['Na\ :sup:`+`', '\Cl\ :sup:`-`', 'H\ :sup:`+`', 'OH\ :sup:`-`']", "dimensionless", 4 + +**Note** + :sup:`1` DOF number takes account of the indices of the corresponding parameter. + + :sup:`2` A user should provide either current or voltage as the electrical input, in correspondence to the +"Constant_Current" or "Constant_Voltage" treatment mode + + +Solution component information +------------------------------ +To fully construct solution properties, users need to provide basic component information of the feed solution to use +this model. Below is a sample: + +.. code-block:: + + ion_dict = { + "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], + "mw_data": { + "H2O": 18e-3, + "Na_+": 23e-3, + "Cl_-": 35.5e-3, + "H_+": 1e-3, + "OH_-": 17.0e-3, + }, + "elec_mobility_data": { + ("Liq", "Na_+"): 5.19e-8, + ("Liq", "Cl_-"): 7.92e-8, + ("Liq", "H_+"): 36.23e-8, + ("Liq", "OH_-"): 20.64e-8, + }, + "charge": {"Na_+": 1, "Cl_-": -1, "H_+": 1, "OH_-": -1}, + "diffusivity_data": { + ("Liq", "Na_+"): 1.33e-9, + ("Liq", "Cl_-"): 2.03e-9, + ("Liq", "H_+"): 9.31e-9, + ("Liq", "OH_-"): 5.27e-9, + }, + } + +This model, by default, uses H\ :sub:`2`\ O as the solvent of the feed solution. + +Information regarding the property package this unit model relies on can be found here: + +:py:mod:`watertap.property_models.ion_DSPMDE_prop_pack` + +Operation without catalyst +-------------------------- +The Mass balance equations are summarized in **Table3**. Further details on these can be found in the *References*. + +.. csv-table:: **Table 3** Mass Balance Equations + :header: "Description", "Equation", "Index set" + + "Component mass balance", ":math:`N_{j, in}^{acidate \: or\: basate}-N_{j, out}^{acidate\: or\: basate}+J_j^{acidate\: or\: basate} bl=0`", ":math:`j \in \left['H_2 O', '{Na^+} ', '{Cl^-} '\right]`" + "mass transfer flux, basate, solute", ":math:`J_j^{C} = -t_j^{bpem}\frac{\xi i_{lim}}{ z_j F}`", ":math:`j \in \left['{Na_+} ', '{Cl^-} '\right]`" + "mass transfer flux, acidate, Water ions", ":math:`J_j^{C} = \frac{i - i_{lim}}{ z_j F}`", ":math:`j \in \left['{H^+} '\right]`" + "mass transfer flux, acidate, Water ions", ":math:`J_j^{C} = 0`", ":math:`j \in \left['{OH^-} '\right]`" + "mass transfer flux, basate, Water ions", ":math:`J_j^{C} = 0`", ":math:`j \in \left['{H^+} '\right]`" + "mass transfer flux, basate, Water ions", ":math:`J_j^{C} = -\frac{i - i_{lim}}{ z_j F}`", ":math:`j \in \left['{OH^-} '\right]`" + "mass transfer flux, acidate H\ :sub:`2`\ O", ":math:`J_j^{C} = t_w^{bpem} \left(\frac{i}{F}\right)+\left(L^{bpem} \right)\left(p_{osm}^CEM-p_{osm}^AEM \right)\left(\frac{\rho_w}{M_w}\right)`", ":math:`j \in \left['H_2 O'\right]`" + "mass transfer flux, basate, H\ :sub:`2`\ O", ":math:`J_j^{C} = -t_w^{bpem} \left(\frac{i}{F}\right)-\left(L^{bpem} \right)\left(p_{osm}^CEM-p_{osm}^AEM \right)\left(\frac{\rho_w}{M_w}\right)`", ":math:`j \in \left['H_2 O'\right]`" + +Overcoming the limiting current corresponds to a potential barrier (:math:`U_{diss}`) gives the relationship :math:`U = i r_{tot}+ U_{diss}`. + + + +Both the current durrent density and potential barrier must be specified, via +``limiting_current_density_method_bpem =LimitingCurrentDensityMethod`` and ``limiting_potential_method_bpem =LimitingpotentialMethod`` +respectively, in the water splitting mode of operation. They can either be user inputs ``InitialValue``, with ``limiting_current_density_data`` +in :math:`A/m^2` and ``limiting_potential_data`` in volts. There is also an option to have these critical quantities computed. For this ``Empirical`` is chosen. + +The limiting current is computed as :math:`i_{lim} = D F (C_{acidate}+C_{basate})^2 / (\sigma \delta)`. The potential barrier +calculation involves kinetics of water splitting. The rate of proton/hydroxide ion formation per unit volume is given as +:math:`R_{H^+/OH^-} = [k_2(0)f(E)C_{H_2O}-k_r C_{H^+}C_{OH^-} ]`. A majority of the production occurs within the small +depletion region :math:`\lambda`, thus the flux is :math:`R_{H^+/OH^-} /\lambda`. When this flux is :math:`0.1 i_{lim}` +the barrier is assumed to be crossed, and the corresponding :math:`E=E_{crit}=U_{diss} \lambda` determines the potential barrier. + +The quantities :math:`C_{H_2 O}, C_{H^+}, C_{OH^-}` are the water proton and hydroxyl concentration in +:math:`mol\, m^{-3}` and are taken to be constants. :math:`f(E)` is the second Wien effect driven enhanacidateent of the +dissociation rate under applied electric field. It requires as input temperature and relative permittivity (:math:`\epsilon_r`). +To close the model :math:`\lambda = E_{crit} \epsilon_0 \epsilon_r / (F \sigma)` + + + +.. csv-table:: **Table 4.** DOF for water splitting without catalyst + :header: "Description", "Symbol", "Variable Name", "Index", "Units" + + "Diffusivity", ":math:`D`", "diffus_mass", "[bpem]", ":math:`m^2 s^{-1}`" + "Salt concentration, basate side ", ":math:`C_{basate}`", "salt_conc_basate", "[bpem]",":math:`mol m^{-3}`" + "Salt concentration, acidate side ", ":math:`C_{acidate}`", "salt_conc_acidate", "[bpem]",":math:`mol m^{-3}`" + "Membrane Fixed charge ", ":math:`\sigma`", "membrane_fixed_charge", "[bpem]",":math:`mol m^{-3}`" + "Dissociation rate constant, zero electric field ", ":math:`k_2(0)`", "kd_zero", "[bpem]",":math:`s^{-1}`" + "Recombination rate constant ", ":math:`k_r`", "k_r", "[bpem]",":math:`L^1 mol^{-1} s^{-1}`" + "Relative permittivity ", ":math:`\epsilon_r`", "relative_permittivity", "[bpem]","Non-dimensional" + +.. csv-table:: **Table 5** Electrical and Performance Equations + :header: "Description", "Equation" + + "Current density", ":math:`i = \frac{I}{bl}`" + "Ohm's Law", ":math:`U = i r_{tot}`" + "Resistance calculation", ":math:`r_{tot}=n\left(r^{acidate}+r^{basate}\right)+r_{el}`" + "Electrical power consumption", ":math:`P=UI`" + "Water-production-specific power consumption", ":math:`P_Q=\frac{UI}{3.6\times 10^6 nQ_{out}^D}`" + "Overall current efficiency", ":math:`I\eta=\sum_{j \in[cation]}{\left[\left(N_{j,in}^basate-N_{j,out}^basate\right)z_j F\right]}`" + +All equations are coded as "constraints" (Pyomo). Isothermal and isobaric conditions apply. + +The model has been validated using the bipolar membrane information available online: Fumatech, Technical Data Sheet for +Fumasep FBM, 2020. Additional inputs were obtained from from Ionescu, Viorel (2023) + + +Operation with catalyst +-------------------------- + +With catalyst present the water production term is modified as :math:`R_{H^+/OH^-} = \frac{Q_m}{K_{a/b}}[k_2(0)f(E)C_{H_2O}-k_r C_{H^+}C_{OH^-} ]`. Here :math:`Q_m` is the concentration of the catalyst and :math:`K_{a/b}` are the equilibrium constants for proton/hydroxide. The flux out of either side of the membrane is :math:`J_{diss} =R_{H^+} /\lambda + R_{OH^-} /\lambda`, with :math:`R_{H^+}` dominating the cation exchange side while the anion exchange side almost exclusively produces :math:`R_{OH^-}`. + +Thus the fluxes become, + +.. csv-table:: **Table 6** Mass Balance Equations + :header: "Description", "Equation", "Index set" + + "mass transfer flux, acidate/basate, Water ions", ":math:`J_j^{C} = J_{diss}`", ":math:`j \in \left['{H^+, OH^-} '\right]`" + "mass transfer flux, acidate H\ :sub:`2`\ O", ":math:`J_j^{C} = t_w^{bpem} \left(\frac{i}{F}\right)+\left(L^{bpem} \right)\left(p_{osm}^CEM-p_{osm}^AEM \right)\left(\frac{\rho_w}{M_w}\right) - J_{diss}`", ":math:`j \in \left['H_2 O'\right]`" + "mass transfer flux, basate, H\ :sub:`2`\ O", ":math:`J_j^{C} = -t_w^{bpem} \left(\frac{i}{F}\right)-\left(L^{bpem} \right)\left(p_{osm}^CEM-p_{osm}^AEM \right)\left(\frac{\rho_w}{M_w}\right) - J_{diss}`", ":math:`j \in \left['H_2 O'\right]`" + +.. csv-table:: **Table 7.** DOF for water splitting with catalyst + :header: "Description", "Symbol", "Variable Name", "Index", "Units" + + "Catalyst concentration on the cation exchange side", ":math:`Q_m`", "membrane_fixed_catalyst_cem", "[bpem]", ":math:`mol \, m^{-3}`" + "Catalyst concentration on the anion exchange side", ":math:`Q_m`", "membrane_fixed_catalyst_aem", "[bpem]", ":math:`mol \, m^{-3}`" + "Equilibrium constant of proton disassociation", ":math:`K_A`", "k_a", "none",":math:`mol \, m^{-3}`" + "Equilibrium constant of hydroxide disassociation", ":math:`K_B`", "k_b", "none",":math:`mol \, m^{-3}`" + +The model has been validated using the experimental data on bipolar membrane information available in Wilhelm et al. (2002) with additional inputs from Mareev et al. (2020). + +Frictional pressure drop +^^^^^^^^^^^^^^^^^^^^^^^^ +This model can optionally calculate pressured drops along the flow path in the diluate and concentrate channels through +config ``has_pressure_change`` and ``pressure_drop_method``. Under the assumption of identical diluate and concentrate +channels and starting flow rates, the flow velocities in the two channels are approximated equal and invariant over the +channel length when calculating the frictional pressure drops. This approximation is based on the evaluation that the +actual velocity variation over the channel length caused by water mass transfer across the consecutive channels leads to +negligible errors as compared to the uncertainties carried by the frictional pressure method itself. **Table 8** gives +essential equations to simulate the pressure drop. Among extensive literatures using these equations, a good reference +paper is by Wright et. al., 2018 (*References*). + +.. csv-table:: **Table 8** Essential equations supporting the pressure drop calculation + :header: "Description", "Equation", "Condition" + + "Frictional pressure drop, Darcy_Weisbach", ":math:`p_L=f\frac{\rho v^2}{2d_H}` \ :sup:`1`", "`has_pressure_change == True` and `pressure_drop_method == PressureDropMethod.Darcy_Weisbach`" + " ", ":math:`p_L=` user-input constant", "`has_pressure_change == True` and `pressure_drop_method == PressureDropMethod.Experimental`" + "Hydraulic diameter", ":math:`d_H=\frac{2db(1-\epsilon)}{d+b}`", "`hydraulic_diameter_method == HydraulicDiameterMethod.conventional`" + " ", ":math:`d_H=\frac{4\epsilon}{\frac{2}{h}+(1-\epsilon)S_{v,sp}}`", "`hydraulic_diameter_method == HydraulicDiameterMethod.spacer_specific_area_known`" + "Reynold number", ":math:`Re=\frac{\rho v d_H}{\mu}`", "`has_pressure_change == True` or `limiting_current_density_method == LimitingCurrentDensityMethod.Theoretical`" + "Schmidt number", ":math:`Sc=\frac{\mu}{\rho D_b}`", "`has_pressure_change == True` or `limiting_current_density_method == LimitingCurrentDensityMethod.Theoretical`" + "Sherwood number", ":math:`Sh=0.29Re^{0.5}Sc^{0.33}`", "`has_pressure_change == True` or `limiting_current_density_method == LimitingCurrentDensityMethod.Theoretical`" + "Darcy's frictional factor", ":math:`f=4\times 50.6\epsilon^{-7.06}Re^{-1}`", "`friction_factor_method == FrictionFactorMethod.Gurreri`" + " ", ":math:`f=4\times 9.6 \epsilon^{-1} Re^{-0.5}`", "`friction_factor_method == FrictionFactorMethod.Kuroda`" + "Pressure balance", ":math:`p_{in}-p_L l =p_{out}`", "`has_pressure_change == True`" + +**Note** + + :sup:`1` We assumed a constant linear velocity (in the cell length direction), :math:`v`, in both channels and along +the flow path. This :math:`v` is calculated based on the average of inlet and outlet volumetric flow rate. + +Nomenclature +------------ +.. csv-table:: **Table 9.** Nomenclature + :header: "Symbol", "Description", "Unit" + :widths: 10, 20, 10 + + "**Parameters**" + ":math:`\rho_w`", "Mass density of water", ":math:`kg\ m^{-3}`" + ":math:`M_w`", "Molecular weight of water", ":math:`kg\ mol^{-1}`" + "**Variables and Parameters**" + ":math:`N`", "Molar flow rate of a component", ":math:`mol\ s^{-1}`" + ":math:`J`", "Molar flux of a component", ":math:`mol\ m^{-2}s^{-1}`" + ":math:`b`", "Cell/membrane width", ":math:`m`" + ":math:`l`", "Cell/membrane length", ":math:`m`" + ":math:`t`", "Ion transport number", "dimensionless" + ":math:`I`", "Current", ":math:`A`" + ":math:`i`", "Current density", ":math:`A m^{-2}`" + ":math:`U`", "Voltage over a stack", ":math:`V`" + ":math:`n`", "Cell pair number", "dimensionless" + ":math:`\xi`", "Current utilization coefficient (including ion diffusion and water electroosmosis)", "dimensionless" + ":math:`z`", "Ion charge", "dimensionless" + ":math:`F`", "Faraday constant", ":math:`C\ mol^{-1}`" + ":math:`\epsilon_0`", "permittivity of free space", ":math:`C\ mol^{-1}`" + ":math:`D`", "Ion Diffusivity", ":math:`F m^-1`" + ":math:`\delta`", "Membrane thickness", ":math:`m`" + ":math:`c`", "Solute concentration", ":math:`mol\ m^{-3}`" + ":math:`t_w`", "Water electroosmotic transport number", "dimensionless" + ":math:`L`", "Water permeability (osmosis)", ":math:`ms^{-1}Pa^{-1}`" + ":math:`p_{osm}`", "Osmotic pressure", ":math:`Pa`" + ":math:`r_{tot}`", "Total areal resistance", ":math:`\Omega m^2`" + ":math:`r`", "Membrane areal resistance", ":math:`\Omega m^2`" + ":math:`r_{el}`", "Electrode areal resistance", ":math:`\Omega m^2`" + ":math:`d`", "Spacer thickness", ":math:`m`" + ":math:`\eta`", "Current efficiency for desalination", "dimensionless" + ":math:`P`", "Power consumption", ":math:`W`" + ":math:`P_Q`", "Specific power consumption", ":math:`kW\ h\ m^{-3}`" + ":math:`Q`", "Volume flow rate", ":math:`m^3s^{-1}`" + ":math:`\phi_d^{ohm}`", "Ohmic potential across a Nernst diffusion layer", ":math:`V`" + "**Subscripts and superscripts**" + ":math:`j`", "Component index", + ":math:`in`", "Inlet", + ":math:`out`", "Outlet", + ":math:`acidate`", "Cation exchange side of bipolar membrane", + ":math:`basate`", "Anion exchange side of bipolar membrane", + +References +---------- +Strathmann, H. (2010). Electrodialysis, a mature technology with a multitude of new applications. +Desalination, 264(3), 268-288. + +Strathmann, H. (2004). Ion-exchange membrane separation processes. Elsevier. Ch. 4. + +Campione, A., Cipollina, A., Bogle, I. D. L., Gurreri, L., Tamburini, A., Tedesco, M., & Micale, G. (2019). +A hierarchical model for novel schemes of electrodialysis desalination. Desalination, 465, 79-93. + +Campione, A., Gurreri, L., Ciofalo, M., Micale, G., Tamburini, A., & Cipollina, A. (2018). +Electrodialysis for water desalination: A critical assessment of recent developments on process +fundamentals, models and applications. Desalination, 434, 121-160. + +Spiegler, K. S. (1971). Polarization at ion exchange membrane-solution interfaces. Desalination, 9(4), 367-385. + +Wright, N. C., Shah, S. R., & Amrose, S. E. (2018). +A robust model of brackish water electrodialysis desalination with experimental comparison at different size scales. +Desalination, 443, 27-43. \ No newline at end of file diff --git a/watertap/costing/unit_models/bipolar_electrodialysis.py b/watertap/costing/unit_models/bipolar_electrodialysis.py new file mode 100644 index 0000000000..aa2706b2d9 --- /dev/null +++ b/watertap/costing/unit_models/bipolar_electrodialysis.py @@ -0,0 +1,183 @@ +################################################################################# +# 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/" +################################################################################# + +import pyomo.environ as pyo +from watertap.costing.util import ( + register_costing_parameter_block, + cost_rectifier, + make_capital_cost_var, + make_fixed_operating_cost_var, +) + + +def build_bipolar_electrodialysis_cost_param_block(blk): + # The following costing itemization and values are referenced to "Desalination 452 (2019) 265–278" + blk.membrane_capital_cost = pyo.Var( + initialize=160, + doc="Membrane and capital costs in [US$/m^2-membrane-area]", + units=pyo.units.USD_2018 / (pyo.units.meter**2), + ) + + blk.factor_membrane_replacement = pyo.Var( + initialize=0.2, + doc="Membrane and equipment (other stack components) housing replacement factor, equal to 1/lifetime.", + units=pyo.units.year**-1, + ) + + blk.stack_electrode_capital_cost = pyo.Var( + initialize=2100, + doc="Electrode cost in [US$/m^2-electrode-area] ", + units=pyo.units.USD_2018 / (pyo.units.meter**2), + ) + + blk.factor_stack_electrode_replacement = pyo.Var( + initialize=0.2, + doc="Stack and electrode replacement factor, equal to 1/lifetime.", + units=pyo.units.year**-1, + ) + blk.acid_flow_cost = pyo.Var( + initialize=-0.12, + doc="Acid flow cost", + units=pyo.units.USD_2018 / (pyo.units.kg), + ) + + blk.base_flow_cost = pyo.Var( + initialize=-0.5, + doc="Acid flow cost", + units=pyo.units.USD_2018 / (pyo.units.kg), + ) + blk.parent_block().register_flow_type("acid_flow_cost", blk.acid_flow_cost) + blk.parent_block().register_flow_type("base_flow_cost", blk.base_flow_cost) + + +@register_costing_parameter_block( + build_rule=build_bipolar_electrodialysis_cost_param_block, + parameter_block_name="bipolar_electrodialysis", +) +def cost_bipolar_electrodialysis( + blk, + cost_electricity_flow=True, + has_rectifier=False, + has_bp_acid_value=False, + has_bp_base_value=False, +): + """ + Function for costing the bipolar electrodialysis unit + + Args: + cost_electricity_flow (:obj:`bool`, optional): Option for including the + costing of electricity. Defaults to True. + has_rectifier (:obj:`bool`, optional): Option for including a rectifier. + Defaults to False. + """ + t0 = blk.flowsheet().time.first() + + # Changed this to grab power from performance table which is identified + # by same key regardless of whether the Electrodialysis unit is 0D or 1D + if cost_electricity_flow: + if not has_rectifier: + blk.costing_package.cost_flow( + pyo.units.convert( + blk.unit_model.get_power_electrical(t0), + to_units=pyo.units.kW, + ), + "electricity", + ) + else: + power = blk.unit_model.get_power_electrical(blk.flowsheet().time.first()) + cost_rectifier(blk, power=power, ac_dc_conversion_efficiency=0.9) + + if has_bp_acid_value: + blk.costing_package.cost_flow( + pyo.units.convert( + blk.unit_model.acid_produced, + to_units=pyo.units.kg * pyo.units.second**-1, + ), + "acid_flow_cost", + ) + if has_bp_base_value: + blk.costing_package.cost_flow( + pyo.units.convert( + blk.unit_model.base_produced, + to_units=pyo.units.kg * pyo.units.second**-1, + ), + "base_flow_cost", + ) + + cost_bipolar_electrodialysis_stack(blk) + + +def cost_bipolar_electrodialysis_stack(blk): + """ + Generic function for costing the stack in an electrodialysis unit. + Assumes the unit_model has a `cell_pair_num`, `cell_width`, and `cell_length` + set of variables used to size the total membrane area. + + """ + make_capital_cost_var(blk) + make_fixed_operating_cost_var(blk) + blk.costing_package.add_cost_factor(blk, "TIC") + if blk.find_component("capital_cost_rectifier") is not None: + blk.capital_cost_constraint = pyo.Constraint( + expr=blk.capital_cost + == blk.cost_factor + * ( + pyo.units.convert( + blk.costing_package.bipolar_electrodialysis.membrane_capital_cost + * ( + 4 + * blk.unit_model.cell_pair_num + * blk.unit_model.cell_width + * blk.unit_model.cell_length + ) + + blk.costing_package.bipolar_electrodialysis.stack_electrode_capital_cost + * (2 * blk.unit_model.cell_width * blk.unit_model.cell_length), + to_units=blk.costing_package.base_currency, + ) + + blk.capital_cost_rectifier + ) + ) + else: + blk.capital_cost_constraint = pyo.Constraint( + expr=blk.capital_cost + == blk.cost_factor + * pyo.units.convert( + blk.costing_package.bipolar_electrodialysis.membrane_capital_cost + * ( + 4 + * blk.unit_model.cell_pair_num + * blk.unit_model.cell_width + * blk.unit_model.cell_length + ) + + blk.costing_package.bipolar_electrodialysis.stack_electrode_capital_cost + * (2 * blk.unit_model.cell_width * blk.unit_model.cell_length), + to_units=blk.costing_package.base_currency, + ) + ) + blk.fixed_operating_cost_constraint = pyo.Constraint( + expr=blk.fixed_operating_cost + == pyo.units.convert( + blk.costing_package.bipolar_electrodialysis.factor_membrane_replacement + * blk.costing_package.bipolar_electrodialysis.membrane_capital_cost + * ( + 4 + * blk.unit_model.cell_pair_num + * blk.unit_model.cell_width + * blk.unit_model.cell_length + ) + + blk.costing_package.bipolar_electrodialysis.factor_stack_electrode_replacement + * blk.costing_package.bipolar_electrodialysis.stack_electrode_capital_cost + * (2 * blk.unit_model.cell_width * blk.unit_model.cell_length), + to_units=blk.costing_package.base_currency + / blk.costing_package.base_period, + ) + ) diff --git a/watertap/unit_models/Bipolar_Electrodialysis_0D.py b/watertap/unit_models/Bipolar_Electrodialysis_0D.py new file mode 100644 index 0000000000..0390d0bfc2 --- /dev/null +++ b/watertap/unit_models/Bipolar_Electrodialysis_0D.py @@ -0,0 +1,2427 @@ +################################################################################# +# 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/" +################################################################################# +import math + +# Import Pyomo libraries +from pyomo.environ import ( + Set, + Var, + check_optimal_termination, + Param, + Suffix, + NonNegativeReals, + value, + log, + Constraint, + sqrt, + units as pyunits, +) +from pyomo.common.config import Bool, ConfigBlock, ConfigValue, In + +# Import IDAES cores +from idaes.core import ( + declare_process_block_class, + MaterialBalanceType, + EnergyBalanceType, + MomentumBalanceType, + UnitModelBlockData, + useDefault, +) +from idaes.core.util.misc import add_object_reference +from watertap.core.solvers import get_solver +from idaes.core.util.tables import create_stream_table_dataframe +from idaes.core.util.config import is_physical_parameter_block +from idaes.core.util.math import smooth_min + +from idaes.core.util.exceptions import ConfigurationError, InitializationError + +import idaes.core.util.scaling as iscale +import idaes.logger as idaeslog +from idaes.core.util.constants import Constants +from enum import Enum + +from watertap.core import ControlVolume0DBlock, InitializationMixin +from watertap.costing.unit_models.bipolar_electrodialysis import ( + cost_bipolar_electrodialysis, +) + +__author__ = " Johnson Dhanasekaran, Xiangyu Bi, Austin Ladshaw, Kejia Hu" + +_log = idaeslog.getLogger(__name__) + + +class LimitingCurrentDensitybpemMethod(Enum): + InitialValue = 0 + Empirical = 1 + + +class LimitingpotentialMethod(Enum): + InitialValue = 0 + Empirical = 1 + + +class ElectricalOperationMode(Enum): + Constant_Current = 0 + Constant_Voltage = 1 + + +class PressureDropMethod(Enum): + none = 0 + experimental = 1 + Darcy_Weisbach = 2 + + +class FrictionFactorMethod(Enum): + fixed = 0 + Gurreri = 1 + Kuroda = 2 + + +class HydraulicDiameterMethod(Enum): + fixed = 0 + spacer_specific_area_known = 1 + conventional = 2 + + +# Name of the unit model +@declare_process_block_class("Bipolar_Electrodialysis_0D") +class BipolarElectrodialysis0DData(InitializationMixin, UnitModelBlockData): + """ + 0D Bipolar and Electrodialysis Model + """ + + # CONFIG are options for the unit model + CONFIG = ConfigBlock() # + + CONFIG.declare( + "dynamic", + ConfigValue( + domain=In([False]), + default=False, + description="Dynamic model flag - must be False", + doc="""Indicates whether this model will be dynamic or not, + **default** = False. The filtration unit does not support dynamic + behavior, thus this must be False.""", + ), + ) + + CONFIG.declare( + "has_holdup", + ConfigValue( + default=False, + domain=In([False]), + description="Holdup construction flag - must be False", + doc="""Indicates whether holdup terms should be constructed or not. + **default** - False. The filtration unit does not have defined volume, thus + this must be False.""", + ), + ) + CONFIG.declare( + "has_pressure_change", + ConfigValue( + default=False, + domain=In([True, False]), + description="Pressure change term construction flag", + doc="""Indicates whether terms for pressure change should be + constructed, + **default** - False. + **Valid values:** { + **True** - include pressure change terms, + **False** - exclude pressure change terms.}""", + ), + ) + CONFIG.declare( + "pressure_drop_method", + ConfigValue( + default=PressureDropMethod.none, + domain=In(PressureDropMethod), + description="Method to calculate the frictional pressure drop in electrodialysis channels", + doc=""" + **default** - ``PressureDropMethod.none`` + + .. csv-table:: + :header: "Configuration Options", "Description" + + "``PressureDropMethod.none``", "The frictional pressure drop is neglected." + "``PressureDropMethod.experimental``", "The pressure drop is calculated by an experimental data as pressure drop per unit lenght." + "``PressureDropMethod.Darcy_Weisbach``", "The pressure drop is calculated by the Darcy-Weisbach equation." + """, + ), + ) + CONFIG.declare( + "friction_factor_method", + ConfigValue( + default=FrictionFactorMethod.fixed, + domain=In(FrictionFactorMethod), + description="Method to calculate the Darcy's friction factor", + doc=""" + **default** - ``FrictionFactorMethod.fixed`` + + .. csv-table:: + :header: "Configuration Options", "Description" + + "``FrictionFactorMethod.fixed``", "Friction factor is fixed by users" + "``FrictionFactorMethod.Gurreri``", "Friction factor evaluated based on Gurreri's work" + "``FrictionFactorMethod.Kuroda``", "Friction factor evaluated based on Kuroda's work" + """, + ), + ) + + CONFIG.declare( + "hydraulic_diameter_method", + ConfigValue( + default=HydraulicDiameterMethod.conventional, + domain=In(HydraulicDiameterMethod), + description="Method to calculate the hydraulic diameter for a rectangular channel in ED", + doc=""" + **default** - ``HydraulicDiameterMethod.conventional`` + + .. csv-table:: + :header: "Configuration Options", "Description" + + "``HydraulicDiameterMethod.fixed``", "Hydraulic diameter is fixed by users" + "``HydraulicDiameterMethod.conventional``", "Conventional method for a rectangular channel with spacer porosity considered" + "``HydraulicDiameterMethod.spacer_specific_area_known``", "A method for spacer-filled channel requiring the spacer specific area data" + """, + ), + ) + + CONFIG.declare( + "operation_mode", + ConfigValue( + default=ElectricalOperationMode.Constant_Current, + domain=In(ElectricalOperationMode), + description="The electrical operation mode. To be selected between Constant Current and Constant Voltage", + ), + ) + + CONFIG.declare( + "limiting_current_density_method_bpem", + ConfigValue( + default=LimitingCurrentDensitybpemMethod.InitialValue, + domain=In(LimitingCurrentDensitybpemMethod), + description="Configuration for method to compute the limiting current density across the bipolar membrane", + doc=""" + **default** - ``LimitingCurrentDensitybpemMethod.InitialValue`` + + .. csv-table:: + :header: "Configuration Options", "Description" + + "``LimitingCurrentDensitybpemMethod.InitialValue``", "Limiting current is calculated from a single initial value given by the user." + "``LimitingCurrentDensitybpemMethod.Empirical``", "Limiting current density is calculated from the empirical relationship" + """, + ), + ) + + CONFIG.declare( + "has_catalyst", + ConfigValue( + default=False, + domain=Bool, + description="""Catalyst action on water spliting, + **default** - False.""", + ), + ) + + CONFIG.declare( + "limiting_potential_method_bpem", + ConfigValue( + default=LimitingpotentialMethod.InitialValue, + domain=In(LimitingpotentialMethod), + description="Configuration for method to compute the limiting potential in bipolar membrane", + doc=""" + **default** - ``LimitingpotentialMethod.InitialValue`` + + .. csv-table:: + :header: "Configuration Options", "Description" + + "``LimitingpotentialMethod.InitialValue``", "Limiting current is calculated from a initial value given by the user." + "``LimitingpotentialMethod.Empirical``", "Limiting current density is caculated from the empirical equation" + """, + ), + ) + + CONFIG.declare( + "limiting_current_density_bpem_data", + ConfigValue( + default=0.5, + description="Limiting current density data input for bipolar membrane", + ), + ) + CONFIG.declare( + "salt_input_cem", + ConfigValue( + default=100, + description="Specified salt concentration on acid (C.E.M side) channel of the bipolar membrane", + ), + ) + CONFIG.declare( + "salt_input_aem", + ConfigValue( + default=100, + description="Specified salt concentration on base (A.E.M side) channel of the bipolar membrane", + ), + ) + + CONFIG.declare( + "limiting_potential_data", + ConfigValue( + default=0.5, + description="Limiting potential of the bipolar membrane input", + ), + ) + + CONFIG.declare( + "material_balance_type", + ConfigValue( + default=MaterialBalanceType.useDefault, + domain=In(MaterialBalanceType), + description="Material balance construction flag", + doc="""Indicates what type of mass balance should be constructed, + **default** - MaterialBalanceType.useDefault. + **Valid values:** { + **MaterialBalanceType.useDefault - refer to property package for default + balance type + **MaterialBalanceType.none** - exclude material balances, + **MaterialBalanceType.componentPhase** - use phase component balances, + **MaterialBalanceType.componentTotal** - use total component balances, + **MaterialBalanceType.elementTotal** - use total element balances, + **MaterialBalanceType.total** - use total material balance.}""", + ), + ) + + CONFIG.declare( + "is_isothermal", + ConfigValue( + default=True, + domain=Bool, + description="""Assume isothermal conditions for control volume(s); energy_balance_type must be EnergyBalanceType.none, + **default** - True.""", + ), + ) + + CONFIG.declare( + "energy_balance_type", + ConfigValue( + default=EnergyBalanceType.none, + domain=In(EnergyBalanceType), + description="Energy balance construction flag", + doc="""Indicates what type of energy balance should be constructed, + **default** - EnergyBalanceType.none. + **Valid values:** { + **EnergyBalanceType.useDefault - refer to property package for default + balance type + **EnergyBalanceType.none** - exclude energy balances, + **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, + **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, + **EnergyBalanceType.energyTotal** - single energy balance for material, + **EnergyBalanceType.energyPhase** - energy balances for each phase.}""", + ), + ) + + CONFIG.declare( + "momentum_balance_type", + ConfigValue( + default=MomentumBalanceType.pressureTotal, + domain=In(MomentumBalanceType), + description="Momentum balance construction flag", + doc="""Indicates what type of momentum balance should be constructed, + **default** - MomentumBalanceType.pressureTotal. + **Valid values:** { + **MomentumBalanceType.none** - exclude momentum balances, + **MomentumBalanceType.pressureTotal** - single pressure balance for material, + **MomentumBalanceType.pressurePhase** - pressure balances for each phase, + **MomentumBalanceType.momentumTotal** - single momentum balance for material, + **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""", + ), + ) + + CONFIG.declare( + "property_package", + ConfigValue( + default=useDefault, + domain=is_physical_parameter_block, + description="Property package to use for control volume", + 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.}""", + ), + ) + + def _validate_config(self): + if ( + self.config.is_isothermal + and self.config.energy_balance_type != EnergyBalanceType.none + ): + raise ConfigurationError( + "If the isothermal assumption is used then the energy balance type must be none" + ) + + def build(self): + # build always starts by calling super().build() + # This triggers a lot of boilerplate in the background for you + super().build() + # this creates blank scaling factors, which are populated later + self.scaling_factor = Suffix(direction=Suffix.EXPORT) + + # Check configs for errors + self._validate_config() + + # Create essential sets. + self.membrane_set = Set(initialize=["bpem"]) + + add_object_reference(self, "ion_set", self.config.property_package.ion_set) + + add_object_reference( + self, "cation_set", self.config.property_package.cation_set + ) + add_object_reference(self, "anion_set", self.config.property_package.anion_set) + # Create unit model parameters and vars + self.water_density = Param( + initialize=1000, + units=pyunits.kg * pyunits.m**-3, + doc="density of water", + ) + + self.cell_num = Var( + initialize=1, + domain=NonNegativeReals, + bounds=(1, 10000), + units=pyunits.dimensionless, + doc="cell pair number in a stack", + ) + + # electrodialysis cell dimensional properties + self.cell_width = Var( + initialize=0.1, + bounds=(1e-3, 1e3), + units=pyunits.meter, + doc="The width of the electrodialysis cell, denoted as b in the model description", + ) + self.cell_length = Var( + initialize=0.5, + bounds=(1e-3, 1e2), + units=pyunits.meter, + doc="The length of the electrodialysis cell, denoted as l in the model description", + ) + self.channel_height = Var( + initialize=0.0001, + units=pyunits.meter, + doc="The distance between the consecutive aem and cem", + ) + self.spacer_porosity = Var( + initialize=0.7, + bounds=(0.01, 1), + units=pyunits.dimensionless, + doc='The prosity of spacer in the ED channels. This is also referred to elsewhere as "void fraction" or "volume parameters"', + ) + + # Material and Operational properties + self.membrane_thickness = Var( + self.membrane_set, + initialize=0.0001, + bounds=(1e-6, 1e-1), + units=pyunits.meter, + doc="Membrane thickness", + ) + self.solute_diffusivity_membrane = Var( + self.membrane_set, + self.ion_set | self.config.property_package.solute_set, + initialize=1e-10, + bounds=(0.0, 1e-6), + units=pyunits.meter**2 * pyunits.second**-1, + doc="Solute (ionic and neutral) diffusivity in the membrane phase", + ) + self.ion_trans_number_membrane = Var( + self.membrane_set, + self.ion_set, + bounds=(0, 1), + units=pyunits.dimensionless, + doc="Ion transference number in the membrane phase", + ) + self.water_trans_number_membrane = Var( + self.membrane_set, + initialize=5, + bounds=(0, 50), + units=pyunits.dimensionless, + doc="Transference number of water in membranes", + ) + self.water_permeability_membrane = Var( + self.membrane_set, + initialize=1e-14, + units=pyunits.meter * pyunits.second**-1 * pyunits.pascal**-1, + doc="Water permeability coefficient", + ) + self.membrane_areal_resistance = Var( + initialize=2e-4, + bounds=(0, 1), + units=pyunits.ohm * pyunits.meter**2, + doc="Surface resistance of membrane", + ) + self.electrodes_resistance = Var( + initialize=0, + bounds=(0, 100), + domain=NonNegativeReals, + units=pyunits.ohm * pyunits.meter**2, + doc="areal resistance of TWO electrode compartments of a stack", + ) + self.current = Var( + self.flowsheet().time, + initialize=1, + bounds=(0, 1e6), + units=pyunits.amp, + doc="Current across a cell-pair or stack", + ) + self.voltage = Var( + self.flowsheet().time, + initialize=100, + bounds=(0, 1000), + units=pyunits.volt, + doc="Voltage across a stack, declared under the 'Constant Voltage' mode only", + ) + self.current_utilization = Var( + initialize=1, + bounds=(0, 1), + units=pyunits.dimensionless, + doc="The current utilization including water electro-osmosis and ion diffusion", + ) + self.shadow_factor = Var( + initialize=1, + bounds=(0, 1), + units=pyunits.dimensionless, + doc="The reduction in area due to limited cross-section available for flow", + ) + + # Performance metrics + self.current_efficiency = Var( + self.flowsheet().time, + initialize=0.9, + bounds=(-1.2, 1.2), + units=pyunits.dimensionless, + doc="The overall current efficiency for deionizaiton", + ) + self.power_electrical = Var( + self.flowsheet().time, + initialize=1, + bounds=(0, 12100), + domain=NonNegativeReals, + units=pyunits.watt, + doc="Electrical power consumption of a stack", + ) + self.specific_power_electrical = Var( + self.flowsheet().time, + initialize=10, + bounds=(0, 1000), + domain=NonNegativeReals, + units=pyunits.kW * pyunits.hour * pyunits.meter**-3, + doc="Acidate-volume-flow-rate-specific electrical power consumption", + ) + self.recovery_mass_H2O = Var( + self.flowsheet().time, + initialize=0.5, + bounds=(0, 1), + domain=NonNegativeReals, + units=pyunits.dimensionless, + doc="water recovery ratio calculated by mass", + ) + self.acid_produced = Var( + initialize=55 * 1e3, + bounds=(0, 1e6), + units=pyunits.kg * pyunits.second**-1, + doc="Acid prodcued", + ) + self.base_produced = Var( + initialize=55 * 1e3, + bounds=(0, 1e6), + units=pyunits.kg * pyunits.second**-1, + doc="Base prodcued", + ) + self.velocity_basate = Var( + self.flowsheet().time, + initialize=0.01, + units=pyunits.meter * pyunits.second**-1, + doc="Linear velocity of flow in the base channel of the bipolar membrane", + ) + self.velocity_acidate = Var( + self.flowsheet().time, + initialize=0.01, + units=pyunits.meter * pyunits.second**-1, + doc="Linear velocity of flow in the acid channel of the bipolar membrane", + ) + # Parameters for bipolar membrane operation + self.elec_field_non_dim = Var( + self.flowsheet().time, + initialize=1, + # bounds=(0, 1e32), + units=pyunits.dimensionless, + doc="Limiting current density across the bipolar membrane as a function of the normalized length", + ) + self.relative_permittivity = Var( + self.membrane_set, + initialize=30, + bounds=(1, 80), + domain=NonNegativeReals, + units=pyunits.dimensionless, + doc="Relative permittivity", + ) + self.membrane_fixed_charge = Var( + self.membrane_set, + initialize=1.5e3, + bounds=(1e-1, 1e5), + units=pyunits.mole * pyunits.meter**-3, + doc="Membrane fixed charge", + ) + self.kr = Var( + self.membrane_set, + initialize=1.33 * 10**11, + bounds=(1e-6, 1e16), + units=pyunits.L * pyunits.mole**-1 * pyunits.second**-1, + doc="Re-association rate constant", + ) + self.k2_zero = Var( + self.membrane_set, + initialize=2 * 10**-5, + bounds=(1e-10, 1e2), + units=pyunits.second**-1, + doc="Dissociation rate constant at no electric field", + ) + self.salt_conc_aem = Var( + self.membrane_set, + initialize=1e3, + bounds=(1e-8, 1e6), + units=pyunits.mole * pyunits.meter**-3, + doc="Salt concentration on the base channel of the bipolar membrane", + ) + self.salt_conc_cem = Var( + self.membrane_set, + initialize=1e3, + bounds=(1e-6, 1e4), + units=pyunits.mole * pyunits.meter**-3, + doc="Salt concentration on the acid channel of the bipolar membrane", + ) + self.diffus_mass = Var( + # self.membrane_set, + initialize=2e-9, + bounds=(1e-16, 1e-6), + units=pyunits.meter**2 * pyunits.second**-1, + doc="The mass diffusivity of the solute as molecules (not individual ions)", + ) + self.conc_water = Var( + self.membrane_set, + initialize=55 * 1e3, + bounds=(1e-2, 1e6), + units=pyunits.mole * pyunits.meter**-3, + doc="Concentration of water within the channel", + ) + + # Limiting operation quantity + self.current_dens_lim_bpem = Var( + self.flowsheet().time, + initialize=1e2, + bounds=(0, 1e5), + units=pyunits.amp * pyunits.meter**-2, + doc="Limiting current density across the bipolar membrane", + ) + + # Fluxes Vars for constructing mass transfer terms + self.generation_cem_flux_in = Var( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, + doc="Molar flux_in of a component generated by water splitting on the acid channel of the bipolar membrane", + ) + self.generation_cem_flux_out = Var( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, + doc="Molar flux_in of a component generated by water splitting on the acid channel of the bipolar membrane", + ) + self.generation_aem_flux_in = Var( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, + doc="Molar flux_in of a component generated by water splitting on the base channel of the bipolar membrane", + ) + self.generation_aem_flux_out = Var( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, + doc="Molar flux_in of a component generated by water splitting on the base channel of the bipolar membrane", + ) + self.elec_migration_bpem_flux_in = Var( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, + doc="Molar flux_in of a component across the membrane driven by electrical migration across the bipolar membrane", + ) + self.elec_migration_bpem_flux_out = Var( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, + doc="Molar flux_out of a component across the membrane driven by electrical migration across the bipolar membrane", + ) + self.nonelec_bpem_flux_in = Var( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, + doc="Molar flux_in of a component across the membrane driven by non-electrical forces across the bipolar membrane", + ) + self.nonelec_bpem_flux_out = Var( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, + doc="Molar flux_out of a component across the membrane driven by non-electrical forces across the bipolar membrane", + ) + + # Build control volume for the base channel of the bipolar channel + self.basate = ControlVolume0DBlock( + dynamic=False, + has_holdup=False, + property_package=self.config.property_package, + property_package_args=self.config.property_package_args, + ) + self.basate.add_state_blocks(has_phase_equilibrium=False) + self.basate.add_material_balances( + balance_type=self.config.material_balance_type, has_mass_transfer=True + ) + self.basate.add_energy_balances( + balance_type=self.config.energy_balance_type, + has_enthalpy_transfer=False, + ) + + if self.config.is_isothermal: + self.basate.add_isothermal_assumption() + self.basate.add_momentum_balances( + balance_type=self.config.momentum_balance_type, + has_pressure_change=self.config.has_pressure_change, + ) + # Build control volume for the acid channel of the bipolar membrane channel + self.acidate = ControlVolume0DBlock( + dynamic=False, + has_holdup=False, + property_package=self.config.property_package, + property_package_args=self.config.property_package_args, + ) + self.acidate.add_state_blocks(has_phase_equilibrium=False) + self.acidate.add_material_balances( + balance_type=self.config.material_balance_type, has_mass_transfer=True + ) + self.acidate.add_energy_balances( + balance_type=self.config.energy_balance_type, + has_enthalpy_transfer=False, + ) + + if self.config.is_isothermal: + self.acidate.add_isothermal_assumption() + self.acidate.add_momentum_balances( + balance_type=self.config.momentum_balance_type, + has_pressure_change=self.config.has_pressure_change, + ) + + # den_mass and visc_d in acidate and basate channels are the same + add_object_reference( + self, "dens_mass", self.acidate.properties_in[0].dens_mass_phase["Liq"] + ) + add_object_reference( + self, "visc_d", self.acidate.properties_in[0].visc_d_phase["Liq"] + ) + + # Add ports (creates inlets and outlets for each channel) + self.add_inlet_port(name="inlet_basate", block=self.basate) + self.add_outlet_port(name="outlet_basate", block=self.basate) + self.add_inlet_port(name="inlet_acidate", block=self.acidate) + self.add_outlet_port(name="outlet_acidate", block=self.acidate) + + # extension options + if self.config.has_catalyst == True: + self._make_catalyst() + + if ( + not self.config.pressure_drop_method == PressureDropMethod.none + ) and self.config.has_pressure_change: + self._pressure_drop_calculation() + + @self.Constraint( + self.flowsheet().time, + doc="Pressure drop expression as calculated by the pressure drop data, " + "base channel of the bipolar membrane.", + ) + def eq_deltaP_basate(self, t): + return self.basate.deltaP[t] == -self.pressure_drop_total[t] + + @self.Constraint( + self.flowsheet().time, + doc="Pressure drop expression as calculated by the pressure drop data," + " acid channel of the bipolar membrane.", + ) + def eq_deltaP_acidate(self, t): + return self.acidate.deltaP[t] == -self.pressure_drop_total[t] + + elif self.config.pressure_drop_method == PressureDropMethod.none and ( + not self.config.has_pressure_change + ): + pass + else: + raise ConfigurationError( + "A valid (not none) pressure_drop_method and has_pressure_change being True " + "must be both used or unused at the same time. " + ) + + # Build Constraints + + @self.Constraint( + self.flowsheet().time, + doc="Calculate flow velocity in a single base (A.E.M side) channel of the bipolar membrane channel," + " based on the average of inlet and outlet", + ) + def eq_get_velocity_basate(self, t): + return self.velocity_basate[ + t + ] * self.cell_width * self.shadow_factor * self.channel_height * self.spacer_porosity * self.cell_num == 0.5 * ( + self.basate.properties_in[0].flow_vol_phase["Liq"] + + self.basate.properties_out[0].flow_vol_phase["Liq"] + ) + + @self.Constraint( + self.flowsheet().time, + doc="Calculate flow velocity in a single acid (C.E.M side) channel of the bipolar membrane channel," + " based on the average of inlet and outlet", + ) + def eq_get_velocity_acidate(self, t): + return self.velocity_acidate[ + t + ] * self.cell_width * self.shadow_factor * self.channel_height * self.spacer_porosity * self.cell_num == 0.5 * ( + self.acidate.properties_in[0].flow_vol_phase["Liq"] + + self.acidate.properties_out[0].flow_vol_phase["Liq"] + ) + + @self.Constraint( + self.flowsheet().time, + doc="Calculate limiting current density across the bipolar membrane", + ) + def eq_current_dens_lim_bpem(self, t): + if ( + self.config.limiting_current_density_method_bpem + == LimitingCurrentDensitybpemMethod.InitialValue + ): + return self.current_dens_lim_bpem[t] == ( + self.config.limiting_current_density_bpem_data + * pyunits.amp + * pyunits.meter**-2 + ) + elif ( + self.config.limiting_current_density_method_bpem + == LimitingCurrentDensitybpemMethod.Empirical + ): + return self.current_dens_lim_bpem[ + t + ] == self.diffus_mass * Constants.faraday_constant * ( + (self.salt_conc_aem["bpem"] + self.salt_conc_cem["bpem"]) * 0.5 + ) ** 2 / ( + self.membrane_thickness["bpem"] * self.membrane_fixed_charge["bpem"] + ) + + @self.Constraint( + self.flowsheet().time, + doc="Calculate the potential drops across the bipolar membrane", + ) + def eq_potential_barrier_bpem(self, t): + if self.config.has_catalyst: + return Constraint.Skip + else: + self.potential_barrier_bpem = Var( + self.flowsheet().time, + initialize=1, + bounds=(0, 5000), + units=pyunits.volt, + doc="Potential barrier across the depletion layer for water splitting to begin", + ) + + if ( + self.config.limiting_potential_method_bpem + == LimitingpotentialMethod.InitialValue + ): + return self.potential_barrier_bpem[t] == ( + self.config.limiting_potential_data * pyunits.volt + ) + + elif ( + self.config.limiting_potential_method_bpem + == LimitingpotentialMethod.Empirical + ): + # [H+][OH-] concentration + kw = 10**-8 * pyunits.mol**2 * pyunits.meter**-6 + + # Fraction of threshold of limiting current: currently 0.1 i_lim + frac = 1 * 10**-1 + # Dimensional pre-factor to evaulate non-dimensional electric field + const = 0.0936 * pyunits.K**2 * pyunits.volt**-1 * pyunits.meter + + @self.Constraint( + self.flowsheet().time, + doc="Calculate the non-dimensional potential drop", + ) + def eq_potential_barrier_bpem_non_dim(self, t): + # [y2, qty_une, qty_deux, qty_trois] = dat + terms = 40 + matrx = 0 + for indx in range(terms): + # rev_indx = terms - indx - 1 + matrx += ( + 2**indx + * self.elec_field_non_dim[t] ** indx + / (math.factorial(indx) * math.factorial(indx + 1)) + ) + + matrx *= self.k2_zero["bpem"] * self.conc_water["bpem"] + matrx += ( + -pyunits.convert( + self.kr["bpem"], + to_units=pyunits.meter**3 + * pyunits.mole**-1 + * pyunits.second**-1, + ) + * kw + ) + return ( + Constants.vacuum_electric_permittivity + * self.relative_permittivity["bpem"] ** 2 + * self.basate.properties_in[t].temperature ** 2 + * Constants.avogadro_number + * Constants.elemental_charge + ) / ( + const + * Constants.faraday_constant + * self.membrane_fixed_charge["bpem"] + ) * matrx * self.elec_field_non_dim[ + t + ] == self.current_dens_lim_bpem[ + t + ] * frac + + # Dimensional electric field + field_generated = ( + self.elec_field_non_dim[t] + * self.relative_permittivity["bpem"] + * self.basate.properties_in[t].temperature ** 2 + / const + ) + + # Depletion length at the junction of the bipolar membrane + lambda_depletion = ( + field_generated + * Constants.vacuum_electric_permittivity + * self.relative_permittivity["bpem"] + / ( + Constants.faraday_constant + * self.membrane_fixed_charge["bpem"] + ) + ) + + return ( + self.potential_barrier_bpem[t] + == field_generated * lambda_depletion + ) + + else: + self.potential_barrier_bpem[t].fix(0 * pyunits.volt) + return Constraint.Skip + + @self.Constraint( + self.flowsheet().time, + self.config.property_package.phase_list, + doc="Current-Voltage relationship", + ) + def eq_current_voltage_relation(self, t, p): + + total_areal_resistance = ( + self.membrane_areal_resistance + + self.channel_height + * ( + 0.5**-1 + * ( + self.basate.properties_in[t].elec_cond_phase["Liq"] + + self.basate.properties_out[t].elec_cond_phase["Liq"] + ) + ** -1 + + 0.5**-1 + * ( + self.acidate.properties_in[t].elec_cond_phase["Liq"] + + self.acidate.properties_out[t].elec_cond_phase["Liq"] + ) + ** -1 + ) + ) * self.cell_num + self.electrodes_resistance + # the average conductivity of each channel's inlet and outlet is taken to represent that of the entire channel + + if self.config.has_catalyst: + voltage_membrane_drop = self.potential_membrane_bpem[t] + + @self.Constraint( + self.flowsheet().time, + doc="Calculate total current generated via catalyst action", + ) + def eq_current_relationship(self, t): + return self.current[t] == ( + self.current_dens_lim_bpem[t] + + self.flux_splitting[t] * Constants.faraday_constant + ) * (self.cell_width * self.shadow_factor * self.cell_length) + + else: + voltage_membrane_drop = self.potential_barrier_bpem[t] + + return ( + self.current[t] + * (self.cell_width * self.shadow_factor * self.cell_length) ** -1 + * total_areal_resistance + + voltage_membrane_drop * self.cell_num + == self.voltage[t] + ) + + @self.Constraint( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + doc="Equation for water splitting acid channel (C.E.M Side) of bipolar membrane flux_in", + ) + def eq_generation_cem_flux_in(self, t, p, j): + if j == "H_+": + if self.config.has_catalyst == True: + return ( + self.generation_cem_flux_in[t, p, j] == self.flux_splitting[t] + ) + + else: + return self.generation_cem_flux_in[t, p, j] == ( + -smooth_min( + -( + self.current[t] / pyunits.amp + - self.current_dens_lim_bpem[t] + * self.cell_width + * self.shadow_factor + * self.cell_length + / pyunits.amp + ), + 0, + ) + * pyunits.amp + / (self.cell_width * self.shadow_factor * self.cell_length) + ) / (Constants.faraday_constant) + + else: + if j == "H2O": + if self.config.has_catalyst == True: + return ( + self.generation_cem_flux_in[t, p, j] + == -self.flux_splitting[t] + ) + else: + return self.generation_cem_flux_in[t, p, j] == ( + smooth_min( + -( + self.current[t] / pyunits.amp + - self.current_dens_lim_bpem[t] + * self.cell_width + * self.shadow_factor + * self.cell_length + / pyunits.amp + ), + 0, + ) + * pyunits.amp + / (self.cell_width * self.shadow_factor * self.cell_length) + ) / (Constants.faraday_constant) + + else: + return ( + self.generation_cem_flux_in[t, p, j] + == 0 * pyunits.mol * pyunits.meter**-2 * pyunits.s**-1 + ) + + @self.Constraint( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + doc="Equation for water splitting base channel (A.E.M Side) of bipolar membrane flux_in", + ) + def eq_generation_aem_flux_in(self, t, p, j): + if j == "OH_-": + if self.config.has_catalyst == True: + return ( + self.generation_aem_flux_in[t, p, j] == self.flux_splitting[t] + ) + + else: + return self.generation_aem_flux_in[t, p, j] == ( + -smooth_min( + -( + self.current[t] / pyunits.amp + - self.current_dens_lim_bpem[t] + * self.cell_width + * self.shadow_factor + * self.cell_length + / pyunits.amp + ), + 0, + ) + * pyunits.amp + / (self.cell_width * self.shadow_factor * self.cell_length) + ) / (Constants.faraday_constant) + + else: + if j == "H2O": + if self.config.has_catalyst == True: + return ( + self.generation_aem_flux_in[t, p, j] + == self.flux_splitting[t] + ) + + else: + return self.generation_aem_flux_in[t, p, j] == ( + smooth_min( + -( + self.current[t] / pyunits.amp + - self.current_dens_lim_bpem[t] + * self.cell_width + * self.shadow_factor + * self.cell_length + / pyunits.amp + ), + 0, + ) + * pyunits.amp + / (self.cell_width * self.shadow_factor * self.cell_length) + ) / (Constants.faraday_constant) + + else: + return ( + self.generation_aem_flux_in[t, p, j] + == 0 * pyunits.mol * pyunits.meter**-2 * pyunits.s**-1 + ) + + @self.Constraint( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + doc="Equation for water splitting acid channel (C.E.M Side) of bipolar membrane flux_out", + ) + def eq_generation_cem_flux_out(self, t, p, j): + return ( + self.generation_cem_flux_in[t, p, j] + == self.generation_cem_flux_out[t, p, j] + ) + + @self.Constraint( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + doc="Equation for water splitting base channel (A.E.M Side) of bipolar membrane flux_out", + ) + def eq_generation_aem_flux_out(self, t, p, j): + return ( + self.generation_aem_flux_in[t, p, j] + == self.generation_aem_flux_out[t, p, j] + ) + + @self.Constraint( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + doc="Equation for electrical migration across the bipolar membrane flux_in", + ) + def eq_elec_migration_bpem_flux_in(self, t, p, j): + if j == "H2O": + return self.elec_migration_bpem_flux_in[t, p, j] == ( + self.water_trans_number_membrane["bpem"] + ) * ( + self.current[t] + / (self.cell_width * self.shadow_factor * self.cell_length) + / Constants.faraday_constant + ) + + elif j in self.ion_set: + if not (j == "H_+" or j == "OH_-"): + if self.config.has_catalyst == False: + + return self.elec_migration_bpem_flux_in[t, p, j] == ( + self.ion_trans_number_membrane["bpem", j] + ) * ( + self.current_utilization + * smooth_min( + self.current[t] / pyunits.amp, + self.current_dens_lim_bpem[t] + * self.cell_width + * self.shadow_factor + * self.cell_length + / pyunits.amp, + ) + * pyunits.amp + / (self.cell_width * self.shadow_factor * self.cell_length) + ) / ( + self.config.property_package.charge_comp[j] + * Constants.faraday_constant + ) + else: + return self.elec_migration_bpem_flux_in[t, p, j] == ( + self.ion_trans_number_membrane["bpem", j] + ) * ( + self.current_utilization * self.current_dens_lim_bpem[t] + ) / ( + self.config.property_package.charge_comp[j] + * Constants.faraday_constant + ) + + else: + + self.elec_migration_bpem_flux_in[t, p, j].fix( + 0 * pyunits.mol * pyunits.m**-2 * pyunits.s**-1 + ) + return Constraint.Skip + else: + self.elec_migration_bpem_flux_in[t, p, j].fix( + 0 * pyunits.mol * pyunits.m**-2 * pyunits.s**-1 + ) + return Constraint.Skip + + @self.Constraint( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + doc="Equation for electrical migration across the bipolar membrane flux_out", + ) + def eq_elec_migration_bpem_flux_out(self, t, p, j): + return ( + self.elec_migration_bpem_flux_out[t, p, j] + == self.elec_migration_bpem_flux_in[t, p, j] + ) + + @self.Constraint( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + doc="Equation for non-electrical flux across the bipolar membrane flux_in", + ) + def eq_nonelec_bpem_flux_in(self, t, p, j): + if j == "H2O": + return self.nonelec_bpem_flux_in[ + t, p, j + ] == self.water_density / self.config.property_package.mw_comp[j] * ( + self.water_permeability_membrane["bpem"] + ) * ( + self.basate.properties_in[t].pressure_osm_phase[p] + - self.acidate.properties_in[t].pressure_osm_phase[p] + ) + + else: + return ( + self.nonelec_bpem_flux_in[t, p, j] + == 0 * pyunits.mol * pyunits.m**-2 * pyunits.s**-1 + ) + + @self.Constraint( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + doc="Equation for non-electrical flux across the bipolar membrane flux_out", + ) + def eq_nonelec_bpem_flux_out(self, t, p, j): + if j == "H2O": + return self.nonelec_bpem_flux_out[ + t, p, j + ] == self.water_density / self.config.property_package.mw_comp[j] * ( + self.water_permeability_membrane["bpem"] + ) * ( + self.basate.properties_out[t].pressure_osm_phase[p] + - self.acidate.properties_out[t].pressure_osm_phase[p] + ) + + else: + return ( + self.nonelec_bpem_flux_out[t, p, j] + == 0 * pyunits.mol * pyunits.m**-2 * pyunits.s**-1 + ) + + # Add constraints for mass transfer terms (base channel/A.E.M Side of the bipolar membrane) + @self.Constraint( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + doc="Mass transfer term for the base channel (A.E.M Side) of the bipolar membrane", + ) + def eq_mass_transfer_term_basate(self, t, p, j): + return ( + self.basate.mass_transfer_term[t, p, j] + == 0.5 + * ( + self.generation_aem_flux_in[t, p, j] + + self.generation_aem_flux_out[t, p, j] + + self.elec_migration_bpem_flux_in[t, p, j] + + self.elec_migration_bpem_flux_out[t, p, j] + + self.nonelec_bpem_flux_in[t, p, j] + + self.nonelec_bpem_flux_out[t, p, j] + ) + * (self.cell_width * self.shadow_factor * self.cell_length) + * self.cell_num + ) + + # Add constraints for mass transfer terms (acid channel/C.E.M Side of the bipolar membrane) + @self.Constraint( + self.flowsheet().time, + self.config.property_package.phase_list, + self.config.property_package.component_list, + doc="Mass transfer term for the acid channel (C.E.M Side) of the bipolar membrane channel", + ) + def eq_mass_transfer_term_acidate(self, t, p, j): + return ( + self.acidate.mass_transfer_term[t, p, j] + == 0.5 + * ( + self.generation_cem_flux_in[t, p, j] + + self.generation_cem_flux_out[t, p, j] + + self.elec_migration_bpem_flux_in[t, p, j] + + self.elec_migration_bpem_flux_out[t, p, j] + + self.nonelec_bpem_flux_in[t, p, j] + + self.nonelec_bpem_flux_out[t, p, j] + ) + * (self.cell_width * self.shadow_factor * self.cell_length) + * self.cell_num + ) + + # Performance: acid/base produced + @self.Constraint( + self.flowsheet().time, + doc="Evaluate Base produced", + ) + def eq_product_basate(self, t): + conc_unit = 1 * pyunits.mole * pyunits.second**-1 + product_net_loc = 0 * pyunits.kg * pyunits.second**-1 + + for j in self.config.property_package.cation_set: + if not j == "H_+": + product_in_loc = smooth_min( + self.basate.properties_in[t].flow_mol_phase_comp["Liq", j] + / conc_unit, + self.basate.properties_in[t].flow_mol_phase_comp["Liq", "OH_-"] + / conc_unit + / self.config.property_package.charge_comp[j], + ) + + product_out_loc = smooth_min( + self.basate.properties_out[t].flow_mol_phase_comp["Liq", j] + / conc_unit, + self.basate.properties_out[t].flow_mol_phase_comp["Liq", "OH_-"] + / conc_unit + / self.config.property_package.charge_comp[j], + ) + + product_net_loc += ( + -1 + * smooth_min(product_in_loc - product_out_loc, 0) + * conc_unit + * ( + self.config.property_package.charge_comp[j] + * self.config.property_package.mw_comp["OH_-"] + + self.config.property_package.mw_comp[j] + ) + ) + + return self.base_produced == product_net_loc + + @self.Constraint( + self.flowsheet().time, + doc="Evaluate Acid produced", + ) + def eq_product_acidate(self, t): + conc_unit = 1 * pyunits.mole * pyunits.second**-1 + product_net_loc = 0 * pyunits.kg * pyunits.second**-1 + + for j in self.config.property_package.anion_set: + if not j == "OH_-": + product_in_loc = smooth_min( + self.acidate.properties_in[t].flow_mol_phase_comp["Liq", j] + / conc_unit, + self.acidate.properties_in[t].flow_mol_phase_comp["Liq", "H_+"] + / conc_unit + / (-self.config.property_package.charge_comp[j]), + ) + + product_out_loc = smooth_min( + self.acidate.properties_out[t].flow_mol_phase_comp["Liq", j] + / conc_unit, + self.acidate.properties_out[t].flow_mol_phase_comp["Liq", "H_+"] + / conc_unit + / (-self.config.property_package.charge_comp[j]), + ) + + product_net_loc += ( + -1 + * smooth_min(product_in_loc - product_out_loc, 0) + * conc_unit + * ( + (-self.config.property_package.charge_comp[j]) + * self.config.property_package.mw_comp["H_+"] + + self.config.property_package.mw_comp[j] + ) + ) + + return self.acid_produced == product_net_loc + + # Performance: Electrical + @self.Constraint( + self.flowsheet().time, + doc="Electrical power consumption of a stack", + ) + def eq_power_electrical(self, t): + return self.power_electrical[t] == self.current[t] * self.voltage[t] + + @self.Constraint( + self.flowsheet().time, + doc="Diluate_volume_flow_rate_specific electrical power consumption of a stack", + ) + def eq_specific_power_electrical(self, t): + return ( + pyunits.convert( + self.specific_power_electrical[t], + pyunits.watt * pyunits.second * pyunits.meter**-3, + ) + * self.acidate.properties_out[t].flow_vol_phase["Liq"] + == self.current[t] * self.voltage[t] + ) + + @self.Constraint( + self.flowsheet().time, + doc="Overall current efficiency evaluation", + ) + def eq_current_efficiency(self, t): + return ( + self.current_efficiency[t] * self.current[t] * self.cell_num + == sum( + self.acidate.properties_in[t].flow_mol_phase_comp["Liq", j] + * self.config.property_package.charge_comp[j] + - self.acidate.properties_out[t].flow_mol_phase_comp["Liq", j] + * self.config.property_package.charge_comp[j] + for j in self.config.property_package.cation_set + ) + * Constants.faraday_constant + ) + + @self.Constraint( + self.flowsheet().time, + doc="Water recovery by mass", + ) + def eq_recovery_mass_H2O(self, t): + return ( + self.recovery_mass_H2O[t] + * ( + self.acidate.properties_in[t].flow_mass_phase_comp["Liq", "H2O"] + + self.basate.properties_in[t].flow_mass_phase_comp["Liq", "H2O"] + ) + == self.acidate.properties_out[t].flow_mass_phase_comp["Liq", "H2O"] + ) + + # Catalyst action: + def _make_catalyst(self): + + self.potential_membrane_bpem = Var( + self.flowsheet().time, + initialize=0.1, + bounds=(0, 1e8), + units=pyunits.volt, + doc="Potential drop across the depletion layer", + ) + self.flux_splitting = Var( + self.flowsheet().time, + initialize=1, + domain=NonNegativeReals, + # bounds=(0, 50000), + units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, + doc="Flux generated", + ) + self.membrane_fixed_catalyst_aem = Var( + self.membrane_set, + initialize=5e3, + bounds=(1e-1, 1e5), + units=pyunits.mole * pyunits.meter**-3, + doc="Membrane fixed charge", + ) + self.membrane_fixed_catalyst_cem = Var( + self.membrane_set, + initialize=5e3, + bounds=(1e-1, 1e5), + units=pyunits.mole * pyunits.meter**-3, + doc="Membrane fixed charge", + ) + + self.k_a = Var( + initialize=1e-3, + bounds=(1e-15, 1e15), + units=pyunits.mole * pyunits.meter**-3, + doc="Membrane fixed charge", + ) + self.k_b = Var( + initialize=3e-2, + bounds=(1e-15, 1e15), + units=pyunits.mole * pyunits.meter**-3, + doc="Membrane fixed charge", + ) + + const = 0.0936 * pyunits.K**2 * pyunits.volt**-1 * pyunits.meter + + @self.Constraint( + self.flowsheet().time, + doc="Calculate the non-dimensional potential drop across the depletion region", + ) + def eq_potential_membrane_bpem_non_dim(self, t): + return self.elec_field_non_dim[t] == const * self.basate.properties_in[ + t + ].temperature ** -2 * self.relative_permittivity["bpem"] ** -1 * sqrt( + ( + Constants.faraday_constant + * self.membrane_fixed_charge["bpem"] + * self.potential_membrane_bpem[t] + ) + / ( + Constants.vacuum_electric_permittivity + * self.relative_permittivity["bpem"] + ) + ) + + @self.Constraint( + self.flowsheet().time, + doc="Calculate the potential barrier at limiting current across the bipolar membrane", + ) + def eq_flux_splitting(self, t): + terms = 40 + matrx = 0 + for indx in range(terms): + matrx += ( + 2**indx + * self.elec_field_non_dim[t] ** indx + / (math.factorial(indx) * math.factorial(indx + 1)) + ) + + matrx *= self.k2_zero["bpem"] * self.conc_water["bpem"] + matrx_a = matrx * self.membrane_fixed_catalyst_cem["bpem"] / self.k_a + matrx_b = matrx * self.membrane_fixed_catalyst_aem["bpem"] / self.k_b + return self.flux_splitting[t] == (matrx_a + matrx_b) * sqrt( + self.potential_membrane_bpem[t] + * Constants.vacuum_electric_permittivity + * self.relative_permittivity["bpem"] + / (Constants.faraday_constant * self.membrane_fixed_charge["bpem"]) + ) + + def _get_fluid_dimensionless_quantities(self): + # self.diffus_mass = Var( + # initialize=1e-9, + # bounds=(1e-16, 1e-6), + # units=pyunits.meter**2 * pyunits.second**-1, + # doc="The mass diffusivity of the solute as molecules (not individual ions)", + # ) + self.hydraulic_diameter = Var( + initialize=1e-3, + bounds=(0, None), + units=pyunits.meter, + doc="The hydraulic diameter of the channel", + ) + self.N_Re = Var( + initialize=50, + bounds=(0, None), + units=pyunits.dimensionless, + doc="Reynolds Number", + ) + # self.N_Sc = Var( + # initialize=2000, + # bounds=(0, None), + # units=pyunits.dimensionless, + # doc="Schmidt Number", + # ) + # self.N_Sh = Var( + # initialize=100, + # bounds=(0, None), + # units=pyunits.dimensionless, + # doc="Sherwood Number", + # ) + + if self.config.hydraulic_diameter_method == HydraulicDiameterMethod.fixed: + _log.warning("Do not forget to FIX the channel hydraulic diameter in [m]!") + else: + + @self.Constraint( + doc="To calculate hydraulic diameter", + ) + def eq_hydraulic_diameter(self): + if ( + self.config.hydraulic_diameter_method + == HydraulicDiameterMethod.conventional + ): + return ( + self.hydraulic_diameter + == 2 + * self.channel_height + * self.cell_width + * self.shadow_factor + * self.spacer_porosity + * (self.channel_height + self.cell_width * self.shadow_factor) + ** -1 + ) + else: + self.spacer_specific_area = Var( + initialize=1e4, + bounds=(0, None), + units=pyunits.meter**-1, + doc="The specific area of the channel", + ) + return ( + self.hydraulic_diameter + == 4 + * self.spacer_porosity + * ( + 2 * self.channel_height**-1 + + (1 - self.spacer_porosity) * self.spacer_specific_area + ) + ** -1 + ) + + @self.Constraint( + doc="To calculate Re", + ) + def eq_Re(self): + + return ( + self.N_Re + == self.dens_mass + * self.velocity_acidate[0] + * self.hydraulic_diameter + * self.visc_d**-1 + ) + + # @self.Constraint( + # doc="To calculate Sc", + # ) + # def eq_Sc(self): + # + # return self.N_Sc == self.visc_d * self.dens_mass**-1 * self.diffus_mass**-1 + # + # @self.Constraint( + # doc="To calculate Sh", + # ) + # def eq_Sh(self): + # + # return self.N_Sh == 0.29 * self.N_Re**0.5 * self.N_Sc**0.33 + + def _pressure_drop_calculation(self): + self.pressure_drop_total = Var( + self.flowsheet().time, + initialize=1e6, + units=pyunits.pascal, + doc="pressure drop over an entire ED stack", + ) + self.pressure_drop = Var( + self.flowsheet().time, + initialize=1e3, + units=pyunits.pascal * pyunits.meter**-1, + doc="pressure drop per unit of length", + ) + + if self.config.pressure_drop_method == PressureDropMethod.experimental: + _log.warning( + "Do not forget to FIX the experimental pressure drop value in [Pa/m]!" + ) + else: # PressureDropMethod.Darcy_Weisbach is used + # if not (self.config.has_Nernst_diffusion_layer == True): + self._get_fluid_dimensionless_quantities() + + self.friction_factor = Var( + initialize=10, + bounds=(0, None), + units=pyunits.dimensionless, + doc="friction factor of the channel fluid", + ) + + @self.Constraint( + self.flowsheet().time, + doc="To calculate pressure drop over an stack", + ) + def eq_pressure_drop_total(self, t): + return ( + self.pressure_drop_total[t] + == self.dens_mass + * self.friction_factor + * self.velocity_acidate[0] ** 2 + * 0.5 + * self.hydraulic_diameter**-1 + * self.cell_length + ) + + if self.config.friction_factor_method == FrictionFactorMethod.fixed: + _log.warning("Do not forget to FIX the Darcy's friction factor value!") + else: + + @self.Constraint( + doc="To calculate friction factor", + ) + def eq_friction_factor(self): + if ( + self.config.friction_factor_method + == FrictionFactorMethod.Gurreri + ): + return ( + self.friction_factor + == 4 * 50.6 * self.spacer_porosity**-7.06 * self.N_Re**-1 + ) + elif ( + self.config.friction_factor_method + == FrictionFactorMethod.Kuroda + ): + return ( + self.friction_factor + == 4 * 9.6 * self.spacer_porosity**-1 * self.N_Re**-0.5 + ) + + @self.Constraint( + self.flowsheet().time, + doc="To calculate total pressure drop over a stack", + ) + def eq_pressure_drop(self, t): + return ( + self.pressure_drop[t] + == self.pressure_drop_total[t] * self.cell_length**-1 + ) + + # initialize method + def initialize_build( + self, + state_args=None, + outlvl=idaeslog.NOTSET, + solver=None, + optarg=None, + ): + """ + General wrapper for pressure changer initialization routines + + Keyword Arguments: + state_args : a dict of arguments to be passed to the property + package(s) to provide an initial state for + initialization (see documentation of the specific + property package) (default = {}). + outlvl : sets output level of initialization routine + optarg : solver options dictionary object (default=None) + solver : str indicating which solver to use during + initialization (default = None) + + Returns: None + """ + init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") + solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") + # Set solver options + opt = get_solver(solver, optarg) + + # --------------------------------------------------------------------- + + # Set the outlet has the same intial condition of the inlet. + for k in self.keys(): + for j in self[k].config.property_package.component_list: + self[k].basate.properties_out[0].flow_mol_phase_comp["Liq", j] = value( + self[k].basate.properties_in[0].flow_mol_phase_comp["Liq", j] + ) + self[k].acidate.properties_out[0].flow_mol_phase_comp["Liq", j] = value( + self[k].acidate.properties_in[0].flow_mol_phase_comp["Liq", j] + ) + + # --------------------------------------------------------------------- + # Initialize concentrate_basate_side block + flags_basate = self.basate.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args, + hold_state=True, + ) + init_log.info_high("Initialization Step 2 Complete.") + # --------------------------------------------------------------------- + # Initialize concentrate_acidate_side block + flags_acidate = self.acidate.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + state_args=state_args, # inlet var + hold_state=True, + ) + init_log.info_high("Initialization Step 3 Complete.") + # --------------------------------------------------------------------- + # Solve unit + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = opt.solve(self, tee=slc.tee) + init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) + # --------------------------------------------------------------------- + # Release state + self.basate.release_state(flags_basate, outlvl) + init_log.info("Initialization Complete: {}".format(idaeslog.condition(res))) + self.acidate.release_state(flags_acidate, outlvl) + init_log.info("Initialization Complete: {}".format(idaeslog.condition(res))) + + if not check_optimal_termination(res): + raise InitializationError(f"Unit model {self.name} failed to initialize") + + def calculate_scaling_factors(self): + super().calculate_scaling_factors() + # Scaling factors that user may setup + # The users are highly encouraged to provide scaling factors for assessable vars below. + # Not providing these vars will give a warning. + if ( + iscale.get_scaling_factor(self.solute_diffusivity_membrane, warning=True) + is None + ): + iscale.set_scaling_factor(self.solute_diffusivity_membrane, 1e10) + if iscale.get_scaling_factor(self.membrane_thickness, warning=True) is None: + iscale.set_scaling_factor(self.membrane_thickness, 1e4) + if ( + iscale.get_scaling_factor(self.water_permeability_membrane, warning=True) + is None + ): + iscale.set_scaling_factor(self.water_permeability_membrane, 1e14) + if iscale.get_scaling_factor(self.cell_num, warning=True) is None: + iscale.set_scaling_factor(self.cell_num, 0.1) + if iscale.get_scaling_factor(self.cell_length, warning=True) is None: + iscale.set_scaling_factor(self.cell_length, 1e1) + if iscale.get_scaling_factor(self.cell_width, warning=True) is None: + iscale.set_scaling_factor(self.cell_width, 1e2) + if iscale.get_scaling_factor(self.shadow_factor, warning=True) is None: + iscale.set_scaling_factor(self.shadow_factor, 1) + if iscale.get_scaling_factor(self.channel_height, warning=True) is None: + iscale.set_scaling_factor(self.channel_height, 1e5) + if iscale.get_scaling_factor(self.spacer_porosity, warning=True) is None: + iscale.set_scaling_factor(self.spacer_porosity, 1) + if ( + iscale.get_scaling_factor(self.membrane_areal_resistance, warning=True) + is None + ): + iscale.set_scaling_factor(self.membrane_areal_resistance, 1e5) + if iscale.get_scaling_factor(self.electrodes_resistance, warning=True) is None: + iscale.set_scaling_factor(self.electrodes_resistance, 1e1) + if iscale.get_scaling_factor(self.current, warning=True) is None: + iscale.set_scaling_factor(self.current, 1) + if iscale.get_scaling_factor(self.voltage, warning=True) is None: + iscale.set_scaling_factor(self.voltage, 1) + if hasattr(self, "conc_water") and ( + iscale.get_scaling_factor(self.conc_water, warning=True) is None + ): + iscale.set_scaling_factor(self.conc_water, 1e-4) + + if hasattr(self, "acid_produced") and ( + iscale.get_scaling_factor(self.acid_produced, warning=True) is None + ): + sf = 1 + for j in self.config.property_package.anion_set: + sf = smooth_min( + iscale.get_scaling_factor( + self.acidate.properties_in[0].flow_mass_phase_comp["Liq", j] + ), + iscale.get_scaling_factor( + self.acidate.properties_in[0].flow_mass_phase_comp["Liq", "H_+"] + ), + ) + + iscale.set_scaling_factor(self.acid_produced, sf) + + if hasattr(self, "base_produced") and ( + iscale.get_scaling_factor(self.base_produced, warning=True) is None + ): + sf = 1 + for j in self.config.property_package.cation_set: + sf = smooth_min( + iscale.get_scaling_factor( + self.basate.properties_in[0].flow_mass_phase_comp["Liq", j] + ), + iscale.get_scaling_factor( + self.basate.properties_in[0].flow_mass_phase_comp["Liq", "OH_-"] + ), + ) + + iscale.set_scaling_factor(self.base_produced, sf) + + if self.config.has_catalyst == True: + if ( + iscale.get_scaling_factor( + self.membrane_fixed_catalyst_cem, warning=True + ) + is None + ): + iscale.set_scaling_factor(self.membrane_fixed_catalyst_cem, 1e-3) + # if self.config.has_catalyst == True: + if ( + iscale.get_scaling_factor( + self.membrane_fixed_catalyst_aem, warning=True + ) + is None + ): + iscale.set_scaling_factor(self.membrane_fixed_catalyst_aem, 1e-3) + + if iscale.get_scaling_factor(self.k_a, warning=True) is None: + iscale.set_scaling_factor(self.k_a, 1e6) + + if iscale.get_scaling_factor(self.k_b, warning=True) is None: + iscale.set_scaling_factor(self.k_b, 1e2) + + if ( + self.config.has_catalyst == True + or self.config.limiting_potential_method_bpem + == LimitingpotentialMethod.Empirical + ) and iscale.get_scaling_factor(self.elec_field_non_dim, warning=True) is None: + iscale.set_scaling_factor(self.elec_field_non_dim, 1e-1) + + if ( + self.config.limiting_potential_method_bpem + == LimitingpotentialMethod.Empirical + and iscale.get_scaling_factor(self.potential_barrier_bpem, warning=True) + is None + ): + iscale.set_scaling_factor(self.potential_barrier_bpem, 1) + + if hasattr(self, "membrane_fixed_charge") and ( + iscale.get_scaling_factor(self.membrane_fixed_charge, warning=True) is None + ): + iscale.set_scaling_factor(self.membrane_fixed_charge, 1e-3) + if hasattr(self, "diffus_mass") and ( + iscale.get_scaling_factor(self.diffus_mass, warning=True) is None + ): + iscale.set_scaling_factor(self.diffus_mass, 1e9) + if hasattr(self, "salt_conc_aem") and ( + iscale.get_scaling_factor(self.salt_conc_aem, warning=True) is None + ): + iscale.set_scaling_factor(self.salt_conc_aem, 1e-2) + if hasattr(self, "salt_conc_cem") and ( + iscale.get_scaling_factor(self.salt_conc_cem, warning=True) is None + ): + iscale.set_scaling_factor(self.salt_conc_cem, 1e-2) + + if ( + self.config.has_catalyst == True + or self.config.limiting_potential_method_bpem + == LimitingpotentialMethod.Empirical + ): + + if hasattr(self, "relative_permittivity") and ( + iscale.get_scaling_factor(self.relative_permittivity, warning=True) + is None + ): + iscale.set_scaling_factor(self.relative_permittivity, 1e-1) + + if iscale.get_scaling_factor(self.kr, warning=True) is None: + iscale.set_scaling_factor(self.kr, 1e-11) + if ( + hasattr(self, "k2_zero") + and iscale.get_scaling_factor(self.k2_zero, warning=True) is None + ): + iscale.set_scaling_factor(self.k2_zero, 1e5) + + # The folloing Vars are built for constructing constraints and their sf are computed from other Vars. + + if ( + self.config.has_catalyst == True + and iscale.get_scaling_factor(self.potential_membrane_bpem, warning=True) + is None + ): + sf = ( + ( + iscale.get_scaling_factor(self.elec_field_non_dim) + * iscale.get_scaling_factor(self.relative_permittivity) + * 293**-2 + / 0.09636**-1 + ) + ** 2 + * value(Constants.vacuum_electric_permittivity) ** -1 + * iscale.get_scaling_factor(self.relative_permittivity) + * value(Constants.faraday_constant) ** -1 + * iscale.get_scaling_factor(self.membrane_fixed_charge) + ) + + iscale.set_scaling_factor(self.potential_membrane_bpem, 1e1) + + if self.config.has_catalyst == True: + if iscale.get_scaling_factor(self.flux_splitting, warning=True) is None: + + terms = 40 + sf = 0 + for indx in range(terms): + sf += ( + 2**indx + * iscale.get_scaling_factor(self.elec_field_non_dim) ** -indx + / (math.factorial(indx) * math.factorial(indx + 1)) + ) + + sf **= -1 + sf *= iscale.get_scaling_factor( + self.k2_zero + ) * iscale.get_scaling_factor(self.conc_water) + sf_a = ( + sf + * iscale.get_scaling_factor(self.membrane_fixed_catalyst_cem) + / iscale.get_scaling_factor(self.k_a) + ) + sf_b = ( + sf + * iscale.get_scaling_factor(self.membrane_fixed_catalyst_aem) + / iscale.get_scaling_factor(self.k_b) + ) + + sf = (sf_a**-1 + sf_b**-1) ** -1 * sqrt( + iscale.get_scaling_factor(self.potential_membrane_bpem) + * value(Constants.vacuum_electric_permittivity) ** -1 + * iscale.get_scaling_factor(self.relative_permittivity) + / ( + value(Constants.faraday_constant) ** -1 + * iscale.get_scaling_factor(self.membrane_fixed_charge) + ) + ) + + iscale.set_scaling_factor(self.flux_splitting, sf) + + iscale.set_scaling_factor( + self.elec_migration_bpem_flux_in, + iscale.get_scaling_factor(self.current) + * iscale.get_scaling_factor(self.cell_length) ** -1 + * iscale.get_scaling_factor(self.cell_width) ** -1 + * iscale.get_scaling_factor(self.shadow_factor) ** -1 + * 1e5, + ) + iscale.set_scaling_factor( + self.elec_migration_bpem_flux_out, + iscale.get_scaling_factor(self.current) + * iscale.get_scaling_factor(self.cell_length) ** -1 + * iscale.get_scaling_factor(self.cell_width) ** -1 + * iscale.get_scaling_factor(self.shadow_factor) ** -1 + * 1e5, + ) + + for ind in self.generation_cem_flux_in: + if ind[2] == "H_+" or "H2O": + if self.config.has_catalyst == True: + sf = 0.5 * iscale.get_scaling_factor(self.flux_splitting) + else: + sf = iscale.get_scaling_factor(self.elec_migration_bpem_flux_in) + else: + sf = 1 + + iscale.set_scaling_factor(self.generation_cem_flux_in[ind], sf) + + for ind in self.generation_cem_flux_out: + if ind[2] == "H_+" or "H2O": + if self.config.has_catalyst == True: + sf = 0.5 * iscale.get_scaling_factor(self.flux_splitting) + else: + sf = iscale.get_scaling_factor(self.elec_migration_bpem_flux_out) + else: + sf = 1 + + iscale.set_scaling_factor(self.generation_cem_flux_out[ind], sf) + + for ind in self.generation_aem_flux_in: + if ind[2] == "OH_-" or "H2O": + if self.config.has_catalyst == True: + sf = iscale.get_scaling_factor(self.flux_splitting) + else: + sf = iscale.get_scaling_factor(self.elec_migration_bpem_flux_in) + else: + sf = 1 + + iscale.set_scaling_factor(self.generation_aem_flux_in[ind], sf) + + for ind in self.generation_aem_flux_out: + if ind[2] == "OH_-" or "H2O": + if self.config.has_catalyst == True: + sf = iscale.get_scaling_factor(self.flux_splitting) + else: + sf = iscale.get_scaling_factor(self.elec_migration_bpem_flux_out) + else: + sf = 1 + + iscale.set_scaling_factor(self.generation_aem_flux_out[ind], sf) + + for ind in self.nonelec_bpem_flux_in: + if ind[2] == "H2O": + sf = ( + 1e-3 + * 0.018 + * iscale.get_scaling_factor(self.water_permeability_membrane) + * iscale.get_scaling_factor( + self.acidate.properties_in[ind[0]].pressure_osm_phase[ind[1]] + ) + ) + else: + sf = 1 + iscale.set_scaling_factor(self.nonelec_bpem_flux_in[ind], sf) + for ind in self.nonelec_bpem_flux_out: + if ind[2] == "H2O": + sf = ( + 1e-3 + * 0.018 + * iscale.get_scaling_factor(self.water_permeability_membrane) + * iscale.get_scaling_factor( + self.acidate.properties_out[ind[0]].pressure_osm_phase[ind[1]] + ) + ) + else: + sf = 1 + + iscale.set_scaling_factor(self.nonelec_bpem_flux_out[ind], sf) + + for ind in self.acidate.mass_transfer_term: + if ind[2] == "H_+": + sf = iscale.get_scaling_factor(self.generation_cem_flux_in[ind]) + else: + if ind[2] == "H2O": + sf = iscale.get_scaling_factor(self.nonelec_bpem_flux_in[ind]) + else: + sf = iscale.get_scaling_factor( + self.elec_migration_bpem_flux_in[ind] + ) + + sf *= ( + iscale.get_scaling_factor(self.cell_width) + * iscale.get_scaling_factor(self.shadow_factor) + * iscale.get_scaling_factor(self.cell_length) + * iscale.get_scaling_factor(self.cell_num) + ) + iscale.set_scaling_factor(self.acidate.mass_transfer_term[ind], sf) + # + for ind in self.basate.mass_transfer_term: + if ind[2] == "OH_-": + sf = iscale.get_scaling_factor(self.generation_aem_flux_in[ind]) + else: + if ind[2] == "H2O": + sf = iscale.get_scaling_factor(self.nonelec_bpem_flux_in[ind]) + else: + sf = iscale.get_scaling_factor( + self.elec_migration_bpem_flux_in[ind] + ) + + sf *= ( + iscale.get_scaling_factor(self.cell_width) + * iscale.get_scaling_factor(self.shadow_factor) + * iscale.get_scaling_factor(self.cell_length) + * iscale.get_scaling_factor(self.cell_num) + ) + iscale.set_scaling_factor(self.basate.mass_transfer_term[ind], sf) + + for ind, c in self.specific_power_electrical.items(): + iscale.set_scaling_factor( + self.specific_power_electrical[ind], + 3.6e6 + * iscale.get_scaling_factor(self.current[ind]) + * iscale.get_scaling_factor(self.voltage[ind]) + * iscale.get_scaling_factor( + self.acidate.properties_out[ind].flow_vol_phase["Liq"] + ) + ** -1, + ) + + for ind in self.velocity_basate: + if ( + iscale.get_scaling_factor(self.velocity_basate[ind], warning=False) + is None + ): + sf = ( + iscale.get_scaling_factor( + self.basate.properties_in[ind].flow_vol_phase["Liq"] + ) + * iscale.get_scaling_factor(self.cell_width) ** -1 + * iscale.get_scaling_factor(self.shadow_factor) ** -1 + * iscale.get_scaling_factor(self.channel_height) ** -1 + * iscale.get_scaling_factor(self.spacer_porosity) ** -1 + * iscale.get_scaling_factor(self.cell_num) ** -1 + ) + + iscale.set_scaling_factor(self.velocity_basate[ind], sf) + + for ind in self.velocity_acidate: + if ( + iscale.get_scaling_factor(self.velocity_acidate[ind], warning=False) + is None + ): + sf = ( + iscale.get_scaling_factor( + self.acidate.properties_in[ind].flow_vol_phase["Liq"] + ) + * iscale.get_scaling_factor(self.cell_width) ** -1 + * iscale.get_scaling_factor(self.shadow_factor) ** -1 + * iscale.get_scaling_factor(self.channel_height) ** -1 + * iscale.get_scaling_factor(self.spacer_porosity) ** -1 + * iscale.get_scaling_factor(self.cell_num) ** -1 + ) + + iscale.set_scaling_factor(self.velocity_acidate[ind], sf) + + if hasattr(self, "spacer_specific_area") and ( + iscale.get_scaling_factor(self.spacer_specific_area, warning=True) is None + ): + iscale.set_scaling_factor(self.spacer_specific_area, 1e-4) + if hasattr(self, "hydraulic_diameter") and ( + iscale.get_scaling_factor(self.hydraulic_diameter, warning=True) is None + ): + iscale.set_scaling_factor(self.hydraulic_diameter, 1e4) + if hasattr(self, "N_Re") and ( + iscale.get_scaling_factor(self.N_Re, warning=True) is None + ): + sf = ( + iscale.get_scaling_factor(self.dens_mass) + * iscale.get_scaling_factor(self.velocity_acidate[0]) + * iscale.get_scaling_factor(self.hydraulic_diameter) + * iscale.get_scaling_factor(self.visc_d) ** -1 + ) + iscale.set_scaling_factor(self.N_Re, sf) + if hasattr(self, "N_Sc") and ( + iscale.get_scaling_factor(self.N_Sc, warning=True) is None + ): + sf = ( + iscale.get_scaling_factor(self.visc_d) + * iscale.get_scaling_factor(self.dens_mass) ** -1 + * iscale.get_scaling_factor(self.diffus_mass) ** -1 + ) + iscale.set_scaling_factor(self.N_Sc, sf) + if hasattr(self, "N_Sh") and ( + iscale.get_scaling_factor(self.N_Sh, warning=True) is None + ): + sf = ( + 10 + * iscale.get_scaling_factor(self.N_Re) ** 0.5 + * iscale.get_scaling_factor(self.N_Sc) ** 0.33 + ) + iscale.set_scaling_factor(self.N_Sh, sf) + if hasattr(self, "friction_factor") and ( + iscale.get_scaling_factor(self.friction_factor, warning=True) is None + ): + if self.config.friction_factor_method == FrictionFactorMethod.fixed: + sf = 0.1 + elif self.config.friction_factor_method == FrictionFactorMethod.Gurreri: + sf = ( + (4 * 50.6) ** -1 + * (iscale.get_scaling_factor(self.spacer_porosity)) ** -7.06 + * iscale.get_scaling_factor(self.N_Re) ** -1 + ) + elif self.config.friction_factor_method == FrictionFactorMethod.Kuroda: + sf = (4 * 9.6) ** -1 * iscale.get_scaling_factor(self.N_Re) ** -0.5 + iscale.set_scaling_factor(self.friction_factor, sf) + + if hasattr(self, "pressure_drop") and ( + iscale.get_scaling_factor(self.pressure_drop, warning=True) is None + ): + if self.config.pressure_drop_method == PressureDropMethod.experimental: + sf = 1e-5 + else: + sf = ( + iscale.get_scaling_factor(self.dens_mass) + * iscale.get_scaling_factor(self.friction_factor) + * iscale.get_scaling_factor(self.velocity_acidate[0]) ** 2 + * 2 + * iscale.get_scaling_factor(self.hydraulic_diameter) ** -1 + ) + iscale.set_scaling_factor(self.pressure_drop, sf) + + if hasattr(self, "pressure_drop_total") and ( + iscale.get_scaling_factor(self.pressure_drop_total, warning=True) is None + ): + if self.config.pressure_drop_method == PressureDropMethod.experimental: + sf = 1e-5 * iscale.get_scaling_factor(self.cell_length) + else: + sf = ( + iscale.get_scaling_factor(self.dens_mass) + * iscale.get_scaling_factor(self.friction_factor) + * iscale.get_scaling_factor(self.velocity_acidate[0]) ** 2 + * 2 + * iscale.get_scaling_factor(self.hydraulic_diameter) ** -1 + * iscale.get_scaling_factor(self.cell_length) + ) + iscale.set_scaling_factor(self.pressure_drop_total, sf) + + if hasattr(self, "current_dens_lim_bpem"): + if iscale.get_scaling_factor(self.current_dens_lim_bpem) is None: + if ( + self.config.limiting_current_density_method_bpem + == LimitingCurrentDensitybpemMethod.InitialValue + ): + sf = self.config.limiting_current_density_bpem_data**-1 + iscale.set_scaling_factor(self.current_dens_lim_bpem, sf) + elif ( + self.config.limiting_current_density_method_bpem + == LimitingCurrentDensitybpemMethod.Empirical + ): + sf = ( + iscale.get_scaling_factor(self.diffus_mass) + * value(Constants.faraday_constant) ** -1 + * (1 / 2 * iscale.get_scaling_factor(self.salt_conc_aem)) ** 2 + / ( + iscale.get_scaling_factor(self.membrane_thickness) + * iscale.get_scaling_factor(self.membrane_fixed_charge) + ) + ) + + iscale.set_scaling_factor(self.current_dens_lim_bpem, sf) + + iscale.set_scaling_factor( + self.power_electrical, + iscale.get_scaling_factor(self.current) + * iscale.get_scaling_factor(self.voltage), + ) + + # Constraint scaling + for ind, c in self.eq_current_voltage_relation.items(): + iscale.constraint_scaling_transform( + c, iscale.get_scaling_factor(self.membrane_areal_resistance) + ) + for ind, c in self.eq_power_electrical.items(): + iscale.constraint_scaling_transform( + c, iscale.get_scaling_factor(self.power_electrical) + ) + for ind, c in self.eq_specific_power_electrical.items(): + iscale.constraint_scaling_transform( + c, iscale.get_scaling_factor(self.specific_power_electrical[ind]) + ) + for ind, c in self.eq_elec_migration_bpem_flux_in.items(): + iscale.constraint_scaling_transform( + c, iscale.get_scaling_factor(self.elec_migration_bpem_flux_in) + ) + for ind, c in self.eq_elec_migration_bpem_flux_out.items(): + iscale.constraint_scaling_transform( + c, iscale.get_scaling_factor(self.elec_migration_bpem_flux_in) + ) + + for ind, c in self.eq_nonelec_bpem_flux_in.items(): + iscale.constraint_scaling_transform( + c, iscale.get_scaling_factor(self.nonelec_bpem_flux_in[ind]) + ) + + for ind, c in self.eq_nonelec_bpem_flux_out.items(): + iscale.constraint_scaling_transform( + c, iscale.get_scaling_factor(self.nonelec_bpem_flux_out[ind]) + ) + + for ind, c in self.eq_mass_transfer_term_basate.items(): + iscale.constraint_scaling_transform( + c, + min( + iscale.get_scaling_factor(self.generation_aem_flux_in[ind]), + iscale.get_scaling_factor(self.elec_migration_bpem_flux_in[ind]), + iscale.get_scaling_factor( + self.nonelec_bpem_flux_in[ind], + self.elec_migration_bpem_flux_out[ind], + ), + iscale.get_scaling_factor(self.nonelec_bpem_flux_out[ind]), + ) + * iscale.get_scaling_factor(self.cell_width) + * iscale.get_scaling_factor(self.shadow_factor) + * iscale.get_scaling_factor(self.cell_length) + * iscale.get_scaling_factor(self.cell_num), + ) + for ind, c in self.eq_mass_transfer_term_acidate.items(): + iscale.constraint_scaling_transform( + c, + min( + iscale.get_scaling_factor(self.generation_cem_flux_in[ind]), + iscale.get_scaling_factor(self.elec_migration_bpem_flux_out[ind]), + iscale.get_scaling_factor( + self.nonelec_bpem_flux_in[ind], + self.elec_migration_bpem_flux_out[ind], + ), + iscale.get_scaling_factor(self.nonelec_bpem_flux_out[ind]), + ) + * iscale.get_scaling_factor(self.cell_width) + * iscale.get_scaling_factor(self.shadow_factor) + * iscale.get_scaling_factor(self.cell_length) + * iscale.get_scaling_factor(self.cell_num), + ) + + for ind, c in self.eq_recovery_mass_H2O.items(): + iscale.constraint_scaling_transform( + c, + iscale.get_scaling_factor( + self.acidate.properties_out[ind].flow_mass_phase_comp["Liq", "H2O"] + ), + ) + + for ind, c in self.eq_power_electrical.items(): + iscale.constraint_scaling_transform( + c, + iscale.get_scaling_factor(self.power_electrical[ind]), + ) + + for ind, c in self.eq_specific_power_electrical.items(): + iscale.constraint_scaling_transform( + c, + iscale.get_scaling_factor(self.specific_power_electrical[ind]) + * iscale.get_scaling_factor( + self.acidate.properties_out[ind].flow_vol_phase["Liq"] + ), + ) + # for ind, c in self.eq_current_efficiency.items(): + # iscale.constraint_scaling_transform( + # c, iscale.get_scaling_factor(self.current[ind]) + # ) + + def _get_stream_table_contents(self, time_point=0): + return create_stream_table_dataframe( + { + "base channel of the bipolar membrane Channel Inlet": self.inlet_basate, + "acid channel of the bipolar membrane Channel Inlet": self.inlet_acidate, + "base channel of the bipolar membrane Channel Outlet": self.outlet_basate, + "acid channel of the bipolar membrane Channel Outlet": self.outlet_acidate, + }, + time_point=time_point, + ) + + def _get_performance_contents(self, time_point=0): + return { + "vars": { + "Total electrical power consumption(Watt)": self.power_electrical[ + time_point + ], + "Specific electrical power consumption (kW*h/m**3)": self.specific_power_electrical[ + time_point + ], + # "Current efficiency for deionzation": self.current_efficiency[ + # time_point + # ], + "Water recovery by mass": self.recovery_mass_H2O[time_point], + }, + "exprs": {}, + "params": {}, + } + + def get_power_electrical(self, time_point=0): + return self.power_electrical[time_point] + + @property + def default_costing_method(self): + return cost_bipolar_electrodialysis diff --git a/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py b/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py new file mode 100644 index 0000000000..b19a63ae66 --- /dev/null +++ b/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py @@ -0,0 +1,756 @@ +################################################################################# +# 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/" +################################################################################# +import numpy as np +import idaes.core.util.scaling as iscale +import idaes.logger as idaeslog +import pytest +from idaes.core import ( + FlowsheetBlock, +) +from idaes.core.util.constants import Constants +from idaes.core.util.model_statistics import degrees_of_freedom +from idaes.core.util.testing import initialization_tester +from pyomo.environ import ( + ConcreteModel, + assert_optimal_termination, + value, +) + +from watertap.unit_models.Bipolar_Electrodialysis_0D import ( + Bipolar_Electrodialysis_0D, + LimitingCurrentDensitybpemMethod, + LimitingpotentialMethod, + PressureDropMethod, + FrictionFactorMethod, + HydraulicDiameterMethod, +) +from watertap.core.solvers import get_solver +from watertap.property_models.multicomp_aq_sol_prop_pack import MCASParameterBlock + +# from watertap.unit_models.electrodialysis_0D import LimitingCurrentDensitybpemMethod + +__author__ = "Johnson Dhanasekaran" + +solver = get_solver() + + +# ----------------------------------------------------------------------------- +# Start test class + + +class Test_catalyst: + @pytest.fixture(scope="class") + def bped(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + ion_dict = { + "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], + "mw_data": { + "H2O": 18e-3, + "Na_+": 23e-3, + "Cl_-": 35.5e-3, + "H_+": 1e-3, + "OH_-": 17.0e-3, + }, + "elec_mobility_data": { + ("Liq", "Na_+"): 5.19e-8, + ("Liq", "Cl_-"): 7.92e-8, + ("Liq", "H_+"): 36.23e-8, + ("Liq", "OH_-"): 20.64e-8, + }, + "charge": {"Na_+": 1, "Cl_-": -1, "H_+": 1, "OH_-": -1}, + "diffusivity_data": { + ("Liq", "Na_+"): 1.33e-9, + ("Liq", "Cl_-"): 2.03e-9, + ("Liq", "H_+"): 9.31e-9, + ("Liq", "OH_-"): 5.27e-9, + }, + } + m.fs.properties = MCASParameterBlock(**ion_dict) + m.fs.unit = Bipolar_Electrodialysis_0D( + property_package=m.fs.properties, + has_catalyst=True, + limiting_current_density_method_bpem=LimitingCurrentDensitybpemMethod.Empirical, + ) + + m.fs.unit.diffus_mass.fix((2.03 + 1.96) * 10**-9 / 2) + m.fs.unit.membrane_fixed_charge["bpem"].fix(5e3) + m.fs.unit.salt_conc_aem["bpem"].fix(2000) + m.fs.unit.salt_conc_cem["bpem"].fix(2000) + m.fs.unit.conc_water["bpem"].fix(50 * 1e3) + m.fs.unit.kr["bpem"].fix(1.3 * 10**10) + m.fs.unit.k2_zero["bpem"].fix(2 * 10**-6) + m.fs.unit.relative_permittivity["bpem"].fix(30) + m.fs.unit.membrane_fixed_catalyst_cem["bpem"].fix(5e3) + m.fs.unit.membrane_fixed_catalyst_aem["bpem"].fix(5e3) + m.fs.unit.k_a.fix(447) + m.fs.unit.k_b.fix(5e4) + + m.fs.unit.ion_trans_number_membrane["bpem", "Na_+"].fix(0.5) + m.fs.unit.ion_trans_number_membrane["bpem", "Cl_-"].fix(0.5) + m.fs.unit.ion_trans_number_membrane["bpem", "H_+"].fix(0.1) + m.fs.unit.ion_trans_number_membrane["bpem", "OH_-"].fix(0.1) + + # Set inlet streams. + m.fs.unit.inlet_basate.pressure.fix(101325) + m.fs.unit.inlet_basate.temperature.fix(298.15) + m.fs.unit.inlet_acidate.pressure.fix(101325) + m.fs.unit.inlet_acidate.temperature.fix(298.15) + m.fs.unit.spacer_porosity.fix(1) + + m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-4) + m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-4) + m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-4) + m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-4) + m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-4) + m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-4) + m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-4) + m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-4) + + m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) + m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) + + m.fs.unit.shadow_factor.fix(1) + m.fs.unit.water_trans_number_membrane["bpem"].fix((5.8 + 4.3) / 2) + m.fs.unit.water_permeability_membrane["bpem"].fix((2.16e-14 + 1.75e-14) / 2) + m.fs.unit.electrodes_resistance.fix(0) + m.fs.unit.current_utilization.fix(1) + m.fs.unit.channel_height.fix(2.7e-4) + m.fs.unit.membrane_areal_resistance.fix((1.89e-4 + 1.77e-4) / 2) + m.fs.unit.cell_width.fix(0.1) + m.fs.unit.cell_length.fix(0.79) + m.fs.unit.membrane_thickness["bpem"].fix(8e-4) + + # Set scaling of critical quantities. + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e3, index=("Liq", "Na_+") + ) + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e3, index=("Liq", "Cl_-") + ) + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e3, index=("Liq", "H_+") + ) + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e3, index=("Liq", "OH_-") + ) + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e2, index=("Liq", "H2O") + ) + m.fs.unit.cell_num.fix(1) + iscale.set_scaling_factor(m.fs.unit.k_a, 1e-2) + iscale.set_scaling_factor(m.fs.unit.k_b, 1e-4) + iscale.set_scaling_factor(m.fs.unit.voltage, 1e-1) + iscale.set_scaling_factor(m.fs.unit.flux_splitting, 1e3) + return m + + @pytest.mark.unit + def test_assign(self, bped): + + # Experimental data from Wilhelm et al. (2002) with additional inputs from Mareev et al. (2020) + expt_current_density = np.array( + [11.7, 15.5, 20.8, 24.4, 30.6, 40.0, 100.6] + ) # in mA/cm2 + expt_membrane_potential = np.array( + [0.088, 0.184, 0.4045, 0.690, 0.858, 0.95, 1.3] + ) # in volts + + for indx, v in enumerate(expt_membrane_potential): + m = bped + m.fs.unit.potential_membrane_bpem[0].fix(v) + + iscale.calculate_scaling_factors(m.fs) + assert degrees_of_freedom(m) == 0 + initialization_tester(m, outlvl=idaeslog.DEBUG) + results = solver.solve(m) + assert_optimal_termination(results) + if indx < 2: + rel_tol = 1e0 + else: + rel_tol = 1.5e-1 + + current_density_computed = ( + 0.1 + * m.fs.unit.current[0] + / (m.fs.unit.cell_width * m.fs.unit.cell_length) + ) # convert to mA/cm2 + assert value(current_density_computed) == pytest.approx( + expt_current_density[indx], rel=rel_tol + ) + + +class Test_limiting_parameters: + @pytest.fixture(scope="class") + def limiting_current_check(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + ion_dict = { + "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], + "mw_data": { + "H2O": 18e-3, + "Na_+": 23e-3, + "Cl_-": 35.5e-3, + "H_+": 1e-3, + "OH_-": 17.0e-3, + }, + "elec_mobility_data": { + ("Liq", "Na_+"): 5.19e-8, + ("Liq", "Cl_-"): 7.92e-8, + ("Liq", "H_+"): 36.23e-8, + ("Liq", "OH_-"): 20.64e-8, + }, + "charge": {"Na_+": 1, "Cl_-": -1, "H_+": 1, "OH_-": -1}, + "diffusivity_data": { + ("Liq", "Na_+"): 1.33e-9, + ("Liq", "Cl_-"): 2.03e-9, + ("Liq", "H_+"): 9.31e-9, + ("Liq", "OH_-"): 5.27e-9, + }, + } + m.fs.properties = MCASParameterBlock(**ion_dict) + m.fs.unit = Bipolar_Electrodialysis_0D( + property_package=m.fs.properties, + limiting_current_density_method_bpem=LimitingCurrentDensitybpemMethod.Empirical, + limiting_potential_method_bpem=LimitingpotentialMethod.InitialValue, + limiting_potential_data=0.5, + has_catalyst=False, + ) + return m + + @pytest.fixture(scope="class") + def potential_barrier_check(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + ion_dict = { + "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], + "mw_data": { + "H2O": 18e-3, + "Na_+": 23e-3, + "Cl_-": 35.5e-3, + "H_+": 1e-3, + "OH_-": 17.0e-3, + }, + "elec_mobility_data": { + ("Liq", "Na_+"): 5.19e-8, + ("Liq", "Cl_-"): 7.92e-8, + ("Liq", "H_+"): 36.23e-8, + ("Liq", "OH_-"): 20.64e-8, + }, + "charge": {"Na_+": 1, "Cl_-": -1, "H_+": 1, "OH_-": -1}, + "diffusivity_data": { + ("Liq", "Na_+"): 1.33e-9, + ("Liq", "Cl_-"): 2.03e-9, + ("Liq", "H_+"): 9.31e-9, + ("Liq", "OH_-"): 5.27e-9, + }, + } + m.fs.properties = MCASParameterBlock(**ion_dict) + m.fs.unit = Bipolar_Electrodialysis_0D( + property_package=m.fs.properties, + limiting_current_density_method_bpem=LimitingCurrentDensitybpemMethod.Empirical, + limiting_potential_method_bpem=LimitingpotentialMethod.Empirical, + has_catalyst=False, + ) + + m.fs.unit.kr["bpem"].fix(1.33 * 10**11) + m.fs.unit.k2_zero["bpem"].fix(2 * 10**-5) + m.fs.unit.relative_permittivity["bpem"].fix(20) + + return m + + @pytest.mark.unit + def test_assign(self, limiting_current_check, potential_barrier_check): + check_m = (limiting_current_check, potential_barrier_check) + for m in check_m: + m.fs.unit.shadow_factor.fix(1) + m.fs.unit.current.fix(1e1) + m.fs.unit.water_trans_number_membrane["bpem"].fix((5.8 + 4.3) / 2) + m.fs.unit.water_permeability_membrane["bpem"].fix((2.16e-14 + 1.75e-14) / 2) + m.fs.unit.electrodes_resistance.fix(0) + m.fs.unit.cell_num.fix(1) + m.fs.unit.current_utilization.fix(1) + m.fs.unit.channel_height.fix(2.7e-4) + m.fs.unit.membrane_areal_resistance.fix((1.89e-4 + 1.77e-4) / 2) + m.fs.unit.cell_width.fix(0.1) + m.fs.unit.cell_length.fix(0.79) + m.fs.unit.membrane_thickness["bpem"].fix(1.3e-4) + m.fs.unit.diffus_mass.fix((2.03 + 1.96) * 10**-9 / 2) + m.fs.unit.ion_trans_number_membrane["bpem", "Na_+"].fix(0.5) + m.fs.unit.ion_trans_number_membrane["bpem", "Cl_-"].fix(0.5) + m.fs.unit.ion_trans_number_membrane["bpem", "H_+"].fix(0.1) + m.fs.unit.ion_trans_number_membrane["bpem", "OH_-"].fix(0.1) + m.fs.unit.conc_water["bpem"].fix(55 * 1e3) + m.fs.unit.kr["bpem"].fix(1.33 * 10**11) + m.fs.unit.k2_zero["bpem"].fix(2 * 10**-5) + m.fs.unit.relative_permittivity["bpem"].fix(20) + m.fs.unit.diffus_mass.fix((2.03 + 1.96) * 10**-9 / 2) + m.fs.unit.salt_conc_aem["bpem"].fix(500 + 2 * 250) + m.fs.unit.salt_conc_cem["bpem"].fix(500 + 2 * 250) + m.fs.unit.membrane_fixed_charge["bpem"].fix(1.5e3) + + # Set inlet streams. + m.fs.unit.inlet_basate.pressure.fix(101325) + m.fs.unit.inlet_basate.temperature.fix(298.15) + m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) + m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-1) + m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-1) + m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-1) + m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-1) + m.fs.unit.inlet_acidate.pressure.fix(101325) + m.fs.unit.inlet_acidate.temperature.fix(298.15) + m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) + m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-1) + m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-1) + m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-1) + m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-1) + m.fs.unit.spacer_porosity.fix(1) + + # Set scaling of critical quantities. + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e1, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e0, index=("Liq", "Na_+") + ) + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e0, index=("Liq", "Cl_-") + ) + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e0, index=("Liq", "H_+") + ) + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e0, index=("Liq", "OH_-") + ) + + # Data on limiting current and potential barrier to water splitting have been obtained from: + # Fumatech, Technical Data Sheet for Fumasep FBM, 2020. With additional modelling parameters obtained from + # Ionescu, Viorel. Advanced Topics in Optoelectronics, Microelectronics, and Nanotechnologies (2023) + iscale.calculate_scaling_factors(check_m[1]) + + # Test computing limiting current in bipolar membrane + iscale.calculate_scaling_factors(check_m[0]) + assert degrees_of_freedom(check_m[0]) == 0 + initialization_tester(check_m[0], outlvl=idaeslog.DEBUG) + results = solver.solve(check_m[0]) + assert_optimal_termination(results) + assert value(check_m[0].fs.unit.current_dens_lim_bpem[0]) == pytest.approx( + 1000, rel=1e-1 + ) + + # Test computing limiting current and potential barrier to water splitting in bipolar membrane + iscale.calculate_scaling_factors(check_m[1]) + assert degrees_of_freedom(check_m[1]) == 0 + initialization_tester(check_m[1], outlvl=idaeslog.DEBUG) + results = solver.solve(check_m[1]) + assert_optimal_termination(results) + assert value(check_m[0].fs.unit.current_dens_lim_bpem[0]) == pytest.approx( + 1000, rel=1e-1 + ) + assert value(check_m[1].fs.unit.potential_barrier_bpem[0]) == pytest.approx( + 0.8, rel=1e-1 + ) + + +class Test_BPED_pressure_drop_components: + @pytest.fixture(scope="class") + def bped_m0(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + ion_dict = { + "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], + "mw_data": { + "H2O": 18e-3, + "Na_+": 23e-3, + "Cl_-": 35.5e-3, + "H_+": 1e-3, + "OH_-": 17.0e-3, + }, + "elec_mobility_data": { + ("Liq", "Na_+"): 5.19e-8, + ("Liq", "Cl_-"): 7.92e-8, + ("Liq", "H_+"): 36.23e-8, + ("Liq", "OH_-"): 20.64e-8, + }, + "charge": {"Na_+": 1, "Cl_-": -1, "H_+": 1, "OH_-": -1}, + "diffusivity_data": { + ("Liq", "Na_+"): 1.33e-9, + ("Liq", "Cl_-"): 2.03e-9, + ("Liq", "H_+"): 9.31e-9, + ("Liq", "OH_-"): 5.27e-9, + }, + } + m.fs.properties = MCASParameterBlock(**ion_dict) + m.fs.unit = Bipolar_Electrodialysis_0D( + property_package=m.fs.properties, + has_catalyst=False, + pressure_drop_method=PressureDropMethod.experimental, + has_pressure_change=True, + ) + return m + + @pytest.fixture(scope="class") + def bped_m1(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + ion_dict = { + "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], + "mw_data": { + "H2O": 18e-3, + "Na_+": 23e-3, + "Cl_-": 35.5e-3, + "H_+": 1e-3, + "OH_-": 17.0e-3, + }, + "elec_mobility_data": { + ("Liq", "Na_+"): 5.19e-8, + ("Liq", "Cl_-"): 7.92e-8, + ("Liq", "H_+"): 36.23e-8, + ("Liq", "OH_-"): 20.64e-8, + }, + "charge": {"Na_+": 1, "Cl_-": -1, "H_+": 1, "OH_-": -1}, + "diffusivity_data": { + ("Liq", "Na_+"): 1.33e-9, + ("Liq", "Cl_-"): 2.03e-9, + ("Liq", "H_+"): 9.31e-9, + ("Liq", "OH_-"): 5.27e-9, + }, + } + m.fs.properties = MCASParameterBlock(**ion_dict) + m.fs.unit = Bipolar_Electrodialysis_0D( + property_package=m.fs.properties, + has_catalyst=False, + pressure_drop_method=PressureDropMethod.Darcy_Weisbach, + friction_factor_method=FrictionFactorMethod.fixed, + hydraulic_diameter_method=HydraulicDiameterMethod.conventional, + has_pressure_change=True, + ) + return m + + @pytest.fixture(scope="class") + def bped_m2(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + ion_dict = { + "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], + "mw_data": { + "H2O": 18e-3, + "Na_+": 23e-3, + "Cl_-": 35.5e-3, + "H_+": 1e-3, + "OH_-": 17.0e-3, + }, + "elec_mobility_data": { + ("Liq", "Na_+"): 5.19e-8, + ("Liq", "Cl_-"): 7.92e-8, + ("Liq", "H_+"): 36.23e-8, + ("Liq", "OH_-"): 20.64e-8, + }, + "charge": {"Na_+": 1, "Cl_-": -1, "H_+": 1, "OH_-": -1}, + "diffusivity_data": { + ("Liq", "Na_+"): 1.33e-9, + ("Liq", "Cl_-"): 2.03e-9, + ("Liq", "H_+"): 9.31e-9, + ("Liq", "OH_-"): 5.27e-9, + }, + } + m.fs.properties = MCASParameterBlock(**ion_dict) + m.fs.unit = Bipolar_Electrodialysis_0D( + property_package=m.fs.properties, + pressure_drop_method=PressureDropMethod.Darcy_Weisbach, + friction_factor_method=FrictionFactorMethod.Gurreri, + hydraulic_diameter_method=HydraulicDiameterMethod.conventional, + has_pressure_change=True, + ) + return m + + @pytest.fixture(scope="class") + def bped_m3(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + ion_dict = { + "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], + "mw_data": { + "H2O": 18e-3, + "Na_+": 23e-3, + "Cl_-": 35.5e-3, + "H_+": 1e-3, + "OH_-": 17.0e-3, + }, + "elec_mobility_data": { + ("Liq", "Na_+"): 5.19e-8, + ("Liq", "Cl_-"): 7.92e-8, + ("Liq", "H_+"): 36.23e-8, + ("Liq", "OH_-"): 20.64e-8, + }, + "charge": {"Na_+": 1, "Cl_-": -1, "H_+": 1, "OH_-": -1}, + "diffusivity_data": { + ("Liq", "Na_+"): 1.33e-9, + ("Liq", "Cl_-"): 2.03e-9, + ("Liq", "H_+"): 9.31e-9, + ("Liq", "OH_-"): 5.27e-9, + }, + } + m.fs.properties = MCASParameterBlock(**ion_dict) + m.fs.unit = Bipolar_Electrodialysis_0D( + property_package=m.fs.properties, + pressure_drop_method=PressureDropMethod.Darcy_Weisbach, + friction_factor_method=FrictionFactorMethod.Kuroda, + hydraulic_diameter_method=HydraulicDiameterMethod.conventional, + has_pressure_change=True, + ) + return m + + @pytest.fixture(scope="class") + def bped_m4(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + ion_dict = { + "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], + "mw_data": { + "H2O": 18e-3, + "Na_+": 23e-3, + "Cl_-": 35.5e-3, + "H_+": 1e-3, + "OH_-": 17.0e-3, + }, + "elec_mobility_data": { + ("Liq", "Na_+"): 5.19e-8, + ("Liq", "Cl_-"): 7.92e-8, + ("Liq", "H_+"): 36.23e-8, + ("Liq", "OH_-"): 20.64e-8, + }, + "charge": {"Na_+": 1, "Cl_-": -1, "H_+": 1, "OH_-": -1}, + "diffusivity_data": { + ("Liq", "Na_+"): 1.33e-9, + ("Liq", "Cl_-"): 2.03e-9, + ("Liq", "H_+"): 9.31e-9, + ("Liq", "OH_-"): 5.27e-9, + }, + } + m.fs.properties = MCASParameterBlock(**ion_dict) + m.fs.unit = Bipolar_Electrodialysis_0D( + property_package=m.fs.properties, + pressure_drop_method=PressureDropMethod.Darcy_Weisbach, + friction_factor_method=FrictionFactorMethod.Kuroda, + hydraulic_diameter_method=HydraulicDiameterMethod.fixed, + has_pressure_change=True, + ) + return m + + @pytest.fixture(scope="class") + def bped_m5(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + ion_dict = { + "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], + "mw_data": { + "H2O": 18e-3, + "Na_+": 23e-3, + "Cl_-": 35.5e-3, + "H_+": 1e-3, + "OH_-": 17.0e-3, + }, + "elec_mobility_data": { + ("Liq", "Na_+"): 5.19e-8, + ("Liq", "Cl_-"): 7.92e-8, + ("Liq", "H_+"): 36.23e-8, + ("Liq", "OH_-"): 20.64e-8, + }, + "charge": {"Na_+": 1, "Cl_-": -1, "H_+": 1, "OH_-": -1}, + "diffusivity_data": { + ("Liq", "Na_+"): 1.33e-9, + ("Liq", "Cl_-"): 2.03e-9, + ("Liq", "H_+"): 9.31e-9, + ("Liq", "OH_-"): 5.27e-9, + }, + } + m.fs.properties = MCASParameterBlock(**ion_dict) + m.fs.unit = Bipolar_Electrodialysis_0D( + property_package=m.fs.properties, + pressure_drop_method=PressureDropMethod.Darcy_Weisbach, + friction_factor_method=FrictionFactorMethod.Kuroda, + hydraulic_diameter_method=HydraulicDiameterMethod.spacer_specific_area_known, + has_pressure_change=True, + ) + return m + + @pytest.mark.unit + def test_deltaP_various_methods( + self, bped_m0, bped_m1, bped_m2, bped_m3, bped_m4, bped_m5 + ): + bped_m = (bped_m0, bped_m1, bped_m2, bped_m3, bped_m4, bped_m5) + for m in bped_m: + m.fs.unit.shadow_factor.fix(1) + m.fs.unit.current.fix(1e2) + m.fs.unit.water_trans_number_membrane["bpem"].fix((5.8 + 4.3) / 2) + m.fs.unit.water_permeability_membrane["bpem"].fix((2.16e-14 + 1.75e-14) / 2) + m.fs.unit.electrodes_resistance.fix(0) + m.fs.unit.cell_num.fix(10) + m.fs.unit.current_utilization.fix(1) + m.fs.unit.channel_height.fix(2.7e-4) + m.fs.unit.membrane_areal_resistance.fix((1.89e-4 + 1.77e-4) / 2) + m.fs.unit.cell_width.fix(0.1) + m.fs.unit.cell_length.fix(0.79) + m.fs.unit.membrane_thickness["bpem"].fix(1.3e-4) + m.fs.unit.diffus_mass.fix((2.03 + 1.96) * 10**-9 / 2) + m.fs.unit.ion_trans_number_membrane["bpem", "Na_+"].fix(0.5) + m.fs.unit.ion_trans_number_membrane["bpem", "Cl_-"].fix(0.5) + m.fs.unit.ion_trans_number_membrane["bpem", "H_+"].fix(0.1) + m.fs.unit.ion_trans_number_membrane["bpem", "OH_-"].fix(0.1) + m.fs.unit.conc_water["bpem"].fix(55 * 1e3) + m.fs.unit.kr["bpem"].fix(1.33 * 10**11) + m.fs.unit.k2_zero["bpem"].fix(2 * 10**-5) + m.fs.unit.relative_permittivity["bpem"].fix(20) + m.fs.unit.diffus_mass.fix((2.03 + 1.96) * 10**-9 / 2) + m.fs.unit.salt_conc_aem["bpem"].fix(500 + 2 * 250) + m.fs.unit.salt_conc_cem["bpem"].fix(500 + 2 * 250) + m.fs.unit.membrane_fixed_charge["bpem"].fix(1.5e3) + + # Set inlet streams. + m.fs.unit.inlet_basate.pressure.fix(201035) + m.fs.unit.inlet_basate.temperature.fix(298.15) + m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) + m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-4) + m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-4) + m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-4) + m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-4) + m.fs.unit.inlet_acidate.pressure.fix(201035) + m.fs.unit.inlet_acidate.temperature.fix(298.15) + m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) + m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-4) + m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-4) + m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-4) + m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-4) + m.fs.unit.spacer_porosity.fix(0.83) + + # Set scaling of critical quantities. + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e1, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e0, index=("Liq", "Na_+") + ) + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e0, index=("Liq", "Cl_-") + ) + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e0, index=("Liq", "H_+") + ) + m.fs.properties.set_default_scaling( + "flow_mol_phase_comp", 1e0, index=("Liq", "OH_-") + ) + + iscale.set_scaling_factor(m.fs.unit.cell_length, 1) + iscale.set_scaling_factor(m.fs.unit.cell_num, 0.1) + iscale.set_scaling_factor(m.fs.unit.voltage, 1e-1) + + # Test bped_m0 + bped_m[0].fs.unit.pressure_drop.fix(1e4) + iscale.calculate_scaling_factors(bped_m[0]) + assert degrees_of_freedom(bped_m[0]) == 0 + initialization_tester(bped_m[0], outlvl=idaeslog.DEBUG) + results = solver.solve(bped_m[0]) + assert_optimal_termination(results) + assert value(bped_m[0].fs.unit.pressure_drop_total[0]) == pytest.approx( + 7900, rel=1e-3 + ) + + # Test bped_m1 + bped_m[1].fs.unit.current.fix(1e2) + iscale.set_scaling_factor(bped_m[1].fs.unit.current, 1e-2) + bped_m[1].fs.unit.diffus_mass.fix(1.6e-9) + bped_m[1].fs.unit.friction_factor.fix(20) + iscale.calculate_scaling_factors(bped_m[1]) + assert degrees_of_freedom(bped_m[1]) == 0 + initialization_tester(bped_m[1], outlvl=idaeslog.DEBUG) + results = solver.solve(bped_m[1]) + assert_optimal_termination(results) + assert value(bped_m[1].fs.unit.N_Re) == pytest.approx(9.491, rel=1e-3) + + assert value(bped_m[1].fs.unit.pressure_drop[0]) == pytest.approx( + 10087.548, rel=1e-3 + ) + + assert value(bped_m[1].fs.unit.pressure_drop_total[0]) == pytest.approx( + 7969.163, rel=1e-3 + ) + + # Test bped_m2 + bped_m[2].fs.unit.diffus_mass.fix(1.6e-9) + iscale.calculate_scaling_factors(bped_m[2]) + assert degrees_of_freedom(bped_m[2]) == 0 + initialization_tester(bped_m[2], outlvl=idaeslog.DEBUG) + results = solver.solve(bped_m[2]) + assert_optimal_termination(results) + assert value(bped_m[2].fs.unit.N_Re) == pytest.approx(9.491, rel=1e-3) + + assert value(bped_m[2].fs.unit.pressure_drop[0]) == pytest.approx( + 40080.279, rel=1e-3 + ) + + assert value(bped_m[2].fs.unit.pressure_drop_total[0]) == pytest.approx( + 31663.421, rel=1e-3 + ) + + # Test bped_m3 + bped_m[3].fs.unit.diffus_mass.fix(1.6e-9) + iscale.calculate_scaling_factors(bped_m[3]) + assert degrees_of_freedom(bped_m[3]) == 0 + initialization_tester(bped_m[3], outlvl=idaeslog.DEBUG) + results = solver.solve(bped_m[3]) + assert_optimal_termination(results) + assert value(bped_m[3].fs.unit.N_Re) == pytest.approx(9.491, rel=1e-3) + + assert value(bped_m[3].fs.unit.pressure_drop[0]) == pytest.approx( + 7574.199, rel=1e-3 + ) + + assert value(bped_m[3].fs.unit.pressure_drop_total[0]) == pytest.approx( + 5983.618, rel=1e-3 + ) + + # Test bped_m4 + bped_m[4].fs.unit.diffus_mass.fix(1.6e-9) + bped_m[4].fs.unit.hydraulic_diameter.fix(1e-3) + iscale.calculate_scaling_factors(bped_m[4]) + assert degrees_of_freedom(bped_m[4]) == 0 + initialization_tester(bped_m[4], outlvl=idaeslog.DEBUG) + results = solver.solve(bped_m[4]) + assert_optimal_termination(results) + assert value(bped_m[4].fs.unit.N_Re) == pytest.approx(21.235, rel=1e-3) + + assert value(bped_m[4].fs.unit.pressure_drop[0]) == pytest.approx( + 2263.540, rel=1e-3 + ) + + assert value(bped_m[4].fs.unit.pressure_drop_total[0]) == pytest.approx( + 1788.196, rel=1e-3 + ) + + # Test bped_m5 + bped_m[5].fs.unit.diffus_mass.fix(1.6e-9) + bped_m[5].fs.unit.spacer_specific_area.fix(10700) + iscale.calculate_scaling_factors(bped_m[5]) + assert degrees_of_freedom(bped_m[5]) == 0 + initialization_tester(bped_m[5], outlvl=idaeslog.DEBUG) + iscale.calculate_scaling_factors(bped_m[5]) + results = solver.solve(bped_m[5]) + assert_optimal_termination(results) + assert value(bped_m[5].fs.unit.N_Re) == pytest.approx(7.641, rel=1e-3) + + assert value(bped_m[5].fs.unit.pressure_drop[0]) == pytest.approx( + 10486.482, rel=1e-3 + ) + + assert value(bped_m[5].fs.unit.pressure_drop_total[0]) == pytest.approx( + 8284.321, rel=1e-3 + ) From 1b65114bd78ca58f953e756496721217baf8b23f Mon Sep 17 00:00:00 2001 From: jjd Date: Tue, 1 Oct 2024 23:32:22 -0400 Subject: [PATCH 02/17] Minor corrections based on autocheck --- .../Biploar_electrodialysis_0D.rst | 6 ++--- .../tests/test_biploar_electrodialysis_0D.py | 22 +++++++++---------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst b/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst index fad18875b3..c5db397c50 100644 --- a/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst +++ b/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst @@ -112,14 +112,12 @@ splitting occurs and the bipolar membrane acts like a simple electrodialysis mem **Note** :sup:`1` DOF number takes account of the indices of the corresponding parameter. - :sup:`2` A user should provide either current or voltage as the electrical input, in correspondence to the -"Constant_Current" or "Constant_Voltage" treatment mode + :sup:`2` A user should provide either current or voltage as the electrical input, in correspondence to the "Constant_Current" or "Constant_Voltage" treatment mode Solution component information ------------------------------ -To fully construct solution properties, users need to provide basic component information of the feed solution to use -this model. Below is a sample: +To fully construct solution properties, users need to provide basic component information of the feed solution to use this model. Below is a sample: .. code-block:: diff --git a/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py b/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py index b19a63ae66..da86c8b912 100644 --- a/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py +++ b/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py @@ -11,12 +11,11 @@ ################################################################################# import numpy as np import idaes.core.util.scaling as iscale -import idaes.logger as idaeslog import pytest from idaes.core import ( FlowsheetBlock, ) -from idaes.core.util.constants import Constants + from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.util.testing import initialization_tester from pyomo.environ import ( @@ -36,7 +35,6 @@ from watertap.core.solvers import get_solver from watertap.property_models.multicomp_aq_sol_prop_pack import MCASParameterBlock -# from watertap.unit_models.electrodialysis_0D import LimitingCurrentDensitybpemMethod __author__ = "Johnson Dhanasekaran" @@ -170,7 +168,7 @@ def test_assign(self, bped): iscale.calculate_scaling_factors(m.fs) assert degrees_of_freedom(m) == 0 - initialization_tester(m, outlvl=idaeslog.DEBUG) + initialization_tester(m) results = solver.solve(m) assert_optimal_termination(results) if indx < 2: @@ -339,7 +337,7 @@ def test_assign(self, limiting_current_check, potential_barrier_check): # Test computing limiting current in bipolar membrane iscale.calculate_scaling_factors(check_m[0]) assert degrees_of_freedom(check_m[0]) == 0 - initialization_tester(check_m[0], outlvl=idaeslog.DEBUG) + initialization_tester(check_m[0]) results = solver.solve(check_m[0]) assert_optimal_termination(results) assert value(check_m[0].fs.unit.current_dens_lim_bpem[0]) == pytest.approx( @@ -349,7 +347,7 @@ def test_assign(self, limiting_current_check, potential_barrier_check): # Test computing limiting current and potential barrier to water splitting in bipolar membrane iscale.calculate_scaling_factors(check_m[1]) assert degrees_of_freedom(check_m[1]) == 0 - initialization_tester(check_m[1], outlvl=idaeslog.DEBUG) + initialization_tester(check_m[1]) results = solver.solve(check_m[1]) assert_optimal_termination(results) assert value(check_m[0].fs.unit.current_dens_lim_bpem[0]) == pytest.approx( @@ -657,7 +655,7 @@ def test_deltaP_various_methods( bped_m[0].fs.unit.pressure_drop.fix(1e4) iscale.calculate_scaling_factors(bped_m[0]) assert degrees_of_freedom(bped_m[0]) == 0 - initialization_tester(bped_m[0], outlvl=idaeslog.DEBUG) + initialization_tester(bped_m[0]) results = solver.solve(bped_m[0]) assert_optimal_termination(results) assert value(bped_m[0].fs.unit.pressure_drop_total[0]) == pytest.approx( @@ -671,7 +669,7 @@ def test_deltaP_various_methods( bped_m[1].fs.unit.friction_factor.fix(20) iscale.calculate_scaling_factors(bped_m[1]) assert degrees_of_freedom(bped_m[1]) == 0 - initialization_tester(bped_m[1], outlvl=idaeslog.DEBUG) + initialization_tester(bped_m[1]) results = solver.solve(bped_m[1]) assert_optimal_termination(results) assert value(bped_m[1].fs.unit.N_Re) == pytest.approx(9.491, rel=1e-3) @@ -688,7 +686,7 @@ def test_deltaP_various_methods( bped_m[2].fs.unit.diffus_mass.fix(1.6e-9) iscale.calculate_scaling_factors(bped_m[2]) assert degrees_of_freedom(bped_m[2]) == 0 - initialization_tester(bped_m[2], outlvl=idaeslog.DEBUG) + initialization_tester(bped_m[2]) results = solver.solve(bped_m[2]) assert_optimal_termination(results) assert value(bped_m[2].fs.unit.N_Re) == pytest.approx(9.491, rel=1e-3) @@ -705,7 +703,7 @@ def test_deltaP_various_methods( bped_m[3].fs.unit.diffus_mass.fix(1.6e-9) iscale.calculate_scaling_factors(bped_m[3]) assert degrees_of_freedom(bped_m[3]) == 0 - initialization_tester(bped_m[3], outlvl=idaeslog.DEBUG) + initialization_tester(bped_m[3]) results = solver.solve(bped_m[3]) assert_optimal_termination(results) assert value(bped_m[3].fs.unit.N_Re) == pytest.approx(9.491, rel=1e-3) @@ -723,7 +721,7 @@ def test_deltaP_various_methods( bped_m[4].fs.unit.hydraulic_diameter.fix(1e-3) iscale.calculate_scaling_factors(bped_m[4]) assert degrees_of_freedom(bped_m[4]) == 0 - initialization_tester(bped_m[4], outlvl=idaeslog.DEBUG) + initialization_tester(bped_m[4]) results = solver.solve(bped_m[4]) assert_optimal_termination(results) assert value(bped_m[4].fs.unit.N_Re) == pytest.approx(21.235, rel=1e-3) @@ -741,7 +739,7 @@ def test_deltaP_various_methods( bped_m[5].fs.unit.spacer_specific_area.fix(10700) iscale.calculate_scaling_factors(bped_m[5]) assert degrees_of_freedom(bped_m[5]) == 0 - initialization_tester(bped_m[5], outlvl=idaeslog.DEBUG) + initialization_tester(bped_m[5]) iscale.calculate_scaling_factors(bped_m[5]) results = solver.solve(bped_m[5]) assert_optimal_termination(results) From 3e85b9fd5f03f203641de54287ba79f6209e8285 Mon Sep 17 00:00:00 2001 From: jjd Date: Tue, 1 Oct 2024 23:42:37 -0400 Subject: [PATCH 03/17] Minor corrections based on autocheck v1.1 --- .../unit_models/Biploar_electrodialysis_0D.rst | 3 +-- watertap/unit_models/Bipolar_Electrodialysis_0D.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst b/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst index c5db397c50..511643fd74 100644 --- a/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst +++ b/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst @@ -267,8 +267,7 @@ paper is by Wright et. al., 2018 (*References*). **Note** - :sup:`1` We assumed a constant linear velocity (in the cell length direction), :math:`v`, in both channels and along -the flow path. This :math:`v` is calculated based on the average of inlet and outlet volumetric flow rate. + :sup:`1` We assumed a constant linear velocity (in the cell length direction), :math:`v`, in both channels and along the flow path. This :math:`v` is calculated based on the average of inlet and outlet volumetric flow rate. Nomenclature ------------ diff --git a/watertap/unit_models/Bipolar_Electrodialysis_0D.py b/watertap/unit_models/Bipolar_Electrodialysis_0D.py index 0390d0bfc2..ec025e8e64 100644 --- a/watertap/unit_models/Bipolar_Electrodialysis_0D.py +++ b/watertap/unit_models/Bipolar_Electrodialysis_0D.py @@ -20,7 +20,6 @@ Suffix, NonNegativeReals, value, - log, Constraint, sqrt, units as pyunits, From 1e3471d9ca2600a2b52641b42441a86d0e3504d8 Mon Sep 17 00:00:00 2001 From: jjd Date: Wed, 2 Oct 2024 00:14:15 -0400 Subject: [PATCH 04/17] Minor corrections based on autocheck v1.2 --- .../unit_models/Biploar_electrodialysis_0D.rst | 7 ++++++- docs/technical_reference/unit_models/index.rst | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst b/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst index 511643fd74..e12e4918e7 100644 --- a/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst +++ b/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst @@ -1,5 +1,5 @@ Bipolar Electrodialysis (0D) -==================== +============================ Introduction ------------ @@ -314,6 +314,11 @@ Nomenclature ":math:`acidate`", "Cation exchange side of bipolar membrane", ":math:`basate`", "Anion exchange side of bipolar membrane", +Class Documentation +------------------- + +* :mod:`watertap.unit_models.Bipolar_Electrodialysis_0D` + References ---------- Strathmann, H. (2010). Electrodialysis, a mature technology with a multitude of new applications. diff --git a/docs/technical_reference/unit_models/index.rst b/docs/technical_reference/unit_models/index.rst index 2015682160..2f80631b09 100644 --- a/docs/technical_reference/unit_models/index.rst +++ b/docs/technical_reference/unit_models/index.rst @@ -6,6 +6,7 @@ Unit Models anaerobic_digester aeration_tank + Biploar_electrodialysis_0D boron_removal clarifier coag_floc_model From ecb5db15341ff1134592de8480bc1be6ae6e83e2 Mon Sep 17 00:00:00 2001 From: jjd Date: Wed, 2 Oct 2024 13:37:37 -0400 Subject: [PATCH 05/17] Minor corrections based on autocheck v1.3 --- .../unit_models/Biploar_electrodialysis_0D.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst b/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst index e12e4918e7..52f0bcb8db 100644 --- a/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst +++ b/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst @@ -99,7 +99,7 @@ splitting occurs and the bipolar membrane acts like a simple electrodialysis mem "Water permeability", ":math:`L`", "water_permeability_membrane", "['bpem']", ":math:`m^{-1}s^{-1}Pa^{-1}`", 1 "Voltage or Current \ :sup:`2`", ":math:`U` or :math:`I`", "voltage or current", "[t]", ":math:`\text{V}` or :math:`A`", 1 "Electrode areal resistance", ":math:`r_{el}`", "electrodes_resistance", "[t]", ":math:`\Omega m^2`", 1 - "Cell pair number", ":math:`n`", "cell_pair_num", "None", "dimensionless", 1 + "Cell number", ":math:`n`", "cell_num", "None", "dimensionless", 1 "Current utilization coefficient", ":math:`\xi`", "current_utilization", "None", "dimensionless", 1 "Shadow factor", ":math:`\xi`", "shadow_factor", "None", "dimensionless", 1 "Spacer thickness", ":math:`s`", "spacer_thickness", "none", ":math:`m` ", 1 @@ -287,7 +287,7 @@ Nomenclature ":math:`I`", "Current", ":math:`A`" ":math:`i`", "Current density", ":math:`A m^{-2}`" ":math:`U`", "Voltage over a stack", ":math:`V`" - ":math:`n`", "Cell pair number", "dimensionless" + ":math:`n`", "Cell number", "dimensionless" ":math:`\xi`", "Current utilization coefficient (including ion diffusion and water electroosmosis)", "dimensionless" ":math:`z`", "Ion charge", "dimensionless" ":math:`F`", "Faraday constant", ":math:`C\ mol^{-1}`" From ff6a753fe9a3b8fc92b4239577d4713aed609ba5 Mon Sep 17 00:00:00 2001 From: jjd Date: Wed, 2 Oct 2024 21:24:49 -0400 Subject: [PATCH 06/17] Minor corrections based on autocheck v1.4 --- .../unit_models/Bipolar_Electrodialysis_0D.py | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/watertap/unit_models/Bipolar_Electrodialysis_0D.py b/watertap/unit_models/Bipolar_Electrodialysis_0D.py index ec025e8e64..475867b5fd 100644 --- a/watertap/unit_models/Bipolar_Electrodialysis_0D.py +++ b/watertap/unit_models/Bipolar_Electrodialysis_0D.py @@ -194,15 +194,6 @@ class BipolarElectrodialysis0DData(InitializationMixin, UnitModelBlockData): ), ) - CONFIG.declare( - "operation_mode", - ConfigValue( - default=ElectricalOperationMode.Constant_Current, - domain=In(ElectricalOperationMode), - description="The electrical operation mode. To be selected between Constant Current and Constant Voltage", - ), - ) - CONFIG.declare( "limiting_current_density_method_bpem", ConfigValue( @@ -256,20 +247,6 @@ class BipolarElectrodialysis0DData(InitializationMixin, UnitModelBlockData): description="Limiting current density data input for bipolar membrane", ), ) - CONFIG.declare( - "salt_input_cem", - ConfigValue( - default=100, - description="Specified salt concentration on acid (C.E.M side) channel of the bipolar membrane", - ), - ) - CONFIG.declare( - "salt_input_aem", - ConfigValue( - default=100, - description="Specified salt concentration on base (A.E.M side) channel of the bipolar membrane", - ), - ) CONFIG.declare( "limiting_potential_data", From a83344276e13e773f327bc59f25beb687184eb9f Mon Sep 17 00:00:00 2001 From: jjd Date: Thu, 10 Oct 2024 12:46:48 -0400 Subject: [PATCH 07/17] Name change: Acidate/Basate to Acidic/Basic --- .../Biploar_electrodialysis_0D.rst | 72 +++--- .../unit_models/bipolar_electrodialysis.py | 8 +- .../unit_models/Bipolar_Electrodialysis_0D.py | 214 +++++++++--------- .../tests/test_biploar_electrodialysis_0D.py | 84 +++---- 4 files changed, 189 insertions(+), 189 deletions(-) diff --git a/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst b/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst index 52f0bcb8db..0b0cd97f23 100644 --- a/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst +++ b/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst @@ -6,7 +6,7 @@ Introduction Bipolar electrodialysis, an electrochemical separation technology, has primarily been used to generate acids and bases from waste salts produced in water purification. By providing in-situ access to valuable raw materials bipolar membranes can -significantly reduce the total cost of the operation. This cell stack is shown in Figure 1 with **basate** and **acidate** channels, that produce base and acid +significantly reduce the total cost of the operation. This cell stack is shown in Figure 1 with **basic** and **acidic** channels, that produce base and acid respectively. More overview of the bipolar electrodialysis technology can be found in the *References*. @@ -17,13 +17,13 @@ electrodialysis technology can be found in the *References*. Figure 1. Schematic representation of a bipolar electrodialysis unit -One bipolar membrane along with the **Acidate** and **Basate** channels can thus be treated as a modelling unit that can +One bipolar membrane along with the **acidic** and **basic** channels can thus be treated as a modelling unit that can multiply to larger-scale systems. The presented bipolar electrodialysis model establishes mathematical descriptions of ion and water transport across the membrane along with water splitting. Modelled transfer mechanisms include electrical migration, diffusion of ions, osmosis, electroosmosis, and water splitting. The following are the key assumptions made: -* The **Acidate** and **Basate** side channels have identical geometry. +* The **acidic** and **basic** side channels have identical geometry. * For each channel, component fluxes are uniform in the bulk solutions (the 0-dimensional assumption) and are set as the average of inlet and outlet of each channel. * Steady state: all variables are independent on time. * Co-current flow operation. @@ -35,20 +35,20 @@ assumptions made: Control Volumes --------------- -This model has two control volumes for the acidate and basate channels. +This model has two control volumes for the acidic and basic channels. -* **Acidate** channel -* **Basate** side channel +* **acidic** channel +* **basic** side channel Ports ----- On the two control volumes, this model provides four ports (Pyomo notation in parenthesis): -* inlet_acidate (inlet) -* outlet_acidate (outlet) -* inlet_basate (inlet) -* outlet_basate (outlet) +* inlet_acidic (inlet) +* outlet_acidic (outlet) +* inlet_basic (inlet) +* outlet_basic (outlet) Sets ---- @@ -89,12 +89,12 @@ splitting occurs and the bipolar membrane acts like a simple electrodialysis mem .. csv-table:: **Table 2.** List of Degree of Freedom (DOF) :header: "Description", "Symbol", "Variable Name", "Index", "Units", "DOF Number \ :sup:`1`" - "Temperature, inlet_acidate", ":math:`T^acidate`", "temperature", "None", ":math:`K`", 1 - "Temperature, inlet_basate", ":math:`T^basate`", "temperature", "None", ":math:`K`", 1 - "Pressure, inlet_acidate",":math:`p^acidate`", "temperature", "None", ":math:`Pa`", 1 - "Pressure, inlet_basate",":math:`p^basate`", "temperature", "None", ":math:`Pa`", 1 - "Component molar flow rate, inlet_acidate", ":math:`N_{j,in}^{acidate}`", "flow_mol_phase_comp", "[t], ['Liq'], ['H\ :sub:`2`\O', 'Na\ :sup:`+`', '\Cl\ :sup:`-`', 'H\ :sup:`+`', 'OH\ :sup:`-`']", ":math:`mol \, s^{-1}`", 5 - "Component molar flow rate, inlet_basate", ":math:`N_{j, in}^{basate}`", "flow_mol_phase_comp", "[t], ['Liq'], ['H\ :sub:`2`\O', 'Na\ :sup:`+`', '\Cl\ :sup:`-`', 'H\ :sup:`+`', 'OH\ :sup:`-`']", ":math:`mol \, s^{-1}`", 5 + "Temperature, inlet_acidic", ":math:`T^acidic`", "temperature", "None", ":math:`K`", 1 + "Temperature, inlet_basic", ":math:`T^basic`", "temperature", "None", ":math:`K`", 1 + "Pressure, inlet_acidic",":math:`p^acidic`", "temperature", "None", ":math:`Pa`", 1 + "Pressure, inlet_basic",":math:`p^basic`", "temperature", "None", ":math:`Pa`", 1 + "Component molar flow rate, inlet_acidic", ":math:`N_{j,in}^{acidic}`", "flow_mol_phase_comp", "[t], ['Liq'], ['H\ :sub:`2`\O', 'Na\ :sup:`+`', '\Cl\ :sup:`-`', 'H\ :sup:`+`', 'OH\ :sup:`-`']", ":math:`mol \, s^{-1}`", 5 + "Component molar flow rate, inlet_basic", ":math:`N_{j, in}^{basic}`", "flow_mol_phase_comp", "[t], ['Liq'], ['H\ :sub:`2`\O', 'Na\ :sup:`+`', '\Cl\ :sup:`-`', 'H\ :sup:`+`', 'OH\ :sup:`-`']", ":math:`mol \, s^{-1}`", 5 "Water transport number", ":math:`t_w`", "water_trans_number_membrane", "['bpem']", "dimensionless", 1 "Water permeability", ":math:`L`", "water_permeability_membrane", "['bpem']", ":math:`m^{-1}s^{-1}Pa^{-1}`", 1 "Voltage or Current \ :sup:`2`", ":math:`U` or :math:`I`", "voltage or current", "[t]", ":math:`\text{V}` or :math:`A`", 1 @@ -103,7 +103,7 @@ splitting occurs and the bipolar membrane acts like a simple electrodialysis mem "Current utilization coefficient", ":math:`\xi`", "current_utilization", "None", "dimensionless", 1 "Shadow factor", ":math:`\xi`", "shadow_factor", "None", "dimensionless", 1 "Spacer thickness", ":math:`s`", "spacer_thickness", "none", ":math:`m` ", 1 - "Membrane areal resistance", ":math:`r`", "membrane_surface_resistance", "['acidate', 'basate']", ":math:`\Omega m^2`", 2 + "Membrane areal resistance", ":math:`r`", "membrane_surface_resistance", "['acidic', 'basic']", ":math:`\Omega m^2`", 2 "Cell width", ":math:`b`", "cell_width", "None", ":math:`\text{m}`", 1 "Cell length", ":math:`l`", "cell_length", "None", ":math:`\text{m}`", 1 "Thickness of ion exchange membranes", ":math:`\delta`", "membrane_thickness", "['bpem']", ":math:`m`", 1 @@ -158,14 +158,14 @@ The Mass balance equations are summarized in **Table3**. Further details on thes .. csv-table:: **Table 3** Mass Balance Equations :header: "Description", "Equation", "Index set" - "Component mass balance", ":math:`N_{j, in}^{acidate \: or\: basate}-N_{j, out}^{acidate\: or\: basate}+J_j^{acidate\: or\: basate} bl=0`", ":math:`j \in \left['H_2 O', '{Na^+} ', '{Cl^-} '\right]`" - "mass transfer flux, basate, solute", ":math:`J_j^{C} = -t_j^{bpem}\frac{\xi i_{lim}}{ z_j F}`", ":math:`j \in \left['{Na_+} ', '{Cl^-} '\right]`" - "mass transfer flux, acidate, Water ions", ":math:`J_j^{C} = \frac{i - i_{lim}}{ z_j F}`", ":math:`j \in \left['{H^+} '\right]`" - "mass transfer flux, acidate, Water ions", ":math:`J_j^{C} = 0`", ":math:`j \in \left['{OH^-} '\right]`" - "mass transfer flux, basate, Water ions", ":math:`J_j^{C} = 0`", ":math:`j \in \left['{H^+} '\right]`" - "mass transfer flux, basate, Water ions", ":math:`J_j^{C} = -\frac{i - i_{lim}}{ z_j F}`", ":math:`j \in \left['{OH^-} '\right]`" - "mass transfer flux, acidate H\ :sub:`2`\ O", ":math:`J_j^{C} = t_w^{bpem} \left(\frac{i}{F}\right)+\left(L^{bpem} \right)\left(p_{osm}^CEM-p_{osm}^AEM \right)\left(\frac{\rho_w}{M_w}\right)`", ":math:`j \in \left['H_2 O'\right]`" - "mass transfer flux, basate, H\ :sub:`2`\ O", ":math:`J_j^{C} = -t_w^{bpem} \left(\frac{i}{F}\right)-\left(L^{bpem} \right)\left(p_{osm}^CEM-p_{osm}^AEM \right)\left(\frac{\rho_w}{M_w}\right)`", ":math:`j \in \left['H_2 O'\right]`" + "Component mass balance", ":math:`N_{j, in}^{acidic \: or\: basic}-N_{j, out}^{acidic\: or\: basic}+J_j^{acidic\: or\: basic} bl=0`", ":math:`j \in \left['H_2 O', '{Na^+} ', '{Cl^-} '\right]`" + "mass transfer flux, basic, solute", ":math:`J_j^{C} = -t_j^{bpem}\frac{\xi i_{lim}}{ z_j F}`", ":math:`j \in \left['{Na_+} ', '{Cl^-} '\right]`" + "mass transfer flux, acidic, Water ions", ":math:`J_j^{C} = \frac{i - i_{lim}}{ z_j F}`", ":math:`j \in \left['{H^+} '\right]`" + "mass transfer flux, acidic, Water ions", ":math:`J_j^{C} = 0`", ":math:`j \in \left['{OH^-} '\right]`" + "mass transfer flux, basic, Water ions", ":math:`J_j^{C} = 0`", ":math:`j \in \left['{H^+} '\right]`" + "mass transfer flux, basic, Water ions", ":math:`J_j^{C} = -\frac{i - i_{lim}}{ z_j F}`", ":math:`j \in \left['{OH^-} '\right]`" + "mass transfer flux, acidic H\ :sub:`2`\ O", ":math:`J_j^{C} = t_w^{bpem} \left(\frac{i}{F}\right)+\left(L^{bpem} \right)\left(p_{osm}^CEM-p_{osm}^AEM \right)\left(\frac{\rho_w}{M_w}\right)`", ":math:`j \in \left['H_2 O'\right]`" + "mass transfer flux, basic, H\ :sub:`2`\ O", ":math:`J_j^{C} = -t_w^{bpem} \left(\frac{i}{F}\right)-\left(L^{bpem} \right)\left(p_{osm}^CEM-p_{osm}^AEM \right)\left(\frac{\rho_w}{M_w}\right)`", ":math:`j \in \left['H_2 O'\right]`" Overcoming the limiting current corresponds to a potential barrier (:math:`U_{diss}`) gives the relationship :math:`U = i r_{tot}+ U_{diss}`. @@ -176,14 +176,14 @@ Both the current durrent density and potential barrier must be specified, via respectively, in the water splitting mode of operation. They can either be user inputs ``InitialValue``, with ``limiting_current_density_data`` in :math:`A/m^2` and ``limiting_potential_data`` in volts. There is also an option to have these critical quantities computed. For this ``Empirical`` is chosen. -The limiting current is computed as :math:`i_{lim} = D F (C_{acidate}+C_{basate})^2 / (\sigma \delta)`. The potential barrier +The limiting current is computed as :math:`i_{lim} = D F (C_{acidic}+C_{basic})^2 / (\sigma \delta)`. The potential barrier calculation involves kinetics of water splitting. The rate of proton/hydroxide ion formation per unit volume is given as :math:`R_{H^+/OH^-} = [k_2(0)f(E)C_{H_2O}-k_r C_{H^+}C_{OH^-} ]`. A majority of the production occurs within the small depletion region :math:`\lambda`, thus the flux is :math:`R_{H^+/OH^-} /\lambda`. When this flux is :math:`0.1 i_{lim}` the barrier is assumed to be crossed, and the corresponding :math:`E=E_{crit}=U_{diss} \lambda` determines the potential barrier. The quantities :math:`C_{H_2 O}, C_{H^+}, C_{OH^-}` are the water proton and hydroxyl concentration in -:math:`mol\, m^{-3}` and are taken to be constants. :math:`f(E)` is the second Wien effect driven enhanacidateent of the +:math:`mol\, m^{-3}` and are taken to be constants. :math:`f(E)` is the second Wien effect driven enhanacidicent of the dissociation rate under applied electric field. It requires as input temperature and relative permittivity (:math:`\epsilon_r`). To close the model :math:`\lambda = E_{crit} \epsilon_0 \epsilon_r / (F \sigma)` @@ -193,8 +193,8 @@ To close the model :math:`\lambda = E_{crit} \epsilon_0 \epsilon_r / (F \sigma)` :header: "Description", "Symbol", "Variable Name", "Index", "Units" "Diffusivity", ":math:`D`", "diffus_mass", "[bpem]", ":math:`m^2 s^{-1}`" - "Salt concentration, basate side ", ":math:`C_{basate}`", "salt_conc_basate", "[bpem]",":math:`mol m^{-3}`" - "Salt concentration, acidate side ", ":math:`C_{acidate}`", "salt_conc_acidate", "[bpem]",":math:`mol m^{-3}`" + "Salt concentration, basic side ", ":math:`C_{basic}`", "salt_conc_basic", "[bpem]",":math:`mol m^{-3}`" + "Salt concentration, acidic side ", ":math:`C_{acidic}`", "salt_conc_acidic", "[bpem]",":math:`mol m^{-3}`" "Membrane Fixed charge ", ":math:`\sigma`", "membrane_fixed_charge", "[bpem]",":math:`mol m^{-3}`" "Dissociation rate constant, zero electric field ", ":math:`k_2(0)`", "kd_zero", "[bpem]",":math:`s^{-1}`" "Recombination rate constant ", ":math:`k_r`", "k_r", "[bpem]",":math:`L^1 mol^{-1} s^{-1}`" @@ -205,10 +205,10 @@ To close the model :math:`\lambda = E_{crit} \epsilon_0 \epsilon_r / (F \sigma)` "Current density", ":math:`i = \frac{I}{bl}`" "Ohm's Law", ":math:`U = i r_{tot}`" - "Resistance calculation", ":math:`r_{tot}=n\left(r^{acidate}+r^{basate}\right)+r_{el}`" + "Resistance calculation", ":math:`r_{tot}=n\left(r^{acidic}+r^{basic}\right)+r_{el}`" "Electrical power consumption", ":math:`P=UI`" "Water-production-specific power consumption", ":math:`P_Q=\frac{UI}{3.6\times 10^6 nQ_{out}^D}`" - "Overall current efficiency", ":math:`I\eta=\sum_{j \in[cation]}{\left[\left(N_{j,in}^basate-N_{j,out}^basate\right)z_j F\right]}`" + "Overall current efficiency", ":math:`I\eta=\sum_{j \in[cation]}{\left[\left(N_{j,in}^basic-N_{j,out}^basic\right)z_j F\right]}`" All equations are coded as "constraints" (Pyomo). Isothermal and isobaric conditions apply. @@ -226,9 +226,9 @@ Thus the fluxes become, .. csv-table:: **Table 6** Mass Balance Equations :header: "Description", "Equation", "Index set" - "mass transfer flux, acidate/basate, Water ions", ":math:`J_j^{C} = J_{diss}`", ":math:`j \in \left['{H^+, OH^-} '\right]`" - "mass transfer flux, acidate H\ :sub:`2`\ O", ":math:`J_j^{C} = t_w^{bpem} \left(\frac{i}{F}\right)+\left(L^{bpem} \right)\left(p_{osm}^CEM-p_{osm}^AEM \right)\left(\frac{\rho_w}{M_w}\right) - J_{diss}`", ":math:`j \in \left['H_2 O'\right]`" - "mass transfer flux, basate, H\ :sub:`2`\ O", ":math:`J_j^{C} = -t_w^{bpem} \left(\frac{i}{F}\right)-\left(L^{bpem} \right)\left(p_{osm}^CEM-p_{osm}^AEM \right)\left(\frac{\rho_w}{M_w}\right) - J_{diss}`", ":math:`j \in \left['H_2 O'\right]`" + "mass transfer flux, acidic/basic, Water ions", ":math:`J_j^{C} = J_{diss}`", ":math:`j \in \left['{H^+, OH^-} '\right]`" + "mass transfer flux, acidic H\ :sub:`2`\ O", ":math:`J_j^{C} = t_w^{bpem} \left(\frac{i}{F}\right)+\left(L^{bpem} \right)\left(p_{osm}^CEM-p_{osm}^AEM \right)\left(\frac{\rho_w}{M_w}\right) - J_{diss}`", ":math:`j \in \left['H_2 O'\right]`" + "mass transfer flux, basic, H\ :sub:`2`\ O", ":math:`J_j^{C} = -t_w^{bpem} \left(\frac{i}{F}\right)-\left(L^{bpem} \right)\left(p_{osm}^CEM-p_{osm}^AEM \right)\left(\frac{\rho_w}{M_w}\right) - J_{diss}`", ":math:`j \in \left['H_2 O'\right]`" .. csv-table:: **Table 7.** DOF for water splitting with catalyst :header: "Description", "Symbol", "Variable Name", "Index", "Units" @@ -311,8 +311,8 @@ Nomenclature ":math:`j`", "Component index", ":math:`in`", "Inlet", ":math:`out`", "Outlet", - ":math:`acidate`", "Cation exchange side of bipolar membrane", - ":math:`basate`", "Anion exchange side of bipolar membrane", + ":math:`acidic`", "Cation exchange side of bipolar membrane", + ":math:`basic`", "Anion exchange side of bipolar membrane", Class Documentation ------------------- diff --git a/watertap/costing/unit_models/bipolar_electrodialysis.py b/watertap/costing/unit_models/bipolar_electrodialysis.py index aa2706b2d9..94a0a5cc8b 100644 --- a/watertap/costing/unit_models/bipolar_electrodialysis.py +++ b/watertap/costing/unit_models/bipolar_electrodialysis.py @@ -119,7 +119,7 @@ def cost_bipolar_electrodialysis( def cost_bipolar_electrodialysis_stack(blk): """ Generic function for costing the stack in an electrodialysis unit. - Assumes the unit_model has a `cell_pair_num`, `cell_width`, and `cell_length` + Assumes the unit_model has a `cell_num`, `cell_width`, and `cell_length` set of variables used to size the total membrane area. """ @@ -135,7 +135,7 @@ def cost_bipolar_electrodialysis_stack(blk): blk.costing_package.bipolar_electrodialysis.membrane_capital_cost * ( 4 - * blk.unit_model.cell_pair_num + * blk.unit_model.cell_num * blk.unit_model.cell_width * blk.unit_model.cell_length ) @@ -154,7 +154,7 @@ def cost_bipolar_electrodialysis_stack(blk): blk.costing_package.bipolar_electrodialysis.membrane_capital_cost * ( 4 - * blk.unit_model.cell_pair_num + * blk.unit_model.cell_num * blk.unit_model.cell_width * blk.unit_model.cell_length ) @@ -170,7 +170,7 @@ def cost_bipolar_electrodialysis_stack(blk): * blk.costing_package.bipolar_electrodialysis.membrane_capital_cost * ( 4 - * blk.unit_model.cell_pair_num + * blk.unit_model.cell_num * blk.unit_model.cell_width * blk.unit_model.cell_length ) diff --git a/watertap/unit_models/Bipolar_Electrodialysis_0D.py b/watertap/unit_models/Bipolar_Electrodialysis_0D.py index 475867b5fd..662ea11f98 100644 --- a/watertap/unit_models/Bipolar_Electrodialysis_0D.py +++ b/watertap/unit_models/Bipolar_Electrodialysis_0D.py @@ -514,7 +514,7 @@ def build(self): bounds=(0, 1000), domain=NonNegativeReals, units=pyunits.kW * pyunits.hour * pyunits.meter**-3, - doc="Acidate-volume-flow-rate-specific electrical power consumption", + doc="acidic-volume-flow-rate-specific electrical power consumption", ) self.recovery_mass_H2O = Var( self.flowsheet().time, @@ -536,13 +536,13 @@ def build(self): units=pyunits.kg * pyunits.second**-1, doc="Base prodcued", ) - self.velocity_basate = Var( + self.velocity_basic = Var( self.flowsheet().time, initialize=0.01, units=pyunits.meter * pyunits.second**-1, doc="Linear velocity of flow in the base channel of the bipolar membrane", ) - self.velocity_acidate = Var( + self.velocity_acidic = Var( self.flowsheet().time, initialize=0.01, units=pyunits.meter * pyunits.second**-1, @@ -682,63 +682,63 @@ def build(self): ) # Build control volume for the base channel of the bipolar channel - self.basate = ControlVolume0DBlock( + self.basic = ControlVolume0DBlock( dynamic=False, has_holdup=False, property_package=self.config.property_package, property_package_args=self.config.property_package_args, ) - self.basate.add_state_blocks(has_phase_equilibrium=False) - self.basate.add_material_balances( + self.basic.add_state_blocks(has_phase_equilibrium=False) + self.basic.add_material_balances( balance_type=self.config.material_balance_type, has_mass_transfer=True ) - self.basate.add_energy_balances( + self.basic.add_energy_balances( balance_type=self.config.energy_balance_type, has_enthalpy_transfer=False, ) if self.config.is_isothermal: - self.basate.add_isothermal_assumption() - self.basate.add_momentum_balances( + self.basic.add_isothermal_assumption() + self.basic.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change, ) # Build control volume for the acid channel of the bipolar membrane channel - self.acidate = ControlVolume0DBlock( + self.acidic = ControlVolume0DBlock( dynamic=False, has_holdup=False, property_package=self.config.property_package, property_package_args=self.config.property_package_args, ) - self.acidate.add_state_blocks(has_phase_equilibrium=False) - self.acidate.add_material_balances( + self.acidic.add_state_blocks(has_phase_equilibrium=False) + self.acidic.add_material_balances( balance_type=self.config.material_balance_type, has_mass_transfer=True ) - self.acidate.add_energy_balances( + self.acidic.add_energy_balances( balance_type=self.config.energy_balance_type, has_enthalpy_transfer=False, ) if self.config.is_isothermal: - self.acidate.add_isothermal_assumption() - self.acidate.add_momentum_balances( + self.acidic.add_isothermal_assumption() + self.acidic.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change, ) - # den_mass and visc_d in acidate and basate channels are the same + # den_mass and visc_d in acidic and basic channels are the same add_object_reference( - self, "dens_mass", self.acidate.properties_in[0].dens_mass_phase["Liq"] + self, "dens_mass", self.acidic.properties_in[0].dens_mass_phase["Liq"] ) add_object_reference( - self, "visc_d", self.acidate.properties_in[0].visc_d_phase["Liq"] + self, "visc_d", self.acidic.properties_in[0].visc_d_phase["Liq"] ) # Add ports (creates inlets and outlets for each channel) - self.add_inlet_port(name="inlet_basate", block=self.basate) - self.add_outlet_port(name="outlet_basate", block=self.basate) - self.add_inlet_port(name="inlet_acidate", block=self.acidate) - self.add_outlet_port(name="outlet_acidate", block=self.acidate) + self.add_inlet_port(name="inlet_basic", block=self.basic) + self.add_outlet_port(name="outlet_basic", block=self.basic) + self.add_inlet_port(name="inlet_acidic", block=self.acidic) + self.add_outlet_port(name="outlet_acidic", block=self.acidic) # extension options if self.config.has_catalyst == True: @@ -754,16 +754,16 @@ def build(self): doc="Pressure drop expression as calculated by the pressure drop data, " "base channel of the bipolar membrane.", ) - def eq_deltaP_basate(self, t): - return self.basate.deltaP[t] == -self.pressure_drop_total[t] + def eq_deltaP_basic(self, t): + return self.basic.deltaP[t] == -self.pressure_drop_total[t] @self.Constraint( self.flowsheet().time, doc="Pressure drop expression as calculated by the pressure drop data," " acid channel of the bipolar membrane.", ) - def eq_deltaP_acidate(self, t): - return self.acidate.deltaP[t] == -self.pressure_drop_total[t] + def eq_deltaP_acidic(self, t): + return self.acidic.deltaP[t] == -self.pressure_drop_total[t] elif self.config.pressure_drop_method == PressureDropMethod.none and ( not self.config.has_pressure_change @@ -782,12 +782,12 @@ def eq_deltaP_acidate(self, t): doc="Calculate flow velocity in a single base (A.E.M side) channel of the bipolar membrane channel," " based on the average of inlet and outlet", ) - def eq_get_velocity_basate(self, t): - return self.velocity_basate[ + def eq_get_velocity_basic(self, t): + return self.velocity_basic[ t ] * self.cell_width * self.shadow_factor * self.channel_height * self.spacer_porosity * self.cell_num == 0.5 * ( - self.basate.properties_in[0].flow_vol_phase["Liq"] - + self.basate.properties_out[0].flow_vol_phase["Liq"] + self.basic.properties_in[0].flow_vol_phase["Liq"] + + self.basic.properties_out[0].flow_vol_phase["Liq"] ) @self.Constraint( @@ -795,12 +795,12 @@ def eq_get_velocity_basate(self, t): doc="Calculate flow velocity in a single acid (C.E.M side) channel of the bipolar membrane channel," " based on the average of inlet and outlet", ) - def eq_get_velocity_acidate(self, t): - return self.velocity_acidate[ + def eq_get_velocity_acidic(self, t): + return self.velocity_acidic[ t ] * self.cell_width * self.shadow_factor * self.channel_height * self.spacer_porosity * self.cell_num == 0.5 * ( - self.acidate.properties_in[0].flow_vol_phase["Liq"] - + self.acidate.properties_out[0].flow_vol_phase["Liq"] + self.acidic.properties_in[0].flow_vol_phase["Liq"] + + self.acidic.properties_out[0].flow_vol_phase["Liq"] ) @self.Constraint( @@ -894,7 +894,7 @@ def eq_potential_barrier_bpem_non_dim(self, t): return ( Constants.vacuum_electric_permittivity * self.relative_permittivity["bpem"] ** 2 - * self.basate.properties_in[t].temperature ** 2 + * self.basic.properties_in[t].temperature ** 2 * Constants.avogadro_number * Constants.elemental_charge ) / ( @@ -911,7 +911,7 @@ def eq_potential_barrier_bpem_non_dim(self, t): field_generated = ( self.elec_field_non_dim[t] * self.relative_permittivity["bpem"] - * self.basate.properties_in[t].temperature ** 2 + * self.basic.properties_in[t].temperature ** 2 / const ) @@ -948,14 +948,14 @@ def eq_current_voltage_relation(self, t, p): * ( 0.5**-1 * ( - self.basate.properties_in[t].elec_cond_phase["Liq"] - + self.basate.properties_out[t].elec_cond_phase["Liq"] + self.basic.properties_in[t].elec_cond_phase["Liq"] + + self.basic.properties_out[t].elec_cond_phase["Liq"] ) ** -1 + 0.5**-1 * ( - self.acidate.properties_in[t].elec_cond_phase["Liq"] - + self.acidate.properties_out[t].elec_cond_phase["Liq"] + self.acidic.properties_in[t].elec_cond_phase["Liq"] + + self.acidic.properties_out[t].elec_cond_phase["Liq"] ) ** -1 ) @@ -1216,8 +1216,8 @@ def eq_nonelec_bpem_flux_in(self, t, p, j): ] == self.water_density / self.config.property_package.mw_comp[j] * ( self.water_permeability_membrane["bpem"] ) * ( - self.basate.properties_in[t].pressure_osm_phase[p] - - self.acidate.properties_in[t].pressure_osm_phase[p] + self.basic.properties_in[t].pressure_osm_phase[p] + - self.acidic.properties_in[t].pressure_osm_phase[p] ) else: @@ -1239,8 +1239,8 @@ def eq_nonelec_bpem_flux_out(self, t, p, j): ] == self.water_density / self.config.property_package.mw_comp[j] * ( self.water_permeability_membrane["bpem"] ) * ( - self.basate.properties_out[t].pressure_osm_phase[p] - - self.acidate.properties_out[t].pressure_osm_phase[p] + self.basic.properties_out[t].pressure_osm_phase[p] + - self.acidic.properties_out[t].pressure_osm_phase[p] ) else: @@ -1256,9 +1256,9 @@ def eq_nonelec_bpem_flux_out(self, t, p, j): self.config.property_package.component_list, doc="Mass transfer term for the base channel (A.E.M Side) of the bipolar membrane", ) - def eq_mass_transfer_term_basate(self, t, p, j): + def eq_mass_transfer_term_basic(self, t, p, j): return ( - self.basate.mass_transfer_term[t, p, j] + self.basic.mass_transfer_term[t, p, j] == 0.5 * ( self.generation_aem_flux_in[t, p, j] @@ -1279,9 +1279,9 @@ def eq_mass_transfer_term_basate(self, t, p, j): self.config.property_package.component_list, doc="Mass transfer term for the acid channel (C.E.M Side) of the bipolar membrane channel", ) - def eq_mass_transfer_term_acidate(self, t, p, j): + def eq_mass_transfer_term_acidic(self, t, p, j): return ( - self.acidate.mass_transfer_term[t, p, j] + self.acidic.mass_transfer_term[t, p, j] == 0.5 * ( self.generation_cem_flux_in[t, p, j] @@ -1300,24 +1300,24 @@ def eq_mass_transfer_term_acidate(self, t, p, j): self.flowsheet().time, doc="Evaluate Base produced", ) - def eq_product_basate(self, t): + def eq_product_basic(self, t): conc_unit = 1 * pyunits.mole * pyunits.second**-1 product_net_loc = 0 * pyunits.kg * pyunits.second**-1 for j in self.config.property_package.cation_set: if not j == "H_+": product_in_loc = smooth_min( - self.basate.properties_in[t].flow_mol_phase_comp["Liq", j] + self.basic.properties_in[t].flow_mol_phase_comp["Liq", j] / conc_unit, - self.basate.properties_in[t].flow_mol_phase_comp["Liq", "OH_-"] + self.basic.properties_in[t].flow_mol_phase_comp["Liq", "OH_-"] / conc_unit / self.config.property_package.charge_comp[j], ) product_out_loc = smooth_min( - self.basate.properties_out[t].flow_mol_phase_comp["Liq", j] + self.basic.properties_out[t].flow_mol_phase_comp["Liq", j] / conc_unit, - self.basate.properties_out[t].flow_mol_phase_comp["Liq", "OH_-"] + self.basic.properties_out[t].flow_mol_phase_comp["Liq", "OH_-"] / conc_unit / self.config.property_package.charge_comp[j], ) @@ -1339,24 +1339,24 @@ def eq_product_basate(self, t): self.flowsheet().time, doc="Evaluate Acid produced", ) - def eq_product_acidate(self, t): + def eq_product_acidic(self, t): conc_unit = 1 * pyunits.mole * pyunits.second**-1 product_net_loc = 0 * pyunits.kg * pyunits.second**-1 for j in self.config.property_package.anion_set: if not j == "OH_-": product_in_loc = smooth_min( - self.acidate.properties_in[t].flow_mol_phase_comp["Liq", j] + self.acidic.properties_in[t].flow_mol_phase_comp["Liq", j] / conc_unit, - self.acidate.properties_in[t].flow_mol_phase_comp["Liq", "H_+"] + self.acidic.properties_in[t].flow_mol_phase_comp["Liq", "H_+"] / conc_unit / (-self.config.property_package.charge_comp[j]), ) product_out_loc = smooth_min( - self.acidate.properties_out[t].flow_mol_phase_comp["Liq", j] + self.acidic.properties_out[t].flow_mol_phase_comp["Liq", j] / conc_unit, - self.acidate.properties_out[t].flow_mol_phase_comp["Liq", "H_+"] + self.acidic.properties_out[t].flow_mol_phase_comp["Liq", "H_+"] / conc_unit / (-self.config.property_package.charge_comp[j]), ) @@ -1392,7 +1392,7 @@ def eq_specific_power_electrical(self, t): self.specific_power_electrical[t], pyunits.watt * pyunits.second * pyunits.meter**-3, ) - * self.acidate.properties_out[t].flow_vol_phase["Liq"] + * self.acidic.properties_out[t].flow_vol_phase["Liq"] == self.current[t] * self.voltage[t] ) @@ -1404,9 +1404,9 @@ def eq_current_efficiency(self, t): return ( self.current_efficiency[t] * self.current[t] * self.cell_num == sum( - self.acidate.properties_in[t].flow_mol_phase_comp["Liq", j] + self.acidic.properties_in[t].flow_mol_phase_comp["Liq", j] * self.config.property_package.charge_comp[j] - - self.acidate.properties_out[t].flow_mol_phase_comp["Liq", j] + - self.acidic.properties_out[t].flow_mol_phase_comp["Liq", j] * self.config.property_package.charge_comp[j] for j in self.config.property_package.cation_set ) @@ -1421,10 +1421,10 @@ def eq_recovery_mass_H2O(self, t): return ( self.recovery_mass_H2O[t] * ( - self.acidate.properties_in[t].flow_mass_phase_comp["Liq", "H2O"] - + self.basate.properties_in[t].flow_mass_phase_comp["Liq", "H2O"] + self.acidic.properties_in[t].flow_mass_phase_comp["Liq", "H2O"] + + self.basic.properties_in[t].flow_mass_phase_comp["Liq", "H2O"] ) - == self.acidate.properties_out[t].flow_mass_phase_comp["Liq", "H2O"] + == self.acidic.properties_out[t].flow_mass_phase_comp["Liq", "H2O"] ) # Catalyst action: @@ -1480,7 +1480,7 @@ def _make_catalyst(self): doc="Calculate the non-dimensional potential drop across the depletion region", ) def eq_potential_membrane_bpem_non_dim(self, t): - return self.elec_field_non_dim[t] == const * self.basate.properties_in[ + return self.elec_field_non_dim[t] == const * self.basic.properties_in[ t ].temperature ** -2 * self.relative_permittivity["bpem"] ** -1 * sqrt( ( @@ -1598,7 +1598,7 @@ def eq_Re(self): return ( self.N_Re == self.dens_mass - * self.velocity_acidate[0] + * self.velocity_acidic[0] * self.hydraulic_diameter * self.visc_d**-1 ) @@ -1655,7 +1655,7 @@ def eq_pressure_drop_total(self, t): self.pressure_drop_total[t] == self.dens_mass * self.friction_factor - * self.velocity_acidate[0] ** 2 + * self.velocity_acidic[0] ** 2 * 0.5 * self.hydraulic_diameter**-1 * self.cell_length @@ -1729,16 +1729,16 @@ def initialize_build( # Set the outlet has the same intial condition of the inlet. for k in self.keys(): for j in self[k].config.property_package.component_list: - self[k].basate.properties_out[0].flow_mol_phase_comp["Liq", j] = value( - self[k].basate.properties_in[0].flow_mol_phase_comp["Liq", j] + self[k].basic.properties_out[0].flow_mol_phase_comp["Liq", j] = value( + self[k].basic.properties_in[0].flow_mol_phase_comp["Liq", j] ) - self[k].acidate.properties_out[0].flow_mol_phase_comp["Liq", j] = value( - self[k].acidate.properties_in[0].flow_mol_phase_comp["Liq", j] + self[k].acidic.properties_out[0].flow_mol_phase_comp["Liq", j] = value( + self[k].acidic.properties_in[0].flow_mol_phase_comp["Liq", j] ) # --------------------------------------------------------------------- - # Initialize concentrate_basate_side block - flags_basate = self.basate.initialize( + # Initialize concentrate_basic_side block + flags_basic = self.basic.initialize( outlvl=outlvl, optarg=optarg, solver=solver, @@ -1747,8 +1747,8 @@ def initialize_build( ) init_log.info_high("Initialization Step 2 Complete.") # --------------------------------------------------------------------- - # Initialize concentrate_acidate_side block - flags_acidate = self.acidate.initialize( + # Initialize concentrate_acidic_side block + flags_acidic = self.acidic.initialize( outlvl=outlvl, optarg=optarg, solver=solver, @@ -1763,9 +1763,9 @@ def initialize_build( init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) # --------------------------------------------------------------------- # Release state - self.basate.release_state(flags_basate, outlvl) + self.basic.release_state(flags_basic, outlvl) init_log.info("Initialization Complete: {}".format(idaeslog.condition(res))) - self.acidate.release_state(flags_acidate, outlvl) + self.acidic.release_state(flags_acidic, outlvl) init_log.info("Initialization Complete: {}".format(idaeslog.condition(res))) if not check_optimal_termination(res): @@ -1823,10 +1823,10 @@ def calculate_scaling_factors(self): for j in self.config.property_package.anion_set: sf = smooth_min( iscale.get_scaling_factor( - self.acidate.properties_in[0].flow_mass_phase_comp["Liq", j] + self.acidic.properties_in[0].flow_mass_phase_comp["Liq", j] ), iscale.get_scaling_factor( - self.acidate.properties_in[0].flow_mass_phase_comp["Liq", "H_+"] + self.acidic.properties_in[0].flow_mass_phase_comp["Liq", "H_+"] ), ) @@ -1839,10 +1839,10 @@ def calculate_scaling_factors(self): for j in self.config.property_package.cation_set: sf = smooth_min( iscale.get_scaling_factor( - self.basate.properties_in[0].flow_mass_phase_comp["Liq", j] + self.basic.properties_in[0].flow_mass_phase_comp["Liq", j] ), iscale.get_scaling_factor( - self.basate.properties_in[0].flow_mass_phase_comp["Liq", "OH_-"] + self.basic.properties_in[0].flow_mass_phase_comp["Liq", "OH_-"] ), ) @@ -2053,7 +2053,7 @@ def calculate_scaling_factors(self): * 0.018 * iscale.get_scaling_factor(self.water_permeability_membrane) * iscale.get_scaling_factor( - self.acidate.properties_in[ind[0]].pressure_osm_phase[ind[1]] + self.acidic.properties_in[ind[0]].pressure_osm_phase[ind[1]] ) ) else: @@ -2066,7 +2066,7 @@ def calculate_scaling_factors(self): * 0.018 * iscale.get_scaling_factor(self.water_permeability_membrane) * iscale.get_scaling_factor( - self.acidate.properties_out[ind[0]].pressure_osm_phase[ind[1]] + self.acidic.properties_out[ind[0]].pressure_osm_phase[ind[1]] ) ) else: @@ -2074,7 +2074,7 @@ def calculate_scaling_factors(self): iscale.set_scaling_factor(self.nonelec_bpem_flux_out[ind], sf) - for ind in self.acidate.mass_transfer_term: + for ind in self.acidic.mass_transfer_term: if ind[2] == "H_+": sf = iscale.get_scaling_factor(self.generation_cem_flux_in[ind]) else: @@ -2091,9 +2091,9 @@ def calculate_scaling_factors(self): * iscale.get_scaling_factor(self.cell_length) * iscale.get_scaling_factor(self.cell_num) ) - iscale.set_scaling_factor(self.acidate.mass_transfer_term[ind], sf) + iscale.set_scaling_factor(self.acidic.mass_transfer_term[ind], sf) # - for ind in self.basate.mass_transfer_term: + for ind in self.basic.mass_transfer_term: if ind[2] == "OH_-": sf = iscale.get_scaling_factor(self.generation_aem_flux_in[ind]) else: @@ -2110,7 +2110,7 @@ def calculate_scaling_factors(self): * iscale.get_scaling_factor(self.cell_length) * iscale.get_scaling_factor(self.cell_num) ) - iscale.set_scaling_factor(self.basate.mass_transfer_term[ind], sf) + iscale.set_scaling_factor(self.basic.mass_transfer_term[ind], sf) for ind, c in self.specific_power_electrical.items(): iscale.set_scaling_factor( @@ -2119,19 +2119,19 @@ def calculate_scaling_factors(self): * iscale.get_scaling_factor(self.current[ind]) * iscale.get_scaling_factor(self.voltage[ind]) * iscale.get_scaling_factor( - self.acidate.properties_out[ind].flow_vol_phase["Liq"] + self.acidic.properties_out[ind].flow_vol_phase["Liq"] ) ** -1, ) - for ind in self.velocity_basate: + for ind in self.velocity_basic: if ( - iscale.get_scaling_factor(self.velocity_basate[ind], warning=False) + iscale.get_scaling_factor(self.velocity_basic[ind], warning=False) is None ): sf = ( iscale.get_scaling_factor( - self.basate.properties_in[ind].flow_vol_phase["Liq"] + self.basic.properties_in[ind].flow_vol_phase["Liq"] ) * iscale.get_scaling_factor(self.cell_width) ** -1 * iscale.get_scaling_factor(self.shadow_factor) ** -1 @@ -2140,16 +2140,16 @@ def calculate_scaling_factors(self): * iscale.get_scaling_factor(self.cell_num) ** -1 ) - iscale.set_scaling_factor(self.velocity_basate[ind], sf) + iscale.set_scaling_factor(self.velocity_basic[ind], sf) - for ind in self.velocity_acidate: + for ind in self.velocity_acidic: if ( - iscale.get_scaling_factor(self.velocity_acidate[ind], warning=False) + iscale.get_scaling_factor(self.velocity_acidic[ind], warning=False) is None ): sf = ( iscale.get_scaling_factor( - self.acidate.properties_in[ind].flow_vol_phase["Liq"] + self.acidic.properties_in[ind].flow_vol_phase["Liq"] ) * iscale.get_scaling_factor(self.cell_width) ** -1 * iscale.get_scaling_factor(self.shadow_factor) ** -1 @@ -2158,7 +2158,7 @@ def calculate_scaling_factors(self): * iscale.get_scaling_factor(self.cell_num) ** -1 ) - iscale.set_scaling_factor(self.velocity_acidate[ind], sf) + iscale.set_scaling_factor(self.velocity_acidic[ind], sf) if hasattr(self, "spacer_specific_area") and ( iscale.get_scaling_factor(self.spacer_specific_area, warning=True) is None @@ -2173,7 +2173,7 @@ def calculate_scaling_factors(self): ): sf = ( iscale.get_scaling_factor(self.dens_mass) - * iscale.get_scaling_factor(self.velocity_acidate[0]) + * iscale.get_scaling_factor(self.velocity_acidic[0]) * iscale.get_scaling_factor(self.hydraulic_diameter) * iscale.get_scaling_factor(self.visc_d) ** -1 ) @@ -2220,7 +2220,7 @@ def calculate_scaling_factors(self): sf = ( iscale.get_scaling_factor(self.dens_mass) * iscale.get_scaling_factor(self.friction_factor) - * iscale.get_scaling_factor(self.velocity_acidate[0]) ** 2 + * iscale.get_scaling_factor(self.velocity_acidic[0]) ** 2 * 2 * iscale.get_scaling_factor(self.hydraulic_diameter) ** -1 ) @@ -2235,7 +2235,7 @@ def calculate_scaling_factors(self): sf = ( iscale.get_scaling_factor(self.dens_mass) * iscale.get_scaling_factor(self.friction_factor) - * iscale.get_scaling_factor(self.velocity_acidate[0]) ** 2 + * iscale.get_scaling_factor(self.velocity_acidic[0]) ** 2 * 2 * iscale.get_scaling_factor(self.hydraulic_diameter) ** -1 * iscale.get_scaling_factor(self.cell_length) @@ -2304,7 +2304,7 @@ def calculate_scaling_factors(self): c, iscale.get_scaling_factor(self.nonelec_bpem_flux_out[ind]) ) - for ind, c in self.eq_mass_transfer_term_basate.items(): + for ind, c in self.eq_mass_transfer_term_basic.items(): iscale.constraint_scaling_transform( c, min( @@ -2321,7 +2321,7 @@ def calculate_scaling_factors(self): * iscale.get_scaling_factor(self.cell_length) * iscale.get_scaling_factor(self.cell_num), ) - for ind, c in self.eq_mass_transfer_term_acidate.items(): + for ind, c in self.eq_mass_transfer_term_acidic.items(): iscale.constraint_scaling_transform( c, min( @@ -2343,7 +2343,7 @@ def calculate_scaling_factors(self): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( - self.acidate.properties_out[ind].flow_mass_phase_comp["Liq", "H2O"] + self.acidic.properties_out[ind].flow_mass_phase_comp["Liq", "H2O"] ), ) @@ -2358,7 +2358,7 @@ def calculate_scaling_factors(self): c, iscale.get_scaling_factor(self.specific_power_electrical[ind]) * iscale.get_scaling_factor( - self.acidate.properties_out[ind].flow_vol_phase["Liq"] + self.acidic.properties_out[ind].flow_vol_phase["Liq"] ), ) # for ind, c in self.eq_current_efficiency.items(): @@ -2369,10 +2369,10 @@ def calculate_scaling_factors(self): def _get_stream_table_contents(self, time_point=0): return create_stream_table_dataframe( { - "base channel of the bipolar membrane Channel Inlet": self.inlet_basate, - "acid channel of the bipolar membrane Channel Inlet": self.inlet_acidate, - "base channel of the bipolar membrane Channel Outlet": self.outlet_basate, - "acid channel of the bipolar membrane Channel Outlet": self.outlet_acidate, + "base channel of the bipolar membrane Channel Inlet": self.inlet_basic, + "acid channel of the bipolar membrane Channel Inlet": self.inlet_acidic, + "base channel of the bipolar membrane Channel Outlet": self.outlet_basic, + "acid channel of the bipolar membrane Channel Outlet": self.outlet_acidic, }, time_point=time_point, ) diff --git a/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py b/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py index da86c8b912..23f2d270b4 100644 --- a/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py +++ b/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py @@ -99,23 +99,23 @@ def bped(self): m.fs.unit.ion_trans_number_membrane["bpem", "OH_-"].fix(0.1) # Set inlet streams. - m.fs.unit.inlet_basate.pressure.fix(101325) - m.fs.unit.inlet_basate.temperature.fix(298.15) - m.fs.unit.inlet_acidate.pressure.fix(101325) - m.fs.unit.inlet_acidate.temperature.fix(298.15) + m.fs.unit.inlet_basic.pressure.fix(101325) + m.fs.unit.inlet_basic.temperature.fix(298.15) + m.fs.unit.inlet_acidic.pressure.fix(101325) + m.fs.unit.inlet_acidic.temperature.fix(298.15) m.fs.unit.spacer_porosity.fix(1) - m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-4) - m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-4) - m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-4) - m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-4) - m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-4) - m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-4) - m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-4) - m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-4) + m.fs.unit.inlet_basic.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-4) + m.fs.unit.inlet_basic.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-4) + m.fs.unit.inlet_basic.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-4) + m.fs.unit.inlet_basic.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-4) + m.fs.unit.inlet_acidic.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-4) + m.fs.unit.inlet_acidic.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-4) + m.fs.unit.inlet_acidic.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-4) + m.fs.unit.inlet_acidic.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-4) - m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) - m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) + m.fs.unit.inlet_basic.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) + m.fs.unit.inlet_acidic.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) m.fs.unit.shadow_factor.fix(1) m.fs.unit.water_trans_number_membrane["bpem"].fix((5.8 + 4.3) / 2) @@ -296,20 +296,20 @@ def test_assign(self, limiting_current_check, potential_barrier_check): m.fs.unit.membrane_fixed_charge["bpem"].fix(1.5e3) # Set inlet streams. - m.fs.unit.inlet_basate.pressure.fix(101325) - m.fs.unit.inlet_basate.temperature.fix(298.15) - m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) - m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-1) - m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-1) - m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-1) - m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-1) - m.fs.unit.inlet_acidate.pressure.fix(101325) - m.fs.unit.inlet_acidate.temperature.fix(298.15) - m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) - m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-1) - m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-1) - m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-1) - m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-1) + m.fs.unit.inlet_basic.pressure.fix(101325) + m.fs.unit.inlet_basic.temperature.fix(298.15) + m.fs.unit.inlet_basic.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) + m.fs.unit.inlet_basic.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-1) + m.fs.unit.inlet_basic.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-1) + m.fs.unit.inlet_basic.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-1) + m.fs.unit.inlet_basic.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-1) + m.fs.unit.inlet_acidic.pressure.fix(101325) + m.fs.unit.inlet_acidic.temperature.fix(298.15) + m.fs.unit.inlet_acidic.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) + m.fs.unit.inlet_acidic.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-1) + m.fs.unit.inlet_acidic.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-1) + m.fs.unit.inlet_acidic.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-1) + m.fs.unit.inlet_acidic.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-1) m.fs.unit.spacer_porosity.fix(1) # Set scaling of critical quantities. @@ -614,20 +614,20 @@ def test_deltaP_various_methods( m.fs.unit.membrane_fixed_charge["bpem"].fix(1.5e3) # Set inlet streams. - m.fs.unit.inlet_basate.pressure.fix(201035) - m.fs.unit.inlet_basate.temperature.fix(298.15) - m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) - m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-4) - m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-4) - m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-4) - m.fs.unit.inlet_basate.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-4) - m.fs.unit.inlet_acidate.pressure.fix(201035) - m.fs.unit.inlet_acidate.temperature.fix(298.15) - m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) - m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-4) - m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-4) - m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-4) - m.fs.unit.inlet_acidate.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-4) + m.fs.unit.inlet_basic.pressure.fix(201035) + m.fs.unit.inlet_basic.temperature.fix(298.15) + m.fs.unit.inlet_basic.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) + m.fs.unit.inlet_basic.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-4) + m.fs.unit.inlet_basic.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-4) + m.fs.unit.inlet_basic.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-4) + m.fs.unit.inlet_basic.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-4) + m.fs.unit.inlet_acidic.pressure.fix(201035) + m.fs.unit.inlet_acidic.temperature.fix(298.15) + m.fs.unit.inlet_acidic.flow_mol_phase_comp[0, "Liq", "H2O"].fix(2.40e-1) + m.fs.unit.inlet_acidic.flow_mol_phase_comp[0, "Liq", "Na_+"].fix(7.38e-4) + m.fs.unit.inlet_acidic.flow_mol_phase_comp[0, "Liq", "Cl_-"].fix(7.38e-4) + m.fs.unit.inlet_acidic.flow_mol_phase_comp[0, "Liq", "H_+"].fix(7.38e-4) + m.fs.unit.inlet_acidic.flow_mol_phase_comp[0, "Liq", "OH_-"].fix(7.38e-4) m.fs.unit.spacer_porosity.fix(0.83) # Set scaling of critical quantities. From 2c2ca6d20448527ef724530f5bd98e198ca65714 Mon Sep 17 00:00:00 2001 From: jjd Date: Wed, 30 Oct 2024 18:58:57 -0400 Subject: [PATCH 08/17] Updates to (mainly) documentation based on first set of reviews. + Correct error in water flux. Updated unit model and test (changes are minor) --- ..._0D.rst => bipolar_electrodialysis_0D.rst} | 12 +++--- .../technical_reference/unit_models/index.rst | 2 +- .../unit_models/bipolar_electrodialysis.py | 32 -------------- .../unit_models/Bipolar_Electrodialysis_0D.py | 10 +++-- .../tests/test_biploar_electrodialysis_0D.py | 42 ++++++++----------- 5 files changed, 30 insertions(+), 68 deletions(-) rename docs/technical_reference/unit_models/{Biploar_electrodialysis_0D.rst => bipolar_electrodialysis_0D.rst} (98%) diff --git a/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst b/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst similarity index 98% rename from docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst rename to docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst index 0b0cd97f23..e5027ccb59 100644 --- a/docs/technical_reference/unit_models/Biploar_electrodialysis_0D.rst +++ b/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst @@ -25,12 +25,13 @@ assumptions made: * The **acidic** and **basic** side channels have identical geometry. * For each channel, component fluxes are uniform in the bulk solutions (the 0-dimensional assumption) and are set as the average of inlet and outlet of each channel. -* Steady state: all variables are independent on time. +* Steady state: all variables are independent of time. * Co-current flow operation. * Ideality assumptions: activity, osmotic, and van't Hoff coefficients are set at one. * All ion-exchange membrane properties (ion and water transport number, resistance, permeability) are constant. * Detailed concentration gradient effect at membrane-water interfaces is neglected. -* Constant pressure and temperature through each channel. +* Constant pressure and temperature through each channel. +* No boundary layer, electric double layer or diffusion layer, has been considered. Control Volumes --------------- @@ -38,7 +39,7 @@ Control Volumes This model has two control volumes for the acidic and basic channels. * **acidic** channel -* **basic** side channel +* **basic** channel Ports ----- @@ -84,7 +85,7 @@ Degrees of Freedom The bipolar membrane model has multiple degrees of freedom, among which temperature, pressure, and component molar flow rate are state variables that are fixed as initial conditions. The rest are parameters that should be provided in order to fully solve the model. The exact degrees of freedom depend on the mode of operation. For the simplest case where no water -splitting occurs and the bipolar membrane acts like a simple electrodialysis memrbane these are: +splitting occurs and the bipolar membrane acts like a simple electrodialysis membrane these are: .. csv-table:: **Table 2.** List of Degree of Freedom (DOF) :header: "Description", "Symbol", "Variable Name", "Index", "Units", "DOF Number \ :sup:`1`" @@ -124,7 +125,6 @@ To fully construct solution properties, users need to provide basic component in ion_dict = { "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], "mw_data": { - "H2O": 18e-3, "Na_+": 23e-3, "Cl_-": 35.5e-3, "H_+": 1e-3, @@ -145,7 +145,7 @@ To fully construct solution properties, users need to provide basic component in }, } -This model, by default, uses H\ :sub:`2`\ O as the solvent of the feed solution. +This model, by default, uses H\ :sub:`2`\ O as the solvent of the feed solution. Please note that H\ :sup:`+` and OH\ :sup:`-` information must be supplied. Otherwise an error will be thrown. Information regarding the property package this unit model relies on can be found here: diff --git a/docs/technical_reference/unit_models/index.rst b/docs/technical_reference/unit_models/index.rst index 2f80631b09..3669c6d20f 100644 --- a/docs/technical_reference/unit_models/index.rst +++ b/docs/technical_reference/unit_models/index.rst @@ -6,7 +6,7 @@ Unit Models anaerobic_digester aeration_tank - Biploar_electrodialysis_0D + bipolar_electrodialysis_0D boron_removal clarifier coag_floc_model diff --git a/watertap/costing/unit_models/bipolar_electrodialysis.py b/watertap/costing/unit_models/bipolar_electrodialysis.py index 94a0a5cc8b..4b02e93206 100644 --- a/watertap/costing/unit_models/bipolar_electrodialysis.py +++ b/watertap/costing/unit_models/bipolar_electrodialysis.py @@ -44,19 +44,6 @@ def build_bipolar_electrodialysis_cost_param_block(blk): doc="Stack and electrode replacement factor, equal to 1/lifetime.", units=pyo.units.year**-1, ) - blk.acid_flow_cost = pyo.Var( - initialize=-0.12, - doc="Acid flow cost", - units=pyo.units.USD_2018 / (pyo.units.kg), - ) - - blk.base_flow_cost = pyo.Var( - initialize=-0.5, - doc="Acid flow cost", - units=pyo.units.USD_2018 / (pyo.units.kg), - ) - blk.parent_block().register_flow_type("acid_flow_cost", blk.acid_flow_cost) - blk.parent_block().register_flow_type("base_flow_cost", blk.base_flow_cost) @register_costing_parameter_block( @@ -67,8 +54,6 @@ def cost_bipolar_electrodialysis( blk, cost_electricity_flow=True, has_rectifier=False, - has_bp_acid_value=False, - has_bp_base_value=False, ): """ Function for costing the bipolar electrodialysis unit @@ -96,23 +81,6 @@ def cost_bipolar_electrodialysis( power = blk.unit_model.get_power_electrical(blk.flowsheet().time.first()) cost_rectifier(blk, power=power, ac_dc_conversion_efficiency=0.9) - if has_bp_acid_value: - blk.costing_package.cost_flow( - pyo.units.convert( - blk.unit_model.acid_produced, - to_units=pyo.units.kg * pyo.units.second**-1, - ), - "acid_flow_cost", - ) - if has_bp_base_value: - blk.costing_package.cost_flow( - pyo.units.convert( - blk.unit_model.base_produced, - to_units=pyo.units.kg * pyo.units.second**-1, - ), - "base_flow_cost", - ) - cost_bipolar_electrodialysis_stack(blk) diff --git a/watertap/unit_models/Bipolar_Electrodialysis_0D.py b/watertap/unit_models/Bipolar_Electrodialysis_0D.py index 662ea11f98..5cd5817df6 100644 --- a/watertap/unit_models/Bipolar_Electrodialysis_0D.py +++ b/watertap/unit_models/Bipolar_Electrodialysis_0D.py @@ -1021,11 +1021,12 @@ def eq_generation_cem_flux_in(self, t, p, j): if self.config.has_catalyst == True: return ( self.generation_cem_flux_in[t, p, j] - == -self.flux_splitting[t] + == -0.5 * self.flux_splitting[t] ) else: return self.generation_cem_flux_in[t, p, j] == ( - smooth_min( + 0.5 + * smooth_min( -( self.current[t] / pyunits.amp - self.current_dens_lim_bpem[t] @@ -1081,12 +1082,13 @@ def eq_generation_aem_flux_in(self, t, p, j): if self.config.has_catalyst == True: return ( self.generation_aem_flux_in[t, p, j] - == self.flux_splitting[t] + == -0.5 * self.flux_splitting[t] ) else: return self.generation_aem_flux_in[t, p, j] == ( - smooth_min( + 0.5 + * smooth_min( -( self.current[t] / pyunits.amp - self.current_dens_lim_bpem[t] diff --git a/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py b/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py index 23f2d270b4..8b58b64d63 100644 --- a/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py +++ b/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py @@ -53,7 +53,6 @@ def bped(self): ion_dict = { "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], "mw_data": { - "H2O": 18e-3, "Na_+": 23e-3, "Cl_-": 35.5e-3, "H_+": 1e-3, @@ -194,7 +193,6 @@ def limiting_current_check(self): ion_dict = { "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], "mw_data": { - "H2O": 18e-3, "Na_+": 23e-3, "Cl_-": 35.5e-3, "H_+": 1e-3, @@ -231,7 +229,6 @@ def potential_barrier_check(self): ion_dict = { "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], "mw_data": { - "H2O": 18e-3, "Na_+": 23e-3, "Cl_-": 35.5e-3, "H_+": 1e-3, @@ -359,6 +356,7 @@ def test_assign(self, limiting_current_check, potential_barrier_check): class Test_BPED_pressure_drop_components: + @pytest.fixture(scope="class") def bped_m0(self): m = ConcreteModel() @@ -366,7 +364,6 @@ def bped_m0(self): ion_dict = { "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], "mw_data": { - "H2O": 18e-3, "Na_+": 23e-3, "Cl_-": 35.5e-3, "H_+": 1e-3, @@ -402,7 +399,6 @@ def bped_m1(self): ion_dict = { "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], "mw_data": { - "H2O": 18e-3, "Na_+": 23e-3, "Cl_-": 35.5e-3, "H_+": 1e-3, @@ -440,7 +436,6 @@ def bped_m2(self): ion_dict = { "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], "mw_data": { - "H2O": 18e-3, "Na_+": 23e-3, "Cl_-": 35.5e-3, "H_+": 1e-3, @@ -477,7 +472,6 @@ def bped_m3(self): ion_dict = { "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], "mw_data": { - "H2O": 18e-3, "Na_+": 23e-3, "Cl_-": 35.5e-3, "H_+": 1e-3, @@ -514,7 +508,6 @@ def bped_m4(self): ion_dict = { "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], "mw_data": { - "H2O": 18e-3, "Na_+": 23e-3, "Cl_-": 35.5e-3, "H_+": 1e-3, @@ -551,7 +544,6 @@ def bped_m5(self): ion_dict = { "solute_list": ["Na_+", "Cl_-", "H_+", "OH_-"], "mw_data": { - "H2O": 18e-3, "Na_+": 23e-3, "Cl_-": 35.5e-3, "H_+": 1e-3, @@ -632,7 +624,7 @@ def test_deltaP_various_methods( # Set scaling of critical quantities. m.fs.properties.set_default_scaling( - "flow_mol_phase_comp", 1e1, index=("Liq", "H2O") + "flow_mol_phase_comp", 1e0, index=("Liq", "H2O") ) m.fs.properties.set_default_scaling( "flow_mol_phase_comp", 1e0, index=("Liq", "Na_+") @@ -672,14 +664,14 @@ def test_deltaP_various_methods( initialization_tester(bped_m[1]) results = solver.solve(bped_m[1]) assert_optimal_termination(results) - assert value(bped_m[1].fs.unit.N_Re) == pytest.approx(9.491, rel=1e-3) + assert value(bped_m[1].fs.unit.N_Re) == pytest.approx(9.585, rel=1e-3) assert value(bped_m[1].fs.unit.pressure_drop[0]) == pytest.approx( - 10087.548, rel=1e-3 + 10286.288, rel=1e-3 ) assert value(bped_m[1].fs.unit.pressure_drop_total[0]) == pytest.approx( - 7969.163, rel=1e-3 + 8126.168, rel=1e-3 ) # Test bped_m2 @@ -689,14 +681,14 @@ def test_deltaP_various_methods( initialization_tester(bped_m[2]) results = solver.solve(bped_m[2]) assert_optimal_termination(results) - assert value(bped_m[2].fs.unit.N_Re) == pytest.approx(9.491, rel=1e-3) + assert value(bped_m[2].fs.unit.N_Re) == pytest.approx(9.585, rel=1e-3) assert value(bped_m[2].fs.unit.pressure_drop[0]) == pytest.approx( - 40080.279, rel=1e-3 + 40473.174, rel=1e-3 ) assert value(bped_m[2].fs.unit.pressure_drop_total[0]) == pytest.approx( - 31663.421, rel=1e-3 + 31973.808, rel=1e-3 ) # Test bped_m3 @@ -706,14 +698,14 @@ def test_deltaP_various_methods( initialization_tester(bped_m[3]) results = solver.solve(bped_m[3]) assert_optimal_termination(results) - assert value(bped_m[3].fs.unit.N_Re) == pytest.approx(9.491, rel=1e-3) + assert value(bped_m[3].fs.unit.N_Re) == pytest.approx(9.585, rel=1e-3) assert value(bped_m[3].fs.unit.pressure_drop[0]) == pytest.approx( - 7574.199, rel=1e-3 + 7685.843, rel=1e-3 ) assert value(bped_m[3].fs.unit.pressure_drop_total[0]) == pytest.approx( - 5983.618, rel=1e-3 + 6071.816, rel=1e-3 ) # Test bped_m4 @@ -724,14 +716,14 @@ def test_deltaP_various_methods( initialization_tester(bped_m[4]) results = solver.solve(bped_m[4]) assert_optimal_termination(results) - assert value(bped_m[4].fs.unit.N_Re) == pytest.approx(21.235, rel=1e-3) + assert value(bped_m[4].fs.unit.N_Re) == pytest.approx(21.443, rel=1e-3) assert value(bped_m[4].fs.unit.pressure_drop[0]) == pytest.approx( - 2263.540, rel=1e-3 + 2296.904, rel=1e-3 ) assert value(bped_m[4].fs.unit.pressure_drop_total[0]) == pytest.approx( - 1788.196, rel=1e-3 + 1814.554, rel=1e-3 ) # Test bped_m5 @@ -743,12 +735,12 @@ def test_deltaP_various_methods( iscale.calculate_scaling_factors(bped_m[5]) results = solver.solve(bped_m[5]) assert_optimal_termination(results) - assert value(bped_m[5].fs.unit.N_Re) == pytest.approx(7.641, rel=1e-3) + assert value(bped_m[5].fs.unit.N_Re) == pytest.approx(7.716, rel=1e-3) assert value(bped_m[5].fs.unit.pressure_drop[0]) == pytest.approx( - 10486.482, rel=1e-3 + 10641.053, rel=1e-3 ) assert value(bped_m[5].fs.unit.pressure_drop_total[0]) == pytest.approx( - 8284.321, rel=1e-3 + 8406.432, rel=1e-3 ) From ecaeaa0eb547b943480e95789a58e541429693b8 Mon Sep 17 00:00:00 2001 From: jjd Date: Wed, 30 Oct 2024 19:14:23 -0400 Subject: [PATCH 09/17] Updates to (mainly) documentation based on first set of reviews. + Correct error in water flux. Updated unit model and test (changes are minor) v1.1 --- docs/_static/unit_models/BPEDdiagram.png | Bin 28577 -> 28344 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/_static/unit_models/BPEDdiagram.png b/docs/_static/unit_models/BPEDdiagram.png index e8442055e973716531bab3c520f4ea67108b127e..17d86beb19e299ecdb4a59165967543900067b17 100644 GIT binary patch literal 28344 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 From 895599a8a041d40b453d26ad1b76a5403e857972 Mon Sep 17 00:00:00 2001 From: jjd Date: Thu, 31 Oct 2024 00:37:25 -0400 Subject: [PATCH 10/17] Updates to (mainly) documentation based on first set of reviews. + Correct error in water flux. Updated unit model and test (changes are minor) v1.2 --- watertap/costing/unit_models/bipolar_electrodialysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/watertap/costing/unit_models/bipolar_electrodialysis.py b/watertap/costing/unit_models/bipolar_electrodialysis.py index 4b02e93206..d2df34adf0 100644 --- a/watertap/costing/unit_models/bipolar_electrodialysis.py +++ b/watertap/costing/unit_models/bipolar_electrodialysis.py @@ -102,7 +102,7 @@ def cost_bipolar_electrodialysis_stack(blk): pyo.units.convert( blk.costing_package.bipolar_electrodialysis.membrane_capital_cost * ( - 4 + 2 * blk.unit_model.cell_num * blk.unit_model.cell_width * blk.unit_model.cell_length @@ -137,7 +137,7 @@ def cost_bipolar_electrodialysis_stack(blk): blk.costing_package.bipolar_electrodialysis.factor_membrane_replacement * blk.costing_package.bipolar_electrodialysis.membrane_capital_cost * ( - 4 + 2 * blk.unit_model.cell_num * blk.unit_model.cell_width * blk.unit_model.cell_length From a56bb734ee4de2547eee22c6bb9d8f9eb0614c8c Mon Sep 17 00:00:00 2001 From: jjd Date: Thu, 31 Oct 2024 00:39:15 -0400 Subject: [PATCH 11/17] Updates to (mainly) documentation based on first set of reviews. + Correct error in water flux. Updated unit model and test (changes are minor) v1.2 --- watertap/costing/unit_models/bipolar_electrodialysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watertap/costing/unit_models/bipolar_electrodialysis.py b/watertap/costing/unit_models/bipolar_electrodialysis.py index d2df34adf0..e70e83b9bf 100644 --- a/watertap/costing/unit_models/bipolar_electrodialysis.py +++ b/watertap/costing/unit_models/bipolar_electrodialysis.py @@ -121,7 +121,7 @@ def cost_bipolar_electrodialysis_stack(blk): * pyo.units.convert( blk.costing_package.bipolar_electrodialysis.membrane_capital_cost * ( - 4 + 2 * blk.unit_model.cell_num * blk.unit_model.cell_width * blk.unit_model.cell_length From 981da4138e84d560e38c50e8a8d6ac7383b03e98 Mon Sep 17 00:00:00 2001 From: jjd Date: Sat, 2 Nov 2024 00:05:24 -0400 Subject: [PATCH 12/17] Updates to (mainly) documentation based on second set of reviews. Cleanup to remove unused performance metrics --- .../bipolar_electrodialysis_0D.rst | 121 +++++++++++------- .../unit_models/Bipolar_Electrodialysis_0D.py | 62 --------- 2 files changed, 72 insertions(+), 111 deletions(-) diff --git a/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst b/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst index e5027ccb59..f68102cdde 100644 --- a/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst +++ b/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst @@ -4,9 +4,10 @@ Bipolar Electrodialysis (0D) Introduction ------------ -Bipolar electrodialysis, an electrochemical separation technology, has primarily been used to generate acids and bases -from waste salts produced in water purification. By providing in-situ access to valuable raw materials bipolar membranes can -significantly reduce the total cost of the operation. This cell stack is shown in Figure 1 with **basic** and **acidic** channels, that produce base and acid +Bipolar electrodialysis, an electrochemical separation technology, is primarily used to generate acids and bases +from waste salts. Recently, multiple proof of concept studies have also shown that starting from Lithium Chloride solution bipolar membranes can produce Lithium Hydroxide. +These are critical for batteries. In water treatment plants starting with waste brine, at the end of water purification, yields high concentrations sodium hydroxide. +These can be new revenue streams. A sketch of the bipolar membrane cell stack is shown in Figure 1 with **basic** and **acidic** channels, that produce base and acid respectively. More overview of the bipolar electrodialysis technology can be found in the *References*. @@ -90,10 +91,10 @@ splitting occurs and the bipolar membrane acts like a simple electrodialysis mem .. csv-table:: **Table 2.** List of Degree of Freedom (DOF) :header: "Description", "Symbol", "Variable Name", "Index", "Units", "DOF Number \ :sup:`1`" - "Temperature, inlet_acidic", ":math:`T^acidic`", "temperature", "None", ":math:`K`", 1 - "Temperature, inlet_basic", ":math:`T^basic`", "temperature", "None", ":math:`K`", 1 - "Pressure, inlet_acidic",":math:`p^acidic`", "temperature", "None", ":math:`Pa`", 1 - "Pressure, inlet_basic",":math:`p^basic`", "temperature", "None", ":math:`Pa`", 1 + "Temperature, inlet_acidic", ":math:`T^{acidic}`", "temperature", "None", ":math:`K`", 1 + "Temperature, inlet_basic", ":math:`T^{basic}`", "temperature", "None", ":math:`K`", 1 + "Pressure, inlet_acidic",":math:`p^{acidic}`", "temperature", "None", ":math:`Pa`", 1 + "Pressure, inlet_basic",":math:`p^{basic}`", "temperature", "None", ":math:`Pa`", 1 "Component molar flow rate, inlet_acidic", ":math:`N_{j,in}^{acidic}`", "flow_mol_phase_comp", "[t], ['Liq'], ['H\ :sub:`2`\O', 'Na\ :sup:`+`', '\Cl\ :sup:`-`', 'H\ :sup:`+`', 'OH\ :sup:`-`']", ":math:`mol \, s^{-1}`", 5 "Component molar flow rate, inlet_basic", ":math:`N_{j, in}^{basic}`", "flow_mol_phase_comp", "[t], ['Liq'], ['H\ :sub:`2`\O', 'Na\ :sup:`+`', '\Cl\ :sup:`-`', 'H\ :sup:`+`', 'OH\ :sup:`-`']", ":math:`mol \, s^{-1}`", 5 "Water transport number", ":math:`t_w`", "water_trans_number_membrane", "['bpem']", "dimensionless", 1 @@ -153,43 +154,45 @@ Information regarding the property package this unit model relies on can be foun Operation without catalyst -------------------------- -The Mass balance equations are summarized in **Table3**. Further details on these can be found in the *References*. + +The simplest water splitting mode is without any catalyst. Hence default the config ``has_catalyst`` is set to false. The Mass balance equations are summarized in **Table3**. Further details on these can be found in the *References*. .. csv-table:: **Table 3** Mass Balance Equations :header: "Description", "Equation", "Index set" "Component mass balance", ":math:`N_{j, in}^{acidic \: or\: basic}-N_{j, out}^{acidic\: or\: basic}+J_j^{acidic\: or\: basic} bl=0`", ":math:`j \in \left['H_2 O', '{Na^+} ', '{Cl^-} '\right]`" "mass transfer flux, basic, solute", ":math:`J_j^{C} = -t_j^{bpem}\frac{\xi i_{lim}}{ z_j F}`", ":math:`j \in \left['{Na_+} ', '{Cl^-} '\right]`" - "mass transfer flux, acidic, Water ions", ":math:`J_j^{C} = \frac{i - i_{lim}}{ z_j F}`", ":math:`j \in \left['{H^+} '\right]`" - "mass transfer flux, acidic, Water ions", ":math:`J_j^{C} = 0`", ":math:`j \in \left['{OH^-} '\right]`" - "mass transfer flux, basic, Water ions", ":math:`J_j^{C} = 0`", ":math:`j \in \left['{H^+} '\right]`" - "mass transfer flux, basic, Water ions", ":math:`J_j^{C} = -\frac{i - i_{lim}}{ z_j F}`", ":math:`j \in \left['{OH^-} '\right]`" - "mass transfer flux, acidic H\ :sub:`2`\ O", ":math:`J_j^{C} = t_w^{bpem} \left(\frac{i}{F}\right)+\left(L^{bpem} \right)\left(p_{osm}^CEM-p_{osm}^AEM \right)\left(\frac{\rho_w}{M_w}\right)`", ":math:`j \in \left['H_2 O'\right]`" - "mass transfer flux, basic, H\ :sub:`2`\ O", ":math:`J_j^{C} = -t_w^{bpem} \left(\frac{i}{F}\right)-\left(L^{bpem} \right)\left(p_{osm}^CEM-p_{osm}^AEM \right)\left(\frac{\rho_w}{M_w}\right)`", ":math:`j \in \left['H_2 O'\right]`" + "mass transfer flux, acidic, proton", ":math:`J_j^{C} = \frac{i - i_{lim}}{F}`", ":math:`j \in \left['{H^+} '\right]`" + "mass transfer flux, acidic, hydroxide", ":math:`J_j^{C} = 0`", ":math:`j \in \left['{OH^-} '\right]`" + "mass transfer flux, basic, proton", ":math:`J_j^{C} = 0`", ":math:`j \in \left['{H^+} '\right]`" + "mass transfer flux, basic, hydroxide", ":math:`J_j^{C} = \frac{i - i_{lim}}{F}`", ":math:`j \in \left['{OH^-} '\right]`" + "mass transfer flux, acidic H\ :sub:`2`\ O", ":math:`J_j^{C} = t_w^{bpem} \left(\frac{i}{F}\right)+\left(L^{bpem} \right)\left(p_{osm}^{CEM}-p_{osm}^{AEM} \right)\left(\frac{\rho_w}{M_w}\right) - 0.5 \frac{i - i_{lim}}{ F}`", ":math:`j \in \left['H_2 O'\right]`" + "mass transfer flux, basic, H\ :sub:`2`\ O", ":math:`J_j^{C} = -t_w^{bpem} \left(\frac{i}{F}\right)-\left(L^{bpem} \right)\left(p_{osm}^{CEM}-p_{osm}^{AEM} \right)\left(\frac{\rho_w}{M_w}\right) - 0.5 \frac{i - i_{lim}}{F}`", ":math:`j \in \left['H_2 O'\right]`" + +Overcoming the limiting current corresponds to a potential barrier, :math:`U_{diss}`. Important quantities are either taken as user input or computed. The appropriate configurations are ``limiting_current_density_method_bpem" for limiting current or ``limiting_potential_method_bpem`` for potential barrier. +These relationships are given in **Table 4** -Overcoming the limiting current corresponds to a potential barrier (:math:`U_{diss}`) gives the relationship :math:`U = i r_{tot}+ U_{diss}`. +.. csv-table:: **Table 4** Essential equations + :header: "Description", "Equation", "Condition" -Both the current durrent density and potential barrier must be specified, via -``limiting_current_density_method_bpem =LimitingCurrentDensityMethod`` and ``limiting_potential_method_bpem =LimitingpotentialMethod`` -respectively, in the water splitting mode of operation. They can either be user inputs ``InitialValue``, with ``limiting_current_density_data`` -in :math:`A/m^2` and ``limiting_potential_data`` in volts. There is also an option to have these critical quantities computed. For this ``Empirical`` is chosen. + "Limiting current density", ":math:`i_{lim} =` user input constant", "``limiting_current_density_method_bpem =LimitingCurrentDensitybpemMethod.InitialValue``" + " ", ":math:`i_{lim} = D F (C_{acidic}+C_{basic})^2 / (\sigma \delta)`", "``limiting_current_density_method_bpem =LimitingCurrentDensitybpemMethod.Empirical``" + "Potential barrier",":math:`U_{diss} =` user input constant", "``limiting_potential_method_bpem =LimitingpotentialMethod.InitialValue``" + " ", ":math:`U_{diss} = E_{crit}\lambda`", "``limiting_potential_method_bpem =LimitingpotentialMethod.Empirical``" + "Depletion length", ":math:`\lambda = E_{crit} \epsilon_0 \epsilon_r / (F \sigma)`", "``limiting_potential_method_bpem =LimitingpotentialMethod.Empirical``" + "Water splitting rate at electric field :math:`E` ", ":math:`R_{H^+/OH^-} (E) = [k_2(0)f(E)C_{H_2O}-k_r C_{H^+}C_{OH^-} ]`", "``limiting_potential_method_bpem =LimitingpotentialMethod.InitialValue``" + "Critical electric field", ":math:`R_{H^+/OH^-}(E = E_{crit})F/\lambda= 0.1 i_{lim}`", "``limiting_potential_method_bpem =LimitingpotentialMethod.Empirical``" -The limiting current is computed as :math:`i_{lim} = D F (C_{acidic}+C_{basic})^2 / (\sigma \delta)`. The potential barrier -calculation involves kinetics of water splitting. The rate of proton/hydroxide ion formation per unit volume is given as -:math:`R_{H^+/OH^-} = [k_2(0)f(E)C_{H_2O}-k_r C_{H^+}C_{OH^-} ]`. A majority of the production occurs within the small -depletion region :math:`\lambda`, thus the flux is :math:`R_{H^+/OH^-} /\lambda`. When this flux is :math:`0.1 i_{lim}` -the barrier is assumed to be crossed, and the corresponding :math:`E=E_{crit}=U_{diss} \lambda` determines the potential barrier. The quantities :math:`C_{H_2 O}, C_{H^+}, C_{OH^-}` are the water proton and hydroxyl concentration in :math:`mol\, m^{-3}` and are taken to be constants. :math:`f(E)` is the second Wien effect driven enhanacidicent of the dissociation rate under applied electric field. It requires as input temperature and relative permittivity (:math:`\epsilon_r`). -To close the model :math:`\lambda = E_{crit} \epsilon_0 \epsilon_r / (F \sigma)` - +Please note that since the unit model is assumed to operate in the water splitting regime and so :math:`U_{diss}` is always computed when ``has_catalyst`` is False. -.. csv-table:: **Table 4.** DOF for water splitting without catalyst +.. csv-table:: **Table 5** DOF for water splitting without catalyst :header: "Description", "Symbol", "Variable Name", "Index", "Units" "Diffusivity", ":math:`D`", "diffus_mass", "[bpem]", ":math:`m^2 s^{-1}`" @@ -200,37 +203,53 @@ To close the model :math:`\lambda = E_{crit} \epsilon_0 \epsilon_r / (F \sigma)` "Recombination rate constant ", ":math:`k_r`", "k_r", "[bpem]",":math:`L^1 mol^{-1} s^{-1}`" "Relative permittivity ", ":math:`\epsilon_r`", "relative_permittivity", "[bpem]","Non-dimensional" -.. csv-table:: **Table 5** Electrical and Performance Equations +.. csv-table:: **Table 6** Electrical and Performance Equations :header: "Description", "Equation" - "Current density", ":math:`i = \frac{I}{bl}`" - "Ohm's Law", ":math:`U = i r_{tot}`" + "Current density", ":math:`i = \frac{I}{\xi bl}`" + "Potential drop", ":math:`U = n U_{diss} + i r_{tot}`" "Resistance calculation", ":math:`r_{tot}=n\left(r^{acidic}+r^{basic}\right)+r_{el}`" "Electrical power consumption", ":math:`P=UI`" - "Water-production-specific power consumption", ":math:`P_Q=\frac{UI}{3.6\times 10^6 nQ_{out}^D}`" - "Overall current efficiency", ":math:`I\eta=\sum_{j \in[cation]}{\left[\left(N_{j,in}^basic-N_{j,out}^basic\right)z_j F\right]}`" All equations are coded as "constraints" (Pyomo). Isothermal and isobaric conditions apply. -The model has been validated using the bipolar membrane information available online: Fumatech, Technical Data Sheet for -Fumasep FBM, 2020. Additional inputs were obtained from from Ionescu, Viorel (2023) +The model used here is derived from works by Wilhelm et al. (2002) and Ionescu, Viorel (2023).It has been validated using the bipolar membrane information available online: Fumatech, Technical Data Sheet for +Fumasep FBM, 2020. Additional inputs were obtained from from Ionescu, Viorel (2023). Operation with catalyst -------------------------- -With catalyst present the water production term is modified as :math:`R_{H^+/OH^-} = \frac{Q_m}{K_{a/b}}[k_2(0)f(E)C_{H_2O}-k_r C_{H^+}C_{OH^-} ]`. Here :math:`Q_m` is the concentration of the catalyst and :math:`K_{a/b}` are the equilibrium constants for proton/hydroxide. The flux out of either side of the membrane is :math:`J_{diss} =R_{H^+} /\lambda + R_{OH^-} /\lambda`, with :math:`R_{H^+}` dominating the cation exchange side while the anion exchange side almost exclusively produces :math:`R_{OH^-}`. +Choosing config ``has_catalyst`` to True enables catalyst action. With catalyst present the Mass balance term is shown in **Table 7** -Thus the fluxes become, - -.. csv-table:: **Table 6** Mass Balance Equations +.. csv-table:: **Table 7** Mass Balance Equations :header: "Description", "Equation", "Index set" - "mass transfer flux, acidic/basic, Water ions", ":math:`J_j^{C} = J_{diss}`", ":math:`j \in \left['{H^+, OH^-} '\right]`" - "mass transfer flux, acidic H\ :sub:`2`\ O", ":math:`J_j^{C} = t_w^{bpem} \left(\frac{i}{F}\right)+\left(L^{bpem} \right)\left(p_{osm}^CEM-p_{osm}^AEM \right)\left(\frac{\rho_w}{M_w}\right) - J_{diss}`", ":math:`j \in \left['H_2 O'\right]`" - "mass transfer flux, basic, H\ :sub:`2`\ O", ":math:`J_j^{C} = -t_w^{bpem} \left(\frac{i}{F}\right)-\left(L^{bpem} \right)\left(p_{osm}^CEM-p_{osm}^AEM \right)\left(\frac{\rho_w}{M_w}\right) - J_{diss}`", ":math:`j \in \left['H_2 O'\right]`" + "mass transfer flux, acidic, proton", ":math:`J_j^{C} =J_{diss}`", ":math:`j \in \left['{H^+} '\right]`" + "mass transfer flux, acidic, hydroxide", ":math:`J_j^{C} = 0`", ":math:`j \in \left['{OH^-} '\right]`" + "mass transfer flux, basic, proton", ":math:`J_j^{C} = 0`", ":math:`j \in \left['{H^+} '\right]`" + "mass transfer flux, basic, hydroxide", ":math:`J_j^{C} = J_{diss}`", ":math:`j \in \left['{OH^-} '\right]`" + "mass transfer flux, acidic H\ :sub:`2`\ O", ":math:`J_j^{C} = t_w^{bpem} \left(\frac{i}{F}\right)+\left(L^{bpem} \right)\left(p_{osm}^{CEM}-p_{osm}^{AEM} \right)\left(\frac{\rho_w}{M_w}\right) - 0.5 J_{diss}`", ":math:`j \in \left['H_2 O'\right]`" + "mass transfer flux, basic, H\ :sub:`2`\ O", ":math:`J_j^{C} = -t_w^{bpem} \left(\frac{i}{F}\right)-\left(L^{bpem} \right)\left(p_{osm}^{CEM}-p_{osm}^{AEM }\right)\left(\frac{\rho_w}{M_w}\right) - 0.5 J_{diss}`", ":math:`j \in \left['H_2 O'\right]`" + +The flux from water splitting :math:`J_{diss}` is given by the equations in **Table 8** + +.. csv-table:: **Table 8** Essential equations + :header: "Description", "Equation" + + "Water splitting flux", ":math:`J_{diss} =R_{K_A} /\lambda + R_{K_B} /\lambda`" + "Water splitting rate", ":math:`R_{K_A/K_B} = \frac{Q_m}{K_{A/B}}[k_2(0)f(E)C_{H_2O}-k_r C_{H^+}C_{OH^-} ]`" + "Depletion length", ":math:`\lambda = E \epsilon_0 \epsilon_r / (F \sigma)`" + "Electric current density", ":math:`i = i_{lim} + F J_{diss}`" + "Potential drop", ":math:`U=n E/\lambda + i r_{tot}`" + +Please note that since the unit model is assumed to operate in the water splitting regime and so :math:`i_{lim}` is always computed when ``has_catalyst`` is True. + + -.. csv-table:: **Table 7.** DOF for water splitting with catalyst +The parameters used are given in **Table 9**. + +.. csv-table:: **Table 9.** DOF for water splitting with catalyst :header: "Description", "Symbol", "Variable Name", "Index", "Units" "Catalyst concentration on the cation exchange side", ":math:`Q_m`", "membrane_fixed_catalyst_cem", "[bpem]", ":math:`mol \, m^{-3}`" @@ -238,7 +257,7 @@ Thus the fluxes become, "Equilibrium constant of proton disassociation", ":math:`K_A`", "k_a", "none",":math:`mol \, m^{-3}`" "Equilibrium constant of hydroxide disassociation", ":math:`K_B`", "k_b", "none",":math:`mol \, m^{-3}`" -The model has been validated using the experimental data on bipolar membrane information available in Wilhelm et al. (2002) with additional inputs from Mareev et al. (2020). +The model used here is based on the analysis by Mareev et al. (2020). It and has been validated using the experimental data on bipolar membrane information available in Wilhelm et al. (2002). Additionaly inputs were obtained from Mareev et al. (2020). Frictional pressure drop ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -247,11 +266,11 @@ config ``has_pressure_change`` and ``pressure_drop_method``. Under the assumpti channels and starting flow rates, the flow velocities in the two channels are approximated equal and invariant over the channel length when calculating the frictional pressure drops. This approximation is based on the evaluation that the actual velocity variation over the channel length caused by water mass transfer across the consecutive channels leads to -negligible errors as compared to the uncertainties carried by the frictional pressure method itself. **Table 8** gives +negligible errors as compared to the uncertainties carried by the frictional pressure method itself. **Table 10** gives essential equations to simulate the pressure drop. Among extensive literatures using these equations, a good reference paper is by Wright et. al., 2018 (*References*). -.. csv-table:: **Table 8** Essential equations supporting the pressure drop calculation +.. csv-table:: **Table 10** Essential equations supporting the pressure drop calculation :header: "Description", "Equation", "Condition" "Frictional pressure drop, Darcy_Weisbach", ":math:`p_L=f\frac{\rho v^2}{2d_H}` \ :sup:`1`", "`has_pressure_change == True` and `pressure_drop_method == PressureDropMethod.Darcy_Weisbach`" @@ -271,7 +290,7 @@ paper is by Wright et. al., 2018 (*References*). Nomenclature ------------ -.. csv-table:: **Table 9.** Nomenclature +.. csv-table:: **Table 11** Nomenclature :header: "Symbol", "Description", "Unit" :widths: 10, 20, 10 @@ -302,9 +321,7 @@ Nomenclature ":math:`r`", "Membrane areal resistance", ":math:`\Omega m^2`" ":math:`r_{el}`", "Electrode areal resistance", ":math:`\Omega m^2`" ":math:`d`", "Spacer thickness", ":math:`m`" - ":math:`\eta`", "Current efficiency for desalination", "dimensionless" ":math:`P`", "Power consumption", ":math:`W`" - ":math:`P_Q`", "Specific power consumption", ":math:`kW\ h\ m^{-3}`" ":math:`Q`", "Volume flow rate", ":math:`m^3s^{-1}`" ":math:`\phi_d^{ohm}`", "Ohmic potential across a Nernst diffusion layer", ":math:`V`" "**Subscripts and superscripts**" @@ -337,4 +354,10 @@ Spiegler, K. S. (1971). Polarization at ion exchange membrane-solution interface Wright, N. C., Shah, S. R., & Amrose, S. E. (2018). A robust model of brackish water electrodialysis desalination with experimental comparison at different size scales. -Desalination, 443, 27-43. \ No newline at end of file +Desalination, 443, 27-43. + +Mareev, S.A., Evdochenko, E., Wessling, M., Kozaderova, O.A., Niftaliev, S.I., Pismenskaya, N.D. and Nikonenko, V.V., 2020. A comprehensive mathematical model of water splitting in bipolar membranes: Impact of the spatial distribution of fixed charges and catalyst at bipolar junction. Journal of Membrane Science, 603, p.118010. + +Wilhelm, F.G., Pünt, I., Van Der Vegt, N.F.A., Wessling, M. and Strathmann, H., 2001. Optimisation strategies for the preparation of bipolar membranes with reduced salt ion leakage in acid–base electrodialysis. Journal of Membrane Science, 182(1-2), pp.13-28. + +Wilhelm, F.G., Van Der Vegt, N.F.A., Strathmann, H. and Wessling, M., 2002. Comparison of bipolar membranes by means of chronopotentiometry. Journal of membrane science, 199(1-2), pp.177-190. \ No newline at end of file diff --git a/watertap/unit_models/Bipolar_Electrodialysis_0D.py b/watertap/unit_models/Bipolar_Electrodialysis_0D.py index 5cd5817df6..a0e5d41be6 100644 --- a/watertap/unit_models/Bipolar_Electrodialysis_0D.py +++ b/watertap/unit_models/Bipolar_Electrodialysis_0D.py @@ -493,13 +493,6 @@ def build(self): ) # Performance metrics - self.current_efficiency = Var( - self.flowsheet().time, - initialize=0.9, - bounds=(-1.2, 1.2), - units=pyunits.dimensionless, - doc="The overall current efficiency for deionizaiton", - ) self.power_electrical = Var( self.flowsheet().time, initialize=1, @@ -516,14 +509,6 @@ def build(self): units=pyunits.kW * pyunits.hour * pyunits.meter**-3, doc="acidic-volume-flow-rate-specific electrical power consumption", ) - self.recovery_mass_H2O = Var( - self.flowsheet().time, - initialize=0.5, - bounds=(0, 1), - domain=NonNegativeReals, - units=pyunits.dimensionless, - doc="water recovery ratio calculated by mass", - ) self.acid_produced = Var( initialize=55 * 1e3, bounds=(0, 1e6), @@ -1398,37 +1383,6 @@ def eq_specific_power_electrical(self, t): == self.current[t] * self.voltage[t] ) - @self.Constraint( - self.flowsheet().time, - doc="Overall current efficiency evaluation", - ) - def eq_current_efficiency(self, t): - return ( - self.current_efficiency[t] * self.current[t] * self.cell_num - == sum( - self.acidic.properties_in[t].flow_mol_phase_comp["Liq", j] - * self.config.property_package.charge_comp[j] - - self.acidic.properties_out[t].flow_mol_phase_comp["Liq", j] - * self.config.property_package.charge_comp[j] - for j in self.config.property_package.cation_set - ) - * Constants.faraday_constant - ) - - @self.Constraint( - self.flowsheet().time, - doc="Water recovery by mass", - ) - def eq_recovery_mass_H2O(self, t): - return ( - self.recovery_mass_H2O[t] - * ( - self.acidic.properties_in[t].flow_mass_phase_comp["Liq", "H2O"] - + self.basic.properties_in[t].flow_mass_phase_comp["Liq", "H2O"] - ) - == self.acidic.properties_out[t].flow_mass_phase_comp["Liq", "H2O"] - ) - # Catalyst action: def _make_catalyst(self): @@ -2341,14 +2295,6 @@ def calculate_scaling_factors(self): * iscale.get_scaling_factor(self.cell_num), ) - for ind, c in self.eq_recovery_mass_H2O.items(): - iscale.constraint_scaling_transform( - c, - iscale.get_scaling_factor( - self.acidic.properties_out[ind].flow_mass_phase_comp["Liq", "H2O"] - ), - ) - for ind, c in self.eq_power_electrical.items(): iscale.constraint_scaling_transform( c, @@ -2363,10 +2309,6 @@ def calculate_scaling_factors(self): self.acidic.properties_out[ind].flow_vol_phase["Liq"] ), ) - # for ind, c in self.eq_current_efficiency.items(): - # iscale.constraint_scaling_transform( - # c, iscale.get_scaling_factor(self.current[ind]) - # ) def _get_stream_table_contents(self, time_point=0): return create_stream_table_dataframe( @@ -2388,10 +2330,6 @@ def _get_performance_contents(self, time_point=0): "Specific electrical power consumption (kW*h/m**3)": self.specific_power_electrical[ time_point ], - # "Current efficiency for deionzation": self.current_efficiency[ - # time_point - # ], - "Water recovery by mass": self.recovery_mass_H2O[time_point], }, "exprs": {}, "params": {}, From c09399b0196588fac70d929bdfd2e58735b0db69 Mon Sep 17 00:00:00 2001 From: jjd Date: Sat, 2 Nov 2024 00:11:03 -0400 Subject: [PATCH 13/17] Updates to (mainly) documentation based on second set of reviews. Cleanup to remove unused performance metrics v1.1 --- .../unit_models/bipolar_electrodialysis_0D.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst b/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst index f68102cdde..120d4e6792 100644 --- a/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst +++ b/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst @@ -62,7 +62,8 @@ This model can mathematically take a multi-component (i.e., one salt molecule to a multi-component solution creates unknown or difficult-to-specify parameters, e.g., the electrical transport numbers through membranes, the multi-ion diffusivity, etc., and physical relationships, which may result in ill-posed or ill-conditioned problems challenging the models' numerical solutions. While we continuously work on advancing our models to absorb new principles revealed by progressing -research, we advise the users be very **cautious** with simulating multi-component systems by this programmed model for aspects stated above. +research, we advise the users be very **cautious** with simulating multi-component system by this programmed model for aspects stated above. +This unit model works with the MCAS property model. .. csv-table:: **Table 1.** List of Set :header: "Description", "Symbol", "Indices" From d0b04492d82e1aceec9aeed0700758617542c5fa Mon Sep 17 00:00:00 2001 From: jjd Date: Fri, 8 Nov 2024 19:15:56 -0500 Subject: [PATCH 14/17] Change pytest to check for numerical accuracy instead of directly against experimental data --- .../tests/test_biploar_electrodialysis_0D.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py b/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py index 8b58b64d63..37d90c2f45 100644 --- a/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py +++ b/watertap/unit_models/tests/test_biploar_electrodialysis_0D.py @@ -154,13 +154,15 @@ def bped(self): def test_assign(self, bped): # Experimental data from Wilhelm et al. (2002) with additional inputs from Mareev et al. (2020) - expt_current_density = np.array( - [11.7, 15.5, 20.8, 24.4, 30.6, 40.0, 100.6] - ) # in mA/cm2 expt_membrane_potential = np.array( [0.088, 0.184, 0.4045, 0.690, 0.858, 0.95, 1.3] ) # in volts + # experimental current density: 11.7, 15.5, 20.8, 24.4, 30.6, 40.0, 100.6] # in mA/cm2 + expected_current_density = np.array( + [19.250, 19.269, 19.606, 22.764, 29.009, 35.278, 99.223] + ) # in mA/cm2 (computed numerically) + for indx, v in enumerate(expt_membrane_potential): m = bped m.fs.unit.potential_membrane_bpem[0].fix(v) @@ -170,10 +172,6 @@ def test_assign(self, bped): initialization_tester(m) results = solver.solve(m) assert_optimal_termination(results) - if indx < 2: - rel_tol = 1e0 - else: - rel_tol = 1.5e-1 current_density_computed = ( 0.1 @@ -181,7 +179,7 @@ def test_assign(self, bped): / (m.fs.unit.cell_width * m.fs.unit.cell_length) ) # convert to mA/cm2 assert value(current_density_computed) == pytest.approx( - expt_current_density[indx], rel=rel_tol + expected_current_density[indx], rel=1e-3 ) @@ -329,6 +327,8 @@ def test_assign(self, limiting_current_check, potential_barrier_check): # Data on limiting current and potential barrier to water splitting have been obtained from: # Fumatech, Technical Data Sheet for Fumasep FBM, 2020. With additional modelling parameters obtained from # Ionescu, Viorel. Advanced Topics in Optoelectronics, Microelectronics, and Nanotechnologies (2023) + # Expected current density is 1000 A/m2 and potential barrier is 0.8 V + iscale.calculate_scaling_factors(check_m[1]) # Test computing limiting current in bipolar membrane @@ -338,7 +338,7 @@ def test_assign(self, limiting_current_check, potential_barrier_check): results = solver.solve(check_m[0]) assert_optimal_termination(results) assert value(check_m[0].fs.unit.current_dens_lim_bpem[0]) == pytest.approx( - 1000, rel=1e-1 + 987.120, rel=1e-3 ) # Test computing limiting current and potential barrier to water splitting in bipolar membrane @@ -348,10 +348,10 @@ def test_assign(self, limiting_current_check, potential_barrier_check): results = solver.solve(check_m[1]) assert_optimal_termination(results) assert value(check_m[0].fs.unit.current_dens_lim_bpem[0]) == pytest.approx( - 1000, rel=1e-1 + 987.120, rel=1e-3 ) assert value(check_m[1].fs.unit.potential_barrier_bpem[0]) == pytest.approx( - 0.8, rel=1e-1 + 0.786, rel=1e-3 ) From 7bed754648af2d0706b0d8decf19c744b85cd48e Mon Sep 17 00:00:00 2001 From: jjd Date: Tue, 12 Nov 2024 15:22:06 -0500 Subject: [PATCH 15/17] fix typos --- .../bipolar_electrodialysis_0D.rst | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst b/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst index 120d4e6792..9d3d434a25 100644 --- a/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst +++ b/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst @@ -104,7 +104,7 @@ splitting occurs and the bipolar membrane acts like a simple electrodialysis mem "Electrode areal resistance", ":math:`r_{el}`", "electrodes_resistance", "[t]", ":math:`\Omega m^2`", 1 "Cell number", ":math:`n`", "cell_num", "None", "dimensionless", 1 "Current utilization coefficient", ":math:`\xi`", "current_utilization", "None", "dimensionless", 1 - "Shadow factor", ":math:`\xi`", "shadow_factor", "None", "dimensionless", 1 + "Shadow factor", ":math:`\beta`", "shadow_factor", "None", "dimensionless", 1 "Spacer thickness", ":math:`s`", "spacer_thickness", "none", ":math:`m` ", 1 "Membrane areal resistance", ":math:`r`", "membrane_surface_resistance", "['acidic', 'basic']", ":math:`\Omega m^2`", 2 "Cell width", ":math:`b`", "cell_width", "None", ":math:`\text{m}`", 1 @@ -207,7 +207,7 @@ Please note that since the unit model is assumed to operate in the water splitti .. csv-table:: **Table 6** Electrical and Performance Equations :header: "Description", "Equation" - "Current density", ":math:`i = \frac{I}{\xi bl}`" + "Current density", ":math:`i = \frac{I}{\beta bl}`" "Potential drop", ":math:`U = n U_{diss} + i r_{tot}`" "Resistance calculation", ":math:`r_{tot}=n\left(r^{acidic}+r^{basic}\right)+r_{el}`" "Electrical power consumption", ":math:`P=UI`" @@ -309,6 +309,7 @@ Nomenclature ":math:`U`", "Voltage over a stack", ":math:`V`" ":math:`n`", "Cell number", "dimensionless" ":math:`\xi`", "Current utilization coefficient (including ion diffusion and water electroosmosis)", "dimensionless" + ":math:`\beta`", "Shadow factor", "dimensionless" ":math:`z`", "Ion charge", "dimensionless" ":math:`F`", "Faraday constant", ":math:`C\ mol^{-1}`" ":math:`\epsilon_0`", "permittivity of free space", ":math:`C\ mol^{-1}`" @@ -339,26 +340,28 @@ Class Documentation References ---------- -Strathmann, H. (2010). Electrodialysis, a mature technology with a multitude of new applications. -Desalination, 264(3), 268-288. - -Strathmann, H. (2004). Ion-exchange membrane separation processes. Elsevier. Ch. 4. +Campione, A., Gurreri, L., Ciofalo, M., Micale, G., Tamburini, A., & Cipollina, A. (2018). +Electrodialysis for water desalination: A critical assessment of recent developments on process +fundamentals, models and applications. Desalination, 434, 121-160. Campione, A., Cipollina, A., Bogle, I. D. L., Gurreri, L., Tamburini, A., Tedesco, M., & Micale, G. (2019). A hierarchical model for novel schemes of electrodialysis desalination. Desalination, 465, 79-93. -Campione, A., Gurreri, L., Ciofalo, M., Micale, G., Tamburini, A., & Cipollina, A. (2018). -Electrodialysis for water desalination: A critical assessment of recent developments on process -fundamentals, models and applications. Desalination, 434, 121-160. +Ionescu, V., 2023, March. A simple one-dimensional model for analysis of a bipolar membrane used in electrodialysis desalination. In Advanced Topics in Optoelectronics, Microelectronics, and Nanotechnologies XI (Vol. 12493, pp. 520-529). SPIE. + +Mareev, S.A., Evdochenko, E., Wessling, M., Kozaderova, O.A., Niftaliev, S.I., Pismenskaya, N.D. and Nikonenko, V.V., 2020. A comprehensive mathematical model of water splitting in bipolar membranes: Impact of the spatial distribution of fixed charges and catalyst at bipolar junction. Journal of Membrane Science, 603, p.118010. Spiegler, K. S. (1971). Polarization at ion exchange membrane-solution interfaces. Desalination, 9(4), 367-385. +Strathmann, H. (2004). Ion-exchange membrane separation processes. Elsevier. Ch. 4. + +Strathmann, H. (2010). Electrodialysis, a mature technology with a multitude of new applications. +Desalination, 264(3), 268-288. + Wright, N. C., Shah, S. R., & Amrose, S. E. (2018). A robust model of brackish water electrodialysis desalination with experimental comparison at different size scales. Desalination, 443, 27-43. -Mareev, S.A., Evdochenko, E., Wessling, M., Kozaderova, O.A., Niftaliev, S.I., Pismenskaya, N.D. and Nikonenko, V.V., 2020. A comprehensive mathematical model of water splitting in bipolar membranes: Impact of the spatial distribution of fixed charges and catalyst at bipolar junction. Journal of Membrane Science, 603, p.118010. - Wilhelm, F.G., Pünt, I., Van Der Vegt, N.F.A., Wessling, M. and Strathmann, H., 2001. Optimisation strategies for the preparation of bipolar membranes with reduced salt ion leakage in acid–base electrodialysis. Journal of Membrane Science, 182(1-2), pp.13-28. Wilhelm, F.G., Van Der Vegt, N.F.A., Strathmann, H. and Wessling, M., 2002. Comparison of bipolar membranes by means of chronopotentiometry. Journal of membrane science, 199(1-2), pp.177-190. \ No newline at end of file From 0e7c3ae3f882ebe4f4bf30fdab35c3bde119373e Mon Sep 17 00:00:00 2001 From: jjd Date: Tue, 12 Nov 2024 15:27:51 -0500 Subject: [PATCH 16/17] fix typos --- .../unit_models/bipolar_electrodialysis_0D.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst b/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst index 9d3d434a25..0b443aa885 100644 --- a/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst +++ b/docs/technical_reference/unit_models/bipolar_electrodialysis_0D.rst @@ -347,6 +347,8 @@ fundamentals, models and applications. Desalination, 434, 121-160. Campione, A., Cipollina, A., Bogle, I. D. L., Gurreri, L., Tamburini, A., Tedesco, M., & Micale, G. (2019). A hierarchical model for novel schemes of electrodialysis desalination. Desalination, 465, 79-93. +Fumatech, Technical Data Sheet for Fumasep FBM, 2020. + Ionescu, V., 2023, March. A simple one-dimensional model for analysis of a bipolar membrane used in electrodialysis desalination. In Advanced Topics in Optoelectronics, Microelectronics, and Nanotechnologies XI (Vol. 12493, pp. 520-529). SPIE. Mareev, S.A., Evdochenko, E., Wessling, M., Kozaderova, O.A., Niftaliev, S.I., Pismenskaya, N.D. and Nikonenko, V.V., 2020. A comprehensive mathematical model of water splitting in bipolar membranes: Impact of the spatial distribution of fixed charges and catalyst at bipolar junction. Journal of Membrane Science, 603, p.118010. From e4ad5098615e4fea3c716b586fa412744c2aa447 Mon Sep 17 00:00:00 2001 From: jjd Date: Fri, 15 Nov 2024 22:32:08 -0500 Subject: [PATCH 17/17] Corrections to the documentation --- docs/_static/unit_models/BPEDdiagram.png | Bin 28344 -> 30050 bytes .../bipolar_electrodialysis_0D.rst | 58 ++++++++---------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/docs/_static/unit_models/BPEDdiagram.png b/docs/_static/unit_models/BPEDdiagram.png index 17d86beb19e299ecdb4a59165967543900067b17..39581fecf45a86ca636796afa4fad80e679ff9f7 100644 GIT binary patch literal 30050 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 literal 28344 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