From 9751574ffd1de7b6f83996efcc0666fcd29ae293 Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Mon, 7 Oct 2024 14:47:11 -0400 Subject: [PATCH 1/8] Fix doc failures in notebooks (#4498) * Update a path * Update docs/conf.py * Try a different cast --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 55a4ac3f61..76dcffb18b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -341,7 +341,7 @@ {% set github_docname = 'github/pybamm-team/pybamm/blob/develop/docs/' + -env.doc2path(env.docname, base=None) %} +env.doc2path(env.docname, base=None) | string() %} {% set notebooks_version = env.config.html_context.notebooks_version %} {% set github_download_url = env.config.html_context.github_download_url %} From a7d476ea4f21a64d1cf3a39f8bc687035b39621d Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Mon, 7 Oct 2024 20:24:10 +0100 Subject: [PATCH 2/8] Implement sodium-ion DFN (#4451) * add sodium-ion model, parameters and examples * update citation keys to be consistent * rename electrolyte conductivity from kappa to sigma * add tests for sodium ion * fix doctests * fix doctest * remove accidentally added example * commented better the volume fraction * write volume fractions as a single number, not a substraction * improve basic tests * update CHANGELOG --------- Co-authored-by: Eric G. Kratz --- CHANGELOG.md | 1 + docs/source/examples/index.rst | 1 + .../notebooks/models/sodium-ion.ipynb | 185 +++++++ pyproject.toml | 1 + src/pybamm/CITATIONS.bib | 459 +++++++++--------- src/pybamm/__init__.py | 1 + src/pybamm/input/parameters/__init__.py | 2 +- .../input/parameters/lithium_ion/Ai2020.py | 6 +- .../input/parameters/lithium_ion/Chen2020.py | 4 +- .../input/parameters/lithium_ion/Ecker2015.py | 4 +- .../parameters/lithium_ion/Marquis2019.py | 4 +- .../parameters/lithium_ion/Mohtat2020.py | 4 +- .../parameters/lithium_ion/NCA_Kim2011.py | 4 +- .../parameters/lithium_ion/ORegan2022.py | 2 +- .../parameters/lithium_ion/Ramadass2004.py | 2 +- .../input/parameters/lithium_ion/Xu2019.py | 4 +- .../parameters/sodium_ion/Chayambuka2022.py | 341 +++++++++++++ .../input/parameters/sodium_ion/__init__.py | 1 + .../input/parameters/sodium_ion/data/D_e.csv | 14 + .../input/parameters/sodium_ion/data/D_n.csv | 40 ++ .../input/parameters/sodium_ion/data/D_p.csv | 35 ++ .../input/parameters/sodium_ion/data/U_n.csv | 21 + .../input/parameters/sodium_ion/data/U_p.csv | 28 ++ .../input/parameters/sodium_ion/data/k_n.csv | 32 ++ .../input/parameters/sodium_ion/data/k_p.csv | 24 + .../parameters/sodium_ion/data/sigma_e.csv | 6 + .../models/full_battery_models/__init__.py | 2 +- .../sodium_ion/__init__.py | 6 + .../sodium_ion/basic_dfn.py | 273 +++++++++++ .../submodels/thermal/surface/lumped.py | 2 +- .../transport_efficiency/bruggeman.py | 6 +- .../cation_exchange_membrane.py | 6 +- .../heterogeneous_catalyst.py | 6 +- .../hyperbola_of_revolution.py | 6 +- .../transport_efficiency/ordered_packing.py | 6 +- .../overlapping_spheres.py | 6 +- .../random_overlapping_cylinders.py | 6 +- src/pybamm/parameters/parameter_sets.py | 4 +- src/pybamm/solvers/jax_bdf_solver.py | 20 +- src/pybamm/spatial_methods/spectral_volume.py | 2 +- .../test_lithium_ion/test_basic_models.py | 11 +- .../test_sodium_ion/__init__.py | 0 .../test_sodium_ion/test_basic_models.py | 34 ++ .../test_sodium_ion/__init__.py | 0 .../test_sodium_ion/test_basic_models.py | 14 + .../test_Chayambuka2022.py | 41 ++ 46 files changed, 1402 insertions(+), 275 deletions(-) create mode 100644 docs/source/examples/notebooks/models/sodium-ion.ipynb create mode 100644 src/pybamm/input/parameters/sodium_ion/Chayambuka2022.py create mode 100644 src/pybamm/input/parameters/sodium_ion/__init__.py create mode 100755 src/pybamm/input/parameters/sodium_ion/data/D_e.csv create mode 100755 src/pybamm/input/parameters/sodium_ion/data/D_n.csv create mode 100755 src/pybamm/input/parameters/sodium_ion/data/D_p.csv create mode 100755 src/pybamm/input/parameters/sodium_ion/data/U_n.csv create mode 100755 src/pybamm/input/parameters/sodium_ion/data/U_p.csv create mode 100755 src/pybamm/input/parameters/sodium_ion/data/k_n.csv create mode 100755 src/pybamm/input/parameters/sodium_ion/data/k_p.csv create mode 100644 src/pybamm/input/parameters/sodium_ion/data/sigma_e.csv create mode 100644 src/pybamm/models/full_battery_models/sodium_ion/__init__.py create mode 100644 src/pybamm/models/full_battery_models/sodium_ion/basic_dfn.py create mode 100644 tests/integration/test_models/test_full_battery_models/test_sodium_ion/__init__.py create mode 100644 tests/integration/test_models/test_full_battery_models/test_sodium_ion/test_basic_models.py create mode 100644 tests/unit/test_models/test_full_battery_models/test_sodium_ion/__init__.py create mode 100644 tests/unit/test_models/test_full_battery_models/test_sodium_ion/test_basic_models.py create mode 100644 tests/unit/test_parameters/test_parameter_sets/test_Chayambuka2022.py diff --git a/CHANGELOG.md b/CHANGELOG.md index fd590fbc12..8ffc210a74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Features - Added Hermite interpolation to the (`IDAKLUSolver`) that improves the accuracy and performance of post-processing variables. ([#4464](https://github.com/pybamm-team/PyBaMM/pull/4464)) +- Added `BasicDFN` model for sodium-ion batteries ([#4451](https://github.com/pybamm-team/PyBaMM/pull/4451)) - Added sensitivity calculation support for `pybamm.Simulation` and `pybamm.Experiment` ([#4415](https://github.com/pybamm-team/PyBaMM/pull/4415)) - Added OpenMP parallelization to IDAKLU solver for lists of input parameters ([#4449](https://github.com/pybamm-team/PyBaMM/pull/4449)) - Added phase-dependent particle options to LAM diff --git a/docs/source/examples/index.rst b/docs/source/examples/index.rst index dab8247ddf..3f77578ef5 100644 --- a/docs/source/examples/index.rst +++ b/docs/source/examples/index.rst @@ -68,6 +68,7 @@ The notebooks are organised into subfolders, and can be viewed in the galleries notebooks/models/SEI-on-cracks.ipynb notebooks/models/simulate-3E-cell.ipynb notebooks/models/simulating-ORegan-2022-parameter-set.ipynb + notebooks/models/sodium-ion.ipynb notebooks/models/SPM.ipynb notebooks/models/SPMe.ipynb notebooks/models/submodel_cracking_DFN_or_SPM.ipynb diff --git a/docs/source/examples/notebooks/models/sodium-ion.ipynb b/docs/source/examples/notebooks/models/sodium-ion.ipynb new file mode 100644 index 0000000000..671e7923e9 --- /dev/null +++ b/docs/source/examples/notebooks/models/sodium-ion.ipynb @@ -0,0 +1,185 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# DFN model for sodium-ion batteries\n", + "\n", + "In this notebook we use the DFN model to simulate sodium-ion batteries. The parameters are based on the article\n", + "> K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based modeling of sodium-ion batteries part II. Model and validation, Electrochimica Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764.\n", + "\n", + "However, the specific values (including the data for the interpolants) are taken from the COMSOL implementation presented in [this example](https://www.comsol.com/model/1d-isothermal-sodium-ion-battery-117341). As usual, we start by importing PyBaMM." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu.\n" + ] + } + ], + "source": [ + "import pybamm" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now need to define the model. In this case we take the `BasicDFN` model for sodium-ion batteries (note how it is called from the `pybamm.sodium_ion` submodule). Note that, at the moment, the model is identical to the one for lithium-ion batteries, but uses different parameter values." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "model = pybamm.sodium_ion.BasicDFN()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to replicate the results in the COMSOL example, we discharge at different C-rates and compare the solutions. We loop over the C-rate dictionary and solve the model for each C-rate. We append the solutions into a list so we can later plots the results." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8ca3353c637e48d28c3b02f42d25fa03", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(FloatSlider(value=0.0, description='t', max=10.80914150213347, step=0.1080914150213347),…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "C_rates = [1 / 12, 5 / 12, 10 / 12, 1]\n", + "solutions = []\n", + "\n", + "for C_rate in C_rates:\n", + " sim = pybamm.Simulation(model, solver=pybamm.IDAKLUSolver(), C_rate=C_rate)\n", + " sol = sim.solve([0, 4000 / C_rate])\n", + " solutions.append(sol)\n", + "\n", + "pybamm.dynamic_plot(solutions)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now perform a manual plot of voltage versus capacity, to compare the results with the COMSOL example." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "for solution, C_rate in zip(solutions, C_rates):\n", + " capacity = [i * 1000 for i in solution[\"Discharge capacity [A.h]\"].entries]\n", + " voltage = solution[\"Voltage [V]\"].entries\n", + " plt.plot(capacity, voltage, label=f\"{(12 * C_rate)} A.m-2\")\n", + "\n", + "plt.xlabel(\"Discharge Capacity [mA.h]\")\n", + "plt.ylabel(\"Voltage [V]\");" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1] Joel A. E. Andersson, Joris Gillis, Greg Horn, James B. Rawlings, and Moritz Diehl. CasADi – A software framework for nonlinear optimization and optimal control. Mathematical Programming Computation, 11(1):1–36, 2019. doi:10.1007/s12532-018-0139-4.\n", + "[2] Kudakwashe Chayambuka, Grietus Mulder, Dmitri L Danilov, and Peter HL Notten. Physics-based modeling of sodium-ion batteries part ii. model and validation. Electrochimica Acta, 404:139764, 2022.\n", + "[3] Charles R. Harris, K. Jarrod Millman, Stéfan J. van der Walt, Ralf Gommers, Pauli Virtanen, David Cournapeau, Eric Wieser, Julian Taylor, Sebastian Berg, Nathaniel J. Smith, and others. Array programming with NumPy. Nature, 585(7825):357–362, 2020. doi:10.1038/s41586-020-2649-2.\n", + "[4] Alan C. Hindmarsh. The PVODE and IDA algorithms. Technical Report, Lawrence Livermore National Lab., CA (US), 2000. doi:10.2172/802599.\n", + "[5] Alan C. Hindmarsh, Peter N. Brown, Keith E. Grant, Steven L. Lee, Radu Serban, Dan E. Shumaker, and Carol S. Woodward. SUNDIALS: Suite of nonlinear and differential/algebraic equation solvers. ACM Transactions on Mathematical Software (TOMS), 31(3):363–396, 2005. doi:10.1145/1089014.1089020.\n", + "[6] Scott G. Marquis, Valentin Sulzer, Robert Timms, Colin P. Please, and S. Jon Chapman. An asymptotic derivation of a single particle model with electrolyte. Journal of The Electrochemical Society, 166(15):A3693–A3706, 2019. doi:10.1149/2.0341915jes.\n", + "[7] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.\n", + "\n" + ] + } + ], + "source": [ + "pybamm.print_citations()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pyproject.toml b/pyproject.toml index a70f712e3e..1db7c927a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -152,6 +152,7 @@ Ramadass2004 = "pybamm.input.parameters.lithium_ion.Ramadass2004:get_parameter_v Xu2019 = "pybamm.input.parameters.lithium_ion.Xu2019:get_parameter_values" ECM_Example = "pybamm.input.parameters.ecm.example_set:get_parameter_values" MSMR_Example = "pybamm.input.parameters.lithium_ion.MSMR_example_set:get_parameter_values" +Chayambuka2022 = "pybamm.input.parameters.sodium_ion.Chayambuka2022:get_parameter_values" [tool.setuptools] include-package-data = true diff --git a/src/pybamm/CITATIONS.bib b/src/pybamm/CITATIONS.bib index 3d853738b4..26919f7c66 100644 --- a/src/pybamm/CITATIONS.bib +++ b/src/pybamm/CITATIONS.bib @@ -22,6 +22,17 @@ @article{Ai2022 author = {Weilong Ai and Niall Kirkaldy and Yang Jiang and Gregory Offer and Huizhi Wang and Billy Wu}, } +@article{Akanni1987, + title={Effective transport coefficients in heterogeneous media}, + author={Akanni, KA and Evans, JW and Abramson, IS}, + journal={Chemical Engineering Science}, + volume={42}, + number={8}, + pages={1945--1954}, + year={1987}, + publisher={Elsevier} +} + @article{Andersson2019, author = {Andersson, Joel A. E. and Gillis, Joris and Horn, Greg and Rawlings, James B. and Diehl, Moritz}, @@ -47,6 +58,39 @@ @article{Baker2018 publisher={IOP Publishing} } +@article{Baltensperger2003, + title={Spectral differencing with a twist}, + author={Baltensperger, Richard and Trummer, Manfred R}, + journal={SIAM journal on scientific computing}, + volume={24}, + number={5}, + pages={1465--1487}, + year={2003}, + publisher={SIAM} +} + +@article{Barletta2022thevenin, + title={Th{\'e}venin’s Battery Model Parameter Estimation Based on Simulink}, + author={Barletta, Giulio and DiPrima, Piera and Papurello, Davide}, + journal={Energies}, + volume={15}, + number={17}, + pages={6207}, + year={2022}, + publisher={MDPI} +} + +@article{Beeckman1990, + title={Mathematical description of heterogeneous materials}, + author={Beeckman, JW}, + journal={Chemical engineering science}, + volume={45}, + number={8}, + pages={2603--2610}, + year={1990}, + publisher={Elsevier} +} + @article{BrosaPlanella2021, title = {Systematic derivation and validation of a reduced thermal-electrochemical model for lithium-ion batteries using asymptotic methods}, author = {Brosa Planella, Ferran and Sheikh, Muhammad and Widanage, W. Dhammika}, @@ -70,6 +114,38 @@ @article{BrosaPlanella2022 doi = {}, } +@article{Bruggeman1935, + title={Berechnung verschiedener physikalischer Konstanten von heterogenen Substanzen. I. Dielektrizit{\"a}tskonstanten und Leitf{\"a}higkeiten der Mischk{\"o}rper aus isotropen Substanzen}, + author={Bruggeman, Von DAG}, + journal={Annalen der physik}, + volume={416}, + number={7}, + pages={636--664}, + year={1935}, + publisher={Wiley Online Library} +} + +@article{Byrne1975, + title={A polyalgorithm for the numerical solution of ordinary differential equations}, + author={Byrne, George D. and Hindmarsh, Alan C.}, + journal={ACM Transactions on Mathematical Software (TOMS)}, + volume={1}, + number={1}, + pages={71--96}, + year={1975}, + publisher={ACM New York, NY, USA} +} + +@article{Chayambuka2022, + title={Physics-based modeling of sodium-ion batteries part II. Model and validation}, + author={Chayambuka, Kudakwashe and Mulder, Grietus and Danilov, Dmitri L and Notten, Peter HL}, + journal={Electrochimica Acta}, + volume={404}, + pages={139764}, + year={2022}, + publisher={Elsevier} +} + @article{Chen2020, author = {Chen, Chang-Hui and Brosa Planella, Ferran and O'Regan, Kieran and Gastol, Dominika and Widanage, W. Dhammika and Kendrick, Emma}, title = {{Development of Experimental Techniques for Parameterization of Multi-scale Lithium-ion Battery Models}}, @@ -140,6 +216,16 @@ @article{Ecker2015ii doi = {10.1149/2.0541509jes}, } +@article{Fan2022, + title={Data-driven identification of lithium-ion batteries: A nonlinear equivalent circuit model with diffusion dynamics}, + author={Fan, Chuanxin and O’Regan, Kieran and Li, Liuying and Higgins, Matthew D and Kendrick, Emma and Widanage, Widanalage D}, + journal={Applied Energy}, + volume={321}, + pages={119336}, + year={2022}, + publisher={Elsevier} +} + @article{Gustafsson2020, doi = {10.21105/joss.02369}, year = {2020}, @@ -152,6 +238,13 @@ @article{Gustafsson2020 journal = {Journal of Open Source Software}, } +@book{Hairer1993, + title={Solving ordinary differential equations. 1, Nonstiff problems}, + author={Hairer, Ernst and N{\o}rsett, Syvert P and Wanner, Gerhard}, + year={1993}, + publisher={Springer-Vlg} +} + @article{Hales2019, title={The cell cooling coefficient: a standard to define heat rejection from lithium-ion batteries}, author={Hales, Alastair and Diaz, Laura Bravo and Marzook, Mohamed Waseem and Zhao, Yan and Patel, Yatish and Offer, Gregory}, @@ -252,7 +345,18 @@ @article{Lain2019 doi = {10.3390/batteries5040064}, } -@article{lin2014lumped, +@article{Landesfeind2019, + title={Temperature and concentration dependence of the ionic transport properties of lithium-ion battery electrolytes}, + author={Landesfeind, Johannes and Gasteiger, Hubert A}, + journal={Journal of The Electrochemical Society}, + volume={166}, + number={14}, + pages={A3079--A3097}, + year={2019}, + publisher={The Electrochemical Society} +} + +@article{Lin2014, title={A lumped-parameter electro-thermal model for cylindrical batteries}, author={Lin, Xinfan and Perez, Hector E and Mohan, Shankar and Siegel, Jason B and Stefanopoulou, Anna G and Ding, Yi and Castanier, Matthew P}, journal={Journal of Power Sources}, @@ -262,6 +366,17 @@ @article{lin2014lumped publisher={Elsevier} } +@article{Mackie1955, + title={The diffusion of electrolytes in a cation-exchange resin membrane I. Theoretical}, + author={Mackie, JS and Meares, P}, + journal={Proceedings of the Royal Society of London. Series A. Mathematical and Physical Sciences}, + volume={232}, + number={1191}, + pages={498--509}, + year={1955}, + publisher={The Royal Society London} +} + @article{Marquis2019, title = {{An asymptotic derivation of a single particle model with electrolyte}}, author = {Marquis, Scott G. and Sulzer, Valentin and Timms, Robert and Please, Colin P. and Chapman, S. Jon}, @@ -321,6 +436,17 @@ @article{Newman1962 publisher={IOP Publishing} } +@article{Nieto2012, +author = {Nieto, Nerea and Diaz, Luis and Gastelurrutia, Jon and Alava, Isabel and Blanco, Francisco and Ramos, Juan and Rivas, Alejandro}, +year = {2012}, +month = {11}, +pages = {A212-A217}, +title = {Thermal Modeling of Large Format Lithium-Ion Cells}, +volume = {160}, +journal = {Journal of the Electrochemical Society}, +doi = {10.1149/2.042302jes} +} + @article{Nyman2008, title={Electrochemical characterisation and modelling of the mass transport phenomena in LiPF6--EC--EMC electrolyte}, author={Nyman, Andreas and Behm, M{\aa}rten and Lindbergh, G{\"o}ran}, @@ -370,6 +496,28 @@ @article{ORegan2022 doi = {10.1016/j.electacta.2022.140700}, } +@article{Petersen1958, + title={Diffusion in a pore of varying cross section}, + author={Petersen, EE}, + journal={AIChE Journal}, + volume={4}, + number={3}, + pages={343--345}, + year={1958}, + publisher={Wiley Online Library} +} + +@article{Ploehn2004, + title={Solvent diffusion model for aging of lithium-ion battery cells}, + author={Ploehn, Harry J and Ramadass, Premanand and White, Ralph E}, + journal={Journal of The Electrochemical Society}, + volume={151}, + number={3}, + pages={A456}, + year={2004}, + publisher={IOP Publishing} +} + @article{Prada2013, title = {{A simplified electrochemical and thermal aging model of LiFePO4-graphite Li-ion batteries: power and capacity fade simulations}}, author = {Prada, Eric and Di Domenico, D. and Creff, Y. and Bernard, J. and Sauvant-Moynot, Val{\'{e}}rie and Huet, Fran{\c{c}}ois}, @@ -439,6 +587,61 @@ @article{Richardson2021 doi = {10.1016/j.electacta.2021.138909}, } +@article{Rieger2016, + title={A new method to model the thickness change of a commercial pouch cell during discharge}, + author={Rieger, Bernhard and Erhard, Simon V and Rumpf, Katharina and Jossen, Andreas}, + journal={Journal of The Electrochemical Society}, + volume={163}, + number={8}, + pages={A1566}, + year={2016}, + publisher={IOP Publishing} +} + +@article{Safari2008, + title={Multimodal physics-based aging model for life prediction of Li-ion batteries}, + author={Safari, M and Morcrette, Mathieu and Teyssot, A and Delacourt, Charles}, + journal={Journal of The Electrochemical Society}, + volume={156}, + number={3}, + pages={A145}, + year={2008}, + publisher={IOP Publishing} +} + +@article{Shampine1997, + title={The matlab ode suite}, + author={Shampine, Lawrence F and Reichelt, Mark W}, + journal={SIAM journal on scientific computing}, + volume={18}, + number={1}, + pages={1--22}, + year={1997}, + publisher={SIAM} +} + +@article{Shen2007, + title={Critical review of the impact of tortuosity on diffusion}, + author={Shen, Lihua and Chen, Zhangxin}, + journal={Chemical Engineering Science}, + volume={62}, + number={14}, + pages={3748--3755}, + year={2007}, + publisher={Elsevier} +} + +@article{Single2018, + title={Identifying the mechanism of continued growth of the solid--electrolyte interphase}, + author={Single, Fabian and Latz, Arnulf and Horstmann, Birger}, + journal={ChemSusChem}, + volume={11}, + number={12}, + pages={1950--1955}, + year={2018}, + publisher={Wiley Online Library} +} + @article{Sripad2020, title={Kinetics of lithium electrodeposition and stripping}, author={Sripad, Shashank and Korff, Daniel and DeCaluwe, Steven C and Viswanathan, Venkatasubramanian}, @@ -510,6 +713,17 @@ @article{Timms2021 doi = {10.1137/20M1336898}, } +@article{Tomadakis1993, + title={Transport properties of random arrays of freely overlapping cylinders with various orientation distributions}, + author={Tomadakis, Manolis M and Sotirchos, Stratis V}, + journal={The Journal of chemical physics}, + volume={98}, + number={1}, + pages={616--626}, + year={1993}, + publisher={American Institute of Physics} +} + @article{Valoen2005, title={Transport properties of LiPF6-based Li-ion battery electrolytes}, author={Val{\o}en, Lars Ole and Reimers, Jan N}, @@ -556,6 +770,17 @@ @article{Wang2002 doi = {10.1006/jcph.2002.7041}, } +@article{Weissberg1963, + title={Effective diffusion coefficient in porous media}, + author={Weissberg, Harold L}, + journal={Journal of Applied Physics}, + volume={34}, + number={9}, + pages={2636--2639}, + year={1963}, + publisher={American Institute of Physics} +} + @article{Weng2023, title={Differential voltage analysis for battery manufacturing process control}, author={Weng, Andrew and Siegel, Jason B and Stefanopoulou, Anna}, @@ -563,6 +788,19 @@ @article{Weng2023 year={2023} } +@article{Wycisk2022, + title = {Modified Plett-model for modeling voltage hysteresis in lithium-ion cells}, + journal = {Journal of Energy Storage}, + volume = {52}, + pages = {105016}, + year = {2022}, + issn = {2352-152X}, + doi = {https://doi.org/10.1016/j.est.2022.105016}, + url = {https://www.sciencedirect.com/science/article/pii/S2352152X22010192}, + author = {Dominik Wycisk and Marc Oldenburger and Marc Gerry Stoye and Toni Mrkonjic and Arnulf Latz}, + keywords = {Lithium-ion battery, Voltage hysteresis, Plett-model, Silicon–graphite anode}, +} + @article{Xu2019, title={Evolution of Dead Lithium Growth in Lithium Metal Batteries: Experimentally Validated Model of the Apparent Capacity Loss}, author={Xu, Shanshan and Chen, Kuan-Hung and Dasgupta, Neil P and Siegel, Jason B and Stefanopoulou, Anna G}, @@ -595,222 +833,3 @@ @article{Zhao2018 year={2018}, publisher={IOP Publishing} } - -@article{Barletta2022thevenin, - title={Th{\'e}venin’s Battery Model Parameter Estimation Based on Simulink}, - author={Barletta, Giulio and DiPrima, Piera and Papurello, Davide}, - journal={Energies}, - volume={15}, - number={17}, - pages={6207}, - year={2022}, - publisher={MDPI} -} - -@article{Nieto2012, -author = {Nieto, Nerea and Diaz, Luis and Gastelurrutia, Jon and Alava, Isabel and Blanco, Francisco and Ramos, Juan and Rivas, Alejandro}, -year = {2012}, -month = {11}, -pages = {A212-A217}, -title = {Thermal Modeling of Large Format Lithium-Ion Cells}, -volume = {160}, -journal = {Journal of the Electrochemical Society}, -doi = {10.1149/2.042302jes} -} - -@article{shampine1997matlab, - title={The matlab ode suite}, - author={Shampine, Lawrence F and Reichelt, Mark W}, - journal={SIAM journal on scientific computing}, - volume={18}, - number={1}, - pages={1--22}, - year={1997}, - publisher={SIAM} -} - -@article{byrne1975polyalgorithm, - title={A polyalgorithm for the numerical solution of ordinary differential equations}, - author={Byrne, George D. and Hindmarsh, Alan C.}, - journal={ACM Transactions on Mathematical Software (TOMS)}, - volume={1}, - number={1}, - pages={71--96}, - year={1975}, - publisher={ACM New York, NY, USA} -} - -@book{hairer1993solving, - title={Solving ordinary differential equations. 1, Nonstiff problems}, - author={Hairer, Ernst and N{\o}rsett, Syvert P and Wanner, Gerhard}, - year={1993}, - publisher={Springer-Vlg} -} - -@article{baltensperger2003spectral, - title={Spectral differencing with a twist}, - author={Baltensperger, Richard and Trummer, Manfred R}, - journal={SIAM journal on scientific computing}, - volume={24}, - number={5}, - pages={1465--1487}, - year={2003}, - publisher={SIAM} -} - -@article{rieger2016new, - title={A new method to model the thickness change of a commercial pouch cell during discharge}, - author={Rieger, Bernhard and Erhard, Simon V and Rumpf, Katharina and Jossen, Andreas}, - journal={Journal of The Electrochemical Society}, - volume={163}, - number={8}, - pages={A1566}, - year={2016}, - publisher={IOP Publishing} -} - -@article{ploehn2004solvent, - title={Solvent diffusion model for aging of lithium-ion battery cells}, - author={Ploehn, Harry J and Ramadass, Premanand and White, Ralph E}, - journal={Journal of The Electrochemical Society}, - volume={151}, - number={3}, - pages={A456}, - year={2004}, - publisher={IOP Publishing} -} - -@article{single2018identifying, - title={Identifying the mechanism of continued growth of the solid--electrolyte interphase}, - author={Single, Fabian and Latz, Arnulf and Horstmann, Birger}, - journal={ChemSusChem}, - volume={11}, - number={12}, - pages={1950--1955}, - year={2018}, - publisher={Wiley Online Library} -} - -@article{safari2008multimodal, - title={Multimodal physics-based aging model for life prediction of Li-ion batteries}, - author={Safari, M and Morcrette, Mathieu and Teyssot, A and Delacourt, Charles}, - journal={Journal of The Electrochemical Society}, - volume={156}, - number={3}, - pages={A145}, - year={2008}, - publisher={IOP Publishing} -} - -@article{landesfeind2019temperature, - title={Temperature and concentration dependence of the ionic transport properties of lithium-ion battery electrolytes}, - author={Landesfeind, Johannes and Gasteiger, Hubert A}, - journal={Journal of The Electrochemical Society}, - volume={166}, - number={14}, - pages={A3079--A3097}, - year={2019}, - publisher={The Electrochemical Society} -} -@article{akanni1987effective, - title={Effective transport coefficients in heterogeneous media}, - author={Akanni, KA and Evans, JW and Abramson, IS}, - journal={Chemical Engineering Science}, - volume={42}, - number={8}, - pages={1945--1954}, - year={1987}, - publisher={Elsevier} -} -@article{petersen1958diffusion, - title={Diffusion in a pore of varying cross section}, - author={Petersen, EE}, - journal={AIChE Journal}, - volume={4}, - number={3}, - pages={343--345}, - year={1958}, - publisher={Wiley Online Library} -} -@article{bruggeman1935berechnung, - title={Berechnung verschiedener physikalischer Konstanten von heterogenen Substanzen. I. Dielektrizit{\"a}tskonstanten und Leitf{\"a}higkeiten der Mischk{\"o}rper aus isotropen Substanzen}, - author={Bruggeman, Von DAG}, - journal={Annalen der physik}, - volume={416}, - number={7}, - pages={636--664}, - year={1935}, - publisher={Wiley Online Library} -} -@article{weissberg1963effective, - title={Effective diffusion coefficient in porous media}, - author={Weissberg, Harold L}, - journal={Journal of Applied Physics}, - volume={34}, - number={9}, - pages={2636--2639}, - year={1963}, - publisher={American Institute of Physics} -} -@article{tomadakis1993transport, - title={Transport properties of random arrays of freely overlapping cylinders with various orientation distributions}, - author={Tomadakis, Manolis M and Sotirchos, Stratis V}, - journal={The Journal of chemical physics}, - volume={98}, - number={1}, - pages={616--626}, - year={1993}, - publisher={American Institute of Physics} -} -@article{beeckman1990mathematical, - title={Mathematical description of heterogeneous materials}, - author={Beeckman, JW}, - journal={Chemical engineering science}, - volume={45}, - number={8}, - pages={2603--2610}, - year={1990}, - publisher={Elsevier} -} -@article{mackie1955diffusion, - title={The diffusion of electrolytes in a cation-exchange resin membrane I. Theoretical}, - author={Mackie, JS and Meares, P}, - journal={Proceedings of the Royal Society of London. Series A. Mathematical and Physical Sciences}, - volume={232}, - number={1191}, - pages={498--509}, - year={1955}, - publisher={The Royal Society London} -} -@article{shen2007critical, - title={Critical review of the impact of tortuosity on diffusion}, - author={Shen, Lihua and Chen, Zhangxin}, - journal={Chemical Engineering Science}, - volume={62}, - number={14}, - pages={3748--3755}, - year={2007}, - publisher={Elsevier} -} -@article{Wycisk2022, - title = {Modified Plett-model for modeling voltage hysteresis in lithium-ion cells}, - journal = {Journal of Energy Storage}, - volume = {52}, - pages = {105016}, - year = {2022}, - issn = {2352-152X}, - doi = {https://doi.org/10.1016/j.est.2022.105016}, - url = {https://www.sciencedirect.com/science/article/pii/S2352152X22010192}, - author = {Dominik Wycisk and Marc Oldenburger and Marc Gerry Stoye and Toni Mrkonjic and Arnulf Latz}, - keywords = {Lithium-ion battery, Voltage hysteresis, Plett-model, Silicon–graphite anode}, -} - -@article{Fan2022, - title={Data-driven identification of lithium-ion batteries: A nonlinear equivalent circuit model with diffusion dynamics}, - author={Fan, Chuanxin and O’Regan, Kieran and Li, Liuying and Higgins, Matthew D and Kendrick, Emma and Widanage, Widanalage D}, - journal={Applied Energy}, - volume={321}, - pages={119336}, - year={2022}, - publisher={Elsevier} -} diff --git a/src/pybamm/__init__.py b/src/pybamm/__init__.py index 51c7f49969..f51ba05d02 100644 --- a/src/pybamm/__init__.py +++ b/src/pybamm/__init__.py @@ -75,6 +75,7 @@ from .models.full_battery_models import lead_acid from .models.full_battery_models import lithium_ion from .models.full_battery_models import equivalent_circuit +from .models.full_battery_models import sodium_ion # Submodel classes from .models.submodels.base_submodel import BaseSubModel diff --git a/src/pybamm/input/parameters/__init__.py b/src/pybamm/input/parameters/__init__.py index 9ef23b743d..3c21058270 100644 --- a/src/pybamm/input/parameters/__init__.py +++ b/src/pybamm/input/parameters/__init__.py @@ -1 +1 @@ -__all__ = ['ecm', 'lead_acid', 'lithium_ion'] +__all__ = ['ecm', 'lead_acid', 'lithium_ion', 'sodium_ion'] diff --git a/src/pybamm/input/parameters/lithium_ion/Ai2020.py b/src/pybamm/input/parameters/lithium_ion/Ai2020.py index f578d59fa5..4bf51f3440 100644 --- a/src/pybamm/input/parameters/lithium_ion/Ai2020.py +++ b/src/pybamm/input/parameters/lithium_ion/Ai2020.py @@ -517,11 +517,11 @@ def lico2_ocp_Ai2020(sto): def get_parameter_values(): """ Parameters for the Enertech cell (Ai2020), from the papers :footcite:t:`Ai2019`, - :footcite:t:`rieger2016new` and references therein. + :footcite:t:`Rieger2016` and references therein. SEI parameters are example parameters for SEI growth from the papers - :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`, - :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and + :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`, + :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and :footcite:t:`Yang2017` .. note:: diff --git a/src/pybamm/input/parameters/lithium_ion/Chen2020.py b/src/pybamm/input/parameters/lithium_ion/Chen2020.py index b3655513a1..eccac74615 100644 --- a/src/pybamm/input/parameters/lithium_ion/Chen2020.py +++ b/src/pybamm/input/parameters/lithium_ion/Chen2020.py @@ -213,8 +213,8 @@ def get_parameter_values(): therein. SEI parameters are example parameters for SEI growth from the papers - :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`, - :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and + :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`, + :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and :footcite:t:`Yang2017` .. note:: diff --git a/src/pybamm/input/parameters/lithium_ion/Ecker2015.py b/src/pybamm/input/parameters/lithium_ion/Ecker2015.py index 05fbbb2fd7..30ca2ef827 100644 --- a/src/pybamm/input/parameters/lithium_ion/Ecker2015.py +++ b/src/pybamm/input/parameters/lithium_ion/Ecker2015.py @@ -488,8 +488,8 @@ def get_parameter_values(): by Dr. Simon O'Kane in the paper :footcite:t:`Richardson2020` SEI parameters are example parameters for SEI growth from the papers - :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`, - :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and + :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`, + :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and :footcite:t:`Yang2017` .. note:: diff --git a/src/pybamm/input/parameters/lithium_ion/Marquis2019.py b/src/pybamm/input/parameters/lithium_ion/Marquis2019.py index 16591eac2d..13b8f57966 100644 --- a/src/pybamm/input/parameters/lithium_ion/Marquis2019.py +++ b/src/pybamm/input/parameters/lithium_ion/Marquis2019.py @@ -338,8 +338,8 @@ def get_parameter_values(): and references therein. SEI parameters are example parameters for SEI growth from the papers - :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`, - :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and + :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`, + :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and :footcite:t:`Yang2017` .. note:: diff --git a/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py b/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py index 0176c3f6a0..044cebe3c5 100644 --- a/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py +++ b/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py @@ -319,8 +319,8 @@ def get_parameter_values(): and references therein. SEI parameters are example parameters for SEI growth from the papers - :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`, - :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and + :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`, + :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and :footcite:t:`Yang2017` SEI parameters diff --git a/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py b/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py index 1af610f58a..da1191fa8c 100644 --- a/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py +++ b/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py @@ -297,8 +297,8 @@ def get_parameter_values(): for the planar effective thermal conductivity. SEI parameters are example parameters for SEI growth from the papers - :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`, - :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and + :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`, + :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and :footcite:t:`Yang2017` .. note:: diff --git a/src/pybamm/input/parameters/lithium_ion/ORegan2022.py b/src/pybamm/input/parameters/lithium_ion/ORegan2022.py index 45c8cdbcbd..d7e240a7b6 100644 --- a/src/pybamm/input/parameters/lithium_ion/ORegan2022.py +++ b/src/pybamm/input/parameters/lithium_ion/ORegan2022.py @@ -921,7 +921,7 @@ def get_parameter_values(): Parameters for an LG M50 cell, from the paper :footcite:t:`ORegan2022` Parameters for a LiPF6 in EC:EMC (3:7 w:w) electrolyte are from the paper - :footcite:t:`landesfeind2019temperature` and references therein. + :footcite:t:`Landesfeind2019` and references therein. """ return { diff --git a/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py b/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py index a1e24da7e3..82c0df76bf 100644 --- a/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py +++ b/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py @@ -355,7 +355,7 @@ def get_parameter_values(): :footcite:t:`Zhao2018` Parameters for SEI growth are from the papers :footcite:t:`Ramadass2004` and - :footcite:t:`safari2008multimodal` + :footcite:t:`Safari2008` .. note:: Ramadass 2004 has mistakes in units and values of SEI parameters, corrected by diff --git a/src/pybamm/input/parameters/lithium_ion/Xu2019.py b/src/pybamm/input/parameters/lithium_ion/Xu2019.py index caee487339..d1c5edea98 100644 --- a/src/pybamm/input/parameters/lithium_ion/Xu2019.py +++ b/src/pybamm/input/parameters/lithium_ion/Xu2019.py @@ -201,8 +201,8 @@ def get_parameter_values(): ^^^^^^^^^^^^^^^^^^^^^^ SEI parameters are example parameters for SEI growth from the papers - :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`, - :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and + :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`, + :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and :footcite:t:`Yang2017`. .. note:: diff --git a/src/pybamm/input/parameters/sodium_ion/Chayambuka2022.py b/src/pybamm/input/parameters/sodium_ion/Chayambuka2022.py new file mode 100644 index 0000000000..f8c423cf76 --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/Chayambuka2022.py @@ -0,0 +1,341 @@ +import pybamm +import os + +path, _ = os.path.split(os.path.abspath(__file__)) + +U_n_data = pybamm.parameters.process_1D_data("U_n.csv", path=path) +U_p_data = pybamm.parameters.process_1D_data("U_p.csv", path=path) +D_n_data = pybamm.parameters.process_1D_data("D_n.csv", path=path) +D_p_data = pybamm.parameters.process_1D_data("D_p.csv", path=path) +k_n_data = pybamm.parameters.process_1D_data("k_n.csv", path=path) +k_p_data = pybamm.parameters.process_1D_data("k_p.csv", path=path) +D_e_data = pybamm.parameters.process_1D_data("D_e.csv", path=path) +sigma_e_data = pybamm.parameters.process_1D_data("sigma_e.csv", path=path) + + +def HC_ocp_Chayambuka2022(sto): + """ + HC open-circuit potential as a function of stochiometry, data taken + from [1]. + + References + ---------- + .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based + modeling of sodium-ion batteries part II. Model and validation, Electrochimica + Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764. + + Parameters + ---------- + sto: :class:`pybamm.Symbol` + Electrode stochiometry + + Returns + ------- + :class:`pybamm.Symbol` + Open-circuit potential + """ + + name, (x, y) = U_n_data + return pybamm.Interpolant(x, y, sto, name) + + +def HC_diffusivity_Chayambuka2022(sto, T): + """ + HC diffusivity as a function of stochiometry, the data is taken from [1]. + + References + ---------- + .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based + modeling of sodium-ion batteries part II. Model and validation, Electrochimica + Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764. + + Parameters + ---------- + sto: :class:`pybamm.Symbol` + Electrode stochiometry + T: :class:`pybamm.Symbol` + Dimensional temperature + + Returns + ------- + :class:`pybamm.Symbol` + Solid diffusivity + """ + + name, (x, y) = D_n_data + c_max = pybamm.Parameter("Maximum concentration in negative electrode [mol.m-3]") + return pybamm.Interpolant(x, y, sto * c_max, name) + + +def HC_electrolyte_exchange_current_density_Chayambuka2022(c_e, c_s_surf, c_s_max, T): + """ + Exchange-current density for Butler-Volmer reactions between HC and NaPF6 in + EC:PC. + + References + ---------- + .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based + modeling of sodium-ion batteries part II. Model and validation, Electrochimica + Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764. + + Parameters + ---------- + c_e : :class:`pybamm.Symbol` + Electrolyte concentration [mol.m-3] + c_s_surf : :class:`pybamm.Symbol` + Particle concentration [mol.m-3] + c_s_max : :class:`pybamm.Symbol` + Maximum particle concentration [mol.m-3] + T : :class:`pybamm.Symbol` + Temperature [K] + + Returns + ------- + :class:`pybamm.Symbol` + Exchange-current density [A.m-2] + """ + name, (x, y) = k_n_data + k_n = pybamm.Interpolant(x, y, c_s_surf, name) + c_e0 = pybamm.Parameter("Initial concentration in electrolyte [mol.m-3]") + + return ( + pybamm.constants.F + * k_n + * (c_e / c_e0) ** 0.5 + * c_s_surf**0.5 + * (c_s_max - c_s_surf) ** 0.5 + / 2 + ) + + +def NVPF_ocp_Chayambuka2022(sto): + """ + NVPF open-circuit potential as a function of stochiometry, data taken + from [1]. + + References + ---------- + .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based + modeling of sodium-ion batteries part II. Model and validation, Electrochimica + Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764. + + Parameters + ---------- + sto: :class:`pybamm.Symbol` + Electrode stochiometry + + Returns + ------- + :class:`pybamm.Symbol` + Open-circuit potential + """ + + name, (x, y) = U_p_data + return pybamm.Interpolant(x, y, sto, name) + + +def NVPF_diffusivity_Chayambuka2022(sto, T): + """ + NVPF diffusivity as a function of stochiometry, the data is taken from [1]. + + References + ---------- + .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based + modeling of sodium-ion batteries part II. Model and validation, Electrochimica + Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764. + + Parameters + ---------- + sto: :class:`pybamm.Symbol` + Electrode stochiometry + T: :class:`pybamm.Symbol` + Dimensional temperature + + Returns + ------- + :class:`pybamm.Symbol` + Solid diffusivity + """ + + name, (x, y) = D_p_data + c_max = pybamm.Parameter("Initial concentration in electrolyte [mol.m-3]") + return pybamm.Interpolant(x, y, sto * c_max, name) + + +def NVPF_electrolyte_exchange_current_density_Chayambuka2022(c_e, c_s_surf, c_s_max, T): + """ + Exchange-current density for Butler-Volmer reactions between NVPF and NaPF6 in + EC:PC. + + References + ---------- + .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based + modeling of sodium-ion batteries part II. Model and validation, Electrochimica + Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764. + + Parameters + ---------- + c_e : :class:`pybamm.Symbol` + Electrolyte concentration [mol.m-3] + c_s_surf : :class:`pybamm.Symbol` + Particle concentration [mol.m-3] + c_s_max : :class:`pybamm.Symbol` + Maximum particle concentration [mol.m-3] + T : :class:`pybamm.Symbol` + Temperature [K] + + Returns + ------- + :class:`pybamm.Symbol` + Exchange-current density [A.m-2] + """ + name, (x, y) = k_p_data + k_p = pybamm.Interpolant(x, y, c_s_surf, name) + c_e0 = pybamm.Parameter("Initial concentration in electrolyte [mol.m-3]") + + return ( + pybamm.constants.F + * k_p + * (c_e / c_e0) ** 0.5 + * c_s_surf**0.5 + * (c_s_max - c_s_surf) ** 0.5 + / 2 + ) + + +def electrolyte_diffusivity_Chayambuka2022(c_e, T): + """ + Diffusivity of NaPF6 in EC:PC (1:1) as a function of ion concentration. The data + comes from [1] + + References + ---------- + .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based + modeling of sodium-ion batteries part II. Model and validation, Electrochimica + Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764. + + Parameters + ---------- + c_e: :class:`pybamm.Symbol` + Dimensional electrolyte concentration + T: :class:`pybamm.Symbol` + Dimensional temperature + + Returns + ------- + :class:`pybamm.Symbol` + Solid diffusivity + """ + + name, (x, y) = D_e_data + D_e = pybamm.Interpolant(x, y, c_e, name) + + # Chayambuka et al. (2022) does not provide temperature dependence + + return D_e + + +def electrolyte_conductivity_Chayambuka2022(c_e, T): + """ + Conductivity of NaPF6 in EC:PC (1:1) as a function of ion concentration. The data + comes from [1]. + + References + ---------- + .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based + modeling of sodium-ion batteries part II. Model and validation, Electrochimica + Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764. + + Parameters + ---------- + c_e: :class:`pybamm.Symbol` + Dimensional electrolyte concentration + T: :class:`pybamm.Symbol` + Dimensional temperature + + Returns + ------- + :class:`pybamm.Symbol` + Solid diffusivity + """ + + name, (x, y) = sigma_e_data + sigma_e = pybamm.Interpolant(x, y, c_e, name) + + # Chayambuka et al. (2022) does not provide temperature dependence + + return sigma_e + + +# Call dict via a function to avoid errors when editing in place +def get_parameter_values(): + """ + Parameters for a sodium-ion cell, from the paper :footcite:t:`Chayambuka2022` and references + therein. The specific parameter values are taken from the COMSOL implementation presented in + [this example](https://www.comsol.com/model/1d-isothermal-sodium-ion-battery-117341). + + """ + + return { + "chemistry": "sodium_ion", + # cell + "Negative electrode thickness [m]": 64e-6, + "Separator thickness [m]": 25e-6, + "Positive electrode thickness [m]": 68e-6, + "Electrode height [m]": 2.54e-4, + "Electrode width [m]": 1, + "Nominal cell capacity [A.h]": 3e-3, + "Current function [A]": 3e-3, + "Contact resistance [Ohm]": 0, + # negative electrode + "Negative electrode conductivity [S.m-1]": 256, + "Maximum concentration in negative electrode [mol.m-3]": 14540, + "Negative particle diffusivity [m2.s-1]": HC_diffusivity_Chayambuka2022, + "Negative electrode OCP [V]": HC_ocp_Chayambuka2022, + "Negative electrode porosity": 0.51, + "Negative electrode active material volume fraction": 0.489, # 1 - 0.51 - 0.001 + "Negative particle radius [m]": 3.48e-6, + "Negative electrode Bruggeman coefficient (electrolyte)": 1.5, + "Negative electrode Bruggeman coefficient (electrode)": 0, + "Negative electrode charge transfer coefficient": 0.5, + "Negative electrode exchange-current density [A.m-2]" + "": HC_electrolyte_exchange_current_density_Chayambuka2022, + "Negative electrode OCP entropic change [V.K-1]": 0, + # positive electrode + "Positive electrode conductivity [S.m-1]": 50, + "Maximum concentration in positive electrode [mol.m-3]": 15320, + "Positive particle diffusivity [m2.s-1]": NVPF_diffusivity_Chayambuka2022, + "Positive electrode OCP [V]": NVPF_ocp_Chayambuka2022, + "Positive electrode porosity": 0.23, + "Positive electrode active material volume fraction": 0.55, # 1 - 0.23 - 0.22 + "Positive particle radius [m]": 0.59e-6, + "Positive electrode Bruggeman coefficient (electrolyte)": 1.5, + "Positive electrode Bruggeman coefficient (electrode)": 0, + "Positive electrode charge transfer coefficient": 0.5, + "Positive electrode exchange-current density [A.m-2]" + "": NVPF_electrolyte_exchange_current_density_Chayambuka2022, + "Positive electrode OCP entropic change [V.K-1]": 0, + # separator + "Separator porosity": 0.55, + "Separator Bruggeman coefficient (electrolyte)": 1.5, + # electrolyte + "Initial concentration in electrolyte [mol.m-3]": 1000, + "Cation transference number": 0.45, + "Thermodynamic factor": 1, + "Electrolyte diffusivity [m2.s-1]": electrolyte_diffusivity_Chayambuka2022, + "Electrolyte conductivity [S.m-1]": electrolyte_conductivity_Chayambuka2022, + # experiment + "Reference temperature [K]": 298.15, + "Ambient temperature [K]": 298.15, + "Number of electrodes connected in parallel to make a cell": 1.0, + "Number of cells connected in series to make a battery": 1.0, + "Lower voltage cut-off [V]": 2.0, + "Upper voltage cut-off [V]": 4.2, + "Open-circuit voltage at 0% SOC [V]": 2.0, + "Open-circuit voltage at 100% SOC [V]": 4.2, + "Initial concentration in negative electrode [mol.m-3]": 13520, + "Initial concentration in positive electrode [mol.m-3]": 3320, + "Initial temperature [K]": 298.15, + # citations + "citations": ["Chayambuka2022"], + } diff --git a/src/pybamm/input/parameters/sodium_ion/__init__.py b/src/pybamm/input/parameters/sodium_ion/__init__.py new file mode 100644 index 0000000000..7591ba5554 --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/__init__.py @@ -0,0 +1 @@ +__all__ = ['Chayambuka2022'] diff --git a/src/pybamm/input/parameters/sodium_ion/data/D_e.csv b/src/pybamm/input/parameters/sodium_ion/data/D_e.csv new file mode 100755 index 0000000000..3fe74a8cb8 --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/data/D_e.csv @@ -0,0 +1,14 @@ +Electrolyte concentration [mol.m-3],Electrolyte diffusivity [m2.s-1] +1.131153,4.14E-11 +124.121064,3.87E-11 +249.37328,3.62E-11 +387.618465,3.33E-11 +538.734332,3.11E-11 +741.699786,2.81E-11 +1013.696117,2.49E-11 +1272.69948,2.25E-11 +1514.27698,2.14E-11 +1811.953531,1.99E-11 +2113.940691,1.86E-11 +2320.97218,1.79E-11 +2471.904616,1.76E-11 diff --git a/src/pybamm/input/parameters/sodium_ion/data/D_n.csv b/src/pybamm/input/parameters/sodium_ion/data/D_n.csv new file mode 100755 index 0000000000..12d42cce5d --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/data/D_n.csv @@ -0,0 +1,40 @@ +Negative particle concentration [mol.m-3],Negative electrode difusivity [m2.s-1] +73.891626,2.57E-16 +246.305419,3.03E-16 +418.719212,3.54E-16 +714.285714,3.99E-16 +1108.374384,4.50E-16 +1477.832512,4.83E-16 +1847.29064,5.31E-16 +2241.37931,6.21E-16 +2733.990148,7.60E-16 +3177.339901,9.20E-16 +3620.689655,1.11E-15 +4113.300493,1.36E-15 +4630.541872,1.57E-15 +5197.044335,1.79E-15 +5714.285714,1.93E-15 +6280.788177,2.02E-15 +6896.551724,1.97E-15 +7413.793103,1.84E-15 +7832.512315,1.67E-15 +8325.123153,1.45E-15 +8669.950739,1.27E-15 +9039.408867,1.06E-15 +9384.236453,8.77E-16 +9729.064039,7.08E-16 +10073.89163,5.44E-16 +10394.08867,4.39E-16 +10714.28571,3.58E-16 +10960.59113,3.14E-16 +11206.89655,2.96E-16 +11502.46305,3.14E-16 +11847.29064,3.80E-16 +12142.85714,4.72E-16 +12487.68473,6.13E-16 +12832.51232,7.78E-16 +13226.60099,1.01E-15 +13719.21182,1.21E-15 +14014.77833,1.28E-15 +14261.08374,1.28E-15 +14433.49754,1.24E-15 diff --git a/src/pybamm/input/parameters/sodium_ion/data/D_p.csv b/src/pybamm/input/parameters/sodium_ion/data/D_p.csv new file mode 100755 index 0000000000..51665bcea9 --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/data/D_p.csv @@ -0,0 +1,35 @@ +Positive particle concentration [mol.m-3],Positive electrode difusivity [m2.s-1] +131.578947,2.51E-15 +592.105263,1.71E-15 +1052.631579,1.16E-15 +1535.087719,7.41E-16 +2017.54386,4.61E-16 +2521.929825,2.62E-16 +2982.45614,1.56E-16 +3355.263158,9.92E-17 +3618.421053,7.92E-17 +3925.438596,6.61E-17 +4254.385965,5.65E-17 +4495.614035,5.53E-17 +4890.350877,6.07E-17 +5263.157895,6.65E-17 +5679.824561,7.64E-17 +6096.491228,8.38E-17 +6578.947368,8.79E-17 +7039.473684,8.81E-17 +7587.719298,8.44E-17 +8201.754386,7.52E-17 +8837.719298,6.46E-17 +9495.614035,5.05E-17 +10109.64912,4.13E-17 +10657.89474,3.45E-17 +11184.21053,3.16E-17 +11776.31579,3.17E-17 +12346.49123,3.40E-17 +12828.94737,3.57E-17 +13333.33333,3.66E-17 +13750,3.58E-17 +14254.38596,3.35E-17 +14649.12281,2.74E-17 +15000,2.29E-17 +15197.36842,1.95E-17 diff --git a/src/pybamm/input/parameters/sodium_ion/data/U_n.csv b/src/pybamm/input/parameters/sodium_ion/data/U_n.csv new file mode 100755 index 0000000000..ddb213b3db --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/data/U_n.csv @@ -0,0 +1,21 @@ +Negative particle stoichiometry,Negative electrode open-circuit potential [V] +0.001436794,1.318963892 +0.001643334,1.21982507 +0.00811789,1.112038542 +0.01027308,1.077546494 +0.035479844,0.978299913 +0.060758448,0.844570264 +0.090185795,0.719443422 +0.127982471,0.577039126 +0.169900951,0.456168787 +0.232688871,0.317967115 +0.320485995,0.1753473 +0.403954777,0.110332349 +0.483167055,0.088439192 +0.574861484,0.075112923 +0.687380455,0.066007238 +0.799899424,0.056901553 +0.891593855,0.043575284 +0.960353451,0.038968561 +0.987446008,0.034541438 +0.995806356,0.021574368 diff --git a/src/pybamm/input/parameters/sodium_ion/data/U_p.csv b/src/pybamm/input/parameters/sodium_ion/data/U_p.csv new file mode 100755 index 0000000000..a2698567e6 --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/data/U_p.csv @@ -0,0 +1,28 @@ +Positive particle stoichiometry,Positive electrode open-circuit potential [V] +0.21,4.288102031 +0.21004478,4.210892773 +0.219269532,4.175283892 +0.261497402,4.172472367 +0.332354842,4.172738781 +0.409317336,4.158176665 +0.474174208,4.152479924 +0.526985168,4.143767596 +0.560316674,4.11121965 +0.575706188,4.048901275 +0.582273973,3.941995276 +0.585975815,3.805375531 +0.590901654,3.725196032 +0.598588947,3.695521965 +0.655863013,3.698707605 +0.696598205,3.69292017 +0.741871138,3.684179499 +0.802205196,3.678465753 +0.864031932,3.675727917 +0.930500897,3.649245158 +0.959279735,3.62262069 +0.979341333,3.530616911 +0.980053837,3.450437411 +0.983058101,3.391026925 +0.992282853,3.355418044 +0.996268304,3.162363722 +0.999940293,3.031684459 diff --git a/src/pybamm/input/parameters/sodium_ion/data/k_n.csv b/src/pybamm/input/parameters/sodium_ion/data/k_n.csv new file mode 100755 index 0000000000..152ebfac4e --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/data/k_n.csv @@ -0,0 +1,32 @@ +Negative particle concentration [mol.m-3],Negative electrode exchange-current density rate [m.s-1] +121.359223,3.33E-11 +412.621359,2.15E-11 +631.067961,1.39E-11 +800.970874,9.40E-12 +970.873786,6.88E-12 +1140.776699,5.71E-12 +1262.135922,5.26E-12 +1529.126214,7.03E-12 +1796.116505,1.04E-11 +2063.106796,1.58E-11 +2500,2.65E-11 +3058.252427,4.18E-11 +3713.592233,5.83E-11 +4660.194175,7.48E-11 +5412.621359,7.96E-11 +6067.961165,7.48E-11 +6868.932039,6.47E-11 +7718.446602,4.94E-11 +8422.330097,3.47E-11 +9004.854369,2.39E-11 +9538.834951,1.61E-11 +10024.27184,1.21E-11 +10388.34951,1.06E-11 +10752.42718,1.13E-11 +11213.59223,1.51E-11 +11723.30097,2.25E-11 +12378.64078,3.54E-11 +12912.62136,4.94E-11 +13470.87379,6.34E-11 +14004.85437,7.48E-11 +14417.47573,7.96E-11 diff --git a/src/pybamm/input/parameters/sodium_ion/data/k_p.csv b/src/pybamm/input/parameters/sodium_ion/data/k_p.csv new file mode 100755 index 0000000000..7c228e907c --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/data/k_p.csv @@ -0,0 +1,24 @@ +Positive particle concentration [mol.m-3],Positive electrode exchange-current density rate [m.s-1] +21.929825,2.27E-10 +372.807018,1.74E-10 +833.333333,1.24E-10 +1293.859649,8.51E-11 +1907.894737,4.83E-11 +2456.140351,2.96E-11 +2960.526316,1.95E-11 +3442.982456,1.45E-11 +3969.298246,1.16E-11 +4802.631579,1.08E-11 +5548.245614,1.17E-11 +6381.578947,1.18E-11 +7192.982456,1.10E-11 +8092.105263,8.79E-12 +8969.298246,6.65E-12 +9649.122807,5.03E-12 +10350.87719,3.87E-12 +11074.5614,3.34E-12 +12171.05263,3.37E-12 +13223.68421,3.60E-12 +14188.59649,3.30E-12 +14934.21053,2.35E-12 +15328.94737,1.91E-12 diff --git a/src/pybamm/input/parameters/sodium_ion/data/sigma_e.csv b/src/pybamm/input/parameters/sodium_ion/data/sigma_e.csv new file mode 100644 index 0000000000..e8a1104901 --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/data/sigma_e.csv @@ -0,0 +1,6 @@ +Electrolyte concentration [mol.m-3],Electrolyte conductivity [S.m-1] +150,0.404 +500,0.72 +1000,0.883 +1500,0.861 +2000,0.76 diff --git a/src/pybamm/models/full_battery_models/__init__.py b/src/pybamm/models/full_battery_models/__init__.py index 135f678289..0260f4dd07 100644 --- a/src/pybamm/models/full_battery_models/__init__.py +++ b/src/pybamm/models/full_battery_models/__init__.py @@ -1,2 +1,2 @@ __all__ = ['base_battery_model', 'equivalent_circuit', 'lead_acid', - 'lithium_ion'] + 'lithium_ion', 'sodium_ion'] diff --git a/src/pybamm/models/full_battery_models/sodium_ion/__init__.py b/src/pybamm/models/full_battery_models/sodium_ion/__init__.py new file mode 100644 index 0000000000..52e4e54952 --- /dev/null +++ b/src/pybamm/models/full_battery_models/sodium_ion/__init__.py @@ -0,0 +1,6 @@ +# +# Root of the sodium-ion models module. +# +from .basic_dfn import BasicDFN + +__all__ = ['basic_dfn'] diff --git a/src/pybamm/models/full_battery_models/sodium_ion/basic_dfn.py b/src/pybamm/models/full_battery_models/sodium_ion/basic_dfn.py new file mode 100644 index 0000000000..c6f618d338 --- /dev/null +++ b/src/pybamm/models/full_battery_models/sodium_ion/basic_dfn.py @@ -0,0 +1,273 @@ +# +# Basic Doyle-Fuller-Newman (DFN) Model +# +import pybamm + + +class BasicDFN(pybamm.lithium_ion.BaseModel): + """Doyle-Fuller-Newman (DFN) model of a sodium-ion battery, from + :footcite:t:`Marquis2019`. + + Parameters + ---------- + name : str, optional + The name of the model. + + """ + + def __init__(self, name="Doyle-Fuller-Newman model"): + super().__init__(name=name) + pybamm.citations.register("Marquis2019") + # `param` is a class containing all the relevant parameters and functions for + # this model. These are purely symbolic at this stage, and will be set by the + # `ParameterValues` class when the model is processed. + param = self.param + + ###################### + # Variables + ###################### + # Variables that depend on time only are created without a domain + Q = pybamm.Variable("Discharge capacity [A.h]") + + # Variables that vary spatially are created with a domain + c_e_n = pybamm.Variable( + "Negative electrolyte concentration [mol.m-3]", + domain="negative electrode", + ) + c_e_s = pybamm.Variable( + "Separator electrolyte concentration [mol.m-3]", + domain="separator", + ) + c_e_p = pybamm.Variable( + "Positive electrolyte concentration [mol.m-3]", + domain="positive electrode", + ) + # Concatenations combine several variables into a single variable, to simplify + # implementing equations that hold over several domains + c_e = pybamm.concatenation(c_e_n, c_e_s, c_e_p) + + # Electrolyte potential + phi_e_n = pybamm.Variable( + "Negative electrolyte potential [V]", + domain="negative electrode", + ) + phi_e_s = pybamm.Variable( + "Separator electrolyte potential [V]", + domain="separator", + ) + phi_e_p = pybamm.Variable( + "Positive electrolyte potential [V]", + domain="positive electrode", + ) + phi_e = pybamm.concatenation(phi_e_n, phi_e_s, phi_e_p) + + # Electrode potential + phi_s_n = pybamm.Variable( + "Negative electrode potential [V]", domain="negative electrode" + ) + phi_s_p = pybamm.Variable( + "Positive electrode potential [V]", + domain="positive electrode", + ) + # Particle concentrations are variables on the particle domain, but also vary in + # the x-direction (electrode domain) and so must be provided with auxiliary + # domains + c_s_n = pybamm.Variable( + "Negative particle concentration [mol.m-3]", + domain="negative particle", + auxiliary_domains={"secondary": "negative electrode"}, + ) + c_s_p = pybamm.Variable( + "Positive particle concentration [mol.m-3]", + domain="positive particle", + auxiliary_domains={"secondary": "positive electrode"}, + ) + + # Constant temperature + T = param.T_init + + ###################### + # Other set-up + ###################### + + # Current density + i_cell = param.current_density_with_time + + # Porosity + # Primary broadcasts are used to broadcast scalar quantities across a domain + # into a vector of the right shape, for multiplying with other vectors + eps_n = pybamm.PrimaryBroadcast( + pybamm.Parameter("Negative electrode porosity"), "negative electrode" + ) + eps_s = pybamm.PrimaryBroadcast( + pybamm.Parameter("Separator porosity"), "separator" + ) + eps_p = pybamm.PrimaryBroadcast( + pybamm.Parameter("Positive electrode porosity"), "positive electrode" + ) + eps = pybamm.concatenation(eps_n, eps_s, eps_p) + + # Active material volume fraction (eps + eps_s + eps_inactive = 1) + eps_s_n = pybamm.Parameter("Negative electrode active material volume fraction") + eps_s_p = pybamm.Parameter("Positive electrode active material volume fraction") + + # transport_efficiency + tor = pybamm.concatenation( + eps_n**param.n.b_e, eps_s**param.s.b_e, eps_p**param.p.b_e + ) + a_n = 3 * param.n.prim.epsilon_s_av / param.n.prim.R_typ + a_p = 3 * param.p.prim.epsilon_s_av / param.p.prim.R_typ + + # Interfacial reactions + # Surf takes the surface value of a variable, i.e. its boundary value on the + # right side. This is also accessible via `boundary_value(x, "right")`, with + # "left" providing the boundary value of the left side + c_s_surf_n = pybamm.surf(c_s_n) + sto_surf_n = c_s_surf_n / param.n.prim.c_max + j0_n = param.n.prim.j0(c_e_n, c_s_surf_n, T) + eta_n = phi_s_n - phi_e_n - param.n.prim.U(sto_surf_n, T) + Feta_RT_n = param.F * eta_n / (param.R * T) + j_n = 2 * j0_n * pybamm.sinh(param.n.prim.ne / 2 * Feta_RT_n) + + c_s_surf_p = pybamm.surf(c_s_p) + sto_surf_p = c_s_surf_p / param.p.prim.c_max + j0_p = param.p.prim.j0(c_e_p, c_s_surf_p, T) + eta_p = phi_s_p - phi_e_p - param.p.prim.U(sto_surf_p, T) + Feta_RT_p = param.F * eta_p / (param.R * T) + j_s = pybamm.PrimaryBroadcast(0, "separator") + j_p = 2 * j0_p * pybamm.sinh(param.p.prim.ne / 2 * Feta_RT_p) + + a_j_n = a_n * j_n + a_j_p = a_p * j_p + a_j = pybamm.concatenation(a_j_n, j_s, a_j_p) + + ###################### + # State of Charge + ###################### + I = param.current_with_time + # The `rhs` dictionary contains differential equations, with the key being the + # variable in the d/dt + self.rhs[Q] = I / 3600 + # Initial conditions must be provided for the ODEs + self.initial_conditions[Q] = pybamm.Scalar(0) + + ###################### + # Particles + ###################### + + # The div and grad operators will be converted to the appropriate matrix + # multiplication at the discretisation stage + N_s_n = -param.n.prim.D(c_s_n, T) * pybamm.grad(c_s_n) + N_s_p = -param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p) + self.rhs[c_s_n] = -pybamm.div(N_s_n) + self.rhs[c_s_p] = -pybamm.div(N_s_p) + # Boundary conditions must be provided for equations with spatial derivatives + self.boundary_conditions[c_s_n] = { + "left": (pybamm.Scalar(0), "Neumann"), + "right": ( + -j_n / (param.F * pybamm.surf(param.n.prim.D(c_s_n, T))), + "Neumann", + ), + } + self.boundary_conditions[c_s_p] = { + "left": (pybamm.Scalar(0), "Neumann"), + "right": ( + -j_p / (param.F * pybamm.surf(param.p.prim.D(c_s_p, T))), + "Neumann", + ), + } + self.initial_conditions[c_s_n] = param.n.prim.c_init + self.initial_conditions[c_s_p] = param.p.prim.c_init + ###################### + # Current in the solid + ###################### + sigma_eff_n = param.n.sigma(T) * eps_s_n**param.n.b_s + i_s_n = -sigma_eff_n * pybamm.grad(phi_s_n) + sigma_eff_p = param.p.sigma(T) * eps_s_p**param.p.b_s + i_s_p = -sigma_eff_p * pybamm.grad(phi_s_p) + # The `algebraic` dictionary contains differential equations, with the key being + # the main scalar variable of interest in the equation + # multiply by Lx**2 to improve conditioning + self.algebraic[phi_s_n] = param.L_x**2 * (pybamm.div(i_s_n) + a_j_n) + self.algebraic[phi_s_p] = param.L_x**2 * (pybamm.div(i_s_p) + a_j_p) + self.boundary_conditions[phi_s_n] = { + "left": (pybamm.Scalar(0), "Dirichlet"), + "right": (pybamm.Scalar(0), "Neumann"), + } + self.boundary_conditions[phi_s_p] = { + "left": (pybamm.Scalar(0), "Neumann"), + "right": (i_cell / pybamm.boundary_value(-sigma_eff_p, "right"), "Neumann"), + } + # Initial conditions must also be provided for algebraic equations, as an + # initial guess for a root-finding algorithm which calculates consistent initial + # conditions + self.initial_conditions[phi_s_n] = pybamm.Scalar(0) + self.initial_conditions[phi_s_p] = param.ocv_init + + ###################### + # Current in the electrolyte + ###################### + i_e = (param.kappa_e(c_e, T) * tor) * ( + param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) + ) + # multiply by Lx**2 to improve conditioning + self.algebraic[phi_e] = param.L_x**2 * (pybamm.div(i_e) - a_j) + self.boundary_conditions[phi_e] = { + "left": (pybamm.Scalar(0), "Neumann"), + "right": (pybamm.Scalar(0), "Neumann"), + } + self.initial_conditions[phi_e] = -param.n.prim.U_init + + ###################### + # Electrolyte concentration + ###################### + N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e) + self.rhs[c_e] = (1 / eps) * ( + -pybamm.div(N_e) + (1 - param.t_plus(c_e, T)) * a_j / param.F + ) + self.boundary_conditions[c_e] = { + "left": (pybamm.Scalar(0), "Neumann"), + "right": (pybamm.Scalar(0), "Neumann"), + } + self.initial_conditions[c_e] = param.c_e_init + + ###################### + # (Some) variables + ###################### + voltage = pybamm.boundary_value(phi_s_p, "right") + num_cells = pybamm.Parameter( + "Number of cells connected in series to make a battery" + ) + # The `variables` dictionary contains all variables that might be useful for + # visualising the solution of the model + self.variables = { + "Negative particle concentration [mol.m-3]": c_s_n, + "Negative particle surface concentration [mol.m-3]": c_s_surf_n, + "Electrolyte concentration [mol.m-3]": c_e, + "Negative electrolyte concentration [mol.m-3]": c_e_n, + "Separator electrolyte concentration [mol.m-3]": c_e_s, + "Positive electrolyte concentration [mol.m-3]": c_e_p, + "Positive particle concentration [mol.m-3]": c_s_p, + "Positive particle surface concentration [mol.m-3]": c_s_surf_p, + "Current [A]": I, + "Current variable [A]": I, # for compatibility with pybamm.Experiment + "Negative electrode potential [V]": phi_s_n, + "Electrolyte potential [V]": phi_e, + "Negative electrolyte potential [V]": phi_e_n, + "Separator electrolyte potential [V]": phi_e_s, + "Positive electrolyte potential [V]": phi_e_p, + "Positive electrode potential [V]": phi_s_p, + "Voltage [V]": voltage, + "Battery voltage [V]": voltage * num_cells, + "Time [s]": pybamm.t, + "Discharge capacity [A.h]": Q, + } + # Events specify points at which a solution should terminate + self.events += [ + pybamm.Event("Minimum voltage [V]", voltage - param.voltage_low_cut), + pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - voltage), + ] + + @property + def default_parameter_values(self): + return pybamm.ParameterValues("Chayambuka2022") diff --git a/src/pybamm/models/submodels/thermal/surface/lumped.py b/src/pybamm/models/submodels/thermal/surface/lumped.py index dc481947e8..9fe3118e2f 100644 --- a/src/pybamm/models/submodels/thermal/surface/lumped.py +++ b/src/pybamm/models/submodels/thermal/surface/lumped.py @@ -19,7 +19,7 @@ class Lumped(pybamm.BaseSubModel): def __init__(self, param, options=None): super().__init__(param, options=options) - pybamm.citations.register("lin2014lumped") + pybamm.citations.register("Lin2014") def get_fundamental_variables(self): T_surf = pybamm.Variable("Surface temperature [K]") diff --git a/src/pybamm/models/submodels/transport_efficiency/bruggeman.py b/src/pybamm/models/submodels/transport_efficiency/bruggeman.py index ec26d7955d..a960f20e66 100644 --- a/src/pybamm/models/submodels/transport_efficiency/bruggeman.py +++ b/src/pybamm/models/submodels/transport_efficiency/bruggeman.py @@ -7,7 +7,7 @@ class Bruggeman(BaseModel): """Submodel for Bruggeman transport_efficiency, - :footcite:t:`bruggeman1935berechnung` + :footcite:t:`Bruggeman1935` Parameters ---------- @@ -28,7 +28,7 @@ def get_coupled_variables(self, variables): for domain in self.options.whole_cell_domains: Domain = domain.capitalize() eps_k = variables[f"{Domain} porosity"] - pybamm.citations.register("bruggeman1935berechnung") + pybamm.citations.register("Bruggeman1935") b_k = self.param.domain_params[domain.split()[0]].b_e tor_k = eps_k**b_k tor_dict[domain] = tor_k @@ -40,7 +40,7 @@ def get_coupled_variables(self, variables): else: Domain = domain.capitalize() phi_k = 1 - variables[f"{Domain} porosity"] - pybamm.citations.register("bruggeman1935berechnung") + pybamm.citations.register("Bruggeman1935") b_k = self.param.domain_params[domain.split()[0]].b_s tor_k = phi_k**b_k tor_dict[domain] = tor_k diff --git a/src/pybamm/models/submodels/transport_efficiency/cation_exchange_membrane.py b/src/pybamm/models/submodels/transport_efficiency/cation_exchange_membrane.py index 3ffb57e7de..b9165cf255 100644 --- a/src/pybamm/models/submodels/transport_efficiency/cation_exchange_membrane.py +++ b/src/pybamm/models/submodels/transport_efficiency/cation_exchange_membrane.py @@ -7,7 +7,7 @@ class CationExchangeMembrane(BaseModel): """Submodel for Cation Exchange Membrane transport_efficiency, - :footcite:t:`bruggeman1935berechnung`, :footcite:t:`shen2007critical` + :footcite:t:`Bruggeman1935`, :footcite:t:`Shen2007` Parameters ---------- @@ -23,8 +23,8 @@ def __init__(self, param, component, options=None): super().__init__(param, component, options=options) def get_coupled_variables(self, variables): - pybamm.citations.register("shen2007critical") - pybamm.citations.register("mackie1955diffusion") + pybamm.citations.register("Shen2007") + pybamm.citations.register("Mackie1955") if self.component == "Electrolyte": tor_dict = {} for domain in self.options.whole_cell_domains: diff --git a/src/pybamm/models/submodels/transport_efficiency/heterogeneous_catalyst.py b/src/pybamm/models/submodels/transport_efficiency/heterogeneous_catalyst.py index 7ec8bc3580..f60f71765c 100644 --- a/src/pybamm/models/submodels/transport_efficiency/heterogeneous_catalyst.py +++ b/src/pybamm/models/submodels/transport_efficiency/heterogeneous_catalyst.py @@ -7,7 +7,7 @@ class HeterogeneousCatalyst(BaseModel): """Submodel for Heterogeneous Catalyst transport_efficiency - :footcite:t:`beeckman1990mathematical`, :footcite:t:`shen2007critical` + :footcite:t:`Beeckman1990`, :footcite:t:`Shen2007` Parameters ---------- @@ -23,8 +23,8 @@ def __init__(self, param, component, options=None): super().__init__(param, component, options=options) def get_coupled_variables(self, variables): - pybamm.citations.register("shen2007critical") - pybamm.citations.register("beeckman1990mathematical") + pybamm.citations.register("Shen2007") + pybamm.citations.register("Beeckman1990") if self.component == "Electrolyte": tor_dict = {} for domain in self.options.whole_cell_domains: diff --git a/src/pybamm/models/submodels/transport_efficiency/hyperbola_of_revolution.py b/src/pybamm/models/submodels/transport_efficiency/hyperbola_of_revolution.py index 306c66b774..fe7e8dfb1d 100644 --- a/src/pybamm/models/submodels/transport_efficiency/hyperbola_of_revolution.py +++ b/src/pybamm/models/submodels/transport_efficiency/hyperbola_of_revolution.py @@ -7,7 +7,7 @@ class HyperbolaOfRevolution(BaseModel): """Submodel for Hyperbola of revolution transport_efficiency - :footcite:t:`petersen1958diffusion`, :footcite:t:`shen2007critical` + :footcite:t:`Petersen1958`, :footcite:t:`Shen2007` Parameters ---------- @@ -23,8 +23,8 @@ def __init__(self, param, component, options=None): super().__init__(param, component, options=options) def get_coupled_variables(self, variables): - pybamm.citations.register("shen2007critical") - pybamm.citations.register("petersen1958diffusion") + pybamm.citations.register("Shen2007") + pybamm.citations.register("Petersen1958") if self.component == "Electrolyte": tor_dict = {} for domain in self.options.whole_cell_domains: diff --git a/src/pybamm/models/submodels/transport_efficiency/ordered_packing.py b/src/pybamm/models/submodels/transport_efficiency/ordered_packing.py index 13b3a3515e..4b9e9b5dc5 100644 --- a/src/pybamm/models/submodels/transport_efficiency/ordered_packing.py +++ b/src/pybamm/models/submodels/transport_efficiency/ordered_packing.py @@ -7,7 +7,7 @@ class OrderedPacking(BaseModel): """Submodel for Ordered Packing transport_efficiency - :footcite:t:`akanni1987effective`, :footcite:t:`shen2007critical` + :footcite:t:`Akanni1987`, :footcite:t:`Shen2007` Parameters ---------- @@ -23,8 +23,8 @@ def __init__(self, param, component, options=None): super().__init__(param, component, options=options) def get_coupled_variables(self, variables): - pybamm.citations.register("shen2007critical") - pybamm.citations.register("akanni1987effective") + pybamm.citations.register("Shen2007") + pybamm.citations.register("Akanni1987") if self.component == "Electrolyte": tor_dict = {} for domain in self.options.whole_cell_domains: diff --git a/src/pybamm/models/submodels/transport_efficiency/overlapping_spheres.py b/src/pybamm/models/submodels/transport_efficiency/overlapping_spheres.py index 9bbed1fd05..ae2dbc590d 100644 --- a/src/pybamm/models/submodels/transport_efficiency/overlapping_spheres.py +++ b/src/pybamm/models/submodels/transport_efficiency/overlapping_spheres.py @@ -7,7 +7,7 @@ class OverlappingSpheres(BaseModel): """Submodel for Overlapping Spheres transport_efficiency - :footcite:t:`weissberg1963effective`, :footcite:t:`shen2007critical` + :footcite:t:`Weissberg1963`, :footcite:t:`Shen2007` Parameters ---------- @@ -23,8 +23,8 @@ def __init__(self, param, component, options=None): super().__init__(param, component, options=options) def get_coupled_variables(self, variables): - pybamm.citations.register("shen2007critical") - pybamm.citations.register("weissberg1963effective") + pybamm.citations.register("Shen2007") + pybamm.citations.register("Weissberg1963") if self.component == "Electrolyte": tor_dict = {} for domain in self.options.whole_cell_domains: diff --git a/src/pybamm/models/submodels/transport_efficiency/random_overlapping_cylinders.py b/src/pybamm/models/submodels/transport_efficiency/random_overlapping_cylinders.py index da32f2f4fe..b9eb49a54e 100644 --- a/src/pybamm/models/submodels/transport_efficiency/random_overlapping_cylinders.py +++ b/src/pybamm/models/submodels/transport_efficiency/random_overlapping_cylinders.py @@ -7,7 +7,7 @@ class RandomOverlappingCylinders(BaseModel): """Submodel for Random Overlapping Cylinders transport_efficiency, - :footcite:t:`tomadakis1993transport`, :footcite:t:`shen2007critical` + :footcite:t:`Tomadakis1993`, :footcite:t:`Shen2007` Parameters ---------- @@ -23,8 +23,8 @@ def __init__(self, param, component, options=None): super().__init__(param, component, options=options) def get_coupled_variables(self, variables): - pybamm.citations.register("shen2007critical") - pybamm.citations.register("tomadakis1993transport") + pybamm.citations.register("Shen2007") + pybamm.citations.register("Tomadakis1993") if self.component == "Electrolyte": tor_dict = {} for domain in self.options.whole_cell_domains: diff --git a/src/pybamm/parameters/parameter_sets.py b/src/pybamm/parameters/parameter_sets.py index a3ddd0ed2e..22b476f4e0 100644 --- a/src/pybamm/parameters/parameter_sets.py +++ b/src/pybamm/parameters/parameter_sets.py @@ -18,7 +18,7 @@ class ParameterSets(Mapping): >>> import pybamm >>> list(pybamm.parameter_sets) - ['Ai2020', 'Chen2020', ...] + ['Ai2020', 'Chayambuka2022', ...] Get the docstring for a parameter set: @@ -26,7 +26,7 @@ class ParameterSets(Mapping): >>> print(pybamm.parameter_sets.get_docstring("Ai2020")) Parameters for the Enertech cell (Ai2020), from the papers :footcite:t:`Ai2019`, - :footcite:t:`rieger2016new` and references therein. + :footcite:t:`Rieger2016` and references therein. ... See also: :ref:`adding-parameter-sets` diff --git a/src/pybamm/solvers/jax_bdf_solver.py b/src/pybamm/solvers/jax_bdf_solver.py index 3db82ca0da..a07ad8505b 100644 --- a/src/pybamm/solvers/jax_bdf_solver.py +++ b/src/pybamm/solvers/jax_bdf_solver.py @@ -68,7 +68,7 @@ def caller(*args): def _bdf_odeint(fun, mass, rtol, atol, y0, t_eval, *args): """ Implements a Backward Difference formula (BDF) implicit multistep integrator. - The basic algorithm is derived in :footcite:t:`byrne1975polyalgorithm`. This + The basic algorithm is derived in :footcite:t:`Byrne1975`. This particular implementation follows that implemented in the Matlab routine ode15s described in :footcite:t:`shamphine1997matlab` and the SciPy implementation :footcite:t:`Virtanen2020`, which features the NDF formulas for improved @@ -362,7 +362,7 @@ def _select_initial_step(atol, rtol, fun, t0, y0, f0, h0): comparing the predicted state against that using the provided function. Optimal step size based on the selected order is obtained using formula (4.12) - in :footcite:t:`hairer1993solving`. + in :footcite:t:`Hairer1993`. """ scale = atol + jnp.abs(y0) * rtol @@ -926,14 +926,14 @@ def ravel_first_arg_(unravel, y_flat, *args): def jax_bdf_integrate(func, y0, t_eval, *args, rtol=1e-6, atol=1e-6, mass=None): """ Backward Difference formula (BDF) implicit multistep integrator. The basic algorithm - is derived in :footcite:t:`byrne1975polyalgorithm`. This particular implementation - follows the Matlab routine ode15s described in :footcite:t:`shampine1997matlab` - and the SciPy implementation :footcite:t:`Virtanen2020` which features - the NDF formulas for improved stability, with associated differences in the - error constants, and calculates the jacobian at J(t_{n+1}, y^0_{n+1}). This - implementation was based on that implemented in the SciPy library - :footcite:t:`Virtanen2020`, which also mainly follows :footcite:t:`shampine1997matlab` - but uses the more standard jacobian update. + is derived in :footcite:t:`Byrne1975`. This particular implementation + follows that implemented in the Matlab routine ode15s described in + :footcite:t:`Shampine1997` and the SciPy implementation + :footcite:t:`Virtanen2020` which features the NDF formulas for improved stability, + with associated differences in the error constants, and calculates the jacobian at + J(t_{n+1}, y^0_{n+1}). This implementation was based on that implemented in the + SciPy library :footcite:t:`Virtanen2020`, which also mainly follows + :footcite:t:`Shampine1997` but uses the more standard jacobian update. Parameters ---------- diff --git a/src/pybamm/spatial_methods/spectral_volume.py b/src/pybamm/spatial_methods/spectral_volume.py index 11c6dfd6d2..8699045dca 100644 --- a/src/pybamm/spatial_methods/spectral_volume.py +++ b/src/pybamm/spatial_methods/spectral_volume.py @@ -176,7 +176,7 @@ def cv_boundary_reconstruction_matrix(self, domains): def chebyshev_differentiation_matrices(self, noe, dod): """ Chebyshev differentiation matrices, from - :footcite:t:`baltensperger2003spectral`. + :footcite:t:`Baltensperger2003`. Parameters ---------- diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py index abb0169d06..3288db75fe 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py @@ -16,7 +16,16 @@ def test_with_experiment(self): ] ) sim = pybamm.Simulation(model, experiment=experiment) - sim.solve(calc_esoh=False) + sol = sim.solve(calc_esoh=False) + + # Check the solve returned a solution + assert sol is not None + + # Check that the solution contains the expected number of cycles + assert len(sol.cycles) == 3 + + # Check that the solution terminated because it reached final time + assert sol.termination == "final time" class TestBasicSPM(BaseBasicModelTest): diff --git a/tests/integration/test_models/test_full_battery_models/test_sodium_ion/__init__.py b/tests/integration/test_models/test_full_battery_models/test_sodium_ion/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/test_models/test_full_battery_models/test_sodium_ion/test_basic_models.py b/tests/integration/test_models/test_full_battery_models/test_sodium_ion/test_basic_models.py new file mode 100644 index 0000000000..2e0321cb1b --- /dev/null +++ b/tests/integration/test_models/test_full_battery_models/test_sodium_ion/test_basic_models.py @@ -0,0 +1,34 @@ +# +# Test basic model classes +# +import pybamm +import pytest + + +class BaseBasicModelTest: + def test_with_experiment(self): + model = self.model + experiment = pybamm.Experiment( + [ + "Discharge at C/3 until 3.5V", + "Hold at 3.5V for 1 hour", + "Rest for 10 min", + ] + ) + sim = pybamm.Simulation(model, experiment=experiment) + sol = sim.solve(calc_esoh=False) + + # Check the solve returned a solution + assert sol is not None + + # Check that the solution contains the expected number of cycles + assert len(sol.cycles) == 3 + + # Check that the solution terminated because it reached final time + assert sol.termination == "final time" + + +class TestBasicDFN(BaseBasicModelTest): + @pytest.fixture(autouse=True) + def setup(self): + self.model = pybamm.sodium_ion.BasicDFN() diff --git a/tests/unit/test_models/test_full_battery_models/test_sodium_ion/__init__.py b/tests/unit/test_models/test_full_battery_models/test_sodium_ion/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/test_models/test_full_battery_models/test_sodium_ion/test_basic_models.py b/tests/unit/test_models/test_full_battery_models/test_sodium_ion/test_basic_models.py new file mode 100644 index 0000000000..6b085d3a79 --- /dev/null +++ b/tests/unit/test_models/test_full_battery_models/test_sodium_ion/test_basic_models.py @@ -0,0 +1,14 @@ +# +# Tests for the basic sodium-ion models +# +import pybamm + + +class TestBasicModels: + def test_dfn_well_posed(self): + model = pybamm.sodium_ion.BasicDFN() + model.check_well_posedness() + + def test_default_parameters(self): + model = pybamm.sodium_ion.BasicDFN() + assert "Chayambuka2022" in model.default_parameter_values["citations"] diff --git a/tests/unit/test_parameters/test_parameter_sets/test_Chayambuka2022.py b/tests/unit/test_parameters/test_parameter_sets/test_Chayambuka2022.py new file mode 100644 index 0000000000..db2eea0d65 --- /dev/null +++ b/tests/unit/test_parameters/test_parameter_sets/test_Chayambuka2022.py @@ -0,0 +1,41 @@ +# +# Tests for Chayambuka et al (2022) parameter set +# + +import pytest +import pybamm + + +class TestChayambuka2022: + def test_functions(self): + param = pybamm.ParameterValues("Chayambuka2022") + sto = pybamm.Scalar(0.5) + T = pybamm.Scalar(298.15) + c_e = 1000 + c_n_max = 14540 + c_p_max = 15320 + + fun_test = { + # Negative electrode + "Negative particle diffusivity [m2.s-1]": ([sto, T], 1.8761e-15), + "Negative electrode OCP [V]": ([sto], 0.0859), + "Negative electrode exchange-current density [A.m-2]" "": ( + [c_e, sto * c_n_max, c_n_max, T], + 0.0202, + ), + # Positive electrode + "Positive particle diffusivity [m2.s-1]": ([sto, T], 1.8700e-15), + "Positive electrode OCP [V]": ([sto], 4.1482), + "Positive electrode exchange-current density [A.m-2]" "": ( + [c_e, sto * c_p_max, c_p_max, T], + 0.0036, + ), + # Electrolyte + "Electrolyte diffusivity [m2.s-1]": ([c_e, T], 2.5061e-10), + "Electrolyte conductivity [S.m-1]": ([c_e, T], 0.8830), + } + + for name, value in fun_test.items(): + assert param.evaluate(param[name](*value[0])) == pytest.approx( + value[1], abs=0.0001 + ) From 0636e947b0bbd4db5a32a79bda4662b6bedbcf3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:21:32 -0400 Subject: [PATCH 3/8] Build(deps): bump the actions group with 3 updates (#4499) Bumps the actions group with 3 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact), [codecov/codecov-action](https://github.com/codecov/codecov-action) and [github/codeql-action](https://github.com/github/codeql-action). Updates `actions/upload-artifact` from 4.4.0 to 4.4.1 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4.4.0...v4.4.1) Updates `codecov/codecov-action` from 4.5.0 to 4.6.0 - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4.5.0...v4.6.0) Updates `github/codeql-action` from 3.26.10 to 3.26.12 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/e2b3eafc8d227b0241d48be5f425d47c2d750a13...c36620d31ac7c881962c3d9dd939c40ec9434f2b) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/periodic_benchmarks.yml | 2 +- .github/workflows/publish_pypi.yml | 8 ++++---- .github/workflows/run_benchmarks_over_history.yml | 2 +- .github/workflows/run_periodic_tests.yml | 2 +- .github/workflows/scorecard.yml | 4 ++-- .github/workflows/test_on_push.yml | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/periodic_benchmarks.yml b/.github/workflows/periodic_benchmarks.yml index b99e2ab7b0..ef66cec238 100644 --- a/.github/workflows/periodic_benchmarks.yml +++ b/.github/workflows/periodic_benchmarks.yml @@ -51,7 +51,7 @@ jobs: LD_LIBRARY_PATH: $HOME/.local/lib - name: Upload results as artifact - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v4.4.1 with: name: asv_periodic_results path: results diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml index 5f68bd2b45..27e6d1162f 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi.yml @@ -92,7 +92,7 @@ jobs: python -c "import pybamm; print(pybamm.IDAKLUSolver())" python -m pytest -m cibw {project}/tests/unit - name: Upload Windows wheels - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v4.4.1 with: name: wheels_windows path: ./wheelhouse/*.whl @@ -129,7 +129,7 @@ jobs: python -m pytest -m cibw {project}/tests/unit - name: Upload wheels for Linux - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v4.4.1 with: name: wheels_manylinux path: ./wheelhouse/*.whl @@ -261,7 +261,7 @@ jobs: python -m pytest -m cibw {project}/tests/unit - name: Upload wheels for macOS (amd64, arm64) - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v4.4.1 with: name: wheels_${{ matrix.os }} path: ./wheelhouse/*.whl @@ -281,7 +281,7 @@ jobs: run: pipx run build --sdist - name: Upload SDist - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v4.4.1 with: name: sdist path: ./dist/*.tar.gz diff --git a/.github/workflows/run_benchmarks_over_history.yml b/.github/workflows/run_benchmarks_over_history.yml index 71687a8b02..ce8eb72ce0 100644 --- a/.github/workflows/run_benchmarks_over_history.yml +++ b/.github/workflows/run_benchmarks_over_history.yml @@ -46,7 +46,7 @@ jobs: ${{ github.event.inputs.commit_start }}..${{ github.event.inputs.commit_end }} - name: Upload results as artifact - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v4.4.1 with: name: asv_over_history_results path: results diff --git a/.github/workflows/run_periodic_tests.yml b/.github/workflows/run_periodic_tests.yml index 9f10a9c6f7..68a8a1ae8b 100644 --- a/.github/workflows/run_periodic_tests.yml +++ b/.github/workflows/run_periodic_tests.yml @@ -89,7 +89,7 @@ jobs: - name: Upload coverage report if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' - uses: codecov/codecov-action@v4.5.0 + uses: codecov/codecov-action@v4.6.0 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 017919c4e7..beb849e9fc 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -59,7 +59,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1 with: name: SARIF file path: results.sarif @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 + uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 with: sarif_file: results.sarif diff --git a/.github/workflows/test_on_push.yml b/.github/workflows/test_on_push.yml index 9224b7df36..9be0c7b3ea 100644 --- a/.github/workflows/test_on_push.yml +++ b/.github/workflows/test_on_push.yml @@ -123,7 +123,7 @@ jobs: - name: Upload coverage report if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' - uses: codecov/codecov-action@v4.5.0 + uses: codecov/codecov-action@v4.6.0 with: token: ${{ secrets.CODECOV_TOKEN }} From 9e62b66c53f423ceb5bb337d4fd893f71358c8d4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 18:44:16 -0400 Subject: [PATCH 4/8] chore: update pre-commit hooks (#4500) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.6.8 → v0.6.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.8...v0.6.9) - [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 51e4d7c23e..b66b38b4db 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.6.8" + rev: "v0.6.9" hooks: - id: ruff args: [--fix, --show-fixes] @@ -19,7 +19,7 @@ repos: additional_dependencies: [black==23.*] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-added-large-files - id: check-case-conflict From e4eb82a3d84f9bfc392cc5796f42de2e38b214ad Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 9 Oct 2024 15:13:34 +0100 Subject: [PATCH 5/8] feat: add discrete time sum expression tree node (#4501) * feat: add discrete time sum expression tree node #4485 * docs: fix math syntax in docstring * remove prints * test casadi solver as well * coverage * coverage * add to changelog and tidy solution test --- CHANGELOG.md | 1 + src/pybamm/__init__.py | 2 + src/pybamm/expression_tree/__init__.py | 2 +- .../expression_tree/discrete_time_sum.py | 88 +++++++++++++++++++ src/pybamm/solvers/__init__.py | 2 +- src/pybamm/solvers/processed_variable.py | 78 ++++++++++------ .../processed_variable_time_integral.py | 28 ++++++ src/pybamm/solvers/solution.py | 28 +++--- .../test_unary_operators.py | 30 +++++++ .../test_solvers/test_processed_variable.py | 61 +++++++++++++ tests/unit/test_solvers/test_solution.py | 40 +++++++++ 11 files changed, 319 insertions(+), 41 deletions(-) create mode 100644 src/pybamm/expression_tree/discrete_time_sum.py create mode 100644 src/pybamm/solvers/processed_variable_time_integral.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ffc210a74..fd69942fd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Added phase-dependent particle options to LAM ([#4369](https://github.com/pybamm-team/PyBaMM/pull/4369)) - Added a lithium ion equivalent circuit model with split open circuit voltages for each electrode (`SplitOCVR`). ([#4330](https://github.com/pybamm-team/PyBaMM/pull/4330)) +- Added the `pybamm.DiscreteTimeSum` expression node to sum an expression over a sequence of data times, and accompanying `pybamm.DiscreteTimeData` class to store the data times and values ([#4501](https://github.com/pybamm-team/PyBaMM/pull/4501)) ## Optimizations diff --git a/src/pybamm/__init__.py b/src/pybamm/__init__.py index f51ba05d02..2deb30f305 100644 --- a/src/pybamm/__init__.py +++ b/src/pybamm/__init__.py @@ -36,6 +36,7 @@ from .expression_tree.broadcasts import * from .expression_tree.functions import * from .expression_tree.interpolant import Interpolant +from .expression_tree.discrete_time_sum import * from .expression_tree.input_parameter import InputParameter from .expression_tree.parameter import Parameter, FunctionParameter from .expression_tree.scalar import Scalar @@ -158,6 +159,7 @@ # Solver classes from .solvers.solution import Solution, EmptySolution, make_cycle_solution +from .solvers.processed_variable_time_integral import ProcessedVariableTimeIntegral from .solvers.processed_variable import ProcessedVariable, process_variable from .solvers.processed_variable_computed import ProcessedVariableComputed from .solvers.base_solver import BaseSolver diff --git a/src/pybamm/expression_tree/__init__.py b/src/pybamm/expression_tree/__init__.py index 0b06746e61..7ac80e5353 100644 --- a/src/pybamm/expression_tree/__init__.py +++ b/src/pybamm/expression_tree/__init__.py @@ -2,4 +2,4 @@ 'concatenations', 'exceptions', 'functions', 'independent_variable', 'input_parameter', 'interpolant', 'matrix', 'operations', 'parameter', 'printing', 'scalar', 'state_vector', 'symbol', - 'unary_operators', 'variable', 'vector'] + 'unary_operators', 'variable', 'vector', 'discrete_time_sum' ] diff --git a/src/pybamm/expression_tree/discrete_time_sum.py b/src/pybamm/expression_tree/discrete_time_sum.py new file mode 100644 index 0000000000..41cd14960d --- /dev/null +++ b/src/pybamm/expression_tree/discrete_time_sum.py @@ -0,0 +1,88 @@ +import pybamm +import numpy as np + + +class DiscreteTimeData(pybamm.Interpolant): + """ + A class for representing data that is only defined at discrete points in time. + This is implemented as a 1D interpolant with the time points as the nodes. + + Parameters + ---------- + + time_points : :class:`numpy.ndarray` + The time points at which the data is defined + data : :class:`numpy.ndarray` + The data to be interpolated + name : str + The name of the data + + """ + + def __init__(self, time_points: np.ndarray, data: np.ndarray, name: str): + super().__init__(time_points, data, pybamm.t, name) + + def create_copy(self, new_children=None, perform_simplifications=True): + """See :meth:`pybamm.Symbol.new_copy()`.""" + return pybamm.DiscreteTimeData(self.x[0], self.y, self.name) + + +class DiscreteTimeSum(pybamm.UnaryOperator): + """ + A node in the expression tree representing a discrete time sum operator. + + .. math:: + \\sum_{i=0}^{N} f(y(t_i), t_i) + + where f is the expression given by the child, and the sum is over the discrete + time points t_i. The set of time points is given by the :class:`pybamm.DiscreteTimeData` node, + which must be somewhere in the expression tree given by the child. If the child + does not contain a :class:`pybamm.DiscreteTimeData` node, then an error will be raised when + the node is created. If the child contains multiple :class:`pybamm.DiscreteTimeData` nodes, + an error will be raised when the node is created. + + + Parameters + ---------- + child: :class:`pybamm.Symbol` + The symbol to be summed + + Attributes + ---------- + data: :class:`pybamm.DiscreteTimeData` + The discrete time data node in the child + + Raises + ------ + :class:`pybamm.ModelError` + If the child does not contain a :class:`pybamm.DiscreteTimeData` node, or if the child + contains multiple :class:`pybamm.DiscreteTimeData` nodes. + """ + + def __init__(self, child: pybamm.Symbol): + self.data = None + for node in child.pre_order(): + if isinstance(node, DiscreteTimeData): + # Check that there is exactly one DiscreteTimeData node in the child + if self.data is not None: + raise pybamm.ModelError( + "DiscreteTimeSum can only have one DiscreteTimeData node in the child" + ) + self.data = node + if self.data is None: + raise pybamm.ModelError( + "DiscreteTimeSum must contain a DiscreteTimeData node" + ) + super().__init__("discrete time sum", child) + + @property + def sum_values(self): + return self.data.y + + @property + def sum_times(self): + return self.data.x[0] + + def _unary_evaluate(self, child): + # return result of evaluating the child, we'll only implement the sum once the model is solved (in pybamm.ProcessedVariable) + return child diff --git a/src/pybamm/solvers/__init__.py b/src/pybamm/solvers/__init__.py index fc8be7e2f8..e9d9d306f4 100644 --- a/src/pybamm/solvers/__init__.py +++ b/src/pybamm/solvers/__init__.py @@ -2,4 +2,4 @@ 'casadi_algebraic_solver', 'casadi_solver', 'dummy_solver', 'idaklu_jax', 'idaklu_solver', 'jax_bdf_solver', 'jax_solver', 'lrudict', 'processed_variable', 'processed_variable_computed', - 'scipy_solver', 'solution'] + 'scipy_solver', 'solution', 'processed_variable_time_integral'] diff --git a/src/pybamm/solvers/processed_variable.py b/src/pybamm/solvers/processed_variable.py index 5cf928ca7f..12cf140b38 100644 --- a/src/pybamm/solvers/processed_variable.py +++ b/src/pybamm/solvers/processed_variable.py @@ -1,6 +1,7 @@ # # Processed Variable class # +from typing import Optional import casadi import numpy as np import pybamm @@ -29,6 +30,8 @@ class ProcessedVariable: `base_Variable.evaluate` (but more efficiently). solution : :class:`pybamm.Solution` The solution object to be used to create the processed variables + time_integral : :class:`pybamm.ProcessedVariableTimeIntegral`, optional + Not none if the variable is to be time-integrated (default is None) """ def __init__( @@ -36,7 +39,7 @@ def __init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=None, + time_integral: Optional[pybamm.ProcessedVariableTimeIntegral] = None, ): self.base_variables = base_variables self.base_variables_casadi = base_variables_casadi @@ -50,7 +53,7 @@ def __init__( self.mesh = base_variables[0].mesh self.domain = base_variables[0].domain self.domains = base_variables[0].domains - self.cumtrapz_ic = cumtrapz_ic + self.time_integral = time_integral # Process spatial variables geometry = solution.all_models[0].geometry @@ -271,18 +274,21 @@ def __call__( self._coords_raw, ) - processed_entries = self._xr_interpolate( - entries_for_interp, - coords, - observe_raw, - t, - x, - r, - y, - z, - R, - fill_value, - ) + if self.time_integral is None: + processed_entries = self._xr_interpolate( + entries_for_interp, + coords, + observe_raw, + t, + x, + r, + y, + z, + R, + fill_value, + ) + else: + processed_entries = entries_for_interp else: processed_entries = entries @@ -343,6 +349,16 @@ def _check_observe_raw(self, t): t_observe (np.ndarray): time points to observe observe_raw (bool): True if observing the raw data """ + # if this is a time integral variable, t must be None and we observe either the + # data times (for a discrete sum) or the solution times (for a continuous sum) + if self.time_integral is not None: + if self.time_integral.method == "discrete": + # discrete sum should be observed at the discrete times + t = self.time_integral.discrete_times + else: + # assume we can do a sufficiently accurate trapezoidal integration at t_pts + t = self.t_pts + observe_raw = (t is None) or ( np.asarray(t).size == len(self.t_pts) and np.all(t == self.t_pts) ) @@ -483,14 +499,14 @@ def __init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=None, + time_integral: Optional[pybamm.ProcessedVariableTimeIntegral] = None, ): self.dimensions = 0 super().__init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=cumtrapz_ic, + time_integral=time_integral, ) def _observe_raw_python(self): @@ -510,13 +526,19 @@ def _observe_raw_python(self): idx += 1 return entries - def _observe_postfix(self, entries, _): - if self.cumtrapz_ic is None: + def _observe_postfix(self, entries, t): + if self.time_integral is None: return entries - - return cumulative_trapezoid( - entries, self.t_pts, initial=float(self.cumtrapz_ic) - ) + if self.time_integral.method == "discrete": + return np.sum(entries, axis=0, initial=self.time_integral.initial_condition) + elif self.time_integral.method == "continuous": + return cumulative_trapezoid( + entries, self.t_pts, initial=float(self.time_integral.initial_condition) + ) + else: + raise ValueError( + "time_integral method must be 'discrete' or 'continuous'" + ) # pragma: no cover def _interp_setup(self, entries, t): # save attributes for interpolation @@ -556,14 +578,14 @@ def __init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=None, + time_integral: Optional[pybamm.ProcessedVariableTimeIntegral] = None, ): self.dimensions = 1 super().__init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=cumtrapz_ic, + time_integral=time_integral, ) def _observe_raw_python(self): @@ -653,14 +675,14 @@ def __init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=None, + time_integral: Optional[pybamm.ProcessedVariableTimeIntegral] = None, ): self.dimensions = 2 super().__init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=cumtrapz_ic, + time_integral=time_integral, ) first_dim_nodes = self.mesh.nodes first_dim_edges = self.mesh.edges @@ -819,14 +841,14 @@ def __init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=None, + time_integral: Optional[pybamm.ProcessedVariableTimeIntegral] = None, ): self.dimensions = 2 super(ProcessedVariable2D, self).__init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=cumtrapz_ic, + time_integral=time_integral, ) y_sol = self.mesh.edges["y"] z_sol = self.mesh.edges["z"] diff --git a/src/pybamm/solvers/processed_variable_time_integral.py b/src/pybamm/solvers/processed_variable_time_integral.py new file mode 100644 index 0000000000..4fcdfb56ba --- /dev/null +++ b/src/pybamm/solvers/processed_variable_time_integral.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +from typing import Literal, Optional, Union +import numpy as np +import pybamm + + +@dataclass +class ProcessedVariableTimeIntegral: + method: Literal["discrete", "continuous"] + initial_condition: np.ndarray + discrete_times: Optional[np.ndarray] + + @staticmethod + def from_pybamm_var( + var: Union[pybamm.DiscreteTimeSum, pybamm.ExplicitTimeIntegral], + ) -> "ProcessedVariableTimeIntegral": + if isinstance(var, pybamm.DiscreteTimeSum): + return ProcessedVariableTimeIntegral( + method="discrete", initial_condition=0.0, discrete_times=var.sum_times + ) + elif isinstance(var, pybamm.ExplicitTimeIntegral): + return ProcessedVariableTimeIntegral( + method="continuous", + initial_condition=var.initial_condition.evaluate(), + discrete_times=None, + ) + else: + raise ValueError("Unsupported variable type") # pragma: no cover diff --git a/src/pybamm/solvers/solution.py b/src/pybamm/solvers/solution.py index c884e79e34..1aa540ab3c 100644 --- a/src/pybamm/solvers/solution.py +++ b/src/pybamm/solvers/solution.py @@ -571,7 +571,7 @@ def update(self, variables): self._update_variable(variable) def _update_variable(self, variable): - cumtrapz_ic = None + time_integral = None pybamm.logger.debug(f"Post-processing {variable}") vars_pybamm = [ model.variables_and_events[variable] for model in self.all_models @@ -591,16 +591,22 @@ def _update_variable(self, variable): "solve. Please re-run the solve with `output_variables` set to " "include this variable." ) - elif isinstance(var_pybamm, pybamm.ExplicitTimeIntegral): - cumtrapz_ic = var_pybamm.initial_condition - cumtrapz_ic = cumtrapz_ic.evaluate() - var_pybamm = var_pybamm.child - var_casadi = self.process_casadi_var( - var_pybamm, - inputs, - ys.shape, + elif isinstance( + var_pybamm, (pybamm.ExplicitTimeIntegral, pybamm.DiscreteTimeSum) + ): + time_integral = pybamm.ProcessedVariableTimeIntegral.from_pybamm_var( + var_pybamm ) - model._variables_casadi[variable] = var_casadi + var_pybamm = var_pybamm.child + if variable in model._variables_casadi: + var_casadi = model._variables_casadi[variable] + else: + var_casadi = self.process_casadi_var( + var_pybamm, + inputs, + ys.shape, + ) + model._variables_casadi[variable] = var_casadi vars_pybamm[i] = var_pybamm elif variable in model._variables_casadi: var_casadi = model._variables_casadi[variable] @@ -613,7 +619,7 @@ def _update_variable(self, variable): model._variables_casadi[variable] = var_casadi vars_casadi.append(var_casadi) var = pybamm.process_variable( - vars_pybamm, vars_casadi, self, cumtrapz_ic=cumtrapz_ic + vars_pybamm, vars_casadi, self, time_integral=time_integral ) self._variables[variable] = var diff --git a/tests/unit/test_expression_tree/test_unary_operators.py b/tests/unit/test_expression_tree/test_unary_operators.py index 812cbd8f6b..0dbafa38c5 100644 --- a/tests/unit/test_expression_tree/test_unary_operators.py +++ b/tests/unit/test_expression_tree/test_unary_operators.py @@ -724,6 +724,36 @@ def test_explicit_time_integral(self): assert expr.create_copy() == expr assert not expr.is_constant() + def test_discrete_time_sum(self): + times = np.array([1, 2, 3, 4, 5]) + values = np.array([2, 2, 3, 3, 1]) + data = pybamm.DiscreteTimeData(times, values, "test") + assert data.name == "test" + np.testing.assert_array_equal(data.x[0], times) + np.testing.assert_array_equal(data.y, values) + + y = pybamm.StateVector(slice(0, 1)) + + # check that raises error if data is not present + with pytest.raises(pybamm.ModelError, match="must contain a DiscreteTimeData"): + pybamm.DiscreteTimeSum(2 * y) + + # check that raises error if two data are present + data2 = pybamm.DiscreteTimeData(values, times, "test2") + with pytest.raises(pybamm.ModelError, match="only have one DiscreteTimeData"): + pybamm.DiscreteTimeSum(data + data2) + + sum = pybamm.DiscreteTimeSum(2 * data - y) + np.testing.assert_array_equal(sum.sum_times, times) + np.testing.assert_array_equal(sum.sum_values, values) + y = np.array([1]) + + # evaluate should return the values to sum up + for i in range(len(times)): + eval = sum.evaluate(y=y, t=times[i]) + expect = 2 * values[i] - y + np.testing.assert_array_equal(eval[0], expect) + def test_to_from_json(self, mocker): # UnaryOperator a = pybamm.Symbol("a", domain=["test"]) diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index 04de88963d..b3ab850607 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -160,6 +160,67 @@ def test_processed_variable_0D(self, hermite_interp): processed_var._observe_raw_cpp(), processed_var._observe_raw_python() ) + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_0D_discrete_data(self, hermite_interp): + y = pybamm.StateVector(slice(0, 1)) + t_sol = np.linspace(0, 1) + y_sol = np.array([np.linspace(0, 5)]) + data_const = 3.6 + if hermite_interp: + yp_sol = 5 * np.ones_like(y_sol) + else: + yp_sol = None + + # hermite interpolation can do order 2 interpolation, otherwise make sure result is linear + order = 2 if hermite_interp else 1 + + # data is same timepoints as solution + data_t = t_sol + data_v = -data_const * data_t + data = pybamm.DiscreteTimeData(data_t, data_v, "test_data") + var = (y - data) ** order + expected_entries = (y_sol - data_v) ** order + var.mesh = None + model = pybamm.BaseModel() + var_casadi = to_casadi(var, y_sol) + processed_var = pybamm.process_variable( + [var], + [var_casadi], + self._sol_default(t_sol, y_sol, yp_sol, model), + ) + np.testing.assert_array_equal(processed_var.entries, expected_entries.flatten()) + np.testing.assert_array_equal(processed_var(data_t), expected_entries.flatten()) + + # data is different timepoints as solution + data_t = np.linspace(0, 1, 7) + data_v = -data_const * data_t + y_sol_interp = (np.interp(data_t, t_sol, y_sol[0]),) + data_v_interp = np.interp(t_sol, data_t, data_v) + data = pybamm.DiscreteTimeData(data_t, data_v, "test_data") + + # check data interp + np.testing.assert_array_almost_equal( + data.evaluate(t=t_sol).flatten(), data_v_interp + ) + + var = (y - data) ** order + expected = (y_sol_interp - data_v) ** order + expected_entries = (y_sol - data_v_interp) ** order + var.mesh = None + model = pybamm.BaseModel() + var_casadi = to_casadi(var, y_sol) + processed_var = pybamm.process_variable( + [var], + [var_casadi], + self._sol_default(t_sol, y_sol, yp_sol, model), + ) + np.testing.assert_array_almost_equal( + processed_var.entries, expected_entries.flatten(), decimal=10 + ) + np.testing.assert_array_almost_equal( + processed_var(t=data_t), expected.flatten(), decimal=10 + ) + @pytest.mark.parametrize("hermite_interp", _hermite_args) def test_processed_variable_0D_no_sensitivity(self, hermite_interp): # without space diff --git a/tests/unit/test_solvers/test_solution.py b/tests/unit/test_solvers/test_solution.py index 4ac9312531..2fb25f79a2 100644 --- a/tests/unit/test_solvers/test_solution.py +++ b/tests/unit/test_solvers/test_solution.py @@ -420,3 +420,43 @@ def test_solution_evals_with_inputs(self): sim.solve(t_eval=np.linspace(0, 10, 10), inputs=inputs) time = sim.solution["Time [h]"](sim.solution.t) assert len(time) == 10 + + _solver_classes = [pybamm.CasadiSolver] + if pybamm.has_idaklu(): + _solver_classes.append(pybamm.IDAKLUSolver) + + @pytest.mark.parametrize("solver_class", _solver_classes) + def test_discrete_data_sum(self, solver_class): + model = pybamm.BaseModel(name="test_model") + c = pybamm.Variable("c") + model.rhs = {c: -c} + model.initial_conditions = {c: 1} + model.variables["c"] = c + + data_times = np.linspace(0, 1, 10) + if solver_class == pybamm.IDAKLUSolver: + t_eval = [data_times[0], data_times[-1]] + t_interp = data_times + else: + t_eval = data_times + t_interp = None + solver = solver_class() + data_values = solver.solve(model, t_eval=t_eval, t_interp=t_interp)["c"].entries + + data = pybamm.DiscreteTimeData(data_times, data_values, "test_data") + data_comparison = pybamm.DiscreteTimeSum((c - data) ** 2) + + model = pybamm.BaseModel(name="test_model2") + a = pybamm.InputParameter("a") + model.rhs = {c: -a * c} + model.initial_conditions = {c: 1} + model.variables["data_comparison"] = data_comparison + + solver = solver_class() + for a in [0.5, 1.0, 2.0]: + sol = solver.solve(model, t_eval=t_eval, inputs={"a": a}) + y_sol = np.exp(-a * data_times) + expected = np.sum((y_sol - data_values) ** 2) + np.testing.assert_array_almost_equal( + sol["data_comparison"](), expected, decimal=2 + ) From 0efe5f6a4328d4bb35c440104139eed1595c3dfe Mon Sep 17 00:00:00 2001 From: Medha Bhardwaj <143182673+medha-14@users.noreply.github.com> Date: Thu, 10 Oct 2024 22:15:47 +0530 Subject: [PATCH 6/8] Removes `param = self.param` to use `self.param` directly (#4494) * 3 files done * all occurances changed * added changelog and fixed spectral_volume issue * fixed docs --------- Co-authored-by: Eric G. Kratz --- CHANGELOG.md | 1 + .../lead_acid/basic_full.py | 87 +++++++------- .../lithium_ion/base_lithium_ion_model.py | 21 ++-- .../lithium_ion/basic_dfn.py | 73 ++++++------ .../lithium_ion/basic_dfn_composite.py | 109 +++++++++--------- .../lithium_ion/basic_dfn_half_cell.py | 73 ++++++------ .../lithium_ion/basic_spm.py | 51 ++++---- .../lithium_ion/electrode_soh.py | 46 ++++---- .../active_material/base_active_material.py | 5 +- .../through_cell/explicit_convection.py | 27 +++-- .../through_cell/full_convection.py | 5 +- .../convection/transverse/full_convection.py | 8 +- .../transverse/uniform_convection.py | 7 +- .../effective_resistance_current_collector.py | 52 ++++----- .../current_collector/potential_pair.py | 24 ++-- .../submodels/electrode/base_electrode.py | 3 +- .../submodels/electrode/ohm/composite_ohm.py | 7 +- .../submodels/electrode/ohm/leading_ohm.py | 7 +- .../base_electrolyte_conductivity.py | 7 +- .../composite_conductivity.py | 27 +++-- .../full_conductivity.py | 5 +- .../integrated_conductivity.py | 24 ++-- .../leading_order_conductivity.py | 7 +- .../full_surface_form_conductivity.py | 16 +-- .../electrolyte_diffusion/full_diffusion.py | 10 +- .../leading_order_diffusion.py | 21 ++-- .../explicit_control_external_circuit.py | 12 +- .../function_control_external_circuit.py | 7 +- .../submodels/interface/base_interface.py | 7 +- .../interface/kinetics/diffusion_limited.py | 5 +- .../inverse_kinetics/inverse_butler_volmer.py | 5 +- .../submodels/interface/sei/sei_growth.py | 18 +-- .../oxygen_diffusion/full_oxygen_diffusion.py | 10 +- .../leading_oxygen_diffusion.py | 17 +-- .../submodels/particle/base_particle.py | 3 +- .../submodels/particle/fickian_diffusion.py | 3 +- .../submodels/particle/msmr_diffusion.py | 3 +- .../particle/x_averaged_polynomial_profile.py | 12 +- .../porosity/reaction_driven_porosity_ode.py | 6 +- .../models/submodels/thermal/base_thermal.py | 38 +++--- .../pouch_cell_1D_current_collectors.py | 25 ++-- 41 files changed, 441 insertions(+), 453 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd69942fd0..84f01d3797 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ ## Breaking changes +- Removed all instances of `param = self.param` and now directly access `self.param` across the codebase. This change simplifies parameter references and enhances readability. ([#4484](https://github.com/pybamm-team/PyBaMM/pull/4494)) - Removed the deprecation warning for the chemistry argument in ParameterValues ([#4466](https://github.com/pybamm-team/PyBaMM/pull/4466)) - The parameters "... electrode OCP entropic change [V.K-1]" and "... electrode volume change" are now expected to be functions of stoichiometry only instead of functions of both stoichiometry and maximum concentration ([#4427](https://github.com/pybamm-team/PyBaMM/pull/4427)) diff --git a/src/pybamm/models/full_battery_models/lead_acid/basic_full.py b/src/pybamm/models/full_battery_models/lead_acid/basic_full.py index 8caac98066..a67501fc72 100644 --- a/src/pybamm/models/full_battery_models/lead_acid/basic_full.py +++ b/src/pybamm/models/full_battery_models/lead_acid/basic_full.py @@ -26,7 +26,6 @@ def __init__(self, name="Basic full model"): # `param` is a class containing all the relevant parameters and functions for # this model. These are purely symbolic at this stage, and will be set by the # `ParameterValues` class when the model is processed. - param = self.param ###################### # Variables @@ -37,17 +36,17 @@ def __init__(self, name="Basic full model"): c_e_n = pybamm.Variable( "Negative electrolyte concentration [mol.m-3]", domain="negative electrode", - scale=param.c_e_init, + scale=self.param.c_e_init, ) c_e_s = pybamm.Variable( "Separator electrolyte concentration [mol.m-3]", domain="separator", - scale=param.c_e_init, + scale=self.param.c_e_init, ) c_e_p = pybamm.Variable( "Positive electrolyte concentration [mol.m-3]", domain="positive electrode", - scale=param.c_e_init, + scale=self.param.c_e_init, ) # Concatenations combine several variables into a single variable, to simplify # implementing equations that hold over several domains @@ -57,17 +56,17 @@ def __init__(self, name="Basic full model"): phi_e_n = pybamm.Variable( "Negative electrolyte potential [V]", domain="negative electrode", - reference=-param.n.prim.U_init, + reference=-self.param.n.prim.U_init, ) phi_e_s = pybamm.Variable( "Separator electrolyte potential [V]", domain="separator", - reference=-param.n.prim.U_init, + reference=-self.param.n.prim.U_init, ) phi_e_p = pybamm.Variable( "Positive electrolyte potential [V]", domain="positive electrode", - reference=-param.n.prim.U_init, + reference=-self.param.n.prim.U_init, ) phi_e = pybamm.concatenation(phi_e_n, phi_e_s, phi_e_p) @@ -78,7 +77,7 @@ def __init__(self, name="Basic full model"): phi_s_p = pybamm.Variable( "Positive electrode potential [V]", domain="positive electrode", - reference=param.ocv_init, + reference=self.param.ocv_init, ) # Porosity @@ -92,29 +91,29 @@ def __init__(self, name="Basic full model"): eps = pybamm.concatenation(eps_n, eps_s, eps_p) # Constant temperature - T = param.T_init + T = self.param.T_init ###################### # Other set-up ###################### # Current density - i_cell = param.current_density_with_time + i_cell = self.param.current_density_with_time # transport_efficiency tor = pybamm.concatenation( - eps_n**param.n.b_e, eps_s**param.s.b_e, eps_p**param.p.b_e + eps_n**self.param.n.b_e, eps_s**self.param.s.b_e, eps_p**self.param.p.b_e ) # Interfacial reactions - F_RT = param.F / (param.R * T) - Feta_RT_n = F_RT * (phi_s_n - phi_e_n - param.n.prim.U(c_e_n, T)) - j0_n = param.n.prim.j0(c_e_n, T) - j_n = 2 * j0_n * pybamm.sinh(param.n.prim.ne / 2 * Feta_RT_n) + F_RT = self.param.F / (self.param.R * T) + Feta_RT_n = F_RT * (phi_s_n - phi_e_n - self.param.n.prim.U(c_e_n, T)) + j0_n = self.param.n.prim.j0(c_e_n, T) + j_n = 2 * j0_n * pybamm.sinh(self.param.n.prim.ne / 2 * Feta_RT_n) j_s = pybamm.PrimaryBroadcast(0, "separator") - Feta_RT_p = F_RT * (phi_s_p - phi_e_p - param.p.prim.U(c_e_p, T)) - j0_p = param.p.prim.j0(c_e_p, T) - j_p = 2 * j0_p * pybamm.sinh(param.p.prim.ne / 2 * (Feta_RT_p)) + Feta_RT_p = F_RT * (phi_s_p - phi_e_p - self.param.p.prim.U(c_e_p, T)) + j0_p = self.param.p.prim.j0(c_e_p, T) + j_p = 2 * j0_p * pybamm.sinh(self.param.p.prim.ne / 2 * (Feta_RT_p)) a_n = pybamm.Parameter("Negative electrode surface area to volume ratio [m-1]") a_p = pybamm.Parameter("Positive electrode surface area to volume ratio [m-1]") @@ -125,7 +124,7 @@ def __init__(self, name="Basic full model"): ###################### # State of Charge ###################### - I = param.current_with_time + I = self.param.current_with_time # The `rhs` dictionary contains differential equations, with the key being the # variable in the d/dt self.rhs[Q] = I / 3600 @@ -135,28 +134,32 @@ def __init__(self, name="Basic full model"): ###################### # Current in the electrolyte ###################### - i_e = (param.kappa_e(c_e, T) * tor) * ( - param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) + i_e = (self.param.kappa_e(c_e, T) * tor) * ( + self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) ) # multiply by Lx**2 to improve conditioning - self.algebraic[phi_e] = (pybamm.div(i_e) - a_j) * param.L_x**2 + self.algebraic[phi_e] = (pybamm.div(i_e) - a_j) * self.param.L_x**2 self.boundary_conditions[phi_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[phi_e] = -param.n.prim.U_init + self.initial_conditions[phi_e] = -self.param.n.prim.U_init ###################### # Current in the solid ###################### - i_s_n = -param.n.sigma(T) * (1 - eps_n) ** param.n.b_s * pybamm.grad(phi_s_n) - sigma_eff_p = param.p.sigma(T) * (1 - eps_p) ** param.p.b_s + i_s_n = ( + -self.param.n.sigma(T) + * (1 - eps_n) ** self.param.n.b_s + * pybamm.grad(phi_s_n) + ) + sigma_eff_p = self.param.p.sigma(T) * (1 - eps_p) ** self.param.p.b_s i_s_p = -sigma_eff_p * pybamm.grad(phi_s_p) # The `algebraic` dictionary contains differential equations, with the key being # the main scalar variable of interest in the equation # multiply by Lx**2 to improve conditioning - self.algebraic[phi_s_n] = (pybamm.div(i_s_n) + a_j_n) * param.L_x**2 - self.algebraic[phi_s_p] = (pybamm.div(i_s_p) + a_j_p) * param.L_x**2 + self.algebraic[phi_s_n] = (pybamm.div(i_s_n) + a_j_n) * self.param.L_x**2 + self.algebraic[phi_s_p] = (pybamm.div(i_s_p) + a_j_p) * self.param.L_x**2 self.boundary_conditions[phi_s_n] = { "left": (pybamm.Scalar(0), "Dirichlet"), "right": (pybamm.Scalar(0), "Neumann"), @@ -169,19 +172,19 @@ def __init__(self, name="Basic full model"): # initial guess for a root-finding algorithm which calculates consistent initial # conditions self.initial_conditions[phi_s_n] = pybamm.Scalar(0) - self.initial_conditions[phi_s_p] = param.ocv_init + self.initial_conditions[phi_s_p] = self.param.ocv_init ###################### # Porosity ###################### DeltaVsurf = pybamm.concatenation( - pybamm.PrimaryBroadcast(param.n.DeltaVsurf, "negative electrode"), + pybamm.PrimaryBroadcast(self.param.n.DeltaVsurf, "negative electrode"), pybamm.PrimaryBroadcast(0, "separator"), - pybamm.PrimaryBroadcast(param.p.DeltaVsurf, "positive electrode"), + pybamm.PrimaryBroadcast(self.param.p.DeltaVsurf, "positive electrode"), ) - deps_dt = DeltaVsurf * a_j / param.F + deps_dt = DeltaVsurf * a_j / self.param.F self.rhs[eps] = deps_dt - self.initial_conditions[eps] = param.epsilon_init + self.initial_conditions[eps] = self.param.epsilon_init self.events.extend( [ pybamm.Event( @@ -203,22 +206,22 @@ def __init__(self, name="Basic full model"): # Electrolyte concentration ###################### N_e = ( - -tor * param.D_e(c_e, T) * pybamm.grad(c_e) - + param.t_plus(c_e, T) * i_e / param.F + -tor * self.param.D_e(c_e, T) * pybamm.grad(c_e) + + self.param.t_plus(c_e, T) * i_e / self.param.F ) s = pybamm.concatenation( - pybamm.PrimaryBroadcast(param.n.prim.s_plus_S, "negative electrode"), + pybamm.PrimaryBroadcast(self.param.n.prim.s_plus_S, "negative electrode"), pybamm.PrimaryBroadcast(0, "separator"), - pybamm.PrimaryBroadcast(param.p.prim.s_plus_S, "positive electrode"), + pybamm.PrimaryBroadcast(self.param.p.prim.s_plus_S, "positive electrode"), ) self.rhs[c_e] = (1 / eps) * ( - -pybamm.div(N_e) + s * a_j / param.F - c_e * deps_dt + -pybamm.div(N_e) + s * a_j / self.param.F - c_e * deps_dt ) self.boundary_conditions[c_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[c_e] = param.c_e_init + self.initial_conditions[c_e] = self.param.c_e_init self.events.append( pybamm.Event( "Zero electrolyte concentration cut-off", pybamm.min(c_e) - 0.002 @@ -242,7 +245,11 @@ def __init__(self, name="Basic full model"): } self.events.extend( [ - pybamm.Event("Minimum voltage [V]", voltage - param.voltage_low_cut), - pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - voltage), + pybamm.Event( + "Minimum voltage [V]", voltage - self.param.voltage_low_cut + ), + pybamm.Event( + "Maximum voltage [V]", self.param.voltage_high_cut - voltage + ), ] ) diff --git a/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py b/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py index b1367f8300..9b801e1130 100644 --- a/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py +++ b/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py @@ -105,7 +105,6 @@ def set_standard_output_variables(self): def set_degradation_variables(self): """Sets variables that quantify degradation (LAM, LLI, etc)""" - param = self.param domains = [d for d in self.options.whole_cell_domains if d != "separator"] for domain in domains: @@ -135,8 +134,8 @@ def set_degradation_variables(self): # LLI is usually defined based only on the percentage lithium lost from # particles - LLI = (1 - n_Li_particles / param.n_Li_particles_init) * 100 - LLI_tot = (1 - n_Li / param.n_Li_init) * 100 + LLI = (1 - n_Li_particles / self.param.n_Li_particles_init) * 100 + LLI_tot = (1 - n_Li / self.param.n_Li_init) * 100 self.variables.update( { @@ -146,15 +145,16 @@ def set_degradation_variables(self): # Total lithium "Total lithium [mol]": n_Li, "Total lithium in particles [mol]": n_Li_particles, - "Total lithium capacity [A.h]": n_Li * param.F / 3600, + "Total lithium capacity [A.h]": n_Li * self.param.F / 3600, "Total lithium capacity in particles [A.h]": n_Li_particles - * param.F + * self.param.F / 3600, # Lithium lost - "Total lithium lost [mol]": param.n_Li_init - n_Li, - "Total lithium lost from particles [mol]": param.n_Li_particles_init + "Total lithium lost [mol]": self.param.n_Li_init - n_Li, + "Total lithium lost from particles [mol]": self.param.n_Li_particles_init - n_Li_particles, - "Total lithium lost from electrolyte [mol]": param.n_Li_e_init - n_Li_e, + "Total lithium lost from electrolyte [mol]": self.param.n_Li_e_init + - n_Li_e, } ) @@ -177,7 +177,7 @@ def set_degradation_variables(self): { "Total lithium lost to side reactions [mol]": n_Li_lost_reactions, "Total capacity lost to side reactions [A.h]": n_Li_lost_reactions - * param.F + * self.param.F / 3600, } ) @@ -502,9 +502,8 @@ def insert_reference_electrode(self, position=None): "electrode manually." ) - param = self.param if position is None: - position = param.n.L + param.s.L / 2 + position = self.param.n.L + self.param.s.L / 2 phi_e_ref = pybamm.EvaluateAt( self.variables["Electrolyte potential [V]"], position diff --git a/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py index 08809b645f..7865b84ff3 100644 --- a/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py +++ b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py @@ -27,7 +27,6 @@ def __init__(self, name="Doyle-Fuller-Newman model"): # `param` is a class containing all the relevant parameters and functions for # this model. These are purely symbolic at this stage, and will be set by the # `ParameterValues` class when the model is processed. - param = self.param ###################### # Variables @@ -90,14 +89,14 @@ def __init__(self, name="Doyle-Fuller-Newman model"): ) # Constant temperature - T = param.T_init + T = self.param.T_init ###################### # Other set-up ###################### # Current density - i_cell = param.current_density_with_time + i_cell = self.param.current_density_with_time # Porosity # Primary broadcasts are used to broadcast scalar quantities across a domain @@ -119,29 +118,29 @@ def __init__(self, name="Doyle-Fuller-Newman model"): # transport_efficiency tor = pybamm.concatenation( - eps_n**param.n.b_e, eps_s**param.s.b_e, eps_p**param.p.b_e + eps_n**self.param.n.b_e, eps_s**self.param.s.b_e, eps_p**self.param.p.b_e ) - a_n = 3 * param.n.prim.epsilon_s_av / param.n.prim.R_typ - a_p = 3 * param.p.prim.epsilon_s_av / param.p.prim.R_typ + a_n = 3 * self.param.n.prim.epsilon_s_av / self.param.n.prim.R_typ + a_p = 3 * self.param.p.prim.epsilon_s_av / self.param.p.prim.R_typ # Interfacial reactions # Surf takes the surface value of a variable, i.e. its boundary value on the # right side. This is also accessible via `boundary_value(x, "right")`, with # "left" providing the boundary value of the left side c_s_surf_n = pybamm.surf(c_s_n) - sto_surf_n = c_s_surf_n / param.n.prim.c_max - j0_n = param.n.prim.j0(c_e_n, c_s_surf_n, T) - eta_n = phi_s_n - phi_e_n - param.n.prim.U(sto_surf_n, T) - Feta_RT_n = param.F * eta_n / (param.R * T) - j_n = 2 * j0_n * pybamm.sinh(param.n.prim.ne / 2 * Feta_RT_n) + sto_surf_n = c_s_surf_n / self.param.n.prim.c_max + j0_n = self.param.n.prim.j0(c_e_n, c_s_surf_n, T) + eta_n = phi_s_n - phi_e_n - self.param.n.prim.U(sto_surf_n, T) + Feta_RT_n = self.param.F * eta_n / (self.param.R * T) + j_n = 2 * j0_n * pybamm.sinh(self.param.n.prim.ne / 2 * Feta_RT_n) c_s_surf_p = pybamm.surf(c_s_p) - sto_surf_p = c_s_surf_p / param.p.prim.c_max - j0_p = param.p.prim.j0(c_e_p, c_s_surf_p, T) - eta_p = phi_s_p - phi_e_p - param.p.prim.U(sto_surf_p, T) - Feta_RT_p = param.F * eta_p / (param.R * T) + sto_surf_p = c_s_surf_p / self.param.p.prim.c_max + j0_p = self.param.p.prim.j0(c_e_p, c_s_surf_p, T) + eta_p = phi_s_p - phi_e_p - self.param.p.prim.U(sto_surf_p, T) + Feta_RT_p = self.param.F * eta_p / (self.param.R * T) j_s = pybamm.PrimaryBroadcast(0, "separator") - j_p = 2 * j0_p * pybamm.sinh(param.p.prim.ne / 2 * Feta_RT_p) + j_p = 2 * j0_p * pybamm.sinh(self.param.p.prim.ne / 2 * Feta_RT_p) a_j_n = a_n * j_n a_j_p = a_p * j_p @@ -150,7 +149,7 @@ def __init__(self, name="Doyle-Fuller-Newman model"): ###################### # State of Charge ###################### - I = param.current_with_time + I = self.param.current_with_time # The `rhs` dictionary contains differential equations, with the key being the # variable in the d/dt self.rhs[Q] = I / 3600 @@ -163,39 +162,39 @@ def __init__(self, name="Doyle-Fuller-Newman model"): # The div and grad operators will be converted to the appropriate matrix # multiplication at the discretisation stage - N_s_n = -param.n.prim.D(c_s_n, T) * pybamm.grad(c_s_n) - N_s_p = -param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p) + N_s_n = -self.param.n.prim.D(c_s_n, T) * pybamm.grad(c_s_n) + N_s_p = -self.param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p) self.rhs[c_s_n] = -pybamm.div(N_s_n) self.rhs[c_s_p] = -pybamm.div(N_s_p) # Boundary conditions must be provided for equations with spatial derivatives self.boundary_conditions[c_s_n] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( - -j_n / (param.F * pybamm.surf(param.n.prim.D(c_s_n, T))), + -j_n / (self.param.F * pybamm.surf(self.param.n.prim.D(c_s_n, T))), "Neumann", ), } self.boundary_conditions[c_s_p] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( - -j_p / (param.F * pybamm.surf(param.p.prim.D(c_s_p, T))), + -j_p / (self.param.F * pybamm.surf(self.param.p.prim.D(c_s_p, T))), "Neumann", ), } - self.initial_conditions[c_s_n] = param.n.prim.c_init - self.initial_conditions[c_s_p] = param.p.prim.c_init + self.initial_conditions[c_s_n] = self.param.n.prim.c_init + self.initial_conditions[c_s_p] = self.param.p.prim.c_init ###################### # Current in the solid ###################### - sigma_eff_n = param.n.sigma(T) * eps_s_n**param.n.b_s + sigma_eff_n = self.param.n.sigma(T) * eps_s_n**self.param.n.b_s i_s_n = -sigma_eff_n * pybamm.grad(phi_s_n) - sigma_eff_p = param.p.sigma(T) * eps_s_p**param.p.b_s + sigma_eff_p = self.param.p.sigma(T) * eps_s_p**self.param.p.b_s i_s_p = -sigma_eff_p * pybamm.grad(phi_s_p) # The `algebraic` dictionary contains differential equations, with the key being # the main scalar variable of interest in the equation # multiply by Lx**2 to improve conditioning - self.algebraic[phi_s_n] = param.L_x**2 * (pybamm.div(i_s_n) + a_j_n) - self.algebraic[phi_s_p] = param.L_x**2 * (pybamm.div(i_s_p) + a_j_p) + self.algebraic[phi_s_n] = self.param.L_x**2 * (pybamm.div(i_s_n) + a_j_n) + self.algebraic[phi_s_p] = self.param.L_x**2 * (pybamm.div(i_s_p) + a_j_p) self.boundary_conditions[phi_s_n] = { "left": (pybamm.Scalar(0), "Dirichlet"), "right": (pybamm.Scalar(0), "Neumann"), @@ -208,34 +207,34 @@ def __init__(self, name="Doyle-Fuller-Newman model"): # initial guess for a root-finding algorithm which calculates consistent initial # conditions self.initial_conditions[phi_s_n] = pybamm.Scalar(0) - self.initial_conditions[phi_s_p] = param.ocv_init + self.initial_conditions[phi_s_p] = self.param.ocv_init ###################### # Current in the electrolyte ###################### - i_e = (param.kappa_e(c_e, T) * tor) * ( - param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) + i_e = (self.param.kappa_e(c_e, T) * tor) * ( + self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) ) # multiply by Lx**2 to improve conditioning - self.algebraic[phi_e] = param.L_x**2 * (pybamm.div(i_e) - a_j) + self.algebraic[phi_e] = self.param.L_x**2 * (pybamm.div(i_e) - a_j) self.boundary_conditions[phi_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[phi_e] = -param.n.prim.U_init + self.initial_conditions[phi_e] = -self.param.n.prim.U_init ###################### # Electrolyte concentration ###################### - N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e) + N_e = -tor * self.param.D_e(c_e, T) * pybamm.grad(c_e) self.rhs[c_e] = (1 / eps) * ( - -pybamm.div(N_e) + (1 - param.t_plus(c_e, T)) * a_j / param.F + -pybamm.div(N_e) + (1 - self.param.t_plus(c_e, T)) * a_j / self.param.F ) self.boundary_conditions[c_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[c_e] = param.c_e_init + self.initial_conditions[c_e] = self.param.c_e_init ###################### # (Some) variables @@ -270,6 +269,6 @@ def __init__(self, name="Doyle-Fuller-Newman model"): } # Events specify points at which a solution should terminate self.events += [ - pybamm.Event("Minimum voltage [V]", voltage - param.voltage_low_cut), - pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - voltage), + pybamm.Event("Minimum voltage [V]", voltage - self.param.voltage_low_cut), + pybamm.Event("Maximum voltage [V]", self.param.voltage_high_cut - voltage), ] diff --git a/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_composite.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_composite.py index 95f65f4d50..273d1c037c 100644 --- a/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_composite.py +++ b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_composite.py @@ -28,7 +28,6 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): # `param` is a class containing all the relevant parameters and functions for # this model. These are purely symbolic at this stage, and will be set by the # `ParameterValues` class when the model is processed. - param = self.param ###################### # Variables @@ -39,17 +38,17 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): c_e_n = pybamm.Variable( "Negative electrolyte concentration [mol.m-3]", domain="negative electrode", - scale=param.c_e_init_av, + scale=self.param.c_e_init_av, ) c_e_s = pybamm.Variable( "Separator electrolyte concentration [mol.m-3]", domain="separator", - scale=param.c_e_init_av, + scale=self.param.c_e_init_av, ) c_e_p = pybamm.Variable( "Positive electrolyte concentration [mol.m-3]", domain="positive electrode", - scale=param.c_e_init_av, + scale=self.param.c_e_init_av, ) # Concatenations combine several variables into a single variable, to simplify # implementing equations that hold over several domains @@ -59,17 +58,17 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): phi_e_n = pybamm.Variable( "Negative electrolyte potential [V]", domain="negative electrode", - reference=-param.n.prim.U_init, + reference=-self.param.n.prim.U_init, ) phi_e_s = pybamm.Variable( "Separator electrolyte potential [V]", domain="separator", - reference=-param.n.prim.U_init, + reference=-self.param.n.prim.U_init, ) phi_e_p = pybamm.Variable( "Positive electrolyte potential [V]", domain="positive electrode", - reference=-param.n.prim.U_init, + reference=-self.param.n.prim.U_init, ) phi_e = pybamm.concatenation(phi_e_n, phi_e_s, phi_e_p) @@ -80,7 +79,7 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): phi_s_p = pybamm.Variable( "Positive electrode potential [V]", domain="positive electrode", - reference=param.ocv_init, + reference=self.param.ocv_init, ) # Particle concentrations are variables on the particle domain, but also vary in # the x-direction (electrode domain) and so must be provided with auxiliary @@ -89,30 +88,30 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): "Negative primary particle concentration [mol.m-3]", domain="negative primary particle", auxiliary_domains={"secondary": "negative electrode"}, - scale=param.n.prim.c_max, + scale=self.param.n.prim.c_max, ) c_s_n_p2 = pybamm.Variable( "Negative secondary particle concentration [mol.m-3]", domain="negative secondary particle", auxiliary_domains={"secondary": "negative electrode"}, - scale=param.n.sec.c_max, + scale=self.param.n.sec.c_max, ) c_s_p = pybamm.Variable( "Positive particle concentration [mol.m-3]", domain="positive particle", auxiliary_domains={"secondary": "positive electrode"}, - scale=param.p.prim.c_max, + scale=self.param.p.prim.c_max, ) # Constant temperature - T = param.T_init + T = self.param.T_init ###################### # Other set-up ###################### # Current density - i_cell = param.current_density_with_time + i_cell = self.param.current_density_with_time # Porosity # Primary broadcasts are used to broadcast scalar quantities across a domain @@ -138,16 +137,16 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): # Tortuosity tor = pybamm.concatenation( - eps_n**param.n.b_e, eps_s**param.s.b_e, eps_p**param.p.b_e + eps_n**self.param.n.b_e, eps_s**self.param.s.b_e, eps_p**self.param.p.b_e ) # Open-circuit potentials c_s_surf_n_p1 = pybamm.surf(c_s_n_p1) - sto_surf_n_p1 = c_s_surf_n_p1 / param.n.prim.c_max - ocp_n_p1 = param.n.prim.U(sto_surf_n_p1, T) + sto_surf_n_p1 = c_s_surf_n_p1 / self.param.n.prim.c_max + ocp_n_p1 = self.param.n.prim.U(sto_surf_n_p1, T) c_s_surf_n_p2 = pybamm.surf(c_s_n_p2) - sto_surf_n_p2 = c_s_surf_n_p2 / param.n.sec.c_max + sto_surf_n_p2 = c_s_surf_n_p2 / self.param.n.sec.c_max k = 100 m_lith = pybamm.sigmoid(i_cell, 0, k) # for lithation (current < 0) m_delith = 1 - m_lith # for delithiation (current > 0) @@ -156,38 +155,42 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): ocp_n_p2 = m_lith * U_lith + m_delith * U_delith c_s_surf_p = pybamm.surf(c_s_p) - sto_surf_p = c_s_surf_p / param.p.prim.c_max - ocp_p = param.p.prim.U(sto_surf_p, T) + sto_surf_p = c_s_surf_p / self.param.p.prim.c_max + ocp_p = self.param.p.prim.U(sto_surf_p, T) # Interfacial reactions # Surf takes the surface value of a variable, i.e. its boundary value on the # right side. This is also accessible via `boundary_value(x, "right")`, with # "left" providing the boundary value of the left side - F_RT = param.F / (param.R * T) - j0_n_p1 = param.n.prim.j0(c_e_n, c_s_surf_n_p1, T) + F_RT = self.param.F / (self.param.R * T) + j0_n_p1 = self.param.n.prim.j0(c_e_n, c_s_surf_n_p1, T) j_n_p1 = ( 2 * j0_n_p1 - * pybamm.sinh(param.n.prim.ne / 2 * F_RT * (phi_s_n - phi_e_n - ocp_n_p1)) + * pybamm.sinh( + self.param.n.prim.ne / 2 * F_RT * (phi_s_n - phi_e_n - ocp_n_p1) + ) ) - j0_n_p2 = param.n.sec.j0(c_e_n, c_s_surf_n_p2, T) + j0_n_p2 = self.param.n.sec.j0(c_e_n, c_s_surf_n_p2, T) j_n_p2 = ( 2 * j0_n_p2 - * pybamm.sinh(param.n.sec.ne / 2 * F_RT * (phi_s_n - phi_e_n - ocp_n_p2)) + * pybamm.sinh( + self.param.n.sec.ne / 2 * F_RT * (phi_s_n - phi_e_n - ocp_n_p2) + ) ) - j0_p = param.p.prim.j0(c_e_p, c_s_surf_p, T) + j0_p = self.param.p.prim.j0(c_e_p, c_s_surf_p, T) a_j_s = pybamm.PrimaryBroadcast(0, "separator") j_p = ( 2 * j0_p - * pybamm.sinh(param.p.prim.ne / 2 * F_RT * (phi_s_p - phi_e_p - ocp_p)) + * pybamm.sinh(self.param.p.prim.ne / 2 * F_RT * (phi_s_p - phi_e_p - ocp_p)) ) # Volumetric - a_n_p1 = 3 * param.n.prim.epsilon_s_av / param.n.prim.R_typ - a_n_p2 = 3 * param.n.sec.epsilon_s_av / param.n.sec.R_typ - a_p = 3 * param.p.prim.epsilon_s_av / param.p.prim.R_typ + a_n_p1 = 3 * self.param.n.prim.epsilon_s_av / self.param.n.prim.R_typ + a_n_p2 = 3 * self.param.n.sec.epsilon_s_av / self.param.n.sec.R_typ + a_p = 3 * self.param.p.prim.epsilon_s_av / self.param.p.prim.R_typ a_j_n_p1 = a_n_p1 * j_n_p1 a_j_n_p2 = a_n_p2 * j_n_p2 a_j_n = a_j_n_p1 + a_j_n_p2 @@ -197,7 +200,7 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): ###################### # State of Charge ###################### - I = param.current_with_time + I = self.param.current_with_time # The `rhs` dictionary contains differential equations, with the key being the # variable in the d/dt self.rhs[Q] = I / 3600 @@ -210,9 +213,9 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): # The div and grad operators will be converted to the appropriate matrix # multiplication at the discretisation stage - N_s_n_p1 = -param.n.prim.D(c_s_n_p1, T) * pybamm.grad(c_s_n_p1) - N_s_n_p2 = -param.n.sec.D(c_s_n_p2, T) * pybamm.grad(c_s_n_p2) - N_s_p = -param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p) + N_s_n_p1 = -self.param.n.prim.D(c_s_n_p1, T) * pybamm.grad(c_s_n_p1) + N_s_n_p2 = -self.param.n.sec.D(c_s_n_p2, T) * pybamm.grad(c_s_n_p2) + N_s_p = -self.param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p) self.rhs[c_s_n_p1] = -pybamm.div(N_s_n_p1) self.rhs[c_s_n_p2] = -pybamm.div(N_s_n_p2) self.rhs[c_s_p] = -pybamm.div(N_s_p) @@ -220,27 +223,27 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): self.boundary_conditions[c_s_n_p1] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( - -j_n_p1 / param.F / pybamm.surf(param.n.prim.D(c_s_n_p1, T)), + -j_n_p1 / self.param.F / pybamm.surf(self.param.n.prim.D(c_s_n_p1, T)), "Neumann", ), } self.boundary_conditions[c_s_n_p2] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( - -j_n_p2 / param.F / pybamm.surf(param.n.sec.D(c_s_n_p2, T)), + -j_n_p2 / self.param.F / pybamm.surf(self.param.n.sec.D(c_s_n_p2, T)), "Neumann", ), } self.boundary_conditions[c_s_p] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( - -j_p / param.F / pybamm.surf(param.p.prim.D(c_s_p, T)), + -j_p / self.param.F / pybamm.surf(self.param.p.prim.D(c_s_p, T)), "Neumann", ), } - self.initial_conditions[c_s_n_p1] = param.n.prim.c_init - self.initial_conditions[c_s_n_p2] = param.n.sec.c_init - self.initial_conditions[c_s_p] = param.p.prim.c_init + self.initial_conditions[c_s_n_p1] = self.param.n.prim.c_init + self.initial_conditions[c_s_n_p2] = self.param.n.sec.c_init + self.initial_conditions[c_s_p] = self.param.p.prim.c_init # Events specify points at which a solution should terminate tolerance = 0.0000001 self.events += [ @@ -272,15 +275,15 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): ###################### # Current in the solid ###################### - sigma_eff_n = param.n.sigma(T) * eps_s_n**param.n.b_s + sigma_eff_n = self.param.n.sigma(T) * eps_s_n**self.param.n.b_s i_s_n = -sigma_eff_n * pybamm.grad(phi_s_n) - sigma_eff_p = param.p.sigma(T) * eps_s_p**param.p.b_s + sigma_eff_p = self.param.p.sigma(T) * eps_s_p**self.param.p.b_s i_s_p = -sigma_eff_p * pybamm.grad(phi_s_p) # The `algebraic` dictionary contains differential equations, with the key being # the main scalar variable of interest in the equation # multiply by Lx**2 to improve conditioning - self.algebraic[phi_s_n] = param.L_x**2 * (pybamm.div(i_s_n) + a_j_n) - self.algebraic[phi_s_p] = param.L_x**2 * (pybamm.div(i_s_p) + a_j_p) + self.algebraic[phi_s_n] = self.param.L_x**2 * (pybamm.div(i_s_n) + a_j_n) + self.algebraic[phi_s_p] = self.param.L_x**2 * (pybamm.div(i_s_p) + a_j_p) self.boundary_conditions[phi_s_n] = { "left": (pybamm.Scalar(0), "Dirichlet"), "right": (pybamm.Scalar(0), "Neumann"), @@ -295,34 +298,34 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): # We evaluate c_n_init at x=0 and c_p_init at x=1 (this is just an initial # guess so actual value is not too important) self.initial_conditions[phi_s_n] = pybamm.Scalar(0) - self.initial_conditions[phi_s_p] = param.ocv_init + self.initial_conditions[phi_s_p] = self.param.ocv_init ###################### # Current in the electrolyte ###################### - i_e = (param.kappa_e(c_e, T) * tor) * ( - param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) + i_e = (self.param.kappa_e(c_e, T) * tor) * ( + self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) ) # multiply by Lx**2 to improve conditioning - self.algebraic[phi_e] = param.L_x**2 * (pybamm.div(i_e) - a_j) + self.algebraic[phi_e] = self.param.L_x**2 * (pybamm.div(i_e) - a_j) self.boundary_conditions[phi_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[phi_e] = -param.n.prim.U_init + self.initial_conditions[phi_e] = -self.param.n.prim.U_init ###################### # Electrolyte concentration ###################### - N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e) + N_e = -tor * self.param.D_e(c_e, T) * pybamm.grad(c_e) self.rhs[c_e] = (1 / eps) * ( - -pybamm.div(N_e) + (1 - param.t_plus(c_e, T)) * a_j / param.F + -pybamm.div(N_e) + (1 - self.param.t_plus(c_e, T)) * a_j / self.param.F ) self.boundary_conditions[c_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[c_e] = param.c_e_init + self.initial_conditions[c_e] = self.param.c_e_init ###################### # (Some) variables @@ -400,8 +403,8 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): } # Events specify points at which a solution should terminate self.events += [ - pybamm.Event("Minimum voltage [V]", voltage - param.voltage_low_cut), - pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - voltage), + pybamm.Event("Minimum voltage [V]", voltage - self.param.voltage_low_cut), + pybamm.Event("Maximum voltage [V]", self.param.voltage_high_cut - voltage), ] @property diff --git a/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py index bc1eba3a83..b23b9dba0f 100644 --- a/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py +++ b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py @@ -36,7 +36,6 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): # `param` is a class containing all the relevant parameters and functions for # this model. These are purely symbolic at this stage, and will be set by the # `ParameterValues` class when the model is processed. - param = self.param ###################### # Variables @@ -69,14 +68,14 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): phi_e = pybamm.concatenation(phi_e_s, phi_e_w) # Constant temperature - T = param.T_init + T = self.param.T_init ###################### # Other set-up ###################### # Current density - i_cell = param.current_density_with_time + i_cell = self.param.current_density_with_time # Define particle surface concentration # Surf takes the surface value of a variable, i.e. its boundary value on the @@ -94,39 +93,39 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): eps_w = pybamm.PrimaryBroadcast( pybamm.Parameter("Positive electrode porosity"), "positive electrode" ) - b_e_s = param.s.b_e - b_e_w = param.p.b_e + b_e_s = self.param.s.b_e + b_e_w = self.param.p.b_e # Interfacial reactions - j0_w = param.p.prim.j0(c_e_w, c_s_surf_w, T) - U_w = param.p.prim.U - ne_w = param.p.prim.ne + j0_w = self.param.p.prim.j0(c_e_w, c_s_surf_w, T) + U_w = self.param.p.prim.U + ne_w = self.param.p.prim.ne # Particle diffusion parameters - D_w = param.p.prim.D - c_w_init = param.p.prim.c_init + D_w = self.param.p.prim.D + c_w_init = self.param.p.prim.c_init # Electrode equation parameters eps_s_w = pybamm.Parameter("Positive electrode active material volume fraction") - b_s_w = param.p.b_s - sigma_w = param.p.sigma + b_s_w = self.param.p.b_s + sigma_w = self.param.p.sigma # Other parameters (for outputs) - c_w_max = param.p.prim.c_max - L_w = param.p.L + c_w_max = self.param.p.prim.c_max + L_w = self.param.p.L eps = pybamm.concatenation(eps_s, eps_w) tor = pybamm.concatenation(eps_s**b_e_s, eps_w**b_e_w) - F_RT = param.F / (param.R * T) - RT_F = param.R * T / param.F + F_RT = self.param.F / (self.param.R * T) + RT_F = self.param.R * T / self.param.F sto_surf_w = c_s_surf_w / c_w_max j_w = ( 2 * j0_w * pybamm.sinh(ne_w / 2 * F_RT * (phi_s_w - phi_e_w - U_w(sto_surf_w, T))) ) - R_w = param.p.prim.R_typ + R_w = self.param.p.prim.R_typ a_w = 3 * eps_s_w / R_w a_j_w = a_w * j_w a_j_s = pybamm.PrimaryBroadcast(0, "separator") @@ -135,7 +134,7 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): ###################### # State of Charge ###################### - I = param.current_with_time + I = self.param.current_with_time # The `rhs` dictionary contains differential equations, with the key being the # variable in the d/dt self.rhs[Q] = I / 3600 @@ -154,7 +153,7 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): # derivatives self.boundary_conditions[c_s_w] = { "left": (pybamm.Scalar(0), "Neumann"), - "right": (-j_w / pybamm.surf(D_w(c_s_w, T)) / param.F, "Neumann"), + "right": (-j_w / pybamm.surf(D_w(c_s_w, T)) / self.param.F, "Neumann"), } self.initial_conditions[c_s_w] = c_w_init @@ -183,21 +182,23 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): ), } # multiply by Lx**2 to improve conditioning - self.algebraic[phi_s_w] = param.L_x**2 * (pybamm.div(i_s_w) + a_j_w) + self.algebraic[phi_s_w] = self.param.L_x**2 * (pybamm.div(i_s_w) + a_j_w) # Initial conditions must also be provided for algebraic equations, as an # initial guess for a root-finding algorithm which calculates consistent # initial conditions - self.initial_conditions[phi_s_w] = param.p.prim.U_init + self.initial_conditions[phi_s_w] = self.param.p.prim.U_init ###################### # Electrolyte concentration ###################### - N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e) + N_e = -tor * self.param.D_e(c_e, T) * pybamm.grad(c_e) self.rhs[c_e] = (1 / eps) * ( - -pybamm.div(N_e) + (1 - param.t_plus(c_e, T)) * a_j / param.F + -pybamm.div(N_e) + (1 - self.param.t_plus(c_e, T)) * a_j / self.param.F ) dce_dx = ( - -(1 - param.t_plus(c_e, T)) * i_cell / (tor * param.F * param.D_e(c_e, T)) + -(1 - self.param.t_plus(c_e, T)) + * i_cell + / (tor * self.param.F * self.param.D_e(c_e, T)) ) self.boundary_conditions[c_e] = { @@ -205,7 +206,7 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[c_e] = param.c_e_init + self.initial_conditions[c_e] = self.param.c_e_init self.events.append( pybamm.Event( "Zero electrolyte concentration cut-off", pybamm.min(c_e) - 0.002 @@ -215,16 +216,16 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): ###################### # Current in the electrolyte ###################### - i_e = (param.kappa_e(c_e, T) * tor) * ( - param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) + i_e = (self.param.kappa_e(c_e, T) * tor) * ( + self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) ) # multiply by Lx**2 to improve conditioning - self.algebraic[phi_e] = param.L_x**2 * (pybamm.div(i_e) - a_j) + self.algebraic[phi_e] = self.param.L_x**2 * (pybamm.div(i_e) - a_j) # reference potential - L_Li = param.n.L - sigma_Li = param.n.sigma - j_Li = param.j0_Li_metal(pybamm.boundary_value(c_e, "left"), c_w_max, T) + L_Li = self.param.n.L + sigma_Li = self.param.n.sigma + j_Li = self.param.j0_Li_metal(pybamm.boundary_value(c_e, "left"), c_w_max, T) eta_Li = 2 * RT_F * pybamm.arcsinh(i_cell / (2 * j_Li)) phi_s_cn = 0 @@ -237,7 +238,7 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[phi_e] = -param.n.prim.U_init + self.initial_conditions[phi_e] = -self.param.n.prim.U_init ###################### # (Some) variables @@ -290,11 +291,15 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): "X-averaged positive particle surface concentration " "[mol.m-3]": c_s_surf_w_av, "Positive particle concentration [mol.m-3]": c_s_w, - "Total lithium in positive electrode [mol]": c_s_vol_av * L_w * param.A_cc, + "Total lithium in positive electrode [mol]": c_s_vol_av + * L_w + * self.param.A_cc, "Electrolyte concentration [mol.m-3]": c_e, "Separator electrolyte concentration [mol.m-3]": c_e_s, "Positive electrolyte concentration [mol.m-3]": c_e_w, - "Total lithium in electrolyte [mol]": c_e_total * param.L_x * param.A_cc, + "Total lithium in electrolyte [mol]": c_e_total + * self.param.L_x + * self.param.A_cc, "Current [A]": I, "Current variable [A]": I, # for compatibility with pybamm.Experiment "Current density [A.m-2]": i_cell, diff --git a/src/pybamm/models/full_battery_models/lithium_ion/basic_spm.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_spm.py index 6bd93f3b27..cd1b968017 100644 --- a/src/pybamm/models/full_battery_models/lithium_ion/basic_spm.py +++ b/src/pybamm/models/full_battery_models/lithium_ion/basic_spm.py @@ -26,7 +26,6 @@ def __init__(self, name="Single Particle Model"): # `param` is a class containing all the relevant parameters and functions for # this model. These are purely symbolic at this stage, and will be set by the # `ParameterValues` class when the model is processed. - param = self.param ###################### # Variables @@ -44,23 +43,23 @@ def __init__(self, name="Single Particle Model"): ) # Constant temperature - T = param.T_init + T = self.param.T_init ###################### # Other set-up ###################### # Current density - i_cell = param.current_density_with_time - a_n = 3 * param.n.prim.epsilon_s_av / param.n.prim.R_typ - a_p = 3 * param.p.prim.epsilon_s_av / param.p.prim.R_typ - j_n = i_cell / (param.n.L * a_n) - j_p = -i_cell / (param.p.L * a_p) + i_cell = self.param.current_density_with_time + a_n = 3 * self.param.n.prim.epsilon_s_av / self.param.n.prim.R_typ + a_p = 3 * self.param.p.prim.epsilon_s_av / self.param.p.prim.R_typ + j_n = i_cell / (self.param.n.L * a_n) + j_p = -i_cell / (self.param.p.L * a_p) ###################### # State of Charge ###################### - I = param.current_with_time + I = self.param.current_with_time # The `rhs` dictionary contains differential equations, with the key being the # variable in the d/dt self.rhs[Q] = I / 3600 @@ -73,8 +72,8 @@ def __init__(self, name="Single Particle Model"): # The div and grad operators will be converted to the appropriate matrix # multiplication at the discretisation stage - N_s_n = -param.n.prim.D(c_s_n, T) * pybamm.grad(c_s_n) - N_s_p = -param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p) + N_s_n = -self.param.n.prim.D(c_s_n, T) * pybamm.grad(c_s_n) + N_s_p = -self.param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p) self.rhs[c_s_n] = -pybamm.div(N_s_n) self.rhs[c_s_p] = -pybamm.div(N_s_p) # Surf takes the surface value of a variable, i.e. its boundary value on the @@ -86,24 +85,24 @@ def __init__(self, name="Single Particle Model"): self.boundary_conditions[c_s_n] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( - -j_n / (param.F * pybamm.surf(param.n.prim.D(c_s_n, T))), + -j_n / (self.param.F * pybamm.surf(self.param.n.prim.D(c_s_n, T))), "Neumann", ), } self.boundary_conditions[c_s_p] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( - -j_p / (param.F * pybamm.surf(param.p.prim.D(c_s_p, T))), + -j_p / (self.param.F * pybamm.surf(self.param.p.prim.D(c_s_p, T))), "Neumann", ), } # c_n_init and c_p_init are functions of r and x, but for the SPM we # take the x-averaged value since there is no x-dependence in the particles - self.initial_conditions[c_s_n] = pybamm.x_average(param.n.prim.c_init) - self.initial_conditions[c_s_p] = pybamm.x_average(param.p.prim.c_init) + self.initial_conditions[c_s_n] = pybamm.x_average(self.param.n.prim.c_init) + self.initial_conditions[c_s_p] = pybamm.x_average(self.param.p.prim.c_init) # Events specify points at which a solution should terminate - sto_surf_n = c_s_surf_n / param.n.prim.c_max - sto_surf_p = c_s_surf_p / param.p.prim.c_max + sto_surf_n = c_s_surf_n / self.param.n.prim.c_max + sto_surf_p = c_s_surf_p / self.param.p.prim.c_max self.events += [ pybamm.Event( "Minimum negative particle surface stoichiometry", @@ -130,14 +129,14 @@ def __init__(self, name="Single Particle Model"): # (Some) variables ###################### # Interfacial reactions - RT_F = param.R * T / param.F - j0_n = param.n.prim.j0(param.c_e_init_av, c_s_surf_n, T) - j0_p = param.p.prim.j0(param.c_e_init_av, c_s_surf_p, T) - eta_n = (2 / param.n.prim.ne) * RT_F * pybamm.arcsinh(j_n / (2 * j0_n)) - eta_p = (2 / param.p.prim.ne) * RT_F * pybamm.arcsinh(j_p / (2 * j0_p)) + RT_F = self.param.R * T / self.param.F + j0_n = self.param.n.prim.j0(self.param.c_e_init_av, c_s_surf_n, T) + j0_p = self.param.p.prim.j0(self.param.c_e_init_av, c_s_surf_p, T) + eta_n = (2 / self.param.n.prim.ne) * RT_F * pybamm.arcsinh(j_n / (2 * j0_n)) + eta_p = (2 / self.param.p.prim.ne) * RT_F * pybamm.arcsinh(j_p / (2 * j0_p)) phi_s_n = 0 - phi_e = -eta_n - param.n.prim.U(sto_surf_n, T) - phi_s_p = eta_p + phi_e + param.p.prim.U(sto_surf_p, T) + phi_e = -eta_n - self.param.n.prim.U(sto_surf_n, T) + phi_s_p = eta_p + phi_e + self.param.p.prim.U(sto_surf_p, T) V = phi_s_p num_cells = pybamm.Parameter( "Number of cells connected in series to make a battery" @@ -157,7 +156,7 @@ def __init__(self, name="Single Particle Model"): c_s_surf_n, "negative electrode" ), "Electrolyte concentration [mol.m-3]": pybamm.PrimaryBroadcast( - param.c_e_init_av, whole_cell + self.param.c_e_init_av, whole_cell ), "X-averaged positive particle concentration [mol.m-3]": c_s_p, "Positive particle surface " @@ -178,6 +177,6 @@ def __init__(self, name="Single Particle Model"): } # Events specify points at which a solution should terminate self.events += [ - pybamm.Event("Minimum voltage [V]", V - param.voltage_low_cut), - pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - V), + pybamm.Event("Minimum voltage [V]", V - self.param.voltage_low_cut), + pybamm.Event("Maximum voltage [V]", self.param.voltage_high_cut - V), ] diff --git a/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py index c5b6a9b911..d40a74b93e 100644 --- a/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py +++ b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py @@ -666,13 +666,12 @@ def get_initial_stoichiometries(self, initial_value, tol=1e-6, inputs=None): The initial stoichiometries that give the desired initial state of charge """ parameter_values = self.parameter_values - param = self.param x_0, x_100, y_100, y_0 = self.get_min_max_stoichiometries(inputs=inputs) if isinstance(initial_value, str) and initial_value.endswith("V"): V_init = float(initial_value[:-1]) - V_min = parameter_values.evaluate(param.ocp_soc_0) - V_max = parameter_values.evaluate(param.ocp_soc_100) + V_min = parameter_values.evaluate(self.param.ocp_soc_0) + V_max = parameter_values.evaluate(self.param.ocp_soc_100) if not V_min <= V_init <= V_max: raise ValueError( @@ -687,8 +686,8 @@ def get_initial_stoichiometries(self, initial_value, tol=1e-6, inputs=None): y = y_0 - soc * (y_0 - y_100) T_ref = parameter_values["Reference temperature [K]"] if self.options["open-circuit potential"] == "MSMR": - xn = param.n.prim.x - xp = param.p.prim.x + xn = self.param.n.prim.x + xp = self.param.p.prim.x Up = pybamm.Variable("Up") Un = pybamm.Variable("Un") soc_model.algebraic[Up] = x - xn(Un, T_ref) @@ -697,8 +696,8 @@ def get_initial_stoichiometries(self, initial_value, tol=1e-6, inputs=None): soc_model.initial_conditions[Up] = V_max soc_model.algebraic[soc] = Up - Un - V_init else: - Up = param.p.prim.U - Un = param.n.prim.U + Up = self.param.p.prim.U + Un = self.param.n.prim.U soc_model.algebraic[soc] = Up(y, T_ref) - Un(x, T_ref) - V_init # initial guess for soc linearly interpolates between 0 and 1 # based on V linearly interpolating between V_max and V_min @@ -741,17 +740,18 @@ def get_min_max_stoichiometries(self, inputs=None): """ inputs = inputs or {} parameter_values = self.parameter_values - param = self.param - Q_n = parameter_values.evaluate(param.n.Q_init, inputs=inputs) - Q_p = parameter_values.evaluate(param.p.Q_init, inputs=inputs) + Q_n = parameter_values.evaluate(self.param.n.Q_init, inputs=inputs) + Q_p = parameter_values.evaluate(self.param.p.Q_init, inputs=inputs) if self.known_value == "cyclable lithium capacity": - Q_Li = parameter_values.evaluate(param.Q_Li_particles_init, inputs=inputs) + Q_Li = parameter_values.evaluate( + self.param.Q_Li_particles_init, inputs=inputs + ) all_inputs = {**inputs, "Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li} elif self.known_value == "cell capacity": Q = parameter_values.evaluate( - param.Q / param.n_electrodes_parallel, inputs=inputs + self.param.Q / self.param.n_electrodes_parallel, inputs=inputs ) all_inputs = {**inputs, "Q_n": Q_n, "Q_p": Q_p, "Q": Q} # Solve the model and check outputs @@ -782,7 +782,6 @@ def get_initial_ocps(self, initial_value, tol=1e-6, inputs=None): The initial open-circuit potentials at the desired initial state of charge """ parameter_values = self.parameter_values - param = self.param x, y = self.get_initial_stoichiometries(initial_value, tol, inputs=inputs) if self.options["open-circuit potential"] == "MSMR": msmr_pot_model = _get_msmr_potential_model( @@ -795,8 +794,8 @@ def get_initial_ocps(self, initial_value, tol=1e-6, inputs=None): Up = sol["Up"].data[0] else: T_ref = parameter_values["Reference temperature [K]"] - Un = parameter_values.evaluate(param.n.prim.U(x, T_ref), inputs=inputs) - Up = parameter_values.evaluate(param.p.prim.U(y, T_ref), inputs=inputs) + Un = parameter_values.evaluate(self.param.n.prim.U(x, T_ref), inputs=inputs) + Up = parameter_values.evaluate(self.param.p.prim.U(y, T_ref), inputs=inputs) return Un, Up def get_min_max_ocps(self): @@ -810,16 +809,17 @@ def get_min_max_ocps(self): The min/max ocps """ parameter_values = self.parameter_values - param = self.param - Q_n = parameter_values.evaluate(param.n.Q_init) - Q_p = parameter_values.evaluate(param.p.Q_init) + Q_n = parameter_values.evaluate(self.param.n.Q_init) + Q_p = parameter_values.evaluate(self.param.p.Q_init) if self.known_value == "cyclable lithium capacity": - Q_Li = parameter_values.evaluate(param.Q_Li_particles_init) + Q_Li = parameter_values.evaluate(self.param.Q_Li_particles_init) inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li} elif self.known_value == "cell capacity": - Q = parameter_values.evaluate(param.Q / param.n_electrodes_parallel) + Q = parameter_values.evaluate( + self.param.Q / self.param.n_electrodes_parallel + ) inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q": Q} # Solve the model and check outputs sol = self.solve(inputs) @@ -834,10 +834,10 @@ def theoretical_energy_integral(self, inputs, points=1000): x_vals = np.linspace(x_100, x_0, num=points) y_vals = np.linspace(y_100, y_0, num=points) # Calculate OCV at each stoichiometry - param = self.param - T = param.T_amb_av(0) + T = self.param.T_amb_av(0) Vs = self.parameter_values.evaluate( - param.p.prim.U(y_vals, T) - param.n.prim.U(x_vals, T), inputs=inputs + self.param.p.prim.U(y_vals, T) - self.param.n.prim.U(x_vals, T), + inputs=inputs, ).flatten() # Calculate dQ Q = Q_p * (y_0 - y_100) diff --git a/src/pybamm/models/submodels/active_material/base_active_material.py b/src/pybamm/models/submodels/active_material/base_active_material.py index ba39adf852..ea0e826e09 100644 --- a/src/pybamm/models/submodels/active_material/base_active_material.py +++ b/src/pybamm/models/submodels/active_material/base_active_material.py @@ -23,7 +23,6 @@ def __init__(self, param, domain, options, phase="primary"): super().__init__(param, domain, options=options, phase=phase) def _get_standard_active_material_variables(self, eps_solid): - param = self.param phase_name = self.phase_name domain, Domain = self.domain_Domain @@ -61,9 +60,9 @@ def _get_standard_active_material_variables(self, eps_solid): C = ( pybamm.yz_average(eps_solid_av) * L - * param.A_cc + * self.param.A_cc * c_s_max - * param.F + * self.param.F / 3600 ) if phase_name == "": diff --git a/src/pybamm/models/submodels/convection/through_cell/explicit_convection.py b/src/pybamm/models/submodels/convection/through_cell/explicit_convection.py index 33b58e2b23..c0423bfc41 100644 --- a/src/pybamm/models/submodels/convection/through_cell/explicit_convection.py +++ b/src/pybamm/models/submodels/convection/through_cell/explicit_convection.py @@ -19,7 +19,6 @@ def __init__(self, param): def get_coupled_variables(self, variables): # Set up - param = self.param p_s = variables["X-averaged separator pressure [Pa]"] for domain in self.options.whole_cell_domains: if domain == "separator": @@ -29,22 +28,30 @@ def get_coupled_variables(self, variables): ] if domain == "negative electrode": x_n = pybamm.standard_spatial_vars.x_n - DeltaV_k = param.n.DeltaV + DeltaV_k = self.param.n.DeltaV p_k = ( - -DeltaV_k * a_j_k_av / param.F * (-(x_n**2) + param.n.L**2) / 2 + -DeltaV_k + * a_j_k_av + / self.param.F + * (-(x_n**2) + self.param.n.L**2) + / 2 + p_s ) - v_box_k = -DeltaV_k * a_j_k_av / param.F * x_n + v_box_k = -DeltaV_k * a_j_k_av / self.param.F * x_n elif domain == "positive electrode": x_p = pybamm.standard_spatial_vars.x_p - DeltaV_k = param.p.DeltaV + DeltaV_k = self.param.p.DeltaV p_k = ( - -DeltaV_k * a_j_k_av / param.F * ((x_p - 1) ** 2 - param.p.L**2) / 2 + -DeltaV_k + * a_j_k_av + / self.param.F + * ((x_p - 1) ** 2 - self.param.p.L**2) + / 2 + p_s ) - v_box_k = -DeltaV_k * a_j_k_av / param.F * (x_p - param.L_x) + v_box_k = -DeltaV_k * a_j_k_av / self.param.F * (x_p - self.param.L_x) div_v_box_k = pybamm.PrimaryBroadcast( - -DeltaV_k * a_j_k_av / param.F, domain + -DeltaV_k * a_j_k_av / self.param.F, domain ) variables.update( @@ -58,13 +65,13 @@ def get_coupled_variables(self, variables): "X-averaged separator transverse volume-averaged acceleration [m.s-2]" ] i_boundary_cc = variables["Current collector current density [A.m-2]"] - v_box_n_right = -param.n.DeltaV * i_boundary_cc / param.F + v_box_n_right = -self.param.n.DeltaV * i_boundary_cc / self.param.F div_v_box_s_av = -div_Vbox_s div_v_box_s = pybamm.PrimaryBroadcast(div_v_box_s_av, "separator") # Simple formula for velocity in the separator x_s = pybamm.standard_spatial_vars.x_s - v_box_s = div_v_box_s_av * (x_s - param.n.L) + v_box_n_right + v_box_s = div_v_box_s_av * (x_s - self.param.n.L) + v_box_n_right variables.update( self._get_standard_sep_velocity_variables(v_box_s, div_v_box_s) diff --git a/src/pybamm/models/submodels/convection/through_cell/full_convection.py b/src/pybamm/models/submodels/convection/through_cell/full_convection.py index 0fdc089de7..07241bb236 100644 --- a/src/pybamm/models/submodels/convection/through_cell/full_convection.py +++ b/src/pybamm/models/submodels/convection/through_cell/full_convection.py @@ -43,8 +43,7 @@ def get_fundamental_variables(self): def get_coupled_variables(self, variables): # Set up - param = self.param - L_n = param.n.L + L_n = self.param.n.L x_s = pybamm.standard_spatial_vars.x_s # Transverse velocity in the separator determines through-cell velocity @@ -52,7 +51,7 @@ def get_coupled_variables(self, variables): "X-averaged separator transverse volume-averaged acceleration [m.s-2]" ] i_boundary_cc = variables["Current collector current density [A.m-2]"] - v_box_n_right = -param.n.DeltaV * i_boundary_cc / self.param.F + v_box_n_right = -self.param.n.DeltaV * i_boundary_cc / self.param.F div_v_box_s_av = -div_Vbox_s div_v_box_s = pybamm.PrimaryBroadcast(div_v_box_s_av, "separator") diff --git a/src/pybamm/models/submodels/convection/transverse/full_convection.py b/src/pybamm/models/submodels/convection/transverse/full_convection.py index 16da47ae47..0a6367fec1 100644 --- a/src/pybamm/models/submodels/convection/transverse/full_convection.py +++ b/src/pybamm/models/submodels/convection/transverse/full_convection.py @@ -37,15 +37,13 @@ def get_fundamental_variables(self): return variables def set_algebraic(self, variables): - param = self.param - p_s = variables["X-averaged separator pressure [Pa]"] # Difference in negative and positive electrode velocities determines the # velocity in the separator i_boundary_cc = variables["Current collector current density [A.m-2]"] - v_box_n_right = -param.n.DeltaV * i_boundary_cc / self.param.F - v_box_p_left = -param.p.DeltaV * i_boundary_cc / self.param.F - d_vbox_s_dx = (v_box_p_left - v_box_n_right) / param.s.L + v_box_n_right = -self.param.n.DeltaV * i_boundary_cc / self.param.F + v_box_p_left = -self.param.p.DeltaV * i_boundary_cc / self.param.F + d_vbox_s_dx = (v_box_p_left - v_box_n_right) / self.param.s.L # Simple formula for velocity in the separator div_Vbox_s = -d_vbox_s_dx diff --git a/src/pybamm/models/submodels/convection/transverse/uniform_convection.py b/src/pybamm/models/submodels/convection/transverse/uniform_convection.py index 15a073c148..a4b05f1ad5 100644 --- a/src/pybamm/models/submodels/convection/transverse/uniform_convection.py +++ b/src/pybamm/models/submodels/convection/transverse/uniform_convection.py @@ -26,15 +26,14 @@ def get_fundamental_variables(self): def get_coupled_variables(self, variables): # Set up - param = self.param z = pybamm.standard_spatial_vars.z # Difference in negative and positive electrode velocities determines the # velocity in the separator i_boundary_cc = variables["Current collector current density [A.m-2]"] - v_box_n_right = -param.n.DeltaV * i_boundary_cc / param.F - v_box_p_left = -param.p.DeltaV * i_boundary_cc / param.F - d_vbox_s_dx = (v_box_p_left - v_box_n_right) / param.s.L + v_box_n_right = -self.param.n.DeltaV * i_boundary_cc / self.param.F + v_box_p_left = -self.param.p.DeltaV * i_boundary_cc / self.param.F + d_vbox_s_dx = (v_box_p_left - v_box_n_right) / self.param.s.L # Simple formula for velocity in the separator div_Vbox_s = -d_vbox_s_dx diff --git a/src/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py b/src/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py index 23001b9d02..808e0a34a3 100644 --- a/src/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py +++ b/src/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py @@ -12,29 +12,28 @@ def default_parameter_values(self): @property def default_geometry(self): geometry = {} - param = self.param if self.options["dimensionality"] == 1: geometry["current collector"] = { - "z": {"min": 0, "max": param.L_z}, + "z": {"min": 0, "max": self.param.L_z}, "tabs": { - "negative": {"z_centre": param.n.centre_z_tab}, - "positive": {"z_centre": param.p.centre_z_tab}, + "negative": {"z_centre": self.param.n.centre_z_tab}, + "positive": {"z_centre": self.param.p.centre_z_tab}, }, } elif self.options["dimensionality"] == 2: geometry["current collector"] = { - "y": {"min": 0, "max": param.L_y}, - "z": {"min": 0, "max": param.L_z}, + "y": {"min": 0, "max": self.param.L_y}, + "z": {"min": 0, "max": self.param.L_z}, "tabs": { "negative": { - "y_centre": param.n.centre_y_tab, - "z_centre": param.n.centre_z_tab, - "width": param.n.L_tab, + "y_centre": self.param.n.centre_y_tab, + "z_centre": self.param.n.centre_z_tab, + "width": self.param.n.L_tab, }, "positive": { - "y_centre": param.p.centre_y_tab, - "z_centre": param.p.centre_z_tab, - "width": param.p.L_tab, + "y_centre": self.param.p.centre_y_tab, + "z_centre": self.param.p.centre_z_tab, + "width": self.param.p.L_tab, }, }, } @@ -131,11 +130,10 @@ def __init__( def get_fundamental_variables(self): # Get necessary parameters - param = self.param - L_cn = param.n.L_cc - L_cp = param.p.L_cc - sigma_cn = param.n.sigma_cc - sigma_cp = param.p.sigma_cc + L_cn = self.param.n.L_cc + L_cp = self.param.p.L_cc + sigma_cn = self.param.n.sigma_cc + sigma_cp = self.param.p.sigma_cc # Set model variables: Note: we solve using a scaled version that is # better conditioned @@ -273,13 +271,12 @@ def __init__(self): self.param = pybamm.LithiumIonParameters() # Get necessary parameters - param = self.param - L_cn = param.n.L_cc - L_cp = param.p.L_cc - L_tab_p = param.p.L_tab + L_cn = self.param.n.L_cc + L_cp = self.param.p.L_cc + L_tab_p = self.param.p.L_tab A_tab_p = L_cp * L_tab_p - sigma_cn = param.n.sigma_cc - sigma_cp = param.p.sigma_cc + sigma_cn = self.param.n.sigma_cc + sigma_cp = self.param.p.sigma_cc # Set model variables -- we solve a auxilliary problem in each current collector # then relate this to the potentials and resistances later @@ -347,11 +344,10 @@ def post_process(self, solution, param_values, V_av, I_av): processed potentials. """ # Get evaluated parameters - param = self.param - L_cn = param_values.evaluate(param.n.L_cc) - L_cp = param_values.evaluate(param.p.L_cc) - sigma_cn = param_values.evaluate(param.n.sigma_cc) - sigma_cp = param_values.evaluate(param.p.sigma_cc) + L_cn = param_values.evaluate(self.param.n.L_cc) + L_cp = param_values.evaluate(self.param.p.L_cc) + sigma_cn = param_values.evaluate(self.param.n.sigma_cc) + sigma_cp = param_values.evaluate(self.param.p.sigma_cc) # Process unit solutions f_n = solution["Unit solution in negative current collector"] diff --git a/src/pybamm/models/submodels/current_collector/potential_pair.py b/src/pybamm/models/submodels/current_collector/potential_pair.py index 68a9066da3..c2a197ae64 100644 --- a/src/pybamm/models/submodels/current_collector/potential_pair.py +++ b/src/pybamm/models/submodels/current_collector/potential_pair.py @@ -23,7 +23,6 @@ def __init__(self, param): pybamm.citations.register("Timms2021") def get_fundamental_variables(self): - param = self.param phi_s_cn = pybamm.Variable( "Negative current collector potential [V]", domain="current collector" ) @@ -35,7 +34,7 @@ def get_fundamental_variables(self): i_boundary_cc = pybamm.Variable( "Current collector current density [A.m-2]", domain="current collector", - scale=param.Q / (param.A_cc * param.n_electrodes_parallel), + scale=self.param.Q / (self.param.A_cc * self.param.n_electrodes_parallel), ) variables.update(self._get_standard_current_variables(i_cc, i_boundary_cc)) @@ -43,16 +42,15 @@ def get_fundamental_variables(self): return variables def set_algebraic(self, variables): - param = self.param - phi_s_cn = variables["Negative current collector potential [V]"] phi_s_cp = variables["Positive current collector potential [V]"] i_boundary_cc = variables["Current collector current density [A.m-2]"] self.algebraic = { - phi_s_cn: (param.n.sigma_cc * param.n.L_cc) * pybamm.laplacian(phi_s_cn) + phi_s_cn: (self.param.n.sigma_cc * self.param.n.L_cc) + * pybamm.laplacian(phi_s_cn) - pybamm.source(i_boundary_cc, phi_s_cn), - i_boundary_cc: (param.p.sigma_cc * param.p.L_cc) + i_boundary_cc: (self.param.p.sigma_cc * self.param.p.L_cc) * pybamm.laplacian(phi_s_cp) + pybamm.source(i_boundary_cc, phi_s_cp), } @@ -77,15 +75,14 @@ def set_boundary_conditions(self, variables): phi_s_cn = variables["Negative current collector potential [V]"] phi_s_cp = variables["Positive current collector potential [V]"] - param = self.param applied_current_density = variables["Total current density [A.m-2]"] - total_current = applied_current_density * param.A_cc + total_current = applied_current_density * self.param.A_cc # In the 1+1D model, the behaviour is averaged over the y-direction, so the # effective tab area is the cell width multiplied by the current collector # thickness - positive_tab_area = param.L_y * param.p.L_cc - pos_tab_bc = -total_current / (param.p.sigma_cc * positive_tab_area) + positive_tab_area = self.param.L_y * self.param.p.L_cc + pos_tab_bc = -total_current / (self.param.p.sigma_cc * positive_tab_area) # Boundary condition needs to be on the variables that go into the Laplacian, # even though phi_s_cp isn't a pybamm.Variable object @@ -111,20 +108,19 @@ def set_boundary_conditions(self, variables): phi_s_cn = variables["Negative current collector potential [V]"] phi_s_cp = variables["Positive current collector potential [V]"] - param = self.param applied_current_density = variables["Total current density [A.m-2]"] - total_current = applied_current_density * param.A_cc + total_current = applied_current_density * self.param.A_cc # Note: we divide by the *numerical* tab area so that the correct total # current is applied. That is, numerically integrating the current density # around the boundary gives the applied current exactly. positive_tab_area = pybamm.BoundaryIntegral( - pybamm.PrimaryBroadcast(param.p.L_cc, "current collector"), + pybamm.PrimaryBroadcast(self.param.p.L_cc, "current collector"), region="positive tab", ) # cc_area appears here due to choice of non-dimensionalisation - pos_tab_bc = -total_current / (param.p.sigma_cc * positive_tab_area) + pos_tab_bc = -total_current / (self.param.p.sigma_cc * positive_tab_area) # Boundary condition needs to be on the variables that go into the Laplacian, # even though phi_s_cp isn't a pybamm.Variable object diff --git a/src/pybamm/models/submodels/electrode/base_electrode.py b/src/pybamm/models/submodels/electrode/base_electrode.py index 4248131a75..3abe563c77 100644 --- a/src/pybamm/models/submodels/electrode/base_electrode.py +++ b/src/pybamm/models/submodels/electrode/base_electrode.py @@ -170,9 +170,8 @@ def _get_standard_whole_cell_variables(self, variables): phi_s_p = variables["Positive electrode potential [V]"] phi_s_cp = pybamm.boundary_value(phi_s_p, "right") if self.options["contact resistance"] == "true": - param = self.param I = variables["Current [A]"] - delta_phi_contact = I * param.R_contact + delta_phi_contact = I * self.param.R_contact else: delta_phi_contact = pybamm.Scalar(0) variables.update( diff --git a/src/pybamm/models/submodels/electrode/ohm/composite_ohm.py b/src/pybamm/models/submodels/electrode/ohm/composite_ohm.py index 4845ea9fb2..7c4d8d62b8 100644 --- a/src/pybamm/models/submodels/electrode/ohm/composite_ohm.py +++ b/src/pybamm/models/submodels/electrode/ohm/composite_ohm.py @@ -26,14 +26,13 @@ def __init__(self, param, domain, options=None): def get_coupled_variables(self, variables): domain = self.domain - param = self.param i_boundary_cc = variables["Current collector current density [A.m-2]"] # import parameters and spatial variables - L_n = param.n.L - L_p = param.p.L - L_x = param.L_x + L_n = self.param.n.L + L_p = self.param.p.L + L_x = self.param.L_x x_n = pybamm.standard_spatial_vars.x_n x_p = pybamm.standard_spatial_vars.x_p diff --git a/src/pybamm/models/submodels/electrode/ohm/leading_ohm.py b/src/pybamm/models/submodels/electrode/ohm/leading_ohm.py index 7e414f94c9..8385a31fc1 100644 --- a/src/pybamm/models/submodels/electrode/ohm/leading_ohm.py +++ b/src/pybamm/models/submodels/electrode/ohm/leading_ohm.py @@ -35,15 +35,14 @@ def get_coupled_variables(self, variables): """ Returns variables which are derived from the fundamental variables in the model. """ - param = self.param i_boundary_cc = variables["Current collector current density [A.m-2]"] phi_s_cn = variables["Negative current collector potential [V]"] # import parameters and spatial variables - L_n = param.n.L - L_p = param.p.L - L_x = param.L_x + L_n = self.param.n.L + L_p = self.param.p.L + L_x = self.param.L_x x_n = pybamm.standard_spatial_vars.x_n x_p = pybamm.standard_spatial_vars.x_p diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py index d1178c8cc2..5a7d3163c2 100644 --- a/src/pybamm/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py +++ b/src/pybamm/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py @@ -217,7 +217,6 @@ def _get_electrolyte_overpotentials(self, variables): The variables including the whole-cell electrolyte potentials and currents. """ - param = self.param if self.options.electrode_types["negative"] == "planar": # No concentration overpotential in the counter electrode @@ -229,7 +228,7 @@ def _get_electrolyte_overpotentials(self, variables): c_e_n = variables["Negative electrolyte concentration [mol.m-3]"] T_n = variables["Negative electrode temperature [K]"] indef_integral_n = pybamm.IndefiniteIntegral( - param.chiRT_over_Fc(c_e_n, T_n) * pybamm.grad(c_e_n), + self.param.chiRT_over_Fc(c_e_n, T_n) * pybamm.grad(c_e_n), pybamm.standard_spatial_vars.x_n, ) @@ -243,11 +242,11 @@ def _get_electrolyte_overpotentials(self, variables): # concentration overpotential indef_integral_s = pybamm.IndefiniteIntegral( - param.chiRT_over_Fc(c_e_s, T_s) * pybamm.grad(c_e_s), + self.param.chiRT_over_Fc(c_e_s, T_s) * pybamm.grad(c_e_s), pybamm.standard_spatial_vars.x_s, ) indef_integral_p = pybamm.IndefiniteIntegral( - param.chiRT_over_Fc(c_e_p, T_p) * pybamm.grad(c_e_p), + self.param.chiRT_over_Fc(c_e_p, T_p) * pybamm.grad(c_e_p), pybamm.standard_spatial_vars.x_p, ) diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/composite_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/composite_conductivity.py index 475d1a4232..d6c7ea6473 100644 --- a/src/pybamm/models/submodels/electrolyte_conductivity/composite_conductivity.py +++ b/src/pybamm/models/submodels/electrolyte_conductivity/composite_conductivity.py @@ -52,23 +52,22 @@ def get_coupled_variables(self, variables): T_av_s = pybamm.PrimaryBroadcast(T_av, "separator") T_av_p = pybamm.PrimaryBroadcast(T_av, "positive electrode") - param = self.param - RT_F_av = param.R * T_av / param.F - RT_F_av_s = param.R * T_av_s / param.F - RT_F_av_p = param.R * T_av_p / param.F - - L_n = param.n.L - L_s = param.s.L - L_p = param.p.L - L_x = param.L_x + RT_F_av = self.param.R * T_av / self.param.F + RT_F_av_s = self.param.R * T_av_s / self.param.F + RT_F_av_p = self.param.R * T_av_p / self.param.F + + L_n = self.param.n.L + L_s = self.param.s.L + L_p = self.param.p.L + L_x = self.param.L_x x_s = pybamm.standard_spatial_vars.x_s x_p = pybamm.standard_spatial_vars.x_p # bulk conductivities - kappa_s_av = param.kappa_e(c_e_av, T_av) * tor_s_av - kappa_p_av = param.kappa_e(c_e_av, T_av) * tor_p_av + kappa_s_av = self.param.kappa_e(c_e_av, T_av) * tor_s_av + kappa_p_av = self.param.kappa_e(c_e_av, T_av) * tor_p_av - chi_av = param.chi(c_e_av, T_av) + chi_av = self.param.chi(c_e_av, T_av) chi_av_s = pybamm.PrimaryBroadcast(chi_av, "separator") chi_av_p = pybamm.PrimaryBroadcast(chi_av, "positive electrode") @@ -79,8 +78,8 @@ def get_coupled_variables(self, variables): x_n = pybamm.standard_spatial_vars.x_n chi_av_n = pybamm.PrimaryBroadcast(chi_av, "negative electrode") T_av_n = pybamm.PrimaryBroadcast(T_av, "negative electrode") - RT_F_av_n = param.R * T_av_n / param.F - kappa_n_av = param.kappa_e(c_e_av, T_av) * tor_n_av + RT_F_av_n = self.param.R * T_av_n / self.param.F + kappa_n_av = self.param.kappa_e(c_e_av, T_av) * tor_n_av i_e_n = i_boundary_cc * x_n / L_n i_e_s = pybamm.PrimaryBroadcast(i_boundary_cc, "separator") i_e_p = i_boundary_cc * (L_x - x_p) / L_p diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/full_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/full_conductivity.py index 5acb7d2434..a688209441 100644 --- a/src/pybamm/models/submodels/electrolyte_conductivity/full_conductivity.py +++ b/src/pybamm/models/submodels/electrolyte_conductivity/full_conductivity.py @@ -46,14 +46,13 @@ def get_fundamental_variables(self): return variables def get_coupled_variables(self, variables): - param = self.param T = variables["Cell temperature [K]"] tor = variables["Electrolyte transport efficiency"] c_e = variables["Electrolyte concentration [mol.m-3]"] phi_e = variables["Electrolyte potential [V]"] - i_e = (param.kappa_e(c_e, T) * tor) * ( - param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) + i_e = (self.param.kappa_e(c_e, T) * tor) * ( + self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) ) # Override print_name diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/integrated_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/integrated_conductivity.py index cb9979c6bb..2250d99f6d 100644 --- a/src/pybamm/models/submodels/electrolyte_conductivity/integrated_conductivity.py +++ b/src/pybamm/models/submodels/electrolyte_conductivity/integrated_conductivity.py @@ -32,7 +32,6 @@ def _higher_order_macinnes_function(self, x): return pybamm.log(x) def get_coupled_variables(self, variables): - param = self.param c_e_av = variables["X-averaged electrolyte concentration [mol.m-3]"] i_boundary_cc = variables["Current collector current density [A.m-2]"] @@ -55,22 +54,21 @@ def get_coupled_variables(self, variables): T_av_s = pybamm.PrimaryBroadcast(T_av, "separator") T_av_p = pybamm.PrimaryBroadcast(T_av, "positive electrode") - RT_F_av = param.R * T_av / param.F - RT_F_av_n = param.R * T_av_n / param.F - RT_F_av_s = param.R * T_av_s / param.F - RT_F_av_p = param.R * T_av_p / param.F + RT_F_av = self.param.R * T_av / self.param.F + RT_F_av_n = self.param.R * T_av_n / self.param.F + RT_F_av_s = self.param.R * T_av_s / self.param.F + RT_F_av_p = self.param.R * T_av_p / self.param.F - param = self.param - L_n = param.n.L - L_p = param.p.L - L_x = param.L_x + L_n = self.param.n.L + L_p = self.param.p.L + L_x = self.param.L_x x_n = pybamm.standard_spatial_vars.x_n x_s = pybamm.standard_spatial_vars.x_s x_p = pybamm.standard_spatial_vars.x_p x_n_edge = pybamm.standard_spatial_vars.x_n_edge x_p_edge = pybamm.standard_spatial_vars.x_p_edge - chi_av = param.chi(c_e_av, T_av) + chi_av = self.param.chi(c_e_av, T_av) chi_av_n = pybamm.PrimaryBroadcast(chi_av, "negative electrode") chi_av_s = pybamm.PrimaryBroadcast(chi_av, "separator") chi_av_p = pybamm.PrimaryBroadcast(chi_av, "positive electrode") @@ -87,13 +85,13 @@ def get_coupled_variables(self, variables): # electrolyte potential indef_integral_n = pybamm.IndefiniteIntegral( - i_e_n_edge / (param.kappa_e(c_e_n, T_av_n) * tor_n), x_n + i_e_n_edge / (self.param.kappa_e(c_e_n, T_av_n) * tor_n), x_n ) indef_integral_s = pybamm.IndefiniteIntegral( - i_e_s_edge / (param.kappa_e(c_e_s, T_av_s) * tor_s), x_s + i_e_s_edge / (self.param.kappa_e(c_e_s, T_av_s) * tor_s), x_s ) indef_integral_p = pybamm.IndefiniteIntegral( - i_e_p_edge / (param.kappa_e(c_e_p, T_av_p) * tor_p), x_p + i_e_p_edge / (self.param.kappa_e(c_e_p, T_av_p) * tor_p), x_p ) integral_n = indef_integral_n diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/leading_order_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/leading_order_conductivity.py index ad2a5b6486..42c7770c54 100644 --- a/src/pybamm/models/submodels/electrolyte_conductivity/leading_order_conductivity.py +++ b/src/pybamm/models/submodels/electrolyte_conductivity/leading_order_conductivity.py @@ -36,10 +36,9 @@ def get_coupled_variables(self, variables): i_boundary_cc = variables["Current collector current density [A.m-2]"] - param = self.param - L_n = param.n.L - L_p = param.p.L - L_x = param.L_x + L_n = self.param.n.L + L_p = self.param.p.L + L_x = self.param.L_x x_n = pybamm.standard_spatial_vars.x_n x_p = pybamm.standard_spatial_vars.x_p diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/full_surface_form_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/full_surface_form_conductivity.py index cceb88f83e..fd32e6a83c 100644 --- a/src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/full_surface_form_conductivity.py +++ b/src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/full_surface_form_conductivity.py @@ -47,7 +47,6 @@ def get_fundamental_variables(self): def get_coupled_variables(self, variables): Domain = self.domain.capitalize() - param = self.param if self.domain in ["negative", "positive"]: conductivity, sigma_eff = self._get_conductivities(variables) @@ -59,7 +58,7 @@ def get_coupled_variables(self, variables): T = variables[f"{Domain} electrode temperature [K]"] i_e = conductivity * ( - param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) + self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) + pybamm.grad(delta_phi) + i_boundary_cc / sigma_eff ) @@ -83,8 +82,8 @@ def get_coupled_variables(self, variables): tor_s = variables["Separator electrolyte transport efficiency"] T = variables["Separator temperature [K]"] - chiRT_over_Fc_e_s = param.chiRT_over_Fc(c_e_s, T) - kappa_s_eff = param.kappa_e(c_e_s, T) * tor_s + chiRT_over_Fc_e_s = self.param.chiRT_over_Fc(c_e_s, T) + kappa_s_eff = self.param.kappa_e(c_e_s, T) * tor_s phi_e = phi_e_n_s + pybamm.IndefiniteIntegral( chiRT_over_Fc_e_s * pybamm.grad(c_e_s) - i_boundary_cc / kappa_s_eff, @@ -124,7 +123,8 @@ def get_coupled_variables(self, variables): grad_left = -i_boundary_cc * pybamm.boundary_value(1 / sigma_eff, "left") grad_right = ( (i_boundary_cc / pybamm.boundary_value(conductivity, "right")) - - pybamm.boundary_value(param.chiRT_over_Fc(c_e, T), "right") * grad_c_e + - pybamm.boundary_value(self.param.chiRT_over_Fc(c_e, T), "right") + * grad_c_e - i_boundary_cc * pybamm.boundary_value(1 / sigma_eff, "right") ) @@ -132,7 +132,8 @@ def get_coupled_variables(self, variables): grad_c_e = pybamm.boundary_gradient(c_e, "left") grad_left = ( (i_boundary_cc / pybamm.boundary_value(conductivity, "left")) - - pybamm.boundary_value(param.chiRT_over_Fc(c_e, T), "left") * grad_c_e + - pybamm.boundary_value(self.param.chiRT_over_Fc(c_e, T), "left") + * grad_c_e - i_boundary_cc * pybamm.boundary_value(1 / sigma_eff, "left") ) grad_right = -i_boundary_cc * pybamm.boundary_value(1 / sigma_eff, "right") @@ -150,14 +151,13 @@ def get_coupled_variables(self, variables): def _get_conductivities(self, variables): Domain = self.domain.capitalize() - param = self.param tor_e = variables[f"{Domain} electrolyte transport efficiency"] tor_s = variables[f"{Domain} electrode transport efficiency"] c_e = variables[f"{Domain} electrolyte concentration [mol.m-3]"] T = variables[f"{Domain} electrode temperature [K]"] sigma = self.domain_param.sigma(T) - kappa_eff = param.kappa_e(c_e, T) * tor_e + kappa_eff = self.param.kappa_e(c_e, T) * tor_e sigma_eff = sigma * tor_s conductivity = kappa_eff / (1 + kappa_eff / sigma_eff) diff --git a/src/pybamm/models/submodels/electrolyte_diffusion/full_diffusion.py b/src/pybamm/models/submodels/electrolyte_diffusion/full_diffusion.py index 2fdd937966..06f95bc2f1 100644 --- a/src/pybamm/models/submodels/electrolyte_diffusion/full_diffusion.py +++ b/src/pybamm/models/submodels/electrolyte_diffusion/full_diffusion.py @@ -67,10 +67,8 @@ def get_coupled_variables(self, variables): v_box = variables["Volume-averaged velocity [m.s-1]"] T = variables["Cell temperature [K]"] - param = self.param - - N_e_diffusion = -tor * param.D_e(c_e, T) * pybamm.grad(c_e) - N_e_migration = param.t_plus(c_e, T) * i_e / param.F + N_e_diffusion = -tor * self.param.D_e(c_e, T) * pybamm.grad(c_e) + N_e_migration = self.param.t_plus(c_e, T) * i_e / self.param.F N_e_convection = c_e * v_box N_e = N_e_diffusion + N_e_migration + N_e_convection @@ -106,7 +104,6 @@ def set_initial_conditions(self, variables): } def set_boundary_conditions(self, variables): - param = self.param c_e = variables["Electrolyte concentration [mol.m-3]"] c_e_conc = variables["Electrolyte concentration concatenation [mol.m-3]"] T = variables["Cell temperature [K]"] @@ -118,7 +115,8 @@ def flux_bc(side): # assuming v_box = 0 for now return ( pybamm.boundary_value( - -(1 - param.t_plus(c_e, T)) / (tor * param.D_e(c_e, T) * param.F), + -(1 - self.param.t_plus(c_e, T)) + / (tor * self.param.D_e(c_e, T) * self.param.F), side, ) * i_boundary_cc diff --git a/src/pybamm/models/submodels/electrolyte_diffusion/leading_order_diffusion.py b/src/pybamm/models/submodels/electrolyte_diffusion/leading_order_diffusion.py index 8dedc28cf5..104b12e34e 100644 --- a/src/pybamm/models/submodels/electrolyte_diffusion/leading_order_diffusion.py +++ b/src/pybamm/models/submodels/electrolyte_diffusion/leading_order_diffusion.py @@ -52,8 +52,6 @@ def get_coupled_variables(self, variables): return variables def set_rhs(self, variables): - param = self.param - c_e_av = variables["X-averaged electrolyte concentration [mol.m-3]"] T_av = variables["X-averaged cell temperature [K]"] @@ -86,17 +84,24 @@ def set_rhs(self, variables): "reaction source terms [A.m-3]" ] source_terms = ( - param.n.L * (sum_s_j_n_0 - param.t_plus(c_e_av, T_av) * sum_a_j_n_0) - + param.p.L * (sum_s_j_p_0 - param.t_plus(c_e_av, T_av) * sum_a_j_p_0) - ) / param.F + self.param.n.L + * (sum_s_j_n_0 - self.param.t_plus(c_e_av, T_av) * sum_a_j_n_0) + + self.param.p.L + * (sum_s_j_p_0 - self.param.t_plus(c_e_av, T_av) * sum_a_j_p_0) + ) / self.param.F self.rhs = { c_e_av: 1 - / (param.n.L * eps_n_av + param.s.L * eps_s_av + param.p.L * eps_p_av) + / ( + self.param.n.L * eps_n_av + + self.param.s.L * eps_s_av + + self.param.p.L * eps_p_av + ) * ( source_terms - - c_e_av * (param.n.L * deps_n_dt_av + param.p.L * deps_p_dt_av) - - c_e_av * param.s.L * div_Vbox_s_av + - c_e_av + * (self.param.n.L * deps_n_dt_av + self.param.p.L * deps_p_dt_av) + - c_e_av * self.param.s.L * div_Vbox_s_av ) } diff --git a/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py b/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py index 760e9e2b20..6d1845c3b0 100644 --- a/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py +++ b/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py @@ -27,20 +27,18 @@ class ExplicitPowerControl(BaseModel): """External circuit with current set explicitly to hit target power.""" def get_coupled_variables(self, variables): - param = self.param - # Current is given as applied power divided by voltage V = variables["Voltage [V]"] P = pybamm.FunctionParameter("Power function [W]", {"Time [s]": pybamm.t}) I = P / V # Update derived variables - i_cell = I / (param.n_electrodes_parallel * param.A_cc) + i_cell = I / (self.param.n_electrodes_parallel * self.param.A_cc) variables = { "Total current density [A.m-2]": i_cell, "Current [A]": I, - "C-rate": I / param.Q, + "C-rate": I / self.param.Q, } return variables @@ -50,8 +48,6 @@ class ExplicitResistanceControl(BaseModel): """External circuit with current set explicitly to hit target resistance.""" def get_coupled_variables(self, variables): - param = self.param - # Current is given as applied voltage divided by resistance V = variables["Voltage [V]"] R = pybamm.FunctionParameter( @@ -60,12 +56,12 @@ def get_coupled_variables(self, variables): I = V / R # Update derived variables - i_cell = I / (param.n_electrodes_parallel * param.A_cc) + i_cell = I / (self.param.n_electrodes_parallel * self.param.A_cc) variables = { "Total current density [A.m-2]": i_cell, "Current [A]": I, - "C-rate": I / param.Q, + "C-rate": I / self.param.Q, } return variables diff --git a/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py b/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py index 60d6fb0e40..fcb18086da 100644 --- a/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py +++ b/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py @@ -29,9 +29,8 @@ def __init__(self, param, external_circuit_function, options, control="algebraic self.control = control def get_fundamental_variables(self): - param = self.param # Current is a variable - i_var = pybamm.Variable("Current variable [A]", scale=param.Q) + i_var = pybamm.Variable("Current variable [A]", scale=self.param.Q) if self.control in ["algebraic", "differential"]: I = i_var elif self.control == "differential with max": @@ -41,13 +40,13 @@ def get_fundamental_variables(self): I = pybamm.maximum(i_var, i_input) # Update derived variables - i_cell = I / (param.n_electrodes_parallel * param.A_cc) + i_cell = I / (self.param.n_electrodes_parallel * self.param.A_cc) variables = { "Current variable [A]": i_var, "Total current density [A.m-2]": i_cell, "Current [A]": I, - "C-rate": I / param.Q, + "C-rate": I / self.param.Q, } return variables diff --git a/src/pybamm/models/submodels/interface/base_interface.py b/src/pybamm/models/submodels/interface/base_interface.py index ab9b80eae0..0ad08d5454 100644 --- a/src/pybamm/models/submodels/interface/base_interface.py +++ b/src/pybamm/models/submodels/interface/base_interface.py @@ -61,7 +61,6 @@ def _get_exchange_current_density(self, variables): j0 : :class: `pybamm.Symbol` The exchange current density. """ - param = self.param phase_param = self.phase_param domain, Domain = self.domain_Domain phase_name = self.phase_name @@ -132,8 +131,8 @@ def _get_exchange_current_density(self, variables): elif self.reaction == "lithium metal plating": # compute T on the surface of the anode (interface with separator) T = pybamm.boundary_value(T, "right") - c_Li_metal = 1 / param.V_bar_Li - j0 = param.j0_Li_metal(c_e, c_Li_metal, T) + c_Li_metal = 1 / self.param.V_bar_Li + j0 = self.param.j0_Li_metal(c_e, c_Li_metal, T) elif self.reaction == "lead-acid main": # If variable was broadcast, take only the orphan @@ -150,7 +149,7 @@ def _get_exchange_current_density(self, variables): if self.domain == "negative": j0 = pybamm.Scalar(0) elif self.domain == "positive": - j0 = param.p.prim.j0_Ox(c_e, T) + j0 = self.param.p.prim.j0_Ox(c_e, T) return j0 diff --git a/src/pybamm/models/submodels/interface/kinetics/diffusion_limited.py b/src/pybamm/models/submodels/interface/kinetics/diffusion_limited.py index 08c2db2175..19b8dbea97 100644 --- a/src/pybamm/models/submodels/interface/kinetics/diffusion_limited.py +++ b/src/pybamm/models/submodels/interface/kinetics/diffusion_limited.py @@ -68,7 +68,6 @@ def get_coupled_variables(self, variables): return variables def _get_diffusion_limited_current_density(self, variables): - param = self.param if self.domain == "negative": if self.order == "leading": j_p = variables[ @@ -81,10 +80,10 @@ def _get_diffusion_limited_current_density(self, variables): c_ox_s = variables["Separator oxygen concentration [mol.m-3]"] N_ox_neg_sep_interface = ( -pybamm.boundary_value(tor_s, "left") - * param.D_ox + * self.param.D_ox * pybamm.boundary_gradient(c_ox_s, "left") ) - j = -N_ox_neg_sep_interface / -param.s_ox_Ox / param.n.L + j = -N_ox_neg_sep_interface / -self.param.s_ox_Ox / self.param.n.L return j diff --git a/src/pybamm/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py b/src/pybamm/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py index 959cb027c1..b49993afd8 100644 --- a/src/pybamm/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py +++ b/src/pybamm/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py @@ -93,8 +93,9 @@ def get_coupled_variables(self, variables): return variables def _get_overpotential(self, j, j0, ne, T, u): - param = self.param - return (2 * (param.R * T) / param.F / ne) * pybamm.arcsinh(j / (2 * j0 * u)) + return (2 * (self.param.R * T) / self.param.F / ne) * pybamm.arcsinh( + j / (2 * j0 * u) + ) class CurrentForInverseButlerVolmer(BaseInterface): diff --git a/src/pybamm/models/submodels/interface/sei/sei_growth.py b/src/pybamm/models/submodels/interface/sei/sei_growth.py index bed4b04952..2f506323ce 100644 --- a/src/pybamm/models/submodels/interface/sei/sei_growth.py +++ b/src/pybamm/models/submodels/interface/sei/sei_growth.py @@ -80,7 +80,6 @@ def get_fundamental_variables(self): return variables def get_coupled_variables(self, variables): - param = self.param phase_param = self.phase_param domain, Domain = self.domain_Domain SEI_option = getattr(getattr(self.options, domain), self.phase)["SEI"] @@ -118,7 +117,7 @@ def get_coupled_variables(self, variables): R_sei = phase_param.R_sei eta_SEI = delta_phi - phase_param.U_sei - j * L_sei * R_sei # Thermal prefactor for reaction, interstitial and EC models - F_RT = param.F / (param.R * T) + F_RT = self.param.F / (self.param.R * T) # Define alpha_SEI depending on whether it is symmetric or asymmetric. This # applies to "reaction limited" and "EC reaction limited" @@ -138,12 +137,12 @@ def get_coupled_variables(self, variables): elif SEI_option == "interstitial-diffusion limited": # Scott Marquis thesis (eq. 5.96) j_sei = -( - phase_param.D_li * phase_param.c_li_0 * param.F / L_sei_outer + phase_param.D_li * phase_param.c_li_0 * self.param.F / L_sei_outer ) * pybamm.exp(-F_RT * delta_phi) elif SEI_option == "solvent-diffusion limited": # Scott Marquis thesis (eq. 5.91) - j_sei = -phase_param.D_sol * phase_param.c_sol * param.F / L_sei_outer + j_sei = -phase_param.D_sol * phase_param.c_sol * self.param.F / L_sei_outer elif SEI_option.startswith("ec reaction limited"): # we have a linear system for j and c @@ -159,7 +158,7 @@ def get_coupled_variables(self, variables): k_exp = phase_param.k_sei * pybamm.exp(-alpha_SEI * F_RT * eta_SEI) L_over_D = L_sei / phase_param.D_ec c_0 = phase_param.c_ec_0 - j_sei = -param.F * c_0 * k_exp / (1 + L_over_D * k_exp) + j_sei = -self.param.F * c_0 * k_exp / (1 + L_over_D * k_exp) c_ec = c_0 / (1 + L_over_D * k_exp) # Get variables related to the concentration @@ -177,7 +176,9 @@ def get_coupled_variables(self, variables): inner_sei_proportion = phase_param.inner_sei_proportion # All SEI growth mechanisms assumed to have Arrhenius dependence - Arrhenius = pybamm.exp(phase_param.E_sei / param.R * (1 / param.T_ref - 1 / T)) + Arrhenius = pybamm.exp( + phase_param.E_sei / self.param.R * (1 / self.param.T_ref - 1 / T) + ) j_inner = inner_sei_proportion * Arrhenius * j_sei j_outer = (1 - inner_sei_proportion) * Arrhenius * j_sei @@ -192,7 +193,6 @@ def get_coupled_variables(self, variables): def set_rhs(self, variables): phase_param = self.phase_param - param = self.param domain, Domain = self.domain_Domain if self.reaction_loc == "x-average": @@ -249,10 +249,10 @@ def set_rhs(self, variables): # V_bar / a converts from SEI moles to SEI thickness # V_bar * j_sei / (F * z_sei) is the rate of SEI thickness change dLdt_SEI_inner = ( - phase_param.V_bar_inner * j_inner / (param.F * phase_param.z_sei) + phase_param.V_bar_inner * j_inner / (self.param.F * phase_param.z_sei) ) dLdt_SEI_outer = ( - phase_param.V_bar_outer * j_outer / (param.F * phase_param.z_sei) + phase_param.V_bar_outer * j_outer / (self.param.F * phase_param.z_sei) ) # we have to add the spreading rate to account for cracking diff --git a/src/pybamm/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py b/src/pybamm/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py index c69312e342..7ecad6fa4c 100644 --- a/src/pybamm/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py +++ b/src/pybamm/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py @@ -58,9 +58,7 @@ def get_coupled_variables(self, variables): # TODO: allow charge and convection? v_box = pybamm.Scalar(0) - param = self.param - - N_ox_diffusion = -tor * param.D_ox * pybamm.grad(c_ox) + N_ox_diffusion = -tor * self.param.D_ox * pybamm.grad(c_ox) N_ox = N_ox_diffusion + c_ox * v_box # Flux in the negative electrode is zero @@ -73,8 +71,6 @@ def get_coupled_variables(self, variables): return variables def set_rhs(self, variables): - param = self.param - eps_s = variables["Separator porosity"] eps_p = variables["Positive electrode porosity"] eps = pybamm.concatenation(eps_s, eps_p) @@ -93,12 +89,12 @@ def set_rhs(self, variables): ] source_terms = pybamm.concatenation( pybamm.FullBroadcast(0, "separator", "current collector"), - param.s_ox_Ox * a_j_ox, + self.param.s_ox_Ox * a_j_ox, ) self.rhs = { c_ox: (1 / eps) - * (-pybamm.div(N_ox) + source_terms / param.F - c_ox * deps_dt) + * (-pybamm.div(N_ox) + source_terms / self.param.F - c_ox * deps_dt) } def set_boundary_conditions(self, variables): diff --git a/src/pybamm/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py b/src/pybamm/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py index 056c7f6715..bdc064c340 100644 --- a/src/pybamm/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py +++ b/src/pybamm/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py @@ -41,8 +41,6 @@ def get_coupled_variables(self, variables): return variables def set_rhs(self, variables): - param = self.param - c_ox_av = variables["X-averaged oxygen concentration [mol.m-3]"] eps_n_av = variables["X-averaged negative electrode porosity"] @@ -62,16 +60,21 @@ def set_rhs(self, variables): ] source_terms = ( - param.n.L * param.s_ox_Ox * a_j_ox_n_av - + param.p.L * param.s_ox_Ox * a_j_ox_p_av + self.param.n.L * self.param.s_ox_Ox * a_j_ox_n_av + + self.param.p.L * self.param.s_ox_Ox * a_j_ox_p_av ) self.rhs = { c_ox_av: 1 - / (param.n.L * eps_n_av + param.s.L * eps_s_av + param.p.L * eps_p_av) + / ( + self.param.n.L * eps_n_av + + self.param.s.L * eps_s_av + + self.param.p.L * eps_p_av + ) * ( - source_terms / param.F - - c_ox_av * (param.n.L * deps_n_dt_av + param.p.L * deps_p_dt_av) + source_terms / self.param.F + - c_ox_av + * (self.param.n.L * deps_n_dt_av + self.param.p.L * deps_p_dt_av) ) } diff --git a/src/pybamm/models/submodels/particle/base_particle.py b/src/pybamm/models/submodels/particle/base_particle.py index fe37d2ff2e..b774e58a0c 100644 --- a/src/pybamm/models/submodels/particle/base_particle.py +++ b/src/pybamm/models/submodels/particle/base_particle.py @@ -28,7 +28,6 @@ def __init__(self, param, domain, options, phase="primary"): self.size_distribution = domain_options["particle size"] == "distribution" def _get_effective_diffusivity(self, c, T, current): - param = self.param domain, Domain = self.domain_Domain domain_param = self.domain_param phase_param = self.phase_param @@ -60,7 +59,7 @@ def _get_effective_diffusivity(self, c, T, current): Omega = pybamm.r_average(phase_param.Omega(sto, T)) E = pybamm.r_average(phase_param.E(sto, T)) nu = phase_param.nu - theta_M = Omega / (param.R * T) * (2 * Omega * E) / (9 * (1 - nu)) + theta_M = Omega / (self.param.R * T) * (2 * Omega * E) / (9 * (1 - nu)) stress_factor = 1 + theta_M * (c - domain_param.c_0) else: stress_factor = 1 diff --git a/src/pybamm/models/submodels/particle/fickian_diffusion.py b/src/pybamm/models/submodels/particle/fickian_diffusion.py index 31c5e6be6c..634e2ce730 100644 --- a/src/pybamm/models/submodels/particle/fickian_diffusion.py +++ b/src/pybamm/models/submodels/particle/fickian_diffusion.py @@ -130,7 +130,6 @@ def get_fundamental_variables(self): def get_coupled_variables(self, variables): domain, Domain = self.domain_Domain phase_name = self.phase_name - param = self.param if self.size_distribution is False: if self.x_average is False: @@ -208,7 +207,7 @@ def get_coupled_variables(self, variables): * pybamm.div(N_s), f"{Domain} {phase_name}particle bc [mol.m-4]": -j * R_nondim - / param.F + / self.param.F / pybamm.surf(D_eff), } ) diff --git a/src/pybamm/models/submodels/particle/msmr_diffusion.py b/src/pybamm/models/submodels/particle/msmr_diffusion.py index fb712dcdef..8967116ce9 100644 --- a/src/pybamm/models/submodels/particle/msmr_diffusion.py +++ b/src/pybamm/models/submodels/particle/msmr_diffusion.py @@ -136,7 +136,6 @@ def get_fundamental_variables(self): def get_coupled_variables(self, variables): domain, Domain = self.domain_Domain phase_name = self.phase_name - param = self.param if self.size_distribution is False: if self.x_average is False: @@ -236,7 +235,7 @@ def get_coupled_variables(self, variables): / dxdU, f"{Domain} {phase_name}particle bc [V.m-1]": j * R_nondim - / param.F + / self.param.F / pybamm.surf(c_max * x * (1 - x) * f * D_eff), } ) diff --git a/src/pybamm/models/submodels/particle/x_averaged_polynomial_profile.py b/src/pybamm/models/submodels/particle/x_averaged_polynomial_profile.py index 8b4b7ffe7c..9dccc0a6c4 100644 --- a/src/pybamm/models/submodels/particle/x_averaged_polynomial_profile.py +++ b/src/pybamm/models/submodels/particle/x_averaged_polynomial_profile.py @@ -97,7 +97,6 @@ def get_fundamental_variables(self): def get_coupled_variables(self, variables): domain = self.domain - param = self.param c_s_av = variables[f"Average {domain} particle concentration [mol.m-3]"] T_av = variables[f"X-averaged {domain} electrode temperature [K]"] @@ -135,7 +134,7 @@ def get_coupled_variables(self, variables): # an extra algebraic equation to solve. For now, using the average c is an # ok approximation and means the SPM(e) still gives a system of ODEs rather # than DAEs. - c_s_surf_xav = c_s_av - (j_xav * R / 5 / param.F / D_eff_av) + c_s_surf_xav = c_s_av - (j_xav * R / 5 / self.param.F / D_eff_av) elif self.name == "quartic profile": # The surface concentration is computed from the average concentration, # the average concentration gradient and the boundary flux (see notes @@ -144,7 +143,9 @@ def get_coupled_variables(self, variables): q_s_av = variables[ f"Average {domain} particle concentration gradient [mol.m-4]" ] - c_s_surf_xav = c_s_av + R / 35 * (8 * q_s_av - (j_xav / param.F / D_eff_av)) + c_s_surf_xav = c_s_av + R / 35 * ( + 8 * q_s_av - (j_xav / self.param.F / D_eff_av) + ) # Set concentration depending on polynomial order # Since c_s_xav doesn't depend on x, we need to define a spatial @@ -223,7 +224,6 @@ def set_rhs(self, variables): # using this model with 2D current collectors with the finite element # method (see #1399) domain = self.domain - param = self.param if self.size_distribution is False: c_s_av = variables[f"Average {domain} particle concentration [mol.m-3]"] @@ -243,7 +243,7 @@ def set_rhs(self, variables): # eq 15 of Subramanian2005 # equivalent to dcdt = -i_cc / (eps * F * L) - dcdt = -3 * j_xav / param.F / R + dcdt = -3 * j_xav / self.param.F / R if self.size_distribution is False: self.rhs = {c_s_av: pybamm.source(dcdt, c_s_av)} @@ -262,7 +262,7 @@ def set_rhs(self, variables): # eq 30 of Subramanian2005 dqdt = ( -30 * pybamm.surf(D_eff_xav) * q_s_av / R**2 - - 45 / 2 * j_xav / param.F / R**2 + - 45 / 2 * j_xav / self.param.F / R**2 ) self.rhs[q_s_av] = pybamm.source(dqdt, q_s_av) diff --git a/src/pybamm/models/submodels/porosity/reaction_driven_porosity_ode.py b/src/pybamm/models/submodels/porosity/reaction_driven_porosity_ode.py index f82a73ae68..8675842ebe 100644 --- a/src/pybamm/models/submodels/porosity/reaction_driven_porosity_ode.py +++ b/src/pybamm/models/submodels/porosity/reaction_driven_porosity_ode.py @@ -46,8 +46,6 @@ def get_fundamental_variables(self): return variables def get_coupled_variables(self, variables): - param = self.param - depsdt_dict = {} for domain in self.options.whole_cell_domains: domain_param = self.param.domain_params[domain.split()[0]] @@ -59,14 +57,14 @@ def get_coupled_variables(self, variables): f"X-averaged {domain} volumetric " "interfacial current density [A.m-3]" ] - depsdt_k_av = domain_param.DeltaVsurf * a_j_k_av / param.F + depsdt_k_av = domain_param.DeltaVsurf * a_j_k_av / self.param.F depsdt_k = pybamm.PrimaryBroadcast(depsdt_k_av, domain) else: Domain = domain.capitalize() a_j_k = variables[ f"{Domain} volumetric interfacial current density [A.m-3]" ] - depsdt_k = domain_param.DeltaVsurf * a_j_k / param.F + depsdt_k = domain_param.DeltaVsurf * a_j_k / self.param.F depsdt_dict[domain] = depsdt_k variables.update(self._get_standard_porosity_change_variables(depsdt_dict)) diff --git a/src/pybamm/models/submodels/thermal/base_thermal.py b/src/pybamm/models/submodels/thermal/base_thermal.py index c5ebbc7dbd..42d90f1bcf 100644 --- a/src/pybamm/models/submodels/thermal/base_thermal.py +++ b/src/pybamm/models/submodels/thermal/base_thermal.py @@ -34,7 +34,6 @@ def _get_standard_fundamental_variables(self, T_dict): For more information about this method in general, see :meth:`pybamm.base_submodel._get_standard_fundamental_variables` """ - param = self.param # The variable T is the concatenation of the temperature in the middle domains # (e.g. negative electrode, separator and positive electrode for a full cell), @@ -46,7 +45,7 @@ def _get_standard_fundamental_variables(self, T_dict): # (y, z) only and time y = pybamm.standard_spatial_vars.y z = pybamm.standard_spatial_vars.z - T_amb = param.T_amb(y, z, pybamm.t) + T_amb = self.param.T_amb(y, z, pybamm.t) T_amb_av = self._yz_average(T_amb) variables = { @@ -69,8 +68,6 @@ def _get_standard_fundamental_variables(self, T_dict): return variables def _get_standard_coupled_variables(self, variables): - param = self.param - # Ohmic heating in solid i_s_p = variables["Positive electrode current density [A.m-2]"] phi_s_p = variables["Positive electrode potential [V]"] @@ -78,7 +75,7 @@ def _get_standard_coupled_variables(self, variables): if self.options.electrode_types["negative"] == "planar": i_boundary_cc = variables["Current collector current density [A.m-2]"] T_n = variables["Negative electrode temperature [K]"] - Q_ohm_s_n = i_boundary_cc**2 / param.n.sigma(T_n) + Q_ohm_s_n = i_boundary_cc**2 / self.param.n.sigma(T_n) else: i_s_n = variables["Negative electrode current density [A.m-2]"] phi_s_n = variables["Negative electrode potential [V]"] @@ -199,11 +196,11 @@ def _get_standard_coupled_variables(self, variables): # Compute the integrated heat source per unit simulated electrode-pair area # in W.m-2. Note: this can still be a function of y and z for # higher-dimensional pouch cell models - Q_ohm_Wm2 = Q_ohm_av * param.L - Q_rxn_Wm2 = Q_rxn_av * param.L - Q_rev_Wm2 = Q_rev_av * param.L - Q_mix_Wm2 = Q_mix_av * param.L - Q_Wm2 = Q_av * param.L + Q_ohm_Wm2 = Q_ohm_av * self.param.L + Q_rxn_Wm2 = Q_rxn_av * self.param.L + Q_rev_Wm2 = Q_rev_av * self.param.L + Q_mix_Wm2 = Q_mix_av * self.param.L + Q_Wm2 = Q_av * self.param.L # Now average over the electrode height and width Q_ohm_Wm2_av = self._yz_average(Q_ohm_Wm2) @@ -216,8 +213,8 @@ def _get_standard_coupled_variables(self, variables): # the product of electrode height * electrode width * electrode stack thickness # Note: we multiply by the number of electrode pairs, since the Q_xx_Wm2_av # variables are per electrode pair - n_elec = param.n_electrodes_parallel - A = param.L_y * param.L_z # *modelled* electrode area + n_elec = self.param.n_electrodes_parallel + A = self.param.L_y * self.param.L_z # *modelled* electrode area Q_ohm_W = Q_ohm_Wm2_av * n_elec * A Q_rxn_W = Q_rxn_Wm2_av * n_elec * A Q_rev_W = Q_rev_Wm2_av * n_elec * A @@ -226,7 +223,7 @@ def _get_standard_coupled_variables(self, variables): # Compute volume-averaged heat source terms over the *entire cell volume*, not # the product of electrode height * electrode width * electrode stack thickness - V = param.V_cell # *actual* cell volume + V = self.param.V_cell # *actual* cell volume Q_ohm_vol_av = Q_ohm_W / V Q_rxn_vol_av = Q_rxn_W / V Q_rev_vol_av = Q_rev_W / V @@ -235,7 +232,7 @@ def _get_standard_coupled_variables(self, variables): # Effective heat capacity T_vol_av = variables["Volume-averaged cell temperature [K]"] - rho_c_p_eff_av = param.rho_c_p_eff(T_vol_av) + rho_c_p_eff_av = self.param.rho_c_p_eff(T_vol_av) variables.update( { @@ -314,7 +311,6 @@ def _current_collector_heating(self, variables): def _heat_of_mixing(self, variables): """Compute heat of mixing source terms.""" - param = self.param if self.options["heat of mixing"] == "true": F = pybamm.constants.F.value @@ -339,8 +335,10 @@ def _heat_of_mixing(self, variables): T_n = variables["Negative electrode temperature [K]"] T_n_part = pybamm.PrimaryBroadcast(T_n, ["negative particle"]) dc_n_dr2 = pybamm.inner(pybamm.grad(c_n), pybamm.grad(c_n)) - D_n = param.n.prim.D(c_n, T_n_part) - dUeq_n = param.n.prim.U(c_n / param.n.prim.c_max, T_n_part).diff(c_n) + D_n = self.param.n.prim.D(c_n, T_n_part) + dUeq_n = self.param.n.prim.U( + c_n / self.param.n.prim.c_max, T_n_part + ).diff(c_n) integrand_r_n = D_n * dc_n_dr2 * dUeq_n integration_variable_r_n = [ pybamm.SpatialVariable("r", domain=integrand_r_n.domain) @@ -360,8 +358,10 @@ def _heat_of_mixing(self, variables): T_p = variables["Positive electrode temperature [K]"] T_p_part = pybamm.PrimaryBroadcast(T_p, ["positive particle"]) dc_p_dr2 = pybamm.inner(pybamm.grad(c_p), pybamm.grad(c_p)) - D_p = param.p.prim.D(c_p, T_p_part) - dUeq_p = param.p.prim.U(c_p / param.p.prim.c_max, T_p_part).diff(c_p) + D_p = self.param.p.prim.D(c_p, T_p_part) + dUeq_p = self.param.p.prim.U(c_p / self.param.p.prim.c_max, T_p_part).diff( + c_p + ) integrand_r_p = D_p * dc_p_dr2 * dUeq_p integration_variable_r_p = [ pybamm.SpatialVariable("r", domain=integrand_r_p.domain) diff --git a/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py b/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py index fb026a9a0a..5c29ef0a9f 100644 --- a/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py +++ b/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py @@ -86,24 +86,23 @@ def set_rhs(self, variables): } def set_boundary_conditions(self, variables): - param = self.param T_surf = variables["Surface temperature [K]"] T_av = variables["X-averaged cell temperature [K]"] # Find tab locations (top vs bottom) - L_y = param.L_y - L_z = param.L_z - neg_tab_z = param.n.centre_z_tab - pos_tab_z = param.p.centre_z_tab + L_y = self.param.L_y + L_z = self.param.L_z + neg_tab_z = self.param.n.centre_z_tab + pos_tab_z = self.param.p.centre_z_tab neg_tab_top_bool = pybamm.Equality(neg_tab_z, L_z) neg_tab_bottom_bool = pybamm.Equality(neg_tab_z, 0) pos_tab_top_bool = pybamm.Equality(pos_tab_z, L_z) pos_tab_bottom_bool = pybamm.Equality(pos_tab_z, 0) # Calculate tab vs non-tab area on top and bottom - neg_tab_area = param.n.L_tab * param.n.L_cc - pos_tab_area = param.p.L_tab * param.p.L_cc - total_area = param.L * param.L_y + neg_tab_area = self.param.n.L_tab * self.param.n.L_cc + pos_tab_area = self.param.p.L_tab * self.param.p.L_cc + total_area = self.param.L * self.param.L_y non_tab_top_area = ( total_area - neg_tab_area * neg_tab_top_bool @@ -118,10 +117,10 @@ def set_boundary_conditions(self, variables): # Calculate heat fluxes weighted by area # Note: can't do y-average of h_edge here since y isn't meshed. Evaluate at # midpoint. - q_tab_n = -param.n.h_tab * (T_av - T_surf) - q_tab_p = -param.p.h_tab * (T_av - T_surf) - q_edge_top = -param.h_edge(L_y / 2, L_z) * (T_av - T_surf) - q_edge_bottom = -param.h_edge(L_y / 2, 0) * (T_av - T_surf) + q_tab_n = -self.param.n.h_tab * (T_av - T_surf) + q_tab_p = -self.param.p.h_tab * (T_av - T_surf) + q_edge_top = -self.param.h_edge(L_y / 2, L_z) * (T_av - T_surf) + q_edge_bottom = -self.param.h_edge(L_y / 2, 0) * (T_av - T_surf) q_top = ( q_tab_n * neg_tab_area * neg_tab_top_bool + q_tab_p * pos_tab_area * pos_tab_top_bool @@ -136,7 +135,7 @@ def set_boundary_conditions(self, variables): # just use left and right for clarity # left = bottom of cell (z=0) # right = top of cell (z=L_z) - lambda_eff = param.lambda_eff(T_av) + lambda_eff = self.param.lambda_eff(T_av) self.boundary_conditions = { T_av: { "left": ( From ab0020ae5bfb70bb24ae369eb8f1d207923cfd66 Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Thu, 10 Oct 2024 13:37:21 -0400 Subject: [PATCH 7/8] More accurate `QuickPlot`s with Hermite interpolation (#4483) * Update CHANGELOG.md accurate quickplots * evenly sample sub-solutions * lowercase variable * move `_solver_args` inside class --------- Co-authored-by: Eric G. Kratz --- CHANGELOG.md | 1 + src/pybamm/plotting/quick_plot.py | 22 +++++++++++++++ tests/unit/test_plotting/test_quick_plot.py | 30 +++++++++++++-------- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84f01d3797..a03ec39715 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Features +- Improved `QuickPlot` accuracy for simulations with Hermite interpolation. ([#4483](https://github.com/pybamm-team/PyBaMM/pull/4483)) - Added Hermite interpolation to the (`IDAKLUSolver`) that improves the accuracy and performance of post-processing variables. ([#4464](https://github.com/pybamm-team/PyBaMM/pull/4464)) - Added `BasicDFN` model for sodium-ion batteries ([#4451](https://github.com/pybamm-team/PyBaMM/pull/4451)) - Added sensitivity calculation support for `pybamm.Simulation` and `pybamm.Experiment` ([#4415](https://github.com/pybamm-team/PyBaMM/pull/4415)) diff --git a/src/pybamm/plotting/quick_plot.py b/src/pybamm/plotting/quick_plot.py index cddce58d77..babfd2e761 100644 --- a/src/pybamm/plotting/quick_plot.py +++ b/src/pybamm/plotting/quick_plot.py @@ -84,6 +84,9 @@ class QuickPlot: variable_limits : str or dict of str, optional How to set the axis limits (for 0D or 1D variables) or colorbar limits (for 2D variables). Options are: + n_t_linear: int, optional + The number of linearly spaced time points added to the t axis for each sub-solution. + Note: this is only used if the solution has hermite interpolation enabled. - "fixed" (default): keep all axes fixes so that all data is visible - "tight": make axes tight to plot at each time @@ -105,6 +108,7 @@ def __init__( time_unit=None, spatial_unit="um", variable_limits="fixed", + n_t_linear=100, ): solutions = self.preprocess_solutions(solutions) @@ -169,6 +173,24 @@ def __init__( min_t = np.min([t[0] for t in self.ts_seconds]) max_t = np.max([t[-1] for t in self.ts_seconds]) + hermite_interp = all(sol.hermite_interpolation for sol in solutions) + + def t_sample(sol): + if hermite_interp and n_t_linear > 2: + # Linearly spaced time points + t_linspace = np.linspace(sol.t[0], sol.t[-1], n_t_linear + 2)[1:-1] + t_plot = np.union1d(sol.t, t_linspace) + else: + t_plot = sol.t + return t_plot + + ts_seconds = [] + for sol in solutions: + # Sample time points for each sub-solution + t_sol = [t_sample(sub_sol) for sub_sol in sol.sub_solutions] + ts_seconds.append(np.concatenate(t_sol)) + self.ts_seconds = ts_seconds + # Set timescale if time_unit is None: # defaults depend on how long the simulation is diff --git a/tests/unit/test_plotting/test_quick_plot.py b/tests/unit/test_plotting/test_quick_plot.py index d5d994117d..188a725680 100644 --- a/tests/unit/test_plotting/test_quick_plot.py +++ b/tests/unit/test_plotting/test_quick_plot.py @@ -7,7 +7,12 @@ class TestQuickPlot: - def test_simple_ode_model(self): + _solver_args = [pybamm.CasadiSolver()] + if pybamm.has_idaklu(): + _solver_args.append(pybamm.IDAKLUSolver()) + + @pytest.mark.parametrize("solver", _solver_args) + def test_simple_ode_model(self, solver): model = pybamm.lithium_ion.BaseModel(name="Simple ODE Model") whole_cell = ["negative electrode", "separator", "positive electrode"] @@ -48,9 +53,6 @@ def test_simple_ode_model(self): "NaN variable": pybamm.Scalar(np.nan), } - # ODEs only (don't use Jacobian) - model.use_jacobian = False - # Process and solve geometry = model.default_geometry param = model.default_parameter_values @@ -59,7 +61,6 @@ def test_simple_ode_model(self): mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) - solver = model.default_solver t_eval = np.linspace(0, 2, 100) solution = solver.solve(model, t_eval) quick_plot = pybamm.QuickPlot( @@ -142,6 +143,13 @@ def test_simple_ode_model(self): assert quick_plot.n_rows == 2 assert quick_plot.n_cols == 1 + if solution.hermite_interpolation: + t_plot = np.union1d( + solution.t, np.linspace(solution.t[0], solution.t[-1], 100 + 2)[1:-1] + ) + else: + t_plot = t_eval + # Test different time units quick_plot = pybamm.QuickPlot(solution, ["a"]) assert quick_plot.time_scaling_factor == 1 @@ -149,28 +157,28 @@ def test_simple_ode_model(self): quick_plot.plot(0) assert quick_plot.time_scaling_factor == 1 np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_xdata(), t_eval + quick_plot.plots[("a",)][0][0].get_xdata(), t_plot ) np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_eval + quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_plot ) quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="minutes") quick_plot.plot(0) assert quick_plot.time_scaling_factor == 60 np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_xdata(), t_eval / 60 + quick_plot.plots[("a",)][0][0].get_xdata(), t_plot / 60 ) np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_eval + quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_plot ) quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="hours") quick_plot.plot(0) assert quick_plot.time_scaling_factor == 3600 np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_xdata(), t_eval / 3600 + quick_plot.plots[("a",)][0][0].get_xdata(), t_plot / 3600 ) np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_eval + quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_plot ) with pytest.raises(ValueError, match="time unit"): pybamm.QuickPlot(solution, ["a"], time_unit="bad unit") From 7ea74f366279b2d9d8511e57e7bdc58b6b2e67a6 Mon Sep 17 00:00:00 2001 From: Medha Bhardwaj <143182673+medha-14@users.noreply.github.com> Date: Thu, 10 Oct 2024 23:40:00 +0530 Subject: [PATCH 8/8] Adds doc strings for attributes in `base_model` and `base_submodel` (#4480) * added docstring for some attributes * documented all public attribtes in base_model * documented all public attributes in base_submodel * minor changes in base_submodel * doc test fixes * handled warnings --------- Co-authored-by: Eric G. Kratz --- src/pybamm/models/base_model.py | 73 ++++++++++++++------ src/pybamm/models/submodels/base_submodel.py | 31 ++++++--- 2 files changed, 73 insertions(+), 31 deletions(-) diff --git a/src/pybamm/models/base_model.py b/src/pybamm/models/base_model.py index 989465e375..f6f47acc55 100644 --- a/src/pybamm/models/base_model.py +++ b/src/pybamm/models/base_model.py @@ -19,29 +19,27 @@ class BaseModel: Attributes ---------- name: str - A string giving the name of the model. + A string representing the name of the model. submodels: dict A dictionary of submodels that the model is composed of. - boundary_conditions: dict - A dictionary that maps expressions (variables) to expressions that represent - the boundary conditions. - variables: dict - A dictionary that maps strings to expressions that represent - the useful variables. - use_jacobian : bool + use_jacobian: bool Whether to use the Jacobian when solving the model (default is True). - convert_to_format : str - Whether to convert the expression trees representing the rhs and - algebraic equations, Jacobain (if using) and events into a different format: + convert_to_format: str + Specifies the format to convert the expression trees representing the RHS, + algebraic equations, Jacobian, and events. + Options are: - - None: keep PyBaMM expression tree structure. - - "python": convert into pure python code that will calculate the result of \ - calling `evaluate(t, y)` on the given expression treeself. - - "casadi": convert into CasADi expression tree, which then uses CasADi's \ - algorithm to calculate the Jacobian. - - "jax": convert into JAX expression tree + - None: retain PyBaMM expression tree structure. + - "python": convert to Python code for evaluating `evaluate(t, y)` on expressions. + - "casadi": convert to CasADi expression tree for Jacobian calculation. + - "jax": convert to JAX expression tree. Default is "casadi". + is_discretised: bool + Indicates whether the model has been discretised (default is False). + y_slices: None or list + Slices of the concatenated state vector after discretisation, used to track + different submodels in the full concatenated solution vector. """ def __init__(self, name="Unnamed model"): @@ -144,6 +142,8 @@ def name(self, value): @property def rhs(self): + """Returns a dictionary mapping expressions (variables) to expressions that represent + the right-hand side (RHS) of the model's differential equations.""" return self._rhs @rhs.setter @@ -152,6 +152,8 @@ def rhs(self, rhs): @property def algebraic(self): + """Returns a dictionary mapping expressions (variables) to expressions that represent + the algebraic equations of the model.""" return self._algebraic @algebraic.setter @@ -160,6 +162,8 @@ def algebraic(self, algebraic): @property def initial_conditions(self): + """Returns a dictionary mapping expressions (variables) to expressions that represent + the initial conditions for the state variables.""" return self._initial_conditions @initial_conditions.setter @@ -170,6 +174,8 @@ def initial_conditions(self, initial_conditions): @property def boundary_conditions(self): + """Returns a dictionary mapping expressions (variables) to expressions representing + the boundary conditions of the model.""" return self._boundary_conditions @boundary_conditions.setter @@ -178,6 +184,7 @@ def boundary_conditions(self, boundary_conditions): @property def variables(self): + """Returns a dictionary mapping strings to expressions representing the model's useful variables.""" return self._variables @variables.setter @@ -200,9 +207,7 @@ def variable_names(self): @property def variables_and_events(self): - """ - Returns variables and events in a single dictionary - """ + """Returns a dictionary containing both models variables and events.""" try: return self._variables_and_events except AttributeError: @@ -214,6 +219,8 @@ def variables_and_events(self): @property def events(self): + """Returns a dictionary mapping expressions (variables) to expressions that represent + the initial conditions for the state variables.""" return self._events @events.setter @@ -222,6 +229,7 @@ def events(self, events): @property def concatenated_rhs(self): + """Returns the concatenated right-hand side (RHS) expressions for the model after discretisation.""" return self._concatenated_rhs @concatenated_rhs.setter @@ -230,6 +238,7 @@ def concatenated_rhs(self, concatenated_rhs): @property def concatenated_algebraic(self): + """Returns the concatenated algebraic equations for the model after discretisation.""" return self._concatenated_algebraic @concatenated_algebraic.setter @@ -238,6 +247,8 @@ def concatenated_algebraic(self, concatenated_algebraic): @property def concatenated_initial_conditions(self): + """Returns the initial conditions for all variables after discretization, providing the + initial values for the state variables.""" return self._concatenated_initial_conditions @concatenated_initial_conditions.setter @@ -246,6 +257,7 @@ def concatenated_initial_conditions(self, concatenated_initial_conditions): @property def mass_matrix(self): + """Returns the mass matrix for the system of differential equations after discretisation.""" return self._mass_matrix @mass_matrix.setter @@ -254,6 +266,7 @@ def mass_matrix(self, mass_matrix): @property def mass_matrix_inv(self): + """Returns the inverse of the mass matrix for the differential equations after discretisation.""" return self._mass_matrix_inv @mass_matrix_inv.setter @@ -262,6 +275,7 @@ def mass_matrix_inv(self, mass_matrix_inv): @property def jacobian(self): + """Returns the Jacobian matrix for the model, computed automatically if `use_jacobian` is True.""" return self._jacobian @jacobian.setter @@ -270,6 +284,8 @@ def jacobian(self, jacobian): @property def jacobian_rhs(self): + """Returns the Jacobian matrix for the right-hand side (RHS) part of the model, computed + if `use_jacobian` is True.""" return self._jacobian_rhs @jacobian_rhs.setter @@ -278,6 +294,8 @@ def jacobian_rhs(self, jacobian_rhs): @property def jacobian_algebraic(self): + """Returns the Jacobian matrix for the algebraic part of the model, computed automatically + during solver setup if `use_jacobian` is True.""" return self._jacobian_algebraic @jacobian_algebraic.setter @@ -286,6 +304,7 @@ def jacobian_algebraic(self, jacobian_algebraic): @property def param(self): + """Returns a dictionary to store parameter values for the model.""" return self._param @param.setter @@ -294,6 +313,7 @@ def param(self, values): @property def options(self): + """Returns the model options dictionary that allows customization of the model's behavior.""" return self._options @options.setter @@ -326,27 +346,32 @@ def length_scales(self, values): @property def geometry(self): + """Returns the geometry of the model.""" return self._geometry @property def default_var_pts(self): + """Returns a dictionary of the default variable points for the model, which is empty by default.""" return {} @property def default_geometry(self): + """Returns a dictionary of the default geometry for the model, which is empty by default.""" return {} @property def default_submesh_types(self): + """Returns a dictionary of the default submesh types for the model, which is empty by default.""" return {} @property def default_spatial_methods(self): + """Returns a dictionary of the default spatial methods for the model, which is empty by default.""" return {} @property def default_solver(self): - """Return default solver based on whether model is ODE/DAE or algebraic""" + """Returns the default solver for the model, based on whether it is an ODE/DAE or algebraic model.""" if len(self.rhs) == 0 and len(self.algebraic) != 0: return pybamm.CasadiAlgebraicSolver() else: @@ -354,15 +379,17 @@ def default_solver(self): @property def default_quick_plot_variables(self): + """Returns the default variables for quick plotting (None by default).""" return None @property def default_parameter_values(self): + """Returns the default parameter values for the model (an empty set of parameters by default).""" return pybamm.ParameterValues({}) @property def parameters(self): - """Returns all the parameters in the model""" + """Returns a list of all parameter symbols used in the model.""" self._parameters = self._find_symbols( (pybamm.Parameter, pybamm.InputParameter, pybamm.FunctionParameter) ) @@ -370,7 +397,7 @@ def parameters(self): @property def input_parameters(self): - """Returns all the input parameters in the model""" + """Returns a list of all input parameter symbols used in the model.""" if self._input_parameters is None: self._input_parameters = self._find_symbols(pybamm.InputParameter) return self._input_parameters diff --git a/src/pybamm/models/submodels/base_submodel.py b/src/pybamm/models/submodels/base_submodel.py index d5e313e153..6b83d1f292 100644 --- a/src/pybamm/models/submodels/base_submodel.py +++ b/src/pybamm/models/submodels/base_submodel.py @@ -28,14 +28,28 @@ class BaseSubModel(pybamm.BaseModel): Attributes ---------- - param: parameter class - The model parameter symbols - boundary_conditions: dict - A dictionary that maps expressions (variables) to expressions that represent - the boundary conditions - variables: dict - A dictionary that maps strings to expressions that represent - the useful variables + param : parameter class + The model parameter symbols. + domain : str + The domain of the submodel, could be either 'Negative', 'Positive', 'Separator', or None. + name : str + The name of the submodel. + external : bool + A boolean flag indicating whether the variables defined by the submodel will be + provided externally by the user. Set to False by default. + options : dict or pybamm.BatteryModelOptions + A dictionary or an instance of `pybamm.BatteryModelOptions` that stores configuration + options for the submodel. + phase_name : str + A string representing the phase of the submodel, which could be "primary", + "secondary", or an empty string if there is only one phase. + phase : str or None + The current phase of the submodel, which could be "primary", "secondary", or None. + boundary_conditions : dict + A dictionary mapping variables to their respective boundary conditions. + variables : dict + A dictionary mapping variable names (strings) to expressions or objects that + represent the useful variables for the submodel. """ def __init__( @@ -112,6 +126,7 @@ def domain(self, domain): @property def domain_Domain(self): + """Returns a tuple containing the current domain and its capitalized form.""" return self._domain, self._Domain def get_parameter_info(self, by_submodel=False):