├── docs ├── requirements.txt ├── source │ ├── demos │ │ └── index.rst │ ├── api.rst │ ├── index.rst │ ├── _static │ │ └── references.bib │ ├── conf.py │ ├── user_guide │ │ ├── index.rst │ │ ├── conc_params.rst │ │ ├── boundary_conditions.rst │ │ ├── echem_params.rst │ │ ├── homog_params.rst │ │ ├── fluid_solver.rst │ │ └── physical_params.rst │ ├── quickstart.rst │ └── examples.rst ├── make.bat └── Makefile ├── demos ├── flow_reactor │ ├── flow_reactor.png │ └── CO2_solution_field.png └── demo_references.bib ├── joss ├── Makefile ├── paper.bib └── paper.md ├── setup.cfg ├── setup.py ├── echemfem ├── __init__.py └── cylindricalmeasure.py ├── CONTRIBUTING.md ├── examples ├── bortels_unstructuredquad_nondim_coarse.geo ├── bortels_structuredquad.geo ├── bortels_structuredquad_nondim.geo ├── bortels_structuredquad_nondim_coarse1664.geo ├── plot_data.py ├── submitter_mms.pl ├── submitter.pl ├── tworxn.py ├── squares.geo ├── squares_small.geo ├── tworxn_irregular.py ├── bortels_twoion.py ├── bortels_threeion.py ├── BMCSL.py ├── bortels_twoion_nondim.py ├── simple_flow_battery.py ├── gupta.py ├── bortels_threeion_nondim.py ├── bicarb.py ├── mesh_bicarb_tandem_example.geo ├── gupta_advection_migration.py ├── carbonate.py ├── carbonate_homog_params.py ├── cylindrical_pore.py └── paper_test.py ├── tests ├── test_notimplemented.py ├── test_diffusion.py ├── test_diffusion_cylindrical.py ├── test_diffusion_finite_size.py ├── test_cylindricalmeasure.py ├── test_advection.py ├── test_heat_equation.py ├── test_diffusion_migration.py ├── test_advection_diffusion_poisson_porous.py ├── test_diffusion_migration_poisson.py ├── test_advection_diffusion_poisson.py ├── test_SUPG.py ├── test_advection_diffusion_migration_porous.py ├── test_advection_diffusion_migration_poisson.py ├── test_advection_diffusion_migration.py └── test_advection_diffusion.py ├── LICENSE ├── NOTICE.md ├── .github └── workflows │ └── docs.yml ├── README.md └── CODE_OF_CONDUCT.md /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==7.1.2 2 | sphinx-rtd-theme==1.3.0rc1 3 | -------------------------------------------------------------------------------- /docs/source/demos/index.rst: -------------------------------------------------------------------------------- 1 | Demos 2 | ===== 3 | 4 | .. toctree:: 5 | :titlesonly: 6 | 7 | flow_reactor.py 8 | -------------------------------------------------------------------------------- /demos/flow_reactor/flow_reactor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LLNL/echemfem/HEAD/demos/flow_reactor/flow_reactor.png -------------------------------------------------------------------------------- /demos/flow_reactor/CO2_solution_field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LLNL/echemfem/HEAD/demos/flow_reactor/CO2_solution_field.png -------------------------------------------------------------------------------- /joss/Makefile: -------------------------------------------------------------------------------- 1 | all: pdf 2 | 3 | pdf: 4 | docker run --rm --volume $(PWD):/data --user $(id -u):$(id -g) --env JOURNAL=joss openjournals/inara 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | 2 | [flake8] 3 | ignore = 4 | E501,F403,F405,E226,E402,E721,E731,E741,W503,F999, 5 | N801,N802,N803,N806,N807,N811,N813,N814,N815,N816 6 | exclude = .git,__pycache__ 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name="EchemFEM", 4 | version="0.0.2", 5 | description="FEM for electrochemical transport", 6 | packages=["echemfem"], 7 | install_requires=["pandas"], 8 | ) 9 | -------------------------------------------------------------------------------- /echemfem/__init__.py: -------------------------------------------------------------------------------- 1 | from echemfem.cylindricalmeasure import CylindricalMeasure 2 | from echemfem.solver import EchemSolver, TransientEchemSolver 3 | from echemfem.flow_solver import FlowSolver, NavierStokesFlowSolver, NavierStokesBrinkmanFlowSolver 4 | from echemfem.utility_meshes import IntervalBoundaryLayerMesh, RectangleBoundaryLayerMesh 5 | -------------------------------------------------------------------------------- /demos/demo_references.bib: -------------------------------------------------------------------------------- 1 | @article{gupta2006calculation, 2 | title={Calculation for the cathode surface concentrations in the electrochemical reduction of {CO}₂ in {KHCO}₃ solutions}, 3 | author={Gupta, N and Gattrell, M and MacDougall, B}, 4 | journal={Journal of applied electrochemistry}, 5 | volume={36}, 6 | number={2}, 7 | pages={161--172}, 8 | year={2006}, 9 | publisher={Springer}, 10 | doi={10.1007/s10800-005-9058-y} 11 | } 12 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | EchemSolver classes 5 | ------------------- 6 | 7 | .. autoclass:: echemfem.EchemSolver 8 | :members: 9 | 10 | .. autoclass:: echemfem.TransientEchemSolver 11 | :members: 12 | 13 | FlowSolver classes 14 | ------------------- 15 | 16 | .. autoclass:: echemfem.FlowSolver 17 | :members: 18 | 19 | .. autoclass:: echemfem.NavierStokesFlowSolver 20 | :members: 21 | 22 | .. autoclass:: echemfem.NavierStokesBrinkmanFlowSolver 23 | :members: 24 | 25 | Utility meshes 26 | -------------- 27 | These are boundary layer meshes, which are modified versions of :func:`firedrake.utility_meshes.IntervalMesh` and :func:`firedrake.utility_meshes.RectangleMesh`. 28 | 29 | .. automodule:: echemfem.utility_meshes 30 | :members: 31 | 32 | .. toctree:: 33 | :hidden: 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to EchemFEM 2 | 3 | We welcome contributions to EchemFEM. To do so please submit a pull request through our 4 | EchemFEM github page at https://github.com/LLNL/echemfem. 5 | 6 | All contributions to EchemFEM must be made under the MIT license. 7 | 8 | Any questions can be sent to roy27@llnl.gov. 9 | 10 | # Attribution 11 | 12 | The EchemFEM project uses git's commit history to track contributions from individual developers. 13 | 14 | Since we want everyone to feel they are getting the proper attribution for their contributions, please add your name to 15 | the list below as part of your commit. 16 | 17 | # Contributors (In Alphabetical Order) 18 | 19 | * Julian Andrej, LLNL 20 | * Victor Beck, LLNL 21 | * Victoria Ehlinger, LLNL 22 | * Nitish Govindarajan, LLNL 23 | * Jack Guo, LLNL 24 | * Tiras Lin, LLNL 25 | * Thomas Roy, LLNL 26 | -------------------------------------------------------------------------------- /examples/bortels_unstructuredquad_nondim_coarse.geo: -------------------------------------------------------------------------------- 1 | La = 5.; 2 | L = 2.; 3 | Lb = 5.; 4 | h = 1.; 5 | lc = 0.25*h; 6 | lc1 = 0.08*h; 7 | Point(1) = {0, 0, 0, lc}; 8 | Point(2) = {La, 0, 0, lc1}; 9 | Point(3) = {La+L, 0, 0, lc1}; 10 | Point(4) = {La+L+Lb, 0, 0, lc}; 11 | Point(5) = {La+L+Lb, h, 0, lc}; 12 | Point(6) = {La+L, h, 0, lc1}; 13 | Point(7) = {La, h, 0, lc1}; 14 | Point(8) = {0, h, 0, lc}; 15 | Line(1) = {1, 2}; 16 | Line(2) = {2, 3}; 17 | Line(3) = {3, 4}; 18 | Line(4) = {4, 5}; 19 | Line(5) = {5, 6}; 20 | Line(6) = {6, 7}; 21 | Line(7) = {7, 8}; 22 | Line(8) = {8, 1}; 23 | Curve Loop(9) = {1, 2, 3, 4, 5, 6, 7, 8}; 24 | Plane Surface(1) = {9}; 25 | 26 | Physical Curve("Inlet", 10) = {8}; 27 | Physical Curve("Outlet", 11) = {4}; 28 | Physical Curve("Anode", 12) = {6}; 29 | Physical Curve("Cathode", 13) = {2}; 30 | Physical Curve("Wall", 14) = {7, 1, 5, 3}; 31 | Physical Surface("Channel", 2) = {1}; 32 | 33 | 34 | Recombine Surface{1}; 35 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to EchemFEM's documentation! 2 | ==================================== 3 | 4 | **EchemFEM** is a Python library based on 5 | `Firedrake `_, providing Finite 6 | Element solvers for electrochemical transport. 7 | 8 | Check out the :doc:`quickstart` section for further information, including 9 | the :ref:`installation` of the package. 10 | 11 | .. note:: 12 | 13 | This project is under active development. 14 | 15 | Contents 16 | -------- 17 | 18 | .. toctree:: 19 | :maxdepth: 2 20 | 21 | quickstart 22 | user_guide/index 23 | 24 | .. toctree:: 25 | :titlesonly: 26 | 27 | demos/index 28 | examples 29 | api 30 | 31 | Copyright and License 32 | --------------------- 33 | 34 | Please see this `LICENSE file `_ for details. 35 | 36 | Copyright (c) 2022-2025, Lawrence Livermore National Security, LLC. 37 | Produced at the Lawrence Livermore National Laboratory. 38 | 39 | LLNL-CODE-837342 40 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | .PHONY: copy_demos 18 | 19 | source/demos: copy_demos 20 | 21 | copy_demos: 22 | install -d source/demos 23 | cp ../demos/*/*.rst ../demos/*/*.png ../demos/demo_references.bib source/demos 24 | for file in source/demos/*.py.rst; do pylit $$file; done 25 | 26 | html: Makefile copy_demos 27 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 28 | # Catch-all target: route all unknown targets to Sphinx using the new 29 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 30 | %: Makefile 31 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 32 | -------------------------------------------------------------------------------- /tests/test_notimplemented.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import EchemSolver 4 | 5 | flows = [["electroneutrality"], 6 | ["poisson", "migration", "electroneutrality"], 7 | ["poisson", "migration", "electroneutrality full"], 8 | ["migration"] 9 | ] 10 | 11 | 12 | class Solver(EchemSolver): 13 | def __init__(self, flow, family="CG"): 14 | mesh = UnitIntervalMesh(2) 15 | conc_params = [] 16 | physical_params = [] 17 | physical_params = {"flow": flow, 18 | } 19 | 20 | super().__init__(conc_params, physical_params, mesh, family=family) 21 | 22 | def set_boundary_markers(self): 23 | self.boundary_markers = {} 24 | 25 | 26 | def _test_flow(flow): 27 | # with pytest.raises(NotImplementedError): 28 | # solver = Solver(flow) 29 | solver = Solver(flow) 30 | 31 | 32 | def test_flows(): 33 | for flow in flows: 34 | try: 35 | _test_flow(flow) 36 | except NotImplementedError as e: 37 | print(e) 38 | 39 | 40 | test_flows() 41 | -------------------------------------------------------------------------------- /examples/bortels_structuredquad.geo: -------------------------------------------------------------------------------- 1 | La = 0.05; 2 | L = 0.02; 3 | Lb = 0.05; 4 | h = 0.01; 5 | lc = 0.15*h; 6 | Point(1) = {0, 0, 0, lc}; 7 | Point(2) = {La, 0, 0, lc}; 8 | Point(3) = {La+L, 0, 0, lc}; 9 | Point(4) = {La+L+Lb, 0, 0, lc}; 10 | Point(5) = {La+L+Lb, h, 0, lc}; 11 | Point(6) = {La+L, h, 0, lc}; 12 | Point(7) = {La, h, 0, lc}; 13 | Point(8) = {0, h, 0, lc}; 14 | Line(1) = {1, 2}; 15 | Line(2) = {2, 3}; 16 | Line(3) = {3, 4}; 17 | Line(4) = {4, 5}; 18 | Line(5) = {5, 6}; 19 | Line(6) = {6, 7}; 20 | Line(7) = {7, 8}; 21 | Line(8) = {8, 1}; 22 | Curve Loop(9) = {1, 2, 3, 4, 5, 6, 7, 8}; 23 | Plane Surface(1) = {9}; 24 | 25 | Physical Curve("Inlet", 10) = {8}; 26 | Physical Curve("Outlet", 11) = {4}; 27 | Physical Curve("Anode", 12) = {6}; 28 | Physical Curve("Cathode", 13) = {2}; 29 | Physical Curve("Wall", 14) = {7, 1, 5, 3}; 30 | Physical Surface("Channel", 2) = {1}; 31 | 32 | 33 | Transfinite Curve{8,-4} = 50 Using Bump .03; 34 | Transfinite Curve{2,-6} = 100 Using Bump .1; 35 | Transfinite Curve{1,-7} = 100 Using Bump .03; 36 | Transfinite Curve{3, -5} = 100 Using Bump .03; 37 | 38 | Transfinite Surface{1} = {1, 4, 5, 8}; 39 | Recombine Surface{1}; 40 | -------------------------------------------------------------------------------- /examples/bortels_structuredquad_nondim.geo: -------------------------------------------------------------------------------- 1 | La = 5.; 2 | L = 2.; 3 | Lb = 5.; 4 | h = 1.; 5 | lc = 0.15*h; 6 | Point(1) = {0, 0, 0, lc}; 7 | Point(2) = {La, 0, 0, lc}; 8 | Point(3) = {La+L, 0, 0, lc}; 9 | Point(4) = {La+L+Lb, 0, 0, lc}; 10 | Point(5) = {La+L+Lb, h, 0, lc}; 11 | Point(6) = {La+L, h, 0, lc}; 12 | Point(7) = {La, h, 0, lc}; 13 | Point(8) = {0, h, 0, lc}; 14 | Line(1) = {1, 2}; 15 | Line(2) = {2, 3}; 16 | Line(3) = {3, 4}; 17 | Line(4) = {4, 5}; 18 | Line(5) = {5, 6}; 19 | Line(6) = {6, 7}; 20 | Line(7) = {7, 8}; 21 | Line(8) = {8, 1}; 22 | Curve Loop(9) = {1, 2, 3, 4, 5, 6, 7, 8}; 23 | Plane Surface(1) = {9}; 24 | 25 | Physical Curve("Inlet", 10) = {8}; 26 | Physical Curve("Outlet", 11) = {4}; 27 | Physical Curve("Anode", 12) = {6}; 28 | Physical Curve("Cathode", 13) = {2}; 29 | Physical Curve("Wall", 14) = {7, 1, 5, 3}; 30 | Physical Surface("Channel", 2) = {1}; 31 | 32 | 33 | Transfinite Curve{8,-4} = 50 Using Bump .03; 34 | Transfinite Curve{2,-6} = 100 Using Bump .1; 35 | Transfinite Curve{1,-7} = 100 Using Bump .03; 36 | Transfinite Curve{3, -5} = 100 Using Bump .03; 37 | 38 | Transfinite Surface{1} = {1, 4, 5, 8}; 39 | Recombine Surface{1}; 40 | -------------------------------------------------------------------------------- /examples/bortels_structuredquad_nondim_coarse1664.geo: -------------------------------------------------------------------------------- 1 | La = 5.; 2 | L = 2.; 3 | Lb = 5.; 4 | h = 1.; 5 | lc = 0.15*h; 6 | Point(1) = {0, 0, 0, lc}; 7 | Point(2) = {La, 0, 0, lc}; 8 | Point(3) = {La+L, 0, 0, lc}; 9 | Point(4) = {La+L+Lb, 0, 0, lc}; 10 | Point(5) = {La+L+Lb, h, 0, lc}; 11 | Point(6) = {La+L, h, 0, lc}; 12 | Point(7) = {La, h, 0, lc}; 13 | Point(8) = {0, h, 0, lc}; 14 | Line(1) = {1, 2}; 15 | Line(2) = {2, 3}; 16 | Line(3) = {3, 4}; 17 | Line(4) = {4, 5}; 18 | Line(5) = {5, 6}; 19 | Line(6) = {6, 7}; 20 | Line(7) = {7, 8}; 21 | Line(8) = {8, 1}; 22 | Curve Loop(9) = {1, 2, 3, 4, 5, 6, 7, 8}; 23 | Plane Surface(1) = {9}; 24 | 25 | Physical Curve("Inlet", 10) = {8}; 26 | Physical Curve("Outlet", 11) = {4}; 27 | Physical Curve("Anode", 12) = {6}; 28 | Physical Curve("Cathode", 13) = {2}; 29 | Physical Curve("Wall", 14) = {7, 1, 5, 3}; 30 | Physical Surface("Channel", 2) = {1}; 31 | 32 | 33 | Transfinite Curve{8,-4} = 17 Using Bump .03; 34 | Transfinite Curve{2,-6} = 23 Using Bump .1; 35 | Transfinite Curve{1,-7} = 22 Using Bump .03; 36 | Transfinite Curve{3, -5} = 22 Using Bump .03; 37 | 38 | Transfinite Surface{1} = {1, 4, 5, 8}; 39 | Recombine Surface{1}; 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2023 Lawrence Livermore National Laboratory 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/source/_static/references.bib: -------------------------------------------------------------------------------- 1 | @article{Taylor1973, 2 | title={A numerical solution of the {N}avier-{S}tokes equations using the finite element technique}, 3 | author={Taylor, Cedric and Hood, Paul}, 4 | journal={Computers \& Fluids}, 5 | volume={1}, 6 | number={1}, 7 | pages={73--100}, 8 | year={1973}, 9 | publisher={Elsevier} 10 | } 11 | 12 | @misc{Ladyzhenskaya1963, 13 | title={The mathematical theory of incompressible viscous flows}, 14 | author={Ladyzhenskaya, O}, 15 | year={1963}, 16 | publisher={Gordon and Breach, New York} 17 | } 18 | 19 | @article{Brezzi1974, 20 | title={On the existence, uniqueness and approximation of saddle-point problems arising from {L}agrangian multipliers}, 21 | author={Brezzi, Franco}, 22 | journal={Publications des s{\'e}minaires de math{\'e}matiques et informatique de Rennes}, 23 | number={S4}, 24 | pages={1--26}, 25 | year={1974} 26 | } 27 | 28 | @article{Babuvska1971, 29 | title={Error-bounds for finite element method}, 30 | author={Babu{\v{s}}ka, Ivo}, 31 | journal={Numerische Mathematik}, 32 | volume={16}, 33 | number={4}, 34 | pages={322--333}, 35 | year={1971}, 36 | publisher={Springer} 37 | } 38 | -------------------------------------------------------------------------------- /examples/plot_data.py: -------------------------------------------------------------------------------- 1 | import pandas 2 | import matplotlib.pyplot as plt 3 | import matplotlib.ticker as tkr 4 | 5 | df = pandas.read_csv("data.csv") 6 | 7 | print(df) 8 | 9 | 10 | def ideal_snes_slope(N): return df['num_processes'][0] * df['SNESSolve'][0] / N 11 | 12 | 13 | def ideal_pcapply_slope( 14 | N): return df['num_processes'][0] * df['PCApply'][0] / N 15 | 16 | 17 | fig = plt.figure() 18 | ax = plt.axes(xscale='log', yscale='log') 19 | 20 | ax.plot(df['num_processes'], df['SNESSolve'], marker='x') 21 | ax.plot(df['num_processes'], [ideal_snes_slope(N) 22 | for N in df['num_processes']], linestyle='dashed') 23 | 24 | # plt.plot(df['num_processes'], df['PCApply'], marker='x') 25 | # ax.plot(df['num_processes'], [ideal_pcapply_slope(N) 26 | # for N in df['num_processes']], linestyle='dashed') 27 | 28 | ax.set_yticks(df['SNESSolve']) 29 | ax.get_yaxis().set_major_formatter(tkr.ScalarFormatter()) 30 | 31 | ax.set_xticks(df['num_processes']) 32 | ax.get_xaxis().set_major_formatter(tkr.ScalarFormatter()) 33 | 34 | ax.minorticks_off() 35 | 36 | ax.set_ylabel('solve time (s)') 37 | ax.set_xlabel('number of processors') 38 | 39 | fig.savefig('foo.png') 40 | fig.clf() 41 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | This work was produced under the auspices of the U.S. Department of Energy by 2 | Lawrence Livermore National Laboratory under Contract DE-AC52-07NA27344. 3 | 4 | This work was prepared as an account of work sponsored by an agency of the 5 | United States Government. Neither the United States Government nor Lawrence 6 | Livermore National Security, LLC, nor any of their employees makes any warranty, 7 | expressed or implied, or assumes any legal liability or responsibility for the 8 | accuracy, completeness, or usefulness of any information, apparatus, product, or 9 | process disclosed, or represents that its use would not infringe privately owned 10 | rights. Reference herein to any specific commercial product, process, or service 11 | by trade name, trademark, manufacturer, or otherwise does not necessarily 12 | constitute or imply its endorsement, recommendation, or favoring by the United 13 | States Government or Lawrence Livermore National Security, LLC. The views and 14 | opinions of authors expressed herein do not necessarily state or reflect those 15 | of the United States Government or Lawrence Livermore National Security, LLC, 16 | and shall not be used for advertising or product endorsement purposes. 17 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build docs 2 | run-name: ${{ github.actor }} is building docs 3 | on: [push] 4 | jobs: 5 | build_docs: 6 | runs-on: ubuntu-latest 7 | container: 8 | image: firedrakeproject/firedrake:latest 9 | options: --user root 10 | volumes: 11 | - ${{ github.workspace }}:/home/firedrake/output 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Install Firedrake and echemfem 16 | run: | 17 | python3 -m pip install -e . 18 | pip3 install sphinx-rtd-theme sphinxcontrib-bibtex 19 | - name: Build docs 20 | run: | 21 | cd docs 22 | make html 23 | - name: Upload artifact 24 | uses: actions/upload-pages-artifact@v3 25 | with: 26 | name: github-pages 27 | path: /__w/echemfem/echemfem/docs/build/html 28 | retention-days: 1 29 | deploy: 30 | name: Deploy Github pages 31 | needs: build_docs 32 | permissions: 33 | pages: write 34 | id-token: write 35 | environment: 36 | name: github-pages 37 | url: http://llnl.github.io/echemfem 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Deploy to GitHub Pages 41 | id: deployment 42 | uses: actions/deploy-pages@v4 43 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | 3 | # -- Project information 4 | 5 | project = 'EchemFEM' 6 | copyright = u'2022-2025, Lawrence Livermore National Laboratory' 7 | author = 'Thomas Roy' 8 | 9 | release = '0.1' 10 | version = '0.0.2' 11 | 12 | # -- General configuration 13 | 14 | extensions = [ 15 | 'sphinx.ext.duration', 16 | 'sphinx.ext.doctest', 17 | 'sphinx.ext.autodoc', 18 | 'sphinx.ext.autosummary', 19 | 'sphinx.ext.intersphinx', 20 | 'sphinx.ext.napoleon', 21 | 'sphinx.ext.mathjax', 22 | 'sphinxcontrib.bibtex', 23 | ] 24 | 25 | mathjax3_config = { 26 | 'loader': {'load': ['[tex]/mhchem']}, 27 | 'tex': {'packages': {'[+]': ['mhchem']}}, 28 | } 29 | 30 | intersphinx_mapping = { 31 | 'python': ('https://docs.python.org/3/', None), 32 | 'numpy': ('https://docs.scipy.org/doc/numpy/', None), 33 | 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), 34 | 'firedrake': ('https://firedrakeproject.org/', None), 35 | } 36 | intersphinx_disabled_domains = ['std'] 37 | 38 | templates_path = ['_templates'] 39 | 40 | # -- Options for sphinxcontrib.bibtex ------------------------------------ 41 | bibtex_bibfiles = ['demos/demo_references.bib', '_static/references.bib'] 42 | 43 | # -- Options for HTML output 44 | 45 | html_theme = 'sphinx_rtd_theme' 46 | 47 | # -- Options for EPUB output 48 | epub_show_urls = 'footnote' 49 | 50 | -------------------------------------------------------------------------------- /examples/submitter_mms.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | use diagnostics; 6 | use Getopt::Long; 7 | 8 | my $N = 0; 9 | my $np = 0; 10 | my $ref_levels = 0; 11 | my $dry = 0; 12 | my $flamegraph = 0; 13 | my $degree = 1; 14 | my $statsfile = 'data_mms.csv'; 15 | my $csolver = 'asm'; 16 | 17 | GetOptions("N=i" => \$N, 18 | "np=i" => \$np, 19 | "ref=i" => \$ref_levels, 20 | "csolver=s" => \$csolver, 21 | "p=i" => \$degree, 22 | "statsfile=s" => \$statsfile, 23 | "flamegraph" => \$flamegraph, 24 | "dry" => \$dry) or die("Error in command line arguments\n"); 25 | 26 | my $jobid = sprintf("mms_%s_%s_%s_%s_%s", $N, $np, $ref_levels, $csolver, $degree); 27 | my $filename = sprintf("%s.job", $jobid); 28 | 29 | my $log_base = sprintf("%s_", $jobid); 30 | my $log_number = 0; 31 | my $logname = $log_base . sprintf("%#.2o", $log_number); 32 | my $logfilename = $logname . '.log'; 33 | 34 | # while (-e $logfilename) { 35 | # $log_number = $log_number + 1; 36 | # $logname = $log_base . sprintf("%#.2o", $log_number); 37 | # } 38 | 39 | my $str = <', $filename) or die $!; 66 | print FH $str; 67 | close(FH); 68 | 69 | system("rm $logfilename"); 70 | system(sprintf("sbatch %s.job", $jobid)); 71 | } 72 | -------------------------------------------------------------------------------- /examples/submitter.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | use diagnostics; 6 | use Getopt::Long; 7 | 8 | my $N = 0; 9 | my $np = 0; 10 | my $ref_levels = 0; 11 | my $dry = 0; 12 | my $flamegraph = 0; 13 | my $degree = 1; 14 | my $statsfile = 'data.csv'; 15 | my $csolver = 'asm'; 16 | 17 | GetOptions("N=i" => \$N, 18 | "np=i" => \$np, 19 | "ref=i" => \$ref_levels, 20 | "csolver=s" => \$csolver, 21 | "p=i" => \$degree, 22 | "statsfile=s" => \$statsfile, 23 | "flamegraph" => \$flamegraph, 24 | "dry" => \$dry) or die("Error in command line arguments\n"); 25 | 26 | my $jobid = sprintf("echem_%s_%s_%s_%s_%s", $N, $np, $ref_levels, $csolver, $degree); 27 | my $filename = sprintf("%s.job", $jobid); 28 | 29 | my $log_base = sprintf("%s_", $jobid); 30 | my $log_number = 0; 31 | my $logname = $log_base . sprintf("%#.2o", $log_number); 32 | my $logfilename = $logname . '.log'; 33 | 34 | # while (-e $logfilename) { 35 | # $log_number = $log_number + 1; 36 | # $logname = $log_base . sprintf("%#.2o", $log_number); 37 | # } 38 | 39 | my $str = <', $filename) or die $!; 66 | print FH $str; 67 | close(FH); 68 | 69 | system("rm $logfilename"); 70 | system(sprintf("sbatch %s.job", $jobid)); 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EchemFEM 2 | 3 | [![DOI](https://zenodo.org/badge/513600791.svg)](https://zenodo.org/badge/latestdoi/513600791) 4 | 5 | [![DOI](https://joss.theoj.org/papers/10.21105/joss.06531/status.svg)](https://doi.org/10.21105/joss.06531) 6 | 7 | This code provides Finite Element solvers for electrochemical transport. 8 | Both continuous Galerkin (CG) and discontinuous Galerkin (DG) schemes are provided. The DG scheme for electroneutral Nernst-Planck is described in [Roy et al., 2023](https://doi.org/10.1016/j.jcp.2022.111859). The CG scheme uses SUPG for the advection-migration term. 9 | 10 | The following transport mechanisms are available: diffusion, advection, electromigration. EchemFEM supports both non-porous and porous cases. The ionic potential can either be described using an electroneutrality constraint or a Poisson equation. In the porous case, the electronic potential can be described by a Poisson equation. 11 | Some finite size effects are also implemented. For example, the generalized modified Poisson-Nernst-Planck model (GMPNP) is used in the examples of [FireCat](https://github.com/LLNL/firecat), coupling it with a microkinetics model. 12 | 13 | 14 | LLNL-CODE-837342 15 | 16 | ## Documentation 17 | 18 | User guide and API documentation can be found [here](https://software.llnl.gov/echemfem/index.html). 19 | 20 | ## Installation 21 | 22 | Please install the open-source finite element library [Firedrake](https://www.firedrakeproject.org/download.html). 23 | 24 | To install EchemFEM, simply run the following in the parent echemfem folder: 25 | ``` 26 | pip install -e . 27 | ``` 28 | The documentation has more details about installation and running the code. 29 | 30 | ## Citing 31 | 32 | To cite EchemFEM, please cite the [JOSS paper](https://doi.org/10.21105/joss.06531). For the DG discretization, please cite the [JCP paper](https://doi.org/10.1016/j.jcp.2022.111859). 33 | -------------------------------------------------------------------------------- /docs/source/user_guide/index.rst: -------------------------------------------------------------------------------- 1 | User Guide 2 | ========== 3 | 4 | This guide will help a user understand existing examples, and design new ones. 5 | 6 | The first step of writing an EchemFEM script is to import the ``EchemSolver`` class: 7 | 8 | .. code-block:: 9 | 10 | from echemfem import EchemSolver 11 | 12 | or for a transient simulation, the ``TransientEchemSolver`` class: 13 | 14 | .. code-block:: 15 | 16 | from echemfem import TransientEchemSolver 17 | 18 | These are abstract classes, which we will use as the base class for a specific model. 19 | To create a customized solver, the user should set the following inputs: 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | 24 | conc_params 25 | physical_params 26 | boundary_conditions 27 | echem_params 28 | homog_params 29 | 30 | Here is a barebone example of how a user might define their own solver. 31 | Several concrete examples can be found in `echemfem/examples 32 | `_. 33 | 34 | .. code-block:: 35 | 36 | class MySolver(EchemSolver): # or TransientEchemSolver 37 | 38 | def __init__(self): 39 | # Here define all custom parameters that may require attributes 40 | 41 | super().__init__(...) # with appropriate arguments 42 | 43 | def set_boundary_markers(self): 44 | self.boundary_markers = ... 45 | 46 | # and some other methods that need to be defined 47 | 48 | Then, to run the simulation, create the object and run the ``solve`` method. 49 | 50 | .. code-block:: 51 | 52 | solver = MySolver() 53 | solver.solve() 54 | 55 | For transient cases, a temporal grid defined as a ``list`` or 56 | ``numpy.ndarray`` must be provided. For example, 57 | 58 | .. code-block:: 59 | 60 | import numpy as np 61 | times = np.linspace(0, 11, 1) # 10 timesteps of size 0.1 62 | solver.solve(times) 63 | 64 | To generate non-trivial velocity fields, some fluid flow solvers are available: 65 | 66 | .. toctree:: 67 | :maxdepth: 2 68 | 69 | fluid_solver 70 | 71 | -------------------------------------------------------------------------------- /tests/test_diffusion.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import EchemSolver 4 | 5 | 6 | class DiffusionSolver(EchemSolver): 7 | def __init__(self, N, family="DG"): 8 | mesh = UnitSquareMesh(N, N, quadrilateral=True) 9 | conc_params = [] 10 | 11 | x, y = SpatialCoordinate(mesh) 12 | C1ex = sin(x) + cos(y) 13 | C2ex = cos(x) + sin(y) 14 | self.C1ex = C1ex 15 | self.C2ex = C2ex 16 | 17 | def f(C): 18 | f1 = - div(grad(C1ex)) 19 | f2 = - div(grad(C2ex)) 20 | return [f1, f2] 21 | 22 | conc_params.append({"name": "C1", 23 | "diffusion coefficient": 1.0, 24 | "bulk": C1ex, 25 | }) 26 | 27 | conc_params.append({"name": "C2", 28 | "diffusion coefficient": 1.0, 29 | "bulk": C2ex, 30 | }) 31 | physical_params = {"flow": ["diffusion"], 32 | "bulk reaction": f, 33 | } 34 | 35 | super().__init__(conc_params, physical_params, mesh, family=family) 36 | 37 | def set_boundary_markers(self): 38 | self.boundary_markers = {"bulk dirichlet": (1, 2, 3, 4), 39 | } 40 | 41 | 42 | def test_convergence(): 43 | err_old = 1e6 44 | for i in range(5): 45 | solver = DiffusionSolver(2**(i + 1)) 46 | solver.setup_solver() 47 | solver.solve() 48 | c1, c2 = solver.u.subfunctions 49 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2) 50 | assert err < 0.29 * err_old 51 | err_old = err 52 | 53 | 54 | def test_convergence_CG(): 55 | err_old = 1e6 56 | for i in range(5): 57 | solver = DiffusionSolver(2**(i + 1), family="CG") 58 | solver.setup_solver() 59 | solver.solve() 60 | c1, c2 = solver.u.subfunctions 61 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2) 62 | assert err < 0.26 * err_old 63 | err_old = err 64 | -------------------------------------------------------------------------------- /tests/test_diffusion_cylindrical.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import EchemSolver 4 | 5 | 6 | class DiffusionSolver(EchemSolver): 7 | def __init__(self, N, family="DG"): 8 | mesh = UnitSquareMesh(N, N, quadrilateral=True) 9 | conc_params = [] 10 | 11 | r, z = SpatialCoordinate(mesh) 12 | C1ex = sin(z) + cos(r) 13 | C2ex = cos(z) * cos(r) + sin(z) * cos(r) 14 | # need dC/dr = 0 at r=0 15 | self.C1ex = C1ex 16 | self.C2ex = C2ex 17 | 18 | def f(C): 19 | f1 = sin(r)/r + cos(r) + sin(z) 20 | f2 = (r * cos(r) + sin(r)) * (cos(z) + sin(z))/r + cos(r) * (cos(z) + sin(z)) 21 | return [f1, f2] 22 | 23 | conc_params.append({"name": "C1", 24 | "diffusion coefficient": 1.0, 25 | "bulk": C1ex, 26 | }) 27 | 28 | conc_params.append({"name": "C2", 29 | "diffusion coefficient": 1.0, 30 | "bulk": C2ex, 31 | }) 32 | physical_params = {"flow": ["diffusion"], 33 | "bulk reaction": f, 34 | } 35 | 36 | super().__init__(conc_params, physical_params, mesh, family=family, cylindrical=True) 37 | 38 | def set_boundary_markers(self): 39 | self.boundary_markers = {"bulk dirichlet": (2, 3, 4), 40 | } 41 | 42 | 43 | def test_convergence(): 44 | err_old = 1e6 45 | for i in range(5): 46 | solver = DiffusionSolver(2**(i + 1)) 47 | solver.setup_solver() 48 | solver.solve() 49 | c1, c2 = solver.u.subfunctions 50 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2) 51 | assert err < 0.29 * err_old 52 | err_old = err 53 | 54 | 55 | def test_convergence_CG(): 56 | err_old = 1e6 57 | for i in range(5): 58 | solver = DiffusionSolver(2**(i + 1), family="CG") 59 | solver.setup_solver() 60 | solver.solve() 61 | c1, c2 = solver.u.subfunctions 62 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2) 63 | assert err < 0.26 * err_old 64 | err_old = err 65 | -------------------------------------------------------------------------------- /docs/source/user_guide/conc_params.rst: -------------------------------------------------------------------------------- 1 | Concentration Parameters 2 | ======================== 3 | 4 | The physical parameters of each species need to be provided in 5 | :attr:`echemfem.EchemSolver.conc_params`, a list containing one dictionary for 6 | each species. Below is a list of different keys that can appear in each dictionary. 7 | Only the first key ``"name"`` is required for every case. 8 | 9 | * :Key: ``"name"`` 10 | :Type: :py:class:`str` 11 | :Description: Species name. E.g. ``"CO2"`` 12 | :Uses: * Name of field in pvd output 13 | * Used to get index in the solution vector. 14 | * :Key: ``"bulk"`` 15 | :Type: :py:class:`float`, firedrake expression 16 | :Description: Concentration at the "bulk". 17 | :Uses: * Initial guess for the concentrations in :meth:`echemfem.EchemSolver.setup_solver` 18 | * Concentration value for ``"bulk dirichlet"``, ``"bulk"``, and ``"inlet"`` :doc:`boundary_conditions` 19 | * :Key: ``"gas"`` 20 | :Type: :py:class:`float`, firedrake expression 21 | :Description: Concentration at the gas interface for the ``"gas"`` :doc:`boundary_conditions`. 22 | * :Key: ``"diffusion coefficient"`` 23 | :Type: :py:class:`float`, firedrake expression 24 | :Description: Diffusion coeffcient of the species. 25 | * :Key: ``"z"`` 26 | :Type: :py:class:`float`, :py:class:`int`, firedrake expression 27 | :Description: Charge number of the species. 28 | * :Key: ``"mass transfer coefficient"`` 29 | :Type: :py:class:`float`, firedrake expression 30 | :Description: Coefficient used in the ``"bulk"`` :doc:`boundary_conditions`. 31 | * :Key: ``"solvated diameter"`` 32 | :Type: :py:class:`float`, firedrake expression 33 | :Description: Solvated diameter of the ionic species if using ``"finite size"`` in :doc:`physical_params`. 34 | * :Key: ``"eliminated"`` 35 | :Type: :py:class:`bool` 36 | :Description: The species to be eliminated via the electroneutrality approximation if using ``"electroneutrality"`` in :doc:`physical_params`. 37 | * :Key: ``"C_ND"`` 38 | :Type: :py:class:`float` 39 | :Description: For the nondimensionalization used in `Roy et al, 2022 `_. These weights are used to get the nondimensional charge conservation equation. In the paper :math:`C_{ND} = c_k^\mathrm{in} / c_\mathrm{ref}`. 40 | 41 | -------------------------------------------------------------------------------- /tests/test_diffusion_finite_size.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import EchemSolver 4 | 5 | 6 | class DiffusionSolver(EchemSolver): 7 | def __init__(self, N, family="DG"): 8 | mesh = UnitSquareMesh(N, N, quadrilateral=True) 9 | conc_params = [] 10 | 11 | x, y = SpatialCoordinate(mesh) 12 | C1ex = sin(x) + cos(y) 13 | C2ex = cos(x) + sin(y) 14 | self.C1ex = C1ex 15 | self.C2ex = C2ex 16 | 17 | def f(C): 18 | num = grad(C1ex) + grad(C2ex) 19 | den = 1.0 - C1ex - C2ex 20 | f1 = - div(grad(C1ex) + C1ex * num/den) 21 | f2 = - div(grad(C2ex) + C2ex * num/den) 22 | return [f1, f2] 23 | 24 | conc_params.append({"name": "C1", 25 | "diffusion coefficient": 1.0, 26 | "bulk": C1ex, 27 | "solvated diameter": 1.0 # m 28 | }) 29 | 30 | conc_params.append({"name": "C2", 31 | "diffusion coefficient": 1.0, 32 | "bulk": C2ex, 33 | "solvated diameter": 1.0 # m 34 | }) 35 | physical_params = {"flow": ["diffusion", "finite size"], 36 | "vacuum permittivity": 1.0, # F/m 37 | "relative permittivity": 1.0, 38 | "Avogadro constant": 1.0, # 1/mol 39 | "bulk reaction": f, 40 | } 41 | 42 | super().__init__(conc_params, physical_params, mesh, family=family) 43 | 44 | def set_boundary_markers(self): 45 | self.boundary_markers = {"bulk dirichlet": (1, 2, 3, 4), 46 | } 47 | 48 | 49 | def test_DG_error(): 50 | with pytest.raises(NotImplementedError): 51 | solver = DiffusionSolver(2, family="DG") 52 | 53 | 54 | def test_convergence_CG(): 55 | err_old = 1e6 56 | for i in range(5): 57 | solver = DiffusionSolver(2**(i + 1), family="CG") 58 | solver.setup_solver() 59 | solver.solve() 60 | c1, c2 = solver.u.subfunctions 61 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2) 62 | assert err < 0.26 * err_old 63 | err_old = err 64 | -------------------------------------------------------------------------------- /docs/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart Guide 2 | ================ 3 | 4 | .. _installation: 5 | 6 | Installation 7 | ------------ 8 | 9 | First, please install the open-source finite element library `Firedrake `_. 10 | On a Mac with Homebrew installed or on an Ubuntu workstation with sudo access, it can be installed using the default configuration as detailed `here `_. 11 | To get started quickly, a `Docker image `_ is also available. 12 | Note that you may need to increase the default memory allocation for Docker in order to run some of the EchemFEM examples. 13 | 14 | EchemFEM is hosted on `GitHub `_, and should be cloned from there. 15 | 16 | To use EchemFEM, first install it using pip within the Firedrake virtual environment. From the echemfem parent directory: 17 | 18 | .. code-block:: console 19 | 20 | (firedrake) $ pip install -e . 21 | 22 | To test your installation, you can run the tests in `echemfem/tests `_ using pytest. 23 | This can take some time, so it is probably enough to run this test: 24 | 25 | .. code-block:: console 26 | 27 | (firedrake) $ pytest test_advection_diffusion_migration.py 28 | 29 | Alternatively, you can run some of the examples below. 30 | 31 | Running Examples 32 | ---------------- 33 | 34 | To get started, a demo for a flow reactor is available :doc:`here `. 35 | Several examples can be found in `echemfem/examples `_. 36 | Examples using the generalized modified Poisson-Nernst-Planck model (GMPNP) can be found in `FireCat `_, where it is coupled with a microkinetics model. 37 | 38 | To run an example: 39 | 40 | .. code-block:: console 41 | 42 | (firedrake) $ python example_name.py 43 | 44 | And to run it in parallel with ``n_proc`` processors: 45 | 46 | .. code-block:: console 47 | 48 | (firedrake) $ mpiexec -n n_proc python example_name.py 49 | 50 | Visualization 51 | ------------- 52 | 53 | The solution fields are stored for visualization with `Paraview `_ in ``results/collection.pvd``. 54 | Alternatively, 1D and 2D functions can be visualized using `matplotlib `_. 55 | More about `Firedrake visualization `_. 56 | -------------------------------------------------------------------------------- /examples/tworxn.py: -------------------------------------------------------------------------------- 1 | from firedrake import * 2 | from echemfem import EchemSolver, RectangleBoundaryLayerMesh 3 | 4 | """ 5 | A 2D flow past the electrode toy model with two species and 6 | advection-diffusion. Taken from 7 | Lin, T.Y., Baker, S.E., Duoss, E.B. and Beck, V.A., 2021. Analysis of 8 | the Reactive CO2 Surface Flux in Electrocatalytic Aqueous Flow 9 | Reactors. Industrial & Engineering Chemistry Research, 60(31), 10 | pp.11824-11833. 11 | """ 12 | 13 | class CarbonateSolver(EchemSolver): 14 | def __init__(self): 15 | 16 | Ly = 0.1 17 | Lx = 1. 18 | 19 | mesh = RectangleBoundaryLayerMesh(50, 50, Lx, Ly, 50, 1e-1, Ly_bdlayer=5e-3, boundary=(3, 1,)) 20 | 21 | C_1_inf = 1. 22 | C_2_inf = Constant(0) 23 | 24 | def bulk_reaction(y): 25 | yC1 = y[0] 26 | yC2 = y[1] 27 | dC1 = -(1.)*(1e3)*yC1*yC2 28 | dC2 = -(2.)*(1e3)*yC1*yC2 29 | return [dC1, dC2] 30 | 31 | conc_params = [] 32 | conc_params.append({"name": "C1", 33 | "diffusion coefficient": 1., 34 | "bulk": C_1_inf, 35 | }) 36 | 37 | conc_params.append({"name": "C2", 38 | "diffusion coefficient": 1., 39 | "bulk": C_2_inf, 40 | }) 41 | 42 | physical_params = {"flow": ["advection", "diffusion"], 43 | "bulk reaction": bulk_reaction, 44 | } 45 | 46 | super().__init__(conc_params, physical_params, mesh, family="DG") 47 | 48 | def neumann(self, C, conc_params, u): 49 | name = conc_params["name"] 50 | 51 | if name == "C1": 52 | return -(1.e6)*u[0] 53 | if name == "C2": 54 | return 2.*(1.e6)*u[0] 55 | 56 | def set_boundary_markers(self): 57 | self.boundary_markers = {"inlet": (1), 58 | "bulk dirichlet": (4), 59 | "outlet": (2,), 60 | "neumann": (3,), 61 | } 62 | 63 | def set_velocity(self): 64 | _, y = SpatialCoordinate(self.mesh) 65 | self.vel = as_vector([(1.e5)*y, Constant(0)]) 66 | 67 | 68 | solver = CarbonateSolver() 69 | solver.setup_solver() 70 | solver.solve() 71 | 72 | n = FacetNormal(solver.mesh) 73 | cC1, _, = solver.u.subfunctions 74 | flux = assemble(dot(grad(cC1), n) * ds(3)) 75 | print("Sh = %f" % flux) 76 | -------------------------------------------------------------------------------- /tests/test_cylindricalmeasure.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import CylindricalMeasure 4 | 5 | mesh = UnitSquareMesh(2, 2) 6 | x, y = SpatialCoordinate(mesh) 7 | V = FunctionSpace(mesh, "DP", 0) 8 | f0 = Function(V).interpolate(conditional(Or(And(x < .4, 9 | And(y > .4, y < .6)), 10 | And(And(x > .6, x < .9), 11 | And(y > .6, y < .9))), 1., 0.)) 12 | f1 = Function(V).interpolate(conditional(Or(And(x < .4, 13 | And(y > .4, y < .6)), 14 | And(And(x > .6, x < .9), 15 | And(y > .6, y < .9))), 0., 1.)) 16 | VS = FunctionSpace(mesh, "HDiv Trace", 0) 17 | f0_S = Function(VS).interpolate(conditional(Or(And(x < .4, 18 | And(y > .4, y < .6)), 19 | And(And(x > .6, x < .9), 20 | And(y > .6, y < .9))), 1., 0.)) 21 | f1_S = Function(VS).interpolate(conditional(Or(And(x < .4, 22 | And(y > .4, y < .6)), 23 | And(And(x > .6, x < .9), 24 | And(y > .6, y < .9))), 0., 1.)) 25 | mesh = RelabeledMesh(mesh, 26 | [f0, f1, f0_S, f1_S], 27 | [1, 2, 1, 2]) 28 | Vc = FunctionSpace(mesh, "CG", 1) 29 | u = Function(Vc).assign(Constant(1)) 30 | r, z = SpatialCoordinate(mesh) 31 | _dx = CylindricalMeasure(r, 'cell') 32 | _ds = CylindricalMeasure(r, 'exterior_facet') 33 | _dS = CylindricalMeasure(r, 'interior_facet') 34 | 35 | 36 | def test_dx(): 37 | a = assemble(u * r * dx) 38 | ar = assemble(u * _dx) 39 | assert (abs(a-ar) < 1e-15) 40 | 41 | 42 | def test_dx_subs(): 43 | a = assemble(u * r * dx((1, 2,))) 44 | ar = assemble(u * _dx((1, 2,))) 45 | assert (abs(a-ar) < 1e-15) 46 | 47 | 48 | def test_ds(): 49 | a = assemble(u * r * ds) 50 | ar = assemble(u * _ds) 51 | assert (abs(a-ar) < 1e-15) 52 | 53 | 54 | def test_ds_subs(): 55 | a = assemble(u * r * ds((2, 3, 4,))) 56 | ar = assemble(u * _ds((2, 3, 4,))) 57 | assert (abs(a-ar) < 1e-15) 58 | 59 | 60 | def test_dS(): 61 | a = assemble(u * r * dS) 62 | ar = assemble(u * _dS) 63 | assert (abs(a-ar) < 1e-15) 64 | 65 | 66 | def test_dS_subs(): 67 | a = assemble(u * r * dS((1, 2,))) 68 | ar = assemble(u * _dS((1, 2,))) 69 | assert (abs(a-ar) < 1e-15) 70 | -------------------------------------------------------------------------------- /tests/test_advection.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import EchemSolver 4 | 5 | 6 | class AdvectionSolver(EchemSolver): 7 | def __init__(self, N, extruded=False): 8 | if extruded: 9 | plane_mesh = UnitSquareMesh(N, N, quadrilateral=True) 10 | mesh = ExtrudedMesh(plane_mesh, N, layer_height=1.0 / N) 11 | x, y, _ = SpatialCoordinate(mesh) 12 | else: 13 | mesh = UnitSquareMesh(N, N, quadrilateral=True) 14 | x, y = SpatialCoordinate(mesh) 15 | 16 | conc_params = [] 17 | 18 | C1ex = sin(x) + cos(y) 19 | C2ex = cos(x) + sin(y) 20 | self.C1ex = C1ex 21 | self.C2ex = C2ex 22 | 23 | def f(C): 24 | f1 = div(self.vel * C1ex) 25 | f2 = div(self.vel * C2ex) 26 | return [f1, f2] 27 | 28 | conc_params.append({"name": "C1", 29 | "bulk": C1ex, 30 | }) 31 | 32 | conc_params.append({"name": "C2", 33 | "bulk": C2ex, 34 | }) 35 | physical_params = {"flow": ["advection"], 36 | "bulk reaction": f, 37 | "v_avg": 1., 38 | } 39 | 40 | super().__init__(conc_params, physical_params, mesh) 41 | 42 | def set_boundary_markers(self): 43 | self.boundary_markers = {"inlet": (1, 2, 3, 4), 44 | "outlet": (1, 2, 3, 4), 45 | } 46 | 47 | def set_velocity(self): 48 | if self.mesh.layers is not None: 49 | _, y, _ = SpatialCoordinate(self.mesh) 50 | else: 51 | _, y = SpatialCoordinate(self.mesh) 52 | 53 | h = 1.0 54 | x_vel = 6. * self.physical_params["v_avg"] / h**2 * y * (h - y) 55 | 56 | if self.mesh.layers is not None: 57 | self.vel = as_vector( 58 | (x_vel, Constant(0.), Constant(0.))) 59 | else: 60 | self.vel = as_vector( 61 | (x_vel, Constant(0.))) 62 | 63 | 64 | def test_convergence(extruded=False): 65 | err_old = 1e6 66 | if extruded: 67 | n = 3 68 | else: 69 | n = 5 70 | for i in range(n): 71 | solver = AdvectionSolver(2**(i + 1), extruded) 72 | solver.setup_solver() 73 | solver.solve() 74 | c1, c2 = solver.u.subfunctions 75 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2) 76 | assert err < 0.26 * err_old 77 | err_old = err 78 | 79 | 80 | def test_convergence_extruded(): 81 | test_convergence(extruded=True) 82 | -------------------------------------------------------------------------------- /examples/squares.geo: -------------------------------------------------------------------------------- 1 | boundary_layer=5e-3; 2 | 3 | size_boundary=0.1; 4 | size_bas=boundary_layer/8; 5 | size_bottom=boundary_layer/40; 6 | height=0.3; 7 | nb_square=5; 8 | Amplitude=0.05; 9 | discretisation_pts=80; 10 | //+ 11 | Point(1) = {1, 0, 0, size_bottom}; 12 | //+ 13 | Point(2) = {1, height, 0, size_boundary}; 14 | //+ 15 | Point(3) = {0, height, 0, size_boundary}; 16 | //+ 17 | Point(4) = {0, 0, 0, size_bottom}; 18 | Point(5) = {0,boundary_layer,0,size_bas}; 19 | Point(6) = {1,boundary_layer,0,size_bas}; 20 | 21 | k=5000; 22 | For i In {1:nb_square} 23 | 24 | Point(k) = {(1/4)/nb_square+(i-1)/nb_square+boundary_layer,boundary_layer,0,size_bas}; 25 | If(i>1) 26 | Line(k-1)={k-1,k}; 27 | EndIf 28 | Point(k+1) = {(1/4)/nb_square+(i-1)/nb_square+boundary_layer,boundary_layer-Amplitude,0,size_bas}; 29 | Point(k+2) = {(3/4)/nb_square+(i-1)/nb_square-boundary_layer,boundary_layer-Amplitude,0,size_bas}; 30 | Point(k+3) = {(3/4)/nb_square+(i-1)/nb_square-boundary_layer,boundary_layer,0,size_bas}; 31 | Line(k)={k,k+1}; 32 | Line(k+1)={k+1,k+2}; 33 | Line(k+2)={k+2,k+3}; 34 | k=k+4; 35 | EndFor 36 | Line(k-1)={k-1,6}; 37 | Line(4999)={5,5000}; 38 | max=k-1; 39 | 40 | 41 | //+ 42 | Line(1) = {1, 6}; 43 | //+ 44 | Line(2) = {6, 2}; 45 | //+ 46 | Line(3) = {2, 3}; 47 | Line(4)={3,5}; 48 | Line(5)={5,4}; 49 | //+ 50 | k=7; 51 | 52 | 53 | linep=7; 54 | linec=1000; 55 | list={5,6}; 56 | 57 | For i In {1:nb_square} 58 | Point(k) = {(1/4)/nb_square+(i-1)/nb_square,0,0,size_bottom}; 59 | If(i>1) 60 | Line(linec)={k-1,k}; 61 | list={list[],linec}; 62 | EndIf 63 | Point(k+1) = {(1/4)/nb_square+(i-1)/nb_square,-Amplitude,0,size_bottom}; 64 | Point(k+2) = {(3/4)/nb_square+(i-1)/nb_square,-Amplitude,0,size_bottom}; 65 | Point(k+3) = {(3/4)/nb_square+(i-1)/nb_square,0,0,size_bottom}; 66 | Line(linec+1)={k,k+1}; 67 | Line(linep+1)={k+1,k+2}; 68 | Line(linec+2)={k+2,k+3}; 69 | list={list[],linec+1,linep+1,linec+2}; 70 | k=k+4; 71 | linep=linep+1; 72 | linec=linec+3; 73 | EndFor 74 | Line(6)={4,7}; 75 | Line(k-1)={k-1,1}; 76 | //+ 77 | Physical Curve("Cathode",11)={1001:linec-1}; 78 | Physical Curve("Pore",10)={8:linep}; 79 | Physical Curve("VerEdgeInlet", 12) = {4:5}; 80 | Physical Curve("VerEdgeOutlet", 14) = {1,2}; 81 | //+ 82 | Physical Curve("HorEdgeTop",13)={3}; 83 | 84 | Curve Loop(1)={2,3,4,4999:max}; 85 | //Curve Loop(2)={1,-max:-4999,5,6:k-1}; 86 | NumPoints = #list[]; 87 | 88 | Printf("The Array Contents are") ; 89 | For index In {0:NumPoints-1} 90 | Printf("%g ",list[index]) ; 91 | EndFor 92 | Curve Loop(2)={1,-max:-4999,list[],k-1}; 93 | Plane Surface(1)={1}; 94 | Plane Surface(2)={2}; 95 | Physical Surface(14)={1,2}; 96 | 97 | Mesh 2; 98 | 99 | -------------------------------------------------------------------------------- /examples/squares_small.geo: -------------------------------------------------------------------------------- 1 | boundary_layer=5e-3; 2 | 3 | size_boundary=0.1; 4 | size_bas=boundary_layer/4; 5 | size_bottom=boundary_layer/20; 6 | height=0.3; 7 | nb_square=2; 8 | Amplitude=0.05; 9 | //+ 10 | Point(1) = {1, 0, 0, size_bottom}; 11 | //+ 12 | Point(2) = {1, height, 0, size_boundary}; 13 | //+ 14 | Point(3) = {0, height, 0, size_boundary}; 15 | //+ 16 | Point(4) = {0, 0, 0, size_bottom}; 17 | Point(5) = {0,boundary_layer,0,size_bas}; 18 | Point(6) = {1,boundary_layer,0,size_bas}; 19 | 20 | k=5000; 21 | For i In {1:nb_square} 22 | 23 | Point(k) = {(1/4)/nb_square+(i-1)/nb_square+boundary_layer,boundary_layer,0,size_bas}; 24 | If(i>1) 25 | Line(k-1)={k-1,k}; 26 | EndIf 27 | Point(k+1) = {(1/4)/nb_square+(i-1)/nb_square+boundary_layer,boundary_layer-Amplitude,0,size_bas}; 28 | Point(k+2) = {(3/4)/nb_square+(i-1)/nb_square-boundary_layer,boundary_layer-Amplitude,0,size_bas}; 29 | Point(k+3) = {(3/4)/nb_square+(i-1)/nb_square-boundary_layer,boundary_layer,0,size_bas}; 30 | Line(k)={k,k+1}; 31 | Line(k+1)={k+1,k+2}; 32 | Line(k+2)={k+2,k+3}; 33 | k=k+4; 34 | EndFor 35 | Line(k-1)={k-1,6}; 36 | Line(4999)={5,5000}; 37 | max=k-1; 38 | 39 | 40 | //+ 41 | Line(1) = {1, 6}; 42 | //+ 43 | Line(2) = {6, 2}; 44 | //+ 45 | Line(3) = {2, 3}; 46 | Line(4)={3,5}; 47 | Line(5)={5,4}; 48 | //+ 49 | k=7; 50 | 51 | 52 | linep=7; 53 | linec=1000; 54 | list={5,6}; 55 | 56 | For i In {1:nb_square} 57 | Point(k) = {(1/4)/nb_square+(i-1)/nb_square,0,0,size_bottom}; 58 | If(i>1) 59 | Line(linec)={k-1,k}; 60 | list={list[],linec}; 61 | EndIf 62 | Point(k+1) = {(1/4)/nb_square+(i-1)/nb_square,-Amplitude,0,size_bottom}; 63 | Point(k+2) = {(3/4)/nb_square+(i-1)/nb_square,-Amplitude,0,size_bottom}; 64 | Point(k+3) = {(3/4)/nb_square+(i-1)/nb_square,0,0,size_bottom}; 65 | Line(linec+1)={k,k+1}; 66 | Line(linep+1)={k+1,k+2}; 67 | Line(linec+2)={k+2,k+3}; 68 | list={list[],linec+1,linep+1,linec+2}; 69 | k=k+4; 70 | linep=linep+1; 71 | linec=linec+3; 72 | EndFor 73 | Line(6)={4,7}; 74 | Line(k-1)={k-1,1}; 75 | //+ 76 | Physical Curve("Cathode", 11)={1001:linec-1}; 77 | Physical Curve("Pore", 10)={8:linep}; 78 | Physical Curve("VerEdgeInlet", 12) = {4:5}; 79 | Physical Curve("VerEdgeOutlet", 14) = {1,2}; 80 | //+ 81 | Physical Curve("HorEdgeTop", 13) = {3}; 82 | Physical Curve("Wall", 15) = {6,14}; 83 | 84 | Curve Loop(1)={2,3,4,4999:max}; 85 | //Curve Loop(2)={1,-max:-4999,5,6:k-1}; 86 | NumPoints = #list[]; 87 | 88 | Printf("The Array Contents are") ; 89 | For index In {0:NumPoints-1} 90 | Printf("%g ",list[index]) ; 91 | EndFor 92 | Curve Loop(2)={1,-max:-4999,list[],k-1}; 93 | Plane Surface(1)={1}; 94 | Plane Surface(2)={2}; 95 | Physical Surface(14)={1,2}; 96 | 97 | Mesh 2; 98 | 99 | -------------------------------------------------------------------------------- /tests/test_heat_equation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import TransientEchemSolver 4 | import numpy as np 5 | 6 | 7 | class HeatEquationSolver(TransientEchemSolver): 8 | def __init__(self, N, family="DG"): 9 | mesh = UnitSquareMesh(N, N, quadrilateral=True) 10 | conc_params = [] 11 | 12 | self.time = Constant(0) # for now, need to define this here 13 | t = self.time 14 | x, y = SpatialCoordinate(mesh) 15 | C1ex = (sin(x) + cos(y)) * exp(t) 16 | C2ex = (cos(x) + sin(y)) * exp(-t) 17 | self.C1ex = C1ex 18 | self.C2ex = C2ex 19 | 20 | def f(C): 21 | f1 = C1ex - div(grad(C1ex)) 22 | f2 = -C2ex - div(grad(C2ex)) 23 | return [f1, f2] 24 | 25 | conc_params.append({"name": "C1", 26 | "diffusion coefficient": 1.0, 27 | "bulk": C1ex, 28 | }) 29 | 30 | conc_params.append({"name": "C2", 31 | "diffusion coefficient": 1.0, 32 | "bulk": C2ex, 33 | }) 34 | physical_params = {"flow": ["diffusion"], 35 | "bulk reaction": f, 36 | } 37 | 38 | super().__init__(conc_params, physical_params, mesh, family=family) 39 | 40 | def set_boundary_markers(self): 41 | self.boundary_markers = {"bulk dirichlet": (1, 2, 3, 4,), 42 | } 43 | 44 | 45 | def test_convergence(): 46 | err_old = 1e6 47 | for i in range(4): 48 | solver = HeatEquationSolver(2**(i + 1)) 49 | solver.setup_solver() 50 | times = np.linspace(0, 1, 1+2**(2*(i+1))) 51 | solver.solve(times) 52 | c1, c2 = solver.u.subfunctions 53 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2) 54 | assert err < 0.27 * err_old 55 | err_old = err 56 | 57 | 58 | def test_convergence_CG(): 59 | err_old = 1e6 60 | for i in range(5): 61 | solver = HeatEquationSolver(2**(i + 1), family="CG") 62 | solver.setup_solver() 63 | times = np.linspace(0, 1, 1+2**(2*i)) 64 | solver.solve(times) 65 | c1, c2 = solver.u.subfunctions 66 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2) 67 | assert err < 0.26 * err_old 68 | err_old = err 69 | 70 | 71 | def test_convergence_BE(): 72 | err_old = 1e6 73 | solver = HeatEquationSolver(16, family="CG") 74 | for i in range(5): 75 | solver.time.assign(0) 76 | solver.setup_solver() 77 | solver.u_old.assign(solver.u) 78 | times = np.linspace(0, 1, 1+2**(i+1)) 79 | solver.solve(times) 80 | c1, c2 = solver.u.subfunctions 81 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2) 82 | assert err < 0.52 * err_old 83 | err_old = err 84 | -------------------------------------------------------------------------------- /tests/test_diffusion_migration.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import EchemSolver 4 | 5 | 6 | class DiffusionMigrationSolver(EchemSolver): 7 | def __init__(self, N, family="DG"): 8 | mesh = UnitSquareMesh(N, N, quadrilateral=True) 9 | conc_params = [] 10 | 11 | x, y = SpatialCoordinate(mesh) 12 | C1ex = cos(x) + sin(y) + 3 13 | C2ex = C1ex 14 | Uex = sin(x) + cos(y) + 3 15 | self.C1ex = C1ex 16 | self.C2ex = C2ex 17 | self.Uex = Uex 18 | 19 | D1 = 0.5 20 | D2 = 1.0 21 | z1 = 2.0 22 | z2 = -2.0 23 | 24 | def f(C): 25 | f1 = div((- D1 * z1 * grad(Uex)) * C1ex) - div(D1 * grad(C1ex)) 26 | f2 = div((- D2 * z2 * grad(Uex)) * C2ex) - div(D2 * grad(C2ex)) 27 | return [f1, f2] 28 | 29 | conc_params.append({"name": "C1", 30 | "diffusion coefficient": D1, 31 | "z": z1, 32 | "bulk": C1ex, 33 | }) 34 | 35 | conc_params.append({"name": "C2", 36 | "diffusion coefficient": D2, 37 | "z": z2, 38 | "bulk": C2ex, 39 | }) 40 | physical_params = { 41 | "flow": [ 42 | "diffusion", 43 | "migration", 44 | "electroneutrality"], 45 | "F": 1.0, 46 | "R": 1.0, 47 | "T": 1.0, 48 | "U_app": Uex, 49 | "bulk reaction": f, 50 | } 51 | 52 | super().__init__(conc_params, physical_params, mesh, p=1, family=family) 53 | 54 | def set_boundary_markers(self): 55 | self.boundary_markers = {"bulk dirichlet": (1, 2, 3, 4,), 56 | "applied": (1, 2, 3, 4,), 57 | } 58 | 59 | 60 | def test_convergence(): 61 | errC_old = 1e6 62 | errU_old = 1e6 63 | for i in range(5): 64 | solver = DiffusionMigrationSolver(2**(i + 1)) 65 | solver.setup_solver() 66 | solver.solve() 67 | c1, U = solver.u.subfunctions 68 | errC = errornorm(solver.C1ex, c1) 69 | errU = errornorm(solver.Uex, U) 70 | assert errC < 0.26 * errC_old 71 | assert errU < 0.26 * errU_old 72 | errC_old = errC 73 | errU_old = errU 74 | 75 | 76 | def test_convergence_CG(): 77 | errC_old = 1e6 78 | errU_old = 1e6 79 | for i in range(5): 80 | solver = DiffusionMigrationSolver(2**(i + 1), family="CG") 81 | solver.setup_solver() 82 | solver.solve() 83 | c1, U = solver.u.subfunctions 84 | errC = errornorm(solver.C1ex, c1) 85 | errU = errornorm(solver.Uex, U) 86 | assert errC < 0.26 * errC_old 87 | assert errU < 0.26 * errU_old 88 | errC_old = errC 89 | errU_old = errU 90 | 91 | 92 | test_convergence_CG() 93 | -------------------------------------------------------------------------------- /docs/source/user_guide/boundary_conditions.rst: -------------------------------------------------------------------------------- 1 | Boundary Conditions 2 | =================== 3 | 4 | The boundary conditions need to set through 5 | :meth:`echemfem.EchemSolver.set_boundary_markers`, which sets a dictionary 6 | containing boundary condition names and their corresponding boundary id. 7 | 8 | In the equations below, we have: 9 | 10 | * :math:`c_k`, the concentration of species :math:`k` 11 | * :math:`\Phi_2`, ionic potential 12 | * :math:`\Phi_1`, electronic potential 13 | * :math:`\mathbf N_k`, the flux of species :math:`k` 14 | * :math:`c_{k,\mathrm{bulk}}`, the bulk concentration of species :math:`k` 15 | * :math:`\mathbf n`, the unit outward normal vector 16 | * :math:`\mathbf u`, the velocity 17 | 18 | Here are the different options for ``boundary_markers`` keys: 19 | 20 | * ``"inlet"``: Inlet boundary condition defined as 21 | 22 | .. math:: 23 | 24 | \mathbf N_k \cdot \mathbf n = c_{k,\mathrm{bulk}} \mathbf u \cdot \mathbf n. 25 | 26 | * ``"outlet"``: Outlet boundary condition defined as 27 | 28 | .. math:: 29 | 30 | \mathbf N_k \cdot \mathbf n = c_k \mathbf u \cdot \mathbf n. 31 | 32 | * ``"bulk dirichlet"``: Dirichlet boundary condition for concentrations using the value ``"bulk"`` provided in :doc:`conc_params` such that 33 | 34 | .. math:: 35 | 36 | c_k = c_{k,\mathrm{bulk}}. 37 | 38 | * ``"bulk"``: Robin boundary condition for concentrations, where :math:`K_{k,MT}` is the value ``"mass transfer coefficient"`` if provided in :doc:`conc_params` for species :math:`k`. Also homogeneous Dirichlet boundary condition of the ionic potential, as follows 39 | 40 | .. math:: 41 | 42 | \begin{align} 43 | \mathbf N_k \cdot \mathbf n &= K_{k,MT} (c_k - c_{k,\mathrm{bulk}}), \\ 44 | \Phi_2 &= 0. 45 | \end{align} 46 | 47 | * ``"neumann"``: Neumann boundary condition defined through the method :meth:`echemfem.EchemSolver.neumann`. 48 | * ``"gas"``: Dirichlet boundary condition for concentrations using the value ``"gas"`` if provided in :doc:`conc_params` for species :math:`k`. 49 | * ``"applied"``: Dirichlet boundary condition for ionic potential, if this is a non-porous case, and for the electronic potential, if this is a porous case. It uses the value ``"U_app"`` given in :doc:`physical_params`. 50 | * ``"liquid applied"``: Dirichlet boundary condition for ionic potential using the value ``"U_app"`` given in :doc:`physical_params`. This is the same as ``"applied"`` in the non-porous case. 51 | * ``"robin"``: Robin boundary condition for the ionic potential when using the Poisson equation, typically used for GMPNP. We pass :math:`U - U^\mathrm{PZC}` together as ``"U_app"`` in :doc:`physical_params`, as well as ``"gap capacitance"`` for :math:`C_\mathrm{gap}`. 52 | 53 | .. math:: 54 | \epsilon_0\epsilon_\mathrm{r} \nabla \Phi_2 \cdot \mathbf{n}= C_\mathrm{gap}\left(U - U^\mathrm{PZC} - \Phi_2\right). 55 | 56 | * ``"poisson neumann"``: Neumann boundary condition for the ionic potential when using the Poisson equation, where :math:`\sigma` is the value ``"surface charge density"`` in :doc:`physical_params`. 57 | 58 | .. math:: 59 | \epsilon_0\epsilon_\mathrm{r} \nabla \Phi_2 \cdot \mathbf{n}= \sigma 60 | 61 | * Charge-transfer reactions: a custom :py:class:`str` can be passed to name the electrodes used in surface charge-transfer reactions defined using :doc:`echem_params`. 62 | 63 | -------------------------------------------------------------------------------- /tests/test_advection_diffusion_poisson_porous.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import EchemSolver 4 | 5 | 6 | class AdvectionDiffusionPoissonSolver(EchemSolver): 7 | def __init__(self, N): 8 | mesh = UnitSquareMesh(N, N, quadrilateral=True) 9 | conc_params = [] 10 | 11 | x, y = SpatialCoordinate(mesh) 12 | C1ex = cos(x) + sin(y) + 3 13 | C2ex = cos(x) - sin(y) + 3 14 | Uex = sin(x) + cos(y) + 3 15 | U2ex = sin(x) + cos(y) + 3 16 | self.C1ex = C1ex 17 | self.C2ex = C2ex 18 | self.Uex = Uex 19 | self.U2ex = U2ex 20 | 21 | def f(C): 22 | D1 = 0.5 23 | D2 = 1.0 24 | z1 = 2.0 25 | z2 = -2.0 26 | K = 1.0 27 | f1 = div((self.vel) * C1ex) - \ 28 | div(self.effective_diffusion(D1) * grad(C1ex)) 29 | f2 = div((self.vel) * C2ex) - \ 30 | div(self.effective_diffusion(D2) * grad(C2ex)) 31 | f3 = div((- self.effective_diffusion(D1) * z1**2 * grad(Uex)) * C1ex) + \ 32 | div((- self.effective_diffusion(D2) * z2**2 * grad(Uex)) * C2ex) 33 | f4 = div(- self.effective_diffusion(K, phase="solid") * grad(U2ex)) 34 | return [f1, f2, f3, f4] 35 | 36 | conc_params.append({"name": "C1", 37 | "diffusion coefficient": 0.5, 38 | "z": 2., 39 | "bulk": C1ex, 40 | }) 41 | 42 | conc_params.append({"name": "C2", 43 | "diffusion coefficient": 1.0, 44 | "z": -2., 45 | "bulk": C2ex, 46 | }) 47 | physical_params = { 48 | "flow": [ 49 | "diffusion", 50 | "advection", 51 | "poisson", 52 | "porous"], 53 | "F": 1.0, 54 | "R": 1.0, 55 | "T": 1.0, 56 | "U_app": Uex, 57 | "bulk reaction": f, 58 | "v_avg": 1., 59 | "porosity": 0.5, 60 | "solid conductivity": 1., 61 | "specific surface area": 1., 62 | "standard potential": 1., 63 | } 64 | 65 | super().__init__(conc_params, physical_params, mesh) 66 | 67 | def set_boundary_markers(self): 68 | self.boundary_markers = {"applied": (1, 2, 3, 4,), 69 | "liquid applied": (1, 2, 3, 4), 70 | "bulk dirichlet": (1, 2, 3, 4,), 71 | } 72 | 73 | def set_velocity(self): 74 | _, y = SpatialCoordinate(self.mesh) 75 | h = 1.0 76 | self.vel = as_vector( 77 | (6. * self.physical_params["v_avg"] / h**2 * y * (h - y), Constant(0.))) 78 | 79 | 80 | def test_convergence(): 81 | err_old = 1e6 82 | for i in range(5): 83 | solver = AdvectionDiffusionPoissonSolver(2**(i + 1)) 84 | solver.setup_solver() 85 | solver.solve() 86 | c1, c2, U1, U2 = solver.u.subfunctions 87 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2)\ 88 | + errornorm(solver.Uex, U1) + errornorm(solver.U2ex, U2) 89 | assert err < 0.29 * err_old 90 | err_old = err 91 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Code of Conduct" 3 | --- 4 | ## Community Code of Conduct 5 | 6 | ### Our Pledge 7 | 8 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 9 | 10 | ### Our Standards 11 | 12 | Examples of behavior that contributes to creating a positive environment include: 13 | 14 | * Using welcoming and inclusive language 15 | * Being respectful of differing viewpoints and experiences 16 | * Gracefully accepting constructive criticism 17 | * Focusing on what is best for the community 18 | * Showing empathy towards other community members 19 | 20 | Examples of unacceptable behavior by participants include: 21 | 22 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 23 | * Trolling, insulting/derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 26 | * Other conduct which could reasonably be considered inappropriate in a professional setting 27 | 28 | ### Our Responsibilities 29 | 30 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 31 | 32 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | ### Scope 35 | 36 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the EchemFEM project or its community. Examples of representing the project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of the project may be further defined and clarified by EchemFEM maintainers. 37 | 38 | ### Enforcement 39 | 40 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [roy27@llnl.gov](mailto:roy27@llnl.gov) or the LLNL GitHub Admins at [github-admin@llnl.gov](mailto:github-admin@llnl.gov) . The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 41 | 42 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project or organization's leadership. 43 | 44 | ### Attribution 45 | 46 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/) ([version 1.4](http://contributor-covenant.org/version/1/4)). 47 | -------------------------------------------------------------------------------- /docs/source/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | Here is a brief overview of the examples in `echemfem/examples `_. 5 | 6 | * **Planar flow reactor** with electroneutral Nernst-Planck. A Butler-Volmer expression is used for the redox reaction, and there are no homogeneous bulk reactions. A custom mesh is used with refinements close to the electrodes. 7 | 8 | * `2D case for binary electrolyte `_ (:math:`\ce{CuSO4}`) 9 | * `Nondimensional version of binary case `_ 10 | * `2D case for three-ion electrolyte `_ (:math:`\ce{CuSO4}` and :math:`\ce{H2SO4}`) 11 | * `Nondimensional version of three ion case `_ 12 | * `3D version of the three-ion case using custom preconditioners for a Discontinuous Galerkin (DG) scheme, and nondimensional `_ 13 | 14 | * Simple 1D reaction-diffusion system for :math:`\ce{CO2}` electrolysis in :math:`\ce{KHCO3}` 15 | 16 | * `Simplified bicarbonate bulk reactions `_ 17 | * `Full bicarbonate bulk reactions `_ 18 | * `Different implementation of the full bicarbonate bulk reactions `_ (using the :doc:`homog_params ` interface) 19 | 20 | * 2D flow-past the electrode with advection-diffusion 21 | 22 | * `Two species toy model with shear flow `_ 23 | * `CO2 electrolysis in bicarbonate with linear charge-transfer kinetics and shear flow `_ 24 | * `Two species toy model with irregular electrode with Navier-Stokes for the flow `_ 25 | 26 | * 1D model for the :math:`\ce{CO2}` electrolysis in a copper catalyst layer of a gas diffusion electrode (GDE). The model uses electroneutral Nernst-Planck in a porous medium. 27 | 28 | * `Using substitution of the electroneutrality equation to eliminate a species `_ 29 | * `Solving the electroneutrality equation explicitly `_ 30 | 31 | * `A simple Vanadium flow battery using advection-diffusion-reaction, Poisson for the ionic potential with a predefinied conductivity and Navier-Stokes-Brinkman for the flow `_ 32 | 33 | * `A symmetric cylindrical pore model for CO2 electrolysis using electroneutral Nernst-Planck and simplified bicarbonate bulk reactions `_ 34 | 35 | * `A tandem flow-past the electrode system with an Ag catalyst placed in front of a Cu catalyst with electroneutral Nernst-Planck and Tafel kinetics `_ 36 | 37 | * `Simple example for the BMCSL model for finite size ion effects `_ 38 | -------------------------------------------------------------------------------- /docs/source/user_guide/echem_params.rst: -------------------------------------------------------------------------------- 1 | Electrochemical Parameters 2 | ========================== 3 | 4 | The parameters of each charge-transfer reaction can be provided through 5 | :attr:`echemfem.EchemSolver.echem_params`, a list containing one dictionary for 6 | each charge-transfer reaction. Below is a list of different keys that can 7 | appear in each dictionary 8 | 9 | * :Key: ``"reaction"`` 10 | :Type: a function 11 | :Description: the current density from a charge-transfer reaction 12 | 13 | Args: 14 | u: solution state. The value of the different concentrations can be recovered through ``u([self.i_c["species name"])`` within a ``echemfem.EchemSolver`` object. 15 | 16 | Returns: 17 | Current density :math:`i_k`. 18 | 19 | * :Key: ``"electrons"`` 20 | :Type: :py:class:`float`, :py:class:`int`, firedrake expression 21 | :Description: the number of electrons :math:`n_k` being transferred in the reaction. 22 | * :Key: ``"stoichiometry"`` 23 | :Type: :py:class:`dict` 24 | :Description: This entry defines the stoichiometry of the reaction. Each key in this dictionary should be the name of a species (as defined in the ``"name"`` key values of ``conc_params``). The corresponding value is the stoichiometry coefficient :math:`s_{j,k}` for the species in this reaction. It should be an integer: negative for a reactant, positive for a product. 25 | * :Key: ``"boundary"`` 26 | :Type: :py:class:`str` 27 | :Description: In the non-porous case, this entry provides the name of the boundary where the charge-transfer reaction happens. This should correspond to a key in the dictionary ``self.boundary_markers`` containing the :doc:`boundary_conditions`. 28 | 29 | Here is an example of a reaction that can be implement via ``echem_params``. CO reduction for ethanol: 30 | 31 | .. math:: 32 | 33 | 2\mathrm{CO} + 7 \mathrm{H}_2 \mathrm{O} + 8 \mathrm{e}^- \rightarrow \mathrm{CH}_3\mathrm{CH}_2\mathrm{OH}_{(\mathrm{aq})} + 8 \mathrm{OH}^- 34 | 35 | Here, we assume the dilute solution case where :math:`\mathrm{H}_2\mathrm{O}` 36 | concentration is not tracked. In ``conc_params``, we will have entries for the other species in this reactions, with ``"name"`` key values: ``"CO"``, ``"OH"``, and ``"C2H6O"``. Assuming we also defined a function ``reaction_C2H6O`` for the current density, we get the following ``echem_params`` entry: 37 | 38 | .. code:: 39 | 40 | {"reaction": reaction_C2H6O, 41 | "electrons": 8, 42 | "stoichiometry": {"CO": -2, 43 | "C2H6O": 1, 44 | "OH": 8}, 45 | } 46 | 47 | In the non-porous case, the reactions are surface reactions, which result in the following boundary conditions for the ionic current :math:`\mathbf{i}_2`, and mass flux :math:`\mathbf{N}_j` of species :math:`j` 48 | 49 | .. math:: 50 | 51 | \mathbf{i}_2 \cdot \mathbf{n} = -\sum_k i_k, 52 | 53 | \mathbf{N}_j \cdot \mathbf n = -\sum_k \frac{s_{j,k} i_k}{n_k F}, 54 | 55 | where :math:`k` represent each charge-transfer reaction in the system, and :math:`F` is the Faraday constant, which must be passed through :doc:`physical_params`. 56 | 57 | In the porous case, i.e. when ``"porous"`` is passed through ``"flow"`` in :doc:`physical_params`, the reactions are volumetric reactions, which are added to the right-hand sides of the equations. In the absence of homogeneous bulk reactions, we have 58 | 59 | .. math:: 60 | 61 | \nabla \cdot \mathbf{i}_1 = -\nabla \cdot \mathbf{i}_2 = - a_v \sum_k i_k, 62 | 63 | \nabla \cdot \mathbf{N}_j = a_v \sum_k \frac{s_{j,k} i_k}{n_k F}, 64 | 65 | where :math:`a_v` is passed as ``"specific surface area"`` through :doc:`physical_params`. 66 | -------------------------------------------------------------------------------- /tests/test_diffusion_migration_poisson.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import EchemSolver 4 | 5 | 6 | class DiffusionMigrationSolver(EchemSolver): 7 | def __init__(self, N, family="DG"): 8 | mesh = UnitSquareMesh(N, N, quadrilateral=True) 9 | conc_params = [] 10 | 11 | x, y = SpatialCoordinate(mesh) 12 | C1ex = cos(x) + sin(y) + 3 13 | C2ex = cos(x) - sin(y) + 3 14 | Uex = sin(x) + cos(y) + 3 15 | self.C1ex = C1ex 16 | self.C2ex = C2ex 17 | self.Uex = Uex 18 | 19 | def f(C): 20 | D1 = 0.5 21 | D2 = 1.0 22 | z1 = 2.0 23 | z2 = -2.0 24 | K = 2e-1 25 | f1 = div((- D1 * z1 * grad(Uex)) * C1ex) - div(D1 * grad(C1ex)) 26 | f2 = div((- D2 * z2 * grad(Uex)) * C2ex) - div(D2 * grad(C2ex)) 27 | # f3 = div(( - D1 * z1**2 * grad(Uex)) * C1ex) + div(( - D2 * z2**2 * grad(Uex)) * C2ex) 28 | f3 = div((-K * grad(Uex))) - z1 * C1ex - z2 * C2ex 29 | return [f1, f2, f3] 30 | 31 | conc_params.append({"name": "C1", 32 | "diffusion coefficient": 0.5, 33 | "z": 2., 34 | "bulk": C1ex, 35 | }) 36 | 37 | conc_params.append({"name": "C2", 38 | "diffusion coefficient": 1.0, 39 | "z": -2., 40 | "bulk": C2ex, 41 | }) 42 | physical_params = {"flow": ["diffusion", "migration", "poisson"], 43 | "F": 1.0, 44 | "R": 1.0, 45 | "T": 1.0, 46 | "vacuum permittivity": 2.0, 47 | "relative permittivity": 1e-1, 48 | "U_app": Uex, 49 | "bulk reaction": f, 50 | } 51 | 52 | super().__init__(conc_params, physical_params, mesh, family=family) 53 | 54 | def set_boundary_markers(self): 55 | self.boundary_markers = {"bulk dirichlet": (1, 2, 3, 4,), 56 | "applied": (1, 2, 3, 4,), 57 | } 58 | 59 | 60 | def test_convergence(): 61 | errC1_old = 1e6 62 | errC2_old = 1e6 63 | errU_old = 1e6 64 | for i in range(5): 65 | solver = DiffusionMigrationSolver(2**(i + 1)) 66 | solver.setup_solver() 67 | solver.solve() 68 | c1, c2, U = solver.u.subfunctions 69 | errC1 = errornorm(solver.C1ex, c1) 70 | errC2 = errornorm(solver.C2ex, c2) 71 | errU = errornorm(solver.Uex, U) 72 | assert errC1 < 0.26 * errC1_old 73 | assert errC2 < 0.26 * errC2_old 74 | assert errU < 0.26 * errU_old 75 | errC1_old = errC1 76 | errC2_old = errC2 77 | errU_old = errU 78 | 79 | 80 | def test_convergence_CG(): 81 | errC1_old = 1e6 82 | errC2_old = 1e6 83 | errU_old = 1e6 84 | for i in range(5): 85 | solver = DiffusionMigrationSolver(2**(i + 1), family="CG") 86 | solver.setup_solver() 87 | solver.solve() 88 | c1, c2, U = solver.u.subfunctions 89 | errC1 = errornorm(solver.C1ex, c1) 90 | errC2 = errornorm(solver.C2ex, c2) 91 | errU = errornorm(solver.Uex, U) 92 | assert errC1 < 0.27 * errC1_old 93 | assert errC2 < 0.27 * errC2_old 94 | assert errU < 0.27 * errU_old 95 | errC1_old = errC1 96 | errC2_old = errC2 97 | errU_old = errU 98 | 99 | 100 | test_convergence_CG() 101 | -------------------------------------------------------------------------------- /tests/test_advection_diffusion_poisson.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import EchemSolver 4 | 5 | 6 | class AdvectionDiffusionPoissonSolver(EchemSolver): 7 | def __init__(self, N, family="DG"): 8 | mesh = UnitSquareMesh(N, N, quadrilateral=True) 9 | conc_params = [] 10 | 11 | x, y = SpatialCoordinate(mesh) 12 | C1ex = cos(x) + sin(y) + 3 13 | C2ex = cos(x) - sin(y) + 3 14 | Uex = sin(x) + cos(y) + 3 15 | self.C1ex = C1ex 16 | self.C2ex = C2ex 17 | self.Uex = Uex 18 | 19 | def f(C): 20 | D1 = 0.5 21 | D2 = 1.0 22 | z1 = 2.0 23 | z2 = -2.0 24 | f1 = div((self.vel) * C1ex) - div(D1 * grad(C1ex)) 25 | f2 = div((self.vel) * C2ex) - div(D2 * grad(C2ex)) 26 | f3 = div((- D1 * z1**2 * grad(Uex)) * C1ex) + \ 27 | div((- D2 * z2**2 * grad(Uex)) * C2ex) 28 | return [f1, f2, f3] 29 | 30 | conc_params.append({"name": "C1", 31 | "diffusion coefficient": 0.5, 32 | "z": 2., 33 | "bulk": C1ex, 34 | }) 35 | 36 | conc_params.append({"name": "C2", 37 | "diffusion coefficient": 1.0, 38 | "z": -2., 39 | "bulk": C2ex, 40 | }) 41 | physical_params = {"flow": ["diffusion", "advection", "poisson"], 42 | "F": 1.0, 43 | "R": 1.0, 44 | "T": 1.0, 45 | "U_app": Uex, 46 | "bulk reaction": f, 47 | "v_avg": 1., 48 | } 49 | 50 | super().__init__(conc_params, physical_params, mesh, family=family) 51 | 52 | def set_boundary_markers(self): 53 | self.boundary_markers = {"applied": (1, 2, 3, 4,), 54 | "bulk dirichlet": (1, 2, 3, 4,), 55 | } 56 | 57 | def set_velocity(self): 58 | _, y = SpatialCoordinate(self.mesh) 59 | h = 1.0 60 | self.vel = as_vector( 61 | (6. * self.physical_params["v_avg"] / h**2 * y * (h - y), Constant(0.))) 62 | 63 | 64 | def test_convergence(): 65 | errC1_old = 1e6 66 | errC2_old = 1e6 67 | errU_old = 1e6 68 | for i in range(5): 69 | solver = AdvectionDiffusionPoissonSolver(2**(i + 1)) 70 | solver.setup_solver() 71 | solver.solve() 72 | c1, c2, U = solver.u.subfunctions 73 | errC1 = errornorm(solver.C1ex, c1) 74 | errC2 = errornorm(solver.C2ex, c2) 75 | errU = errornorm(solver.Uex, U) 76 | assert errC1 < 0.29 * errC1_old 77 | assert errC2 < 0.29 * errC2_old 78 | assert errU < 0.29 * errU_old 79 | errC1_old = errC1 80 | errC2_old = errC2 81 | errU_old = errU 82 | 83 | 84 | def test_convergence_CG(): 85 | errC1_old = 1e6 86 | errC2_old = 1e6 87 | errU_old = 1e6 88 | for i in range(5): 89 | solver = AdvectionDiffusionPoissonSolver(2**(i + 1), family="CG") 90 | solver.setup_solver() 91 | solver.solve() 92 | c1, c2, U = solver.u.subfunctions 93 | errC1 = errornorm(solver.C1ex, c1) 94 | errC2 = errornorm(solver.C2ex, c2) 95 | errU = errornorm(solver.Uex, U) 96 | assert errC1 < 0.26 * errC1_old 97 | assert errC2 < 0.26 * errC2_old 98 | assert errU < 0.26 * errU_old 99 | errC1_old = errC1 100 | errC2_old = errC2 101 | errU_old = errU 102 | 103 | -------------------------------------------------------------------------------- /tests/test_SUPG.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import EchemSolver 4 | 5 | 6 | class AdvectionDiffusionSolver(EchemSolver): 7 | def __init__(self, N, D, extruded=False): 8 | if extruded: 9 | plane_mesh = UnitSquareMesh(N, N, quadrilateral=True) 10 | mesh = ExtrudedMesh(plane_mesh, N, layer_height=1.0 / N, 11 | extrusion_type='uniform') 12 | x, y, _ = SpatialCoordinate(mesh) 13 | else: 14 | mesh = UnitSquareMesh(N, N, quadrilateral=True) 15 | x, y = SpatialCoordinate(mesh) 16 | 17 | conc_params = [] 18 | 19 | C1ex = sin(x) + cos(y) 20 | C2ex = cos(x) + sin(y) 21 | self.C1ex = C1ex 22 | self.C2ex = C2ex 23 | 24 | def f(C): 25 | f1 = div(self.vel * C1ex - D * grad(C1ex)) 26 | f2 = div(self.vel * C2ex - D * grad(C2ex)) 27 | return [f1, f2] 28 | 29 | conc_params.append({"name": "C1", 30 | "diffusion coefficient": D, 31 | "bulk": C1ex, 32 | }) 33 | 34 | conc_params.append({"name": "C2", 35 | "diffusion coefficient": D, 36 | "bulk": C2ex, 37 | }) 38 | physical_params = {"flow": ["diffusion", "advection"], 39 | "bulk reaction": f, 40 | "v_avg": 1.0, 41 | } 42 | 43 | super().__init__(conc_params, physical_params, mesh, family="CG", SUPG=True) 44 | 45 | def set_boundary_markers(self): 46 | self.boundary_markers = {"inlet": (1, 2, 3, 4), 47 | "outlet": (1, 2, 3, 4), 48 | "bulk dirichlet": (1, 2, 3, 4), 49 | } 50 | 51 | def set_velocity(self): 52 | if self.mesh.layers is not None: 53 | _, y, _ = SpatialCoordinate(self.mesh) 54 | else: 55 | _, y = SpatialCoordinate(self.mesh) 56 | 57 | h = 1.0 58 | x_vel = 6. * self.physical_params["v_avg"] / h**2 * y * (h - y) 59 | 60 | if self.mesh.layers is not None: 61 | self.vel = as_vector( 62 | (x_vel, Constant(0.), Constant(0.))) 63 | else: 64 | self.vel = as_vector( 65 | (x_vel, Constant(0.))) 66 | 67 | 68 | def test_convergence_low_peclet_CG(extruded=False): 69 | err_old = 1e6 70 | if extruded: 71 | n = 3 72 | else: 73 | n = 5 74 | for i in range(n): 75 | solver = AdvectionDiffusionSolver(2**(i + 1), 1., extruded=extruded) 76 | solver.setup_solver(initial_guess=False) 77 | solver.solve() 78 | c1, c2 = solver.u.subfunctions 79 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2) 80 | assert err < 0.26 * err_old 81 | err_old = err 82 | 83 | 84 | def test_convergence_high_peclet_CG(extruded=False): 85 | err_old = 1e6 86 | if extruded: 87 | n = 3 88 | else: 89 | n = 5 90 | for i in range(n): 91 | solver = AdvectionDiffusionSolver(2**(i + 2), 1e-4, extruded=extruded) 92 | solver.setup_solver(initial_guess=False) 93 | solver.solve() 94 | c1, c2 = solver.u.subfunctions 95 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2) 96 | assert err < 0.26 * err_old 97 | err_old = err 98 | 99 | 100 | def test_convergence_low_peclet_extruded_CG(): 101 | test_convergence_low_peclet_CG(extruded=True) 102 | 103 | 104 | def test_convergence_high_peclet_extruded_CG(): 105 | test_convergence_high_peclet_CG(extruded=True) 106 | -------------------------------------------------------------------------------- /tests/test_advection_diffusion_migration_porous.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import EchemSolver 4 | 5 | 6 | class AdvectionDiffusionMigrationSolver(EchemSolver): 7 | def __init__(self, N, vavg): 8 | mesh = UnitSquareMesh(N, N, quadrilateral=True) 9 | conc_params = [] 10 | 11 | x, y = SpatialCoordinate(mesh) 12 | C1ex = cos(x) + sin(y) + 3 13 | C2ex = C1ex 14 | U1ex = sin(x) + cos(y) + 3 15 | U2ex = sin(x) + cos(y) + 3 16 | self.C1ex = C1ex 17 | self.C2ex = C2ex 18 | self.U1ex = U1ex 19 | self.U2ex = U2ex 20 | 21 | def f(C): 22 | D1 = 0.5 23 | D2 = 1.0 24 | z1 = 2.0 25 | z2 = -2.0 26 | K = 1.0 27 | f1 = div((self.vel - self.effective_diffusion(D1) * z1 * grad(U1ex)) * C1ex) - \ 28 | div(self.effective_diffusion(D1) * grad(C1ex)) 29 | f2 = div((self.vel - self.effective_diffusion(D2) * z2 * grad(U1ex)) * C2ex) - \ 30 | div(self.effective_diffusion(D2) * grad(C2ex)) 31 | f3 = div(- self.effective_diffusion(K, phase="solid") * grad(U2ex)) 32 | return [f1, f2, f3] 33 | 34 | conc_params.append({"name": "C1", 35 | "diffusion coefficient": 0.5, 36 | "z": 2., 37 | "bulk": C1ex, 38 | }) 39 | 40 | conc_params.append({"name": "C2", 41 | "diffusion coefficient": 1.0, 42 | "z": -2., 43 | "bulk": C2ex, 44 | }) 45 | physical_params = {"flow": ["diffusion", "advection", "migration", "electroneutrality", "porous"], 46 | "F": 1.0, # C/mol 47 | "R": 1.0, # J/K/mol 48 | "T": 1.0, # K 49 | "U_app": U1ex, # V 50 | "bulk reaction": f, 51 | "v_avg": vavg, 52 | "porosity": 0.5, 53 | "solid conductivity": 1., 54 | "specific surface area": 1., 55 | "standard potential": 0., 56 | } 57 | 58 | super().__init__(conc_params, physical_params, mesh) 59 | 60 | def set_boundary_markers(self): 61 | self.boundary_markers = {"bulk dirichlet": (1, 2, 3, 4,), 62 | "applied": (1, 2, 3, 4,), 63 | "liquid applied": (1, 2, 3, 4), 64 | } 65 | 66 | def set_velocity(self): 67 | _, y = SpatialCoordinate(self.mesh) 68 | h = 1.0 69 | self.vel = as_vector( 70 | (6. * self.physical_params["v_avg"] / h**2 * y * (h - y), Constant(0.))) 71 | 72 | 73 | def test_convergence_low_peclet(): 74 | err_old = 1e6 75 | for i in range(7): 76 | solver = AdvectionDiffusionMigrationSolver(2**(i + 1), 1.0) 77 | solver.setup_solver() 78 | solver.solve() 79 | c1, U1, U2 = solver.u.subfunctions 80 | err = errornorm(solver.C1ex, c1) + \ 81 | errornorm(solver.U1ex, U1) + errornorm(solver.U2ex, U2) 82 | assert err < 0.29 * err_old 83 | err_old = err 84 | 85 | 86 | def test_convergence_high_peclet(): 87 | err_old = 1e6 88 | for i in range(3, 7): 89 | solver = AdvectionDiffusionMigrationSolver(2**(i + 1), 1e3) 90 | solver.setup_solver() 91 | solver.solve() 92 | c1, U1, U2 = solver.u.subfunctions 93 | err = errornorm(solver.C1ex, c1) + \ 94 | errornorm(solver.U1ex, U1) + errornorm(solver.U2ex, U2) 95 | assert err < 0.29 * err_old 96 | err_old = err 97 | -------------------------------------------------------------------------------- /examples/tworxn_irregular.py: -------------------------------------------------------------------------------- 1 | from firedrake import * 2 | from echemfem import EchemSolver, NavierStokesFlowSolver 3 | 4 | """ 5 | A 2D flow past an irregular electrode toy model with two species and 6 | advection-diffusion, and Navier-Stokes for the flow. The electrode surface 7 | consists of square blocks. The mesh can be create in GMSH using the following 8 | command: 9 | 10 | gmsh -2 squares_small.geo 11 | 12 | The model is adapated from 13 | Lin, T.Y., Baker, S.E., Duoss, E.B. and Beck, V.A., 2021. Analysis of 14 | the Reactive CO2 Surface Flux in Electrocatalytic Aqueous Flow 15 | Reactors. Industrial & Engineering Chemistry Research, 60(31), 16 | pp.11824-11833. 17 | """ 18 | 19 | peclet=10 20 | damkohler=10 21 | Ly = 0.1 22 | Lx = 1. 23 | diffusion_coefficient=Lx/peclet 24 | mass_transfert_coefficient=damkohler/Lx*diffusion_coefficient 25 | mesh=Mesh('squares_small.msh') 26 | 27 | class CarbonateSolver(EchemSolver): 28 | def __init__(self): 29 | 30 | C_1_inf = 1. 31 | C_2_inf = Constant(0) 32 | 33 | 34 | def bulk_reaction(y): 35 | yC1=y[0]; 36 | yC2=y[1]; 37 | dC1 = -(1.)*(1e3)*yC1*yC2 38 | dC2 = -(2.)*(1e3)*yC1*yC2 39 | return [0,0] 40 | 41 | conc_params = [] 42 | conc_params.append({"name": "C1", 43 | "diffusion coefficient": diffusion_coefficient, 44 | "bulk": C_1_inf, 45 | }) 46 | 47 | conc_params.append({"name": "C2", 48 | "diffusion coefficient": diffusion_coefficient, 49 | "bulk": C_2_inf, 50 | }) 51 | 52 | 53 | physical_params = {"flow": ["advection","diffusion"], 54 | "bulk reaction": bulk_reaction, 55 | } 56 | 57 | super().__init__(conc_params, physical_params, mesh, family="DG") 58 | 59 | def neumann(self, C, conc_params, u): 60 | name = conc_params["name"] 61 | 62 | if name == "C1": 63 | return -(mass_transfert_coefficient)*u[0] 64 | if name == "C2": 65 | return 2.*(1.e6)*u[0] 66 | 67 | def set_boundary_markers(self): 68 | self.boundary_markers = {"inlet": (12), 69 | "bulk dirichlet": (13), 70 | "outlet": (14,), 71 | "neumann": (11,), 72 | } 73 | 74 | def set_velocity(self): 75 | boundary_markers = {"no slip": (11,10,15), 76 | "inlet velocity": (12,13,), 77 | "outlet velocity": (14,) 78 | } 79 | 80 | x, y = SpatialCoordinate(mesh) 81 | vel = as_vector([y, Constant(0)]) 82 | flow_params = {"inlet velocity": vel, 83 | "outlet velocity": vel, 84 | "Reynolds number": 100 85 | } 86 | NS_solver = NavierStokesFlowSolver(mesh, flow_params, boundary_markers) 87 | NS_solver.setup_solver() 88 | NS_solver.solve() 89 | self.vel = NS_solver.vel 90 | 91 | 92 | solver = CarbonateSolver() 93 | solver.setup_solver() 94 | solver.solve() 95 | 96 | n = FacetNormal(solver.mesh) 97 | cC1, _, = solver.u.subfunctions 98 | flux = assemble(dot(grad(cC1), n) * ds(11)) 99 | flux1 = assemble(dot(grad(cC1), n)/dot(grad(cC1), n)*ds(11)) 100 | 101 | 102 | print("Flux = %f" % flux) 103 | print("surface : ",flux1) 104 | fichier=open('results_square.dat','a') 105 | fichier.write(str(peclet)+' '+str(damkohler)+' '+str(flux)+' '+str(flux1)+' \n') 106 | fichier.close() 107 | 108 | VTKFile("SquareWave_"+str(peclet)+str(damkohler)+".pvd").write(cC1) 109 | -------------------------------------------------------------------------------- /echemfem/cylindricalmeasure.py: -------------------------------------------------------------------------------- 1 | import numbers 2 | 3 | from itertools import chain 4 | 5 | from ufl.measure import Measure 6 | from ufl.core.expr import Expr 7 | from ufl.checks import is_true_ufl_scalar 8 | from ufl.constantvalue import as_ufl 9 | from ufl.domain import extract_domains 10 | 11 | 12 | class CylindricalMeasure(Measure): 13 | def __init__(self, radius, *args, **kwargs): 14 | self.radius = radius 15 | super().__init__(*args, **kwargs) 16 | 17 | def __rmul__(self, integrand): 18 | """Multiply a scalar expression with measure to construct a form with 19 | a single integral. 20 | 21 | This is to implement the notation 22 | 23 | form = integrand * self.radius * self 24 | 25 | Integration properties are taken from this Measure object. 26 | 27 | """ 28 | # Avoid circular imports 29 | from ufl.integral import Integral 30 | from ufl.form import Form 31 | 32 | # Allow python literals: 1*dx and 1.0*dx 33 | if isinstance(integrand, (int, float)): 34 | integrand = as_ufl(integrand) 35 | 36 | # Let other types implement multiplication with Measure if 37 | # they want to (to support the dolfin-adjoint TimeMeasure) 38 | if not isinstance(integrand, Expr): 39 | return NotImplemented 40 | 41 | # Allow only scalar integrands 42 | if not is_true_ufl_scalar(integrand): 43 | raise ValueError( 44 | "Can only integrate scalar expressions. The integrand is a " 45 | f"tensor expression with value shape {integrand.ufl_shape} and " 46 | f"free indices with labels {integrand.ufl_free_indices}.") 47 | 48 | # If we have a tuple of domain ids build the integrals one by 49 | # one and construct as a Form in one go. 50 | subdomain_id = self.subdomain_id() 51 | if isinstance(subdomain_id, tuple): 52 | return Form(list(chain(*((integrand * self.reconstruct(subdomain_id=d)).integrals() 53 | for d in subdomain_id)))) 54 | 55 | # Check that we have an integer subdomain or a string 56 | # ("everywhere" or "otherwise", any more?) 57 | if not isinstance(subdomain_id, (str, numbers.Integral,)): 58 | raise ValueError("Expecting integer or string domain id.") 59 | 60 | # If we don't have an integration domain, try to find one in 61 | # integrand 62 | domain = self.ufl_domain() 63 | if domain is None: 64 | domains = extract_domains(integrand) 65 | if len(domains) == 1: 66 | domain, = domains 67 | elif len(domains) == 0: 68 | raise ValueError("This integral is missing an integration domain.") 69 | else: 70 | raise ValueError("Multiple domains found, making the choice of integration domain ambiguous.") 71 | 72 | # Otherwise create and return a one-integral form 73 | integral = Integral(integrand=self.radius*integrand, 74 | integral_type=self.integral_type(), 75 | domain=domain, 76 | subdomain_id=subdomain_id, 77 | metadata=self.metadata(), 78 | subdomain_data=self.subdomain_data()) 79 | return Form([integral]) 80 | 81 | def reconstruct(self, 82 | integral_type=None, 83 | subdomain_id=None, 84 | domain=None, 85 | metadata=None, 86 | subdomain_data=None): 87 | if subdomain_id is None: 88 | subdomain_id = self.subdomain_id() 89 | if domain is None: 90 | domain = self.ufl_domain() 91 | if metadata is None: 92 | metadata = self.metadata() 93 | if subdomain_data is None: 94 | subdomain_data = self.subdomain_data() 95 | return CylindricalMeasure(self.radius, self.integral_type(), 96 | domain=domain, subdomain_id=subdomain_id, 97 | metadata=metadata, subdomain_data=subdomain_data) 98 | -------------------------------------------------------------------------------- /joss/paper.bib: -------------------------------------------------------------------------------- 1 | @article{roy2023scalable, 2 | title={A scalable {DG} solver for the electroneutral {N}ernst-{P}lanck equations}, 3 | author={Roy, Thomas and Andrej, Julian and Beck, Victor A}, 4 | journal={Journal of Computational Physics}, 5 | volume={475}, 6 | pages={111859}, 7 | year={2023}, 8 | publisher={Elsevier}, 9 | doi={10.1016/j.jcp.2022.111859} 10 | } 11 | 12 | @article{govindarajan2023coupling, 13 | title={Coupling Microkinetics with Continuum Transport Models to Understand Electrochemical CO$_2$ Reduction in Flow Reactors}, 14 | author={Govindarajan, Nitish and Lin, Tiras Y and Roy, Thomas and Hahn, Christopher and Varley, Joel B}, 15 | journal={PRX Energy}, 16 | volume={2}, 17 | number={3}, 18 | pages={033010}, 19 | year={2023}, 20 | publisher={APS}, 21 | doi={10.1103/PRXEnergy.2.033010} 22 | } 23 | 24 | @manual{FiredrakeUserManual, 25 | title = {Firedrake User Manual}, 26 | author = {David A. Ham and Paul H. J. Kelly and Lawrence Mitchell and Colin J. Cotter and Robert C. Kirby and Koki Sagiyama and Nacime Bouziani and Sophia Vorderwuelbecke and Thomas J. Gregory and Jack Betteridge and Daniel R. Shapero and Reuben W. Nixon-Hill and Connor J. Ward and Patrick E. Farrell and Pablo D. Brubeck and India Marsden and Thomas H. Gibson and Miklós Homolya and Tianjiao Sun and Andrew T. T. McRae and Fabio Luporini and Alastair Gregory and Michael Lange and Simon W. Funke and Florian Rathgeber and Gheorghe-Teodor Bercea and Graham R. Markall}, 27 | organization = {Imperial College London and University of Oxford and Baylor University and University of Washington}, 28 | edition = {First edition}, 29 | year = {2023}, 30 | month = {5}, 31 | doi = {10.25561/104839}, 32 | } 33 | 34 | @TechReport{ petsc-user-ref, 35 | author = {Satish Balay and Shrirang Abhyankar and Mark~F. Adams and Steven Benson and Jed 36 | Brown and Peter Brune and Kris Buschelman and Emil Constantinescu and Lisandro 37 | Dalcin and Alp Dener and Victor Eijkhout and Jacob Faibussowitsch and William~D. 38 | Gropp and V\'{a}clav Hapla and Tobin Isaac and Pierre Jolivet and Dmitry Karpeev 39 | and Dinesh Kaushik and Matthew~G. Knepley and Fande Kong and Scott Kruger and 40 | Dave~A. May and Lois Curfman McInnes and Richard Tran Mills and Lawrence Mitchell 41 | and Todd Munson and Jose~E. Roman and Karl Rupp and Patrick Sanan and Jason Sarich 42 | and Barry~F. Smith and Stefano Zampini and Hong Zhang and Hong Zhang and Junchao 43 | Zhang}, 44 | title = {{PETSc/TAO} Users Manual}, 45 | institution = {Argonne National Laboratory}, 46 | number = {ANL-21/39 - Revision 3.20}, 47 | doi = {10.2172/1968587}, 48 | year = {2023} 49 | } 50 | 51 | @Misc{ petsc-web-page, 52 | author = {Satish Balay and Shrirang Abhyankar and Mark~F. Adams and Steven Benson and Jed 53 | Brown and Peter Brune and Kris Buschelman and Emil~M. Constantinescu and Lisandro 54 | Dalcin and Alp Dener and Victor Eijkhout and Jacob Faibussowitsch and William~D. 55 | Gropp and V\'{a}clav Hapla and Tobin Isaac and Pierre Jolivet and Dmitry Karpeev 56 | and Dinesh Kaushik and Matthew~G. Knepley and Fande Kong and Scott Kruger and 57 | Dave~A. May and Lois Curfman McInnes and Richard Tran Mills and Lawrence Mitchell 58 | and Todd Munson and Jose~E. Roman and Karl Rupp and Patrick Sanan and Jason Sarich 59 | and Barry~F. Smith and Stefano Zampini and Hong Zhang and Hong Zhang and Junchao 60 | Zhang}, 61 | title = {{PETS}c {W}eb page}, 62 | url = {https://petsc.org/}, 63 | howpublished = {\url{https://petsc.org/}}, 64 | year = {2023} 65 | } 66 | 67 | @article{catmap, 68 | doi = {10.1007/s10562-015-1495-6}, 69 | author = {Medford, Andrew J. and Shi, Chuan and Hoffmann, Max J. and Lausche, Adam C. and Fitzgibbon, Sean R. and Bligaard, Thomas and N{\o}rskov, Jens K.}, 70 | isbn = {1572-879X}, 71 | journal = {Catal. Lett.}, 72 | number = {3}, 73 | pages = {794--807}, 74 | title = {{CatMAP}: A Software Package for Descriptor-Based Microkinetic Mapping of Catalytic Trends}, 75 | volume = {145}, 76 | year = {2015}, 77 | } 78 | -------------------------------------------------------------------------------- /examples/bortels_twoion.py: -------------------------------------------------------------------------------- 1 | from firedrake import * 2 | from echemfem import EchemSolver 3 | import argparse 4 | parser = argparse.ArgumentParser(add_help=False) 5 | parser.add_argument("--family", type=str, default='DG') 6 | args, _ = parser.parse_known_args() 7 | 8 | """ 9 | 2D Flow-plate reactor with electroneutral Nernst-Planck. Using a custom GMSH 10 | mesh with refinement close to the electrodes. GMSH is used to get the mesh file 11 | from the .geo file as follows: 12 | 13 | gmsh -2 bortels_structuredquad.geo 14 | 15 | Two ion case taken from: 16 | Bortels, L., Deconinck, J. and Van Den Bossche, B., 1996. The multi-dimensional 17 | upwinding method as a new simulation tool for the analysis of multi-ion 18 | electrolytes controlled by diffusion, convection and migration. Part 1. Steady 19 | state analysis of a parallel plane flow channel. Journal of Electroanalytical 20 | Chemistry, 404(1), pp.15-26. 21 | """ 22 | 23 | class BortelsSolver(EchemSolver): 24 | def __init__(self): 25 | conc_params = [] 26 | 27 | conc_params.append({"name": "Cu", 28 | "diffusion coefficient": 7.2e-10, # m^2/s 29 | "z": 2., 30 | "bulk": 10., # mol/m^3 31 | }) 32 | 33 | conc_params.append({"name": "SO4", 34 | "diffusion coefficient": 10.65e-10, # m^2/s 35 | "z": -2., 36 | "bulk": 10., # mol/m^3 37 | }) 38 | physical_params = {"flow": ["advection", "diffusion", "migration", "electroneutrality"], 39 | "F": 96485.3329, # C/mol 40 | "R": 8.3144598, # J/K/mol 41 | "T": 273.15 + 25., # K 42 | "U_app": 0.1, # V 43 | "v_avg": 0.01 # m/s 44 | } 45 | 46 | def reaction(u, V): 47 | # Butler-Volmer 48 | C = u[0] 49 | U = u[1] 50 | C_b = conc_params[0]["bulk"] 51 | J0 = 30. # A/m^2 52 | F = physical_params["F"] 53 | R = physical_params["R"] 54 | T = physical_params["T"] 55 | eta = V - U 56 | return J0 * (exp(F / R / T * (eta)) 57 | - (C / C_b) * exp(-F / R / T * (eta))) 58 | 59 | def reaction_cathode(u): 60 | return reaction(u, Constant(0)) 61 | 62 | def reaction_anode(u): 63 | return reaction(u, Constant(physical_params["U_app"])) 64 | # will need to make U_app a class member if it needs updating later 65 | 66 | echem_params = [] 67 | 68 | # For Butler-Volmer, the consumption/production is determined by 69 | # the applied potential. Stoichiometry is positive. 70 | echem_params.append({"reaction": reaction_cathode, 71 | "electrons": 2, 72 | "stoichiometry": {"Cu": 1}, 73 | "boundary": "cathode", 74 | }) 75 | 76 | echem_params.append({"reaction": reaction_anode, 77 | "electrons": 2, 78 | "stoichiometry": {"Cu": 1}, 79 | "boundary": "anode", 80 | }) 81 | super().__init__( 82 | conc_params, 83 | physical_params, 84 | Mesh('bortels_structuredquad.msh'), 85 | echem_params=echem_params, 86 | family=args.family) 87 | 88 | def set_boundary_markers(self): 89 | # Inlet: 10, Outlet 11, Anode: 12, Cathode: 13, Wall: 14 90 | self.boundary_markers = {"inlet": (10,), 91 | "outlet": (11,), 92 | "anode": (12,), 93 | "cathode": (13,), 94 | } 95 | 96 | def set_velocity(self): 97 | _, y = SpatialCoordinate(self.mesh) 98 | h = 0.01 # m 99 | self.vel = as_vector( 100 | (6. * self.physical_params["v_avg"] / h**2 * y * (h - y), Constant(0.))) # m/s 101 | 102 | 103 | solver = BortelsSolver() 104 | solver.setup_solver() 105 | solver.solve() 106 | -------------------------------------------------------------------------------- /tests/test_advection_diffusion_migration_poisson.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import EchemSolver 4 | 5 | 6 | class DiffusionMigrationSolver(EchemSolver): 7 | def __init__(self, N, extruded=False, gmg=False): 8 | if extruded and gmg: 9 | plane_mesh = UnitSquareMesh(N, N, quadrilateral=True) 10 | plane_mesh_hierarchy = MeshHierarchy(plane_mesh, 1) 11 | extruded_hierarchy = ExtrudedMeshHierarchy( 12 | plane_mesh_hierarchy, 1.0, 2) 13 | mesh = extruded_hierarchy[-1] 14 | x, y, _ = SpatialCoordinate(mesh) 15 | elif extruded: 16 | plane_mesh = UnitSquareMesh(N, N, quadrilateral=True) 17 | mesh = ExtrudedMesh(plane_mesh, N, 1.0 / N) 18 | x, y, _ = SpatialCoordinate(mesh) 19 | else: 20 | # Create an initial coarse mesh 21 | initial_mesh = UnitSquareMesh(2, 2, quadrilateral=True) 22 | # Create a mesh hierarchy with uniform refinements 23 | hierarchy = MeshHierarchy(initial_mesh, N) 24 | # Use the finest mesh for the initial discretization 25 | mesh = hierarchy[-1] 26 | x, y = SpatialCoordinate(mesh) 27 | 28 | conc_params = [] 29 | 30 | C1ex = cos(x) + sin(y) + 3 31 | C2ex = cos(x) - sin(y) + 3 32 | Uex = sin(x) + cos(y) + 3 33 | self.C1ex = C1ex 34 | self.C2ex = C2ex 35 | self.Uex = Uex 36 | 37 | def f(C): 38 | D1 = 0.5 39 | D2 = 1.0 40 | z1 = 2.0 41 | z2 = -2.0 42 | f1 = div((self.vel - D1 * z1 * grad(Uex)) 43 | * C1ex) - div(D1 * grad(C1ex)) 44 | f2 = div((self.vel - D2 * z2 * grad(Uex)) 45 | * C2ex) - div(D2 * grad(C2ex)) 46 | f3 = div((- D1 * z1**2 * grad(Uex)) * C1ex) + \ 47 | div((- D2 * z2**2 * grad(Uex)) * C2ex) 48 | return [f1, f2, f3] 49 | 50 | conc_params.append({"name": "C1", 51 | "diffusion coefficient": 0.5, 52 | "z": 2., 53 | "bulk": C1ex, 54 | }) 55 | 56 | conc_params.append({"name": "C2", 57 | "diffusion coefficient": 1.0, 58 | "z": -2., 59 | "bulk": C2ex, 60 | }) 61 | physical_params = { 62 | "flow": [ 63 | "diffusion", 64 | "advection", 65 | "migration", 66 | "poisson"], 67 | "F": 1.0, 68 | "R": 1.0, 69 | "T": 1.0, 70 | "U_app": Uex, 71 | "bulk reaction": f, 72 | "v_avg": 1., 73 | } 74 | 75 | super().__init__(conc_params, physical_params, mesh) 76 | # Select a geometric multigrid preconditioner to make use of the 77 | # MeshHierarchy 78 | self.init_solver_parameters(pc_type="gmg") 79 | 80 | def set_boundary_markers(self): 81 | self.boundary_markers = {"bulk dirichlet": (1, 2, 3, 4,), 82 | "applied": (1, 2, 3, 4,), 83 | } 84 | 85 | def set_velocity(self): 86 | if self.mesh.geometric_dimension() == 3: 87 | _, y, _ = SpatialCoordinate(self.mesh) 88 | else: 89 | _, y = SpatialCoordinate(self.mesh) 90 | 91 | h = 1.0 92 | x_vel = 6. * self.physical_params["v_avg"] / h**2 * y * (h - y) 93 | 94 | if self.mesh.geometric_dimension() == 3: 95 | self.vel = as_vector( 96 | (x_vel, Constant(0.), Constant(0.))) 97 | else: 98 | self.vel = as_vector( 99 | (x_vel, Constant(0.))) 100 | 101 | 102 | def test_convergence(extruded=False, gmg=False): 103 | err_old = 1e6 104 | for i in range(3): 105 | solver = DiffusionMigrationSolver(2**(i + 1), extruded, gmg) 106 | solver.setup_solver() 107 | solver.solve() 108 | c1, c2, U = solver.u.subfunctions 109 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2)\ 110 | + errornorm(solver.Uex, U) 111 | assert err < 0.29 * err_old 112 | err_old = err 113 | 114 | 115 | test_convergence(extruded=True, gmg=True) 116 | -------------------------------------------------------------------------------- /examples/bortels_threeion.py: -------------------------------------------------------------------------------- 1 | from firedrake import * 2 | from echemfem import EchemSolver 3 | import argparse 4 | import petsc4py 5 | petsc4py.PETSc.Sys.popErrorHandler() 6 | parser = argparse.ArgumentParser(add_help=False) 7 | parser.add_argument("--family", type=str, default='DG') 8 | args, _ = parser.parse_known_args() 9 | 10 | """ 11 | 2D Flow-plate reactor with electroneutral Nernst-Planck. Using a custom gmsh 12 | mesh with refinement close to the electrodes. GMSH is used to get the mesh file 13 | from the .geo file as follows: 14 | 15 | gmsh -2 bortels_structuredquad.geo 16 | 17 | Three ion case taken from: 18 | Bortels, L., Deconinck, J. and Van Den Bossche, B., 1996. The multi-dimensional 19 | upwinding method as a new simulation tool for the analysis of multi-ion 20 | electrolytes controlled by diffusion, convection and migration. Part 1. Steady 21 | state analysis of a parallel plane flow channel. Journal of Electroanalytical 22 | Chemistry, 404(1), pp.15-26. 23 | """ 24 | 25 | class BortelsSolver(EchemSolver): 26 | def __init__(self): 27 | conc_params = [] 28 | 29 | conc_params.append({"name": "Cu", 30 | "diffusion coefficient": 7.2e-10, # m^2/s 31 | "z": 2., 32 | "bulk": 10., # mol/m^3 33 | }) 34 | conc_params.append({"name": "SO4", 35 | "diffusion coefficient": 10.65e-10, # m^2/s 36 | "z": -2., 37 | "bulk": 1010., # mol/m^3 38 | }) 39 | conc_params.append({"name": "H", 40 | "diffusion coefficient": 93.12e-10, # m^2/s 41 | "z": 1., 42 | "bulk": 2000., # mol/m^3 43 | }) 44 | 45 | physical_params = {"flow": ["advection", "diffusion", "migration", "electroneutrality"], 46 | "F": 96485.3329, # C/mol 47 | "R": 8.3144598, # J/K/mol 48 | "T": 273.15 + 25., # K 49 | "U_app": 0.006, # V 50 | "v_avg": 0.03 # m/s 51 | } 52 | 53 | def reaction(u, V): 54 | C = u[0] 55 | U = u[2] 56 | C_b = conc_params[0]["bulk"] 57 | J0 = 30. # A/m^2 58 | F = physical_params["F"] 59 | R = physical_params["R"] 60 | T = physical_params["T"] 61 | eta = V - U 62 | return J0 * (exp(F / R / T * (eta)) 63 | - (C / C_b) * exp(-F / R / T * (eta))) 64 | 65 | def reaction_cathode(u): 66 | return reaction(u, Constant(0)) 67 | 68 | def reaction_anode(u): 69 | return reaction(u, Constant(physical_params["U_app"])) 70 | # will need to make U_app a class member if it needs updating later 71 | 72 | echem_params = [] 73 | 74 | echem_params.append({"reaction": reaction_cathode, 75 | "electrons": 2, 76 | "stoichiometry": {"Cu": 1}, 77 | "boundary": "cathode", 78 | }) 79 | 80 | echem_params.append({"reaction": reaction_anode, 81 | "electrons": 2, 82 | "stoichiometry": {"Cu": 1}, 83 | "boundary": "anode", 84 | }) 85 | super().__init__( 86 | conc_params, 87 | physical_params, 88 | Mesh('bortels_structuredquad.msh'), 89 | echem_params=echem_params, 90 | family=args.family) 91 | 92 | def set_boundary_markers(self): 93 | # Inlet: 10, Outlet 11, Anode: 12, Cathode: 13, Wall: 14 94 | self.boundary_markers = {"inlet": (10,), 95 | "outlet": (11,), 96 | "anode": (12,), 97 | "cathode": (13,), 98 | } 99 | 100 | def set_velocity(self): 101 | _, y = SpatialCoordinate(self.mesh) 102 | h = 0.01 # m 103 | self.vel = as_vector( 104 | (6. * self.physical_params["v_avg"] / h**2 * y * (h - y), Constant(0.))) # m/s 105 | 106 | 107 | solver = BortelsSolver() 108 | solver.setup_solver() 109 | solver.solve() 110 | -------------------------------------------------------------------------------- /examples/BMCSL.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Friday Jan 27 2023 3 | 4 | @author: Nitish Govindarajan 5 | 6 | Simple example for the BMCSL model for finite size ion effects 7 | 8 | Code to reproduce Figure 1 from doi:10.1016/j.jcis.2007.08.006 9 | Biesheuvel, P.M. and Van Soestbergen, M., 2007. Counterion volume effects in 10 | mixed electrical double layers. Journal of Colloid and Interface Science, 11 | 316(2), pp.490-499. 12 | """ 13 | 14 | # Import required libraries 15 | 16 | from firedrake import * 17 | from echemfem import EchemSolver, IntervalBoundaryLayerMesh 18 | from math import log10 19 | import matplotlib.pyplot as plt 20 | import matplotlib.gridspec as gridspec 21 | from matplotlib.ticker import FormatStrFormatter 22 | import numpy as np 23 | import math 24 | 25 | # Initialize bulk concentrations (1 mM CsCl + 9 mM LiCl) 26 | 27 | C_Cs_bulk = 1 28 | 29 | C_Li_bulk = 9 30 | 31 | C_Cl_bulk = 10 32 | 33 | 34 | class BMCSLSolver(EchemSolver): 35 | 36 | def __init__(self): 37 | 38 | delta = 0.00008 # Boundary layer thickness (m) 39 | 40 | mesh = IntervalBoundaryLayerMesh(200, delta, 800, 1e-8, boundary=(2,)) 41 | 42 | x = SpatialCoordinate(mesh)[0] 43 | 44 | conc_params = [] 45 | 46 | conc_params.append({"name": "Cl", 47 | "diffusion coefficient": 20.6e-10, # m^2/s 48 | "bulk": C_Cl_bulk, # mol/m3 49 | "z": -1, 50 | "solvated diameter": 0.0 # co-ion size not relevant at sufficiently high negative surface charge density 51 | }) 52 | 53 | conc_params.append({"name": "Li", 54 | "diffusion coefficient": 10.6e-10, # m^2/s 55 | "bulk": C_Li_bulk, # mol/m3 56 | "z": 1, 57 | "solvated diameter": 7.6e-10 # m (a_Li = 3.8 A) 58 | }) 59 | 60 | conc_params.append({"name": "Cs", 61 | "diffusion coefficient": 20.6e-10, # m^2/s 62 | "bulk": C_Cs_bulk, # mol/m3 63 | "z": 1, 64 | "solvated diameter": 6.6e-10 # m (a_Cs = 3.3 A) 65 | }) 66 | 67 | self.U_solid = Constant(0) 68 | 69 | physical_params = {"flow": ["migration", "poisson", "diffusion finite size_BMCSL"], # Poisson with BMCSL finite size correction 70 | "F": 96485., # C/mol 71 | "R": 8.3144598, # J/K/mol 72 | "T": 273.15 + 25., # K 73 | "U_app": conditional(gt(x, 1e-6), 0.0, 0.0), 74 | "vacuum permittivity": 8.8541878128e-12, # F/m 75 | "relative permittivity": 78.4, 76 | "Avogadro constant": 6.02214076e23, # 1/mol 77 | "surface charge density": Constant(0), # C/m^2 78 | } 79 | 80 | super().__init__(conc_params, physical_params, mesh, family="CG", p=2) 81 | 82 | def set_boundary_markers(self): 83 | self.boundary_markers = {"bulk dirichlet": (1,), # C = C_0 84 | "applied": (1,), # U_liquid = 0 85 | "poisson neumann": (2,), 86 | } 87 | 88 | 89 | solver = BMCSLSolver() 90 | 91 | 92 | x = SpatialCoordinate(solver.mesh)[0] 93 | 94 | solver.setup_solver() 95 | 96 | solver.solve() 97 | 98 | 99 | xmesh = Function(solver.V).interpolate(solver.mesh.coordinates[0]) 100 | 101 | np.savetxt('xmesh.tsv', xmesh.dat.data) 102 | 103 | 104 | # Surface charge densities in C/m2 105 | 106 | sigmas = np.arange(0, -0.625, -0.025) 107 | 108 | 109 | store_sigmas = [-0.3, -0.4, -0.5, -0.6] # surface charge densities reported in Figure 1 110 | 111 | for sigma in sigmas: 112 | 113 | sigma = round(sigma, 2) 114 | 115 | solver.physical_params["surface charge density"].assign(sigma) 116 | 117 | solver.solve() 118 | 119 | C_Cl, C_Li, C_Cs, Phi = solver.u.subfunctions 120 | 121 | if (sigma in store_sigmas): 122 | 123 | print(sigma) 124 | 125 | np.savetxt('Cs_{0:.1g}.tsv'.format(sigma), C_Cs.dat.data) 126 | 127 | np.savetxt('Li_{0:.1g}.tsv'.format(sigma), C_Li.dat.data) 128 | 129 | np.savetxt('phi_{0:.1g}.tsv'.format(sigma), Phi.dat.data) 130 | -------------------------------------------------------------------------------- /tests/test_advection_diffusion_migration.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import EchemSolver 4 | 5 | 6 | class AdvectionDiffusionMigrationSolver(EchemSolver): 7 | def __init__(self, N, vavg, family="DG"): 8 | mesh = UnitSquareMesh(N, N, quadrilateral=True) 9 | conc_params = [] 10 | 11 | x, y = SpatialCoordinate(mesh) 12 | C1ex = cos(x) + sin(y) + 3 13 | C2ex = C1ex 14 | Uex = sin(x) + cos(y) + 3 15 | self.C1ex = C1ex 16 | self.C2ex = C2ex 17 | self.Uex = Uex 18 | 19 | def f(C): 20 | D1 = 0.5 21 | D2 = 1.0 22 | z1 = 2.0 23 | z2 = -2.0 24 | f1 = div((self.vel - D1 * z1 * grad(Uex)) 25 | * C1ex) - div(D1 * grad(C1ex)) 26 | f2 = div((self.vel - D2 * z2 * grad(Uex)) 27 | * C2ex) - div(D2 * grad(C2ex)) 28 | return [f1, f2] 29 | 30 | conc_params.append({"name": "C1", 31 | "diffusion coefficient": 0.5, 32 | "z": 2., 33 | "bulk": C1ex, 34 | }) 35 | 36 | conc_params.append({"name": "C2", 37 | "diffusion coefficient": 1.0, 38 | "z": -2., 39 | "bulk": C2ex, 40 | }) 41 | physical_params = {"flow": ["diffusion", "advection", "migration", "electroneutrality"], 42 | "F": 1.0, # C/mol 43 | "R": 1.0, # J/K/mol 44 | "T": 1.0, # K 45 | "U_app": Uex, # V 46 | "bulk reaction": f, 47 | "v_avg": Constant(vavg), 48 | } 49 | 50 | super().__init__(conc_params, physical_params, mesh, family=family) 51 | 52 | def set_boundary_markers(self): 53 | self.boundary_markers = { # "inlet": (1,2,3,4,), 54 | "applied": (1, 2, 3, 4,), 55 | # "outlet": (1,2,3,4,), 56 | "bulk dirichlet": (1, 2, 3, 4,), 57 | } 58 | 59 | def set_velocity(self): 60 | _, y = SpatialCoordinate(self.mesh) 61 | h = 1.0 62 | self.vel = as_vector( 63 | (6. * self.physical_params["v_avg"] / h**2 * y * (h - y), Constant(0.))) 64 | 65 | 66 | def test_convergence_low_peclet(): 67 | errC_old = 1e6 68 | errU_old = 1e6 69 | for i in range(6): 70 | solver = AdvectionDiffusionMigrationSolver(2**(i + 1), 1.0) 71 | solver.setup_solver() 72 | solver.solve() 73 | c1, U = solver.u.subfunctions 74 | errC = errornorm(solver.C1ex, c1) 75 | errU = errornorm(solver.Uex, U) 76 | assert errC < 0.26 * errC_old 77 | assert errU < 0.26 * errU_old 78 | errC_old = errC 79 | errU_old = errU 80 | 81 | 82 | def test_convergence_high_peclet(): 83 | errC_old = 1e6 84 | errU_old = 1e6 85 | for i in range(6): 86 | solver = AdvectionDiffusionMigrationSolver(2**(i + 1), 1e5) 87 | solver.setup_solver() 88 | solver.solve() 89 | c1, U = solver.u.subfunctions 90 | errC = errornorm(solver.C1ex, c1) 91 | errU = errornorm(solver.Uex, U) 92 | assert errC < 0.36 * errC_old # p+1/2 convergence 93 | assert errU < 0.26 * errU_old 94 | errC_old = errC 95 | errU_old = errU 96 | 97 | 98 | def test_convergence_low_peclet_CG(): 99 | errC_old = 1e6 100 | errU_old = 1e6 101 | for i in range(6): 102 | solver = AdvectionDiffusionMigrationSolver(2**(i + 1), 1.0, family="CG") 103 | solver.setup_solver() 104 | solver.solve() 105 | c1, U = solver.u.subfunctions 106 | errC = errornorm(solver.C1ex, c1) 107 | errU = errornorm(solver.Uex, U) 108 | assert errC < 0.26 * errC_old 109 | assert errU < 0.26 * errU_old 110 | errC_old = errC 111 | errU_old = errU 112 | 113 | 114 | def test_convergence_high_peclet_CG(): 115 | errC_old = 1e6 116 | errU_old = 1e6 117 | for i in range(6): 118 | solver = AdvectionDiffusionMigrationSolver(2**(i + 2), 1e5, family="CG") 119 | solver.setup_solver() 120 | solver.solve() 121 | c1, U = solver.u.subfunctions 122 | errC = errornorm(solver.C1ex, c1) 123 | errU = errornorm(solver.Uex, U) 124 | assert errC < 0.66 * errC_old 125 | assert errU < 0.51 * errU_old 126 | errC_old = errC 127 | errU_old = errU 128 | -------------------------------------------------------------------------------- /examples/bortels_twoion_nondim.py: -------------------------------------------------------------------------------- 1 | from firedrake import * 2 | from echemfem import EchemSolver 3 | 4 | """ 5 | 2D Flow-plate reactor with electroneutral Nernst-Planck. Using a custom gmsh 6 | mesh with refinement close to the electrodes. GMSH is used to get the mesh file 7 | from the .geo file as follows: 8 | 9 | gmsh -2 bortels_structuredquad_nondim.geo 10 | 11 | Two ion case taken from: 12 | Bortels, L., Deconinck, J. and Van Den Bossche, B., 1996. The multi-dimensional 13 | upwinding method as a new simulation tool for the analysis of multi-ion 14 | electrolytes controlled by diffusion, convection and migration. Part 1. Steady 15 | state analysis of a parallel plane flow channel. Journal of Electroanalytical 16 | Chemistry, 404(1), pp.15-26. 17 | 18 | Nondimensionalization from: 19 | Roy, T., Andrej, J. and Beck, V.A., 2023. A scalable DG solver for the 20 | electroneutral Nernst-Planck equations. Journal of Computational Physics, 475, 21 | p.111859. 22 | """ 23 | 24 | class BortelsSolver(EchemSolver): 25 | def __init__(self): 26 | conc_params = [] 27 | 28 | D_Cu = 7.2e-10 # m^2/s 29 | J0 = 30. # A/m^2 30 | C_Cu = 10. # mol/m^3 31 | D_SO4 = 10.65e-10 # m^2/s 32 | C_SO4 = 10. # mol/m^3 33 | F = 96485.3329 # C/mol 34 | R = 8.3144598 # J/K/mol 35 | T = 273.15 + 25. # K 36 | U_app = 0.1 # V 37 | v_avg = 0.01 # m/s 38 | 39 | L = 0.01 # m 40 | 41 | D_Cu_hat = D_Cu / L / v_avg 42 | D_SO4_hat = D_SO4 / L / v_avg 43 | J0_hat = J0 / v_avg / C_Cu / F 44 | U_app_hat = F / R / T * U_app 45 | 46 | conc_params = [] 47 | conc_params.append({"name": "Cu", 48 | "diffusion coefficient": D_Cu_hat, 49 | "z": 2., 50 | "bulk": 1., 51 | }) 52 | 53 | conc_params.append({"name": "SO4", 54 | "diffusion coefficient": D_SO4_hat, 55 | "z": -2., 56 | "bulk": 1., 57 | }) 58 | 59 | physical_params = { 60 | "flow": [ 61 | "advection", 62 | "diffusion", 63 | "migration", 64 | "electroneutrality"], 65 | "F": 1.0, 66 | "R": 1.0, 67 | "T": 1.0, 68 | "U_app": U_app_hat, 69 | "v_avg": 1.0} 70 | 71 | def reaction(u, V): 72 | C = u[0] 73 | U = u[1] 74 | C_b = conc_params[0]["bulk"] 75 | J0 = J0_hat 76 | F = physical_params["F"] 77 | R = physical_params["R"] 78 | T = physical_params["T"] 79 | eta = V - U 80 | return J0 * (exp(F / R / T * (eta)) 81 | - (C / C_b) * exp(-F / R / T * (eta))) 82 | 83 | def reaction_cathode(u): 84 | return reaction(u, Constant(0)) 85 | 86 | def reaction_anode(u): 87 | return reaction(u, Constant(physical_params["U_app"])) 88 | # will need to make U_app a class member if it needs updating later 89 | 90 | echem_params = [] 91 | 92 | echem_params.append({"reaction": reaction_cathode, 93 | "electrons": 2, 94 | "stoichiometry": {"Cu": 1}, 95 | "boundary": "cathode", 96 | }) 97 | 98 | echem_params.append({"reaction": reaction_anode, 99 | "electrons": 2, 100 | "stoichiometry": {"Cu": 1}, 101 | "boundary": "anode", 102 | }) 103 | 104 | mesh = Mesh('bortels_structuredquad_nondim.msh') 105 | super().__init__(conc_params, physical_params, mesh, echem_params=echem_params) 106 | 107 | def set_boundary_markers(self): 108 | # Inlet: 10, Outlet 11, Anode: 12, Cathode: 13, Wall: 14 109 | self.boundary_markers = {"inlet": (10,), 110 | "outlet": (11,), 111 | "anode": (12,), 112 | "cathode": (13,), 113 | } 114 | 115 | def set_velocity(self): 116 | _, y = SpatialCoordinate(self.mesh) 117 | h = 1.0 118 | self.vel = as_vector( 119 | (6. * self.physical_params["v_avg"] / h**2 * y * (h - y), Constant(0.))) # m/s 120 | 121 | 122 | solver = BortelsSolver() 123 | solver.setup_solver() 124 | solver.solve() 125 | -------------------------------------------------------------------------------- /joss/paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'EchemFEM: A Firedrake-based Python package for electrochemical transport' 3 | tags: 4 | - Python 5 | - Firedrake 6 | - Finite Element Method 7 | - electrochemistry 8 | authors: 9 | - name: Thomas Roy 10 | orcid: 0000-0000-0000-0000 11 | corresponding: true # (This is how to denote the corresponding author) 12 | affiliation: 1 13 | - name: Author Without ORCID 14 | affiliation: 2 15 | affiliations: 16 | - name: Lawrence Livermore National Laboratory, CA, USA 17 | index: 1 18 | - name: Institution Name, Country 19 | index: 2 20 | date: 12 December 2023 21 | bibliography: paper.bib 22 | 23 | --- 24 | 25 | # Summary 26 | 27 | 28 | The shift from fossil fuels towards renewable energy brings about a substantial increase in clean but intermittent electricity. 29 | Thankfully, diverse electrochemical technologies, including energy storage and electrochemical manufacturing, can harness this surplus energy that would otherwise go to waste. 30 | Managing the growing prevalence of renewable energy underscores the importance of developing and scaling up these technologies. 31 | Likewise, the electrification of transport creates an increasing need for efficient electrochemical energy storage devices such as batteries and supercapacitors. 32 | Naturally, simulation tools are required to assist in the design of efficient and industrial-scale electrochemical devices. 33 | 34 | 35 | Modeling and simulation are used extensively to describe the physics of the electrochemical and transport mechanisms in electrochemical devices. 36 | These devices have various applications, such as batteries and supercapacitors, which are used for energy storage. 37 | Flow batteries have a similar function, but with a flowing electrolyte. 38 | Electrolyzers can be used to transform carbon dioxide into useful products or create hydrogen from water. 39 | Proton-exchange membrane fuel cells reverse this process to harness electricity from hydrogen. 40 | While these devices vary wildly in purpose, the governing equations used to describe them are very similar. 41 | Transport of charged chemical species in a fluid is often modeled using the Nernst-Planck equation, 42 | which includes the usual advection and diffusion transport as well as *electromigration*, where charged species are transported by an electric field. 43 | 44 | 45 | EchemFEM provides a high-level user interface for a finite element implementation of the Nernst-Planck equation. 46 | The user is simply required to provide physical parameters as well as functions describing for chemical reactions. 47 | All transport options are then selected using keyword arguments. 48 | Ionic charge can either be modeled using the Poisson equation or the electroneutrality approximation. 49 | The simulated devices can be resolved electrolyte-electrode interfaces, or have homogenized porous electrodes, in which case electron conduction is also modeled. 50 | Finite size effects are also available to allow the use of models such as Generalized Modified Poisson-Nernst-Planck (GMPNP). 51 | 52 | 53 | EchemFEM is based on Firedrake [@FiredrakeUserManual], an open-source finite element package, 54 | enabling straightforward implementation of the governing equations in Python. 55 | Firedake has access to scalable, customizable, solvers through its interface with PETSc [@petsc-user-ref,petsc-web-page], allowing for parallelization and scalability on computing clusters. 56 | This balance between usability and scalability permits a seamless transition from prototyping to large-scale simulation. 57 | EchemFEM leverages Firedrake's capabilities while further increasing the ease-of-use. 58 | Indeed, since the governing equations are already implemented, little to no knowledge of Firedrake and the finite element method is required to use EchemFEM 59 | 60 | 61 | 62 | Examples 63 | 64 | # Statement of need 65 | 66 | 67 | [@roy2023scalable] 68 | 69 | 70 | 71 | # Acknowledgements 72 | 73 | This work was performed under the auspices of the U.S. Department of Energy by Lawrence Livermore National Laboratory (LLNL) under Contract DE-AC52-07NA27344, and was partially supported by a Cooperative Research and Development Agreement (CRADA) between LLNL and TotalEnergies American Services, Inc. (affiliate of TotalEnergies SE) under agreement number TC02307 and Laboratory Directed Research and Development (LDRD) funding under projects 19-ERD-035 and 22-SI-006. 74 | 75 | # References 76 | -------------------------------------------------------------------------------- /examples/simple_flow_battery.py: -------------------------------------------------------------------------------- 1 | from firedrake import * 2 | from echemfem import EchemSolver, NavierStokesBrinkmanFlowSolver 3 | import argparse 4 | 5 | 6 | """ 7 | A simple Vanadium flow battery using advection-diffusion-reaction, Poisson for 8 | the ionic potential with a predefinied conductivity and Navier-Stokes-Brinkman 9 | for the flow. 10 | 11 | Model taken from 12 | Lin, T.Y., Baker, S.E., Duoss, E.B. and Beck, V.A., 2022. Topology optimization 13 | of 3D flow fields for flow batteries. Journal of The Electrochemical Society, 14 | 169(5), p.050540. 15 | """ 16 | 17 | parser = argparse.ArgumentParser(add_help=False) 18 | parser.add_argument("--family", type=str, default='CG') 19 | parser.add_argument("--vel_file", type=str, default=None) 20 | args, _ = parser.parse_known_args() 21 | if args.family == "CG": 22 | SUPG = True 23 | else: 24 | SUPG = False 25 | 26 | current_density = 50 * 10 # A/m2 -> 50 mA/cm2 27 | 28 | electrode_thickness = 500e-6 # m 29 | electrode_length = 500e-6 # m 30 | mesh = RectangleMesh(50, 50, electrode_length, electrode_thickness, quadrilateral=True) 31 | 32 | class FlowBatterySolver(EchemSolver): 33 | def __init__(self): 34 | conc_params = [] 35 | 36 | conc_params.append({"name": "V2", 37 | "diffusion coefficient": 2.4e-10, # m^2/s 38 | "bulk": 1000., # mol/m^3 39 | }) 40 | 41 | conc_params.append({"name": "V3", 42 | "diffusion coefficient": 2.4e-10, # m^2/s 43 | "bulk": 1000., # mol/m^3 44 | }) 45 | 46 | physical_params = {"flow": ["advection", "diffusion", "poisson", "porous"], 47 | "F": 96485.3329, # C/mol 48 | "R": 8.3144598, # J/K/mol 49 | "T": 273.15 + 25., # K 50 | "solid conductivity": 1e4, # S/m 51 | "liquid conductivity": 40, # S/m 52 | "specific surface area": 8e4, # 1/m 53 | "porosity": 0.68, 54 | "U_app": 0., # ground U_solid = 0 55 | "applied current density": Constant(current_density), 56 | } 57 | 58 | def reaction(u): 59 | # Butler-Volmer 60 | V2 = u[0] 61 | V3 = u[1] 62 | Phi2 = u[2] 63 | Phi1 = u[3] 64 | #Phi2 = -0.2499 65 | #Phi1 = 0 66 | Cref = 1. # mol/m3 67 | J0 = 0.016 # A/m^2 68 | U0 = -0.25 69 | F = physical_params["F"] 70 | R = physical_params["R"] 71 | T = physical_params["T"] 72 | beta = 0.5 * F / R / T 73 | eta = Phi1 - Phi2 - U0 74 | return J0 / Cref * (V2 * exp(beta * eta) 75 | - V3 * exp(-beta * eta)) 76 | 77 | echem_params = [] 78 | echem_params.append({"reaction": reaction, 79 | "electrons": 1, 80 | "stoichiometry": {"V2": -1, 81 | "V3": 1}, 82 | }) 83 | super().__init__( 84 | conc_params, 85 | physical_params, 86 | mesh, 87 | echem_params=echem_params, 88 | family=args.family, 89 | SUPG=SUPG) 90 | 91 | def set_boundary_markers(self): 92 | self.boundary_markers = {"inlet": (1,), 93 | "outlet": (2,), 94 | "applied": (3,), # U_solid = 0 95 | "applied liquid current": (4,), 96 | } 97 | 98 | def set_velocity(self): 99 | boundary_markers = {"no slip": (3,4,), 100 | "inlet pressure": (1,), 101 | "outlet pressure": (2,), 102 | } 103 | 104 | flow_params = {"outlet pressure": 0., 105 | "inlet pressure": 1e-1, 106 | "density": 1e3, # kg/m3 107 | "dynamic viscosity": 8.9e-4, # Pa s 108 | "permeability": 5.53e-11 # m2 109 | } 110 | 111 | NSB_solver = NavierStokesBrinkmanFlowSolver(mesh, flow_params, boundary_markers) 112 | NSB_solver.setup_solver() 113 | NSB_solver.solve() 114 | self.vel = NSB_solver.vel 115 | 116 | 117 | solver = FlowBatterySolver() 118 | solver.setup_solver(initial_solve=False) 119 | solver.solve() 120 | -------------------------------------------------------------------------------- /tests/test_advection_diffusion.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import EchemSolver 4 | 5 | 6 | class AdvectionDiffusionSolver(EchemSolver): 7 | def __init__(self, N, D, extruded=False, family="DG"): 8 | if extruded: 9 | plane_mesh = UnitSquareMesh(N, N, quadrilateral=True) 10 | mesh = ExtrudedMesh(plane_mesh, N, layer_height=1.0 / N, 11 | extrusion_type='uniform') 12 | x, y, _ = SpatialCoordinate(mesh) 13 | else: 14 | mesh = UnitSquareMesh(N, N, quadrilateral=True) 15 | x, y = SpatialCoordinate(mesh) 16 | 17 | conc_params = [] 18 | 19 | C1ex = sin(x) + cos(y) 20 | C2ex = cos(x) + sin(y) 21 | self.C1ex = C1ex 22 | self.C2ex = C2ex 23 | 24 | def f(C): 25 | f1 = div(self.vel * C1ex - D * grad(C1ex)) 26 | f2 = div(self.vel * C2ex - D * grad(C2ex)) 27 | return [f1, f2] 28 | 29 | conc_params.append({"name": "C1", 30 | "diffusion coefficient": D, 31 | "bulk": C1ex, 32 | }) 33 | 34 | conc_params.append({"name": "C2", 35 | "diffusion coefficient": D, 36 | "bulk": C2ex, 37 | }) 38 | physical_params = {"flow": ["diffusion", "advection"], 39 | "bulk reaction": f, 40 | "v_avg": 1.0, 41 | } 42 | 43 | super().__init__(conc_params, physical_params, mesh, family=family) 44 | 45 | def set_boundary_markers(self): 46 | self.boundary_markers = {"inlet": (1, 2, 3, 4), 47 | "outlet": (1, 2, 3, 4), 48 | "bulk dirichlet": (1, 2, 3, 4), 49 | } 50 | 51 | def set_velocity(self): 52 | if self.mesh.layers is not None: 53 | _, y, _ = SpatialCoordinate(self.mesh) 54 | else: 55 | _, y = SpatialCoordinate(self.mesh) 56 | 57 | h = 1.0 58 | x_vel = 6. * self.physical_params["v_avg"] / h**2 * y * (h - y) 59 | 60 | if self.mesh.layers is not None: 61 | self.vel = as_vector( 62 | (x_vel, Constant(0.), Constant(0.))) 63 | else: 64 | self.vel = as_vector( 65 | (x_vel, Constant(0.))) 66 | 67 | 68 | def test_convergence_low_peclet(extruded=False): 69 | err_old = 1e6 70 | if extruded: 71 | n = 3 72 | else: 73 | n = 5 74 | for i in range(n): 75 | solver = AdvectionDiffusionSolver(2**(i + 1), 1., extruded=extruded) 76 | solver.setup_solver() 77 | solver.solve() 78 | c1, c2 = solver.u.subfunctions 79 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2) 80 | assert err < 0.29 * err_old 81 | err_old = err 82 | 83 | 84 | def test_convergence_high_peclet(extruded=False): 85 | err_old = 1e6 86 | if extruded: 87 | n = 3 88 | else: 89 | n = 5 90 | for i in range(n): 91 | solver = AdvectionDiffusionSolver(2**(i + 1), 1e-3, extruded=extruded) 92 | solver.setup_solver() 93 | solver.solve() 94 | c1, c2 = solver.u.subfunctions 95 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2) 96 | assert err < 0.29 * err_old 97 | err_old = err 98 | 99 | 100 | def test_convergence_low_peclet_CG(extruded=False): 101 | err_old = 1e6 102 | if extruded: 103 | n = 3 104 | else: 105 | n = 5 106 | for i in range(n): 107 | solver = AdvectionDiffusionSolver(2**(i + 1), 1., extruded=extruded, family="CG") 108 | solver.setup_solver() 109 | solver.solve() 110 | c1, c2 = solver.u.subfunctions 111 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2) 112 | assert err < 0.29 * err_old 113 | err_old = err 114 | 115 | 116 | def test_convergence_high_peclet_CG(extruded=False): 117 | err_old = 1e6 118 | if extruded: 119 | n = 2 120 | else: 121 | n = 5 122 | for i in range(n): 123 | solver = AdvectionDiffusionSolver(2**(i + 2), 1./20., extruded=extruded, family="CG") 124 | solver.setup_solver() 125 | solver.solve() 126 | c1, c2 = solver.u.subfunctions 127 | err = errornorm(solver.C1ex, c1) + errornorm(solver.C2ex, c2) 128 | assert err < 0.62 * err_old # CG not enough here 129 | err_old = err 130 | 131 | 132 | def test_convergence_low_peclet_extruded(): 133 | test_convergence_low_peclet(extruded=True) 134 | 135 | 136 | def test_convergence_high_peclet_extruded(): 137 | test_convergence_high_peclet(extruded=True) 138 | 139 | 140 | def test_convergence_low_peclet_extruded_CG(): 141 | test_convergence_low_peclet_CG(extruded=True) 142 | 143 | 144 | def test_convergence_high_peclet_extruded_CG(): 145 | test_convergence_high_peclet_CG(extruded=True) 146 | -------------------------------------------------------------------------------- /examples/gupta.py: -------------------------------------------------------------------------------- 1 | from firedrake import * 2 | from echemfem import EchemSolver, IntervalBoundaryLayerMesh 3 | 4 | 5 | class GuptaSolver(EchemSolver): 6 | """ 7 | A 1D example of diffusion-reaction for CO2 electrolysis with simplified 8 | bicarbonate bulk reactions. 9 | 10 | Steady-state version of example from 11 | Gupta, N., Gattrell, M. and MacDougall, B., 2006. Calculation for the 12 | cathode surface concentrations in the electrochemical reduction of CO2 in 13 | KHCO3 solutions. Journal of applied electrochemistry, 36(2), pp.161-172. 14 | """ 15 | 16 | def __init__(self): 17 | 18 | delta = 0.0001 19 | mesh = IntervalBoundaryLayerMesh(100, delta, 100, 1e-6) 20 | 21 | def bulk_reaction(C): 22 | C_CO2 = C[0] 23 | C_HCO3 = C[1] 24 | C_CO3 = C[2] 25 | C_OH = C[3] 26 | 27 | k1f = 5.93e3 # 1/M/s 28 | k2f = 1e8 # 1/M/s 29 | k1r = 1.34e-4 # 1/s 30 | k2r = 2.15e4 # 1/s 31 | dCO2 = k1r * C_HCO3 - k1f * C_OH * C_CO2 32 | dHCO3 = k1f * C_OH * C_CO2 - k1r * C_HCO3 \ 33 | - k2f * C_HCO3 * C_OH + k2r * C_CO3 34 | dCO3 = k2f * C_HCO3 * C_OH - k2r * C_CO3 35 | dOH = - k1f * C_CO2 * C_OH + k1r * C_HCO3 \ 36 | - k2f * C_HCO3 * C_OH + k2r * C_CO3 37 | return [dCO2, dHCO3, dCO3, dOH] 38 | 39 | conc_params = [] 40 | 41 | conc_params.append({"name": "CO2", 42 | "diffusion coefficient": 19.1e-10, # m^2/s 43 | "bulk": 0.0342, # M 44 | }) 45 | 46 | conc_params.append({"name": "HCO3", 47 | "diffusion coefficient": 9.23e-10, # m^2/s 48 | "bulk": 0.499, # M 49 | }) 50 | 51 | conc_params.append({"name": "CO3", 52 | "diffusion coefficient": 11.9e-10, # m^2/s 53 | "bulk": 7.6e-4, # M 54 | }) 55 | 56 | conc_params.append({"name": "OH", 57 | "diffusion coefficient": 52.7e-10, # m^2/s 58 | "bulk": 3.3e-7, # M 59 | }) 60 | 61 | physical_params = {"flow": ["diffusion"], 62 | "bulk reaction": bulk_reaction, 63 | } 64 | 65 | super().__init__(conc_params, physical_params, mesh, family="CG") 66 | 67 | def neumann(self, C, conc_params, u): 68 | name = conc_params["name"] 69 | if name in ["HCO3", "CO3"]: 70 | return Constant(0) 71 | j = 0.05 # 50 A/m^2. Adjustment for concentration units 72 | F = 96485.3329 # C/mol 73 | zeffCH4 = 8. 74 | zeffC2H4 = 12. 75 | zeffCO = 2. 76 | zeffHCOO = 2. 77 | zeffH2 = 2. 78 | cefCH4 = 0.25 79 | cefC2H4 = 0.20 80 | cefCO = 0.05 81 | cefHCOO = 0.10 82 | cefH2 = 0.40 83 | if name == "CO2": 84 | return -(j / F) * (cefHCOO / zeffHCOO + cefCO / zeffCO 85 | + cefCH4 / zeffCH4 + 2 * cefC2H4 / zeffC2H4) 86 | if name == "OH": 87 | return (j / F) * (cefHCOO / zeffHCOO + 2 * cefCO / zeffCO 88 | + 8 * cefCH4 / zeffCH4 + 12 * cefC2H4 / zeffC2H4 89 | + 2 * cefH2 / zeffH2) # note error in Gupta et al. 90 | 91 | def set_boundary_markers(self): 92 | self.boundary_markers = {"bulk dirichlet": (1,), 93 | "neumann": (2,), 94 | } 95 | 96 | 97 | solver = GuptaSolver() 98 | solver.setup_solver() 99 | solver.solve() 100 | 101 | ## Plotting 102 | 103 | C_CO2, C_HCO3, C_CO3, C_OH = solver.u.subfunctions 104 | # OH boundary layer 105 | x = solver.mesh.coordinates 106 | C_OH_bl = Function(solver.V).assign(C_OH).dat.data[100:] 107 | x_bl = x.dat.data[100:] 108 | 109 | import matplotlib.pyplot as plt 110 | import matplotlib.gridspec as gridspec 111 | from matplotlib.ticker import FormatStrFormatter 112 | filename = "gupta.png" 113 | fig = plt.figure(constrained_layout=True, figsize=(16, 8)) 114 | spec = gridspec.GridSpec(ncols=3, nrows=2, figure=fig) 115 | ax1 = fig.add_subplot(spec[0, 0]) 116 | ax2 = fig.add_subplot(spec[0, 1]) 117 | ax3 = fig.add_subplot(spec[1, 0]) 118 | ax4 = fig.add_subplot(spec[1, 1]) 119 | ax5 = fig.add_subplot(spec[1, 2]) 120 | 121 | plot(C_CO2, axes=ax1) 122 | ax1.set(xlabel='distance (m)', 123 | ylabel='CO$_2$ concentration (M)') 124 | plot(C_HCO3, axes=ax2) 125 | ax2.set(xlabel='distance (m)', 126 | ylabel='HCO$_3$ concentration (M)') 127 | plot(C_CO3, axes=ax3) 128 | ax3.set(xlabel='distance (m)', 129 | ylabel='CO$_3$ concentration (M)') 130 | plot(C_OH, axes=ax4) 131 | ax4.set(xlabel='distance (m)', 132 | ylabel='OH concentration (M)') 133 | plt.plot(x_bl, C_OH_bl, color='k', linewidth=2) 134 | ax5.set(xlabel='distance (m)', 135 | ylabel='OH concentration (M)') 136 | 137 | plt.savefig(filename) 138 | -------------------------------------------------------------------------------- /examples/bortels_threeion_nondim.py: -------------------------------------------------------------------------------- 1 | from firedrake import * 2 | from echemflow import EchemSolver 3 | 4 | """ 5 | 2D Flow-plate reactor with electroneutral Nernst-Planck. Using a custom gmsh 6 | mesh with refinement close to the electrodes. GMSH is used to get the mesh file 7 | from the .geo file as follows: 8 | 9 | gmsh -2 bortels_structuredquad_nondim.geo 10 | 11 | Three ion case taken from: 12 | Bortels, L., Deconinck, J. and Van Den Bossche, B., 1996. The multi-dimensional 13 | upwinding method as a new simulation tool for the analysis of multi-ion 14 | electrolytes controlled by diffusion, convection and migration. Part 1. Steady 15 | state analysis of a parallel plane flow channel. Journal of Electroanalytical 16 | Chemistry, 404(1), pp.15-26. 17 | 18 | Nondimensionalization from: 19 | Roy, T., Andrej, J. and Beck, V.A., 2023. A scalable DG solver for the 20 | electroneutral Nernst-Planck equations. Journal of Computational Physics, 475, 21 | p.111859. 22 | """ 23 | 24 | class BortelsSolver(EchemSolver): 25 | def __init__(self): 26 | conc_params = [] 27 | 28 | D_Cu = 7.2e-10 # m^2/s 29 | J0 = 30. # A/m^2 30 | C_Cu = 10. # mol/m^3 31 | D_SO4 = 10.65e-10 # m^2/s 32 | C_SO4 = 1010. # mol/m^3 33 | D_H = 93.12e-10 # m^2/s 34 | C_H = 2000. # mol/m^3 35 | F = 96485.3329 # C/mol 36 | R = 8.3144598 # J/K/mol 37 | T = 273.15 + 25. # K 38 | U_app = 0.006 # V 39 | v_avg = 0.03 # m/s 40 | 41 | L = 0.01 # m 42 | 43 | D_Cu_hat = D_Cu / L / v_avg 44 | D_SO4_hat = D_SO4 / L / v_avg 45 | D_H_hat = D_H / L / v_avg 46 | J0_hat = J0 / v_avg / C_Cu / F 47 | U_app_hat = F / R / T * U_app 48 | 49 | conc_params = [] 50 | conc_params.append({"name": "Cu", 51 | "diffusion coefficient": D_Cu_hat, 52 | "z": 2., 53 | "bulk": 1., 54 | "C_ND": 1., 55 | }) 56 | 57 | conc_params.append({"name": "SO4", 58 | "diffusion coefficient": D_SO4_hat, 59 | "z": -2., 60 | "bulk": 1., 61 | "C_ND": C_SO4 / C_Cu, 62 | }) 63 | 64 | conc_params.append({"name": "H", 65 | "diffusion coefficient": D_H_hat, 66 | "z": 1., 67 | "bulk": 1., 68 | "C_ND": C_H / C_Cu, 69 | }) 70 | 71 | physical_params = { 72 | "flow": [ 73 | "advection", 74 | "diffusion", 75 | "migration", 76 | "electroneutrality"], 77 | "F": 1.0, 78 | "R": 1.0, 79 | "T": 1.0, 80 | "U_app": U_app_hat, 81 | "v_avg": 1.0} 82 | 83 | def reaction(u, V): 84 | C = u[0] 85 | U = u[2] 86 | C_b = conc_params[0]["bulk"] 87 | J0 = J0_hat 88 | F = physical_params["F"] 89 | R = physical_params["R"] 90 | T = physical_params["T"] 91 | eta = V - U 92 | return J0 * (exp(F / R / T * (eta)) 93 | - (C / C_b) * exp(-F / R / T * (eta))) 94 | 95 | def reaction_cathode(u): 96 | return reaction(u, Constant(0)) 97 | 98 | def reaction_anode(u): 99 | return reaction(u, Constant(physical_params["U_app"])) 100 | # will need to make U_app a class member if it needs updating later 101 | 102 | echem_params = [] 103 | 104 | echem_params.append({"reaction": reaction_cathode, 105 | "electrons": 2, 106 | "stoichiometry": {"Cu": 1}, 107 | "boundary": "cathode", 108 | }) 109 | 110 | echem_params.append({"reaction": reaction_anode, 111 | "electrons": 2, 112 | "stoichiometry": {"Cu": 1}, 113 | "boundary": "anode", 114 | }) 115 | 116 | mesh = Mesh('bortels_structuredquad_nondim.msh') 117 | super().__init__(conc_params, physical_params, mesh, echem_params=echem_params) 118 | 119 | def set_boundary_markers(self): 120 | # Inlet: 10, Outlet 11, Anode: 12, Cathode: 13, Wall: 14 121 | self.boundary_markers = {"inlet": (10,), 122 | "outlet": (11,), 123 | "anode": (12,), 124 | "cathode": (13,), 125 | } 126 | 127 | def set_velocity(self): 128 | _, y = SpatialCoordinate(self.mesh) 129 | h = 1.0 130 | self.vel = as_vector( 131 | (6. * self.physical_params["v_avg"] / h**2 * y * (h - y), Constant(0.))) # m/s 132 | 133 | 134 | solver = BortelsSolver() 135 | solver.setup_solver() 136 | solver.solve() 137 | -------------------------------------------------------------------------------- /examples/bicarb.py: -------------------------------------------------------------------------------- 1 | from firedrake import * 2 | from echemfem import EchemSolver 3 | 4 | 5 | class CarbonateSolver(EchemSolver): 6 | def __init__(self): 7 | """ 8 | Flow past an electrode with bicarbonate bulk reactions and linear CO2 electrolysis 9 | Example reproduced from: 10 | Lin, T.Y., Baker, S.E., Duoss, E.B. and Beck, V.A., 2021. Analysis of 11 | the Reactive CO2 Surface Flux in Electrocatalytic Aqueous Flow 12 | Reactors. Industrial & Engineering Chemistry Research, 60(31), 13 | pp.11824-11833. 14 | """ 15 | 16 | Ly = 1e-3 # .6e-3 # m 17 | Lx = 1e-2 # m 18 | 19 | mesh = RectangleMesh(200, 100, Lx, Ly, quadrilateral=True) 20 | 21 | k1f = (8.42E3)/1000 22 | k1r = 1.97E-4 23 | k2f = 3.71E-2 24 | k2r = (8.697E4)/1000 25 | k3f = 1.254E6 26 | k3r = (6E9)/1000 27 | k4f = (1.242E12)/1000 28 | k4r = 59.44 29 | k5f = (2.3E10)/1000 30 | k5r = (2.3E-4)*1000 31 | 32 | C_1_inf = 0.034*1000 33 | C_K = 1.*1000 34 | Keq = k1f*k3f/(k1r*k3r) 35 | 36 | C_3_inf = -C_1_inf*Keq/4 + C_1_inf*Keq/4*sqrt(1+8*C_K/(C_1_inf*Keq)) 37 | C_4_inf = (C_K - C_3_inf)/2 38 | C_2_inf = C_3_inf/C_1_inf*k1r/k1f 39 | C_5_inf = (k5r/k5f)/C_2_inf 40 | 41 | def bulk_reaction(y): 42 | yCO2 = y[0] 43 | yOH = y[1] 44 | yHCO3 = y[2] 45 | yCO3 = y[3] 46 | yH = y[4] 47 | 48 | dCO2 = -(k1f) * yCO2*yOH \ 49 | + (k1r) * yHCO3 \ 50 | - (k2f) * yCO2 \ 51 | + (k2r) * yHCO3*yH 52 | 53 | dOH = -(k1f) * yCO2*yOH \ 54 | + (k1r) * yHCO3 \ 55 | + (k3f) * yCO3 \ 56 | - (k3r) * yOH*yHCO3\ 57 | - (k5f) * yOH*yH\ 58 | + (k5r) 59 | 60 | dHCO3 = (k1f) * yCO2*yOH\ 61 | - (k1r) * yHCO3\ 62 | + (k3f) * yCO3\ 63 | - (k3r) * yOH*yHCO3\ 64 | + (k2f) * yCO2 \ 65 | - (k2r) * yHCO3*yH \ 66 | + (k4f) * yCO3*yH\ 67 | - (k4r) * yHCO3 68 | 69 | dCO3 = -(k3f) * yCO3 \ 70 | + (k3r) * yOH*yHCO3\ 71 | - (k4f)*yCO3*yH\ 72 | + (k4r) * yHCO3 73 | 74 | dH = (k2f) * yCO2 \ 75 | - (k2r) * yHCO3*yH \ 76 | - (k4f) * yCO3*yH\ 77 | + (k4r) * yHCO3\ 78 | - (k5f) * yOH*yH\ 79 | + (k5r) 80 | 81 | return [dCO2, dOH, dHCO3, dCO3, dH] # , 0.] 82 | 83 | C_CO2_bulk = C_1_inf 84 | C_OH_bulk = C_2_inf 85 | C_HCO3_bulk = C_3_inf 86 | C_CO32_bulk = C_4_inf 87 | C_H_bulk = C_5_inf 88 | C_K_bulk = C_K 89 | 90 | conc_params = [] 91 | 92 | conc_params.append({"name": "CO2", 93 | "diffusion coefficient": 1.91E-9, # m^2/s 94 | "bulk": C_CO2_bulk, # mol/m3 95 | }) 96 | 97 | conc_params.append({"name": "OH", 98 | "diffusion coefficient": 5.29E-9, # m^2/s 99 | "bulk": C_OH_bulk, # mol/m3 100 | }) 101 | 102 | conc_params.append({"name": "HCO3", 103 | "diffusion coefficient": 1.185E-9, # m^2/s 104 | "bulk": C_HCO3_bulk, # mol/m3 105 | }) 106 | 107 | conc_params.append({"name": "CO3", 108 | "diffusion coefficient": .92E-9, # m^2/s 109 | "bulk": C_CO32_bulk, # mol/m3 110 | }) 111 | 112 | conc_params.append({"name": "H", 113 | "diffusion coefficient": 9.311E-9, # m^2/s 114 | "bulk": C_H_bulk, # mol/m3 115 | }) 116 | 117 | # conc_params.append({"name": "K", 118 | # "diffusion coefficient": 1.96E-9, # m^2/s 119 | # "bulk": C_K_bulk, # mol/m3 120 | # }) 121 | 122 | physical_params = {"flow": ["advection", "diffusion"], 123 | "bulk reaction": bulk_reaction, 124 | } 125 | 126 | super().__init__(conc_params, physical_params, mesh) 127 | 128 | def neumann(self, C, conc_params, u): 129 | name = conc_params["name"] 130 | if name in ["HCO3", "CO3", "H"]: 131 | return Constant(0) 132 | 133 | if name == "CO2": 134 | return -(1.91E-1)*u[0] 135 | if name == "OH": 136 | return 2*(1.91E-1)*u[0] 137 | 138 | def set_boundary_markers(self): 139 | self.boundary_markers = {"inlet": (1), 140 | "bulk dirichlet": (4), 141 | "outlet": (2,), 142 | "neumann": (3,), 143 | } 144 | 145 | def set_velocity(self): 146 | _, y = SpatialCoordinate(self.mesh) 147 | self.vel = as_vector([1.91*y, Constant(0)]) # m/s 148 | 149 | 150 | solver = CarbonateSolver() 151 | solver.setup_solver() 152 | solver.solve() 153 | 154 | n = FacetNormal(solver.mesh) 155 | cCO2, _, _, _, _ = solver.u.subfunctions 156 | flux = assemble(dot(grad(cCO2), n) * ds(3)) 157 | print("flux = %f" % flux) 158 | -------------------------------------------------------------------------------- /examples/mesh_bicarb_tandem_example.geo: -------------------------------------------------------------------------------- 1 | La = 0.00025; 2 | L = 0.001; 3 | Lb = 0.00025; 4 | h = 6e-4; 5 | lc = L/7.; 6 | delta = 1e-8; //boundary layer 1 7 | delta2 = 1e-6;// refined region around points 8 | delta2b = 2e-4; //start of electrode 9 | delta3 = 0.4*h; //boundary layer 2 10 | delta3b = 0.01*h; //transition between layers 11 | lc2 = lc; 12 | 13 | Point(1) = {0, 0, 0, lc}; 14 | Point(2) = {La-delta2, 0, 0, lc2}; 15 | Point(3) = {La, 0, 0, lc2}; 16 | Point(4) = {La+0.1*delta2b, 0, 0, lc2}; 17 | Point(5) = {La+delta2b, 0, 0, lc2}; 18 | Point(6) = {La+L-delta2, 0, 0, lc2}; 19 | Point(7) = {La+L, 0, 0, lc2}; 20 | Point(8) = {La+L+delta2, 0, 0, lc2}; 21 | Point(9) = {La+L+Lb, 0, 0, lc}; 22 | 23 | Point(10) = {0, delta, 0, lc2}; 24 | Point(11) = {La-delta2, delta, 0, lc2}; 25 | Point(12) = {La, delta, 0, lc2}; 26 | Point(13) = {La+0.1*delta2b, delta, 0, lc2}; 27 | Point(14) = {La+delta2b, delta, 0, lc2}; 28 | Point(15) = {La+L-delta2, delta, 0, lc2}; 29 | Point(16) = {La+L, delta, 0, lc2}; 30 | Point(17) = {La+L+delta2, delta, 0, lc2}; 31 | Point(18) = {La+L+Lb, delta, 0, lc2}; 32 | 33 | Point(19) = {0, h, 0, lc}; 34 | Point(20) = {La+L+Lb, h, 0, lc}; 35 | 36 | Point(21) = {La + delta2b, delta3b, 0, lc2}; 37 | Point(22) = {La + L - delta2, delta3b, 0, lc2}; 38 | 39 | Point(23) = {La + delta2b, delta3, 0, lc2}; 40 | Point(24) = {La + L - delta2, delta3, 0, lc2}; 41 | 42 | //horizontal lines 43 | Line(1) = {1,2}; 44 | Line(2) = {2,3}; 45 | Line(3) = {3,4}; 46 | Line(4) = {4,5}; 47 | Line(5) = {5,6}; 48 | Line(6) = {6,7}; 49 | Line(7) = {7,8}; 50 | Line(8) = {8,9}; 51 | 52 | Line(9) = {10,11}; 53 | Line(10) = {11,12}; 54 | Line(11) = {12,13}; 55 | Line(12) = {13,14}; 56 | Line(13) = {14,15}; 57 | Line(14) = {15,16}; 58 | Line(15) = {16,17}; 59 | Line(16) = {17,18}; 60 | 61 | Line(17) = {19,20}; 62 | 63 | //vertical lines 64 | Line(18) = {1,10}; 65 | Line(19) = {2,11}; 66 | Line(20) = {3,12}; 67 | Line(21) = {4,13}; 68 | Line(22) = {5,14}; 69 | Line(23) = {6,15}; 70 | Line(24) = {7,16}; 71 | Line(25) = {8,17}; 72 | Line(26) = {9,18}; 73 | 74 | Line(27) = {10,19}; 75 | Line(28) = {18,20}; 76 | 77 | // extra lines 78 | Line(29) = {21,22}; 79 | Line(30) = {14,21}; 80 | Line(31) = {15,22}; 81 | 82 | Line(32) = {23,24}; 83 | Line(33) = {21,23}; 84 | Line(34) = {22,24}; 85 | 86 | Curve Loop(26) = {1,19,-9,-18}; 87 | Curve Loop(27) = {2,20,-10,-19}; 88 | Curve Loop(28) = {3,21,-11,-20}; 89 | Curve Loop(29) = {4,22,-12,-21}; 90 | Curve Loop(30) = {5,23,-13,-22}; 91 | Curve Loop(31) = {6,24,-14,-23}; 92 | Curve Loop(32) = {7,25,-15,-24}; 93 | Curve Loop(33) = {8,26,-16,-25}; 94 | 95 | Curve Loop(34) = {9,10,11,12,30,33,32,-34,-31,14,15,16,28,-17,-27}; 96 | Curve Loop(35) = {13,31,-29,-30}; 97 | Curve Loop(36) = {29,34,-32,-33}; 98 | 99 | Plane Surface(1) = {26}; 100 | Plane Surface(2) = {27}; 101 | Plane Surface(3) = {28}; 102 | Plane Surface(4) = {29}; 103 | Plane Surface(5) = {30}; 104 | Plane Surface(6) = {31}; 105 | Plane Surface(7) = {32}; 106 | Plane Surface(8) = {33}; 107 | Plane Surface(11) = {34}; 108 | Plane Surface(9) = {35}; 109 | Plane Surface(10) = {36}; 110 | 111 | Physical Curve("Inlet", 1) = {18,27}; 112 | Physical Curve("Outlet", 2) = {26,28}; 113 | Physical Curve("Bulk", 3) = {17}; 114 | Physical Curve("Cathode", 4) = {3,4,5,6}; 115 | Physical Curve("Wall", 5) = {1,2,7,8}; 116 | Physical Surface("Channel", 1) = {1,2,3,4,5,6,7,8,9,10,11}; 117 | 118 | 119 | //y-dir 120 | Transfinite Curve{18,19,20,21,22,23,24,25,26} = 26 Using Progression 1.15; 121 | 122 | 123 | //x-dir electrode 124 | Transfinite Curve{2,10} = 3 Using Progression 1/1.4; 125 | Transfinite Curve{3,11} = 5 Using Progression 1.35; 126 | Transfinite Curve{4,12} = 3 Using Progression 1.0; 127 | Transfinite Curve{6,14} = 3 Using Progression 1/1.4; 128 | Transfinite Curve{7,15} = 3 Using Progression 1.4; 129 | Transfinite Curve{5,13,29,32} = 59 Using Bump 0.4; 130 | 131 | //x-dir inlet transition 132 | Transfinite Curve{1,9} = 51 Using Progression .9; 133 | 134 | //x-dir outlet transition 135 | Transfinite Curve{8,16} = 71 Using Progression 1./.95; 136 | 137 | //y-dir diffusion bdlayer 138 | Transfinite Curve{30,31} = 7 Using Progression 1.4; 139 | Transfinite Curve{33,34} = 21 Using Progression 1; 140 | 141 | //boundary layer 142 | Transfinite Surface{1} = {1,2,11,10}; 143 | Transfinite Surface{2} = {2,3,12,11}; 144 | Transfinite Surface{3} = {3,4,13,12}; 145 | Transfinite Surface{4} = {4,5,14,13}; 146 | Transfinite Surface{5} = {5,6,15,14}; 147 | Transfinite Surface{6} = {6,7,16,15}; 148 | Transfinite Surface{7} = {7,8,17,16}; 149 | Transfinite Surface{8} = {8,9,18,17}; 150 | Transfinite Surface{9} = {14,15,22,21}; 151 | Transfinite Surface{10} = {21,22,24,23}; 152 | 153 | //coarse quads 154 | 155 | // Electrode start and end 156 | Field[1] = Distance; 157 | // Field[1].PointsList = {10,11,12,13,14,15}; 158 | Field[1].PointsList = {12,16}; 159 | 160 | Field[2] = Threshold; 161 | Field[2].InField = 1; 162 | Field[2].SizeMin = 8e-11; 163 | Field[2].SizeMax = lc; 164 | Field[2].DistMin = 2e-10; 165 | Field[2].DistMax = 0.8*h; 166 | 167 | Field[3] = Distance; 168 | Field[3].CurvesList = {13,14,15,16}; 169 | Field[3].NumPointsPerCurve = 100; 170 | 171 | Field[4] = Threshold; 172 | Field[4].InField = 3; 173 | Field[4].SizeMin = 0.01/100.; 174 | Field[4].SizeMax = lc; 175 | Field[4].DistMin = 0.5*h; 176 | Field[4].DistMax = h; 177 | //Field[4].Sigmoid = 1; 178 | 179 | Field[5] = Distance; 180 | Field[5].CurvesList = {16}; 181 | Field[5].NumPointsPerCurve = 100; 182 | 183 | Field[6] = Threshold; 184 | Field[6].InField = 5; 185 | Field[6].SizeMin = 0.35*lc; 186 | Field[6].SizeMax = lc; 187 | Field[6].DistMin = delta3; 188 | Field[6].DistMax = 0.5*h; 189 | 190 | Field[7] = Distance; 191 | Field[7].CurvesList = {10}; 192 | Field[7].NumPointsPerCurve = 100; 193 | 194 | Field[8] = Threshold; 195 | Field[8].InField = 7; 196 | Field[8].SizeMin = 0.8*delta2b/22; 197 | Field[8].SizeMax = lc; 198 | Field[8].DistMin = delta3*0.1; 199 | Field[8].DistMax = 0.5*h; 200 | 201 | Field[9] = Min; 202 | Field[9].FieldsList = {2,4,6, 8}; 203 | Background Field = 9; 204 | 205 | Mesh.Algorithm = 8; 206 | Mesh.RecombinationAlgorithm = 2; 207 | Recombine Surface{1,2,3,4,5,6,7,8,9,10,11}; 208 | -------------------------------------------------------------------------------- /examples/gupta_advection_migration.py: -------------------------------------------------------------------------------- 1 | from firedrake import * 2 | from echemfem import EchemSolver, RectangleBoundaryLayerMesh 3 | 4 | 5 | class GuptaSolver(EchemSolver): 6 | """ 7 | Flow past a flat plate electrode for CO2 reduction using electroneutral 8 | Nernst-Planck. The homogenous bulk reactions and the constant-rate 9 | charge-transfer kinetics are taken from: 10 | 11 | Gupta, N., Gattrell, M. and MacDougall, B., 2006. Calculation for the 12 | cathode surface concentrations in the electrochemical reduction of CO2 in 13 | KHCO3 solutions. Journal of applied electrochemistry, 36(2), pp.161-172. 14 | """ 15 | 16 | def __init__(self): 17 | 18 | Ly = 1e-3 19 | Lx = 5e-3 20 | 21 | mesh = RectangleBoundaryLayerMesh(100, 50, Lx, Ly, 50, 1e-6, boundary=(3,)) 22 | x, y = SpatialCoordinate(mesh) 23 | active = conditional(And(x >= 1e-3, x < Lx-1e-3), 1., 0.) 24 | 25 | conc_params = [] 26 | 27 | conc_params.append({"name": "CO2", 28 | "diffusion coefficient": 19.1e-10, # m^2/s 29 | "bulk": 34.2, # mol/m3 30 | "z": 0, 31 | }) 32 | 33 | conc_params.append({"name": "HCO3", 34 | "diffusion coefficient": 9.23e-10, # m^2/s 35 | "bulk": 499., # mol/m3 36 | "z": -1, 37 | }) 38 | 39 | conc_params.append({"name": "CO3", 40 | "diffusion coefficient": 11.9e-10, # m^2/s 41 | "bulk": 7.6e-1, # mol/m3 42 | "z": -2, 43 | }) 44 | 45 | conc_params.append({"name": "OH", 46 | "diffusion coefficient": 52.7e-10, # m^2/s 47 | "bulk": 3.3e-4, # mol/m3 48 | "z": -1, 49 | }) 50 | 51 | conc_params.append({"name": "K", 52 | "diffusion coefficient": 19.6E-10, # m^2/s 53 | "bulk": 499. + 7.6e-1 + 3.3e-4, # mol/m3 54 | "z": 1, 55 | }) 56 | 57 | homog_params = [] 58 | 59 | homog_params.append({"stoichiometry": {"CO2": -1, 60 | "OH": -1, 61 | "HCO3": 1, 62 | }, 63 | "forward rate constant": 5.93, 64 | "backward rate constant": 1.34e-4 65 | }) 66 | 67 | homog_params.append({"stoichiometry": {"HCO3": -1, 68 | "OH": -1, 69 | "CO3": 1, 70 | }, 71 | "forward rate constant": 1e5, 72 | "backward rate constant": 2.15e4 73 | }) 74 | 75 | physical_params = {"flow": ["diffusion", "electroneutrality", "migration", "advection"], 76 | "F": 96485.3329, # C/mol 77 | "R": 8.3144598, # J/K/mol 78 | "T": 273.15 + 25., # K 79 | } 80 | 81 | def current(cef): 82 | j = 50. 83 | def curr(u): 84 | return cef * j * active 85 | return curr 86 | 87 | echem_params = [] 88 | 89 | echem_params.append({"reaction": current(0.1), # HCOO 90 | "stoichiometry": {"CO2": -1, 91 | "OH": 1 92 | }, 93 | "electrons": 2, 94 | "boundary": "electrode", 95 | }) 96 | 97 | echem_params.append({"reaction": current(0.05), # CO 98 | "stoichiometry": {"CO2": -1, 99 | "OH": 2 100 | }, 101 | "electrons": 2, 102 | "boundary": "electrode", 103 | }) 104 | 105 | echem_params.append({"reaction": current(0.25), # CH4 106 | "stoichiometry": {"CO2": -1, 107 | "OH": 8 108 | }, 109 | "electrons": 8, 110 | "boundary": "electrode", 111 | }) 112 | 113 | echem_params.append({"reaction": current(0.2), # C2H4 114 | "stoichiometry": {"CO2": -2, 115 | "OH": 12 116 | }, 117 | "electrons": 12, 118 | "boundary": "electrode", 119 | }) 120 | 121 | echem_params.append({"reaction": current(0.4), # H2 122 | "stoichiometry": {"OH": 2 123 | }, 124 | "electrons": 2, 125 | "boundary": "electrode", 126 | }) 127 | 128 | super().__init__(conc_params, physical_params, mesh, family="CG", echem_params=echem_params, homog_params=homog_params) 129 | 130 | def set_boundary_markers(self): 131 | self.boundary_markers = {"inlet": (1,), 132 | "outlet": (2,), 133 | "bulk": (4,), 134 | "electrode": (3,), 135 | } 136 | def set_velocity(self): 137 | _, y = SpatialCoordinate(self.mesh) 138 | self.vel = as_vector([1.91*y, Constant(0)]) # m/s 139 | 140 | solver = GuptaSolver() 141 | solver.setup_solver() 142 | solver.solve() 143 | -------------------------------------------------------------------------------- /docs/source/user_guide/homog_params.rst: -------------------------------------------------------------------------------- 1 | Homogeneous Reaction Parameters 2 | =============================== 3 | 4 | The parameters of homogeneous/bulk reactions can be provided through 5 | :attr:`echemfem.EchemSolver.homog_params`, a list containing one dictionary for 6 | each reaction. Below is a list of different keys that should appear in each 7 | dictionary 8 | 9 | * :Key: ``"stoichiometry"`` 10 | :Type: :py:class:`dict` of :py:class:`int` 11 | :Description: This entry defines the stoichiometry of the reaction. Each key in this dictionary should be the name of a species (as defined in the ``"name"`` key values of ``conc_params``). The corresponding value is the stoichemetry coefficient for the species in this reaction. It should be an integer: negative for a reactant, positive for a product. 12 | * :Key: ``"forward rate constant"`` 13 | :Type: :py:class:`float` or Firedrake expression 14 | :Description: Rate constant for the forward reaction. 15 | * :Key: ``"backward rate constant"`` or ``"equilibrium constant"`` (optional) 16 | :Type: :py:class:`float` or Firedrake expression 17 | :Description: Rate constant for the backward reaction or equilibrium constant of the reaction. For a reversible reaction, only of one these is required. For an irreversible reaction, leave unset. 18 | * :Key: ``"reference concentration"`` (optional) 19 | :Type: :py:class:`float` or Firedrake expression 20 | :Description: Value of 1M in the appropriate units. By setting this, the rate constants are assumed to have the same units (typically 1/s), and the equilibrium constant to be nondimensional. 21 | * :Key: ``"reaction order"`` (optional) 22 | :Type: :py:class:`dict` of :py:class:`int` 23 | :Description: This entry can be used to enforce reaction order. Each key in this dictionary should be the name of a species (as defined in the ``"name"`` key values of ``conc_params``). The corresponding value (positive integer) is reaction order of the species in this reaction. If unset, the absolute value of the stoichiometry coeffcient is the reaction order. 24 | 25 | Assuming an arbitrary homogeneous reaction system where the forward and 26 | backward rate constants for reaction :math:`i` are :math:`k_i^f` and 27 | :math:`k_i^b`, respectively, and the stoichiometry coefficient of species 28 | :math:`j` in reaction :math:`i` is :math:`s_{j,i}`, then, according to the law 29 | of mass action, the reaction for species :math:`j` is given by 30 | 31 | .. math:: 32 | 33 | R_j = \sum_i s_{j, i} \left( k_i^f \prod_{n, s_{n, i} < 0} c_n^{-s_{n, i}} - k_i^b \prod_{n, s_{n, i} > 0} c_n^{s_{n, i}} \right), 34 | 35 | where :math:`c_n` is the concentration of species :math:`n`. If the equilibrium 36 | constant :math:`K_i` is provided instead of the backward rate constant, then it 37 | is automatically recovered via :math:`k_i^b = \dfrac{k_i^f}{K_i}`. 38 | The above formula assumes single-step reactions so that the reaction orders are 39 | given by the stoichiometry coefficients. If that is not the case, the exponents 40 | can be replaced by the ``"reaction order"`` inputs. 41 | 42 | Note that here the rate constants can have different units, depending on the 43 | number of reactants or products. It is also common to write the reactions in 44 | terms of dimensionless "activities" instead of concentrations. Then, the rate 45 | constants have the same units (typically 1/s) and the equilibrium constants are 46 | dimensionless. With this definition of rate constants, we instead write the 47 | reaction for species :math:`j` as 48 | 49 | 50 | .. math:: 51 | 52 | R_j = \sum_i s_{j, i} c_\mathrm{ref} \left( k_i^f \prod_{n, s_{n, i} < 0} a_n^{-s_{n, i}} - k_i^b \prod_{n, s_{n, i} > 0} a_n^{s_{n, i}} \right), 53 | 54 | where :math:`a_n = c_n / c_\mathrm{ref}` is the activity of species :math:`n` 55 | and :math:`c_\mathrm{ref} = 1\text{M}` is a reference concentration, which 56 | needs to be provided in the correct units to ``homog_params``. 57 | 58 | Here is an example of a reaction system that can be implement via 59 | ``homog_params``. Simplified CO2 - bicarbonate homogeneous reactions: 60 | 61 | .. math:: 62 | 63 | \mathrm{CO}_2 + \mathrm{OH}^- \xrightleftharpoons[k_1^b]{k_1^f} \mathrm{HCO}_3^- 64 | 65 | \mathrm{HCO}_3^- + \mathrm{OH}^- \xrightleftharpoons[k_2^b]{k_2^f} \mathrm{CO}_3^{2-} + \mathrm{H}_2\mathrm{O} 66 | 67 | Here, we assume the dilute solution case where :math:`\mathrm{H}_2\mathrm{O}` 68 | concentration is not tracked. In ``conc_params``, we will have entries for the 69 | other species in this reactions, with ``"name"`` key values: ``"CO2"``, 70 | ``"OH"``, ``"HCO3"``, and ``"CO3"``. We get the following ``homog_params`` 71 | list: 72 | 73 | .. code:: 74 | 75 | [{"stoichiometry": {"CO": -1, 76 | "OH": -1, 77 | "HCO3": 1}, 78 | "forward rate constant": k1f, 79 | "backward rate constant": k1b 80 | }, 81 | {"stoichiometry": {"HCO3": -1, 82 | "OH": -1, 83 | "CO3": 1}, 84 | "forward rate constant": k2f, 85 | "backward rate constant": k2b 86 | }] 87 | 88 | Now here is another bicarbonate approximation with a high pH approximation, 89 | where the reaction system is simplified into a single irreversible reaction: 90 | 91 | .. math:: 92 | 93 | \mathrm{CO}_2 + 2\mathrm{OH}^- \xrightarrow[]{k_1^f} \mathrm{CO}_3^{2-} + \mathrm{H}_2\mathrm{O} 94 | 95 | Since this is not actually a single-step reaction, we need to specify the 96 | reaction order for :math:`\mathrm{OH}^-`. We can use the following 97 | ``homog_params`` list: 98 | 99 | .. code:: 100 | 101 | [{"stoichiometry": {"CO2": -1, 102 | "OH": -2, 103 | "CO3": 1}, 104 | "reaction order": {"OH": 1}, 105 | "forward rate constant": k1f 106 | }] 107 | 108 | 109 | As an alternative to the ``homog_params`` interface, the reactions can be 110 | written directly and passed as a function in ``physical_params["bulk 111 | reaction"]`` (:doc:`physical_params`). See ``examples/cylindrical_pore.py`` for 112 | the direct implementation of the high pH approximation of the bicarbonate bulk 113 | reactions. See ``examples/carbonate.py`` and 114 | ``examples/carbonate_homog_params.py`` for two equivalent implementations of 115 | the full bicarbonate bulk reactions. 116 | -------------------------------------------------------------------------------- /docs/source/user_guide/fluid_solver.rst: -------------------------------------------------------------------------------- 1 | Fluid Flow Solvers 2 | =================== 3 | 4 | For convenience, we provide a simple implementation of fluid flow equations in 5 | :class:`echemfem.FlowSolver`, which can be used to obtain a velocity field for 6 | the :class:`echemfem.EchemSolver`. 7 | 8 | Navier-Stokes Solver 9 | -------------------- 10 | 11 | The :class:`echemfem.NavierStokesFlowSolver` class contains an implementation 12 | for the steady-state incompressible Navier-Stokes equations. Both 13 | nondimensional and dimensional equations are available. 14 | 15 | We are solving for velocity :math:`\mathbf u` and pressure :math:`p`. The 16 | dimensional version of the momentum equation is 17 | 18 | .. math:: 19 | 20 | -\nu \nabla^2 \mathbf u + \mathbf u \cdot \nabla \mathbf u + \frac{1}{\rho} \nabla p = 0, 21 | 22 | where :math:`\nu` is the kinematic viscosity and :math:`\rho` is the density. 23 | It is also common to use the dynamic viscosity :math:`\mu = \nu \rho` 24 | 25 | The nondimensional version is 26 | 27 | .. math:: 28 | 29 | - \frac{1}{\mathrm{Re}}\nabla^2 \mathbf u + \mathbf u \cdot \nabla \mathbf u + \nabla p = 0, 30 | 31 | where all quantities have been nondimensionalized, and :math:`\mathrm{Re}= 32 | \frac{\bar {\mathbf u} L}{\nu}` is the Reynolds number, where :math:`L` is the 33 | characteristic length. 34 | 35 | In both cases, we also have the incompressibility condition 36 | 37 | .. math:: 38 | 39 | \nabla \cdot \mathbf{u} = 0. 40 | 41 | Physical parameters are passed as a 42 | :py:class:`dict` in the ``fluid_params`` argument. 43 | Specifically, for the dimensional version, the user must pass the following keys: 44 | 45 | * ``"density"``: :math:`\rho` 46 | 47 | * ``"dynamic viscosity"`` or ``"kinematic viscosity"``: :math:`\mu` and :math:`\nu`, respectively 48 | 49 | 50 | To use the nondimensional version, the user must pass the following key: 51 | 52 | * ``"Reynolds number"``: :math:`\mathrm{Re}` 53 | 54 | Navier-Stokes-Brinkman Solver 55 | ----------------------------- 56 | 57 | The :class:`echemfem.NavierStokesBrinkmanFlowSolver` class contains an 58 | implementation for the steady-state incompressible Navier-Stokes-Brinkman 59 | equations. Both nondimensional and dimensional equations are available. 60 | 61 | The dimensional version of the momentum equation is 62 | 63 | .. math:: 64 | 65 | -\nabla\cdot\left(\nu_\mathrm{eff} \nabla\mathbf u \right)+ \mathbf u \cdot \nabla \mathbf u + \frac{1}{\rho} \nabla p + \nu K^{-1} \mathbf u = 0, 66 | 67 | where :math:`\nu_\mathrm{eff}` is the effective viscosity in the porous medium, 68 | and :math:`K`, its permeability. It is also common to use the effective dynamic 69 | viscosity :math:`\mu_\mathrm{eff} = \nu_\mathrm{eff} \rho`. 70 | 71 | The inverse permeability :math:`K^{-1}` can be provided directly in cases where 72 | it is zero in some regions, i.e. liquid-only regions. It is common to take 73 | :math:`\nu_\mathrm{eff}=\nu` for simplicity (the default here). 74 | 75 | The nondimensional implementation currently assumes :math:`\nu_\mathrm{eff}=\nu` 76 | and :math:`K^{-1}>0`, such that 77 | 78 | 79 | .. math:: 80 | 81 | - \frac{1}{\mathrm{Re}}\nabla^2 \mathbf u + \mathbf u \cdot \nabla \mathbf u + \nabla p + \frac{1}{\mathrm{Re}\mathrm{Da}} \mathbf u = 0, 82 | 83 | where all quantities have been nondimensionalized and 84 | :math:`\mathrm{Da}=\frac{K}{L^2}` is the Darcy number. 85 | 86 | In addition to the parameters provided for Navier-Stokes, the physical 87 | parameters below must be provided. 88 | For the dimensional version, the user must also pass the following keys: 89 | 90 | * ``"permeability"`` or ``"inverse permeability"``: :math:`K` and :math:`K^{-1}`, respectively 91 | 92 | * Optional: ``"effective kinematic viscosity"`` or ``"effective dynamic viscosity"``: :math:`\nu_\mathrm{eff}` and :math:`\mu_\mathrm{eff}`, respectively 93 | 94 | To use the nondimensional version, the user must pass the following key: 95 | 96 | * ``"Darcy number"``: :math:`\mathrm{Da}` 97 | 98 | Boundary conditions 99 | ------------------- 100 | 101 | A :py:class:`dict` containing boundary markers is passed when creating a 102 | ``FlowSolver`` object. Below are the different options for ``boundary_markers`` 103 | keys. 104 | 105 | * ``"no slip"``: Sets velocity to zero, :math:`\mathbf u = 0`. 106 | 107 | * ``"inlet velocity"``: Sets inlet velocity, :math:`\mathbf u = \mathbf 108 | u_\mathrm{in}`, which is passed in ``fluid_params`` as ``"inlet velocity"``. 109 | 110 | * ``"outlet velocity"``: Sets outlet velocity, :math:`\mathbf u = \mathbf 111 | u_\mathrm{out}`, which is passed in ``fluid_params`` as ``"outlet 112 | velocity"``. 113 | 114 | * ``"inlet pressure"``: Sets inlet velocity, :math:`p = p_\mathrm{in}`, which 115 | is passed in ``fluid_params`` as ``"inlet pressure"``. 116 | 117 | * ``"outlet pressure"``: Sets outlet pressure, :math:`p = p_\mathrm{out}`, 118 | which is passed in ``fluid_params`` as ``"outlet pressure"``. 119 | 120 | Numerical considerations 121 | ------------------------ 122 | 123 | The discretization is done using Taylor-Hood elements :cite:`Taylor1973` 124 | (piecewise linear pressure, piecewise quadratic velocity), which satisfy the 125 | inf-sup condition :cite:`Ladyzhenskaya1963,Brezzi1974,Babuvska1971`. 126 | 127 | To achieve convergence other than at a low Reynolds number, it may be required 128 | to do continuation on the parameter so that Newton's method has better initial 129 | guesses. This can be done for example, by passing the Reynolds number as a 130 | :class:`firedrake.constant.Constant`, and assigning a larger value after each 131 | solve. 132 | 133 | The highest Reynolds number that can be provided is dictated by the validity of 134 | the steady-sate assumption. Indeed, as the Reynolds number increases, steady 135 | flow becomes unstable. The critical Reynolds number at which transient effects 136 | occur is problem dependent, typically somewhere within 137 | :math:`10^2<\mathrm{Re}<10^4`. 138 | 139 | For the Darcy number, using a very small value (:math:`\mathrm{Da}\sim 140 | 10^{-6}`) can be used to simulate a nearly impermeable wall, which is commonly 141 | done in topology optimization. For smaller values, convergence issues can 142 | arise. Reversly, as :math:`\mathrm{Da}\to\infty`, we simply recover 143 | Navier-Stokes. 144 | 145 | .. rubric:: References 146 | 147 | .. bibliography:: ../_static/references.bib 148 | :filter: docname in docnames 149 | -------------------------------------------------------------------------------- /examples/carbonate.py: -------------------------------------------------------------------------------- 1 | from firedrake import * 2 | from echemfem import EchemSolver, IntervalBoundaryLayerMesh 3 | 4 | """ 5 | A 1D example of diffusion-reaction for CO2 electrolysis with bicarbonate bulk 6 | reactions. The bulk reactions and charge-transfer reactions are implemented by 7 | hand. 8 | 9 | Steady-state version of example from 10 | Gupta, N., Gattrell, M. and MacDougall, B., 2006. Calculation for the 11 | cathode surface concentrations in the electrochemical reduction of CO2 in 12 | KHCO3 solutions. Journal of applied electrochemistry, 36(2), pp.161-172. 13 | 14 | Using the bicarbonate bulk reactions from 15 | Schulz, K.G., Riebesell, U., Rost, B., Thoms, S. and Zeebe, R.E., 2006. 16 | Determination of the rate constants for the carbon dioxide to bicarbonate 17 | inter-conversion in pH-buffered seawater systems. Marine chemistry, 18 | 100(1-2), pp.53-65. 19 | """ 20 | 21 | class CarbonateSolver(EchemSolver): 22 | def __init__(self): 23 | 24 | delta = 0.0001 25 | mesh = IntervalBoundaryLayerMesh(100, delta, 100, 1e-7) 26 | 27 | # Reaction Rates in SI units 28 | k1f = 2.23 # m3/mol.s 29 | k1r = 9.71e-5 # 1/s 30 | k2f = 3.71e-2 # 1/s 31 | k2r = 2.67e1 # m3/mol.s 32 | k3f = 3.06e5 # 1/s 33 | k3r = 6.0e6 # m3/mol.s 34 | k4f = 5.0e7 # m3/mol.s 35 | k4r = 59.44 # 1/s 36 | k5f = 2.31e7 # m3/mols. (Note - typo in Schulz) 37 | k5r = 1.4 # mol/m3.s 38 | # Bulk Conditions 39 | C_HCO3_bulk = 500. # mol/m3 40 | C_CO32_bulk = 6.5 # mol/m3 41 | C_OH_bulk = k3f / k3r * C_CO32_bulk / C_HCO3_bulk # mol/m3 42 | C_H_bulk = (k5r / k5f) / C_OH_bulk # mol/m3 43 | C_CO2_bulk = (k1r / k1f) * C_HCO3_bulk / C_OH_bulk # mol/m3 44 | 45 | def bulk_reaction(C): 46 | s = [-k1f * C[0] * C[1] + k1r * C[4] - k2f * C[0] + k2r * C[4] * C[2], 47 | -k1f * C[0] * C[1] + k1r * C[4] + k3f * C[3] - k3r * C[4] * C[1] - k5f * C[1] * C[2] + k5r, 48 | + k2f * C[0] - k2r * C[4] * C[2] - k4f * C[3] * C[2] + k4r * C[4] - k5f * C[1] * C[2] + k5r, 49 | - k3f * C[3] + k3r * C[4] * C[1] - k4f * C[3] * C[2] + k4r * C[4], 50 | k1f * C[0] * C[1] - k1r * C[4] + k2f * C[0] - k2r * C[4] * C[2] + k3f * C[3] - k3r * C[4] * C[1] + k4f * C[3] * C[2] - k4r * C[4]] 51 | return s 52 | 53 | conc_params = [] 54 | 55 | conc_params.append({"name": "CO2", 56 | "diffusion coefficient": 19.1e-10, # m^2/s 57 | "bulk": C_CO2_bulk, # mol/m3 58 | }) 59 | 60 | conc_params.append({"name": "OH", 61 | "diffusion coefficient": 54.0e-10, # m^2/s 62 | "bulk": C_OH_bulk, # mol/m3 63 | }) 64 | 65 | conc_params.append({"name": "H", 66 | "diffusion coefficient": 96.0e-10, # m^2/s 67 | "bulk": C_H_bulk, # mol/m3 68 | }) 69 | 70 | conc_params.append({"name": "CO3", 71 | "diffusion coefficient": 7.0e-10, # m^2/s 72 | "bulk": C_CO32_bulk, # mol/m3 73 | }) 74 | 75 | conc_params.append({"name": "HCO3", 76 | "diffusion coefficient": 9.4e-10, # m^2/s 77 | "bulk": C_HCO3_bulk, # mol/m3 78 | }) 79 | 80 | physical_params = {"flow": ["diffusion"], 81 | "F": 96485., # C/mol 82 | "bulk reaction": bulk_reaction, 83 | } 84 | 85 | super().__init__(conc_params, physical_params, mesh, family="CG") 86 | 87 | def neumann(self, C, conc_params, u): 88 | name = conc_params["name"] 89 | if name in ["HCO3", "CO3", "H"]: 90 | return Constant(0) 91 | j = 50. # A/m^2 92 | F = self.physical_params["F"] 93 | zeffCH4 = 8. 94 | zeffC2H4 = 12. 95 | zeffCO = 2. 96 | zeffHCOO = 2. 97 | zeffH2 = 2. 98 | cefCH4 = 0.25 99 | cefC2H4 = 0.20 100 | cefCO = 0.05 101 | cefHCOO = 0.10 102 | cefH2 = 0.40 103 | if name == "CO2": 104 | print(-(j / F) * (cefHCOO / zeffHCOO + cefCO / zeffCO 105 | + cefCH4 / zeffCH4 + 2 * cefC2H4 / zeffC2H4)) 106 | return -(j / F) * (cefHCOO / zeffHCOO + cefCO / zeffCO 107 | + cefCH4 / zeffCH4 + 2 * cefC2H4 / zeffC2H4) 108 | if name == "OH": 109 | return (j / F) * (cefHCOO / zeffHCOO + 2 * cefCO / zeffCO 110 | + 8 * cefCH4 / zeffCH4 + 12 * cefC2H4 / zeffC2H4 111 | + 2 * cefH2 / zeffH2) # note error in Gupta et al. 112 | 113 | def set_boundary_markers(self): 114 | self.boundary_markers = {"bulk dirichlet": (1,), # U_solid = U_app, C = C_0 115 | "neumann": (2,), 116 | } 117 | 118 | 119 | solver = CarbonateSolver() 120 | solver.setup_solver() 121 | solver.solve() 122 | 123 | ### Plotting 124 | 125 | C_CO2, C_OH, C_H, C_CO3, C_HCO3 = solver.u.subfunctions 126 | # OH boundary layer 127 | x = solver.mesh.coordinates 128 | C_OH_bl = Function(solver.V).assign(C_OH).dat.data[100:] 129 | x_bl = x.dat.data[100:] 130 | 131 | import matplotlib.pyplot as plt 132 | import matplotlib.gridspec as gridspec 133 | from matplotlib.ticker import FormatStrFormatter 134 | filename = "carbonate.png" 135 | fig = plt.figure(constrained_layout=True, figsize=(16, 8)) 136 | spec = gridspec.GridSpec(ncols=3, nrows=2, figure=fig) 137 | ax1 = fig.add_subplot(spec[0, 0]) 138 | ax2 = fig.add_subplot(spec[0, 1]) 139 | ax3 = fig.add_subplot(spec[1, 0]) 140 | ax4 = fig.add_subplot(spec[1, 1]) 141 | ax5 = fig.add_subplot(spec[0, 2]) 142 | ax6 = fig.add_subplot(spec[1, 2]) 143 | 144 | plot(C_CO2, axes=ax1) 145 | ax1.set(xlabel='distance (m)', 146 | ylabel='CO$_2$ concentration (M)') 147 | plot(C_HCO3, axes=ax2) 148 | ax2.set(xlabel='distance (m)', 149 | ylabel='HCO$_3$ concentration (M)') 150 | plot(C_CO3, axes=ax3) 151 | ax3.set(xlabel='distance (m)', 152 | ylabel='CO$_3$ concentration (M)') 153 | plot(C_OH, axes=ax4) 154 | ax4.set(xlabel='distance (m)', 155 | ylabel='OH concentration (M)') 156 | plot(C_H, axes=ax5) 157 | ax5.set(xlabel='distance (m)', 158 | ylabel='H concentration (M)') 159 | plt.plot(x_bl, C_OH_bl, axes=ax6, color='k', linewidth=2) 160 | ax6.set(xlabel='distance (m)', 161 | ylabel='OH concentration (M)') 162 | 163 | plt.savefig(filename) 164 | -------------------------------------------------------------------------------- /docs/source/user_guide/physical_params.rst: -------------------------------------------------------------------------------- 1 | Physical Parameters 2 | =================== 3 | 4 | The general physical parameters need to be provided in 5 | :attr:`echemfem.EchemSolver.physical_params`, a dictionary. 6 | 7 | The only key required in all cases is the following one: 8 | 9 | :Key: ``"flow"`` 10 | :Type: :py:class:`list` of :py:class:`str` 11 | :Description: Each :py:class:`str` represents a transport mechanism. Below is a 12 | list of tested options and their parameter requirements, and, if 13 | relevant, the added contribution to the flux of species :math:`k`, 14 | :math:`\mathbf N_k`. 15 | 16 | * ``"diffusion"``: Fickian diffusion 17 | 18 | * ``conc_params``: ``"diffusion coefficient"`` defines :math:`D_k` 19 | 20 | .. math:: 21 | 22 | \mathbf N_k \mathrel{+}= -D_k \nabla c_k 23 | 24 | * ``"advection"``: Advection 25 | 26 | * ``conc_params``: ``"diffusion coefficient"`` 27 | * Velocity :math:`\mathbf u` defined by overwriting the method :meth:`echemfem.EchemSolver.set_velocity` 28 | 29 | .. math:: 30 | 31 | \mathbf N_k \mathrel{+}= c_k \mathbf u 32 | 33 | * ``"migration"``: Electromigration (where the mobility constant is given by the Nernst-Einstein relation) 34 | 35 | * ``conc_params``: ``"diffusion coefficient"``, ``"z"`` 36 | * ``physical_params``: ``"F"``, ``"R"``, ``"T"`` 37 | 38 | .. math:: 39 | 40 | \mathbf N_k \mathrel{+}= -z_k F\frac{D_k}{RT} c_k \nabla \Phi_2. 41 | 42 | * ``"electroneutrality"``: Electroneutrality approximation. Implemented through 43 | a charge-conservation equation as described in `Roy et al, 2022 44 | `_. This eliminates a 45 | concentration from the solution variables. By default, the last concentration 46 | in ``conc_params`` is eliminated. It can also be specified by passing 47 | ``"eliminated": True`` in a species' ``conc_params`` entry. 48 | 49 | .. math:: 50 | 51 | \sum_k z_k c_k = 0. 52 | 53 | * ``"electroneutrality full"``: Electroneutrality approximation, but implemented explicitly. 54 | 55 | * ``"porous"``: For flow in a porous media. Diffusion coefficients are replaced 56 | with the effective diffusion coefficients using the Bruggeman correlation. A 57 | Poisson equation is solved for the electronic potential :math:`\Phi_1`. If 58 | using :doc:`echem_params`, the reaction is volumetric and the provided 59 | reaction is multiplied by the specific surface area, :math:`a_v`. 60 | 61 | * ``physical_params``: 62 | 63 | * ``"solid conductivity"``: defines the electronic conductivity in the bulk of the solid, denoted :math:`\sigma` 64 | * ``"saturation"``: defines :math:`S`, the liquid saturation. By default :math:`S=1`. 65 | * ``"porosity"``: defines :math:`\epsilon` 66 | * ``"specific surface area"``: defines :math:`a_v`. 67 | 68 | .. math:: 69 | 70 | D_k^\mathrm{eff} = (\epsilon S)^{1.5} D_k, 71 | 72 | -\nabla \cdot \left((1-\epsilon)^{1.5} \sigma \nabla \Phi_1 \right) = -a_v \sum_j i_j. 73 | 74 | * ``"poisson"``: Poisson equation for the ionic potential. 75 | 76 | * ``physical_params``: 77 | 78 | * ``"vacuum permittivity"``: :math:`\epsilon_0` 79 | * ``"relative permittivity"``: :math:`\epsilon_r` 80 | 81 | .. math:: 82 | 83 | -\nabla \cdot \left( \epsilon_0 \epsilon_r \nabla \Phi_2 \right) = F\sum_k z_k c_k, 84 | 85 | neglacting reaction terms. If the vacuum and relative permittivity are not 86 | defined, the ionic conductivity is used instead :math:`\kappa = \sum_k z_k^2 87 | F^2 \frac{D_k}{RT} c_k`. 88 | 89 | * ``"finite size"``: Finite-size effect for ion interaction. Choosing 90 | ``"poisson"``, ``"diffusion"``, ``"migration"``, and ``"finite size"`` gives 91 | the Generalized Poisson-Nernst-Planck model (GMPNP). 92 | 93 | * ``conc_params``: ``"solvated diameter"``, :math:`a_k` of species :math:`k` 94 | * ``physical_params``: ``"Avogadro constant"``, :math:`N_A` 95 | 96 | .. math:: 97 | 98 | \mathbf N_k \mathrel{+}= -D_k c_k \left(\frac{N_A \sum_j a_j^3 \nabla 99 | c_j}{1- N_A \sum_j a_j^3 c_j}\right) 100 | 101 | Below is a list of other keys that can appear in the dictionary 102 | 103 | 104 | * :Key: ``"bulk reaction"`` 105 | :Type: a function 106 | :Description: Homogeneous/bulk reactions to be added to the right-hand side of mass conservation equations. These can instead be set using ``homog_params`` (:doc:`homog_params`). 107 | 108 | Args: 109 | u: solution state. The value of the different concentrations can be recovered through ``u([self.i_c["species name"])`` within a ``echemfem.EchemSolver`` object. 110 | 111 | Returns: 112 | List of length equal to the number of species, each entry being the reaction term of the corresponding species. The order of the reaction terms must be the same as the order of the species in ``conc_params``, which can also be found through ``self.i_c``. 113 | 114 | * :Key: ``"F"`` 115 | :Type: :py:class:`float`, firedrake expression 116 | :Description: Faraday Constant 117 | * :Key: ``"R"`` 118 | :Type: :py:class:`float`, firedrake expression 119 | :Description: Molar Gas Constant 120 | * :Key: ``"T"`` 121 | :Type: :py:class:`float`, firedrake expression 122 | :Description: Absolute Temperature 123 | * :Key: ``"porosity"`` 124 | :Type: :py:class:`float`, firedrake expression 125 | :Description: Void fraction of the porous medium. Used when ``"porous"`` is in ``physical_params["flow"]``. 126 | * :Key: ``"solid conductivity"`` 127 | :Type: :py:class:`float`, firedrake expression 128 | :Description: Electronic conductivity of the bulk solid material. Used when ``"porous"`` is in ``physical_params["flow"]``. 129 | * :Key: ``"specific surface area"`` 130 | :Type: :py:class:`float`, firedrake expression 131 | :Description: Specific surface area of the porous medium. Used when ``"porous"`` is in ``physical_params["flow"]``, if :doc:`echem_params` are used. 132 | * :Key: ``"saturation"`` 133 | :Type: :py:class:`float`, firedrake expression 134 | :Description: Ratio of the void fraction occupied by liquid. Default value of ``1.0``. Used when ``"porous"`` is in ``physical_params["flow"]``. 135 | * :Key: ``"vacuum permittivity"`` 136 | :Type: :py:class:`float`, firedrake expression 137 | :Description: Used when ``"poisson"`` is in ``physical_params["flow"]``. 138 | * :Key: ``"relative permittivity"`` 139 | :Type: :py:class:`float`, firedrake expression 140 | :Description: Used when ``"poisson"`` is in ``physical_params["flow"]``. 141 | * :Key: ``"Avogadro constant"`` 142 | :Type: :py:class:`float`, firedrake expression 143 | :Description: Used when ``"finite size"`` is in ``physical_params["flow"]``. 144 | * :Key: ``"U_app"`` 145 | :Type: :py:class:`float`, firedrake expression 146 | :Description: Applied potential. Used for ``"applied"``, ``"liquid applied"``, and ``"robin"`` :doc:`boundary_conditions`. 147 | * :Key: ``"gap capacitance"`` 148 | :Type: :py:class:`float`, firedrake expression 149 | :Description: Used for ``"robin"`` :doc:`boundary_conditions`. 150 | * :Key: ``"surface charge density"`` 151 | :Type: :py:class:`float`, firedrake expression 152 | :Description: Used for ``"poisson neumann"`` :doc:`boundary_conditions`. 153 | 154 | -------------------------------------------------------------------------------- /examples/carbonate_homog_params.py: -------------------------------------------------------------------------------- 1 | from firedrake import * 2 | from echemfem import EchemSolver, IntervalBoundaryLayerMesh 3 | 4 | """ 5 | A 1D example of diffusion-reaction for CO2 electrolysis with bicarbonate bulk 6 | reactions. The charge-transfer reactions are implemented by hand but the bulk 7 | reactions are implemented using the homog_params interface. 8 | 9 | Steady-state version of example from 10 | Gupta, N., Gattrell, M. and MacDougall, B., 2006. Calculation for the 11 | cathode surface concentrations in the electrochemical reduction of CO2 in 12 | KHCO3 solutions. Journal of applied electrochemistry, 36(2), pp.161-172. 13 | 14 | Using the bicarbonate bulk reactions from 15 | Schulz, K.G., Riebesell, U., Rost, B., Thoms, S. and Zeebe, R.E., 2006. 16 | Determination of the rate constants for the carbon dioxide to bicarbonate 17 | inter-conversion in pH-buffered seawater systems. Marine chemistry, 18 | 100(1-2), pp.53-65. 19 | """ 20 | 21 | class CarbonateSolver(EchemSolver): 22 | def __init__(self): 23 | 24 | delta = 0.0001 25 | mesh = IntervalBoundaryLayerMesh(100, delta, 100, 1e-7) 26 | 27 | # Reaction Rates in SI units 28 | k1f = 2.23 # m3/mol.s 29 | k1r = 9.71e-5 # 1/s 30 | k2f = 3.71e-2 # 1/s 31 | k2r = 2.67e1 # m3/mol.s 32 | k3f = 3.06e5 # 1/s 33 | k3r = 6.0e6 # m3/mol.s 34 | k4f = 5.0e7 # m3/mol.s 35 | k4r = 59.44 # 1/s 36 | k5f = 2.31e7 # m3/mols. (Note - typo in Schulz) 37 | k5r = 1.4 # mol/m3.s 38 | # Bulk Conditions 39 | C_HCO3_bulk = 500. # mol/m3 40 | C_CO32_bulk = 6.5 # mol/m3 41 | C_OH_bulk = k3f / k3r * C_CO32_bulk / C_HCO3_bulk # mol/m3 42 | C_H_bulk = (k5r / k5f) / C_OH_bulk # mol/m3 43 | C_CO2_bulk = (k1r / k1f) * C_HCO3_bulk / C_OH_bulk # mol/m3 44 | 45 | homog_params = [] 46 | 47 | homog_params.append({"stoichiometry": {"CO2": -1, 48 | "OH": -1, 49 | "HCO3": 1}, 50 | "forward rate constant": k1f, 51 | "backward rate constant": k1r 52 | }) 53 | 54 | homog_params.append({"stoichiometry": {"CO2": -1, 55 | "H": 1, 56 | "HCO3": 1}, 57 | "forward rate constant": k2f, 58 | "backward rate constant": k2r 59 | }) 60 | 61 | homog_params.append({"stoichiometry": {"CO3": -1, 62 | "OH": 1, 63 | "HCO3": 1}, 64 | "forward rate constant": k3f, 65 | "backward rate constant": k3r 66 | }) 67 | 68 | homog_params.append({"stoichiometry": {"CO3": -1, 69 | "H": -1, 70 | "HCO3": 1}, 71 | "forward rate constant": k4f, 72 | "backward rate constant": k4r 73 | }) 74 | 75 | homog_params.append({"stoichiometry": {"OH": -1, 76 | "H": -1}, 77 | "forward rate constant": k5f, 78 | "backward rate constant": k5r 79 | }) 80 | 81 | conc_params = [] 82 | 83 | conc_params.append({"name": "CO2", 84 | "diffusion coefficient": 19.1e-10, # m^2/s 85 | "bulk": C_CO2_bulk, # mol/m3 86 | }) 87 | 88 | conc_params.append({"name": "OH", 89 | "diffusion coefficient": 54.0e-10, # m^2/s 90 | "bulk": C_OH_bulk, # mol/m3 91 | }) 92 | 93 | conc_params.append({"name": "H", 94 | "diffusion coefficient": 96.0e-10, # m^2/s 95 | "bulk": C_H_bulk, # mol/m3 96 | }) 97 | 98 | conc_params.append({"name": "CO3", 99 | "diffusion coefficient": 7.0e-10, # m^2/s 100 | "bulk": C_CO32_bulk, # mol/m3 101 | }) 102 | 103 | conc_params.append({"name": "HCO3", 104 | "diffusion coefficient": 9.4e-10, # m^2/s 105 | "bulk": C_HCO3_bulk, # mol/m3 106 | }) 107 | 108 | physical_params = {"flow": ["diffusion"], 109 | "F": 96485., # C/mol 110 | } 111 | 112 | super().__init__(conc_params, physical_params, mesh, family="CG", homog_params=homog_params) 113 | 114 | def neumann(self, C, conc_params, u): 115 | name = conc_params["name"] 116 | if name in ["HCO3", "CO3", "H"]: 117 | return Constant(0) 118 | j = 50. # A/m^2 119 | F = self.physical_params["F"] 120 | zeffCH4 = 8. 121 | zeffC2H4 = 12. 122 | zeffCO = 2. 123 | zeffHCOO = 2. 124 | zeffH2 = 2. 125 | cefCH4 = 0.25 126 | cefC2H4 = 0.20 127 | cefCO = 0.05 128 | cefHCOO = 0.10 129 | cefH2 = 0.40 130 | if name == "CO2": 131 | print(-(j / F) * (cefHCOO / zeffHCOO + cefCO / zeffCO 132 | + cefCH4 / zeffCH4 + 2 * cefC2H4 / zeffC2H4)) 133 | return -(j / F) * (cefHCOO / zeffHCOO + cefCO / zeffCO 134 | + cefCH4 / zeffCH4 + 2 * cefC2H4 / zeffC2H4) 135 | if name == "OH": 136 | return (j / F) * (cefHCOO / zeffHCOO + 2 * cefCO / zeffCO 137 | + 8 * cefCH4 / zeffCH4 + 12 * cefC2H4 / zeffC2H4 138 | + 2 * cefH2 / zeffH2) # note error in Gupta et al. 139 | 140 | def set_boundary_markers(self): 141 | self.boundary_markers = {"bulk dirichlet": (1,), # U_solid = U_app, C = C_0 142 | "neumann": (2,), 143 | } 144 | 145 | 146 | solver = CarbonateSolver() 147 | solver.setup_solver() 148 | solver.solve() 149 | 150 | ### Plotting 151 | 152 | C_CO2, C_OH, C_H, C_CO3, C_HCO3 = solver.u.subfunctions 153 | # OH boundary layer 154 | x = solver.mesh.coordinates 155 | C_OH_bl = Function(solver.V).assign(C_OH).dat.data[100:] 156 | x_bl = x.dat.data[100:] 157 | 158 | import matplotlib.pyplot as plt 159 | import matplotlib.gridspec as gridspec 160 | from matplotlib.ticker import FormatStrFormatter 161 | filename = "carbonate.png" 162 | fig = plt.figure(constrained_layout=True, figsize=(16, 8)) 163 | spec = gridspec.GridSpec(ncols=3, nrows=2, figure=fig) 164 | ax1 = fig.add_subplot(spec[0, 0]) 165 | ax2 = fig.add_subplot(spec[0, 1]) 166 | ax3 = fig.add_subplot(spec[1, 0]) 167 | ax4 = fig.add_subplot(spec[1, 1]) 168 | ax5 = fig.add_subplot(spec[0, 2]) 169 | ax6 = fig.add_subplot(spec[1, 2]) 170 | 171 | plot(C_CO2, axes=ax1) 172 | ax1.set(xlabel='distance (m)', 173 | ylabel='CO$_2$ concentration (M)') 174 | plot(C_HCO3, axes=ax2) 175 | ax2.set(xlabel='distance (m)', 176 | ylabel='HCO$_3$ concentration (M)') 177 | plot(C_CO3, axes=ax3) 178 | ax3.set(xlabel='distance (m)', 179 | ylabel='CO$_3$ concentration (M)') 180 | plot(C_OH, axes=ax4) 181 | ax4.set(xlabel='distance (m)', 182 | ylabel='OH concentration (M)') 183 | plot(C_H, axes=ax5) 184 | ax5.set(xlabel='distance (m)', 185 | ylabel='H concentration (M)') 186 | plt.plot(x_bl, C_OH_bl, axes=ax6, color='k', linewidth=2) 187 | ax6.set(xlabel='distance (m)', 188 | ylabel='OH concentration (M)') 189 | 190 | plt.savefig(filename) 191 | -------------------------------------------------------------------------------- /examples/cylindrical_pore.py: -------------------------------------------------------------------------------- 1 | from firedrake import * 2 | from echemfem import EchemSolver 3 | import numpy as np 4 | """ 5 | A symmetric cylindrical pore model for CO2 electrolysis using electroneutral 6 | Nernst-Planck and simplified bicarbonate bulk reactions. 7 | 8 | Elucidating Mass Transport Regimes in Gas Diffusion Electrodes for CO2 Electroreduction 9 | Thomas Moore, Xiaoxing Xia, Sarah E. Baker, Eric B. Duoss, and Victor A. Beck 10 | ACS Energy Letters 2021 6 (10), 3600-3606 11 | """ 12 | # operating conditions 13 | T = 293.15 # temperature (K) 14 | Vcell = Constant(-0.0) # potential (V) 15 | 16 | # physical constants 17 | R = 8.3144598 # ideal gas constant (J/mol/K) 18 | F = 96485.33289 # Faraday's constant (C/mol) 19 | 20 | # CO2RR 21 | i0CO2RR = 4.71e-4 * 10 # exchange current density (A/m2) 22 | alphacCO2RR = 0.44 # cathode coefficient 23 | U0CO2RR = -0.11 # standard potential (V) 24 | 25 | # HER 26 | i0HER = 1.16e-6 * 10 # exchange current density (A/m2) 27 | alphacHER = 0.36 # cathode coefficient 28 | U0HER = 0.0 # standard potential (V) 29 | 30 | # bulk reaction 31 | k2 = 2.19e3 * 1e-3 # m^3/mol/s 32 | 33 | # electrolyte properties 34 | cref = 1e3 # reference concentration (mol/m3) 35 | 36 | # diffusion coefficients (m2/s) 37 | # [CO2 OH- CO32 K+ CO H2] 38 | D = [1.910e-9, 5.29e-9, 0.92e-9, 1.96e-9, 2.03e-9, 4.5e-9] 39 | 40 | # charges 41 | z = [0, -1, -2, 1, 0, 0] 42 | 43 | # Bulk Electrolyte concentrations 44 | cKb = 1000 # mol/m3 45 | c0OHb = 1000 46 | c0CO3b = 0. 47 | c0CO2b = 0. 48 | c0COb = 0. 49 | c0H2b = 0. 50 | cb = np.array([c0CO2b, c0OHb, c0CO3b, cKb, c0COb, c0H2b]) 51 | H_CO2 = 0.015e3 52 | H_H2 = 0. 53 | H_CO = 0. 54 | 55 | 56 | class PorousSolver(EchemSolver): 57 | def __init__(self): 58 | 59 | def bulk_reaction(y): 60 | i_CO2 = self.i_c["CO2"] 61 | i_OH = self.i_c["OH"] 62 | i_CO3 = self.i_c["CO3"] 63 | yCO2 = y[i_CO2] 64 | yOH = y[i_OH] 65 | rCO2 = - k2 * yCO2 * yOH 66 | rOH = 2 * rCO2 67 | rCO3 = - rCO2 68 | rxns = [0.] * self.num_c 69 | rxns[i_CO2] = rCO2 70 | rxns[i_OH] = rOH 71 | rxns[i_CO3] = rCO3 72 | return rxns 73 | 74 | mesh = RectangleMesh(100, 500, 2e-6, 10e-6, quadrilateral=True) 75 | _, Z = SpatialCoordinate(mesh) 76 | active = conditional(le(Z, 5e-6), 1., 0.) 77 | conc_params = [] 78 | 79 | conc_params.append({"name": "CO2", 80 | "diffusion coefficient": D[0], 81 | "z": z[0], 82 | "bulk": cb[0], 83 | "gas": H_CO2, 84 | }) 85 | 86 | conc_params.append({"name": "OH", 87 | "diffusion coefficient": D[1], 88 | "z": z[1], 89 | "bulk": cb[1], 90 | }) 91 | 92 | conc_params.append({"name": "CO3", 93 | "diffusion coefficient": D[2], 94 | "z": z[2], 95 | "bulk": cb[2], 96 | }) 97 | 98 | conc_params.append({"name": "K", 99 | "diffusion coefficient": D[3], 100 | "z": z[3], 101 | "bulk": cb[3], 102 | "eliminated": True, 103 | }) 104 | 105 | conc_params.append({"name": "CO", 106 | "diffusion coefficient": D[4], 107 | "z": z[4], 108 | "bulk": cb[4], 109 | "gas": H_CO, 110 | }) 111 | 112 | conc_params.append({"name": "H2", 113 | "diffusion coefficient": D[5], 114 | "z": z[5], 115 | "bulk": cb[5], 116 | "gas": H_H2, 117 | }) 118 | 119 | physical_params = {"flow": ["diffusion", "migration", "electroneutrality"], 120 | "F": F, # C/mol 121 | "R": R, # J/K/mol 122 | "T": T, # K 123 | "U_app": Vcell, # V 124 | "bulk reaction": bulk_reaction, 125 | } 126 | 127 | def reaction_CO2RR(u): 128 | CCO2 = u[self.i_c["CO2"]] 129 | Phi2 = u[self.i_Ul] 130 | Phi1 = physical_params["U_app"] 131 | UCO2RR = U0CO2RR 132 | etaCO2RR = Phi1 - Phi2 - UCO2RR # reaction overpotential (V) 133 | iCO2RR = i0CO2RR * CCO2 / cref * exp(-((alphacCO2RR * F) / (R * T)) * etaCO2RR) 134 | return active * iCO2RR 135 | 136 | def reaction_HER(u): 137 | Phi2 = u[self.i_Ul] 138 | Phi1 = physical_params["U_app"] 139 | UHER = U0HER 140 | etaHER = Phi1 - Phi2 - UHER # reaction overpotential (V) 141 | iHER = i0HER * exp(-((alphacHER * F) / (R * T)) * etaHER) 142 | return active * iHER 143 | echem_params = [] 144 | 145 | echem_params.append({"reaction": reaction_CO2RR, 146 | "electrons": 2, 147 | "stoichiometry": {"CO2": -1, # reactant 148 | "OH": 2, 149 | "CO": 1}, # product 150 | "boundary": "catalyst", 151 | }) 152 | 153 | echem_params.append({"reaction": reaction_HER, 154 | "electrons": 2, 155 | "stoichiometry": {"OH": 2, 156 | "H2": 1}, # product 157 | "boundary": "catalyst", 158 | }) 159 | 160 | super().__init__(conc_params, physical_params, mesh, echem_params=echem_params, family="CG", cylindrical=True) 161 | 162 | def set_boundary_markers(self): 163 | self.boundary_markers = {"gas": (3,), # C = C_gas 164 | "bulk": (4,), # V = 0 165 | "bulk dirichlet": (4,), # C = C_bulk 166 | "catalyst": (2,), # CO2R 167 | } 168 | 169 | 170 | solver = PorousSolver() 171 | solver.setup_solver(initial_solve=False) 172 | solver.save_solutions = False 173 | solver.solve() 174 | 175 | ## Plotting 176 | 177 | # getting active region 178 | _, Z = SpatialCoordinate(solver.mesh) 179 | active = conditional(le(Z, 5e-6), 1., 0.) 180 | 181 | def get_boundary_dofs(V, i): 182 | u = Function(V) 183 | bc = DirichletBC(V, active, i) 184 | bc.apply(u) 185 | return np.where(u.vector()[:] == 1) 186 | 187 | dofs = get_boundary_dofs(solver.V, 2) 188 | Z_cat = Function(solver.V).interpolate(Z).dat.data[dofs] 189 | 190 | import matplotlib.pyplot as plt 191 | Vlist = np.linspace(-0.71, -1.31, num=13) 192 | for Vs in Vlist: 193 | solver.U_app.assign(Vs) 194 | print("V = %d mV" % np.rint(Vs * 1000)) 195 | solver.solve() 196 | # [CO2 OH- CO32- CO H2] 197 | cCO2, cOH, cCO3, cCO, cH2, phi2 = solver.u.subfunctions 198 | cK = Function(solver.V).assign(2 * cCO3 + cOH) 199 | 200 | filename = "cylindrical_pore/CO2_%dmV.png" % np.rint(-Vs * 1000) 201 | fig, axes = plt.subplots() 202 | levels = np.linspace(0, H_CO2, 41) 203 | contours = tricontourf(cCO2, levels=levels, axes=axes, cmap="turbo") 204 | fig.colorbar(contours) 205 | plt.axis('scaled') 206 | plt.savefig(filename) 207 | plt.clf() 208 | 209 | filename = "cylindrical_pore/catalyst_CO2_%dmV.png" % np.rint(-Vs * 1000) 210 | fig, axes = plt.subplots() 211 | plt.plot(Z_cat, cCO2.dat.data[dofs]) 212 | plt.savefig(filename) 213 | plt.clf() 214 | -------------------------------------------------------------------------------- /examples/paper_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from firedrake import * 3 | from echemfem import EchemSolver 4 | from mpi4py import MPI 5 | from petsc4py import PETSc 6 | """ 7 | Convergence tests for the DG scheme in 8 | Roy, T., Andrej, J. and Beck, V.A., 2023. A scalable DG solver for the 9 | electroneutral Nernst-Planck equations. Journal of Computational Physics, 475, 10 | p.111859. 11 | """ 12 | 13 | PETSc.Sys.popErrorHandler() 14 | 15 | # set_log_level(DEBUG) 16 | GLOBAL_N_RANKS = MPI.Comm.Get_size(MPI.COMM_WORLD) 17 | if GLOBAL_N_RANKS % 8 == 0: 18 | REDFACTOR = int(GLOBAL_N_RANKS / 8) 19 | else: 20 | REDFACTOR = 1 21 | 22 | 23 | class DiffusionMigrationSolver(EchemSolver): 24 | def __init__(self, N, extruded=False, gmg=False): 25 | if extruded and gmg: 26 | N = int(N / 2) 27 | plane_mesh = UnitSquareMesh(N, N, quadrilateral=True) 28 | plane_mesh_hierarchy = MeshHierarchy(plane_mesh, 2) 29 | extruded_hierarchy = ExtrudedMeshHierarchy( 30 | plane_mesh_hierarchy, 1.0, N) 31 | mesh = extruded_hierarchy[-1] 32 | x, y, _ = SpatialCoordinate(mesh) 33 | elif extruded: 34 | plane_mesh = UnitSquareMesh(N, N, quadrilateral=True) 35 | mesh = ExtrudedMesh(plane_mesh, N, 1.0 / N) 36 | x, y, _ = SpatialCoordinate(mesh) 37 | else: 38 | # Create an initial coarse mesh 39 | initial_mesh = UnitSquareMesh(2, 2, quadrilateral=True) 40 | # Create a mesh hierarchy with uniform refinements 41 | hierarchy = MeshHierarchy(initial_mesh, N) 42 | # Use the finest mesh for the initial discretization 43 | mesh = hierarchy[-1] 44 | x, y = SpatialCoordinate(mesh) 45 | 46 | conc_params = [] 47 | 48 | C1ex = cos(x) + sin(y) + 3 49 | C2ex = cos(x) + sin(y) + 3 50 | Uex = sin(x) + cos(y) + 3 51 | self.C1ex = C1ex 52 | self.C2ex = C2ex 53 | self.Uex = Uex 54 | 55 | D1 = 0.5e-5 56 | D2 = 1.0e-5 57 | z1 = 2.0 58 | z2 = -2.0 59 | 60 | def f(C): 61 | f1 = div((self.vel - D1 * z1 * grad(Uex)) 62 | * C1ex) - div(D1 * grad(C1ex)) 63 | f2 = div((self.vel - D2 * z2 * grad(Uex)) 64 | * C2ex) - div(D2 * grad(C2ex)) 65 | return [f1, f2] 66 | 67 | conc_params.append({"name": "C1", 68 | "diffusion coefficient": D1, 69 | "z": z1, 70 | "bulk": C1ex, 71 | }) 72 | 73 | conc_params.append({"name": "C2", 74 | "diffusion coefficient": D2, 75 | "z": z2, 76 | "bulk": C2ex, 77 | }) 78 | physical_params = { 79 | "flow": [ 80 | "diffusion", 81 | "advection", 82 | "migration", 83 | "electroneutrality"], 84 | "F": 1.0, 85 | "R": 1.0, 86 | "T": 1.0, 87 | "U_app": Uex, 88 | "bulk reaction": f, 89 | "v_avg": 1.0, 90 | } 91 | 92 | super().__init__(conc_params, physical_params, mesh, p=1) 93 | # Select a geometric multigrid preconditioner to make use of the 94 | # MeshHierarchy 95 | U_is = self.num_mass 96 | is_list = [str(i) for i in range(self.num_mass)] 97 | C_is = ",".join(is_list) 98 | 99 | self.init_solver_parameters( 100 | custom_solver={ 101 | # "snes_view": None, 102 | # "snes_converged_reason": None, 103 | # "snes_monitor": None, 104 | "snes_rtol": 1e-10, 105 | # "ksp_monitor": None, 106 | # "ksp_converged_reason": None, 107 | "ksp_type": "fgmres", 108 | "ksp_rtol": 1e-3, 109 | "pc_type": "fieldsplit", 110 | "pc_fieldsplit_0_fields": U_is, 111 | "pc_fieldsplit_1_fields": C_is, 112 | "fieldsplit_0": { 113 | # "ksp_converged_reason": None, 114 | "ksp_rtol": 1e-1, 115 | "ksp_type": "cg", 116 | "pc_type": "hypre", 117 | "pc_hypre_boomeramg": { 118 | "strong_threshold": 0.7, 119 | "coarsen_type": "HMIS", 120 | "agg_nl": 3, 121 | "interp_type": "ext+i", 122 | "agg_num_paths": 5, 123 | # "print_statistics": None, 124 | }, 125 | }, 126 | "fieldsplit_1": { 127 | # "ksp_converged_reason": None, 128 | "ksp_rtol": 1e-1, 129 | "ksp_type": "gmres", 130 | "pc_type": "mg", 131 | "mg_levels_ksp_type": "richardson", 132 | "mg_levels_pc_type": "bjacobi", 133 | "mg_levels_sub_pc_type": "ilu", 134 | "mg_coarse": { 135 | "pc_type": "python", 136 | "pc_python_type": "firedrake.AssembledPC", 137 | "assembled": { 138 | "mat_type": "aij", 139 | "pc_type": "telescope", 140 | "pc_telescope_reduction_factor": REDFACTOR, 141 | "pc_telescope_subcomm_type": "contiguous", 142 | "telescope_pc_type": "lu", 143 | "telescope_pc_factor_mat_solver_type": "mumps", 144 | } 145 | }, 146 | }, 147 | }, 148 | custom_potential_solver={ 149 | "mat_type": "aij", 150 | # "snes_view": None, 151 | # "snes_monitor": None, 152 | "snes_rtol": 1e-6, 153 | # "ksp_monitor": None, 154 | # "ksp_converged_reason": None, 155 | "ksp_type": "cg", 156 | "ksp_rtol": 1e-2, 157 | "pc_type": "hypre", 158 | "pc_hypre_boomeramg": { 159 | "strong_threshold": 0.7, 160 | "coarsen_type": "HMIS", 161 | "agg_nl": 3, 162 | "interp_type": "ext+i", 163 | "agg_num_paths": 5, 164 | }, 165 | }) 166 | 167 | def set_boundary_markers(self): 168 | self.boundary_markers = {"bulk dirichlet": (1, 2, 3, 4,), 169 | "applied": (1, 2, 3, 4,), 170 | } 171 | 172 | def set_velocity(self): 173 | if self.mesh.geometric_dimension() == 3: 174 | _, y, _ = SpatialCoordinate(self.mesh) 175 | else: 176 | _, y = SpatialCoordinate(self.mesh) 177 | 178 | h = 1.0 179 | x_vel = 6. * self.physical_params["v_avg"] / h**2 * y * (h - y) 180 | 181 | if self.mesh.geometric_dimension() == 3: 182 | self.vel = as_vector( 183 | (x_vel, Constant(0.), Constant(0.))) 184 | else: 185 | self.vel = as_vector( 186 | (x_vel, Constant(0.))) 187 | 188 | 189 | def test_convergence(extruded=False, gmg=False): 190 | err_old = 1e6 191 | for i in [2, 4, 8, 16, 32]: 192 | if gmg: 193 | solver = DiffusionMigrationSolver(i, extruded, gmg) 194 | else: 195 | solver = DiffusionMigrationSolver(i, extruded, gmg) 196 | solver.setup_solver() 197 | solver.solve() 198 | c1, U = solver.u.subfunctions 199 | errc1 = errornorm(solver.C1ex, c1) 200 | errU = errornorm(solver.Uex, U) 201 | err = errc1 + errU 202 | # only prints number of cells of the 2D mesh 203 | PETSc.Sys.Print("cells = {} L2err = {:.5E} C1err = {:.5E} Uerr = {:.5E}".format( 204 | solver.comm.allreduce(solver.mesh.cell_set.size, op=MPI.SUM), err, errc1, errU)) 205 | err_old = err 206 | 207 | 208 | test_convergence(extruded=True, gmg=True) 209 | --------------------------------------------------------------------------------