diff --git a/.flake8 b/.flake8 index 5915461f29..bad0d2ef96 100644 --- a/.flake8 +++ b/.flake8 @@ -12,6 +12,7 @@ exclude= share, pyvenv.cfg, third-party, + sundials-5.0.0, ignore= # False positive for white space before ':' on list slice # black should format these correctly diff --git a/.gitignore b/.gitignore index b130bb9287..729976a997 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,9 @@ input/* !input/comsol_results/ !input/drive_cycles +# keep images required by notebooks +!examples/notebooks/Creating%20Models/SEI.png + # simulation outputs out/ config.py @@ -59,6 +62,7 @@ htmlcov/ pyproject.toml # virtual enviroment +env/ venv/ venv3.5/ PyBaMM-env/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 27d615ba05..4061ec3f63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,48 @@ +# [v0.2.1](https://github.com/pybamm-team/PyBaMM/tree/v0.2.1) - 2020-03-31 + +New expression tree node types, models, parameter sets and solvers, as well as general bug fixes and new examples. + +## Features + +- Store variable slices in model for inspection ([#925](https://github.com/pybamm-team/PyBaMM/pull/925)) +- Added LiNiCoO2 parameter set from Ecker et. al. ([#922](https://github.com/pybamm-team/PyBaMM/pull/922)) +- Made t_plus (optionally) a function of electrolyte concentration, and added (1 + dlnf/dlnc) to models ([#921](https://github.com/pybamm-team/PyBaMM/pull/921)) +- Added `DummySolver` for empty models ([#915](https://github.com/pybamm-team/PyBaMM/pull/915)) +- Added functionality to broadcast to edges ([#891](https://github.com/pybamm-team/PyBaMM/pull/891)) +- Reformatted and cleaned up `QuickPlot` ([#886](https://github.com/pybamm-team/PyBaMM/pull/886)) +- Added thermal effects to lead-acid models ([#885](https://github.com/pybamm-team/PyBaMM/pull/885)) +- Add new symbols `VariableDot`, representing the derivative of a variable wrt time, + and `StateVectorDot`, representing the derivative of a state vector wrt time + ([#858](https://github.com/pybamm-team/PyBaMM/issues/858)) +- Added a helper function for info on function parameters ([#881](https://github.com/pybamm-team/PyBaMM/pull/881)) +- Added additional notebooks showing how to create and compare models ([#877](https://github.com/pybamm-team/PyBaMM/pull/877)) +- Added `Minimum`, `Maximum` and `Sign` operators + ([#876](https://github.com/pybamm-team/PyBaMM/pull/876)) +- Added a search feature to `FuzzyDict` ([#875](https://github.com/pybamm-team/PyBaMM/pull/875)) +- Add ambient temperature as a function of time ([#872](https://github.com/pybamm-team/PyBaMM/pull/872)) +- Added `CasadiAlgebraicSolver` for solving algebraic systems with CasADi ([#868](https://github.com/pybamm-team/PyBaMM/pull/868)) +- Added electrolyte functions from Landesfeind ([#860](https://github.com/pybamm-team/PyBaMM/pull/860)) +- Add new symbols `VariableDot`, representing the derivative of a variable wrt time, + and `StateVectorDot`, representing the derivative of a state vector wrt time + ([#858](https://github.com/pybamm-team/PyBaMM/issues/858)) + +## Bug fixes + +- Fixed tight layout for QuickPlot in jupyter notebooks ([#930](https://github.com/pybamm-team/PyBaMM/pull/930)) +- Fixed bug raised if function returns a scalar ([#919](https://github.com/pybamm-team/PyBaMM/pull/919)) +- Fixed event handling in `ScipySolver` ([#905](https://github.com/pybamm-team/PyBaMM/pull/905)) +- Made input handling clearer in solvers ([#905](https://github.com/pybamm-team/PyBaMM/pull/905)) +- Updated Getting started notebook 2 ([#903](https://github.com/pybamm-team/PyBaMM/pull/903)) +- Reformatted external circuit submodels ([#879](https://github.com/pybamm-team/PyBaMM/pull/879)) +- Some bug fixes to generalize specifying models that aren't battery models, see [#846](https://github.com/pybamm-team/PyBaMM/issues/846) +- Reformatted interface submodels to be more readable ([#866](https://github.com/pybamm-team/PyBaMM/pull/866)) +- Removed double-counted "number of electrodes connected in parallel" from simulation ([#864](https://github.com/pybamm-team/PyBaMM/pull/864)) + +## Breaking changes + +- Changed keyword argument `u` for inputs (when evaluating an object) to `inputs` ([#905](https://github.com/pybamm-team/PyBaMM/pull/905)) +- Removed "set external temperature" and "set external potential" options. Use "external submodels" option instead ([#862](https://github.com/pybamm-team/PyBaMM/pull/862)) + # [v0.2.0](https://github.com/pybamm-team/PyBaMM/tree/v0.2.0) - 2020-02-26 This release introduces many new features and optimizations. All models can now be solved using the pip installation - in particular, the DFN can be solved in around 0.1s. Other highlights include an improved user interface, simulations of experimental protocols (GITT, CCCV, etc), new parameter sets for NCA and LGM50, drive cycles, "input parameters" and "external variables" for quickly solving models with different parameter values and coupling with external software, and general bug fixes and optimizations. diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md new file mode 100644 index 0000000000..c3cae738dc --- /dev/null +++ b/CODE-OF-CONDUCT.md @@ -0,0 +1,46 @@ +# PyBaMM Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at vsulzer@umich.edu. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ \ No newline at end of file diff --git a/INSTALL-WINDOWS-WSL.md b/INSTALL-WINDOWS-WSL.md new file mode 100644 index 0000000000..608dfd63f8 --- /dev/null +++ b/INSTALL-WINDOWS-WSL.md @@ -0,0 +1,89 @@ +We recommend the use of Windows Subsystem for Linux (WSL) to install PyBaMM, see the +instructions below to get PyBaMM working using Windows, WSL and VSCode. + +## Install WSL + +Follow the instructions from Microsoft +[here](https://docs.microsoft.com/en-us/windows/wsl/install-win10). When given the +option, choose the Ubuntu 18.04 LTS distribution to install. Don't forget to initialise +the Ubuntu installation using the instructions given +[here](https://docs.microsoft.com/en-us/windows/wsl/initialize-distro). + +## Install PyBaMM + +Open a terminal window in your installed Ubuntu distribution by selecting "Ubuntu" from +the start menu. This should give you a bash prompt in your home directory. + +To download the PyBaMM source code, you first need to install git, which you can do by +typing + +```bash +sudo apt install git-core +``` + +For easier integration with WSL, we recommend that you install PyBaMM in your *Windows* +Documents folder, for example by first navigating to + +```bash +$ cd /mnt/c/Users/USER_NAME/Documents +``` + +where USER_NAME is your username. Exact path to Windows documents may vary. Now use git to clone the PyBaMM repository: + +```bash +git clone https://github.com/pybamm-team/PyBaMM.git +``` + +This will create a new directly called `PyBaMM`, you can move to this directory in bash +using the `cd` command: + +```bash +cd PyBaMM +``` + +If you are unfamiliar with the linux command line, you might find it useful to work through this +[tutorial](https://tutorials.ubuntu.com/tutorial/command-line-for-beginners) provided by Ubuntu. + +Now head over and follow the installation instructions for PyBaMM for linux +[here](INSTALL-LINUX-MAC.md). + +## Use Visual Studio Code to run PyBaMM + +You will probably want to use a native Windows IDE such as Visual Studio Code or the +full Microsoft Visual Studio IDE. Both of these packages can connect to WSL so that you +can write python code in a native windows environment, while at the same time using WSL +to run the code using your installed Ubuntu distribution. The following instructions +assume that you are using Visual Studio Code. + +First, setup VSCode to run within the `PyBaMM` directory that you created above, using +the instructions provided [here](https://code.visualstudio.com/docs/remote/wsl). + +Once you have opened the `PyBaMM` folder in vscode, use the `Extensions` panel to +install the `Python` extension from Microsoft. Note that extensions are either installed +on the Windows (Local) or on in WSL (WSL:Ubuntu), so even if you have used VSCode +previously with the Python extension, you probably haven't installed it in WSL. Make +sure to reload after installing the Python extension so that it is available. + +If you have installed PyBaMM into the virtual environment `env` as in the PyBaMM linux +install guide, then VSCode should automatically start using this environment and you +should see something similar to "Python 3.6.8 64-bit ('env': venv)" in the bottom bar. + +To test that vscode can run a PyBaMM script, navigate to the `examples/scripts` folder +and right click on the `create-model.py` script. Select "Run current file in Python +Interactive Window". This should run the script, which sets up and solves a model of SEI +thickness using PyBaMM. You should see a plot of SEI thickness versus time pop up in the +interactive window. + +The Python Interactive Window in VSCode can be used to view plots, but is restricted in +functionality and cannot, for example, launch separate windows to show plot. To setup an +xserver on windows and use this to launch windows for plotting, follow these +instructions: + +1. Install VcXsrv from [here](https://sourceforge.net/projects/vcxsrv/). +1. Set the display port in the WSL command-line: `echo "export DISPLAY=localhost:0.0" >> + ~/.bashrc` +1. Install python3-tk in WSL: `sudo apt-get install python3-tk` +1. Set the matplotlib backend to TKAgg in WSL: `echo "backend : TKAgg" >> + ~/.config/matplotlib/matplotlibrc` +1. Before running the code, just launch XLaunch (with the default settings) from within + Windows. Then the code works as usual. diff --git a/INSTALL-WINDOWS.md b/INSTALL-WINDOWS.md index 608dfd63f8..6f69bf72e8 100644 --- a/INSTALL-WINDOWS.md +++ b/INSTALL-WINDOWS.md @@ -1,89 +1,54 @@ -We recommend the use of Windows Subsystem for Linux (WSL) to install PyBaMM, see the -instructions below to get PyBaMM working using Windows, WSL and VSCode. +## Prerequisites -## Install WSL +To use and/or contribute to PyBaMM, you must have Python 3.6 or 3.7 installed (note that 3.8 is not yet supported). -Follow the instructions from Microsoft -[here](https://docs.microsoft.com/en-us/windows/wsl/install-win10). When given the -option, choose the Ubuntu 18.04 LTS distribution to install. Don't forget to initialise -the Ubuntu installation using the instructions given -[here](https://docs.microsoft.com/en-us/windows/wsl/initialize-distro). +To install Python 3 download the installation files from [Python's website](https://www.python.org/downloads/windows/). Make sure +to tick the box on `Add Python 3.X to PATH`. For more detailed instructions please see the +[official Python on Windows guide](https://docs.python.org/3.7/using/windows.html). ## Install PyBaMM -Open a terminal window in your installed Ubuntu distribution by selecting "Ubuntu" from -the start menu. This should give you a bash prompt in your home directory. +### User install +Launch the Command Prompt and go to the directory where you want to install PyBaMM. You can find a reminder of how to +navigate the terminal [here](http://www.cs.columbia.edu/~sedwards/classes/2015/1102-fall/Command%20Prompt%20Cheatsheet.pdf). -To download the PyBaMM source code, you first need to install git, which you can do by -typing +We recommend to install PyBaMM within a virtual environment, in order not +to alter any distribution python files. -```bash -sudo apt install git-core -``` - -For easier integration with WSL, we recommend that you install PyBaMM in your *Windows* -Documents folder, for example by first navigating to +To create a virtual environment `env` within your current directory type: ```bash -$ cd /mnt/c/Users/USER_NAME/Documents +python -m venv env ``` - -where USER_NAME is your username. Exact path to Windows documents may vary. Now use git to clone the PyBaMM repository: +You can then "activate" the environment using: ```bash -git clone https://github.com/pybamm-team/PyBaMM.git +env\Scripts\activate.bat ``` +Now all the calls to pip described below will install PyBaMM and its dependencies into +the environment `env`. When you are ready to exit the environment and go back to your +original system, just type: -This will create a new directly called `PyBaMM`, you can move to this directory in bash -using the `cd` command: - -```bash -cd PyBaMM +```bash +deactivate ``` -If you are unfamiliar with the linux command line, you might find it useful to work through this -[tutorial](https://tutorials.ubuntu.com/tutorial/command-line-for-beginners) provided by Ubuntu. - -Now head over and follow the installation instructions for PyBaMM for linux -[here](INSTALL-LINUX-MAC.md). - -## Use Visual Studio Code to run PyBaMM - -You will probably want to use a native Windows IDE such as Visual Studio Code or the -full Microsoft Visual Studio IDE. Both of these packages can connect to WSL so that you -can write python code in a native windows environment, while at the same time using WSL -to run the code using your installed Ubuntu distribution. The following instructions -assume that you are using Visual Studio Code. - -First, setup VSCode to run within the `PyBaMM` directory that you created above, using -the instructions provided [here](https://code.visualstudio.com/docs/remote/wsl). - -Once you have opened the `PyBaMM` folder in vscode, use the `Extensions` panel to -install the `Python` extension from Microsoft. Note that extensions are either installed -on the Windows (Local) or on in WSL (WSL:Ubuntu), so even if you have used VSCode -previously with the Python extension, you probably haven't installed it in WSL. Make -sure to reload after installing the Python extension so that it is available. +PyBaMM can be installed via pip: +```bash +pip install pybamm +``` -If you have installed PyBaMM into the virtual environment `env` as in the PyBaMM linux -install guide, then VSCode should automatically start using this environment and you -should see something similar to "Python 3.6.8 64-bit ('env': venv)" in the bottom bar. +PyBaMM's dependencies (such as `numpy`, `scipy`, etc) will be installed automatically when you install PyBaMM using `pip`. -To test that vscode can run a PyBaMM script, navigate to the `examples/scripts` folder -and right click on the `create-model.py` script. Select "Run current file in Python -Interactive Window". This should run the script, which sets up and solves a model of SEI -thickness using PyBaMM. You should see a plot of SEI thickness versus time pop up in the -interactive window. +For an introduction to virtual environments, see (https://realpython.com/python-virtual-environments-a-primer/). -The Python Interactive Window in VSCode can be used to view plots, but is restricted in -functionality and cannot, for example, launch separate windows to show plot. To setup an -xserver on windows and use this to launch windows for plotting, follow these -instructions: +## Uninstall PyBaMM +PyBaMM can be uninstalled by running +```bash +pip uninstall pybamm +``` +in your virtual environment. -1. Install VcXsrv from [here](https://sourceforge.net/projects/vcxsrv/). -1. Set the display port in the WSL command-line: `echo "export DISPLAY=localhost:0.0" >> - ~/.bashrc` -1. Install python3-tk in WSL: `sudo apt-get install python3-tk` -1. Set the matplotlib backend to TKAgg in WSL: `echo "backend : TKAgg" >> - ~/.config/matplotlib/matplotlibrc` -1. Before running the code, just launch XLaunch (with the default settings) from within - Windows. Then the code works as usual. +## Installation using WSL +If you want to install the optional PyBaMM solvers, you have to use the Windows Subsystem for Linux (WSL). You can find +the installation instructions [here](INSTALL-WINDOWS-WSL.md). diff --git a/README.md b/README.md index 0808ced567..e7f74a89b6 100644 --- a/README.md +++ b/README.md @@ -53,16 +53,15 @@ For further examples, see the list of repositories that use PyBaMM [here](https: ### Linux -For instructions on installing PyBaMM on Debian-based distributions, please see [here](INSTALL-LINUX-MAC.md) +For instructions on installing PyBaMM on Debian-based distributions, please see [here](INSTALL-LINUX-MAC.md). ### Mac OS -For instructions on installing PyBaMM on Mac OS distributions, please see [here](INSTALL-LINUX-MAC.md) +For instructions on installing PyBaMM on Mac OS distributions, please see [here](INSTALL-LINUX-MAC.md). ### Windows -We recommend using Windows Subsystem for Linux to install PyBaMM on a Windows OS, for -instructions please see [here](INSTALL-WINDOWS.md) +For instructions on installing PyBaMM on Windows distributions, please see [here](INSTALL-WINDOWS.md). If you want to install the optional solvers (such as scikits-odes and KLU solvers), install PyBaMM on the Windows Subsystem for Linux following the instructions [here](INSTALL-WINDOWS-WSL.md) ## Citing PyBaMM diff --git a/docs/conf.py b/docs/conf.py index e7cd1274e8..d4a743192f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,7 +28,7 @@ # The short X.Y version version = "0.2" # The full version, including alpha/beta/rc tags -release = "0.2.0" +release = "0.2.1" # -- General configuration --------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index ebbecaca68..e7910fdc97 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -31,9 +31,10 @@ Contents source/spatial_methods/index source/solvers/index source/experiments/index + source/simulation + source/quick_plot source/processed_variable source/util - source/simulation source/citations source/parameters_cli diff --git a/docs/source/expression_tree/binary_operator.rst b/docs/source/expression_tree/binary_operator.rst index d8082d8c4c..826956a3ef 100644 --- a/docs/source/expression_tree/binary_operator.rst +++ b/docs/source/expression_tree/binary_operator.rst @@ -28,4 +28,20 @@ Binary Operators .. autoclass:: pybamm.Heaviside :members: +.. autoclass:: pybamm.EqualHeaviside + :members: + +.. autoclass:: pybamm.NotEqualHeaviside + :members: + +.. autoclass:: pybamm.Minimum + :members: + +.. autoclass:: pybamm.Maximum + :members: + +.. autofunction:: pybamm.minimum + +.. autofunction:: pybamm.maximum + .. autofunction:: pybamm.source diff --git a/docs/source/expression_tree/broadcasts.rst b/docs/source/expression_tree/broadcasts.rst index 743e48ce79..d4f58fe275 100644 --- a/docs/source/expression_tree/broadcasts.rst +++ b/docs/source/expression_tree/broadcasts.rst @@ -12,3 +12,14 @@ Broadcasting Operators .. autoclass:: pybamm.SecondaryBroadcast :members: + +.. autoclass:: pybamm.FullBroadcastToEdges + :members: + +.. autoclass:: pybamm.PrimaryBroadcastToEdges + :members: + +.. autoclass:: pybamm.SecondaryBroadcastToEdges + :members: + +.. autofunction:: pybamm.ones_like diff --git a/docs/source/expression_tree/state_vector.rst b/docs/source/expression_tree/state_vector.rst index 4a0792b3b7..4370f6e980 100644 --- a/docs/source/expression_tree/state_vector.rst +++ b/docs/source/expression_tree/state_vector.rst @@ -3,3 +3,6 @@ State Vector .. autoclass:: pybamm.StateVector :members: + +.. autoclass:: pybamm.StateVectorDot + :members: diff --git a/docs/source/expression_tree/unary_operator.rst b/docs/source/expression_tree/unary_operator.rst index ed0bc19af3..d481f51bd3 100644 --- a/docs/source/expression_tree/unary_operator.rst +++ b/docs/source/expression_tree/unary_operator.rst @@ -10,6 +10,9 @@ Unary Operators .. autoclass:: pybamm.AbsoluteValue :members: +.. autoclass:: pybamm.Sign + :members: + .. autoclass:: pybamm.Index :members: @@ -67,4 +70,12 @@ Unary Operators .. autofunction:: pybamm.x_average +.. autofunction:: pybamm.r_average + +.. autofunction:: pybamm.z_average + +.. autofunction:: pybamm.yz_average + .. autofunction:: pybamm.boundary_value + +.. autofunction:: pybamm.sign diff --git a/docs/source/expression_tree/variable.rst b/docs/source/expression_tree/variable.rst index 0e610f5f28..510e366cf1 100644 --- a/docs/source/expression_tree/variable.rst +++ b/docs/source/expression_tree/variable.rst @@ -4,5 +4,9 @@ Variable .. autoclass:: pybamm.Variable :members: +.. autoclass:: pybamm.VariableDot + :members: + .. autoclass:: pybamm.ExternalVariable :members: + diff --git a/docs/source/models/submodels/current_collector/index.rst b/docs/source/models/submodels/current_collector/index.rst index e13e842515..b638ca80d8 100644 --- a/docs/source/models/submodels/current_collector/index.rst +++ b/docs/source/models/submodels/current_collector/index.rst @@ -10,4 +10,3 @@ Current Collector homogeneous_current_collector potential_pair quite_conductive_potential_pair - set_potential_single_particle diff --git a/docs/source/models/submodels/current_collector/set_potential_single_particle.rst b/docs/source/models/submodels/current_collector/set_potential_single_particle.rst deleted file mode 100644 index b87ebbf42e..0000000000 --- a/docs/source/models/submodels/current_collector/set_potential_single_particle.rst +++ /dev/null @@ -1,11 +0,0 @@ -Set Potential Single Particle Models -==================================== - -.. autoclass:: pybamm.current_collector.BaseSetPotentialSingleParticle - :members: - -.. autoclass:: pybamm.current_collector.SetPotentialSingleParticle1plus1D - :members: - -.. autoclass:: pybamm.current_collector.SetPotentialSingleParticle2plus1D - :members: diff --git a/docs/source/models/submodels/interface/diffusion_limited.rst b/docs/source/models/submodels/interface/diffusion_limited.rst new file mode 100644 index 0000000000..5ec3e6d559 --- /dev/null +++ b/docs/source/models/submodels/interface/diffusion_limited.rst @@ -0,0 +1,5 @@ +Diffusion-limited +================= + +.. autoclass:: pybamm.interface.DiffusionLimited + :members: diff --git a/docs/source/models/submodels/interface/diffusion_limited/base_diffusion_limited.rst b/docs/source/models/submodels/interface/diffusion_limited/base_diffusion_limited.rst deleted file mode 100644 index 88c851856f..0000000000 --- a/docs/source/models/submodels/interface/diffusion_limited/base_diffusion_limited.rst +++ /dev/null @@ -1,5 +0,0 @@ -Base Model -========== - -.. autoclass:: pybamm.interface.diffusion_limited.BaseModel - :members: diff --git a/docs/source/models/submodels/interface/diffusion_limited/full_diffusion_limited.rst b/docs/source/models/submodels/interface/diffusion_limited/full_diffusion_limited.rst deleted file mode 100644 index 67de8cd799..0000000000 --- a/docs/source/models/submodels/interface/diffusion_limited/full_diffusion_limited.rst +++ /dev/null @@ -1,5 +0,0 @@ -Full Model -========== - -.. autoclass:: pybamm.interface.diffusion_limited.FullDiffusionLimited - :members: diff --git a/docs/source/models/submodels/interface/diffusion_limited/index.rst b/docs/source/models/submodels/interface/diffusion_limited/index.rst deleted file mode 100644 index bb3b3879b4..0000000000 --- a/docs/source/models/submodels/interface/diffusion_limited/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Diffusion-limited Kinetics -========================== - -.. toctree:: - :maxdepth: 1 - - base_diffusion_limited - full_diffusion_limited - leading_diffusion_limited diff --git a/docs/source/models/submodels/interface/diffusion_limited/leading_diffusion_limited.rst b/docs/source/models/submodels/interface/diffusion_limited/leading_diffusion_limited.rst deleted file mode 100644 index f97febf61f..0000000000 --- a/docs/source/models/submodels/interface/diffusion_limited/leading_diffusion_limited.rst +++ /dev/null @@ -1,5 +0,0 @@ -Leading-order Model -=================== - -.. autoclass:: pybamm.interface.diffusion_limited.LeadingOrderDiffusionLimited - :members: diff --git a/docs/source/models/submodels/interface/first_order_kinetics/first_order_kinetics.rst b/docs/source/models/submodels/interface/first_order_kinetics/first_order_kinetics.rst new file mode 100644 index 0000000000..32522d6205 --- /dev/null +++ b/docs/source/models/submodels/interface/first_order_kinetics/first_order_kinetics.rst @@ -0,0 +1,5 @@ +First-order Kinetics +==================== + +.. autoclass:: pybamm.interface.FirstOrderKinetics + :members: diff --git a/docs/source/models/submodels/interface/first_order_kinetics/index.rst b/docs/source/models/submodels/interface/first_order_kinetics/index.rst new file mode 100644 index 0000000000..fab79b9b44 --- /dev/null +++ b/docs/source/models/submodels/interface/first_order_kinetics/index.rst @@ -0,0 +1,8 @@ +First-order Kinetics +==================== + +.. toctree:: + :maxdepth: 1 + + inverse_first_order_kinetics + first_order_kinetics diff --git a/docs/source/models/submodels/interface/inverse_kinetics/base_inverse_first_order_kinetics.rst b/docs/source/models/submodels/interface/first_order_kinetics/inverse_first_order_kinetics.rst similarity index 51% rename from docs/source/models/submodels/interface/inverse_kinetics/base_inverse_first_order_kinetics.rst rename to docs/source/models/submodels/interface/first_order_kinetics/inverse_first_order_kinetics.rst index a71d574071..58e1fb83b5 100644 --- a/docs/source/models/submodels/interface/inverse_kinetics/base_inverse_first_order_kinetics.rst +++ b/docs/source/models/submodels/interface/first_order_kinetics/inverse_first_order_kinetics.rst @@ -1,5 +1,5 @@ Base Inverse First-order Kinetics ================================= -.. autoclass:: pybamm.interface.inverse_kinetics.BaseInverseFirstOrderKinetics +.. autoclass:: pybamm.interface.InverseFirstOrderKinetics :members: diff --git a/docs/source/models/submodels/interface/index.rst b/docs/source/models/submodels/interface/index.rst index 6e128e4418..21585d2841 100644 --- a/docs/source/models/submodels/interface/index.rst +++ b/docs/source/models/submodels/interface/index.rst @@ -5,8 +5,7 @@ Interface :maxdepth: 1 base_interface - diffusion_limited/index - inverse_kinetics/index kinetics/index - lead_acid - lithium_ion + inverse_kinetics/index + first_order_kinetics/index + diffusion_limited diff --git a/docs/source/models/submodels/interface/inverse_kinetics/base_inverse_kinetics.rst b/docs/source/models/submodels/interface/inverse_kinetics/base_inverse_kinetics.rst deleted file mode 100644 index 4b444d17f3..0000000000 --- a/docs/source/models/submodels/interface/inverse_kinetics/base_inverse_kinetics.rst +++ /dev/null @@ -1,5 +0,0 @@ -Base Inverse Kinetics -===================== - -.. autoclass:: pybamm.interface.inverse_kinetics.BaseInverseKinetics - :members: diff --git a/docs/source/models/submodels/interface/inverse_kinetics/index.rst b/docs/source/models/submodels/interface/inverse_kinetics/index.rst index e0d3271a32..d36fa61109 100644 --- a/docs/source/models/submodels/interface/inverse_kinetics/index.rst +++ b/docs/source/models/submodels/interface/inverse_kinetics/index.rst @@ -4,6 +4,4 @@ Inverse Interface Kinetics .. toctree:: :maxdepth: 1 - base_inverse_first_order_kinetics - base_inverse_kinetics inverse_butler_volmer diff --git a/docs/source/models/submodels/interface/kinetics/base_first_order_kinetics.rst b/docs/source/models/submodels/interface/kinetics/base_first_order_kinetics.rst deleted file mode 100644 index 40ef5fdd0d..0000000000 --- a/docs/source/models/submodels/interface/kinetics/base_first_order_kinetics.rst +++ /dev/null @@ -1,5 +0,0 @@ -Base First-order Kinetics -========================= - -.. autoclass:: pybamm.interface.kinetics.BaseFirstOrderKinetics - :members: diff --git a/docs/source/models/submodels/interface/kinetics/base_kinetics.rst b/docs/source/models/submodels/interface/kinetics/base_kinetics.rst index 9ed0210e2a..b91c17f810 100644 --- a/docs/source/models/submodels/interface/kinetics/base_kinetics.rst +++ b/docs/source/models/submodels/interface/kinetics/base_kinetics.rst @@ -1,5 +1,5 @@ Base Kinetics ============= -.. autoclass:: pybamm.interface.kinetics.BaseModel +.. autoclass:: pybamm.interface.BaseKinetics :members: diff --git a/docs/source/models/submodels/interface/kinetics/butler_volmer.rst b/docs/source/models/submodels/interface/kinetics/butler_volmer.rst index d3d8ca4396..8d456d9bf8 100644 --- a/docs/source/models/submodels/interface/kinetics/butler_volmer.rst +++ b/docs/source/models/submodels/interface/kinetics/butler_volmer.rst @@ -1,8 +1,5 @@ Butler-Volmer ============= -.. autoclass:: pybamm.interface.kinetics.ButlerVolmer - :members: - -.. autoclass:: pybamm.interface.kinetics.FirstOrderButlerVolmer +.. autoclass:: pybamm.interface.ButlerVolmer :members: diff --git a/docs/source/models/submodels/interface/kinetics/index.rst b/docs/source/models/submodels/interface/kinetics/index.rst index 52525eb56f..3dc0fcc859 100644 --- a/docs/source/models/submodels/interface/kinetics/index.rst +++ b/docs/source/models/submodels/interface/kinetics/index.rst @@ -5,7 +5,6 @@ Interface Kinetics :maxdepth: 1 base_kinetics - base_first_order_kinetics butler_volmer no_reaction tafel diff --git a/docs/source/models/submodels/interface/kinetics/no_reaction.rst b/docs/source/models/submodels/interface/kinetics/no_reaction.rst index 2613da77fc..3d1e91455d 100644 --- a/docs/source/models/submodels/interface/kinetics/no_reaction.rst +++ b/docs/source/models/submodels/interface/kinetics/no_reaction.rst @@ -1,5 +1,5 @@ No Reaction =========== -.. autoclass:: pybamm.interface.kinetics.NoReaction +.. autoclass:: pybamm.interface.NoReaction :members: diff --git a/docs/source/models/submodels/interface/kinetics/tafel.rst b/docs/source/models/submodels/interface/kinetics/tafel.rst index 84eff4ceee..dcb425214b 100644 --- a/docs/source/models/submodels/interface/kinetics/tafel.rst +++ b/docs/source/models/submodels/interface/kinetics/tafel.rst @@ -1,11 +1,8 @@ Tafel ===== -.. autoclass:: pybamm.interface.kinetics.ForwardTafel - :members: - -.. autoclass:: pybamm.interface.kinetics.FirstOrderForwardTafel +.. autoclass:: pybamm.interface.ForwardTafel :members: -.. autoclass:: pybamm.interface.kinetics.BackwardTafel +.. autoclass:: pybamm.interface.BackwardTafel :members: diff --git a/docs/source/models/submodels/interface/lead_acid.rst b/docs/source/models/submodels/interface/lead_acid.rst deleted file mode 100644 index f4200b0136..0000000000 --- a/docs/source/models/submodels/interface/lead_acid.rst +++ /dev/null @@ -1,14 +0,0 @@ -Lead Acid -========= - -.. autoclass:: pybamm.interface.lead_acid.ButlerVolmer - :members: - -.. autoclass:: pybamm.interface.lead_acid.InverseButlerVolmer - :members: - -.. autoclass:: pybamm.interface.lead_acid.FirstOrderButlerVolmer - :members: - -.. autoclass:: pybamm.interface.lead_acid.InverseFirstOrderKinetics - :members: diff --git a/docs/source/models/submodels/interface/lithium_ion.rst b/docs/source/models/submodels/interface/lithium_ion.rst deleted file mode 100644 index 08f3556755..0000000000 --- a/docs/source/models/submodels/interface/lithium_ion.rst +++ /dev/null @@ -1,8 +0,0 @@ -Lithium-Ion -=========== - -.. autoclass:: pybamm.interface.lithium_ion.ButlerVolmer - :members: - -.. autoclass:: pybamm.interface.lithium_ion.InverseButlerVolmer - :members: diff --git a/docs/source/models/submodels/particle/fast/base_fast_particle.rst b/docs/source/models/submodels/particle/fast/base_fast_particle.rst deleted file mode 100644 index 0fd382e7f8..0000000000 --- a/docs/source/models/submodels/particle/fast/base_fast_particle.rst +++ /dev/null @@ -1,5 +0,0 @@ -Base Model -========== - -.. autoclass:: pybamm.particle.fast.BaseModel - :members: diff --git a/docs/source/models/submodels/particle/fast/fast_many_particles.rst b/docs/source/models/submodels/particle/fast/fast_many_particles.rst deleted file mode 100644 index 727edb6253..0000000000 --- a/docs/source/models/submodels/particle/fast/fast_many_particles.rst +++ /dev/null @@ -1,5 +0,0 @@ -Many Particle -============= - -.. autoclass:: pybamm.particle.fast.ManyParticles - :members: diff --git a/docs/source/models/submodels/particle/fast/fast_single_particle.rst b/docs/source/models/submodels/particle/fast/fast_single_particle.rst deleted file mode 100644 index 72990961dc..0000000000 --- a/docs/source/models/submodels/particle/fast/fast_single_particle.rst +++ /dev/null @@ -1,5 +0,0 @@ -Single Particle -=============== - -.. autoclass:: pybamm.particle.fast.SingleParticle - :members: diff --git a/docs/source/models/submodels/particle/fast/index.rst b/docs/source/models/submodels/particle/fast/index.rst deleted file mode 100644 index a565803646..0000000000 --- a/docs/source/models/submodels/particle/fast/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Fast -==== - -.. toctree:: - - base_fast_particle - fast_many_particles - fast_single_particle diff --git a/docs/source/models/submodels/particle/fast_many_particles.rst b/docs/source/models/submodels/particle/fast_many_particles.rst new file mode 100644 index 0000000000..703a49047c --- /dev/null +++ b/docs/source/models/submodels/particle/fast_many_particles.rst @@ -0,0 +1,5 @@ +Fast Many Particles +=================== + +.. autoclass:: pybamm.particle.FastManyParticles + :members: diff --git a/docs/source/models/submodels/particle/fast_single_particle.rst b/docs/source/models/submodels/particle/fast_single_particle.rst new file mode 100644 index 0000000000..6c1de67f5c --- /dev/null +++ b/docs/source/models/submodels/particle/fast_single_particle.rst @@ -0,0 +1,5 @@ +Fast Single Particle +==================== + +.. autoclass:: pybamm.particle.FastSingleParticle + :members: diff --git a/docs/source/models/submodels/particle/fickian/fickian_many_particles.rst b/docs/source/models/submodels/particle/fickian/fickian_many_particles.rst deleted file mode 100644 index 4e27cf5d40..0000000000 --- a/docs/source/models/submodels/particle/fickian/fickian_many_particles.rst +++ /dev/null @@ -1,7 +0,0 @@ -Many Particle -============= - -.. autoclass:: pybamm.particle.fickian.ManyParticles - :members: - - diff --git a/docs/source/models/submodels/particle/fickian/fickian_single_particle.rst b/docs/source/models/submodels/particle/fickian/fickian_single_particle.rst deleted file mode 100644 index 96c6332a8c..0000000000 --- a/docs/source/models/submodels/particle/fickian/fickian_single_particle.rst +++ /dev/null @@ -1,6 +0,0 @@ -Single Particle -=============== - -.. autoclass:: pybamm.particle.fickian.SingleParticle - :members: - diff --git a/docs/source/models/submodels/particle/fickian/index.rst b/docs/source/models/submodels/particle/fickian/index.rst deleted file mode 100644 index a6f8c36ceb..0000000000 --- a/docs/source/models/submodels/particle/fickian/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Fickian -======== - -.. toctree:: - - fickian_many_particles - fickian_single_particle - diff --git a/docs/source/models/submodels/particle/fickian_many_particles.rst b/docs/source/models/submodels/particle/fickian_many_particles.rst new file mode 100644 index 0000000000..c9834bc5d9 --- /dev/null +++ b/docs/source/models/submodels/particle/fickian_many_particles.rst @@ -0,0 +1,7 @@ +Fickian Many Particles +====================== + +.. autoclass:: pybamm.particle.FickianManyParticles + :members: + + diff --git a/docs/source/models/submodels/particle/fickian_single_particle.rst b/docs/source/models/submodels/particle/fickian_single_particle.rst new file mode 100644 index 0000000000..008b2e8b48 --- /dev/null +++ b/docs/source/models/submodels/particle/fickian_single_particle.rst @@ -0,0 +1,6 @@ +Fickian Single Particle +======================= + +.. autoclass:: pybamm.particle.FickianSingleParticle + :members: + diff --git a/docs/source/models/submodels/particle/index.rst b/docs/source/models/submodels/particle/index.rst index 78c4ca7f11..a28c75a83a 100644 --- a/docs/source/models/submodels/particle/index.rst +++ b/docs/source/models/submodels/particle/index.rst @@ -5,5 +5,7 @@ Particle :maxdepth: 1 base_particle - fickian/index - fast/index + fickian_single_particle + fickian_many_particles + fast_single_particle + fast_many_particles diff --git a/docs/source/models/submodels/thermal/x_lumped/index.rst b/docs/source/models/submodels/thermal/x_lumped/index.rst index aaad69755f..3df689d0ea 100644 --- a/docs/source/models/submodels/thermal/x_lumped/index.rst +++ b/docs/source/models/submodels/thermal/x_lumped/index.rst @@ -9,4 +9,3 @@ X-lumped x_lumped_0D_current_collector x_lumped_1D_current_collector x_lumped_2D_current_collector - x_lumped_1D_set_temperature diff --git a/docs/source/models/submodels/thermal/x_lumped/x_lumped_1D_set_temperature.rst b/docs/source/models/submodels/thermal/x_lumped/x_lumped_1D_set_temperature.rst deleted file mode 100644 index ea31df59d9..0000000000 --- a/docs/source/models/submodels/thermal/x_lumped/x_lumped_1D_set_temperature.rst +++ /dev/null @@ -1,5 +0,0 @@ -Set Temperature 1D current collector -==================================== - -.. autoclass:: pybamm.thermal.x_lumped.SetTemperature1D - :members: diff --git a/docs/source/quick_plot.rst b/docs/source/quick_plot.rst new file mode 100644 index 0000000000..b26903ba14 --- /dev/null +++ b/docs/source/quick_plot.rst @@ -0,0 +1,5 @@ +Quick Plot +========== + +.. autoclass:: pybamm.QuickPlot + :members: diff --git a/docs/source/solvers/algebraic_solvers.rst b/docs/source/solvers/algebraic_solvers.rst index cb87d0e0f5..df241f9270 100644 --- a/docs/source/solvers/algebraic_solvers.rst +++ b/docs/source/solvers/algebraic_solvers.rst @@ -3,3 +3,6 @@ Algebraic Solvers .. autoclass:: pybamm.AlgebraicSolver :members: + +.. autoclass:: pybamm.CasadiAlgebraicSolver + :members: diff --git a/docs/source/solvers/base_solvers.rst b/docs/source/solvers/base_solver.rst similarity index 63% rename from docs/source/solvers/base_solvers.rst rename to docs/source/solvers/base_solver.rst index 9b1d73f48b..9c0bd4f617 100644 --- a/docs/source/solvers/base_solvers.rst +++ b/docs/source/solvers/base_solver.rst @@ -1,5 +1,5 @@ -Base Solvers -============ +Base Solver +=========== .. autoclass:: pybamm.BaseSolver :members: diff --git a/docs/source/solvers/dummy_solver.rst b/docs/source/solvers/dummy_solver.rst new file mode 100644 index 0000000000..88c4e572e3 --- /dev/null +++ b/docs/source/solvers/dummy_solver.rst @@ -0,0 +1,5 @@ +Dummy Solver +============ + +.. autoclass:: pybamm.DummySolver + :members: diff --git a/docs/source/solvers/index.rst b/docs/source/solvers/index.rst index d85865dec7..7954aea119 100644 --- a/docs/source/solvers/index.rst +++ b/docs/source/solvers/index.rst @@ -3,9 +3,10 @@ Solvers .. toctree:: - algebraic_solvers - base_solvers + base_solver + dummy_solver scipy_solver scikits_solvers casadi_solver + algebraic_solvers solution diff --git a/examples/notebooks/Creating Models/1-an-ode-model.ipynb b/examples/notebooks/Creating Models/1-an-ode-model.ipynb new file mode 100644 index 0000000000..00dbabe62c --- /dev/null +++ b/examples/notebooks/Creating Models/1-an-ode-model.ipynb @@ -0,0 +1,279 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Creating a simple ODE model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this series of notebooks, we will run through the steps involved in creating a new model within pybamm. Before using pybamm we recommend following the [Getting Started](../Getting%20Started) guides.\n", + "\n", + "In this notebook we create and solve the following simple ODE model:\n", + "\n", + "\\begin{align*}\n", + " \\frac{\\textrm{d} x}{\\textrm{d} t} &= 4x - 2y, \\quad x(0) = 1, \\\\\n", + " \\frac{\\textrm{d} y}{\\textrm{d} t} &= 3x - y, \\quad y(0) = 2.\n", + "\\end{align*}\n", + "\n", + "We begin by importing the pybamm library into this notebook, along with numpy and matplotlib, which we use for plotting:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pybamm\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting up the model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We first initialise the model using the `BaseModel` class. This sets up the required structure for our model. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "model = pybamm.BaseModel()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we define the variables in the model using the `Variable` class. In more complicated models we can give the variables more informative string names, but here we simply name the variables \"x\" and \"y\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "x = pybamm.Variable(\"x\")\n", + "y = pybamm.Variable(\"y\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now use the symbols we have created for our variables to write out our governing equations. Note that the governing equations must be provied in the explicit form `d/dt = rhs` since pybamm only stores the right hand side (rhs) and assumes that the left hand side is the time derivative. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "dxdt = 4 * x - 2 * y\n", + "dydt = 3 * x - y" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The governing equations must then be added to the dictionary `model.rhs`. The dictionary stores key and item pairs, where the key is the variable which is governed by the equation stored in the corresponding item. Note that the keys are the symbols that represent the variables and are not the variable names (e.g. the key is `x`, not the string \"x\")." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "model.rhs = {x: dxdt, y: dydt} " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The initial conditions are also stored in a dictionary, `model.initial_conditions`, which again uses the variable as the key" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "model.initial_conditions = {x: pybamm.Scalar(1), y: pybamm.Scalar(2)}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we can add any variables of interest to our model. Note that these can be things other than the variables that are solved for. For example, we may want to store the variable defined by $z=x+4y$ as a model output. Variables are added to the model using the `model.variables` dictionary as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "model.variables = {\"x\": x, \"y\": y, \"z\": x + 4 * y}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the keys of this dictionary are strings (i.e. the names of the variables). The string names can be different from the variable symbol, and should in general be something informative. The model is now completely defined and is ready to be discretised and solved!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using the model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We first discretise the model using the `pybamm.Discretisation` class. Calling the method `process_model` turns the model variables into a `pybamm.StateVector` object that can be passed to a solver. Since the model is a system of ODEs we do not need to provide a mesh or worry about any spatial dependence, so we can use the default discretisation. Details on how to provide a mesh will be covered in the following notebook." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "disc = pybamm.Discretisation() # use the default discretisation\n", + "disc.process_model(model);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that the model has been discretised it is ready to be solved. Here we choose the ODE solver `pybamm.ScipySolver` and solve, returning the solution at 20 time points in the interval $t \\in [0, 1]$" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "solver = pybamm.ScipySolver()\n", + "t = np.linspace(0, 1, 20)\n", + "solution = solver.solve(model, t)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After solving, we can extract the variables from the solution. These are automatically post-processed so that the solutions can be called at any time $t$ using interpolation. The times at which the model was solved at are stored in `solution.t` and the statevectors at those times are stored in `solution.y`" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "t_sol, y_sol = solution.t, solution.y # get solution times and states\n", + "x = solution[\"x\"] # extract and process x from the solution\n", + "y = solution[\"y\"] # extract and process y from the solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We then plot the numerical solution against the exact solution." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "t_fine = np.linspace(0, t[-1], 1000)\n", + "\n", + "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))\n", + "ax1.plot(t_fine, 2 * np.exp(t_fine) - np.exp(2 * t_fine), t_sol, x(t_sol), \"o\")\n", + "ax1.set_xlabel(\"t\")\n", + "ax1.legend([\"2*exp(t) - exp(2*t)\", \"x\"], loc=\"best\")\n", + "\n", + "ax2.plot(t_fine, 3 * np.exp(t_fine) - np.exp(2 * t_fine), t_sol, y(t_sol), \"o\")\n", + "ax2.set_xlabel(\"t\")\n", + "ax2.legend([\"3*exp(t) - exp(2*t)\", \"y\"], loc=\"best\")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the [next notebook](./2-a-pde-model.ipynb) we show how to create, discretise and solve a PDE model in pybamm." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.7.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/notebooks/Creating Models/2-a-pde-model.ipynb b/examples/notebooks/Creating Models/2-a-pde-model.ipynb new file mode 100644 index 0000000000..09acd1e1c5 --- /dev/null +++ b/examples/notebooks/Creating Models/2-a-pde-model.ipynb @@ -0,0 +1,306 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Creating a simple PDE model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the [previous notebook](./1-an-ode-model.ipynb) we show how to create, discretise and solve an ODE model in pybamm. In this notebook we show how to create and solve a PDE problem, which will require meshing of the spatial domain.\n", + "\n", + "As an example, we consider the problem of linear diffusion on a unit sphere,\n", + "\\begin{equation*}\n", + " \\frac{\\partial c}{\\partial t} = \\nabla \\cdot (\\nabla c),\n", + "\\end{equation*}\n", + "with the following boundary and initial conditions:\n", + "\\begin{equation*}\n", + " \\left.\\frac{\\partial c}{\\partial r}\\right\\vert_{r=0} = 0, \\quad \\left.\\frac{\\partial c}{\\partial r}\\right\\vert_{r=1} = 2, \\quad \\left.c\\right\\vert_{t=0} = 1.\n", + "\\end{equation*}\n", + "\n", + "As before, we begin by importing the pybamm library into this notebook, along with any other packages we require:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pybamm\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting up the model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As in the previous example, we start with a `pybamm.BaseModel` object and define our model variables. Since we are now solving a PDE we need to tell pybamm the domain each variable belongs to so that it can be discretised in space in the correct way. This is done by passing the keyword argument `domain`, and in this example we choose the domain \"negative particle\"." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "model = pybamm.BaseModel()\n", + "\n", + "c = pybamm.Variable(\"Concentration\", domain=\"negative particle\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that we have given our variable the (useful) name \"Concentration\", but the symbol representing this variable is simply `c`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We then state out governing equations. Sometime it is useful to define intermediate quantities in order to express the governing equations more easily. In this example we define the flux, then define the rhs to be minus the divergence of the flux. The equation is then added to the dictionary `model.rhs`" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "N = -pybamm.grad(c) # define the flux\n", + "dcdt = -pybamm.div(N) # define the rhs equation\n", + "\n", + "model.rhs = {c: dcdt} # add the equation to rhs dictionary" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Unlike ODE models, PDE models require both initial and boundary conditions. Similar to initial conditions, boundary conditions can be added using the dictionary `model.boundary_conditions`. Boundary conditions for each variable are provided as a dictionary of the form `{side: (value, type)`, where, in 1D, side can be \"left\" or \"right\", value is the value of the boundary conditions, and type is the type of boundary condition (at present, this can be \"Dirichlet\" or \"Neumann\")." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# initial conditions\n", + "model.initial_conditions = {c: pybamm.Scalar(1)}\n", + "\n", + "# boundary conditions\n", + "lbc = pybamm.Scalar(0)\n", + "rbc = pybamm.Scalar(2)\n", + "model.boundary_conditions = {c: {\"left\": (lbc, \"Dirichlet\"), \"right\": (rbc, \"Neumann\")}}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that in our example the boundary conditions take constant values, but the value can be any valid pybamm expression.\n", + "\n", + "Finally, we add any variables of interest to the dictionary `model.variables`" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "model.variables = {\"Concentration\": c, \"Flux\": N}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using the model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now the model is now completely defined all that remains is to discretise and solve. Since this model is a PDE we need to define the geometry on which it will be solved, and choose how to mesh the geometry and discretise in space." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Defining a geometry and mesh\n", + "\n", + "We can define spatial variables in a similar way to how we defined model variables, providing a domain and a coordinate system. The geometry on which we wish to solve the model is defined using a nested dictionary. The first key is the domain name (here \"negative particle\") and the entry is a dictionary giving the limits of the domain. Domains can have \"primary\", \"secondary\" or \"tabs\" dimensions. \"primary\" dimensions correspond to dimensions on which spatial operators will be applied (e.g. the gradient and divergence). In contrast, spatial operators do not act along \"secondary\" dimensions. This allows for multiple independent particles to be included into a model (such as the DFN model)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# define geometry\n", + "r = pybamm.SpatialVariable(\n", + " \"r\", domain=[\"negative particle\"], coord_sys=\"spherical polar\"\n", + ")\n", + "geometry = {\n", + " \"negative particle\": {\n", + " \"primary\": {r: {\"min\": pybamm.Scalar(0), \"max\": pybamm.Scalar(1)}}\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We then create a mesh using the `pybamm.MeshGenerator` class. As inputs this class takes the type of mesh and any parameters required by the mesh. In this case we choose a uniform one-dimensional mesh which doesn't require any parameters. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# mesh and discretise\n", + "submesh_types = {\"negative particle\": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh)}\n", + "var_pts = {r: 20}\n", + "mesh = pybamm.Mesh(geometry, submesh_types, var_pts)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Example of meshes that do require parameters include the `pybamm.Exponential1DSubMesh` which clusters points close to one or both boundaries using an exponential rule. It takes a parameter which sets how closely the points are clustered together, and also lets the users select the side on which more points should be clustered. For example, to create a mesh with more nodes clustered to the right (i.e. the surface in the particle problem), using a stretch factor of 2, we pass an instance of the exponential submesh class and a dictionary of parameters into the `MeshGenerator` class as follows: `pybamm.MeshGenerator(pybamm.Exponential1DSubMesh, submesh_params={\"side\": \"right\", \"stretch\": 2})`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After defining a mesh we choose a spatial method. Here we choose the Finite Volume Method. We then set up a discretisation by passing the mesh and spatial methods to the class `pybamm.Discretisation`. The model is then processed, turning the variables into (slices of) a statevector, spatial variables into vector and spatial operators into matrix-vector multiplications." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "spatial_methods = {\"negative particle\": pybamm.FiniteVolume()}\n", + "disc = pybamm.Discretisation(mesh, spatial_methods)\n", + "disc.process_model(model);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that the model has been discretised we are ready to solve. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Solving the model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As before, we choose a solver and times at which we want the solution returned. We then solve, extract the variables we are interested in, and plot the result." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# solve\n", + "solver = pybamm.ScipySolver()\n", + "t = np.linspace(0, 1, 100)\n", + "solution = solver.solve(model, t)\n", + "\n", + "# post-process, so that the solution can be called at any time t or space r\n", + "# (using interpolation)\n", + "c = solution[\"Concentration\"]\n", + "\n", + "# plot\n", + "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))\n", + "\n", + "ax1.plot(solution.t, c(solution.t, r=1))\n", + "ax1.set_xlabel(\"t\")\n", + "ax1.set_ylabel(\"Surface concentration\")\n", + "r = np.linspace(0, 1, 100)\n", + "ax2.plot(r, c(t=0.5, r=r))\n", + "ax2.set_xlabel(\"r\")\n", + "ax2.set_ylabel(\"Concentration at t=0.5\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the [next notebook](./3-negative-particle-problem.ipynb) we build on the example here to to solve the problem of diffusion in the negative electrode particle within the single particle model. In doing so we will also cover how to include parameters in a model. " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.7.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/notebooks/Creating Models/3-negative-particle-problem.ipynb b/examples/notebooks/Creating Models/3-negative-particle-problem.ipynb new file mode 100644 index 0000000000..de365eab89 --- /dev/null +++ b/examples/notebooks/Creating Models/3-negative-particle-problem.ipynb @@ -0,0 +1,324 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# A step towards the Single Particle Model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the [previous notebook](./2-a-pde-model.ipynb) we saw how to solve a PDE model in pybamm. Now it is time to solve a real-life battery problem! We consider the problem of spherical diffusion in the negative electrode particle within the single particle model. That is,\n", + "\\begin{equation*}\n", + " \\frac{\\partial c}{\\partial t} = \\nabla \\cdot (D \\nabla c),\n", + "\\end{equation*}\n", + "with the following boundary and initial conditions:\n", + "\\begin{equation*}\n", + " \\left.\\frac{\\partial c}{\\partial r}\\right\\vert_{r=0} = 0, \\quad \\left.\\frac{\\partial c}{\\partial r}\\right\\vert_{r=R} = -\\frac{j}{FD}, \\quad \\left.c\\right\\vert_{t=0} = c_0,\n", + "\\end{equation*}\n", + "where $c$ is the concentration, $r$ the radial coordinate, $t$ time, $R$ the particle radius, $D$ the diffusion coefficient, $j$ the interfacial current density, $F$ Faraday's constant, and $c_0$ the initial concentration. \n", + "\n", + "In this example we use the following parameters:\n", + "\n", + "| Symbol | Units | Value |\n", + "|:-------|:-------------------|:-----------------------------------------------|\n", + "| $R$ | m | $10 \\times 10^{-6}$ |\n", + "| $D$ | m${^2}$ s$^{-1}$ | $3.9 \\times 10^{-14}$ |\n", + "| $j$ | A m$^{-2}$ | $1.4$ |\n", + "| $F$ | C mol$^{-1}$ | $96485$ |\n", + "| $c_0$ | mol m$^{-3}$ | $2.5 \\times 10^{4}$ |\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that all battery models in PyBaMM are written in dimensionless form for better numerical conditioning This is discussed further in [the simple SEI model notebook](./5-a-simple-SEI-model.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting up the model\n", + "As before, we begin by importing the pybamm library into this notebook, along with any other packages we require, and start with an empty `pybamm.BaseModel`\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pybamm\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "model = pybamm.BaseModel()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We then define all of the model variables and parameters. Parameters are created using the `pybamm.Parameter` class and are given informative names (with units). Later, we will provide parameter values and the `Parameter` objects will be turned into numerical values. For more information please see the [parameter values notebook](../parameter-values.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "R = pybamm.Parameter(\"Particle radius [m]\")\n", + "D = pybamm.Parameter(\"Diffusion coefficient [m2.s-1]\")\n", + "j = pybamm.Parameter(\"Interfacial current density [A.m-2]\")\n", + "F = pybamm.Parameter(\"Faraday constant [C.mol-1]\")\n", + "c0 = pybamm.Parameter(\"Initial concentration [mol.m-3]\")\n", + "\n", + "c = pybamm.Variable(\"Concentration [mol.m-3]\", domain=\"negative particle\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we define our model equations, boundary and initial conditions, as in the previous example. " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# governing equations\n", + "N = -D * pybamm.grad(c) # flux\n", + "dcdt = -pybamm.div(N)\n", + "model.rhs = {c: dcdt} \n", + "\n", + "# boundary conditions \n", + "lbc = pybamm.Scalar(0)\n", + "rbc = -j / F / D\n", + "model.boundary_conditions = {c: {\"left\": (lbc, \"Dirichlet\"), \"right\": (rbc, \"Neumann\")}}\n", + "\n", + "# initial conditions \n", + "model.initial_conditions = {c: c0}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we add any variables of interest to the dictionary `model.variables`" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "model.variables = {\n", + " \"Concentration [mol.m-3]\": c,\n", + " \"Surface concentration [mol.m-3]\": pybamm.surf(c),\n", + " \"Flux [mol.m-2.s-1]\": N,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using the model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to discretise and solve the model we need to provide values for all of the parameters. This is done via the `pybamm.ParameterValues` class, which accepts a dictionary of parameter names and values" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "param = pybamm.ParameterValues(\n", + " {\n", + " \"Particle radius [m]\": 10e-6,\n", + " \"Diffusion coefficient [m2.s-1]\": 3.9e-14,\n", + " \"Interfacial current density [A.m-2]\": 1.4,\n", + " \"Faraday constant [C.mol-1]\": 96485,\n", + " \"Initial concentration [mol.m-3]\": 2.5e4,\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here all of the parameters are simply scalars, but they can also be functions or read in from data (see [parameter values notebook](../parameter-values.ipynb))." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As in the previous example, we define the particle geometry. Note that in this example the definition of the geometry contains a parameter, the particle radius $R$" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "r = pybamm.SpatialVariable(\"r\", domain=[\"negative particle\"], coord_sys=\"spherical polar\")\n", + "geometry = {\"negative particle\": {\"primary\": {r: {\"min\": pybamm.Scalar(0), \"max\": R}}}}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Both the model and geometry can now be processed by the parameter class. This replaces the parameters with the values" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "param.process_model(model)\n", + "param.process_geometry(geometry)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now set up our mesh, choose a spatial method, and discretise our model" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "submesh_types = {\"negative particle\": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh)}\n", + "var_pts = {r: 20}\n", + "mesh = pybamm.Mesh(geometry, submesh_types, var_pts)\n", + "\n", + "spatial_methods = {\"negative particle\": pybamm.FiniteVolume()}\n", + "disc = pybamm.Discretisation(mesh, spatial_methods)\n", + "disc.process_model(model);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The model is now discretised and ready to be solved." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Solving the model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As is the previous example, we choose a solver and times at which we want the solution returned." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA6AAAAEYCAYAAABCw5uAAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nOzdd3xU1bbA8d9KDySkQGgJEDoGpIYmLeBFihR7ufaGCor92tu7evUKNkRQsQAqgteGCgJSQgcpRkB6ldBrCIRAynp/zAEjpgyQyaSs7+dzPjOzT5k1vPvcWefsvbaoKsYYY4wxxhhjjKf5eDsAY4wxxhhjjDFlgyWgxhhjjDHGGGOKhCWgxhhjjDHGGGOKhCWgxhhjjDHGGGOKhCWgxhhjjDHGGGOKhJ+3AyhqlSpV0tjYWG+HYYwxpgRZtmzZflWN8nYc3mD9pjHGmHORV99Z5hLQ2NhYli5d6u0wjDHGlCAiss3bMXiL9ZvGGGPORV59pw3BNcYYY4wxxhhTJCwBNcYYY4wxxhhTJCwBNcYYY4wxxhhTJMrcHFBjjDHGGGNM2ZORkUFycjLp6eneDqVUCQoKIiYmBn9/f7eOtwTUGGOMMcYYU+olJycTGhpKbGwsIuLtcEoFVeXAgQMkJydTu3Ztt86xIbjGGGOMMcaYUi89PZ2KFSta8lmIRISKFSue1VNljyWgIlJDRGaJyGoR+V1EHnDaXxCRHSKS5Gy9c5zzpIhsFJF1ItIjR3tPp22jiDyRo722iCx22ieISICnfo8xxhjjSfn0m1c7n7NFJD7H8Tfk6EuTnP3NnX2JTr95al9lpz3Q6S83Ov1nrDd+qzHGeIsln4XvbP9NPfkENBN4RFXjgHbAIBGJc/a9qarNnW0ygLPvOqAx0BMYISK+IuILvAv0AuKA63Nc57/OteoBh4A7PPh7jDHGGE/Kq99cBVwBzMl5sKp+fqovBW4CtqhqUo5DbsjR1+512u4ADjn95pu4+lFjjDGmyHhsDqiq7gJ2Oe9TRWQNEJ3PKf2B8ap6AtgiIhuBNs6+jaq6GUBExgP9net1A/7pHDMGeAEYWdi/5UyvT1tHcIAvAxPqefqrjDHGlBF59Zuq+jMUeIf5emC8G1/TH1dfCfAVMFxERFX1XON2x+G0kzz93SrK+ftSPtCP4ABfyvn7Ui7Qj3IBvs6W9/sgfx97amGMMaVEkRQhcob4tAAWAx2A+0TkZmAprru9h3Alp4tynJbMnwnr9jPa2wIVgcOqmpnL8Wd+/wBgAEDNmjXP+/es2pHCtoNploAaY4zxiDP6TXdciyu5zOkTEckCvgZecpLMaJw+VVUzRSQFV3+6/4zvL9R+M+1kFmt2HSHtRBZpJzNJO5lFZrb7Oa8IlPP3JTjAj/KBrqS0UkgA1cKCqBoWTPWwIKqGBVE9PJiqYUFUCHKvEqMxxhSlw4cPM27cOAYOHOjW8cOHD+ett95i06ZN7Nu3j0qVKgGuwj8PPPAAkydPply5cowePZqWLVsCMGbMGF566SUAnnnmGW655RYAli1bxq233srx48fp3bs3b7/9NiJCQkICu3btYsiQIfTr1y/XOCZMmMDTTz9No0aN+PHHH8/3n8HzCaiIhODq/B5U1SMiMhL4N6DO6+vA7Z6MQVU/AD4AiI+PP++7vB3qVWLWpDXsTkmnaljQecdnjDHGnHJmv+nG8W2BNFVdlaP5BlXdISKhzrVuAsa6G0Nh95vVw4OZ+UjCX9pOZmZz/GQWaRmZHDuR5XrvJKdpBbw/diKLfUdPsG73PvYdPcGZz29DAv2oGhZENWfLLUkNDfSzp6rGmCJ1+PBhRowY4XYC2qFDB/r06UNCQsJf2n/66Sc2bNjAhg0bWLx4Mffeey+LFy/m4MGDvPjiiyxduhQRoVWrVvTr14+IiAjuvfdeRo0aRdu2benduzdTpkyhV69eAHz++efEx8fnEoHLtddeS5UqVRg6dOg5//acPJqAiog/ro7vc1X9BkBV9+TYPwo4lUbvAGrkOD3GaSOP9gNAuIj4OU9Bcx7vUR3ru+4+/LRqF7d1cK/csDHGGFOQ3PpNN1wHfJGzQVV3OK+pIjIO15SWsfzZ1yaLiB8Qhqs/LXIBfj4E+PkQxvk9rczIymbPkXR2p6SzMyWd3SnH2XnY9XlXynHW7U7NNUktH+BLtfBgqoUFUbtSeRpUCaVh1VAaVAklLNieoBpT2r34w++s3lngPb6zEle9As/3bZzn/ieeeIJNmzbRvHlzunfvzpAhQ/K9XosWLXJtnzhxIjfffDMiQrt27Th8+DC7du0iMTGR7t27ExkZCUD37t2ZMmUKCQkJHDlyhHbt2gFw88038913351OQHMaNmwY7733Hn5+fsTFxTF+vDuzO86OxxJQcd1W/AhYo6pv5Giv5sxzAbgcV3EFgO+BcSLyBlAdqA/8AghQX0Rq4+o4rwP+qaoqIrOAq3DNe7kFmOip35NTo6oVaFUrgg/nbuG61jUJDvAtiq81xhhTiuXVbxZwjg9wDdApR5sfEK6q+52Etg8w3dn9Pa7+ciGu/nOmp+d/epq/rw8xEeWIiSiX5zEnM7PZm5p7kroz5TjfLN/B0ROZp4+vFhZ0OiFt6LzWqxxCkL/198aYc/fqq6+yatUqkpKSSE1NpXnz5rkeN27cOOLi4nLdB7Bjxw5q1Pjz+VxMTAw7duzItz0mJuZv7XnFuGXLFgIDAzl8+PDZ/kS3ePIJaAdcQ35WisipqnxP4api2xzXENytwN0Aqvq7iHwJrMZVCXCQqmYBiMh9wFTAF/hYVX93rvc4MF5EXgJ+xdVxF4lHL2nI9aMW8faMDTzRq1FRfa0xxpjSK69+MxB4B4gCJolIkqqeWqqsM7D9VKE+RyAw1Uk+fXEln6OcfR8BnzqF/g7iuqlb6gX45Z+kqio7U9JZt/sI63YfZf2eVNbtTmXh5gOczMwGwEegVsXyNKwSSoPTiWkIsRXL4+dry6obU9Lk96SyKISGhpKUlFTwgUWsadOm3HDDDVx22WVcdtllHvkOT1bBnYfr6eWZJudzzsvAy7m0T87tPKfDbXNme1FoX7ci18THMGruZhIaRtGuTkVvhGGMMaaUyKffBPg2j3MScS3ZkrPtGNAqj+PTgavPPcrSSUSIDg8mOjyYbo2qnG7PzMpm64G00wnp+j2prNuTyrTVuzlVQynA14e6lUNoWCWEBlVDuTA6jBY1IwgJLJI6j8aYEio1NZVOnTrluq+gJ6DR0dFs3/5njdbk5GSio6OJjo4mMTHxL+0JCQlER0eTnJz8t+NzM2nSJObMmcMPP/zAyy+/zMqVK/HzK9z/ntl/Hc/DM33iWLbtEPd8tozvBnYgtlJ5b4dkjDHGmELi5+tDvcoh1KscQu8Lq51uT8/IYuPeo6cT0vW7U/lly0G+S9oJuJ6WxlWvQHytSOJjI4ivFWlFC40xhIaGkpqaevr9uT4B7devH8OHD+e6665j8eLFhIWFUa1aNXr06MFTTz3FoUOHAJg2bRqvvPIKkZGRVKhQgUWLFtG2bVvGjh3L/fff/7frZmdns337drp27UrHjh0ZP348R48eJTw8/Nx/dC4sAT0PFYL8+fjW1lz27nxuH72E8QPaUbmCdTDGGGNMaRbk70uT6DCaRIf9pT3leAYrkg+zZOshlm49yIQl2xm9YCsAMRHBtI79MyGtXzkEHx+rwmtMWVKxYkU6dOhAkyZN6NWrV4FFiIYNG8Zrr73G7t27adq0Kb179+bDDz+kd+/eTJ48mXr16lGuXDk++eQTACIjI3n22Wdp3bo1AM8999zpgkQjRow4vQxLr169ci1AlJWVxY033khKSgqqyuDBgws9+QSQEl574KzFx8fr0qVLC/WaS7ce5OaPf6FqWBDj77Ik1BhjShsRWaaqedeoL8U80W+WFRlZ2azZdeR0Qrpk6yH2Hz0BQIUgP+JzJKRNY8KsyJExHrZmzRouuOACb4dRrCQkJDB06NB8l2EBSExMZOjQoXmuA5rbv21efac9AS0E8bGRjL6tDbd+8gtXvbeQ0be1pk5UiLfDMsYYY4wX+fv60DQmnKYx4dzRsTaqyh8H004npEu3HWLm2r2Aay5pk+gKzlPSSFrViiCyfICXf4ExprSLjIzk1ltv5T//+Q/9+vXL9ZgJEybw4osv0qpVruUFzpo9AS1Ey/84xJ1jlqKqfHhLPK1qRXrke4wxxhQtewJqT0A95eCxkyzb9mdCuiL5MBlZrr/NGlUNJaFhZS6+oDItaoRbtV1jztOaNWto1KgRrlWvTGFRVdauXev2E1BLQAvZ1v3HuG30EnYcPs5LlzXhmvgaBZ9kjDGmWLME1BLQopKekcWK5BSWbD3I3A37WLr1EJnZSliwP10aRNGtUWW6NIgiwp6OGnPWtmzZQmhoKBUrVrQktJCoKgcOHCA1NZXatWv/ZZ8loI6i6EgPHjvJfeOWs2DTAa5vU4Pn+za2eR3GGFOCWQJqCai3HEnPYO76/cxcu5fEdXs5cOwkPgItakbQrVFlujaszAXVQu2PaWPckJGRQXJyMunp6d4OpVQJCgoiJiYGf3//v7RbAuooqo40Myub139ez8jETVwYHcbIG1vmuQC2McaY4s0SUEtAi4PsbGXFjhRmrt3LrLV7WbkjBYBqYUEkNKxMt0aV6VCvIuUCrMSHMcb7LAF1FHVHOu333Tzy5W+IwMuXX0jfZtWL7LuNMcYUDktALQEtjvYeSSdx3T5mrt3L3A37OHYyiwA/H9rVqUi3hlF0a1SFmhXt5rcxxjssAXV4oyPdduAYD4xPImn7Ya5oEc2L/RsTGuRf8InGGGOKBUtALQEt7k5mZrNk60Fmrt3LzLV72bL/GAB1o8rTrVFlusdVJb5WhK09aowpMpaAOrzVkWZkZTN85kbembmB6uHBvHltc1rHWpVcY4wpCSwBtQS0pNmy/9jpobqLtxwgI0upHhZE3+bV6d8s2uaNGmM8zhJQh7c70mXbDvHQhCS2H0rjjg61eeSShgQHWIEiY4wpziwBtQS0JDt6IpMZa/YwMWknc9bvIzNbqV85hMtaRNOvWXVqRNowXWNM4bME1FEcOtKjJzJ59ac1fLboD2pVLMerVzSlfd2KXo3JGGNM3iwBtQS0tDh47CSTVu7i+6QdLNl6CICWNcO5rEU0vS+sRqWQQC9HaIwpLSwBdRSnjnThpgM88c0Kth1I48Z2NXmi1wWEBFrlOmOMKW4sAS0e/aYpXMmH0vj+t518n7STtbtT8fUROtarxGUtqtM9rqr9TWKMOS+WgDqKW0d6/GQWr09bx0fzt1CtQhAvX34hXRtV9nZYxhhjcrAEtPj0m8Yz1u4+wsQkVzK64/Bxgvx96B5Xlf7NqtO5QRQBfj7eDtEYU8JYAuoorh3p8j8O8fhXK9iw9yi9mlTlub5xVAsL9nZYxhhjsAS0OPabxjOys5XlfxxiYtJOflyxk0NpGYQF+9P7wmpc1rw6rWMjrZKuMcYtloA6inNHejIzm1FzNzNsxgb8fISHL2nILe1r4edrdx2NMcabLAEtnv2m8ayMrGzmbdjPxKQdTFu9h7STWVQPC6J/i2iub13T1hg1xuTLElBHSehI/ziQxrMTVzF7/T4aV6/Ay5dfSPMa4d4OyxhjyixLQIt3v2k8L+1kJj+vdlXSnb1+H9mqdGkQxU3tapHQsDK+9lTUGHMGS0AdJaUjVVV+WrWbF3/4nb2pJ7ixbS0e7dGQsGB/b4dmjDFljiWgxb/fNEVnd0o6X/zyB+OX/MGeIyeIDg/mn21rcm3rGlZF1xhzmiWgjpLWkaamZ/D6tPWMXbiVyPIB/KtnI65qGWPzL4wxpghZAlpy+k1TdDKyspm+eg+fLtrGgk0H8PcVejWpxk3taxFfKwIR+1vFmLLMElBHSe1IV+1I4bmJq1j+x2Ga1wjnxX6NaWbDco0xpkhYAlry+k1TtDbuPcrni7fx1bJkUtMzaVgllBvb1+LyFtG2nIsxZVRefadVtykhmkSH8dU9F/H61c1IPnScy0bM54mvV3Dg6Alvh2aMMaaQiEgNEZklIqtF5HcRecBpv9r5nC0i8TmOjxWR4yKS5Gzv5djXSkRWishGERkmzuMoEYkUkZ9FZIPzGlH0v9SUNvUqh/B838Ysfupi/nvlhfj7Cc9+t4q2L0/nme9Wsnb3EW+HaIwpJvJ9AioiK9y4xj5VvbjwQvKs0nAn90h6BsOmb2D0gq2UC/Dl4e4NuLGdVcs1xhhPKaonoCJSDaimqstFJBRYBlwGKJANvA88qqpLneNjgR9VtUku1/oFGAwsBiYDw1T1JxF5DTioqq+KyBNAhKo+nldMpaHfNEVPVfktOYVPF27jhxU7OZmZTevYCG5sV4ueTaoS6Ofr7RCNMR6WV99Z0JgIX6B3ftcFvs/jC2sAY4EquDrOD1T1bREZAvQFTgKbgNtU9bDTia4B1jmXWKSq9zjXagWMBoJxdaIPqKqKSCQwAYgFtgLXqOqhAn5TiVchyJ9n+sRxXZsavPD9al74YTXjl2znhX6NaVenorfDM8YYc45UdRewy3mfKiJrgGhV/Rlwe06dk8hWUNVFzuexuBLZn4D+QIJz6BggEcgzATXmXIgIzWuE07xGOM9cegFfLUvms8XbeGB8EpVCArgmvgb/bFuTmAhbysWYsqagR2Z3q+q2fLatwMA8zs0EHlHVOKAdMEhE4oCfgSaq2hRYDzyZ45xNqtrc2e7J0T4SuAuo72w9nfYngBmqWh+Y4XwuM+pVDuXTO9rw3o0tSU3P5LoPFnH/F7+yK+W4t0Mzxhhznpwbsy1wPcHMT20R+VVEZotIJ6ctGkjOcUyy0wZQxUl0AXbjulFsjMdElA/grs51mPVIAmNvb0OLmhG8N3sTnV6bxZ1jlrBk60Fvh2iMKUL5PgFV1XkFXSCvY/K5izstx2GLgKvyu77dxc2fiNCzSTW6NKjMyNmbeG/2Jqav3sO9CXUZ0LkOQf42xMUYY0oaEQkBvgYeVNX8Js/tAmqq6gFntNB3ItLY3e9xRhP9bS6OiAwABgDUrFnz7II3Jg8+PkLnBlF0bhDFjsPHGf/LH3y++A+ufm8hrWMjGJhQj4SGUVY915hSLt8noCLSSER+EpFJIlJXREaLyGER+UVELnD3S/K5i3s7rkTyFLuLe46CnbmgMx7uQkLDKN74eT0Xvz6bH37bSVmrdGyMMSWZiPjjSj4/V9Vv8jtWVU+o6gHn/TJcU1saADuAmByHxjhtAHucm7unbvLuzeW6H6hqvKrGR0VFne9PMuZvosODeeSShsx/vBvP941jx6Hj3DZ6Cb2HzeP733aSmZXt7RCNMR5S0BDcD4ARwGfATGAKEAH8GxjuzhfkdRdXRJ7GNUz3c6fp1F3cFsDDwDgRqeDuD1FXlpVrpiUiA0RkqYgs3bdvn7uXLJFqRJZj5I2t+OKudlQI9uf+L37lmvcXsjI5xduhGWOMKYBTqfYjYI2qvuHG8VEi4uu8r4Nrmspm5+bsERFp51zzZmCic9r3wC3O+1tytBtT5IIDfLmtQ21m/6srQ69uxsnMLAZ/8SsXvzGbzxdvIz0jy9shGmMKWUFVcH91EkJEZKOq1suxb7mqtsz34q67uD8CU3N2pCJyK3A3cLGqpuVxbiLwKK47trNUtZHTfj2QoKp3i8g65/0u5y5uoqo2zC+mslTNLytb+d/S7Qydto4Dx05yZcsY/tWjIZUrBHk7NGOMKVGKsApuR2AusBJX1VuAp4BA4B0gCjgMJKlqDxG5Evg/IMM5/nlV/cG5Vjx/FvD7CbjfGXJbEfgSqAlsw1XAL89JeGWp3zTel52tTFu9h5GJG/ktOYWo0EDu7FibG9rVsvVEjSlh8uo7C1yGxSkWhIgMVNUROfatyq3se479gmte5kFVfTBHe0/gDaCLqu7L0R7lHJvl3MWdC1yoqgdzKSX/jqpOdirqHshRSj5SVf+V3z9EWexIU9MzGD5zIx/P34K/rw+Dutbjjo61bX6oMca4qagS0OKoLPabxvtUlQWbDjAycRPzNu6nQpAft1wUy60XxVIxJNDb4Rlj3HCuCejduOagHD2jvR5wX87EMpdz87qLOwzXndwDTtsiVb2nKO7iQtnuSLfuP8Z/Jq9h2uo9xEQE81TvC+jVpKpN9jfGmAJYAlo2+01TPPy2/TAjEzcxdfVuAv18uK51Te7qXIfo8GBvh2aMycc5JaClkXWksGDjfv7vx9Ws3Z1Km9qRPNcnjibRYd4Oyxhjii1LQMt2v2mKh417U3lv9ma++9VVT6t/82ju6VKH+lVCvRyZMSY3hZaAujP3szizjtQlK1sZv+QPXp+2nkNpJ7mmVQ0e7dGQqFAb1mKMMWeyBNT6TVN87Dh8nFFzNjN+yR+kZ2RzSVwVBnatR/Ma4d4OzRiTQ2EmoKcLE5VE1pH+VcrxDIbP3MDoBVsJ9PNlUNd63N4xlkA/mx9qjDGnWAJq/aYpfg4eO8no+VsYvWArR9Iz6VCvIg93b0irWhHeDs0YQ959Z0HLsORmUiHEY4qJsGB/nr40jmkPdaFdnUj+O2Ut3d+Yw5RVu239UGOMMcYUW5HlA3j4koYsePJinurdiHW7U7ly5ALuGruU9XtSvR2eMSYPZ/UE1FmX83QN7IIK/hRHdic3f3M37OPfP65m/Z6jtK0dybM2P9QYY+wJqPWbpgQ4diKTT+Zv4f3Zmzl6MpMrWsTwUPf6xESU83ZoxpRJ5zUE16mG+yKQjqtCrQCqqnUKO1BPs460YJlZ2Yxfsp03fnbND72yZQyP9WhIFVs/1BhTRlkCav2mKTkOHTvJiMSNjFm4DRRuaFeT+7rWs+VbjCli55uAbgDaq+p+TwRXlKwjdd+R9AzenbmRT+ZvxddHuKdLXQZ0rkNwgM0PNcaULZaAWr9pSp6dh4/z9vQN/G/ZdoL9fbmrcx3u7FSHkEC/gk82xpy3850DuglIK9yQTHFXIcifJ3tfwPSHu9C1URRvTl9P16GJfLM8mexsmx9qjDHGmOKrengw/72qKdMe6kLnBlG8NX0DnV+bxUfztnAiM8vb4RlTZrn7BLQF8AmwGDhxql1VB3suNM+wO7nnbsnWg/z7x9WsSE6haUwYz1waR5vakd4OyxhjPM6egFq/aUq+37Yf5r9T1rJg0wGiw4N5qHsDLm8Rja+PeDs0Y0ql830C+j4wE1gELMuxmTKkdWwk3w3swJvXNmNf6gmueX8h9362jD8O2MNxY4wxxhRvzWqEM+6udnx2R1siywfw6P9+o+dbc5j2u1X+N6YoufsEtESv/ZmT3cktHMdPZjFq7mZGJm4iK1u5rUMsg7rVo0KQv7dDM8aYQmdPQK3fNKWLqvLTqt0MnbqOzfuP0aJmOI/3bES7OhW9HZoxpcb5FiH6D7AV+IG/DsG1ZVjKuD1H0hk6dR1fLU8molwAD3VvwPWta+Dney5LzBpjTPF0NgmoiBwp6BBgl6o2OP/IPM/6TVOaZWZl89WyZN6avoHdR9Lp0iCKx3o0tCXojCkE55uAbsml2ZZhMaet2pHCv39czeItB6lfOYSnL72AhIaVvR2WMcYUirNMQAscNVSSRhZZv2nKgvSMLMYu3Mq7szaRcjyDvs2q83jPhraGqDHn4bwS0NLEOlLPUVWmrd7DK5PXsPVAGp0bRPHMpRfQoEqot0MzxpjzcpYJaB1V3Xy+xxQX1m+asiTleAaj5mzmw3mbUYWBCfW4u0sdgvxtCTpjztb5FiHK7YJVzy8kU9qICD0aV2XaQ1145tILSPrjED3fmsPT367kwNETBV/AGGNKgdwSSxGJLOgYY4z3hQX782iPhsx4JIF/XFCFN6ev5x9vzLZCRcYUovOZqPdRoUVhSpUAPx/u7FSH2Y915eb2sYxfsp2EIYm8P3uTrbtljCn1RKSDiKwRkd9FpK2I/AwsEZHtItLe2/EZYwoWHR7Muze0ZNydbSkX4MuAT5dxyydL2LTvqLdDM6bEsyG4xuM27j3KK5PXMGPtXmpEBvNkrwvo1aQqIrbuljGmZDjLIbi/AHcAIbiK912mqvNEpCXwjqp28GCohc76TVPWZWRlM3bhNt76eT3pmVnc3rE293erT0ign7dDM6ZYO6chuCISmd/muXBNaVKvcggf3dqaz+5oS/kAPwZ+vpxr3l/Ib9sPezs0Y4zxBH9VXamqC4F9qjoPQFWXA8HeDc0Yc7b8fX24o2NtZj6awGXNo3l/9ma6DU3ku1932LBcY85BQUNwlwFLndczN7sdas5Kx/qVmDS4E69ecSFb9qfR/935PDQhiV0px70dmjHGFKacfeuTZ+wLKMpAjDGFJyo0kCFXN+ObgRdRNSyIByckcc37C/l9Z4q3QzOmRLEhuMYrjp7IZGTiRkbN3YKPwIBOdbi7S13K23AWY0wxdJZDcPsB01U17Yz2usCVqvqaJ2L0FOs3jfm77Gzly6XbeW3qOg6nneSGtrV45JIGhJeze0zGnHLey7A4HWpn52Oiqv5YiPEVGetIi5fkQ2n8d8o6fvhtJ5VDA3m0R0OuahmDj4/NDzXGFB9nk4DmcX5VVd1dmDEVFes3jclbSloGb/y8jk8XbTtdQfe61jXxtb9jjDm/ZVhE5FXgAWC1sz0gIv8p3BBNWRQTUY53rm/B1/deRHREMP/6agV9h89j4aYD3g7NGGMK0+SCDhCRGiIyS0RWOxV0H3Dar3Y+Z4tIfI7ju4vIMhFZ6bx2y7EvUUTWiUiSs1V22gNFZIKIbBSRxSISW/g/1ZiyI6ycPy/2b8KkwZ2oXyWUp79dRf9357Fs2yFvh2ZMseXuMiy9ge6q+rGqfgz0BPp4LixT1rSqFcE3917EsOtbcDgtg+tHLWLA2KVs2X/M26EZY0xhcOdxSCbwiKrGAe2AQSISB6wCrgDmnHH8fqCvql4I3AJ8esb+G1S1ubPtddruAA6paj3gTeC/5/ZzjDE5XVCtAhMGtOPt65qzL/UEV45cwMNfJrE3Nd3boRlT7JzNOqDhOd6HFXYgxogI/ZpVZ8YjXXisR0Pmb9zPJW/O5t8/riYlLcPb4RljzPkYVdABqrrLqZSLquv1WrQAACAASURBVKYCa4BoVV2jqutyOf5XVd3pfPwdCBaRwAK+pj8wxnn/FXCx2JpYxhQKEaF/82hmPpLAvQl1+eG3nXQbOptRczaTkZXt7fCMKTbcTUBfAX4VkdEiMgZXFdyXPReWKcuC/H0Z1LUesx5L4KpWMXwyfwtdhs5i9Pwt9h9wY0yJIiIRItIUWCQiLZ21QN05LxZoASx286uuBJar6okcbZ84w2+fzZFkRgPbAVQ1E0gBKuby/QNEZKmILN23b5+bIRhjAMoH+vF4z0ZMfbAz8bERvDx5Df2Hz2fVDquWawy4mYCq6he4hgN9A3wNtFfVCfmdk89clkgR+VlENjivEU67iMgwZ17KipydtIjc4hy/QURuydHeypn7stE51+7iliKVQ4N45YqmTBrcicbVK/DCD6vp8dYcZqzZY+tuGWOKPRH5N7ACGAa87mxD3TgvBFdf+6CqHnHj+Ma4htLenaP5Bmdobidnu+lsYlfVD1Q1XlXjo6KizuZUY4yjTlQIn9zamvdubMne1BP0f3c+r01ZS3pGlrdDM8arzmYI7qkeyA+4SESuKOD4vOayPAHMUNX6wAznM0AvoL6zDQBGgithBZ4H2gJtgOdPJa3OMXflOK/nWfweU0JcUK0Cn93Rlo9ucdXeuGPMUm766BfW7Crw7zJjjPGma4C6qpqgql2drVt+J4iIP67k83NV/aagLxCRGOBb4GZV3XSqXVV3OK+pwDhc/SfADqCGc64frik1VvXNGA8REXo2qcb0hztzeYtoRiRuovewuSzdetDboRnjNe5Wwf0Y+BjXEJ++zpZvEaK85rLw1/knY4DLnPf9gbHqsggIF5FqQA/gZ1U9qKqHgJ+Bns6+Cqq6SF2Pw8bmuJYpZUSEiy+owtQHO/NC3zhW7Uzh0mFzeeLrFTbB3xhTXK3ir/UT8uWM4vkIWKOqb7hxfDgwCXhCVefnaPcTkUrOe39c/fUqZ/f3uAoWAVwFzFQbUmKMx4WXC2Do1c0Ye3sbTmRkc/X7C3l+4iqOncj0dmjGFDk/N49r5zzJPCdnzGWpoqq7nF27gSrO+9PzUhzJTlt+7cm5tOf2/QNwPVWlZs2a5/ozTDHg7+vDrR1qc3mLGN6ZuYExC7fyw287Gdi1Hnd0rE2Qv6+3QzTGmFNO1U9YBZyem6mq/fI4vgOuobIrRSTJaXsKCATewTUSaZKIJKlqD+A+oB7wnIg85xx/CXAMmOokn77AdP4sgvQR8KmIbAQOAtcVyi81xrilc4Mopj3UmSFT1zFm4Vamr9nLK1dcSOcGNtTdlB3uJqALRSROVVef7RecOZcl5zRNVVUR8fidV1X9APgAXAtqe/r7jOeFlfPnmT5x3NiuFq/+tJYhU9fx+aJtPN6rEX2bVsfHFoA2xnjfGFxzM1cCBVZQU9V55L1cy7e5HP8S8FIex7fK4zvSgasLisUY4znlA/14oV9jLm1ajce/XsHNH//CVa1iePbSOMLK+Xs7PGM8zt05oGNxJaHrnAJBK0VkRUEn5TGXZY8zfBbn9dTaZKfnpThinLb82mNyaTdlSGyl8rx3UyvGD2hHZEgAD4xP4vKRC2xuhTGmOEhT1WGqOktVZ5/avB2UMaZ4aB0byeTBnRiYUJdvf93BP96czZRVuwo+0ZgSzt0E9CNcw4J68uf8z775nZDPXJac809uASbmaL/ZqYbbDkhxhupOBS5xStlH4BpeNNXZd0RE2jnfdXOOa5kypl2dinw/qCOvX92M3SnHueq9hQz6fDnbD6Z5OzRjTNk1V0ReEZH2p5ZgcXcZFmNM2RDk78u/ejZi4qAORIUEcs9ny7n3s2VW38KUauJO7QERWaiq7c/qwiIdgbn8dejRU7jmgX4J1AS2Adeo6kEniRyOK8lNA25T1aXOtW53zgV4WVU/cdrjgdFAMPATcH9BxRTi4+N16dKlZ/NTTAmTdjKTUXO28N7sTWRlK7d1jGVQ13pUCLJhLcaYcyMiy1Q1/izPmZVLsxZUCbe4sX7TmKKRkZXNB3M28/aMDQT7+/JsnziubBmNrTJoSqq8+k53E9ARuCr5/cBfCykUWCK+uLGOtOzYcySdIVPX8fXyZCLKBfBQ9wZc37oGfr5ns/qQMcacWwJaWli/aUzR2rj3KI9/vYJl2w7RuUEU/7m8CTER5bwdljFnLa++092/xINxJZ6X4OYyLMZ4W5UKQQy9uhk/3NeRBlVCePa7VfR6ey6z1u0t+GRjjPEAG4JrjClIvcoh/O/u9rzQN46lWw/S4805jF24lexsq6NpSge3noCWJnYnt2xSVX5evYf/TF7D1gNpdG4QxdO9L6Bh1VBvh2aMKQEK6wmoiIxS1bsKI6aiYv2mMd6z/WAaT327krkb9tM6NoJXr2xK3agQb4dljFvO6Qmos35mQRcu8BhjvE1EuKRxVaY91IXn+sTx2/bD9Hp7Dk99u5J9qScKvoAxxhSCkpZ8GmO8q0ZkOcbe3oYhVzVl3e5Uer09lw/nbranoaZEy/cJqIhsBh7N73zg/1S1cWEH5il2J9cAHE47ydszNvDpwm0E+fsysGtdbu9QmyB/X2+HZowphs7mCWhBw2xVdXnhRFU0rN80pnjYeySdp75dxfQ1e+hUvxJDr25GlQpB3g7LmDydUxEiEfnEjWunqOqD5xNcUbKO1OS0ed9RXvlpLT+v3kN0eDBP9GpEn6bVrOKcMeYvzjIBza367SlWBdcYc85UlXG//MG/f1xNsL8vr1zRlJ5Nqno7LGNydV5VcEsT60hNbhZs2s9LP65h9a4jtKgZzrN94mhZM8LbYRljigmrgmv9pjHFyca9R3lwwq+s2nGE61rX4Nk+cZQP9PN2WMb8xflWwTWmVLuobiV+uL8jr13VlB2HjnPFiAXc/8WvJB9K83ZoxpgSSkT8RWSwiHzlbPeJiC1IbIw5b/Uqh/DNvR24N6EuE5Zup8878/ht+2Fvh2WMWywBNcbh6yNcE1+DWY8mMPji+vy8ejfdXp/Na1PWkpqe4e3wjDElz0igFTDC2Vo5bcYYc94C/Hx4vGcjxt3ZjvSMLK4cuYB3Z20kywoUmWLOElBjzlA+0I+Huzdg1qMJ9LmwGiMSN9F1aCLjFv9BZla2t8MzxpQcrVX1FlWd6Wy3Aa29HZQxpnRpX7ciUx7oTI8mVRkydR3Xj1pkI7hMseZWAioigSLyTxF5SkSeO7V5OjhjvKlaWDBvXNuc7+/rQJ1KITz17UouHTaPOev3eTs0Y0zJkCUidU99EJE6QJYX4zHGlFJh5fwZfn0LXr+6Gb/vSKHX23OZmLTD22EZkyt3n4BOBPoDmcCxHJsxpV7TmHAm3N2O925syfGMLG7++Bdu/eQXNuxJ9XZoxpji7TFglogkishsYCbwiJdjMsaUUiLCla1i+OmBztSvHMID45N4aEISR2wakSlm3KqCKyKrVLVJEcTjcVbNz5yPE5lZfLpwG2/P2EDaySz+2aYmD/6jPhVDAr0dmjHGg861Cq6IBAINnY/rVPVE4UbmedZvGlPyZGZlM3zWRt6ZuZFqYUG8dW1z4mMjvR2WKWPOtwruAhG5sJBjMqbECfTz5c5OdZj9WFdubFuTcb/8QcKQRN6fvYkTmTayzhjzJxHxBXoACcA/gEEi8rBXgzLGlAl+vj48+I8GfHl3e0TgmvcX8sa0dWRYLQtTDLibgHYElonIOhFZISIrRWSFJwMzpjiLLB/Ai/2bMPXBzrSpHckrP63lH2/MZtKKXZS1tXWNMXn6AbgVqAiE5tiMMaZItKoVweTBnbi8RQzDZm7k6vcWsnW/zaIz3uXuENxaubWr6rZCj8jDbCiR8YR5G/bz0qTVrN2dSnytCJ7pE0fzGuHeDssYU0jOZQiuiKxQ1aaeiqmoWL9pTOnw44qdPPXNSjKzlRf6NebqVjGIiLfDMqXYeQ3BdRLNcKCvs4WXxOTTGE/pWL8SkwZ34r9XXsjWA2lc9u58Hhz/KzsOH/d2aMYY7/lJRC7xdhDGGAPQp2l1pjzYmaYxYfzrqxUMGrecw2knvR2WKYPcXYblAeBzoLKzfSYi93syMGNKGl8f4drWNUl8LIH7utbjp1W76TY0kaFT13H0RKa3wzPGFL1FwLciclxEjohIqogc8XZQxpiyq3p4MJ/f2Y4nejVi2u97uHTYPFYkH/Z2WKaMcXcO6B1AW1V9TlWfA9oBd3kuLGNKrpBAPx7t0ZCZjybQq0lVhs/aSNehiUxY8gdZ2TY/1Jgy5A2gPVBOVSuoaqiqVsjvBBGpISKzRGS1iPzu3ABGRK52PmeLSPwZ5zwpIhudOg09crT3dNo2isgTOdpri8hip32CiAQU7s82xhRnvj7CPV3q8tW9FwFw1ciFfL54m9WwMEXG3QRU+Ovi2VlOmzEmD9Hhwbx1XQu+G9SBWpHlePzrlVw6bC7zN+73dmjGmKKxHVilZ/dXXSbwiKrG4brZO0hE4oBVwBXAnJwHO/uuAxoDPYERIuLrVOB9F+gFxAHXO8cC/Bd4U1XrAYdw3WQ2xpQxzWuE8+P9HWlftyJPf7uKR778jbSTNmLLeJ67CegnwGIReUFEXsA1rOgjj0VlTCnSvEY4/7unPe/+syVHT2Ryw4eLuWP0EjbuPert0IwxnrUZSHSeUD58asvvBFXdparLnfepwBogWlXXqOq6XE7pD4xX1ROqugXYCLRxto2qullVTwLjgf7iqjjSDfjKOX8McFkh/FZjTAkUUT6AT25tzcPdG/Bt0g4uf3cBm/bZ3yfGs9wtQvQGcBtw0NluU9W3PBmYMaWJiHBp02pMf7gLT/ZqxC9bDtLjrTk8P3EVh45ZAQBjSqktwAwggHNYhkVEYoEWwOJ8DovG9aT1lGSnLa/2isBhVc08o/3M7x4gIktFZOm+ffvcDdkYUwL5+AiDL67PmNvasDc1nf7D5zN55S5vh2VKMb/8dopIBVU9IiKRwFZnO7UvUlUPejY8Y0qXIH9f7u5Sl6taxfDW9A18tvgPvv11B4Mvrs9N7WsR6Ofr7RCNMYVEVV8813NFJAT4GnhQVYu8cJGqfgB8AK5lWIr6+40xRa9zgygmDe7EoHHLGfj5cm7vUJsnezfC39fdAZPGuKeg/0WNc16XAUtzbKc+G2POQcWQQP59WROmPNCJlrUieGnSGi55cw5TVu2yIgDGlHDOVJVzPkZE/HEln5+r6jcFXGoHUCPH5xinLa/2A0C4iPid0W6MMVQPD2bCgPbcelEsH8/fwnUfLGJ3Srq3wzKlTL5PQFW1j/Nau2jCMaZsqV8llNG3tWH2+n28PGk193y2nDa1I3n20jgujAnzdnjGmHNzZwHLrQiuwkEv/G2Ha47mR8AaZ/pLQb4HxonIG0B1oD7wi/Md9UWkNq4E8zrgn6qqIjILuArXvNBbgInu/jBjTOkX4OfDC/0a06pWBE98vYJLh81l2PUt6FCvkrdDM6WEu+uAznCn7Yz9H4vIXhFZlaNtgogkOdtWEUly2mOdddJO7XsvxzmtRGSlUy5+mNM5IyKRIvKziGxwXiPc/dHGFDddGkQxeXAn/nP5hWzed5S+w+fx8JdJ7Eo57u3QjDFnbxR/nfN55hbiHJObDsBNQLccfWJvEblcRJJxLesySUSmAqjq78CXwGpgCjBIVbOcOZ73AVNxFTL60jkW4HHgYRHZiGtOqBUVNMb8Td9m1Zl4X0ciywdw00eLGT5zA9m2nJwpBJLfcD8RCQLKAbOABP5ceqUCMEVVG+VzbmfgKDBWVZvksv91IEVV/88ptPBjHsf9AgzGVYRhMjBMVX8SkdeAg6r6qrO+WYSqPl7QD46Pj9elS230sCm+UtMzGJG4iY/mbcFHYEDnutzduQ7lA/MdsGCM8SARWaaq8QUfWfpYv2lM2XbsRCZPfbuSiUk76dowijevbU54OVs+2BQsr76zoCegd+Oa79nIeT21TQSG53eiqs7BVTE3t2AEuAb4ooCgqwEVVHWRs47aWP4sF98fV/l4sDLyphQJDfLn8Z6NmPFwF7rHVWXYjA10HZrIl0u3251HY4wxxhSp8oF+vHVtc/7dvzHzNu7n0mHzWJF82NthmRIs3wRUVd925n8+qqp1VLW2szVT1XwT0AJ0Avao6oYcbbVF5FcRmS0inZy2aFwl4k/JWS6+iqqeqhG9G6iS15dZOXlTEtWILMc717fg63svIjoimH99tYK+w+exYNN+b4dmjDHGmDJERLipfSz/u+ciAK4auZDPF2+zwonmnLi7Dug7ItJERK4RkZtPbefxvdfz16efu4CaqtoCeBhXQYUK7l7MeTqa5/8HqOoHqhqvqvFRUVHnGrMxXtGqVgTf3HsR71zfgsNpGfxz1GLuGruUzbZQtDHGGGOKUPMa4fx4f0fa163I09+u4pEvfyPtZGbBJxqTg1uTykTkeVxzQONwzcPsBczDNST2rDil368AWp1qU9UTwAnn/TIR2QQ0wFW5LybH6TnLxe8RkWqqussZqrv3bGMxpqQQEfo2q073uCp8PH8LI2Zt4pI353BT+1o8cHF9m4thTDEkIlHAXUAsOfpbVb3dWzEZY8z5iigfwCe3tmb4rI28OX09v+88wogbW1I3KsTboZkSwt2VZa8CLgZ2q+ptQDPgXNeI+AewVlVPD60VkSgR8XXe18FVRn6zM8T2iIi0c+aN3syf5eK/x1U+HqyMvCkjgvx9GZhQj1mPJnBN6xqMWbCVLkMS+XjeFk5mZns7PGPMX03E1VdOBybl2IwxpkTz8REGX1yfsbe3YW9qOv2Hz2fKql0Fn2gM7iegx1U1G8h0hsbu5a8LXP+NiHwBLAQaikiyiNzh7LqOvxcf6gyscJZl+Qq4R1VPFTAaCHwIbAQ2AT857a8C3UVkA66k9lU3f4sxJV5UaCD/ufxCfnqgM01jwvi/H1fT4605TPt9t83HMKb4KKeqj6vql6r69anN20EZY0xh6VQ/ikmDO1Gvcgj3fLacYTM22N8hpkD5LsNy+iCREcBTuJLHR3Atr5LkPA0tUaycvCltVJXE9ft4edIaNu49Srs6kTxzaRxNos91kIIx5kznsgyLiLwELFDVyR4Kq0hYv2mMKUh6RhZPfbuSb5bvoE/Tagy5qhnBAb7eDst4WV59Z4EJqDP0NUZVtzufY3EtjbLCA3F6nHWkprTKzMrmiyXbefPn9RxKO8mVLWN4rEdDqlQI8nZoxpR455iApgLlgZNAhtOsqup2kb3iwPpNY4w7VJUP5mzm1SlraVI9jFE3x1M1zP4GKcvOdR3QUxVmJ+f4vLWkJp/GlGZ+vj7c1K4WiY8lMKBzHb5P2knCkETenr7BKtQZ4wWqGqqqPqoa5LwPLWnJpzHGuEtEuLtLXUbdFM/mfUfpN3weSdttvVDzd+7OAV0uIq09GokxplBUCPLnyV4XMP3hLnRrVJk3p6+n29DZfL0smexsm5dhTFESkX4iMtTZ+ng7HmOM8bR/xFXhm4EdCPDz4dr3FzIxaUfBJ5kyxd0EtC2wUEQ2icgKEVkpIvYU1JhirGbFcrx7Q0u+uqc9VSoE8sj/fqPfu/NYvPmAt0MzpkwQkVeBB4DVzvaAiLzi3aiMMcbzGlYNZeKgDjSrEc4D45MYOnWd3QQ3p7lbhKhWbu2quq3QI/Iwm8tiyqLsbOWHFTv5709r2ZmSTs/GVXmiVyNiK5X3dmjGlAjnOAd0BdDcqSKPs9zYr6ra1BMxeor1m8aYc3UyM5vnJq5i/JLtXBJXhTevbU75QL+CTzSlwjnPAXW8pKrbcm7AS4UbojHGU3x8hP7No5n5aAKPXtKAORv20f3N2bz042pS0jIKvoAx5lyF53hvpamNMWVKgJ8Pr1xxIc/1iWP6mj1cOXIByYfSvB2W8TJ3E9DGOT84d3FbFX44xhhPCvL35b5u9Ul8LIErW8bw0fwtdBk6i9Hzt5CRle3t8IwpbV4BfhWR0SIyBlgGvOzlmIwxpkiJCLd3rM0nt7Vhx+Hj9B8+n6VbD3o7LONF+SagIvKkU0a+qYgccbZUYC8wsUgiNMYUusqhQbx6ZVMm3d+JxtUr8MIPq+nx1hxmrNljC0gbU0hU9QugHfAN8DXQXlUneDcqY4zxji4Novh2YAdCg/y4ftQi/rd0u7dDMl6SbwKqqq+oaigwRFUrOFuoqlZU1SeLKEZjjIfEVa/AZ3e05eNbXcPz7xizlBs/WszqnUe8HJkxJZeINHJeWwLVgGRnq+60GWNMmVSvcgjfDepAm9qRPPbVCl6etJosK05U5rhVhAhARKKBWsDpmcOqOsdDcXmMFVMwJncZWdmMW/wHb05fT8rxDK5pVYNHejSgcqgtIm3M2RQhEpEPVHWAiMzKZbeqardCDs+jrN80xhS2jKxs/v3jasYu3EbXhlEMu74FoUH+3g7LFLK8+k53q+C+ClyHq4x8ltOsqtqvUKMsAtaRGpO/lLQMhs/awOgFW/H39WFgQl3u7FSHIH9fb4dmjNecYxXcIFVNL6ituLN+0xjjKZ8u2sYL3/9OnUrl+fCWeGpVtOr8pcn5VsG9HGioqr1Vta+zlbjk0xhTsLBy/jx9aRzTH+5ClwZRDJ22nm5DE/nu1x22hpcxZ2eBm23GGFMm3dSuFp/e3oa9qSfo/+58Fm6ytcrLAncT0M2APRc3pgypVbE8I29sxYQB7agYEsiDE5K4fMR8lljlOmPyJSJVRaQVECwiLUSkpbMlAOW8HJ4xxhQrF9WrxMRBHagUEshNHy1m3OI/vB2S8TB3V4JNA5JEZAZw4lSjqg72SFTGmGKjbZ2KTBzUge+SdvDalHVc/d5CLr2wGo/3bETNiva3tDG56AHcCsQAb+RoTwWe8kZAxhhTnMVWKs83Ay9i8Be/8tS3K1m/J5Vn+8Th6yPeDs14gLsJ6PfOZowpg3x8hCtaxtCrSTVGzd3MyMRN/Lx6D7d1iGVQt3pUsMIBxpymqmOAMSJypap+7e14jDGmJKgQ5M9Ht7Tm5Ulr+Hj+FnanpPPWdc2tBkUpdDZVcIOBmqq6zrMheZYVUzDm/O05ks7Qqev4ankyEeUCeOgf9bm+TU38fN0d1W9MyXIuRYic8y4FGgOny0mr6v/lc3wNYCxQBVDgA1V9W0QigQlALLAVuEZVD4nIY8ANzul+wAVAlKoeFJGtuJ66ZgGZp+LP61p5xWT9pjGmqH04dzMvT15DfK0IRt0cT3i5AG+HZM7BeRUhEpG+QBIwxfncXETsiagxZVSVCkEMuboZP9zXkQZVQnh24u/0fHsus9btxd2bWsaUdiLyHnAtcD8gwNW4ljPLTybwiKrGAe2AQSISBzwBzFDV+sAM5zOqOkRVm6tqc+BJYLaq5pyo3dXZn/MPgFyvZYwxxcWdnerwzvUt+G17Cle9t5DkQ2neDskUIncfV7wAtAEOA6hqElDHQzEZY0qIJtFhfHFXO0bdHE9WtnLbJ0u4+eNfWLv7iLdDM6Y4uEhVbwYOqeqLQHugQX4nqOouVV3uvE8F1gDRQH9gjHPYGOCyXE6/HvjCjbjcuZYxxnhVn6bVGXtHG/YcSeeKEQv4fWeKt0MyhcTdBDRDVc/8v3p2YQdjjCl5RITucVWY+mBnnu8bx4rkFHq/PZcnv1nJvtQTBV/AmNLr1HqfaSJSHcgAqrl7sojEAi2AxUAVVd3l7NqNa4huzmPLAT2BnHNOFZgmIstEZECO9nyv5VxvgIgsFZGl+/btczdkY4wpVO3qVOTrey/C10e49v1FzNuw39shmULgbgL6u4j8E/AVkfoi8g62lpkxJocAPx9u61Cb2Y8lcOtFtfnf0u0kDJnFu7M2kp6R5e3wjPGGH0QkHBgCLMc133KcOyeKSAiuZPJBVf3LkAJ1jXM/c6x7X2D+GcNvO6pqS6AXrqG8nc/8njyuhap+oKrxqhofFRXlTsjGGOMRDaqE8s3Ai4iJCObWT37h21+TvR2SOU/uJqD34yqicAJX55kCPOipoIwxJVd4uQCe6xvHtIc606FeJYZMXcfFr8/m+9922vxQU2aIiA+ueZaHnUq4tYBGqvqcG+f640o+P1fVb5zmPSJSzdlfDdh7xmnXccbwW1Xd4bzuBb7FNZXGnWsZY0yxUi0smC/vaU/r2EgemvAbIxI32t8UJZhbCaiqpqnq06ra2tmeUdX0gs80xpRVdaJC+ODmeL64qx3h5fwZ/MWvXDFyAcu25Vls05hSQ1WzgXdzfD6Ry1SWvxERAT4C1qhqzjVEvwducd7fAkzMcU4Y0OWMtvIiEnrqPXAJsKqgaxljTHFVIcif0be3pm+z6rw2ZR3Pf/87WdmWhJZE7lbB/dkZRnTqc4SITPVcWMaY0qJ93Yp8f19HhlzVlB2HjnPlyAXcN2452w9aRTtT6s0QkSudpNJdHYCbgG4ikuRsvYFXge4isgH4h/P5lMuBaap6LEdbFWCeiPwG/AJMUtUpzr78rmWMMcVWoJ8vb1/bnAGd6zB24TYGfr7MpvmUQG6tAyoiv6pqi4Laztj/MdAH2KuqTZy2F4C7gFMVDZ5S1cnOvieBO3CtVzZYVac67T2BtwFf4ENVfdVprw2MByoCy4CbVPVkQb/F1jMzxnvSTmby/uzNvD9nE9kKd3Sszf+3d99xUhTpH8c/zwZYck4SJAoCImFBMLGYwHCiZ06gmNEz63E/zzvDBe/MOSECZhRzRiQZyDlINiAICBJMKPD8/pjac47bMIu72zO73/fr1a/pqa7ufmp6dnuqu7pqcE4rqmVlRh2aSIF2ZxxQM9sKVCE2tMpPxIZicXevXgIhlhidN0UkGQ37cCW3vLmQrs1qMXRANrWqaKzQZPObxgEFdppZs7iN7UkenRbsYjixHvl2dVfumGVxlc/2xJ5f6RDWedDM0s0snVgTpiOB9sBpIS/Av8K2WgPfEqu8ikgSq1whgysP34tx1+Twu0578ND4kBW9CwAAIABJREFU5eTcNp6np3zO9h3qWFvKFnev5u5p7l7B3auH9ylV+RQRSVaDDmzBA6d3Zd5Xmznh4Y/VsiqFJFoBvZ5YU54nzewpYCKxAa/z5e4TgY0F5YnTH3guPCOzElhGrLOEHsAyd18R7m4+B/QPzZkOAV4M62scM5EU0qhGJe44eV9ev/RAWtWvyvUvz+eoeycxYYmGe5Cyw8zGJpImIiK756h9GvHUufvxzdZt/P6hj5n/lcYKTQWJdkL0DtAVeJ5YJbBbbhPZ3XCpmc01s2FmViukNQa+jMuzKqTll14H2OTu23dJz5PGMxNJTvs0qcHzF/Tk4TO7sW37TgYOm8rAYVNZunZr1KGJ7DYzyzKz2kDd0GdC7TA1p4BzlYiIFF2PFrUZffH+ZKYZpzzyCRN1MTvpJXoHFKAisTuaW4D2eY0nloCHgFZAZ2ANcMdubKPINJ6ZSPIyM/p1bMiYK3vz56P3ZtYX39Lvnkn8+ZV5fPPdtqjDE9kdFxLrm6BdeM2dXgXujzAuEZEyqU2Darw0+ACa1q7MoOHTGD1DY4Ums4xEMpnZv4BTgAVA7oNaTqwpbsLcfW3cNh8D3ghvvwKaxmVtEtLIJ30DUNPMMsJd0Pj8IpKCKmSkcd5BLTmhaxPuGbuUJyd/zquzVnPJIa05e//mZGWmRx2iSELc/R7gHjP7g7vfF3U8IiLlQcMaWYy6qBcXPTmDq1+Yw9dbfmJwTiuK1hG5lIZE74AeB7R196Pd/XdhOraoO8sd+Do4nv8ek+xUM6sYerdtQ6zb+GlAGzNrYWYViHVU9JrHuu4dB5wY1tc4ZiJlRK0qFbjx2A68e8XB7NeyNre+/SmH3zWBN+eu0aDTklLc/T4z29/MTjezAblT1HGJiJRV1bMyGX5OD/p33oPb3l3MDa/O11ihSSihO6DACiATSLg9nJk9C+QQewZmFfBXIMfMOhO7e/oZsWZKuPsCMxsFLCTWXf0l7r4jbOdS4F1iw7AMc/cFYRd/BJ4zs78Bs4gN3C0iZUTr+lUZOrA7Hy37hlveWMglz8yk2561uOGY9nRuWrPwDYhEzMyeJPbYyWxiQ4xB7Pw3MrKgRETKuAoZadx1cmca1sjikQkrWLdlG/ee1kUtqZJIouOAjgb2BcYSVwl198tKLrSSofHMRFLPjp3OizO+5Pb3lrB+6zb6d96D6/q1o3HNSlGHJuXEbo4Dugho7yl+617nTRFJVU98tJKbXl/IQW3q8shZ3ahcIdF7b1Ic8jt3JnoUXguTiEipS08zTunejKM77cEjE5bz6MQVvDP/a847qAUX57SmakWdUCQpzQcaEut0T0REStk5B7SgasUM/jh6Lmc9PpVhZ3enRqXMqMMq9xL61ebuI8IzmHuFpMXu/kvJhSUi8r+qVszg6iPaclqPZtz27mIeGLec56et4poj9uKk7Kakp6mjAUkqdYGFZjaV/249VOQ+FEREZPeclN2UKhUzuPy5WZz+2GRGDupBnaoVow6rXEu0CW4OMILYc5tGrGfage5epF5wk4GaEomUHXO+3MTf3lzItM++pV3Davz56PYc2KZu1GFJGbSbTXB755Xu7hOKJ6rSofOmiJQF4xav46InZ9C0dmWePm8/GlTPijqkMi+/c2eiveDeARzh7r3d/WCgL3BXcQYoIlJU+zatyagLe/HgGV35/uftnPn4FAYNn8aydVujDk0kt6L5GZAZ5qcBMyMNSkSknOrTtj4jBvVgzaYfOenhT/hy4w9Rh1RuJVoBzXT3xblv3H0JsV5xRUQiZWYctU8j3r+qN/93VDumrdxI37sn8ddX57Px+5+jDk/KMTM7H3gReCQkNQZeiS4iEZHyrWfLOjx9fk82//gLJz38CcvWfRd1SOVSohXQ6WY21MxywvQYoPY4IpI0Kmakc8HBrRh/bQ6n92jGU1O+oPdt43hs4gq2bd9R+AZEit8lwAHAFgB3XwrUjzQiEZFyrnPTmjx/YU+273ROfuQT5n+1OeqQyp1EK6AXExuj87IwLQxpIiJJpU7VitxyXEfeufwgsvesxd/fWsThd07k7XlrSPHRMCT1bHP3/9yGN7MMYuOAiohIhNo1rM6oC3uSlZHGaY9NZsbn30YdUrmSaAU0A7jH3X/v7r8H7gU0mquIJK02DarxxDk9GDmoB5Uy07n46Zmc8shk5q7aFHVoUn5MMLP/AyqZ2eHAC8DrEcckIiJAy3pVeeHi/alTpQJnPT6Fj5Z9E3VI5UaiFdCxQPyI75WA94s/HBGR4nXwXvV487ID+efv92HFN99x7P0fcdXzs1mz+ceoQ5OybwiwHpgHXAi8Bfw50ohEROQ/GtesxKiLetG0VmXOGT6N9xeujTqkciHRCmiWu//nKd0wX7lkQhIRKV4Z6Wmc1qMZ467JYXBOK96Yt4Y+t4/nzvcW8/227VGHJ2VXJWCYu5/k7icCw/jvi7kiIhKx+tWyeP7CnuzdsBoXPTWD1+asjjqkMi/RCuj3ZtY1942ZdQN0+0BEUkq1rEyu69eOD67uzRHtG3LvB8vIuX08o6Z9yY6dejRPip1aD4mIpICalSvw1Hn70XXPWlz+3Cyem/pF1CGVaYlWQK8AXjCzSWb2IfA8cGnJhSUiUnKa1KrMvad14aXB+9O0ViWuGz2X3933IR/r+Q8pXmo9JCKSIqplZTLinB4c3KYeQ16ax9BJK6IOqcxKqALq7tOAdsR6vr0I2NvdZ5RkYCIiJa1rs1qMvnh/7jutC5t//IXTh07hvBHTWbFe44JJsVDrIRGRFFKpQjqPDcjmyI4N+dubi7jn/aXqQb8EJHoHFHf/xd3nh+mXkgxKRKS0mBm/23cPxl7dmz/2a8fkFRs44q6J3PjaAr79/ufCNyCSvyK3HjKzpmY2zswWmtkCM7s8pNc2szFmtjS81grpOWa22cxmh+kvcdvqZ2aLzWyZmQ2JS29hZlNC+vNmVqFESi8ikoIqZKRx32ldOKFrE+56fwn/fPtTVUKLWcIVUBGRsiwrM52Lc1ox/tocTunelJGffEbO7eN5/MOV/Lx9Z9ThSQrazdZD24Gr3b090BO4xMzaE+tRd6y7tyH2bOmQuHUmuXvnMN0MYGbpwAPAkUB74LSwHYB/AXe5e2vgW+DcYiiuiEiZkZGexm0ndmJArz15dOIKrn9lPjvVV0SxUQVURCRO3aoV+fvx+/D25Qezb9Oa3PLGQo64awLvLvhaV0Bld3QHOgFdiVUCBxSU2d3XuPvMML8VWAQ0BvoDI0K2EcBxhey3B7DM3Ve4+8/Ac0B/MzPgEODFImxLRKTcSUszbjq2AxfntOKZKV9w1ajZ/LJDF6SLQ0IVUIs5M7dpj5k1M7MeJRuaiEh02jasxshBPRh+Tncy09O48MkZnProZOZ/tTnq0CRFmNmTwO3AgcQqot2B7CKs3xzoAkwBGrj7mrDoa6BBXNZeZjbHzN42sw4hrTHwZVyeVSGtDrDJ3bfvkr7rvi8ws+lmNn39+vWJhiwiUqaYGX/s145r+7blldmrueTpmWoVVQwyEsz3ILCT2FXTm4GtwGhiJ1MRkTIrp219Dmxdl+emfcldY5bwu/s/5PddmnBt37Y0rJEVdXiS3LKB9r4bt87NrCqx8+wV7r4lduMyxt3dzHK3ORPY092/M7OjgFeANr81cHd/FHgUIDs7W7f+RaRcu6RPa6pUSOfG1xdyyTMzeeD0rlTIUEPS3ZXoJ7efu18C/ATg7t8C6rRARMqFjPQ0zuy5J+OuzeHCg1vx+pzV9Ll9PHe/v4Qfft5e+AakvJoPNCzqSmaWSazy+bS7vxSS15pZo7C8EbAOwN235A714u5vAZlmVhf4Cmgat9kmIW0DUNPMMnZJFxGRApx9QAtuOrYDYxau5Q/PzlRz3N8g0QroL6FDAwcws3rE7oiKiJQb1bMyGXJkO8Ze3ZtD9q7P3e8vpc/t43lxxip1TiB5qQssNLN3zey13KmgFcIzmo8Di9z9zrhFrwEDw/xA4NWQv2FYh/BoTBqxSuY0oE3o8bYCcCrwWrgbOw44cddtiYhIwQbu35y//q497y5Yy2XPzlIldDdZIi2DzOwM4BRinSiMIHbi+rO7v1Cy4RW/7Oxsnz59etRhiEgZMOPzjdz8xiLmfLmJjo2r8+ej29OzZZ2ow5ISYGYz3D3h5zfDOr3zSnf3CQWscyAwCZjHrxd6/4/Yc6CjgGbA58DJ7r7RzC4l1svudmJjjF7l7h+HbR0F3A2kA8Pc/e8hvSWxTolqA7OAM919W34x6bwpIvLfhn24kpvfWMhR+zTknlO7kJmu5rh5ye/cmVAFNGygHXAoYMS6gl9UvCGWDp1IRaQ47dzpvD53Nf96+1NWb/6Jvh0a8Kcj96Z53SpRhybFaHcqoGG9BvzaX8JUd19XvJGVPJ03RUT+19BJK/jbm4s4ep9G3HNqZzJUCf0f+Z07E+qEyMx6Agvc/YHwvrqZ7efuU4o5ThGRlJKWZvTv3Ji+HRry+IcreXDcMg7/dAIDejXnskPaUKNyZtQhSkTM7GTgNmA8sYu395nZte7+YoEriohI0jvvoJYA/O3NRZjB3aeoEpqoRHvBfYhY89tc3+WRJiJSbmVlpnNJn9aclN2Eu8Ys4YmPVjJ65iouP7QNZ/bcU81zyqfrge65dz1D/wnv8+sYnCIiksLOO6glO935x1ufYmbcdfK+qoQmINFPyOK7kXf3nRRSeTWzYWa2zszmx6XdZmafmtlcM3vZzGqG9OZm9qOZzQ7Tw3HrdDOzeWa2zMzujetsobaZjTGzpeG1VlEKLiJSEupXy+Kfv+/Em5cdRMc9anDT6wvpe9dE3l+4lt0YjUNSW9ouTW43kPh5V0REUsAFB7diyJHteH3Oaq4aNYft6pioUImeCFeY2WVmlhmmy4EVhawzHOi3S9oYoKO7dwKWAH+KW7bc3TuH6aK49IeA84mNa9YmbptDiD2L2gYYG96LiCSFvRtV58lze/DE2d0xg/NGTueMoVNYsHpz1KFJ6Xkn9IB7tpmdDbwJvB1xTCIiUswu6t2KP/Zrx2tzVnPNC3PYoZ7xC5RoBfQiYH9iY4WtAvYDLihoBXefCGzcJe09d88dNG8ysfHH8hXGOqvu7pPDHdiRwHFhcX9iPfISXo/LYxMiIpExM/q0q887VxzMzf07sGjNFo6570Oue3EO67b8FHV4UsLc/VrgEaBTmB519+uijUpERErCxTmtuLZvW16ZvZprVQktUELPgIYmRKcW874HAc/HvW9hZrOALcSGeJkENCZW4c21KqQBNHD3NWH+a6BBfjsyswsIFeZmzZoVT/QiIgnKTE9jQK/m9O/cmAfGLeOJj1byxtw1XNy7Fecd1JJKFdKjDlGKkZm1JnaO+sjdXwJeCukHmlkrd18ebYQiIlISLunTGnfn9veWgMFtJ+5LeppFHVbSSbQX3CzgXKADkJWb7u6DdmenZnY9sTHLng5Ja4Bm7r7BzLoBr5hZh0S35+5uZvleZnD3R4FHIdad/O7ELCLyW9WolMn/HbU3Z+zXjFvf/pQ7xizhmalfcF2/tvTftzFpOkmVFXfz34+Y5Noclv2udMMREZHScukhbXCHO8YswTD+fWInVUJ3kWgT3CeBhkBfYAKxprNbd2eH4TmYY4Azcjs2cvdt7r4hzM8AlgN7EWvyG99Mt0lIA1gbmujmNtVNubHVRKR82rNOFR46sxujLuxFvWoVufL5ORz34EdMXbmx8JUlFTRw93m7Joa05qUfjoiIlKY/HNqGKw/bi9EzVzFk9Fx2qjnuf0m0Atra3W8Avnf3EcDRxJ4DLRIz6wdcBxzr7j/Epdczs/Qw35JYZ0MrQhPbLWbWM/R+OwB4Naz2GjAwzA+MSxcRSQk9WtTmlcEHcNcp+7J+6zZOfuQTLn5qBl9s+KHwlSWZ1SxgWaVSi0JERCJz+WFtuPzQNrwwYxVDXlIlNF6i44D+El43mVlHYs9c1i9oBTN7FsgB6prZKuCvxJokVQTGhNFUJocebw8GbjazX4CdwEXunnsrYDCxHnUrEes9MLcHwVuBUWZ2LvA5cHKCZRERSRppacbxXZrQr0Mjhk5awUMTljN20TrOPqA5l/RpTY1KmVGHKEU33czOd/fH4hPN7DxgRkQxiYhIKbvisDa4O/d+sIw0M/5x/D563IbY+J6FZ4qdNEcD+xCrDFYFbnD3R0o0uhKQnZ3t06dPjzoMEZE8rd3yE3e8t5gXZqyiZqVMrjx8L07r0YxMDWwdKTOb4e7ZCeZtALwM/MyvFc5soAJwvLt/XTJRlgydN0VEdp+7c+eYJdz3wTJO69GUvx9Xfiqh+Z07C7wDamaXu/s9wCJ3/xaYCLQsoRhFRMq9BtWz+PeJ+zJw/+b87Y1F/OXVBYz4+DOuP3pv+rStT2g9IknM3dcC+5tZH6BjSH7T3T+IMCwREYmAmXHV4Xux050Hxi3HzPhb/47lphKal8Ka4J4D3APcB3Qt+XBERASgwx41eOb8/Ri7aB3/eGsRg4ZP56A2dbn+6L1p17B61OFJAtx9HDAu6jhERCRaZsY1R7Rlp8ND45eTZnBL/47l9qJyYRXQRWa2FGhsZnPj0o3Y6CedSi40EZHyzcw4rH0Deretx1OTP+fu95dy1D2TOKV7U648fC/qV8sqfCMiIiISOTPjur5tcYeHJyzHMG7u36FcVkILrIC6+2lm1hB4Fzi2dEISEZF4melpnHNAC47v0pj7PljGyE8+47XZqxncpzXnHtiCrMz0qEMUERGRQpgZf+zXFnfnkYkrqFIxgyFHtos6rFKXSC+464H57v55SQcjIiL5q1m5Ajcc054ze+7JrW8v4rZ3F/PMlC+4rl9bjt13j3J5FVVERCSVmBlDjmzH9z9v5+EJy6leKYPBOa2jDqtUFdqtorvvAJqZWYVSiEdERArRom4VHjkrm2fP70nNyplc/txsjn/wY2Z8vrHwlUVERCRSZsbNx3akf+c9+Pc7i3lycvm6z5foOKArgY/M7DXg+9xEd7+zRKISEZFC9WpVh9cvPZCXZn3Fbe9+ygkPfcLRnRoxpF87mtauHHV4IiIiko+0NOP2k/bl+23b+cur86lWMYPjujSOOqxSkejAcsuBN0L+anGTiIhEKC3NOLFbE8Zdk8MVh7Xhg0XrOPTOCdz69qds+emXqMMTERGRfGSmp3H/6V3p2aIOV78whzEL10YdUqkwd486hlKlAbVFpCz7evNP3P7eYkbPXEXtyhW48vC9OLV7UzLSE73eKHnJbzDt8kDnTRGRkvXdtu2c8dhkFn29leFnd2f/1nWjDqlY5HfuTOgXiZmNM7MPdp2KP0wREfktGtbI4vaT9uX1Sw+kdf2q/PmV+Rx5zyTGL14XdWgiIiKSh6oVMxh+Tg+a16nMeSOnM+uLb6MOqUQlekn8GuDaMN0AzAZ0OVREJEl1bFyD5y7oySNndeOXHTs5+4lpDBw2lSVrt0YdmhTAzJqGi74LzWyBmV0e0mub2RgzWxpea4X0M8xsrpnNM7OPzWzfuG19FtJnm9n0uPQ8tyUiItGpVaUCT567H3WrVuTsJ6ax+Ouye75OqALq7jPipo/c/Sogp2RDExGR38LM6NuhIe9d2ZsbjmnPrC++pd/dE7n+5Xl88922qMOTvG0Hrnb39kBP4BIzaw8MAca6extgbHgPsU4Ce7v7PsAtwKO7bK+Pu3fepQlUftsSEZEINaiexdPn7UdWZhpnPj6Fzzd8X/hKKSjRJri146a6ZtYXqFHCsYmISDGokJHGuQe2YMK1fRjQqznPT/uSnNvG89D45fz0y46ow5M47r7G3WeG+a3AIqAx0B8YEbKNAI4LeT5299y2WpOBJgnsJs9tiYhI9JrWrsxT5+7H9h07OWPoFL7e/FPUIRW7RJvgziDW5HYG8AlwNXBuSQUlIiLFr1aVCtx4bAfevfJgerasw7/e+ZTD7pzAG3NXU946pEsFZtYc6AJMARq4+5qw6GugQR6rnAu8HffegffMbIaZXRCXnsi2REQkIm0aVGPEoB5s+uEXznx8Chu//znqkIpVok1wW7h7y/Daxt2PcPcPSzo4EREpfq3qVWXowGyeOW8/qmVlcukzszjhoY/LfKcHqcTMqgKjgSvcfUv8Mo9dLfBd8vchVgH9Y1zyge7eFTiSWFPeg3fdT17bCtu7wMymm9n09evX/+byiIhI0XRqUpOhA7P5cuMPDBw2la1laGi1AiugZtbdzBrGvR9gZq+a2b1mVrvkwxMRkZKyf+u6vPGHA/n3iZ348tsfOf7Bj7ns2Vms+vaHqEMr18wsk1jl82l3fykkrzWzRmF5I2BdXP5OwFCgv7tvyE1396/C6zrgZaBHYduKW/dRd8929+x69eoVdxFFRCQBPVvW4aEzu7JozRbOHTG9zDw2U9gd0EeAnwHCldNbgZHAZv63owMREUkx6WnGydlNGX9NDpcd0pr3Fn7NIXdM4N/vfMp327ZHHV65Y2YGPA4scvc74xa9BgwM8wOBV0P+ZsBLwFnuviRuO1XMrFruPHAEML+gbYmISPI5pF0D7jylM9M+28jFT83g5+07ow7pNyusApru7hvD/CnAo+4+2t1vAFqXbGgiIlJaqlTM4Koj2vLB1Tkcs08jHhy/nJzbxvHs1C/YsVPPh5aiA4CzgEPC8CmzzewoYheADzezpcBh4T3AX4A6wIO7DLfSAPjQzOYAU4E33f2dsCy/bYmISBI6dt89+Ptx+zBu8XquGjU75c/LGYUsTzezDHffDhwKxHdiUNi6IiKSYvaoWYk7T+nM2Qc055Y3FvKnl+Yx4uPPuP7ovTmojZpilrTQv4Lls/jQPPKfB5yXR/oKYN9d08OyDXltS0REktfp+zVj60+/8M+3P6VaVgb/OH4fYo1mUk9hd0CfBSaY2avAj8AkADNrTawZroiIlEGdmtRk1IW9eOiMrvzw8w7Oenwq5zwxlWXryu7A2CIiIsnswt6tuKRPK56d+iX/fPvTlO3BvsC7mO7+dzMbCzQC3vNfS5kG/KGkgxMRkeiYGUfu04hD9q7PyI8/594PltL37kmcsV8zrjhsL2pXqRB1iCIiIuXKNUe0ZcuP23l04gpqVMrkkj6p91Rkoc1o3X1yHmlL8sorIiJlT8WMdM4/uCUndGvCPe8v4akpX/DyrK/4wyGtGbh/cypmpEcdooiISLlgZtx0bAe+27ad295dTLWsDAb0ah51WEWS0DigIiIitatU4Kb+HXn3ioPo3rw2/3jrUw67cwJvzVuTss2AREREUk1amvHvEztx2N4N+MurC3h51qqoQyqSEq2AmtkwM1tnZvPj0mqb2RgzWxpea4V0C+OLLjOzuWbWNW6dgSH/UjMbGJfezczmhXXutVR9EldEJIW0rl+NYWd358lze1ClQgaDn57JyY98wpwvN0UdmoiISLmQmZ7G/ad3oVfLOlzzwlzGffo/QzonrZK+Azoc6LdL2hBgrLu3AcaG9wBHAm3CdAHwEMQqrMBfgf2IDaL919xKa8hzftx6u+5LRERKyEFt6vHmZQdx6+/3YeU3P9D/gY+48vnZrN70Y9ShiYiIlHlZmek8NjCbvRtVY/DTM1PmQnCJVkDdfSKwcZfk/sCIMD8COC4ufaTHTAZqmlkjoC8wxt03uvu3wBigX1hW3d0nh86RRsZtS0RESkF6mnFqj2aMvzaHS/q04s15a+hz+3jueG8x32/bHnV4IiIiZVrVihkMO7s7dapWYNDwaXy+4fuoQypUFM+ANnD3NWH+a2KDZQM0Br6My7cqpBWUviqPdBERKWVVK2Zwbd92fHB1b/p1bMh9Hywj5/bxjJr2ZcoPmC0iIpLM6lfLYsSgHux0Z+CwqWz4blvUIRUo0k6Iwp3LEv9lYmYXmNl0M5u+fv36kt6diEi51aRWZe45tQsvD96fZrUrc93ouRxz34d8vOybqEMTEREps1rVq8rQgd1Zs/knBo2Yzg8/J28rpCgqoGtD81nCa+4Ts18BTePyNQlpBaU3ySP9f7j7o+6e7e7Z9erVK5ZCiIhI/ro0q8WLF/Xi/tO7sPWnXzh96BTOGzGN5eu/izo0ERGRMqnbnrW497QuzFu1iT88M4vtO3ZGHVKeoqiAvgbk9mQ7EHg1Ln1A6A23J7A5NNV9FzjCzGqFzoeOAN4Ny7aYWc/Q++2AuG2JiEjEzIxjOu3B+1f1ZsiR7Zi8YiN975rIja8t4Nvvf446PBERkTKnb4eG3NS/I2M/XccNr85PymHSMkpy42b2LJAD1DWzVcR6s70VGGVm5wKfAyeH7G8BRwHLgB+AcwDcfaOZ3QJMC/ludvfcjo0GE+tptxLwdphERCSJZGWmc1HvVpzYrQl3v7+EkZ98xkszV3HZoW0Y0Ks5FTI0JLWIiEhxOavnnny9+UceGLecRjUqcdmhbaIO6b9YMtaKS1J2drZPnz496jBERMqtJWu38o+3FjF+8Xqa16nMkCP3pm+HBiTzUM5mNsPds6OOIwo6b4qIpB535+oX5vDSzK/49wmdOLl708JXKmb5nTt12VlERErVXg2qMfycHowY1IMKGWlc9NQMTnl0MvNWbY46NBERkTLBzPjXCZ04qE1d/vTyPMYtXlf4SqVEFVAREYlE773q8dZlB/H34zuyfN13HPvAh1w9ag5fb/4p6tBERERSXmZ6Gg+d2Y12DatxydMzmbtqU9QhAaqAiohIhDLS0zhjvz0Zf20OFx7citfnrCbn9nHcNWZJUnchLyIikgqqVszgibO7U6tyBQYNn8YXG36IOiRVQEVEJHrVsjIZcmQ7xl7dm8P2bsA9Y5fS5/bxvDD9S3buLF99FYiIiBSn+tWzGDGoB9t3OgOfmMqG77ZFGo8qoCIikjSa1q7M/ad3ZfTF+9OoRiWufXEuv7v/Qz5ZviHq0ERERFJW6/pRzZ1nAAAR00lEQVRVeXxgNqs3/ci5I6bz4887IotFFVAREUk63fasxcuD9+fe07qw6YdfOO2xyVwwcjorv/k+6tBERERSUrc9a3PvaV2Yu2oTf3h2Jtt37IwkDlVARUQkKZkZx+67B2Ov7s11/dry8fINHH7nBG5+fSGbfvg56vBERERSTt8ODbnp2A68v2gdN7y6gCiG5FQFVEREklpWZjqDc1oz7pocTspuyvCPV9L7tvEM+3Alv0R09VZERCRVndWrOYNzWvHs1C+4/4Nlpb5/VUBFRCQl1KtWkX/+fh/euvwgOjWpwc1vLKTvXRMZs3BtJFdwRUREUtW1fdvy+y6NuWPMEl6Y/mWp7lsVUBERSSntGlZn5KAePHF2d8zg/JHTOf2xKSxYvTnq0H4zM2tqZuPMbKGZLTCzy0N6bTMbY2ZLw2utkG5mdq+ZLTOzuWbWNW5bA0P+pWY2MC69m5nNC+vca2ZW+iUVEZEomRm3ntCJA1vXZchL8xi/eF2p7VsVUBERSTlmRp929XnnioO5pX8HFq/dyjH3fch1L85h7Zafog7vt9gOXO3u7YGewCVm1h4YAox19zbA2PAe4EigTZguAB6CWIUV+CuwH9AD+GtupTXkOT9uvX6lUC4REUkyFTLSeOjMrrRtUI3BT89k3qrSuZCrCqiIiKSszPQ0zurVnHHX5HDBQS15ZdZq+tw+nnvHLo20i/nd5e5r3H1mmN8KLAIaA/2BESHbCOC4MN8fGOkxk4GaZtYI6AuMcfeN7v4tMAboF5ZVd/fJHmu3PDJuWyIiUs5Uy8pk+DndqVW5AucMn8oXG34o8X2qAioiIimvRqVM/nTU3oy56mBy2tbjzjFL6HP7eN6cuybq0HabmTUHugBTgAbunluYr4EGYb4xEP/wzqqQVlD6qjzSd933BWY23cymr1+//jeXRUREklf96lmMGNSD7TudgU9MZeP3JdvTvCqgIiJSZuxZpwoPntGNFy7qRf3qFVm3NTWb45pZVWA0cIW7b4lfFu5clmivS+7+qLtnu3t2vXr1SnJXIiKSBFrXr8rQAdn89MsOVm/6sUT3lVGiWxcREYlA9+a1eWXwAexMwd5xzSyTWOXzaXd/KSSvNbNG7r4mNKPN7S3iK6Bp3OpNQtpXQM4u6eNDepM88ouISDmX3bw246/NoWJGeonuR3dARUSkTEpLMzLSU+s0F3qkfRxY5O53xi16DcjtyXYg8Gpc+oDQG25PYHNoqvsucISZ1QqdDx0BvBuWbTGznmFfA+K2JSIi5VxJVz5Bd0BFRESSyQHAWcA8M5sd0v4PuBUYZWbnAp8DJ4dlbwFHAcuAH4BzANx9o5ndAkwL+W52941hfjAwHKgEvB0mERGRUqEKqIiISJJw9w+B/MblPDSP/A5cks+2hgHD8kifDnT8DWGKiIjsttRqmyQiIiIiIiIpSxVQERERERERKRWqgIqIiIiIiEipUAVURERERERESoUqoCIiIiIiIlIqVAEVERERERGRUmGxHtzLDzNbT2wMtd+qLvBNMWwnamWlHFB2ylJWygFlpywqR/Ip7bLs6e71SnF/SSPB82ZZ+m6VBn1eRafPrGj0eRWdPrOiSeTzyvPcWe4qoMXFzKa7e3bUcfxWZaUcUHbKUlbKAWWnLCpH8ilLZSkLdDyKRp9X0ekzKxp9XkWnz6xofsvnpSa4IiIiIiIiUipUARUREREREZFSoQro7ns06gCKSVkpB5SdspSVckDZKYvKkXzKUlnKAh2PotHnVXT6zIpGn1fR6TMrmt3+vPQMqIiIiIiIiJQK3QEVERERERGRUqEKqIiIiIiIiJQKVUCLyMz6mdliM1tmZkOijicRZvaZmc0zs9lmNj2k1TazMWa2NLzWCulmZveG8s01s64Rxj3MzNaZ2fy4tCLHbWYDQ/6lZjYwicpyo5l9FY7LbDM7Km7Zn0JZFptZ37j0SL9/ZtbUzMaZ2UIzW2Bml4f0lDouBZQjFY9JlplNNbM5oSw3hfQWZjYlxPW8mVUI6RXD+2VhefPCyhhxOYab2cq4Y9I5pCfld6s8ivpvIJXk979HCmZm6WY2y8zeiDqWVGBmNc3sRTP71MwWmVmvqGNKZmZ2Zfh7nG9mz5pZVtQxJZt8fsfm+dsvIe6uKcEJSAeWAy2BCsAcoH3UcSUQ92dA3V3S/g0MCfNDgH+F+aOAtwEDegJTIoz7YKArMH934wZqAyvCa60wXytJynIjcE0eeduH71ZFoEX4zqUnw/cPaAR0DfPVgCUh3pQ6LgWUIxWPiQFVw3wmMCV81qOAU0P6w8DFYX4w8HCYPxV4vqAyJkE5hgMn5pE/Kb9b5W1Khr+BVJry+98TdVzJPgFXAc8Ab0QdSypMwAjgvDBfAagZdUzJOgGNgZVApfB+FHB21HEl20QRfpMnMukOaNH0AJa5+wp3/xl4DugfcUy7qz+xf1CE1+Pi0kd6zGSgppk1iiJAd58IbNwluahx9wXGuPtGd/8WGAP0K/no/1s+ZclPf+A5d9/m7iuBZcS+e5F//9x9jbvPDPNbgUXE/nmn1HEpoBz5SeZj4u7+XXibGSYHDgFeDOm7HpPcY/UicKiZGfmXsVQUUI78JOV3qxyK/G8glezG/55yz8yaAEcDQ6OOJRWYWQ1ilYXHAdz9Z3ffFG1USS8DqGRmGUBlYHXE8SSdIv4mL5QqoEXTGPgy7v0qUuPE4cB7ZjbDzC4IaQ3cfU2Y/xpoEOaTvYxFjTvZy3NpaD44LK7pQkqUJTTd7ELsTlXKHpddygEpeExC87TZwDpiFa7lwCZ3355HXP+JOSzfDNQhCcqyazncPfeY/D0ck7vMrGJIS+pjUo7o895NefzvkbzdDVwH7Iw6kBTRAlgPPBGaLQ81sypRB5Ws3P0r4HbgC2ANsNnd34s2qpSR32+/QqkCWj4c6O5dgSOBS8zs4PiFHrt3nnLj8aRq3HEeAloBnYn907sj2nASZ2ZVgdHAFe6+JX5ZKh2XPMqRksfE3Xe4e2egCbE7Uu0iDmm37FoOM+sI/IlYeboTa1b7xwhDFCkWBf0PlV+Z2THAOnefEXUsKSSDWFPJh9y9C/A9seaRkodwobk/sYr7HkAVMzsz2qhST1F/+6kCWjRfAU3j3jcJaUktXN3B3dcBLxP7gbo2t2lteF0Xsid7GYsad9KWx93Xhh/cO4HH+LW5Y1KXxcwyif1wetrdXwrJKXdc8ipHqh6TXKGZ1TigF7EmqRl5xPWfmMPyGsAGkqgsceXoF5osurtvA54gxY5JOaDPu4jy+R8qeTsAONbMPiPWvPsQM3sq2pCS3ipgVVwLkheJVUglb4cBK919vbv/ArwE7B9xTKkiv99+hVIFtGimAW1C75IViHXg8VrEMRXIzKqYWbXceeAIYD6xuHN7hxwIvBrmXwMGhB4mexJrirCG5FHUuN8FjjCzWuEq1xEhLXK7PFt7PLHjArGynGqx3kpbAG2AqSTB9y88K/g4sMjd74xblFLHJb9ypOgxqWdmNcN8JeBwYs+VjQNODNl2PSa5x+pE4INw5TK/MpaKfMrxadzJzYg9XxJ/TJLuu1UORf43kEoK+B8qeXD3P7l7E3dvTuy79YG76+5UAdz9a+BLM2sbkg4FFkYYUrL7AuhpZpXD3+ehxM6hUrj8fvsVzpOgZ6VUmoj1vLiE2DNW10cdTwLxtiTWK+EcYEFuzMSe+RoLLAXeB2qHdAMeCOWbB2RHGPuzxJpB/kLsit65uxM3MIhYhyrLgHOSqCxPhljnhj/iRnH5rw9lWQwcmSzfP+BAYk0s5gKzw3RUqh2XAsqRisekEzArxDwf+EtIb0msArkMeAGoGNKzwvtlYXnLwsoYcTk+CMdkPvAUv/aUm5TfrfI4Rf03kEpTfv97oo4rFSYgB/WCm+hn1RmYHr5nr6CewAv7vG4CPg3nmSdzz5ea/uszSvg3eSKThY2KiIiIiIiIlCg1wRUREREREZFSoQqoiIiIiIiIlApVQEVERERERKRUqAIqIiIiIiIipUIVUBERERERESkVqoCKiIiIiIhIqVAFVCRFmVkdM5sdpq/N7Ku49x+XwP7ONrP1Zja0gDyVwv5/NrO6xR2DiIgkNzPbEc4D883sBTOrXMT1r4hfx8zeMrOaBeS/0cyu+S0xJxjXcDM7McwPNbP2v3F7OWa22czeKuJ6F5nZgN+y791hZqeY2TIze6O09y1lT0bUAYjI7nH3DcQGm8bMbgS+c/fbS3i3z7v7pQXE9CPQ2cw+K+E4REQkOf3o7rnnpqeBi4A7E1nRzNKBK4CngB8A3P2oEooTM8tw9+1FXc/dzyumECa5+zFF3PfDiebd3fLls9/nzWwtUOKVfSn7dAdUpAwys+/Ca46ZTTCzV81shZndamZnmNlUM5tnZq1CvnpmNtrMpoXpgAT20SFsZ7aZzTWzNiVdLhERSSmTgNYAZvaKmc0wswVmdkFuBjP7zszuMLM5wPXAHsA4MxsXln+W26LGzAaE880cM3ty152ZWSszeyfsZ5KZtcsjz41m9qSZfQQ8aWbNQ96ZYdo/5DMzu9/MFpvZ+0D9uG2MN7Ps3Pjj0k80s+Fh/qRwF3iOmU0s7IMqwvn6P3d8zay1mb0f9jEzlD8nlOc1YGHId1WIZb6ZXRHSmpvZIjN7LByT98ysUlh2mZktDJ/1c4XFLlJUugMqUvbtC+wNbARWAEPdvYeZXQ78gdjV5nuAu9z9QzNrBrwb1inIRcA97v60mVUA0kusBCIiklLMLAM4EngnJA1y942hkjPNzEaHljxVgCnufnVYbxDQx92/2WV7HYA/A/u7+zdmVjuP3T4KXOTuS81sP+BB4JA88rUHDnT3Hy3W3Pdwd/8pXEh9FsgGjgfahrwNiFXmhhXhI/gL0Nfdv7ICmhDvIpHzdbyngVvd/WUzyyJ2Y6kp0BXo6O4rzawbcA6wH2DAFDObAHwLtAFOc/fzzWwUcAKxu89DgBbuvq0IsYskTBVQkbJvmruvATCz5cB7IX0e0CfMHwa0N7PcdaqbWVV3/478fQJcb2ZNgJfcfWnxhy4iIimmkpnNDvOTgMfD/GVmdnyYb0qs8rMB2AGMTmC7hwAv5FZM3X1j/EIzqwrsD7wQdy6rmM+2XguPjABkAvebWecQy14h/WDgWXffAaw2sw8SiDHeR8DwULF7KcF1EjlfE5ZXAxq7+8sA7v5TSAeY6u4rQ9YDgZfd/fuw/CXgIOA1YKW75x6rGUDzMD8XeNrMXgFeSbTAIolSBVSk7NsWN78z7v1Ofv0fkAb0zD2BJcLdnzGzKcDRwFtmdqG7F/UELSIiZct/ngHNZWY5xC509nL3H8xsPJAVFv8UKnm/VRqwadd95+P7uPkrgbXE7j6mAQmfBwOPm8/6T6L7ReEu7NHADDPrFu74FiSR83Uivi88y//sbwdQKcwfTawC/jtiF5r3Ka5nSUVAz4CKSMx7xJr3ABCuBBfIzFoCK9z9XuBVoFPJhSciIimsBvBtqHy2A3oWkHcrUC2P9A+Ak8ysDsCuTXDdfQuw0sxOCsvNzPZNMLY17r4TOItfHyeZCJxiZulm1ohd7kDGWWtme5tZGrFmu4T9t3L3Ke7+F2A9sbu+xcbdtwKrzOy4sL+KlnePw5OA48yssplVCTFOym+7oRxN3X0c8Edin0/V4oxdRBVQEQG4DMgOHQ4sJPZ8Z2FOBuaHplYdgZElGaCIiKSsd4AMM1sE3ApMLiDvo8A7FjohyuXuC4C/AxMs1mFRXj3rngGcG5YvAPonENuDwMCwTjt+vXv4MrCU2LOfI4k9dpKXIcAbwMfAmrj020LnQfPDsjkJxFJUZxFr2jw37KPhrhncfSYwHJgKTCH2XOmsAraZDjxlZvOAWcC97r6puAOX8s3cvfBcIlLumdnZQHZBw7DE5f0s5P2msLwiIiLlTWiWfE1Rh2GJUirGLMlJd0BFJFE/Akea2dD8MphZbucTmcSeWREREZH/9TPQ0czeijqQRJjZKcTuFn8bdSyS+nQHVEREREREREqF7oCKiIiIiIhIqVAFVEREREREREqFKqAiIiIiIiJSKlQBFRERERERkVLx/wROpnry5+OwAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# solve\n", + "solver = pybamm.ScipySolver()\n", + "t = np.linspace(0, 3600, 600)\n", + "solution = solver.solve(model, t)\n", + "\n", + "# post-process, so that the solution can be called at any time t or space r\n", + "# (using interpolation)\n", + "c = solution[\"Concentration [mol.m-3]\"]\n", + "c_surf = solution[\"Surface concentration [mol.m-3]\"]\n", + "\n", + "# plot\n", + "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))\n", + "\n", + "ax1.plot(solution.t, c_surf(solution.t))\n", + "ax1.set_xlabel(\"Time [s]\")\n", + "ax1.set_ylabel(\"Surface concentration [mol.m-3]\")\n", + "\n", + "r = mesh[\"negative particle\"][0].nodes # radial position\n", + "time = 1000 # time in seconds\n", + "ax2.plot(r * 1e6, c(t=time, r=r), label=\"t={}[s]\".format(time))\n", + "ax2.set_xlabel(\"Particle radius [microns]\")\n", + "ax2.set_ylabel(\"Concentration [mol.m-3]\")\n", + "ax2.legend()\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the [next notebook](./4-comparing-full-and-reduced-order-models.ipynb) we consider the limit of fast diffusion in the particle. This leads to a reduced-order model for the particle behaviour, which we compare with the full (Fickian diffusion) model. " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.7.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/notebooks/Creating Models/4-comparing-full-and-reduced-order-models.ipynb b/examples/notebooks/Creating Models/4-comparing-full-and-reduced-order-models.ipynb new file mode 100644 index 0000000000..c7d9dccbbe --- /dev/null +++ b/examples/notebooks/Creating Models/4-comparing-full-and-reduced-order-models.ipynb @@ -0,0 +1,383 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Comparing full and reduced-order models" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the [previous notebook](./3-negative-particle-problem.ipynb) we saw how to solve the problem of diffusion on a sphere, motivated by the problem in the negative particle in battery modelling. In this notebook we consider a reduced-order ODE model for the particle behaviour, suitable in the limit of fast diffusion. We also show how to compare the results of the full and reduced-order models. \n", + "\n", + "In the limit of fast diffusion in the particles the concentration is uniform in $r$. This result in the following ODE model for the (uniform) concentration in the particle \n", + "\n", + "\\begin{equation*}\n", + " \\frac{\\textrm{d} c}{\\textrm{d} t} = -3\\frac{j}{RF}\n", + "\\end{equation*}\n", + "with the initial condition:\n", + "\\begin{equation*}\n", + "\\left.c\\right\\vert_{t=0} = c_0,\n", + "\\end{equation*}\n", + "where $c$ is the concentration, $r$ the radial coordinate, $t$ time, $R$ the particle radius, $D$ the diffusion coefficient, $j$ the interfacial current density, $F$ Faraday's constant, and $c_0$ the initial concentration. \n", + "\n", + "As in the previous example we use the following parameters:\n", + "\n", + "| Symbol | Units | Value |\n", + "|:-------|:-------------------|:-----------------------------------------------|\n", + "| $R$ | m | $10 \\times 10^{-6}$ |\n", + "| $D$ | m${^2}$ s$^{-1}$ | $3.9 \\times 10^{-14}$ |\n", + "| $j$ | A m$^{-2}$ | $1.4$ |\n", + "| $F$ | C mol$^{-1}$ | $96485$ |\n", + "| $c_0$ | mol m$^{-3}$ | $2.5 \\times 10^{4}$ |" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting up the models\n", + "As in the single particle diffusion example, we begin by importing the pybamm library into this notebook, along with any other packages we require. In this notebook we want to compare the results of the full and reduced-order models, so we create two empty `pybamm.BaseModel` objects. We can pass in a name when we create the model, for easy reference. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pybamm\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "full_model = pybamm.BaseModel(name=\"full model\")\n", + "reduced_model = pybamm.BaseModel(name=\"reduced model\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It can be useful to add the models to a list so that we can perform the same operations on each model easily" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "models = [full_model, reduced_model]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We then define our parameters, as seen previously, " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "R = pybamm.Parameter(\"Particle radius [m]\")\n", + "D = pybamm.Parameter(\"Diffusion coefficient [m2.s-1]\")\n", + "j = pybamm.Parameter(\"Interfacial current density [A.m-2]\")\n", + "F = pybamm.Parameter(\"Faraday constant [C.mol-1]\")\n", + "c0 = pybamm.Parameter(\"Initial concentration [mol.m-3]\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The reduced order model solves and ODE for the (uniform) concentration in the particle. In the parameter regime where this is valid, we expect that the solution of the ODE model should agree with the average concentration in the PDE mode. In anticipation of this, we create two variables: the concentration (which we will use in the PDE model), and the average concentration (which we will use in the ODE model). This will make it straightforward to compare the results in a consistent way. Note that the average concentration doesn't have a domain since it is a scalar quantity." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "c = pybamm.Variable(\"Concentration [mol.m-3]\", domain=\"negative particle\")\n", + "c_av = pybamm.Variable(\"Average concentration [mol.m-3]\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we define our model equations, initial and boundary conditions (where appropriate)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# governing equations for full model\n", + "N = -D * pybamm.grad(c) # flux\n", + "dcdt = -pybamm.div(N)\n", + "full_model.rhs = {c: dcdt} \n", + "\n", + "# governing equations for reduced model\n", + "dc_avdt = -3 * j / R / F\n", + "reduced_model.rhs = {c_av: dc_avdt} \n", + "\n", + "# initial conditions (these are the same for both models)\n", + "full_model.initial_conditions = {c: c0}\n", + "reduced_model.initial_conditions = {c_av: c0}\n", + "\n", + "# boundary conditions (only required for full model)\n", + "lbc = pybamm.Scalar(0)\n", + "rbc = -j / F / D\n", + "full_model.boundary_conditions = {c: {\"left\": (lbc, \"Dirichlet\"), \"right\": (rbc, \"Neumann\")}}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now populate the variables dictionary of both models with any variables of interest. We can compute the average concentration in the full model using the operator `pybamm.r_average`. We may also wish to compare the concentration profile predicted by the full model with the uniform concentration profile predicted by the reduced model. We can use the operator `pybamm.PrimaryBroadcast` to broadcast the scalar valued uniform concentration across the particle domain so that it can be visualised as a function of $r$. \n", + "\n", + "Note: the \"Primary\" refers to the fact the we are broadcasting in only one dimension. For some models, such as the DFN, variables may depend on a \"pseudo-dimension\" (e.g. the position in $x$ across the cell), but spatial operators only act in the \"primary dimension\" (e.g. the position in $r$ within the particle). If you are unfamiliar with battery models, do not worry, the details of this are not important for this example. For more information see the [broadcasts notebook](../expression_tree/broadcasts.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# full model\n", + "full_model.variables = {\n", + " \"Concentration [mol.m-3]\": c,\n", + " \"Surface concentration [mol.m-3]\": pybamm.surf(c),\n", + " \"Average concentration [mol.m-3]\": pybamm.r_average(c),\n", + "}\n", + "\n", + "# reduced model\n", + "reduced_model.variables = {\n", + " \"Concentration [mol.m-3]\": pybamm.PrimaryBroadcast(c_av, \"negative particle\"),\n", + " \"Surface concentration [mol.m-3]\": c_av, # in this model the surface concentration is just equal to the scalar average concentration \n", + " \"Average concentration [mol.m-3]\": c_av,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using the model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As before, we provide out parameter values" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "param = pybamm.ParameterValues(\n", + " {\n", + " \"Particle radius [m]\": 10e-6,\n", + " \"Diffusion coefficient [m2.s-1]\": 3.9e-14,\n", + " \"Interfacial current density [A.m-2]\": 1.4,\n", + " \"Faraday constant [C.mol-1]\": 96485,\n", + " \"Initial concentration [mol.m-3]\": 2.5e4,\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We then define and process our geometry, and process both of the models" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# geometry\n", + "r = pybamm.SpatialVariable(\"r\", domain=[\"negative particle\"], coord_sys=\"spherical polar\")\n", + "geometry = {\"negative particle\": {\"primary\": {r: {\"min\": pybamm.Scalar(0), \"max\": R}}}}\n", + "param.process_geometry(geometry)\n", + "\n", + "# models\n", + "for model in models:\n", + " param.process_model(model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now set up our mesh, choose a spatial method, and discretise our models. Note that, even though the reduced-order model is an ODE model, we discretise using the mesh for the particle so that our `PrimaryBroadcast` operator is discretised in the correct way." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# mesh\n", + "submesh_types = {\"negative particle\": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh)}\n", + "var_pts = {r: 20}\n", + "mesh = pybamm.Mesh(geometry, submesh_types, var_pts)\n", + "\n", + "# discretisation\n", + "spatial_methods = {\"negative particle\": pybamm.FiniteVolume()}\n", + "disc = pybamm.Discretisation(mesh, spatial_methods)\n", + "\n", + "# process models\n", + "for model in models:\n", + " disc.process_model(model);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Both models are now discretised and ready to be solved." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Solving the model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As before, we choose a solver and times at which we want the solution returned. We then solve both models, post-process the results, and create a slider plot to compare the results." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "dbcd70fa09c6404dab80036156401985", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(FloatSlider(value=0.0, description='t', max=3600.0, step=1.0), Output()), _dom_classes=(…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# loop over models to solve\n", + "solver = pybamm.ScipySolver()\n", + "t = np.linspace(0, 3600, 600)\n", + "solutions = [None] * len(models) # create list to hold solutions\n", + "for i, model in enumerate(models):\n", + " solutions[i] = solver.solve(model, t)\n", + "\n", + "# post-process the solution of the full model\n", + "c_full = solutions[0][\"Concentration [mol.m-3]\"]\n", + "c_av_full = solutions[0][\"Average concentration [mol.m-3]\"]\n", + "\n", + "\n", + "# post-process the solution of the full model\n", + "c_reduced = solutions[1][\"Concentration [mol.m-3]\"]\n", + "c_av_reduced = solutions[1][\"Average concentration [mol.m-3]\"]\n", + "\n", + "# plot\n", + "r = mesh[\"negative particle\"][0].nodes # radial position\n", + "\n", + "def plot(t):\n", + " fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))\n", + " \n", + " # Plot concetration as a function of r\n", + " ax1.plot(r * 1e6, c_full(t=t,r=r), label=\"Full Model\")\n", + " ax1.plot(r * 1e6, c_reduced(t=t,r=r), label=\"Reduced Model\") \n", + " ax1.set_xlabel(\"Particle radius [microns]\")\n", + " ax1.set_ylabel(\"Concentration [mol.m-3]\")\n", + " ax1.legend()\n", + " \n", + " # Plot average concentration over time\n", + " t_hour = np.linspace(0, 3600, 600) # plot over full hour\n", + " c_min = c_av_reduced(t=3600) * 0.98 # minimum axes limit \n", + " c_max = param[\"Initial concentration [mol.m-3]\"] * 1.02 # maximum axes limit \n", + " \n", + " ax2.plot(t_hour, c_av_full(t=t_hour), label=\"Full Model\")\n", + " ax2.plot(t_hour, c_av_reduced(t=t_hour), label=\"Reduced Model\") \n", + " ax2.plot([t, t], [c_min, c_max], \"k--\") # plot line to track time\n", + " ax2.set_xlabel(\"Time [s]\")\n", + " ax2.set_ylabel(\"Average concentration [mol.m-3]\") \n", + " ax2.legend()\n", + "\n", + " plt.tight_layout()\n", + " plt.show()\n", + " \n", + "import ipywidgets as widgets\n", + "widgets.interact(plot, t=widgets.FloatSlider(min=0,max=3600,step=1,value=0));\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "From the results we pbserve that the reduced-order model does a good job of predicting the average concentration, but, since it is only an ODE model, cannot predicted the spatial dis" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the [next notebook](./5-a-simple-SEI-model.ipynb) we consider a simple model for SEI growth, and show how to correctly pose the model in non-dimensional form and then create and solve it using pybamm." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.7.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/notebooks/Creating Models/5-a-simple-SEI-model.ipynb b/examples/notebooks/Creating Models/5-a-simple-SEI-model.ipynb new file mode 100644 index 0000000000..d9ae44aa71 --- /dev/null +++ b/examples/notebooks/Creating Models/5-a-simple-SEI-model.ipynb @@ -0,0 +1,661 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Creating a Simple Model for SEI Growth\n", + "Before adding a new model, please read the [contribution guidelines](https://github.com/pybamm-team/PyBaMM/blob/master/CONTRIBUTING.md)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this notebook, we will run through the steps involved in creating a new model within pybamm. We will then solve and plot the outputs of the model. We have chosen to implement a very simple model of SEI growth. We first give a brief derivation of the model and discuss how to nondimensionalise the model so that we can show the full process of model conception to solution within a single notebook. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note: if you run the entire notebook and then try to evaluate the earlier cells, you will likely receive an error. This is because the state of objects is mutated as it is passed through various stages of processing. In this case, we recommend that you restart the Kernel and then evaluate cells in turn through the notebook. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## A Simple Model of Solid Electrolyte Interphase (SEI) Growth" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The SEI is a porous layer that forms on the surfaces of negative electrode particles from the products of electrochemical reactions which consume lithium and electrolyte solvents. In the first few cycles of use, a lithium-ion battery loses a large amount of capacity; this is generally attributed to lithium being consumed to produce SEI. However, after a few cycles, the rate of capacity loss slows at a rate often (but not always) reported to scale with the square root of time. SEI growth is therefore often considered to be limited in some way by a diffusion process." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Dimensional Model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We shall first state our model in dimensional form, but to enter the model in pybamm, we strongly recommend converting models into dimensionless form. The main reason for this is that dimensionless models are typically better conditioned than dimensional models and so several digits of accuracy can be gained. To distinguish between the dimensional and dimensionless models, we shall always employ a superscript $*$ on dimensional variables. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![SEI.png](SEI.png \"SEI Model Schematic\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In our simple SEI model, we consider a one-dimensional SEI which extends from the surface of a planar negative electrode at $x^*=0$ until $x^*=L^*$, where $L^*$ is the thickness of the SEI. Since the SEI is porous, there is some electrolyte within the region $x^*\\in[0, L^*]$ and therefore some concentration of solvent, $c^*$. Within the porous SEI, the solvent is transported via a diffusion process according to:\n", + "$$\n", + "\\frac{\\partial c^*}{\\partial t^*} = - \\nabla^* \\cdot N^*, \\quad N^* = - D^*(c^*) \\nabla^* c^* \\label{dim:eqn:solvent-diffusion}\\tag{1}\\\\\n", + "$$\n", + "where $t^*$ is the time, $N^*$ is the solvent flux, and $D^*(c^*)$ is the effective solvent diffusivity (a function of the solvent concentration).\n", + "\n", + "On the electrode-SEI surface ($x^*=0$) the solvent is consumed by the SEI growth reaction, $R^*$. We assume that diffusion of solvent in the bulk electrolyte ($x^*>L^*$) is fast so that on the SEI-electrolyte surface ($x^*=L^*$) the concentration of solvent is fixed at the value $c^*_{\\infty}$. Therefore, the boundary conditions are\n", + "$$\n", + " N^*|_{x^*=0} = - R^*, \\quad c^*|_{x^*=L^*} = c^*_{\\infty},\n", + "$$\n", + "We also assume that the concentration of solvent within the SEI is initially uniform and equal to the bulk electrolyte solvent concentration, so that the initial condition is\n", + "$$\n", + " c^*|_{t^*=0} = c^*_{\\infty}\n", + "$$\n", + "\n", + "Since the SEI is growing, we require an additional equation for the SEI thickness. The thickness of the SEI grows at a rate proportional to the SEI growth reaction $R^*$, where the constant of proportionality is the partial molar volume of the reaction products, $\\hat{V}^*$. We also assume that the SEI is initially of thickness $L^*_0$. Therefore, we have\n", + "$$\n", + " \\frac{d L^*}{d t^*} = \\hat{V}^* R^*, \\quad L^*|_{t^*=0} = L^*_0\n", + "$$\n", + "\n", + "Finally, we assume for the sake of simplicity that the SEI growth reaction is irreversible and that the potential difference across the SEI is constant. The reaction is also assumed to be proportional to the concentration of solvent at the electrode-SEI surface ($x^*=0$). Therefore, the reaction flux is given by\n", + "$$\n", + " R^* = k^* c^*|_{x^*=0}\n", + "$$\n", + "where $k^*$ is the reaction rate constant (which is in general dependent upon the potential difference across the SEI)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Non-dimensionalisation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To convert the model into dimensionless form, we scale the dimensional variables and dimensional functions. For this model, we choose to scale $x^*$ by the current SEI thickness, the current SEI thickness by the initial SEI thickness, solvent concentration with the bulk electrolyte solvent concentration, and the solvent diffusion with the solvent diffusion in the electrolyte. We then use these scalings to infer the scaling for the solvent flux. Therefore, we have\n", + "$$\n", + "x^* = L^* x, \\quad L^*= L^*_0 L \\quad c^* = c^*_{\\infty} c, \\quad D^*(c^*) = D^*(c^*_{\\infty}) D(c), \\quad \n", + "N^* = \\frac{D^*(c^*_{\\infty}) c^*_{\\infty}}{L^*_0}N.\n", + "$$\n", + "We also choose to scale time by the solvent diffusion timescale so that \n", + "$$\n", + "t^* = \\frac{(L^*_0)^2}{D^*(c^*_{\\infty})}t.\n", + "$$\n", + "Finally, we choose to scale the reaction flux in the same way as the solvent flux so that we have\n", + "$$\n", + " R^* = \\frac{D^*(c^*_{\\infty}) c^*_{\\infty}}{L^*_0} R.\n", + "$$\n", + "\n", + "We note that there are multiple possible choices of scalings. Whilst they will all give the ultimately give the same answer, some choices are better than others depending on the situation under study." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Dimensionless Model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After substituting in the scalings from the previous section, we obtain the dimensionless form of the model given by:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Solvent diffusion through SEI:\n", + "\\begin{align}\n", + "\\frac{\\partial c}{\\partial t} = \\frac{\\hat{V} R}{L} x \\cdot \\nabla c - \\frac{1}{L}\\nabla \\cdot N, \\quad N = - \\frac{1}{L}D(c) \\nabla c, \\label{eqn:solvent-diffusion}\\tag{1}\\\\\n", + "N|_{x=0} = - R, \\quad c|_{x=1} = 1 \\label{bc:solvent-diffusion}\\tag{2} \\quad\n", + "c|_{t=0} = 1; \n", + "\\end{align}\n", + "\n", + "Growth reaction:\n", + "$$\n", + "R = k c|_{x=0}; \\label{eqn:reaction}\\tag{3}\n", + "$$\n", + "\n", + "SEI thickness:\n", + "$$\n", + "\\frac{d L}{d t} = \\hat{V} R, \\quad L|_{t=0} = 1; \\label{eqn:SEI-thickness}\\tag{4}\n", + "$$\n", + "where the dimensionless parameters are given by\n", + "$$\n", + " k = \\frac{k^* L^*_0}{D^*(c^*_{\\infty})}, \\quad \\hat{V} = \\hat{V}^* c^*_{\\infty}, \\quad \n", + " D(c) = \\frac{D^*(c^*)}{D^*(c^*_{\\infty})}. \\label{parameters}\\tag{5}\n", + "$$\n", + "In the above, the additional advective term in the diffusion equation arises due to our choice to scale the spatial coordinate $x^*$ with the time-dependent SEI layer thickness $L^*$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Entering the Model into PyBaMM" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As always, we begin by importing pybamm and changing our working directory to the root of the pybamm folder." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pybamm\n", + "import numpy as np\n", + "import os\n", + "os.chdir(pybamm.__path__[0]+'/..')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A model is defined in six steps:\n", + "1. Initialise model\n", + "2. Define parameters and variables\n", + "3. State governing equations\n", + "4. State boundary conditions\n", + "5. State initial conditions\n", + "6. State output variables\n", + "\n", + "We shall proceed through each step to enter our simple SEI growth model." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 1. Initialise model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We first initialise the model using the `BaseModel` class. This sets up the required structure for our model. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "model = pybamm.BaseModel()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 2. Define parameters and variables" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In our SEI model, we have two dimensionless parameters, $k$ and $\\hat{V}$, and one dimensionless function $D(c)$, which are all given in terms of the dimensional parameters, see (5). In pybamm, inputs are dimensional, so we first state all the dimensional parameters. We then define the dimensionless parameters, which are expressed an non-dimensional groupings of dimensional parameters. To define the dimensional parameters, we use the `Parameter` object to create parameter symbols. Parameters which are functions are defined using `FunctionParameter` object and should be defined within a python function as shown. " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# dimensional parameters\n", + "k_dim = pybamm.Parameter(\"Reaction rate constant\")\n", + "L_0_dim = pybamm.Parameter(\"Initial thickness\")\n", + "V_hat_dim = pybamm.Parameter(\"Partial molar volume\")\n", + "c_inf_dim = pybamm.Parameter(\"Bulk electrolyte solvent concentration\")\n", + "\n", + "def D_dim(cc):\n", + " return pybamm.FunctionParameter(\"Diffusivity\", {\"Solvent concentration [mol.m-3]\": cc})\n", + "\n", + "# dimensionless parameters\n", + "k = k_dim * L_0_dim / D_dim(c_inf_dim)\n", + "V_hat = V_hat_dim * c_inf_dim\n", + "\n", + "def D(cc):\n", + " c_dim = c_inf_dim * cc\n", + " return D_dim(c_dim) / D_dim(c_inf_dim)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now define the dimensionless variables in our model. Since these are the variables we solve for directly, we do not need to write them in terms of the dimensional variables. We simply use `SpatialVariable` and `Variable` to create the required symbols: " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "x = pybamm.SpatialVariable(\"x\", domain=\"SEI layer\", coord_sys=\"cartesian\")\n", + "c = pybamm.Variable(\"Solvent concentration\", domain=\"SEI layer\")\n", + "L = pybamm.Variable(\"SEI thickness\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 3. State governing equations" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now use the symbols we have created for our parameters and variables to write out our governing equations. Note that before we use the reaction flux and solvent flux, we must derive new symbols for them from the defined parameter and variable symbols. Each governing equation must also be stated in the explicit form `d/dt = rhs` since pybamm only stores the right hand side (rhs) and assumes that the left hand side is the time derivative. The governing equations are then simply" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# SEI reaction flux\n", + "R = k * pybamm.BoundaryValue(c, \"left\")\n", + "\n", + "# solvent concentration equation\n", + "N = - (1 / L) * D(c) * pybamm.grad(c)\n", + "dcdt = (V_hat * R) * pybamm.inner(x / L, pybamm.grad(c)) - (1 / L) * pybamm.div(N)\n", + "\n", + "# SEI thickness equation\n", + "dLdt = V_hat * R" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once we have stated the equations, we can add them to the `model.rhs` dictionary. This is a dictionary whose keys are the variables being solved for, and whose values correspond right hand sides of the governing equations for each variable." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "model.rhs = {c: dcdt, L: dLdt}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 4. State boundary conditions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We only have boundary conditions on the solvent concentration equation. We must state where a condition is Neumann (on the gradient) or Dirichlet (on the variable itself). \n", + "\n", + "The boundary condition on the electrode-SEI (x=0) boundary is: \n", + "$$\n", + " N|_{x=0} = - R, \\quad N|_{x=0} = - \\frac{1}{L} D(c|_{x=0} )\\nabla c|_{x=0}\n", + "$$\n", + "which is a Neumann condition. To implement this boundary condition in pybamm, we must first rearrange the equation so that the gradient of the concentration, $\\nabla c|_{x=0}$, is the subject. Therefore we have\n", + "$$\n", + " \\nabla c|_{x=0} = \\frac{L R}{D(c|_{x=0} )}\n", + "$$\n", + "which we enter into pybamm as " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# electrode-SEI boundary condition (x=0) (lbc = left boundary condition)\n", + "D_left = pybamm.BoundaryValue(D(c), \"left\") # pybamm requires BoundaryValue(D(c)) and not D(BoundaryValue(c)) \n", + "grad_c_left = L * R / D_left" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "On the SEI-electrolyte boundary (x=1), we have the boundary condition\n", + "$$\n", + " c|_{x=1} = 1\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "which is a Dirichlet condition and is just entered as" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "c_right = pybamm.Scalar(1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now load these boundary conditions into the `model.boundary_conditions` dictionary in the following way, being careful to state the type of boundary condition: " + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "model.boundary_conditions = {c: {\"left\": (grad_c_left, \"Neumann\"), \"right\": (c_right, \"Dirichlet\")}}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 5. State initial conditions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are two initial conditions in our model:\n", + "$$\n", + " c|_{t=0} = 1, \\quad L|_{t=0} = 1\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "which are simply written in pybamm as" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "c_init = pybamm.Scalar(1)\n", + "L_init = pybamm.Scalar(1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "and then included into the `model.initial_conditions` dictionary:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "model.initial_conditions = {c: c_init, L: L_init}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 6. State output variables" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We already have everything required in model for the model to be used and solved, but we have not yet stated what we actually want to output from the model. PyBaMM allows users to output any combination of symbols as an output variable therefore allowing the user the flexibility to output important quantities without further tedious postprocessing steps. \n", + "\n", + "Some useful outputs for this simple model are:\n", + "- the SEI thickness\n", + "- the SEI growth rate\n", + "- the solvent concentration\n", + "\n", + "These are added to the model by adding entries to the `model.variables` dictionary" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "model.variables = {\"SEI thickness\": L, \"SEI growth rate\": dLdt, \"Solvent concentration\": c}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also output the dimensional versions of these variables by multiplying by the scalings used to non-dimensionalise. By convention, we recommend including the units in the output variables name so that they do not overwrite the dimensionless output variables. To add new entries to the dictionary we used the method `.update()`." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "L_dim = L_0_dim * L\n", + "dLdt_dim = (D_dim(c_inf_dim) / L_0_dim ) * dLdt\n", + "c_dim = c_inf_dim * c\n", + "\n", + "model.variables.update({\n", + " \"SEI thickness [m]\": L_dim, \n", + " \"SEI growth rate [m/s]\": dLdt_dim, \n", + " \"Solvent concentration [mols/m^3]\": c_dim\n", + " }\n", + " )\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The model is now fully defined and ready to be used. If you plan on reusing the model several times, you can additionally set model defaults which may include: a default geometry to run the model on, a default set of parameter values, a default solver, etc." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using the Model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The model will now behave in the same way as any of the inbuilt PyBaMM models. However, to demonstrate that the model works we display the steps involved in solving the model but we will not go into details within this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "# define geometry\n", + "geometry = pybamm.Geometry()\n", + "geometry.add_domain(\"SEI layer\", {\"primary\": {x: {\"min\": pybamm.Scalar(0), \"max\": pybamm.Scalar(1)}}})\n", + "\n", + "def Diffusivity(cc):\n", + " return cc * 10**(-5)\n", + "\n", + "# parameter values (not physically based, for example only!)\n", + "param = pybamm.ParameterValues(\n", + " {\n", + " \"Reaction rate constant\": 20,\n", + " \"Initial thickness\": 1e-6,\n", + " \"Partial molar volume\": 10,\n", + " \"Bulk electrolyte solvent concentration\": 1,\n", + " \"Diffusivity\": Diffusivity,\n", + " }\n", + ")\n", + "\n", + "# process model and geometry\n", + "param.process_model(model)\n", + "param.process_geometry(geometry)\n", + "\n", + "# mesh and discretise\n", + "submesh_types = {\"SEI layer\": pybamm.Uniform1DSubMesh}\n", + "var_pts = {x: 100}\n", + "mesh = pybamm.Mesh(geometry, submesh_types, var_pts)\n", + " \n", + "spatial_methods = {\"SEI layer\": pybamm.FiniteVolume()}\n", + "disc = pybamm.Discretisation(mesh, spatial_methods)\n", + "disc.process_model(model)\n", + "\n", + "# solve\n", + "solver = pybamm.ScipySolver()\n", + "t = np.linspace(0, 100, 100)\n", + "solution = solver.solve(model, t)\n", + "\n", + "# Extract output variables\n", + "L_out = solution[\"SEI thickness\"]\n", + "c_out = solution[\"Solvent concentration\"]\n", + "x = np.linspace(0, 1, 100)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using these outputs, we can now plot the SEI thickness as a function of time and also the solvent concentration profile within the SEI. We use a slider to plot the concentration profile at different times." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "efe1fe18458a42d88056baf689f6da80", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(FloatSlider(value=0.0, description='t'), Output()), _dom_classes=('widget-interact',))" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "def plot(t):\n", + " f, (ax1, ax2) = plt.subplots(1, 2 ,figsize=(10,5))\n", + " ax1.plot(solution.t, L_out(solution.t))\n", + " ax1.plot([t], [L_out(t)], 'r.')\n", + " plot_c, = ax2.plot(x * L_out(t), c_out(t, x))\n", + " ax1.set_ylabel('SEI thickness')\n", + " ax1.set_xlabel('t')\n", + " ax2.set_ylabel('Solvent concentration')\n", + " ax2.set_xlabel('x')\n", + " ax2.set_ylim(0, 1.1)\n", + " ax2.set_xlim(0, x[-1]*L_out(solution.t[-1]))\n", + " plt.show()\n", + " \n", + "import ipywidgets as widgets\n", + "widgets.interact(plot, t=widgets.FloatSlider(min=0,max=solution.t[-1],step=0.1,value=0));" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Formally adding your model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The purpose of this notebook has been to go through the steps involved in getting a simple model working within PyBaMM. However, if you plan on reusing your model and want greater flexibility then we recommend that you create a new class for your model. We have set out instructions on how to do this in the \"Adding a Model\" tutorial in the documentation. " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.7.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/notebooks/Getting Started/Tutorial 1 - How to run a model.ipynb b/examples/notebooks/Getting Started/Tutorial 1 - How to run a model.ipynb index 9603f7e4c8..1ce1f19cc6 100644 --- a/examples/notebooks/Getting Started/Tutorial 1 - How to run a model.ipynb +++ b/examples/notebooks/Getting Started/Tutorial 1 - How to run a model.ipynb @@ -139,7 +139,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.8" + "version": "3.6.9" } }, "nbformat": 4, diff --git a/examples/notebooks/Getting Started/Tutorial 2 - Setting Parameter Values.ipynb b/examples/notebooks/Getting Started/Tutorial 2 - Setting Parameter Values.ipynb index 61bb53611c..76ca3069c5 100644 --- a/examples/notebooks/Getting Started/Tutorial 2 - Setting Parameter Values.ipynb +++ b/examples/notebooks/Getting Started/Tutorial 2 - Setting Parameter Values.ipynb @@ -92,7 +92,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "4e12f3f5188c4c47b9055d2a0cbd66ea", + "model_id": "051690c8629b4ab3992cfb364c4aa8e1", "version_major": 2, "version_minor": 0 }, @@ -166,7 +166,7 @@ "metadata": {}, "outputs": [], "source": [ - "parameter_values2.update({\"Typical current [A]\": 1.36})" + "parameter_values2.update({\"Current function [A]\": 1.36})" ] }, { @@ -182,7 +182,7 @@ "metadata": {}, "outputs": [], "source": [ - "sim.specs(parameter_values=parameter_values2)" + "sim2.specs(parameter_values=parameter_values2)" ] }, { @@ -200,12 +200,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e89691f9bbf84d6180b793ab2b22bd14", + "model_id": "260a89fad4db4fadbf409bc123a18285", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=0.4898989898989899, step=0.05), Output()), _…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=0.4852877005347593, step=0.05), Output()), _…" ] }, "metadata": {}, @@ -213,8 +213,8 @@ } ], "source": [ - "sim.solve()\n", - "sim.plot()" + "sim2.solve()\n", + "sim2.plot()" ] }, { @@ -259,99 +259,100 @@ { "data": { "text/plain": [ - "{'Negative current collector thickness [m]': 2.5e-05,\n", - " 'Negative electrode thickness [m]': 0.0001,\n", - " 'Separator thickness [m]': 2.5e-05,\n", - " 'Positive electrode thickness [m]': 0.0001,\n", - " 'Positive current collector thickness [m]': 2.5e-05,\n", - " 'Electrode height [m]': 0.13699999999999998,\n", - " 'Electrode width [m]': 0.207,\n", - " 'Negative tab width [m]': 0.04,\n", - " 'Negative tab centre y-coordinate [m]': 0.06,\n", - " 'Negative tab centre z-coordinate [m]': 0.13699999999999998,\n", - " 'Positive tab width [m]': 0.04,\n", - " 'Positive tab centre y-coordinate [m]': 0.147,\n", - " 'Positive tab centre z-coordinate [m]': 0.13699999999999998,\n", - " 'Negative current collector conductivity [S.m-1]': 59600000.0,\n", - " 'Positive current collector conductivity [S.m-1]': 35500000.0,\n", - " 'Negative current collector density [kg.m-3]': 8954.0,\n", - " 'Positive current collector density [kg.m-3]': 2707.0,\n", - " 'Negative current collector specific heat capacity [J.kg-1.K-1]': 385.0,\n", - " 'Positive current collector specific heat capacity [J.kg-1.K-1]': 897.0,\n", - " 'Negative current collector thermal conductivity [W.m-1.K-1]': 401.0,\n", - " 'Positive current collector thermal conductivity [W.m-1.K-1]': 237.0,\n", - " 'Cell capacity [A.h]': 0.68,\n", - " 'Negative electrode conductivity [S.m-1]': 100.0,\n", - " 'Maximum concentration in negative electrode [mol.m-3]': 24983.2619938437,\n", - " 'Negative electrode diffusivity [m2.s-1]': ,\n", - " 'Negative electrode OCP [V]': ,\n", - " 'Negative electrode porosity': 0.3,\n", - " 'Negative electrode active material volume fraction': 0.7,\n", - " 'Negative particle radius [m]': 1e-05,\n", - " 'Negative electrode surface area density [m-1]': 180000.0,\n", - " 'Negative electrode Bruggeman coefficient (electrolyte)': 1.5,\n", - " 'Negative electrode Bruggeman coefficient (electrode)': 1.5,\n", - " 'Negative electrode cation signed stoichiometry': -1.0,\n", - " 'Negative electrode electrons in reaction': 1.0,\n", - " 'Negative electrode reference exchange-current density [A.m-2(m3.mol)1.5]': 2e-05,\n", - " 'Reference OCP vs SHE in the negative electrode [V]': nan,\n", - " 'Negative electrode charge transfer coefficient': 0.5,\n", - " 'Negative electrode double-layer capacity [F.m-2]': 0.2,\n", - " 'Negative electrode density [kg.m-3]': 1657.0,\n", - " 'Negative electrode specific heat capacity [J.kg-1.K-1]': 700.0,\n", - " 'Negative electrode thermal conductivity [W.m-1.K-1]': 1.7,\n", - " 'Negative electrode OCP entropic change [V.K-1]': ,\n", - " 'Reference temperature [K]': 298.15,\n", - " 'Negative electrode reaction rate': ,\n", - " 'Negative reaction rate activation energy [J.mol-1]': 37480.0,\n", - " 'Negative solid diffusion activation energy [J.mol-1]': 42770.0,\n", - " 'Positive electrode conductivity [S.m-1]': 10.0,\n", - " 'Maximum concentration in positive electrode [mol.m-3]': 51217.9257309275,\n", - " 'Positive electrode diffusivity [m2.s-1]': ,\n", - " 'Positive electrode OCP [V]': ,\n", - " 'Positive electrode porosity': 0.3,\n", - " 'Positive electrode active material volume fraction': 0.7,\n", - " 'Positive particle radius [m]': 1e-05,\n", - " 'Positive electrode surface area density [m-1]': 150000.0,\n", - " 'Positive electrode Bruggeman coefficient (electrolyte)': 1.5,\n", - " 'Positive electrode Bruggeman coefficient (electrode)': 1.5,\n", - " 'Positive electrode cation signed stoichiometry': -1.0,\n", - " 'Positive electrode electrons in reaction': 1.0,\n", - " 'Positive electrode reference exchange-current density [A.m-2(m3.mol)1.5]': 6e-07,\n", - " 'Reference OCP vs SHE in the positive electrode [V]': nan,\n", - " 'Positive electrode charge transfer coefficient': 0.5,\n", - " 'Positive electrode double-layer capacity [F.m-2]': 0.2,\n", - " 'Positive electrode density [kg.m-3]': 3262.0,\n", - " 'Positive electrode specific heat capacity [J.kg-1.K-1]': 700.0,\n", - " 'Positive electrode thermal conductivity [W.m-1.K-1]': 2.1,\n", - " 'Positive electrode OCP entropic change [V.K-1]': ,\n", - " 'Positive electrode reaction rate': ,\n", - " 'Positive reaction rate activation energy [J.mol-1]': 39570.0,\n", - " 'Positive solid diffusion activation energy [J.mol-1]': 18550.0,\n", - " 'Separator porosity': 1.0,\n", - " 'Separator Bruggeman coefficient (electrolyte)': 1.5,\n", - " 'Separator Bruggeman coefficient (electrode)': 1.5,\n", - " 'Separator density [kg.m-3]': 397.0,\n", - " 'Separator specific heat capacity [J.kg-1.K-1]': 700.0,\n", - " 'Separator thermal conductivity [W.m-1.K-1]': 0.16,\n", - " 'Typical electrolyte concentration [mol.m-3]': 1000.0,\n", - " 'Cation transference number': 0.4,\n", - " 'Electrolyte diffusivity [m2.s-1]': ,\n", - " 'Electrolyte conductivity [S.m-1]': ,\n", - " 'Electrolyte diffusion activation energy [J.mol-1]': 37040.0,\n", - " 'Electrolyte conductivity activation energy [J.mol-1]': 34700.0,\n", - " 'Heat transfer coefficient [W.m-2.K-1]': 10.0,\n", - " 'Number of electrodes connected in parallel to make a cell': 1.0,\n", - " 'Number of cells connected in series to make a battery': 1.0,\n", - " 'Lower voltage cut-off [V]': 3.105,\n", - " 'Upper voltage cut-off [V]': 4.7,\n", - " 'C-rate': 2.0,\n", - " 'Current function': 1.36,\n", - " 'Initial concentration in negative electrode [mol.m-3]': 19986.609595075,\n", - " 'Initial concentration in positive electrode [mol.m-3]': 30730.7554385565,\n", - " 'Initial concentration in electrolyte [mol.m-3]': 1000.0,\n", - " 'Initial temperature [K]': 298.15,\n", - " 'Typical current [A]': 1.36}" + "{ 'Ambient temperature [K]': 298.15,\n", + " 'C-rate': 1.9981898750543627,\n", + " 'Cation transference number': 0.4,\n", + " 'Cell capacity [A.h]': 0.680616,\n", + " 'Current function [A]': 1.36,\n", + " 'Electrode height [m]': 0.13699999999999998,\n", + " 'Electrode width [m]': 0.207,\n", + " 'Electrolyte conductivity [S.m-1]': ,\n", + " 'Electrolyte conductivity activation energy [J.mol-1]': 34700.0,\n", + " 'Electrolyte diffusion activation energy [J.mol-1]': 37040.0,\n", + " 'Electrolyte diffusivity [m2.s-1]': ,\n", + " 'Heat transfer coefficient [W.m-2.K-1]': 10.0,\n", + " 'Initial concentration in electrolyte [mol.m-3]': 1000.0,\n", + " 'Initial concentration in negative electrode [mol.m-3]': 19986.609595075,\n", + " 'Initial concentration in positive electrode [mol.m-3]': 30730.755438556498,\n", + " 'Initial temperature [K]': 298.15,\n", + " 'Lower voltage cut-off [V]': 3.105,\n", + " 'Maximum concentration in negative electrode [mol.m-3]': 24983.2619938437,\n", + " 'Maximum concentration in positive electrode [mol.m-3]': 51217.9257309275,\n", + " 'Negative current collector conductivity [S.m-1]': 59600000.0,\n", + " 'Negative current collector density [kg.m-3]': 8954.0,\n", + " 'Negative current collector specific heat capacity [J.kg-1.K-1]': 385.0,\n", + " 'Negative current collector thermal conductivity [W.m-1.K-1]': 401.0,\n", + " 'Negative current collector thickness [m]': 2.5e-05,\n", + " 'Negative electrode Bruggeman coefficient (electrode)': 1.5,\n", + " 'Negative electrode Bruggeman coefficient (electrolyte)': 1.5,\n", + " 'Negative electrode OCP [V]': ,\n", + " 'Negative electrode OCP entropic change [V.K-1]': ,\n", + " 'Negative electrode active material volume fraction': 0.7,\n", + " 'Negative electrode cation signed stoichiometry': -1.0,\n", + " 'Negative electrode charge transfer coefficient': 0.5,\n", + " 'Negative electrode conductivity [S.m-1]': 100.0,\n", + " 'Negative electrode density [kg.m-3]': 1657.0,\n", + " 'Negative electrode diffusivity [m2.s-1]': ,\n", + " 'Negative electrode double-layer capacity [F.m-2]': 0.2,\n", + " 'Negative electrode electrons in reaction': 1.0,\n", + " 'Negative electrode porosity': 0.3,\n", + " 'Negative electrode reaction rate': ,\n", + " 'Negative electrode specific heat capacity [J.kg-1.K-1]': 700.0,\n", + " 'Negative electrode surface area density [m-1]': 180000.0,\n", + " 'Negative electrode thermal conductivity [W.m-1.K-1]': 1.7,\n", + " 'Negative electrode thickness [m]': 0.0001,\n", + " 'Negative particle distribution in x': 1.0,\n", + " 'Negative particle radius [m]': 1e-05,\n", + " 'Negative reaction rate activation energy [J.mol-1]': 37480.0,\n", + " 'Negative solid diffusion activation energy [J.mol-1]': 42770.0,\n", + " 'Negative tab centre y-coordinate [m]': 0.06,\n", + " 'Negative tab centre z-coordinate [m]': 0.13699999999999998,\n", + " 'Negative tab width [m]': 0.04,\n", + " 'Number of cells connected in series to make a battery': 1.0,\n", + " 'Number of electrodes connected in parallel to make a cell': 1.0,\n", + " 'Positive current collector conductivity [S.m-1]': 35500000.0,\n", + " 'Positive current collector density [kg.m-3]': 2707.0,\n", + " 'Positive current collector specific heat capacity [J.kg-1.K-1]': 897.0,\n", + " 'Positive current collector thermal conductivity [W.m-1.K-1]': 237.0,\n", + " 'Positive current collector thickness [m]': 2.5e-05,\n", + " 'Positive electrode Bruggeman coefficient (electrode)': 1.5,\n", + " 'Positive electrode Bruggeman coefficient (electrolyte)': 1.5,\n", + " 'Positive electrode OCP [V]': ,\n", + " 'Positive electrode OCP entropic change [V.K-1]': ,\n", + " 'Positive electrode active material volume fraction': 0.7,\n", + " 'Positive electrode cation signed stoichiometry': -1.0,\n", + " 'Positive electrode charge transfer coefficient': 0.5,\n", + " 'Positive electrode conductivity [S.m-1]': 10.0,\n", + " 'Positive electrode density [kg.m-3]': 3262.0,\n", + " 'Positive electrode diffusivity [m2.s-1]': ,\n", + " 'Positive electrode double-layer capacity [F.m-2]': 0.2,\n", + " 'Positive electrode electrons in reaction': 1.0,\n", + " 'Positive electrode porosity': 0.3,\n", + " 'Positive electrode reaction rate': ,\n", + " 'Positive electrode specific heat capacity [J.kg-1.K-1]': 700.0,\n", + " 'Positive electrode surface area density [m-1]': 150000.0,\n", + " 'Positive electrode thermal conductivity [W.m-1.K-1]': 2.1,\n", + " 'Positive electrode thickness [m]': 0.0001,\n", + " 'Positive particle distribution in x': 1.0,\n", + " 'Positive particle radius [m]': 1e-05,\n", + " 'Positive reaction rate activation energy [J.mol-1]': 39570.0,\n", + " 'Positive solid diffusion activation energy [J.mol-1]': 18550.0,\n", + " 'Positive tab centre y-coordinate [m]': 0.147,\n", + " 'Positive tab centre z-coordinate [m]': 0.13699999999999998,\n", + " 'Positive tab width [m]': 0.04,\n", + " 'Reference OCP vs SHE in the negative electrode [V]': nan,\n", + " 'Reference OCP vs SHE in the positive electrode [V]': nan,\n", + " 'Reference temperature [K]': 298.15,\n", + " 'Separator Bruggeman coefficient (electrode)': 1.5,\n", + " 'Separator Bruggeman coefficient (electrolyte)': 1.5,\n", + " 'Separator density [kg.m-3]': 397.0,\n", + " 'Separator porosity': 1.0,\n", + " 'Separator specific heat capacity [J.kg-1.K-1]': 700.0,\n", + " 'Separator thermal conductivity [W.m-1.K-1]': 0.16,\n", + " 'Separator thickness [m]': 2.5e-05,\n", + " 'Typical current [A]': 0.680616,\n", + " 'Typical electrolyte concentration [mol.m-3]': 1000.0,\n", + " 'Upper voltage cut-off [V]': 4.7}" ] }, "execution_count": 10, @@ -380,7 +381,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.3" + "version": "3.7.5" } }, "nbformat": 4, diff --git a/examples/notebooks/change-input-current.ipynb b/examples/notebooks/change-input-current.ipynb index 4b6516f1d3..7fb47d3ade 100644 --- a/examples/notebooks/change-input-current.ipynb +++ b/examples/notebooks/change-input-current.ipynb @@ -66,12 +66,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "528740d94922483b86f14724fcfa32b8", + "model_id": "8247adc1a3fd42dba31101bc6b501ed9", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=0.0006659775771737802, step=0.005), Output()…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=15.050167224080266, step=0.15050167224080266…" ] }, "metadata": {}, @@ -98,9 +98,7 @@ "\n", "# plot\n", "quick_plot = pybamm.QuickPlot(solution)\n", - "\n", - "import ipywidgets as widgets\n", - "widgets.interact(quick_plot.plot, t=widgets.FloatSlider(min=0,max=solution.t[-1],step=0.005,value=0));" + "quick_plot.dynamic_plot();" ] }, { @@ -118,12 +116,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "2811c58f82064ae6b2e558d33c5551fd", + "model_id": "511a6922f3f34350a543367723f09572", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=0.022125255063884477, step=0.005), Output())…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=500.0, step=5.0), Output()), _dom_classes=('…" ] }, "metadata": {}, @@ -136,9 +134,7 @@ "\n", "# plot\n", "quick_plot = pybamm.QuickPlot(solution)\n", - "\n", - "import ipywidgets as widgets\n", - "widgets.interact(quick_plot.plot, t=widgets.FloatSlider(min=0,max=solution.t[-1],step=0.005,value=0));" + "quick_plot.dynamic_plot();" ] }, { @@ -158,12 +154,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "8bad57c00aec41bd85c17bec2981a8da", + "model_id": "def542dad39b4ddebcd9a555cf684580", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=0.026550306076661374, step=0.001), Output())…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=600.0, step=6.0), Output()), _dom_classes=('…" ] }, "metadata": {}, @@ -195,9 +191,7 @@ "\n", "# plot\n", "quick_plot = pybamm.QuickPlot(solution)\n", - "\n", - "import ipywidgets as widgets\n", - "widgets.interact(quick_plot.plot, t=widgets.FloatSlider(min=0,max=solution.t[-1],step=0.001,value=0));" + "quick_plot.dynamic_plot();" ] }, { @@ -279,12 +273,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "47c47acd733b46428148e9dc7cdf373e", + "model_id": "7f4df64df94d410fb9aff92003538b14", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=0.0013275153038330688, step=6.63757651916534…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=30.0, step=0.3), Output()), _dom_classes=('w…" ] }, "metadata": {}, @@ -309,9 +303,7 @@ "# plot current and voltage\n", "output_variables = [\"Current [A]\", \"Terminal voltage [V]\"]\n", "quick_plot = pybamm.QuickPlot(solution, output_variables, label)\n", - "\n", - "import ipywidgets as widgets\n", - "widgets.interact(quick_plot.plot, t=widgets.FloatSlider(min=0,max=solution.t[-1],step=solution.t[-1]/20,value=0));" + "quick_plot.dynamic_plot();" ] }, { diff --git a/examples/notebooks/compare-comsol-discharge-curve.ipynb b/examples/notebooks/compare-comsol-discharge-curve.ipynb index 645d516f93..5630360617 100644 --- a/examples/notebooks/compare-comsol-discharge-curve.ipynb +++ b/examples/notebooks/compare-comsol-discharge-curve.ipynb @@ -214,7 +214,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.6" + "version": "3.6.9" } }, "nbformat": 4, diff --git a/examples/notebooks/compare-ecker-data.ipynb b/examples/notebooks/compare-ecker-data.ipynb new file mode 100644 index 0000000000..23fdc414d3 --- /dev/null +++ b/examples/notebooks/compare-ecker-data.ipynb @@ -0,0 +1,243 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Comparing with Experimental Data\n", + "\n", + "In this notebook we show how to compare results generated in PyBaMM with experimental data. We compare the results of the DFN model (see the [DFN notebook](./models/DFN.ipynb)) with the experimental data from Ecker et. al. [1]. Results are compared for a constant current discharge at 1C and at 5C." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First we import pybamm and any other packages required by this example, and then change our working directory to the root of the pybamm folder." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pybamm\n", + "import os\n", + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "os.chdir(pybamm.__path__[0]+'/..')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We then load the Ecker data in from the `.csv` files using `pandas`" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "voltage_data_1C = pd.read_csv(\"pybamm/input/discharge_data/Ecker_1C.csv\", header=None).to_numpy()\n", + "voltage_data_5C = pd.read_csv(\"pybamm/input/discharge_data/Ecker_5C.csv\", header=None).to_numpy()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the data is Time [s] vs Voltage [V]." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We load the DFN model and select the parameter set from the Ecker paper [1]. We update the C-rate an `InputParameter` so that we can re-run the same model at different C-rates without the need to rebuild the model. This is done by passing the flag `[input]`." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# choose DFN\n", + "model = pybamm.lithium_ion.DFN()\n", + "\n", + "# pick parameters, keeping C-rate as an input to be changed for each solve\n", + "chemistry = pybamm.parameter_sets.Ecker2015\n", + "parameter_values = pybamm.ParameterValues(chemistry=chemistry)\n", + "parameter_values.update({\"C-rate\": \"[input]\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For this comparison we choose a fine mesh of 1 finite volume per micron in the electrodes and separator and 1 finite volume per 0.1 micron in the particles" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "var = pybamm.standard_spatial_vars\n", + "var_pts = {\n", + " var.x_n: int(parameter_values.evaluate(pybamm.geometric_parameters.L_n / 1e-6)),\n", + " var.x_s: int(parameter_values.evaluate(pybamm.geometric_parameters.L_s / 1e-6)),\n", + " var.x_p: int(parameter_values.evaluate(pybamm.geometric_parameters.L_p / 1e-6)),\n", + " var.r_n: int(parameter_values.evaluate(pybamm.geometric_parameters.R_n / 1e-7)),\n", + " var.r_p: int(parameter_values.evaluate(pybamm.geometric_parameters.R_p / 1e-7)),\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We create a simulation using our model, parameters and number of grid points" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "sim = pybamm.Simulation(model, parameter_values=parameter_values, var_pts=var_pts)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can then solve the model for a 1C and 5C discharge " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "C_rates = [1, 5] # C-rates to solve for\n", + "t_evals = [\n", + " np.linspace(0, 3800, 100), \n", + " np.linspace(0, 720, 100)\n", + "] # times to return the solution at\n", + "solutions = [None] * len(C_rates) # empty list that will hold solutions\n", + "\n", + "# loop over C-rates\n", + "for i, C_rate in enumerate(C_rates):\n", + " sim.solve(t_eval=t_evals[i], solver=pybamm.CasadiSolver(mode=\"fast\"),inputs={\"C-rate\": C_rate})\n", + " solutions[i] = sim.solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally we plot the numerical solution against the experimental data" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))\n", + "\n", + "# plot the 1C results\n", + "t_sol = solutions[0].t\n", + "ax1.plot(solutions[0][\"Time [s]\"](t_sol), solutions[0][\"Terminal voltage [V]\"](t_sol))\n", + "ax1.plot(voltage_data_1C[:,0], voltage_data_1C[:,1], \"o\")\n", + "ax1.set_xlabel(\"Time [s]\")\n", + "ax1.set_ylabel(\"Voltage [V]\")\n", + "ax1.set_title(\"1C\")\n", + "ax1.legend([\"DFN\", \"Experiment\"], loc=\"best\")\n", + "\n", + "# plot the 5C results\n", + "t_sol = solutions[1].t\n", + "ax2.plot(solutions[1][\"Time [s]\"](t_sol), solutions[1][\"Terminal voltage [V]\"](t_sol))\n", + "ax2.plot(voltage_data_5C[:,0], voltage_data_5C[:,1], \"o\")\n", + "ax2.set_xlabel(\"Time [s]\")\n", + "ax2.set_ylabel(\"Voltage [V]\")\n", + "ax2.set_title(\"5C\")\n", + "ax2.legend([\"DFN\", \"Experiment\"], loc=\"best\")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For a 1C discharge we observe an excellent agreement between the model and experiment, both in terms of the overall shape of the curve and the capacity. The agreement between model and experiment is less good at 5C, but in line with other implementations of the DFN (e.g. [2]). " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "[1] Ecker, Madeleine, et al. \"Parameterization of a physico-chemical model of a lithium-ion battery II. Model validation.\" Journal of The Electrochemical Society 162.9 (2015): A1849-A1857.\n", + "\n", + "[2] Richardson, Giles, et. al. \"Generalised single particle models for high-rate operation of graded lithium-ion electrodes: Systematic derivation and validation.\" Electrochemica Acta 339 (2020): 135862" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.6.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/notebooks/create-model.ipynb b/examples/notebooks/create-model.ipynb deleted file mode 100644 index 0ae441220b..0000000000 --- a/examples/notebooks/create-model.ipynb +++ /dev/null @@ -1,666 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Creating a Simple Model\n", - "Before adding a new model, please read the [contribution guidelines](https://github.com/pybamm-team/PyBaMM/blob/master/CONTRIBUTING.md)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this notebook, we will run through the steps involved in creating a new model within pybamm. We will then solve and plot the outputs of the model. We have choosen to implement a very simple model of SEI growth. We first give a brief derivation of the model and discuss how to nondimensionalise the model so that we can show the full process of model conception to solution within a single notebook. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note: if you run the entire notebook and then try to evaluate the ealier cells, you will likely recieve an error. This is becuase the state of objects is mutated as it is passed through various processing. In this case, we recommend that you restart the Kernal and then evaluate cells in turn through the notebook. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## A Simple Model of Solid Electrolyte Interphase (SEI) Growth" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The SEI is a porous layer that forms on the surfaces of negative electrode particles from the products of electrochemical reactions which consume lithium and electrolyte solvents. In the first few cycles of use, a lithium-ion battery loses a large amount of capacity; this is generally attributed to lithium being consumed to produce SEI. However, after a few cycles, the rate of capacity loss slows at a rate often (but not always) reported to scale with the square root of time. SEI growth is therefore often considered to be limited in some way by a diffusion process." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Dimensional Model" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We shall first state our model in dimensional form but to enter the model in pybamm, we strongly recommend converting models into dimensionless form. The main reason for this is that dimensionless models are typically better conditioned than dimensional models and so several digits of accuracy can be gained. To distinguish between the dimensional and dimensionless models, we shall always employ a superscript $*$ on dimensional variables. " - ] - }, - { - "attachments": { - "SEI.png": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAACGQAAAGRCAYAAAApCfcPAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAABM5QAATOUBdc7wlQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7N13YFvlvf/x75Esyxree8R2YmfhTGeQQBZhhU0ZDZRLaYEW2tsftNALLR20hV5ob0tvS8u6UKClYTWhpGkLBEoYCRkEEmeHJI7jvWTLQ7KscX5/0NAQfI5k50iWnffrP/IcneeRJcviPJ/z/SryL5dffvlvVVX9TwEAAIhzD/7i0MtJ1qBtuNcBAAAAAACi77ebbq3+8+7PjR3udQAAAIRlUm/dcVfW3k/+czjXAgAAAAAAAAAAAAAAMBoRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwGIEMAAAAAAAAAAAAAAAAgxHIAAAAAAAAAAAAAAAAMBiBDAAAAAAAAAAAAAAAAIMRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwGIEMAAAAAAAAAAAAAAAAgxHIAAAAAAAAAAAAAAAAMBiBDAAAAAAAAAAAAAAAAIMRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwGIEMAAAAAAAAAAAAAAAAgxHIAAAAAAAAAAAAAAAAMBiBDAAAAAAAAAAAAAAAAIMRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwGIEMAAAAAAAAAAAAAAAAgxHIAAAAAAAAAAAAAAAAMBiBDAAAAAAAAAAAAAAAAIMRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwGIEMAAAAAAAAAAAAAAAAgxHIAAAAAAAAAAAAAAAAMBiBDAAAAAAAAAAAAAAAAIMRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwGIEMAAAAAAAAAAAAAAAAgxHIAAAAAAAAAAAAAAAAMBiBDAAAAAAAAAAAAAAAAIMRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwGIEMAAAAAAAAAAAAAAAAgxHIAAAAAAAAAAAAAAAAMBiBDAAAAAAAAAAAAAAAAIMRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwGIEMAAAAAAAAAAAAAAAAgxHIAAAAAAAAAAAAAAAAMBiBDAAAAAAAAAAAAAAAAIMRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwGIEMAAAAAAAAAAAAAAAAgxHIAAAAAAAAAAAAAAAAMBiBDAAAAAAAAAAAAAAAAIMRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwGIEMAAAAAAAAAAAAAAAAgxHIAAAAAAAAAAAAAAAAMBiBDAAAAAAAAAAAAAAAAIMRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwGIEMAAAAAAAAAAAAAAAAgxHIAAAAAAAAAAAAAAAAMBiBDAAAAAAAAAAAAAAAAIMRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwGIEMAAAAAAAAAAAAAAAAgxHIAAAAAAAAAAAAAAAAMBiBDAAAAAAAAAAAAAAAAIMRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwGIEMAAAAAAAAAAAAAAAAgxHIAAAAAAAAAAAAAAAAMBiBDAAAAAAAAAAAAAAAAIMRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwGIEMAAAAAAAAAAAAAAAAgxHIAAAAAAAAAAAAAAAAMBiBDAAAAAAAAAAAAAAAAIMRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwGIEMAAAAAAAAAAAAAAAAgxHIAAAAAAAAAAAAAAAAMBiBDAAAAAAAAAAAAAAAAIMRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwGIEMAAAAAAAAAAAAAAAAgxHIAAAAAAAAAAAAAAAAMBiBDAAAAAAAAAAAAAAAAIMRyAAAAAAAAAAAAAAAADAYgQwAAAAAAAAAAAAAAACDEcgAAAAAAAAAAAAAAAAwWMJwLwAjz/z588XpdA445na7ZfPmzTFeEYB4l5ycLPPmzdMc37t3r9TW1sZwRQAAAAAAAAAAAEB0EcjAoF155ZVSUlIy4Nj+/fsJZAD4jMzMTLn55ps1xx9//HECGQAAAAAAAAAAABhVCGQAAAAAAAAAAADghKh+r6n+b98p0xp3jl3QkTb9yrZYrglA5JpeuXtsoK9zwL1ja+Y4T/bCW+tjvSZgNCCQEUVWq1Xuueee4V7GgPx+v3zve98b7mUAAAAAAAAAAIBRINTvNXW8/8xY7QOCCoEMIH51bHuxKNDTbBtozF40q51ABjA0BDKiyGQySVmZZhh0WPl8vuFeAgAAAAAAAAAAAAAAoxaBDAADOu+88yQzM3PAse7ubnn55ZdjvCIAAAAAAAAAGB3cO1/OrFv1/6YN9zoGYi+a6Rp7/cvbh3sdAACMBgQyAAxo8eLFMn78+AHHGhsbCWQAAAAAAAAAwFCF/Eqov9cy3MsYSKjfy94RAAAG4Y8qAAAAAAAAAAAAgLDUoF85/IflmtVdHKXzXDln3FEbyzUBsdK5/cWsjg+eLdQazz//no+Scis8sVwT4h+BDAAAAAAAAAAAAADhqUGl5+BbuVrD5kR7IJbLAWLJ17rPoff+D/S0HhbNUZysTMO9AAAAAAAAAAAAAAAAgNGGChnD7Nlnn5X9+/fHfN5QKBTzOQEAAAAA8eFgtS/pjX/2pO/e2+c8VO1zNLcEkjpcAavHq5q9faEEERGLRQklJipBp8MUsFiUUFaWxVdUaPEUj7F4x5ZYvZMnJfXOnmnrsSQq6nA/HwAAAAAAgHhEIGOYHT58WKqqqoZ7GQAAAACAUW7rh17nM8+68t94szu3vsFvD3e836+a/H7V1NsbsoiI1NX7Hdu2S8axx1itSnB8eVLX1Iok9+xKu3vZ2cmuvDyL38h1X3LloRkfbvemG3lOo/zuf4u2X3R+qmuwj9v6odf5ueWH5ugd85cXxm2unGHrHfrqAAAAAADAcCOQAQAAAADAKPba691pv/x1S9nWDz2ZqsG1LHw+1bxzlzd95y5v+rMvdMh/3SXqpIk29+KFjtaLzkttO3WuvftE5/B6VfPRUEi88ftFGcrjQkFVwj2nYEAd0rkBAMDoUXbjmvfMtrRArOc1We2U2AYAwCAEMgAAAAAAGIXqG/yJt367btK6t3vyjA5iaAmFRNm9x5u2e4837eHH2sbfcF3mwZ//d8HB2MwOAAAwulizx3vNjqyYBzIAAIBxCGQAAAAAADDKrPlHV8at366b2tkZtA7nOvr9qmk45wcAAAAAABhOXBgBAAAAAGAUefzJ9rwbbj5SOdxhDAAAAAAAgJMdFTIAAAAAABglVjzfkf3dHzZMDYVEGexjFUUkJdncb7MpQafT7DeZRPV6Q+aenpClpzdk8VPtAgAAAAAAYFAIZGBUUBRFcnNzJScnR7KyssRut4vFYhERkf7+funt7ZWWlhZpbGyUjo6OYV5tZJKTkyUvL0+ys7PF4XCI0+kUEZFQKCQej0c8Ho+43W6pra0Vt9s9zKuNb2azWXJzc6WoqEhSU1PFZrOJ2WyWvr4+cbvd0tTUJHV1ddLf32/IfFarVfLz8yUnJ0dSU1M/ee1ERDwej3R1dUlzc7M0NjaK1+s1ZM5oMJlMUlBQIGPGjJG0tDRJSkoSERGv1ysej0fq6+ulrq5OfD7fMK906JxO5yevldPpFLvd/slYT0+PdHR0SHNzszQ0NEgwGBzGlQIAAIS3vcrr+PZ366dFGsYwmxW1cqat/ewzUlpnzrR1zZxu601PM2v2KHe1BxM2bO5N2fqBJ2XHLm/K7j19qc0tAZtxzwAAAAAAAGB0IZCBESsrK0tOP/10mTp1qkycOPFTG6l6WltbZdeuXfLee+/Jtm3bJBDQvN4YUw6HQ2bPni2VlZUyYcIEycnJifix3d3dUlNTIzt37pSqqio5cOBAxJvH+fn5MmXKlM/8e2pqquZj7Ha7nH322RGv73jV1dVy4MCBsMdVVlZKZmbmgGN+v1/WrVun+djk5GRZuHChVFZWypQpUz4J6Gh54IEHZP369WHXpKWkpEQWLlwoU6ZMkXHjxonZbA77mFAoJDU1NbJjxw5Zv359RD+TaFMURWbNmiWLFy+WadOmfSpMMpBQKCT79u2TLVu2yLp16+I+HJSQkCAzZ86UuXPnyqRJk6SgoCCix/l8Pjlw4IBs2bJFNmzYIO3t7VFeKQAAwOAEg6LcfEvtFJ9PDftF1GpVgtf9R2b1t76RU5uTY/ZHOkdGpjlw4XkprgvPS3Ed/bct73ucf36pM3ftP7tza4706395jJLFC5zNP/1xwf7hmLt4TOLITScDAAAAAICoI5CBEWf69Oly6aWXytSpU0VRBl2FV7Kzs2XJkiWyZMkS6ezslL/+9a/yyiuvSF9fXxRWG9748ePloosuklNPPVUSEob2K5mcnCxTpkyRKVOmyFVXXSUej0fWr18vr7/+ethN/okTJ8rNN988qPlSU1MH/ZhjrVy5MqLwwYUXXijTp08fcKyrq2vAQEZaWppcccUVsnTpUrFaI2+ZPZT3kqIocuqpp8pll10mZWVlg368yWSSsWPHytixY+Xiiy+W2tpaWbVqlaxfvz7m1RgURZH58+fLNddcI3l5eRE/zmQyyeTJk2Xy5Mly1VVXyRtvvCHPPfec9PT0RHG1g5eeni4XXnihnHXWWWFDJgOxWq1SUVEhFRUVct1118nGjRtl5cqVUl1dHYXVAgAADN4TT7Xn7f/Ip52q/pdZM+3tj/2ueGdpicWQIMGc2faeObPtPT8TObhjV5/98d+3Fb68xl3U3RPST0QbyOE0ByZPssZv6TkAAAAMK1/LXpunYbvT315t83c3WUP9HrMa9JtERExWZ8BsT/Nb00u9SfnTeuxFlb1iMqvDvWY9QW9HgufIFqev/aDd33EkKej3mEPeLouIiGKxhkwWR8Cc5AwkphX3JeVP6bUVTO9VEpJCw73u+KSKt77K0ddU5fC1HnAE+joTQn3dCaKqismaHEhMze+zZI7zpk29rE0xW074faEG/Yq3douzr3mPw9dx2Bb0uC0hX3eCiIhitoRMVkfQ4sz1JWaVeRxj5nRbMkoJgMMwqr/X1FuzKdnX8pG9v+OwLejrSQj5ev71/ksMmazOQGJqYV9i1livs+T0LnNyTsQ3cEAfgQyMGCUlJXLTTTfJxIkTDTtnWlqaXHvttXL++efL448/Lps3bzbs3OEUFBTI9ddfLzNnzjT83EcrWJx99tlSXV0td95550nRbmHp0qVy3XXXDWnDfbCBjNLSUvna174m5eXlg55Ly5gxY+TWW2+VSy+9VB599FHZt2+fYefWk5aWJrfccotm+CVSiYmJct5558lpp50mjz32mGzcuNGgFZ7Ymi6//HK55JJLwlZJidTR8Mq8efPk1VdflT/96U/i8XgMOTcAAMBQPfx/bWPDHbN0SXLTH58o2ZmUpETlYuzUiiTPr39Z9NF//6Tw4ONPtuX/8VlXcTTmAQAAAPSofq+ps+rPWZ07Xs7z1m9LD3o7Ir5zz5RoD9gKK11pUy5uSq+8qkWxOIY/yBAKKN3716Z17nw511OzKaO/s9YhqhrxBW3FZFYtaUW9jpJ57c7yM9pTJi3rMFmTI9owcL3/h1xP3daUT68nqDu3t3l3at1fbh0f6fqOlznn+gZb4fTecMfVr/52uRryD7iWpJxJvVmnfa1B67HuXX/NcG97Pr/n8HtZQW9n2PeHvWj229assiHd1RvsbUtwffCn3K7df8/ra9qZFvL3hS+v/S8Jzhyvo2Ree/qMzzcmTzynM97DQke1rnugyNdZo9neMnPOdQ22wsqwr7FhQkGlYc13x4VCPpPWIfnn/uSQ2ZYaPPYxdatv+8wGkK9xp+6NEG0bHh3TueOlyEvgHyPBlubPO/fHh4fyWC19jVX2jg9W5HcffDurv+1Aihrm9/ffFEnMKO5xli9tyai8uslWNCu+7sIdYQhkIO4piiJXXnmlXHHFFRG1gRiKzMxMufPOO+Xvf/+7PPXUU1ENLyiKIpdeeqlcffXVUXs+xxo7dqyYTKZRHcgwmUxy/fXXy3nnnTfkcwwmkHHRRRfJtddeG7XXr6SkRH7605/KCy+8IC+++KKoavS+Y5WVlcldd90laWlphp0zNTVVvv3tb8uKFStk1apVhp13sMaNGye33377oCp+DIaiKLJs2TKZPn26/OxnP5Pa2tqozAMAABDOm2/1pB6p1W8XUlKc2BPNMMaxHA4ldOs3sutv/UZ2fUODPzHa8wEAAAAiIn53Q2LLWw8Ud25/cUzI1zOku7NC/Z6E3up3c3qr381pWvvT/vTKL9TknnnnkUgDDEYKejsSWt/5TVHHB8+PCfQ0a25uh6OGgkq/q8bZ76pxdnz4fIlitoSc5UuaM+dcV5c86bwOvcf2fPRmpnvX6qLBzHd0rqGuN3ncwo5IAhkdH6woUQO+AS/SO8YuaBkokNGx7fnsljd/Ud7ffih5cKsa/DV6X9vBpJZ1/1Pq3rm6UGud4QR6WmzuXauL3LtWF1nSCnuz5n+1OnPeTY1GVOuIpqDfY+54/xnNmwZC3k5L8dVP747Vety7V2e0b35Cs8y5NWeS22xL/ejYf1PVkOg9By3d+16LrE/6ABKcuV5jAhmquKtWZrW++1Cpt2F7xlDP0e+qcbo2P+l0bX5qnH3M7LbcM+846CxfGt996+MUgQzENZvNJrfddptUVlbGZL7zzz9f8vLy5Oc//7n4/cZX4on18zkZKIoi3/jGN2Tx4sUnfJ5Ijvna174mZ5555gnNFel6li9fLsXFxfKrX/0qKoGayZMny1133SV2u93wcyuKItdcc42YzWZ58cUXDT9/OIsWLZKvf/3rhlXF0JOfny/33Xef3HvvvbJ3796ozwcAAHC81X9zh7375hf3F+6KRRjjeAUFlv5YzwkAAICTixr0Ky1v/qy4bf0jZSG/17B9r2CfO7Ftw8PjO6tWFRWc/5M9qdOuaDPq3LpCQaX1nV8Xtr7zYHmwr8vwgLMa9Ju6963N7963Nt9ROq913I1/+9DoOeJNsLfVcuSFm07pOfhW7pBOEHFVAREJBZSmN+4vblv/UPlQgxgD8XfWOxr/cfeU9s1PlxRe/MAuZ9nCLqPObbTs025qaHv3d+PVYP+AFSm697+eH/R27Dfb0gOxWI9ryx91Q0XpM6+ui8U6YsFbv81R//K3JnsbqoYYxBiIKp7aLVnVT30+K7XigrrCi3+13+zIiMlrN1polmYBhpvNZpMf/OAHQw4vqKo6pE3syspKueOOOwyvfpCcnCx33333CYUxAoHAqK50MRTLly8/4TCGSPhAhqIo8vWvf33IYQyfb2it3ubPny+33Xab4e/HwsJC+c53vjPoMIbX65W2tjbp6OiIKLS0fPlyWbBgwVCXOSRnn3223HLLLUMOYwQCg/8eYbPZ5Pvf/76MHz/kSnwAAABDtvUDb7re+NjSxO6li53cxQIAAIBRx9d2IOmj3y2e27LuVxONDGMcK9DTbDvyws2V9au/XT6ojfkh8LUdTDrw8Jmzm9b+9JRohDGOF+h1jfqKdr6WvbaPHj577pDDGCISaYUMf1eT5cAj58xqfetXE40MYxyrv/1Q8uGnLz+15fX/jtsWkWZHtj9lwpmNWuMhf5/ZtenJ6JS2Po6/szaxt/pdzZsYTImOQObc65pisZZoa3nj/uKDj54739gwxrFUce9aU/TRw0tO9dZ/4IjOHKMTFTIQlxISEuR73/ueTJw4MaLja2trpaqqSnbv3i1HjhyR7u5u6e7uFpGPgxCpqakyceJEqayslJkzZ4rVqt8SrLKyUq677jr5/e9/f8LPRUQkMTFRvve970W8WauqquzZs0e2bdsm+/fvl8bGRmlvb/+kdYXNZhOn0ykZGRlSXl4u5eXlMmHChCG1Zjhy5Ii89NJLn/n3JUuWSHr6wNd1e3p6ZO3atYOe66jdu42pRDV58mS54oorNMf7+vpk9+7dUl1dLW63W7q6usTj8YjNZpPs7GwpLS2VadOmSUpKSthAxhVXXCFLly6NaF09PT3yzjvvyLZt2+Tw4cPS1vbv4HR6erqUlpbKjBkzZNGiRZKSkqJzpo/NmzfP0PdjUlKS3HXXXeJ0hq8a19/fL++++65s3rxZ9u3bJ11d/w7dKooiubm5MnXqVFm4cKFUVFR85vGKoshNN90kv/71rw1ZeziLFi2Sm266KaKKJx6P55PPjT179khnZ6d0d3eL3++XxMRESUlJkcLCQpk5c6bMmTMn7O+XzWaTO+64Q+68805xuVxGPSUAAICwqmt8ul/szjojpSVWawEAAABipXv/62m1L3xlRiyCCyKquDY/Oc7vrk8qveaZXWIyG94yonv/a2m1L3x1ZrCvO/plf08Sftdh66EnLp0T6G1NivZcvpa9tkO/v2z2ibSXiZQaCirN6345ydd+0DFm+eN7RKKaExqSjHk31rn3/KNQa9y17fmi7CW3Rb0yRdvGxwpVnSBVyqRlDcPRkshIatCv1Kz4YsWJtEsZDH9nvePQE5fOLbl2xVbn2AVxW6klnhDIQFy66aabZPLkyWGP2759u7z00kuyY8cOzWOOhjPq6urkjTfekIyMDFm+fLksXbpUTCbtIjEXXHCB7Ny5UzZv3jyk53CUoihyyy23RBTGCAQCsnbtWlm9erW0tGhfM/V6veL1eqW1tVX27dv3yb+XlJTI6aefLqeddprk5+dHtL5Dhw7JoUOHPvPvU6ZM0QxkdHd3yzPPPBPR+aPFbDbL17/+9QE33hsaGmTVqlWyYcOGsJUpFEWR2bNnS2+vdku66dOny/Lly8OuyefzycqVK2XNmjWa83Z0dEhHR4d8+OGH8swzz8h5550nn//858Vm0/+OdsEFF8i+fftk/fr1YdcRzg033BBReOfNN9+UZ555Rjo7OwccV1VVmpqapKmpSdauXSuTJk2Sr3zlK1JaWvqp4+x2u3z5y18+4XWHM378eM33xLE6OjpkzZo18uqrr4rX6x3wmP7+fmlra5O2tjbZvn27PP3007J48WK5+uqrJSsrS/PcGRkZcuutt8qPfvSjTwJUAAAA0dTUErR4PCHd/7cvL0sM238ZAAAAGEm69/4j/cjzN1aG/H2DrEKgiDnJ6TdZU/xqoN8U7HMnarVVGHDefa8VHHn+y2rx1X/YNdg163HvfCmz9s//OWMoVRWUBGvQbEvvV8wJITXgN4X6uy2hfs9Jv/8X8nWbDz39+Uojwhiqql8Zxdd2MGkoYQyTxRYwJaX6FbMlpAZ8pqCnPUkvPHC8zh1/GSOiyMehjPjiLFvitmaP7/K1fjTgXan9bQdSeg6+kxLd1iuquKte0gyFiIhkzvvKiG5Xogb9yuGnr5jWc+jdQVeAURKsQbM12a8kWEMhX0/Cx+G2yPY1Qv29lpo/fmHW2OtXbbEXze4Z9MJPMif9BzLiz4IFC8JWIvB6vfLQQw/Jhg0bBn1+l8slDz/8sLzzzjty55136rZs+OpXvyq7du3S3awP56yzzpL58+eHPe7AgQPy4IMPSl3d0D/7a2pqpKamRp599lmprKyUyy+/POIqIyONw+EQh+PTFZFCoZC8+OKLsnLlyohbu6iqKlu2bNEct1qtcvPNN4fd5G9qapL7779famtrI5pXRMTv98vq1avl/ffflzvvvFOKinTbmMmNN94oVVVVn1R/GYqKioqwv1+BQEB+97vfydtvvz2oc+/du1e+853vyM033yxLliz51NhQqrcMRlJSktx2221h25Rs2LBBHnroIc0ghhZVVWXdunWyceNGuf3223VbD02ZMkXOOuusE6oiAwAAEKn2Nn/Yu+fy8yz9sVgLAAAAEAueI1ucR57/ysxIwhgmS1LQMW5Rs6P09I7k8jM6kvJP8RxfTSDY22px7/pbRs9Hr2d1ffTPvHChCPeuvxU2v35vT+5Z3685waciIiI9B9elDiaMkeDI6kuecFazo2yRy1k6v8uSVvzZuwNDQaWvea+t9/D6VE/9B6meug/T+tsOhC/XfJz0mcsbbQVTP7VhroYCSvMbP9O8o9aWe0pn6rTPNQx2rqOSCmYYsrnbsOa7Zf3tB5O1xq3ZE9yOklNd1pyJvQmOrP4EZ16/yZygBvu7zX3N+x19DdtSeqo3ZAV6mm0S0p5H9XtNNX+6ZkZkYQxF7IXTXSlTPteYMvFslzVn4qcuVKuBPlNvzcZk96412e6dqwuDnnb9cu8i0rnjpTHWrLLenDO/eyT8/LGVXnl1bdOrP/lsae1/cW1+vMhZttCYkuoDcO9cnenvatTcBLTlntJpL54z4PtNUUySd/b3PrO2nkPrM3oOrtPc8MiY88XqxLQxg9uA+BeTNWXQ/dRrVnyxItIwhjWrvMtZvqTVOW5hh7NssfszlUFCAcVzZIvTvftvWV37XsvT+/0R+TiUcWTFl2aM/9o/N5mTc8L3uD+JEchAXElJSZEbbrhB95jm5ma59957paFhyH/PRURk586d8v3vf1/uvvtuSU1NHfCY9PR0ueSSS2TFihVDmiMrK0uuu+66sMetX79eHnzwQfH7OShqKwAAIABJREFUjfm8UlVVtm7dKlu3bpUpU6ZIKKTzbWGUCAQC8stf/vKEK5oc7/LLL5ecHM32YiLycUWOH/7wh9LR0TGkORoaGuQHP/iB/PjHP5biYu22bykpKXLNNdfII488MqR5FEUJW6kiFAqd0M/R7/fLb3/7WwmFQhG3eDHCNddcE/Z1euqpp+Svf/3rCc3T19cn999/v3zjG9+QRYsWaR531VVXydtvvx22QgsAAMCJ8vaFIr6bDwAAABjp/O6GxJoV180M+b26+1smq9OfUfmFmpxF36oNt1FodmT7M+Z+qTlj7pea/a7DBxpf/WG5e/ffC0RVNe/Sa337NxOSy8902UvnD/3uOfm4pcaR526IKIxhSRvTm7PomwcyZl/bErZlismsJuVXeJLyKzyZIo0iIr7mPTbXhyvyunb/I6/fVa270XpU8qRlHcmTln3qwrca6DPpBTISM0p6sxdHvxWFnv72g87ewxuyPzOgmNTUScsaspd8+7CtcLrmnbjJE87pFJF6EVU6t/85y2xP19wor135nxO1qkAcKzGzrLvwovt3O8uXurWOURKSQs6yJW5n2RJ3wfn3Hmp+4/7itg2Ploer4tLy1q8m2kvmuZ3lZ2ieezhkzr2+qeXNX04M9fcO+Pvavf/1/KC3Y7/Zpv3zPRGuLU/r3gWbNusL2u9Tk1kd6H0c8nvMeoGM1CmXtDjLlsTkdWh69UelkbQpsRfNas9edGt1yikX6PdaNyWo9tL53fbS+d35599T7dr8ZF7LugfK9UIt/q5G+5E/f23y2C+vrBrCUzhpEMgYZkuWLJFJkybFdM4jR44M+s73WLnyyislJUX771ZPT48hYYyjampq5H//93/lhz/8oWYFhAsuuED++te/DqkqwfLly8O2oti4caP8+te/jriiw2Dt3LkzKueNNw899JDhYQyn0ynnn3++7jEej0fuu+++IYcxjurq6pL77rtPfvazn+n+DixdulRWrVql29JGy+zZs2Xs2LG6xzz77LMn/HNUVVUeffRRKSwsjEmFloKCAjn33HN1j1m5cuUJhzGOCgaD8tBDD0lhYaGUlZUNeExaWposW7ZMXn75ZUPmBAAA0JKWag574aq1LUAPagAAAIwKdau+MTlcJQJb7imdY65+qsqaVdY32PNbMkp9xVf/YVfH1j+21q++Y5rWRrgaCip1f/lmxYRbNmwKG47QpMqRF2+uCHo7w1RBUCRjzrXVBRfcd1BJSBry3ZfW3Mne/GX3VOcvu6faXbUqs/Wd34wLBXyjMuA90AZygjPHO+by3+5wjj9z4B7dA1IkbfqVbVqj3fteS3PvXD0m3FlSJ19QP+bzj+5RLLaIXz8lISmUd+6PDqdMPLe95tkvz9RrvaKGgkr96tsrJty66T3FbImbXtoma3Iw5ZQL6zu3PV8y0HjI32d2bXoyL3uJ8QEef2dtYm/1+s+Gco6uLdHhz5zzxSaj540Vz+ENyW3rHxqvd4xitoRyz/ivfdlLbo+8tPu/Hy0Zc69vSp36ubbDT39+uqfug0ytI3sOrsvr+HBFY/rML7QOfp6TA4GMYRZJKwujbdy4MS4DGVlZWXL22WdrjquqKr/4xS8MC2McVVVVJS+99JJcdtllA44nJSXJ4sWLZc2aNYM6b0FBwWfaNhyvpqZGfvOb30QtjHGy+Oc//ylvvfWW4eddtmxZ2EDNE088Ydh7sqWlRf7v//5Pbr/9ds1jzGazXHTRRfLEE08M+vwXX3yx7vj+/fvlL3/5y6DPO5BAICAPPvigPPDAA5KYmGjIObVcddVVYjZrB8g3b94szz77rKFz+v1+eeCBB+SBBx4Qq3Xg/18699xzZfXq1aKqcfP9FwAAjEL5eYlh25F8uM2T8qVrM5pjsR4AAAAgWlzv/yG35+BbuqX5kyctayi5+qldJ7opnT7r2pYEZ+77NSuum60VyvC1HUhp3/REXub8rzYOZY629b8r8NRuydI7RjGZ1YILf16VMfdLhn6fT512WXvqtMva+xqrtHu6jyKW9DE9ZTes3jpge5chU6Xh79+bLKL/VkubemntmOWP7zm+VU6k7KXzu8fd8JctBx+/eK5eC5N+V42z5c2fFRvVSscoWfO/Wte57YUSrZ+Ta9vzRdEIZLRvfLxQDQU0A0cpk85rVCyOkVlePhRQal+6dYoaCmq+qUyJjkDJtSved45d0KV1TCTMtvTAuBv/+uHhPyyfqtcapfmfPx+fPv3zbWJKYENkAAQyEDeWLVsmFov2jVvvvfee7NixIypzr1q1Ss4991xxOBwDji9dunTQgYxzzz1XTCbtcGkoFJIHH3yQlgYnyOPxyB//+EfDz6soipxxxhm6x+zdu1fWrVtn6LwbNmyQc845R6ZOnap5zKJFi+QPf/jDoFrc5OXlyeTJmlXkROTjlh5GtrdpbGyUv/3tb/K5z33OsHMeLzMzUzfY5vf75cknn4xKKKKpqUlee+01ueiiiwYcz83NlYqKipOmSg0AABgeDocSslqVoM+naiZU33q3J1sNyUfKqLz3DQAAYPSqfenWSSZzYkw3DFOnXtqcOuWS9ljOGQk16Fda3vwf3bvB7WNmtxsRxjgqeeI5nblL79zbtPaeU7SOaV3/u3GZ87/SONjNdtXfa2p9+8Fy/aMUKbjgvh1GhzGOlZQ/zROtc8cLc1JK/9jr/vyBsWEMkY6tf8rpbz+k2/rFXjjDVXTFI3uHGsY4ypozyVu8/LFth59ePkcvZNC+6anSnMW31w6mEke02Qpn9NoLp7s89dsyBhrvbzuQ0nPwnRRn2cITCg58miqdVSsL9Y7IOu2rw9pW50S0vfdIvt57TzElhIou/922Ew1jfHK+hKRQ8dVP7frot4uT/e76AUNc/o5ap2vL07kZp94wYquORBOBDMQFs9msu/kdCATkT3/6U9Tm93q98uqrr2pWySgpKZGcnJyI20RYLJaw1THeeOMNqa6uHuxScZy33npLuroM/Dv9LxMmTJC8PM02YCIi8uKLLxo+r4jI888/rxvIcDqdMmvWLNm4cWPE5zz99NM12/KIiOzevVv27ds3qHVGYvXq1XLRRRdJQkJ0/twsXbpUN/j0yiuvDKm9S6TWrFkj559/vmaFjtmzZxPIAAAAUTemyNJ74GC/Zt+72jq/8/d/cOXd8KUMLowAAACMIN17XymI9ZyJWeN64zGQ0bbhoQK/u0GzmkOCM8db+sXnthvdriF78Tfreg6+mal1Z7i/s87RtftvGSmnXOgazHlb3/5NUaC3TbMFhYhIxuz/qGZz88TlX3DfLmtW+aDb14TTuv6RUr1xU6I9UHTlozuNek86y5a4M0+9/lDbe49pBnmC3g5r23uP5WcvurXeiDmNkj7r2lqtQIaIiGvz74ucZQt3GzWfe+fqzIHa1hxly6/otBXN6jFqvlhSg36l9Z2HBu6l/i85i7+5P7XiokF9JoVjtqUHiq98ZPvBJy6eJ6o64GaTa+szRXxmDYz7YxAXKioqJC0tTXN827Zt0tQU3d/hcC0vpk+fHvG5KioqxOl0ao6HQiF56aWXIj4ftL3xxhtROW9lZaXuuMvlkqqqqqjMvXfv3rDv93DrO96sWbN0x6PVxqirq0u2bdsWlXOLfBw00fPKK69EbW4Rkba2Nt3AxcyZM6M6PwAAgIjIjKn2sD2Q77mvcfK27d6BSwICAAAAcc71/jPFeuM5i755wGxLD0Rj7tyzvn9Qr8KB64MVgwzOqOL68LkxekdY0op6Cy68/+Dgzovj2cfMbk+feVWr0efta6yy+1r2aG9qiUjG3C9VGx0EyTvnh4cTnDlevWPc2/+sWxliOKTPuqbFbM/UrFDSvf+1/KC3w7C7Ol1bni7SXc/Ma2qNmivWOrc9lx3oabZpjVvSxvRmL/l2VJ6fvfS0bue4RZp3wHobdmT4WvZqru1kRiADcSHc5vJ7770X9TXU19dLZ6f2dcwJEyZEfK7Zs2frju/cuVOam2nhfKJqamqiVmVk2rRpuuPvvPOOoe09jqWqathWKOHWd6ykpCQpL9eufhcIBKL6OxatsEd2draMGaP9/y3V1dVRD3KJiOzatUtzrLCwUGw2vn8AAIDouvCClLAlwbp7QpbLv1A9+w8rXLo9twEAAIB403NwXapeef7EzLLuzHk3Ru1CoL14To+j9FTNTX3P4Y1ZEgpG3JOie++r6f7OOt2wdO6Zd+5XEpLipu3ESJVx6vVHonFe19Y/5euNmyy2QO4Zdxg+t2KxhTLnXFejd4y3eU+ar3lPXF2UVswWNW3KpZotQkL+PrNr85P6Jcsj5HfXJ/ZWr8/WGjdZk/0Zc744YjfoOrau0A2b5J5xx0dGVwo6VvaiWw5rj6ri3rU6K1pzj2QEMhAX9DaXVVWV999/P+prUFVVdu/WrohUWloa8blOOUWzpZyIyKBaTUDb3r17o3LehIQEGTdunO4xGzZsiMrckZ4/Oztb0tPTIzpXeXm5ZksNEZE9e/ZIT0/0qnO9//77EgwGDT9vuFDKpk2bDJ9zIHqfG4qiDOqzAwAAYCjOPze1Iz/PErb3c2dn0Pqt/6qffsayA7P/uKIjJxA4wUbGAAAAQAx0bl+lGypOn3ZZvZjMUduAFBFJOeVCzQ3cYJ870VPznmZg5HjuXWs0N4tFRBIzSnrSZ15teFWHk43ZnulLn3Z5WzTO3XPoXd1N5+TyJc0ma7LxF8VFJHPulxoVU4JOWEcV9+41cbcpnnnaTfWKzu9px4fP6wYNItW+8bECNRTQ3P9OPeX8BsViG5Fhp2CvK8FTtzVTa9xsS+tPn/n5qH52OMuWuBOcuZpVWnoOvaO5vpOZYeVfMDTvvfdeTO7gPtaRI1EJBA5ZYmKiFBVpf862trZGdbP4+Lm0FBREVnXMYrHoPh8Rkf379w9qXRjYoUOHonLewsJCSUjQ/ngMBAJSU6MbQj1hDQ0N4vF4xG7XbHMmxcXF0tHREfZcxcW61fyi9nM8yufzSUNDg241i6EoK9NtkyaHDx82dD4tLS36N6Tm5+fLnj17YrIWAABwcjKbRb35xszqu+9tqojk+Kod3oxv/lddxt33NPafNs/Zes7ZzrZzzkp15eWY/dFeKwAAADBYvYd1Nr8VRU2feVXU73ZPnnC2q/Hv39cc7z2yOcU+dkFXJOfqqX5bN5CRWnFJg16LFEQmZeLZTWJKMDyoE+xtS/C17k/ROyZt5lWNRs97lDk5z28vmdfWW/1ujtYxvdXvZsgZ/xVXbTmsWWV99pJ5rb3V6wdct6/tQEpP9bspzgh/jwamSmfVSp0NOkUy531Vs1JHvHPvWp2p6lTjcZYtbo7Ge/54jjGzXO49fx+wNU5f877UaM8/EhHIGGbr1q2LSfWHeFZSUqJ7935dXew+G91ut+ZYUlKSOBwO6e3t1T1HcXGx7vPp6+uLu1DMSBXNQIae2tpa8fuje61aVVWprq6Wigrta+pjxoyR7du3hz1XuIBQLIILhw8fNjyQMXbsWN3xWH12dHXpfz/MzCQQCgAAou9rX81ueGGlu3DXHq9uH+NjubuCif94zV34j9fchSL1Ulhg8VScktQ5fZq9a+5su3v+XEe3zabE/Z1D26s8aV/+yhH9MoVGU0SefKxYu1QaAAAADOF31yf2u2qcWuPWrPFdloxSX7TXYc0q70tw5ngDPS0DtoLwNu6MqEKGv6vJ4u+s121Xkj5z+YhtpxBP7GPmaPeoPwG9NRtTRA1pboqbEh3+lEnnhb+T8gSknnJBs14gI143xTNP/XKtViBDRMS18Yki59gFQ/7/LPeuNRl+d6PmXa62gikdtsIZ+pt8cazn0NsZeuMpk8+LSWUdW/GpnVqBjKCn3ep31ydaUgv7Y7GWkYJABoZdbq5+C2OTySSnnXZa1OY3mUxis338HaqkpET32NTU1LCBjKws/UpQBw8ejEr7hpORy+WKynnDbaDHqvJCuEBGpBv94d6T0a72cXSOhQsXGnrOvDztlnKqqkpZWVnY0MaJSEpK+iR8FQgENKuqRNpaBgAA4ESYzaL+/pExVedcdHCeuyuYOJRz1Df47fUNfvtrr3cXfHxORS0tsfRMnmRzz6m0uc9YnNxRcUpS2NYosVbf4HfUN7h1L2obTfn48iuBDAAAEFU5S761z2Sxx/RismPswqhsYg+Vp+Y93UoESVnlsSmvLSIWR7ZPK5Dh76zTLnV8DM8R/edjSc33WHMmabYDQOQcJaeeQKUFbX0NVZoBIRGRpOzx3dFuoWMvnqv73AK9rUnB3laL2ZEdV1UQU6dc2m5J/YFHKzTRvX9tXtDbsd9sSw8M5fyuzU/p3p2aPvMLI7Y6hohIX8te3c8PW+HMmHweJqYX9umN+1r22glkfBqBDAy7cJvFM2bMkBkzZsRoNfqsVmvYY8I9n7a2qLQsOymFC8cMVUaGbshQ2tvbozLvYOeJdKM/3POJpO3JiTJ6DovFIsnJ2qFzRVHkW9/6lqFzDlVi4pD2QwAAAAatvNza96enSrd+4UuHZ3UNMZRxrGBQVQ4e6k8+eKg/ec3f3UV339skmelm37Rpto6FC5zt1y7PaM7INA/pQhkAAADCy5p/c73ZkXVSf9/yNGzXrTyRmFUWs7vdTfZ0zQ1Gf29L+M0DEfE2VOk+n6TcyVEJEZxsTImOgDU3OsEWX/tB3TC4NSf6r6Etf6rHZEkKhvx9muXavQ1VDuf4M+MqYCWiSNq0K+ta3/nNhIFGQ35vgmvzk3nZi28bdHDC765P1KsaYk5K9mfMvnbkVp8JBZT+9mrNzw+TxRawZpXpBiWMYnHm6YYt+jtqI/o8PJkQyMCwS0uLuKLusItkY1Vvk1hExOOJuxvKRqRAIBC1tiFOp27ANWavoder/30x3DqPcjj0bxYMN48RjP6ZpaWliaKMjD6KkQS5AAAAjDL/VHv3318q2/TFGw/POFTdH1HZ5MFo7wha33yrJ+/Nt3ry7v+f5skLTnO2XP359PqLL0h1JSRI1HvVAgAA4OTi76gbsCLFUX3Ne5KbXrk7emVyjxHoaU7SGgt5OyO6COjvbNA8h4iINWtCzCp+jGZme5pPJDrXj/3dLbqvYVJ+RfRfQ5NZtaSP7fa17NHcYOt318Xlhens026qb9vwcLka9JsGGu/48PmioQQy2jc+VqCGAgOeU0Qk5ZQL6xWLLe5bcmrxd9YlqsF+zeenJFhDTa/8KCafhcE+t26+INDbwl2qxyGQgWGXlKT7tyuuHG1PoCdcaCNaVR1ONtEMEVgslmGb+1jh3iuRVl7QOy6awZZjGR3IGEkhB5NJ8zsSAABAVEyeZPW+vXbCph/d2zj2mec6Svv6QuH/R2YI+vtV0z/Xdef9c1133r3FiT133ZG3/4rPpVISEAAAAIYJ9OhXnuje/3p+9/7XY7UcTaGA9kbpsQI9TbrPJ8GZ4zNmRSc3k8URtcoygd5W3dfQkpIXk9fQ4sz0+Vq0xwPuhri8iG5OzvM7y89o6t73WsFA4762Ayk91e+mOMcuGESlEVU6q1bqtCtRJGv+V0d0uxJfx2HdzdSgtzOx9d3fjo/VevSE+r1RuQYxkrFLhGEXbvN7pAn3fKiQYQxVjd7Nd/HyGoYLfkQayEhI0M7exSpcYvQ8IymQAQAAMBxsNiX0s58WHNz8zoR3ll+eVmOxKFG9E6jmSL/zpm8cqTznooOzPtjm1S/RBgAAAEQo0Ns2Ii4E6t25fqxgX7fuxWdzUupJ3aLGKGarM2o/x1B/r+7N7uaktJi8hmZrsu48wb6uuL0pP2v+V2r1xl2bntAJV3yWe9eaDL+70a41biuY5krKnzaiN+f8XU0jpuqEGugjf3AcfiAYdqPt7vFQSP86p97mOOJDvLyG4SqyhFvnUXrhlUiqvhjB6J/ZaPvcAAAAiJbCAkv/Q78Zs2/rholvf/vW3L0TJ1jd0Zxv6weezAsvOzjv4cfaBrzbCQAAABiMUNA3Mi4EqqoiEXTwCwV8uhdkzXYCGUZQLLZgtM6tBgZutXGU2Rab11CxpujOEwr0xW2VAmf5UndiZlm31nj3vrV5Qa874vW7Nj+lG+DImHXNiK6OISKi9vfG7et5PFUNjYx+8zHEzjCGXX9/v+74tm3bpLW1NUar0dfR0RH2mHDPx+HgZrF4F66Fh82m27bQMHa7ZqBTRMK/147Sez42m00URYlqxZGj8xgp3HN3uVyydetWQ+ccqkOHDg33EgAAAKSwwNL/3Ttyjnz3jpwjO3b12f/8UmfO++970rfv9KZ7vSFDrw34fKr5Bz9pnFLf6Lfee3d+tZHnHkjFZFvnpZekNkR7HgAAAMSe6u8fMZuQkQhXScNsSyeQYYjo7QerIf1AhsmaErUwyKfmSbTrzqNG2EZnuGRUXlXbtPanpww0FvJ7E1ybn8zPXvzNsEEKv7s+sbf63RytcXNSan/6rGt0mruMDKHACAmnYUAEMjDswm2sbty4UdauXRuj1Zy4vr4+3fFwm+wYfj6ffou3WL2G4eYJt86j9N6TiqJIUlJS1FuXGP0zC/fce3t75ZFHHjF0TgAAgNFiakWSZ2pF3mERORwIiLJxU0/yund703fu8qbs3deXXN/gd4RCJ3YFU1VFHn6sbbzDZg5+946cI8asfGAlJYm9t/2/7BF/xxMAAABGP8WUoFv2WO33suka7xSz7t2Nqj82G+dquA16s/57bbhlzL2hseWtX00I9XsG3Kvu2PZcUSSBjPaN/1eghgKaP4uUUy5sUBKS4vpnEQk1RNWJkYxABoad261fMTcnRzPYFpfCVdGgQkb8C/eejNVrGG6ezs7OiM7jdruloEC7YrTNZhtxgYxIPjdiUfkDAABgpEtIEHXB6c6uBac7u47+W3d3yLz5fU/ylg88Ke9v7U3btt2b3tEZHFLv7gcebJ44c4ate9k5yeHLDQIAAADHURISdTdSM+Z++ZA50RGTigThhd8vNSVYdZ9PwNNhMWw5iApTQmJQ7w0X6uuIyd5ryNejO48pzkMIZltqMGXSeQ2dVSuLBxr3tX6U4ql+N8U+dkHXQOMfU6WzaqVOuxJFsubfNCrC++ZE/TY8ttxTOp3jl8ZFuwHH2NP5///jEMjAsGtra9MdH2mBjPb2dt3x4uIB/7YgjoQL1eiFG4xUVKTb9kxcLldE5wn3fPLz8yM+11AZ/TPzer3S29urGVqxWq2SkpISNrgBAACAz0pONgXPPMPZeeYZzk4ROSIism271/Hci515/3jNnVdX7484oRwKiXL7d+qmLF44ab3NpsT1BUEAAADEH1OCVXcTMn3G8iZ78ZyeWK3nRJks+puqwb5O9u3iXLiQUMDrjslrGPR16wcyLPotTeJB1mk31WkFMkRE2jY9UVQ8dsFurXH3rjUZfneD5t2g9qKZ7Un5FZ4TXWc8UMIEMsyOLF/esh9HvWUohobSRxh2zc3NuuMjLcDQ2NioO15YWEiVjDgX7j05duzYmKyjtLRUd7ylJbK2Z+GeT7h5jFBSUmL4OZuamnTHR9pnBwAAQDybMd3We/+9+Qe3b560/vePFm+tmGyLrFybiDQ1B2w//1UzX84AAAAwaAmOLN3exb72j2yxWosREpJzdHueB70uKmTEuQS7/nsy6HXFpkJGX5fue8WSnBdZz/NhZCua1WMrmKp5R2n3vrV5Qa/brDXu2vK07l2t6ZXXjIrqGCIilpSCfr3x/q6GEfVZeLIhkIFhd+jQIQnqFHgqLi6W7OzsGK7oxLS0tEhXl3YFJUVRpLy8PIYrwmDV1NTojmdlZUlKSkpU15CYmCiFhYW6x4Rb51FHjui37I5FICMacxw6dEh3vLKy0vA5AQAAIHLJhantb79evvmeH+bvtFqViO66WvFcR7G/X6XnLQAAAAbFkpKnG2Dwtx8eUZuQlpR83U3y/vZqY3s/w3Bmp34go695rzMW6/C5DuvOk5BaEPeBDBGRjFnX1mqNhfzeBNfmJ/MHGvO7GxJ7D72jWWLfbEvrT591TWR3tY4A1swy3b7zga7GEfVZeLIhkIFh5/P5pK5OP6Q20jZWDx48qDteUVERo5VEh6KM7uuoHR0dYVtdTJo0KaprmDRpkpjNmsFPCQaDYX9vjjp8+LDu+MSJEweztEHLzc2VtLQ0w8/70Ucf6Y7PmjXL8DkBAADwb1+/Kavh5RfGbXI6Tf5wx7a1B5Je/ltXZizWBQAAgNHDklGq227A07QzunfOGcyaPaFXb9zbvC85VmvB0FjTS3Tfk33Ne6L+Gvrd9YlBT7tV75ik7Am6G/jxIn3WNS1mW5pm9YeObc8NWAWjfeNjBWoooLnPnTrl4nrFbFGNWGM8sKQV9ZsstoDWeKi/1+Jr2UcoI04RyEBc2LZtm+743LlzY7QSY3z44Ye64wsXLoz7UIPPpx2eTEgY/W3sdu3apTu+cOHCqM6/ePFi3fH9+/dLf79uhapP1NbW6lZtKSwsjGqVjAULFkTlvNu3bxdV1f4+VVhYGLbKCAAAAE7MnNn2nkcfLN5mMknYC13/eLVr5JQ+BAAAQFxwjJmjfWFTRDyHN2ZJKBjfF9uPYS85Vff59LftTw16O0b/BfgRLCmvokdv3Ne2P+qBDE/Ne7pBJFOiI2DNGRmBDCUhKZQ65eJ6rXFf60cpnup3j3u+qnRWrdJpV6JI5twbNc85MilizZ6g+/nh3vVyVqxWg8EhkIG4sGnTJt3x6dOnj6iN1a1bt+qO5+TkyOzZs2O0mqHxerX/VicmJsZwJcOjqqpKd3z27Nlit0enepzVapV58+bpHhNufcdSVVV27type0y0QhMi0QuvtLW1hW1bcv7550dlbgAAAPzbsnOSO5adk9oQ7rjtOzzGl00DAADAqOYYe3qXYjJrhn+Dfe7Enup3RkyVjKTcUzzmpBTNO+3UUFBx73g5vivLKdqvh4hISB3drQpZR24QAAAgAElEQVTtY+Z064373U32aFcq6D6wLkNv3Jo1rltk5LwMWfNvrhPFpPm+atv0+09tELp3rcnwu+s1N2jsRZXtSfkVupVMhkoJ8/6PJlvBNN3S7j0H3iKQEacIZCAu7N+/X5qamjTHFUWRq6++OoYrOjFNTU2yd+9e3WOuvPLKuK6SoRfIsNvtYjKN7o+PLVu2SDCo3Q47MTFRli5dGpW5Fy9eLElJSbrHbNy4cVDnDHf8okWLolL5ZMKECTJmzBjDz3vUW2+9pTt+5plnSm5ubtTmBwAAwMe+c3tOdbhjjtT6ncHgCLoqCAAAgGFnsiYHbflTO/SO6dy+agRdAFTEXjKvTe+Izh0r82O1mqFQzBZVMVtCWuNqwDeqNw+sORO9Ccl5OtUnVHG9/8e8qC0gFFC69r6m+x5xlMxzRW3+KLDmTPQ6Sk7V/L3o3vdaftDr/qTHu2vL0zrVMUTSZ//H/2fvvsOkKu/+j3+mbZntHRbYpS5IlaogFhSUGI09scQSY0wsj0YTE+NPY4yJxhhb9DGxRIzRRBNLjI9GRVAQEVFEWVBAlrq9t5nZ3Wm/P1aUNrPtzJzZ3ffruua62J17zvnO7uww5z6f8727t957L1jinKFPHEkKej0Re/2nTlgc9r3DXfpxlt9V44jU/tF7A/pNEf1HMBjUa6+9FnbM3LlzNWPGjChV1Hdvvvlm2PvHjBmjk046KUrV9JzbHTo8aLfblZ09sIN2jY2N+vTTT8OO+fa3v620tDRD9xsfH69zzjkn7JiSkhLt3r27R9v96KOP1NbWFvL+rKwsLVq0qEfb7I5IB6mWL18eNjzkcDh02WWXxXT4CQAAYCCYNDHBPSzfEfYKJL8/aKmr89N+GQAAAD2SUrSoOtz9TZteHrbvydpYlzr+pJpw97t2rslpqyiOTHtmg1gdCSFPSgd97f3md9FbSQVz6sLd3/z5qxEL1TR99kqm310XH25M8riFYeuLRZmzL94T6r6A12Ov//DJIZLkbSqPc+1YlRtqrC0xoz1j+nlh3zP6whafEjaQEeiIXCAjpWhhoy0hLXSHHX+HtfrdP4YNq8AcBDIQM5YvX67GxsawY66++mrl5PSPZYffe+89VVeHf8+/+OKLNXLkyOgU1ENlZeGX1yooKIhSJeZ54403wt6flJSkCy64wNB9XnjhhcrMDNttTK+//nqPt9ve3q7ly5eHHXPuuecqPd24LtJHHXWUpk6datj2DsXj8ei///1v2DEzZszQ6aefHtE6AAAAIE2elBD+gE5SZZWXq3UAAADQIxmzL6mw2OJCdmQItLc6KpfePiqaNfVFxvTv1NgSUrwhBwQDlsqlvxkTxZJ6zGJ3+kLdF/A0DfjP/GlTTq8Kd39Hw+7kpo0vRWTpmbo1j4c9OWNPymlLGbsg7NIWsSh9ypl14TqPNKx/drgk1a15LD/o94Y8v502+bQyi80RsWVFbAmpIV/7kuRz1cZFat+y2oOpE08Ou1xo/YdPjfS3VA74v8H+hkAGYobH49Gzzz4bdkxaWpp++ctfdnnC2kijR4/WpEmTevw4n8/X5fOJi4vTTTfdpCFDIte9qrd27AjfcXjWrFlRqsQ869at6/LncPzxx2v+/PmG7G/+/PlavHhx2DG1tbVdLtMRyquvvhp2GZbk5GT9+Mc/ls3W9wDz0KFDdfnll/d5O93x4osvdhnmuuCCC7Rw4cKo1CNJiYmJOuGEE6K2PwAAgFiQlWkPeaXOXtXV3shNTgEAAGBAcqQO8aYWLaoIN6Zh3dMj3bs/TI5WTX1hcSQGUieeEvaKyJaty4Y2bXw5Iif0jWCNTw55UrqjuSwxmrWYIW3iKfX2pOzQLaklVbx+24Sg32to6+bmz17NdO1cE/aq5dSJ3yyX1RaxQELEWG3BjGlnh1xqpL1ma5p7x6rUxg0vhO4AYbEEs+f+IPzVxn1kc2aGDlNJ6qjfHtHuNjnzr94jizXk7zfQ3urY8+/rxkeyBvQcgQzElGXLlmnz5s1hx+Tn5+t3v/udxo+P7PvJtGnT9Ktf/Up33323ioqKerWNlStXatOmTWHHZGVl6Te/+U2v9xHOiBEjdO2118pu73lX4J07dyoYDP1/9nHHHaf8/Py+lBfzgsGgnn766bBjLBaLrr76as2ZM6dP+5ozZ46uvvrqLpfWeOaZZ8KGKsKprKzUW2+9FXbMlClTdPXVV8tq7f1/Dzk5ObrllluUnByd4x+Px6MlS5aEHWOxWPSjH/1I3/3udw0JnISSmZmpCy+8UI8++qjOPffciO0HAAAgFiU5bV1+ULXZLf1vYhAAAACmyz3+hh0Wqz1kl4yg32vd/c8fTPO7avrFleF5x9+wy2KPD/P5Oaiyl6+f4q3fGXZpCrPEZ49pCXWf39MUH6t1G8ZqC6ZP/3bIJTYkydu4J6nqrTsKjdploL3FVv7qjRPDjbFY7YHso64IGWqIdVlzf1hmsTlC/p3v+ff1k7xNZSEDD87hs+ricyeEXuPcAInDpruk0OdxPJWbUiK5//jcCZ6U8eEDai1b3syveedeli6JIQQyTJaenq68vDxTbkYuTWCUQCCgBx54QC6XK+y4vSGG733ve0pNTTVs/0OHDtWZZ56pe++9V7/85S81ZcqUPm0vGAzqoYcekscT/v0/IyNDv/nNb3T++ecrPr5vn1MsFosOO+ww/exnP9N9992nY445psuT/Ifi8Xi0bdu2kPc7HA7dfvvtmj9/vhyOfvEZt1c++eQTrV69OuwYh8OhG264Qeecc06PT/bbbDadddZZuuGGG7r8ORYXF+vdd9/t0fYP9Oyzz6q1tTXsmGOOOUY333yz0tLSerz9iRMn6o477lBeXl5vS+yVVatW6Z133gk7xmKx6IwzztBdd92lyZMnG7bv+Ph4zZs3Tz/5yU/0pz/9Saeffrqczphe4hEAACAiaut9XXa/SEmx9i5dDAAAgEEtYegUd9rUs7o8AV7yl9Nn+FuqozJhHWhvsVW/dUeBp3Rdj69Mc6QXtmcc/u3d4cb4PY1x25ecMdPbVBZzXeYShkwMGciQgmr49J+50avGHLnHXr/HGpcUtltC7XsPj23Z/HpGn3cW8Ft2PfPdyd6m8rATz6mHLS6Pzx4btnNHLHOk5Xckjzk25HIwHXUlYcMOmbO+G/Ewii0xw+dIG+IOdb97z7osv6cpcleFShp28h1fhA90SZXL7jys7v1Hh0ayjn21lryTVv3WHWGX0xnMen7ZPAx1xRVXmLbvdevW6Y477jBt/6FUV1frnnvu0U033RS2s4PVatUpp5yihQsX6t1339Xq1au1ZcsWtbe3d3tfKSkpGjVqlCZMmKAjjzxShYWGhRW/Ul1drfvvv1833nhj2GDE3hPzixYt0htvvKGVK1eqvDzsUlBfsdvtGj16tI488kgdddRRys7ONqT2d999V+PGjQt5f3p6uq677jq5XC5t375dpaWl8ng8crtD/l8kSdqyZYs+++wzQ2qMhscee0xFRUVhf65Wq1Xnnnuu5s+fr3/9619as2aNfL7QS4nZbDYdccQROvvss7v1umtubtZDDz0UtmtJdzQ3N+uRRx7RT37yk7Djpk2bpj/+8Y96/vnntWzZsi5/p/n5+TrjjDO0YMGCQ77OV65cqWOOOaZPtXfl0Ucf1dChQ7vsnjNq1Cjddttt2rJli5YvX64NGzaourq62/ux2WwaMWKERo8erZkzZ2r69Ol9DlIBAAAMBDU13i4/FBUMj+/+ARsAAACwj/xv/raktWRFrq+lMuSSGO3Vm9O++PPCIwrOfniDc9T85kjU4W0qi6tecV9B46f/GhFob3UkFsxa25vtDP3G7dtbvngrz9tUEfIke0fD7uRtfz7xiMLznlzvLJgd/kq7Hgh6Pdba9/6U37rjvcxR33thQ08f7xw2PUwgQ6pd/ejorFkXVdpShoQNLPRntsQMX868H5VUvXPPhFBjggGfdfdzl00v+M7j61MmLG7ozX6Cfq9l198vmtS6fVXYqyCtjkTf0JNuK+nNPmJJ1pHf39Oy9a0eBwlszqz29MO/UxOJmg4Un13UEurvNuhrt5W/+ouxI85+eEuk9u/IHNmee/T/fFH19h9CvvYUDFjKX71pSlv158n537yzxGJPCNl5pPeCatrwQnbNqodHeso/zXSOmFWXu/CmsEGzwYpABmLSp59+qgcffFDXXHNNlx0HEhIStGjRIi1atEh+v1/btm3Tzp071dzcrNbWVrW0tMhutyspKemrW1ZWlkaNGqWcnLBLbRnmo48+0pIlS3TppZd2OTY1NVXnnHOOzjnnHFVXV2vr1q2qqKhQQ0OD2traZLValZycrKSkJKWnp2vMmDEqLCyMyDIMy5Yt09lnn91lF5KkpCRNmTKl2x1FXnjhhX4VyGhubtYf/vAH3XbbbV2eeB8+fLiuu+46ud1ubdy4Ubt27VJdXZ3cbrecTqcyMjJUWFioKVOmKCkpqVv79/v9uu+++1RbW2vE09Hq1as1depULVq0KOy45ORkXXLJJTrvvPNUXFysrVu3qqqqSi0tLXI4HEpJSdHw4cM1ZcoUjR49OmTgaOvWrXrllVciHshob2/XnXfeqV//+tcqKOg6iDl+/Pivwhu1tbXavHmz6urq1NLSotbWVrW3tyspKUlOp1NJSUlKTU1VQUGBCgoKBnRXGAAAgN7wdgQtGza2hW3DmOS0+nJzbQN2QhYAAMBozZtfz7TGp5jSYSwhd7w70q3/e8qWmOEbcdaDG3Y+de6cYMAf8upHb1OZc/uSM49In3b27ryFN+9wpOV39HXffk+DvfGTf+U0f/5qrmvXBzlBv7fPHeit8Sn+Yd+6d+OuZ747O9zz8bVUJm7/y6lHZs6+eEfeCTfttCWm9fo10V5bktCw7qkh9ev+Ueh318XH5xQ19WY7KeNOaLQ6En0Br+eQ5xn9nob4rQ8dNzfn2Gu3ZUw/v6ovNcey3ON/tqdh48v5HbXbQp5ECXg99l1/v3hm9vwrt+WdcNMui83R7asu26s+T9zzzx9O8VRt6rLlfc78q7c5Mkf2+wB8StGJjXGZo1o66nf0aOmP9Mmnl/bkZ9sXyWOOrW0tWREyINP4yXOFvpbKhJyj/2dn8tgFvfob60ruCTfubt2xKsu1c02YE51B1X/41KjWkhU5ucf//IsMgwIr7p3vpzR8+nxey9alQ8ItIYOvEchAzFq1apXa2tp0/fXXd/vqc5vNtt9J1ljy6quvKhgM6tJLL+32EiK5ubnKzTWvs1dbW5uWLFmia6+91rQaYsUXX3yhu+66S7/4xS+6dTLe6XRqzpw5mjNnTp/26/f7de+992rDhh6HlMN67LHHlJOTo8MPP7zLsfHx8Zo1a5ZmzZrV4/3U19frnnvuUXJyj7v29UpLS4tuueUW3XjjjTrssMO6/bjs7GzNnz8/gpUBAAAMbK/8tzmzudkftpXy+KKEiExEAQAADFSlL13b9eRdhGTPu+KLoSf/ZodZ+w8leezxTUMW37ap4rVbJkuhz70GA35Lw/rnChs3vDgieeyCytTxi2pTihY2ONILunXC2u+qt7t3f5DiKl2X6t71QaZ7z4dZRoQwDpQy/sTG3AU/3Vy17K6wk5lBv9dat+bxMQ3rny1Im3RqWfq071Qmj5rXIqst7AnooN9r8ZR9nNxasiK9+bPXhngqN6YrGOz5GucHsDgSAyljj69q+vzVYaHG+Fw1CRWv3Ty58vVbJznSh7scacPd1jin32pPDBvOyD7qyt1GdgOJKKs9WPidxzaUPHrykaHCKVJnp4yalX8sair+d37mnO/vypx1YWW4kIqndF1y7epHhzdtenl4d153SSPn1eSe8PMB05kgY/p3SquW/a77E/wWazBr3uVlESxpP5nTz6uuWn7XhKCvPeSV0q0lK/JaS1bkWeOTvfGZo1ptzswOW1yyL9zfrM2Z7h122n1fdK8KiwrP+1vxtkdPmt1Rtz1seKWjfldy6fNXTq9admdr+qTTKpLHHlefNGp+c7cCLAG/xVOx0ekp/SjFXfpxWuv2FTnhuvrg0AhkIKZ99NFHuvnmm3XdddcpPz/f7HL67LXXXlN9fb2uuuoqOZ394/1q5cqVGjlypE477TSzSzHdp59+qttvv10//elPu+waYgS32637779f69atM3zbfr9fv//973XDDTdo+vTphm9fklpbW3XnnXeqtrY2aoGMvfu97bbbdPHFF2vx4sXdDkABAAD0Vy+93JR1+qlpdRbDp4e7x++X5f6HasZ0NW7WTGevWvQCAAAA+8qed0W5z1XnqFlx//hwoQypM8jQsuXN/JYtb+ZLkiN9mCsuvdBtTUj12hPTvbbEjA5/R4s90Nbs8Le12APtrXZvU6nT21zp7GrbRsld8LM9HQ27Exs+/sfIrsYG2lsdDR//Y2TDx/8YaY1z+hJyipodqcM81qSMDltcii/gddkDbc12n6fJ4W2uSOyo254S9HdE5Egh57if7Gze8np+uO4eUmc4pqN+V3JH/a5uTRKnTfpmtfpLIENSwtCp7vxTfr+x9N/XTlMwEPZn0dGwO7nyjVsnVS29/bCEnPHNcdljWm3OdK/VkeTztzU5vE1liW3VW1LDLctzIEf6CFfheUuKpYEzD5515GUVNSsfGBcu5LKvpMI5tfHZY9siXddetpRcb/qUM/c0rO/e36ynojijO9u1J+d5hp2mbgYyJFtSpm/0957/uOSxU2d3p1OFt2FPcs2qh8bVrHpIVkeCPz5nXLPNmd1uT0zzWhMzvFarLehra7H725ocgfYWu89VH+dt2JES8LYZ36J/kCGQgZi3fft2/fSnP9UFF1ygxYsXR2RpjnDq6+u1e7dxwcI1a9Zo165duuKKKzRp0iTDthtJTz31lMrKynTRRRdF9cR6LNq0aZN+/vOf65prrulRB4aeKikp0QMPPKCyssiFOtvb2/W73/1O3//+93XiiScauu3q6mr99re/VWlpqaHb7S6v16vHH39c69ev1/e//33l5YVdXs9wfr9fGzdujOo+AQDA4HXtDaVT77i7su2aK3NLvntuRnW0gxl33VM9YtNnni5b6J59Rnp1NOoBAADAwDdk0c277M5Mb+Ubv5rUVSBgX97GsiRvY1n31pKOouFnPrTVYnME6z98alR3HxPocNvdZZ9kquyTSJYWUuKwaa7MWZdsr1v7ly7D2QNdxszzqwMdrRvLX/t/k7sKZUidHTM8VZvSu7MUSTiO9GGuMZe9/JEtKdvXl+3EGltihi91wkkVjcX/HtGd8RkzL4z6iYihJ9++veWL5Xm+1qpuh2ciwZFe2D7mh29+sOPJs2a0V29O6+7jAt42m6e8e0ER9J1J188APdPe3q4nnnhCP/7xj/Xee+/J74/scmPt7e1auXKlfv3rX+vyyy83vENBRUWFbr31Vt1///0qLy83dNt7BYNBFRcX65577pHP1/f/i5ctW6arrrpKzzzzjGkn2WNFdXW1brnlFv35z39WfX29odtubm7WkiVLdOONN0Y0jLGXz+fTI488orvvvlt1dXWGbPPtt9/WDTfcEBOvk3Xr1umaa67Rk08+adjzC6ekpERPPPGELrvsMj3wwAMR3x8AAMBe23d0pPz4htLDZx+9Ze5fn67P8/ujc3nUgw/XDLvvwaou14wcXxTfNHN6Yr+5yg0AAACxL/uoK8sLv/v0h/bkPI/ZtRhh2Gn3fTH0G7dvtNjjI3sCxED5p9y5PWX8iZE5ydHPZM29vGL4mQ9+YnUkRiUckTh0SsOYH/z3Q0d6YbeW4elvsub+aE93xtmTctoypp1dG+l6DmRLzPCNvPDpj23OLNN//o7UId5xV7z5YfrUs3YPpE4pAwkdMiIoGAyqtTV255va2nrXvcftdod8Xh5PZD/3lJeX695771VWVpYWLlyoI444QoWFhX3ebjAY1K5du1RcXKwNGzZo06ZNam+P7HtoMBjUu+++q1WrVmn27NlasGCBpk+fLofD0ettBgIBbdu2TZ988olWrFihyspKAyvuXArixRdf1IsvvqicnByNGjVKBQUFKiwsVHJyspKSkpSQkKCEhATFx8eH3E5HR0e39ufxeEK+1lwuV6+eg1GCwaCWLl2qd955R8cdd5wWLFigoqKiXi+PsX37dr3zzjt66623Iv7aO5Q1a9Zo/fr1Ovnkk7V48WJlZ2f36PHBYFAffPCB/vOf/2jLli0H3R8IBMK+H3q93h7X3F0+n0+vvPKKXnvtNR1xxBE6+uijNW3atLCv0e5qbm7Wxo0bv3rvMPpvDgAAoKd27OxIuf7nZdPuvq/afcZpaWU/uCS7vKDAYfgHzJaWgO3mX5WPfua5hlHBbnRyvu5/ckuMrgEAAABIKTqxseh/Vr1f/uqNY5s2/ntET7pl9JXFag84C+fUJmQXGXZiJPuoK8udhXOay1++fqKnom/dE6LCaguOvPDvG6vfvrup5t2HxgU6XIP6vGPG9HNrEnInfLDnhasm96RbQU9YrLZg+ozzdg479Q/bLDZHdNbVMYGzYHZr4tBJjV39HaRPOa1MVrspP4fEYTNc465cuqb0xR9PaN2+Mrptug9gcSQFRnz70c2pExbXVLx52wRvY2lUOwHZU4Z4UiecVBXNffYng/qNMdLa2tp08cUXm12G4W6++WazS1BdXZ2ee+45Pffcc8rNzdWECRM0duxYFRYWKjs7W1lZWQcFG4LBoFwulzwej6qrq1VWVqaysjKVlpZqx44dampqMuW5BINBrV27VmvXrlViYqImTpyo8ePHa8yYMcrNzVVOTk7I51JXV/fV89i5c6c2btwYtRBQTU2NampqtHbt2ojt4+67747Yto3i9Xq1dOlSLV26VDk5OZo8ebImTpyo4cOHKy8vT2lpB3/mamlpUVVVlcrKyvT555+ruLg4Jk7kt7e366WXXtLLL7+sSZMmafr06SoqKtLw4cOVkpJy0NjS0lKVlpaquLhYH3/8cdi/od27d5v+fuj3+7V69WqtXr1acXFxmjBhgsaNG6fRo0crLy9P2dnZBz1PqfO5ejweNTU17fe+sWfPHu3evVvB7pyBAAAAiLKKSq/z4Udqx/35sdqxU6c4G765OLXyzG+l14wc2bdwRkOD3/7Xp+uHPPxIzZi6Bn+3Eq6zZjjrzjkzPWJXLDU2+uJWvdeaGqntd+WoucnN0V4mBgAAAF+zJWX6Rnz70c3ZR121p2r570a1bnt7SNDvjdgntPicoqb0yadVZMy+pMKROsTwK82cw2e1jr1qxdq69x8bWrP64VHehj2GryNuc2a1p006tSx77g8MaNNsUe6Cn+3JmnNZRe37Dw9r+uy1Ie3VW9OkwTlvmjjscFfR1SvX1qx8YFjt+4+O9rlqEozatnPE7NqhJ936hXPk3BajthnLMmZ8d4/n1V+EDGRYrLZg5pGXR77VeBiO9ML2UZe+9Kl75+qU2jWPD28tWZHn9zTGmVVP2tQz61Innbq69t0Hh9V99GRhJJdossYl+VLGHVeVMeO7ZSnjFzXSnSM0Ahno96qrq1VdXa2VK1fu932Hw/HVFfBer9eUrgM95fF4tG7duoOWSNn3uXR0dHS7wwSiq6amRm+//bbefvvt/b6fnPz152WXyxXzJ/ADgYCKi4tVXFz81ffsdrsSEjo/N3o8nogvGxRpHR0d2rBhgzZs2HDQfUlJSV91OukPvy8AAIBwAgFZPvnUnfnJp+7M395VqWH5DtfMw50NU6YkNk+flthy2Ph495AhjpCTyA0NfvuHH7uTi4vbUla+15L1wYfubK832O3J7ZRkq/eRBws2GvNsDm31GlfOad/ekRPJfYRTVjL5rYQES8Cs/QMAgP7JmpDui88e22x2HaE40ob0fELdaguGe06O1F5sswcSh01zjbzwHxu9jXu21n3wRH7rtmW5nsrP0xQM9Oksoc2Z1e4smFWXPPro+rSJJ9dFZ4kIi7LmXl6RNfcHFU0bXshu+PT5oa7tq3ICXk+vz+vZU4Z4kkceWZtcdGJt+tQza43urmBLyvTlLbx5V97Cm3f5PU02T9m6ZE/pJylt1VuSg16Xzd/htgfaW+3+DrdNgdCBGWtCereW/EjIGtsS8LcfcjtxGSPcvX0ehrDagjnHXV+aPf/K8rq1S4Y0bXhpqLtsfWZvXou2hBRv8tjjKzOPuLQsedT8iL5nxGeParUlpBzy+NCMn6nFkRD2OMtZcERtfPaY3i0HYDDnyHktBSPnfS7p8/babQnuPetS2iqKk71NZYkBr9sW6HDbg163zd/eGvJv2J6cY8h7i8XmCOYcd31pznHXlTZt/HdW06cvDWndsSrH39bUp6CIxWoLJuQd1pg0cm598rjj65LHLGgayF1ajPTVH/5ZZ531UDAYvMrMYgAAALrjwT9sfzkh3p9odh0AAMSCgqJNC1yuQK/XPoyLswQy0m3tCQlWv8NhCXR0BK1tbQGbyx2w93W7f3288KMTT0hp7O02JOnEU0pmrlvvzurLNiKpN4GMDz9yJy8+rWReuDGvvzxm9exZzthdBxUAgCh66INrdzz/2RmjzK4D/Y/fVeNoLXk31b3nw7T2+h1JvuaKBJ+7Lj7Q7rIHA36rggFZ7Al+a5zTb3Uk+myJ6d649OHu+Oyxrvi8Ca6EIVNc8dljY+KEb9DXZnXteC+1defqtLaqz1N8zeWJPldtfKC91RH0+yyyWL98Lgk+a1yyz5Ga70nIHuOKzx3vco44ojlh6CRzQwqDnL+l0tG8bXm6a9cH6R11O5K8zRWJgbZmR8DrsSnot8piC1jjU3yO5Kx2e+owd8KQiS3Jo+Y3Jo2a3zxYT3qXPLJ4hnvPhyHXVx9+9sPrMw7/Tk00a+q3An6Lu/TjJNeuNWltFcUp3uayRG9zVUKgvdUR8Lr3ew1aHYk+m8PptyVnt+gqRyIAACAASURBVMdljXYl5E5oTcyb4E7In+ayxqf076t1o8UavLb4puzNe7+kQwYAAAAAAINYR0fQWlXtMzTo6HRafQ/fN/yTvoYxAAAAgL6wJeV406aeWZc29cw6s2vpK4s9IZA87oTG5HEn8Bm7H7KlDPFmTD+/JmP6+QQIuqG9tiTBXbouZDDfnpTTljH1rIgtjTngWG1BZ8HsVmfBbEL/JiCQAQAAAAAADFNYENf6yIMjNtDdAQAAAADQG3VrHh0WbomX9KlnlMpqH5SdQ9D/dHvdVwAAAAAAEHueeaJw3QXnZuwckmf3mFlHQoLVf9klWSXvLS9aQxgDAAAAANAbQb/X0rjhpeGh7rdYbcHsuT8si2ZNQF/QIQMAAAAAgH7s6PnJzUfPT26WtHX1GnfKv15sGLLi3dac3Xs6koNRuF4oNdXWcerJqeU/vTZvV0GBoz3yewQAAAAADFQN6/+R63fXxYe6P2nkUdWOzJEce6LfIJABAAAAAMAAMe9IZ8u8I50tkr6orPQ6li5vzXjv/dbMdevdGTt2diQHgwrZ8rUnMtJt7UfOSa49eXFKzRnfSq9NTLQEjNjuoYwYEeduavY7IrX9vrJZ1ePYi9NpDYwdE9/c1ZjeVwUAAAAA/VP9B48Xhrs/e97lu6NVC2AEAhkAAAAAAAxAQ4Y4vBeen1F94fkZ1ZLU0hKwFW9qc36+uS1p6xftSSU72pKqa/wJbrff5nYH7K2ugN3jCdhtNkswLs7idzgsgbRUmzctzdYxJM/eNrIw3j1+XLxr7pFJzePHxUdteZS//GnE59HaV7RMmpjg/mBl0Rqz6wAAAACAWNK06ZVMT8Wm9FD3x2UWtqZMWNwQzZqAviKQAQAAAADAIJCSYvXv00EDAAAAAIDYEfBZqpb+pijckMyZF+2WMY0fgaixml0AAAAAAAAAAAAAAGDwqnzz9pHttdtSQ91vT8ppy573w/Jo1gQYgUAGAAAAAAAAAAAAAMAUNSvuH17z3v+ODTcme96PtlsciYFo1QQYhSVLAAAAAAAAAAAAAABREfR6rB31O+NbSt7JaFz/3DBPRXFGuPGOtKHu7PlXlUWrPsBIBDIAAAAAAAAAAAAAAIbb8cRp01q3r8rr/RYsGnrSrzZbbI6gcVUB0cOSJQAAAAAAAAAAAACAmJM26dTStKln15pdB9BbBDIAAAAAAAAAAAAAADElLnts87DT791qdh1AXxDIAAAAAAAAAAAAAADEjLjMwtbRl768zpaY4TO7FqAv7GYXAAAAAAAAAAAAAACALJZg+pSz9gw77Q9fWONT/GaXA/QVgQwAAAAAAAAAAAAAgGkcacPcyWOOrc456so98XmHecyuBzAKgQwAAAAAAAAAAAAAgOEy51xSmjzm2LoDv2+xJwRsznSvPTHT5xx2eKstZYjXjPqASCOQAQAAAAAAAAAAAAAwXNrkMw4KYwCDidXsAgAAAAAAAAAAAAAAAAYaAhkAAAAAAAAAAAAAAAAGI5ABAAAAAAAAAAAAAABgMAIZAAAAAAAAAAAAAAAABiOQAQAAAAAAAAAAAAAAYDACGQAAAAAAAAAAAAAAAAYjkAEAAAAAAAAAAAAAAGAwAhkAAAAAAAAAAAAAAAAGI5ABAAAAAAAAAAAAAABgMAIZAAAAAAAAAAAAAAAABiOQAQAAAAAAAAAAAAAAYDACGQAAAAAAAAAAAAAAAAYjkAEAAAAAAAAAAAAAAGAwAhkAAAAAAAAAAAAAAAAGI5ABAAAAAAAAAAAAAABgMAIZAAAAAAAAAAAAAAAABiOQAQAAAAAAAAAAAAAAYDACGQAAAAAAAAAAAAAAAAYjkAEAAAAAAAAAAAAAAGAwAhkAAAAAAAAAAAAAAAAGI5ABAAAAAAAAAAAAAABgMAIZAAAAAAAAAAAAAAAABiOQAQAAAAAAAAAAAAAAYDACGQAAAAAAAAAAAAAAAAYjkAEAAAAAAAAAAAAAAGAwAhkAAAAAAAAAAAAAAAAGI5ABAAAAAAAAAAAAAABgMAIZAAAAAAAAAAAAAAAABiOQAQAAAAAAAAAAAAAAYDACGQAAAAAAAAAAAAAAAAYjkAEAAAAAAAAAAAAAAGAwAhkAAAAAAAAAAAAAAAAGI5ABAAAAAAAAAAAAAABgMAIZAAAAAAAAAAAAAAAABiOQAQAAAAAAAAAAAAAAYDACGQAAAAAAAAAAAAAAAAYjkAEAAAAAAAAAAAAAAGAwAhkAAAAAAAAAAAAAAAAGI5ABAAAAAAAAAAAAAABgMAIZAAAAAAAAAAAAAAAABiOQAQAAAAAAAAAAAAAAYDACGQAAAAAAAAAAAAAAAAYjkAEAAAAAAAAAAAAAAGAwAhkAAAAAAAAAAAAAAAAGI5ABAAAAAAAAAAAAAABgMAIZAAAAAAAAAAAAAAAABiOQAQAAAAAAAAAAAAAAYDACGQAAAAAAAAAAAAAAAAYjkAEAAAAAAAAAAAAAAGAwAhkAAAAAAAAAAAAAAAAGI5ABAAAAAAAAAAAAAABgMLvZBQAAAAAAAAAAAAAAgPDq1z4xpGr5H8b5XDUJ8TnjmtMnfasiY/YlFY7UIV6za8OhEcgAAAAAAAAAAAAAACCGtW57O63slZ9NUTBokaT26i1pVdV3p1WvuK/IWTinNuPwb5enTzun1mJPCJhdK75GIAMAAAAAAAAAAAAAgBjW8Mm/huwNY+wrGPBZXTtW57p2rM4tf+1mb2rRosqMmReUJ49d0GRGndgfgQwAAAAAAAAAAAAAAGKYxWoPdjUm0N7qaCx+aURj8UsjHOnDXGkTT63InPO98vjssW3RqBEHs5pdAAAAAAAAAAAAAAAACC1z1oXlFquty1DGXt7GsqTa1X8eu/WBI4/e9vDxs2rfeyjf72mgYUOUEcgAAAAAAAAAAAAAACCGOQtmt2bMvGBnjx8YDFo85Z9mVvz31smf3zXp2J1/O3dK08aXshTwHbT8CYxHAgYAAAAAAAAAAAAAgBiXf/IdJa3b3snpaNid3JvHB33ttpYtS4e2bFk61ObMak8Zu6Aqa87FZc6R81qMrhWd6JABAAAAAAAAAAAAAECMszgSA8NOu3eTLNZuL10Sit9dF9+44fmCksdPnbv5nsPnVb5+6yhv4544I+rE1whkAAAAAAAAAAAAAADQDySPXdCUMf07u4zcprdhT3LNqofGbbl35rHbHz9lev3aJ/OCXg9ZAgPwQwQAAAAAAAAAAAAAoJ8Y+o3fltgSM9qN3m4w4Le4dr6fU/afn0z77M6i43b+7bzJTRtfzpL63JBj0CKQAQAAAAAAAAAAAABAP2FLTPPnHHVlSST3Eehw21u2vJm/+9lLZ26+e+r8ild/Mbq9enNiJPc5EBHIAAAAAAAAAAAAAACgH8k55pqyuIyC1mjsy9tU7qx9/9GxW/941NFb7z/iyOq37ijwt1Q6orHv/o5ABgAAAAAAAAAAAAAA/YnVHsw9/sYvor3b9tptqVXv3DPh8z8cfuyOJWdOa1j/bE7Q77VEu47+gkAGAAAAAAAAAAAAAAD9TMb079TE505oMmPfQb/X2lqyIq/0haumf/67w44pfeHqIvfuD5PNqCWWEcgAAAAAAAAAAAAAAKAfyp73w51m1+D3NMQ3rP/HyJJHF8/bfM/h8ypfv3WUt3F3vNl1xQICGQAAAAAAAAAAAAAA9EOZM86vdqQNdZtdx17ehj3JNaseGrfl3lnHbH/sm9PrP/jLkKDXM2hzCYP2iQMAAAAAAAAAAAAA0K9Z7cHMWRftMruMAwUDfotr15qcsld+NvWzO4uO2/m38yY3bXw5SwqaXVpU2c0uAAAAAAAAAAAAAAAA9E72vCvKa1Y9PDbQ3uIwu5ZDCXS47S1b3sxv2fJmviN1qDtt4jcrMmdfUh6fd5jH7NoijQ4ZAAAAAAAAAAAAAAD0U9b4FH9q0cJKs+voDm9zhbN2zeNjtj44/+it9x9xZPVbdxR4mytjMkhiBAIZAAAAAAAAAAAAAAD0Yxkzzy83u4aeaq/dllr1zj0Tttxz+LHbH//m9Pq1T+YF/V6L2XUZiSVLAAAAAAAAAAAAAADox5LHHt/kSMt3e5vKnWbX0lNBv9fq2rkmx7VzTU7l0ts7UiecVJE588Jy58i5LWbX1ld0yAAAAAAAAAAAAAAAoJ9Lm/jNCrNr6Cu/pzGuYf1zhSWPnzJ38z2Hzyv/v5+Paa/ekmh2Xb1FIAMAAAAAAAAAAAAAgH4u/fDzKs2uwUjehj3JdWseH7P1wfnzSx5ZPKNuzeNDgl5Xv8o4sGQJAAAAAAAAAAAAAAD9XOKwaS57cp7H11rVbztKHFIwYHHv+TDbvefD7Mo3fuVLGn10dcb0cyvSJn+rTrKYXV1YBDIAAAAAAAAAAAAAABgAkgqPqGva9J/hZtcRKQGvx96y5c38li1v5tuTsttSDltckX3E98sShk51m13bofSrdh4AAAAAAAAAAAAAAODQksctqDO7hmjxuWoTGj56etQX/7tg/rYHj55T8849I/yuGofZde2LQAYAAAAAAAAAAAAAAANA2mGn1FmstqDZdUSbp+qz9Mq37jjs899POXbHkjOnNax/Nifo95q+nglLlgAAAAAAAAAAAAAAMADYkjJ9jozC1o667Slm12KGoN9rbS1ZkddasiKv/JWfeVPGnVCVMeO88pTxJzaaUQ+BDAAAAAAAAAAAAAAABojE3AnNgzWQsa9Ah8vRtOk/w5s2/We4I32EK3XCSZVZc75XHp87wROtGliyBAAAAAAAAAAAAACAASIhf0qz2TXEGm/jnqS6NY+P2frg/PnbHjxmTs2Ke4f7PU22SO+XDhkAAAAAAAAAAAAAAAwQzuEzW8yuIWYFgxZP1aZ0z9JN6dXv3FuUNObY6swZ55WnTvhGg6y2oNG7I5ABAAAAAAAAAAAAAMAA4SyY0yKLJahg0GJ2LbEs4PXYWza/nt+y+fV8e1JOW+rEb5ZnzrqoInHYNJdR+yCQAQAAAAAAAAAAAADAAGGNT/HbEjM7/O66eLNr6S98rpqE+g+fHF3/4ZOj47PHNqdPPq08Y/YllY60/I6+bNciyfC2G0Css9lsslqtZpcBAOilOIffq0GY601Osvosg/B5AwD6L4fDEoiPt/rNrgMAgJ5Iclp9NpuFefMYUtaQ2dbiz00wuw4AiFkWW9Aa5+TYC/txl36cGWhvcZhdR39msdoDzoJZdelTzynPmHFujcWeEOjyQdbgtcU3ZW/+ahsikAEAAAAAAAAAAAAAAHBI1rgkb8q4E6oyZpxXnjJ+UaNCXTVKIAMAAAAAAAAAAAAAAKDnHOnDXakTFldmzfleeXzuBM9+dxLIAAAAAAAAAAAAAAAA6AOLJejMn9aQNu3s8ozp51fZEtP8BDIAAAAAAAAAAAAAAAAMYrE5Akkj59XIottat614TJJPIpABAAAAAAAAAAAAAABglFJJ90m6j0AGAAAAAAAAAAAAAACAsW4lkAEAAAAAAAAAAAAAAGCsKqvZFQAAAAAAAAAAAAAAAAwwiQQyAAAAAAAAAAAAAAAAjPVPliwBAAAAAAAAAAAAAAAwhl/S3yVdaTe7EgAAAAAAAAAAAAAAgH7uM0n/krRE0i5JIpABAAAAAAAAAAAAAADQc/WSnpf0N0mrDryTQAYAAAAAAAAAAAAAAED3eCS9rM4QxpuSfKEGEsgAAAAAAAAAAAAAAAAIb506QxhPS6rrzgPsXz5osMgwuwAAAHooXpLT7CJMlibJ2o1xTZICEa4FANA93X3vBgAAAAAAAGLZbkn/kPS4pG09fbDF8HIAAACMtVNSYTfGFUn6IrKlAAAAABigHJKSzS4CgCTpCkm/7ca4xyX9LMK1ALEuRXTDBxDaG5LGml1EP9Uk6T+SnpK0TFKwtxviTRoAAMS67n7Q4UpsAAAAAL3lldRgdhEAJEnubo7rEH+3AH8DAMLJM7uAfsYr6XV1LknyiqQ2IzZKIAMAAMS67i5DQucvAAAAAAD6v+5emME8AAAAoY1UZxcddO1jdXbC+IekaqM3TiADAADEOiZiAAAAAAAYPJgHAACg72aaXUCMK5f0vKQnJa2P5I4IZAAAgFjHRAwAAAAAAIMH8wAAAPTddLMLiEFt6lyK5G+S/ivJF42dEsgAAACxjokYAAAAAAAGD+YBAADouxlmFxAjApLe19dLkrREuwACGQAAINYxEQMAAAAAwODBPAAAAH1jEUuWbFZnJ4ynJe02sxACGQAAINYxEQMAAAAAwODBPAAAAH0zTVKu2UWYoE7Ss+rshrHW5Fq+QiADAADEOiZiAAAAAAAYPLo7D2CNaBUAAPRfJ5ldQBR1SHpTnSGMl7/8OqYQyAAAALGOQAYAAAAAAINHoJvjmAcAAODQBkMg4zN1hjCWSKo2uZawCGQAAIBYRyADAAAAAIDBg3kAAAB6L0nSPLOLiJAySS9IekLSpybX0m0EMgAAQKxjIgYAAAAAgMGDeQAAAHrvZEnxZhdhoBZ1hjCekrRC3e+kFTMIZAAAgFjHRAwAAAAAAIMH8wAAAPTehWYXYAC/pGXqDGG8JMltbjl9QyADAADEOiZiAAAAAAAYPJgHAACgd3IkLTa7iD74XNI/JT0paaeplRiIQAYAAIh13W1BZo1oFQAAAAAAIBoIZAAA0DvnS3KYXUQP1Ut6XtLfJK0yuZaIIJABAABiHRMxAAAAAAAMHswDAADQOxebXUA3tUl6RZ1LkrwuyWduOZFFIAMAAMQ6JmIAAAAAABg8mAcAAKDnjpM03ewiurBOnZ0wnpFUa3ItUUMgAwAAxDomYgAAAAAAGDxYuhQAgJ77qdkFhLBdnSGMv0kqMbkWUxDIAAAAsY5ABgAAAAAAgwfzAAAA9MxkSSebXcQ+miT9R51LkixT9/9vH5AIZAAAgFjHRAwAAAAAAIMH8wAAAPTMDTL//0W/pLfV2QnjBUkuc8uJHQQyAABArGMiBgAAAACAwYN5AAAAum+8pPNM3P8n6uyE8XdJVSbWEbMIZAAAgFjHRAwAAAAAAIMH8wAAAHTf3ZIcUd5nhaR/SfqrpI+jvO9+h0AGAACIdUzEAAAAAAAweDAPAABA9yyQdGqU9uWR9JI6lyRZqs4lStANfGABAACxbraktG6MWyepIcK1AAAAAACAyMqTNOUQ3/dKat3n60ZJJVGpCACA2GOVtFbSzAjvZ52kRyU9K6k5wvsCAAAAAAAAAAAAAAAw1Y/U2VUqErctkm6WVBi1ZwMAAAAAAAAAAAAAAGCykersVmFkCKNB0lOSFopVNgAAAAAAAAAAAAAAwCBjkfSGjAlh+CQtlXSRJGc0nwQAAAAAAAAAAAAAAEAsuVJ9D2KslfQ/krKjXDsAAAAAAAAAAAAAAEDMGSmpRb0LYeyWdKekw6JdNAAAAAAAAAAAAAAAQCy7XT0LYbRI+qukEyRZTagXkuxmFwAAAAAAAAAAAAAAAMLqzhIjAUnLJT0l6UVJrohWBAAAAAAAAAAAAAAA0M+dotDdMDZJulHScNOqAwAAAAAAAAAAAAAA6KdukdSmzhBGtaQ/SpplakUAAAAAAAAAAAAAAAADQIqkkZJsJtcBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBEWcwuAAAAxJQ4SZ/v8/UkSW0m1QIAAAAAAMx1q6T5X/77Hkmvm1gL+qfhkpZ8+e9GSeeYWAsAAAAAAKaKlxTc55ZobjkAAAAAAMBEL+jrOYJLTa4F/VORvn4NVZlcCwAAUWc1uwAAAAAAAAAAAAAAAICBhkAGAAAAAAAAAAAAAACAwQhkAAAweMR9eYv1bQIAAAAAgMiLxPkBzjkMLryGAADoAv+xAQAweFwvaaOkkwza3rGSPv5yuwAAAAAAoP/Ik/SZpIskWQzYnlPSryS9adD20D/8WtIrkgoN2t4MSaslfcOg7QEAAAAAEBVDJDVJCn55e0VSwSHGxe8zJigp8RBjhkp6SlLgyzEtkoYZXzIAAAAAAIiQp/T1sf87kiaFGPfCPuMuDTHmVEk79hl3jpGFImaNluRR5+/cpc5ATvwhxhXp69dGVYhtZUh6QJLvy3FbQ2wLAAAAAICYdIakVu0ftmiW9BNJjn3GhQtkONTZDaP5gDGtks6MbPkAAAAAAMAg2ZJqtf+xfYekuyQlHTA2XCBjrKT/HrCdoKT/i1ThiCm/0MG/+02SFhwwLlwgwyrpBzr06/H4SBUOAAAAAEAkZEv6naR27X+Qu0XSoi/HhApkHC1pwwH3eSU9Iik/OuUDAAAAAACDpKizo4Fb+x/rl6lzGZO9DhXISPzysZ4DHrtH0uWSbJEuHjFjrqSVOjiY8YqkEV+OCRXIOFydy5Ps+7iApH9KGheF2gEAAAAAiIgidR7c7l1yZN+D5TEHfG+4OkMXB45dKmlKtAsHAAAAAACGGqbO436v9j/uXy5pog4OZJwqafsBY+sk/VyHXvYUg8NCScU6uKPqryRN1v6BjHTtvzzJ3tsqSUdFuW4AAAAAACLmSEkrdPAyJuG+XvHl4wAAAAAAwMAxSdJ/tP8cQJukyn2+3nXA/W51duLMMKFexB6HpCskVWj/18m+rxuPOkMZ+97/iaTFJtQLAAAAAEBULJS0Xge3l9z39pmkc8wqEAAAAAAARMWhLt448OZXZ+fNkeaUiBjnVGfHlEaFfx3tFkvcAAAAAAAGCZukG3XoA+RfiIPjWJSkzlaxf5D0X0nbJNWrs81s4Mt/75L0lqQHJX1HXLUEAAAAAOieMyTV6OA5gvfU2U0D6EqOpCd18GvIJ+kGSfGmVQYAAAAAQBQlqfPKhSYdOpDhUmcL0jSzCsR+5kj6uzrbfIa70uRQN6+k/5O0SJIl2oUDAAAAAPqFSZJe0aGPKzskPSIpz7TqBofR6gy/hLuNMq26rjnU2f3iwOVJ9t42q7MTK3MTAAAAAIABa+/B8YFre4a61aozuMEVDOYYo84wRU9DGKFuqyVNj+ozAAAAAADEshHqDFv41PUxZYs6L95IMaXSgW+Kuv4dTDatutAs6gxabFP35ibWSDrGlEoBAAAAAIiQvQfHW7X/QXClpMsO+N4PdHBgY6u4iiHaLlfvOmJ0dfNJukX8LgEAAABgMMuSdK+kNu1/zPh/6gzz7/36SR08l1CmzrkEljo1Vn8MZCyUtE7719igzqVW935dL+l+dXbw3Hfci5ImRL9kAAAAAACMNVfSu9r/oNcv6Sl1TsDEH3BfojqXKnlABx8sr5W0ILrlDzpWSf8r44MYB96eF51PAAAAAGCwSVRnJ8wG7X+MWCrpoi/HvLDP9y9VZ7fNa9XZIWPfx7AEhbH6UyBjkqR/av/aAuqca8qTVLTP96u+fMxUSasOeMze+amhUawdAAAAAABDHKZDr/964LIVhwpk7HW49r8yZt8rZg6LbPmDkkXSnxT5MMbe28vqnFgDAAAAAAxsVnWGJ3Zo/+PCDnVekJG8z9gDAxl7DVfnyfMDjy3fF0tQGKE/BDJCLXGzWZ3dMvY6VCBD6pz3uEidHVv3fbxLncvhpEW2fAAAAAAAjHO89j+4rVPnFS3WA8aFC2RIXx8sVx0w7qRIFT6I3aaeBSrKJL2qzgmxJyX9W9L2Hm7j8Wg8MQAAAACAqTIkVWv/48HlkiYeYmyoQMZep+rgY88XjS950OkPgYwbtH89rZJ+JSnugHGhAhl7pf//9u49WI6yzOP49xxySEgCgcASFJIgiQExkCB35Kpmragocr+JCxgsLFxhiQuCFrisSFGLiTdYFY1ylQC1WNxkUagNJshlI4IKLELIpojcEiAk5JiTnNk/OqeS3c3pt2fm7e6Znu+nagoq8/b7/IaZKurtPO/bJI1AGzd29AOH5BFakiRJkqS83MqGIyP/ZpAxoYaMARsvlm+NG1PANJKjOkM3X/pIdqNMTZlrAnA5yY2RLE0Zp216GkmSJElShcxgQ3N/2jow1JAByb2DS4HVwDvAe6Kl7Fzt0JDRAzy9PsudJCdmbEqoIWPAxiezzokXU5IkSZKkYowF9g2MydqQMWAfYFzz0bSRUcBfCN94eZrkBk1WOwL/kWHetxn8JookSZIkqRq6gXOBEYFxWRoyBkwETmo+mmiPhgyAg4EjAmOyNmRA8rv8HDCm+WiSJEmSJLWeehsyFN8swjddHqWxZ6kOAW7LMP/cpj6BJEmSJKkq6mnIUDzt0pCRRT0NGZIkVc7/fW68JEmSyvNe4JzAmMXAdOCtBuZfC5wMPBwYdxzJLhdJkiRJkiRJktQgGzIkSZJax0ySUywGUwNOAZY1UWMNyRGyKwPjvtJEDUmSJEmSJEmSOp4NGZIkSa1hDHBaYMz1wPwItRYDVwTGTCc5IlWSJEmSJEmSJEmSJDWpC9h7o5fNm8WZSfqzYdcCEyLWGw68Hqg5K2I9SZIkSVL7OYjksZbHAe8pOUsn2YP09XoNmFxauvpsyYbf0CdLziJJkiRJkjrUQtJvtNyWQ81vBmq+TPojVCRJkiRJUnxVasiQJEmSJEkq1a6Eb7QcWVLdj+RQV5IkSZIkDc6GDEmSKsIdj5IkZbM9cHpgzFLg+hwzDAPOATZLGfMO8H2gP8ccim9a4P0VwH051H0WeAKYmjLmo8CvcqgtSZIkSe3s4ySPgkxzJ9CbY4b9gXGBMQuAl3LMIEmSJEmSFMV1hHcnnJRj/asz1D8jx/rKzx2kf6/35lh7dqD2EznWliRJkqR2dSbhNfr3cqw/GVgVqP84MDTHDMqPJ2RIkiRJkjrOCOBPpC+G3wR2yaH2sYG6NeDnOdRV/rqA5aR/t1/LsX7ot9UPjM6xviRJkiS1q7I2bmS5P/E2yWMq1Z5syJAkSZIkdaTJJI8FSVsQPwZsHrHmeGBZoOZzwFYRa6o4EwjfZDkix/o7ZKj/4RzrS5Ik9AFHygAADQNJREFUSVK7ytIY8QbxN278LFCzBpwcuaaKZUOGJEmSJKljfZ7wovhbkWr1AA8HavUCe0Wqp+IdQ/r320dyky9PzwUyzMy5viRJkiS1q6I3bpweqFUDrolUS+WxIUOSJEmS1NFuIH1R3A8cFaHO7ECdGnB2hDoqz9dJ/36fLyDDLwIZrisggyRJkiS1q6I2bkwCVgTqPAVsEaGWymVDhiRJkiSpo40EniF9Ybwc2LmJGh8naexIq3FrE/OrNYSeOfxAARm+F8jwmwIySJIkSVI7y3vjxjDgiUCNlcD7mqih1mFDhiRJkiSp4+1J+FjSR2jsWNKxwOuBuZ8HRjX1CdQKHiL9e/5pARn+MZDhpQIySJIkSVI7y3vjxrWBuWvAaQ2nV6uxIUOSpIoYUnYASZLa2JMkf5H93ZQx+wGXARfUMe8Q4GZg25QxfcApwFt1zNsqLgB2KTtEZJcDixu8dnzg/SUNzluPUPZ3kezG6i0giyRJkiS1o5XA8cBvGfyRIdsAtwCHAGvqmPsE4MzAmDm05+Mm9wc+VnaIyBYA95UdQpIkSZKkqriJ8LGkn6xjvisD89WAcyNlL8MCwp+v3V77NPHfozcw94wm5s7qwECGGrBTATkkSZIkqd2dQ3h9dUUd800k2YyRNt+zwJZx4hfu7yl/TR/7dVWE/y6ekCFJkiRJ0npbkzw+JG2RvAwYl2Gu6SQNHGlz3QV0Rf0ExbIhY4MRGeY+usG56zExQ44pBeSQJEmSpCqItXFjKLAwMNdq2nu9ZkPGptmQIUlSRXSXHUCSpAp4k+T40LTjRkcDN5D+uLAdSY4XTWu2WAJ8lmThrfaX9liaAatzT5GtxujcU0iSJElSNXwBWJTyfhfJI0ZCGzdmAXsFxnwR+H32aJIkSSqSDRmSJMXxOHBhYMwhwCWDvLcZSTPGdinXrwVOIjltQ9WwVYYxvbmnyNaQMSr3FJIkSZJUDVk3blzP4Bs3jgXODtSZC1xbdzpJkiQVxoYMSZLimQ3cERhzETBtE3/+deBDgWsvBuY3kEuta/MMY1rlhIwsWSVJkiRJiccIb9w4lE1v3JhAuNHiz8CMBnJJkiSpQDZkSJIUTw04A3gxZUw3yaNL3r3Rnx1B+CbNL4F/aSacWlKrNGT0En4Mjg0ZkiRJklSfRjZu9JDcN0g7pfCvJCdwrGgqnSRJknKX9hx7SZJUvzdIboo8xOB/gb09cCPwEZJHlNxI8siSwbwCnA70x4tZqq8C25YdIrLnG7yuJ8OYIh5ZUiO5oTcsZYwNGZIkSZJUn4GNG1OBnQcZM7BxYy9gKclmjAMC854PLIwTsXTzgPPKDhHZf5YdQJIkSZKkqvsyyY2XtNelwK8DY9YChxUbXQU6lPDvZFJBWVYHcpxZUA5JkiRJqpr9SJrg09ZcDwKfItmMkTbutoKzqxx7EL5fMLm0dJIkSZIklawL+AXhxXPo9bWig6tQBxD+DUwpIEcX4Zt+pxaQQ5IkSZKqKsvGjbWB918Ati46uEphQ4YkSZIkSQGjgRdpvBnjQdIfZaL29wHCv4PQUbUxbJEhx/EF5JAkSZKkqmp248Ya4MDCU6ssNmRIklQR3WUHkCSpwpYDJwJ9DVz7KnAKsC5qIrWaNRnGbJF7imw1smSVJEmSJG1aDTgdWNzg9RcCD8eLI0mSpCLYkCFJUr5+C1xa5zX9wGeApdHTqNW8lWHMsNxTZGvIeDP3FJIkSZJUbY1u3LgHmBU/jiRJkiRJUvvrBn5J9mNILysnpkowgvDv4egCckzMkGNKATkkSZIkqRNcRPZ7BEuA7cqJqRL5yBJJkiRJkuownuTxI6HF9DxgSEkZVY7VpP8mziogw4GBDDVgxwJySJIkSVIn6CY5UTO0DusDDi4po8plQ4YkSRXhI0skSSrGRWT7/+5KksYNdY7XAu+PLSDDuMD7NWBZATkkSZIkqRPsSbZTCJcBz+ScRZIkSTmyIUOSpPwdR/ZTDqYD5+eYRa1nceD9ULNEDOMD7/8F6C0ghyRJkiRV3UjgZmBYhrFjgOuArlwTSZIkKTceiS5JUr4mAD+q85pvAgvWv6poDsnRm1XyGeDpBq9dRPoRtEU0ZIRO4VhUQAZJkiRJ6gTXALvVMX468A/AVfnEKd3fAp8tO0RkdwM3lR1CkiS1BhsyJEnKz1DgFmBUndcNAX4OTAWWxw7VAnYF9i47RGQjmrg21OzQCo8seaGADJIkSZJUdZ8DTm3guoGNGw/HjdMSdgNOLjtEZC9jQ4YkSVrPR5ZIkpSfK2m88WAs8DM8lrQTPBl4fzzNNXxksXvg/VBGSZIkSVK69wPfbvDaHpKNG6PjxZEkSVIRbMiQJCkfRwFfDIx5KfD+J4AvxImjFva7wPtDgP1yrL8DMDEwZmGO9SVJkiSp6oaTnKA5PGXMKqCW8v444IcxQ0mSJCl/NmRIkhTfWOBa0k+3eA7Yk/Bxo1cBH4iUS61pEfBGYMwHc6x/cOD9GvBEjvUlSZIkqeq+S3JCRpoZwOzAmGNw44YkSVJbsSFDkqS4hpAcI7ptyphe4HhgOXDi+n8OZijJLpqtYgVUy6kB8wJjymzIeJL036gkSZIkaXAnAGcExlwN3AxcQHjjxreAvSLkkiRJUgFsyJAkKa7LgYMCY85lw4kD/w2cFRg/EY8lrbpfBd4/ENg8p9qHBd6/P6e6kiRJklR1WdbzTwEz1/97H9k2bszFjRuSJEltIe0odUmSVJ/pwN2k///1NuC4Tfz51cDZgfnPBH7SWLSWsi8wquwQkT0KrGji+l2BZwJjjgTuaqLGpkwCng2MmUa4YUSSJEmS9L8NJTntIu00i1XAPvz/9eCxwK2B+W8had5od7sDB5QdIrKngMeanGMPkhMrQ2P+0GQdSZIkSZLawg7AyySPnxjs9TyDNyIMBRYGrl8N7JnbJ1DZfkf69z83h5rfCNR8meQxPJIkSZKk+nyf9PVWDTgt5fprMlz/d/lEVwvYg/D3P7m0dJIkSZIkFagb+DXpi+Q1hHd8TATeCszzB2B49E+gVjCT9O9+LbBLxHrDgdcCNWdFrCdJkiRJneIYwn+ZHjoBcxjhjRsrSU6YUPXYkCFJkiRJ0nr/RHiRfF7GuU7IMFfo+bNqTzsAvaR/9z+OWO/CQK1+PJFFkiRJkuo1DlhG+nrrv4AtM8z1XpLHY7pxo/PYkCFJkiRJEnA4yckFaQvku4GuOuacE5ivBpwaJb1azQ9J/97XAQdFqDMOeDtQ654IdSRJkiSpk/QAC0hfa60GptYx54mB+WrAD+LEVwuxIUOSJEmS1PG2B5aSvjheAmxX57wjgD8G5n0b2LXpT6BWMwnoI/27fxEY3USNHmB+oEYNOLSJGpIkSZLUia4ivNY6q4F5f5ph3lOai64WY0OGJEmSJKmjdQP3kb4w7gMObnD+9wOrAvP/nuSZsqqW7xC+6fIoMKqBuXuA2zPMf3tTn0CSJEmSOs/HSB79mLbWmtvg3COAPwXmduNGtdiQIUmSJEnqaBcTXhhf1GSNszLU+HaTNdR6tgFeIfzdPwNMqWPenYB5GeZdBezc/MeQJEmSpI6xE/Aa6WutP9NYY/2AyYQ3bjwODG2ihlpHloaMw4F3lfDqye9jS5IkSZIEBwBrSF8UPwBsFqHWDYE6NeDTEeqotUwH1hH+7tcCPwb2S5lrN+BKwjfuBl5nRP80kiRJklRdQ4CHSF9nrSG5l9Cszwfq1IDZEeqofFkaMsp67Z/j55YkSZIkdbhtgBdJX5i+QrJjIIaRJCchpNV7A080qKJ/pr4bIq8C9wM3AdcD9wBL6pxjTiGfTJIkSZKq4xuE11pfiljvxkCtfuCoiPVUDhsyJEmSJEkdpwu4g/RF6TpgWuS6ewO9gbqPAJtHrqtydQH/SnE3VO7Fo20lSZIkqR5HkJxcmLbWuotkfRfLSODZQM3luHGj3dmQIUmSJEnqOOcTXpRellPtczPUviKn2ipPN3A1+d9MuR0YVtBnkiRJkqQqGAMsJX2ttQTYNofaewN/DdR+BOjJobaKYUOGJEmSJKmj7Ev4Zsc8kmfH5qEL+LdA/X7gyJzqq1wzgHeIfxOlD/gqcXdrSZIkSVLVdQP/Tni99cEcM5wXqF8DLs+xvvJlQ4YkSZIkqWNsDbxA+mJ0OTA+5xzbAIsCOV4F3p1zDpVjF+BO4t1AmQ9MKfQTSJIkSVI1XEJ4zfWVnDNk2bixDvhozjmUDxsyJEmqCHdDSpIUdjHw6cCYS4C7C8hyEPCdwJgHgS8XkEXl2Af4EnA0MLzOa/uAe0l+Qw+Q3EiRJEmSJGW3L3APsFnKmPnAp0hOsszT6PW1xqSMeZXkpI5lOWdRXO8DflN2iEFMAxaWHUKSpHZhQ4YkSVJ72gI4DDgc2B2YBGwHjCQ5Pncl8DrJqSp/JLlJdz+wooSskiRJkiRJkiR1nP8BL5lddiQu6vYAAAAASUVORK5CYII=" - } - }, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![SEI.png](attachment:SEI.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In our simple SEI model, we consider a one-dimensional SEI which extends from the surface of a planar negative electrode at $x^*=0$ until $x^*=L^*$, where $L^*$ is the thickness of the SEI. Since the SEI is porous, there is some electrolyte within the region $x^*\\in[0, L^*]$ and therefore some concentration of solvent, $c^*$. Within the porous SEI, the solvent is transported via a diffusion process according to:\n", - "$$\n", - "\\frac{\\partial c^*}{\\partial t^*} = - \\nabla^* \\cdot N^*, \\quad N^* = - D^*(c^*) \\nabla^* c^* \\label{dim:eqn:solvent-diffusion}\\tag{1}\\\\\n", - "$$\n", - "where $t^*$ is the time, $N^*$ is the solvent flux, and $D^*(c^*)$ is the effective solvent diffusivity (a function of the solvent concentration).\n", - "\n", - "On the electrode-SEI surface ($x^*=0$) the solvent is consumed by the SEI growth reaction, $R^*$. We asssume that diffusion of solvent in the bulk electrolyte ($x^*>L^*$) is fast so that on the SEI-electrolyte surface ($x^*=L^*$) the concentration of solvent is fixed at the value $c^*_{\\infty}$. Therefore, the boundary conditions are\n", - "$$\n", - " N^*|_{x^*=0} = - R^*, \\quad c^*|_{x^*=L^*} = c^*_{\\infty},\n", - "$$\n", - "We also assume that the concentration of solvent within the SEI is initially uniform and equal to the bulk electrolyte solvent concentration, so that the initial condition is\n", - "$$\n", - " c^*|_{t^*=0} = c^*_{\\infty}\n", - "$$\n", - "\n", - "Since the SEI is growing, we require an additional equation for the SEI thickness. The thickness of the SEI grows at a rate proportial to the SEI growth reaction $R^*$, where the constant of proportionality is the partial molar volume of the reaction products, $\\hat{V}^*$. We also assume that the SEI is initially of thickness $L^*_0$. Therefore, we have\n", - "$$\n", - " \\frac{d L^*}{d t^*} = \\hat{V}^* R^*, \\quad L^*|_{t^*=0} = L^*_0\n", - "$$\n", - "\n", - "Finally, we assume for the sake of simplicity that the SEI growth reaction is irreverisble and that the potential difference across the SEI is constant. The reaction is also assumed to be proportional to the concentration of solvent at the electrode-SEI surface ($x^*=0$). Therefore, the reaction flux is given by\n", - "$$\n", - " R^* = k^* c^*|_{x^*=0}\n", - "$$\n", - "where $k^*$ is the reaction rate constant (which is in general dependent upon the potential differnce across the SEI)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Non-dimensionalisation" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To convert the model into dimensionless form, we scale the dimensional variables and dimensional functions. For this model, we choose to scale $x^*$ by the current SEI thickness, the current SEI thickness by the initial SEI thickness, solvent concentration with the bulk electrolyte solvent concentration, and the solvent diffusion with the solvent diffusion in the electrolyte. We then use these scalings to infer the scaling for the solvent flux. Therefore, we have\n", - "$$\n", - "x^* = L^* x, \\quad L^*= L^*_0 L \\quad c^* = c^*_{\\infty} c, \\quad D^*(c^*) = D^*(c^*_{\\infty}) D(c), \\quad \n", - "N^* = \\frac{D^*(c^*_{\\infty}) c^*_{\\infty}}{L^*_0}N.\n", - "$$\n", - "We also choose to scale time by the solvent diffusion timescale so that \n", - "$$\n", - "t^* = \\frac{(L^*_0)^2}{D^*(c^*_{\\infty})}t.\n", - "$$\n", - "Finally, we choose to scale the reaction flux in the same way as the solvent flux so that we have\n", - "$$\n", - " R^* = \\frac{D^*(c^*_{\\infty}) c^*_{\\infty}}{L^*_0} R.\n", - "$$\n", - "\n", - "We note that there are multiple possible choices of scalings. Whilst they will all give the ultimately give the same answer, some choices are better than others depending on the situation under study." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Dimensionless Model" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "After substituting in the scalings from the previous section, we obtain the dimensionless form of the model given by:" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Solvent diffusion through SEI:\n", - "\\begin{align}\n", - "\\frac{\\partial c}{\\partial t} = \\frac{\\hat{V} R}{L} x \\cdot \\nabla c - \\frac{1}{L}\\nabla \\cdot N, \\quad N = - \\frac{1}{L}D(c) \\nabla c, \\label{eqn:solvent-diffusion}\\tag{1}\\\\\n", - "N|_{x=0} = - R, \\quad c|_{x=1} = 1 \\label{bc:solvent-diffusion}\\tag{2} \\quad\n", - "c|_{t=0} = 1; \n", - "\\end{align}\n", - "\n", - "Growth reaction:\n", - "$$\n", - "R = k c|_{x=0}; \\label{eqn:reaction}\\tag{3}\n", - "$$\n", - "\n", - "SEI thickness:\n", - "$$\n", - "\\frac{d L}{d t} = \\hat{V} R, \\quad L|_{t=0} = 1; \\label{eqn:SEI-thickness}\\tag{4}\n", - "$$\n", - "where the dimensionless parameters are given by\n", - "$$\n", - " k = \\frac{k^* L^*_0}{D^*(c^*_{\\infty})}, \\quad \\hat{V} = \\hat{V}^* c^*_{\\infty}, \\quad \n", - " D(c) = \\frac{D^*(c^*)}{D^*(c^*_{\\infty})}. \\label{parameters}\\tag{5}\n", - "$$\n", - "In the above, the additional advective term in the diffusion equation arises due to our choice to scale the spatial coordinate $x^*$ with the time-dependent SEI layer thickness $L^*$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Converting the Model into PyBaMM" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As always, we begin by importing pybamm and changing our working directory to the root of the pybamm folder." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import pybamm\n", - "import numpy as np\n", - "import os\n", - "os.chdir(pybamm.__path__[0]+'/..')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "A model is defined in six steps:\n", - "1. Initialise model\n", - "2. Define parameters and variables\n", - "3. State governing equations\n", - "4. State boundary conditions\n", - "5. State initial conditions\n", - "6. State output variables\n", - "\n", - "We shall proceed through each step to enter our simple SEI growth model." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 1. Initialise model" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We first initialise the model using the `BaseModel` class. This sets up the required structure for our model. " - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "model = pybamm.BaseModel()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 2. Define parameters and variables" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In our SEI model, we have two dimensionless parameters: $k$ and $\\hat{V}$ aswell as one dimensionless function $D(c)$ which are given in terms of the dimensional parameters, see (5). In pybamm, inputs are dimensional so we first state all the dimensional parameters and then define the dimensionless parameters in terms of them. To define the dimensional parameters, we use the `Parameter` to create parameter symbols. Parameters which are functions are defined using `FunctionParameter` and should be defined within a python function as shown. " - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "# dimensional parameters\n", - "k_dim = pybamm.Parameter(\"Reaction rate constant\")\n", - "L_0_dim = pybamm.Parameter(\"Initial thickness\")\n", - "V_hat_dim = pybamm.Parameter(\"Partial molar volume\")\n", - "c_inf_dim = pybamm.Parameter(\"Bulk electrolyte solvent concentration\")\n", - "\n", - "def D_dim(cc):\n", - " return pybamm.FunctionParameter(\"Diffusivity\", cc)\n", - "\n", - "# dimensionless parameters\n", - "k = k_dim * L_0_dim / D_dim(c_inf_dim)\n", - "V_hat = V_hat_dim * c_inf_dim\n", - "\n", - "def D(cc):\n", - " c_dim = c_inf_dim * cc\n", - " return D_dim(c_dim) / D_dim(c_inf_dim)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now define the dimensionless variables in our model. Since we solve for these, we do not need to write them in terms of the dimensional variables. We simply use `SpatialVariable` and `Variable` to create the required symbols: " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "x = pybamm.SpatialVariable(\"x\", domain=\"SEI layer\", coord_sys=\"cartesian\")\n", - "c = pybamm.Variable(\"Solvent concentration\", domain=\"SEI layer\")\n", - "L = pybamm.Variable(\"SEI thickness\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 3. State governing equations" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can now use the symbols we have created for our parameters and variables to write out our governing equations. Note that before we use the reaction flux and solvent flux, we must derive new symbols for them from the defined parameter and variable symbols. Each governing equation must also be stated in the form `d/dt = rhs` since pybamm only stores the right hand side (rhs) and assumes that the left hand side is the time derivative. The govenering equations are then simply" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "# SEI reaction flux\n", - "R = k * pybamm.BoundaryValue(c, \"left\")\n", - "\n", - "# solvent concentration equation\n", - "N = - (1 / L) * D(c) * pybamm.grad(c)\n", - "dcdt = (V_hat * R) * pybamm.inner(x / L, pybamm.grad(c)) - (1 / L) * pybamm.div(N)\n", - "\n", - "# SEI thickness equation\n", - "dLdt = V_hat * R" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Once we have stated the equations, we can add them to the `model.rhs` dictionary. This is a dictionary which stores the right had sides of the governing equations. Each key in the dictionary corresponds to the variable which the equation is being solved for." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "model.rhs = {c: dcdt, L: dLdt}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 4. State boundary conditions" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We only have boundary conditions on the solvent concentration equation. We must state where a condition is Neumann (on the gradient) or Dirichlet (on the variable itself). \n", - "\n", - "The boundary condition on the electrode-SEI (x=0) boundary is: \n", - "$$\n", - " N|_{x=0} = - R, \\quad N|_{x=0} = - \\frac{1}{L} D(c|_{x=0} )\\nabla c|_{x=0}\n", - "$$\n", - "which is a Neumann condition. To implement this boundary condition in pybamm, we must first rearrange the equation so that the gradient of the concentration, $\\nabla c|_{x=0}$, is the subject. Therefore we have\n", - "$$\n", - " \\nabla c|_{x=0} = \\frac{L R}{D(c|_{x=0} )}\n", - "$$\n", - "which we enter into pybamm as " - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "# electrode-SEI boundary condition (x=0) (lbc = left boundary condition)\n", - "D_left = pybamm.BoundaryValue(D(c), \"left\") # pybamm requires BoundaryValue(D(c)) and not D(BoundaryValue(c)) \n", - "grad_c_left = L * R / D_left" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "On the SEI-electrolyte boundary (x=1), we have the boundary condition\n", - "$$\n", - " c|_{x=1} = 1\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "which is a Dirichlet condition and is just entered as" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "c_right = pybamm.Scalar(1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now load these boundary conditions into the `model.boundary_conditions` dictionary in the following way, being careful to state the type of boundary condition: " - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "model.boundary_conditions = {c: {\"left\": (grad_c_left, \"Neumann\"), \"right\": (c_right, \"Dirichlet\")}}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 5. State initial conditions" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There are two initial conditions in our model:\n", - "$$\n", - " c|_{t=0} = 1, \\quad L|_{t=0} = 1\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "which are simply written in pybamm as" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "c_init = pybamm.Scalar(1)\n", - "L_init = pybamm.Scalar(1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "and then included into the `model.initial_conditions` dictionary:" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "model.initial_conditions = {c: c_init, L: L_init}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 6. State output variables" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We already have everything required in model for the model to be used and solved but we have not yet stated what we actually want to output from the model. PyBaMM allows users to output any combination of symbols as an output variable therefore allowing the user the flexibility to output important quanities without further tedious postprocessing steps. \n", - "\n", - "Some useful outputs for this simple model are:\n", - "- the SEI thickness\n", - "- the SEI growth rate\n", - "- the solvent concentration\n", - "\n", - "These are added to the model by simply entering" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "model.variables = {\"SEI thickness\": L, \"SEI growth rate\": dLdt, \"Solvent concentration\": c}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can also output the dimensional versions of these variables by multiplying by the scalings in the Non-dimensionalisation section. By convention, we recommend include in the units in the output variables name so that they do not overwrite the dimensionless output variables. We also `.update` the disctionary so that we add to the previous output variables." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "L_dim = L_0_dim * L\n", - "dLdt_dim = (D_dim(c_inf_dim) / L_0_dim ) * dLdt\n", - "c_dim = c_inf_dim * c\n", - "\n", - "model.variables.update({\n", - " \"SEI thickness [m]\": L_dim, \n", - " \"SEI growth rate [m/s]\": dLdt_dim, \n", - " \"Solvent concentration [mols/m^3]\": c_dim\n", - " }\n", - " )\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And thats it, the model is fully defined and ready to be used. If you plan on reusing the model several times, you can additionally set model defaults which include: a default geometry to run the model on, a default set of parameter values, a default solver, etc." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Using the Model" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The model will now behave in the same way as any of the inbuilt PyBaMM models. However, to demonstrate that the model works we display the steps involved in solving the model but we will not go into details within this notebook." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "# define geometry\n", - "geometry = pybamm.Geometry()\n", - "geometry.add_domain(\"SEI layer\", {\"primary\": {x: {\"min\": pybamm.Scalar(0), \"max\": pybamm.Scalar(1)}}})\n", - "\n", - "def Diffusivity(cc):\n", - " return cc * 10**(-5)\n", - "\n", - "# parameter values (not physically based, for example only!)\n", - "param = pybamm.ParameterValues(\n", - " {\n", - " \"Reaction rate constant\": 20,\n", - " \"Initial thickness\": 1e-6,\n", - " \"Partial molar volume\": 10,\n", - " \"Bulk electrolyte solvent concentration\": 1,\n", - " \"Diffusivity\": Diffusivity,\n", - " }\n", - ")\n", - "\n", - "# process model and geometry\n", - "param.process_model(model)\n", - "param.process_geometry(geometry)\n", - "\n", - "# mesh and discretise\n", - "submesh_types = {\"SEI layer\": pybamm.Uniform1DSubMesh}\n", - "var_pts = {x: 100}\n", - "mesh = pybamm.Mesh(geometry, submesh_types, var_pts)\n", - " \n", - "spatial_methods = {\"SEI layer\": pybamm.FiniteVolume()}\n", - "disc = pybamm.Discretisation(mesh, spatial_methods)\n", - "disc.process_model(model)\n", - "\n", - "# solve\n", - "solver = pybamm.ScipySolver()\n", - "t = np.linspace(0, 100, 100)\n", - "solution = solver.solve(model, t)\n", - "\n", - "# Extract output variables\n", - "L_out = solution[\"SEI thickness\"]\n", - "c_out = solution[\"Solvent concentration\"]\n", - "x = np.linspace(0, 1, 100)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Using these outputs, we can now plot the SEI thickness as a function of time and also the solvent concentration profile within the SEI. We use a slider to plot the concentration profile at different times." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0f7f4e8850354ea3a745351e85077a75", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t'), Output()), _dom_classes=('widget-interact',))" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "def plot(t):\n", - " f, (ax1, ax2) = plt.subplots(1, 2 ,figsize=(10,5))\n", - " ax1.plot(solution.t, L_out(solution.t))\n", - " ax1.plot([t], [L_out(t)], 'r.')\n", - " plot_c, = ax2.plot(x * L_out(t), c_out(t, x))\n", - " ax1.set_ylabel('SEI thickness')\n", - " ax1.set_xlabel('t')\n", - " ax2.set_ylabel('Solvent concentration')\n", - " ax2.set_xlabel('x')\n", - " ax2.set_ylim(0, 1.1)\n", - " ax2.set_xlim(0, x[-1]*L_out(solution.t[-1]))\n", - " plt.show()\n", - " \n", - "import ipywidgets as widgets\n", - "widgets.interact(plot, t=widgets.FloatSlider(min=0,max=solution.t[-1],step=0.1,value=0));" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Formally adding your model" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The purpose of this notebook has been to go through the steps involved in getting a simple model working within PyBaMM. However, if you plan on reusing your model and want greater flexibility then we recommend that you create a new class for your model. We have set out instructions on how to do this in the \"Adding a Model\" tutorial in the documentation. " - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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.7.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/notebooks/expression_tree/broadcasts.ipynb b/examples/notebooks/expression_tree/broadcasts.ipynb index 41580784c6..8401a89eee 100644 --- a/examples/notebooks/expression_tree/broadcasts.ipynb +++ b/examples/notebooks/expression_tree/broadcasts.ipynb @@ -261,7 +261,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.3" + "version": "3.6.9" } }, "nbformat": 4, diff --git a/examples/notebooks/expression_tree/expression-tree.ipynb b/examples/notebooks/expression_tree/expression-tree.ipynb index 9bcfeb3194..bf28c8fe2e 100644 --- a/examples/notebooks/expression_tree/expression-tree.ipynb +++ b/examples/notebooks/expression_tree/expression-tree.ipynb @@ -64,7 +64,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We can also calculate the expression tree representing the gradient of the equation with respect to $t$ (which is of course simply the scalar value 1)," + "We can also calculate the expression tree representing the gradient of the equation with respect to $t$," ] }, { @@ -84,7 +84,7 @@ "![](expression_tree2.png)\n", "\n", "\n", - "...and evaluate this expression, which will again give 1." + "...and evaluate this expression," ] }, { @@ -95,7 +95,7 @@ { "data": { "text/plain": [ - "1.0" + "array([[-11.]])" ] }, "execution_count": 4, @@ -104,7 +104,7 @@ } ], "source": [ - "diff_wrt_equation.evaluate(1, np.array([2]))" + "diff_wrt_equation.evaluate(t=1, y=np.array([2]), y_dot=np.array([2]))" ] }, { @@ -202,6 +202,13 @@ "\n", "After the third stage, our expression tree is now able to be evaluated by one of the solver classes. Note that we have used a single equation above to illustrate the different types of expression trees in PyBaMM, but any given models will consist of many RHS or algebraic equations, along with boundary conditions. See [here](https://github.com/pybamm-team/PyBaMM/blob/master/examples/notebooks/add-model.ipynb) for more details of PyBaMM models." ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -220,7 +227,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.3" + "version": "3.6.7" } }, "nbformat": 4, diff --git a/examples/notebooks/expression_tree/expression_tree2.png b/examples/notebooks/expression_tree/expression_tree2.png index 949690cfc0..09f6e2900e 100644 Binary files a/examples/notebooks/expression_tree/expression_tree2.png and b/examples/notebooks/expression_tree/expression_tree2.png differ diff --git a/examples/notebooks/models/DFN.ipynb b/examples/notebooks/models/DFN.ipynb index e32c93e5ee..d837a6c558 100644 --- a/examples/notebooks/models/DFN.ipynb +++ b/examples/notebooks/models/DFN.ipynb @@ -18,7 +18,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The DFN comprises equations for charge and mass conservation in the solid the solid and electrolyte, and also prescribes behaviour for the electrochemical reactions occuring on the interface between the solid an electrolyte. For more information please see [1] or other standard texts. \n", + "The DFN comprises equations for charge and mass conservation in the solid the solid and electrolyte, and also prescribes behaviour for the electrochemical reactions occurring on the interface between the solid an electrolyte. For more information please see [1] or other standard texts. \n", "\n", "Below we summarise the dimensionless form of the DFN, with all parameters give in the table at the end of this notebook. Here we use a roman subscript $\\text{k} \\in \\text{n, s, p}$ is used to denote the regions negative electrode, separator, and positive electrode, respectively.\n", "\n", @@ -97,7 +97,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Below we show how to solve the DFN model, using the default geometry, mesh, paramters, discretisation and solver provided with PyBaMM. For a more detailed example, see the notebook on the [SPM](https://github.com/pybamm-team/PyBaMM/blob/master/examples/notebooks/models/SPM.ipynb).\n", + "Below we show how to solve the DFN model, using the default geometry, mesh, parameters, discretisation and solver provided with PyBaMM. For a more detailed example, see the notebook on the [SPM](https://github.com/pybamm-team/PyBaMM/blob/master/examples/notebooks/models/SPM.ipynb).\n", "\n", "First we need to import pybamm, and then change our working directory to the root of the pybamm folder." ] @@ -151,32 +151,21 @@ "cell_type": "code", "execution_count": 3, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# set mesh\n", "mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts)\n", "\n", "# discretise model\n", "disc = pybamm.Discretisation(mesh, model.default_spatial_methods)\n", - "disc.process_model(model)" + "disc.process_model(model);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The model is now ready to be solved. We select the default DAE solver for the DFN. Note that in order to succesfully solve the system of DAEs we are required to give consistant initial conditions. This is handled automatically by PyBaMM during the solve operation.\n" + "The model is now ready to be solved. We select the default DAE solver for the DFN. Note that in order to successfully solve the system of DAEs we are required to give consistent initial conditions. This is handled automatically by PyBaMM during the solve operation.\n" ] }, { @@ -195,7 +184,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "To get a quick overview of the model outputs we can use the QuickPlot class, which plots a common set of useful outputs. The method Quickplot.plot(t) is simply a function which either can be used satically to create a plot for a particular time, or interactively with a slider widget." + "To get a quick overview of the model outputs we can use the QuickPlot class, which plots a common set of useful outputs. The method `Quickplot.dynamic_plot` makes a slider widget." ] }, { @@ -206,12 +195,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "564a7d77b9894de29f0e926ee1e0e2d6", + "model_id": "29f22a3ab3bd4ce8825acdd6fb655f36", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=0.15930183645996823, step=0.05), Output()), …" + "interactive(children=(FloatSlider(value=0.0, description='t', max=3599.9999999999995, step=35.99999999999999),…" ] }, "metadata": {}, @@ -220,9 +209,7 @@ ], "source": [ "quick_plot = pybamm.QuickPlot(solution)\n", - "\n", - "import ipywidgets as widgets\n", - "widgets.interact(quick_plot.plot, t=widgets.FloatSlider(min=0,max=solution.t[-1],step=0.05,value=0));" + "quick_plot.dynamic_plot();" ] }, { @@ -236,7 +223,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In the table below, we provide the dimensionless parameters in the DFN in terms of the dimensional parameters in mcmb2528_lif6-in-ecdmc_lico2_parameters_Dualfoil.csv. We use a superscript * to indicate dimensional quanitities. \n", + "In the table below, we provide the dimensionless parameters in the DFN in terms of the dimensional parameters in mcmb2528_lif6-in-ecdmc_lico2_parameters_Dualfoil.csv. We use a superscript * to indicate dimensional quantities. \n", "\n", "| Parameter | Expression |Interpretation |\n", "|:--------------------------|:----------------------------------------|:------------------------------------------|\n", @@ -257,7 +244,7 @@ "metadata": {}, "source": [ "## References\n", - "[1] Doyle, Marc, Thomas F. Fuller, and John Newman. \"Modeling of galvanostatic charge and discharge of the lithium/polymer/insertion cell.\" Journal of the Electrochemical Society 140.6 (1993): 1526-1533." + "[1] M. Doyle, T.F. Fuller, and J. Newman. Modeling of galvanostatic charge and discharge of thelithium/polymer/insertion cell.Journal of the Electrochemical society, 140(6):1526–1533, 1993" ] }, { @@ -284,7 +271,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.3" + "version": "3.6.9" } }, "nbformat": 4, diff --git a/examples/notebooks/models/SPM.ipynb b/examples/notebooks/models/SPM.ipynb index ed78cad085..e2ae07e3c8 100644 --- a/examples/notebooks/models/SPM.ipynb +++ b/examples/notebooks/models/SPM.ipynb @@ -18,7 +18,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The SPM consists of two spherically symmetric diffusion equations: one within a representative negative particle ($\\text{k}=\\text{n}$) and one within a representative positive particle ($\\text{k}=\\text{p}$). In the centre of the particle the classical no-flux condition is imposed. Since the SPM assumes that all particles in an electrode behave in exactly the same way, the flux on the surface of a particle is simply the current $I$ divided by the thickness of the electrode $L_{\\text{k}}$. The concentration of lithium in electrode $\\text{k}$ is denoted $c_{\\text{k}}$ and the current is denoted by $I$. All parameters in the model stated here are dimensionless and are given in terms of dimensional parameters at the end of this notebook. The model equations for the SPM are then: \n", + "The SPM consists of two spherically symmetric diffusion equations: one within a representative negative particle ($\\text{k}=\\text{n}$) and one within a representative positive particle ($\\text{k}=\\text{p}$). In the centre of the particle the standard no-flux condition is imposed. Since the SPM assumes that all particles in an electrode behave in exactly the same way, the flux on the surface of a particle is simply the current $I$ divided by the thickness of the electrode $L_{\\text{k}}$. The concentration of lithium in electrode $\\text{k}$ is denoted $c_{\\text{k}}$ and the current is denoted by $I$. All parameters in the model stated here are dimensionless and are given in terms of dimensional parameters at the end of this notebook. The model equations for the SPM are then: \n", "\\begin{align}\n", "\\mathcal{C}_{\\text{k}} \\frac{\\partial c_{\\text{s,k}}}{\\partial t} &= -\\frac{1}{r_{\\text{k}}^2} \\frac{\\partial}{\\partial r_{\\text{k}}} \\left(r_{\\text{k}}^2 N_{\\text{s,k}}\\right), \\\\\n", "N_{\\text{s,k}} &= -D_{\\text{s,k}}(c_{\\text{s,k}}) \\frac{\\partial c_{\\text{s,k}}}{\\partial r_{\\text{k}}}, \\quad \\text{k} \\in \\text{n, p}, \\end{align}\n", @@ -37,7 +37,9 @@ "V = U_{\\text{p}}(c_{\\text{p}})\\big|_{r_{\\text{p}}=1} - U_{\\text{n}}(c_{\\text{n}})\\big|_{r_{\\text{n}}=1} -2\\sinh^{-1}\\left(\\frac{I}{j_{\\text{0,p}} L_{\\text{p}}}\\right) - 2\\sinh^{-1}\\left(\\frac{I}{j_{\\text{0,n}} L_{\\text{n}}}\\right)\n", "$$\n", "with the exchange current densities given by\n", - "$$j_{\\text{0,k}} = \\frac{\\gamma_{\\text{k}}}{\\mathcal{C}_{\\text{r,k}}}(c_{\\text{k}})^{1/2}(1-c_{\\text{k}})^{1/2} $$" + "$$j_{\\text{0,k}} = \\frac{\\gamma_{\\text{k}}}{\\mathcal{C}_{\\text{r,k}}}(c_{\\text{k}})^{1/2}(1-c_{\\text{k}})^{1/2} $$\n", + "\n", + "More details can be found in [[1]](#ref)." ] }, { @@ -46,7 +48,7 @@ "source": [ "## Example solving SPM using PyBaMM\n", "\n", - "Below we show how to solve the SPM model, using the default geometry, mesh, paramters, discretisation and solver provided with PyBaMM.\n", + "Below we show how to solve the Single Particle Model, using the default geometry, mesh, parameters, discretisation and solver provided with PyBaMM.\n", "\n", "First we need to import `pybamm`, and then change our working directory to the root of the pybamm folder. " ] @@ -68,7 +70,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We then get the SPM model equations:" + "We then create an instance of the SPM:" ] }, { @@ -119,7 +121,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We need a geometry to define our model equations over. In pybamm this is represented by the [`pybamm.Geometry`](https://pybamm.readthedocs.io/en/latest/source/geometry/geometry.html) class. In this case we use the default geometry object defined by the model" + "We need a geometry in which to define our model equations. In pybamm this is represented by the [`pybamm.Geometry`](https://pybamm.readthedocs.io/en/latest/source/geometry/geometry.html) class. In this case we use the default geometry object defined by the model" ] }, { @@ -135,7 +137,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This geometry object defines a number of geometry domains, each with its own name, spatial variables and min/max limits (the latter are represented as equations similar to the rhs equation shown above). For instance, the SPM model has the following domains:" + "This geometry object defines a number of domains, each with its own name, spatial variables and min/max limits (the latter are represented as equations similar to the rhs equation shown above). For instance, the SPM has the following domains:" ] }, { @@ -178,7 +180,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Both the model equations and the geometry are defined by a set of parameters, such as $\\gamma_p$ or $L_p$. We can substitute these symbolic parameters in the model with values by using the [`pybamm.ParameterValues`](https://pybamm.readthedocs.io/en/latest/source/parameters/parameter_values.html) class, which takes either a python dictionary or CSV file with the mapping between parameter names and values. Rather than create our own instance of `pybamm.ParameterValues`, we will use the default parameter set included in the model" + "Both the model equations and the geometry include parameters, such as $\\gamma_p$ or $L_p$. We can substitute these symbolic parameters in the model with values by using the [`pybamm.ParameterValues`](https://pybamm.readthedocs.io/en/latest/source/parameters/parameter_values.html) class, which takes either a python dictionary or CSV file with the mapping between parameter names and values. Rather than create our own instance of `pybamm.ParameterValues`, we will use the default parameter set included in the model" ] }, { @@ -213,7 +215,7 @@ "source": [ "The next step is to mesh the input geometry. We can do this using the [`pybamm.Mesh`](https://pybamm.readthedocs.io/en/latest/source/meshes/meshes.html) class. This class takes in the geometry of the problem, and also two dictionaries containing the type of mesh to use within each domain of the geometry (i.e. within the positive or negative electrode domains), and the number of mesh points. \n", "\n", - "The default mesh types and the default number of points to use in each variable for the SPM model are:" + "The default mesh types and the default number of points to use in each variable for the SPM are:" ] }, { @@ -270,7 +272,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The next step is to discretise the model equations over this mesh. We do this using the [`pybamm.Discretisation`](https://pybamm.readthedocs.io/en/latest/source/discretisations/discretisation.html) class, which takes both the mesh we have already created, and a dictionary of spatial methods to use for each geometry domain. For the case of the SPM model, we use the following defaults for the spatial discretisation methods:" + "The next step is to discretise the model equations using this mesh. We do this using the [`pybamm.Discretisation`](https://pybamm.readthedocs.io/en/latest/source/discretisations/discretisation.html) class, which takes both the mesh we have already created, and a dictionary of spatial methods to use for each geometry domain. For the case of the SPM, we use the following defaults for the spatial discretisation methods:" ] }, { @@ -305,28 +307,17 @@ "cell_type": "code", "execution_count": 11, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "disc = pybamm.Discretisation(mesh, model.default_spatial_methods)\n", - "disc.process_model(model)" + "disc.process_model(model);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "After this stage, all the equations in `model` have been discretised into purely linear algebra expressions that are ready to be evaluated within a time-stepping loop of a given solver. For example, the rhs expression for $\\partial{c_n}/\\partial{t}$ that we visualised above is now represented by:" + "After this stage, all of the variables in `model` have been discretised into `pybamm.StateVector` objects, and spatial operators have been replaced by matrix-vector multiplications, ready to be evaluated within a time-stepping algorithm of a given solver. For example, the rhs expression for $\\partial{c_n}/\\partial{t}$ that we visualised above is now represented by:" ] }, { @@ -344,7 +335,7 @@ "source": [ "![](spm2.png)\n", "\n", - "Now we are ready to run the time-stepping loop to solve the model. Once again we use the default ODE solver like so:" + "Now we are ready to run the time-stepping routine to solve the model. Once again we use the default ODE solver." ] }, { @@ -375,7 +366,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Each model in pybamm has a list of relevent variables defined in the model, for use in visualising the model solution or for comparison with other models. The SPM model defines the following variables:" + "Each model in pybamm has a list of relevant variables defined in the model, for use in visualising the model solution or for comparison with other models. The SPM defines the following variables:" ] }, { @@ -522,6 +513,8 @@ "\t- Volume-averaged cell temperature [K]\n", "\t- Heat flux\n", "\t- Heat flux [W.m-2]\n", + "\t- Ambient temperature [K]\n", + "\t- Ambient temperature\n", "\t- Electrolyte tortuosity\n", "\t- Negative electrolyte tortuosity\n", "\t- Positive electrolyte tortuosity\n", @@ -783,7 +776,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "af941ec6637443ad80198a75b4df53ef", + "model_id": "57ac59ccd1af44dab95f2bca72d12771", "version_major": 2, "version_minor": 0 }, @@ -798,12 +791,13 @@ "source": [ "c_s_n = solution['Negative particle concentration']\n", "c_s_p = solution['Positive particle concentration']\n", - "r = np.linspace(0,1,100)\n", + "r_n = mesh[\"negative particle\"][0].nodes\n", + "r_p = mesh[\"positive particle\"][0].nodes\n", "\n", "def plot_concentrations(t):\n", " f, (ax1, ax2) = plt.subplots(1, 2 ,figsize=(10,5))\n", - " plot_c_n, = ax1.plot(r, c_s_n_surf(x=r,t=t))\n", - " plot_c_p, = ax2.plot(r, c_s_p_surf(x=r,t=t))\n", + " plot_c_n, = ax1.plot(r_n, c_s_n(r=r_n,t=t,x=0.1)) # can evaluate at arbitrary x (single representative particle)\n", + " plot_c_p, = ax2.plot(r_p, c_s_p(r=r_p,t=t,x=0.9)) # can evaluate at arbitrary x (single representative particle)\n", " ax1.set_ylabel('Negative particle concentration')\n", " ax2.set_ylabel('Positive particle concentration')\n", " ax1.set_xlabel(r'$r_n$')\n", @@ -820,7 +814,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The QuickPlot class can be used to plot the common set of useful outputs which should give you a good initial overview of the model. The method Quickplot.plot(t) is simply a function like plot_concentrations(t) above. We can therefore either use it statically for a particularl t or employ the slider widget. " + "The QuickPlot class can be used to plot the common set of useful outputs which should give you a good initial overview of the model. The method `Quickplot.dynamic_plot` employs the slider widget. " ] }, { @@ -831,12 +825,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "3efb4d7173ef48e29e089fad6cb6f8a7", + "model_id": "22b9213dc69b48b1b9c4f51718aac0f3", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=0.9999999999999999, step=0.05), Output()), _…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=3599.9999999999995, step=35.99999999999999),…" ] }, "metadata": {}, @@ -845,7 +839,7 @@ ], "source": [ "quick_plot = pybamm.QuickPlot(solution)\n", - "widgets.interact(quick_plot.plot, t=widgets.FloatSlider(min=0,max=quick_plot.max_t,step=0.05,value=0));" + "quick_plot.dynamic_plot();" ] }, { @@ -859,7 +853,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In the table below, we provide the dimensionless parameters in the SPM in terms of the dimensional parameters in LCO.csv. We use a superscript * to indicate dimensional quanitities. \n", + "In the table below, we provide the dimensionless parameters in the SPM in terms of the dimensional parameters in LCO.csv. We use a superscript * to indicate dimensional quantities. \n", "\n", "| Parameter | Expression |Interpretation |\n", "|:--------------------------|:----------------------------------------|:------------------------------------------|\n", @@ -871,11 +865,11 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], - "source": [] + "source": [ + "[1] 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" + ] } ], "metadata": { diff --git a/examples/notebooks/models/SPMe.ipynb b/examples/notebooks/models/SPMe.ipynb index edbc5eaeb6..449155039c 100644 --- a/examples/notebooks/models/SPMe.ipynb +++ b/examples/notebooks/models/SPMe.ipynb @@ -20,9 +20,9 @@ "source": [ "i) The SPMe comprises an equation for the lithium concentration in a representative particle in the negative electrode $c_{\\text{s,n}}$, an equation for the lithium concentration in a representative particle in the positive electrode $c_{\\text{s,p}}$, and an equation which governs the behaviour of the first-order correction to the lithium concentration in the electrolyte $c_{\\text{e,k}}$. Here we use a roman subscript $\\text{k} \\in \\text{n, s, p}$ is used to denote the regions negative electrode, separator, and positive electrode, respectively. \n", "\n", - "ii) At the centre of each particle the standard no-flux condition is imposed, and the flux on the surface of the particle is simply the current $I$ divided by the thickness of the electrode $L_{\\text{k}}$, as in the SPM. Since lithium is transferred between the electrolyte and particles, the flux through the particle surface also enters the electrolyte diffusion equation as a source/sink term. There is no ransfer of lithium between the electrolyte and current collectors, which leads to no flux boundary conditions on the lithium concentration in the electrolyte $c_{\\text{e,k}}$ at either end of the cell. \n", + "ii) At the centre of each particle the standard no-flux condition is imposed, and the flux on the surface of the particle is simply the current $I$ divided by the thickness of the electrode $L_{\\text{k}}$, as in the SPM. Since lithium is transferred between the electrolyte and particles, the flux through the particle surface also enters the electrolyte diffusion equation as a source/sink term. There is no transfer of lithium between the electrolyte and current collectors, which leads to no flux boundary conditions on the lithium concentration in the electrolyte $c_{\\text{e,k}}$ at either end of the cell. \n", "\n", - "iii) We must also impose initial conditons which correspond to setting an initial concentration in each particle $c_{\\text{s,k}}(t=0) = c_{\\text{s,k,0}}$, and to having no deviation from the initial (uniform) lithium concentration in the electrolyte $c_{\\text{e,k}}(t=0) = 0$. \n", + "iii) We must also impose initial conditions which correspond to setting an initial concentration in each particle $c_{\\text{s,k}}(t=0) = c_{\\text{s,k,0}}$, and to having no deviation from the initial (uniform) lithium concentration in the electrolyte $c_{\\text{e,k}}(t=0) = 0$. \n", "\n", "\n", "The model equations for the SPMe read: \n", @@ -39,7 +39,7 @@ "\t\t -\\frac{I}{L_{\\text{p}}}, \\quad &\\text{k}=\\text{p}, \n", "\\end{cases} \\\\\n", "c_{\\text{s,k}}(r_{\\text{k}},0) = c_{\\text{s,k,0}}, \\quad \\text{k} \\in \\text{n, p},$$\n", - "where $D_{\\text{s,k}}$ is the diffusion coefficient in the solid, $N_{\\text{s,k}}$ denotes the flux of lithium ions in the solid particle within the region $\\text{k}$, and $r_{\\text{k}} \\in[0,1]$ is the radial coordinate of the particle in electrode $\\text{k}$. All other relevant paramters are given in the table at the end of this notebook.\n", + "where $D_{\\text{s,k}}$ is the diffusion coefficient in the solid, $N_{\\text{s,k}}$ denotes the flux of lithium ions in the solid particle within the region $\\text{k}$, and $r_{\\text{k}} \\in[0,1]$ is the radial coordinate of the particle in electrode $\\text{k}$. All other relevant parameters are given in the table at the end of this notebook.\n", "\n", "\n", "#### Electrolyte: \n", @@ -60,7 +60,7 @@ "$$\n", "N_{\\text{e,n}}\\big|_{x=0} = 0, \\quad N_{\\text{e,p}}\\big|_{x=1}=0, \\\\\n", "c_{\\text{e,k}}(x,0) = 0, \\quad \\text{k} \\in \\text{n, s, p},$$\n", - "where $D_{\\text{e}}$ is the diffusion coefficient in the solid, $N_{\\text{e,k}}$ denotes the flux of lithium ions in the electrolyte within the region $\\text{k}$, and $x\\in[0,1]$ is the macroscopic through-cell distance. This equation is also solved subject to continiuity of concentration and flux at the electrode/separator interfaces.\n", + "where $D_{\\text{e}}$ is the diffusion coefficient in the solid, $N_{\\text{e,k}}$ denotes the flux of lithium ions in the electrolyte within the region $\\text{k}$, and $x\\in[0,1]$ is the macroscopic through-cell distance. This equation is also solved subject to continuity of concentration and flux at the electrode/separator interfaces.\n", "\n", "### Voltage Expression\n", "The terminal voltage is obtained from the expression: \n", @@ -81,7 +81,9 @@ "\\begin{equation} \n", " \\bar{c}_{\\text{e,n}} = \\frac{1}{L_{\\text{n}}}\\int_0^{L_{\\text{n}}} c_{\\text{e,n}} \\, \\text{d}x, \\quad\n", " \\bar{c}_{\\text{e,p}} = \\frac{1}{L_{\\text{p}}}\\int_{1-L_{\\text{p}}}^{1} c_{\\text{e,p}} \\, \\text{d}x.\n", - "\\end{equation} \n" + "\\end{equation} \n", + "\n", + "More details can be found in [[1]](#ref)." ] }, { @@ -95,7 +97,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Below we show how to solve the SPMe model, using the default geometry, mesh, paramters, discretisation and solver provided with PyBaMM. For a more detailed example, see the notebook on the [SPM](https://github.com/pybamm-team/PyBaMM/blob/master/examples/notebooks/models/SPM.ipynb).\n", + "Below we show how to solve the SPMe model, using the default geometry, mesh, parameters, discretisation and solver provided with PyBaMM. For a more detailed example, see the notebook on the [SPM](https://github.com/pybamm-team/PyBaMM/blob/master/examples/notebooks/models/SPM.ipynb).\n", "\n", "First we need to import `pybamm`, and then change our working directory to the root of the pybamm folder." ] @@ -149,25 +151,14 @@ "cell_type": "code", "execution_count": 3, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# set mesh\n", "mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts)\n", "\n", "# discretise model\n", "disc = pybamm.Discretisation(mesh, model.default_spatial_methods)\n", - "disc.process_model(model)" + "disc.process_model(model);" ] }, { @@ -193,7 +184,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "To get a quick overview of the model outputs we can use the QuickPlot class, which plots a common set of useful outputs. The method Quickplot.plot(t) is simply a function which either can be used satically to create a plot for a particular time, or interactively with a slider widget." + "To get a quick overview of the model outputs we can use the QuickPlot class, which plots a common set of useful outputs. The method `Quickplot.dynamic_plot` makes a slider widget." ] }, { @@ -204,12 +195,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "4b296e4b16c9438083c80708c52f8f1b", + "model_id": "2e47cb08b18f4d94ae85e6a334ca22ef", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=0.15930183645996823, step=0.05), Output()), …" + "interactive(children=(FloatSlider(value=0.0, description='t', max=3599.9999999999995, step=35.99999999999999),…" ] }, "metadata": {}, @@ -218,9 +209,7 @@ ], "source": [ "quick_plot = pybamm.QuickPlot(solution)\n", - "\n", - "import ipywidgets as widgets\n", - "widgets.interact(quick_plot.plot, t=widgets.FloatSlider(min=0,max=solution.t[-1],step=0.05,value=0));" + "quick_plot.dynamic_plot();" ] }, { @@ -234,7 +223,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In the table below, we provide the dimensionless parameters in the SPMe in terms of the dimensional parameters in LCO.csv. We use a superscript * to indicate dimensional quanitities. \n", + "In the table below, we provide the dimensionless parameters in the SPMe in terms of the dimensional parameters in LCO.csv. We use a superscript * to indicate dimensional quantities. \n", "\n", "| Parameter | Expression |Interpretation |\n", "|:--------------------------|:----------------------------------------|:------------------------------------------|\n", @@ -250,11 +239,11 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], - "source": [] + "source": [ + "[1] 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" + ] } ], "metadata": { diff --git a/examples/notebooks/models/compare-lithium-ion.ipynb b/examples/notebooks/models/compare-lithium-ion.ipynb index 6991752250..039d41f713 100644 --- a/examples/notebooks/models/compare-lithium-ion.ipynb +++ b/examples/notebooks/models/compare-lithium-ion.ipynb @@ -11,9 +11,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We compare three one-dimensional lithium-ion battery models: [the Doyle-Fuller-Newman (DFN) model](./DFN.ipynb), [the single particle model (SPM)](./SPM.ipynb), and [the single particle model with electrolyte (SPMe)](./SPMe.ipynb). Further details on these models can be found in [[1]](#ref).\n", - "\n", - "[1] Scott G. Marquis, Valentin Sulzer, Robert Timms, Colin P. Please, and S. Jon Chapman. \"An asymptotic derivation of a single particle model with electrolyte.\" arXiv preprint arXiv:1905.12553 (2019).\n" + "We compare three one-dimensional lithium-ion battery models: [the Doyle-Fuller-Newman (DFN) model](./DFN.ipynb), [the single particle model (SPM)](./SPM.ipynb), and [the single particle model with electrolyte (SPMe)](./SPMe.ipynb). Further details on these models can be found in [[1]](#ref).\n" ] }, { @@ -104,7 +102,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 4, @@ -249,9 +247,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "Solved the Doyle-Fuller-Newman model in 2.037 seconds\n", - "Solved the Single Particle Model in 0.325 seconds\n", - "Solved the Single Particle Model with electrolyte in 0.398 seconds\n" + "Solved the Doyle-Fuller-Newman model in 2.143 seconds\n", + "Solved the Single Particle Model in 0.379 seconds\n", + "Solved the Single Particle Model with electrolyte in 0.416 seconds\n" ] } ], @@ -316,7 +314,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Alternatively the inbuilt `QuickPlot` functionality can be employed to compare a set of variables over the discharge. We must first create a list of the models and a list of the solutions (instead of disctionary)" + "Alternatively the inbuilt `QuickPlot` functionality can be employed to compare a set of variables over the discharge. We must first create a list of the solutions" ] }, { @@ -325,9 +323,7 @@ "metadata": {}, "outputs": [], "source": [ - "list_of_models = [models[name] for name in models.keys()]\n", - "list_of_solutions = [solutions[name] for name in models.keys()]\n", - "a_mesh = list(mesh.values())[0]" + "list_of_solutions = list(solutions.values())" ] }, { @@ -345,12 +341,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ef5d5ce48a40490bb47a73be0c7b66b1", + "model_id": "19d3d199c5124fbf9d3b0db122a4d5d1", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=0.6755852842809364, step=0.05), Output()), _…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=2432.1070234113713, step=24.321070234113712)…" ] }, "metadata": {}, @@ -359,8 +355,7 @@ ], "source": [ "quick_plot = pybamm.QuickPlot(list_of_solutions)\n", - "import ipywidgets as widgets\n", - "widgets.interact(quick_plot.plot, t=widgets.FloatSlider(min=0,max=quick_plot.max_t,step=0.05,value=0));" + "quick_plot.dynamic_plot();" ] }, { @@ -379,18 +374,18 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "7b919c702a3940a0bddfccd55fca15a0", + "model_id": "f219eff18b464c9696e64cbbf19c008f", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=0.11839464882943145, step=0.05), Output()), …" + "interactive(children=(FloatSlider(value=0.0, description='t', max=426.2207357859532, step=4.262207357859532), …" ] }, "metadata": {}, @@ -405,18 +400,22 @@ " solutions[model_name] = model.default_solver.solve(model, t_eval, inputs={\"Current function [A]\": 5})\n", "\n", "# Plot\n", - "list_of_models = list(models.values())\n", "list_of_solutions = list(solutions.values())\n", - "\n", - "quick_plot = pybamm.QuickPlot(list_of_solutions)\n", - "widgets.interact(quick_plot.plot, t=widgets.FloatSlider(min=0,max=quick_plot.max_t,step=0.05,value=0));" + "quick_plot.dynamic_plot();" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By increasing the current we observe less agreement between the models, as expected. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "By increasing the current, we observe less agreement between the model as expected. " + "[1] 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" ] } ], diff --git a/examples/notebooks/models/lead-acid.ipynb b/examples/notebooks/models/lead-acid.ipynb index 1922a8af59..28eaa4d996 100644 --- a/examples/notebooks/models/lead-acid.ipynb +++ b/examples/notebooks/models/lead-acid.ipynb @@ -265,9 +265,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "Solved the LOQS model in 0.146 seconds\n", - "Solved the Composite model in 1.127 seconds\n", - "Solved the Full model in 1.102 seconds\n" + "Solved the LOQS model in 0.137 seconds\n", + "Solved the Composite model in 1.160 seconds\n", + "Solved the Full model in 1.117 seconds\n" ] } ], @@ -343,12 +343,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "d3bad7c275604722af2babfb0a6cb2ee", + "model_id": "b1feaebac09b4bddace9494faa7d04b1", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=17.000000000000004, step=0.05), Output()), _…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=61200.00000000001, step=612.0000000000001), …" ] }, "metadata": {}, @@ -358,8 +358,7 @@ "source": [ "solution_values = [solutions[model] for model in models]\n", "quick_plot = pybamm.QuickPlot(solution_values)\n", - "import ipywidgets as widgets\n", - "widgets.interact(quick_plot.plot, t=widgets.FloatSlider(min=0,max=quick_plot.max_t,step=0.05,value=0));" + "quick_plot.dynamic_plot();" ] }, { @@ -379,12 +378,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "762755a1960d42f4a9d14b60b6e3d68e", + "model_id": "0955c956c0be444f805555ee5bca92e7", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=0.9898989898989901, step=0.05), Output()), _…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=3563.636363636364, step=35.63636363636364), …" ] }, "metadata": {}, @@ -399,7 +398,7 @@ "# Plot\n", "solution_values = [solutions[model] for model in models]\n", "quick_plot = pybamm.QuickPlot(solution_values)\n", - "widgets.interact(quick_plot.plot, t=widgets.FloatSlider(min=0,max=quick_plot.max_t,step=0.05,value=0));" + "quick_plot.dynamic_plot();" ] }, { diff --git a/examples/notebooks/parameter-values.ipynb b/examples/notebooks/parameter-values.ipynb index e8b21c0965..b3ec7afe6c 100644 --- a/examples/notebooks/parameter-values.ipynb +++ b/examples/notebooks/parameter-values.ipynb @@ -47,7 +47,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "parameter values are \n" + "parameter values are \n" ] } ], @@ -73,7 +73,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "parameter values are \n" + "parameter values are \n" ] } ], @@ -109,7 +109,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Marquis2019 chemistry set is {'chemistry': 'lithium-ion', 'cell': 'kokam_Marquis2019', 'anode': 'graphite_mcmb2528_Marquis2019', 'separator': 'separator_Marquis2019', 'cathode': 'lico2_Marquis2019', 'electrolyte': 'lipf6_Marquis2019', 'experiment': '1C_discharge_from_full_Marquis2019'}\n", + "Marquis2019 chemistry set is {'chemistry': 'lithium-ion', 'cell': 'kokam_Marquis2019', 'anode': 'graphite_mcmb2528_Marquis2019', 'separator': 'separator_Marquis2019', 'cathode': 'lico2_Marquis2019', 'electrolyte': 'lipf6_Marquis2019', 'experiment': '1C_discharge_from_full_Marquis2019', 'citation': 'marquis2019asymptotic'}\n", "Negative current collector thickness is 2.5e-05 m\n" ] } @@ -138,7 +138,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "parameter values are \n" + "parameter values are \n" ] } ], @@ -165,7 +165,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "parameter values are \n" + "parameter values are \n" ] } ], @@ -218,7 +218,7 @@ "a = pybamm.Parameter(\"a\")\n", "b = pybamm.Parameter(\"b\")\n", "c = pybamm.Parameter(\"c\")\n", - "func = pybamm.FunctionParameter(\"square function\", a)\n", + "func = pybamm.FunctionParameter(\"square function\", {\"a\": a})\n", "\n", "expr = a + b * c\n", "try:\n", @@ -295,8 +295,8 @@ "d = pybamm.InputParameter(\"d\")\n", "expr = 2 + d\n", "expr_eval = parameter_values.process_symbol(expr)\n", - "print(\"with d = {}, {} = {}\".format(3, expr_eval, expr_eval.evaluate(u={\"d\": 3})))\n", - "print(\"with d = {}, {} = {}\".format(5, expr_eval, expr_eval.evaluate(u={\"d\": 5})))" + "print(\"with d = {}, {} = {}\".format(3, expr_eval, expr_eval.evaluate(inputs={\"d\": 3})))\n", + "print(\"with d = {}, {} = {}\".format(5, expr_eval, expr_eval.evaluate(inputs={\"d\": 5})))" ] }, { @@ -329,7 +329,7 @@ "outputs": [ { "data": { - "image/png": "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fTfXXnttuf5WsUrH0I7H0JgxY8jIyOChhx76dV/p6el88sknDB+uzwYiUjM99tEi/u/HLqT0uImhax4PDbtNSQ0lnxWYN5WnCu5hwB+Ab8xsdrjtBqAtgLs/BrwFDAEWAluA88N968zsNuCr8PPGuPu66IW/E2nDIW04z36+mFve+Jb39jmSjpWyYxGRqmnTpk2cdtppPPjggyWuMkLo6tWECRP46KOPgFDyUPrK47vvvssbb7zBfffdB4SWBlm6dCldunTh6aefJi0tjYsvvpjDDjvs1+ecdNJJJCcnk5yczIABA/jf//5H8+bN2dVUjDvuuIM77riDu+66i4ceeohbb721RP9tt92GmTFr1ixGjx5drvmGzZs3jzifVMqvKh1Der9FpCZ765uV/N+0+Qzr2YohZw6BHVfarDC7TEDd/VNgpzXJPXRmv7yMvgnAhD2KLgoGd2/B6Knf8p9vVnLVPvWDCkNEpFzKe5Up2goKCjjttNM455xzOPXUUyNus33YYlncnVdffZVOnTrt0LdgwQLq1au3wwf+0gmImZGcnFxiHcbzzz+fWbNm0apVqxLz+ADOOecchgwZskMCuv11txchKs/SGnl5eTsMCa2KdAzteAxFUl3ebxGR3TVzyTquenk2B7ZtyL2np1X68lO7VQW3KmreIImD2zXmrW9WBh2KiEhMcndGjBhBly5d+Mtf/rLHr3P88cfzz3/+89erjbNmzQIgJyeHkSNH8vHHH7N27VomTZr063OmTJlCXl4ea9eu5cMPP+Tggw/mgAMOYPHixb9u89RTT5UoIrNgwYISz+/cufNux1q/fn02btxYou2HH36ge/fuu/1aEvvHkN5vEZGQRdmbuPCZDFo3TOaJcw8mKTG+0mOo9gkowNAeLflh9SYWrN64641FRGqYzz77jGeffZb333+fXr160atXrx2uNJbHzTffTEFBAWlpaXTr1o2bb74ZgKuvvprLL7+cAw44gCeffJLrrruONWvWAJCWlsaAAQM45JBDuPnmm2nVqhV169alQ4cOLFy4MOJ+rrvuOrp3705aWhrvvvtumUt+7ExaWhrx8fH07Nnz1yJEH3zwAUOHDt3t15LYP4YGDBjAvHnzfi1CBHq/RaTm+XlTPuc99RVxZjx9/sE0rltr10+qAFbR63DtifT0dM/IyIja663ZkEffu6Zz5cCO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QKF3vwpYyyiclJ1nH6Zl3f8IsNy7PwHY4PWl0rs/IWzxe8sdSuYEcgvbNvCefxa5T+w/j54nlD8blvU/iD7MIHMO2sm9po8DSrPuPw2cHcL4p3a/dsgknbe87ZHX6y8MffDml5M5/2Vc2LaBtz7vBG7sZ5lIvBeGcZ1EOkcOog+hzZN5xh/aHJlvH7zllCdHL/7Q58g8+qA8OfKxhypPjsoGHMaVeS6ZUZzeABZ505aQ+RUUoAj4vffGfBaYndV2kdfuNeCckMb/KLAZeNG7LfWmnwys8t6Iq4DPh3gb/A/wshfrE8ChWW0/522bdcBnwxi/9/9i4Pu92oViG5D5la0B6CRzTP7ngcuAy7z5BvzU698q4Pgwrf88+3AbsD3rfbDSmz7bW/8vea+xRSGN/6tZ74HlZH1J8Hv9hbEP3jKfITMwTXa7sGyDU8mcY/OvrNfJuVF7L4zAeol0jsyzD6HOk3nEH+ocmU8fvP8Xozw5WvGHOkfm2QflyZGPP1R5svvXBhEREREREZERFeVzQEVERERERCRCVICKiIiIiIhIIFSAioiIiIiISCBUgIqIiIiIiEggVICKiIiIiIhIIFSAioxhZlZhZl8e7ThERETCSHlSJHgqQEXGtgpAiVVERMSf8qRIwFSAioxt3wcONLMXzeyHox2MiIhIyChPigTMnHOjHYOIjBAzmwXc75ybO8qhiIiIhI7ypEjwtAdUREREREREAqECVERERERERAKhAlRkbNsBlI52ECIiIiGlPCkSMBWgImOYc64R+IeZrdbgCiIiIj0pT4oET4MQiYiIiIiISCC0B1REREREREQCoQJUREREREREAqECVERERERERAKhAlREREREREQCoQJUREREREREAqECVERERERERAKhAlREREREREQC8f/tjayVNCsiOgAAAABJRU5ErkJggg==\n", + "image/png": "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fTfXXnttuf5WsUrH0I7H0JgxY8jIyOChhx76dV/p6el88sknDB+uzwYiUjM99tEi/u/HLqT0uImhax4PDbtNSQ0lnxWYN5WnCu5hwB+Ab8xsdrjtBqAtgLs/BrwFDAEWAluA88N968zsNuCr8PPGuPu66IW/E2nDIW04z36+mFve+Jb39jmSjpWyYxGRqmnTpk2cdtppPPjggyWuMkLo6tWECRP46KOPgFDyUPrK47vvvssbb7zBfffdB4SWBlm6dCldunTh6aefJi0tjYsvvpjDDjvs1+ecdNJJJCcnk5yczIABA/jf//5H8+bN2dVUjDvuuIM77riDu+66i4ceeohbb721RP9tt92GmTFr1ixGjx5drvmGzZs3jzifVMqvKh1Der9FpCZ765uV/N+0+Qzr2YohZw6BHVfarDC7TEDd/VNgpzXJPXRmv7yMvgnAhD2KLgoGd2/B6Knf8p9vVnLVPvWDCkNEpFzKe5Up2goKCjjttNM455xzOPXUUyNus33YYlncnVdffZVOnTrt0LdgwQLq1au3wwf+0gmImZGcnFxiHcbzzz+fWbNm0apVqxLz+ADOOecchgwZskMCuv11txchKs/SGnl5eTsMCa2KdAzteAxFUl3ebxGR3TVzyTquenk2B7ZtyL2np1X68lO7VQW3KmreIImD2zXmrW9WBh2KiEhMcndGjBhBly5d+Mtf/rLHr3P88cfzz3/+89erjbNmzQIgJyeHkSNH8vHHH7N27VomTZr063OmTJlCXl4ea9eu5cMPP+Tggw/mgAMOYPHixb9u89RTT5UoIrNgwYISz+/cufNux1q/fn02btxYou2HH36ge/fuu/1aEvvHkN5vEZGQRdmbuPCZDFo3TOaJcw8mKTG+0mOo9gkowNAeLflh9SYWrN64641FRGqYzz77jGeffZb333+fXr160atXrx2uNJbHzTffTEFBAWlpaXTr1o2bb74ZgKuvvprLL7+cAw44gCeffJLrrruONWvWAJCWlsaAAQM45JBDuPnmm2nVqhV169alQ4cOLFy4MOJ+rrvuOrp3705aWhrvvvtumUt+7ExaWhrx8fH07Nnz1yJEH3zwAUOHDt3t15LYP4YGDBjAvHnzfi1CBHq/RaTm+XlTPuc99RVxZjx9/sE0rltr10+qAFbR63DtifT0dM/IyIja663ZkEffu6Zz5cCO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QKF3vwpYyyiclJ1nH6Zl3f8IsNy7PwHY4PWl0rs/IWzxe8sdSuYEcgvbNvCefxa5T+w/j54nlD8blvU/iD7MIHMO2sm9po8DSrPuPw2cHcL4p3a/dsgknbe87ZHX6y8MffDml5M5/2Vc2LaBtz7vBG7sZ5lIvBeGcZ1EOkcOog+hzZN5xh/aHJlvH7zllCdHL/7Q58g8+qA8OfKxhypPjsoGHMaVeS6ZUZzeABZ505aQ+RUUoAj4vffGfBaYndV2kdfuNeCckMb/KLAZeNG7LfWmnwys8t6Iq4DPh3gb/A/wshfrE8ChWW0/522bdcBnwxi/9/9i4Pu92oViG5D5la0B6CRzTP7ngcuAy7z5BvzU698q4Pgwrf88+3AbsD3rfbDSmz7bW/8vea+xRSGN/6tZ74HlZH1J8Hv9hbEP3jKfITMwTXa7sGyDU8mcY/OvrNfJuVF7L4zAeol0jsyzD6HOk3nEH+ocmU8fvP8Xozw5WvGHOkfm2QflyZGPP1R5svvXBhEREREREZERFeVzQEVERERERCRCVICKiIiIiIhIIFSAioiIiIiISCBUgIqIiIiIiEggVICKiIiIiIhIIFSAioxhZlZhZl8e7ThERETCSHlSJHgqQEXGtgpAiVVERMSf8qRIwFSAioxt3wcONLMXzeyHox2MiIhIyChPigTMnHOjHYOIjBAzmwXc75ybO8qhiIiIhI7ypEjwtAdUREREREREAqECVERERERERAKhAlRkbNsBlI52ECIiIiGlPCkSMBWgImOYc64R+IeZrdbgCiIiIj0pT4oET4MQiYiIiIiISCC0B1REREREREQCoQJUREREREREAqECVERERERERAKhAlREREREREQCoQJUREREREREAqECVERERERERAKhAlREREREREQC8f/tjayVNCsiOgAAAABJRU5ErkJggg==\n", "text/plain": [ "
" ] @@ -401,7 +401,7 @@ { "data": { "text/plain": [ - "{Variable(-0x4a013e7c3cccb9f1, u, children=[], domain=[], auxiliary_domains={}): Multiplication(0x786762aded2a40a5, *, children=['-a', 'y[0:1]'], domain=[], auxiliary_domains={})}" + "{Variable(-0x2cdf9e1c15ea083, u, children=[], domain=[], auxiliary_domains={}): Multiplication(0x1d65a94f24de5058, *, children=['-a', 'y[0:1]'], domain=[], auxiliary_domains={})}" ] }, "execution_count": 12, @@ -481,9 +481,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.6" + "version": "3.7.5" } }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/examples/notebooks/solution-data-and-processed-variables.ipynb b/examples/notebooks/solution-data-and-processed-variables.ipynb index 45a687237d..bf328e862d 100644 --- a/examples/notebooks/solution-data-and-processed-variables.ipynb +++ b/examples/notebooks/solution-data-and-processed-variables.ipynb @@ -22,12 +22,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "038a9f3ebf4c4549aa098f7bab633c92", + "model_id": "ac10fba1880a4043b34f9f0a5093dc36", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=0.9750000000000001, step=0.05), Output()), _…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=3510.0, step=35.1), Output()), _dom_classes=…" ] }, "metadata": {}, @@ -66,9 +66,7 @@ "solution = solver.solve(model, t_eval)\n", "\n", "quick_plot = pybamm.QuickPlot(solution)\n", - "\n", - "import ipywidgets as widgets\n", - "widgets.interact(quick_plot.plot, t=widgets.FloatSlider(min=0,max=quick_plot.max_t,step=0.05,value=0));" + "quick_plot.dynamic_plot();" ] }, { @@ -86,7 +84,7 @@ { "data": { "text/plain": [ - "dict_keys(['Negative particle surface concentration', 'Electrolyte concentration', 'Positive particle surface concentration', 'Current [A]', 'Negative electrode potential [V]', 'Electrolyte potential [V]', 'Positive electrode potential [V]', 'Terminal voltage [V]'])" + "dict_keys(['Negative particle surface concentration [mol.m-3]', 'Electrolyte concentration [mol.m-3]', 'Positive particle surface concentration [mol.m-3]', 'Current [A]', 'Negative electrode potential [V]', 'Electrolyte potential [V]', 'Positive electrode potential [V]', 'Terminal voltage [V]'])" ] }, "execution_count": 2, @@ -115,7 +113,7 @@ } ], "source": [ - "solution.data['Negative particle surface concentration'].shape" + "solution.data['Negative particle surface concentration [mol.m-3]'].shape" ] }, { @@ -154,7 +152,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "['Active material volume fraction', 'Battery voltage [V]', 'C-rate', 'Cell temperature', 'Cell temperature [K]', 'Current [A]', 'Current collector current density', 'Current collector current density [A.m-2]', 'Discharge capacity [A.h]', 'Electrode current density', 'Electrode tortuosity', 'Electrolyte concentration', 'Electrolyte concentration [Molar]', 'Electrolyte concentration [mol.m-3]', 'Electrolyte current density', 'Electrolyte current density [A.m-2]', 'Electrolyte flux', 'Electrolyte flux [mol.m-2.s-1]', 'Electrolyte potential', 'Electrolyte potential [V]', 'Electrolyte pressure', 'Electrolyte tortuosity', 'Exchange current density', 'Exchange current density [A.m-2]', 'Exchange current density per volume [A.m-3]', 'Gradient of electrolyte potential', 'Gradient of negative electrode potential', 'Gradient of negative electrolyte potential', 'Gradient of positive electrode potential', 'Gradient of positive electrolyte potential', 'Gradient of separator electrolyte potential', 'Heat flux', 'Heat flux [W.m-2]', 'Interfacial current density', 'Interfacial current density [A.m-2]', 'Interfacial current density per volume [A.m-3]', 'Irreversible electrochemical heating', 'Irreversible electrochemical heating [W.m-3]', 'Leading-order active material volume fraction', 'Leading-order current collector current density', 'Leading-order electrode tortuosity', 'Leading-order electrolyte tortuosity', 'Leading-order negative electrode active material volume fraction', 'Leading-order negative electrode porosity', 'Leading-order negative electrode tortuosity', 'Leading-order negative electrolyte tortuosity', 'Leading-order porosity', 'Leading-order positive electrode active material volume fraction', 'Leading-order positive electrode porosity', 'Leading-order positive electrode tortuosity', 'Leading-order positive electrolyte tortuosity', 'Leading-order separator active material volume fraction', 'Leading-order separator porosity', 'Leading-order separator tortuosity', 'Leading-order x-averaged negative electrode active material volume fraction', 'Leading-order x-averaged negative electrode porosity', 'Leading-order x-averaged negative electrode porosity change', 'Leading-order x-averaged negative electrode tortuosity', 'Leading-order x-averaged negative electrolyte tortuosity', 'Leading-order x-averaged positive electrode active material volume fraction', 'Leading-order x-averaged positive electrode porosity', 'Leading-order x-averaged positive electrode porosity change', 'Leading-order x-averaged positive electrode tortuosity', 'Leading-order x-averaged positive electrolyte tortuosity', 'Leading-order x-averaged separator active material volume fraction', 'Leading-order x-averaged separator porosity', 'Leading-order x-averaged separator porosity change', 'Leading-order x-averaged separator tortuosity', 'Local voltage', 'Local voltage [V]', 'Measured battery open circuit voltage [V]', 'Measured open circuit voltage', 'Measured open circuit voltage [V]', 'Negative current collector potential', 'Negative current collector potential [V]', 'Negative current collector temperature', 'Negative current collector temperature [K]', 'Negative electrode active material volume fraction', 'Negative electrode active volume fraction', 'Negative electrode average extent of lithiation', 'Negative electrode current density', 'Negative electrode current density [A.m-2]', 'Negative electrode entropic change', 'Negative electrode exchange current density', 'Negative electrode exchange current density [A.m-2]', 'Negative electrode exchange current density per volume [A.m-3]', 'Negative electrode interfacial current density', 'Negative electrode interfacial current density [A.m-2]', 'Negative electrode interfacial current density per volume [A.m-3]', 'Negative electrode ohmic losses', 'Negative electrode ohmic losses [V]', 'Negative electrode open circuit potential', 'Negative electrode open circuit potential [V]', 'Negative electrode porosity', 'Negative electrode porosity change', 'Negative electrode potential', 'Negative electrode potential [V]', 'Negative electrode reaction overpotential', 'Negative electrode reaction overpotential [V]', 'Negative electrode surface potential difference', 'Negative electrode surface potential difference [V]', 'Negative electrode temperature', 'Negative electrode temperature [K]', 'Negative electrode tortuosity', 'Negative electrode volume-averaged concentration', 'Negative electrode volume-averaged concentration [mol.m-3]', 'Negative electrolyte concentration', 'Negative electrolyte concentration [Molar]', 'Negative electrolyte concentration [mol.m-3]', 'Negative electrolyte current density', 'Negative electrolyte current density [A.m-2]', 'Negative electrolyte potential', 'Negative electrolyte potential [V]', 'Negative electrolyte tortuosity', 'Negative particle concentration', 'Negative particle concentration [mol.m-3]', 'Negative particle flux', 'Negative particle surface concentration', 'Negative particle surface concentration [mol.m-3]', 'Ohmic heating', 'Ohmic heating [W.m-3]', 'Porosity', 'Porosity change', 'Positive current collector potential', 'Positive current collector potential [V]', 'Positive current collector temperature', 'Positive current collector temperature [K]', 'Positive electrode active material volume fraction', 'Positive electrode active volume fraction', 'Positive electrode average extent of lithiation', 'Positive electrode current density', 'Positive electrode current density [A.m-2]', 'Positive electrode entropic change', 'Positive electrode exchange current density', 'Positive electrode exchange current density [A.m-2]', 'Positive electrode exchange current density per volume [A.m-3]', 'Positive electrode interfacial current density', 'Positive electrode interfacial current density [A.m-2]', 'Positive electrode interfacial current density per volume [A.m-3]', 'Positive electrode ohmic losses', 'Positive electrode ohmic losses [V]', 'Positive electrode open circuit potential', 'Positive electrode open circuit potential [V]', 'Positive electrode porosity', 'Positive electrode porosity change', 'Positive electrode potential', 'Positive electrode potential [V]', 'Positive electrode reaction overpotential', 'Positive electrode reaction overpotential [V]', 'Positive electrode surface potential difference', 'Positive electrode surface potential difference [V]', 'Positive electrode temperature', 'Positive electrode temperature [K]', 'Positive electrode tortuosity', 'Positive electrode volume-averaged concentration', 'Positive electrode volume-averaged concentration [mol.m-3]', 'Positive electrolyte concentration', 'Positive electrolyte concentration [Molar]', 'Positive electrolyte concentration [mol.m-3]', 'Positive electrolyte current density', 'Positive electrolyte current density [A.m-2]', 'Positive electrolyte potential', 'Positive electrolyte potential [V]', 'Positive electrolyte tortuosity', 'Positive particle concentration', 'Positive particle concentration [mol.m-3]', 'Positive particle flux', 'Positive particle surface concentration', 'Positive particle surface concentration [mol.m-3]', 'Reversible heating', 'Reversible heating [W.m-3]', 'Separator active material volume fraction', 'Separator electrolyte concentration', 'Separator electrolyte concentration [Molar]', 'Separator electrolyte concentration [mol.m-3]', 'Separator electrolyte potential', 'Separator electrolyte potential [V]', 'Separator porosity', 'Separator porosity change', 'Separator temperature', 'Separator temperature [K]', 'Separator tortuosity', 'Terminal power [W]', 'Terminal voltage', 'Terminal voltage [V]', 'Time', 'Time [h]', 'Time [min]', 'Time [s]', 'Total current density', 'Total current density [A.m-2]', 'Total heating', 'Total heating [W.m-3]', 'Volume-averaged cell temperature', 'Volume-averaged cell temperature [K]', 'Volume-averaged total heating', 'Volume-averaged total heating [W.m-3]', 'Volume-averaged velocity', 'Volume-averaged velocity [m.s-1]', 'X-averaged battery concentration overpotential [V]', 'X-averaged battery electrolyte ohmic losses [V]', 'X-averaged battery open circuit voltage [V]', 'X-averaged battery reaction overpotential [V]', 'X-averaged battery solid phase ohmic losses [V]', 'X-averaged cell temperature', 'X-averaged cell temperature [K]', 'X-averaged concentration overpotential', 'X-averaged concentration overpotential [V]', 'X-averaged electrolyte concentration', 'X-averaged electrolyte concentration [Molar]', 'X-averaged electrolyte concentration [mol.m-3]', 'X-averaged electrolyte ohmic losses', 'X-averaged electrolyte ohmic losses [V]', 'X-averaged electrolyte overpotential', 'X-averaged electrolyte overpotential [V]', 'X-averaged electrolyte potential', 'X-averaged electrolyte potential [V]', 'X-averaged negative electrode active material volume fraction', 'X-averaged negative electrode entropic change', 'X-averaged negative electrode exchange current density', 'X-averaged negative electrode exchange current density [A.m-2]', 'X-averaged negative electrode exchange current density per volume [A.m-3]', 'X-averaged negative electrode interfacial current density', 'X-averaged negative electrode interfacial current density [A.m-2]', 'X-averaged negative electrode interfacial current density per volume [A.m-3]', 'X-averaged negative electrode ohmic losses', 'X-averaged negative electrode ohmic losses [V]', 'X-averaged negative electrode open circuit potential', 'X-averaged negative electrode open circuit potential [V]', 'X-averaged negative electrode porosity', 'X-averaged negative electrode porosity change', 'X-averaged negative electrode potential', 'X-averaged negative electrode potential [V]', 'X-averaged negative electrode reaction overpotential', 'X-averaged negative electrode reaction overpotential [V]', 'X-averaged negative electrode surface potential difference', 'X-averaged negative electrode surface potential difference [V]', 'X-averaged negative electrode temperature', 'X-averaged negative electrode temperature [K]', 'X-averaged negative electrode tortuosity', 'X-averaged negative electrode total interfacial current density', 'X-averaged negative electrode total interfacial current density [A.m-2]', 'X-averaged negative electrode total interfacial current density per volume [A.m-3]', 'X-averaged negative electrolyte concentration', 'X-averaged negative electrolyte concentration [mol.m-3]', 'X-averaged negative electrolyte potential', 'X-averaged negative electrolyte potential [V]', 'X-averaged negative electrolyte tortuosity', 'X-averaged negative particle concentration', 'X-averaged negative particle concentration [mol.m-3]', 'X-averaged negative particle flux', 'X-averaged negative particle surface concentration', 'X-averaged negative particle surface concentration [mol.m-3]', 'X-averaged open circuit voltage', 'X-averaged open circuit voltage [V]', 'X-averaged porosity change', 'X-averaged positive electrode active material volume fraction', 'X-averaged positive electrode entropic change', 'X-averaged positive electrode exchange current density', 'X-averaged positive electrode exchange current density [A.m-2]', 'X-averaged positive electrode exchange current density per volume [A.m-3]', 'X-averaged positive electrode interfacial current density', 'X-averaged positive electrode interfacial current density [A.m-2]', 'X-averaged positive electrode interfacial current density per volume [A.m-3]', 'X-averaged positive electrode ohmic losses', 'X-averaged positive electrode ohmic losses [V]', 'X-averaged positive electrode open circuit potential', 'X-averaged positive electrode open circuit potential [V]', 'X-averaged positive electrode porosity', 'X-averaged positive electrode porosity change', 'X-averaged positive electrode potential', 'X-averaged positive electrode potential [V]', 'X-averaged positive electrode reaction overpotential', 'X-averaged positive electrode reaction overpotential [V]', 'X-averaged positive electrode surface potential difference', 'X-averaged positive electrode surface potential difference [V]', 'X-averaged positive electrode temperature', 'X-averaged positive electrode temperature [K]', 'X-averaged positive electrode tortuosity', 'X-averaged positive electrode total interfacial current density', 'X-averaged positive electrode total interfacial current density [A.m-2]', 'X-averaged positive electrode total interfacial current density per volume [A.m-3]', 'X-averaged positive electrolyte concentration', 'X-averaged positive electrolyte concentration [mol.m-3]', 'X-averaged positive electrolyte potential', 'X-averaged positive electrolyte potential [V]', 'X-averaged positive electrolyte tortuosity', 'X-averaged positive particle concentration', 'X-averaged positive particle concentration [mol.m-3]', 'X-averaged positive particle flux', 'X-averaged positive particle surface concentration', 'X-averaged positive particle surface concentration [mol.m-3]', 'X-averaged reaction overpotential', 'X-averaged reaction overpotential [V]', 'X-averaged separator active material volume fraction', 'X-averaged separator electrolyte concentration', 'X-averaged separator electrolyte concentration [mol.m-3]', 'X-averaged separator electrolyte potential', 'X-averaged separator electrolyte potential [V]', 'X-averaged separator porosity', 'X-averaged separator porosity change', 'X-averaged separator temperature', 'X-averaged separator temperature [K]', 'X-averaged separator tortuosity', 'X-averaged solid phase ohmic losses', 'X-averaged solid phase ohmic losses [V]', 'X-averaged total heating', 'X-averaged total heating [W.m-3]', 'r_n', 'r_n [m]', 'r_p', 'r_p [m]', 'x', 'x [m]', 'x_n', 'x_n [m]', 'x_p', 'x_p [m]', 'x_s', 'x_s [m]']\n" + "['Active material volume fraction', 'Ambient temperature', 'Ambient temperature [K]', 'Battery voltage [V]', 'C-rate', 'Cell temperature', 'Cell temperature [K]', 'Current [A]', 'Current collector current density', 'Current collector current density [A.m-2]', 'Discharge capacity [A.h]', 'Electrode current density', 'Electrode tortuosity', 'Electrolyte concentration', 'Electrolyte concentration [Molar]', 'Electrolyte concentration [mol.m-3]', 'Electrolyte current density', 'Electrolyte current density [A.m-2]', 'Electrolyte flux', 'Electrolyte flux [mol.m-2.s-1]', 'Electrolyte potential', 'Electrolyte potential [V]', 'Electrolyte pressure', 'Electrolyte tortuosity', 'Exchange current density', 'Exchange current density [A.m-2]', 'Exchange current density per volume [A.m-3]', 'Gradient of electrolyte potential', 'Gradient of negative electrode potential', 'Gradient of negative electrolyte potential', 'Gradient of positive electrode potential', 'Gradient of positive electrolyte potential', 'Gradient of separator electrolyte potential', 'Heat flux', 'Heat flux [W.m-2]', 'Interfacial current density', 'Interfacial current density [A.m-2]', 'Interfacial current density per volume [A.m-3]', 'Irreversible electrochemical heating', 'Irreversible electrochemical heating [W.m-3]', 'Leading-order active material volume fraction', 'Leading-order current collector current density', 'Leading-order electrode tortuosity', 'Leading-order electrolyte tortuosity', 'Leading-order negative electrode active material volume fraction', 'Leading-order negative electrode porosity', 'Leading-order negative electrode tortuosity', 'Leading-order negative electrolyte tortuosity', 'Leading-order porosity', 'Leading-order positive electrode active material volume fraction', 'Leading-order positive electrode porosity', 'Leading-order positive electrode tortuosity', 'Leading-order positive electrolyte tortuosity', 'Leading-order separator active material volume fraction', 'Leading-order separator porosity', 'Leading-order separator tortuosity', 'Leading-order x-averaged negative electrode active material volume fraction', 'Leading-order x-averaged negative electrode porosity', 'Leading-order x-averaged negative electrode porosity change', 'Leading-order x-averaged negative electrode tortuosity', 'Leading-order x-averaged negative electrolyte tortuosity', 'Leading-order x-averaged positive electrode active material volume fraction', 'Leading-order x-averaged positive electrode porosity', 'Leading-order x-averaged positive electrode porosity change', 'Leading-order x-averaged positive electrode tortuosity', 'Leading-order x-averaged positive electrolyte tortuosity', 'Leading-order x-averaged separator active material volume fraction', 'Leading-order x-averaged separator porosity', 'Leading-order x-averaged separator porosity change', 'Leading-order x-averaged separator tortuosity', 'Local voltage', 'Local voltage [V]', 'Measured battery open circuit voltage [V]', 'Measured open circuit voltage', 'Measured open circuit voltage [V]', 'Negative current collector potential', 'Negative current collector potential [V]', 'Negative current collector temperature', 'Negative current collector temperature [K]', 'Negative electrode active material volume fraction', 'Negative electrode active volume fraction', 'Negative electrode average extent of lithiation', 'Negative electrode current density', 'Negative electrode current density [A.m-2]', 'Negative electrode entropic change', 'Negative electrode exchange current density', 'Negative electrode exchange current density [A.m-2]', 'Negative electrode exchange current density per volume [A.m-3]', 'Negative electrode interfacial current density', 'Negative electrode interfacial current density [A.m-2]', 'Negative electrode interfacial current density per volume [A.m-3]', 'Negative electrode ohmic losses', 'Negative electrode ohmic losses [V]', 'Negative electrode open circuit potential', 'Negative electrode open circuit potential [V]', 'Negative electrode porosity', 'Negative electrode porosity change', 'Negative electrode potential', 'Negative electrode potential [V]', 'Negative electrode reaction overpotential', 'Negative electrode reaction overpotential [V]', 'Negative electrode surface potential difference', 'Negative electrode surface potential difference [V]', 'Negative electrode temperature', 'Negative electrode temperature [K]', 'Negative electrode tortuosity', 'Negative electrode volume-averaged concentration', 'Negative electrode volume-averaged concentration [mol.m-3]', 'Negative electrolyte concentration', 'Negative electrolyte concentration [Molar]', 'Negative electrolyte concentration [mol.m-3]', 'Negative electrolyte current density', 'Negative electrolyte current density [A.m-2]', 'Negative electrolyte potential', 'Negative electrolyte potential [V]', 'Negative electrolyte tortuosity', 'Negative particle concentration', 'Negative particle concentration [mol.m-3]', 'Negative particle flux', 'Negative particle surface concentration', 'Negative particle surface concentration [mol.m-3]', 'Ohmic heating', 'Ohmic heating [W.m-3]', 'Porosity', 'Porosity change', 'Positive current collector potential', 'Positive current collector potential [V]', 'Positive current collector temperature', 'Positive current collector temperature [K]', 'Positive electrode active material volume fraction', 'Positive electrode active volume fraction', 'Positive electrode average extent of lithiation', 'Positive electrode current density', 'Positive electrode current density [A.m-2]', 'Positive electrode entropic change', 'Positive electrode exchange current density', 'Positive electrode exchange current density [A.m-2]', 'Positive electrode exchange current density per volume [A.m-3]', 'Positive electrode interfacial current density', 'Positive electrode interfacial current density [A.m-2]', 'Positive electrode interfacial current density per volume [A.m-3]', 'Positive electrode ohmic losses', 'Positive electrode ohmic losses [V]', 'Positive electrode open circuit potential', 'Positive electrode open circuit potential [V]', 'Positive electrode porosity', 'Positive electrode porosity change', 'Positive electrode potential', 'Positive electrode potential [V]', 'Positive electrode reaction overpotential', 'Positive electrode reaction overpotential [V]', 'Positive electrode surface potential difference', 'Positive electrode surface potential difference [V]', 'Positive electrode temperature', 'Positive electrode temperature [K]', 'Positive electrode tortuosity', 'Positive electrode volume-averaged concentration', 'Positive electrode volume-averaged concentration [mol.m-3]', 'Positive electrolyte concentration', 'Positive electrolyte concentration [Molar]', 'Positive electrolyte concentration [mol.m-3]', 'Positive electrolyte current density', 'Positive electrolyte current density [A.m-2]', 'Positive electrolyte potential', 'Positive electrolyte potential [V]', 'Positive electrolyte tortuosity', 'Positive particle concentration', 'Positive particle concentration [mol.m-3]', 'Positive particle flux', 'Positive particle surface concentration', 'Positive particle surface concentration [mol.m-3]', 'Reversible heating', 'Reversible heating [W.m-3]', 'Separator active material volume fraction', 'Separator electrolyte concentration', 'Separator electrolyte concentration [Molar]', 'Separator electrolyte concentration [mol.m-3]', 'Separator electrolyte potential', 'Separator electrolyte potential [V]', 'Separator porosity', 'Separator porosity change', 'Separator temperature', 'Separator temperature [K]', 'Separator tortuosity', 'Terminal power [W]', 'Terminal voltage', 'Terminal voltage [V]', 'Time', 'Time [h]', 'Time [min]', 'Time [s]', 'Total current density', 'Total current density [A.m-2]', 'Total heating', 'Total heating [W.m-3]', 'Volume-averaged cell temperature', 'Volume-averaged cell temperature [K]', 'Volume-averaged total heating', 'Volume-averaged total heating [W.m-3]', 'Volume-averaged velocity', 'Volume-averaged velocity [m.s-1]', 'X-averaged battery concentration overpotential [V]', 'X-averaged battery electrolyte ohmic losses [V]', 'X-averaged battery open circuit voltage [V]', 'X-averaged battery reaction overpotential [V]', 'X-averaged battery solid phase ohmic losses [V]', 'X-averaged cell temperature', 'X-averaged cell temperature [K]', 'X-averaged concentration overpotential', 'X-averaged concentration overpotential [V]', 'X-averaged electrolyte concentration', 'X-averaged electrolyte concentration [Molar]', 'X-averaged electrolyte concentration [mol.m-3]', 'X-averaged electrolyte ohmic losses', 'X-averaged electrolyte ohmic losses [V]', 'X-averaged electrolyte overpotential', 'X-averaged electrolyte overpotential [V]', 'X-averaged electrolyte potential', 'X-averaged electrolyte potential [V]', 'X-averaged negative electrode active material volume fraction', 'X-averaged negative electrode entropic change', 'X-averaged negative electrode exchange current density', 'X-averaged negative electrode exchange current density [A.m-2]', 'X-averaged negative electrode exchange current density per volume [A.m-3]', 'X-averaged negative electrode interfacial current density', 'X-averaged negative electrode interfacial current density [A.m-2]', 'X-averaged negative electrode interfacial current density per volume [A.m-3]', 'X-averaged negative electrode ohmic losses', 'X-averaged negative electrode ohmic losses [V]', 'X-averaged negative electrode open circuit potential', 'X-averaged negative electrode open circuit potential [V]', 'X-averaged negative electrode porosity', 'X-averaged negative electrode porosity change', 'X-averaged negative electrode potential', 'X-averaged negative electrode potential [V]', 'X-averaged negative electrode reaction overpotential', 'X-averaged negative electrode reaction overpotential [V]', 'X-averaged negative electrode surface potential difference', 'X-averaged negative electrode surface potential difference [V]', 'X-averaged negative electrode temperature', 'X-averaged negative electrode temperature [K]', 'X-averaged negative electrode tortuosity', 'X-averaged negative electrode total interfacial current density', 'X-averaged negative electrode total interfacial current density [A.m-2]', 'X-averaged negative electrode total interfacial current density per volume [A.m-3]', 'X-averaged negative electrolyte concentration', 'X-averaged negative electrolyte concentration [mol.m-3]', 'X-averaged negative electrolyte potential', 'X-averaged negative electrolyte potential [V]', 'X-averaged negative electrolyte tortuosity', 'X-averaged negative particle concentration', 'X-averaged negative particle concentration [mol.m-3]', 'X-averaged negative particle flux', 'X-averaged negative particle surface concentration', 'X-averaged negative particle surface concentration [mol.m-3]', 'X-averaged open circuit voltage', 'X-averaged open circuit voltage [V]', 'X-averaged porosity change', 'X-averaged positive electrode active material volume fraction', 'X-averaged positive electrode entropic change', 'X-averaged positive electrode exchange current density', 'X-averaged positive electrode exchange current density [A.m-2]', 'X-averaged positive electrode exchange current density per volume [A.m-3]', 'X-averaged positive electrode interfacial current density', 'X-averaged positive electrode interfacial current density [A.m-2]', 'X-averaged positive electrode interfacial current density per volume [A.m-3]', 'X-averaged positive electrode ohmic losses', 'X-averaged positive electrode ohmic losses [V]', 'X-averaged positive electrode open circuit potential', 'X-averaged positive electrode open circuit potential [V]', 'X-averaged positive electrode porosity', 'X-averaged positive electrode porosity change', 'X-averaged positive electrode potential', 'X-averaged positive electrode potential [V]', 'X-averaged positive electrode reaction overpotential', 'X-averaged positive electrode reaction overpotential [V]', 'X-averaged positive electrode surface potential difference', 'X-averaged positive electrode surface potential difference [V]', 'X-averaged positive electrode temperature', 'X-averaged positive electrode temperature [K]', 'X-averaged positive electrode tortuosity', 'X-averaged positive electrode total interfacial current density', 'X-averaged positive electrode total interfacial current density [A.m-2]', 'X-averaged positive electrode total interfacial current density per volume [A.m-3]', 'X-averaged positive electrolyte concentration', 'X-averaged positive electrolyte concentration [mol.m-3]', 'X-averaged positive electrolyte potential', 'X-averaged positive electrolyte potential [V]', 'X-averaged positive electrolyte tortuosity', 'X-averaged positive particle concentration', 'X-averaged positive particle concentration [mol.m-3]', 'X-averaged positive particle flux', 'X-averaged positive particle surface concentration', 'X-averaged positive particle surface concentration [mol.m-3]', 'X-averaged reaction overpotential', 'X-averaged reaction overpotential [V]', 'X-averaged separator active material volume fraction', 'X-averaged separator electrolyte concentration', 'X-averaged separator electrolyte concentration [mol.m-3]', 'X-averaged separator electrolyte potential', 'X-averaged separator electrolyte potential [V]', 'X-averaged separator porosity', 'X-averaged separator porosity change', 'X-averaged separator temperature', 'X-averaged separator temperature [K]', 'X-averaged separator tortuosity', 'X-averaged solid phase ohmic losses', 'X-averaged solid phase ohmic losses [V]', 'X-averaged total heating', 'X-averaged total heating [W.m-3]', 'r_n', 'r_n [m]', 'r_p', 'r_p [m]', 'x', 'x [m]', 'x_n', 'x_n [m]', 'x_p', 'x_p [m]', 'x_s', 'x_s [m]']\n" ] } ], @@ -172,7 +170,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 6, @@ -199,7 +197,7 @@ { "data": { "text/plain": [ - "dict_keys(['Negative particle surface concentration', 'Electrolyte concentration', 'Positive particle surface concentration', 'Current [A]', 'Negative electrode potential [V]', 'Electrolyte potential [V]', 'Positive electrode potential [V]', 'Terminal voltage [V]', 'Time [h]'])" + "dict_keys(['Negative particle surface concentration [mol.m-3]', 'Electrolyte concentration [mol.m-3]', 'Positive particle surface concentration [mol.m-3]', 'Current [A]', 'Negative electrode potential [V]', 'Electrolyte potential [V]', 'Positive electrode potential [V]', 'Terminal voltage [V]', 'Time [h]'])" ] }, "execution_count": 7, @@ -461,7 +459,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 15, @@ -491,13 +489,6 @@ ")\n", "plt.legend()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/notebooks/unsteady_heat_equation.ipynb b/examples/notebooks/unsteady_heat_equation.ipynb index 927d3e6eb1..008970af36 100644 --- a/examples/notebooks/unsteady_heat_equation.ipynb +++ b/examples/notebooks/unsteady_heat_equation.ipynb @@ -421,7 +421,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.3" + "version": "3.6.9" } }, "nbformat": 4, diff --git a/examples/notebooks/using-model-options_thermal-example.ipynb b/examples/notebooks/using-model-options_thermal-example.ipynb index 9f9ff48dda..eb70b9ffe1 100644 --- a/examples/notebooks/using-model-options_thermal-example.ipynb +++ b/examples/notebooks/using-model-options_thermal-example.ipynb @@ -140,12 +140,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "321d32d08b3f417caa1062450f00732c", + "model_id": "c3d04dd597c24caf83c370bd01fe6131", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=1.0, step=0.05), Output()), _dom_classes=('w…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=1.0, step=0.01), Output()), _dom_classes=('w…" ] }, "metadata": {}, @@ -160,9 +160,7 @@ " \"Cell temperature [K]\",\n", "]\n", "quick_plot = pybamm.QuickPlot(solution, output_variables)\n", - "\n", - "import ipywidgets as widgets\n", - "widgets.interact(quick_plot.plot, t=widgets.FloatSlider(min=0,max=1,step=0.05,value=0));" + "quick_plot.dynamic_plot();" ] }, { @@ -204,9 +202,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.8" + "version": "3.7.3" } }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/examples/notebooks/using-submodels.ipynb b/examples/notebooks/using-submodels.ipynb index e04871cc3c..ffe6892cba 100644 --- a/examples/notebooks/using-submodels.ipynb +++ b/examples/notebooks/using-submodels.ipynb @@ -61,21 +61,21 @@ "name": "stdout", "output_type": "stream", "text": [ - "external circuit \n", - "porosity \n", - "electrolyte tortuosity \n", - "electrode tortuosity \n", - "convection \n", - "negative interface \n", - "positive interface \n", - "negative particle \n", - "positive particle \n", - "negative electrode \n", - "electrolyte conductivity \n", - "electrolyte diffusion \n", - "positive electrode \n", - "thermal \n", - "current collector \n" + "external circuit \n", + "porosity \n", + "electrolyte tortuosity \n", + "electrode tortuosity \n", + "convection \n", + "negative interface \n", + "positive interface \n", + "negative particle \n", + "positive particle \n", + "negative electrode \n", + "leading-order electrolyte conductivity \n", + "electrolyte diffusion \n", + "positive electrode \n", + "thermal \n", + "current collector \n" ] } ], @@ -113,7 +113,7 @@ "metadata": {}, "outputs": [], "source": [ - "model.submodels[\"negative particle\"] = pybamm.particle.fast.SingleParticle(model.param, \"Negative\")" + "model.submodels[\"negative particle\"] = pybamm.particle.FastSingleParticle(model.param, \"Negative\")" ] }, { @@ -132,21 +132,21 @@ "name": "stdout", "output_type": "stream", "text": [ - "external circuit \n", - "porosity \n", - "electrolyte tortuosity \n", - "electrode tortuosity \n", - "convection \n", - "negative interface \n", - "positive interface \n", - "negative particle \n", - "positive particle \n", - "negative electrode \n", - "electrolyte conductivity \n", - "electrolyte diffusion \n", - "positive electrode \n", - "thermal \n", - "current collector \n" + "external circuit \n", + "porosity \n", + "electrolyte tortuosity \n", + "electrode tortuosity \n", + "convection \n", + "negative interface \n", + "positive interface \n", + "negative particle \n", + "positive particle \n", + "negative electrode \n", + "leading-order electrolyte conductivity \n", + "electrolyte diffusion \n", + "positive electrode \n", + "thermal \n", + "current collector \n" ] } ], @@ -213,9 +213,9 @@ { "data": { "text/plain": [ - "{Variable(0x65ae5959b31807d6, Discharge capacity [A.h], children=[], domain=[], auxiliary_domains={}): Division(0x5f50eeb4014eba6e, /, children=['Current function [A] * 96485.33289 * Maximum concentration in negative electrode [mol.m-3] * Negative electrode thickness [m] + Separator thickness [m] + Positive electrode thickness [m] / function (absolute)', '3600.0'], domain=[], auxiliary_domains={}),\n", - " Variable(-0x399950b4aedd9f35, X-averaged negative particle surface concentration, children=[], domain=['current collector'], auxiliary_domains={}): Division(-0x34c10cfd67aea950, /, children=['-3.0 * broadcast(Current function [A] / Typical current [A] * function (sign)) / Negative electrode thickness [m] / Negative electrode thickness [m] + Separator thickness [m] + Positive electrode thickness [m]', 'Negative electrode surface area density [m-1] * Negative particle radius [m]'], domain=['current collector'], auxiliary_domains={}),\n", - " Variable(0x17f4b3f07723f4ba, X-averaged positive particle concentration, children=[], domain=['positive particle'], auxiliary_domains={'secondary': \"['current collector']\"}): Multiplication(0x25c2a4426903dac0, *, children=['-1.0 / Positive particle radius [m] ** 2.0 / Positive electrode diffusivity [m2.s-1] / 96485.33289 * Maximum concentration in negative electrode [mol.m-3] * Negative electrode thickness [m] + Separator thickness [m] + Positive electrode thickness [m] / function (absolute)', 'div(-Positive electrode diffusivity [m2.s-1] / Positive electrode diffusivity [m2.s-1] * grad(X-averaged positive particle concentration))'], domain=['positive particle'], auxiliary_domains={'secondary': \"['current collector']\"})}" + "{Variable(-0x2c47f6fd051d1243, Discharge capacity [A.h], children=[], domain=[], auxiliary_domains={}): Division(-0x70df7b427b87dd10, /, children=['Current function [A] * 96485.33289 * Maximum concentration in negative electrode [mol.m-3] * Negative electrode thickness [m] + Separator thickness [m] + Positive electrode thickness [m] / function (absolute)', '3600.0'], domain=[], auxiliary_domains={}),\n", + " Variable(0x5e5659f748ce0d05, X-averaged negative particle surface concentration, children=[], domain=['current collector'], auxiliary_domains={}): Division(0x4b85bce951b678a1, /, children=['-3.0 * broadcast(Current function [A] / Typical current [A] * function (sign)) / Negative electrode thickness [m] / Negative electrode thickness [m] + Separator thickness [m] + Positive electrode thickness [m]', 'Negative electrode surface area density [m-1] * Negative particle radius [m]'], domain=['current collector'], auxiliary_domains={}),\n", + " Variable(0x69c21118c2fdf5f5, X-averaged positive particle concentration, children=[], domain=['positive particle'], auxiliary_domains={'secondary': \"['current collector']\"}): Multiplication(-0x6f1911180dfd65f9, *, children=['-1.0 / Positive particle radius [m] ** 2.0 / Positive electrode diffusivity [m2.s-1] / 96485.33289 * Maximum concentration in negative electrode [mol.m-3] * Negative electrode thickness [m] + Separator thickness [m] + Positive electrode thickness [m] / function (absolute)', 'div(-Positive electrode diffusivity [m2.s-1] / Positive electrode diffusivity [m2.s-1] * grad(X-averaged positive particle concentration))'], domain=['positive particle'], auxiliary_domains={'secondary': \"['current collector']\"})}" ] }, "execution_count": 9, @@ -370,10 +370,10 @@ "metadata": {}, "outputs": [], "source": [ - "model.submodels[\"negative particle\"] = pybamm.particle.fast.SingleParticle(\n", + "model.submodels[\"negative particle\"] = pybamm.particle.FastSingleParticle(\n", " model.param, \"Negative\"\n", ")\n", - "model.submodels[\"positive particle\"] = pybamm.particle.fast.SingleParticle(\n", + "model.submodels[\"positive particle\"] = pybamm.particle.FastSingleParticle(\n", " model.param, \"Positive\"\n", ")" ] @@ -382,7 +382,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In the Single Particle Model, the overpotential can be obtianed by inverting the Butler-Volmer relation, so we choose the `InverseButlerVolmer` submodel for the interface" + "In the Single Particle Model, the overpotential can be obtianed by inverting the Butler-Volmer relation, so we choose the `InverseButlerVolmer` submodel for the interface, with the \"main\" lithium-ion reaction" ] }, { @@ -393,10 +393,10 @@ "source": [ "model.submodels[\n", " \"negative interface\"\n", - "] = pybamm.interface.lithium_ion.InverseButlerVolmer(model.param, \"Negative\")\n", + "] = pybamm.interface.InverseButlerVolmer(model.param, \"Negative\", \"lithium-ion main\")\n", "model.submodels[\n", " \"positive interface\"\n", - "] = pybamm.interface.lithium_ion.InverseButlerVolmer(model.param, \"Positive\")\n" + "] = pybamm.interface.InverseButlerVolmer(model.param, \"Positive\", \"lithium-ion main\")\n" ] }, { diff --git a/examples/scripts/DFN.py b/examples/scripts/DFN.py index 9155d9eaa7..fb9c4563b9 100644 --- a/examples/scripts/DFN.py +++ b/examples/scripts/DFN.py @@ -5,7 +5,7 @@ import pybamm import numpy as np -pybamm.set_logging_level("DEBUG") +pybamm.set_logging_level("INFO") # load model @@ -36,5 +36,19 @@ solution = solver.solve(model, t_eval) # plot -plot = pybamm.QuickPlot(solution) +plot = pybamm.QuickPlot( + solution, + [ + "Negative particle concentration [mol.m-3]", + "Electrolyte concentration [mol.m-3]", + "Positive particle concentration [mol.m-3]", + "Current [A]", + "Negative electrode potential [V]", + "Electrolyte potential [V]", + "Positive electrode potential [V]", + "Terminal voltage [V]", + ], + time_unit="seconds", + spatial_unit="um", +) plot.dynamic_plot() diff --git a/examples/scripts/DFN_ambient_temperature.py b/examples/scripts/DFN_ambient_temperature.py new file mode 100644 index 0000000000..f50e066fc5 --- /dev/null +++ b/examples/scripts/DFN_ambient_temperature.py @@ -0,0 +1,52 @@ +# +# Example showing how to solve the DFN with a varying ambient temperature +# + +import pybamm +import numpy as np + +pybamm.set_logging_level("DEBUG") + + +# load model +options = {"thermal": "x-lumped"} +model = pybamm.lithium_ion.DFN(options) + +# create geometry +geometry = model.default_geometry + +# load parameter values and process model and geometry + + +def ambient_temperature(t): + return 300 + t * 100 / 3600 + + +param = model.default_parameter_values +param.update( + {"Ambient temperature [K]": ambient_temperature}, check_already_exists=False +) +param.process_model(model) +param.process_geometry(geometry) + +# set mesh +var = pybamm.standard_spatial_vars +var_pts = {var.x_n: 30, var.x_s: 30, var.x_p: 30, var.r_n: 10, var.r_p: 10} +mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) + +# discretise model +disc = pybamm.Discretisation(mesh, model.default_spatial_methods) +disc.process_model(model) + +# solve model +t_eval = np.linspace(0, 3600 / 2, 100) +solver = pybamm.CasadiSolver(mode="fast") +solver.rtol = 1e-3 +solver.atol = 1e-6 +solution = solver.solve(model, t_eval) + +# plot +plot = pybamm.QuickPlot( + solution, ["X-averaged cell temperature [K]", "Ambient temperature [K]"] +) +plot.dynamic_plot() diff --git a/examples/scripts/SPMe.py b/examples/scripts/SPMe.py index 364c7c0508..75921b13d2 100644 --- a/examples/scripts/SPMe.py +++ b/examples/scripts/SPMe.py @@ -5,7 +5,7 @@ import pybamm import numpy as np -pybamm.set_logging_level("DEBUG") +pybamm.set_logging_level("INFO") # load model model = pybamm.lithium_ion.SPMe() @@ -31,5 +31,19 @@ solution = model.default_solver.solve(model, t_eval) # plot -plot = pybamm.QuickPlot(solution) +plot = pybamm.QuickPlot( + solution, + [ + "Negative particle concentration [mol.m-3]", + "Electrolyte concentration [mol.m-3]", + "Positive particle concentration [mol.m-3]", + "Current [A]", + "Negative electrode potential [V]", + "Electrolyte potential [V]", + "Positive electrode potential [V]", + "Terminal voltage [V]", + ], + time_unit="seconds", + spatial_unit="um", +) plot.dynamic_plot() diff --git a/examples/scripts/compare_comsol/compare_comsol_DFN.py b/examples/scripts/compare_comsol/compare_comsol_DFN.py index 85878bfaaf..2f0b3a83d7 100644 --- a/examples/scripts/compare_comsol/compare_comsol_DFN.py +++ b/examples/scripts/compare_comsol/compare_comsol_DFN.py @@ -78,18 +78,22 @@ def get_interp_fun(variable_name, domain): def myinterp(t): try: return interp.interp1d( - comsol_t, variable, fill_value="extrapolate", bounds_error=False + comsol_t, variable, fill_value="extrapolate", bounds_error=False, )(t)[:, np.newaxis] except ValueError as err: raise ValueError( - """Failed to interpolate '{}' with time range [{}, {}] at time {}. - Original error: {}""".format( - variable_name, comsol_t[0], comsol_t[-1], t, err - ) + ( + "Failed to interpolate '{}' with time range [{}, {}] at time {}." + + "Original error: {}" + ).format(variable_name, comsol_t[0], comsol_t[-1], t, err) ) # Make sure to use dimensional time - fun = pybamm.Function(myinterp, pybamm.t, name=variable_name + "_comsol") + fun = pybamm.Function( + myinterp, + pybamm.t * pybamm_model.timescale.evaluate(), + name=variable_name + "_comsol", + ) fun.domain = domain fun.mesh = mesh.combine_submeshes(*domain) fun.secondary_mesh = None @@ -109,7 +113,7 @@ def myinterp(t): fill_value="extrapolate", bounds_error=False, ), - pybamm.t, + pybamm.t * pybamm_model.timescale.evaluate(), ) comsol_voltage.mesh = None comsol_voltage.secondary_mesh = None diff --git a/examples/scripts/compare_lead_acid.py b/examples/scripts/compare_lead_acid.py index fe9ea46768..4c2b1bad67 100644 --- a/examples/scripts/compare_lead_acid.py +++ b/examples/scripts/compare_lead_acid.py @@ -55,5 +55,5 @@ "Electrolyte potential [V]", "Terminal voltage [V]", ] -plot = pybamm.QuickPlot(solutions, output_variables) +plot = pybamm.QuickPlot(solutions, output_variables, linestyles=[":", "--", "-"]) plot.dynamic_plot() diff --git a/examples/scripts/compare_lithium_ion.py b/examples/scripts/compare_lithium_ion.py index 7f5dac6b58..342509f95d 100644 --- a/examples/scripts/compare_lithium_ion.py +++ b/examples/scripts/compare_lithium_ion.py @@ -16,7 +16,7 @@ pybamm.set_logging_level("INFO") # load models -options = {"thermal": "isothermal"} +options = {"thermal": "x-lumped"} models = [ pybamm.lithium_ion.SPM(options), pybamm.lithium_ion.SPMe(options), @@ -51,5 +51,5 @@ solutions[i] = model.default_solver.solve(model, t_eval) # plot -plot = pybamm.QuickPlot(solutions) +plot = pybamm.QuickPlot(solutions, linestyles=[":", "--", "-"]) plot.dynamic_plot() diff --git a/examples/scripts/compare_lithium_ion_3D.py b/examples/scripts/compare_lithium_ion_3D.py index a28a5ebce3..b09d0d6a38 100644 --- a/examples/scripts/compare_lithium_ion_3D.py +++ b/examples/scripts/compare_lithium_ion_3D.py @@ -20,9 +20,9 @@ pybamm.lithium_ion.SPM( {"current collector": "potential pair", "dimensionality": 2}, name="2+1D SPM" ), - pybamm.lithium_ion.SPMe( - {"current collector": "potential pair", "dimensionality": 2}, name="2+1D SPMe" - ), + # pybamm.lithium_ion.SPMe( + # {"current collector": "potential pair", "dimensionality": 2}, name="2+1D SPMe" + # ), ] # load parameter values and process models @@ -56,7 +56,6 @@ solutions[i] = solution # plot -# TO DO: plotting 3D variables -output_variables = ["Terminal voltage [V]"] +output_variables = ["Terminal voltage [V]", "Negative current collector potential [V]"] plot = pybamm.QuickPlot(solutions, output_variables) plot.dynamic_plot() diff --git a/examples/scripts/create-model.py b/examples/scripts/create-model.py index 8c2d702835..73b9161351 100644 --- a/examples/scripts/create-model.py +++ b/examples/scripts/create-model.py @@ -18,7 +18,7 @@ def D_dim(cc): - return pybamm.FunctionParameter("Diffusivity", cc) + return pybamm.FunctionParameter("Diffusivity", {"Concentration [mol.m-3]": cc}) # dimensionless parameters diff --git a/examples/scripts/custom_model.py b/examples/scripts/custom_model.py index 5e6f084e2c..0a6429357a 100644 --- a/examples/scripts/custom_model.py +++ b/examples/scripts/custom_model.py @@ -22,18 +22,18 @@ model.submodels["positive electrode"] = pybamm.electrode.ohm.LeadingOrder( model.param, "Positive" ) -model.submodels["negative particle"] = pybamm.particle.fast.SingleParticle( +model.submodels["negative particle"] = pybamm.particle.FastSingleParticle( model.param, "Negative" ) -model.submodels["positive particle"] = pybamm.particle.fast.SingleParticle( +model.submodels["positive particle"] = pybamm.particle.FastSingleParticle( model.param, "Positive" ) -model.submodels[ - "negative interface" -] = pybamm.interface.lithium_ion.InverseButlerVolmer(model.param, "Negative") -model.submodels[ - "positive interface" -] = pybamm.interface.lithium_ion.InverseButlerVolmer(model.param, "Positive") +model.submodels["negative interface"] = pybamm.interface.InverseButlerVolmer( + model.param, "Negative", "lithium-ion main" +) +model.submodels["positive interface"] = pybamm.interface.InverseButlerVolmer( + model.param, "Positive", "lithium-ion main" +) electrolyte = pybamm.electrolyte.stefan_maxwell model.submodels["electrolyte diffusion"] = electrolyte.diffusion.ConstantConcentration( model.param diff --git a/examples/scripts/drive_cycle.py b/examples/scripts/drive_cycle.py index 1e9c708dad..f04d24a19a 100644 --- a/examples/scripts/drive_cycle.py +++ b/examples/scripts/drive_cycle.py @@ -3,15 +3,29 @@ # import pybamm +pybamm.set_logging_level("INFO") + # load model and update parameters so the input current is the US06 drive cycle -model = pybamm.lithium_ion.DFN() +model = pybamm.lithium_ion.SPMe({"thermal": "x-lumped"}) param = model.default_parameter_values param["Current function [A]"] = "[current data]US06" # create and run simulation using the CasadiSolver in "fast" mode, remembering to # pass in the updated parameters sim = pybamm.Simulation( - model, parameter_values=param, solver=pybamm.CasadiSolver(mode="fast") + model, parameter_values=param, solver=pybamm.CasadiSolver(mode="fast"), ) sim.solve() -sim.plot() +sim.plot( + [ + "Negative particle surface concentration [mol.m-3]", + "Electrolyte concentration [mol.m-3]", + "Positive particle surface concentration [mol.m-3]", + "Current [A]", + "Negative electrode potential [V]", + "Electrolyte potential [V]", + "Positive electrode potential [V]", + "Terminal voltage [V]", + "X-averaged cell temperature", + ] +) diff --git a/examples/scripts/rate_capability.py b/examples/scripts/rate_capability.py index abfa578445..692264313e 100644 --- a/examples/scripts/rate_capability.py +++ b/examples/scripts/rate_capability.py @@ -16,13 +16,9 @@ for i, C_rate in enumerate(C_rates): experiment = pybamm.Experiment( ["Discharge at {:.4f}C until 3.2V".format(C_rate)], - period="{:.4f} seconds".format(10 / C_rate) - ) - sim = pybamm.Simulation( - model, - experiment=experiment, - solver=pybamm.CasadiSolver() + period="{:.4f} seconds".format(10 / C_rate), ) + sim = pybamm.Simulation(model, experiment=experiment, solver=pybamm.CasadiSolver()) sim.solve() capacity = sim.solution["Discharge capacity [A.h]"] @@ -35,12 +31,12 @@ plt.figure(1) plt.scatter(C_rates, capacities) -plt.xlabel('C-rate') -plt.ylabel('Capacity [Ah]') +plt.xlabel("C-rate") +plt.ylabel("Capacity [Ah]") plt.figure(2) plt.scatter(currents * voltage_av, capacities * voltage_av) -plt.xlabel('Power [W]') -plt.ylabel('Energy [Wh]') +plt.xlabel("Power [W]") +plt.ylabel("Energy [Wh]") plt.show() diff --git a/pybamm/CITATIONS.txt b/pybamm/CITATIONS.txt index 416790a95c..318c1fcd85 100644 --- a/pybamm/CITATIONS.txt +++ b/pybamm/CITATIONS.txt @@ -135,3 +135,37 @@ year={2020} doi = {10.5281/zenodo.3597656}, url = {https://doi.org/10.5281/zenodo.3597656} } + +@article{ecker2015i, + title={Parameterization of a physico-chemical model of a lithium-ion battery i. determination of parameters}, + author={Ecker, Madeleine and Tran, Thi Kim Dung and Dechent, Philipp and K{\"a}bitz, Stefan and Warnecke, Alexander and Sauer, Dirk Uwe}, + journal={Journal of the Electrochemical Society}, + volume={162}, + number={9}, + pages={A1836--A1848}, + year={2015}, + publisher={The Electrochemical Society} +} + +@article{ecker2015ii, + title={Parameterization of a physico-chemical model of a lithium-ion battery ii. model validation}, + author={Ecker, Madeleine and K{\"a}bitz, Stefan and Laresgoiti, Izaro and Sauer, Dirk Uwe}, + journal={Journal of The Electrochemical Society}, + volume={162}, + number={9}, + pages={A1849--A1857}, + year={2015}, + publisher={The Electrochemical Society} +} + +@article{richardson2020, +title = "Generalised single particle models for high-rate operation of graded lithium-ion electrodes: Systematic derivation and validation", +journal = "Electrochimica Acta", +volume = "339", +pages = "135862", +year = "2020", +issn = "0013-4686", +doi = "https://doi.org/10.1016/j.electacta.2020.135862", +url = "http://www.sciencedirect.com/science/article/pii/S0013468620302541", +author = "G. Richardson and I. Korotkin and R. Ranom and M. Castle and J.M. Foster", +} diff --git a/pybamm/__init__.py b/pybamm/__init__.py index da9022e60a..9caa190c1e 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -65,33 +65,9 @@ def version(formatted=False): # # Classes for the Expression Tree # -from .expression_tree.symbol import ( - Symbol, - domain_size, - create_object_of_size, - evaluate_for_shape_using_domain, -) -from .expression_tree.binary_operators import ( - is_scalar_zero, - is_matrix_zero, - BinaryOperator, - Addition, - Power, - Subtraction, - Multiplication, - MatrixMultiplication, - Division, - Inner, - inner, - Heaviside, - source, -) -from .expression_tree.concatenations import ( - Concatenation, - NumpyConcatenation, - DomainConcatenation, - SparseStack, -) +from .expression_tree.symbol import * +from .expression_tree.binary_operators import * +from .expression_tree.concatenations import * from .expression_tree.array import Array from .expression_tree.matrix import Matrix from .expression_tree.unary_operators import * @@ -99,36 +75,16 @@ def version(formatted=False): from .expression_tree.interpolant import Interpolant from .expression_tree.input_parameter import InputParameter from .expression_tree.parameter import Parameter, FunctionParameter -from .expression_tree.broadcasts import ( - Broadcast, - PrimaryBroadcast, - SecondaryBroadcast, - FullBroadcast, - ones_like, -) +from .expression_tree.broadcasts import * from .expression_tree.scalar import Scalar -from .expression_tree.variable import Variable, ExternalVariable -from .expression_tree.independent_variable import ( - IndependentVariable, - Time, - SpatialVariable, -) +from .expression_tree.variable import Variable, ExternalVariable, VariableDot +from .expression_tree.variable import VariableBase +from .expression_tree.independent_variable import * from .expression_tree.independent_variable import t from .expression_tree.vector import Vector -from .expression_tree.state_vector import StateVector +from .expression_tree.state_vector import StateVectorBase, StateVector, StateVectorDot -from .expression_tree.exceptions import ( - DomainError, - OptionError, - ModelError, - SolverError, - SolverWarning, - ShapeError, - ModelWarning, - UndefinedOperationError, - GeometryError, - InputError, -) +from .expression_tree.exceptions import * # Operations from .expression_tree.operations.simplify import ( @@ -193,7 +149,7 @@ def version(formatted=False): Geometry2DCurrentCollector, ) -from .expression_tree.independent_variable import KNOWN_SPATIAL_VARS, KNOWN_COORD_SYS +from .expression_tree.independent_variable import KNOWN_COORD_SYS from .geometry import standard_spatial_vars # @@ -243,8 +199,10 @@ def version(formatted=False): # from .solvers.solution import Solution, _BaseSolution from .solvers.base_solver import BaseSolver +from .solvers.dummy_solver import DummySolver from .solvers.algebraic_solver import AlgebraicSolver from .solvers.casadi_solver import CasadiSolver +from .solvers.casadi_algebraic_solver import CasadiAlgebraicSolver from .solvers.scikits_dae_solver import ScikitsDaeSolver from .solvers.scikits_ode_solver import ScikitsOdeSolver, have_scikits_odes from .solvers.scipy_solver import ScipySolver @@ -260,9 +218,9 @@ def version(formatted=False): # other # from .processed_variable import ProcessedVariable -from .quick_plot import QuickPlot, ax_min, ax_max +from .quick_plot import QuickPlot, dynamic_plot, ax_min, ax_max -from .simulation import Simulation, load_sim +from .simulation import Simulation, load_sim, is_notebook # # Remove any imported modules, so we don't expose them as part of pybamm diff --git a/pybamm/discretisations/discretisation.py b/pybamm/discretisations/discretisation.py index b74ac08a72..9a84cffd3e 100644 --- a/pybamm/discretisations/discretisation.py +++ b/pybamm/discretisations/discretisation.py @@ -113,14 +113,19 @@ def process_model(self, model, inplace=True, check_model=True): Raises ------ :class:`pybamm.ModelError` - If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) + If an empty model is passed (`model.rhs = {}` and `model.algebraic = {}` and + `model.variables = {}`) """ pybamm.logger.info("Start discretising {}".format(model.name)) # Make sure model isn't empty - if len(model.rhs) == 0 and len(model.algebraic) == 0: + if ( + len(model.rhs) == 0 + and len(model.algebraic) == 0 + and len(model.variables) == 0 + ): raise pybamm.ModelError("Cannot discretise empty model") # Check well-posedness to avoid obscure errors model.check_well_posedness() @@ -132,6 +137,8 @@ def process_model(self, model, inplace=True, check_model=True): # Set the y split for variables pybamm.logger.info("Set variable slices for {}".format(model.name)) self.set_variable_slices(variables) + # Keep a record of y_slices in the model + model.y_slices = self.y_slices_explicit # now add extrapolated external variables to the boundary conditions # if required by the spatial method @@ -209,6 +216,7 @@ def set_variable_slices(self, variables): """ # Set up y_slices y_slices = defaultdict(list) + y_slices_explicit = defaultdict(list) start = 0 end = 0 # Iterate through unpacked variables, adding appropriate slices to y_slices @@ -226,15 +234,20 @@ def set_variable_slices(self, variables): for child, mesh in meshes.items(): for domain_mesh in mesh: submesh = domain_mesh[i] - end += submesh.npts_for_broadcast + end += submesh.npts_for_broadcast_to_nodes y_slices[child.id].append(slice(start, end)) + y_slices_explicit[child].append(slice(start, end)) start = end else: end += self._get_variable_size(variable) y_slices[variable.id].append(slice(start, end)) + y_slices_explicit[variable].append(slice(start, end)) start = end - self.y_slices = y_slices + # Convert y_slices back to normal dictionary + self.y_slices = dict(y_slices) + # Also keep a record of what the y_slices are, to be stored in the model + self.y_slices_explicit = dict(y_slices_explicit) # reset discretised_symbols self._discretised_symbols = {} @@ -248,7 +261,7 @@ def _get_variable_size(self, variable): size = 0 for dom in variable.domain: for submesh in self.spatial_methods[dom].mesh[dom]: - size += submesh.npts_for_broadcast + size += submesh.npts_for_broadcast_to_nodes return size def _preprocess_external_variables(self, model): @@ -514,6 +527,7 @@ def process_rhs_and_algebraic(self, model): equations) and processed_concatenated_algebraic """ + # Discretise right-hand sides, passing domain from variable processed_rhs = self.process_dict(model.rhs) @@ -614,12 +628,16 @@ def create_mass_matrix(self, model): mass_algebraic = csr_matrix((mass_algebraic_size, mass_algebraic_size)) mass_list.append(mass_algebraic) - # Create block diagonal (sparse) mass matrix and inverse (if model has odes) - mass_matrix = pybamm.Matrix(block_diag(mass_list, format="csr")) - if model.rhs.keys(): - mass_matrix_inv = pybamm.Matrix(block_diag(mass_inv_list, format="csr")) + # Create block diagonal (sparse) mass matrix (if model is not empty) + # and inverse (if model has odes) + if len(model.rhs) + len(model.algebraic) > 0: + mass_matrix = pybamm.Matrix(block_diag(mass_list, format="csr")) + if len(model.rhs) > 0: + mass_matrix_inv = pybamm.Matrix(block_diag(mass_inv_list, format="csr")) + else: + mass_matrix_inv = None else: - mass_matrix_inv = None + mass_matrix, mass_matrix_inv = None, None return mass_matrix, mass_matrix_inv @@ -855,6 +873,13 @@ def _process_symbol(self, symbol): disc_children = [self.process_symbol(child) for child in symbol.children] return symbol._function_new_copy(disc_children) + elif isinstance(symbol, pybamm.VariableDot): + return pybamm.StateVectorDot( + *self.y_slices[symbol.get_variable().id], + domain=symbol.domain, + auxiliary_domains=symbol.auxiliary_domains + ) + elif isinstance(symbol, pybamm.Variable): # Check if variable is a standard variable or an external variable if any(symbol.id == var.id for var in self.external_variables.values()): @@ -885,8 +910,23 @@ def _process_symbol(self, symbol): return out else: + # add a try except block for a more informative error if a variable + # can't be found. This should usually be caught earlier by + # model.check_well_posedness, but won't be if debug_mode is False + try: + y_slices = self.y_slices[symbol.id] + except KeyError: + raise pybamm.ModelError( + """ + No key set for variable '{}'. Make sure it is included in either + model.rhs, model.algebraic, or model.external_variables in an + unmodified form (e.g. not Broadcasted) + """.format( + symbol.name + ) + ) return pybamm.StateVector( - *self.y_slices[symbol.id], + *y_slices, domain=symbol.domain, auxiliary_domains=symbol.auxiliary_domains ) @@ -995,18 +1035,20 @@ def check_initial_conditions(self, model): # Individual for var, eqn in model.initial_conditions.items(): assert isinstance( - eqn.evaluate(t=0, u="shape test"), np.ndarray + eqn.evaluate(t=0, inputs="shape test"), np.ndarray ), pybamm.ModelError( """ initial_conditions must be numpy array after discretisation but they are {} for variable '{}'. """.format( - type(eqn.evaluate(t=0, u="shape test")), var + type(eqn.evaluate(t=0, inputs="shape test")), var ) ) # Concatenated assert ( - type(model.concatenated_initial_conditions.evaluate(t=0, u="shape test")) + type( + model.concatenated_initial_conditions.evaluate(t=0, inputs="shape test") + ) is np.ndarray ), pybamm.ModelError( """ diff --git a/pybamm/expression_tree/array.py b/pybamm/expression_tree/array.py index 348fbed616..6acdd07950 100644 --- a/pybamm/expression_tree/array.py +++ b/pybamm/expression_tree/array.py @@ -98,6 +98,6 @@ def new_copy(self): self.entries_string, ) - def _base_evaluate(self, t=None, y=None, u=None): + def _base_evaluate(self, t=None, y=None, y_dot=None, inputs=None): """ See :meth:`pybamm.Symbol._base_evaluate()`. """ return self._entries diff --git a/pybamm/expression_tree/binary_operators.py b/pybamm/expression_tree/binary_operators.py index 65c459926b..cc85897df9 100644 --- a/pybamm/expression_tree/binary_operators.py +++ b/pybamm/expression_tree/binary_operators.py @@ -13,7 +13,7 @@ def is_scalar_zero(expr): Utility function to test if an expression evaluates to a constant scalar zero """ if expr.is_constant(): - result = expr.evaluate_ignoring_errors() + result = expr.evaluate_ignoring_errors(t=None) return isinstance(result, numbers.Number) and result == 0 else: return False @@ -24,7 +24,7 @@ def is_matrix_zero(expr): Utility function to test if an expression evaluates to a constant matrix zero """ if expr.is_constant(): - result = expr.evaluate_ignoring_errors() + result = expr.evaluate_ignoring_errors(t=None) return (issparse(result) and result.count_nonzero() == 0) or ( isinstance(result, np.ndarray) and np.all(result == 0) ) @@ -32,12 +32,12 @@ def is_matrix_zero(expr): return False -def is_one(expr): +def is_scalar_one(expr): """ Utility function to test if an expression evaluates to a constant scalar one """ if expr.is_constant(): - result = expr.evaluate_ignoring_errors() + result = expr.evaluate_ignoring_errors(t=None) return isinstance(result, numbers.Number) and result == 1 else: return False @@ -162,21 +162,23 @@ def _binary_new_copy(self, left, right): "Default behaviour for new_copy" return self.__class__(left, right) - def evaluate(self, t=None, y=None, u=None, known_evals=None): + def evaluate(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): """ See :meth:`pybamm.Symbol.evaluate()`. """ if known_evals is not None: id = self.id try: return known_evals[id], known_evals except KeyError: - left, known_evals = self.left.evaluate(t, y, u, known_evals) - right, known_evals = self.right.evaluate(t, y, u, known_evals) + left, known_evals = self.left.evaluate(t, y, y_dot, inputs, known_evals) + right, known_evals = self.right.evaluate( + t, y, y_dot, inputs, known_evals + ) value = self._binary_evaluate(left, right) known_evals[id] = value return value, known_evals else: - left = self.left.evaluate(t, y, u) - right = self.right.evaluate(t, y, u) + left = self.left.evaluate(t, y, y_dot, inputs) + right = self.right.evaluate(t, y, y_dot, inputs) return self._binary_evaluate(left, right) def _evaluate_for_shape(self): @@ -252,8 +254,12 @@ def _binary_simplify(self, left, right): if is_scalar_zero(right): return pybamm.Scalar(1) - # anything to the power of one is itself + # zero to the power of anything is zero if is_scalar_zero(left): + return pybamm.Scalar(0) + + # anything to the power of one is itself + if is_scalar_one(right): return left return self.__class__(left, right) @@ -425,9 +431,9 @@ def _binary_simplify(self, left, right): return zeros_of_shape(shape) # anything multiplied by a scalar one returns itself - if is_one(left): + if is_scalar_one(left): return right - if is_one(right): + if is_scalar_one(right): return left return pybamm.simplify_multiplication_division(self.__class__, left, right) @@ -549,7 +555,7 @@ def _binary_simplify(self, left, right): return pybamm.Array(np.inf * np.ones(left.shape_for_testing)) # anything divided by one is itself - if is_one(right): + if is_scalar_one(right): return left return pybamm.simplify_multiplication_division(self.__class__, left, right) @@ -622,9 +628,9 @@ def _binary_simplify(self, left, right): return zeros_of_shape(shape) # anything multiplied by a scalar one returns itself - if is_one(left): + if is_scalar_one(left): return right - if is_one(right): + if is_scalar_one(right): return left return pybamm.simplify_multiplication_division(self.__class__, left, right) @@ -657,23 +663,10 @@ class Heaviside(BinaryOperator): **Extends:** :class:`BinaryOperator` """ - def __init__(self, left, right, equal): + def __init__(self, name, left, right): """ See :meth:`pybamm.BinaryOperator.__init__()`. """ - # 'equal' determines whether to return 1 or 0 when left = right - self.equal = equal - if equal is True: - name = "<=" - else: - name = "<" super().__init__(name, left, right) - def __str__(self): - """ See :meth:`pybamm.Symbol.__str__()`. """ - if self.equal is True: - return "{!s} <= {!s}".format(self.left, self.right) - else: - return "{!s} < {!s}".format(self.left, self.right) - def diff(self, variable): """ See :meth:`pybamm.Symbol.diff()`. """ # Heaviside should always be multiplied by something else so hopefully don't @@ -686,18 +679,112 @@ def _binary_jac(self, left_jac, right_jac): # need to worry about shape return pybamm.Scalar(0) + +class EqualHeaviside(Heaviside): + "A heaviside function with equality (return 1 when left = right)" + + def __init__(self, left, right): + """ See :meth:`pybamm.BinaryOperator.__init__()`. """ + super().__init__("<=", left, right) + + def __str__(self): + """ See :meth:`pybamm.Symbol.__str__()`. """ + return "{!s} <= {!s}".format(self.left, self.right) + def _binary_evaluate(self, left, right): """ See :meth:`pybamm.BinaryOperator._binary_evaluate()`. """ # don't raise RuntimeWarning for NaNs with np.errstate(invalid="ignore"): - if self.equal is True: - return left <= right - else: - return left < right + return left <= right - def _binary_new_copy(self, left, right): - """ See :meth:`pybamm.BinaryOperator._binary_new_copy()`. """ - return Heaviside(left, right, self.equal) + +class NotEqualHeaviside(Heaviside): + "A heaviside function without equality (return 0 when left = right)" + + def __init__(self, left, right): + super().__init__("<", left, right) + + def __str__(self): + """ See :meth:`pybamm.Symbol.__str__()`. """ + return "{!s} < {!s}".format(self.left, self.right) + + def _binary_evaluate(self, left, right): + """ See :meth:`pybamm.BinaryOperator._binary_evaluate()`. """ + # don't raise RuntimeWarning for NaNs + with np.errstate(invalid="ignore"): + return left < right + + +class Minimum(BinaryOperator): + " Returns the smaller of two objects " + + def __init__(self, left, right): + super().__init__("minimum", left, right) + + def __str__(self): + """ See :meth:`pybamm.Symbol.__str__()`. """ + return "minimum({!s}, {!s})".format(self.left, self.right) + + def _diff(self, variable): + """ See :meth:`pybamm.Symbol._diff()`. """ + left, right = self.orphans + return (left <= right) * left.diff(variable) + (left > right) * right.diff( + variable + ) + + def _binary_jac(self, left_jac, right_jac): + """ See :meth:`pybamm.BinaryOperator._binary_jac()`. """ + left, right = self.orphans + return (left <= right) * left_jac + (left > right) * right_jac + + def _binary_evaluate(self, left, right): + """ See :meth:`pybamm.BinaryOperator._binary_evaluate()`. """ + # don't raise RuntimeWarning for NaNs + return np.minimum(left, right) + + +class Maximum(BinaryOperator): + " Returns the smaller of two objects " + + def __init__(self, left, right): + super().__init__("maximum", left, right) + + def __str__(self): + """ See :meth:`pybamm.Symbol.__str__()`. """ + return "maximum({!s}, {!s})".format(self.left, self.right) + + def _diff(self, variable): + """ See :meth:`pybamm.Symbol._diff()`. """ + left, right = self.orphans + return (left >= right) * left.diff(variable) + (left < right) * right.diff( + variable + ) + + def _binary_jac(self, left_jac, right_jac): + """ See :meth:`pybamm.BinaryOperator._binary_jac()`. """ + left, right = self.orphans + return (left >= right) * left_jac + (left < right) * right_jac + + def _binary_evaluate(self, left, right): + """ See :meth:`pybamm.BinaryOperator._binary_evaluate()`. """ + # don't raise RuntimeWarning for NaNs + return np.maximum(left, right) + + +def minimum(left, right): + """ + Returns the smaller of two objects. Not to be confused with :meth:`pybamm.min`, + which returns min function of child. + """ + return pybamm.simplify_if_constant(Minimum(left, right), keep_domains=True) + + +def maximum(left, right): + """ + Returns the larger of two objects. Not to be confused with :meth:`pybamm.max`, + which returns max function of child. + """ + return pybamm.simplify_if_constant(Maximum(left, right), keep_domains=True) def source(left, right, boundary=False): diff --git a/pybamm/expression_tree/broadcasts.py b/pybamm/expression_tree/broadcasts.py index 31228d8085..2f85f11e7a 100644 --- a/pybamm/expression_tree/broadcasts.py +++ b/pybamm/expression_tree/broadcasts.py @@ -37,7 +37,7 @@ def __init__( child, broadcast_domain, broadcast_auxiliary_domains=None, - broadcast_type="full", + broadcast_type="full to nodes", name=None, ): # Convert child to scalar if it is a number @@ -84,7 +84,9 @@ class PrimaryBroadcast(Broadcast): """ def __init__(self, child, broadcast_domain, name=None): - super().__init__(child, broadcast_domain, broadcast_type="primary", name=name) + super().__init__( + child, broadcast_domain, broadcast_type="primary to nodes", name=name + ) def check_and_set_domains( self, child, broadcast_type, broadcast_domain, broadcast_auxiliary_domains @@ -127,8 +129,8 @@ def check_and_set_domains( return domain, auxiliary_domains def _unary_new_copy(self, child): - """ See :meth:`pybamm.UnaryOperator.simplify()`. """ - return PrimaryBroadcast(child, self.broadcast_domain) + """ See :meth:`pybamm.UnaryOperator._unary_new_copy()`. """ + return self.__class__(child, self.broadcast_domain) def _evaluate_for_shape(self): """ @@ -140,6 +142,18 @@ def _evaluate_for_shape(self): return np.outer(child_eval, vec).reshape(-1, 1) +class PrimaryBroadcastToEdges(PrimaryBroadcast): + "A primary broadcast onto the edges of the domain" + + def __init__(self, child, broadcast_domain, name=None): + name = name or "broadcast to edges" + super().__init__(child, broadcast_domain, name) + self.broadcast_type = "primary to edges" + + def evaluates_on_edges(self): + return True + + class SecondaryBroadcast(Broadcast): """A node in the expression tree representing a primary broadcasting operator. Broadcasts in a `secondary` dimension only. That is, makes explicit copies of the @@ -162,7 +176,9 @@ class SecondaryBroadcast(Broadcast): """ def __init__(self, child, broadcast_domain, name=None): - super().__init__(child, broadcast_domain, broadcast_type="secondary", name=name) + super().__init__( + child, broadcast_domain, broadcast_type="secondary to nodes", name=name + ) def check_and_set_domains( self, child, broadcast_type, broadcast_domain, broadcast_auxiliary_domains @@ -207,7 +223,7 @@ def check_and_set_domains( return domain, auxiliary_domains def _unary_new_copy(self, child): - """ See :meth:`pybamm.UnaryOperator.simplify()`. """ + """ See :meth:`pybamm.UnaryOperator._unary_new_copy()`. """ return SecondaryBroadcast(child, self.broadcast_domain) def _evaluate_for_shape(self): @@ -220,6 +236,18 @@ def _evaluate_for_shape(self): return np.outer(vec, child_eval).reshape(-1, 1) +class SecondaryBroadcastToEdges(SecondaryBroadcast): + "A secondary broadcast onto the edges of a domain" + + def __init__(self, child, broadcast_domain, name=None): + name = name or "broadcast to edges" + super().__init__(child, broadcast_domain, name) + self.broadcast_type = "secondary to edges" + + def evaluates_on_edges(self): + return True + + class FullBroadcast(Broadcast): "A class for full broadcasts" @@ -230,7 +258,7 @@ def __init__(self, child, broadcast_domain, auxiliary_domains, name=None): child, broadcast_domain, broadcast_auxiliary_domains=auxiliary_domains, - broadcast_type="full", + broadcast_type="full to nodes", name=name, ) @@ -250,7 +278,7 @@ def check_and_set_domains( return domain, auxiliary_domains def _unary_new_copy(self, child): - """ See :meth:`pybamm.UnaryOperator.simplify()`. """ + """ See :meth:`pybamm.UnaryOperator._unary_new_copy()`. """ return FullBroadcast(child, self.broadcast_domain, self.auxiliary_domains) def _evaluate_for_shape(self): @@ -266,6 +294,21 @@ def _evaluate_for_shape(self): return child_eval * vec +class FullBroadcastToEdges(FullBroadcast): + """ + A full broadcast onto the edges of a domain (edges of primary dimension, nodes of + other dimensions) + """ + + def __init__(self, child, broadcast_domain, auxiliary_domains, name=None): + name = name or "broadcast to edges" + super().__init__(child, broadcast_domain, auxiliary_domains, name) + self.broadcast_type = "full to edges" + + def evaluates_on_edges(self): + return True + + def ones_like(*symbols): """ Create a symbol with the same shape as the input symbol and with constant value '1', diff --git a/pybamm/expression_tree/concatenations.py b/pybamm/expression_tree/concatenations.py index e1225bf03a..70efff8c6e 100644 --- a/pybamm/expression_tree/concatenations.py +++ b/pybamm/expression_tree/concatenations.py @@ -54,7 +54,7 @@ def _concatenation_evaluate(self, children_eval): else: return self.concatenation_function(children_eval) - def evaluate(self, t=None, y=None, u=None, known_evals=None): + def evaluate(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): """ See :meth:`pybamm.Symbol.evaluate()`. """ children = self.cached_children if known_evals is not None: @@ -62,14 +62,14 @@ def evaluate(self, t=None, y=None, u=None, known_evals=None): children_eval = [None] * len(children) for idx, child in enumerate(children): children_eval[idx], known_evals = child.evaluate( - t, y, u, known_evals + t, y, y_dot, inputs, known_evals ) known_evals[self.id] = self._concatenation_evaluate(children_eval) return known_evals[self.id], known_evals else: children_eval = [None] * len(children) for idx, child in enumerate(children): - children_eval[idx] = child.evaluate(t, y, u) + children_eval[idx] = child.evaluate(t, y, y_dot, inputs) return self._concatenation_evaluate(children_eval) def new_copy(self): diff --git a/pybamm/expression_tree/exceptions.py b/pybamm/expression_tree/exceptions.py index a71172cc48..d0f4341a20 100644 --- a/pybamm/expression_tree/exceptions.py +++ b/pybamm/expression_tree/exceptions.py @@ -61,14 +61,6 @@ class ModelWarning(UserWarning): pass -class UndefinedOperationError(Exception): - """ - Undefined operation: Raised when a mathematical operation is not well-defined - """ - - pass - - class InputError(Exception): """ An external variable has been input incorrectly into PyBaMM diff --git a/pybamm/expression_tree/functions.py b/pybamm/expression_tree/functions.py index 2601034b98..614ac959fd 100644 --- a/pybamm/expression_tree/functions.py +++ b/pybamm/expression_tree/functions.py @@ -152,19 +152,21 @@ def _function_jac(self, children_jacs): return jacobian - def evaluate(self, t=None, y=None, u=None, known_evals=None): + def evaluate(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): """ See :meth:`pybamm.Symbol.evaluate()`. """ if known_evals is not None: if self.id not in known_evals: evaluated_children = [None] * len(self.children) for i, child in enumerate(self.children): evaluated_children[i], known_evals = child.evaluate( - t, y, u, known_evals=known_evals + t, y, y_dot, inputs, known_evals=known_evals ) known_evals[self.id] = self._function_evaluate(evaluated_children) return known_evals[self.id], known_evals else: - evaluated_children = [child.evaluate(t, y, u) for child in self.children] + evaluated_children = [ + child.evaluate(t, y, y_dot, inputs) for child in self.children + ] return self._function_evaluate(evaluated_children) def _evaluate_for_shape(self): @@ -341,12 +343,18 @@ def log10(child): def max(child): - " Returns max function of child. " + """ + Returns max function of child. Not to be confused with :meth:`pybamm.maximum`, which + returns the larger of two objects. + """ return pybamm.simplify_if_constant(Function(np.max, child), keep_domains=True) def min(child): - " Returns min function of child. " + """ + Returns min function of child. Not to be confused with :meth:`pybamm.minimum`, which + returns the smaller of two objects. + """ return pybamm.simplify_if_constant(Function(np.min, child), keep_domains=True) diff --git a/pybamm/expression_tree/independent_variable.py b/pybamm/expression_tree/independent_variable.py index 9dea22f5a7..bce3153ee3 100644 --- a/pybamm/expression_tree/independent_variable.py +++ b/pybamm/expression_tree/independent_variable.py @@ -4,9 +4,6 @@ import pybamm KNOWN_COORD_SYS = ["cartesian", "spherical polar"] -KNOWN_SPATIAL_VARS = ["x", "y", "z", "r", "x_n", "x_s", "x_p", "r_n", "r_p"] -KNOWN_SPATIAL_VARS_EXTENDED = [v + "_edge" for v in KNOWN_SPATIAL_VARS] -KNOWN_SPATIAL_VARS.extend(KNOWN_SPATIAL_VARS_EXTENDED) class IndependentVariable(pybamm.Symbol): @@ -51,7 +48,7 @@ def new_copy(self): """ See :meth:`pybamm.Symbol.new_copy()`. """ return Time() - def _base_evaluate(self, t, y=None, u=None): + def _base_evaluate(self, t=None, y=None, y_dot=None, inputs=None): """ See :meth:`pybamm.Symbol._base_evaluate()`. """ if t is None: raise ValueError("t must be provided") @@ -84,8 +81,6 @@ def __init__(self, name, domain=None, auxiliary_domains=None, coord_sys=None): super().__init__(name, domain=domain, auxiliary_domains=auxiliary_domains) domain = self.domain - if name not in KNOWN_SPATIAL_VARS: - raise ValueError(f"name must be in {KNOWN_SPATIAL_VARS} but is '{name}'") if domain == []: raise ValueError("domain must be provided") @@ -109,10 +104,32 @@ def __init__(self, name, domain=None, auxiliary_domains=None, coord_sys=None): def new_copy(self): """ See :meth:`pybamm.Symbol.new_copy()`. """ - return SpatialVariable( + return self.__class__( self.name, self.domain, self.auxiliary_domains, self.coord_sys ) +class SpatialVariableEdge(SpatialVariable): + """A node in the expression tree representing a spatial variable, which evaluates + on the edges + + Parameters + ---------- + name : str + name of the node (e.g. "x", "y", "z", "r", "x_n", "x_s", "x_p", "r_n", "r_p") + domain : iterable of str + list of domains that this variable is valid over (e.g. "cartesian", "spherical + polar") + + *Extends:* :class:`Symbol` + """ + + def __init__(self, name, domain=None, auxiliary_domains=None, coord_sys=None): + super().__init__(name, domain, auxiliary_domains, coord_sys) + + def evaluates_on_edges(self): + return True + + # the independent variable time t = Time() diff --git a/pybamm/expression_tree/input_parameter.py b/pybamm/expression_tree/input_parameter.py index 148c062b0c..8405fb76cb 100644 --- a/pybamm/expression_tree/input_parameter.py +++ b/pybamm/expression_tree/input_parameter.py @@ -36,18 +36,18 @@ def _jac(self, variable): """ See :meth:`pybamm.Symbol._jac()`. """ return pybamm.Scalar(0) - def _base_evaluate(self, t=None, y=None, u=None): - # u should be a dictionary + def _base_evaluate(self, t=None, y=None, y_dot=None, inputs=None): + # inputs should be a dictionary # convert 'None' to empty dictionary for more informative error - if u is None: - u = {} - if not isinstance(u, dict): + if inputs is None: + inputs = {} + if not isinstance(inputs, dict): # if the special input "shape test" is passed, just return 1 - if u == "shape test": + if inputs == "shape test": return 1 - raise TypeError("inputs u should be a dictionary") + raise TypeError("inputs should be a dictionary") try: - return u[self.name] + return inputs[self.name] # raise more informative error if can't find name in dict except KeyError: raise KeyError("Input parameter '{}' not found".format(self.name)) diff --git a/pybamm/expression_tree/operations/convert_to_casadi.py b/pybamm/expression_tree/operations/convert_to_casadi.py index 5e89ab75cf..7a19c33d12 100644 --- a/pybamm/expression_tree/operations/convert_to_casadi.py +++ b/pybamm/expression_tree/operations/convert_to_casadi.py @@ -13,7 +13,7 @@ def __init__(self, casadi_symbols=None): pybamm.citations.register("Andersson2019") - def convert(self, symbol, t=None, y=None, u=None): + def convert(self, symbol, t, y, y_dot, inputs): """ This function recurses down the tree, converting the PyBaMM expression tree to a CasADi expression tree @@ -26,8 +26,10 @@ def convert(self, symbol, t=None, y=None, u=None): A casadi symbol representing time y : :class:`casadi.MX` A casadi symbol representing state vectors - u : dict - A dictionary of casadi symbols representing inputs + y_dot : :class:`casadi.MX` + A casadi symbol representing time derivatives of state vectors + inputs : dict + A dictionary of casadi symbols representing parameters Returns ------- @@ -37,14 +39,14 @@ def convert(self, symbol, t=None, y=None, u=None): try: return self._casadi_symbols[symbol.id] except KeyError: - # Change u to empty dictionary if it's None - u = u or {} - casadi_symbol = self._convert(symbol, t, y, u) + # Change inputs to empty dictionary if it's None + inputs = inputs or {} + casadi_symbol = self._convert(symbol, t, y, y_dot, inputs) self._casadi_symbols[symbol.id] = casadi_symbol return casadi_symbol - def _convert(self, symbol, t=None, y=None, u=None): + def _convert(self, symbol, t, y, y_dot, inputs): """ See :meth:`CasadiConverter.convert()`. """ if isinstance( symbol, @@ -56,30 +58,41 @@ def _convert(self, symbol, t=None, y=None, u=None): pybamm.ExternalVariable, ), ): - return casadi.MX(symbol.evaluate(t, y, u)) + return casadi.MX(symbol.evaluate(t, y, y_dot, inputs)) elif isinstance(symbol, pybamm.StateVector): if y is None: raise ValueError("Must provide a 'y' for converting state vectors") return casadi.vertcat(*[y[y_slice] for y_slice in symbol.y_slices]) + elif isinstance(symbol, pybamm.StateVectorDot): + if y_dot is None: + raise ValueError("Must provide a 'y_dot' for converting state vectors") + return casadi.vertcat(*[y_dot[y_slice] for y_slice in symbol.y_slices]) + elif isinstance(symbol, pybamm.BinaryOperator): left, right = symbol.children # process children - converted_left = self.convert(left, t, y, u) - converted_right = self.convert(right, t, y, u) + converted_left = self.convert(left, t, y, y_dot, inputs) + converted_right = self.convert(right, t, y, y_dot, inputs) + + if isinstance(symbol, pybamm.Minimum): + return casadi.fmin(converted_left, converted_right) + if isinstance(symbol, pybamm.Maximum): + return casadi.fmax(converted_left, converted_right) + # _binary_evaluate defined in derived classes for specific rules return symbol._binary_evaluate(converted_left, converted_right) elif isinstance(symbol, pybamm.UnaryOperator): - converted_child = self.convert(symbol.child, t, y, u) + converted_child = self.convert(symbol.child, t, y, y_dot, inputs) if isinstance(symbol, pybamm.AbsoluteValue): return casadi.fabs(converted_child) return symbol._unary_evaluate(converted_child) elif isinstance(symbol, pybamm.Function): converted_children = [ - self.convert(child, t, y, u) for child in symbol.children + self.convert(child, t, y, y_dot, inputs) for child in symbol.children ] # Special functions if symbol.function == np.min: @@ -110,7 +123,7 @@ def _convert(self, symbol, t=None, y=None, u=None): return symbol._function_evaluate(converted_children) elif isinstance(symbol, pybamm.Concatenation): converted_children = [ - self.convert(child, t, y, u) for child in symbol.children + self.convert(child, t, y, y_dot, inputs) for child in symbol.children ] if isinstance(symbol, (pybamm.NumpyConcatenation, pybamm.SparseStack)): return casadi.vertcat(*converted_children) diff --git a/pybamm/expression_tree/operations/evaluate.py b/pybamm/expression_tree/operations/evaluate.py index a11769b065..e43a961c59 100644 --- a/pybamm/expression_tree/operations/evaluate.py +++ b/pybamm/expression_tree/operations/evaluate.py @@ -92,6 +92,10 @@ def find_symbols(symbol, constant_symbols, variable_symbols): "if scipy.sparse.issparse({1}) else " "{0} * {1}".format(children_vars[0], children_vars[1]) ) + elif isinstance(symbol, pybamm.Minimum): + symbol_str = "np.minimum({},{})".format(children_vars[0], children_vars[1]) + elif isinstance(symbol, pybamm.Maximum): + symbol_str = "np.maximum({},{})".format(children_vars[0], children_vars[1]) else: symbol_str = children_vars[0] + " " + symbol.name + " " + children_vars[1] @@ -168,7 +172,7 @@ def find_symbols(symbol, constant_symbols, variable_symbols): symbol_str = "t" elif isinstance(symbol, pybamm.InputParameter): - symbol_str = "u['{}']".format(symbol.name) + symbol_str = "inputs['{}']".format(symbol.name) else: raise NotImplementedError( @@ -265,7 +269,7 @@ def __init__(self, symbol): self._result_var, "return" + self._result_var, "eval" ) - def evaluate(self, t=None, y=None, u=None, known_evals=None): + def evaluate(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): """ Acts as a drop-in replacement for :func:`pybamm.Symbol.evaluate` """ diff --git a/pybamm/expression_tree/operations/jacobian.py b/pybamm/expression_tree/operations/jacobian.py index 42bd3683da..b231c0c784 100644 --- a/pybamm/expression_tree/operations/jacobian.py +++ b/pybamm/expression_tree/operations/jacobian.py @@ -5,8 +5,22 @@ class Jacobian(object): - def __init__(self, known_jacs=None): + """ + Helper class to calculate the jacobian of an expression. + + Parameters + ---------- + + known_jacs: dict {variable ids -> :class:`pybamm.Symbol`} + cached jacobians + + clear_domain: bool + wether or not the jacobian clears the domain (default True) + """ + + def __init__(self, known_jacs=None, clear_domain=True): self._known_jacs = known_jacs or {} + self._clear_domain = clear_domain def jac(self, symbol, variable): """ @@ -75,6 +89,7 @@ def _jac(self, symbol, variable): ) ) - # jacobian removes the domain(s) - jac.clear_domains() + # jacobian by default removes the domain(s) + if self._clear_domain: + jac.clear_domains() return jac diff --git a/pybamm/expression_tree/parameter.py b/pybamm/expression_tree/parameter.py index 9c2049ac8c..eca440ebd2 100644 --- a/pybamm/expression_tree/parameter.py +++ b/pybamm/expression_tree/parameter.py @@ -48,19 +48,23 @@ class FunctionParameter(pybamm.Symbol): name : str name of the node - child : :class:`Symbol` - child node + inputs : dict + A dictionary with string keys and :class:`pybamm.Symbol` values representing + the function inputs. The string keys should provide a reasonable description + of what the input to the function is + (e.g. "Electrolyte concentration [mol.m-3]") diff_variable : :class:`pybamm.Symbol`, optional if diff_variable is specified, the FunctionParameter node will be replaced by a :class:`pybamm.Function` and then differentiated with respect to diff_variable. Default is None. - """ - def __init__(self, name, *children, diff_variable=None): + def __init__( + self, name, inputs, diff_variable=None, + ): # assign diff variable self.diff_variable = diff_variable - children_list = list(children) + children_list = list(inputs.values()) # Turn numbers into scalars for idx, child in enumerate(children_list): @@ -76,6 +80,37 @@ def __init__(self, name, *children, diff_variable=None): auxiliary_domains=auxiliary_domains, ) + self.input_names = list(inputs.keys()) + + @property + def input_names(self): + return self._input_names + + def print_input_names(self): + if self._input_names: + for inp in self._input_names: + print(inp) + + @input_names.setter + def input_names(self, inp=None): + if inp: + if inp.__class__ is list: + for i in inp: + if i.__class__ is not str: + raise TypeError( + "Inputs must be a provided as" + + "a dictionary of the form:" + + "{{str: :class:`pybamm.Symbol`}}" + ) + else: + raise TypeError( + "Inputs must be a provided as" + + " a dictionary of the form:" + + "{{str: :class:`pybamm.Symbol`}}" + ) + + self._input_names = inp + def set_id(self): """See :meth:`pybamm.Symbol.set_id` """ self._id = hash( @@ -107,17 +142,24 @@ def diff(self, variable): """ See :meth:`pybamm.Symbol.diff()`. """ # return a new FunctionParameter, that knows it will need to be differentiated # when the parameters are set - return FunctionParameter(self.name, *self.orphans, diff_variable=variable) + children_list = self.orphans + input_names = self._input_names + + input_dict = {input_names[i]: children_list[i] for i in range(len(input_names))} + + return FunctionParameter(self.name, input_dict, diff_variable=variable) def new_copy(self): """ See :meth:`pybamm.Symbol.new_copy()`. """ - return self._function_parameter_new_copy(self.orphans) + return self._function_parameter_new_copy(self._input_names, self.orphans) - def _function_parameter_new_copy(self, children): + def _function_parameter_new_copy(self, input_names, children): """Returns a new copy of the function parameter. Inputs ------ + input_names : : list + A list of str of the names of the children/function inputs children : : list A list of the children of the function @@ -126,7 +168,12 @@ def _function_parameter_new_copy(self, children): : :pybamm.FunctionParameter A new copy of the function parameter """ - return FunctionParameter(self.name, *children, diff_variable=self.diff_variable) + + input_dict = {input_names[i]: children[i] for i in range(len(input_names))} + + return FunctionParameter( + self.name, input_dict, diff_variable=self.diff_variable + ) def _evaluate_for_shape(self): """ diff --git a/pybamm/expression_tree/scalar.py b/pybamm/expression_tree/scalar.py index b96d618c70..fffc461960 100644 --- a/pybamm/expression_tree/scalar.py +++ b/pybamm/expression_tree/scalar.py @@ -51,7 +51,7 @@ def set_id(self): (self.__class__, self.name) + tuple(self.domain) + tuple(str(self._value)) ) - def _base_evaluate(self, t=None, y=None, u=None): + def _base_evaluate(self, t=None, y=None, y_dot=None, inputs=None): """ See :meth:`pybamm.Symbol._base_evaluate()`. """ return self._value diff --git a/pybamm/expression_tree/state_vector.py b/pybamm/expression_tree/state_vector.py index d99079b085..e0a07cbc76 100644 --- a/pybamm/expression_tree/state_vector.py +++ b/pybamm/expression_tree/state_vector.py @@ -7,7 +7,7 @@ from scipy.sparse import csr_matrix, vstack -class StateVector(pybamm.Symbol): +class StateVectorBase(pybamm.Symbol): """ node in the expression tree that holds a slice to read from an external vector type @@ -32,6 +32,7 @@ class StateVector(pybamm.Symbol): def __init__( self, *y_slices, + base_name="y", name=None, domain=None, auxiliary_domains=None, @@ -42,9 +43,10 @@ def __init__( raise TypeError("all y_slices must be slice objects") if name is None: if y_slices[0].start is None: - name = "y[:{:d}]".format(y_slice.stop) + name = base_name + "[:{:d}]".format(y_slice.stop) else: - name = "y[{:d}:{:d}".format(y_slices[0].start, y_slices[0].stop) + name = base_name + \ + "[{:d}:{:d}".format(y_slices[0].start, y_slices[0].stop) if len(y_slices) > 1: name += ",{:d}:{:d}".format(y_slices[1].start, y_slices[1].stop) if len(y_slices) > 2: @@ -103,21 +105,29 @@ def set_id(self): + tuple(self.domain) ) - def _base_evaluate(self, t=None, y=None, u=None): - """ See :meth:`pybamm.Symbol._base_evaluate()`. """ - if y is None: - raise TypeError("StateVector cannot evaluate input 'y=None'") - if y.shape[0] < len(self.evaluation_array): - raise ValueError( - "y is too short, so value with slice is smaller than expected" + def _jac_diff_vector(self, variable): + """ + Differentiate a slice of a StateVector of size m with respect to another slice + of a different StateVector of size n. This returns a (sparse) zero matrix of + size m x n + + Parameters + ---------- + variable : :class:`pybamm.Symbol` + The variable with respect to which to differentiate + + """ + if len(variable.y_slices) > 1: + raise NotImplementedError( + "Jacobian only implemented for a single-slice StateVector" ) - else: - out = (y[: len(self._evaluation_array)])[self._evaluation_array] - if isinstance(out, np.ndarray) and out.ndim == 1: - out = out[:, np.newaxis] - return out + slices_size = self.y_slices[0].stop - self.y_slices[0].start + variable_size = variable.last_point - variable.first_point - def _jac(self, variable): + # Return zeros of correct size since no entries match + return pybamm.Matrix(csr_matrix((slices_size, variable_size))) + + def _jac_same_vector(self, variable): """ Differentiate a slice of a StateVector of size m with respect to another slice of a StateVector of size n. This returns a (sparse) matrix of size @@ -180,3 +190,136 @@ def _evaluate_for_shape(self): See :meth:`pybamm.Symbol.evaluate_for_shape()` """ return np.nan * np.ones((self.size, 1)) + + +class StateVector(StateVectorBase): + """ + node in the expression tree that holds a slice to read from an external vector type + + Parameters + ---------- + + y_slice: slice + the slice of an external y to read + name: str, optional + the name of the node + domain : iterable of str, optional + list of domains the parameter is valid over, defaults to empty list + auxiliary_domains : dict of str, optional + dictionary of auxiliary domains + evaluation_array : list, optional + List of boolean arrays representing slices. Default is None, in which case the + evaluation_array is computed from y_slices. + + *Extends:* :class:`Array` + """ + + def __init__( + self, + *y_slices, + name=None, + domain=None, + auxiliary_domains=None, + evaluation_array=None, + ): + super().__init__(*y_slices, + base_name="y", name=name, domain=domain, + auxiliary_domains=auxiliary_domains, + evaluation_array=evaluation_array) + + def _base_evaluate(self, t=None, y=None, y_dot=None, inputs=None): + """ See :meth:`pybamm.Symbol._base_evaluate()`. """ + if y is None: + raise TypeError("StateVector cannot evaluate input 'y=None'") + if y.shape[0] < len(self.evaluation_array): + raise ValueError( + "y is too short, so value with slice is smaller than expected" + ) + + out = (y[: len(self._evaluation_array)])[self._evaluation_array] + if isinstance(out, np.ndarray) and out.ndim == 1: + out = out[:, np.newaxis] + return out + + def diff(self, variable): + if variable.id == self.id: + return pybamm.Scalar(1) + if variable.id == pybamm.t.id: + return StateVectorDot(*self._y_slices, name=self.name + "'", + domain=self.domain, + auxiliary_domains=self.auxiliary_domains, + evaluation_array=self.evaluation_array) + else: + return pybamm.Scalar(0) + + def _jac(self, variable): + if isinstance(variable, pybamm.StateVector): + return self._jac_same_vector(variable) + elif isinstance(variable, pybamm.StateVectorDot): + return self._jac_diff_vector(variable) + + +class StateVectorDot(StateVectorBase): + """ + node in the expression tree that holds a slice to read from the ydot + + Parameters + ---------- + + y_slice: slice + the slice of an external ydot to read + name: str, optional + the name of the node + domain : iterable of str, optional + list of domains the parameter is valid over, defaults to empty list + auxiliary_domains : dict of str, optional + dictionary of auxiliary domains + evaluation_array : list, optional + List of boolean arrays representing slices. Default is None, in which case the + evaluation_array is computed from y_slices. + + *Extends:* :class:`Array` + """ + + def __init__( + self, + *y_slices, + name=None, + domain=None, + auxiliary_domains=None, + evaluation_array=None, + ): + super().__init__(*y_slices, + base_name="y_dot", name=name, domain=domain, + auxiliary_domains=auxiliary_domains, + evaluation_array=evaluation_array) + + def _base_evaluate(self, t=None, y=None, y_dot=None, inputs=None): + """ See :meth:`pybamm.Symbol._base_evaluate()`. """ + if y_dot is None: + raise TypeError("StateVectorDot cannot evaluate input 'y_dot=None'") + if y_dot.shape[0] < len(self.evaluation_array): + raise ValueError( + "y_dot is too short, so value with slice is smaller than expected" + ) + + out = (y_dot[: len(self._evaluation_array)])[self._evaluation_array] + if isinstance(out, np.ndarray) and out.ndim == 1: + out = out[:, np.newaxis] + return out + + def diff(self, variable): + if variable.id == self.id: + return pybamm.Scalar(1) + elif variable.id == pybamm.t.id: + raise pybamm.ModelError( + "cannot take second time derivative of a state vector" + ) + else: + return pybamm.Scalar(0) + + def _jac(self, variable): + if isinstance(variable, pybamm.StateVectorDot): + return self._jac_same_vector(variable) + elif isinstance(variable, pybamm.StateVector): + return self._jac_diff_vector(variable) diff --git a/pybamm/expression_tree/symbol.py b/pybamm/expression_tree/symbol.py index 3bc390cc26..5b0f895121 100644 --- a/pybamm/expression_tree/symbol.py +++ b/pybamm/expression_tree/symbol.py @@ -431,27 +431,27 @@ def __rpow__(self, other): return pybamm.simplify_if_constant(pybamm.Power(other, self), keep_domains=True) def __lt__(self, other): - """return a :class:`Heaviside` object""" + """return a :class:`NotEqualHeaviside` object""" return pybamm.simplify_if_constant( - pybamm.Heaviside(self, other, equal=False), keep_domains=True + pybamm.NotEqualHeaviside(self, other), keep_domains=True ) def __le__(self, other): - """return a :class:`Heaviside` object""" + """return a :class:`EqualHeaviside` object""" return pybamm.simplify_if_constant( - pybamm.Heaviside(self, other, equal=True), keep_domains=True + pybamm.EqualHeaviside(self, other), keep_domains=True ) def __gt__(self, other): - """return a :class:`Heaviside` object""" + """return a :class:`NotEqualHeaviside` object""" return pybamm.simplify_if_constant( - pybamm.Heaviside(other, self, equal=False), keep_domains=True + pybamm.NotEqualHeaviside(other, self), keep_domains=True ) def __ge__(self, other): - """return a :class:`Heaviside` object""" + """return a :class:`EqualHeaviside` object""" return pybamm.simplify_if_constant( - pybamm.Heaviside(other, self, equal=True), keep_domains=True + pybamm.EqualHeaviside(other, self), keep_domains=True ) def __neg__(self): @@ -485,6 +485,11 @@ def diff(self, variable): return pybamm.Scalar(1) elif any(variable.id == x.id for x in self.pre_order()): return self._diff(variable) + elif variable.id == pybamm.t.id and any( + isinstance(x, (pybamm.VariableBase, pybamm.StateVectorBase)) + for x in self.pre_order() + ): + return self._diff(variable) else: return pybamm.Scalar(0) @@ -492,12 +497,13 @@ def _diff(self, variable): "Default behaviour for differentiation, overriden by Binary and Unary Operators" raise NotImplementedError - def jac(self, variable, known_jacs=None): + def jac(self, variable, known_jacs=None, clear_domain=True): """ Differentiate a symbol with respect to a (slice of) a State Vector. See :class:`pybamm.Jacobian`. """ - return pybamm.Jacobian(known_jacs).jac(self, variable) + jac = pybamm.Jacobian(known_jacs, clear_domain=clear_domain) + return jac.jac(self, variable) def _jac(self, variable): """ @@ -506,7 +512,7 @@ def _jac(self, variable): """ raise NotImplementedError - def _base_evaluate(self, t=None, y=None, u=None): + def _base_evaluate(self, t=None, y=None, y_dot=None, inputs=None): """evaluate expression tree will raise a ``NotImplementedError`` if this member function has not @@ -520,7 +526,11 @@ def _base_evaluate(self, t=None, y=None, u=None): time at which to evaluate (default None) y : numpy.array, optional - array to evaluate when solving (default None) + array with state values to evaluate when solving (default None) + + y_dot : numpy.array, optional + array with time derivatives of state values to evaluate when solving + (default None) """ raise NotImplementedError( @@ -530,7 +540,7 @@ def _base_evaluate(self, t=None, y=None, u=None): ) ) - def evaluate(self, t=None, y=None, u=None, known_evals=None): + def evaluate(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): """Evaluate expression tree (wrapper to allow using dict of known values). If the dict 'known_evals' is provided, the dict is searched for self.id; if self.id is in the keys, return that value; otherwise, evaluate using @@ -541,8 +551,11 @@ def evaluate(self, t=None, y=None, u=None, known_evals=None): t : float or numeric type, optional time at which to evaluate (default None) y : numpy.array, optional - array to evaluate when solving (default None) - u : dict, optional + array with state values to evaluate when solving (default None) + y_dot : numpy.array, optional + array with time derivatives of state values to evaluate when solving + (default None) + inputs : dict, optional dictionary of inputs to use when solving (default None) known_evals : dict, optional dictionary containing known values (default None) @@ -556,10 +569,10 @@ def evaluate(self, t=None, y=None, u=None, known_evals=None): """ if known_evals is not None: if self.id not in known_evals: - known_evals[self.id] = self._base_evaluate(t, y, u) + known_evals[self.id] = self._base_evaluate(t, y, y_dot, inputs) return known_evals[self.id], known_evals else: - return self._base_evaluate(t, y, u) + return self._base_evaluate(t, y, y_dot, inputs) def evaluate_for_shape(self): """Evaluate expression tree to find its shape. For symbols that cannot be @@ -598,11 +611,13 @@ def is_constant(self): # do the search, return true if no relevent nodes are found return not any((isinstance(n, search_types)) for n in self.pre_order()) - def evaluate_ignoring_errors(self): + def evaluate_ignoring_errors(self, t=0): """ Evaluates the expression. If a node exists in the tree that cannot be evaluated - as a scalar or vector (e.g. Parameter, Variable, StateVector, InputParameter), - then None is returned. Otherwise the result of the evaluation is given + as a scalar or vector (e.g. Time, Parameter, Variable, StateVector), then None + is returned. If there is an InputParameter in the tree then a 1 is returned. + Otherwise the result of the evaluation is given. + See Also -------- @@ -610,7 +625,7 @@ def evaluate_ignoring_errors(self): """ try: - result = self.evaluate(t=0, u="shape test") + result = self.evaluate(t=t, inputs="shape test") except NotImplementedError: # return None if NotImplementedError is raised # (there is a e.g. Parameter, Variable, ... in the tree) @@ -620,9 +635,15 @@ def evaluate_ignoring_errors(self): # (there is a e.g. StateVector in the tree) if error.args[0] == "StateVector cannot evaluate input 'y=None'": return None + elif error.args[0] == "StateVectorDot cannot evaluate input 'y_dot=None'": + return None else: raise error except ValueError as e: + # return None if specific ValueError is raised + # (there is a e.g. Time in the tree) + if e.args[0] == "t must be provided": + return None raise pybamm.ShapeError("Cannot find shape (original error: {})".format(e)) return result @@ -667,12 +688,12 @@ def simplify(self, simplified_symbols=None): """ Simplify the expression tree. See :class:`pybamm.Simplification`. """ return pybamm.Simplification(simplified_symbols).simplify(self) - def to_casadi(self, t=None, y=None, u=None, casadi_symbols=None): + def to_casadi(self, t=None, y=None, y_dot=None, inputs=None, casadi_symbols=None): """ Convert the expression tree to a CasADi expression tree. See :class:`pybamm.CasadiConverter`. """ - return pybamm.CasadiConverter(casadi_symbols).convert(self, t, y, u) + return pybamm.CasadiConverter(casadi_symbols).convert(self, t, y, y_dot, inputs) def new_copy(self): """ @@ -702,7 +723,7 @@ def shape(self): # Try with some large y, to avoid having to use pre_order (slow) try: y = np.linspace(0.1, 0.9, int(1e4)) - evaluated_self = self.evaluate(0, y, u="shape test") + evaluated_self = self.evaluate(0, y, y, inputs="shape test") # If that fails, fall back to calculating how big y should really be except ValueError: state_vectors_in_node = [ @@ -716,7 +737,7 @@ def shape(self): ) # Pick a y that won't cause RuntimeWarnings y = np.linspace(0.1, 0.9, min_y_size) - evaluated_self = self.evaluate(0, y) + evaluated_self = self.evaluate(0, y, y, inputs="shape test") # Return shape of evaluated object if isinstance(evaluated_self, numbers.Number): diff --git a/pybamm/expression_tree/unary_operators.py b/pybamm/expression_tree/unary_operators.py index 0b4f68e22d..84c4bfb94a 100644 --- a/pybamm/expression_tree/unary_operators.py +++ b/pybamm/expression_tree/unary_operators.py @@ -3,7 +3,7 @@ # import numpy as np import pybamm -from scipy.sparse import csr_matrix +from scipy.sparse import issparse, csr_matrix class UnaryOperator(pybamm.Symbol): @@ -63,15 +63,17 @@ def _unary_evaluate(self, child): """Perform unary operation on a child. """ raise NotImplementedError - def evaluate(self, t=None, y=None, u=None, known_evals=None): + def evaluate(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): """ See :meth:`pybamm.Symbol.evaluate()`. """ if known_evals is not None: if self.id not in known_evals: - child, known_evals = self.child.evaluate(t, y, u, known_evals) + child, known_evals = self.child.evaluate( + t, y, y_dot, inputs, known_evals + ) known_evals[self.id] = self._unary_evaluate(child) return known_evals[self.id], known_evals else: - child = self.child.evaluate(t, y, u) + child = self.child.evaluate(t, y, y_dot, inputs) return self._unary_evaluate(child) def _evaluate_for_shape(self): @@ -125,23 +127,45 @@ def __init__(self, child): def diff(self, variable): """ See :meth:`pybamm.Symbol.diff()`. """ - # Derivative is not well-defined - raise pybamm.UndefinedOperationError( - "Derivative of absolute function is not defined" - ) + child = self.child.new_copy() + return Sign(child) * child.diff(variable) def _unary_jac(self, child_jac): """ See :meth:`pybamm.UnaryOperator._unary_jac()`. """ - # Derivative is not well-defined - raise pybamm.UndefinedOperationError( - "Derivative of absolute function is not defined" - ) + child = self.child.new_copy() + return Sign(child) * child_jac def _unary_evaluate(self, child): """ See :meth:`UnaryOperator._unary_evaluate()`. """ return np.abs(child) +class Sign(UnaryOperator): + """A node in the expression tree representing a `sign` operator + + **Extends:** :class:`UnaryOperator` + """ + + def __init__(self, child): + """ See :meth:`pybamm.UnaryOperator.__init__()`. """ + super().__init__("sign", child) + + def diff(self, variable): + """ See :meth:`pybamm.Symbol.diff()`. """ + return pybamm.Scalar(0) + + def _unary_jac(self, child_jac): + """ See :meth:`pybamm.UnaryOperator._unary_jac()`. """ + return pybamm.Scalar(0) + + def _unary_evaluate(self, child): + """ See :meth:`UnaryOperator._unary_evaluate()`. """ + if issparse(child): + return csr_matrix.sign(child) + else: + return np.sign(child) + + class Index(UnaryOperator): """A node in the expression tree, which stores the index that should be extracted from its child after the child has been evaluated. @@ -431,8 +455,6 @@ def integration_variable(self): def set_id(self): """ See :meth:`pybamm.Symbol.set_id()` """ - if not isinstance(self.integration_variable, list): - self.integration_variable = [self.integration_variable] self._id = hash( (self.__class__, self.name) + tuple( @@ -881,6 +903,9 @@ def x_average(symbol): :class:`Symbol` the new averaged symbol """ + # Can't take average if the symbol evaluates on edges + if symbol.evaluates_on_edges(): + raise ValueError("Can't take the x-average of a symbol that evaluates on edges") # If symbol doesn't have a domain, its average value is itself if symbol.domain in [[], ["current collector"]]: new_symbol = symbol.new_copy() @@ -924,8 +949,9 @@ def x_average(symbol): x = pybamm.standard_spatial_vars.x_p l = pybamm.geometric_parameters.l_p else: - raise pybamm.DomainError("domain '{}' not recognised".format(symbol.domain)) - + x = pybamm.SpatialVariable("x", domain=symbol.domain) + v = pybamm.ones_like(symbol) + l = pybamm.Integral(v, x) return Integral(symbol, x) / l @@ -942,6 +968,9 @@ def z_average(symbol): :class:`Symbol` the new averaged symbol """ + # Can't take average if the symbol evaluates on edges + if symbol.evaluates_on_edges(): + raise ValueError("Can't take the z-average of a symbol that evaluates on edges") # Symbol must have domain [] or ["current collector"] if symbol.domain not in [[], ["current collector"]]: raise pybamm.DomainError( @@ -1003,6 +1032,38 @@ def yz_average(symbol): return Integral(symbol, [y, z]) / (l_y * l_z) +def r_average(symbol): + """convenience function for creating an average in the r-direction + + Parameters + ---------- + symbol : :class:`pybamm.Symbol` + The function to be averaged + + Returns + ------- + :class:`Symbol` + the new averaged symbol + """ + # Can't take average if the symbol evaluates on edges + if symbol.evaluates_on_edges(): + raise ValueError("Can't take the r-average of a symbol that evaluates on edges") + # If symbol doesn't have a particle domain, its r-averaged value is itself + if symbol.domain not in [["positive particle"], ["negative particle"]]: + new_symbol = symbol.new_copy() + new_symbol.parent = None + return new_symbol + # If symbol is a Broadcast, its average value is its child + elif isinstance(symbol, pybamm.Broadcast): + return symbol.orphans[0] + else: + r = pybamm.SpatialVariable("r", symbol.domain) + v = pybamm.FullBroadcast( + pybamm.Scalar(1), symbol.domain, symbol.auxiliary_domains + ) + return Integral(symbol, r) / Integral(v, r) + + def boundary_value(symbol, side): """convenience function for creating a :class:`pybamm.BoundaryValue` @@ -1040,30 +1101,6 @@ def boundary_value(symbol, side): return BoundaryValue(symbol, side) -def r_average(symbol): - """convenience function for creating an average in the r-direction - - Parameters - ---------- - symbol : :class:`pybamm.Symbol` - The function to be averaged - - Returns - ------- - :class:`Symbol` - the new averaged symbol - """ - # If symbol doesn't have a particle domain, its r-averaged value is itself - if symbol.domain not in [["positive particle"], ["negative particle"]]: - new_symbol = symbol.new_copy() - new_symbol.parent = None - return new_symbol - # If symbol is a Broadcast, its average value is its child - elif isinstance(symbol, pybamm.Broadcast): - return symbol.orphans[0] - else: - r = pybamm.SpatialVariable("r", symbol.domain) - v = pybamm.FullBroadcast( - pybamm.Scalar(1), symbol.domain, symbol.auxiliary_domains - ) - return Integral(symbol, r) / Integral(v, r) +def sign(symbol): + " Returns a :class:`Sign` object. " + return Sign(symbol) diff --git a/pybamm/expression_tree/variable.py b/pybamm/expression_tree/variable.py index 935305c924..374344ab01 100644 --- a/pybamm/expression_tree/variable.py +++ b/pybamm/expression_tree/variable.py @@ -6,7 +6,7 @@ import numpy as np -class Variable(pybamm.Symbol): +class VariableBase(pybamm.Symbol): """A node in the expression tree represending a dependent variable This node will be discretised by :class:`.Discretisation` and converted @@ -48,8 +48,100 @@ def _evaluate_for_shape(self): ) +class Variable(VariableBase): + """A node in the expression tree represending a dependent variable + + This node will be discretised by :class:`.Discretisation` and converted + to a :class:`pybamm.StateVector` node. + + Parameters + ---------- + + name : str + name of the node + domain : iterable of str + list of domains that this variable is valid over + auxiliary_domains : dict + dictionary of auxiliary domains ({'secondary': ..., 'tertiary': ...}). For + example, for the single particle model, the particle concentration would be a + Variable with domain 'negative particle' and secondary auxiliary domain 'current + collector'. For the DFN, the particle concentration would be a Variable with + domain 'negative particle', secondary domain 'negative electrode' and tertiary + domain 'current collector' + + *Extends:* :class:`Symbol` + """ + + def __init__(self, name, domain=None, auxiliary_domains=None): + super().__init__(name, domain=domain, auxiliary_domains=auxiliary_domains) + + def diff(self, variable): + if variable.id == self.id: + return pybamm.Scalar(1) + elif variable.id == pybamm.t.id: + return pybamm.VariableDot( + self.name + "'", + domain=self.domain, + auxiliary_domains=self.auxiliary_domains, + ) + else: + return pybamm.Scalar(0) + + +class VariableDot(VariableBase): + """ + A node in the expression tree represending the time derviative of a dependent + variable + + This node will be discretised by :class:`.Discretisation` and converted + to a :class:`pybamm.StateVectorDot` node. + + Parameters + ---------- + + name : str + name of the node + domain : iterable of str + list of domains that this variable is valid over + auxiliary_domains : dict + dictionary of auxiliary domains ({'secondary': ..., 'tertiary': ...}). For + example, for the single particle model, the particle concentration would be a + Variable with domain 'negative particle' and secondary auxiliary domain 'current + collector'. For the DFN, the particle concentration would be a Variable with + domain 'negative particle', secondary domain 'negative electrode' and tertiary + domain 'current collector' + + *Extends:* :class:`Symbol` + """ + + def __init__(self, name, domain=None, auxiliary_domains=None): + super().__init__(name, domain=domain, auxiliary_domains=auxiliary_domains) + + def get_variable(self): + """ + return a :class:`.Variable` corresponding to this VariableDot + + Note: Variable._jac adds a dash to the name of the corresponding VariableDot, so + we remove this here + + """ + return Variable( + self.name[:-1], + domain=self._domain, + auxiliary_domains=self._auxiliary_domains, + ) + + def diff(self, variable): + if variable.id == self.id: + return pybamm.Scalar(1) + elif variable.id == pybamm.t.id: + raise pybamm.ModelError("cannot take second time derivative of a Variable") + else: + return pybamm.Scalar(0) + + class ExternalVariable(Variable): - """A node in the expression tree represending an external variable variable + """A node in the expression tree representing an external variable variable This node will be discretised by :class:`.Discretisation` and converted to a :class:`.Vector` node. @@ -84,18 +176,18 @@ def _evaluate_for_shape(self): """ See :meth:`pybamm.Symbol.evaluate_for_shape_using_domain()` """ return np.nan * np.ones((self.size, 1)) - def _base_evaluate(self, t=None, y=None, u=None): - # u should be a dictionary + def _base_evaluate(self, t=None, y=None, y_dot=None, inputs=None): + # inputs should be a dictionary # convert 'None' to empty dictionary for more informative error - if u is None: - u = {} - if not isinstance(u, dict): + if inputs is None: + inputs = {} + if not isinstance(inputs, dict): # if the special input "shape test" is passed, just return 1 - if u == "shape test": + if inputs == "shape test": return self.evaluate_for_shape() - raise TypeError("inputs u should be a dictionary") + raise TypeError("inputs should be a dictionary") try: - out = u[self.name] + out = inputs[self.name] if isinstance(out, numbers.Number) or out.shape[0] == 1: return out * np.ones((self.size, 1)) elif out.shape[0] != self.size: @@ -109,3 +201,13 @@ def _base_evaluate(self, t=None, y=None, u=None): # raise more informative error if can't find name in dict except KeyError: raise KeyError("External variable '{}' not found".format(self.name)) + + def diff(self, variable): + if variable.id == self.id: + return pybamm.Scalar(1) + elif variable.id == pybamm.t.id: + raise pybamm.ModelError( + "cannot take time derivative of an external variable" + ) + else: + return pybamm.Scalar(0) diff --git a/pybamm/geometry/standard_spatial_vars.py b/pybamm/geometry/standard_spatial_vars.py index cbb23736d1..8f547ef6da 100644 --- a/pybamm/geometry/standard_spatial_vars.py +++ b/pybamm/geometry/standard_spatial_vars.py @@ -51,40 +51,40 @@ ) # Domains at cell edges -x_n_edge = pybamm.SpatialVariable( - "x_n_edge", +x_n_edge = pybamm.SpatialVariableEdge( + "x_n", domain=["negative electrode"], auxiliary_domains={"secondary": "current collector"}, coord_sys="cartesian", ) -x_s_edge = pybamm.SpatialVariable( - "x_s_edge", +x_s_edge = pybamm.SpatialVariableEdge( + "x_s", domain=["separator"], auxiliary_domains={"secondary": "current collector"}, coord_sys="cartesian", ) -x_p_edge = pybamm.SpatialVariable( - "x_p_edge", +x_p_edge = pybamm.SpatialVariableEdge( + "x_p", domain=["positive electrode"], auxiliary_domains={"secondary": "current collector"}, coord_sys="cartesian", ) -x_edge = pybamm.SpatialVariable( - "x_edge", +x_edge = pybamm.SpatialVariableEdge( + "x", domain=whole_cell, auxiliary_domains={"secondary": "current collector"}, coord_sys="cartesian", ) -y_edge = pybamm.SpatialVariable( - "y_edge", domain="current collector", coord_sys="cartesian" +y_edge = pybamm.SpatialVariableEdge( + "y", domain="current collector", coord_sys="cartesian" ) -z_edge = pybamm.SpatialVariable( - "z_edge", domain="current collector", coord_sys="cartesian" +z_edge = pybamm.SpatialVariableEdge( + "z", domain="current collector", coord_sys="cartesian" ) -r_n_edge = pybamm.SpatialVariable( - "r_n_edge", +r_n_edge = pybamm.SpatialVariableEdge( + "r_n", domain=["negative particle"], auxiliary_domains={ "secondary": "negative electrode", @@ -92,8 +92,8 @@ }, coord_sys="spherical polar", ) -r_p_edge = pybamm.SpatialVariable( - "r_p_edge", +r_p_edge = pybamm.SpatialVariableEdge( + "r_p", domain=["positive particle"], auxiliary_domains={ "secondary": "positive electrode", diff --git a/pybamm/input/discharge_data/Ecker_1C.csv b/pybamm/input/discharge_data/Ecker_1C.csv new file mode 100644 index 0000000000..dbd1215881 --- /dev/null +++ b/pybamm/input/discharge_data/Ecker_1C.csv @@ -0,0 +1,31 @@ +20.3084233101775,4.10984760218981 +137.255118370379,4.06170981679401 +247.454888715569,4.02086563524606 +393.638257540821,3.98148017446767 +530.825726746058,3.94063599291972 +670.262170856298,3.91146157752832 +820.943489491555,3.87499355828908 +955.88198379179,3.84581914289769 +1097.56740280703,3.81226856519758 +1243.75077163228,3.79038775365404 +1383.18721574253,3.76121333826264 +1529.37058456778,3.73641508517996 +1666.55805377301,3.7247453190234 +1808.24347278826,3.70578194901899 +1947.6799168985,3.69848834517114 +2087.11636100874,3.68827729978416 +2231.05075492899,3.67514881285803 +2379.48309865925,3.66056160516233 +2521.16851767449,3.63576335207965 +2658.35598687973,3.60075405360997 +2802.29038079997,3.55407498898374 +2939.47785000521,3.51031336589665 +3081.16326902045,3.47384534665741 +3225.0976629407,3.43008372357031 +3344.29333290591,3.38194593817451 +3470.23592758612,3.30755117892646 +3553.44799907126,3.21273432890442 +3616.41929641137,3.11500003734325 +3652.40289489144,3.01872446655165 +3688.3864933715,2.89910936344693 +3715.37419223154,2.76636577341609 diff --git a/pybamm/input/discharge_data/Ecker_5C.csv b/pybamm/input/discharge_data/Ecker_5C.csv new file mode 100644 index 0000000000..621281ddff --- /dev/null +++ b/pybamm/input/discharge_data/Ecker_5C.csv @@ -0,0 +1,33 @@ +0,4.00772806063782 +2.10996257174233,3.9630519063978 +9.53641973294314,3.90466605332464 +22.2674891521443,3.85148732048119 +40.7157517827462,3.80418814691602 +60.9764236014983,3.75781321732131 +82.7105988252504,3.72697310974463 +103.708022346502,3.69851401469471 +126.547325124005,3.67723564231538 +149.386627901507,3.65476333775538 +175.172937489009,3.63351237271449 +204.643005589013,3.60393814158189 +277.21304828527,3.55566182788309 +341.310446402776,3.51924603989306 +391.409562172782,3.4910575923102 +409.091603032784,3.47331305363065 +430.457402405285,3.45679670630482 +454.401832736538,3.43791647603876 +477.60951136529,3.41067186867334 +499.712062440292,3.38938664445941 +530.287258094045,3.35265909799465 +551.653057466548,3.32539736104271 +574.123984392801,3.29098230875862 +594.384656211552,3.25535276879001 +615.013703881554,3.214950926016 +634.169248146556,3.17572931175344 +650.377785601557,3.11737737526159 +664.37606794906,3.06736240853066 +677.63759859406,3.00540126815833 +685.373491470311,2.94219480684576 +693.846136049062,2.87660733300644 +701.213653074063,2.79787632742773 +706.002539140314,2.71195774734383 diff --git a/pybamm/input/parameters/lead-acid/anodes/lead_Sulzer2019/parameters.csv b/pybamm/input/parameters/lead-acid/anodes/lead_Sulzer2019/parameters.csv index 56fc70558f..e57bb5b5bd 100644 --- a/pybamm/input/parameters/lead-acid/anodes/lead_Sulzer2019/parameters.csv +++ b/pybamm/input/parameters/lead-acid/anodes/lead_Sulzer2019/parameters.csv @@ -34,3 +34,10 @@ Electrons in hydrogen reaction,2,, Negative electrode reference exchange-current density (hydrogen) [A.m-2],1.56E-11,srinivasan2003mathematical, Hydrogen reference OCP vs SHE [V],0,srinivasan2003mathematical, Negative electrode double-layer capacity [F.m-2],0.2,, +,,, +# Density,,, +Negative electrode density [kg.m-3],113400,CRC Handbook of Chemistry and Physics, +,,, +# Thermal parameters,,, +Negative electrode specific heat capacity [J.kg-1.K-1],130,CRC Handbook of Chemistry and Physics, +Negative electrode thermal conductivity [W.m-1.K-1],35,CRC Handbook of Chemistry and Physics, diff --git a/pybamm/input/parameters/lead-acid/cathodes/lead_dioxide_Sulzer2019/parameters.csv b/pybamm/input/parameters/lead-acid/cathodes/lead_dioxide_Sulzer2019/parameters.csv index cd38f78b40..eba7599301 100644 --- a/pybamm/input/parameters/lead-acid/cathodes/lead_dioxide_Sulzer2019/parameters.csv +++ b/pybamm/input/parameters/lead-acid/cathodes/lead_dioxide_Sulzer2019/parameters.csv @@ -34,3 +34,10 @@ Electrons in hydrogen reaction,2,, Positive electrode reference exchange-current density (hydrogen) [A.m-2],0,srinivasan2003mathematical, Hydrogen reference OCP vs SHE [V],0,srinivasan2003mathematical, Positive electrode double-layer capacity [F.m-2],0.2,, +,,, +# Density,,, +Positive electrode density [kg.m-3],9375,Pubchem, +,,, +# Thermal parameters,,, +Positive electrode specific heat capacity [J.kg-1.K-1],256,NIST Chemistry WebBook SRD69, +Positive electrode thermal conductivity [W.m-1.K-1],35,assume same as lead, diff --git a/pybamm/input/parameters/lead-acid/cells/BBOXX_Sulzer2019/parameters.csv b/pybamm/input/parameters/lead-acid/cells/BBOXX_Sulzer2019/parameters.csv index 85bebe1816..5a5f2bfb84 100644 --- a/pybamm/input/parameters/lead-acid/cells/BBOXX_Sulzer2019/parameters.csv +++ b/pybamm/input/parameters/lead-acid/cells/BBOXX_Sulzer2019/parameters.csv @@ -2,9 +2,11 @@ Name [units],Value,Reference,Notes # Empty rows and rows starting with ‘#’ will be ignored,,, ,,, # Macroscale geometry,,, +Negative current collector thickness [m],0,, Negative electrode thickness [m],0.0009,Manufacturer, Separator thickness [m],0.0015,Manufacturer, Positive electrode thickness [m],0.00125,Manufacturer, +Positive current collector thickness [m],0,, Electrode height [m],0.114,Manufacturer, Electrode width [m],0.065,Manufacturer, Negative tab width [m],0.04,,Estimated value @@ -17,3 +19,18 @@ Positive tab centre z-coordinate [m],0.114,Tab at top, # Electrical,,, Cell capacity [A.h],17,Manufacturer, Typical current [A],1,, + +,,, +# Density,,, +Negative current collector density [kg.m-3],11300, same as electrode, +Positive current collector density [kg.m-3],9375,same as electrode, + +,,, +# Specific heat capacity,,, +Negative current collector specific heat capacity [J.kg-1.K-1],130,CRC Handbook of Chemistry and Physics, +Positive current collector specific heat capacity [J.kg-1.K-1],256,NIST Chemistry WebBook SRD69, +,,, +# Thermal conductivity,,, +Negative current collector thermal conductivity [W.m-1.K-1],35,CRC Handbook of Chemistry and Physics, +Positive current collector thermal conductivity [W.m-1.K-1],35,assume same as lead, + diff --git a/pybamm/input/parameters/lead-acid/electrolytes/sulfuric_acid_Sulzer2019/parameters.csv b/pybamm/input/parameters/lead-acid/electrolytes/sulfuric_acid_Sulzer2019/parameters.csv index a12c9efef2..3336a7fcc2 100644 --- a/pybamm/input/parameters/lead-acid/electrolytes/sulfuric_acid_Sulzer2019/parameters.csv +++ b/pybamm/input/parameters/lead-acid/electrolytes/sulfuric_acid_Sulzer2019/parameters.csv @@ -4,6 +4,7 @@ Name [units],Value,Reference,Notes # Electrolyte properties,,, Typical electrolyte concentration [mol.m-3],5650,, Cation transference number,0.7,, +1 + dlnf/dlnc,1,, Partial molar volume of water [m3.mol-1],1.75E-05,, Partial molar volume of anions [m3.mol-1],3.15E-05,, Partial molar volume of cations [m3.mol-1],1.35E-05,, diff --git a/pybamm/input/parameters/lead-acid/experiments/1C_discharge_from_full/parameters.csv b/pybamm/input/parameters/lead-acid/experiments/1C_discharge_from_full/parameters.csv index c1b130f248..063e70a78b 100644 --- a/pybamm/input/parameters/lead-acid/experiments/1C_discharge_from_full/parameters.csv +++ b/pybamm/input/parameters/lead-acid/experiments/1C_discharge_from_full/parameters.csv @@ -4,12 +4,16 @@ Name [units],Value,Reference,Notes # Temperature,,, Reference temperature [K],294.85,Room temperature, Maximum temperature [K],333.15,, +Ambient temperature [K], 294.85,, +Heat transfer coefficient [W.m-2.K-1],10,, +Initial temperature [K],294.85,Room temperature, + ,,, # Electrical Number of electrodes connected in parallel to make a cell,8,Manufacturer, Number of cells connected in series to make a battery,6,Manufacturer, -Lower voltage cut-off [V],1.73,,(just under) 10.5V across 6-cell battery -Upper voltage cut-off [V],2.44,,(just over) 14.5V across 6-cell battery +Lower voltage cut-off [V],1.73,(just under) 10.5V across 6-cell battery, +Upper voltage cut-off [V],2.44,(just over) 14.5V across 6-cell battery, C-rate,0.1,, ,,, # Initial conditions diff --git a/pybamm/input/parameters/lead-acid/separators/agm_Sulzer2019/parameters.csv b/pybamm/input/parameters/lead-acid/separators/agm_Sulzer2019/parameters.csv index 539bddd464..2b7f78e357 100644 --- a/pybamm/input/parameters/lead-acid/separators/agm_Sulzer2019/parameters.csv +++ b/pybamm/input/parameters/lead-acid/separators/agm_Sulzer2019/parameters.csv @@ -4,3 +4,6 @@ Name [units],Value,Reference,Notes Maximum porosity of separator,0.92,, Separator Bruggeman coefficient (electrolyte),1.5,, Separator Bruggeman coefficient (electrode),1.5,, +Separator density [kg.m-3],1680, Bulk density from Gigova 2006, +Separator specific heat capacity [J.kg-1.K-1],700, Electronics Cooling (fiberglass), +Separator thermal conductivity [W.m-1.K-1],0.04, University Physics Sears et al. 1999 (fiberglass), \ No newline at end of file diff --git a/pybamm/input/parameters/lithium-ion/anodes/graphite_Chen2020/graphite_LGM50_electrolyte_reaction_rate_Chen2020.py b/pybamm/input/parameters/lithium-ion/anodes/graphite_Chen2020/graphite_LGM50_electrolyte_reaction_rate_Chen2020.py index 2216a5aa0d..9b2a6c92be 100644 --- a/pybamm/input/parameters/lithium-ion/anodes/graphite_Chen2020/graphite_LGM50_electrolyte_reaction_rate_Chen2020.py +++ b/pybamm/input/parameters/lithium-ion/anodes/graphite_Chen2020/graphite_LGM50_electrolyte_reaction_rate_Chen2020.py @@ -26,7 +26,7 @@ def graphite_LGM50_electrolyte_reaction_rate_Chen2020(T, T_inf, E_r, R_g): Reaction rate """ - m_ref = 6.48E-7 + m_ref = 6.48e-7 arrhenius = exp(E_r / R_g * (1 / T_inf - 1 / T)) return m_ref * arrhenius diff --git a/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/README.md b/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/README.md new file mode 100644 index 0000000000..48d5988c43 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/README.md @@ -0,0 +1,12 @@ +# Graphite anode parameters + +Parameters for a graphite anode, from the papers: + +> Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of a lithium-ion battery I. determination of parameters." Journal of the Electrochemical Society 162.9 (2015): A1836-A1848. + +>Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of a lithium-ion battery II. Model validation." Journal of The Electrochemical Society 162.9 (2015): A1849-A1857. + +The fits to data for the electrode and electrolyte properties are those provided +by Dr. Simon O’Kane in the paper: + +> Richardson, Giles, et. al. "Generalised single particle models for high-rate operation of graded lithium-ion electrodes: Systematic derivation and validation." Electrochemica Acta 339 (2020): 135862 diff --git a/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/graphite_diffusivity_Ecker2015.py b/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/graphite_diffusivity_Ecker2015.py new file mode 100644 index 0000000000..7d8ffab42f --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/graphite_diffusivity_Ecker2015.py @@ -0,0 +1,42 @@ +from pybamm import exp + + +def graphite_diffusivity_Ecker2015(sto, T, T_inf, E_D_s, R_g): + """ + Graphite diffusivity as a function of stochiometry [1, 2, 3]. + + References + ---------- + .. [1] Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of + a lithium-ion battery i. determination of parameters." Journal of the + Electrochemical Society 162.9 (2015): A1836-A1848. + .. [2] Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of + a lithium-ion battery ii. model validation." Journal of The Electrochemical + Society 162.9 (2015): A1849-A1857. + .. [3] Richardson, Giles, et. al. "Generalised single particle models for + high-rate operation of graded lithium-ion electrodes: Systematic derivation + and validation." Electrochemica Acta 339 (2020): 135862 + + Parameters + ---------- + sto: :class: `numpy.Array` + Electrode stochiometry + T: :class: `numpy.Array` + Dimensional temperature + T_inf: double + Reference temperature + E_D_s: double + Solid diffusion activation energy + R_g: double + The ideal gas constant + + Returns + ------- + : double + Solid diffusivity + """ + + D_ref = 8.4e-13 * exp(-11.3 * sto) + 8.2e-15 + arrhenius = exp(-E_D_s / (R_g * T)) * exp(E_D_s / (R_g * 296)) + + return D_ref * arrhenius diff --git a/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/graphite_electrolyte_reaction_rate_Ecker2015.py b/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/graphite_electrolyte_reaction_rate_Ecker2015.py new file mode 100644 index 0000000000..0b08fce441 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/graphite_electrolyte_reaction_rate_Ecker2015.py @@ -0,0 +1,46 @@ +from pybamm import exp +from scipy import constants + + +def graphite_electrolyte_reaction_rate_Ecker2015(T, T_inf, E_r, R_g): + """ + Reaction rate for Butler-Volmer reactions between graphite and LiPF6 in EC:DMC. + + References + ---------- + .. [1] Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of + a lithium-ion battery i. determination of parameters." Journal of the + Electrochemical Society 162.9 (2015): A1836-A1848. + .. [2] Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of + a lithium-ion battery ii. model validation." Journal of The Electrochemical + Society 162.9 (2015): A1849-A1857. + .. [3] Richardson, Giles, et. al. "Generalised single particle models for + high-rate operation of graded lithium-ion electrodes: Systematic derivation + and validation." Electrochemica Acta 339 (2020): 135862 + + Parameters + ---------- + T: :class: `numpy.Array` + Dimensional temperature + T_inf: double + Reference temperature + E_r: double + Reaction activation energy + R_g: double + The ideal gas constant + + Returns + ------- + :`numpy.Array` + Reaction rate + """ + + k_ref = 1.995 * 1e-10 + + # multiply by Faraday's constant to get correct units + F = constants.physical_constants["Faraday constant"][0] + m_ref = F * k_ref + + arrhenius = exp(-E_r / (R_g * T)) * exp(E_r / (R_g * T_inf)) + + return m_ref * arrhenius diff --git a/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/graphite_ocp_Ecker2015.csv b/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/graphite_ocp_Ecker2015.csv new file mode 100644 index 0000000000..a578a7fa09 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/graphite_ocp_Ecker2015.csv @@ -0,0 +1,41 @@ +0.0015151515151514694, 1.4325153374233128 +0.0060606060606061, 0.8619631901840491 +0.010606060606060619, 0.7914110429447854 +0.016666666666666607, 0.6595092024539877 +0.021212121212121238, 0.5797546012269938 +0.022727272727272707, 0.5245398773006136 +0.030303030303030276, 0.4754601226993864 +0.039393939393939315, 0.4141104294478526 +0.045454545454545414, 0.3680981595092023 +0.05303030303030298, 0.3312883435582821 +0.06666666666666665, 0.28220858895705514 +0.07878787878787874, 0.24846625766871155 +0.08939393939393936, 0.2239263803680982 +0.10151515151515145, 0.22085889570552153 +0.12727272727272732, 0.21165644171779152 +0.14242424242424245, 0.2024539877300613 +0.15909090909090917, 0.19938650306748462 +0.17727272727272725, 0.19325153374233128 +0.19393939393939397, 0.18404907975460127 +0.21363636363636362, 0.1809815950920246 +0.23333333333333328, 0.17177914110429437 +0.25757575757575757, 0.16564417177914104 +0.2787878787878788, 0.16257668711656437 +0.303030303030303, 0.15337423312883436 +0.32878787878787885, 0.14110429447852746 +0.35151515151515156, 0.13496932515337412 +0.3712121212121211, 0.13190184049079745 +0.39242424242424234, 0.128834355828221 +0.5681818181818183, 0.1257668711656441 +0.5878787878787879, 0.12269938650306744 +0.6060606060606062, 0.1165644171779141 +0.6272727272727272, 0.10122699386503076 +0.6545454545454545, 0.0950920245398772 +0.6742424242424243, 0.0950920245398772 +0.6939393939393939, 0.08895705521472386 +0.7181818181818183, 0.08895705521472386 +0.7393939393939393, 0.08588957055214719 +0.8909090909090911, 0.08282208588957074 +0.956060606060606, 0.08272208588957074 +0.9772727272727273, 0.07975460122699385 +1, 0.07055214723926384 diff --git a/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/graphite_ocp_Ecker2015_function.py b/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/graphite_ocp_Ecker2015_function.py new file mode 100644 index 0000000000..5b57c4fb52 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/graphite_ocp_Ecker2015_function.py @@ -0,0 +1,62 @@ +from pybamm import exp, tanh + + +def graphite_ocp_Ecker2015_function(sto): + """ + Graphite OCP as a function of stochiometry [1, 2, 3]. + + References + ---------- + .. [1] Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of + a lithium-ion battery i. determination of parameters." Journal of the + Electrochemical Society 162.9 (2015): A1836-A1848. + .. [2] Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of + a lithium-ion battery ii. model validation." Journal of The Electrochemical + Society 162.9 (2015): A1849-A1857. + .. [3] Richardson, Giles, et. al. "Generalised single particle models for + high-rate operation of graded lithium-ion electrodes: Systematic derivation + and validation." Electrochemica Acta 339 (2020): 135862 + + Parameters + ---------- + sto: :class: `numpy.Array` + Electrode stochiometry + + Returns + ------- + : double + Open circuit potential + """ + + # Graphite Anode from Ecker, Kabitz, Laresgoiti et al. + # Analytical fit (WebPlotDigitizer + gnuplot) + a = 0.716502 + b = 369.028 + c = 0.12193 + d = 35.6478 + e = 0.0530947 + g = 0.0169644 + h = 27.1365 + i = 0.312832 + j = 0.0199313 + k = 28.5697 + m = 0.614221 + n = 0.931153 + o = 36.328 + p = 1.10743 + q = 0.140031 + r = 0.0189193 + s = 21.1967 + t = 0.196176 + + u_eq = ( + a * exp(-b * sto) + + c * exp(-d * (sto - e)) + - r * tanh(s * (sto - t)) + - g * tanh(h * (sto - i)) + - j * tanh(k * (sto - m)) + - n * exp(o * (sto - p)) + + q + ) + + return u_eq diff --git a/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/measured_graphite_diffusivity_Ecker2015.csv b/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/measured_graphite_diffusivity_Ecker2015.csv new file mode 100644 index 0000000000..936c491f96 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/measured_graphite_diffusivity_Ecker2015.csv @@ -0,0 +1,23 @@ +0.04291659469592768,2.5318983605709854e-13 +0.08025337997334403,4.438292394020732e-14 +0.12014956931041065,3.1947426343055446e-14 +0.15796120718295306,2.6077962994674755e-14 +0.19575227382393767,2.255908580891848e-14 +0.2357650334731653,1.16865022242173e-14 +0.27144412034088194,1.2386927328130891e-14 +0.3100288936661386,2.0291954487412547e-14 +0.3484165260222996,3.2604716406478646e-15 +0.3866704453733347,7.640156635519089e-16 +0.4264157790123111,8.417747371928056e-16 +0.4641862744217376,7.717093525037831e-16 +0.5018779134435263,8.837921553351043e-16 +0.5412118224513462,3.109171322297572e-15 +0.5766749113877059,6.062184667385398e-15 +0.6148036890800979,3.595937210129949e-14 +0.6545815938357071,2.0323093806678467e-15 +0.7681399348426814,7.802664217099549e-16 +0.8089429726375879,7.728942922045022e-16 +0.8823034129113143,9.65829674321414e-16 +0.9220864604748132,9.566999594213241e-16 +0.9607500901877076,1.2545776383613633e-15 +0.9976360226401492,1.395684713792463e-14 diff --git a/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/parameters.csv b/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/parameters.csv new file mode 100644 index 0000000000..bebcb7dcd4 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/anodes/graphite_Ecker2015/parameters.csv @@ -0,0 +1,32 @@ +Name [units],Value,Reference,Notes +# Empty rows and rows starting with ‘#’ will be ignored,,, +,,, +# Electrode properties,,, +Negative electrode conductivity [S.m-1],14,, +Maximum concentration in negative electrode [mol.m-3],31920,, +Measured negative electrode diffusivity [m2.s-1],[data]measured_graphite_diffusivity_Ecker2015,, +Negative electrode diffusivity [m2.s-1],[function]graphite_diffusivity_Ecker2015,, +Measured negative electrode OCP [V],[data]graphite_ocp_Ecker2015,, +Negative electrode OCP [V],[function]graphite_ocp_Ecker2015_function,, +,,, +# Microstructure,,, +Negative electrode porosity,0.329,, +Negative electrode active material volume fraction, 0.555,, +Negative particle radius [m],1.37E-05,, +Negative particle distribution in x,1,, +Negative electrode surface area density [m-1], 81548,, +Negative electrode Bruggeman coefficient (electrolyte),1.6372789338386007,Solve for permeability factor B=0.162=eps^b, +Negative electrode Bruggeman coefficient (electrode),0,No Bruggeman correction to the solid conductivity, +,,, +# Interfacial reactions,,, +Negative electrode cation signed stoichiometry,-1,, +Negative electrode electrons in reaction,1,, +,,, +# Thermal parameters,,, +Negative electrode OCP entropic change [V.K-1],0,, +,,, +# Activation energies,,, +Reference temperature [K],296.15,23C, +Negative electrode reaction rate,[function]graphite_electrolyte_reaction_rate_Ecker2015,, +Negative reaction rate activation energy [J.mol-1],53400,, +Negative solid diffusion activation energy [J.mol-1],3.03E+04,, diff --git a/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/README.md b/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/README.md new file mode 100644 index 0000000000..7d9d9dca8b --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/README.md @@ -0,0 +1,12 @@ +# Lithium Nickel Cobalt Oxide cathode parameters + +Parameters for a Lithium Nickel Cobalt Oxide cathode, from the papers + +> Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of a lithium-ion battery i. determination of parameters." Journal of the Electrochemical Society 162.9 (2015): A1836-A1848. + +>Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of a lithium-ion battery II. Model validation." Journal of The Electrochemical Society 162.9 (2015): A1849-A1857. + +The fits to data for the electrode and electrolyte properties are those provided +by Dr. Simon O’Kane in the paper: + +> Richardson, Giles, et. al. "Generalised single particle models for high-rate operation of graded lithium-ion electrodes: Systematic derivation and validation." Electrochemica Acta 339 (2020): 135862 diff --git a/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/measured_nco_diffusivity_Ecker2015.csv b/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/measured_nco_diffusivity_Ecker2015.csv new file mode 100644 index 0000000000..72c9f0796c --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/measured_nco_diffusivity_Ecker2015.csv @@ -0,0 +1,15 @@ +0.13943217665615135,1.8256540288152508e-13 +0.2,3.329858559514642e-13 +0.2618296529968453,3.0801228532525374e-13 +0.32239747634069404,2.6333920293821577e-13 +0.38675078864353307,1.9811969945521707e-13 +0.4460567823343848,1.418878868334243e-13 +0.5078864353312302,7.201182417750914e-14 +0.5684542586750789,2.857708698828691e-14 +0.6315457413249213,4.541608403537004e-15 +0.6933753943217664,5.479444754774098e-14 +0.7526813880126184,2.0296886656972416e-13 +0.8157728706624605,1.5882865084008814e-13 +0.8750788643533123,1.3446092039291705e-13 +0.9406940063091483,2.0545053272039484e-14 +1.0,5.4462929807295865e-15 diff --git a/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/nco_diffusivity_Ecker2015.py b/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/nco_diffusivity_Ecker2015.py new file mode 100644 index 0000000000..db3b91eea0 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/nco_diffusivity_Ecker2015.py @@ -0,0 +1,42 @@ +from pybamm import exp + + +def nco_diffusivity_Ecker2015(sto, T, T_inf, E_D_s, R_g): + """ + NCO diffusivity as a function of stochiometry [1, 2, 3]. + + References + ---------- + .. [1] Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of + a lithium-ion battery i. determination of parameters." Journal of the + Electrochemical Society 162.9 (2015): A1836-A1848. + .. [2] Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of + a lithium-ion battery ii. model validation." Journal of The Electrochemical + Society 162.9 (2015): A1849-A1857. + .. [3] Richardson, Giles, et. al. "Generalised single particle models for + high-rate operation of graded lithium-ion electrodes: Systematic derivation + and validation." Electrochemica Acta 339 (2020): 135862 + + Parameters + ---------- + sto: :class: `numpy.Array` + Electrode stochiometry + T: :class: `numpy.Array` + Dimensional temperature + T_inf: double + Reference temperature + E_D_s: double + Solid diffusion activation energy + R_g: double + The ideal gas constant + + Returns + ------- + : double + Solid diffusivity + """ + + D_ref = 3.7e-13 - 3.4e-13 * exp(-12 * (sto - 0.62) * (sto - 0.62)) + arrhenius = exp(-E_D_s / (R_g * T)) * exp(E_D_s / (R_g * T_inf)) + + return D_ref * arrhenius diff --git a/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/nco_electrolyte_reaction_rate_Ecker2015.py b/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/nco_electrolyte_reaction_rate_Ecker2015.py new file mode 100644 index 0000000000..29555c2ea5 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/nco_electrolyte_reaction_rate_Ecker2015.py @@ -0,0 +1,46 @@ +from pybamm import exp +from scipy import constants + + +def nco_electrolyte_reaction_rate_Ecker2015(T, T_inf, E_r, R_g): + """ + Reaction rate for Butler-Volmer reactions between NCO and LiPF6 in EC:DMC [1, 2, 3]. + + References + ---------- + .. [1] Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of + a lithium-ion battery i. determination of parameters." Journal of the + Electrochemical Society 162.9 (2015): A1836-A1848. + .. [2] Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of + a lithium-ion battery ii. model validation." Journal of The Electrochemical + Society 162.9 (2015): A1849-A1857. + .. [3] Richardson, Giles, et. al. "Generalised single particle models for + high-rate operation of graded lithium-ion electrodes: Systematic derivation + and validation." Electrochemica Acta 339 (2020): 135862 + + Parameters + ---------- + T: :class: `numpy.Array` + Dimensional temperature + T_inf: double + Reference temperature + E_r: double + Reaction activation energy + R_g: double + The ideal gas constant + + Returns + ------- + : double + Reaction rate + """ + + k_ref = 5.196e-11 + + # multiply by Faraday's constant to get correct units + F = constants.physical_constants["Faraday constant"][0] + m_ref = F * k_ref + + arrhenius = exp(-E_r / (R_g * T)) * exp(E_r / (R_g * T_inf)) + + return m_ref * arrhenius diff --git a/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/nco_ocp_Ecker2015.csv b/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/nco_ocp_Ecker2015.csv new file mode 100644 index 0000000000..eb8776016f --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/nco_ocp_Ecker2015.csv @@ -0,0 +1,43 @@ +0.001066078485433497, 4.584263211444843 +0.041273779983457604, 4.542437342855614 +0.065398400882272, 4.52965007211667 +0.08722543883834222, 4.523324753404195 +0.11154152498238534, 4.4989323580008005 +0.13547468063597112, 4.46985870879382 +0.15940783628955701, 4.429538485743097 +0.18174544823290373, 4.389207099839924 +0.2056786038864893, 4.3421389324829525 +0.228016215829836, 4.29955823181103 +0.25115159962830247, 4.2547337977965825 +0.275084755281888, 4.216662889514609 +0.2982201390803543, 4.178586399806408 +0.3221532947339403, 4.147263435830681 +0.3452886785324063, 4.111436260891228 +0.3692218341859923, 4.082362611684249 +0.3939527616946974, 4.055543858672244 +0.41469482992780504, 4.030946513297861 +0.4402235292916299, 4.006382656480829 +0.46176336937985707, 3.9840402073014207 +0.4864942968885624, 3.9684680281331595 +0.5088319088319091, 3.9528791046862226 +0.5327650644854949, 3.941799973629234 +0.5566982201390807, 3.923972898265999 +0.5782380602273078, 3.919624967236581 +0.598182356605296, 3.913016558585964 +0.6484419834778261, 3.911118873669402 +0.6723751391314119, 3.9112863164561573 +0.6971060666401172, 3.897963452056645 +0.7194436785834639, 3.882374528609708 +0.7433768342370495, 3.860048823708975 +0.7649166743252769, 3.817462541610827 +0.7880520581237431, 3.7883833109776224 +0.8127829856324482, 3.759315243196868 +0.8351205975757949, 3.732479745906187 +0.8590537532293809, 3.7079047262367046 +0.8821891370278472, 3.6810748103722486 +0.9053245208263132, 3.649746264970295 +0.9300554483350187, 3.6116809381145454 +0.9515952884232457, 3.5780919150913926 +0.9683484973807557, 3.557965292123382 +0.9827083907729073, 3.5423205544141934 +0.9994615997304173, 3.5199446166774333 diff --git a/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/nco_ocp_Ecker2015_function.py b/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/nco_ocp_Ecker2015_function.py new file mode 100644 index 0000000000..27905fca4a --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/nco_ocp_Ecker2015_function.py @@ -0,0 +1,56 @@ +from pybamm import tanh + + +def nco_ocp_Ecker2015_function(sto): + """ + NCO OCP as a function of stochiometry [1, 2, 3]. + + References + ---------- + .. [1] Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of + a lithium-ion battery i. determination of parameters." Journal of the + Electrochemical Society 162.9 (2015): A1836-A1848. + .. [2] Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of + a lithium-ion battery ii. model validation." Journal of The Electrochemical + Society 162.9 (2015): A1849-A1857. + .. [3] Richardson, Giles, et. al. "Generalised single particle models for + high-rate operation of graded lithium-ion electrodes: Systematic derivation + and validation." Electrochemica Acta 339 (2020): 135862 + + Parameters + ---------- + sto: double + Stochiometry of material (li-fraction) + + """ + + # LiNiCo from Ecker, Kabitz, Laresgoiti et al. + # Analytical fit (WebPlotDigitizer + gnuplot) + a = -2.35211 + c = 0.0747061 + d = 31.886 + e = 0.0219921 + g = 0.640243 + h = 5.48623 + i = 0.439245 + j = 3.82383 + k = 4.12167 + m = 0.176187 + n = 0.0542123 + o = 18.2919 + p = 0.762272 + q = 4.23285 + r = -6.34984 + s = 2.66395 + t = 0.174352 + + u_eq = ( + a * sto + - c * tanh(d * (sto - e)) + - r * tanh(s * (sto - t)) + - g * tanh(h * (sto - i)) + - j * tanh(k * (sto - m)) + - n * tanh(o * (sto - p)) + + q + ) + return u_eq diff --git a/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/parameters.csv b/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/parameters.csv new file mode 100644 index 0000000000..208ed621b4 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/parameters.csv @@ -0,0 +1,33 @@ +Name [units],Value,Reference,Notes +# Empty rows and rows starting with ‘#’ will be ignored,,, +,,, +# Electrode properties,,, +Positive electrode conductivity [S.m-1],68.1,, +Maximum concentration in positive electrode [mol.m-3],48580,, +Measured positive electrode diffusivity [m2.s-1],[data]measured_nco_diffusivity_Ecker2015,, +Positive electrode diffusivity [m2.s-1],[function]nco_diffusivity_Ecker2015,, +Measured positive electrode OCP [V],[data]nco_ocp_Ecker2015,, +Positive electrode OCP [V],[function]nco_ocp_Ecker2015_function,, +,,, +# Microstructure,,, +Positive electrode porosity,0.296,, +Positive electrode active material volume fraction, 0.58,, +Positive particle radius [m],6.5E-06,, +Positive particle distribution in x,1,, +Positive electrode surface area density [m-1],188455,, +Positive electrode Bruggeman coefficient (electrolyte),1.5442267190786427,Solve for permeability factor B=0.1526=eps^b, +Positive electrode Bruggeman coefficient (electrode),0,No Bruggeman correction to solid conductivity, +,,, +# Interfacial reactions,,, +Positive electrode cation signed stoichiometry,-1,, +Positive electrode electrons in reaction,1,, + +,,, +# Thermal parameters,,, +Positive electrode OCP entropic change [V.K-1],0,, +,,, +# Activation energies,,, +Reference temperature [K],296.15,23C, +Positive electrode reaction rate,[function]nco_electrolyte_reaction_rate_Ecker2015,, +Positive reaction rate activation energy [J.mol-1],4.36E+04,, +Positive solid diffusion activation energy [J.mol-1],8.06E+04,, diff --git a/pybamm/input/parameters/lithium-ion/cathodes/nmc_Chen2020/nmc_LGM50_electrolyte_reaction_rate_Chen2020.py b/pybamm/input/parameters/lithium-ion/cathodes/nmc_Chen2020/nmc_LGM50_electrolyte_reaction_rate_Chen2020.py index 065f126c91..9868f6cb6f 100644 --- a/pybamm/input/parameters/lithium-ion/cathodes/nmc_Chen2020/nmc_LGM50_electrolyte_reaction_rate_Chen2020.py +++ b/pybamm/input/parameters/lithium-ion/cathodes/nmc_Chen2020/nmc_LGM50_electrolyte_reaction_rate_Chen2020.py @@ -25,7 +25,7 @@ def nmc_LGM50_electrolyte_reaction_rate_Chen2020(T, T_inf, E_r, R_g): : double Reaction rate """ - m_ref = 3.59E-6 + m_ref = 3.59e-6 arrhenius = exp(E_r / R_g * (1 / T_inf - 1 / T)) return m_ref * arrhenius diff --git a/pybamm/input/parameters/lithium-ion/cells/kokam_Ecker2015/README.md b/pybamm/input/parameters/lithium-ion/cells/kokam_Ecker2015/README.md new file mode 100644 index 0000000000..c2ecfde14d --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/cells/kokam_Ecker2015/README.md @@ -0,0 +1,7 @@ +# Kokam SLPB 75106100 cell parameters + +Parameters for a Kokam SLPB 75106100 cell, from the papers + +> Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of a lithium-ion battery I. determination of parameters." Journal of the Electrochemical Society 162.9 (2015): A1836-A1848. + +>Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of a lithium-ion battery II. Model validation." Journal of The Electrochemical Society 162.9 (2015): A1849-A1857. diff --git a/pybamm/input/parameters/lithium-ion/cells/kokam_Ecker2015/parameters.csv b/pybamm/input/parameters/lithium-ion/cells/kokam_Ecker2015/parameters.csv new file mode 100644 index 0000000000..d6f636959e --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/cells/kokam_Ecker2015/parameters.csv @@ -0,0 +1,15 @@ +Name [units],Value,Reference,Notes +# Empty rows and rows starting with ‘#’ will be ignored,,, +,,, +# Macroscale geometry,,, +Negative current collector thickness [m],1.40E-05,, +Negative electrode thickness [m],7.4E-05,, +Separator thickness [m],2E-05,, +Positive electrode thickness [m],5.4E-05,, +Positive current collector thickness [m],2.5E-05,, +Electrode height [m],1.01E-01,, +Electrode width [m],8.50E-02,, +,,, +# Electrical,,, +Cell capacity [A.h], 0.15625, 7.5/48 (parameter set for a single layer cell), +Typical current [A], 0.15652,, diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Ecker2015/README.md b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Ecker2015/README.md new file mode 100644 index 0000000000..a7524227f2 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Ecker2015/README.md @@ -0,0 +1,12 @@ +# LiPF6 electrolyte parameters + +Parameters for a LiPF6 electrolyte, from the papers + +> Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of a lithium-ion battery i. determination of parameters." Journal of the Electrochemical Society 162.9 (2015): A1836-A1848. + +>Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of a lithium-ion battery II. Model validation." Journal of The Electrochemical Society 162.9 (2015): A1849-A1857. + +The fits to data for the electrode and electrolyte properties are those provided +by Dr. Simon O’Kane in the paper: + +> Richardson, Giles, et. al. "Generalised single particle models for high-rate operation of graded lithium-ion electrodes: Systematic derivation and validation." Electrochemica Acta 339 (2020): 135862 diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Ecker2015/electrolyte_conductivity_Ecker2015.py b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Ecker2015/electrolyte_conductivity_Ecker2015.py new file mode 100644 index 0000000000..45609d47ef --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Ecker2015/electrolyte_conductivity_Ecker2015.py @@ -0,0 +1,49 @@ +from pybamm import exp + + +def electrolyte_conductivity_Ecker2015(c_e, T, T_inf, E_k_e, R_g): + """ + Conductivity of LiPF6 in EC:DMC as a function of ion concentration [1, 2, 3]. + + References + ---------- + .. [1] Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of + a lithium-ion battery i. determination of parameters." Journal of the + Electrochemical Society 162.9 (2015): A1836-A1848. + .. [2] Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of + a lithium-ion battery ii. model validation." Journal of The Electrochemical + Society 162.9 (2015): A1849-A1857. + .. [3] Richardson, Giles, et. al. "Generalised single particle models for + high-rate operation of graded lithium-ion electrodes: Systematic derivation + and validation." Electrochemica Acta 339 (2020): 135862 + + Parameters + ---------- + c_e: :class: `numpy.Array` + Dimensional electrolyte concentration + T: :class: `numpy.Array` + Dimensional temperature + T_inf: double + Reference temperature + E_k_e: double + Electrolyte conductivity activation energy + R_g: double + The ideal gas constant + + Returns + ------- + :`numpy.Array` + Solid diffusivity + """ + + # mol/m^3 to mol/l + cm = 1e-3 * c_e + + # value at T = 296K + sigma_e_296 = 0.2667 * cm ** 3 - 1.2983 * cm ** 2 + 1.7919 * cm + 0.1726 + + # add temperature dependence + C = 296 * exp(E_k_e / (R_g * 296)) + sigma_e = C * sigma_e_296 * exp(-E_k_e / (R_g * T)) / T + + return sigma_e diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Ecker2015/electrolyte_diffusivity_Ecker2015.py b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Ecker2015/electrolyte_diffusivity_Ecker2015.py new file mode 100644 index 0000000000..f476343de7 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Ecker2015/electrolyte_diffusivity_Ecker2015.py @@ -0,0 +1,58 @@ +import pybamm +from scipy import constants + + +def electrolyte_diffusivity_Ecker2015(c_e, T, T_inf, E_D_e, R_g): + """ + Diffusivity of LiPF6 in EC:DMC as a function of ion concentration [1, 2, 3]. + + References + ---------- + .. [1] Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of + a lithium-ion battery i. determination of parameters." Journal of the + Electrochemical Society 162.9 (2015): A1836-A1848. + .. [2] Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of + a lithium-ion battery ii. model validation." Journal of The Electrochemical + Society 162.9 (2015): A1849-A1857. + .. [3] Richardson, Giles, et. al. "Generalised single particle models for + high-rate operation of graded lithium-ion electrodes: Systematic derivation + and validation." Electrochemica Acta 339 (2020): 135862 + + Parameters + ---------- + c_e: :class: `numpy.Array` + Dimensional electrolyte concentration + T: :class: `numpy.Array` + Dimensional temperature + T_inf: double + Reference temperature + E_D_e: double + Electrolyte diffusion activation energy + R_g: double + The ideal gas constant + + Returns + ------- + :`numpy.Array` + Solid diffusivity + """ + + # The diffusivity epends on the electrolyte conductivity + E_k_e = pybamm.Parameter("Electrolyte conductivity activation energy [J.mol-1]") + inputs = { + "Electrolyte concentration [mol.m-3]": c_e, + "Temperature [K]": T, + "Reference temperature [K]": T_inf, + "Activation energy [J.mol-1]": E_k_e, + "Ideal gas constant [J.mol-1.K-1]": R_g, + } + sigma_e = pybamm.FunctionParameter("Electrolyte conductivity [S.m-1]", inputs) + + # constants + k_b = constants.physical_constants["Boltzmann constant"][0] + F = constants.physical_constants["Faraday constant"][0] + q_e = constants.physical_constants["electron volt"][0] + + D_c_e = (k_b / (F * q_e)) * sigma_e * T / c_e + + return D_c_e diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Ecker2015/parameters.csv b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Ecker2015/parameters.csv new file mode 100644 index 0000000000..3d5d6eee40 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Ecker2015/parameters.csv @@ -0,0 +1,14 @@ +Name [units],Value,Reference,Notes +# Empty rows and rows starting with ‘#’ will be ignored,,, +,,, +# Electrolyte properties,,, +Typical electrolyte concentration [mol.m-3],1000,, +Cation transference number,0.26,, +1 + dlnf/dlnc,1,, +Electrolyte diffusivity [m2.s-1],[function]electrolyte_diffusivity_Ecker2015,, +Electrolyte conductivity [S.m-1],[function]electrolyte_conductivity_Ecker2015,, +,,, +# Activation energies,,, +Reference temperature [K],296.15,23C, +Electrolyte diffusion activation energy [J.mol-1],1.71E+04,, +Electrolyte conductivity activation energy [J.mol-1],1.71E+04,, diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/parameters.csv b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/parameters.csv index 9ea675fe67..524cf362b2 100644 --- a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/parameters.csv +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/parameters.csv @@ -4,6 +4,7 @@ Name [units],Value,Reference,Notes # Electrolyte properties,,, Typical electrolyte concentration [mol.m-3],1200,, Cation transference number,0.4,Reported as a function in Kim2011 (Implement later), +1 + dlnf/dlnc,1,, Electrolyte diffusivity [m2.s-1],[function]electrolyte_diffusivity_Kim2011,, Electrolyte conductivity [S.m-1],[function]electrolyte_conductivity_Kim2011,, ,,, diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_conductivity_Landesfeind2019_EC_DMC_1_1.py b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_conductivity_Landesfeind2019_EC_DMC_1_1.py new file mode 100644 index 0000000000..2e9cf02686 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_conductivity_Landesfeind2019_EC_DMC_1_1.py @@ -0,0 +1,26 @@ +import electrolyte_conductivity_Landesfeind2019_base as base +import numpy as np + + +def electrolyte_conductivity_Landesfeind2019_EC_DMC_1_1(c_e, T, T_inf, E_k_e, R_g): + """ + Conductivity of LiPF6 in EC:DMC (1:1) as a function of ion concentration and + Temperature. The data comes from [1]. + References + ---------- + .. [1] Landesfeind, J. and Gasteiger, H.A., 2019. Temperature and Concentration + Dependence of the Ionic Transport Properties of Lithium-Ion Battery Electrolytes. + Journal of The Electrochemical Society, 166(14), pp.A3079-A3097. + ---------- + c_e: :class: `numpy.Array` + Dimensional electrolyte concentration + T: :class: `numpy.Array` + Dimensional temperature + Returns + ------- + :`numpy.Array` + Electrolyte diffusivity + """ + coeffs = np.array([7.98e-1, 2.28e2, -1.22, 5.09e-1, -4e-3, 3.79e-3]) + + return base.electrolyte_conductivity_Landesfeind2019_base(c_e, T, coeffs) diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_conductivity_Landesfeind2019_EC_EMC_3_7.py b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_conductivity_Landesfeind2019_EC_EMC_3_7.py new file mode 100644 index 0000000000..978e9cf8c1 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_conductivity_Landesfeind2019_EC_EMC_3_7.py @@ -0,0 +1,25 @@ +import electrolyte_conductivity_Landesfeind2019_base as base +import numpy as np + + +def electrolyte_conductivity_Landesfeind2019_EC_EMC_3_7(c_e, T, T_inf, E_k_e, R_g): + """ + Conductivity of LiPF6 in EC:EMC (3:7) as a function of ion concentration and + Temperature. The data comes from [1]. + References + ---------- + .. [1] Landesfeind, J. and Gasteiger, H.A., 2019. Temperature and Concentration + Dependence of the Ionic Transport Properties of Lithium-Ion Battery Electrolytes. + Journal of The Electrochemical Society, 166(14), pp.A3079-A3097. + ---------- + c_e: :class: `numpy.Array` + Dimensional electrolyte concentration + T: :class: `numpy.Array` + Dimensional temperature + Returns + ------- + :`numpy.Array` + Electrolyte diffusivity + """ + coeffs = np.array([5.21e-1, 2.28e2, -1.06, 3.53e-1, -3.59e-3, 1.48e-3]) + return base.electrolyte_conductivity_Landesfeind2019_base(c_e, T, coeffs) diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_conductivity_Landesfeind2019_EMC_FEC_19_1.py b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_conductivity_Landesfeind2019_EMC_FEC_19_1.py new file mode 100644 index 0000000000..fbeba02856 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_conductivity_Landesfeind2019_EMC_FEC_19_1.py @@ -0,0 +1,25 @@ +import electrolyte_conductivity_Landesfeind2019_base as base +import numpy as np + + +def electrolyte_conductivity_Landesfeind2019_EMC_FEC_19_1(c_e, T, T_inf, E_k_e, R_g): + """ + Conductivity of LiPF6 in EMC:FEC (19:1) as a function of ion concentration and + Temperature. The data comes from [1]. + References + ---------- + .. [1] Landesfeind, J. and Gasteiger, H.A., 2019. Temperature and Concentration + Dependence of the Ionic Transport Properties of Lithium-Ion Battery Electrolytes. + Journal of The Electrochemical Society, 166(14), pp.A3079-A3097. + ---------- + c_e: :class: `numpy.Array` + Dimensional electrolyte concentration + T: :class: `numpy.Array` + Dimensional temperature + Returns + ------- + :`numpy.Array` + Electrolyte diffusivity + """ + coeffs = np.array([2.51e-2, 1.75e2, 1.23, 2.05e-1, -8.81e-2, 2.83e-3]) + return base.electrolyte_conductivity_Landesfeind2019_base(c_e, T, coeffs) diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_conductivity_Landesfeind2019_base.py b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_conductivity_Landesfeind2019_base.py new file mode 100644 index 0000000000..4a2e45b50b --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_conductivity_Landesfeind2019_base.py @@ -0,0 +1,32 @@ +from pybamm import exp, sqrt + + +def electrolyte_conductivity_Landesfeind2019_base(c_e, T, coeffs): + """ + Conductivity of LiPF6 in solvent_X as a function of ion concentration and + Temperature. The data comes from [1]. + References + ---------- + .. [1] Landesfeind, J. and Gasteiger, H.A., 2019. Temperature and Concentration + Dependence of the Ionic Transport Properties of Lithium-Ion Battery Electrolytes. + Journal of The Electrochemical Society, 166(14), pp.A3079-A3097. + ---------- + c_e: :class: `numpy.Array` + Dimensional electrolyte concentration + T: :class: `numpy.Array` + Dimensional temperature + coeffs: :class: `numpy.Array` + Fitting parameter coefficients + Returns + ------- + :`numpy.Array` + Electrolyte diffusivity + """ + c = c_e / 1000 # mol.m-3 -> mol.l + p1, p2, p3, p4, p5, p6 = coeffs + A = p1 * (1 + (T - p2)) + B = 1 + p3 * sqrt(c) + p4 * (1 + p5 * exp(1000 / T)) * c + C = 1 + c ** 4 * (p6 * exp(1000 / T)) + sigma_e = A * c * B / C # mS.cm-1 + + return sigma_e / 10 diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_diffusivity_Landesfeind2019_EC_DMC_1_1.py b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_diffusivity_Landesfeind2019_EC_DMC_1_1.py new file mode 100644 index 0000000000..510cb02fb5 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_diffusivity_Landesfeind2019_EC_DMC_1_1.py @@ -0,0 +1,26 @@ +import electrolyte_diffusivity_Landesfeind2019_base as base +import numpy as np + + +def electrolyte_diffusivity_Landesfeind2019_EC_DMC_1_1(c_e, T, T_inf, E_k_e, R_g): + """ + Diffusivity of LiPF6 in EC:DMC (1:1) as a function of ion concentration and + Temperature. The data comes from [1]. + References + ---------- + .. [1] Landesfeind, J. and Gasteiger, H.A., 2019. Temperature and Concentration + Dependence of the Ionic Transport Properties of Lithium-Ion Battery Electrolytes. + Journal of The Electrochemical Society, 166(14), pp.A3079-A3097. + ---------- + c_e: :class: `numpy.Array` + Dimensional electrolyte concentration + T: :class: `numpy.Array` + Dimensional temperature + Returns + ------- + :`numpy.Array` + Electrolyte diffusivity + """ + coeffs = np.array([1.47e3, 1.33, -1.69e3, -5.63e2]) + + return base.electrolyte_diffusivity_Landesfeind2019_base(c_e, T, coeffs) diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_diffusivity_Landesfeind2019_EC_EMC_3_7.py b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_diffusivity_Landesfeind2019_EC_EMC_3_7.py new file mode 100644 index 0000000000..108ffc1693 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_diffusivity_Landesfeind2019_EC_EMC_3_7.py @@ -0,0 +1,25 @@ +import electrolyte_diffusivity_Landesfeind2019_base as base +import numpy as np + + +def electrolyte_diffusivity_Landesfeind2019_EC_EMC_3_7(c_e, T, T_inf, E_k_e, R_g): + """ + Diffusivity of LiPF6 in EC:EMC (3:7) as a function of ion concentration and + Temperature. The data comes from [1]. + References + ---------- + .. [1] Landesfeind, J. and Gasteiger, H.A., 2019. Temperature and Concentration + Dependence of the Ionic Transport Properties of Lithium-Ion Battery Electrolytes. + Journal of The Electrochemical Society, 166(14), pp.A3079-A3097. + ---------- + c_e: :class: `numpy.Array` + Dimensional electrolyte concentration + T: :class: `numpy.Array` + Dimensional temperature + Returns + ------- + :`numpy.Array` + Electrolyte diffusivity + """ + coeffs = np.array([1.01e3, 1.01, -1.56e3, -4.87e2]) + return base.electrolyte_diffusivity_Landesfeind2019_base(c_e, T, coeffs) diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_diffusivity_Landesfeind2019_EMC_FEC_19_1.py b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_diffusivity_Landesfeind2019_EMC_FEC_19_1.py new file mode 100644 index 0000000000..871751e43e --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_diffusivity_Landesfeind2019_EMC_FEC_19_1.py @@ -0,0 +1,25 @@ +import electrolyte_diffusivity_Landesfeind2019_base as base +import numpy as np + + +def electrolyte_diffusivity_Landesfeind2019_EMC_FEC_19_1(c_e, T, T_inf, E_k_e, R_g): + """ + Diffusivity of LiPF6 in EMC:FEC (19:1) as a function of ion concentration and + Temperature. The data comes from [1]. + References + ---------- + .. [1] Landesfeind, J. and Gasteiger, H.A., 2019. Temperature and Concentration + Dependence of the Ionic Transport Properties of Lithium-Ion Battery Electrolytes. + Journal of The Electrochemical Society, 166(14), pp.A3079-A3097. + ---------- + c_e: :class: `numpy.Array` + Dimensional electrolyte concentration + T: :class: `numpy.Array` + Dimensional temperature + Returns + ------- + :`numpy.Array` + Electrolyte diffusivity + """ + coeffs = np.array([5.86e2, 1.33, -1.38e3, -5.82e2]) + return base.electrolyte_diffusivity_Landesfeind2019_base(c_e, T, coeffs) diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_diffusivity_Landesfeind2019_base.py b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_diffusivity_Landesfeind2019_base.py new file mode 100644 index 0000000000..7c78b94152 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019/electrolyte_diffusivity_Landesfeind2019_base.py @@ -0,0 +1,32 @@ +from pybamm import exp + + +def electrolyte_diffusivity_Landesfeind2019_base(c_e, T, coeffs): + """ + Conductivity of LiPF6 in solvent_X as a function of ion concentration and + Temperature. The data comes from [1]. + References + ---------- + .. [1] Landesfeind, J. and Gasteiger, H.A., 2019. Temperature and Concentration + Dependence of the Ionic Transport Properties of Lithium-Ion Battery Electrolytes. + Journal of The Electrochemical Society, 166(14), pp.A3079-A3097. + ---------- + c_e: :class: `numpy.Array` + Dimensional electrolyte concentration + T: :class: `numpy.Array` + Dimensional temperature + coeffs: :class: `numpy.Array` + Fitting parameter coefficients + Returns + ------- + :`numpy.Array` + Electrolyte diffusivity + """ + c = c_e / 1000 # mol.m-3 -> mol.l + p1, p2, p3, p4 = coeffs + A = p1 * exp(p2 * c) + B = exp(p3 / T) + C = exp(p4 * c / T) + D_e = A * B * C * 1e-10 # m2/s + + return D_e diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Marquis2019/parameters.csv b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Marquis2019/parameters.csv index c77aca897b..cd52483d6d 100644 --- a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Marquis2019/parameters.csv +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Marquis2019/parameters.csv @@ -4,6 +4,7 @@ Name [units],Value,Reference,Notes # Electrolyte properties,,, Typical electrolyte concentration [mol.m-3],1000,Scott Moura FastDFN, Cation transference number,0.4,Scott Moura FastDFN, +1 + dlnf/dlnc,1,, Electrolyte diffusivity [m2.s-1],[function]electrolyte_diffusivity_Capiglia1999,, Electrolyte conductivity [S.m-1],[function]electrolyte_conductivity_Capiglia1999,, ,,, diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Nyman2008/electrolyte_conductivity_Nyman2008.py b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Nyman2008/electrolyte_conductivity_Nyman2008.py index 8963ebe575..168b35e6e3 100644 --- a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Nyman2008/electrolyte_conductivity_Nyman2008.py +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Nyman2008/electrolyte_conductivity_Nyman2008.py @@ -29,9 +29,7 @@ def electrolyte_conductivity_Nyman2008(c_e, T, T_inf, E_k_e, R_g): """ sigma_e = ( - 0.1297 * (c_e / 1000) ** 3 - - 2.51 * (c_e / 1000) ** 1.5 - + 3.329 * (c_e / 1000) + 0.1297 * (c_e / 1000) ** 3 - 2.51 * (c_e / 1000) ** 1.5 + 3.329 * (c_e / 1000) ) arrhenius = exp(E_k_e / R_g * (1 / T_inf - 1 / T)) diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Nyman2008/electrolyte_diffusivity_Nyman2008.py b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Nyman2008/electrolyte_diffusivity_Nyman2008.py index af26eb3549..a93baf8183 100644 --- a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Nyman2008/electrolyte_diffusivity_Nyman2008.py +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Nyman2008/electrolyte_diffusivity_Nyman2008.py @@ -28,11 +28,7 @@ def electrolyte_diffusivity_Nyman2008(c_e, T, T_inf, E_D_e, R_g): Solid diffusivity """ - D_c_e = ( - 8.794E-11 * (c_e / 1000) ** 2 - - 3.972E-10 * (c_e / 1000) - + 4.862E-10 - ) + D_c_e = 8.794e-11 * (c_e / 1000) ** 2 - 3.972e-10 * (c_e / 1000) + 4.862e-10 arrhenius = exp(E_D_e / R_g * (1 / T_inf - 1 / T)) return D_c_e * arrhenius diff --git a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Nyman2008/parameters.csv b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Nyman2008/parameters.csv index f751d17511..f291c1b66f 100644 --- a/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Nyman2008/parameters.csv +++ b/pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Nyman2008/parameters.csv @@ -4,6 +4,7 @@ Name [units],Value,Reference,Notes # Electrolyte properties,,, Typical electrolyte concentration [mol.m-3],1000,Chen 2020, Cation transference number,0.2594,Chen 2020, +1 + dlnf/dlnc,1,, Electrolyte diffusivity [m2.s-1],[function]electrolyte_diffusivity_Nyman2008,Nyman 2008," " Electrolyte conductivity [S.m-1],[function]electrolyte_conductivity_Nyman2008,Nyman 2008," " ,,, diff --git a/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Chen2020/parameters.csv b/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Chen2020/parameters.csv index f35d901edd..a826e8176f 100644 --- a/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Chen2020/parameters.csv +++ b/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Chen2020/parameters.csv @@ -4,6 +4,8 @@ Name [units],Value,Reference,Notes # Temperature Reference temperature [K],298.15,25C, Heat transfer coefficient [W.m-2.K-1],10,, +Ambient temperature [K], 298.15,, + ,,, # Electrical Number of electrodes connected in parallel to make a cell,1,, diff --git a/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Ecker2015/README.md b/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Ecker2015/README.md new file mode 100644 index 0000000000..ae69fbf332 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Ecker2015/README.md @@ -0,0 +1,6 @@ +# 1C discharge from full + +Discharge lithium-ion battery from full charge at 1C, using the initial conditions from the paper + +>Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of a lithium-ion battery II. Model validation." Journal of The Electrochemical Society 162.9 (2015): A1849-A1857.. + diff --git a/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Ecker2015/parameters.csv b/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Ecker2015/parameters.csv new file mode 100644 index 0000000000..fd2cc7c03e --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Ecker2015/parameters.csv @@ -0,0 +1,21 @@ +Name [units],Value,Reference,Notes +# Empty rows and rows starting with ‘#’ will be ignored,,, +,,, +# Temperature +Reference temperature [K],296.15,23C, +Heat transfer coefficient [W.m-2.K-1],10, The paper does not consider thermal effects so a typical value is chosen, +Ambient temperature [K], 298.15,, + +,,, +# Electrical +Number of electrodes connected in parallel to make a cell,1,, +Number of cells connected in series to make a battery,1,, +Lower voltage cut-off [V],2.5,, +Upper voltage cut-off [V],4.2,, +C-rate,1,, +,,, +# Initial conditions +Initial concentration in negative electrode [mol.m-3], 26120.05,, +Initial concentration in positive electrode [mol.m-3], 12630.8,, +Initial concentration in electrolyte [mol.m-3],1000,, +Initial temperature [K],298.15,, diff --git a/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Kim2011/parameters.csv b/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Kim2011/parameters.csv index 1ed5821c2f..335b774a1d 100644 --- a/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Kim2011/parameters.csv +++ b/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Kim2011/parameters.csv @@ -4,6 +4,8 @@ Name [units],Value,Reference,Notes # Temperature Reference temperature [K],298.15,25C, Heat transfer coefficient [W.m-2.K-1],25,, +Ambient temperature [K], 298.15,, + ,,, # Electrical Number of electrodes connected in parallel to make a cell,1,, diff --git a/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Marquis2019/parameters.csv b/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Marquis2019/parameters.csv index 80a2746b6a..37f1a11a3b 100644 --- a/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Marquis2019/parameters.csv +++ b/pybamm/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Marquis2019/parameters.csv @@ -3,6 +3,7 @@ Name [units],Value,Reference,Notes ,,, # Temperature Reference temperature [K],298.15,25C, +Ambient temperature [K], 298.15,, Heat transfer coefficient [W.m-2.K-1],10,, ,,, # Electrical diff --git a/pybamm/input/parameters/lithium-ion/separators/separator_Ecker2015/README.md b/pybamm/input/parameters/lithium-ion/separators/separator_Ecker2015/README.md new file mode 100644 index 0000000000..27cf2f78f5 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/separators/separator_Ecker2015/README.md @@ -0,0 +1,7 @@ +# Separator parameters + +Parameters for the separator from the papers + +> Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of a lithium-ion battery i. determination of parameters." Journal of the Electrochemical Society 162.9 (2015): A1836-A1848. + +>Ecker, Madeleine, et al. "Parameterization of a physico-chemical model of a lithium-ion battery II. Model validation." Journal of The Electrochemical Society 162.9 (2015): A1849-A1857. diff --git a/pybamm/input/parameters/lithium-ion/separators/separator_Ecker2015/parameters.csv b/pybamm/input/parameters/lithium-ion/separators/separator_Ecker2015/parameters.csv new file mode 100644 index 0000000000..b1f6cf21a9 --- /dev/null +++ b/pybamm/input/parameters/lithium-ion/separators/separator_Ecker2015/parameters.csv @@ -0,0 +1,6 @@ +Name [units],Value,Reference,Notes +# Empty rows and rows starting with ‘#’ will be ignored,,, +,,, +Separator porosity,0.508,, +Separator Bruggeman coefficient (electrolyte),1.9804586773134942, Solve for permeability factor B=0.304=eps^b, +Separator Bruggeman coefficient (electrode),0,No Bruggeman correction to solid conductivity, diff --git a/pybamm/meshes/meshes.py b/pybamm/meshes/meshes.py index f6da5633c7..7cfa06ad2e 100644 --- a/pybamm/meshes/meshes.py +++ b/pybamm/meshes/meshes.py @@ -171,6 +171,11 @@ def combine_submeshes(self, *submeshnames): ) coord_sys = self[submeshnames[0]][i].coord_sys submeshes[i] = pybamm.SubMesh1D(combined_submesh_edges, coord_sys) + # add in internal boundaries + submeshes[i].internal_boundaries = [ + self[submeshname][i].edges[0] for submeshname in submeshnames[1:] + ] + return submeshes def add_ghost_meshes(self): diff --git a/pybamm/meshes/one_dimensional_submeshes.py b/pybamm/meshes/one_dimensional_submeshes.py index 69ea5ace75..48aace8740 100644 --- a/pybamm/meshes/one_dimensional_submeshes.py +++ b/pybamm/meshes/one_dimensional_submeshes.py @@ -33,6 +33,7 @@ def __init__(self, edges, coord_sys, tabs=None): self.d_nodes = np.diff(self.nodes) self.npts = self.nodes.size self.coord_sys = coord_sys + self.internal_boundaries = [] # Add tab locations in terms of "left" and "right" if tabs: diff --git a/pybamm/models/base_model.py b/pybamm/models/base_model.py index f8ae8c5c17..410d5a9a68 100644 --- a/pybamm/models/base_model.py +++ b/pybamm/models/base_model.py @@ -381,6 +381,7 @@ def check_well_posedness(self, post_discretisation=False): post_discretisation : boolean A flag indicating tests to be skipped after discretisation """ + self.check_for_time_derivatives() self.check_well_determined(post_discretisation) self.check_algebraic_equations(post_discretisation) self.check_ics_bcs() @@ -391,6 +392,35 @@ def check_well_posedness(self, post_discretisation=False): if pybamm.settings.debug_mode is True and post_discretisation is False: self.check_variables() + def check_for_time_derivatives(self): + # Check that no variable time derivatives exist in the rhs equations + for key, eq in self.rhs.items(): + for node in eq.pre_order(): + if isinstance(node, pybamm.VariableDot): + raise pybamm.ModelError( + "time derivative of variable" + + " found ({}) in rhs equation {}".format(node, key) + ) + if isinstance(node, pybamm.StateVectorDot): + raise pybamm.ModelError( + "time derivative of state vector" + + " found ({}) in rhs equation {}".format(node, key) + ) + + # Check that no variable time derivatives exist in the algebraic equations + for key, eq in self.algebraic.items(): + for node in eq.pre_order(): + if isinstance(node, pybamm.VariableDot): + raise pybamm.ModelError( + "time derivative of variable found ({}) in algebraic" + "equation {}".format(node, key) + ) + if isinstance(node, pybamm.StateVectorDot): + raise pybamm.ModelError( + "time derivative of state vector found ({}) in algebraic" + "equation {}".format(node, key) + ) + def check_well_determined(self, post_discretisation): """ Check that the model is not under- or over-determined. """ # Equations (differential and algebraic) @@ -410,6 +440,13 @@ def check_well_determined(self, post_discretisation): vars_in_eqns.update( [x.id for x in eqn.pre_order() if isinstance(x, pybamm.Variable)] ) + vars_in_eqns.update( + [ + x.get_variable().id + for x in eqn.pre_order() + if isinstance(x, pybamm.VariableDot) + ] + ) for var, eqn in self.algebraic.items(): vars_in_algebraic_keys.update( [x.id for x in var.pre_order() if isinstance(x, pybamm.Variable)] @@ -417,11 +454,25 @@ def check_well_determined(self, post_discretisation): vars_in_eqns.update( [x.id for x in eqn.pre_order() if isinstance(x, pybamm.Variable)] ) + vars_in_eqns.update( + [ + x.get_variable().id + for x in eqn.pre_order() + if isinstance(x, pybamm.VariableDot) + ] + ) for var, side_eqn in self.boundary_conditions.items(): for side, (eqn, typ) in side_eqn.items(): vars_in_eqns.update( [x.id for x in eqn.pre_order() if isinstance(x, pybamm.Variable)] ) + vars_in_eqns.update( + [ + x.get_variable().id + for x in eqn.pre_order() + if isinstance(x, pybamm.VariableDot) + ] + ) # If any keys are repeated between rhs and algebraic then the model is # overdetermined if not set(vars_in_rhs_keys).isdisjoint(vars_in_algebraic_keys): @@ -572,6 +623,32 @@ def check_variables(self): ) ) + def info(self, symbol_name): + """ + Provides helpful summary information for a symbol. + + Parameters + ---------- + parameter_name : str + """ + + div = "-----------------------------------------" + symbol = find_symbol_in_model(self, symbol_name) + + if not symbol: + return None + + print(div) + print(symbol_name, "\n") + print(type(symbol)) + + if isinstance(symbol, pybamm.FunctionParameter): + print("") + print("Inputs:") + symbol.print_input_names() + + print(div) + @property def default_solver(self): "Return default solver based on whether model is ODE model or DAE model" @@ -582,3 +659,33 @@ def default_solver(self): return pybamm.IDAKLUSolver() else: return pybamm.CasadiSolver(mode="safe") + + +# helper functions for finding symbols +def find_symbol_in_tree(tree, name): + if name == tree.name: + return tree + elif len(tree.children) > 0: + for child in tree.children: + child_return = find_symbol_in_tree(child, name) + if child_return: + return child_return + + +def find_symbol_in_dict(dic, name): + for tree in dic.values(): + tree_return = find_symbol_in_tree(tree, name) + if tree_return: + return tree_return + + +def find_symbol_in_model(model, name): + dics = [ + model.rhs, + model.algebraic, + model.variables, + ] + for dic in dics: + dic_return = find_symbol_in_dict(dic, name) + if dic_return: + return dic_return diff --git a/pybamm/models/event.py b/pybamm/models/event.py index 5a9cafb159..cc2509e238 100644 --- a/pybamm/models/event.py +++ b/pybamm/models/event.py @@ -13,6 +13,7 @@ class EventType(Enum): to the discontinuity and then restart just after the discontinuity. """ + TERMINATION = 0 DISCONTINUITY = 1 @@ -40,11 +41,11 @@ def __init__(self, name, expression, event_type=EventType.TERMINATION): self._expression = expression self._event_type = event_type - def evaluate(self, t=None, y=None, u=None, known_evals=None): + def evaluate(self, t=None, y=None, y_dot=None, inputs=None, known_evals=None): """ Acts as a drop-in replacement for :func:`pybamm.Symbol.evaluate` """ - return self._expression.evaluate(t, y, u, known_evals) + return self._expression.evaluate(t, y, y_dot, inputs, known_evals) def __str__(self): return self._name diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index bdc74f271b..97119f2b73 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -37,18 +37,13 @@ class BaseBatteryModel(pybamm.BaseModel): (default) or "varying". Not currently implemented in any of the models. * "current collector" : str, optional Sets the current collector model to use. Can be "uniform" (default), - "potential pair", "potential pair quite conductive", or - "set external potential". The submodel "set external potential" can only - be used with the SPM. + "potential pair" or "potential pair quite conductive". * "particle" : str, optional Sets the submodel to use to describe behaviour within the particle. Can be "Fickian diffusion" (default) or "fast diffusion". * "thermal" : str, optional Sets the thermal model to use. Can be "isothermal" (default), - "x-full", "x-lumped", "xyz-lumped", "lumped" or "set external - temperature". Must be "isothermal" for lead-acid models. If the - option "set external temperature" is selected then "dimensionality" - must be 1. + "x-full", "x-lumped", "xyz-lumped" or "lumped". * "thermal current collector" : bool, optional Whether to include thermal effects in the current collector in one-dimensional models (default is False). Note that this option @@ -156,14 +151,18 @@ def options(self, extra_options): "thermal current collector": False, "external submodels": [], } - options = default_options + options = pybamm.FuzzyDict(default_options) # any extra options overwrite the default options if extra_options is not None: for name, opt in extra_options.items(): if name in default_options: options[name] = opt else: - raise pybamm.OptionError("option {} not recognised".format(name)) + raise pybamm.OptionError( + "Option '{}' not recognised. Best matches are {}".format( + name, options.get_best_matches(name) + ) + ) # Some standard checks to make sure options are compatible if not ( @@ -193,7 +192,6 @@ def options(self, extra_options): "uniform", "potential pair", "potential pair quite conductive", - "set external potential", ]: raise pybamm.OptionError( "current collector model '{}' not recognised".format( @@ -212,7 +210,6 @@ def options(self, extra_options): "x-lumped", "xyz-lumped", "lumped", - "set external temperature", ]: raise pybamm.OptionError( "Unknown thermal model '{}'".format(options["thermal"]) @@ -224,31 +221,22 @@ def options(self, extra_options): # Options that are incompatible with models if isinstance(self, pybamm.lithium_ion.BaseModel): - # if options["surface form"] is not False: - # raise pybamm.OptionError( - # "surface form not implemented for lithium-ion models" - # ) if options["convection"] is True: raise pybamm.OptionError( "convection not implemented for lithium-ion models" ) if isinstance(self, pybamm.lead_acid.BaseModel): - if options["thermal"] != "isothermal": + if options["thermal"] != "isothermal" and options["dimensionality"] != 0: raise pybamm.OptionError( - "thermal effects not implemented for lead-acid models" + "Lead-acid models can only have thermal " + "effects if dimensionality is 0." ) + if options["thermal current collector"] is True: raise pybamm.OptionError( - "thermal effects not implemented for lead-acid models" - ) - if options["current collector"] == "set external potential" and not isinstance( - self, pybamm.lithium_ion.SPM - ): - raise pybamm.OptionError( - "option {} only compatible with SPM".format( - options["current collector"] + "Thermal current collector effects are not implemented " + "for lead-acid models." ) - ) self._options = options @@ -281,7 +269,7 @@ def set_standard_output_variables(self): } ) if self.options["dimensionality"] == 1: - self.variables.update({"y": var.y, "y [m]": var.y * L_y}) + self.variables.update({"z": var.z, "z [m]": var.z * L_z}) elif self.options["dimensionality"] == 2: self.variables.update( {"y": var.y, "y [m]": var.y * L_y, "z": var.z, "z [m]": var.z * L_z} @@ -544,14 +532,6 @@ def set_thermal_submodel(self): self.param ) - elif self.options["thermal"] == "set external temperature": - if self.options["dimensionality"] == 1: - thermal_submodel = pybamm.thermal.x_lumped.SetTemperature1D(self.param) - elif self.options["dimensionality"] in [0, 2]: - raise NotImplementedError( - """Set temperature model only implemented for 1D current - collectors""" - ) self.submodels["thermal"] = thermal_submodel def set_current_collector_submodel(self): @@ -563,20 +543,6 @@ def set_current_collector_submodel(self): submodel = pybamm.current_collector.PotentialPair1plus1D(self.param) elif self.options["dimensionality"] == 2: submodel = pybamm.current_collector.PotentialPair2plus1D(self.param) - elif self.options["current collector"] == "set external potential": - if self.options["dimensionality"] == 1: - submodel = pybamm.current_collector.SetPotentialSingleParticle1plus1D( - self.param - ) - elif self.options["dimensionality"] == 2: - submodel = pybamm.current_collector.SetPotentialSingleParticle2plus1D( - self.param - ) - elif self.options["dimensionality"] == 0: - raise NotImplementedError( - """Set potential model only implemented for 1D or 2D current - collectors""" - ) self.submodels["current collector"] = submodel def set_voltage_variables(self): diff --git a/pybamm/models/full_battery_models/lead_acid/base_lead_acid_model.py b/pybamm/models/full_battery_models/lead_acid/base_lead_acid_model.py index 97fb53115a..4a9da79620 100644 --- a/pybamm/models/full_battery_models/lead_acid/base_lead_acid_model.py +++ b/pybamm/models/full_battery_models/lead_acid/base_lead_acid_model.py @@ -63,19 +63,19 @@ def set_reactions(self): icd = " interfacial current density" self.reactions = { "main": { - "Negative": {"s": param.s_n, "aj": "Negative electrode" + icd}, - "Positive": {"s": param.s_p, "aj": "Positive electrode" + icd}, + "Negative": {"s": -param.s_plus_n_S, "aj": "Negative electrode" + icd}, + "Positive": {"s": -param.s_plus_p_S, "aj": "Positive electrode" + icd}, } } if "oxygen" in self.options["side reactions"]: self.reactions["oxygen"] = { "Negative": { - "s": -(param.s_plus_Ox + param.t_plus), + "s": -param.s_plus_Ox, "s_ox": -param.s_ox_Ox, "aj": "Negative electrode oxygen" + icd, }, "Positive": { - "s": -(param.s_plus_Ox + param.t_plus), + "s": -param.s_plus_Ox, "s_ox": -param.s_ox_Ox, "aj": "Positive electrode oxygen" + icd, }, diff --git a/pybamm/models/full_battery_models/lead_acid/full.py b/pybamm/models/full_battery_models/lead_acid/full.py index 331b26b993..02d6afc23b 100644 --- a/pybamm/models/full_battery_models/lead_acid/full.py +++ b/pybamm/models/full_battery_models/lead_acid/full.py @@ -61,11 +61,11 @@ def set_convection_submodel(self): self.submodels["convection"] = pybamm.convection.Full(self.param) def set_interfacial_submodel(self): - self.submodels["negative interface"] = pybamm.interface.lead_acid.ButlerVolmer( - self.param, "Negative" + self.submodels["negative interface"] = pybamm.interface.ButlerVolmer( + self.param, "Negative", "lead-acid main" ) - self.submodels["positive interface"] = pybamm.interface.lead_acid.ButlerVolmer( - self.param, "Positive" + self.submodels["positive interface"] = pybamm.interface.ButlerVolmer( + self.param, "Positive", "lead-acid main" ) def set_solid_submodel(self): @@ -108,22 +108,21 @@ def set_side_reaction_submodels(self): self.submodels["oxygen diffusion"] = pybamm.oxygen_diffusion.Full( self.param, self.reactions ) - self.submodels[ - "positive oxygen interface" - ] = pybamm.interface.lead_acid_oxygen.ForwardTafel(self.param, "Positive") + self.submodels["positive oxygen interface"] = pybamm.interface.ForwardTafel( + self.param, "Positive", "lead-acid oxygen" + ) self.submodels[ "negative oxygen interface" - ] = pybamm.interface.lead_acid_oxygen.FullDiffusionLimited( - self.param, "Negative" + ] = pybamm.interface.DiffusionLimited( + self.param, "Negative", "lead-acid oxygen", order="full" ) else: self.submodels["oxygen diffusion"] = pybamm.oxygen_diffusion.NoOxygen( self.param ) - self.submodels[ - "positive oxygen interface" - ] = pybamm.interface.lead_acid_oxygen.NoReaction(self.param, "Positive") - self.submodels[ - "negative oxygen interface" - ] = pybamm.interface.lead_acid_oxygen.NoReaction(self.param, "Negative") - + self.submodels["positive oxygen interface"] = pybamm.interface.NoReaction( + self.param, "Positive", "lead-acid oxygen" + ) + self.submodels["negative oxygen interface"] = pybamm.interface.NoReaction( + self.param, "Negative", "lead-acid oxygen" + ) diff --git a/pybamm/models/full_battery_models/lead_acid/higher_order.py b/pybamm/models/full_battery_models/lead_acid/higher_order.py index 4255856d39..ef61ae462a 100644 --- a/pybamm/models/full_battery_models/lead_acid/higher_order.py +++ b/pybamm/models/full_battery_models/lead_acid/higher_order.py @@ -81,7 +81,7 @@ def set_leading_order_model(self): self.options, name="LOQS model (for composite model)" ) self.update(leading_order_model) - self.reaction_submodels = leading_order_model.reaction_submodels + self.leading_order_reaction_submodels = leading_order_model.reaction_submodels # Leading-order variables leading_order_variables = {} @@ -99,16 +99,14 @@ def set_leading_order_model(self): def set_average_interfacial_submodel(self): self.submodels[ "x-averaged negative interface" - ] = pybamm.interface.lead_acid.InverseFirstOrderKinetics(self.param, "Negative") - self.submodels[ - "x-averaged negative interface" - ].reaction_submodels = self.reaction_submodels["Negative"] - self.submodels[ - "x-averaged positive interface" - ] = pybamm.interface.lead_acid.InverseFirstOrderKinetics(self.param, "Positive") + ] = pybamm.interface.InverseFirstOrderKinetics( + self.param, "Negative", self.leading_order_reaction_submodels["Negative"] + ) self.submodels[ "x-averaged positive interface" - ].reaction_submodels = self.reaction_submodels["Positive"] + ] = pybamm.interface.InverseFirstOrderKinetics( + self.param, "Positive", self.leading_order_reaction_submodels["Positive"] + ) def set_electrolyte_conductivity_submodel(self): self.submodels[ @@ -131,24 +129,32 @@ def set_full_interface_submodel(self): densities """ # Main reaction - self.submodels[ - "negative interface" - ] = pybamm.interface.lead_acid.FirstOrderButlerVolmer(self.param, "Negative") - self.submodels[ - "positive interface" - ] = pybamm.interface.lead_acid.FirstOrderButlerVolmer(self.param, "Positive") + self.submodels["negative interface"] = pybamm.interface.FirstOrderKinetics( + self.param, + "Negative", + pybamm.interface.ButlerVolmer(self.param, "Negative", "lead-acid main"), + ) + self.submodels["positive interface"] = pybamm.interface.FirstOrderKinetics( + self.param, + "Positive", + pybamm.interface.ButlerVolmer(self.param, "Positive", "lead-acid main"), + ) # Oxygen if "oxygen" in self.options["side reactions"]: self.submodels[ "positive oxygen interface" - ] = pybamm.interface.lead_acid_oxygen.FirstOrderForwardTafel( - self.param, "Positive" + ] = pybamm.interface.FirstOrderKinetics( + self.param, + "Positive", + pybamm.interface.ForwardTafel( + self.param, "Positive", "lead-acid oxygen" + ), ) self.submodels[ "negative oxygen interface" - ] = pybamm.interface.lead_acid_oxygen.FullDiffusionLimited( - self.param, "Negative" + ] = pybamm.interface.DiffusionLimited( + self.param, "Negative", "lead-acid oxygen", order="full" ) def set_full_convection_submodel(self): diff --git a/pybamm/models/full_battery_models/lead_acid/loqs.py b/pybamm/models/full_battery_models/lead_acid/loqs.py index 18cdb4ea2e..8855242565 100644 --- a/pybamm/models/full_battery_models/lead_acid/loqs.py +++ b/pybamm/models/full_battery_models/lead_acid/loqs.py @@ -122,21 +122,31 @@ def set_interfacial_submodel(self): if self.options["surface form"] is False: self.submodels[ "leading-order negative interface" - ] = pybamm.interface.lead_acid.InverseButlerVolmer(self.param, "Negative") + ] = pybamm.interface.InverseButlerVolmer( + self.param, "Negative", "lead-acid main" + ) self.submodels[ "leading-order positive interface" - ] = pybamm.interface.lead_acid.InverseButlerVolmer(self.param, "Positive") + ] = pybamm.interface.InverseButlerVolmer( + self.param, "Positive", "lead-acid main" + ) else: self.submodels[ "leading-order negative interface" - ] = pybamm.interface.lead_acid.ButlerVolmer(self.param, "Negative") + ] = pybamm.interface.ButlerVolmer(self.param, "Negative", "lead-acid main") self.submodels[ "leading-order positive interface" - ] = pybamm.interface.lead_acid.ButlerVolmer(self.param, "Positive") + ] = pybamm.interface.ButlerVolmer(self.param, "Positive", "lead-acid main") + # always use forward Butler-Volmer for the reaction submodel to be passed to the + # higher order model self.reaction_submodels = { - "Negative": [self.submodels["leading-order negative interface"]], - "Positive": [self.submodels["leading-order positive interface"]], + "Negative": [ + pybamm.interface.ButlerVolmer(self.param, "Negative", "lead-acid main") + ], + "Positive": [ + pybamm.interface.ButlerVolmer(self.param, "Positive", "lead-acid main") + ], } def set_negative_electrode_submodel(self): @@ -186,22 +196,24 @@ def set_side_reaction_submodels(self): ] = pybamm.oxygen_diffusion.LeadingOrder(self.param, self.reactions) self.submodels[ "leading-order positive oxygen interface" - ] = pybamm.interface.lead_acid_oxygen.ForwardTafel(self.param, "Positive") + ] = pybamm.interface.ForwardTafel( + self.param, "Positive", "lead-acid oxygen" + ) self.submodels[ "leading-order negative oxygen interface" - ] = pybamm.interface.lead_acid_oxygen.LeadingOrderDiffusionLimited( - self.param, "Negative" + ] = pybamm.interface.DiffusionLimited( + self.param, "Negative", "lead-acid oxygen", order="leading" ) else: self.submodels[ "leading-order oxygen diffusion" ] = pybamm.oxygen_diffusion.NoOxygen(self.param) - self.submodels[ - "leading-order positive oxygen interface" - ] = pybamm.interface.lead_acid_oxygen.NoReaction(self.param, "Positive") self.submodels[ "leading-order negative oxygen interface" - ] = pybamm.interface.lead_acid_oxygen.NoReaction(self.param, "Negative") + ] = pybamm.interface.NoReaction(self.param, "Negative", "lead-acid oxygen") + self.submodels[ + "leading-order positive oxygen interface" + ] = pybamm.interface.NoReaction(self.param, "Positive", "lead-acid oxygen") self.reaction_submodels["Negative"].append( self.submodels["leading-order negative oxygen interface"] ) diff --git a/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py b/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py index 0901269bb5..d2403a63b4 100644 --- a/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py +++ b/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py @@ -43,13 +43,7 @@ def set_reactions(self): icd = " interfacial current density" self.reactions = { "main": { - "Negative": { - "s": 1 - self.param.t_plus, - "aj": "Negative electrode" + icd, - }, - "Positive": { - "s": 1 - self.param.t_plus, - "aj": "Positive electrode" + icd, - }, + "Negative": {"s": 1, "aj": "Negative electrode" + icd}, + "Positive": {"s": 1, "aj": "Positive electrode" + icd}, } } diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py b/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py index fd01abfa2c..96310b217f 100644 --- a/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py +++ b/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py @@ -264,7 +264,7 @@ def __init__(self, name="Doyle-Fuller-Newman model"): ###################### N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e) self.rhs[c_e] = (1 / eps) * ( - -pybamm.div(N_e) / param.C_e + (1 - param.t_plus) * j / param.gamma_e + -pybamm.div(N_e) / param.C_e + (1 - param.t_plus(c_e)) * j / param.gamma_e ) self.boundary_conditions[c_e] = { "left": (pybamm.Scalar(0), "Neumann"), diff --git a/pybamm/models/full_battery_models/lithium_ion/dfn.py b/pybamm/models/full_battery_models/lithium_ion/dfn.py index 74bf6ae34c..5bf6354b47 100644 --- a/pybamm/models/full_battery_models/lithium_ion/dfn.py +++ b/pybamm/models/full_battery_models/lithium_ion/dfn.py @@ -23,8 +23,8 @@ class DFN(BaseModel): References ---------- .. [1] SG Marquis, V Sulzer, R Timms, CP Please and SJ Chapman. “An asymptotic - derivation of a single particle model with electrolyte”. In: arXiv preprint - arXiv:1905.12553 (2019). + derivation of a single particle model with electrolyte”. Journal of The + Electrochemical Society, 166(15):A3693–A3706, 2019 **Extends:** :class:`pybamm.lithium_ion.BaseModel` @@ -60,27 +60,27 @@ def set_convection_submodel(self): def set_interfacial_submodel(self): - self.submodels[ - "negative interface" - ] = pybamm.interface.lithium_ion.ButlerVolmer(self.param, "Negative") - self.submodels[ - "positive interface" - ] = pybamm.interface.lithium_ion.ButlerVolmer(self.param, "Positive") + self.submodels["negative interface"] = pybamm.interface.ButlerVolmer( + self.param, "Negative", "lithium-ion main" + ) + self.submodels["positive interface"] = pybamm.interface.ButlerVolmer( + self.param, "Positive", "lithium-ion main" + ) def set_particle_submodel(self): if self.options["particle"] == "Fickian diffusion": - self.submodels["negative particle"] = pybamm.particle.fickian.ManyParticles( + self.submodels["negative particle"] = pybamm.particle.FickianManyParticles( self.param, "Negative" ) - self.submodels["positive particle"] = pybamm.particle.fickian.ManyParticles( + self.submodels["positive particle"] = pybamm.particle.FickianManyParticles( self.param, "Positive" ) elif self.options["particle"] == "fast diffusion": - self.submodels["negative particle"] = pybamm.particle.fast.ManyParticles( + self.submodels["negative particle"] = pybamm.particle.FastManyParticles( self.param, "Negative" ) - self.submodels["positive particle"] = pybamm.particle.fast.ManyParticles( + self.submodels["positive particle"] = pybamm.particle.FastManyParticles( self.param, "Positive" ) diff --git a/pybamm/models/full_battery_models/lithium_ion/spm.py b/pybamm/models/full_battery_models/lithium_ion/spm.py index 127399bb69..5acd8a19e9 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spm.py +++ b/pybamm/models/full_battery_models/lithium_ion/spm.py @@ -23,8 +23,8 @@ class SPM(BaseModel): References ---------- .. [1] SG Marquis, V Sulzer, R Timms, CP Please and SJ Chapman. “An asymptotic - derivation of a single particle model with electrolyte”. In: arXiv preprint - arXiv:1905.12553 (2019). + derivation of a single particle model with electrolyte”. Journal of The + Electrochemical Society, 166(15):A3693–A3706, 2019 **Extends:** :class:`pybamm.lithium_ion.BaseModel` """ @@ -61,35 +61,35 @@ def set_convection_submodel(self): def set_interfacial_submodel(self): if self.options["surface form"] is False: - self.submodels[ - "negative interface" - ] = pybamm.interface.lithium_ion.InverseButlerVolmer(self.param, "Negative") - self.submodels[ - "positive interface" - ] = pybamm.interface.lithium_ion.InverseButlerVolmer(self.param, "Positive") + self.submodels["negative interface"] = pybamm.interface.InverseButlerVolmer( + self.param, "Negative", "lithium-ion main" + ) + self.submodels["positive interface"] = pybamm.interface.InverseButlerVolmer( + self.param, "Positive", "lithium-ion main" + ) else: - self.submodels[ - "negative interface" - ] = pybamm.interface.lithium_ion.ButlerVolmer(self.param, "Negative") + self.submodels["negative interface"] = pybamm.interface.ButlerVolmer( + self.param, "Negative", "lithium-ion main" + ) - self.submodels[ - "positive interface" - ] = pybamm.interface.lithium_ion.ButlerVolmer(self.param, "Positive") + self.submodels["positive interface"] = pybamm.interface.ButlerVolmer( + self.param, "Positive", "lithium-ion main" + ) def set_particle_submodel(self): if self.options["particle"] == "Fickian diffusion": - self.submodels[ - "negative particle" - ] = pybamm.particle.fickian.SingleParticle(self.param, "Negative") - self.submodels[ - "positive particle" - ] = pybamm.particle.fickian.SingleParticle(self.param, "Positive") + self.submodels["negative particle"] = pybamm.particle.FickianSingleParticle( + self.param, "Negative" + ) + self.submodels["positive particle"] = pybamm.particle.FickianSingleParticle( + self.param, "Positive" + ) elif self.options["particle"] == "fast diffusion": - self.submodels["negative particle"] = pybamm.particle.fast.SingleParticle( + self.submodels["negative particle"] = pybamm.particle.FastSingleParticle( self.param, "Negative" ) - self.submodels["positive particle"] = pybamm.particle.fast.SingleParticle( + self.submodels["positive particle"] = pybamm.particle.FastSingleParticle( self.param, "Positive" ) diff --git a/pybamm/models/full_battery_models/lithium_ion/spme.py b/pybamm/models/full_battery_models/lithium_ion/spme.py index 18c5bf14d8..4317cdb1a7 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spme.py +++ b/pybamm/models/full_battery_models/lithium_ion/spme.py @@ -24,8 +24,8 @@ class SPMe(BaseModel): References ---------- .. [1] SG Marquis, V Sulzer, R Timms, CP Please and SJ Chapman. “An asymptotic - derivation of a single particle model with electrolyte”. In: arXiv preprint - arXiv:1905.12553 (2019). + derivation of a single particle model with electrolyte”. Journal of The + Electrochemical Society, 166(15):A3693–A3706, 2019 **Extends:** :class:`pybamm.lithium_ion.BaseModel` """ @@ -71,27 +71,27 @@ def set_convection_submodel(self): def set_interfacial_submodel(self): - self.submodels[ - "negative interface" - ] = pybamm.interface.lithium_ion.InverseButlerVolmer(self.param, "Negative") - self.submodels[ - "positive interface" - ] = pybamm.interface.lithium_ion.InverseButlerVolmer(self.param, "Positive") + self.submodels["negative interface"] = pybamm.interface.InverseButlerVolmer( + self.param, "Negative", "lithium-ion main" + ) + self.submodels["positive interface"] = pybamm.interface.InverseButlerVolmer( + self.param, "Positive", "lithium-ion main" + ) def set_particle_submodel(self): if self.options["particle"] == "Fickian diffusion": - self.submodels[ - "negative particle" - ] = pybamm.particle.fickian.SingleParticle(self.param, "Negative") - self.submodels[ - "positive particle" - ] = pybamm.particle.fickian.SingleParticle(self.param, "Positive") + self.submodels["negative particle"] = pybamm.particle.FickianSingleParticle( + self.param, "Negative" + ) + self.submodels["positive particle"] = pybamm.particle.FickianSingleParticle( + self.param, "Positive" + ) elif self.options["particle"] == "fast diffusion": - self.submodels["negative particle"] = pybamm.particle.fast.SingleParticle( + self.submodels["negative particle"] = pybamm.particle.FastSingleParticle( self.param, "Negative" ) - self.submodels["positive particle"] = pybamm.particle.fast.SingleParticle( + self.submodels["positive particle"] = pybamm.particle.FastSingleParticle( self.param, "Positive" ) diff --git a/pybamm/models/submodels/current_collector/__init__.py b/pybamm/models/submodels/current_collector/__init__.py index e4a9774255..1f765b4e30 100644 --- a/pybamm/models/submodels/current_collector/__init__.py +++ b/pybamm/models/submodels/current_collector/__init__.py @@ -17,8 +17,3 @@ QuiteConductivePotentialPair1plus1D, QuiteConductivePotentialPair2plus1D, ) -from .set_potential_single_particle import ( - BaseSetPotentialSingleParticle, - SetPotentialSingleParticle1plus1D, - SetPotentialSingleParticle2plus1D, -) diff --git a/pybamm/models/submodels/current_collector/set_potential_single_particle.py b/pybamm/models/submodels/current_collector/set_potential_single_particle.py deleted file mode 100644 index e519bd57f1..0000000000 --- a/pybamm/models/submodels/current_collector/set_potential_single_particle.py +++ /dev/null @@ -1,115 +0,0 @@ -# -# Class for one-dimensional current collectors in which the potential is held -# fixed and the current is determined from the I-V relationship used in the SPM(e) -# -import pybamm -from .base_current_collector import BaseModel - - -class BaseSetPotentialSingleParticle(BaseModel): - """A submodel for current collectors which *doesn't* update the potentials - during solve. This class uses the current-voltage relationship from the - SPM(e) (see [1]_) to calculate the current. - - Parameters - ---------- - param : parameter class - The parameters to use for this submodel - - References - ---------- - .. [1] SG Marquis, V Sulzer, R Timms, CP Please and SJ Chapman. “An asymptotic - derivation of a single particle model with electrolyte”. In: arXiv preprint - arXiv:1905.12553 (2019). - - - **Extends:** :class:`pybamm.current_collector.BaseModel` - """ - - def __init__(self, param): - super().__init__(param) - - def get_fundamental_variables(self): - - phi_s_cn = pybamm.standard_variables.phi_s_cn - - variables = self._get_standard_negative_potential_variables(phi_s_cn) - - # TO DO: grad not implemented for 2D yet - i_cc = pybamm.Scalar(0) - i_boundary_cc = pybamm.standard_variables.i_boundary_cc - - variables.update(self._get_standard_current_variables(i_cc, i_boundary_cc)) - # Hack to get the leading-order current collector current density - # Note that this should be different from the actual (composite) current - # collector current density for 2+1D models, but not sure how to implement this - # using current structure of lithium-ion models - variables["Leading-order current collector current density"] = variables[ - "Current collector current density" - ] - - return variables - - def set_rhs(self, variables): - phi_s_cn = variables["Negative current collector potential"] - - # Dummy equations so that PyBaMM doesn't change the potentials during solve - # i.e. d_phi/d_t = 0. Potentials are set externally between steps. - self.rhs = {phi_s_cn: pybamm.Scalar(0)} - - def set_algebraic(self, variables): - ocp_p_av = variables["X-averaged positive electrode open circuit potential"] - ocp_n_av = variables["X-averaged negative electrode open circuit potential"] - eta_r_n_av = variables["X-averaged negative electrode reaction overpotential"] - eta_r_p_av = variables["X-averaged positive electrode reaction overpotential"] - eta_e_av = variables["X-averaged electrolyte overpotential"] - delta_phi_s_n_av = variables["X-averaged negative electrode ohmic losses"] - delta_phi_s_p_av = variables["X-averaged positive electrode ohmic losses"] - - i_boundary_cc = variables["Current collector current density"] - v_boundary_cc = variables["Local voltage"] - # The voltage-current expression from the SPM(e) - local_voltage_expression = ( - ocp_p_av - - ocp_n_av - + eta_r_p_av - - eta_r_n_av - + eta_e_av - + delta_phi_s_p_av - - delta_phi_s_n_av - ) - self.algebraic = {i_boundary_cc: v_boundary_cc - local_voltage_expression} - - def set_initial_conditions(self, variables): - - applied_current = variables["Total current density"] - cc_area = self._get_effective_current_collector_area() - phi_s_cn = variables["Negative current collector potential"] - i_boundary_cc = variables["Current collector current density"] - - self.initial_conditions = { - phi_s_cn: pybamm.Scalar(0), - i_boundary_cc: applied_current / cc_area, - } - - -class SetPotentialSingleParticle1plus1D(BaseSetPotentialSingleParticle): - "Class for 1+1D set potential model" - - def __init__(self, param): - super().__init__(param) - - def _get_effective_current_collector_area(self): - "In the 1+1D models the current collector effectively has surface area l_z" - return self.param.l_z - - -class SetPotentialSingleParticle2plus1D(BaseSetPotentialSingleParticle): - "Class for 1+1D set potential model" - - def __init__(self, param): - super().__init__(param) - - def _get_effective_current_collector_area(self): - "Return the area of the current collector" - return self.param.l_y * self.param.l_z diff --git a/pybamm/models/submodels/electrode/base_electrode.py b/pybamm/models/submodels/electrode/base_electrode.py index a4e786f32a..6b2368f0d6 100644 --- a/pybamm/models/submodels/electrode/base_electrode.py +++ b/pybamm/models/submodels/electrode/base_electrode.py @@ -190,4 +190,3 @@ def _get_standard_whole_cell_variables(self, variables): ) return variables - diff --git a/pybamm/models/submodels/electrolyte/base_electrolyte_diffusion.py b/pybamm/models/submodels/electrolyte/base_electrolyte_diffusion.py index 0f4df2a527..cf99fa04fe 100644 --- a/pybamm/models/submodels/electrolyte/base_electrolyte_diffusion.py +++ b/pybamm/models/submodels/electrolyte/base_electrolyte_diffusion.py @@ -105,8 +105,10 @@ def _get_standard_flux_variables(self, N_e): def set_events(self, variables): c_e = variables["Electrolyte concentration"] - self.events.append(pybamm.Event( - "Zero electrolyte concentration cut-off", - pybamm.min(c_e) - 0.002, - pybamm.EventType.TERMINATION - )) + self.events.append( + pybamm.Event( + "Zero electrolyte concentration cut-off", + pybamm.min(c_e) - 0.002, + pybamm.EventType.TERMINATION, + ) + ) diff --git a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/base_higher_order_stefan_maxwell_conductivity.py b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/base_higher_order_stefan_maxwell_conductivity.py index 0620bfdf8b..bcbd59b878 100644 --- a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/base_higher_order_stefan_maxwell_conductivity.py +++ b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/base_higher_order_stefan_maxwell_conductivity.py @@ -62,14 +62,9 @@ def get_coupled_variables(self, variables): kappa_p_av = param.kappa_e(c_e_av, T_av) * tor_p_av chi_av = param.chi(c_e_av) - if chi_av.domain == ["current collector"]: - 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") - else: - chi_av_n = chi_av - chi_av_s = chi_av - chi_av_p = chi_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") # electrolyte current i_e_n = i_boundary_cc_0 * x_n / l_n diff --git a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/surface_potential_form/full_surface_form_stefan_maxwell_conductivity.py b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/surface_potential_form/full_surface_form_stefan_maxwell_conductivity.py index 945437ac41..ec55d41038 100644 --- a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/surface_potential_form/full_surface_form_stefan_maxwell_conductivity.py +++ b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/surface_potential_form/full_surface_form_stefan_maxwell_conductivity.py @@ -172,7 +172,7 @@ def _get_neg_pos_coupled_variables(self, variables): phi_e = phi_s - delta_phi variables.update(self._get_domain_potential_variables(phi_e)) - + variables.update({"test": pybamm.x_average(phi_s)}) return variables def _get_sep_coupled_variables(self, variables): diff --git a/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/composite_stefan_maxwell_diffusion.py b/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/composite_stefan_maxwell_diffusion.py index a6e077a849..81328365c2 100644 --- a/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/composite_stefan_maxwell_diffusion.py +++ b/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/composite_stefan_maxwell_diffusion.py @@ -73,12 +73,16 @@ def set_rhs(self, variables): } def _get_source_terms_leading_order(self, variables): + param = self.param + c_e_n = variables["Negative electrolyte concentration"] + c_e_p = variables["Positive electrolyte concentration"] + return sum( pybamm.Concatenation( - reaction["Negative"]["s"] + (reaction["Negative"]["s"] - param.t_plus(c_e_n)) * variables["Leading-order " + reaction["Negative"]["aj"].lower()], pybamm.FullBroadcast(0, "separator", "current collector"), - reaction["Positive"]["s"] + (reaction["Positive"]["s"] - param.t_plus(c_e_p)) * variables["Leading-order " + reaction["Positive"]["aj"].lower()], ) / self.param.gamma_e diff --git a/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/constant_stefan_maxwell_diffusion.py b/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/constant_stefan_maxwell_diffusion.py index fb2547342b..09c1b49590 100644 --- a/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/constant_stefan_maxwell_diffusion.py +++ b/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/constant_stefan_maxwell_diffusion.py @@ -29,7 +29,7 @@ def get_fundamental_variables(self): variables = self._get_standard_concentration_variables(c_e) - N_e = pybamm.FullBroadcast( + N_e = pybamm.FullBroadcastToEdges( 0, ["negative electrode", "separator", "positive electrode"], "current collector", diff --git a/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/first_order_stefan_maxwell_diffusion.py b/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/first_order_stefan_maxwell_diffusion.py index 91398217ad..9f9b980f85 100644 --- a/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/first_order_stefan_maxwell_diffusion.py +++ b/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/first_order_stefan_maxwell_diffusion.py @@ -57,7 +57,7 @@ def get_coupled_variables(self, variables): # Right-hand sides rhs_n = d_epsc_n_0_dt - sum( - reaction["Negative"]["s"] + (reaction["Negative"]["s"] - param.t_plus(c_e_0)) * variables[ "Leading-order x-averaged " + reaction["Negative"]["aj"].lower() ] @@ -65,7 +65,7 @@ def get_coupled_variables(self, variables): ) rhs_s = d_epsc_s_0_dt rhs_p = d_epsc_p_0_dt - sum( - reaction["Positive"]["s"] + (reaction["Positive"]["s"] - param.t_plus(c_e_0)) * variables[ "Leading-order x-averaged " + reaction["Positive"]["aj"].lower() ] diff --git a/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/full_stefan_maxwell_diffusion.py b/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/full_stefan_maxwell_diffusion.py index e007a5e6f2..31c37a4f7a 100644 --- a/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/full_stefan_maxwell_diffusion.py +++ b/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/full_stefan_maxwell_diffusion.py @@ -59,15 +59,16 @@ def set_rhs(self, variables): deps_dt = variables["Porosity change"] c_e = variables["Electrolyte concentration"] N_e = variables["Electrolyte flux"] - # i_e = variables["Electrolyte current density"] + c_e_n = variables["Negative electrolyte concentration"] + c_e_p = variables["Positive electrolyte concentration"] - # source_term = ((param.s - param.t_plus) / param.gamma_e) * pybamm.div(i_e) - # source_term = pybamm.div(i_e) / param.gamma_e # lithium-ion source_terms = sum( pybamm.Concatenation( - reaction["Negative"]["s"] * variables[reaction["Negative"]["aj"]], + (reaction["Negative"]["s"] - param.t_plus(c_e_n)) + * variables[reaction["Negative"]["aj"]], pybamm.FullBroadcast(0, "separator", "current collector"), - reaction["Positive"]["s"] * variables[reaction["Positive"]["aj"]], + (reaction["Positive"]["s"] - param.t_plus(c_e_p)) + * variables[reaction["Positive"]["aj"]], ) / param.gamma_e for reaction in self.reactions.values() diff --git a/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/leading_stefan_maxwell_diffusion.py b/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/leading_stefan_maxwell_diffusion.py index 9e167532c5..3ef1517e79 100644 --- a/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/leading_stefan_maxwell_diffusion.py +++ b/pybamm/models/submodels/electrolyte/stefan_maxwell/diffusion/leading_stefan_maxwell_diffusion.py @@ -35,7 +35,7 @@ def get_fundamental_variables(self): def get_coupled_variables(self, variables): - N_e = pybamm.FullBroadcast( + N_e = pybamm.FullBroadcastToEdges( 0, ["negative electrode", "separator", "positive electrode"], "current collector", @@ -60,10 +60,10 @@ def set_rhs(self, variables): source_terms = sum( param.l_n - * rxn["Negative"]["s"] + * (rxn["Negative"]["s"] - param.t_plus(c_e_av)) * variables["X-averaged " + rxn["Negative"]["aj"].lower()] + param.l_p - * rxn["Positive"]["s"] + * (rxn["Positive"]["s"] - param.t_plus(c_e_av)) * variables["X-averaged " + rxn["Positive"]["aj"].lower()] for rxn in self.reactions.values() ) diff --git a/pybamm/models/submodels/external_circuit/__init__.py b/pybamm/models/submodels/external_circuit/__init__.py index 21966d10b5..f181a9cca9 100644 --- a/pybamm/models/submodels/external_circuit/__init__.py +++ b/pybamm/models/submodels/external_circuit/__init__.py @@ -8,4 +8,3 @@ LeadingOrderVoltageFunctionControl, LeadingOrderPowerFunctionControl, ) - diff --git a/pybamm/models/submodels/external_circuit/base_external_circuit.py b/pybamm/models/submodels/external_circuit/base_external_circuit.py index 9c69e00eab..cd37a82a6f 100644 --- a/pybamm/models/submodels/external_circuit/base_external_circuit.py +++ b/pybamm/models/submodels/external_circuit/base_external_circuit.py @@ -10,20 +10,6 @@ class BaseModel(pybamm.BaseSubModel): def __init__(self, param): super().__init__(param) - def _get_current_variables(self, i_cell): - param = self.param - I = i_cell * abs(param.I_typ) - i_cell_dim = I / (param.n_electrodes_parallel * param.A_cc) - - variables = { - "Total current density": i_cell, - "Total current density [A.m-2]": i_cell_dim, - "Current [A]": I, - "C-rate": I / param.Q, - } - - return variables - def get_fundamental_variables(self): Q = pybamm.Variable("Discharge capacity [A.h]") variables = {"Discharge capacity [A.h]": Q} @@ -50,4 +36,3 @@ def get_fundamental_variables(self): Q = pybamm.Variable("Leading-order discharge capacity [A.h]") variables = {"Discharge capacity [A.h]": Q} return variables - diff --git a/pybamm/models/submodels/external_circuit/current_control_external_circuit.py b/pybamm/models/submodels/external_circuit/current_control_external_circuit.py index 0368852226..952a7f87bc 100644 --- a/pybamm/models/submodels/external_circuit/current_control_external_circuit.py +++ b/pybamm/models/submodels/external_circuit/current_control_external_circuit.py @@ -34,4 +34,3 @@ class LeadingOrderCurrentControl(CurrentControl, LeadingOrderBaseModel): def __init__(self, param): super().__init__(param) - diff --git a/pybamm/models/submodels/external_circuit/function_control_external_circuit.py b/pybamm/models/submodels/external_circuit/function_control_external_circuit.py index 6e3812f99a..f4d91dc3dd 100644 --- a/pybamm/models/submodels/external_circuit/function_control_external_circuit.py +++ b/pybamm/models/submodels/external_circuit/function_control_external_circuit.py @@ -12,13 +12,21 @@ def __init__(self, param, external_circuit_function): super().__init__(param) self.external_circuit_function = external_circuit_function - def _get_current_variable(self): - return pybamm.Variable("Total current density") - def get_fundamental_variables(self): + param = self.param # Current is a variable - i_cell = self._get_current_variable() - variables = self._get_current_variables(i_cell) + i_cell = pybamm.Variable("Total current density") + + # Update derived variables + I = i_cell * abs(param.I_typ) + i_cell_dim = I / (param.n_electrodes_parallel * param.A_cc) + + variables = { + "Total current density": i_cell, + "Total current density [A.m-2]": i_cell_dim, + "Current [A]": I, + "C-rate": I / param.Q, + } # Add discharge capacity variable variables.update(super().get_fundamental_variables()) @@ -45,25 +53,27 @@ class VoltageFunctionControl(FunctionControl): """ def __init__(self, param): - super().__init__(param, constant_voltage) + super().__init__(param, self.constant_voltage) - -def constant_voltage(variables): - V = variables["Terminal voltage [V]"] - return V - pybamm.FunctionParameter("Voltage function [V]", pybamm.t) + def constant_voltage(self, variables): + V = variables["Terminal voltage [V]"] + return V - pybamm.FunctionParameter( + "Voltage function [V]", {"Time [s]": pybamm.t * self.param.timescale} + ) class PowerFunctionControl(FunctionControl): """External circuit with power control. """ def __init__(self, param): - super().__init__(param, constant_power) - + super().__init__(param, self.constant_power) -def constant_power(variables): - I = variables["Current [A]"] - V = variables["Terminal voltage [V]"] - return I * V - pybamm.FunctionParameter("Power function [W]", pybamm.t) + def constant_power(self, variables): + I = variables["Current [A]"] + V = variables["Terminal voltage [V]"] + return I * V - pybamm.FunctionParameter( + "Power function [W]", {"Time [s]": pybamm.t * self.param.timescale} + ) class LeadingOrderFunctionControl(FunctionControl, LeadingOrderBaseModel): @@ -83,12 +93,24 @@ class LeadingOrderVoltageFunctionControl(LeadingOrderFunctionControl): """ def __init__(self, param): - super().__init__(param, constant_voltage) + super().__init__(param, self.constant_voltage) + + def constant_voltage(self, variables): + V = variables["Terminal voltage [V]"] + return V - pybamm.FunctionParameter( + "Voltage function [V]", {"Time [s]": pybamm.t * self.param.timescale} + ) class LeadingOrderPowerFunctionControl(LeadingOrderFunctionControl): """External circuit with power control, at leading order. """ def __init__(self, param): - super().__init__(param, constant_power) - + super().__init__(param, self.constant_power) + + def constant_power(self, variables): + I = variables["Current [A]"] + V = variables["Terminal voltage [V]"] + return I * V - pybamm.FunctionParameter( + "Power function [W]", {"Time [s]": pybamm.t * self.param.timescale} + ) diff --git a/pybamm/models/submodels/interface/__init__.py b/pybamm/models/submodels/interface/__init__.py index 581e1bca52..123fde06bb 100644 --- a/pybamm/models/submodels/interface/__init__.py +++ b/pybamm/models/submodels/interface/__init__.py @@ -1,4 +1,5 @@ from .base_interface import BaseInterface -from . import lead_acid -from . import lead_acid_oxygen -from . import lithium_ion +from .kinetics import * +from .inverse_kinetics import * +from .first_order_kinetics import * +from .diffusion_limited import DiffusionLimited diff --git a/pybamm/models/submodels/interface/base_interface.py b/pybamm/models/submodels/interface/base_interface.py index 8dc98a1fc5..b00349cd26 100644 --- a/pybamm/models/submodels/interface/base_interface.py +++ b/pybamm/models/submodels/interface/base_interface.py @@ -13,13 +13,146 @@ class BaseInterface(pybamm.BaseSubModel): ---------- param : parameter class The parameters to use for this submodel - + domain : str + The domain to implement the model, either: 'Negative' or 'Positive'. + reaction : str + The name of the reaction being implemented **Extends:** :class:`pybamm.BaseSubModel` """ - def __init__(self, param, domain): + def __init__(self, param, domain, reaction): super().__init__(param, domain) + self.reaction = reaction + if reaction == "lithium-ion main": + self.reaction_name = "" # empty reaction name for the main reaction + elif reaction == "lead-acid main": + self.reaction_name = "" # empty reaction name for the main reaction + elif reaction == "lead-acid oxygen": + self.reaction_name = " oxygen" + + def _get_exchange_current_density(self, variables): + """ + A private function to obtain the exchange current density + + Parameters + ---------- + variables: dict + The variables in the full model. + + Returns + ------- + j0 : :class: `pybamm.Symbol` + The exchange current density. + """ + c_e = variables[self.domain + " electrolyte concentration"] + + if self.reaction == "lithium-ion main": + c_s_surf = variables[self.domain + " particle surface concentration"] + T = variables[self.domain + " electrode temperature"] + + # If variable was broadcast, take only the orphan + if ( + isinstance(c_s_surf, pybamm.Broadcast) + and isinstance(c_e, pybamm.Broadcast) + and isinstance(T, pybamm.Broadcast) + ): + c_s_surf = c_s_surf.orphans[0] + c_e = c_e.orphans[0] + T = T.orphans[0] + if self.domain == "Negative": + prefactor = self.param.m_n(T) / self.param.C_r_n + elif self.domain == "Positive": + prefactor = self.param.gamma_p * self.param.m_p(T) / self.param.C_r_p + j0 = prefactor * ( + c_e ** (1 / 2) * c_s_surf ** (1 / 2) * (1 - c_s_surf) ** (1 / 2) + ) + + elif self.reaction == "lead-acid main": + # If variable was broadcast, take only the orphan + if isinstance(c_e, pybamm.Broadcast): + c_e = c_e.orphans[0] + if self.domain == "Negative": + j0 = self.param.j0_n_S_ref * c_e + elif self.domain == "Positive": + c_w = self.param.c_w(c_e) + j0 = self.param.j0_p_S_ref * c_e ** 2 * c_w + + elif self.reaction == "lead-acid oxygen": + # If variable was broadcast, take only the orphan + if isinstance(c_e, pybamm.Broadcast): + c_e = c_e.orphans[0] + if self.domain == "Negative": + j0 = pybamm.Scalar(0) + elif self.domain == "Positive": + j0 = self.param.j0_p_Ox_ref * c_e # ** self.param.exponent_e_Ox + + return j0 + + def _get_open_circuit_potential(self, variables): + """ + A private function to obtain the open circuit potential and entropic change + + Parameters + ---------- + variables: dict + The variables in the full model. + + Returns + ------- + ocp : :class:`pybamm.Symbol` + The open-circuit potential + dUdT : :class:`pybamm.Symbol` + The entropic change in open-circuit potential due to temperature + + """ + + if self.reaction == "lithium-ion main": + c_s_surf = variables[self.domain + " particle surface concentration"] + T = variables[self.domain + " electrode temperature"] + + # If variable was broadcast, take only the orphan + if isinstance(c_s_surf, pybamm.Broadcast) and isinstance( + T, pybamm.Broadcast + ): + c_s_surf = c_s_surf.orphans[0] + T = T.orphans[0] + + if self.domain == "Negative": + ocp = self.param.U_n(c_s_surf, T) + dUdT = self.param.dUdT_n(c_s_surf) + elif self.domain == "Positive": + ocp = self.param.U_p(c_s_surf, T) + dUdT = self.param.dUdT_p(c_s_surf) + elif self.reaction == "lead-acid main": + c_e = variables[self.domain + " electrolyte concentration"] + # If c_e was broadcast, take only the orphan + if isinstance(c_e, pybamm.Broadcast): + c_e = c_e.orphans[0] + if self.domain == "Negative": + ocp = self.param.U_n(c_e, self.param.T_init) + elif self.domain == "Positive": + ocp = self.param.U_p(c_e, self.param.T_init) + dUdT = pybamm.Scalar(0) + + elif self.reaction == "lead-acid oxygen": + if self.domain == "Negative": + ocp = self.param.U_n_Ox + elif self.domain == "Positive": + ocp = self.param.U_p_Ox + dUdT = pybamm.Scalar(0) + + return ocp, dUdT + + def _get_number_of_electrons_in_reaction(self): + "Returns the number of electrons in the reaction" + if self.reaction in ["lead-acid main", "lithium-ion main"]: + if self.domain == "Negative": + return self.param.ne_n + elif self.domain == "Positive": + return self.param.ne_p + elif self.reaction == "lead-acid oxygen": + return self.param.ne_Ox def _get_delta_phi(self, variables): "Calculate delta_phi, and derived variables, using phi_s and phi_e" diff --git a/pybamm/models/submodels/interface/diffusion_limited/base_diffusion_limited.py b/pybamm/models/submodels/interface/diffusion_limited.py similarity index 53% rename from pybamm/models/submodels/interface/diffusion_limited/base_diffusion_limited.py rename to pybamm/models/submodels/interface/diffusion_limited.py index 3d8258721a..9ccb7f6a33 100644 --- a/pybamm/models/submodels/interface/diffusion_limited/base_diffusion_limited.py +++ b/pybamm/models/submodels/interface/diffusion_limited.py @@ -3,10 +3,10 @@ # import pybamm -from ..base_interface import BaseInterface +from .base_interface import BaseInterface -class BaseModel(BaseInterface): +class DiffusionLimited(BaseInterface): """ Leading-order submodel for diffusion-limited kinetics @@ -16,13 +16,17 @@ class BaseModel(BaseInterface): model parameters domain : str The domain to implement the model, either: 'Negative' or 'Positive'. - + reaction : str + The name of the reaction being implemented + order : str + The order of the model ("leading" or "full") **Extends:** :class:`pybamm.interface.BaseInterface` """ - def __init__(self, param, domain): - super().__init__(param, domain) + def __init__(self, param, domain, reaction, order): + super().__init__(param, domain, reaction) + self.order = order def get_coupled_variables(self, variables): # Calculate delta_phi_s from phi_s and phi_e if it isn't already known @@ -68,17 +72,57 @@ def get_coupled_variables(self, variables): return variables - def _get_exchange_current_density(self, variables): - raise NotImplementedError - - def _get_open_circuit_potential(self, variables): - raise NotImplementedError - def _get_diffusion_limited_current_density(self, variables): - raise NotImplementedError + param = self.param + if self.domain == "Negative": + if self.order == "leading": + j_p = variables[ + "X-averaged positive electrode" + + self.reaction_name + + " interfacial current density" + ] + j = -self.param.l_p * j_p / self.param.l_n + elif self.order == "full": + tor_s = variables["Separator tortuosity"] + c_ox_s = variables["Separator oxygen concentration"] + N_ox_neg_sep_interface = ( + -pybamm.boundary_value(tor_s, "left") + * param.curlyD_ox + * pybamm.BoundaryGradient(c_ox_s, "left") + ) + N_ox_neg_sep_interface.domain = ["current collector"] + + j = -N_ox_neg_sep_interface / param.C_e / param.s_ox_Ox / param.l_n + + return j def _get_dj_dc(self, variables): return pybamm.Scalar(0) def _get_dj_ddeltaphi(self, variables): return pybamm.Scalar(0) + + def _get_j_diffusion_limited_first_order(self, variables): + """ + First-order correction to the interfacial current density due to + diffusion-limited effects. For a general model the correction term is zero, + since the reaction is not diffusion-limited + """ + if self.order == "leading": + j_leading_order = variables[ + "Leading-order x-averaged " + + self.domain.lower() + + " electrode" + + self.reaction_name + + " interfacial current density" + ] + param = self.param + if self.domain == "Negative": + N_ox_s_p = variables["Oxygen flux"].orphans[1] + N_ox_neg_sep_interface = N_ox_s_p[0] + + j = -N_ox_neg_sep_interface / param.C_e / param.s_ox_Ox / param.l_n + + return (j - j_leading_order) / param.C_e + else: + return pybamm.Scalar(0) diff --git a/pybamm/models/submodels/interface/diffusion_limited/__init__.py b/pybamm/models/submodels/interface/diffusion_limited/__init__.py deleted file mode 100644 index 74bc1df85c..0000000000 --- a/pybamm/models/submodels/interface/diffusion_limited/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .base_diffusion_limited import BaseModel -from .leading_diffusion_limited import LeadingOrderDiffusionLimited -from .full_diffusion_limited import FullDiffusionLimited diff --git a/pybamm/models/submodels/interface/diffusion_limited/full_diffusion_limited.py b/pybamm/models/submodels/interface/diffusion_limited/full_diffusion_limited.py deleted file mode 100644 index a9f4bfc9f2..0000000000 --- a/pybamm/models/submodels/interface/diffusion_limited/full_diffusion_limited.py +++ /dev/null @@ -1,40 +0,0 @@ -# -# Full diffusion limited kinetics -# -import pybamm -from .base_diffusion_limited import BaseModel - - -class FullDiffusionLimited(BaseModel): - """ - Full submodel for diffusion-limited kinetics - - Parameters - ---------- - param : - model parameters - domain : str - The domain to implement the model, either: 'Negative' or 'Positive'. - - - **Extends:** :class:`pybamm.interface.diffusion_limited.BaseModel` - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - - def _get_diffusion_limited_current_density(self, variables): - param = self.param - if self.domain == "Negative": - tor_s = variables["Separator tortuosity"] - c_ox_s = variables["Separator oxygen concentration"] - N_ox_neg_sep_interface = ( - -pybamm.boundary_value(tor_s, "left") - * param.curlyD_ox - * pybamm.BoundaryGradient(c_ox_s, "left") - ) - N_ox_neg_sep_interface.domain = ["current collector"] - - j = -N_ox_neg_sep_interface / param.C_e / param.s_ox_Ox / param.l_n - - return j diff --git a/pybamm/models/submodels/interface/diffusion_limited/leading_diffusion_limited.py b/pybamm/models/submodels/interface/diffusion_limited/leading_diffusion_limited.py deleted file mode 100644 index 1818dd5357..0000000000 --- a/pybamm/models/submodels/interface/diffusion_limited/leading_diffusion_limited.py +++ /dev/null @@ -1,56 +0,0 @@ -# -# Leading-order diffusion limited kinetics -# -from .base_diffusion_limited import BaseModel - - -class LeadingOrderDiffusionLimited(BaseModel): - """ - Leading-order submodel for diffusion-limited kinetics - - Parameters - ---------- - param : - model parameters - domain : str - The domain to implement the model, either: 'Negative' or 'Positive'. - - - **Extends:** :class:`pybamm.interface.diffusion_limited.BaseModel` - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - - def _get_diffusion_limited_current_density(self, variables): - if self.domain == "Negative": - j_p = variables[ - "X-averaged positive electrode" - + self.reaction_name - + " interfacial current density" - ] - j = -self.param.l_p * j_p / self.param.l_n - - return j - - def _get_j_diffusion_limited_first_order(self, variables): - """ - First-order correction to the interfacial current density due to - diffusion-limited effects. For a general model the correction term is zero, - since the reaction is not diffusion-limited - """ - j_leading_order = variables[ - "Leading-order x-averaged " - + self.domain.lower() - + " electrode" - + self.reaction_name - + " interfacial current density" - ] - param = self.param - if self.domain == "Negative": - N_ox_s_p = variables["Oxygen flux"].orphans[1] - N_ox_neg_sep_interface = N_ox_s_p[0] - - j = -N_ox_neg_sep_interface / param.C_e / param.s_ox_Ox / param.l_n - - return (j - j_leading_order) / param.C_e diff --git a/pybamm/models/submodels/interface/first_order_kinetics/__init__.py b/pybamm/models/submodels/interface/first_order_kinetics/__init__.py new file mode 100644 index 0000000000..0cbde56392 --- /dev/null +++ b/pybamm/models/submodels/interface/first_order_kinetics/__init__.py @@ -0,0 +1,2 @@ +from .first_order_kinetics import FirstOrderKinetics +from .inverse_first_order_kinetics import InverseFirstOrderKinetics diff --git a/pybamm/models/submodels/interface/kinetics/base_first_order_kinetics.py b/pybamm/models/submodels/interface/first_order_kinetics/first_order_kinetics.py similarity index 80% rename from pybamm/models/submodels/interface/kinetics/base_first_order_kinetics.py rename to pybamm/models/submodels/interface/first_order_kinetics/first_order_kinetics.py index b1d005fae3..524edc4601 100644 --- a/pybamm/models/submodels/interface/kinetics/base_first_order_kinetics.py +++ b/pybamm/models/submodels/interface/first_order_kinetics/first_order_kinetics.py @@ -1,12 +1,12 @@ # # First-order Butler-Volmer kinetics # -from .base_kinetics import BaseModel +from ..base_interface import BaseInterface -class BaseFirstOrderKinetics(BaseModel): +class FirstOrderKinetics(BaseInterface): """ - Base first-order kinetics + First-order kinetics Parameters ---------- @@ -14,13 +14,15 @@ class BaseFirstOrderKinetics(BaseModel): model parameters domain : str The domain to implement the model, either: 'Negative' or 'Positive'. - + leading_order_model : :class:`pybamm.interface.kinetics.BaseKinetics` + The leading-order model with respect to which this is first-order **Extends:** :class:`pybamm.interface.BaseInterface` """ - def __init__(self, param, domain): - super().__init__(param, domain) + def __init__(self, param, domain, leading_order_model): + super().__init__(param, domain, leading_order_model.reaction) + self.leading_order_model = leading_order_model def get_coupled_variables(self, variables): # Unpack @@ -30,8 +32,8 @@ def get_coupled_variables(self, variables): c_e = variables[self.domain + " electrolyte concentration"] c_e_1 = (c_e - c_e_0) / self.param.C_e - dj_dc_0 = self._get_dj_dc(variables) - dj_ddeltaphi_0 = self._get_dj_ddeltaphi(variables) + dj_dc_0 = self.leading_order_model._get_dj_dc(variables) + dj_ddeltaphi_0 = self.leading_order_model._get_dj_ddeltaphi(variables) # Update delta_phi with new phi_e and phi_s variables = self._get_delta_phi(variables) diff --git a/pybamm/models/submodels/interface/inverse_kinetics/base_inverse_first_order_kinetics.py b/pybamm/models/submodels/interface/first_order_kinetics/inverse_first_order_kinetics.py similarity index 65% rename from pybamm/models/submodels/interface/inverse_kinetics/base_inverse_first_order_kinetics.py rename to pybamm/models/submodels/interface/first_order_kinetics/inverse_first_order_kinetics.py index ae10a0696c..f931b66d8e 100644 --- a/pybamm/models/submodels/interface/inverse_kinetics/base_inverse_first_order_kinetics.py +++ b/pybamm/models/submodels/interface/first_order_kinetics/inverse_first_order_kinetics.py @@ -1,13 +1,14 @@ # # First-order Butler-Volmer kinetics # +from ..base_interface import BaseInterface -from ..kinetics.base_first_order_kinetics import BaseFirstOrderKinetics - -class BaseInverseFirstOrderKinetics(BaseFirstOrderKinetics): +class InverseFirstOrderKinetics(BaseInterface): """ - Base inverse first-order kinetics + Base inverse first-order kinetics. This class needs to consider *all* of the + leading-order submodels simultaneously in order to find the first-order correction + to the potentials Parameters ---------- @@ -15,13 +16,15 @@ class BaseInverseFirstOrderKinetics(BaseFirstOrderKinetics): model parameters domain : str The domain to implement the model, either: 'Negative' or 'Positive'. + leading_order_models : :class:`pybamm.interface.kinetics.BaseKinetics` + The leading-order models with respect to which this is first-order - - **Extends:** :class:`pybamm.interface.kinetics.BaseFirstOrderKinetics` + **Extends:** :class:`pybamm.interface.BaseInterface` """ - def __init__(self, param, domain): - super().__init__(param, domain) + def __init__(self, param, domain, leading_order_models): + super().__init__(param, domain, "inverse") + self.leading_order_models = leading_order_models def _get_die1dx(self, variables): i_boundary_cc = variables["Current collector current density"] @@ -51,16 +54,15 @@ def get_coupled_variables(self, variables): # Get derivatives of leading-order terms sum_dj_dc_0 = sum( - reaction_submodel._get_dj_dc(variables) - for reaction_submodel in self.reaction_submodels + submodel._get_dj_dc(variables) for submodel in self.leading_order_models ) sum_dj_ddeltaphi_0 = sum( - reaction_submodel._get_dj_ddeltaphi(variables) - for reaction_submodel in self.reaction_submodels + submodel._get_dj_ddeltaphi(variables) + for submodel in self.leading_order_models ) sum_j_diffusion_limited_first_order = sum( - reaction_submodel._get_j_diffusion_limited_first_order(variables) - for reaction_submodel in self.reaction_submodels + submodel._get_j_diffusion_limited_first_order(variables) + for submodel in self.leading_order_models ) delta_phi_1_av = ( diff --git a/pybamm/models/submodels/interface/inverse_kinetics/__init__.py b/pybamm/models/submodels/interface/inverse_kinetics/__init__.py index 15eded391a..1283fde47b 100644 --- a/pybamm/models/submodels/interface/inverse_kinetics/__init__.py +++ b/pybamm/models/submodels/interface/inverse_kinetics/__init__.py @@ -1,3 +1 @@ -from .base_inverse_kinetics import BaseInverseKinetics -from .base_inverse_first_order_kinetics import BaseInverseFirstOrderKinetics from .inverse_butler_volmer import InverseButlerVolmer diff --git a/pybamm/models/submodels/interface/inverse_kinetics/base_inverse_kinetics.py b/pybamm/models/submodels/interface/inverse_kinetics/base_inverse_kinetics.py deleted file mode 100644 index 244f3d2d65..0000000000 --- a/pybamm/models/submodels/interface/inverse_kinetics/base_inverse_kinetics.py +++ /dev/null @@ -1,82 +0,0 @@ -# -# Bulter volmer class -# - -import pybamm -from ..base_interface import BaseInterface - - -class BaseInverseKinetics(BaseInterface): - """ - A base submodel that implements the inverted form of the Butler-Volmer relation to - solve for the reaction overpotential. - - Parameters - ---------- - param - Model parameters - domain : iter of str, optional - The domain(s) in which to compute the interfacial current. Default is None, - in which case j.domain is used. - - **Extends:** :class:`pybamm.interface.kinetics.ButlerVolmer` - - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - - def get_coupled_variables(self, variables): - ocp, dUdT = self._get_open_circuit_potential(variables) - - j0 = self._get_exchange_current_density(variables) - j_tot_av = self._get_average_total_interfacial_current_density(variables) - # Broadcast to match j0's domain - if j0.domain in [[], ["current collector"]]: - j = j_tot_av - else: - j = pybamm.PrimaryBroadcast(j_tot_av, [self.domain.lower() + " electrode"]) - - if self.domain == "Negative": - ne = self.param.ne_n - elif self.domain == "Positive": - ne = self.param.ne_p - # Note: T must have the same domain as j0 and eta_r - if j0.domain in ["current collector", ["current collector"]]: - T = variables["X-averaged cell temperature"] - else: - T = variables[self.domain + " electrode temperature"] - - eta_r = self._get_overpotential(j, j0, ne, T) - delta_phi = eta_r + ocp - - variables.update(self._get_standard_interfacial_current_variables(j)) - variables.update( - self._get_standard_total_interfacial_current_variables(j_tot_av) - ) - variables.update(self._get_standard_exchange_current_variables(j0)) - variables.update(self._get_standard_overpotential_variables(eta_r)) - variables.update( - self._get_standard_surface_potential_difference_variables(delta_phi) - ) - variables.update(self._get_standard_ocp_variables(ocp, dUdT)) - - if ( - "Negative electrode" + self.reaction_name + " interfacial current density" - in variables - and "Positive electrode" - + self.reaction_name - + " interfacial current density" - in variables - ): - variables.update( - self._get_standard_whole_cell_interfacial_current_variables(variables) - ) - variables.update( - self._get_standard_whole_cell_exchange_current_variables(variables) - ) - - return variables - - def _get_overpotential(self, j, j0, ne, T): - raise NotImplementedError diff --git a/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py b/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py index 2587dc24d4..4e305418d0 100644 --- a/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py +++ b/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py @@ -2,11 +2,10 @@ # Inverse Bulter-Volmer class # import pybamm -from .base_inverse_kinetics import BaseInverseKinetics -from ..kinetics.butler_volmer import ButlerVolmer +from ..base_interface import BaseInterface -class InverseButlerVolmer(BaseInverseKinetics, ButlerVolmer): +class InverseButlerVolmer(BaseInterface): """ A base submodel that implements the inverted form of the Butler-Volmer relation to solve for the reaction overpotential. @@ -18,13 +17,67 @@ class InverseButlerVolmer(BaseInverseKinetics, ButlerVolmer): domain : iter of str, optional The domain(s) in which to compute the interfacial current. Default is None, in which case j.domain is used. + reaction : str + The name of the reaction being implemented **Extends:** :class:`pybamm.interface.kinetics.ButlerVolmer` """ - def __init__(self, param, domain): - super().__init__(param, domain) + def __init__(self, param, domain, reaction): + super().__init__(param, domain, reaction) + + def get_coupled_variables(self, variables): + ocp, dUdT = self._get_open_circuit_potential(variables) + + j0 = self._get_exchange_current_density(variables) + j_tot_av = self._get_average_total_interfacial_current_density(variables) + # Broadcast to match j0's domain + if j0.domain in [[], ["current collector"]]: + j = j_tot_av + else: + j = pybamm.PrimaryBroadcast(j_tot_av, [self.domain.lower() + " electrode"]) + + if self.domain == "Negative": + ne = self.param.ne_n + elif self.domain == "Positive": + ne = self.param.ne_p + # Note: T must have the same domain as j0 and eta_r + if j0.domain in ["current collector", ["current collector"]]: + T = variables["X-averaged cell temperature"] + else: + T = variables[self.domain + " electrode temperature"] + + eta_r = self._get_overpotential(j, j0, ne, T) + delta_phi = eta_r + ocp + + variables.update(self._get_standard_interfacial_current_variables(j)) + variables.update( + self._get_standard_total_interfacial_current_variables(j_tot_av) + ) + variables.update(self._get_standard_exchange_current_variables(j0)) + variables.update(self._get_standard_overpotential_variables(eta_r)) + variables.update( + self._get_standard_surface_potential_difference_variables(delta_phi) + ) + variables.update(self._get_standard_ocp_variables(ocp, dUdT)) + + if ( + "Negative electrode" + self.reaction_name + " interfacial current density" + in variables + and "Positive electrode" + + self.reaction_name + + " interfacial current density" + in variables + ): + variables.update( + self._get_standard_whole_cell_interfacial_current_variables(variables) + ) + variables.update( + self._get_standard_whole_cell_exchange_current_variables(variables) + ) + + return variables def _get_overpotential(self, j, j0, ne, T): return (2 * (1 + self.param.Theta * T) / ne) * pybamm.arcsinh(j / (2 * j0)) diff --git a/pybamm/models/submodels/interface/kinetics/__init__.py b/pybamm/models/submodels/interface/kinetics/__init__.py index df2db6a477..3b2a0ae25f 100644 --- a/pybamm/models/submodels/interface/kinetics/__init__.py +++ b/pybamm/models/submodels/interface/kinetics/__init__.py @@ -1,5 +1,4 @@ -from .base_kinetics import BaseModel -from .base_first_order_kinetics import BaseFirstOrderKinetics -from .butler_volmer import ButlerVolmer, FirstOrderButlerVolmer -from .tafel import ForwardTafel, BackwardTafel, FirstOrderForwardTafel +from .base_kinetics import BaseKinetics +from .butler_volmer import ButlerVolmer +from .tafel import ForwardTafel, BackwardTafel from .no_reaction import NoReaction diff --git a/pybamm/models/submodels/interface/kinetics/base_kinetics.py b/pybamm/models/submodels/interface/kinetics/base_kinetics.py index b65181e792..8c03371fda 100644 --- a/pybamm/models/submodels/interface/kinetics/base_kinetics.py +++ b/pybamm/models/submodels/interface/kinetics/base_kinetics.py @@ -6,7 +6,7 @@ from ..base_interface import BaseInterface -class BaseModel(BaseInterface): +class BaseKinetics(BaseInterface): """ Base submodel for kinetics @@ -16,13 +16,14 @@ class BaseModel(BaseInterface): model parameters domain : str The domain to implement the model, either: 'Negative' or 'Positive'. - + reaction : str + The name of the reaction being implemented **Extends:** :class:`pybamm.interface.BaseInterface` """ - def __init__(self, param, domain): - super().__init__(param, domain) + def __init__(self, param, domain, reaction): + super().__init__(param, domain, reaction) def get_coupled_variables(self, variables): # Calculate delta_phi from phi_s and phi_e if it isn't already known @@ -75,15 +76,6 @@ def get_coupled_variables(self, variables): return variables - def _get_exchange_current_density(self, variables): - raise NotImplementedError - - def _get_kinetics(self, j0, ne, eta_r, T): - raise NotImplementedError - - def _get_open_circuit_potential(self, variables): - raise NotImplementedError - def _get_dj_dc(self, variables): """ Default to calculate derivative of interfacial current density with respect to diff --git a/pybamm/models/submodels/interface/kinetics/butler_volmer.py b/pybamm/models/submodels/interface/kinetics/butler_volmer.py index 0ee18a8f6b..9a6480c78d 100644 --- a/pybamm/models/submodels/interface/kinetics/butler_volmer.py +++ b/pybamm/models/submodels/interface/kinetics/butler_volmer.py @@ -3,11 +3,10 @@ # import pybamm -from .base_kinetics import BaseModel -from .base_first_order_kinetics import BaseFirstOrderKinetics +from .base_kinetics import BaseKinetics -class ButlerVolmer(BaseModel): +class ButlerVolmer(BaseKinetics): """ Base submodel which implements the forward Butler-Volmer equation: @@ -20,20 +19,21 @@ class ButlerVolmer(BaseModel): model parameters domain : str The domain to implement the model, either: 'Negative' or 'Positive'. + reaction : str + The name of the reaction being implemented - - **Extends:** :class:`pybamm.interface.kinetics.BaseModel` + **Extends:** :class:`pybamm.interface.kinetics.BaseKinetics` """ - def __init__(self, param, domain): - super().__init__(param, domain) + def __init__(self, param, domain, reaction): + super().__init__(param, domain, reaction) def _get_kinetics(self, j0, ne, eta_r, T): prefactor = ne / (2 * (1 + self.param.Theta * T)) return 2 * j0 * pybamm.sinh(prefactor * eta_r) def _get_dj_dc(self, variables): - "See :meth:`pybamm.interface.kinetics.BaseModel._get_dj_dc`" + "See :meth:`pybamm.interface.kinetics.BaseKinetics._get_dj_dc`" c_e, delta_phi, j0, ne, ocp, T = self._get_interface_variables_for_first_order( variables ) @@ -44,15 +44,10 @@ def _get_dj_dc(self, variables): ) def _get_dj_ddeltaphi(self, variables): - "See :meth:`pybamm.interface.kinetics.BaseModel._get_dj_ddeltaphi`" + "See :meth:`pybamm.interface.kinetics.BaseKinetics._get_dj_ddeltaphi`" _, delta_phi, j0, ne, ocp, T = self._get_interface_variables_for_first_order( variables ) eta_r = delta_phi - ocp prefactor = ne / (2 * (1 + self.param.Theta * T)) return 2 * j0 * prefactor * pybamm.cosh(prefactor * eta_r) - - -class FirstOrderButlerVolmer(ButlerVolmer, BaseFirstOrderKinetics): - def __init__(self, param, domain): - super().__init__(param, domain) diff --git a/pybamm/models/submodels/interface/kinetics/no_reaction.py b/pybamm/models/submodels/interface/kinetics/no_reaction.py index d16c32a933..dc26b27bfe 100644 --- a/pybamm/models/submodels/interface/kinetics/no_reaction.py +++ b/pybamm/models/submodels/interface/kinetics/no_reaction.py @@ -3,10 +3,10 @@ # import pybamm -from .base_kinetics import BaseModel +from .base_kinetics import BaseKinetics -class NoReaction(BaseModel): +class NoReaction(BaseKinetics): """ Base submodel for when no reaction occurs @@ -16,13 +16,14 @@ class NoReaction(BaseModel): model parameters domain : str The domain to implement the model, either: 'Negative' or 'Positive'. + reaction : str + The name of the reaction being implemented - - **Extends:** :class:`pybamm.interface.kinetics.BaseModel` + **Extends:** :class:`pybamm.interface.kinetics.BaseKinetics` """ - def __init__(self, param, domain): - super().__init__(param, domain) + def __init__(self, param, domain, reaction): + super().__init__(param, domain, reaction) def _get_kinetics(self, j0, ne, eta_r, T): return pybamm.Scalar(0) diff --git a/pybamm/models/submodels/interface/kinetics/tafel.py b/pybamm/models/submodels/interface/kinetics/tafel.py index 26ab18e807..4c74296430 100644 --- a/pybamm/models/submodels/interface/kinetics/tafel.py +++ b/pybamm/models/submodels/interface/kinetics/tafel.py @@ -2,11 +2,10 @@ # Tafel classes # import pybamm -from .base_kinetics import BaseModel -from .base_first_order_kinetics import BaseFirstOrderKinetics +from .base_kinetics import BaseKinetics -class ForwardTafel(BaseModel): +class ForwardTafel(BaseKinetics): """ Base submodel which implements the forward Tafel equation: @@ -19,13 +18,14 @@ class ForwardTafel(BaseModel): model parameters domain : str The domain to implement the model, either: 'Negative' or 'Positive'. + reaction : str + The name of the reaction being implemented - - **Extends:** :class:`pybamm.interface.kinetics.BaseModel` + **Extends:** :class:`pybamm.interface.kinetics.BaseKinetics` """ - def __init__(self, param, domain): - super().__init__(param, domain) + def __init__(self, param, domain, reaction): + super().__init__(param, domain, reaction) def _get_kinetics(self, j0, ne, eta_r, T): return j0 * pybamm.exp((ne / (2 * (1 + self.param.Theta * T))) * eta_r) @@ -56,12 +56,7 @@ def _get_dj_ddeltaphi(self, variables): ) -class FirstOrderForwardTafel(ForwardTafel, BaseFirstOrderKinetics): - def __init__(self, param, domain): - super().__init__(param, domain) - - -class BackwardTafel(BaseModel): +class BackwardTafel(BaseKinetics): """ Base submodel which implements the backward Tafel equation: @@ -76,7 +71,7 @@ class BackwardTafel(BaseModel): The domain to implement the model, either: 'Negative' or 'Positive'. - **Extends:** :class:`pybamm.interface.kinetics.BaseModel` + **Extends:** :class:`pybamm.interface.kinetics.BaseKinetics` """ def __init__(self, param, domain): diff --git a/pybamm/models/submodels/interface/lead_acid.py b/pybamm/models/submodels/interface/lead_acid.py deleted file mode 100644 index 1e19b9ceae..0000000000 --- a/pybamm/models/submodels/interface/lead_acid.py +++ /dev/null @@ -1,136 +0,0 @@ -# -# Lead-acid interface classes -# -import pybamm -from .base_interface import BaseInterface -from . import inverse_kinetics, kinetics - - -class BaseInterfaceLeadAcid(BaseInterface): - """ - Base lead-acid interface class - - Parameters - ---------- - param : - model parameters - domain : str - The domain to implement the model, either: 'Negative' or 'Positive'. - - - **Extends:** :class:`pybamm.interface.BaseInterface` - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - self.reaction_name = "" # empty reaction name, assumed to be the main reaction - - def _get_exchange_current_density(self, variables): - """ - A private function to obtain the exchange current density for a lead acid - deposition reaction. - - Parameters - ---------- - variables: dict - ` The variables in the full model. - - Returns - ------- - j0 : :class: `pybamm.Symbol` - The exchange current density. - """ - c_e = variables[self.domain + " electrolyte concentration"] - # If c_e was broadcast, take only the orphan - if isinstance(c_e, pybamm.Broadcast): - c_e = c_e.orphans[0] - - if self.domain == "Negative": - j0 = self.param.j0_n_S_ref * c_e - - elif self.domain == "Positive": - c_w = self.param.c_w(c_e) - j0 = self.param.j0_p_S_ref * c_e ** 2 * c_w - - return j0 - - def _get_open_circuit_potential(self, variables): - """ - A private function to obtain the open circuit potential and entropic change - - Parameters - ---------- - variables: dict - The variables in the full model. - - Returns - ------- - ocp : :class:`pybamm.Symbol` - The open-circuit potential - dUdT : :class:`pybamm.Symbol` - The entropic change in open-circuit potential due to temperature - - """ - - c_e = variables[self.domain + " electrolyte concentration"] - # If c_e was broadcast, take only the orphan - if isinstance(c_e, pybamm.Broadcast): - c_e = c_e.orphans[0] - - if self.domain == "Negative": - ocp = self.param.U_n(c_e, self.param.T_init) - elif self.domain == "Positive": - ocp = self.param.U_p(c_e, self.param.T_init) - - dUdT = pybamm.Scalar(0) - - return ocp, dUdT - - def _get_number_of_electrons_in_reaction(self): - if self.domain == "Negative": - ne = self.param.ne_n - elif self.domain == "Positive": - ne = self.param.ne_p - return ne - - -class ButlerVolmer(BaseInterfaceLeadAcid, kinetics.ButlerVolmer): - """ - Extends :class:`BaseInterfaceLeadAcid` (for exchange-current density, etc) and - :class:`kinetics.ButlerVolmer` (for kinetics) - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - - -class FirstOrderButlerVolmer(BaseInterfaceLeadAcid, kinetics.FirstOrderButlerVolmer): - """ - Extends :class:`BaseInterfaceLeadAcid` (for exchange-current density, etc) and - :class:`kinetics.FirstOrderButlerVolmer` (for kinetics) - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - - -class InverseFirstOrderKinetics( - BaseInterfaceLeadAcid, inverse_kinetics.BaseInverseFirstOrderKinetics -): - """ - Extends :class:`BaseInterfaceLeadAcid` (for exchange-current density, etc) and - :class:`kinetics.BaseInverseFirstOrderKinetics` (for kinetics) - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - - -class InverseButlerVolmer(BaseInterfaceLeadAcid, inverse_kinetics.InverseButlerVolmer): - """ - Extends :class:`BaseInterfaceLeadAcid` (for exchange-current density, etc) and - :class:`inverse_kinetics.InverseButlerVolmer` (for kinetics) - """ - - def __init__(self, param, domain): - super().__init__(param, domain) diff --git a/pybamm/models/submodels/interface/lead_acid_oxygen.py b/pybamm/models/submodels/interface/lead_acid_oxygen.py deleted file mode 100644 index 5844471616..0000000000 --- a/pybamm/models/submodels/interface/lead_acid_oxygen.py +++ /dev/null @@ -1,138 +0,0 @@ -# -# Interface classes for oxygen reaction in lead-acid batteries -# -import pybamm -from .base_interface import BaseInterface -from . import kinetics, diffusion_limited - - -class BaseInterfaceOxygenLeadAcid(BaseInterface): - """ - Base lead-acid interface class - - Parameters - ---------- - param : - model parameters - domain : str - The domain to implement the model, either: 'Negative' or 'Positive'. - - - **Extends:** :class:`pybamm.interface.BaseInterface` - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - self.reaction_name = " oxygen" - - def _get_exchange_current_density(self, variables): - """ - A private function to obtain the exchange current density for a lead acid - deposition reaction. - - Parameters - ---------- - variables: dict - The variables in the full model. - - Returns - ------- - j0 : :class: `pybamm.Symbol` - The exchange current density. - """ - c_e = variables[self.domain + " electrolyte concentration"] - # If c_e was broadcast, take only the orphan - if isinstance(c_e, pybamm.Broadcast): - c_e = c_e.orphans[0] - - if self.domain == "Negative": - j0 = pybamm.Scalar(0) - elif self.domain == "Positive": - j0 = self.param.j0_p_Ox_ref * c_e # ** self.param.exponent_e_Ox - - return j0 - - def _get_open_circuit_potential(self, variables): - """ - A private function to obtain the open circuit potential and entropic change - - Parameters - ---------- - variables: dict - The variables in the full model. - - Returns - ------- - ocp : :class:`pybamm.Symbol` - The open-circuit potential - dUdT : :class:`pybamm.Symbol` - The entropic change in open-circuit potential due to temperature - - """ - if self.domain == "Negative": - ocp = self.param.U_n_Ox - elif self.domain == "Positive": - ocp = self.param.U_p_Ox - - dUdT = pybamm.Scalar(0) - - return ocp, dUdT - - def _get_number_of_electrons_in_reaction(self): - return self.param.ne_Ox - - -class ForwardTafel(BaseInterfaceOxygenLeadAcid, kinetics.ForwardTafel): - """ - Extends :class:`BaseInterfaceOxygenLeadAcid` (for exchange-current density, etc) and - :class:`kinetics.ForwardTafel` (for kinetics) - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - - -class FirstOrderForwardTafel( - BaseInterfaceOxygenLeadAcid, kinetics.FirstOrderForwardTafel -): - """ - Extends :class:`BaseInterfaceOxygenLeadAcid` (for exchange-current density, etc) and - :class:`kinetics.ForwardTafel` (for kinetics) - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - - -class LeadingOrderDiffusionLimited( - BaseInterfaceOxygenLeadAcid, diffusion_limited.LeadingOrderDiffusionLimited -): - """ - Extends :class:`BaseInterfaceOxygenLeadAcid` (for exchange-current density, etc) and - :class:`kinetics.LeadingOrderDiffusionLimited` (for kinetics) - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - - -class FullDiffusionLimited( - BaseInterfaceOxygenLeadAcid, diffusion_limited.FullDiffusionLimited -): - """ - Extends :class:`BaseInterfaceOxygenLeadAcid` (for exchange-current density, etc) and - :class:`kinetics.FullDiffusionLimited` (for kinetics) - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - - -class NoReaction(BaseInterfaceOxygenLeadAcid, kinetics.NoReaction): - """ - Extends :class:`BaseInterfaceOxygenLeadAcid` (for exchange-current density, etc) and - :class:`kinetics.NoReaction` (for kinetics) - """ - - def __init__(self, param, domain): - super().__init__(param, domain) diff --git a/pybamm/models/submodels/interface/lithium_ion.py b/pybamm/models/submodels/interface/lithium_ion.py deleted file mode 100644 index f4cc6b9d88..0000000000 --- a/pybamm/models/submodels/interface/lithium_ion.py +++ /dev/null @@ -1,131 +0,0 @@ -# -# Lithium-ion interface classes -# -from .base_interface import BaseInterface -from . import inverse_kinetics, kinetics -import pybamm - - -class BaseInterfaceLithiumIon(BaseInterface): - """ - Base lthium-ion interface class - - Parameters - ---------- - param : - model parameters - domain : str - The domain to implement the model, either: 'Negative' or 'Positive'. - - - **Extends:** :class:`pybamm.interface.BaseInterface` - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - self.reaction_name = "" # empty reaction name, assumed to be the main reaction - - def _get_exchange_current_density(self, variables): - """ - A private function to obtain the exchange current density for a lithium-ion - deposition reaction. - - Parameters - ---------- - variables: dict - ` The variables in the full model. - - Returns - ------- - j0 : :class: `pybamm.Symbol` - The exchange current density. - """ - c_s_surf = variables[self.domain + " particle surface concentration"] - c_e = variables[self.domain + " electrolyte concentration"] - T = variables[self.domain + " electrode temperature"] - - # If variable was broadcast, take only the orphan - if ( - isinstance(c_s_surf, pybamm.Broadcast) - and isinstance(c_e, pybamm.Broadcast) - and isinstance(T, pybamm.Broadcast) - ): - c_s_surf = c_s_surf.orphans[0] - c_e = c_e.orphans[0] - T = T.orphans[0] - - if self.domain == "Negative": - prefactor = self.param.m_n(T) / self.param.C_r_n - - elif self.domain == "Positive": - prefactor = self.param.gamma_p * self.param.m_p(T) / self.param.C_r_p - - j0 = prefactor * ( - c_e ** (1 / 2) * c_s_surf ** (1 / 2) * (1 - c_s_surf) ** (1 / 2) - ) - - return j0 - - def _get_open_circuit_potential(self, variables): - """ - A private function to obtain the open circuit potential and entropic change - - Parameters - ---------- - variables: dict - The variables in the full model. - - Returns - ------- - ocp : :class:`pybamm.Symbol` - The open-circuit potential - dUdT : :class:`pybamm.Symbol` - The entropic change in open-circuit potential due to temperature - - """ - c_s_surf = variables[self.domain + " particle surface concentration"] - T = variables[self.domain + " electrode temperature"] - - # If variable was broadcast, take only the orphan - if isinstance(c_s_surf, pybamm.Broadcast) and isinstance(T, pybamm.Broadcast): - c_s_surf = c_s_surf.orphans[0] - T = T.orphans[0] - - if self.domain == "Negative": - ocp = self.param.U_n(c_s_surf, T) - dUdT = self.param.dUdT_n(c_s_surf) - - elif self.domain == "Positive": - ocp = self.param.U_p(c_s_surf, T) - dUdT = self.param.dUdT_p(c_s_surf) - - return ocp, dUdT - - def _get_number_of_electrons_in_reaction(self): - if self.domain == "Negative": - ne = self.param.ne_n - elif self.domain == "Positive": - ne = self.param.ne_p - return ne - - -class ButlerVolmer(BaseInterfaceLithiumIon, kinetics.ButlerVolmer): - """ - Extends :class:`BaseInterfaceLithiumIon` (for exchange-current density, etc) and - :class:`kinetics.ButlerVolmer` (for kinetics) - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - - -class InverseButlerVolmer( - BaseInterfaceLithiumIon, inverse_kinetics.InverseButlerVolmer -): - """ - Extends :class:`BaseInterfaceLithiumIon` (for exchange-current density, etc) and - :class:`inverse_kinetics.InverseButlerVolmer` (for kinetics) - """ - - def __init__(self, param, domain): - super().__init__(param, domain) diff --git a/pybamm/models/submodels/oxygen_diffusion/no_oxygen.py b/pybamm/models/submodels/oxygen_diffusion/no_oxygen.py index 0dc6b5f142..658cc729ac 100644 --- a/pybamm/models/submodels/oxygen_diffusion/no_oxygen.py +++ b/pybamm/models/submodels/oxygen_diffusion/no_oxygen.py @@ -29,7 +29,7 @@ def get_fundamental_variables(self): variables = self._get_standard_concentration_variables(c_ox) - N_e = pybamm.FullBroadcast( + N_e = pybamm.FullBroadcastToEdges( 0, ["negative electrode", "separator", "positive electrode"], "current collector", diff --git a/pybamm/models/submodels/particle/__init__.py b/pybamm/models/submodels/particle/__init__.py index 601bf2de9f..374c59674c 100644 --- a/pybamm/models/submodels/particle/__init__.py +++ b/pybamm/models/submodels/particle/__init__.py @@ -1,3 +1,5 @@ from .base_particle import BaseParticle -from . import fickian -from . import fast +from .fickian_many_particles import FickianManyParticles +from .fickian_single_particle import FickianSingleParticle +from .fast_many_particles import FastManyParticles +from .fast_single_particle import FastSingleParticle diff --git a/pybamm/models/submodels/particle/base_particle.py b/pybamm/models/submodels/particle/base_particle.py index f4234d6458..98ed2a050a 100644 --- a/pybamm/models/submodels/particle/base_particle.py +++ b/pybamm/models/submodels/particle/base_particle.py @@ -89,4 +89,3 @@ def set_events(self, variables): pybamm.EventType.TERMINATION, ) ) - diff --git a/pybamm/models/submodels/particle/fast/__init__.py b/pybamm/models/submodels/particle/fast/__init__.py deleted file mode 100644 index 3b23cc74e1..0000000000 --- a/pybamm/models/submodels/particle/fast/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .base_fast_particle import BaseModel -from .fast_many_particles import ManyParticles -from .fast_single_particle import SingleParticle diff --git a/pybamm/models/submodels/particle/fast/base_fast_particle.py b/pybamm/models/submodels/particle/fast/base_fast_particle.py deleted file mode 100644 index a39e00b519..0000000000 --- a/pybamm/models/submodels/particle/fast/base_fast_particle.py +++ /dev/null @@ -1,37 +0,0 @@ -# -# Base class for particles each with uniform concentration (i.e. infinitely fast -# diffusion in r) -# -from ..base_particle import BaseParticle - - -class BaseModel(BaseParticle): - """Base class for molar conservation in particles with uniform concentration - in r (i.e. infinitely fast diffusion within particles). - - Parameters - ---------- - param : parameter class - The parameters to use for this submodel - domain : str - The domain of the model either 'Negative' or 'Positive' - - - **Extends:** :class:`pybamm.particle.BaseParticle` - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - - def _unpack(self, variables): - raise NotImplementedError - - def set_rhs(self, variables): - - c, _, j = self._unpack(variables) - - if self.domain == "Negative": - self.rhs = {c: -3 * j / self.param.a_n} - - elif self.domain == "Positive": - self.rhs = {c: -3 * j / self.param.a_p / self.param.gamma_p} diff --git a/pybamm/models/submodels/particle/fast/fast_many_particles.py b/pybamm/models/submodels/particle/fast_many_particles.py similarity index 74% rename from pybamm/models/submodels/particle/fast/fast_many_particles.py rename to pybamm/models/submodels/particle/fast_many_particles.py index b1ca21a7f1..25e44544f2 100644 --- a/pybamm/models/submodels/particle/fast/fast_many_particles.py +++ b/pybamm/models/submodels/particle/fast_many_particles.py @@ -4,10 +4,10 @@ # import pybamm -from .base_fast_particle import BaseModel +from .base_particle import BaseParticle -class ManyParticles(BaseModel): +class FastManyParticles(BaseParticle): """Base class for molar conservation in many particles with uniform concentration in r (i.e. infinitely fast diffusion within particles). @@ -19,7 +19,7 @@ class ManyParticles(BaseModel): The domain of the model either 'Negative' or 'Positive' - **Extends:** :class:`pybamm.particle.fast.BaseModel` + **Extends:** :class:`pybamm.particle.BaseParticle` """ def __init__(self, param, domain): @@ -33,7 +33,7 @@ def get_fundamental_variables(self): c_s = pybamm.PrimaryBroadcast(c_s_surf, ["negative particle"]) c_s_xav = pybamm.x_average(c_s) - N_s = pybamm.FullBroadcast( + N_s = pybamm.FullBroadcastToEdges( 0, ["negative particle"], auxiliary_domains={ @@ -41,14 +41,14 @@ def get_fundamental_variables(self): "tertiary": "current collector", }, ) - N_s_xav = pybamm.x_average(N_s) + N_s_xav = pybamm.FullBroadcast(0, "negative electrode", "current collector") elif self.domain == "Positive": c_s_surf = pybamm.standard_variables.c_s_p_surf c_s = pybamm.PrimaryBroadcast(c_s_surf, ["positive particle"]) c_s_xav = pybamm.x_average(c_s) - N_s = pybamm.FullBroadcast( + N_s = pybamm.FullBroadcastToEdges( 0, ["positive particle"], auxiliary_domains={ @@ -56,23 +56,24 @@ def get_fundamental_variables(self): "tertiary": "current collector", }, ) - N_s_xav = pybamm.x_average(N_s) + N_s_xav = pybamm.FullBroadcast(0, "positive electrode", "current collector") variables = self._get_standard_concentration_variables(c_s, c_s_xav) variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) return variables - def _unpack(self, variables): + def set_rhs(self, variables): c_s_surf = variables[self.domain + " particle surface concentration"] - N_s = variables[self.domain + " particle flux"] j = variables[self.domain + " electrode interfacial current density"] + if self.domain == "Negative": + self.rhs = {c_s_surf: -3 * j / self.param.a_n} - return c_s_surf, N_s, j + elif self.domain == "Positive": + self.rhs = {c_s_surf: -3 * j / self.param.a_p / self.param.gamma_p} def set_initial_conditions(self, variables): - c, _, _ = self._unpack(variables) - + c_s_surf = variables[self.domain + " particle surface concentration"] if self.domain == "Negative": x_n = pybamm.standard_spatial_vars.x_n c_init = self.param.c_n_init(x_n) @@ -81,4 +82,4 @@ def set_initial_conditions(self, variables): x_p = pybamm.standard_spatial_vars.x_p c_init = self.param.c_p_init(x_p) - self.initial_conditions = {c: c_init} + self.initial_conditions = {c_s_surf: c_init} diff --git a/pybamm/models/submodels/particle/fast/fast_single_particle.py b/pybamm/models/submodels/particle/fast_single_particle.py similarity index 75% rename from pybamm/models/submodels/particle/fast/fast_single_particle.py rename to pybamm/models/submodels/particle/fast_single_particle.py index 3cfc6c9d6a..60046017d1 100644 --- a/pybamm/models/submodels/particle/fast/fast_single_particle.py +++ b/pybamm/models/submodels/particle/fast_single_particle.py @@ -4,10 +4,10 @@ # import pybamm -from .base_fast_particle import BaseModel +from .base_particle import BaseParticle -class SingleParticle(BaseModel): +class FastSingleParticle(BaseParticle): """Base class for molar conservation in a single x-averaged particle with uniform concentration in r (i.e. infinitely fast diffusion within particles). @@ -19,7 +19,7 @@ class SingleParticle(BaseModel): The domain of the model either 'Negative' or 'Positive' - **Extends:** :class:`pybamm.particle.fast.BaseModel` + **Extends:** :class:`pybamm.particle.BaseParticle` """ def __init__(self, param, domain): @@ -35,7 +35,7 @@ def get_fundamental_variables(self): c_s_xav = pybamm.PrimaryBroadcast(c_s_surf_xav, ["negative particle"]) c_s = pybamm.SecondaryBroadcast(c_s_xav, ["negative electrode"]) - N_s = pybamm.FullBroadcast( + N_s = pybamm.FullBroadcastToEdges( 0, ["negative particle"], auxiliary_domains={ @@ -43,14 +43,14 @@ def get_fundamental_variables(self): "tertiary": "current collector", }, ) - N_s_xav = pybamm.x_average(N_s) + N_s_xav = pybamm.FullBroadcast(0, "negative electrode", "current collector") elif self.domain == "Positive": c_s_surf_xav = pybamm.standard_variables.c_s_p_surf_xav c_s_xav = pybamm.PrimaryBroadcast(c_s_surf_xav, ["positive particle"]) c_s = pybamm.SecondaryBroadcast(c_s_xav, ["positive electrode"]) - N_s = pybamm.FullBroadcast( + N_s = pybamm.FullBroadcastToEdges( 0, ["positive particle"], auxiliary_domains={ @@ -58,25 +58,29 @@ def get_fundamental_variables(self): "tertiary": "current collector", }, ) - N_s_xav = pybamm.x_average(N_s) + N_s_xav = pybamm.FullBroadcast(0, "positive electrode", "current collector") variables = self._get_standard_concentration_variables(c_s, c_s_xav) variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) return variables - def _unpack(self, variables): + def set_rhs(self, variables): + c_s_surf_xav = variables[ "X-averaged " + self.domain.lower() + " particle surface concentration" ] - N_s_xav = variables["X-averaged " + self.domain.lower() + " particle flux"] - j_av = variables[ + j_xav = variables[ "X-averaged " + self.domain.lower() + " electrode interfacial current density" ] - return c_s_surf_xav, N_s_xav, j_av + if self.domain == "Negative": + self.rhs = {c_s_surf_xav: -3 * j_xav / self.param.a_n} + + elif self.domain == "Positive": + self.rhs = {c_s_surf_xav: -3 * j_xav / self.param.a_p / self.param.gamma_p} def set_initial_conditions(self, variables): """ @@ -84,7 +88,9 @@ def set_initial_conditions(self, variables): arbitrarily evaluate them at x=0 in the negative electrode and x=1 in the positive electrode (they will usually be constant) """ - c, _, _ = self._unpack(variables) + c_s_surf_xav = variables[ + "X-averaged " + self.domain.lower() + " particle surface concentration" + ] if self.domain == "Negative": c_init = self.param.c_n_init(0) @@ -92,4 +98,4 @@ def set_initial_conditions(self, variables): elif self.domain == "Positive": c_init = self.param.c_p_init(1) - self.initial_conditions = {c: c_init} + self.initial_conditions = {c_s_surf_xav: c_init} diff --git a/pybamm/models/submodels/particle/fickian/__init__.py b/pybamm/models/submodels/particle/fickian/__init__.py deleted file mode 100644 index 86a9ba03be..0000000000 --- a/pybamm/models/submodels/particle/fickian/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .fickian_many_particles import ManyParticles -from .fickian_single_particle import SingleParticle diff --git a/pybamm/models/submodels/particle/fickian/fickian_many_particles.py b/pybamm/models/submodels/particle/fickian_many_particles.py similarity index 89% rename from pybamm/models/submodels/particle/fickian/fickian_many_particles.py rename to pybamm/models/submodels/particle/fickian_many_particles.py index 87677fde30..3cb2a639ac 100644 --- a/pybamm/models/submodels/particle/fickian/fickian_many_particles.py +++ b/pybamm/models/submodels/particle/fickian_many_particles.py @@ -2,10 +2,10 @@ # Class for many particles with Fickian diffusion # import pybamm -from ..base_particle import BaseParticle +from .base_particle import BaseParticle -class ManyParticles(BaseParticle): +class FickianManyParticles(BaseParticle): """Base class for molar conservation in many particles which employs Fick's law. @@ -52,12 +52,18 @@ def get_coupled_variables(self, variables): if self.domain == "Negative": x = pybamm.standard_spatial_vars.x_n - R = pybamm.FunctionParameter("Negative particle distribution in x", x) + R = pybamm.FunctionParameter( + "Negative particle distribution in x", + {"Dimensionless through-cell position (x_n)": x}, + ) variables.update({"Negative particle distribution in x": R}) elif self.domain == "Positive": x = pybamm.standard_spatial_vars.x_p - R = pybamm.FunctionParameter("Positive particle distribution in x", x) + R = pybamm.FunctionParameter( + "Positive particle distribution in x", + {"Dimensionless through-cell position (x_p)": x}, + ) variables.update({"Positive particle distribution in x": R}) return variables diff --git a/pybamm/models/submodels/particle/fickian/fickian_single_particle.py b/pybamm/models/submodels/particle/fickian_single_particle.py similarity index 98% rename from pybamm/models/submodels/particle/fickian/fickian_single_particle.py rename to pybamm/models/submodels/particle/fickian_single_particle.py index 84579de66c..1d049dffd7 100644 --- a/pybamm/models/submodels/particle/fickian/fickian_single_particle.py +++ b/pybamm/models/submodels/particle/fickian_single_particle.py @@ -3,10 +3,10 @@ # import pybamm -from ..base_particle import BaseParticle +from .base_particle import BaseParticle -class SingleParticle(BaseParticle): +class FickianSingleParticle(BaseParticle): """Base class for molar conservation in a single x-averaged particle which employs Fick's law. diff --git a/pybamm/models/submodels/porosity/base_porosity.py b/pybamm/models/submodels/porosity/base_porosity.py index 1aebacf3df..8d9157635e 100644 --- a/pybamm/models/submodels/porosity/base_porosity.py +++ b/pybamm/models/submodels/porosity/base_porosity.py @@ -96,25 +96,33 @@ def _get_standard_porosity_change_variables(self, deps_dt, set_leading_order=Fal def set_events(self, variables): eps_n = variables["Negative electrode porosity"] eps_p = variables["Positive electrode porosity"] - self.events.append(pybamm.Event( - "Zero negative electrode porosity cut-off", - pybamm.min(eps_n), - pybamm.EventType.TERMINATION - )) - self.events.append(pybamm.Event( - "Max negative electrode porosity cut-off", - pybamm.max(eps_n) - 1, - pybamm.EventType.TERMINATION - )) - - self.events.append(pybamm.Event( - "Zero positive electrode porosity cut-off", - pybamm.min(eps_p), - pybamm.EventType.TERMINATION - )) - - self.events.append(pybamm.Event( - "Max positive electrode porosity cut-off", - pybamm.max(eps_p) - 1, - pybamm.EventType.TERMINATION - )) + self.events.append( + pybamm.Event( + "Zero negative electrode porosity cut-off", + pybamm.min(eps_n), + pybamm.EventType.TERMINATION, + ) + ) + self.events.append( + pybamm.Event( + "Max negative electrode porosity cut-off", + pybamm.max(eps_n) - 1, + pybamm.EventType.TERMINATION, + ) + ) + + self.events.append( + pybamm.Event( + "Zero positive electrode porosity cut-off", + pybamm.min(eps_p), + pybamm.EventType.TERMINATION, + ) + ) + + self.events.append( + pybamm.Event( + "Max positive electrode porosity cut-off", + pybamm.max(eps_p) - 1, + pybamm.EventType.TERMINATION, + ) + ) diff --git a/pybamm/models/submodels/thermal/base_thermal.py b/pybamm/models/submodels/thermal/base_thermal.py index 030a631297..9f532cadae 100644 --- a/pybamm/models/submodels/thermal/base_thermal.py +++ b/pybamm/models/submodels/thermal/base_thermal.py @@ -30,6 +30,9 @@ def _get_standard_fundamental_variables(self, T, T_cn, T_cp): T_x_av = self._x_average(T, T_cn, T_cp) T_vol_av = self._yz_average(T_x_av) + T_amb_dim = param.T_amb_dim(pybamm.t * param.timescale) + T_amb = param.T_amb(pybamm.t * param.timescale) + q = self._flux_law(T) variables = { @@ -64,6 +67,8 @@ def _get_standard_fundamental_variables(self, T, T_cn, T_cp): + param.T_ref, "Heat flux": q, "Heat flux [W.m-2]": q, + "Ambient temperature [K]": T_amb_dim, + "Ambient temperature": T_amb, } return variables diff --git a/pybamm/models/submodels/thermal/isothermal/isothermal.py b/pybamm/models/submodels/thermal/isothermal/isothermal.py index f4d6a7f446..9f99df373e 100644 --- a/pybamm/models/submodels/thermal/isothermal/isothermal.py +++ b/pybamm/models/submodels/thermal/isothermal/isothermal.py @@ -22,8 +22,8 @@ def __init__(self, param): super().__init__(param) def get_fundamental_variables(self): - - T_x_av = pybamm.PrimaryBroadcast(self.param.T_init, "current collector") + T_amb = self.param.T_amb(pybamm.t * self.param.timescale) + T_x_av = pybamm.PrimaryBroadcast(T_amb, "current collector") T_n = pybamm.PrimaryBroadcast(T_x_av, "negative electrode") T_s = pybamm.PrimaryBroadcast(T_x_av, "separator") T_p = pybamm.PrimaryBroadcast(T_x_av, "positive electrode") @@ -33,6 +33,7 @@ def get_fundamental_variables(self): T_cp = T_x_av variables = self._get_standard_fundamental_variables(T, T_cn, T_cp) + return variables def get_coupled_variables(self, variables): @@ -56,7 +57,7 @@ def get_coupled_variables(self, variables): def _flux_law(self, T): """Zero heat flux since temperature is constant""" - q = pybamm.FullBroadcast( + q = pybamm.FullBroadcastToEdges( pybamm.Scalar(0), ["negative electrode", "separator", "positive electrode"], "current collector", diff --git a/pybamm/models/submodels/thermal/x_full/base_x_full.py b/pybamm/models/submodels/thermal/x_full/base_x_full.py index 22ef947378..3b939d8a72 100644 --- a/pybamm/models/submodels/thermal/x_full/base_x_full.py +++ b/pybamm/models/submodels/thermal/x_full/base_x_full.py @@ -25,6 +25,7 @@ def get_fundamental_variables(self): T = pybamm.standard_variables.T T_cn = pybamm.BoundaryValue(T, "left") T_cp = pybamm.BoundaryValue(T, "right") + variables = self._get_standard_fundamental_variables(T, T_cn, T_cp) return variables diff --git a/pybamm/models/submodels/thermal/x_full/x_full_no_current_collector.py b/pybamm/models/submodels/thermal/x_full/x_full_no_current_collector.py index a919e6003b..f70acd4824 100644 --- a/pybamm/models/submodels/thermal/x_full/x_full_no_current_collector.py +++ b/pybamm/models/submodels/thermal/x_full/x_full_no_current_collector.py @@ -35,11 +35,18 @@ def set_boundary_conditions(self, variables): T = variables["Cell temperature"] T_n_left = pybamm.boundary_value(T, "left") T_p_right = pybamm.boundary_value(T, "right") + T_amb = variables["Ambient temperature"] self.boundary_conditions = { T: { - "left": (self.param.h * T_n_left / self.param.lambda_n, "Neumann"), - "right": (-self.param.h * T_p_right / self.param.lambda_p, "Neumann"), + "left": ( + self.param.h * (T_n_left - T_amb) / self.param.lambda_n, + "Neumann", + ), + "right": ( + -self.param.h * (T_p_right - T_amb) / self.param.lambda_p, + "Neumann", + ), } } diff --git a/pybamm/models/submodels/thermal/x_lumped/__init__.py b/pybamm/models/submodels/thermal/x_lumped/__init__.py index de180a7f5d..b07e6f3d1f 100644 --- a/pybamm/models/submodels/thermal/x_lumped/__init__.py +++ b/pybamm/models/submodels/thermal/x_lumped/__init__.py @@ -3,4 +3,3 @@ from .x_lumped_0D_current_collectors import CurrentCollector0D from .x_lumped_1D_current_collectors import CurrentCollector1D from .x_lumped_2D_current_collectors import CurrentCollector2D -from .x_lumped_1D_set_temperature import SetTemperature1D diff --git a/pybamm/models/submodels/thermal/x_lumped/base_x_lumped.py b/pybamm/models/submodels/thermal/x_lumped/base_x_lumped.py index fa93592e60..9fff53d818 100644 --- a/pybamm/models/submodels/thermal/x_lumped/base_x_lumped.py +++ b/pybamm/models/submodels/thermal/x_lumped/base_x_lumped.py @@ -33,6 +33,7 @@ def get_fundamental_variables(self): T_cp = T_x_av variables = self._get_standard_fundamental_variables(T, T_cn, T_cp) + return variables def get_coupled_variables(self, variables): @@ -41,7 +42,7 @@ def get_coupled_variables(self, variables): def _flux_law(self, T): """Fast heat diffusion (temperature has no spatial dependence)""" - q = pybamm.FullBroadcast( + q = pybamm.FullBroadcastToEdges( pybamm.Scalar(0), ["negative electrode", "separator", "positive electrode"], "current collector", diff --git a/pybamm/models/submodels/thermal/x_lumped/x_lumped_0D_current_collectors.py b/pybamm/models/submodels/thermal/x_lumped/x_lumped_0D_current_collectors.py index 1d2025a16a..d8a75059be 100644 --- a/pybamm/models/submodels/thermal/x_lumped/x_lumped_0D_current_collectors.py +++ b/pybamm/models/submodels/thermal/x_lumped/x_lumped_0D_current_collectors.py @@ -13,11 +13,12 @@ def __init__(self, param): def set_rhs(self, variables): T_av = variables["X-averaged cell temperature"] Q_av = variables["X-averaged total heating"] + T_amb = variables["Ambient temperature"] cooling_coeff = self._surface_cooling_coefficient() self.rhs = { - T_av: (self.param.B * Q_av + cooling_coeff * T_av) / self.param.C_th + T_av: self.param.B * Q_av + cooling_coeff * (T_av - T_amb) / self.param.C_th } def _current_collector_heating(self, variables): diff --git a/pybamm/models/submodels/thermal/x_lumped/x_lumped_1D_current_collectors.py b/pybamm/models/submodels/thermal/x_lumped/x_lumped_1D_current_collectors.py index fa96d98e70..a897f92348 100644 --- a/pybamm/models/submodels/thermal/x_lumped/x_lumped_1D_current_collectors.py +++ b/pybamm/models/submodels/thermal/x_lumped/x_lumped_1D_current_collectors.py @@ -15,15 +15,21 @@ def __init__(self, param): def set_rhs(self, variables): T_av = variables["X-averaged cell temperature"] Q_av = variables["X-averaged total heating"] + T_amb = variables["Ambient temperature"] cooling_coeff = self._surface_cooling_coefficient() self.rhs = { - T_av: (pybamm.laplacian(T_av) + self.param.B * Q_av + cooling_coeff * T_av) + T_av: ( + pybamm.laplacian(T_av) + + self.param.B * Q_av + + cooling_coeff * (T_av - T_amb) + ) / self.param.C_th } def set_boundary_conditions(self, variables): + T_amb = variables["Ambient temperature"] T_av = variables["X-averaged cell temperature"] T_av_left = pybamm.boundary_value(T_av, "negative tab") T_av_right = pybamm.boundary_value(T_av, "positive tab") @@ -35,11 +41,11 @@ def set_boundary_conditions(self, variables): self.boundary_conditions = { T_av: { "negative tab": ( - self.param.h * T_av_left / self.param.delta, + self.param.h * (T_av_left - T_amb) / self.param.delta, "Neumann", ), "positive tab": ( - -self.param.h * T_av_right / self.param.delta, + -self.param.h * (T_av_right - T_amb) / self.param.delta, "Neumann", ), "no tab": (pybamm.Scalar(0), "Neumann"), diff --git a/pybamm/models/submodels/thermal/x_lumped/x_lumped_1D_set_temperature.py b/pybamm/models/submodels/thermal/x_lumped/x_lumped_1D_set_temperature.py deleted file mode 100644 index 226a7e59a2..0000000000 --- a/pybamm/models/submodels/thermal/x_lumped/x_lumped_1D_set_temperature.py +++ /dev/null @@ -1,47 +0,0 @@ -# -# Class for thermal submodel in which the temperature is set externally -# -import pybamm - -from .base_x_lumped import BaseModel - - -class SetTemperature1D(BaseModel): - """Class for x-lumped thermal submodel which *doesn't* update the temperature. - Instead, the temperature can be set (as a function of space) externally. - Note, this model computes the heat generation terms for inspection after solve. - - Parameters - ---------- - param : parameter class - The parameters to use for this submodel - - - **Extends:** :class:`pybamm.thermal.BaseModel` - """ - - def __init__(self, param): - super().__init__(param) - - def set_rhs(self, variables): - T_av = variables["X-averaged cell temperature"] - - # Dummy equation so that PyBaMM doesn't change the temperature during solve - # i.e. d_T/d_t = 0. The (local) temperature is set externally between steps. - self.rhs = {T_av: pybamm.Scalar(0)} - - def _current_collector_heating(self, variables): - """Returns the heat source terms in the 1D current collector""" - phi_s_cn = variables["Negative current collector potential"] - phi_s_cp = variables["Positive current collector potential"] - Q_s_cn = self.param.sigma_cn_prime * pybamm.inner( - pybamm.grad(phi_s_cn), pybamm.grad(phi_s_cn) - ) - Q_s_cp = self.param.sigma_cp_prime * pybamm.inner( - pybamm.grad(phi_s_cp), pybamm.grad(phi_s_cp) - ) - return Q_s_cn, Q_s_cp - - def _yz_average(self, var): - """Computes the y-z average by integration over z (no y-direction)""" - return pybamm.z_average(var) diff --git a/pybamm/models/submodels/thermal/x_lumped/x_lumped_2D_current_collectors.py b/pybamm/models/submodels/thermal/x_lumped/x_lumped_2D_current_collectors.py index 811e20c9e1..fa3cc51af7 100644 --- a/pybamm/models/submodels/thermal/x_lumped/x_lumped_2D_current_collectors.py +++ b/pybamm/models/submodels/thermal/x_lumped/x_lumped_2D_current_collectors.py @@ -15,6 +15,7 @@ def __init__(self, param): def set_rhs(self, variables): T_av = variables["X-averaged cell temperature"] Q_av = variables["X-averaged total heating"] + T_amb = variables["Ambient temperature"] cooling_coeff = self._surface_cooling_coefficient() @@ -25,9 +26,9 @@ def set_rhs(self, variables): T_av: ( pybamm.laplacian(T_av) + self.param.B * pybamm.source(Q_av, T_av) - + cooling_coeff * pybamm.source(T_av, T_av) + + cooling_coeff * pybamm.source(T_av - T_amb, T_av) - (self.param.h / self.param.delta) - * pybamm.source(T_av, T_av, boundary=True) + * pybamm.source(T_av - T_amb, T_av, boundary=True) ) / self.param.C_th } diff --git a/pybamm/models/submodels/thermal/x_lumped/x_lumped_no_current_collectors.py b/pybamm/models/submodels/thermal/x_lumped/x_lumped_no_current_collectors.py index db66a8f36f..d67f0b47a8 100644 --- a/pybamm/models/submodels/thermal/x_lumped/x_lumped_no_current_collectors.py +++ b/pybamm/models/submodels/thermal/x_lumped/x_lumped_no_current_collectors.py @@ -27,13 +27,14 @@ def __init__(self, param): def set_rhs(self, variables): T_av = variables["X-averaged cell temperature"] Q_av = variables["X-averaged total heating"] + T_amb = variables["Ambient temperature"] # Get effective properties rho_eff, _ = self._effective_properties() cooling_coeff = self._surface_cooling_coefficient() self.rhs = { - T_av: (self.param.B * Q_av + cooling_coeff * T_av) + T_av: (self.param.B * Q_av + cooling_coeff * (T_av - T_amb)) / (self.param.C_th * rho_eff) } diff --git a/pybamm/models/submodels/thermal/xyz_lumped/base_xyz_lumped.py b/pybamm/models/submodels/thermal/xyz_lumped/base_xyz_lumped.py index f331416774..e4cbb17696 100644 --- a/pybamm/models/submodels/thermal/xyz_lumped/base_xyz_lumped.py +++ b/pybamm/models/submodels/thermal/xyz_lumped/base_xyz_lumped.py @@ -45,11 +45,12 @@ def get_coupled_variables(self, variables): def set_rhs(self, variables): T_vol_av = variables["Volume-averaged cell temperature"] Q_vol_av = variables["Volume-averaged total heating"] + T_amb = variables["Ambient temperature"] cooling_coeff = self._surface_cooling_coefficient() self.rhs = { - T_vol_av: (self.param.B * Q_vol_av + cooling_coeff * T_vol_av) + T_vol_av: (self.param.B * Q_vol_av + cooling_coeff * (T_vol_av - T_amb)) / self.param.C_th } diff --git a/pybamm/parameters/electrical_parameters.py b/pybamm/parameters/electrical_parameters.py index c83405ca5a..b98c0d2e7f 100644 --- a/pybamm/parameters/electrical_parameters.py +++ b/pybamm/parameters/electrical_parameters.py @@ -26,7 +26,7 @@ # the user may provide the typical timescale as a parameter. timescale = pybamm.Parameter("Typical timescale [s]") dimensional_current_with_time = pybamm.FunctionParameter( - "Current function [A]", pybamm.t * timescale + "Current function [A]", {"Time[s]": pybamm.t * timescale} ) dimensional_current_density_with_time = dimensional_current_with_time / ( n_electrodes_parallel * pybamm.geometric_parameters.A_cc diff --git a/pybamm/parameters/parameter_sets.py b/pybamm/parameters/parameter_sets.py index 3b6747c683..b3d42c028f 100644 --- a/pybamm/parameters/parameter_sets.py +++ b/pybamm/parameters/parameter_sets.py @@ -10,17 +10,6 @@ # # Lithium-ion # -Marquis2019 = { - "chemistry": "lithium-ion", - "cell": "kokam_Marquis2019", - "anode": "graphite_mcmb2528_Marquis2019", - "separator": "separator_Marquis2019", - "cathode": "lico2_Marquis2019", - "electrolyte": "lipf6_Marquis2019", - "experiment": "1C_discharge_from_full_Marquis2019", - "citation": "marquis2019asymptotic", -} - NCA_Kim2011 = { "chemistry": "lithium-ion", "cell": "Kim2011", @@ -32,6 +21,28 @@ "citation": "kim2011multi", } +Ecker2015 = { + "chemistry": "lithium-ion", + "cell": "kokam_Ecker2015", + "anode": "graphite_Ecker2015", + "separator": "separator_Ecker2015", + "cathode": "LiNiCoO2_Ecker2015", + "electrolyte": "lipf6_Ecker2015", + "experiment": "1C_discharge_from_full_Ecker2015", + "citation": ["ecker2015i", "ecker2015ii", "richardson2020"], +} + +Marquis2019 = { + "chemistry": "lithium-ion", + "cell": "kokam_Marquis2019", + "anode": "graphite_mcmb2528_Marquis2019", + "separator": "separator_Marquis2019", + "cathode": "lico2_Marquis2019", + "electrolyte": "lipf6_Marquis2019", + "experiment": "1C_discharge_from_full_Marquis2019", + "citation": "marquis2019asymptotic", +} + Chen2020 = { "chemistry": "lithium-ion", "cell": "LGM50_Chen2020", diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py index dd596e746f..ec939e2e93 100644 --- a/pybamm/parameters/parameter_values.py +++ b/pybamm/parameters/parameter_values.py @@ -6,6 +6,7 @@ import os import numbers import numpy as np +from pprint import pformat class ParameterValues: @@ -58,12 +59,10 @@ def __init__(self, values=None, chemistry=None): # Must provide either values or chemistry, not both (nor neither) if values is not None and chemistry is not None: raise ValueError( - """ - Only one of values and chemistry can be provided. To change parameters - slightly from a chemistry, first load parameters with the chemistry - (param = pybamm.ParameterValues(chemistry=...)) and then update with - param.update({dict of values}). - """ + "Only one of values and chemistry can be provided. To change parameters" + " slightly from a chemistry, first load parameters with the chemistry" + " (param = pybamm.ParameterValues(chemistry=...)) and then update with" + " param.update({dict of values})." ) if values is None and chemistry is None: raise ValueError("values and chemistry cannot both be None") @@ -74,9 +73,12 @@ def __init__(self, values=None, chemistry=None): if values is not None: # If base_parameters is a filename, load from that filename if isinstance(values, str): + path = os.path.split(values)[0] values = self.read_parameters_csv(values) + else: + path = None # Don't check parameter already exists when first creating it - self.update(values, check_already_exists=False) + self.update(values, check_already_exists=False, path=path) # Initialise empty _processed_symbols dict (for caching) self._processed_symbols = {} @@ -91,6 +93,9 @@ def __setitem__(self, key, value): def __delitem__(self, key): del self._dict_items[key] + def __repr__(self): + return pformat(self._dict_items, width=1) + def keys(self): "Get the keys of the dictionary" return self._dict_items.keys() @@ -103,13 +108,23 @@ def items(self): "Get the items of the dictionary" return self._dict_items.items() + def search(self, key, print_values=True): + """ + Search dictionary for keys containing 'key'. + + See :meth:`pybamm.FuzzyDict.search()`. + """ + return self._dict_items.search(key, print_values) + def update_from_chemistry(self, chemistry): """ Load standard set of components from a 'chemistry' dictionary """ base_chemistry = chemistry["chemistry"] # Create path to file - path = os.path.join("input", "parameters", base_chemistry) + path = os.path.join( + pybamm.root_dir(), "pybamm", "input", "parameters", base_chemistry + ) # Load each component name for component_group in [ "cell", @@ -143,10 +158,13 @@ def update_from_chemistry(self, chemistry): path=component_path, ) - # register citations + # register (list of) citations if "citation" in chemistry: - citation = chemistry["citation"] - pybamm.citations.register(citation) + citations = chemistry["citation"] + if not isinstance(citations, list): + citations = [citations] + for citation in citations: + pybamm.citations.register(citation) def read_parameters_csv(self, filename): """Reads parameters from csv file into dict. @@ -207,13 +225,10 @@ def update(self, values, check_conflict=False, check_already_exists=True, path=" self._dict_items[name] except KeyError as err: raise KeyError( - """ - Cannot update parameter '{}' as it does not have a default - value. ({}). If you are sure you want to update this parameter, - use param.update({{name: value}}, check_already_exists=False) - """.format( - name, err.args[0] - ) + "Cannot update parameter '{}' as it does not ".format(name) + + "have a default value. ({}). If you are ".format(err.args[0]) + + "sure you want to update this parameter, use " + + "param.update({{name: value}}, check_already_exists=False)" ) # if no conflicts, update, loading functions and data if they are specified # Functions are flagged with the string "[function]" @@ -227,7 +242,9 @@ def update(self, values, check_conflict=False, check_already_exists=True, path=" # Data is flagged with the string "[data]" or "[current data]" elif value.startswith("[current data]") or value.startswith("[data]"): if value.startswith("[current data]"): - data_path = os.path.join("input", "drive_cycles") + data_path = os.path.join( + pybamm.root_dir(), "pybamm", "input", "drive_cycles" + ) filename = os.path.join(data_path, value[14:] + ".csv") function_name = value[14:] else: @@ -257,16 +274,12 @@ def check_and_update_parameter_values(self, values): # Make sure typical current is non-zero if "Typical current [A]" in values and values["Typical current [A]"] == 0: raise ValueError( - """ - "Typical current [A]" cannot be zero. A possible alternative is to set - "Current function [A]" to `0` instead. - """ + "'Typical current [A]' cannot be zero. A possible alternative is to " + "set 'Current function [A]' to `0` instead." ) if "C-rate" in values and "Current function [A]" in values: raise ValueError( - """ - Cannot provide both "C-rate" and "Current function [A]" simultaneously - """ + "Cannot provide both 'C-rate' and 'Current function [A]' simultaneously" ) # If the capacity of the cell has been provided, make sure "C-rate" and current # match with the stated capacity @@ -326,7 +339,8 @@ def process_model(self, unprocessed_model, inplace=True): Raises ------ :class:`pybamm.ModelError` - If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) + If an empty model is passed (`model.rhs = {}` and `model.algebraic = {}` and + `model.variables = {}`) """ pybamm.logger.info( @@ -342,7 +356,11 @@ def process_model(self, unprocessed_model, inplace=True): # create a blank model of the same class model = unprocessed_model.new_copy() - if len(unprocessed_model.rhs) == 0 and len(unprocessed_model.algebraic) == 0: + if ( + len(unprocessed_model.rhs) == 0 + and len(unprocessed_model.algebraic) == 0 + and len(unprocessed_model.variables) == 0 + ): raise pybamm.ModelError("Cannot process parameters for empty model") for variable, equation in model.rhs.items(): @@ -514,6 +532,9 @@ def _process_symbol(self, symbol): # return differentiated function new_diff_variable = self.process_symbol(symbol.diff_variable) function_out = function.diff(new_diff_variable) + # Convert possible float output to a pybamm scalar + if isinstance(function_out, numbers.Number): + return pybamm.Scalar(function_out) # Process again just to be sure return self.process_symbol(function_out) @@ -575,6 +596,9 @@ def evaluate(self, symbol): else: raise ValueError("symbol must evaluate to a constant scalar") + def _ipython_key_completions_(self): + return list(self._dict_items.keys()) + class CurrentToCrate: "Convert a current function to a C-rate function" diff --git a/pybamm/parameters/standard_parameters_lead_acid.py b/pybamm/parameters/standard_parameters_lead_acid.py index e48faefcb1..15e45d3fcd 100644 --- a/pybamm/parameters/standard_parameters_lead_acid.py +++ b/pybamm/parameters/standard_parameters_lead_acid.py @@ -49,7 +49,6 @@ # Electrolyte properties c_e_typ = pybamm.Parameter("Typical electrolyte concentration [mol.m-3]") -t_plus = pybamm.Parameter("Cation transference number") V_w = pybamm.Parameter("Partial molar volume of water [m3.mol-1]") V_plus = pybamm.Parameter("Partial molar volume of cations [m3.mol-1]") V_minus = pybamm.Parameter("Partial molar volume of anions [m3.mol-1]") @@ -164,26 +163,34 @@ Q_p_max_dimensional = pybamm.Parameter("Positive electrode volumetric capacity [C.m-3]") -# Fake thermal -Delta_T = pybamm.Scalar(0) - +# thermal +Delta_T = pybamm.thermal_parameters.Delta_T # -------------------------------------------------------------------------------------- "2. Dimensional Functions" +def t_plus(c_e): + "Dimensionless transference number (i.e. c_e is dimensionless)" + inputs = {"Electrolyte concentration [mol.m-3]": c_e * c_e_typ} + return pybamm.FunctionParameter("Cation transference number", inputs) + + def D_e_dimensional(c_e, T): "Dimensional diffusivity in electrolyte" - return pybamm.FunctionParameter("Electrolyte diffusivity [m2.s-1]", c_e) + inputs = {"Electrolyte concentration [mol.m-3]": c_e} + return pybamm.FunctionParameter("Electrolyte diffusivity [m2.s-1]", inputs) def kappa_e_dimensional(c_e, T): "Dimensional electrolyte conductivity" - return pybamm.FunctionParameter("Electrolyte conductivity [S.m-1]", c_e) + inputs = {"Electrolyte concentration [mol.m-3]": c_e} + return pybamm.FunctionParameter("Electrolyte conductivity [S.m-1]", inputs) def chi_dimensional(c_e): - return pybamm.FunctionParameter("Darken thermodynamic factor", c_e) + inputs = {"Electrolyte concentration [mol.m-3]": c_e} + return pybamm.FunctionParameter("Darken thermodynamic factor", inputs) def c_w_dimensional(c_e, c_ox=0, c_hy=0): @@ -224,31 +231,37 @@ def mu_dimensional(c_e): """ Dimensional viscosity of electrolyte [kg.m-1.s-1]. """ - return pybamm.FunctionParameter("Electrolyte viscosity [kg.m-1.s-1]", c_e) + inputs = {"Electrolyte concentration [mol.m-3]": c_e} + return pybamm.FunctionParameter("Electrolyte viscosity [kg.m-1.s-1]", inputs) def U_n_dimensional(c_e, T): "Dimensional open-circuit voltage in the negative electrode [V]" + inputs = {"Electrolyte molar mass [mol.kg-1]": m_dimensional(c_e)} return pybamm.FunctionParameter( - "Negative electrode open-circuit potential [V]", m_dimensional(c_e) + "Negative electrode open-circuit potential [V]", inputs ) def U_p_dimensional(c_e, T): "Dimensional open-circuit voltage in the positive electrode [V]" + inputs = {"Electrolyte molar mass [mol.kg-1]": m_dimensional(c_e)} return pybamm.FunctionParameter( - "Positive electrode open-circuit potential [V]", m_dimensional(c_e) + "Positive electrode open-circuit potential [V]", inputs ) D_e_typ = D_e_dimensional(c_e_typ, T_ref) rho_typ = rho_dimensional(c_e_typ) mu_typ = mu_dimensional(c_e_typ) + +inputs = {"Electrolyte concentration [mol.m-3]": pybamm.Scalar(1)} U_n_ref = pybamm.FunctionParameter( - "Negative electrode open-circuit potential [V]", pybamm.Scalar(1) + "Negative electrode open-circuit potential [V]", inputs ) +inputs = {"Electrolyte concentration [mol.m-3]": pybamm.Scalar(1)} U_p_ref = pybamm.FunctionParameter( - "Positive electrode open-circuit potential [V]", pybamm.Scalar(1) + "Positive electrode open-circuit potential [V]", inputs ) @@ -277,18 +290,27 @@ def U_p_dimensional(c_e, T): # Electrolyte diffusion timescale tau_diffusion_e = L_x ** 2 / D_e_typ +# Thermal diffusion timescale +tau_th_yz = pybamm.thermal_parameters.tau_th_yz + # Choose discharge timescale timescale = tau_discharge # -------------------------------------------------------------------------------------- "4. Dimensionless Parameters" +# Timescale ratios +C_th = tau_th_yz / tau_discharge # Macroscale Geometry l_n = pybamm.geometric_parameters.l_n l_s = pybamm.geometric_parameters.l_s l_p = pybamm.geometric_parameters.l_p +l_x = pybamm.geometric_parameters.l_x l_y = pybamm.geometric_parameters.l_y l_z = pybamm.geometric_parameters.l_z +a_cc = pybamm.geometric_parameters.a_cc +l = pybamm.geometric_parameters.l +delta = pybamm.geometric_parameters.delta # In lead-acid the current collector and electrodes are the same (same thickness) l_cn = l_n l_cp = l_p @@ -302,7 +324,7 @@ def U_p_dimensional(c_e, T): centre_z_tab_p = pybamm.geometric_parameters.centre_z_tab_p # Diffusive kinematic relationship coefficient -omega_i = c_e_typ * M_e / rho_typ * (t_plus + M_minus / M_e) +omega_i = c_e_typ * M_e / rho_typ * (t_plus(1) + M_minus / M_e) # Migrative kinematic relationship coefficient (electrolyte) omega_c_e = c_e_typ * M_e / rho_typ * (1 - M_w * V_e / V_w * M_e) C_e = tau_diffusion_e / tau_discharge @@ -339,12 +361,10 @@ def U_p_dimensional(c_e, T): # Main s_plus_n_S = s_plus_n_S_dim / ne_n_S s_plus_p_S = s_plus_p_S_dim / ne_p_S -s_n = -(s_plus_n_S + t_plus) # Dimensionless rection rate (neg) -s_p = -(s_plus_p_S + t_plus) # Dimensionless rection rate (pos) -s = pybamm.Concatenation( - pybamm.FullBroadcast(s_n, ["negative electrode"], "current collector"), +s_plus_S = pybamm.Concatenation( + pybamm.FullBroadcast(s_plus_n_S, ["negative electrode"], "current collector"), pybamm.FullBroadcast(0, ["separator"], "current collector"), - pybamm.FullBroadcast(s_p, ["positive electrode"], "current collector"), + pybamm.FullBroadcast(s_plus_p_S, ["positive electrode"], "current collector"), ) j0_n_S_ref = j0_n_S_ref_dimensional / interfacial_current_scale_n j0_p_S_ref = j0_p_S_ref_dimensional / interfacial_current_scale_p @@ -399,11 +419,45 @@ def U_p_dimensional(c_e, T): ) / potential_scale # Electrolyte volumetric capacity -Q_e_max = (l_n * eps_n_max + l_s * eps_s_max + l_p * eps_p_max) / (s_p - s_n) +Q_e_max = (l_n * eps_n_max + l_s * eps_s_max + l_p * eps_p_max) / ( + s_plus_n_S - s_plus_p_S +) Q_e_max_dimensional = Q_e_max * c_e_typ * F capacity = Q_e_max_dimensional * n_electrodes_parallel * A_cs * L_x +# Thermal +rho_cn = pybamm.thermal_parameters.rho_cn +rho_n = pybamm.thermal_parameters.rho_n +rho_s = pybamm.thermal_parameters.rho_s +rho_p = pybamm.thermal_parameters.rho_p +rho_cp = pybamm.thermal_parameters.rho_cp + +rho_k = pybamm.thermal_parameters.rho_k +rho = rho_n * l_n + rho_s * l_s + rho_p * l_p + +lambda_cn = pybamm.thermal_parameters.lambda_cn +lambda_n = pybamm.thermal_parameters.lambda_n +lambda_s = pybamm.thermal_parameters.lambda_s +lambda_p = pybamm.thermal_parameters.lambda_p +lambda_cp = pybamm.thermal_parameters.lambda_cp + +lambda_k = pybamm.thermal_parameters.lambda_k + +Theta = pybamm.thermal_parameters.Theta +h = pybamm.thermal_parameters.h +B = ( + i_typ + * R + * T_ref + * tau_th_yz + / (pybamm.thermal_parameters.rho_eff_dim * F * Delta_T * L_x) +) + +T_amb_dim = pybamm.thermal_parameters.T_amb_dim +T_amb = pybamm.thermal_parameters.T_amb + # Initial conditions +T_init = pybamm.thermal_parameters.T_init q_init = pybamm.Parameter("Initial State of Charge") c_e_init = q_init c_ox_init = c_ox_init_dim / c_ox_typ @@ -428,11 +482,6 @@ def c_p_init(x): return c_e_init -# Thermal effects not implemented for lead-acid, but parameters needed for consistency -T_init = pybamm.Scalar(0) -Theta = pybamm.Scalar(0) # ratio of typical temperature change to ambient temperature - - # -------------------------------------------------------------------------------------- "5. Dimensionless Functions" @@ -455,7 +504,7 @@ def kappa_e(c_e, T): def chi(c_e, c_ox=0, c_hy=0): return ( chi_dimensional(c_e_typ * c_e) - * (2 * (1 - t_plus)) + * (2 * (1 - t_plus(c_e))) / (V_w * c_T(c_e_typ * c_e, c_e_typ * c_ox, c_e_typ * c_hy)) ) @@ -491,7 +540,7 @@ def U_p(c_e_p, T): # 6. Input current and voltage dimensional_current_with_time = pybamm.FunctionParameter( - "Current function [A]", pybamm.t * timescale + "Current function [A]", {"Time [s]": pybamm.t * timescale} ) dimensional_current_density_with_time = dimensional_current_with_time / ( n_electrodes_parallel * pybamm.geometric_parameters.A_cc @@ -500,3 +549,6 @@ def U_p(c_e_p, T): dimensional_current_with_time / I_typ * pybamm.Function(np.sign, I_typ) ) + +"Remove any temporary variables" +del inputs diff --git a/pybamm/parameters/standard_parameters_lithium_ion.py b/pybamm/parameters/standard_parameters_lithium_ion.py index a841dfe713..2b3a8323bc 100644 --- a/pybamm/parameters/standard_parameters_lithium_ion.py +++ b/pybamm/parameters/standard_parameters_lithium_ion.py @@ -108,15 +108,17 @@ def c_n_init_dimensional(x): "Initial concentration as a function of dimensionless position x" + inputs = {"Dimensionless through-cell position (x_n)": x} return pybamm.FunctionParameter( - "Initial concentration in negative electrode [mol.m-3]", x + "Initial concentration in negative electrode [mol.m-3]", inputs ) def c_p_init_dimensional(x): "Initial concentration as a function of dimensionless position x" + inputs = {"Dimensionless through-cell position (x_p)": x} return pybamm.FunctionParameter( - "Initial concentration in positive electrode [mol.m-3]", x + "Initial concentration in positive electrode [mol.m-3]", inputs ) @@ -140,54 +142,88 @@ def c_p_init_dimensional(x): def D_e_dimensional(c_e, T): "Dimensional diffusivity in electrolyte" - return pybamm.FunctionParameter( - "Electrolyte diffusivity [m2.s-1]", c_e, T, T_ref, E_D_e, R - ) + inputs = { + "Electrolyte concentration [mol.m-3]": c_e, + "Temperature [K]": T, + "Reference temperature [K]": T_ref, + "Activation energy [J.mol-1]": E_D_e, + "Ideal gas constant [J.mol-1.K-1]": R, + } + return pybamm.FunctionParameter("Electrolyte diffusivity [m2.s-1]", inputs) def kappa_e_dimensional(c_e, T): "Dimensional electrolyte conductivity" - return pybamm.FunctionParameter( - "Electrolyte conductivity [S.m-1]", c_e, T, T_ref, E_k_e, R - ) + inputs = { + "Electrolyte concentration [mol.m-3]": c_e, + "Temperature [K]": T, + "Reference temperature [K]": T_ref, + "Activation energy [J.mol-1]": E_k_e, + "Ideal gas constant [J.mol-1.K-1]": R, + } + return pybamm.FunctionParameter("Electrolyte conductivity [S.m-1]", inputs) def D_n_dimensional(sto, T): """Dimensional diffusivity in negative particle. Note this is defined as a function of stochiometry""" - return pybamm.FunctionParameter( - "Negative electrode diffusivity [m2.s-1]", sto, T, T_ref, E_D_s_n, R - ) + + inputs = { + "Negative particle stoichiometry": sto, + "Temperature [K]": T, + "Reference temperature [K]": T_ref, + "Activation energy [J.mol-1]": E_D_s_n, + "Ideal gas constant [J.mol-1.K-1]": R, + } + + return pybamm.FunctionParameter("Negative electrode diffusivity [m2.s-1]", inputs) def D_p_dimensional(sto, T): """Dimensional diffusivity in positive particle. Note this is defined as a function of stochiometry""" - return pybamm.FunctionParameter( - "Positive electrode diffusivity [m2.s-1]", sto, T, T_ref, E_D_s_p, R - ) + inputs = { + "Positive particle stoichiometry": sto, + "Temperature [K]": T, + "Reference temperature [K]": T_ref, + "Activation energy [J.mol-1]": E_D_s_p, + "Ideal gas constant [J.mol-1.K-1]": R, + } + return pybamm.FunctionParameter("Positive electrode diffusivity [m2.s-1]", inputs) def m_n_dimensional(T): "Dimensional negative reaction rate" - return pybamm.FunctionParameter( - "Negative electrode reaction rate", T, T_ref, E_r_n, R - ) + inputs = { + "Temperature [K]": T, + "Reference temperature [K]": T_ref, + "Activation energy [J.mol-1]": E_r_n, + "Ideal gas constant [J.mol-1.K-1]": R, + } + return pybamm.FunctionParameter("Negative electrode reaction rate", inputs) def m_p_dimensional(T): "Dimensional negative reaction rate" - return pybamm.FunctionParameter( - "Positive electrode reaction rate", T, T_ref, E_r_p, R - ) + inputs = { + "Temperature [K]": T, + "Reference temperature [K]": T_ref, + "Activation energy [J.mol-1]": E_r_p, + "Ideal gas constant [J.mol-1.K-1]": R, + } + return pybamm.FunctionParameter("Positive electrode reaction rate", inputs) def dUdT_n_dimensional(sto): """ Dimensional entropic change of the negative electrode open-circuit potential [V.K-1] """ + inputs = { + "Negative particle stoichiometry": sto, + "Max negative particle concentration [mol.m-3]": c_n_max, + } return pybamm.FunctionParameter( - "Negative electrode OCP entropic change [V.K-1]", sto, c_n_max + "Negative electrode OCP entropic change [V.K-1]", inputs ) @@ -195,28 +231,36 @@ def dUdT_p_dimensional(sto): """ Dimensional entropic change of the positive electrode open-circuit potential [V.K-1] """ + inputs = { + "Positive particle stoichiometry": sto, + "Max positive particle concentration [mol.m-3]": c_p_max, + } return pybamm.FunctionParameter( - "Positive electrode OCP entropic change [V.K-1]", sto, c_p_max + "Positive electrode OCP entropic change [V.K-1]", inputs ) def U_n_dimensional(sto, T): "Dimensional open-circuit potential in the negative electrode [V]" - u_ref = pybamm.FunctionParameter("Negative electrode OCP [V]", sto) + inputs = {"Negative particle stoichiometry": sto} + u_ref = pybamm.FunctionParameter("Negative electrode OCP [V]", inputs) return u_ref + (T - T_ref) * dUdT_n_dimensional(sto) def U_p_dimensional(sto, T): "Dimensional open-circuit potential in the positive electrode [V]" - u_ref = pybamm.FunctionParameter("Positive electrode OCP [V]", sto) + inputs = {"Positive particle stoichiometry": sto} + u_ref = pybamm.FunctionParameter("Positive electrode OCP [V]", inputs) return u_ref + (T - T_ref) * dUdT_p_dimensional(sto) -# can maybe improve ref value at some stage -U_n_ref = U_n_dimensional(pybamm.Scalar(0.2), T_ref) +# Reference OCP based on initial concentration at current collector/electrode interface +sto_n_init = c_n_init_dimensional(0) / c_n_max +U_n_ref = U_n_dimensional(sto_n_init, T_ref) -# can maybe improve ref value at some stage -U_p_ref = U_p_dimensional(pybamm.Scalar(0.7), T_ref) +# Reference OCP based on initial concentration at current collector/electrode interface +sto_p_init = c_p_init_dimensional(1) / c_p_max +U_p_ref = U_p_dimensional(sto_p_init, T_ref) m_n_ref_dimensional = m_n_dimensional(T_ref) m_p_ref_dimensional = m_p_dimensional(T_ref) @@ -290,16 +334,18 @@ def U_p_dimensional(sto, T): centre_z_tab_p = pybamm.geometric_parameters.centre_z_tab_p # Microscale geometry -epsilon_n = pybamm.FunctionParameter( - "Negative electrode porosity", pybamm.standard_spatial_vars.x_n -) -epsilon_s = pybamm.FunctionParameter( - "Separator porosity", pybamm.standard_spatial_vars.x_s -) -epsilon_p = pybamm.FunctionParameter( - "Positive electrode porosity", pybamm.standard_spatial_vars.x_p -) + +inputs = {"Through-cell distance (x_n) [m]": pybamm.standard_spatial_vars.x_n} +epsilon_n = pybamm.FunctionParameter("Negative electrode porosity", inputs) + +inputs = {"Through-cell distance (x_s) [m]": pybamm.standard_spatial_vars.x_s} +epsilon_s = pybamm.FunctionParameter("Separator porosity", inputs) + +inputs = {"Through-cell distance (x_p) [m]": pybamm.standard_spatial_vars.x_p} +epsilon_p = pybamm.FunctionParameter("Positive electrode porosity", inputs) + epsilon = pybamm.Concatenation(epsilon_n, epsilon_s, epsilon_p) + epsilon_s_n = pybamm.Parameter("Negative electrode active material volume fraction") epsilon_s_p = pybamm.Parameter("Positive electrode active material volume fraction") epsilon_inactive_n = 1 - epsilon_n - epsilon_s_n @@ -324,17 +370,27 @@ def U_p_dimensional(sto, T): alpha_prime = alpha / delta # Electrolyte Properties -t_plus = pybamm.Parameter("Cation transference number") + + +def t_plus(c_e): + "Dimensionless transference number (i.e. c_e is dimensionless)" + inputs = {"Electrolyte concentration [mol.m-3]": c_e * c_e_typ} + return pybamm.FunctionParameter("Cation transference number", inputs) + + +def one_plus_dlnf_dlnc(c_e): + inputs = {"Electrolyte concentration [mol.m-3]": c_e * c_e_typ} + return pybamm.FunctionParameter("1 + dlnf/dlnc", inputs) + + beta_surf = pybamm.Scalar(0) -s = 1 - t_plus # (1-2*t_plus) is for Nernst-Planck # 2*(1-t_plus) for Stefan-Maxwell # Bizeray et al (2016) "Resolving a discrepancy ..." -# note: this is a function for consistancy with lead-acid def chi(c_e): - return 2 * (1 - t_plus) + return (2 * (1 - t_plus(c_e))) * (one_plus_dlnf_dlnc(c_e)) # Electrochemical Reactions @@ -379,6 +435,9 @@ def chi(c_e): / (pybamm.thermal_parameters.rho_eff_dim * F * Delta_T * L_x) ) +T_amb_dim = pybamm.thermal_parameters.T_amb_dim +T_amb = pybamm.thermal_parameters.T_amb + # Initial conditions T_init = pybamm.thermal_parameters.T_init c_e_init = c_e_init_dimensional / c_e_typ @@ -469,7 +528,7 @@ def dUdT_p(c_s_p): # 6. Input current and voltage dimensional_current_with_time = pybamm.FunctionParameter( - "Current function [A]", pybamm.t * timescale + "Current function [A]", {"Time [s]": pybamm.t * timescale} ) dimensional_current_density_with_time = dimensional_current_with_time / ( n_electrodes_parallel * pybamm.geometric_parameters.A_cc @@ -477,3 +536,7 @@ def dUdT_p(c_s_p): current_with_time = ( dimensional_current_with_time / I_typ * pybamm.Function(np.sign, I_typ) ) + + +"Remove any temporary variables" +del inputs diff --git a/pybamm/parameters/thermal_parameters.py b/pybamm/parameters/thermal_parameters.py index 4041c4f5b1..15f0f9ee8f 100644 --- a/pybamm/parameters/thermal_parameters.py +++ b/pybamm/parameters/thermal_parameters.py @@ -110,3 +110,14 @@ h = h_dim * pybamm.geometric_parameters.L_x / lambda_eff_dim T_init = (T_init_dim - T_ref) / Delta_T + +# -------------------------------------------------------------------------------------- +# Ambient temperature + + +def T_amb_dim(t): + return pybamm.FunctionParameter("Ambient temperature [K]", {"Times [s]": t}) + + +def T_amb(t): + return (T_amb_dim(t) - T_ref) / Delta_T # dimensionless T_amb diff --git a/pybamm/parameters_cli.py b/pybamm/parameters_cli.py index 39c6290fc3..7b85af5f71 100644 --- a/pybamm/parameters_cli.py +++ b/pybamm/parameters_cli.py @@ -33,7 +33,7 @@ def get_parser(description): """ parser = argparse.ArgumentParser(description=description) parser.add_argument( - "parameter_dir", type=str, help="Name of the parameter directory", + "parameter_dir", type=str, help="Name of the parameter directory" ) parser.add_argument("battery_type", choices=["lithium-ion", "lead-acid"]) parser.add_argument( @@ -148,6 +148,7 @@ def list_parameters(arguments=None): >>> from pybamm.parameters_cli import list_parameters >>> list_parameters(["lithium-ion", "anodes"]) Available package parameters: + * graphite_Ecker2015 * graphite_Chen2020 * graphite_mcmb2528_Marquis2019 * graphite_Kim2011 diff --git a/pybamm/processed_variable.py b/pybamm/processed_variable.py index 23d74a461c..f10afd5042 100644 --- a/pybamm/processed_variable.py +++ b/pybamm/processed_variable.py @@ -21,8 +21,6 @@ class ProcessedVariable(object): When evaluated, returns an array of size (m,n) solution : :class:`pybamm.Solution` The solution object to be used to create the processed variables - interp_kind : str - The method to use for interpolation known_evals : dict Dictionary of known evaluations, to be used to speed up finding the solution """ @@ -41,14 +39,14 @@ def __init__(self, base_variable, solution, known_evals=None): self.base_eval, self.known_evals[solution.t[0]] = base_variable.evaluate( solution.t[0], solution.y[:, 0], - {name: inp[0] for name, inp in solution.inputs.items()}, + inputs={name: inp[0] for name, inp in solution.inputs.items()}, known_evals=self.known_evals[solution.t[0]], ) else: self.base_eval = base_variable.evaluate( solution.t[0], solution.y[:, 0], - {name: inp[0] for name, inp in solution.inputs.items()}, + inputs={name: inp[0] for name, inp in solution.inputs.items()}, ) # handle 2D (in space) finite element variables differently @@ -59,34 +57,47 @@ def __init__(self, base_variable, solution, known_evals=None): ): if len(solution.t) == 1: # space only (steady solution) - self.initialise_2Dspace_scikit_fem() + self.initialise_2D_fixed_t_scikit_fem() else: - self.initialise_3D_scikit_fem() + self.initialise_2D_scikit_fem() # check variable shape else: if len(solution.t) == 1: raise pybamm.SolverError( - """ - Solution time vector must have length > 1. Check whether simulation - terminated too early. - """ + "Solution time vector must have length > 1. Check whether " + "simulation terminated too early." ) elif ( isinstance(self.base_eval, numbers.Number) or len(self.base_eval.shape) == 0 or self.base_eval.shape[0] == 1 ): - self.initialise_1D() + self.initialise_0D() else: n = self.mesh[0].npts base_shape = self.base_eval.shape[0] + # Try some shapes that could make the variable a 1D variable if base_shape in [n, n + 1]: - self.initialise_2D() + self.initialise_1D() else: - self.initialise_3D() - - def initialise_1D(self): + # Try some shapes that could make the variable a 2D variable + first_dim_nodes = self.mesh[0].nodes + first_dim_edges = self.mesh[0].edges + second_dim_pts = self.base_variable.secondary_mesh[0].nodes + if self.base_eval.size // len(second_dim_pts) in [ + len(first_dim_nodes), + len(first_dim_edges), + ]: + self.initialise_2D() + else: + # Raise error for 3D variable + raise NotImplementedError( + "Shape not recognized for {} ".format(base_variable) + + "(note processing of 3D variables is not yet implemented)" + ) + + def initialise_0D(self): # initialise empty array of the correct size entries = np.empty(len(self.t_sol)) # Evaluate the base_variable index-by-index @@ -96,10 +107,10 @@ def initialise_1D(self): inputs = {name: inp[idx] for name, inp in self.inputs.items()} if self.known_evals: entries[idx], self.known_evals[t] = self.base_variable.evaluate( - t, u, inputs, known_evals=self.known_evals[t] + t, u, inputs=inputs, known_evals=self.known_evals[t] ) else: - entries[idx] = self.base_variable.evaluate(t, u, inputs) + entries[idx] = self.base_variable.evaluate(t, u, inputs=inputs) # No discretisation provided, or variable has no domain (function of t only) self._interpolation_function = interp.interp1d( @@ -107,9 +118,9 @@ def initialise_1D(self): ) self.entries = entries - self.dimensions = 1 + self.dimensions = 0 - def initialise_2D(self): + def initialise_1D(self): len_space = self.base_eval.shape[0] entries = np.empty((len_space, len(self.t_sol))) @@ -120,12 +131,12 @@ def initialise_2D(self): inputs = {name: inp[idx] for name, inp in self.inputs.items()} if self.known_evals: eval_and_known_evals = self.base_variable.evaluate( - t, u, inputs, known_evals=self.known_evals[t] + t, u, inputs=inputs, known_evals=self.known_evals[t] ) entries[:, idx] = eval_and_known_evals[0][:, 0] self.known_evals[t] = eval_and_known_evals[1] else: - entries[:, idx] = self.base_variable.evaluate(t, u, inputs)[:, 0] + entries[:, idx] = self.base_variable.evaluate(t, u, inputs=inputs)[:, 0] # Process the discretisation to get x values nodes = self.mesh[0].nodes @@ -147,7 +158,7 @@ def initialise_2D(self): # assign attributes for reference (either x_sol or r_sol) self.entries = entries - self.dimensions = 2 + self.dimensions = 1 if self.domain[0] in ["negative particle", "positive particle"]: self.first_dimension = "r" self.r_sol = space @@ -165,7 +176,8 @@ def initialise_2D(self): self.first_dimension = "x" self.x_sol = space - self.first_dim_pts = space + self.first_dim_pts = edges + self.internal_boundaries = self.mesh[0].internal_boundaries # set up interpolation # note that the order of 't' and 'space' is the reverse of what you'd expect @@ -174,9 +186,9 @@ def initialise_2D(self): self.t_sol, space, entries_for_interp, kind="linear", fill_value=np.nan ) - def initialise_3D(self): + def initialise_2D(self): """ - Initialise a 3D object that depends on x and r, or x and z. + Initialise a 2D object that depends on x and r, or x and z. """ first_dim_nodes = self.mesh[0].nodes first_dim_edges = self.mesh[0].edges @@ -209,10 +221,8 @@ def initialise_3D(self): self.z_sol = second_dim_pts else: raise pybamm.DomainError( - """ Cannot process 3D object with domain '{}' - and auxiliary_domains '{}'""".format( - self.domain, self.auxiliary_domains - ) + "Cannot process 3D object with domain '{}' " + "and auxiliary_domains '{}'".format(self.domain, self.auxiliary_domains) ) first_dim_size = len(first_dim_pts) @@ -226,7 +236,7 @@ def initialise_3D(self): inputs = {name: inp[idx] for name, inp in self.inputs.items()} if self.known_evals: eval_and_known_evals = self.base_variable.evaluate( - t, u, inputs, known_evals=self.known_evals[t] + t, u, inputs=inputs, known_evals=self.known_evals[t] ) entries[:, :, idx] = np.reshape( eval_and_known_evals[0], @@ -236,14 +246,14 @@ def initialise_3D(self): self.known_evals[t] = eval_and_known_evals[1] else: entries[:, :, idx] = np.reshape( - self.base_variable.evaluate(t, u, inputs), + self.base_variable.evaluate(t, u, inputs=inputs), [first_dim_size, second_dim_size], order="F", ) # assign attributes for reference self.entries = entries - self.dimensions = 3 + self.dimensions = 2 self.first_dim_pts = first_dim_pts self.second_dim_pts = second_dim_pts @@ -255,7 +265,7 @@ def initialise_3D(self): fill_value=np.nan, ) - def initialise_2Dspace_scikit_fem(self): + def initialise_2D_fixed_t_scikit_fem(self): y_sol = self.mesh[0].edges["y"] len_y = len(y_sol) z_sol = self.mesh[0].edges["z"] @@ -265,7 +275,7 @@ def initialise_2Dspace_scikit_fem(self): inputs = {name: inp[0] for name, inp in self.inputs.items()} entries = np.reshape( - self.base_variable.evaluate(0, self.u_sol, inputs), [len_y, len_z] + self.base_variable.evaluate(0, self.u_sol, inputs=inputs), [len_y, len_z] ) # assign attributes for reference @@ -275,13 +285,15 @@ def initialise_2Dspace_scikit_fem(self): self.z_sol = z_sol self.first_dimension = "y" self.second_dimension = "z" + self.first_dim_pts = y_sol + self.second_dim_pts = z_sol # set up interpolation self._interpolation_function = interp.interp2d( y_sol, z_sol, entries, kind="linear", fill_value=np.nan ) - def initialise_3D_scikit_fem(self): + def initialise_2D_scikit_fem(self): y_sol = self.mesh[0].edges["y"] len_y = len(y_sol) z_sol = self.mesh[0].edges["z"] @@ -296,22 +308,24 @@ def initialise_3D_scikit_fem(self): if self.known_evals: eval_and_known_evals = self.base_variable.evaluate( - t, u, inputs, known_evals=self.known_evals[t] + t, u, inputs=inputs, known_evals=self.known_evals[t] ) entries[:, :, idx] = np.reshape(eval_and_known_evals[0], [len_y, len_z]) self.known_evals[t] = eval_and_known_evals[1] else: entries[:, :, idx] = np.reshape( - self.base_variable.evaluate(t, u, inputs), [len_y, len_z] + self.base_variable.evaluate(t, u, inputs=inputs), [len_y, len_z] ) # assign attributes for reference self.entries = entries - self.dimensions = 3 + self.dimensions = 2 self.y_sol = y_sol self.z_sol = z_sol self.first_dimension = "y" self.second_dimension = "z" + self.first_dim_pts = y_sol + self.second_dim_pts = z_sol # set up interpolation self._interpolation_function = interp.RegularGridInterpolator( @@ -322,28 +336,28 @@ def __call__(self, t=None, x=None, r=None, y=None, z=None, warn=True): """ Evaluate the variable at arbitrary t (and x, r, y and/or z), using interpolation """ - if self.dimensions == 1: + if self.dimensions == 0: out = self._interpolation_function(t) + elif self.dimensions == 1: + out = self.call_1D(t, x, r, z) elif self.dimensions == 2: if t is None: out = self._interpolation_function(y, z) else: - out = self.call_2D(t, x, r, z) - elif self.dimensions == 3: - out = self.call_3D(t, x, r, y, z) + out = self.call_2D(t, x, r, y, z) if warn is True and np.isnan(out).any(): pybamm.logger.warning( "Calling variable outside interpolation range (returns 'nan')" ) return out - def call_2D(self, t, x, r, z): - "Evaluate a 2D variable" + def call_1D(self, t, x, r, z): + "Evaluate a 1D variable" spatial_var = eval_dimension_name(self.first_dimension, x, r, None, z) return self._interpolation_function(t, spatial_var) - def call_3D(self, t, x, r, y, z): - "Evaluate a 3D variable" + def call_2D(self, t, x, r, y, z): + "Evaluate a 2D variable" first_dim = eval_dimension_name(self.first_dimension, x, r, y, z) second_dim = eval_dimension_name(self.second_dimension, x, r, y, z) if isinstance(first_dim, np.ndarray): diff --git a/pybamm/quick_plot.py b/pybamm/quick_plot.py index 9c49794e20..7f569840d9 100644 --- a/pybamm/quick_plot.py +++ b/pybamm/quick_plot.py @@ -3,10 +3,17 @@ # import numpy as np import pybamm -import warnings from collections import defaultdict +class LoopList(list): + "A list which loops over itself when accessing an index so that it never runs out." + + def __getitem__(self, i): + # implement looping by calling "(i) modulo (length of list)" + return super().__getitem__(i % len(self)) + + def ax_min(data): "Calculate appropriate minimum axis value for plotting" data_min = np.nanmin(data) @@ -37,22 +44,34 @@ def split_long_string(title, max_words=4): return first_line + "\n" + second_line +def dynamic_plot(*args, **kwargs): + """ + Creates a :class:`pybamm.QuickPlot` object (with arguments 'args' and keyword + arguments 'kwargs') and then calls :meth:`pybamm.QuickPlot.dynamic_plot`. + The key-word argument 'testing' is passed to the 'dynamic_plot' method, not the + `QuickPlot' class. + + Returns + ------- + plot : :class:`pybamm.QuickPlot` + The 'QuickPlot' object that was created + """ + kwargs_for_class = {k: v for k, v in kwargs.items() if k != "testing"} + plot = pybamm.QuickPlot(*args, **kwargs_for_class) + plot.dynamic_plot(kwargs.get("testing", False)) + return plot + + class QuickPlot(object): """ Generates a quick plot of a subset of key outputs of the model so that the model - outputs can be easily assessed. The axis limits can be set using: - self.axis["Variable name"] = [x_min, x_max, y_min, y_max] - They can be reset to the default values by using self.reset_axis. + outputs can be easily assessed. Parameters ---------- - models: (iter of) :class:`pybamm.BaseModel` - The model(s) to plot the outputs of. - meshes: (iter of) :class:`pybamm.Mesh` - The mesh(es) on which the model(s) were solved. - solutions: (iter of) :class:`pybamm.Solver` - The numerical solution(s) for the model(s) which contained the solution to the - model(s). + solutions: (iter of) :class:`pybamm.Solution` or :class:`pybamm.Simulation` + The numerical solution(s) for the model(s), or the simulation object(s) + containing the solution(s). output_variables : list of str, optional List of variables to plot labels : list of str, optional @@ -62,6 +81,21 @@ class QuickPlot(object): ["r", "b", "k", "g", "m", "c"] linestyles : list of str, optional The linestyles to loop over when plotting. Defaults to ["-", ":", "--", "-."] + figsize : tuple of floats, optional + The size of the figure to make + time_unit : str, optional + Format for the time output ("hours", "minutes" or "seconds") + spatial_unit : str, optional + Format for the spatial axes ("m", "mm" or "um") + 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: + + - "fixed" (default): keep all axes fixes so that all data is visible + - "tight": make axes tight to plot at each time + - dictionary: fine-grain control for each variable, can be either "fixed" or \ + "tight" or a specific tuple (lower, upper). + """ def __init__( @@ -71,59 +105,120 @@ def __init__( labels=None, colors=None, linestyles=None, + figsize=None, + time_unit=None, + spatial_unit="um", + variable_limits="fixed", ): - if isinstance(solutions, pybamm.Solution): + if isinstance(solutions, (pybamm.Solution, pybamm.Simulation)): solutions = [solutions] elif not isinstance(solutions, list): - raise TypeError("'solutions' must be 'pybamm.Solution' or list") + raise TypeError( + "solutions must be 'pybamm.Solution' or 'pybamm.Simulation' or list" + ) + + # Extract solution from any simulations + for idx, sol in enumerate(solutions): + if isinstance(sol, pybamm.Simulation): + # 'sol' is actually a 'Simulation' object here so it has a 'Solution' + # attribute + solutions[idx] = sol.solution models = [solution.model for solution in solutions] # Set labels - self.labels = labels or [model.name for model in models] + if labels is None: + self.labels = [model.name for model in models] + else: + if len(labels) != len(models): + raise ValueError( + "labels '{}' have different length to models '{}'".format( + labels, [model.name for model in models] + ) + ) + self.labels = labels - # Set colors and linestyles - self.colors = colors - self.linestyles = linestyles + # Set colors, linestyles, figsize, axis limits + # call LoopList to make sure list index never runs out + self.colors = LoopList(colors or ["r", "b", "k", "g", "m", "c"]) + self.linestyles = LoopList(linestyles or ["-", ":", "--", "-."]) + self.figsize = figsize or (15, 8) - # Time scale in hours - self.time_scale = models[0].timescale_eval / 3600 # Spatial scales (default to 1 if information not in model) + if spatial_unit == "m": + spatial_factor = 1 + self.spatial_unit = "m" + elif spatial_unit == "mm": + spatial_factor = 1e3 + self.spatial_unit = "mm" + elif spatial_unit == "um": # micrometers + spatial_factor = 1e6 + self.spatial_unit = "$\mu m$" + else: + raise ValueError("spatial unit '{}' not recognized".format(spatial_unit)) + variables = models[0].variables - self.spatial_scales = {"x": 1, "y": 1, "z": 1, "r_n": 1, "r_p": 1} - if "x [m]" and "x" in variables: - self.spatial_scales["x"] = (variables["x [m]"] / variables["x"]).evaluate()[ + # empty spatial scales, will raise error later if can't find a particular one + self.spatial_scales = {} + if "x [m]" in variables and "x" in variables: + x_scale = (variables["x [m]"] / variables["x"]).evaluate()[ -1 - ] - if "y [m]" and "y" in variables: - self.spatial_scales["y"] = (variables["y [m]"] / variables["y"]).evaluate()[ - -1 - ] - if "z [m]" and "z" in variables: - self.spatial_scales["z"] = (variables["z [m]"] / variables["z"]).evaluate()[ - -1 - ] - if "r_n [m]" and "r_n" in variables: - self.spatial_scales["r_n"] = ( + ] * spatial_factor + self.spatial_scales.update({dom: x_scale for dom in variables["x"].domain}) + if "y [m]" in variables and "y" in variables: + self.spatial_scales["current collector y"] = ( + variables["y [m]"] / variables["y"] + ).evaluate()[-1] * spatial_factor + if "z [m]" in variables and "z" in variables: + self.spatial_scales["current collector z"] = ( + variables["z [m]"] / variables["z"] + ).evaluate()[-1] * spatial_factor + if "r_n [m]" in variables and "r_n" in variables: + self.spatial_scales["negative particle"] = ( variables["r_n [m]"] / variables["r_n"] - ).evaluate()[-1] - if "r_p [m]" and "r_p" in variables: - self.spatial_scales["r_p"] = ( + ).evaluate()[-1] * spatial_factor + if "r_p [m]" in variables and "r_p" in variables: + self.spatial_scales["positive particle"] = ( variables["r_p [m]"] / variables["r_p"] - ).evaluate()[-1] + ).evaluate()[-1] * spatial_factor # Time parameters + model_timescale_in_seconds = models[0].timescale_eval self.ts = [solution.t for solution in solutions] - self.min_t = np.min([t[0] for t in self.ts]) * self.time_scale - self.max_t = np.max([t[-1] for t in self.ts]) * self.time_scale + min_t = np.min([t[0] for t in self.ts]) * model_timescale_in_seconds + max_t = np.max([t[-1] for t in self.ts]) * model_timescale_in_seconds + + # Set timescale + if time_unit is None: + # defaults depend on how long the simulation is + if max_t >= 3600: + time_scaling_factor = 3600 # time in hours + self.time_unit = "h" + else: + time_scaling_factor = 1 # time in seconds + self.time_unit = "s" + elif time_unit == "seconds": + time_scaling_factor = 1 + self.time_unit = "s" + elif time_unit == "minutes": + time_scaling_factor = 60 + self.time_unit = "min" + elif time_unit == "hours": + time_scaling_factor = 3600 + self.time_unit = "h" + else: + raise ValueError("time unit '{}' not recognized".format(time_unit)) + self.time_scale = model_timescale_in_seconds / time_scaling_factor + self.min_t = min_t / time_scaling_factor + self.max_t = max_t / time_scaling_factor # Default output variables for lead-acid and lithium-ion if output_variables is None: if isinstance(models[0], pybamm.lithium_ion.BaseModel): output_variables = [ - "Negative particle surface concentration", - "Electrolyte concentration", - "Positive particle surface concentration", + "Negative particle surface concentration [mol.m-3]", + "Electrolyte concentration [mol.m-3]", + "Positive particle surface concentration [mol.m-3]", "Current [A]", "Negative electrode potential [V]", "Electrolyte potential [V]", @@ -139,75 +234,182 @@ def __init__( "Electrolyte potential [V]", "Terminal voltage [V]", ] - # else plot all variables in first model + + # Prepare dictionary of variables + # output_variables is a list of strings or lists, e.g. + # ["var 1", ["variable 2", "var 3"]] + output_variable_tuples = [] + self.variable_limits = {} + for variable_list in output_variables: + # Make sure we always have a list of lists of variables, e.g. + # [["var 1"], ["variable 2", "var 3"]] + if isinstance(variable_list, str): + variable_list = [variable_list] + + # Store the key as a tuple + variable_tuple = tuple(variable_list) + output_variable_tuples.append(variable_tuple) + + # axis limits + if variable_limits in ["fixed", "tight"]: + self.variable_limits[variable_tuple] = variable_limits else: - output_variables = models[0].variables + # If there is only one variable, extract it + if len(variable_tuple) == 1: + variable = variable_tuple[0] + else: + variable = variable_tuple + try: + self.variable_limits[variable_tuple] = variable_limits[variable] + except KeyError: + # if variable_tuple is not provided, default to "fixed" + self.variable_limits[variable_tuple] = "fixed" + except TypeError: + raise TypeError( + "variable_limits must be 'fixed', 'tight', or a dict" + ) - self.set_output_variables(output_variables, solutions) + self.set_output_variables(output_variable_tuples, solutions) self.reset_axis() def set_output_variables(self, output_variables, solutions): # Set up output variables self.variables = {} - self.spatial_variable = {} + self.spatial_variable_dict = {} + self.first_dimensional_spatial_variable = {} + self.second_dimensional_spatial_variable = {} + self.first_spatial_scale = {} + self.second_spatial_scale = {} + self.is_x_r = {} # Calculate subplot positions based on number of variables supplied self.subplot_positions = {} self.n_rows = int(len(output_variables) // np.sqrt(len(output_variables))) self.n_cols = int(np.ceil(len(output_variables) / self.n_rows)) - # Process output variables into a form that can be plotted - processed_variables = {} - for solution in solutions: - processed_variables[solution] = {} - for variable_list in output_variables: - # Make sure we always have a list of lists of variables - if isinstance(variable_list, str): - variable_list = [variable_list] - # Add all variables to the list of variables that should be processed - processed_variables[solution].update( - {var: solution[var] for var in variable_list} - ) - - # Prepare dictionary of variables - for k, variable_list in enumerate(output_variables): - # Make sure we always have a list of lists of variables - if isinstance(variable_list, str): - variable_list = [variable_list] - + for k, variable_tuple in enumerate(output_variables): # Prepare list of variables - key = tuple(variable_list) - self.variables[key] = [None] * len(solutions) + variables = [None] * len(solutions) # process each variable in variable_list for each model for i, solution in enumerate(solutions): - # self.variables is a dictionary of lists of lists - self.variables[key][i] = [ - processed_variables[solution][var] for var in variable_list - ] + # variables lists of lists, so variables[i] is a list + variables[i] = [] + for var in variable_tuple: + sol = solution[var] + # Check variable isn't all-nan + if np.all(np.isnan(sol.entries)): + raise ValueError("All-NaN variable '{}' provided".format(var)) + # If ok, add to the list of solutions + else: + variables[i].append(sol) # Make sure variables have the same dimensions and domain - first_variable = self.variables[key][0][0] + # just use the first solution to check this + first_solution = variables[0] + first_variable = first_solution[0] domain = first_variable.domain - for variable in self.variables[key][0]: + # check all other solutions against the first solution + for idx, variable in enumerate(first_solution): if variable.domain != domain: - raise ValueError("mismatching variable domains") - - # Set the x variable for any two-dimensional variables - if first_variable.dimensions == 2: - spatial_variable_key = first_variable.first_dimension - spatial_variable_value = first_variable.first_dim_pts - self.spatial_variable[key] = ( - spatial_variable_key, - spatial_variable_value, + raise ValueError( + "Mismatching variable domains. " + "'{}' has domain '{}', but '{}' has domain '{}'".format( + variable_tuple[0], + domain, + variable_tuple[idx], + variable.domain, + ) + ) + self.spatial_variable_dict[variable_tuple] = {} + + # Set the x variable (i.e. "x" or "r" for any one-dimensional variables) + if first_variable.dimensions == 1: + ( + spatial_var_name, + spatial_var_value, + spatial_scale, + ) = self.get_spatial_var(variable_tuple, first_variable, "first") + self.spatial_variable_dict[variable_tuple] = { + spatial_var_name: spatial_var_value + } + self.first_dimensional_spatial_variable[variable_tuple] = ( + spatial_var_value * spatial_scale ) + self.first_spatial_scale[variable_tuple] = spatial_scale + + elif first_variable.dimensions == 2: + # Don't allow 2D variables if there are multiple solutions + if len(variables) > 1: + raise NotImplementedError( + "Cannot plot 2D variables when comparing multiple solutions, " + "but '{}' is 2D".format(variable_tuple[0]) + ) + # But do allow if just a single solution + else: + # Add both spatial variables to the variable_tuples + ( + first_spatial_var_name, + first_spatial_var_value, + first_spatial_scale, + ) = self.get_spatial_var(variable_tuple, first_variable, "first") + ( + second_spatial_var_name, + second_spatial_var_value, + second_spatial_scale, + ) = self.get_spatial_var(variable_tuple, first_variable, "second") + self.spatial_variable_dict[variable_tuple] = { + first_spatial_var_name: first_spatial_var_value, + second_spatial_var_name: second_spatial_var_value, + } + self.first_dimensional_spatial_variable[variable_tuple] = ( + first_spatial_var_value * first_spatial_scale + ) + self.second_dimensional_spatial_variable[variable_tuple] = ( + second_spatial_var_value * second_spatial_scale + ) + if first_spatial_var_name == "r" and second_spatial_var_name == "x": + self.is_x_r[variable_tuple] = True + else: + self.is_x_r[variable_tuple] = False + + # Store variables and subplot position + self.variables[variable_tuple] = variables + self.subplot_positions[variable_tuple] = (self.n_rows, self.n_cols, k + 1) + + def get_spatial_var(self, key, variable, dimension): + "Return the appropriate spatial variable(s)" + + # Extract name and dimensionless value + # Special case for current collector, which is 2D but in a weird way (both + # first and second variables are in the same domain, not auxiliary domain) + if dimension == "first": + spatial_var_name = variable.first_dimension + spatial_var_value = variable.first_dim_pts + domain = variable.domain[0] + elif dimension == "second": + spatial_var_name = variable.second_dimension + spatial_var_value = variable.second_dim_pts + if variable.domain[0] == "current collector": + domain = "current collector" + else: + domain = variable.auxiliary_domains["secondary"][0] + + if domain == "current collector": + domain += " {}".format(spatial_var_name) + + # Get scale + try: + spatial_scale = self.spatial_scales[domain] + except KeyError: + raise KeyError( + ( + "Can't find spatial scale for '{}', make sure both '{} [m]' " + + "and '{}' are defined in the model variables" + ).format(domain, *[spatial_var_name] * 2) + ) - # Don't allow 3D variables - elif any(var.dimensions == 3 for var in self.variables[key][0]): - raise NotImplementedError("cannot plot 3D variables") - - # Define subplot position - self.subplot_positions[key] = (self.n_rows, self.n_cols, k + 1) + return spatial_var_name, spatial_var_value, spatial_scale def reset_axis(self): """ @@ -215,61 +417,62 @@ def reset_axis(self): These are calculated to fit around the minimum and maximum values of all the variables in each subplot """ - self.axis = {} + self.axis_limits = {} for key, variable_lists in self.variables.items(): - if variable_lists[0][0].dimensions == 1: - spatial_var_name, spatial_var_value = "x", None + if variable_lists[0][0].dimensions == 0: x_min = self.min_t x_max = self.max_t + elif variable_lists[0][0].dimensions == 1: + x_min = self.first_dimensional_spatial_variable[key][0] + x_max = self.first_dimensional_spatial_variable[key][-1] elif variable_lists[0][0].dimensions == 2: - spatial_var_name, spatial_var_value = self.spatial_variable[key] - if spatial_var_name == "r": - if "negative" in key[0].lower(): - spatial_var_scaled = ( - spatial_var_value * self.spatial_scales["r_n"] - ) - elif "positive" in key[0].lower(): - spatial_var_scaled = ( - spatial_var_value * self.spatial_scales["r_p"] - ) + # different order based on whether the domains are x-r, x-z or y-z + if self.is_x_r[key] is True: + x_min = self.second_dimensional_spatial_variable[key][0] + x_max = self.second_dimensional_spatial_variable[key][-1] + y_min = self.first_dimensional_spatial_variable[key][0] + y_max = self.first_dimensional_spatial_variable[key][-1] else: - spatial_var_scaled = ( - spatial_var_value * self.spatial_scales[spatial_var_name] - ) - x_min = spatial_var_scaled[0] - x_max = spatial_var_scaled[-1] - - # Get min and max y values - y_min = np.min( - [ - ax_min( - var( - self.ts[i], - **{spatial_var_name: spatial_var_value}, - warn=False - ) - ) - for i, variable_list in enumerate(variable_lists) - for var in variable_list - ] - ) - y_max = np.max( - [ - ax_max( - var( - self.ts[i], - **{spatial_var_name: spatial_var_value}, - warn=False - ) - ) - for i, variable_list in enumerate(variable_lists) - for var in variable_list - ] - ) - if y_min == y_max: - y_min -= 1 - y_max += 1 - self.axis[key] = [x_min, x_max, y_min, y_max] + x_min = self.first_dimensional_spatial_variable[key][0] + x_max = self.first_dimensional_spatial_variable[key][-1] + y_min = self.second_dimensional_spatial_variable[key][0] + y_max = self.second_dimensional_spatial_variable[key][-1] + + # Create axis for contour plot + self.axis_limits[key] = [x_min, x_max, y_min, y_max] + + # Get min and max variable values + if self.variable_limits[key] == "fixed": + # fixed variable limits: calculate "globlal" min and max + spatial_vars = self.spatial_variable_dict[key] + var_min = np.min( + [ + ax_min(var(self.ts[i], **spatial_vars, warn=False)) + for i, variable_list in enumerate(variable_lists) + for var in variable_list + ] + ) + var_max = np.max( + [ + ax_max(var(self.ts[i], **spatial_vars, warn=False)) + for i, variable_list in enumerate(variable_lists) + for var in variable_list + ] + ) + if var_min == var_max: + var_min -= 1 + var_max += 1 + elif self.variable_limits[key] == "tight": + # tight variable limits: axes will adjust each time + var_min, var_max = None, None + else: + # user-specified axis limits + var_min, var_max = self.variable_limits[key] + + if variable_lists[0][0].dimensions in [0, 1]: + self.axis_limits[key] = [x_min, x_max, var_min, var_max] + else: + self.variable_limits[key] = (var_min, var_max) def plot(self, t): """Produces a quick plot with the internal states at time t. @@ -281,125 +484,261 @@ def plot(self, t): """ import matplotlib.pyplot as plt + import matplotlib.gridspec as gridspec + from matplotlib import cm, colors t /= self.time_scale - self.fig, self.ax = plt.subplots(self.n_rows, self.n_cols, figsize=(15, 8)) - plt.tight_layout() - plt.subplots_adjust(left=-0.1) + self.fig = plt.figure(figsize=self.figsize) + + self.gridspec = gridspec.GridSpec(self.n_rows, self.n_cols) self.plots = {} self.time_lines = {} + self.colorbars = {} + self.axes = [] + + # initialize empty handles, to be created only if the appropriate plots are made + solution_handles = [] - colors = self.colors or ["r", "b", "k", "g", "m", "c"] - linestyles = self.linestyles or ["-", ":", "--", "-."] - fontsize = 42 // self.n_cols + if self.n_cols == 1: + fontsize = 30 + else: + fontsize = 42 // self.n_cols for k, (key, variable_lists) in enumerate(self.variables.items()): - if len(self.variables) == 1: - ax = self.ax - else: - ax = self.ax.flat[k] - ax.set_xlim(self.axis[key][:2]) - ax.set_ylim(self.axis[key][2:]) + ax = self.fig.add_subplot(self.gridspec[k]) + self.axes.append(ax) + x_min, x_max, y_min, y_max = self.axis_limits[key] + ax.set_xlim(x_min, x_max) + if y_min is not None and y_max is not None: + ax.set_ylim(y_min, y_max) ax.xaxis.set_major_locator(plt.MaxNLocator(3)) self.plots[key] = defaultdict(dict) + variable_handles = [] # Set labels for the first subplot only (avoid repetition) - if variable_lists[0][0].dimensions == 2: - # 2D plot: plot as a function of x at time t - spatial_var_name, spatial_var_value = self.spatial_variable[key] - ax.set_xlabel(spatial_var_name + " [m]", fontsize=fontsize) + if variable_lists[0][0].dimensions == 0: + # 0D plot: plot as a function of time, indicating time t with a line + ax.set_xlabel("Time [{}]".format(self.time_unit), fontsize=fontsize) for i, variable_list in enumerate(variable_lists): for j, variable in enumerate(variable_list): - if spatial_var_name == "r": - if "negative" in key[0].lower(): - spatial_scale = self.spatial_scales["r_n"] - elif "positive" in key[0].lower(): - spatial_scale = self.spatial_scales["r_p"] + if len(variable_list) == 1: + # single variable -> use linestyle to differentiate model + linestyle = self.linestyles[i] else: - spatial_scale = self.spatial_scales[spatial_var_name] - (self.plots[key][i][j],) = ax.plot( - spatial_var_value * spatial_scale, - variable( - t, **{spatial_var_name: spatial_var_value}, warn=False - ), - lw=2, - color=colors[i], - linestyle=linestyles[j], - ) - else: - # 1D plot: plot as a function of time, indicating time t with a line - ax.set_xlabel("Time [h]", fontsize=fontsize) - for i, variable_list in enumerate(variable_lists): - for j, variable in enumerate(variable_list): + # multiple variables -> use linestyle to differentiate + # variables (color differentiates models) + linestyle = self.linestyles[j] full_t = self.ts[i] (self.plots[key][i][j],) = ax.plot( full_t * self.time_scale, variable(full_t, warn=False), lw=2, - color=colors[i], - linestyle=linestyles[j], + color=self.colors[i], + linestyle=linestyle, ) - y_min, y_max = self.axis[key][2:] + variable_handles.append(self.plots[key][0][j]) + solution_handles.append(self.plots[key][i][0]) + y_min, y_max = ax.get_ylim() + ax.set_ylim(y_min, y_max) (self.time_lines[key],) = ax.plot( [t * self.time_scale, t * self.time_scale], [y_min, y_max], "k--" ) + elif variable_lists[0][0].dimensions == 1: + # 1D plot: plot as a function of x at time t + # Read dictionary of spatial variables + spatial_vars = self.spatial_variable_dict[key] + spatial_var_name = list(spatial_vars.keys())[0] + ax.set_xlabel( + "{} [{}]".format(spatial_var_name, self.spatial_unit), + fontsize=fontsize, + ) + for i, variable_list in enumerate(variable_lists): + for j, variable in enumerate(variable_list): + if len(variable_list) == 1: + # single variable -> use linestyle to differentiate model + linestyle = self.linestyles[i] + else: + # multiple variables -> use linestyle to differentiate + # variables (color differentiates models) + linestyle = self.linestyles[j] + (self.plots[key][i][j],) = ax.plot( + self.first_dimensional_spatial_variable[key], + variable(t, **spatial_vars, warn=False), + lw=2, + color=self.colors[i], + linestyle=linestyle, + zorder=10, + ) + variable_handles.append(self.plots[key][0][j]) + solution_handles.append(self.plots[key][i][0]) + # add dashed lines for boundaries between subdomains + y_min, y_max = ax.get_ylim() + ax.set_ylim(y_min, y_max) + for bnd in variable_lists[0][0].internal_boundaries: + bnd_dim = bnd * self.first_spatial_scale[key] + ax.plot( + [bnd_dim, bnd_dim], [y_min, y_max], color="0.5", lw=1, zorder=0 + ) + elif variable_lists[0][0].dimensions == 2: + # Read dictionary of spatial variables + spatial_vars = self.spatial_variable_dict[key] + # there can only be one entry in the variable list + variable = variable_lists[0][0] + # different order based on whether the domains are x-r, x-z or y-z + if self.is_x_r[key] is True: + x_name = list(spatial_vars.keys())[1][0] + y_name = list(spatial_vars.keys())[0][0] + x = self.second_dimensional_spatial_variable[key] + y = self.first_dimensional_spatial_variable[key] + var = variable(t, **spatial_vars, warn=False) + else: + x_name = list(spatial_vars.keys())[0][0] + y_name = list(spatial_vars.keys())[1][0] + x = self.first_dimensional_spatial_variable[key] + y = self.second_dimensional_spatial_variable[key] + var = variable(t, **spatial_vars, warn=False).T + ax.set_xlabel( + "{} [{}]".format(x_name, self.spatial_unit), fontsize=fontsize + ) + ax.set_ylabel( + "{} [{}]".format(y_name, self.spatial_unit), fontsize=fontsize + ) + vmin, vmax = self.variable_limits[key] + ax.contourf( + x, y, var, levels=100, vmin=vmin, vmax=vmax, cmap="coolwarm" + ) + if vmin is None and vmax is None: + vmin = ax_min(var) + vmax = ax_max(var) + self.colorbars[key] = self.fig.colorbar( + cm.ScalarMappable( + colors.Normalize(vmin=vmin, vmax=vmax), cmap="coolwarm" + ), + ax=ax, + ) # Set either y label or legend entries if len(key) == 1: title = split_long_string(key[0]) ax.set_title(title, fontsize=fontsize) else: ax.legend( + variable_handles, [split_long_string(s, 6) for s in key], bbox_to_anchor=(0.5, 1), fontsize=8, loc="lower center", ) - if k == len(self.variables) - 1: - ax.legend(self.labels, loc="upper right", bbox_to_anchor=(1, -0.2)) - def dynamic_plot(self, testing=False): - """ - Generate a dynamic plot with a slider to control the time. We recommend using - ipywidgets instead of this function if you are using jupyter notebooks + # Set global legend + if len(solution_handles) > 0: + self.fig.legend(solution_handles, self.labels, loc="lower right") + + # Fix layout + bottom = 0.05 + 0.03 * max((len(self.labels) - 2), 0) + self.gridspec.tight_layout(self.fig, rect=[0, bottom, 1, 1]) + + def dynamic_plot(self, testing=False, step=None): """ + Generate a dynamic plot with a slider to control the time. - import matplotlib.pyplot as plt - from matplotlib.widgets import Slider + Parameters + ---------- + step : float + For notebook mode, size of steps to allow in the slider. Defaults to 1/100th + of the total time. + testing : bool + Whether to actually make the plot (turned off for unit tests) - # create an initial plot at time 0 - self.plot(0) + """ + if pybamm.is_notebook(): # pragma: no cover + import ipywidgets as widgets + + step = step or self.max_t / 100 + widgets.interact( + self.plot, + t=widgets.FloatSlider(min=0, max=self.max_t, step=step, value=0), + continuous_update=False, + ) + else: + import matplotlib.pyplot as plt + from matplotlib.widgets import Slider - axcolor = "lightgoldenrodyellow" - axfreq = plt.axes([0.315, 0.02, 0.37, 0.03], facecolor=axcolor) - self.sfreq = Slider(axfreq, "Time", 0, self.max_t, valinit=0) - self.sfreq.on_changed(self.update) + # create an initial plot at time 0 + self.plot(0) - # ignore the warning about tight layout - warnings.simplefilter("ignore") - self.fig.tight_layout() - warnings.simplefilter("always") + axcolor = "lightgoldenrodyellow" + ax_slider = plt.axes([0.315, 0.02, 0.37, 0.03], facecolor=axcolor) + self.slider = Slider( + ax_slider, "Time [{}]".format(self.time_unit), 0, self.max_t, valinit=0 + ) + self.slider.on_changed(self.slider_update) - if not testing: # pragma: no cover - plt.show() + if not testing: # pragma: no cover + plt.show() - def update(self, val): + def slider_update(self, t): """ Update the plot in self.plot() with values at new time """ - t = self.sfreq.val + from matplotlib import cm, colors + t_dimensionless = t / self.time_scale - for key, plot in self.plots.items(): - if self.variables[key][0][0].dimensions == 2: - spatial_var_name, spatial_var_value = self.spatial_variable[key] + for k, (key, plot) in enumerate(self.plots.items()): + ax = self.axes[k] + if self.variables[key][0][0].dimensions == 0: + self.time_lines[key].set_xdata([t]) + elif self.variables[key][0][0].dimensions == 1: + var_min = np.inf + var_max = -np.inf for i, variable_lists in enumerate(self.variables[key]): for j, variable in enumerate(variable_lists): - plot[i][j].set_ydata( - variable( - t_dimensionless, - **{spatial_var_name: spatial_var_value}, - warn=False - ) + var = variable( + t_dimensionless, + **self.spatial_variable_dict[key], + warn=False ) - else: - self.time_lines[key].set_xdata([t]) + plot[i][j].set_ydata(var) + var_min = min(var_min, np.nanmin(var)) + var_max = max(var_max, np.nanmax(var)) + # update boundaries between subdomains + y_min, y_max = self.axis_limits[key][2:] + if y_min is None and y_max is None: + y_min, y_max = ax_min(var_min), ax_max(var_max) + ax.set_ylim(y_min, y_max) + for bnd in self.variables[key][0][0].internal_boundaries: + bnd_dim = bnd * self.first_spatial_scale[key] + ax.plot( + [bnd_dim, bnd_dim], + [y_min, y_max], + color="0.5", + lw=1, + zorder=0, + ) + elif self.variables[key][0][0].dimensions == 2: + # 2D plot: plot as a function of x and y at time t + # Read dictionary of spatial variables + spatial_vars = self.spatial_variable_dict[key] + # there can only be one entry in the variable list + variable = self.variables[key][0][0] + vmin, vmax = self.variable_limits[key] + if self.is_x_r[key] is True: + x = self.second_dimensional_spatial_variable[key] + y = self.first_dimensional_spatial_variable[key] + var = variable(t_dimensionless, **spatial_vars, warn=False) + else: + x = self.first_dimensional_spatial_variable[key] + y = self.second_dimensional_spatial_variable[key] + var = variable(t_dimensionless, **spatial_vars, warn=False).T + ax.contourf( + x, y, var, levels=100, vmin=vmin, vmax=vmax, cmap="coolwarm" + ) + if (vmin, vmax) == (None, None): + vmin = ax_min(var) + vmax = ax_max(var) + cb = self.colorbars[key] + cb.update_bruteforce( + cm.ScalarMappable( + colors.Normalize(vmin=vmin, vmax=vmax), cmap="coolwarm" + ) + ) self.fig.canvas.draw_idle() diff --git a/pybamm/simulation.py b/pybamm/simulation.py index 2a5387feee..72874b7dee 100644 --- a/pybamm/simulation.py +++ b/pybamm/simulation.py @@ -9,12 +9,15 @@ import sys -def isnotebook(): +def is_notebook(): try: shell = get_ipython().__class__.__name__ - if shell == "ZMQInteractiveShell": - return True # Jupyter notebook or qtconsole - elif shell == "TerminalInteractiveShell": + if shell == "ZMQInteractiveShell": # pragma: no cover + # Jupyter notebook or qtconsole + cfg = get_ipython().config + nb = len(cfg["InteractiveShell"].keys()) == 0 + return nb + elif shell == "TerminalInteractiveShell": # pragma: no cover return False # Terminal running IPython else: return False # Other type (?) @@ -28,17 +31,11 @@ def constant_current_constant_voltage_constant_power(variables): s_I = pybamm.InputParameter("Current switch") s_V = pybamm.InputParameter("Voltage switch") s_P = pybamm.InputParameter("Power switch") - n_electrodes_parallel = pybamm.electrical_parameters.n_electrodes_parallel n_cells = pybamm.electrical_parameters.n_cells return ( - s_I * (I - pybamm.InputParameter("Current input [A]") / n_electrodes_parallel) + s_I * (I - pybamm.InputParameter("Current input [A]")) + s_V * (V - pybamm.InputParameter("Voltage input [V]") / n_cells) - + s_P - * ( - V * I - - pybamm.InputParameter("Power input [W]") - / (n_cells * n_electrodes_parallel) - ) + + s_P * (V * I - pybamm.InputParameter("Power input [W]") / n_cells) ) @@ -49,13 +46,12 @@ class Simulation: ---------- model : :class:`pybamm.BaseModel` The model to be simulated - experiment : : class:`pybamm.Experiment` (optional) + experiment : :class:`pybamm.Experiment` (optional) The experimental conditions under which to solve the model geometry: :class:`pybamm.Geometry` (optional) The geometry upon which to solve the model - parameter_values: dict (optional) - A dictionary of parameters and their corresponding numerical - values + parameter_values: :class:`pybamm.ParameterValues` (optional) + Parameters and their corresponding numerical values. submesh_types: dict (optional) A dictionary of the types of submesh to use on each subdomain var_pts: dict (optional) @@ -107,7 +103,7 @@ def __init__( self.reset(update_model=False) # ignore runtime warnings in notebooks - if isnotebook(): + if is_notebook(): # pragma: no cover import warnings warnings.filterwarnings("ignore") @@ -212,21 +208,18 @@ def set_up_experiment(self, model, experiment): # add current and voltage events to the model # current events both negative and positive to catch specification - n_electrodes_parallel = pybamm.electrical_parameters.n_electrodes_parallel n_cells = pybamm.electrical_parameters.n_cells self.model.events.extend( [ pybamm.Event( "Current cut-off (positive) [A] [experiment]", self.model.variables["Current [A]"] - - abs(pybamm.InputParameter("Current cut-off [A]")) - / n_electrodes_parallel, + - abs(pybamm.InputParameter("Current cut-off [A]")), ), pybamm.Event( "Current cut-off (negative) [A] [experiment]", self.model.variables["Current [A]"] - + abs(pybamm.InputParameter("Current cut-off [A]")) - / n_electrodes_parallel, + + abs(pybamm.InputParameter("Current cut-off [A]")), ), pybamm.Event( "Voltage cut-off [V] [experiment]", @@ -393,6 +386,8 @@ def solve( # to correspond to a single discharge elif t_eval is None: C_rate = self._parameter_values["C-rate"] + if isinstance(C_rate, pybamm.InputParameter): + C_rate = inputs["C-rate"] try: t_end = 3600 / C_rate except TypeError: @@ -537,17 +532,9 @@ def plot(self, quick_plot_vars=None, testing=False): if quick_plot_vars is None: quick_plot_vars = self.quick_plot_vars - plot = pybamm.QuickPlot(self._solution, output_variables=quick_plot_vars) - - if isnotebook(): - import ipywidgets as widgets - - widgets.interact( - plot.plot, - t=widgets.FloatSlider(min=0, max=plot.max_t, step=0.05, value=0), - ) - else: - plot.dynamic_plot(testing=testing) + self.quick_plot = pybamm.dynamic_plot( + self._solution, output_variables=quick_plot_vars, testing=testing + ) @property def model(self): diff --git a/pybamm/solvers/algebraic_solver.py b/pybamm/solvers/algebraic_solver.py index db366fc1f0..7ab1f6cb0c 100644 --- a/pybamm/solvers/algebraic_solver.py +++ b/pybamm/solvers/algebraic_solver.py @@ -1,12 +1,13 @@ # # Algebraic solver class # +import casadi import pybamm import numpy as np from scipy import optimize -class AlgebraicSolver(object): +class AlgebraicSolver(pybamm.BaseSolver): """Solve a discretised model which contains only (time independent) algebraic equations using a root finding algorithm. Note: this solver could be extended for quasi-static models, or models in @@ -17,22 +18,16 @@ class AlgebraicSolver(object): ---------- method : str, optional The method to use to solve the system (default is "lm") - tolerance : float, optional + tol : float, optional The tolerance for the solver (default is 1e-6). """ def __init__(self, method="lm", tol=1e-6): - self.method = method + super().__init__(method=method) self.tol = tol self.name = "Algebraic solver ({})".format(method) - - @property - def method(self): - return self._method - - @method.setter - def method(self, value): - self._method = value + self.algebraic_solver = True + pybamm.citations.register("virtanen2020scipy") @property def tol(self): @@ -42,178 +37,78 @@ def tol(self): def tol(self, value): self._tol = value - def solve(self, model): - """Calculate the solution of the model. - - Parameters - ---------- - model : :class:`pybamm.BaseModel` - The model whose solution to calculate. Must only contain algebraic - equations. - - """ - pybamm.logger.info("Start solving {} with {}".format(model.name, self.name)) - - # Set up - timer = pybamm.Timer() - start_time = timer.time() - concatenated_algebraic, jac = self.set_up(model) - set_up_time = timer.time() - start_time - - # Create function to evaluate algebraic - def algebraic(y): - return concatenated_algebraic.evaluate(0, y, known_evals={})[0][:, 0] - - # Create function to evaluate jacobian - if jac is not None: - - def jacobian(y): - # Note: we only use this solver for time independent algebraic - # systems, so jac is arbitrarily evaluated at t=0. Also, needs - # to be converted from sparse to dense, so in very large - # algebraic models it may be best to switch use_jacobian to False - # by default. - return jac.evaluate(0, y, known_evals={})[0].toarray() - - else: - jacobian = None - - # Use "initial conditions" set in model as initial guess - y0_guess = model.concatenated_initial_conditions.evaluate(t=0) - - # Solve - solve_start_time = timer.time() - pybamm.logger.info("Calling root finding algorithm") - solution = self.root(algebraic, y0_guess, jacobian=jacobian) - solution.model = model - - # Assign times - solution.solve_time = timer.time() - solve_start_time - solution.set_up_time = set_up_time - - pybamm.logger.info("Finish solving {}".format(model.name)) - pybamm.logger.info( - "Set-up time: {}, Solve time: {}, Total time: {}".format( - timer.format(solution.set_up_time), - timer.format(solution.solve_time), - timer.format(solution.total_time), - ) - ) - return solution - - def root(self, algebraic, y0_guess, jacobian=None): + def _integrate(self, model, t_eval, inputs=None): """ Calculate the solution of the algebraic equations through root-finding Parameters ---------- - algebraic : method - Function that takes in y and returns the value of the algebraic - equations - y0_guess : array-like - Array of the user's guess for the solution, used to initialise - the root finding algorithm - jacobian : method, optional - A function that takes in t and y and returns the Jacobian. If - None, the solver will approximate the Jacobian if required. + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. + t_eval : :class:`numpy.array`, size (k,) + The times at which to compute the solution + inputs : dict, optional + Any input parameters to pass to the model when solving """ + inputs = inputs or {} + if model.convert_to_format == "casadi": + inputs = casadi.vertcat(*[x for x in inputs.values()]) - def root_fun(y0): - "Evaluates algebraic using y0" - out = algebraic(y0) - pybamm.logger.debug( - "Evaluating algebraic equations, L2-norm is {}".format( - np.linalg.norm(out) - ) - ) - return out - - if jacobian: - sol = optimize.root( - root_fun, y0_guess, method=self.method, tol=self.tol, jac=jacobian - ) - else: - sol = optimize.root(root_fun, y0_guess, method=self.method, tol=self.tol) - - if sol.success and np.all(sol.fun < self.tol * len(sol.x)): - termination = "success" - # Return solution object (no events, so pass None to t_event, y_event) - return pybamm.Solution([0], sol.x[:, np.newaxis], termination=termination) - elif not sol.success: - raise pybamm.SolverError( - "Could not find acceptable solution: {}".format(sol.message) - ) - else: - raise pybamm.SolverError( - """ - Could not find acceptable solution: solver terminated - successfully, but maximum solution error ({}) above tolerance ({}) - """.format( - np.max(sol.fun), self.tol * len(sol.x) + algebraic = model.algebraic_eval + y0 = model.y0 + + y = np.empty((len(y0), len(t_eval))) + + for idx, t in enumerate(t_eval): + + def root_fun(y): + "Evaluates algebraic using y" + out = algebraic(t, y, inputs) + pybamm.logger.debug( + "Evaluating algebraic equations at t={}, L2-norm is {}".format( + t, np.linalg.norm(out) + ) ) - ) + return out - def set_up(self, model): - """Unpack model, perform checks, simplify and calculate jacobian. + if model.jacobian_eval is not None: - Parameters - ---------- - model : :class:`pybamm.BaseModel` - The model whose solution to calculate. Must have attributes rhs and - initial_conditions - - Returns - ------- - concatenated_algebraic : :class:`pybamm.Concatenation` - Algebraic equations, which should evaluate to zero - jac : :class:`pybamm.SparseStack` - Jacobian matrix for the differential and algebraic equations - - Raises - ------ - :class:`pybamm.SolverError` - If the model contains any time derivatives, i.e. rhs equations (in - which case an ODE or DAE solver should be used instead) - """ - if len(model.rhs) > 0: - raise pybamm.SolverError( - """Cannot use algebraic solver to solve model with time derivatives""" - ) - - # create simplified algebraic expressions - concatenated_algebraic = model.concatenated_algebraic - - if model.use_simplify: - # set up simplification object, for re-use of dict - simp = pybamm.Simplification() - pybamm.logger.info("Simplifying algebraic") - concatenated_algebraic = simp.simplify(concatenated_algebraic) - - if model.use_jacobian: - # Create Jacobian from concatenated algebraic - y = pybamm.StateVector( - slice(0, np.size(model.concatenated_initial_conditions)) - ) - # set up Jacobian object, for re-use of dict - jacobian = pybamm.Jacobian() - pybamm.logger.info("Calculating jacobian") - jac = jacobian.jac(concatenated_algebraic, y) - model.jacobian = jac - model.jacobian_algebraic = jac - - if model.use_simplify: - pybamm.logger.info("Simplifying jacobian") - jac = simp.simplify(jac) - - if model.convert_to_format == "python": - pybamm.logger.info("Converting jacobian to python") - jac = pybamm.EvaluatorPython(jac) - - else: - jac = None - - if model.convert_to_format == "python": - pybamm.logger.info("Converting algebraic to python") - concatenated_algebraic = pybamm.EvaluatorPython(concatenated_algebraic) - - return concatenated_algebraic, jac + def jac(y): + return model.jacobian_eval(t, y, inputs) + + else: + jac = None + + # Evaluate algebraic with new t and previous y0, if it's already close + # enough then keep it + if np.all(abs(algebraic(t, y0, inputs)) < self.tol): + pybamm.logger.debug("Keeping same solution at t={}".format(t)) + y[:, idx] = y0 + # Otherwise calculate new y0 + else: + sol = optimize.root( + root_fun, y0, method=self.method, tol=self.tol, jac=jac, + ) + + if sol.success and np.all(abs(sol.fun) < self.tol): + # update initial guess for the next iteration + y0 = sol.x + # update solution array + y[:, idx] = y0 + elif not sol.success: + raise pybamm.SolverError( + "Could not find acceptable solution: {}".format(sol.message) + ) + else: + raise pybamm.SolverError( + """ + Could not find acceptable solution: solver terminated + successfully, but maximum solution error ({}) + above tolerance ({}) + """.format( + np.max(sol.fun), self.tol + ) + ) + + # Return solution object (no events, so pass None to t_event, y_event) + return pybamm.Solution(t_eval, y, termination="success") diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 648edf6469..c3ed588f94 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -55,6 +55,7 @@ def __init__( # Defaults, can be overwritten by specific solver self.name = "Base solver" self.ode_solver = False + self.algebraic_solver = False @property def method(self): @@ -116,25 +117,40 @@ def set_up(self, model, inputs=None): Any input parameters to pass to the model when solving """ - inputs = inputs or {} - y0 = model.concatenated_initial_conditions.evaluate(0, None, inputs) - - # Set model timescale - model.timescale_eval = model.timescale.evaluate(u=inputs) # Check model.algebraic for ode solvers if self.ode_solver is True and len(model.algebraic) > 0: raise pybamm.SolverError( "Cannot use ODE solver '{}' to solve DAE model".format(self.name) ) + # Check model.rhs for algebraic solvers + if self.algebraic_solver is True and len(model.rhs) > 0: + raise pybamm.SolverError( + """Cannot use algebraic solver to solve model with time derivatives""" + ) + + inputs = inputs or {} + y0 = model.concatenated_initial_conditions.evaluate(0, None, inputs=inputs) + + # Set model timescale + model.timescale_eval = model.timescale.evaluate(inputs=inputs) if self.ode_solver is True: self.root_method = None if ( - isinstance(self, pybamm.CasadiSolver) or self.root_method == "casadi" + isinstance(self, (pybamm.CasadiSolver, pybamm.CasadiAlgebraicSolver)) ) and model.convert_to_format != "casadi": pybamm.logger.warning( - f"Converting {model.name} to CasADi for solving with CasADi solver" + "Converting {} to CasADi for solving with CasADi solver".format( + model.name + ) + ) + model.convert_to_format = "casadi" + if self.root_method == "casadi" and model.convert_to_format != "casadi": + pybamm.logger.warning( + "Converting {} to CasADi for calculating ICs with CasADi".format( + model.name + ) ) model.convert_to_format = "casadi" @@ -148,19 +164,20 @@ def set_up(self, model, inputs=None): # Convert model attributes to casadi t_casadi = casadi.MX.sym("t") y_diff = casadi.MX.sym( - "y_diff", len(model.concatenated_rhs.evaluate(0, y0, inputs)) + "y_diff", len(model.concatenated_rhs.evaluate(0, y0, inputs=inputs)) ) y_alg = casadi.MX.sym( - "y_alg", len(model.concatenated_algebraic.evaluate(0, y0, inputs)) + "y_alg", + len(model.concatenated_algebraic.evaluate(0, y0, inputs=inputs)), ) y_casadi = casadi.vertcat(y_diff, y_alg) - u_casadi = {} + p_casadi = {} for name, value in inputs.items(): if isinstance(value, numbers.Number): - u_casadi[name] = casadi.MX.sym(name) + p_casadi[name] = casadi.MX.sym(name) else: - u_casadi[name] = casadi.MX.sym(name, value.shape[0]) - u_casadi_stacked = casadi.vertcat(*[u for u in u_casadi.values()]) + p_casadi[name] = casadi.MX.sym(name, value.shape[0]) + p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()]) def process(func, name, use_jacobian=None): def report(string): @@ -194,26 +211,24 @@ def report(string): else: # Process with CasADi report(f"Converting {name} to CasADi") - func = func.to_casadi(t_casadi, y_casadi, u_casadi) + func = func.to_casadi(t_casadi, y_casadi, inputs=p_casadi) if use_jacobian: report(f"Calculating jacobian for {name} using CasADi") jac_casadi = casadi.jacobian(func, y_casadi) jac = casadi.Function( - name, [t_casadi, y_casadi, u_casadi_stacked], [jac_casadi] + name, [t_casadi, y_casadi, p_casadi_stacked], [jac_casadi] ) else: jac = None func = casadi.Function( - name, [t_casadi, y_casadi, u_casadi_stacked], [func] + name, [t_casadi, y_casadi, p_casadi_stacked], [func] ) if name == "residuals": func_call = Residuals(func, name, model) else: func_call = SolverCallable(func, name, model) - func_call.set_inputs(inputs) if jac is not None: jac_call = SolverCallable(jac, name + "_jac", model) - jac_call.set_inputs(inputs) else: jac_call = None return func, func_call, jac_call @@ -229,22 +244,31 @@ def report(string): model.concatenated_algebraic.pre_order(), ): if isinstance(symbol, pybamm.Heaviside): + found_t = False # Dimensionless if symbol.right.id == pybamm.t.id: expr = symbol.left + found_t = True elif symbol.left.id == pybamm.t.id: expr = symbol.right + found_t = True # Dimensional elif symbol.right.id == (pybamm.t * model.timescale).id: expr = symbol.left.new_copy() / symbol.right.right.new_copy() + found_t = True elif symbol.left.id == (pybamm.t * model.timescale).id: expr = symbol.right.new_copy() / symbol.left.right.new_copy() - - model.events.append( - pybamm.Event( - str(symbol), expr.new_copy(), pybamm.EventType.DISCONTINUITY + found_t = True + + # Update the events if the heaviside function depended on t + if found_t: + model.events.append( + pybamm.Event( + str(symbol), + expr.new_copy(), + pybamm.EventType.DISCONTINUITY, + ) ) - ) # Process rhs, algebraic and event expressions rhs, rhs_eval, jac_rhs = process(model.concatenated_rhs, "RHS") @@ -276,50 +300,46 @@ def report(string): # Note: when we pass to casadi the ode part of the problem must be in explicit # form so we pre-multiply by the inverse of the mass matrix if self.root_method == "casadi" or isinstance(self, pybamm.CasadiSolver): - mass_matrix_inv = casadi.MX(model.mass_matrix_inv.entries) - explicit_rhs = mass_matrix_inv @ rhs(t_casadi, y_casadi, u_casadi_stacked) - model.casadi_rhs = casadi.Function( - "rhs", [t_casadi, y_casadi, u_casadi_stacked], [explicit_rhs] - ) + # can use DAE solver to solve model with algebraic equations only + if len(model.rhs) > 0: + mass_matrix_inv = casadi.MX(model.mass_matrix_inv.entries) + explicit_rhs = mass_matrix_inv @ rhs( + t_casadi, y_casadi, p_casadi_stacked + ) + model.casadi_rhs = casadi.Function( + "rhs", [t_casadi, y_casadi, p_casadi_stacked], [explicit_rhs] + ) model.casadi_algebraic = algebraic - # Calculate consistent initial conditions for the algebraic equations - if len(model.algebraic) > 0: - all_states = pybamm.NumpyConcatenation( - model.concatenated_rhs, model.concatenated_algebraic - ) - # Process again, uses caching so should be quick - residuals, residuals_eval, jacobian_eval = process(all_states, "residuals") - model.residuals_eval = residuals_eval - model.jacobian_eval = jacobian_eval - y0_guess = y0.flatten() - model.y0 = self.calculate_consistent_state(model, 0, y0_guess, inputs) - else: + if self.algebraic_solver is True: + # we don't calculate consistent initial conditions + # for an algebraic solver as this will be the job of the algebraic solver + model.residuals_eval = Residuals(algebraic, "residuals", model) + model.jacobian_eval = jac_algebraic + model.y0 = y0.flatten() + elif len(model.algebraic) == 0: # can use DAE solver to solve ODE model + # - no initial condition initialization needed model.residuals_eval = Residuals(rhs, "residuals", model) model.jacobian_eval = jac_rhs model.y0 = y0.flatten() + # Calculate consistent initial conditions for the algebraic equations + else: + if len(model.rhs) > 0: + all_states = pybamm.NumpyConcatenation( + model.concatenated_rhs, model.concatenated_algebraic + ) + # Process again, uses caching so should be quick + residuals_eval, jacobian_eval = process(all_states, "residuals")[1:] + model.residuals_eval = residuals_eval + model.jacobian_eval = jacobian_eval + else: + model.residuals_eval = Residuals(algebraic, "residuals", model) + model.jacobian_eval = jac_algebraic + y0_guess = y0.flatten() + model.y0 = self.calculate_consistent_state(model, 0, y0_guess, inputs) pybamm.logger.info("Finish solver set-up") - def set_inputs(self, model, ext_and_inputs): - """ - Set values that are controlled externally, such as external variables and input - parameters - - Parameters - ---------- - ext_and_inputs : dict - Any external variables or input parameters to pass to the model when solving - - """ - model.rhs_eval.set_inputs(ext_and_inputs) - model.algebraic_eval.set_inputs(ext_and_inputs) - model.residuals_eval.set_inputs(ext_and_inputs) - for evnt in model.terminate_events_eval: - evnt.set_inputs(ext_and_inputs) - if model.jacobian_eval: - model.jacobian_eval.set_inputs(ext_and_inputs) - def calculate_consistent_state(self, model, time=0, y0_guess=None, inputs=None): """ Calculate consistent state for the algebraic equations through @@ -346,40 +366,43 @@ def calculate_consistent_state(self, model, time=0, y0_guess=None, inputs=None): if y0_guess is None: y0_guess = model.concatenated_initial_conditions.flatten() + inputs = inputs or {} + if model.convert_to_format == "casadi": + inputs = casadi.vertcat(*[x for x in inputs.values()]) + # Split y0_guess into differential and algebraic - len_rhs = model.rhs_eval(time, y0_guess).shape[0] + len_rhs = model.rhs_eval(time, y0_guess, inputs).shape[0] y0_diff, y0_alg_guess = np.split(y0_guess, [len_rhs]) - inputs = inputs or {} # Solve using casadi or scipy if self.root_method == "casadi": # Set up - u_stacked = casadi.vertcat(*[x for x in inputs.values()]) - u = casadi.MX.sym("u", u_stacked.shape[0]) + p = casadi.MX.sym("p", inputs.shape[0]) y_alg = casadi.MX.sym("y_alg", y0_alg_guess.shape[0]) y = casadi.vertcat(y0_diff, y_alg) - alg_root = model.casadi_algebraic(time, y, u) + alg_root = model.casadi_algebraic(time, y, p) # Solve - # set error_on_fail to False and just check the final output is small - # enough roots = casadi.rootfinder( "roots", "newton", - dict(x=y_alg, p=u, g=alg_root), + dict(x=y_alg, p=p, g=alg_root), {"abstol": self.root_tol}, ) try: - y0_alg = roots(y0_alg_guess, u_stacked).full().flatten() + y0_alg = roots(y0_alg_guess, inputs).full().flatten() success = True message = None # Check final output fun = model.casadi_algebraic( - time, casadi.vertcat(y0_diff, y0_alg), u_stacked + time, casadi.vertcat(y0_diff, y0_alg), inputs ) + abs_fun = casadi.fabs(fun) + max_fun = casadi.mmax(fun) except RuntimeError as err: success = False message = err.args[0] - fun = None + abs_fun = None + max_fun = None else: algebraic = model.algebraic_eval jac = model.jac_algebraic_eval @@ -387,7 +410,7 @@ def calculate_consistent_state(self, model, time=0, y0_guess=None, inputs=None): def root_fun(y0_alg): "Evaluates algebraic using y0_diff (fixed) and y0_alg (changed by algo)" y0 = np.concatenate([y0_diff, y0_alg]) - out = algebraic(time, y0) + out = algebraic(time, y0, inputs) pybamm.logger.debug( "Evaluating algebraic equations at t={}, L2-norm is {}".format( time * model.timescale, np.linalg.norm(out) @@ -396,14 +419,14 @@ def root_fun(y0_alg): return out if jac: - if issparse(jac(0, y0_guess)): + if issparse(jac(0, y0_guess, inputs)): def jac_fn(y0_alg): """ Evaluates jacobian using y0_diff (fixed) and y0_alg (varying) """ y0 = np.concatenate([y0_diff, y0_alg]) - return jac(0, y0)[:, len_rhs:].toarray() + return jac(0, y0, inputs)[:, len_rhs:].toarray() else: @@ -412,7 +435,7 @@ def jac_fn(y0_alg): Evaluates jacobian using y0_diff (fixed) and y0_alg (varying) """ y0 = np.concatenate([y0_diff, y0_alg]) - return jac(0, y0)[:, len_rhs:] + return jac(0, y0, inputs)[:, len_rhs:] else: jac_fn = None @@ -430,9 +453,11 @@ def jac_fn(y0_alg): y0_alg = sol.x success = sol.success fun = sol.fun + abs_fun = np.abs(fun) + max_fun = np.max(fun) message = sol.message - if success and np.all(fun < self.root_tol * len(y0_alg)): + if success and np.all(abs_fun < self.root_tol): # Return full set of consistent initial conditions (y0_diff unchanged) y0_consistent = np.concatenate([y0_diff, y0_alg]) pybamm.logger.info("Finish calculating consistent initial conditions") @@ -447,11 +472,11 @@ def jac_fn(y0_alg): Could not find consistent initial conditions: solver terminated successfully, but maximum solution error ({}) above tolerance ({}) """.format( - np.max(fun), self.root_tol * len(y0_alg) + max_fun, self.root_tol ) ) - def solve(self, model, t_eval, external_variables=None, inputs=None): + def solve(self, model, t_eval=None, external_variables=None, inputs=None): """ Execute the solver setup and calculate the solution of the model at specified times. @@ -472,14 +497,26 @@ def solve(self, model, t_eval, external_variables=None, inputs=None): Raises ------ :class:`pybamm.ModelError` - If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) + If an empty model is passed (`model.rhs = {}` and `model.algebraic={}` and + `model.variables = {}`) """ pybamm.logger.info("Start solving {} with {}".format(model.name, self.name)) # Make sure model isn't empty if len(model.rhs) == 0 and len(model.algebraic) == 0: - raise pybamm.ModelError("Cannot solve empty model") + if not isinstance(self, pybamm.DummySolver): + raise pybamm.ModelError( + "Cannot solve empty model, use `pybamm.DummySolver` instead" + ) + + # t_eval can only be None if the solver is an algebraic solver. In that case + # set it to 0 + if t_eval is None: + if self.algebraic_solver is True: + t_eval = np.array([0]) + else: + raise ValueError("t_eval cannot be None") # Make sure t_eval is monotonic if (np.diff(t_eval) < 0).any(): @@ -495,14 +532,6 @@ def solve(self, model, t_eval, external_variables=None, inputs=None): inputs = inputs or {} ext_and_inputs = {**external_variables, **inputs} - # Raise warning if t_eval looks like it was supposed to be dimensionless - # already - if t_eval[-1] < 0.5: - raise pybamm.SolverError( - """It looks like t_eval might be dimensionless. - t_eval should now be provided in seconds""" - ) - # Set up (if not done already) if model not in self.models_set_up: self.set_up(model, ext_and_inputs) @@ -513,12 +542,10 @@ def solve(self, model, t_eval, external_variables=None, inputs=None): # Non-dimensionalise time t_eval_dimensionless = t_eval / model.timescale_eval # Solve - # Set inputs and external - self.set_inputs(model, ext_and_inputs) # Calculate discontinuities discontinuities = [ - event.expression.evaluate(u=inputs) + event.expression.evaluate(inputs=inputs) for event in model.discontinuity_events_eval ] @@ -662,7 +689,8 @@ def step( Raises ------ :class:`pybamm.ModelError` - If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) + If an empty model is passed (`model.rhs = {}` and `model.algebraic = {}` and + `model.variables = {}`) """ @@ -676,7 +704,10 @@ def step( # Make sure model isn't empty if len(model.rhs) == 0 and len(model.algebraic) == 0: - raise pybamm.ModelError("Cannot step empty model") + if not isinstance(self, pybamm.DummySolver): + raise pybamm.ModelError( + "Cannot step empty model, use `pybamm.DummySolver` instead" + ) # Set timer timer = pybamm.Timer() @@ -705,7 +736,6 @@ def step( # Step t_eval = np.linspace(t, t + dt_dimensionless, npts) # Set inputs and external - self.set_inputs(model, ext_and_inputs) pybamm.logger.info("Calling solver") timer.reset() @@ -767,7 +797,7 @@ def get_termination_reason(self, solution, events): event.expression.evaluate( solution.t_event, solution.y_event, - {k: v[-1] for k, v in solution.inputs.items()}, + inputs={k: v[-1] for k, v in solution.inputs.items()}, ) ) termination_event = min(final_event_values, key=final_event_values.get) @@ -783,22 +813,13 @@ def __init__(self, function, name, model): self._function = function if isinstance(function, casadi.Function): self.form = "casadi" - self.inputs = casadi.DM() else: self.form = "python" - self.inputs = {} self.name = name self.model = model - - def set_inputs(self, inputs): - "Set inputs" - if self.form == "python": - self.inputs = inputs - elif self.form == "casadi": - self.inputs = casadi.vertcat(*[x for x in inputs.values()]) self.timescale = self.model.timescale_eval - def __call__(self, t, y): + def __call__(self, t, y, inputs): y = y[:, np.newaxis] if self.name in ["RHS", "algebraic", "residuals", "event"]: pybamm.logger.debug( @@ -806,19 +827,20 @@ def __call__(self, t, y): self.name, self.model.name, t * self.timescale ) ) - return self.function(t, y).flatten() + return self.function(t, y, inputs).flatten() else: - return self.function(t, y) + return self.function(t, y, inputs) - def function(self, t, y): + def function(self, t, y, inputs): if self.form == "casadi": + states_eval = self._function(t, y, inputs) if self.name in ["RHS", "algebraic", "residuals", "event"]: - return self._function(t, y, self.inputs).full() + return states_eval.full() else: # keep jacobians sparse - return self._function(t, y, self.inputs) + return states_eval else: - return self._function(t, y, self.inputs, known_evals={})[0] + return self._function(t, y, inputs=inputs, known_evals={})[0] class Residuals(SolverCallable): @@ -826,8 +848,9 @@ class Residuals(SolverCallable): def __init__(self, function, name, model): super().__init__(function, name, model) - self.mass_matrix = model.mass_matrix.entries + if model.mass_matrix is not None: + self.mass_matrix = model.mass_matrix.entries - def __call__(self, t, y, ydot): - states_eval = super().__call__(t, y) + def __call__(self, t, y, ydot, inputs): + states_eval = super().__call__(t, y, inputs) return states_eval - self.mass_matrix @ ydot diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/pybamm/solvers/casadi_algebraic_solver.py new file mode 100644 index 0000000000..8506f3a15e --- /dev/null +++ b/pybamm/solvers/casadi_algebraic_solver.py @@ -0,0 +1,115 @@ +# +# Casadi algebraic solver class +# +import casadi +import pybamm +import numpy as np + + +class CasadiAlgebraicSolver(pybamm.BaseSolver): + """Solve a discretised model which contains only (time independent) algebraic + equations using CasADi's root finding algorithm. + Note: this solver could be extended for quasi-static models, or models in + which the time derivative is manually discretised and results in a (possibly + nonlinear) algebaric system at each time level. + + Parameters + ---------- + tol : float, optional + The tolerance for the solver (default is 1e-6). + """ + + def __init__(self, method="lm", tol=1e-6, **extra_options): + super().__init__() + self.tol = tol + self.name = "CasADi algebraic solver" + self.algebraic_solver = True + self.extra_options = extra_options + pybamm.citations.register("Andersson2019") + + @property + def tol(self): + return self._tol + + @tol.setter + def tol(self, value): + self._tol = value + + def _integrate(self, model, t_eval, inputs=None): + """ + Calculate the solution of the algebraic equations through root-finding + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. + t_eval : :class:`numpy.array`, size (k,) + The times at which to compute the solution + inputs : dict, optional + Any input parameters to pass to the model when solving + """ + y0 = model.y0 + + y = np.empty((len(y0), len(t_eval))) + + # Set up + inputs = casadi.vertcat(*[x for x in inputs.values()]) + t_sym = casadi.MX.sym("t") + y_sym = casadi.MX.sym("y_alg", y0.shape[0]) + p_sym = casadi.MX.sym("p", inputs.shape[0]) + + t_p_sym = casadi.vertcat(t_sym, p_sym) + alg = model.casadi_algebraic(t_sym, y_sym, p_sym) + + # Set up rootfinder + roots = casadi.rootfinder( + "roots", + "newton", + dict(x=y_sym, p=t_p_sym, g=alg), + {**self.extra_options, "abstol": self.tol}, + ) + for idx, t in enumerate(t_eval): + # Evaluate algebraic with new t and previous y0, if it's already close + # enough then keep it + if np.all(abs(model.algebraic_eval(t, y0, inputs)) < self.tol): + pybamm.logger.debug( + "Keeping same solution at t={}".format(t * model.timescale_eval) + ) + y[:, idx] = y0 + # Otherwise calculate new y0 + else: + t_inputs = casadi.vertcat(t, inputs) + # Solve + try: + y_sol = roots(y0, t_inputs).full().flatten() + success = True + message = None + # Check final output + fun = model.casadi_algebraic(t, y_sol, inputs) + except RuntimeError as err: + success = False + message = err.args[0] + fun = None + + if success and np.all(casadi.fabs(fun) < self.tol): + # update initial guess for the next iteration + y0 = y_sol + # update solution array + y[:, idx] = y_sol + elif not success: + raise pybamm.SolverError( + "Could not find acceptable solution: {}".format(message) + ) + else: + raise pybamm.SolverError( + """ + Could not find acceptable solution: solver terminated + successfully, but maximum solution error ({}) + above tolerance ({}) + """.format( + casadi.mmax(fun), self.tol + ) + ) + + # Return solution object (no events, so pass None to t_event, y_event) + return pybamm.Solution(t_eval, y, termination="success") diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 1029a8560e..dd0bf0f352 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -88,7 +88,16 @@ def _integrate(self, model, t_eval, inputs=None): Any external variables or input parameters to pass to the model when solving """ inputs = inputs or {} - + # convert inputs to casadi format + inputs = casadi.vertcat(*[x for x in inputs.values()]) + + if len(model.rhs) == 0: + # casadi solver won't allow solving algebraic model so we have to raise an + # error here + raise pybamm.SolverError( + "Cannot use CasadiSolver to solve algebraic model, " + "use CasadiAlgebraicSolver instead" + ) if self.mode == "fast": integrator = self.get_integrator(model, t_eval, inputs) solution = self._run_integrator(integrator, model, model.y0, inputs, t_eval) @@ -105,7 +114,10 @@ def _integrate(self, model, t_eval, inputs=None): t = t_eval[0] init_event_signs = np.sign( np.concatenate( - [event(t, model.y0) for event in model.terminate_events_eval] + [ + event(t, model.y0, inputs) + for event in model.terminate_events_eval + ] ) ) pybamm.logger.info("Start solving {} with {}".format(model.name, self.name)) @@ -146,7 +158,7 @@ def _integrate(self, model, t_eval, inputs=None): new_event_signs = np.sign( np.concatenate( [ - event(t, current_step_sol.y[:, -1]) + event(t, current_step_sol.y[:, -1], inputs) for event in model.terminate_events_eval ] ) @@ -175,7 +187,6 @@ def get_integrator(self, model, t_eval, inputs): y0 = model.y0 rhs = model.casadi_rhs algebraic = model.casadi_algebraic - u_stacked = casadi.vertcat(*[x for x in inputs.values()]) options = { "grid": t_eval, @@ -187,22 +198,22 @@ def get_integrator(self, model, t_eval, inputs): # set up and solve t = casadi.MX.sym("t") - u = casadi.MX.sym("u", u_stacked.shape[0]) - y_diff = casadi.MX.sym("y_diff", rhs(t_eval[0], y0, u).shape[0]) - problem = {"t": t, "x": y_diff, "p": u} - if algebraic(t_eval[0], y0, u).is_empty(): + p = casadi.MX.sym("p", inputs.shape[0]) + y_diff = casadi.MX.sym("y_diff", rhs(t_eval[0], y0, p).shape[0]) + problem = {"t": t, "x": y_diff, "p": p} + if algebraic(t_eval[0], y0, p).is_empty(): method = "cvodes" - problem.update({"ode": rhs(t, y_diff, u)}) + problem.update({"ode": rhs(t, y_diff, p)}) else: options["calc_ic"] = True method = "idas" - y_alg = casadi.MX.sym("y_alg", algebraic(t_eval[0], y0, u).shape[0]) + y_alg = casadi.MX.sym("y_alg", algebraic(t_eval[0], y0, p).shape[0]) y_full = casadi.vertcat(y_diff, y_alg) problem.update( { "z": y_alg, - "ode": rhs(t, y_full, u), - "alg": algebraic(t, y_full, u), + "ode": rhs(t, y_full, p), + "alg": algebraic(t, y_full, p), } ) self.problems[model] = problem @@ -217,12 +228,11 @@ def get_integrator(self, model, t_eval, inputs): ) def _run_integrator(self, integrator, model, y0, inputs, t_eval): - rhs_size = model.rhs_eval(t_eval[0], y0).shape[0] + rhs_size = model.rhs_eval(t_eval[0], y0, inputs).shape[0] y0_diff, y0_alg = np.split(y0, [rhs_size]) try: # Try solving - u_stacked = casadi.vertcat(*[x for x in inputs.values()]) - sol = integrator(x0=y0_diff, z0=y0_alg, p=u_stacked, **self.extra_options) + sol = integrator(x0=y0_diff, z0=y0_alg, p=inputs, **self.extra_options) y_values = np.concatenate([sol["xf"].full(), sol["zf"].full()]) return pybamm.Solution(t_eval, y_values) except RuntimeError as e: diff --git a/pybamm/solvers/dummy_solver.py b/pybamm/solvers/dummy_solver.py new file mode 100644 index 0000000000..483bba8a77 --- /dev/null +++ b/pybamm/solvers/dummy_solver.py @@ -0,0 +1,36 @@ +# +# Dummy solver class, for empty models +# +import pybamm +import numpy as np + + +class DummySolver(pybamm.BaseSolver): + """Dummy solver class for empty models. """ + + def __init__(self): + super().__init__() + self.name = "Dummy solver" + + def _integrate(self, model, t_eval, inputs=None): + """ + Solve an empty model. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. + t_eval : :class:`numpy.array`, size (k,) + The times at which to compute the solution + inputs : dict, optional + Any input parameters to pass to the model when solving + + Returns + ------- + object + An object containing the times and values of the solution, as well as + various diagnostic messages. + + """ + y_sol = np.zeros((1, t_eval.size)) + return pybamm.Solution(t_eval, y_sol, termination="final time") diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index 138524b5ad..8909a78bbe 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -1,6 +1,7 @@ # # Solver class using sundials with the KLU sparse linear solver # +import casadi import pybamm import numpy as np import scipy.sparse as sparse @@ -146,6 +147,8 @@ def _integrate(self, model, t_eval, inputs=None): t_eval : numeric type The times at which to compute the solution """ + if model.rhs_eval.form == "casadi": + inputs = casadi.vertcat(*[x for x in inputs.values()]) if model.jacobian_eval is None: raise pybamm.SolverError("KLU requires the Jacobian to be provided") @@ -161,17 +164,17 @@ def _integrate(self, model, t_eval, inputs=None): mass_matrix = model.mass_matrix.entries if model.jacobian_eval: - jac_y0_t0 = model.jacobian_eval(t_eval[0], y0) + jac_y0_t0 = model.jacobian_eval(t_eval[0], y0, inputs) if sparse.issparse(jac_y0_t0): def jacfn(t, y, cj): - j = model.jacobian_eval(t, y) - cj * mass_matrix + j = model.jacobian_eval(t, y, inputs) - cj * mass_matrix return j else: def jacfn(t, y, cj): - jac_eval = model.jacobian_eval(t, y) - cj * mass_matrix + jac_eval = model.jacobian_eval(t, y, inputs) - cj * mass_matrix return sparse.csr_matrix(jac_eval) class SundialsJacobian: @@ -207,12 +210,14 @@ def get_jac_col_ptrs(self): def rootfn(t, y): return_root = np.ones((num_of_events,)) - return_root[:] = [event(t, y) for event in model.terminate_events_eval] + return_root[:] = [ + event(t, y, inputs) for event in model.terminate_events_eval + ] return return_root # get ids of rhs and algebraic variables - rhs_ids = np.ones(model.rhs_eval(0, y0).shape) + rhs_ids = np.ones(model.rhs_eval(0, y0, inputs).shape) alg_ids = np.zeros(len(y0) - len(rhs_ids)) ids = np.concatenate((rhs_ids, alg_ids)) @@ -221,7 +226,7 @@ def rootfn(t, y): t_eval, y0, ydot0, - model.residuals_eval, + lambda t, y, ydot: model.residuals_eval(t, y, ydot, inputs), jac_class.jac_res, jac_class.get_jac_data, jac_class.get_jac_row_vals, diff --git a/pybamm/solvers/scikits_dae_solver.py b/pybamm/solvers/scikits_dae_solver.py index 65f06caf07..77adbe3dd2 100644 --- a/pybamm/solvers/scikits_dae_solver.py +++ b/pybamm/solvers/scikits_dae_solver.py @@ -1,6 +1,7 @@ # # Solver class using Scipy's adaptive time stepper # +import casadi import pybamm import numpy as np @@ -68,6 +69,9 @@ def _integrate(self, model, t_eval, inputs=None): Any input parameters to pass to the model when solving """ + if model.convert_to_format == "casadi": + inputs = casadi.vertcat(*[x for x in inputs.values()]) + residuals = model.residuals_eval y0 = model.y0 events = model.terminate_events_eval @@ -75,10 +79,10 @@ def _integrate(self, model, t_eval, inputs=None): mass_matrix = model.mass_matrix.entries def eqsres(t, y, ydot, return_residuals): - return_residuals[:] = residuals(t, y, ydot) + return_residuals[:] = residuals(t, y, ydot, inputs) def rootfn(t, y, ydot, return_root): - return_root[:] = [event(t, y) for event in events] + return_root[:] = [event(t, y, inputs) for event in events] extra_options = { "old_api": False, @@ -88,17 +92,17 @@ def rootfn(t, y, ydot, return_root): } if jacobian: - jac_y0_t0 = jacobian(t_eval[0], y0) + jac_y0_t0 = jacobian(t_eval[0], y0, inputs) if sparse.issparse(jac_y0_t0): def jacfn(t, y, ydot, residuals, cj, J): - jac_eval = jacobian(t, y) - cj * mass_matrix + jac_eval = jacobian(t, y, inputs) - cj * mass_matrix J[:][:] = jac_eval.toarray() else: def jacfn(t, y, ydot, residuals, cj, J): - jac_eval = jacobian(t, y) - cj * mass_matrix + jac_eval = jacobian(t, y, inputs) - cj * mass_matrix J[:][:] = jac_eval extra_options.update({"jacfn": jacfn}) diff --git a/pybamm/solvers/scikits_ode_solver.py b/pybamm/solvers/scikits_ode_solver.py index b8bf57854c..21536420f4 100644 --- a/pybamm/solvers/scikits_ode_solver.py +++ b/pybamm/solvers/scikits_ode_solver.py @@ -1,6 +1,7 @@ # # Solver class using Scipy's adaptive time stepper # +import casadi import pybamm import numpy as np @@ -61,23 +62,26 @@ def _integrate(self, model, t_eval, inputs=None): Any input parameters to pass to the model when solving """ + if model.rhs_eval.form == "casadi": + inputs = casadi.vertcat(*[x for x in inputs.values()]) + derivs = model.rhs_eval y0 = model.y0 events = model.terminate_events_eval jacobian = model.jacobian_eval def eqsydot(t, y, return_ydot): - return_ydot[:] = derivs(t, y) + return_ydot[:] = derivs(t, y, inputs) def rootfn(t, y, return_root): - return_root[:] = [event(t, y) for event in events] + return_root[:] = [event(t, y, inputs) for event in events] if jacobian: - jac_y0_t0 = jacobian(t_eval[0], y0) + jac_y0_t0 = jacobian(t_eval[0], y0, inputs) if sparse.issparse(jac_y0_t0): def jacfn(t, y, fy, J): - J[:][:] = jacobian(t, y).toarray() + J[:][:] = jacobian(t, y, inputs).toarray() def jac_times_vecfn(v, Jv, t, y, userdata): Jv[:] = userdata._jac_eval * v @@ -86,14 +90,14 @@ def jac_times_vecfn(v, Jv, t, y, userdata): else: def jacfn(t, y, fy, J): - J[:][:] = jacobian(t, y) + J[:][:] = jacobian(t, y, inputs) def jac_times_vecfn(v, Jv, t, y, userdata): Jv[:] = np.matmul(userdata._jac_eval, v) return 0 def jac_times_setupfn(t, y, fy, userdata): - userdata._jac_eval = jacobian(t, y) + userdata._jac_eval = jacobian(t, y, inputs) return 0 extra_options = { diff --git a/pybamm/solvers/scipy_solver.py b/pybamm/solvers/scipy_solver.py index 980332183b..45389f146c 100644 --- a/pybamm/solvers/scipy_solver.py +++ b/pybamm/solvers/scipy_solver.py @@ -1,6 +1,7 @@ # # Solver class using Scipy's adaptive time stepper # +import casadi import pybamm import scipy.integrate as it @@ -46,22 +47,34 @@ def _integrate(self, model, t_eval, inputs=None): various diagnostic messages. """ + if model.convert_to_format == "casadi": + inputs = casadi.vertcat(*[x for x in inputs.values()]) + extra_options = {"rtol": self.rtol, "atol": self.atol} # check for user-supplied Jacobian implicit_methods = ["Radau", "BDF", "LSODA"] if np.any([self.method in implicit_methods]): if model.jacobian_eval: - extra_options.update({"jac": model.jacobian_eval}) + extra_options.update( + {"jac": lambda t, y: model.jacobian_eval(t, y, inputs)} + ) # make events terminal so that the solver stops when they are reached if model.terminate_events_eval: - for event in model.terminate_events_eval: - event.terminal = True - extra_options.update({"events": model.terminate_events_eval}) + + def event_wrapper(event): + def event_fn(t, y): + return event(t, y, inputs) + + event_fn.terminal = True + return event_fn + + events = [event_wrapper(event) for event in model.terminate_events_eval] + extra_options.update({"events": events}) sol = it.solve_ivp( - model.rhs_eval, + lambda t, y: model.rhs_eval(t, y, inputs), (t_eval[0], t_eval[-1]), model.y0, t_eval=t_eval, diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 9e4855ca66..8734ad8a54 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -233,10 +233,10 @@ def save_data(self, filename, variables=None, to_format="pickle"): savemat(filename, data) elif to_format == "csv": for name, var in data.items(): - if var.ndim == 2: + if var.ndim >= 2: raise ValueError( - "only 1D variables can be saved to csv, but '{}' is 2D".format( - name + "only 0D variables can be saved to csv, but '{}' is {}D".format( + name, var.ndim - 1 ) ) df = pd.DataFrame(data) @@ -331,4 +331,3 @@ def append(self, solution, start_index=1, create_sub_solutions=False): copy_this=solution, ) ) - diff --git a/pybamm/spatial_methods/finite_volume.py b/pybamm/spatial_methods/finite_volume.py index 03ce66a244..d748f55909 100644 --- a/pybamm/spatial_methods/finite_volume.py +++ b/pybamm/spatial_methods/finite_volume.py @@ -40,7 +40,7 @@ def build(self, mesh): # add npts_for_broadcast to mesh domains for this particular discretisation for dom in mesh.keys(): for i in range(len(mesh[dom])): - mesh[dom][i].npts_for_broadcast = mesh[dom][i].npts + mesh[dom][i].npts_for_broadcast_to_nodes = mesh[dom][i].npts def spatial_variable(self, symbol): """ @@ -57,9 +57,11 @@ def spatial_variable(self, symbol): :class:`pybamm.Vector` Contains the discretised spatial variable """ - # for finite volume we use the cell centres symbol_mesh = self.mesh.combine_submeshes(*symbol.domain) - entries = np.concatenate([mesh.nodes for mesh in symbol_mesh]) + if symbol.evaluates_on_edges(): + entries = np.concatenate([mesh.edges for mesh in symbol_mesh]) + else: + entries = np.concatenate([mesh.nodes for mesh in symbol_mesh]) return pybamm.Vector( entries, domain=symbol.domain, auxiliary_domains=symbol.auxiliary_domains ) diff --git a/pybamm/spatial_methods/scikit_finite_element.py b/pybamm/spatial_methods/scikit_finite_element.py index 48f01d3934..b384481e9f 100644 --- a/pybamm/spatial_methods/scikit_finite_element.py +++ b/pybamm/spatial_methods/scikit_finite_element.py @@ -36,7 +36,7 @@ def build(self, mesh): # add npts_for_broadcast to mesh domains for this particular discretisation for dom in mesh.keys(): for i in range(len(mesh[dom])): - mesh[dom][i].npts_for_broadcast = mesh[dom][i].npts + mesh[dom][i].npts_for_broadcast_to_nodes = mesh[dom][i].npts def spatial_variable(self, symbol): """ diff --git a/pybamm/spatial_methods/spatial_method.py b/pybamm/spatial_methods/spatial_method.py index 1976b806c0..371d50ebc4 100644 --- a/pybamm/spatial_methods/spatial_method.py +++ b/pybamm/spatial_methods/spatial_method.py @@ -38,7 +38,7 @@ def build(self, mesh): # add npts_for_broadcast to mesh domains for this particular discretisation for dom in mesh.keys(): for i in range(len(mesh[dom])): - mesh[dom][i].npts_for_broadcast = mesh[dom][i].npts + mesh[dom][i].npts_for_broadcast_to_nodes = mesh[dom][i].npts self._mesh = mesh @property @@ -62,7 +62,7 @@ def spatial_variable(self, symbol): Contains the discretised spatial variable """ symbol_mesh = self.mesh.combine_submeshes(*symbol.domain) - if symbol.name.endswith("_edge"): + if symbol.evaluates_on_edges(): entries = np.concatenate([mesh.edges for mesh in symbol_mesh]) else: entries = np.concatenate([mesh.nodes for mesh in symbol_mesh]) @@ -81,7 +81,8 @@ def broadcast(self, symbol, domain, auxiliary_domains, broadcast_type): domain : iterable of strings The domain to broadcast to broadcast_type : str - The type of broadcast, either: 'primary' or 'full' + The type of broadcast: 'primary to node', 'primary to edges', 'secondary to + nodes', 'secondary to edges', 'full to nodes' or 'full to edges' Returns ------- @@ -90,14 +91,24 @@ def broadcast(self, symbol, domain, auxiliary_domains, broadcast_type): """ primary_domain_size = sum( - self.mesh[dom][0].npts_for_broadcast for dom in domain + self.mesh[dom][0].npts_for_broadcast_to_nodes for dom in domain ) - + secondary_domain_size = sum( + self.mesh[dom][0].npts_for_broadcast_to_nodes + for dom in auxiliary_domains.get("secondary", []) + ) # returns empty list if auxiliary_domains doesn't have "secondary" key full_domain_size = sum( - subdom.npts_for_broadcast for dom in domain for subdom in self.mesh[dom] + subdom.npts_for_broadcast_to_nodes + for dom in domain + for subdom in self.mesh[dom] ) + if broadcast_type.endswith("to edges"): + # add one point to each domain for broadcasting to edges + primary_domain_size += 1 + secondary_domain_size += 1 + full_domain_size += 1 - if broadcast_type == "primary": + if broadcast_type.startswith("primary"): # Make copies of the child stacked on top of each other sub_vector = np.ones((primary_domain_size, 1)) if symbol.shape_for_testing == (): @@ -107,11 +118,7 @@ def broadcast(self, symbol, domain, auxiliary_domains, broadcast_type): matrix = csr_matrix(kron(eye(symbol.shape_for_testing[0]), sub_vector)) out = pybamm.Matrix(matrix) @ symbol out.domain = domain - elif broadcast_type == "secondary": - secondary_domain_size = sum( - self.mesh[dom][0].npts_for_broadcast - for dom in auxiliary_domains["secondary"] - ) + elif broadcast_type.startswith("secondary"): kron_size = full_domain_size // primary_domain_size # Symbol may be on edges so need to calculate size carefully symbol_primary_size = symbol.shape[0] // kron_size @@ -121,7 +128,7 @@ def broadcast(self, symbol, domain, auxiliary_domains, broadcast_type): # Repeat for secondary points matrix = csr_matrix(kron(eye(kron_size), sub_matrix)) out = pybamm.Matrix(matrix) @ symbol - elif broadcast_type == "full": + elif broadcast_type.startswith("full"): out = symbol * pybamm.Vector(np.ones(full_domain_size), domain=domain) out.auxiliary_domains = auxiliary_domains diff --git a/pybamm/util.py b/pybamm/util.py index 885d2421b3..f58e04280f 100644 --- a/pybamm/util.py +++ b/pybamm/util.py @@ -91,6 +91,37 @@ def __getitem__(self, key): best_matches = self.get_best_matches(key) raise KeyError(f"'{key}' not found. Best matches are {best_matches}") + def search(self, key, print_values=False): + """ + Search dictionary for keys containing 'key'. If print_values is True, then + both the keys and values will be printed. Otherwise just the values will + be printed. If no results are found, the best matches are printed. + """ + key = key.lower() + + # Sort the keys so results are stored in alphabetical order + keys = list(self.keys()) + keys.sort() + results = {} + + # Check if any of the dict keys contain the key we are searching for + for k in keys: + if key in k.lower(): + results[k] = self[k] + + if results == {}: + # If no results, return best matches + best_matches = self.get_best_matches(key) + print( + f"No results for search using '{key}'. Best matches are {best_matches}" + ) + elif print_values: + # Else print results, including dict items + print("\n".join("{}\t{}".format(k, v) for k, v in results.items())) + else: + # Just print keys + print("\n".join("{}".format(k) for k in results.keys())) + class Timer(object): """ diff --git a/pybamm/version b/pybamm/version index 6e91a93f84..bfa665d579 100644 --- a/pybamm/version +++ b/pybamm/version @@ -1 +1 @@ -0, 2, 0 +0, 2, 1 diff --git a/tests/integration/test_models/standard_output_comparison.py b/tests/integration/test_models/standard_output_comparison.py index b36ac1a7bc..e96a060669 100644 --- a/tests/integration/test_models/standard_output_comparison.py +++ b/tests/integration/test_models/standard_output_comparison.py @@ -68,9 +68,9 @@ def compare(self, var, tol=1e-2): var0 = model_variables[0] spatial_pts = {} - if var0.dimensions >= 2: + if var0.dimensions >= 1: spatial_pts[var0.first_dimension] = var0.first_dim_pts - if var0.dimensions >= 3: + if var0.dimensions >= 2: spatial_pts[var0.second_dimension] = var0.second_dim_pts # Calculate tolerance based on the value of var0 diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_composite.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_composite.py index b8f5f3fe3a..3e20babeb3 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_composite.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_composite.py @@ -63,6 +63,17 @@ def test_basic_processing_algebraic(self): modeltest = tests.StandardModelTest(model, parameter_values=param) modeltest.test_all() # solver=pybamm.CasadiSolver()) + # def test_thermal(self): + # options = {"thermal": "x-lumped"} + # model = pybamm.lead_acid.Composite(options) + # modeltest = tests.StandardModelTest(model) + # modeltest.test_all() + + # options = {"thermal": "x-full"} + # model = pybamm.lead_acid.Composite(options) + # modeltest = tests.StandardModelTest(model) + # modeltest.test_all() + class TestLeadAcidCompositeExtended(unittest.TestCase): def test_basic_processing(self): diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py index 54529ed5fc..61a6afe67f 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py @@ -85,6 +85,17 @@ def test_set_up(self): optimtest.set_up_model(simplify=False, to_python=False) optimtest.set_up_model(simplify=True, to_python=False) + def test_thermal(self): + options = {"thermal": "x-lumped"} + model = pybamm.lead_acid.Full(options) + modeltest = tests.StandardModelTest(model) + modeltest.test_all() + + options = {"thermal": "x-full"} + model = pybamm.lead_acid.Full(options) + modeltest = tests.StandardModelTest(model) + modeltest.test_all() + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_loqs.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_loqs.py index 368cf5e97c..7d049f2e6c 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_loqs.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_loqs.py @@ -55,6 +55,17 @@ def test_basic_processing_with_convection(self): modeltest = tests.StandardModelTest(model) modeltest.test_all() + def test_thermal(self): + options = {"thermal": "x-lumped"} + model = pybamm.lead_acid.LOQS(options) + modeltest = tests.StandardModelTest(model) + modeltest.test_all() + + options = {"thermal": "x-full"} + model = pybamm.lead_acid.LOQS(options) + modeltest = tests.StandardModelTest(model) + modeltest.test_all() + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/integration/test_models/test_submodels/test_external_circuit/test_function_control.py b/tests/integration/test_models/test_submodels/test_external_circuit/test_function_control.py index e424b0f763..c079d6b59d 100644 --- a/tests/integration/test_models/test_submodels/test_external_circuit/test_function_control.py +++ b/tests/integration/test_models/test_submodels/test_external_circuit/test_function_control.py @@ -67,7 +67,7 @@ def constant_voltage(variables): params = [model.default_parameter_values for model in models] # First model: 4.1V charge - params[0]["Voltage function [V]"] = 4.1 + params[0].update({"Voltage function [V]": 4.1}, check_already_exists=False) # set parameters and discretise models var = pybamm.standard_spatial_vars @@ -111,7 +111,7 @@ def constant_power(variables): params = [model.default_parameter_values for model in models] # First model: 4W charge - params[0]["Power function [W]"] = 4 + params[0].update({"Power function [W]": 4}, check_already_exists=False) # set parameters and discretise models for i, model in enumerate(models): diff --git a/tests/integration/test_quick_plot.py b/tests/integration/test_quick_plot.py deleted file mode 100644 index 9be762f7b0..0000000000 --- a/tests/integration/test_quick_plot.py +++ /dev/null @@ -1,89 +0,0 @@ -import pybamm -import unittest -import numpy as np - - -class TestQuickPlot(unittest.TestCase): - """ - Tests that QuickPlot is created correctly - """ - - def test_plot_lithium_ion(self): - spm = pybamm.lithium_ion.SPM() - spme = pybamm.lithium_ion.SPMe() - geometry = spm.default_geometry - param = spm.default_parameter_values - param.process_model(spm) - param.process_model(spme) - param.process_geometry(geometry) - mesh = pybamm.Mesh(geometry, spme.default_submesh_types, spme.default_var_pts) - disc_spm = pybamm.Discretisation(mesh, spm.default_spatial_methods) - disc_spme = pybamm.Discretisation(mesh, spme.default_spatial_methods) - disc_spm.process_model(spm) - disc_spme.process_model(spme) - t_eval = np.linspace(0, 3600, 100) - solution_spm = spm.default_solver.solve(spm, t_eval) - solution_spme = spme.default_solver.solve(spme, t_eval) - quick_plot = pybamm.QuickPlot([solution_spm, solution_spme]) - quick_plot.plot(0) - - # update the axis - new_axis = [0, 0.5, 0, 1] - quick_plot.axis.update({("Electrolyte concentration",): new_axis}) - self.assertEqual(quick_plot.axis[("Electrolyte concentration",)], new_axis) - - # and now reset them - quick_plot.reset_axis() - self.assertNotEqual(quick_plot.axis[("Electrolyte concentration",)], new_axis) - - # check dynamic plot loads - quick_plot.dynamic_plot(testing=True) - - quick_plot.update(0.01) - - # Test with different output variables - output_vars = [ - "Negative particle surface concentration", - "Electrolyte concentration", - "Positive particle surface concentration", - ] - quick_plot = pybamm.QuickPlot(solution_spm, output_vars) - self.assertEqual(len(quick_plot.axis), 3) - quick_plot.plot(0) - - # update the axis - new_axis = [0, 0.5, 0, 1] - quick_plot.axis.update({("Electrolyte concentration",): new_axis}) - self.assertEqual(quick_plot.axis[("Electrolyte concentration",)], new_axis) - - # and now reset them - quick_plot.reset_axis() - self.assertNotEqual(quick_plot.axis[("Electrolyte concentration",)], new_axis) - - # check dynamic plot loads - quick_plot.dynamic_plot(testing=True) - - quick_plot.update(0.01) - - def test_plot_lead_acid(self): - loqs = pybamm.lead_acid.LOQS() - geometry = loqs.default_geometry - param = loqs.default_parameter_values - param.process_model(loqs) - param.process_geometry(geometry) - mesh = pybamm.Mesh(geometry, loqs.default_submesh_types, loqs.default_var_pts) - disc_loqs = pybamm.Discretisation(mesh, loqs.default_spatial_methods) - disc_loqs.process_model(loqs) - t_eval = np.linspace(0, 3600, 100) - solution_loqs = loqs.default_solver.solve(loqs, t_eval) - - pybamm.QuickPlot(solution_loqs) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_solvers/test_external_variables.py b/tests/integration/test_solvers/test_external_variables.py index cd2a43abb9..03fbb82a63 100644 --- a/tests/integration/test_solvers/test_external_variables.py +++ b/tests/integration/test_solvers/test_external_variables.py @@ -49,8 +49,8 @@ def test_external_variables_SPMe(self): var = "Terminal voltage [V]" t = sim.solution.t[-1] y = sim.solution.y[:, -1] - u = external_variables - sim.built_model.variables[var].evaluate(t, y, u) + inputs = external_variables + sim.built_model.variables[var].evaluate(t, y, inputs=inputs) sim.solution[var](t) diff --git a/tests/integration/test_solvers/test_idaklu.py b/tests/integration/test_solvers/test_idaklu.py index 69aabb5861..7f0a1bd1fc 100644 --- a/tests/integration/test_solvers/test_idaklu.py +++ b/tests/integration/test_solvers/test_idaklu.py @@ -72,4 +72,3 @@ def test_changing_grid(self): debug = True pybamm.settings.debug_mode = True unittest.main() - diff --git a/tests/unit/test_citations.py b/tests/unit/test_citations.py index 0590120f23..39ed2da20c 100644 --- a/tests/unit/test_citations.py +++ b/tests/unit/test_citations.py @@ -101,6 +101,12 @@ def test_parameter_citations(self): pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Sulzer2019) self.assertIn("sulzer2019physical", citations._papers_to_cite) + citations._reset() + pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Ecker2015) + self.assertIn("ecker2015i", citations._papers_to_cite) + self.assertIn("ecker2015ii", citations._papers_to_cite) + self.assertIn("richardson2020", citations._papers_to_cite) + def test_solver_citations(self): # Test that solving each solver adds the right citations citations = pybamm.citations @@ -110,6 +116,11 @@ def test_solver_citations(self): pybamm.ScipySolver() self.assertIn("virtanen2020scipy", citations._papers_to_cite) + citations._reset() + self.assertNotIn("virtanen2020scipy", citations._papers_to_cite) + pybamm.AlgebraicSolver() + self.assertIn("virtanen2020scipy", citations._papers_to_cite) + if pybamm.have_scikits_odes(): citations._reset() self.assertNotIn("scikits-odes", citations._papers_to_cite) diff --git a/tests/unit/test_discretisations/test_discretisation.py b/tests/unit/test_discretisations/test_discretisation.py index d30b6f11f5..48a09b98a5 100644 --- a/tests/unit/test_discretisations/test_discretisation.py +++ b/tests/unit/test_discretisations/test_discretisation.py @@ -82,7 +82,7 @@ def test_adding_0D_external_variable(self): disc.process_model(model) self.assertIsInstance(model.variables["b"], pybamm.ExternalVariable) - self.assertEqual(model.variables["b"].evaluate(u={"b": np.array([1])}), 1) + self.assertEqual(model.variables["b"].evaluate(inputs={"b": np.array([1])}), 1) def test_adding_0D_external_variable_fail(self): model = pybamm.BaseModel() @@ -132,9 +132,11 @@ def test_adding_1D_external_variable(self): self.assertEqual(disc.y_slices[a.id][0], slice(0, 10, None)) + self.assertEqual(model.y_slices[a][0], slice(0, 10, None)) + b_test = np.ones((10, 1)) np.testing.assert_array_equal( - model.variables["b"].evaluate(u={"b": b_test}), b_test + model.variables["b"].evaluate(inputs={"b": b_test}), b_test ) # check that b is added to the boundary conditions @@ -199,13 +201,13 @@ def test_concatenation_external_variables(self): b_test = np.linspace(0, 1, 15)[:, np.newaxis] np.testing.assert_array_equal( - model.variables["b"].evaluate(u={"b": b_test}), b_test + model.variables["b"].evaluate(inputs={"b": b_test}), b_test ) np.testing.assert_array_equal( - model.variables["b1"].evaluate(u={"b": b_test}), b_test[:10] + model.variables["b1"].evaluate(inputs={"b": b_test}), b_test[:10] ) np.testing.assert_array_equal( - model.variables["b2"].evaluate(u={"b": b_test}), b_test[10:] + model.variables["b2"].evaluate(inputs={"b": b_test}), b_test[10:] ) # check that b is added to the boundary conditions @@ -329,6 +331,13 @@ def test_process_symbol_base(self): var_disc = disc.process_symbol(var) self.assertIsInstance(var_disc, pybamm.StateVector) self.assertEqual(var_disc.y_slices[0], disc.y_slices[var.id][0]) + + # variable dot + var_dot = pybamm.VariableDot("var'") + var_dot_disc = disc.process_symbol(var_dot) + self.assertIsInstance(var_dot_disc, pybamm.StateVectorDot) + self.assertEqual(var_dot_disc.y_slices[0], disc.y_slices[var.id][0]) + # scalar scal = pybamm.Scalar(5) scal_disc = disc.process_symbol(scal) @@ -693,6 +702,42 @@ def test_process_model_ode(self): with self.assertRaises(pybamm.ModelError): disc.process_model(model) + # test that any time derivatives of variables in rhs raises an + # error + model = pybamm.BaseModel() + model.rhs = {c: pybamm.div(N) + c.diff(pybamm.t), + T: pybamm.div(q), S: pybamm.div(p)} + model.initial_conditions = { + c: pybamm.Scalar(2), + T: pybamm.Scalar(5), + S: pybamm.Scalar(8), + } + model.boundary_conditions = { + c: {"left": (0, "Neumann"), "right": (0, "Neumann")}, + T: {"left": (0, "Neumann"), "right": (0, "Neumann")}, + S: {"left": (0, "Neumann"), "right": (0, "Neumann")}, + } + model.variables = {"ST": S * T} + with self.assertRaises(pybamm.ModelError): + disc.process_model(model) + + def test_process_model_fail(self): + # one equation + c = pybamm.Variable("c") + d = pybamm.Variable("d") + model = pybamm.BaseModel() + model.rhs = {c: -c} + model.initial_conditions = {c: pybamm.Scalar(3)} + model.variables = {"c": c, "d": d} + + disc = pybamm.Discretisation() + # turn debug mode off to not check well posedness + debug_mode = pybamm.settings.debug_mode + pybamm.settings.debug_mode = False + with self.assertRaisesRegex(pybamm.ModelError, "No key set for variable"): + disc.process_model(model) + pybamm.settings.debug_mode = debug_mode + def test_process_model_dae(self): # one rhs equation and one algebraic whole_cell = ["negative electrode", "separator", "positive electrode"] @@ -790,6 +835,20 @@ def test_process_model_dae(self): jacobian = expr.evaluate(0, y0, known_evals=known_evals)[0] np.testing.assert_array_equal(jacobian_actual, jacobian.toarray()) + # check that any time derivatives of variables in algebraic raises an + # error + model = pybamm.BaseModel() + model.rhs = {c: pybamm.div(N)} + model.algebraic = {d: d - 2 * c.diff(pybamm.t)} + model.initial_conditions = {d: pybamm.Scalar(6), c: pybamm.Scalar(3)} + model.boundary_conditions = { + c: {"left": (0, "Neumann"), "right": (0, "Neumann")} + } + model.variables = {"c": c, "N": N, "d": d} + + with self.assertRaises(pybamm.ModelError): + disc.process_model(model) + def test_process_model_concatenation(self): # concatenation of variables as the key cn = pybamm.Variable("c", domain=["negative electrode"]) @@ -874,7 +933,7 @@ def test_broadcast(self): # scalar broad = disc.process_symbol(pybamm.FullBroadcast(a, whole_cell, {})) np.testing.assert_array_equal( - broad.evaluate(u={"a": 7}), + broad.evaluate(inputs={"a": 7}), 7 * np.ones_like(combined_submesh[0].nodes[:, np.newaxis]), ) self.assertEqual(broad.domain, whole_cell) @@ -892,8 +951,16 @@ def test_broadcast(self): self.assertIsInstance(broad1_disc.children[0], pybamm.StateVector) self.assertIsInstance(broad1_disc.children[1], pybamm.Vector) + # broadcast to edges + broad_to_edges = pybamm.FullBroadcastToEdges(a, ["negative electrode"], None) + broad_to_edges_disc = disc.process_symbol(broad_to_edges) + np.testing.assert_array_equal( + broad_to_edges_disc.evaluate(inputs={"a": 7}), + 7 * np.ones_like(mesh["negative electrode"][0].edges[:, np.newaxis]), + ) + def test_broadcast_2D(self): - # broadcast in 2D --> Outer symbol + # broadcast in 2D --> MatrixMultiplication var = pybamm.Variable("var", ["current collector"]) disc = get_1p1d_discretisation_for_testing() mesh = disc.mesh @@ -914,6 +981,22 @@ def test_broadcast_2D(self): np.outer(y_test, np.ones(mesh["separator"][0].npts)).reshape(-1, 1), ) + # test broadcast to edges + broad_to_edges = pybamm.PrimaryBroadcastToEdges(var, "separator") + broad_to_edges_disc = disc.process_symbol(broad_to_edges) + self.assertIsInstance(broad_to_edges_disc, pybamm.MatrixMultiplication) + self.assertIsInstance(broad_to_edges_disc.children[0], pybamm.Matrix) + self.assertIsInstance(broad_to_edges_disc.children[1], pybamm.StateVector) + self.assertEqual( + broad_to_edges_disc.shape, + ((mesh["separator"][0].npts + 1) * mesh["current collector"][0].npts, 1), + ) + y_test = np.linspace(0, 1, mesh["current collector"][0].npts) + np.testing.assert_array_equal( + broad_to_edges_disc.evaluate(y=y_test), + np.outer(y_test, np.ones(mesh["separator"][0].npts + 1)).reshape(-1, 1), + ) + def test_secondary_broadcast_2D(self): # secondary broadcast in 2D --> Matrix multiplication disc = get_discretisation_for_testing() @@ -931,6 +1014,22 @@ def test_secondary_broadcast_2D(self): (mesh["negative particle"][0].npts * mesh["negative electrode"][0].npts, 1), ) + # test broadcast to edges + broad_to_edges = pybamm.SecondaryBroadcastToEdges(var, "negative electrode") + disc.set_variable_slices([var]) + broad_to_edges_disc = disc.process_symbol(broad_to_edges) + self.assertIsInstance(broad_to_edges_disc, pybamm.MatrixMultiplication) + self.assertIsInstance(broad_to_edges_disc.children[0], pybamm.Matrix) + self.assertIsInstance(broad_to_edges_disc.children[1], pybamm.StateVector) + self.assertEqual( + broad_to_edges_disc.shape, + ( + mesh["negative particle"][0].npts + * (mesh["negative electrode"][0].npts + 1), + 1, + ), + ) + def test_concatenation(self): a = pybamm.Symbol("a") b = pybamm.Symbol("b") diff --git a/tests/unit/test_expression_tree/test_binary_operators.py b/tests/unit/test_expression_tree/test_binary_operators.py index a35a4df290..e6c8dc26b0 100644 --- a/tests/unit/test_expression_tree/test_binary_operators.py +++ b/tests/unit/test_expression_tree/test_binary_operators.py @@ -292,19 +292,32 @@ def test_heaviside(self): a = pybamm.Scalar(1) b = pybamm.StateVector(slice(0, 1)) heav = a < b - self.assertFalse(heav.equal) self.assertEqual(heav.evaluate(y=np.array([2])), 1) self.assertEqual(heav.evaluate(y=np.array([1])), 0) self.assertEqual(heav.evaluate(y=np.array([0])), 0) self.assertEqual(str(heav), "1.0 < y[0:1]") heav = a >= b - self.assertTrue(heav.equal) self.assertEqual(heav.evaluate(y=np.array([2])), 0) self.assertEqual(heav.evaluate(y=np.array([1])), 1) self.assertEqual(heav.evaluate(y=np.array([0])), 1) self.assertEqual(str(heav), "y[0:1] <= 1.0") + def test_minimum_maximum(self): + a = pybamm.Scalar(1) + b = pybamm.StateVector(slice(0, 1)) + minimum = pybamm.minimum(a, b) + self.assertEqual(minimum.evaluate(y=np.array([2])), 1) + self.assertEqual(minimum.evaluate(y=np.array([1])), 1) + self.assertEqual(minimum.evaluate(y=np.array([0])), 0) + self.assertEqual(str(minimum), "minimum(1.0, y[0:1])") + + maximum = pybamm.maximum(a, b) + self.assertEqual(maximum.evaluate(y=np.array([2])), 2) + self.assertEqual(maximum.evaluate(y=np.array([1])), 1) + self.assertEqual(maximum.evaluate(y=np.array([0])), 1) + self.assertEqual(str(maximum), "maximum(1.0, y[0:1])") + class TestIsZero(unittest.TestCase): def test_is_scalar_zero(self): diff --git a/tests/unit/test_expression_tree/test_broadcasts.py b/tests/unit/test_expression_tree/test_broadcasts.py index 8febf50bc5..d666f67e12 100644 --- a/tests/unit/test_expression_tree/test_broadcasts.py +++ b/tests/unit/test_expression_tree/test_broadcasts.py @@ -112,6 +112,35 @@ def test_ones_like(self): self.assertEqual(ones_like_ab.domain, a.domain) self.assertEqual(ones_like_ab.auxiliary_domains, a.auxiliary_domains) + def test_broadcast_to_edges(self): + a = pybamm.Symbol("a") + broad_a = pybamm.PrimaryBroadcastToEdges(a, ["negative electrode"]) + self.assertEqual(broad_a.name, "broadcast to edges") + self.assertEqual(broad_a.children[0].name, a.name) + self.assertEqual(broad_a.domain, ["negative electrode"]) + self.assertTrue(broad_a.evaluates_on_edges()) + + a = pybamm.Symbol( + "a", + domain=["negative particle"], + auxiliary_domains={"secondary": "current collector"}, + ) + broad_a = pybamm.SecondaryBroadcastToEdges(a, ["negative electrode"]) + self.assertEqual(broad_a.domain, ["negative particle"]) + self.assertEqual( + broad_a.auxiliary_domains, + {"secondary": ["negative electrode"], "tertiary": ["current collector"]}, + ) + self.assertTrue(broad_a.evaluates_on_edges()) + + a = pybamm.Symbol("a") + broad_a = pybamm.FullBroadcastToEdges( + a, ["negative electrode"], "current collector" + ) + self.assertEqual(broad_a.domain, ["negative electrode"]) + self.assertEqual(broad_a.auxiliary_domains["secondary"], ["current collector"]) + self.assertTrue(broad_a.evaluates_on_edges()) + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_expression_tree/test_d_dt.py b/tests/unit/test_expression_tree/test_d_dt.py new file mode 100644 index 0000000000..f9eb5a2cd9 --- /dev/null +++ b/tests/unit/test_expression_tree/test_d_dt.py @@ -0,0 +1,70 @@ +# +# Tests for the Scalar class +# +import pybamm + +import unittest +import numpy as np + + +class TestDDT(unittest.TestCase): + def test_time_derivative(self): + a = pybamm.Scalar(5).diff(pybamm.t) + self.assertIsInstance(a, pybamm.Scalar) + self.assertEqual(a.value, 0) + + a = pybamm.t.diff(pybamm.t) + self.assertIsInstance(a, pybamm.Scalar) + self.assertEqual(a.value, 1) + + a = (pybamm.t**2).diff(pybamm.t) + self.assertEqual(a.id, (2 * pybamm.t ** 1 * 1).id) + self.assertEqual(a.simplify().id, (2 * pybamm.t).id) + self.assertEqual(a.evaluate(t=1), 2) + + a = (2 + pybamm.t**2).diff(pybamm.t) + self.assertEqual(a.simplify().id, (2 * pybamm.t).id) + self.assertEqual(a.evaluate(t=1), 2) + + def test_time_derivative_of_variable(self): + + a = (pybamm.Variable('a')).diff(pybamm.t) + self.assertIsInstance(a, pybamm.VariableDot) + self.assertEqual(a.name, "a'") + + p = pybamm.Parameter('p') + a = (1 + p * pybamm.Variable('a')).diff(pybamm.t).simplify() + self.assertIsInstance(a, pybamm.Multiplication) + self.assertEqual(a.children[0].name, 'p') + self.assertEqual(a.children[1].name, "a'") + + with self.assertRaises(pybamm.ModelError): + a = (pybamm.Variable('a')).diff(pybamm.t).diff(pybamm.t) + + with self.assertRaises(pybamm.ModelError): + a = pybamm.ExternalVariable("a", 1).diff(pybamm.t) + + def test_time_derivative_of_state_vector(self): + + sv = pybamm.StateVector(slice(0, 10)) + y_dot = np.linspace(0, 2, 19) + + a = sv.diff(pybamm.t) + self.assertIsInstance(a, pybamm.StateVectorDot) + self.assertEqual(a.name[-1], "'") + np.testing.assert_array_equal( + a.evaluate(y_dot=y_dot), np.linspace(0, 1, 10)[:, np.newaxis] + ) + + with self.assertRaises(pybamm.ModelError): + a = (sv).diff(pybamm.t).diff(pybamm.t) + + +if __name__ == "__main__": + print("Add -v for more debug output") + import sys + + if "-v" in sys.argv: + debug = True + pybamm.settings.debug_mode = True + unittest.main() diff --git a/tests/unit/test_expression_tree/test_functions.py b/tests/unit/test_expression_tree/test_functions.py index 8180723295..b986c3ff1b 100644 --- a/tests/unit/test_expression_tree/test_functions.py +++ b/tests/unit/test_expression_tree/test_functions.py @@ -130,11 +130,14 @@ def test_arcsinh(self): a = pybamm.InputParameter("a") fun = pybamm.arcsinh(a) self.assertIsInstance(fun, pybamm.Arcsinh) - self.assertEqual(fun.evaluate(u={"a": 3}), np.arcsinh(3)) + self.assertEqual(fun.evaluate(inputs={"a": 3}), np.arcsinh(3)) h = 0.0000001 self.assertAlmostEqual( - fun.diff(a).evaluate(u={"a": 3}), - (pybamm.arcsinh(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(u={"a": 3})) + fun.diff(a).evaluate(inputs={"a": 3}), + ( + pybamm.arcsinh(pybamm.Scalar(3 + h)).evaluate() + - fun.evaluate(inputs={"a": 3}) + ) / h, places=5, ) @@ -144,11 +147,14 @@ def test_cos(self): fun = pybamm.cos(a) self.assertIsInstance(fun, pybamm.Cos) self.assertEqual(fun.children[0].id, a.id) - self.assertEqual(fun.evaluate(u={"a": 3}), np.cos(3)) + self.assertEqual(fun.evaluate(inputs={"a": 3}), np.cos(3)) h = 0.0000001 self.assertAlmostEqual( - fun.diff(a).evaluate(u={"a": 3}), - (pybamm.cos(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(u={"a": 3})) + fun.diff(a).evaluate(inputs={"a": 3}), + ( + pybamm.cos(pybamm.Scalar(3 + h)).evaluate() + - fun.evaluate(inputs={"a": 3}) + ) / h, places=5, ) @@ -163,11 +169,14 @@ def test_cosh(self): fun = pybamm.cosh(a) self.assertIsInstance(fun, pybamm.Cosh) self.assertEqual(fun.children[0].id, a.id) - self.assertEqual(fun.evaluate(u={"a": 3}), np.cosh(3)) + self.assertEqual(fun.evaluate(inputs={"a": 3}), np.cosh(3)) h = 0.0000001 self.assertAlmostEqual( - fun.diff(a).evaluate(u={"a": 3}), - (pybamm.cosh(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(u={"a": 3})) + fun.diff(a).evaluate(inputs={"a": 3}), + ( + pybamm.cosh(pybamm.Scalar(3 + h)).evaluate() + - fun.evaluate(inputs={"a": 3}) + ) / h, places=5, ) @@ -177,11 +186,14 @@ def test_exp(self): fun = pybamm.exp(a) self.assertIsInstance(fun, pybamm.Exponential) self.assertEqual(fun.children[0].id, a.id) - self.assertEqual(fun.evaluate(u={"a": 3}), np.exp(3)) + self.assertEqual(fun.evaluate(inputs={"a": 3}), np.exp(3)) h = 0.0000001 self.assertAlmostEqual( - fun.diff(a).evaluate(u={"a": 3}), - (pybamm.exp(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(u={"a": 3})) + fun.diff(a).evaluate(inputs={"a": 3}), + ( + pybamm.exp(pybamm.Scalar(3 + h)).evaluate() + - fun.evaluate(inputs={"a": 3}) + ) / h, places=5, ) @@ -189,22 +201,28 @@ def test_exp(self): def test_log(self): a = pybamm.InputParameter("a") fun = pybamm.log(a) - self.assertEqual(fun.evaluate(u={"a": 3}), np.log(3)) + self.assertEqual(fun.evaluate(inputs={"a": 3}), np.log(3)) h = 0.0000001 self.assertAlmostEqual( - fun.diff(a).evaluate(u={"a": 3}), - (pybamm.log(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(u={"a": 3})) + fun.diff(a).evaluate(inputs={"a": 3}), + ( + pybamm.log(pybamm.Scalar(3 + h)).evaluate() + - fun.evaluate(inputs={"a": 3}) + ) / h, places=5, ) # Base 10 fun = pybamm.log10(a) - self.assertEqual(fun.evaluate(u={"a": 3}), np.log10(3)) + self.assertEqual(fun.evaluate(inputs={"a": 3}), np.log10(3)) h = 0.0000001 self.assertAlmostEqual( - fun.diff(a).evaluate(u={"a": 3}), - (pybamm.log10(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(u={"a": 3})) + fun.diff(a).evaluate(inputs={"a": 3}), + ( + pybamm.log10(pybamm.Scalar(3 + h)).evaluate() + - fun.evaluate(inputs={"a": 3}) + ) / h, places=5, ) @@ -228,11 +246,14 @@ def test_sin(self): fun = pybamm.sin(a) self.assertIsInstance(fun, pybamm.Sin) self.assertEqual(fun.children[0].id, a.id) - self.assertEqual(fun.evaluate(u={"a": 3}), np.sin(3)) + self.assertEqual(fun.evaluate(inputs={"a": 3}), np.sin(3)) h = 0.0000001 self.assertAlmostEqual( - fun.diff(a).evaluate(u={"a": 3}), - (pybamm.sin(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(u={"a": 3})) + fun.diff(a).evaluate(inputs={"a": 3}), + ( + pybamm.sin(pybamm.Scalar(3 + h)).evaluate() + - fun.evaluate(inputs={"a": 3}) + ) / h, places=5, ) @@ -242,11 +263,14 @@ def test_sinh(self): fun = pybamm.sinh(a) self.assertIsInstance(fun, pybamm.Sinh) self.assertEqual(fun.children[0].id, a.id) - self.assertEqual(fun.evaluate(u={"a": 3}), np.sinh(3)) + self.assertEqual(fun.evaluate(inputs={"a": 3}), np.sinh(3)) h = 0.0000001 self.assertAlmostEqual( - fun.diff(a).evaluate(u={"a": 3}), - (pybamm.sinh(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(u={"a": 3})) + fun.diff(a).evaluate(inputs={"a": 3}), + ( + pybamm.sinh(pybamm.Scalar(3 + h)).evaluate() + - fun.evaluate(inputs={"a": 3}) + ) / h, places=5, ) @@ -255,11 +279,14 @@ def test_sqrt(self): a = pybamm.InputParameter("a") fun = pybamm.sqrt(a) self.assertIsInstance(fun, pybamm.Sqrt) - self.assertEqual(fun.evaluate(u={"a": 3}), np.sqrt(3)) + self.assertEqual(fun.evaluate(inputs={"a": 3}), np.sqrt(3)) h = 0.0000001 self.assertAlmostEqual( - fun.diff(a).evaluate(u={"a": 3}), - (pybamm.sqrt(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(u={"a": 3})) + fun.diff(a).evaluate(inputs={"a": 3}), + ( + pybamm.sqrt(pybamm.Scalar(3 + h)).evaluate() + - fun.evaluate(inputs={"a": 3}) + ) / h, places=5, ) @@ -267,11 +294,14 @@ def test_sqrt(self): def test_tanh(self): a = pybamm.InputParameter("a") fun = pybamm.tanh(a) - self.assertEqual(fun.evaluate(u={"a": 3}), np.tanh(3)) + self.assertEqual(fun.evaluate(inputs={"a": 3}), np.tanh(3)) h = 0.0000001 self.assertAlmostEqual( - fun.diff(a).evaluate(u={"a": 3}), - (pybamm.tanh(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(u={"a": 3})) + fun.diff(a).evaluate(inputs={"a": 3}), + ( + pybamm.tanh(pybamm.Scalar(3 + h)).evaluate() + - fun.evaluate(inputs={"a": 3}) + ) / h, places=5, ) diff --git a/tests/unit/test_expression_tree/test_independent_variable.py b/tests/unit/test_expression_tree/test_independent_variable.py index 734d839ec9..a00fa9079d 100644 --- a/tests/unit/test_expression_tree/test_independent_variable.py +++ b/tests/unit/test_expression_tree/test_independent_variable.py @@ -36,6 +36,7 @@ def test_time(self): def test_spatial_variable(self): x = pybamm.SpatialVariable("x", "negative electrode") self.assertEqual(x.name, "x") + self.assertFalse(x.evaluates_on_edges()) y = pybamm.SpatialVariable("y", "separator") self.assertEqual(y.name, "y") z = pybamm.SpatialVariable("z", "positive electrode") @@ -45,8 +46,6 @@ def test_spatial_variable(self): with self.assertRaises(NotImplementedError): x.evaluate() - with self.assertRaisesRegex(ValueError, "name must be"): - pybamm.SpatialVariable("not a variable", ["negative electrode"]) with self.assertRaisesRegex(ValueError, "domain must be"): pybamm.SpatialVariable("x", []) with self.assertRaises(pybamm.DomainError): @@ -58,6 +57,11 @@ def test_spatial_variable(self): with self.assertRaises(pybamm.DomainError): pybamm.SpatialVariable("x", ["negative particle"]) + def test_spatial_variable_edge(self): + x = pybamm.SpatialVariableEdge("x", "negative electrode") + self.assertEqual(x.name, "x") + self.assertTrue(x.evaluates_on_edges()) + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_expression_tree/test_input_parameter.py b/tests/unit/test_expression_tree/test_input_parameter.py index f5c24e3909..0df4ade271 100644 --- a/tests/unit/test_expression_tree/test_input_parameter.py +++ b/tests/unit/test_expression_tree/test_input_parameter.py @@ -10,8 +10,8 @@ class TestInputParameter(unittest.TestCase): def test_input_parameter_init(self): a = pybamm.InputParameter("a") self.assertEqual(a.name, "a") - self.assertEqual(a.evaluate(u={"a": 1}), 1) - self.assertEqual(a.evaluate(u={"a": 5}), 5) + self.assertEqual(a.evaluate(inputs={"a": 1}), 1) + self.assertEqual(a.evaluate(inputs={"a": 5}), 5) def test_evaluate_for_shape(self): a = pybamm.InputParameter("a") @@ -20,9 +20,9 @@ def test_evaluate_for_shape(self): def test_errors(self): a = pybamm.InputParameter("a") with self.assertRaises(TypeError): - a.evaluate(u="not a dictionary") + a.evaluate(inputs="not a dictionary") with self.assertRaises(KeyError): - a.evaluate(u={"bad param": 5}) + a.evaluate(inputs={"bad param": 5}) # if u is not provided it gets turned into a dictionary and then raises KeyError with self.assertRaises(KeyError): a.evaluate() diff --git a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py index 017cae7f7a..c493343571 100644 --- a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py +++ b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py @@ -42,16 +42,21 @@ def myfunction(x, y): f = pybamm.Function(myfunction, b, d) self.assertEqual(f.to_casadi(), casadi.MX(3)) + # use classes to avoid simplification # addition - self.assertEqual((a + b).to_casadi(), casadi.MX(1)) + self.assertEqual((pybamm.Addition(a, b)).to_casadi(), casadi.MX(1)) # subtraction - self.assertEqual((c - d).to_casadi(), casadi.MX(-3)) + self.assertEqual(pybamm.Subtraction(c, d).to_casadi(), casadi.MX(-3)) # multiplication - self.assertEqual((c * d).to_casadi(), casadi.MX(-2)) + self.assertEqual(pybamm.Multiplication(c, d).to_casadi(), casadi.MX(-2)) # power - self.assertEqual((c ** d).to_casadi(), casadi.MX(1)) + self.assertEqual(pybamm.Power(c, d).to_casadi(), casadi.MX(1)) # division - self.assertEqual((b / d).to_casadi(), casadi.MX(1 / 2)) + self.assertEqual(pybamm.Division(b, d).to_casadi(), casadi.MX(1 / 2)) + + # minimum and maximum + self.assertEqual(pybamm.Minimum(a, b).to_casadi(), casadi.MX(0)) + self.assertEqual(pybamm.Maximum(a, b).to_casadi(), casadi.MX(1)) def test_convert_array_symbols(self): # Arrays @@ -61,9 +66,11 @@ def test_convert_array_symbols(self): casadi_t = casadi.MX.sym("t") casadi_y = casadi.MX.sym("y", 10) + casadi_y_dot = casadi.MX.sym("y_dot", 10) pybamm_t = pybamm.Time() pybamm_y = pybamm.StateVector(slice(0, 10)) + pybamm_y_dot = pybamm.StateVectorDot(slice(0, 10)) # Time self.assertEqual(pybamm_t.to_casadi(casadi_t, casadi_y), casadi_t) @@ -71,6 +78,11 @@ def test_convert_array_symbols(self): # State Vector self.assert_casadi_equal(pybamm_y.to_casadi(casadi_t, casadi_y), casadi_y) + # State Vector Dot + self.assert_casadi_equal( + pybamm_y_dot.to_casadi(casadi_t, casadi_y, casadi_y_dot), casadi_y_dot + ) + def test_special_functions(self): a = pybamm.Array(np.array([1, 2, 3, 4, 5])) self.assert_casadi_equal(pybamm.max(a).to_casadi(), casadi.MX(5), evalf=True) @@ -154,7 +166,8 @@ def myfunction(x, y): def test_convert_input_parameter(self): casadi_t = casadi.MX.sym("t") casadi_y = casadi.MX.sym("y", 10) - casadi_us = { + casadi_ydot = casadi.MX.sym("ydot", 10) + casadi_inputs = { "Input 1": casadi.MX.sym("Input 1"), "Input 2": casadi.MX.sym("Input 2"), } @@ -165,25 +178,26 @@ def test_convert_input_parameter(self): # Input only self.assert_casadi_equal( - pybamm_u1.to_casadi(casadi_t, casadi_y, casadi_us), casadi_us["Input 1"] + pybamm_u1.to_casadi(casadi_t, casadi_y, casadi_ydot, casadi_inputs), + casadi_inputs["Input 1"], ) # More complex expr = pybamm_u1 + pybamm_y self.assert_casadi_equal( - expr.to_casadi(casadi_t, casadi_y, casadi_us), - casadi_us["Input 1"] + casadi_y, + expr.to_casadi(casadi_t, casadi_y, casadi_ydot, casadi_inputs), + casadi_inputs["Input 1"] + casadi_y, ) expr = pybamm_u2 * pybamm_y self.assert_casadi_equal( - expr.to_casadi(casadi_t, casadi_y, casadi_us), - casadi_us["Input 2"] * casadi_y, + expr.to_casadi(casadi_t, casadi_y, casadi_ydot, casadi_inputs), + casadi_inputs["Input 2"] * casadi_y, ) def test_convert_external_variable(self): casadi_t = casadi.MX.sym("t") casadi_y = casadi.MX.sym("y", 10) - casadi_us = { + casadi_inputs = { "External 1": casadi.MX.sym("External 1", 3), "External 2": casadi.MX.sym("External 2", 10), } @@ -194,14 +208,15 @@ def test_convert_external_variable(self): # External only self.assert_casadi_equal( - pybamm_u1.to_casadi(casadi_t, casadi_y, casadi_us), casadi_us["External 1"] + pybamm_u1.to_casadi(casadi_t, casadi_y, inputs=casadi_inputs), + casadi_inputs["External 1"], ) # More complex expr = pybamm_u2 + pybamm_y self.assert_casadi_equal( - expr.to_casadi(casadi_t, casadi_y, casadi_us), - casadi_us["External 2"] + casadi_y, + expr.to_casadi(casadi_t, casadi_y, inputs=casadi_inputs), + casadi_inputs["External 2"] + casadi_y, ) def test_errors(self): @@ -210,6 +225,11 @@ def test_errors(self): ValueError, "Must provide a 'y' for converting state vectors" ): y.to_casadi() + y_dot = pybamm.StateVectorDot(slice(0, 10)) + with self.assertRaisesRegex( + ValueError, "Must provide a 'y_dot' for converting state vectors" + ): + y_dot.to_casadi() var = pybamm.Variable("var") with self.assertRaisesRegex(TypeError, "Cannot convert symbol of type"): var.to_casadi() diff --git a/tests/unit/test_expression_tree/test_operations/test_copy.py b/tests/unit/test_expression_tree/test_operations/test_copy.py index 32afe6c273..e3fdb43c7d 100644 --- a/tests/unit/test_expression_tree/test_operations/test_copy.py +++ b/tests/unit/test_expression_tree/test_operations/test_copy.py @@ -25,7 +25,7 @@ def test_symbol_new_copy(self): -a, abs(a), pybamm.Function(np.sin, a), - pybamm.FunctionParameter("function", a), + pybamm.FunctionParameter("function", {"a": a}), pybamm.grad(a), pybamm.div(a), pybamm.Integral(a, pybamm.t), diff --git a/tests/unit/test_expression_tree/test_operations/test_evaluate.py b/tests/unit/test_expression_tree/test_operations/test_evaluate.py index 88bc911e1f..ba4f642ed9 100644 --- a/tests/unit/test_expression_tree/test_operations/test_evaluate.py +++ b/tests/unit/test_expression_tree/test_operations/test_evaluate.py @@ -19,7 +19,7 @@ def test_find_symbols(self): a = pybamm.StateVector(slice(0, 1)) b = pybamm.StateVector(slice(1, 2)) - # test a * b + # test a + b constant_symbols = OrderedDict() variable_symbols = OrderedDict() expr = a + b @@ -356,6 +356,20 @@ def test_evaluator_python(self): result = evaluator.evaluate(t=t, y=y) np.testing.assert_allclose(result, expr.evaluate(t=t, y=y)) + # test something with a minimum or maximum + a = pybamm.Vector(np.array([1, 2])) + expr = pybamm.minimum(a, pybamm.StateVector(slice(0, 2))) + evaluator = pybamm.EvaluatorPython(expr) + for t, y in zip(t_tests, y_tests): + result = evaluator.evaluate(t=t, y=y) + np.testing.assert_allclose(result, expr.evaluate(t=t, y=y)) + + expr = pybamm.maximum(a, pybamm.StateVector(slice(0, 2))) + evaluator = pybamm.EvaluatorPython(expr) + for t, y in zip(t_tests, y_tests): + result = evaluator.evaluate(t=t, y=y) + np.testing.assert_allclose(result, expr.evaluate(t=t, y=y)) + # test something with an index expr = pybamm.Index(A @ pybamm.StateVector(slice(0, 2)), 0) evaluator = pybamm.EvaluatorPython(expr) diff --git a/tests/unit/test_expression_tree/test_operations/test_jac.py b/tests/unit/test_expression_tree/test_operations/test_jac.py index 270b030c44..56895d26b4 100644 --- a/tests/unit/test_expression_tree/test_operations/test_jac.py +++ b/tests/unit/test_expression_tree/test_operations/test_jac.py @@ -105,9 +105,57 @@ def test_nonlinear(self): dfunc_dy = func.jac(y).evaluate(y=y0) np.testing.assert_array_equal(jacobian, dfunc_dy.toarray()) - func = pybamm.AbsoluteValue(v) - with self.assertRaises(pybamm.UndefinedOperationError): - func.jac(y) + def test_multislice_raises(self): + y1 = pybamm.StateVector(slice(0, 4), slice(7, 8)) + y_dot1 = pybamm.StateVectorDot(slice(0, 4), slice(7, 8)) + y2 = pybamm.StateVector(slice(4, 7)) + with self.assertRaises(NotImplementedError): + y1.jac(y1) + with self.assertRaises(NotImplementedError): + y2.jac(y1) + with self.assertRaises(NotImplementedError): + y_dot1.jac(y1) + + def test_linear_ydot(self): + y = pybamm.StateVector(slice(0, 4)) + y_dot = pybamm.StateVectorDot(slice(0, 4)) + u = pybamm.StateVector(slice(0, 2)) + v = pybamm.StateVector(slice(2, 4)) + u_dot = pybamm.StateVectorDot(slice(0, 2)) + v_dot = pybamm.StateVectorDot(slice(2, 4)) + + y0 = np.ones(4) + y_dot0 = np.ones(4) + + func = u_dot + jacobian = np.array([[1, 0, 0, 0], [0, 1, 0, 0]]) + dfunc_dy = func.jac(y_dot).evaluate(y=y0, y_dot=y_dot0) + np.testing.assert_array_equal(jacobian, dfunc_dy.toarray()) + + func = -v_dot + jacobian = np.array([[0, 0, -1, 0], [0, 0, 0, -1]]) + dfunc_dy = func.jac(y_dot).evaluate(y=y0, y_dot=y_dot0) + np.testing.assert_array_equal(jacobian, dfunc_dy.toarray()) + + func = u_dot + jacobian = np.array([[0, 0, 0, 0], [0, 0, 0, 0]]) + dfunc_dy = func.jac(y).evaluate(y=y0, y_dot=y_dot0) + np.testing.assert_array_equal(jacobian, dfunc_dy.toarray()) + + func = -v_dot + jacobian = np.array([[0, 0, 0, 0], [0, 0, 0, 0]]) + dfunc_dy = func.jac(y).evaluate(y=y0, y_dot=y_dot0) + np.testing.assert_array_equal(jacobian, dfunc_dy.toarray()) + + func = u + jacobian = np.array([[0, 0, 0, 0], [0, 0, 0, 0]]) + dfunc_dy = func.jac(y_dot).evaluate(y=y0, y_dot=y_dot0) + np.testing.assert_array_equal(jacobian, dfunc_dy.toarray()) + + func = -v + jacobian = np.array([[0, 0, 0, 0], [0, 0, 0, 0]]) + dfunc_dy = func.jac(y_dot).evaluate(y=y0, y_dot=y_dot0) + np.testing.assert_array_equal(jacobian, dfunc_dy.toarray()) def test_functions(self): y = pybamm.StateVector(slice(0, 4)) @@ -248,6 +296,34 @@ def test_jac_of_heaviside(self): ((a < y) * y ** 2).jac(y).evaluate(y=-5 * np.ones(5)), 0 ) + def test_jac_of_minimum_maximum(self): + y = pybamm.StateVector(slice(0, 10)) + y_test = np.linspace(0, 2, 10) + np.testing.assert_array_equal( + np.diag(pybamm.minimum(1, y ** 2).jac(y).evaluate(y=y_test)), + 2 * y_test * (y_test < 1), + ) + np.testing.assert_array_equal( + np.diag(pybamm.maximum(1, y ** 2).jac(y).evaluate(y=y_test)), + 2 * y_test * (y_test > 1), + ) + + def test_jac_of_abs(self): + y = pybamm.StateVector(slice(0, 10)) + absy = abs(y) + jac = absy.jac(y) + y_test = np.linspace(-2, 2, 10) + np.testing.assert_array_equal( + np.diag(jac.evaluate(y=y_test).toarray()), np.sign(y_test) + ) + + def test_jac_of_sign(self): + y = pybamm.StateVector(slice(0, 10)) + func = pybamm.sign(y) * y + jac = func.jac(y) + y_test = np.linspace(-2, 2, 10) + np.testing.assert_array_equal(np.diag(jac.evaluate(y=y_test)), np.sign(y_test)) + def test_jac_of_domain_concatenation(self): # create mesh mesh = get_mesh_for_testing() diff --git a/tests/unit/test_expression_tree/test_operations/test_simplify.py b/tests/unit/test_expression_tree/test_operations/test_simplify.py index 129aef5e98..d898709a7f 100644 --- a/tests/unit/test_expression_tree/test_operations/test_simplify.py +++ b/tests/unit/test_expression_tree/test_operations/test_simplify.py @@ -16,6 +16,7 @@ def test_symbol_simplify(self): d = pybamm.Scalar(-1) e = pybamm.Scalar(2) g = pybamm.Variable("g") + gdot = pybamm.VariableDot("g'") # negate self.assertIsInstance((-a).simplify(), pybamm.Scalar) @@ -45,11 +46,11 @@ def myfunction(x, y): self.assertEqual((f).simplify().evaluate(), 0) # FunctionParameter - f = pybamm.FunctionParameter("function", b) + f = pybamm.FunctionParameter("function", {"b": b}) self.assertIsInstance((f).simplify(), pybamm.FunctionParameter) self.assertEqual((f).simplify().children[0].id, b.id) - f = pybamm.FunctionParameter("function", a, b) + f = pybamm.FunctionParameter("function", {"a": a, "b": b}) self.assertIsInstance((f).simplify(), pybamm.FunctionParameter) self.assertEqual((f).simplify().children[0].id, a.id) self.assertEqual((f).simplify().children[1].id, b.id) @@ -175,6 +176,18 @@ def myfunction(x, y): self.assertIsInstance(expr.children[1], pybamm.Negate) self.assertIsInstance(expr.children[1].children[0], pybamm.Parameter) + expr = (e * g * b).simplify() + self.assertIsInstance(expr, pybamm.Multiplication) + self.assertIsInstance(expr.children[0], pybamm.Scalar) + self.assertEqual(expr.children[0].evaluate(), 2.0) + self.assertIsInstance(expr.children[1], pybamm.Variable) + + expr = (e * gdot * b).simplify() + self.assertIsInstance(expr, pybamm.Multiplication) + self.assertIsInstance(expr.children[0], pybamm.Scalar) + self.assertEqual(expr.children[0].evaluate(), 2.0) + self.assertIsInstance(expr.children[1], pybamm.VariableDot) + expr = (e + (g - c)).simplify() self.assertIsInstance(expr, pybamm.Addition) self.assertIsInstance(expr.children[0], pybamm.Scalar) diff --git a/tests/unit/test_expression_tree/test_parameter.py b/tests/unit/test_expression_tree/test_parameter.py index fc03e73f45..ee3231a0d8 100644 --- a/tests/unit/test_expression_tree/test_parameter.py +++ b/tests/unit/test_expression_tree/test_parameter.py @@ -22,7 +22,7 @@ def test_evaluate_for_shape(self): class TestFunctionParameter(unittest.TestCase): def test_function_parameter_init(self): var = pybamm.Variable("var") - func = pybamm.FunctionParameter("func", var) + func = pybamm.FunctionParameter("func", {"var": var}) self.assertEqual(func.name, "func") self.assertEqual(func.children[0].id, var.id) self.assertEqual(func.domain, []) @@ -30,14 +30,50 @@ def test_function_parameter_init(self): def test_function_parameter_diff(self): var = pybamm.Variable("var") - func = pybamm.FunctionParameter("a", var).diff(var) + func = pybamm.FunctionParameter("a", {"var": var}).diff(var) self.assertEqual(func.diff_variable, var) def test_evaluate_for_shape(self): a = pybamm.Parameter("a") - func = pybamm.FunctionParameter("func", 2 * a) + func = pybamm.FunctionParameter("func", {"2a": 2 * a}) self.assertIsInstance(func.evaluate_for_shape(), numbers.Number) + def test_copy(self): + a = pybamm.Parameter("a") + func = pybamm.FunctionParameter("func", {"2a": 2 * a}) + + new_func = func.new_copy() + self.assertEqual(func.input_names, new_func.input_names) + + def test_print_input_names(self): + var = pybamm.Variable("var") + func = pybamm.FunctionParameter("a", {"var": var}) + func.print_input_names() + + def test_get_children_domains(self): + var = pybamm.Variable("var", domain=["negative electrode"]) + var_2 = pybamm.Variable("var", domain=["positive electrode"]) + with self.assertRaises(pybamm.DomainError): + pybamm.FunctionParameter("a", {"var": var, "var 2": var_2}) + + def test_set_input_names(self): + + var = pybamm.Variable("var") + func = pybamm.FunctionParameter("a", {"var": var}) + + new_input_names = ["first", "second"] + func.input_names = new_input_names + + self.assertEqual(func.input_names, new_input_names) + + with self.assertRaises(TypeError): + new_input_names = {"wrong": "input type"} + func.input_names = new_input_names + + with self.assertRaises(TypeError): + new_input_names = [var] + func.input_names = new_input_names + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_expression_tree/test_state_vector.py b/tests/unit/test_expression_tree/test_state_vector.py index f6809565d3..38f3cd2c39 100644 --- a/tests/unit/test_expression_tree/test_state_vector.py +++ b/tests/unit/test_expression_tree/test_state_vector.py @@ -62,6 +62,27 @@ def test_failure(self): pybamm.StateVector(slice(0, 10), 1) +class TestStateVectorDot(unittest.TestCase): + def test_evaluate(self): + sv = pybamm.StateVectorDot(slice(0, 10)) + y_dot = np.linspace(0, 2, 19) + np.testing.assert_array_equal( + sv.evaluate(y_dot=y_dot), np.linspace(0, 1, 10)[:, np.newaxis] + ) + + # Try evaluating with a y that is too short + y_dot2 = np.ones(5) + with self.assertRaisesRegex( + ValueError, + "y_dot is too short, so value with slice is smaller than expected" + ): + sv.evaluate(y_dot=y_dot2) + + def test_name(self): + sv = pybamm.StateVectorDot(slice(0, 10)) + self.assertEqual(sv.name, "y_dot[0:10]") + + if __name__ == "__main__": print("Add -v for more debug output") import sys diff --git a/tests/unit/test_expression_tree/test_symbol.py b/tests/unit/test_expression_tree/test_symbol.py index dbff68dd3a..588796f12a 100644 --- a/tests/unit/test_expression_tree/test_symbol.py +++ b/tests/unit/test_expression_tree/test_symbol.py @@ -168,6 +168,13 @@ def test_symbol_evaluation(self): with self.assertRaises(NotImplementedError): a.evaluate() + def test_evaluate_ignoring_errors(self): + self.assertIsNone(pybamm.t.evaluate_ignoring_errors(t=None)) + self.assertEqual(pybamm.t.evaluate_ignoring_errors(t=0), 0) + self.assertIsNone(pybamm.Parameter("a").evaluate_ignoring_errors()) + self.assertIsNone(pybamm.StateVector(slice(0, 1)).evaluate_ignoring_errors()) + self.assertEqual(pybamm.InputParameter("a").evaluate_ignoring_errors(), 1) + def test_symbol_is_constant(self): a = pybamm.Variable("a") self.assertFalse(a.is_constant()) @@ -374,7 +381,7 @@ def test_shape_and_size_for_testing(self): param = pybamm.Parameter("a") self.assertEqual(param.shape_for_testing, ()) - func = pybamm.FunctionParameter("func", state) + func = pybamm.FunctionParameter("func", {"state": state}) self.assertEqual(func.shape_for_testing, state.shape_for_testing) concat = pybamm.Concatenation() diff --git a/tests/unit/test_expression_tree/test_symbolic_diff.py b/tests/unit/test_expression_tree/test_symbolic_diff.py index 1c0e6d1e55..25c6b76749 100644 --- a/tests/unit/test_expression_tree/test_symbolic_diff.py +++ b/tests/unit/test_expression_tree/test_symbolic_diff.py @@ -60,6 +60,12 @@ def test_diff_zero(self): self.assertEqual(func.diff(b).id, pybamm.Scalar(0).id) self.assertNotEqual(func.diff(a).id, pybamm.Scalar(0).id) + def test_diff_state_vector_dot(self): + a = pybamm.StateVectorDot(slice(0, 1)) + b = pybamm.StateVector(slice(1, 2)) + self.assertEqual(a.diff(a).id, pybamm.Scalar(1).id) + self.assertEqual(a.diff(b).id, pybamm.Scalar(0).id) + def test_diff_heaviside(self): a = pybamm.Scalar(1) b = pybamm.StateVector(slice(0, 1)) @@ -68,6 +74,20 @@ def test_diff_heaviside(self): self.assertEqual(func.diff(b).evaluate(y=np.array([2])), 2) self.assertEqual(func.diff(b).evaluate(y=np.array([-2])), 0) + def test_diff_maximum_minimum(self): + a = pybamm.Scalar(1) + b = pybamm.StateVector(slice(0, 1)) + + func = pybamm.minimum(a, b ** 3) + self.assertEqual(func.diff(b).evaluate(y=np.array([10])), 0) + self.assertEqual(func.diff(b).evaluate(y=np.array([2])), 0) + self.assertEqual(func.diff(b).evaluate(y=np.array([-2])), 3 * (-2) ** 2) + + func = pybamm.maximum(a, b ** 3) + self.assertEqual(func.diff(b).evaluate(y=np.array([10])), 3 * 10 ** 2) + self.assertEqual(func.diff(b).evaluate(y=np.array([2])), 3 * 2 ** 2) + self.assertEqual(func.diff(b).evaluate(y=np.array([-2])), 0) + def test_exceptions(self): a = pybamm.Symbol("a") b = pybamm.Symbol("b") diff --git a/tests/unit/test_expression_tree/test_unary_operators.py b/tests/unit/test_expression_tree/test_unary_operators.py index 4ae1eb7740..3fad2f3c0e 100644 --- a/tests/unit/test_expression_tree/test_unary_operators.py +++ b/tests/unit/test_expression_tree/test_unary_operators.py @@ -5,6 +5,7 @@ import unittest import numpy as np +from scipy.sparse import diags class TestUnaryOperators(unittest.TestCase): @@ -38,6 +39,18 @@ def test_absolute(self): absb = pybamm.AbsoluteValue(b) self.assertEqual(absb.evaluate(), 4) + def test_sign(self): + b = pybamm.Scalar(-4) + signb = pybamm.sign(b) + self.assertEqual(signb.evaluate(), -1) + + A = diags(np.linspace(-1, 1, 5)) + b = pybamm.Matrix(A) + signb = pybamm.sign(b) + np.testing.assert_array_equal( + np.diag(signb.evaluate().toarray()), [-1, -1, 0, 1, 1] + ) + def test_gradient(self): a = pybamm.Symbol("a") grad = pybamm.Gradient(a) @@ -158,10 +171,14 @@ def test_diff(self): self.assertEqual((-a).diff(a).evaluate(y=y), -1) self.assertEqual((-a).diff(-a).evaluate(), 1) - # absolute value (not implemented) - absa = abs(a) - with self.assertRaises(pybamm.UndefinedOperationError): - absa.diff(a) + # absolute value + self.assertEqual((a ** 3).diff(a).evaluate(y=y), 3 * 5 ** 2) + self.assertEqual((abs(a ** 3)).diff(a).evaluate(y=y), 3 * 5 ** 2) + self.assertEqual((a ** 3).diff(a).evaluate(y=-y), 3 * 5 ** 2) + self.assertEqual((abs(a ** 3)).diff(a).evaluate(y=-y), -3 * 5 ** 2) + + # sign + self.assertEqual((pybamm.sign(a)).diff(a).evaluate(y=y), 0) # spatial operator (not implemented) spatial_a = pybamm.SpatialOperator("name", a) @@ -252,7 +269,7 @@ def test_boundary_value(self): pybamm.boundary_value(var, "negative tab") pybamm.boundary_value(var, "positive tab") - def test_average(self): + def test_x_average(self): a = pybamm.Scalar(1) average_a = pybamm.x_average(a) self.assertEqual(average_a.id, a.id) @@ -282,12 +299,24 @@ def test_average(self): self.assertIsInstance(av_a, pybamm.Division) self.assertIsInstance(av_a.children[0], pybamm.Integral) self.assertEqual(av_a.children[0].integration_variable[0].domain, x.domain) - # electrode domains go to current collector when averaged self.assertEqual(av_a.domain, []) - a = pybamm.Symbol("a", domain="bad domain") - with self.assertRaises(pybamm.DomainError): - pybamm.x_average(a) + a = pybamm.Symbol("a", domain="new domain") + av_a = pybamm.x_average(a) + self.assertEqual(av_a.domain, []) + self.assertIsInstance(av_a, pybamm.Division) + self.assertIsInstance(av_a.children[0], pybamm.Integral) + self.assertEqual(av_a.children[0].integration_variable[0].domain, a.domain) + self.assertIsInstance(av_a.children[1], pybamm.Integral) + self.assertEqual(av_a.children[1].integration_variable[0].domain, a.domain) + self.assertEqual(av_a.children[1].children[0].id, pybamm.ones_like(a).id) + + # x-average of symbol that evaluates on edges raises error + symbol_on_edges = pybamm.PrimaryBroadcastToEdges(1, "domain") + with self.assertRaisesRegex( + ValueError, "Can't take the x-average of a symbol that evaluates on edges" + ): + pybamm.x_average(symbol_on_edges) def test_r_average(self): a = pybamm.Scalar(1) @@ -309,9 +338,12 @@ def test_r_average(self): # electrode domains go to current collector when averaged self.assertEqual(av_a.domain, []) - a = pybamm.Symbol("a", domain="bad domain") - with self.assertRaises(pybamm.DomainError): - pybamm.x_average(a) + # r-average of symbol that evaluates on edges raises error + symbol_on_edges = pybamm.PrimaryBroadcastToEdges(1, "domain") + with self.assertRaisesRegex( + ValueError, "Can't take the r-average of a symbol that evaluates on edges" + ): + pybamm.r_average(symbol_on_edges) def test_yz_average(self): a = pybamm.Scalar(1) @@ -351,6 +383,13 @@ def test_yz_average(self): with self.assertRaises(pybamm.DomainError): pybamm.yz_average(a) + # average of symbol that evaluates on edges raises error + symbol_on_edges = pybamm.PrimaryBroadcastToEdges(1, "domain") + with self.assertRaisesRegex( + ValueError, "Can't take the z-average of a symbol that evaluates on edges" + ): + pybamm.z_average(symbol_on_edges) + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_expression_tree/test_variable.py b/tests/unit/test_expression_tree/test_variable.py index 1e4e1b0bee..8e4a8be11f 100644 --- a/tests/unit/test_expression_tree/test_variable.py +++ b/tests/unit/test_expression_tree/test_variable.py @@ -16,6 +16,14 @@ def test_variable_init(self): self.assertEqual(a.domain[0], "test") self.assertRaises(TypeError, pybamm.Variable("a", domain="test")) + def test_variable_diff(self): + a = pybamm.Variable("a") + b = pybamm.Variable("b") + self.assertIsInstance(a.diff(a), pybamm.Scalar) + self.assertEqual(a.diff(a).evaluate(), 1) + self.assertIsInstance(a.diff(b), pybamm.Scalar) + self.assertEqual(a.diff(b).evaluate(), 0) + def test_variable_id(self): a1 = pybamm.Variable("a", domain=["negative electrode"]) a2 = pybamm.Variable("a", domain=["negative electrode"]) @@ -26,29 +34,64 @@ def test_variable_id(self): self.assertNotEqual(a1.id, a4.id) +class TestVariableDot(unittest.TestCase): + def test_variable_init(self): + a = pybamm.VariableDot("a'") + self.assertEqual(a.name, "a'") + self.assertEqual(a.domain, []) + a = pybamm.VariableDot("a", domain=["test"]) + self.assertEqual(a.domain[0], "test") + self.assertRaises(TypeError, pybamm.Variable("a", domain="test")) + + def test_variable_id(self): + a1 = pybamm.VariableDot("a", domain=["negative electrode"]) + a2 = pybamm.VariableDot("a", domain=["negative electrode"]) + self.assertEqual(a1.id, a2.id) + a3 = pybamm.VariableDot("b", domain=["negative electrode"]) + a4 = pybamm.VariableDot("a", domain=["positive electrode"]) + self.assertNotEqual(a1.id, a3.id) + self.assertNotEqual(a1.id, a4.id) + + def test_variable_diff(self): + a = pybamm.VariableDot("a") + b = pybamm.Variable("b") + self.assertIsInstance(a.diff(a), pybamm.Scalar) + self.assertEqual(a.diff(a).evaluate(), 1) + self.assertIsInstance(a.diff(b), pybamm.Scalar) + self.assertEqual(a.diff(b).evaluate(), 0) + + class TestExternalVariable(unittest.TestCase): def test_external_variable_scalar(self): a = pybamm.ExternalVariable("a", 1) self.assertEqual(a.size, 1) - self.assertEqual(a.evaluate(u={"a": 3}), 3) + self.assertEqual(a.evaluate(inputs={"a": 3}), 3) with self.assertRaisesRegex(KeyError, "External variable"): a.evaluate() - with self.assertRaisesRegex(TypeError, "inputs u"): - a.evaluate(u="not a dictionary") + with self.assertRaisesRegex(TypeError, "inputs should be a dictionary"): + a.evaluate(inputs="not a dictionary") def test_external_variable_vector(self): a = pybamm.ExternalVariable("a", 10) self.assertEqual(a.size, 10) a_test = 2 * np.ones((10, 1)) - np.testing.assert_array_equal(a.evaluate(u={"a": a_test}), a_test) + np.testing.assert_array_equal(a.evaluate(inputs={"a": a_test}), a_test) - np.testing.assert_array_equal(a.evaluate(u={"a": 2}), a_test) + np.testing.assert_array_equal(a.evaluate(inputs={"a": 2}), a_test) with self.assertRaisesRegex(ValueError, "External variable"): - a.evaluate(u={"a": np.ones((5, 1))}) + a.evaluate(inputs={"a": np.ones((5, 1))}) + + def test_external_variable_diff(self): + a = pybamm.ExternalVariable("a", 10) + b = pybamm.Variable("b") + self.assertIsInstance(a.diff(a), pybamm.Scalar) + self.assertEqual(a.diff(a).evaluate(), 1) + self.assertIsInstance(a.diff(b), pybamm.Scalar) + self.assertEqual(a.diff(b).evaluate(), 0) if __name__ == "__main__": diff --git a/tests/unit/test_meshes/test_meshes.py b/tests/unit/test_meshes/test_meshes.py index 508a783723..0680010233 100644 --- a/tests/unit/test_meshes/test_meshes.py +++ b/tests/unit/test_meshes/test_meshes.py @@ -189,9 +189,11 @@ def test_combine_submeshes(self): ), 0, ) + np.testing.assert_almost_equal(submesh[0].internal_boundaries, [0.1 / 0.6]) with self.assertRaises(pybamm.DomainError): mesh.combine_submeshes("negative electrode", "positive electrode") + # test errors geometry = { "negative electrode": { "primary": { @@ -206,7 +208,6 @@ def test_combine_submeshes(self): } param.process_geometry(geometry) - # create mesh mesh = pybamm.Mesh(geometry, submesh_types, var_pts) with self.assertRaisesRegex(pybamm.DomainError, "trying"): diff --git a/tests/unit/test_models/test_base_model.py b/tests/unit/test_models/test_base_model.py index b9c5e6c703..fe642ec57b 100644 --- a/tests/unit/test_models/test_base_model.py +++ b/tests/unit/test_models/test_base_model.py @@ -92,6 +92,7 @@ def test_variables_set_get(self): variables = {"c": "alpha", "d": "beta"} model.variables = variables self.assertEqual(variables, model.variables) + self.assertEqual(model.variable_names(), list(variables.keys())) def test_jac_set_get(self): model = pybamm.BaseModel() @@ -248,6 +249,56 @@ def test_check_well_posedness_variables(self): ): model.check_well_posedness(post_discretisation=True) + # model must be in semi-explicit form + model = pybamm.BaseModel() + model.rhs = {c: d.diff(pybamm.t), d: -1} + model.initial_conditions = {c: 1, d: 1} + with self.assertRaisesRegex( + pybamm.ModelError, "time derivative of variable found", + ): + model.check_well_posedness() + + # model must be in semi-explicit form + model = pybamm.BaseModel() + model.algebraic = { + c: 2 * d - c, + d: c * d.diff(pybamm.t) - d, + } + model.initial_conditions = {c: 1, d: 1} + with self.assertRaisesRegex( + pybamm.ModelError, "time derivative of variable found", + ): + model.check_well_posedness() + + # model must be in semi-explicit form + model = pybamm.BaseModel() + model.rhs = {c: d.diff(pybamm.t), d: -1} + model.initial_conditions = {c: 1, d: 1} + with self.assertRaisesRegex( + pybamm.ModelError, "time derivative of variable found", + ): + model.check_well_posedness() + + # model must be in semi-explicit form + model = pybamm.BaseModel() + model.algebraic = { + d: 5 * pybamm.StateVector(slice(0, 15)) - 1, + c: 5 * pybamm.StateVectorDot(slice(0, 15)) - 1, + } + with self.assertRaisesRegex( + pybamm.ModelError, "time derivative of state vector found", + ): + model.check_well_posedness(post_discretisation=True) + + # model must be in semi-explicit form + model = pybamm.BaseModel() + model.rhs = {c: 5 * pybamm.StateVectorDot(slice(0, 15)) - 1} + model.initial_conditions = {c: 1} + with self.assertRaisesRegex( + pybamm.ModelError, "time derivative of state vector found", + ): + model.check_well_posedness(post_discretisation=True) + def test_check_well_posedness_initial_boundary_conditions(self): # Well-posed model - Dirichlet whole_cell = ["negative electrode", "separator", "positive electrode"] diff --git a/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py b/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py index aeda97fc0b..22983330a9 100644 --- a/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py +++ b/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py @@ -108,7 +108,7 @@ def test_default_spatial_methods(self): ) def test_bad_options(self): - with self.assertRaisesRegex(pybamm.OptionError, "option"): + with self.assertRaisesRegex(pybamm.OptionError, "Option"): pybamm.BaseBatteryModel({"bad option": "bad option"}) with self.assertRaisesRegex(pybamm.OptionError, "current collector model"): pybamm.BaseBatteryModel({"current collector": "bad current collector"}) @@ -122,8 +122,6 @@ def test_bad_options(self): pybamm.BaseBatteryModel({"surface form": "bad surface form"}) with self.assertRaisesRegex(pybamm.OptionError, "particle model"): pybamm.BaseBatteryModel({"particle": "bad particle"}) - with self.assertRaisesRegex(pybamm.OptionError, "option set external"): - pybamm.BaseBatteryModel({"current collector": "set external potential"}) with self.assertRaisesRegex(pybamm.OptionError, "operating mode"): pybamm.BaseBatteryModel({"operating mode": "bad operating mode"}) diff --git a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_base_lead_acid_model.py b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_base_lead_acid_model.py index 3dbee28979..43f02eae8f 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_base_lead_acid_model.py +++ b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_base_lead_acid_model.py @@ -29,13 +29,17 @@ def test_default_geometry(self): def test_incompatible_options(self): with self.assertRaisesRegex( - pybamm.OptionError, "thermal effects not implemented" + pybamm.OptionError, + "Thermal current collector effects are " + "not implemented for lead-acid models.", ): - pybamm.lead_acid.BaseModel({"thermal": "x-full"}) + pybamm.lead_acid.BaseModel({"thermal current collector": True}) + with self.assertRaisesRegex( - pybamm.OptionError, "thermal effects not implemented" + pybamm.OptionError, + "Lead-acid models can only have thermal " "effects if dimensionality is 0.", ): - pybamm.lead_acid.BaseModel({"thermal current collector": True}) + pybamm.lead_acid.BaseModel({"dimensionality": 1, "thermal": "x-full"}) if __name__ == "__main__": diff --git a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py index 0c7757d17f..cb5675944b 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py +++ b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py @@ -170,7 +170,7 @@ def test_well_posed_function(self): def external_circuit_function(variables): I = variables["Current [A]"] V = variables["Terminal voltage [V]"] - return V + I - pybamm.FunctionParameter("Function", pybamm.t) + return V + I - pybamm.FunctionParameter("Function", {"Time [s]": pybamm.t}) options = {"operating mode": external_circuit_function} model = pybamm.lead_acid.LOQS(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py index f6517319dc..98d64aa800 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py @@ -142,23 +142,6 @@ def test_x_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.DFN(options) model.check_well_posedness() - def test_x_lumped_thermal_set_temperature_1D(self): - options = { - "current collector": "potential pair", - "dimensionality": 1, - "thermal": "set external temperature", - } - model = pybamm.lithium_ion.DFN(options) - model.check_well_posedness() - - options = { - "current collector": "potential pair", - "dimensionality": 2, - "thermal": "set external temperature", - } - with self.assertRaises(NotImplementedError): - model = pybamm.lithium_ion.DFN(options) - def test_particle_fast_diffusion(self): options = {"particle": "fast diffusion"} model = pybamm.lithium_ion.DFN(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py index 9d75316b73..9fd9fd09a7 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py @@ -39,18 +39,6 @@ def test_well_posed_2plus1D(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() - options = {"current collector": "set external potential", "dimensionality": 0} - with self.assertRaises(NotImplementedError): - pybamm.lithium_ion.SPM(options) - - options = {"current collector": "set external potential", "dimensionality": 1} - model = pybamm.lithium_ion.SPM(options) - model.check_well_posedness() - - options = {"current collector": "set external potential", "dimensionality": 2} - model = pybamm.lithium_ion.SPM(options) - model.check_well_posedness() - def test_x_full_thermal_model_no_current_collector(self): options = {"thermal": "x-full"} model = pybamm.lithium_ion.SPM(options) @@ -155,23 +143,6 @@ def test_x_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() - def test_x_lumped_thermal_set_temperature_1D(self): - options = { - "current collector": "potential pair", - "dimensionality": 1, - "thermal": "set external temperature", - } - model = pybamm.lithium_ion.SPM(options) - model.check_well_posedness() - - options = { - "current collector": "potential pair", - "dimensionality": 2, - "thermal": "set external temperature", - } - with self.assertRaises(NotImplementedError): - model = pybamm.lithium_ion.SPM(options) - def test_particle_fast_diffusion(self): options = {"particle": "fast diffusion"} model = pybamm.lithium_ion.SPM(options) @@ -203,7 +174,7 @@ def test_well_posed_function(self): def external_circuit_function(variables): I = variables["Current [A]"] V = variables["Terminal voltage [V]"] - return V + I - pybamm.FunctionParameter("Function", pybamm.t) + return V + I - pybamm.FunctionParameter("Function", {"Time [s]": pybamm.t}) options = {"operating mode": external_circuit_function} model = pybamm.lithium_ion.SPM(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py index 9020d70b53..1fc5285a45 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py @@ -142,23 +142,6 @@ def test_x_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.SPMe(options) model.check_well_posedness() - def test_x_lumped_thermal_set_temperature_1D(self): - options = { - "current collector": "potential pair", - "dimensionality": 1, - "thermal": "set external temperature", - } - model = pybamm.lithium_ion.SPMe(options) - model.check_well_posedness() - - options = { - "current collector": "potential pair", - "dimensionality": 2, - "thermal": "set external temperature", - } - with self.assertRaises(NotImplementedError): - model = pybamm.lithium_ion.SPMe(options) - def test_particle_fast_diffusion(self): options = {"particle": "fast diffusion"} model = pybamm.lithium_ion.SPMe(options) diff --git a/tests/unit/test_models/test_model_info.py b/tests/unit/test_models/test_model_info.py new file mode 100644 index 0000000000..58c5c67c52 --- /dev/null +++ b/tests/unit/test_models/test_model_info.py @@ -0,0 +1,27 @@ +# +# Tests getting model info +# +import pybamm +import unittest + + +class TestModelInfo(unittest.TestCase): + def test_find_parameter_info(self): + model = pybamm.lithium_ion.SPM() + model.info("Negative electrode diffusivity [m2.s-1]") + model = pybamm.lithium_ion.SPMe() + model.info("Negative electrode diffusivity [m2.s-1]") + model = pybamm.lithium_ion.DFN() + model.info("Negative electrode diffusivity [m2.s-1]") + + model.info("Not a parameter") + + +if __name__ == "__main__": + print("Add -v for more debug output") + import sys + + if "-v" in sys.argv: + debug = True + pybamm.settings.debug_mode = True + unittest.main() diff --git a/tests/unit/test_models/test_submodels/test_current_collector/test_set_potential_spm_1plus1d.py b/tests/unit/test_models/test_submodels/test_current_collector/test_set_potential_spm_1plus1d.py deleted file mode 100644 index 5a29752e76..0000000000 --- a/tests/unit/test_models/test_submodels/test_current_collector/test_set_potential_spm_1plus1d.py +++ /dev/null @@ -1,60 +0,0 @@ -# -# Test base current collector submodel -# - -import pybamm -import tests -import unittest -import pybamm.models.submodels.current_collector as cc - - -class TestSetPotentialSPM1plus1DModel(unittest.TestCase): - def test_public_functions(self): - param = pybamm.standard_parameters_lithium_ion - submodel = cc.SetPotentialSingleParticle1plus1D(param) - val = pybamm.PrimaryBroadcast(0.0, "current collector") - variables = { - "X-averaged positive electrode open circuit potential": val, - "X-averaged negative electrode open circuit potential": val, - "X-averaged positive electrode reaction overpotential": val, - "X-averaged negative electrode reaction overpotential": val, - "X-averaged electrolyte overpotential": val, - "X-averaged positive electrode ohmic losses": val, - "X-averaged negative electrode ohmic losses": val, - "Total current density": 0, - "Local voltage": val, - } - std_tests = tests.StandardSubModelTests(submodel, variables) - - std_tests.test_all() - - -class TestSetPotetetialSPM2plus1DModel(unittest.TestCase): - def test_public_functions(self): - param = pybamm.standard_parameters_lithium_ion - submodel = cc.SetPotentialSingleParticle2plus1D(param) - val = pybamm.PrimaryBroadcast(0.0, "current collector") - variables = { - "X-averaged positive electrode open circuit potential": val, - "X-averaged negative electrode open circuit potential": val, - "X-averaged positive electrode reaction overpotential": val, - "X-averaged negative electrode reaction overpotential": val, - "X-averaged electrolyte overpotential": val, - "X-averaged positive electrode ohmic losses": val, - "X-averaged negative electrode ohmic losses": val, - "Total current density": 0, - "Local voltage": val, - } - std_tests = tests.StandardSubModelTests(submodel, variables) - - std_tests.test_all() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_diffusion/test_leading_stefan_maxwell_diffusion.py b/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_diffusion/test_leading_stefan_maxwell_diffusion.py index 9be903d1b0..fc3d23e750 100644 --- a/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_diffusion/test_leading_stefan_maxwell_diffusion.py +++ b/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_diffusion/test_leading_stefan_maxwell_diffusion.py @@ -13,11 +13,11 @@ def test_public_functions(self): reactions = { "main": { "Negative": { - "s": param.s_n, + "s": -param.s_plus_n_S, "aj": "Negative electrode interfacial current density", }, "Positive": { - "s": param.s_p, + "s": -param.s_plus_p_S, "aj": "Positive electrode interfacial current density", }, } diff --git a/tests/unit/test_models/test_submodels/test_external_circuit/test_function_control.py b/tests/unit/test_models/test_submodels/test_external_circuit/test_function_control.py index 4a06002baa..2315fac9ee 100644 --- a/tests/unit/test_models/test_submodels/test_external_circuit/test_function_control.py +++ b/tests/unit/test_models/test_submodels/test_external_circuit/test_function_control.py @@ -9,7 +9,14 @@ def external_circuit_function(variables): I = variables["Current [A]"] V = variables["Terminal voltage [V]"] - return V + I - pybamm.FunctionParameter("Current plus voltage function", pybamm.t) + return ( + V + + I + - pybamm.FunctionParameter( + "Current plus voltage function", + {"Time [s]": pybamm.t * pybamm.standard_parameters_lithium_ion.timescale}, + ) + ) class TestFunctionControl(unittest.TestCase): diff --git a/tests/unit/test_models/test_submodels/test_interface/test_base_interface.py b/tests/unit/test_models/test_submodels/test_interface/test_base_interface.py index eeba882b72..cc2691eba7 100644 --- a/tests/unit/test_models/test_submodels/test_interface/test_base_interface.py +++ b/tests/unit/test_models/test_submodels/test_interface/test_base_interface.py @@ -9,11 +9,11 @@ class TestBaseInterface(unittest.TestCase): def test_public_functions(self): - submodel = pybamm.interface.BaseInterface(None, "Negative") + submodel = pybamm.interface.BaseInterface(None, "Negative", None) std_tests = tests.StandardSubModelTests(submodel) std_tests.test_all() - submodel = pybamm.interface.BaseInterface(None, "Positive") + submodel = pybamm.interface.BaseInterface(None, "Positive", None) std_tests = tests.StandardSubModelTests(submodel) std_tests.test_all() diff --git a/tests/unit/test_models/test_submodels/test_interface/test_lead_acid.py b/tests/unit/test_models/test_submodels/test_interface/test_lead_acid.py index 674e4729a6..424b92e261 100644 --- a/tests/unit/test_models/test_submodels/test_interface/test_lead_acid.py +++ b/tests/unit/test_models/test_submodels/test_interface/test_lead_acid.py @@ -22,7 +22,7 @@ def test_public_functions(self): "Negative electrolyte concentration": a_n, "Negative electrode temperature": a_n, } - submodel = pybamm.interface.lead_acid.ButlerVolmer(param, "Negative") + submodel = pybamm.interface.ButlerVolmer(param, "Negative", "lead-acid main") std_tests = tests.StandardSubModelTests(submodel, variables) std_tests.test_all() @@ -37,7 +37,7 @@ def test_public_functions(self): "Negative electrode interfacial current density": a_n, "Negative electrode exchange current density": a_n, } - submodel = pybamm.interface.lead_acid.ButlerVolmer(param, "Positive") + submodel = pybamm.interface.ButlerVolmer(param, "Positive", "lead-acid main") std_tests = tests.StandardSubModelTests(submodel, variables) std_tests.test_all() diff --git a/tests/unit/test_models/test_submodels/test_interface/test_lithium_ion.py b/tests/unit/test_models/test_submodels/test_interface/test_lithium_ion.py index 2f5352e2b6..48238f199d 100644 --- a/tests/unit/test_models/test_submodels/test_interface/test_lithium_ion.py +++ b/tests/unit/test_models/test_submodels/test_interface/test_lithium_ion.py @@ -23,7 +23,7 @@ def test_public_functions(self): "Negative particle surface concentration": a_n, "Negative electrode temperature": a_n, } - submodel = pybamm.interface.lithium_ion.ButlerVolmer(param, "Negative") + submodel = pybamm.interface.ButlerVolmer(param, "Negative", "lithium-ion main") std_tests = tests.StandardSubModelTests(submodel, variables) std_tests.test_all() @@ -39,7 +39,7 @@ def test_public_functions(self): "Negative electrode exchange current density": a_n, "Positive electrode temperature": a_p, } - submodel = pybamm.interface.lithium_ion.ButlerVolmer(param, "Positive") + submodel = pybamm.interface.ButlerVolmer(param, "Positive", "lithium-ion main") std_tests = tests.StandardSubModelTests(submodel, variables) std_tests.test_all() diff --git a/tests/unit/test_models/test_submodels/test_particle/test_fast/__init__.py b/tests/unit/test_models/test_submodels/test_particle/test_fast/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/unit/test_models/test_submodels/test_particle/test_fast/test_base_fast_particle.py b/tests/unit/test_models/test_submodels/test_particle/test_fast/test_base_fast_particle.py deleted file mode 100644 index ed8302c972..0000000000 --- a/tests/unit/test_models/test_submodels/test_particle/test_fast/test_base_fast_particle.py +++ /dev/null @@ -1,30 +0,0 @@ -# -# Test base fast submodel -# - -import pybamm -import tests -import unittest - - -class TestBaseModel(unittest.TestCase): - def test_public_functions(self): - submodel = pybamm.particle.fast.BaseModel(None, "Negative") - std_tests = tests.StandardSubModelTests(submodel) - with self.assertRaises(NotImplementedError): - std_tests.test_all() - - submodel = pybamm.particle.fast.BaseModel(None, "Positive") - std_tests = tests.StandardSubModelTests(submodel) - with self.assertRaises(NotImplementedError): - std_tests.test_all() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_submodels/test_particle/test_fast/test_fast_many_particles.py b/tests/unit/test_models/test_submodels/test_particle/test_fast_many_particles.py similarity index 87% rename from tests/unit/test_models/test_submodels/test_particle/test_fast/test_fast_many_particles.py rename to tests/unit/test_models/test_submodels/test_particle/test_fast_many_particles.py index cb842fc9a2..de87f3ca0e 100644 --- a/tests/unit/test_models/test_submodels/test_particle/test_fast/test_fast_many_particles.py +++ b/tests/unit/test_models/test_submodels/test_particle/test_fast_many_particles.py @@ -20,12 +20,12 @@ def test_public_functions(self): variables = {"Negative electrode interfacial current density": a_n} - submodel = pybamm.particle.fast.ManyParticles(param, "Negative") + submodel = pybamm.particle.FastManyParticles(param, "Negative") std_tests = tests.StandardSubModelTests(submodel, variables) std_tests.test_all() variables = {"Positive electrode interfacial current density": a_p} - submodel = pybamm.particle.fast.ManyParticles(param, "Positive") + submodel = pybamm.particle.FastManyParticles(param, "Positive") std_tests = tests.StandardSubModelTests(submodel, variables) std_tests.test_all() diff --git a/tests/unit/test_models/test_submodels/test_particle/test_fast/test_fast_single_particle.py b/tests/unit/test_models/test_submodels/test_particle/test_fast_single_particle.py similarity index 85% rename from tests/unit/test_models/test_submodels/test_particle/test_fast/test_fast_single_particle.py rename to tests/unit/test_models/test_submodels/test_particle/test_fast_single_particle.py index 6d1a3a21a6..28677bc9e6 100644 --- a/tests/unit/test_models/test_submodels/test_particle/test_fast/test_fast_single_particle.py +++ b/tests/unit/test_models/test_submodels/test_particle/test_fast_single_particle.py @@ -14,12 +14,12 @@ def test_public_functions(self): a = pybamm.PrimaryBroadcast(pybamm.Scalar(0), "current collector") variables = {"X-averaged negative electrode interfacial current density": a} - submodel = pybamm.particle.fast.SingleParticle(param, "Negative") + submodel = pybamm.particle.FastSingleParticle(param, "Negative") std_tests = tests.StandardSubModelTests(submodel, variables) std_tests.test_all() variables = {"X-averaged positive electrode interfacial current density": a} - submodel = pybamm.particle.fast.SingleParticle(param, "Positive") + submodel = pybamm.particle.FastSingleParticle(param, "Positive") std_tests = tests.StandardSubModelTests(submodel, variables) std_tests.test_all() diff --git a/tests/unit/test_models/test_submodels/test_particle/test_fickian/__init__.py b/tests/unit/test_models/test_submodels/test_particle/test_fickian/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/unit/test_models/test_submodels/test_particle/test_fickian/test_fickian_many_particles.py b/tests/unit/test_models/test_submodels/test_particle/test_fickian_many_particles.py similarity index 88% rename from tests/unit/test_models/test_submodels/test_particle/test_fickian/test_fickian_many_particles.py rename to tests/unit/test_models/test_submodels/test_particle/test_fickian_many_particles.py index e0d803b2ef..8953203915 100644 --- a/tests/unit/test_models/test_submodels/test_particle/test_fickian/test_fickian_many_particles.py +++ b/tests/unit/test_models/test_submodels/test_particle/test_fickian_many_particles.py @@ -23,7 +23,7 @@ def test_public_functions(self): "Negative electrode temperature": a_n, } - submodel = pybamm.particle.fickian.ManyParticles(param, "Negative") + submodel = pybamm.particle.FickianManyParticles(param, "Negative") std_tests = tests.StandardSubModelTests(submodel, variables) std_tests.test_all() @@ -31,7 +31,7 @@ def test_public_functions(self): "Positive electrode interfacial current density": a_p, "Positive electrode temperature": a_p, } - submodel = pybamm.particle.fickian.ManyParticles(param, "Positive") + submodel = pybamm.particle.FickianManyParticles(param, "Positive") std_tests = tests.StandardSubModelTests(submodel, variables) std_tests.test_all() diff --git a/tests/unit/test_models/test_submodels/test_particle/test_fickian/test_fickian_single_particle.py b/tests/unit/test_models/test_submodels/test_particle/test_fickian_single_particle.py similarity index 86% rename from tests/unit/test_models/test_submodels/test_particle/test_fickian/test_fickian_single_particle.py rename to tests/unit/test_models/test_submodels/test_particle/test_fickian_single_particle.py index 8cf3c43d06..fbf27e700c 100644 --- a/tests/unit/test_models/test_submodels/test_particle/test_fickian/test_fickian_single_particle.py +++ b/tests/unit/test_models/test_submodels/test_particle/test_fickian_single_particle.py @@ -17,7 +17,7 @@ def test_public_functions(self): "X-averaged negative electrode temperature": a, } - submodel = pybamm.particle.fickian.SingleParticle(param, "Negative") + submodel = pybamm.particle.FickianSingleParticle(param, "Negative") std_tests = tests.StandardSubModelTests(submodel, variables) std_tests.test_all() @@ -25,7 +25,7 @@ def test_public_functions(self): "X-averaged positive electrode interfacial current density": a, "X-averaged positive electrode temperature": a, } - submodel = pybamm.particle.fickian.SingleParticle(param, "Positive") + submodel = pybamm.particle.FickianSingleParticle(param, "Positive") std_tests = tests.StandardSubModelTests(submodel, variables) std_tests.test_all() diff --git a/tests/unit/test_models/test_submodels/test_thermal/test_x_lumped/test_x_lumped_1D_set_temperature.py b/tests/unit/test_models/test_submodels/test_thermal/test_x_lumped/test_x_lumped_1D_set_temperature.py deleted file mode 100644 index d3ece87c76..0000000000 --- a/tests/unit/test_models/test_submodels/test_thermal/test_x_lumped/test_x_lumped_1D_set_temperature.py +++ /dev/null @@ -1,40 +0,0 @@ -# -# Test x-lumped submodel with 1D current collectors in which the temperature is -# set externally -# - -import pybamm -import tests -import unittest - -from tests.unit.test_models.test_submodels.test_thermal.coupled_variables import ( - coupled_variables, -) - - -class TestSetTemperature1D(unittest.TestCase): - def test_public_functions(self): - param = pybamm.standard_parameters_lithium_ion - phi_s_cn = pybamm.PrimaryBroadcast(pybamm.Scalar(0), ["current collector"]) - phi_s_cp = pybamm.PrimaryBroadcast(pybamm.Scalar(3), ["current collector"]) - - coupled_variables.update( - { - "Negative current collector potential": phi_s_cn, - "Positive current collector potential": phi_s_cp, - } - ) - - submodel = pybamm.thermal.x_lumped.SetTemperature1D(param) - std_tests = tests.StandardSubModelTests(submodel, coupled_variables) - std_tests.test_all() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_parameters/test_parameter_sets/test_LGM50_Chen2020.py b/tests/unit/test_parameters/test_parameter_sets/test_LGM50_Chen2020.py index 7fdd260e51..915db76e96 100644 --- a/tests/unit/test_parameters/test_parameter_sets/test_LGM50_Chen2020.py +++ b/tests/unit/test_parameters/test_parameter_sets/test_LGM50_Chen2020.py @@ -23,8 +23,8 @@ def test_load_params(self): electrolyte = pybamm.ParameterValues({}).read_parameters_csv( pybamm.get_parameters_filepath( - "input/parameters/lithium-ion/electrolytes/lipf6_Nyman2008/" + - "parameters.csv", + "input/parameters/lithium-ion/electrolytes/lipf6_Nyman2008/" + + "parameters.csv", ) ) self.assertEqual(electrolyte["Reference temperature [K]"], "298.15") @@ -34,9 +34,7 @@ def test_load_params(self): "input/parameters/lithium-ion/cells/LGM50_Chen2020/parameters.csv", ) ) - self.assertAlmostEqual( - cell["Negative current collector thickness [m]"], 12E-6 - ) + self.assertAlmostEqual(cell["Negative current collector thickness [m]"], 12e-6) def test_standard_lithium_parameters(self): diff --git a/tests/unit/test_parameters/test_parameter_sets/test_Landesfeind2020.py b/tests/unit/test_parameters/test_parameter_sets/test_Landesfeind2020.py new file mode 100644 index 0000000000..6a7e16e6b9 --- /dev/null +++ b/tests/unit/test_parameters/test_parameter_sets/test_Landesfeind2020.py @@ -0,0 +1,75 @@ +# +# Tests for LG M50 parameter set loads +# +import pybamm +import unittest +import os +import numpy as np + + +class TestLandesfeind(unittest.TestCase): + def test_electrolyte_conductivity(self): + root = pybamm.root_dir() + p = "pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019" + k_path = os.path.join(root, p) + files = [ + f + for f in os.listdir(k_path) + if ".py" in f and "_base" not in f and "conductivity" in f + ] + files.sort() + funcs = [pybamm.load_function(os.path.join(k_path, f)) for f in files] + T_ref = 298.15 + T = T_ref + 30.0 + c = 1000.0 + k = [np.around(f(c, T, np.nan, np.nan, np.nan).value, 6) for f in funcs] + self.assertEqual(k, [1.839786, 1.361015, 0.750259]) + T += 20 + k = [np.around(f(c, T, np.nan, np.nan, np.nan).value, 6) for f in funcs] + self.assertEqual(k, [2.292425, 1.664438, 0.880755]) + + chemistry = pybamm.parameter_sets.Chen2020 + param = pybamm.ParameterValues(chemistry=chemistry) + param["Electrolyte conductivity [S.m-1]"] = funcs[0] + model = pybamm.lithium_ion.SPM() + sim = pybamm.Simulation(model, parameter_values=param) + sim.set_parameters() + sim.build() + + def test_electrolyte_diffusivity(self): + root = pybamm.root_dir() + p = "pybamm/input/parameters/lithium-ion/electrolytes/lipf6_Landesfeind2019" + d_path = os.path.join(root, p) + files = [ + f + for f in os.listdir(d_path) + if ".py" in f and "_base" not in f and "diffusivity" in f + ] + files.sort() + funcs = [pybamm.load_function(os.path.join(d_path, f)) for f in files] + T_ref = 298.15 + T = T_ref + 30.0 + c = 1000.0 + D = [np.around(f(c, T, np.nan, np.nan, np.nan).value, 16) for f in funcs] + self.assertEqual(D, [5.796505e-10, 5.417881e-10, 5.608856e-10]) + T += 20 + D = [np.around(f(c, T, np.nan, np.nan, np.nan).value, 16) for f in funcs] + self.assertEqual(D, [8.5992e-10, 7.752815e-10, 7.907549e-10]) + + chemistry = pybamm.parameter_sets.Chen2020 + param = pybamm.ParameterValues(chemistry=chemistry) + param["Electrolyte diffusivity [m2.s-1]"] = funcs[0] + model = pybamm.lithium_ion.SPM() + sim = pybamm.Simulation(model, parameter_values=param) + sim.set_parameters() + sim.build() + + +if __name__ == "__main__": + print("Add -v for more debug output") + import sys + + if "-v" in sys.argv: + debug = True + pybamm.settings.debug_mode = True + unittest.main() diff --git a/tests/unit/test_parameters/test_parameter_sets/test_NCO_Ecker2015.py b/tests/unit/test_parameters/test_parameter_sets/test_NCO_Ecker2015.py new file mode 100644 index 0000000000..5d83923187 --- /dev/null +++ b/tests/unit/test_parameters/test_parameter_sets/test_NCO_Ecker2015.py @@ -0,0 +1,56 @@ +# +# Tests for Ecker parameter set +# +import pybamm +import unittest + + +class TestEcker(unittest.TestCase): + def test_load_params(self): + anode = pybamm.ParameterValues({}).read_parameters_csv( + pybamm.get_parameters_filepath( + "input/parameters/lithium-ion/anodes/graphite_Ecker2015/parameters.csv" + ) + ) + self.assertEqual(anode["Negative electrode porosity"], "0.329") + + path = "input/parameters/lithium-ion/cathodes/LiNiCoO2_Ecker2015/parameters.csv" + cathode = pybamm.ParameterValues({}).read_parameters_csv( + pybamm.get_parameters_filepath(path) + ) + self.assertEqual(cathode["Positive electrode conductivity [S.m-1]"], "68.1") + + electrolyte = pybamm.ParameterValues({}).read_parameters_csv( + pybamm.get_parameters_filepath( + "input/parameters/lithium-ion/electrolytes/lipf6_Ecker2015/" + + "parameters.csv" + ) + ) + self.assertEqual(electrolyte["Reference temperature [K]"], "296.15") + + cell = pybamm.ParameterValues({}).read_parameters_csv( + pybamm.get_parameters_filepath( + "input/parameters/lithium-ion/cells/kokam_Ecker2015/parameters.csv" + ) + ) + self.assertAlmostEqual(cell["Negative current collector thickness [m]"], 14e-6) + + def test_standard_lithium_parameters(self): + + chemistry = pybamm.parameter_sets.Ecker2015 + parameter_values = pybamm.ParameterValues(chemistry=chemistry) + + model = pybamm.lithium_ion.DFN() + sim = pybamm.Simulation(model, parameter_values=parameter_values) + sim.set_parameters() + sim.build() + + +if __name__ == "__main__": + print("Add -v for more debug output") + import sys + + if "-v" in sys.argv: + debug = True + pybamm.settings.debug_mode = True + unittest.main() diff --git a/tests/unit/test_parameters/test_parameter_values.py b/tests/unit/test_parameters/test_parameter_values.py index ba2ef688d7..8596c90152 100644 --- a/tests/unit/test_parameters/test_parameter_values.py +++ b/tests/unit/test_parameters/test_parameter_values.py @@ -14,9 +14,12 @@ def test_read_parameters_csv(self): data = pybamm.ParameterValues({}).read_parameters_csv( pybamm.get_parameters_filepath( os.path.join( - "input", "parameters", - "lithium-ion", "cathodes", - "lico2_Marquis2019", "parameters.csv" + "input", + "parameters", + "lithium-ion", + "cathodes", + "lico2_Marquis2019", + "parameters.csv", ) ) ) @@ -34,7 +37,7 @@ def test_init(self): param = pybamm.ParameterValues( values=pybamm.get_parameters_filepath( "input/parameters/lithium-ion/cathodes/lico2_Marquis2019/" - + "parameters.csv", + + "parameters.csv" ) ) self.assertEqual(param["Reference temperature [K]"], 298.15) @@ -127,12 +130,14 @@ def test_check_and_update_parameter_values(self): # if only C-rate and capacity provided, update current values = {"C-rate": "[input]", "Cell capacity [A.h]": 10} param = pybamm.ParameterValues(values) - self.assertEqual(param["Current function [A]"](2).evaluate(u={"C-rate": 1}), 10) + self.assertEqual( + param["Current function [A]"](2).evaluate(inputs={"C-rate": 1}), 10 + ) # if only current and capacity provided, update C-rate values = {"Current function [A]": "[input]", "Cell capacity [A.h]": 10} param = pybamm.ParameterValues(values) self.assertEqual( - param["C-rate"](5).evaluate(u={"Current function [A]": 5}), 0.5 + param["C-rate"](5).evaluate(inputs={"Current function [A]": 5}), 0.5 ) def test_process_symbol(self): @@ -280,7 +285,7 @@ def test_process_input_parameter(self): a = pybamm.Parameter("a") processed_a = parameter_values.process_symbol(a) self.assertIsInstance(processed_a, pybamm.InputParameter) - self.assertEqual(processed_a.evaluate(u={"a": 5}), 5) + self.assertEqual(processed_a.evaluate(inputs={"a": 5}), 5) # process binary operation b = pybamm.Parameter("b") @@ -289,7 +294,7 @@ def test_process_input_parameter(self): self.assertIsInstance(processed_add, pybamm.Addition) self.assertIsInstance(processed_add.children[0], pybamm.InputParameter) self.assertIsInstance(processed_add.children[1], pybamm.Scalar) - self.assertEqual(processed_add.evaluate(u={"a": 4}), 7) + self.assertEqual(processed_add.evaluate(inputs={"a": 4}), 7) def test_process_function_parameter(self): parameter_values = pybamm.ParameterValues( @@ -297,17 +302,18 @@ def test_process_function_parameter(self): "a": 3, "func": pybamm.load_function("process_symbol_test_function.py"), "const": 254, + "float_func": lambda x: 42, } ) a = pybamm.InputParameter("a") # process function - func = pybamm.FunctionParameter("func", a) + func = pybamm.FunctionParameter("func", {"a": a}) processed_func = parameter_values.process_symbol(func) - self.assertEqual(processed_func.evaluate(u={"a": 3}), 369) + self.assertEqual(processed_func.evaluate(inputs={"a": 3}), 369) # process constant function - const = pybamm.FunctionParameter("const", a) + const = pybamm.FunctionParameter("const", {"a": a}) processed_const = parameter_values.process_symbol(const) self.assertIsInstance(processed_const, pybamm.Scalar) self.assertEqual(processed_const.evaluate(), 254) @@ -315,14 +321,19 @@ def test_process_function_parameter(self): # process differentiated function parameter diff_func = func.diff(a) processed_diff_func = parameter_values.process_symbol(diff_func) - self.assertEqual(processed_diff_func.evaluate(u={"a": 3}), 123) + self.assertEqual(processed_diff_func.evaluate(inputs={"a": 3}), 123) + + # function parameter that returns a python float + func = pybamm.FunctionParameter("float_func", {"a": a}) + processed_func = parameter_values.process_symbol(func) + self.assertEqual(processed_func.evaluate(), 42) # function itself as input (different to the variable being an input) parameter_values = pybamm.ParameterValues({"func": "[input]"}) a = pybamm.Scalar(3) - func = pybamm.FunctionParameter("func", a) + func = pybamm.FunctionParameter("func", {"a": a}) processed_func = parameter_values.process_symbol(func) - self.assertEqual(processed_func.evaluate(u={"func": 13}), 13) + self.assertEqual(processed_func.evaluate(inputs={"func": 13}), 13) def test_process_inline_function_parameters(self): def D(c): @@ -331,15 +342,15 @@ def D(c): parameter_values = pybamm.ParameterValues({"Diffusivity": D}) a = pybamm.InputParameter("a") - func = pybamm.FunctionParameter("Diffusivity", a) + func = pybamm.FunctionParameter("Diffusivity", {"a": a}) processed_func = parameter_values.process_symbol(func) - self.assertEqual(processed_func.evaluate(u={"a": 3}), 9) + self.assertEqual(processed_func.evaluate(inputs={"a": 3}), 9) # process differentiated function parameter diff_func = func.diff(a) processed_diff_func = parameter_values.process_symbol(diff_func) - self.assertEqual(processed_diff_func.evaluate(u={"a": 3}), 6) + self.assertEqual(processed_diff_func.evaluate(inputs={"a": 3}), 6) def test_multi_var_function_with_parameters(self): def D(a, b): @@ -362,7 +373,7 @@ def D(a, b): a = pybamm.Parameter("a") b = pybamm.Parameter("b") - func = pybamm.FunctionParameter("Diffusivity", a, b) + func = pybamm.FunctionParameter("Diffusivity", {"a": a, "b": b}) processed_func = parameter_values.process_symbol(func) self.assertEqual(processed_func.evaluate(), 3) @@ -375,7 +386,7 @@ def test_process_interpolant(self): ) a = pybamm.Parameter("a") - func = pybamm.FunctionParameter("Diffusivity", a) + func = pybamm.FunctionParameter("Diffusivity", {"a": a}) processed_func = parameter_values.process_symbol(func) self.assertIsInstance(processed_func, pybamm.Interpolant) @@ -394,20 +405,20 @@ def test_interpolant_against_function(self): "interpolation": "[data]lico2_data_example", }, path=os.path.join( - "input", "parameters", "lithium-ion", "cathodes", "lico2_Marquis2019", + "input", "parameters", "lithium-ion", "cathodes", "lico2_Marquis2019" ), check_already_exists=False, ) a = pybamm.InputParameter("a") - func = pybamm.FunctionParameter("function", a) - interp = pybamm.FunctionParameter("interpolation", a) + func = pybamm.FunctionParameter("function", {"a": a}) + interp = pybamm.FunctionParameter("interpolation", {"a": a}) processed_func = parameter_values.process_symbol(func) processed_interp = parameter_values.process_symbol(interp) np.testing.assert_array_almost_equal( - processed_func.evaluate(u={"a": 0.6}), - processed_interp.evaluate(u={"a": 0.6}), + processed_func.evaluate(inputs={"a": 0.6}), + processed_interp.evaluate(inputs={"a": 0.6}), decimal=4, ) @@ -417,8 +428,8 @@ def test_interpolant_against_function(self): processed_diff_func = parameter_values.process_symbol(diff_func) processed_diff_interp = parameter_values.process_symbol(diff_interp) np.testing.assert_array_almost_equal( - processed_diff_func.evaluate(u={"a": 0.6}), - processed_diff_interp.evaluate(u={"a": 0.6}), + processed_diff_func.evaluate(inputs={"a": 0.6}), + processed_diff_interp.evaluate(inputs={"a": 0.6}), decimal=2, ) diff --git a/tests/unit/test_parameters/test_parameters_cli.py b/tests/unit/test_parameters/test_parameters_cli.py index 469a100bd0..05bb15cf75 100644 --- a/tests/unit/test_parameters/test_parameters_cli.py +++ b/tests/unit/test_parameters/test_parameters_cli.py @@ -119,3 +119,13 @@ def test_list_params(self): # ./input/parameters/lithium-ion/cathodes/tmp_dir # but must not intefere with existing input dir if it exists # in the current dir... + + +if __name__ == "__main__": + print("Add -v for more debug output") + import sys + + if "-v" in sys.argv: + debug = True + pybamm.settings.debug_mode = True + unittest.main() diff --git a/tests/unit/test_parameters/test_standard_parameters_lead_acid.py b/tests/unit/test_parameters/test_standard_parameters_lead_acid.py index 3aa676eb04..fa4da125de 100644 --- a/tests/unit/test_parameters/test_standard_parameters_lead_acid.py +++ b/tests/unit/test_parameters/test_standard_parameters_lead_acid.py @@ -50,7 +50,7 @@ def test_parameters_defaults_lead_acid(self): def test_concatenated_parameters(self): # create - s_param = pybamm.standard_parameters_lead_acid.s + s_param = pybamm.standard_parameters_lead_acid.s_plus_S self.assertIsInstance(s_param, pybamm.Concatenation) self.assertEqual( s_param.domain, ["negative electrode", "separator", "positive electrode"] diff --git a/tests/unit/test_processed_variable.py b/tests/unit/test_processed_variable.py index 79a31c9c9f..b488df8a3b 100644 --- a/tests/unit/test_processed_variable.py +++ b/tests/unit/test_processed_variable.py @@ -9,7 +9,7 @@ class TestProcessedVariable(unittest.TestCase): - def test_processed_variable_1D(self): + def test_processed_variable_0D(self): # without space t = pybamm.t y = pybamm.StateVector(slice(0, 1)) @@ -20,7 +20,7 @@ def test_processed_variable_1D(self): processed_var = pybamm.ProcessedVariable(var, pybamm.Solution(t_sol, y_sol)) np.testing.assert_array_equal(processed_var.entries, t_sol * y_sol[0]) - def test_processed_variable_2D(self): + def test_processed_variable_1D(self): t = pybamm.t var = pybamm.Variable("var", domain=["negative electrode", "separator"]) x = pybamm.SpatialVariable("x", domain=["negative electrode", "separator"]) @@ -60,7 +60,7 @@ def test_processed_variable_2D(self): x_s_edge.entries[:, 0], processed_x_s_edge.entries[:, 0] ) - def test_processed_variable_2D_unknown_domain(self): + def test_processed_variable_1D_unknown_domain(self): x = pybamm.SpatialVariable("x", domain="SEI layer", coord_sys="cartesian") geometry = pybamm.Geometry() geometry.add_domain( @@ -86,7 +86,7 @@ def test_processed_variable_2D_unknown_domain(self): c.mesh = mesh["SEI layer"] pybamm.ProcessedVariable(c, solution) - def test_processed_variable_3D_x_r(self): + def test_processed_variable_2D_x_r(self): var = pybamm.Variable( "var", domain=["negative particle"], @@ -111,7 +111,7 @@ def test_processed_variable_3D_x_r(self): np.reshape(y_sol, [len(r_sol), len(x_sol), len(t_sol)]), ) - def test_processed_variable_3D_x_z(self): + def test_processed_variable_2D_x_z(self): var = pybamm.Variable( "var", domain=["negative electrode", "separator"], @@ -151,7 +151,7 @@ def test_processed_variable_3D_x_z(self): x_s_edge.entries.flatten(), processed_x_s_edge.entries[:, :, 0].T.flatten() ) - def test_processed_variable_3D_scikit(self): + def test_processed_variable_2D_scikit(self): var = pybamm.Variable("var", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() @@ -159,7 +159,6 @@ def test_processed_variable_3D_scikit(self): y = disc.mesh["current collector"][0].edges["y"] z = disc.mesh["current collector"][0].edges["z"] var_sol = disc.process_symbol(var) - var_sol.mesh = disc.mesh["current collector"] t_sol = np.linspace(0, 1) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] * np.linspace(0, 5) @@ -168,7 +167,7 @@ def test_processed_variable_3D_scikit(self): processed_var.entries, np.reshape(u_sol, [len(y), len(z), len(t_sol)]) ) - def test_processed_variable_2Dspace_scikit(self): + def test_processed_variable_2D_fixed_t_scikit(self): var = pybamm.Variable("var", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() @@ -176,7 +175,6 @@ def test_processed_variable_2Dspace_scikit(self): y = disc.mesh["current collector"][0].edges["y"] z = disc.mesh["current collector"][0].edges["z"] var_sol = disc.process_symbol(var) - var_sol.mesh = disc.mesh["current collector"] t_sol = np.array([0]) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] @@ -185,7 +183,7 @@ def test_processed_variable_2Dspace_scikit(self): processed_var.entries, np.reshape(u_sol, [len(y), len(z)]) ) - def test_processed_var_1D_interpolation(self): + def test_processed_var_0D_interpolation(self): # without spatial dependence t = pybamm.t y = pybamm.StateVector(slice(0, 1)) @@ -212,7 +210,7 @@ def test_processed_var_1D_interpolation(self): np.testing.assert_array_equal(processed_eqn(2), np.nan) pybamm.set_logging_level("WARNING") - def test_processed_var_2D_interpolation(self): + def test_processed_var_1D_interpolation(self): t = pybamm.t var = pybamm.Variable("var", domain=["negative electrode", "separator"]) x = pybamm.SpatialVariable("x", domain=["negative electrode", "separator"]) @@ -263,7 +261,7 @@ def test_processed_var_2D_interpolation(self): processed_r_n(0, r=np.linspace(0, 1))[:, 0], np.linspace(0, 1) ) - def test_processed_var_3D_interpolation(self): + def test_processed_var_2D_interpolation(self): var = pybamm.Variable( "var", domain=["negative particle"], @@ -326,7 +324,7 @@ def test_processed_var_3D_interpolation(self): processed_var(t_sol, x_sol, r_sol).shape, (10, 35, 50) ) - def test_processed_var_3D_secondary_broadcast(self): + def test_processed_var_2D_secondary_broadcast(self): var = pybamm.Variable("var", domain=["negative particle"]) broad_var = pybamm.SecondaryBroadcast(var, "negative electrode") x = pybamm.SpatialVariable("x", domain=["negative electrode"]) @@ -379,7 +377,7 @@ def test_processed_var_3D_secondary_broadcast(self): processed_var(t_sol, x_sol, r_sol).shape, (10, 35, 50) ) - def test_processed_var_3D_scikit_interpolation(self): + def test_processed_var_2D_scikit_interpolation(self): var = pybamm.Variable("var", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() @@ -387,7 +385,6 @@ def test_processed_var_3D_scikit_interpolation(self): y_sol = disc.mesh["current collector"][0].edges["y"] z_sol = disc.mesh["current collector"][0].edges["z"] var_sol = disc.process_symbol(var) - var_sol.mesh = disc.mesh["current collector"] t_sol = np.linspace(0, 1) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] * np.linspace(0, 5) @@ -417,7 +414,7 @@ def test_processed_var_3D_scikit_interpolation(self): # 3 scalars np.testing.assert_array_equal(processed_var(0.2, y=0.2, z=0.2).shape, ()) - def test_processed_var_2Dspace_scikit_interpolation(self): + def test_processed_var_2D_fixed_t_scikit_interpolation(self): var = pybamm.Variable("var", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() @@ -425,7 +422,6 @@ def test_processed_var_2Dspace_scikit_interpolation(self): y_sol = disc.mesh["current collector"][0].edges["y"] z_sol = disc.mesh["current collector"][0].edges["z"] var_sol = disc.process_symbol(var) - var_sol.mesh = disc.mesh["current collector"] t_sol = np.array([0]) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] @@ -532,6 +528,22 @@ def test_solution_too_short(self): ): pybamm.ProcessedVariable(var, pybamm.Solution(t_sol, y_sol)) + def test_3D_raises_error(self): + var = pybamm.Variable( + "var", + domain=["negative electrode"], + auxiliary_domains={"secondary": ["current collector"]}, + ) + + disc = tests.get_2p1d_discretisation_for_testing() + disc.set_variable_slices([var]) + var_sol = disc.process_symbol(var) + t_sol = np.array([0, 1, 2]) + u_sol = np.ones(var_sol.shape[0] * 3)[:, np.newaxis] + + with self.assertRaisesRegex(NotImplementedError, "Shape not recognized"): + pybamm.ProcessedVariable(var_sol, pybamm.Solution(t_sol, u_sol)) + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_quick_plot.py b/tests/unit/test_quick_plot.py index 2abe645568..a9b0779f76 100644 --- a/tests/unit/test_quick_plot.py +++ b/tests/unit/test_quick_plot.py @@ -37,6 +37,12 @@ def test_simple_ode_model(self): "c broadcasted positive electrode": pybamm.PrimaryBroadcast( c, "positive particle" ), + "x [m]": pybamm.standard_spatial_vars.x, + "x": pybamm.standard_spatial_vars.x, + "r_n [m]": pybamm.standard_spatial_vars.r_n, + "r_n": pybamm.standard_spatial_vars.r_n, + "r_p [m]": pybamm.standard_spatial_vars.r_p, + "r_p": pybamm.standard_spatial_vars.r_p, } # ODEs only (don't use jacobian) @@ -53,26 +59,35 @@ def test_simple_ode_model(self): solver = model.default_solver t_eval = np.linspace(0, 2, 100) solution = solver.solve(model, t_eval) - quick_plot = pybamm.QuickPlot(solution) + quick_plot = pybamm.QuickPlot( + solution, + [ + "a", + "b broadcasted", + "c broadcasted", + "b broadcasted negative electrode", + "c broadcasted positive electrode", + ], + ) quick_plot.plot(0) # update the axis new_axis = [0, 0.5, 0, 1] - quick_plot.axis.update({("a",): new_axis}) - self.assertEqual(quick_plot.axis[("a",)], new_axis) + quick_plot.axis_limits.update({("a",): new_axis}) + self.assertEqual(quick_plot.axis_limits[("a",)], new_axis) # and now reset them quick_plot.reset_axis() - self.assertNotEqual(quick_plot.axis[("a",)], new_axis) + self.assertNotEqual(quick_plot.axis_limits[("a",)], new_axis) # check dynamic plot loads quick_plot.dynamic_plot(testing=True) - quick_plot.update(0.01) + quick_plot.slider_update(0.01) # Test with different output variables quick_plot = pybamm.QuickPlot(solution, ["b broadcasted"]) - self.assertEqual(len(quick_plot.axis), 1) + self.assertEqual(len(quick_plot.axis_limits), 1) quick_plot.plot(0) quick_plot = pybamm.QuickPlot( @@ -85,39 +100,166 @@ def test_simple_ode_model(self): "c broadcasted positive electrode", ], ) - self.assertEqual(len(quick_plot.axis), 5) + self.assertEqual(len(quick_plot.axis_limits), 5) quick_plot.plot(0) # update the axis new_axis = [0, 0.5, 0, 1] var_key = ("c broadcasted",) - quick_plot.axis.update({var_key: new_axis}) - self.assertEqual(quick_plot.axis[var_key], new_axis) + quick_plot.axis_limits.update({var_key: new_axis}) + self.assertEqual(quick_plot.axis_limits[var_key], new_axis) # and now reset them quick_plot.reset_axis() - self.assertNotEqual(quick_plot.axis[var_key], new_axis) + self.assertNotEqual(quick_plot.axis_limits[var_key], new_axis) # check dynamic plot loads quick_plot.dynamic_plot(testing=True) - quick_plot.update(0.01) + quick_plot.slider_update(0.01) # Test longer name model.variables["Variable with a very long name"] = model.variables["a"] - quick_plot = pybamm.QuickPlot(solution) + quick_plot = pybamm.QuickPlot(solution, ["Variable with a very long name"]) quick_plot.plot(0) - # Test errors - with self.assertRaisesRegex(ValueError, "mismatching variable domains"): - pybamm.QuickPlot(solution, [["a", "b broadcasted"]]) - model.variables["3D variable"] = disc.process_symbol( + # Test different inputs + quick_plot = pybamm.QuickPlot( + [solution, solution], + ["a"], + colors=["r", "g", "b"], + linestyles=["-", "--"], + figsize=(1, 2), + labels=["sol 1", "sol 2"], + ) + self.assertEqual(quick_plot.colors, ["r", "g", "b"]) + self.assertEqual(quick_plot.linestyles, ["-", "--"]) + self.assertEqual(quick_plot.figsize, (1, 2)) + self.assertEqual(quick_plot.labels, ["sol 1", "sol 2"]) + + # Test different time units + quick_plot = pybamm.QuickPlot(solution, ["a"]) + self.assertEqual(quick_plot.time_scale, 1) + quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="seconds") + self.assertEqual(quick_plot.time_scale, 1) + quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="minutes") + self.assertEqual(quick_plot.time_scale, 1 / 60) + quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="hours") + self.assertEqual(quick_plot.time_scale, 1 / 3600) + with self.assertRaisesRegex(ValueError, "time unit"): + pybamm.QuickPlot(solution, ["a"], time_unit="bad unit") + # long solution defaults to hours instead of seconds + solution_long = solver.solve(model, np.linspace(0, 1e5)) + quick_plot = pybamm.QuickPlot(solution_long, ["a"]) + self.assertEqual(quick_plot.time_scale, 1 / 3600) + + # Test different spatial units + quick_plot = pybamm.QuickPlot(solution, ["a"]) + self.assertEqual(quick_plot.spatial_unit, "$\mu m$") + quick_plot = pybamm.QuickPlot(solution, ["a"], spatial_unit="m") + self.assertEqual(quick_plot.spatial_unit, "m") + quick_plot = pybamm.QuickPlot(solution, ["a"], spatial_unit="mm") + self.assertEqual(quick_plot.spatial_unit, "mm") + quick_plot = pybamm.QuickPlot(solution, ["a"], spatial_unit="um") + self.assertEqual(quick_plot.spatial_unit, "$\mu m$") + with self.assertRaisesRegex(ValueError, "spatial unit"): + pybamm.QuickPlot(solution, ["a"], spatial_unit="bad unit") + + # Test 2D variables + model.variables["2D variable"] = disc.process_symbol( pybamm.FullBroadcast( 1, "negative particle", {"secondary": "negative electrode"} ) ) - with self.assertRaisesRegex(NotImplementedError, "cannot plot 3D variables"): - pybamm.QuickPlot(solution, ["3D variable"]) + quick_plot = pybamm.QuickPlot(solution, ["2D variable"]) + quick_plot.plot(0) + quick_plot.dynamic_plot(testing=True) + quick_plot.slider_update(0.01) + + with self.assertRaisesRegex(NotImplementedError, "Cannot plot 2D variables"): + pybamm.QuickPlot([solution, solution], ["2D variable"]) + + # Test different variable limits + quick_plot = pybamm.QuickPlot( + solution, ["a", ["c broadcasted", "c broadcasted"]], variable_limits="tight" + ) + self.assertEqual(quick_plot.axis_limits[("a",)][2:], [None, None]) + self.assertEqual( + quick_plot.axis_limits[("c broadcasted", "c broadcasted")][2:], [None, None] + ) + quick_plot.plot(0) + quick_plot.slider_update(1) + + quick_plot = pybamm.QuickPlot( + solution, ["2D variable"], variable_limits="tight" + ) + self.assertEqual(quick_plot.variable_limits[("2D variable",)], (None, None)) + quick_plot.plot(0) + quick_plot.slider_update(1) + + quick_plot = pybamm.QuickPlot( + solution, + ["a", ["c broadcasted", "c broadcasted"]], + variable_limits={"a": [1, 2], ("c broadcasted", "c broadcasted"): [3, 4]}, + ) + self.assertEqual(quick_plot.axis_limits[("a",)][2:], [1, 2]) + self.assertEqual( + quick_plot.axis_limits[("c broadcasted", "c broadcasted")][2:], [3, 4] + ) + quick_plot.plot(0) + quick_plot.slider_update(1) + + quick_plot = pybamm.QuickPlot( + solution, ["a", "b broadcasted"], variable_limits={"a": "tight"} + ) + self.assertEqual(quick_plot.axis_limits[("a",)][2:], [None, None]) + self.assertNotEqual( + quick_plot.axis_limits[("b broadcasted",)][2:], [None, None] + ) + quick_plot.plot(0) + quick_plot.slider_update(1) + + with self.assertRaisesRegex( + TypeError, "variable_limits must be 'fixed', 'tight', or a dict" + ): + pybamm.QuickPlot( + solution, ["a", "b broadcasted"], variable_limits="bad variable limits" + ) + + # Test errors + with self.assertRaisesRegex(ValueError, "Mismatching variable domains"): + pybamm.QuickPlot(solution, [["a", "b broadcasted"]]) + with self.assertRaisesRegex(ValueError, "labels"): + pybamm.QuickPlot( + [solution, solution], ["a"], labels=["sol 1", "sol 2", "sol 3"] + ) + + # Remove 'x [m]' from the variables and make sure a key error is raise + del solution.model.variables["x [m]"] + with self.assertRaisesRegex( + KeyError, "Can't find spatial scale for 'negative electrode'", + ): + pybamm.QuickPlot(solution, ["b broadcasted"]) + + # No variable can be NaN + model.variables["NaN variable"] = disc.process_symbol(pybamm.Scalar(np.nan)) + with self.assertRaisesRegex( + ValueError, "All-NaN variable 'NaN variable' provided" + ): + pybamm.QuickPlot(solution, ["NaN variable"]) + + def test_spm_simulation(self): + # SPM + model = pybamm.lithium_ion.SPM() + sim = pybamm.Simulation(model) + + t_eval = np.linspace(0, 10, 2) + sim.solve(t_eval) + + # mixed simulation and solution input + # solution should be extracted from the simulation + quick_plot = pybamm.QuickPlot([sim, sim.solution]) + quick_plot.plot(0) def test_loqs_spm_base(self): t_eval = np.linspace(0, 10, 2) @@ -142,11 +284,53 @@ def test_loqs_spm_base(self): output_variables = [ "X-averaged negative particle concentration [mol.m-3]", "X-averaged positive particle concentration [mol.m-3]", + "Negative particle concentration [mol.m-3]", + "Positive particle concentration [mol.m-3]", ] pybamm.QuickPlot(solution, output_variables) + def test_plot_2plus1D_spm(self): + spm = pybamm.lithium_ion.SPM( + {"current collector": "potential pair", "dimensionality": 2} + ) + geometry = spm.default_geometry + param = spm.default_parameter_values + param.process_model(spm) + param.process_geometry(geometry) + var = pybamm.standard_spatial_vars + var_pts = { + var.x_n: 5, + var.x_s: 5, + var.x_p: 5, + var.r_n: 5, + var.r_p: 5, + var.y: 5, + var.z: 5, + } + mesh = pybamm.Mesh(geometry, spm.default_submesh_types, var_pts) + disc_spm = pybamm.Discretisation(mesh, spm.default_spatial_methods) + disc_spm.process_model(spm) + t_eval = np.linspace(0, 3600, 100) + solution_spm = spm.default_solver.solve(spm, t_eval) + + quick_plot = pybamm.QuickPlot( + solution_spm, + [ + "Negative current collector potential [V]", + "Positive current collector potential [V]", + "Terminal voltage [V]", + ], + ) + quick_plot.dynamic_plot(testing=True) + quick_plot.slider_update(1) + + with self.assertRaisesRegex(NotImplementedError, "Shape not recognized for"): + pybamm.QuickPlot( + solution_spm, ["Negative particle concentration [mol.m-3]"], + ) + def test_failure(self): - with self.assertRaisesRegex(TypeError, "'solutions' must be"): + with self.assertRaisesRegex(TypeError, "solutions must be"): pybamm.QuickPlot(1) diff --git a/tests/unit/test_solvers/test_algebraic_solver.py b/tests/unit/test_solvers/test_algebraic_solver.py index 772426bb6d..b4ef18ecff 100644 --- a/tests/unit/test_solvers/test_algebraic_solver.py +++ b/tests/unit/test_solvers/test_algebraic_solver.py @@ -35,53 +35,64 @@ def test_wrong_solver(self): def test_simple_root_find(self): # Simple system: a single algebraic equation - def algebraic(y): - return y + 2 + class Model: + y0 = np.array([2]) + jacobian_eval = None + convert_to_format = "python" + + def algebraic_eval(self, t, y, inputs): + return y + 2 solver = pybamm.AlgebraicSolver() - y0 = np.array([2]) - solution = solver.root(algebraic, y0) + model = Model() + solution = solver._integrate(model, np.array([0])) np.testing.assert_array_equal(solution.y, -2) def test_root_find_fail(self): - def algebraic(y): - # algebraic equation has no real root - return y ** 2 + 1 + class Model: + y0 = np.array([2]) + jacobian_eval = None + convert_to_format = "casadi" - solver = pybamm.AlgebraicSolver(method="hybr") - y0 = np.array([2]) + def algebraic_eval(self, t, y, inputs): + # algebraic equation has no real root + return y ** 2 + 1 + model = Model() + + solver = pybamm.AlgebraicSolver(method="hybr") with self.assertRaisesRegex( pybamm.SolverError, "Could not find acceptable solution: The iteration is not making", ): - solver.root(algebraic, y0) + solver._integrate(model, np.array([0])) + solver = pybamm.AlgebraicSolver() with self.assertRaisesRegex( pybamm.SolverError, "Could not find acceptable solution: solver terminated" ): - solver.root(algebraic, y0) + solver._integrate(model, np.array([0])) def test_with_jacobian(self): A = np.array([[4, 3], [1, -1]]) b = np.array([0, 7]) - def algebraic(y): - return A @ y - b + class Model: + y0 = np.zeros(2) + convert_to_format = "python" + + def algebraic_eval(self, t, y, inputs): + return A @ y - b - def jac(y): - return A + def jacobian_eval(self, t, y, inputs): + return A - y0 = np.zeros(2) + model = Model() sol = np.array([3, -4])[:, np.newaxis] solver = pybamm.AlgebraicSolver() - - solution_no_jac = solver.root(algebraic, y0) - solution_with_jac = solver.root(algebraic, y0, jacobian=jac) - - np.testing.assert_array_almost_equal(solution_no_jac.y, sol) - np.testing.assert_array_almost_equal(solution_with_jac.y, sol) + solution = solver._integrate(model, np.array([0])) + np.testing.assert_array_almost_equal(solution.y, sol) def test_model_solver(self): # Create model @@ -117,6 +128,50 @@ def test_model_solver(self): model.variables["var2"].evaluate(t=None, y=solution_no_jac.y), sol[100:] ) + def test_model_solver_with_time(self): + # Create model + model = pybamm.BaseModel() + var1 = pybamm.Variable("var1") + var2 = pybamm.Variable("var2") + model.algebraic = {var1: var1 - 3 * pybamm.t, var2: 2 * var1 - var2} + model.initial_conditions = {var1: pybamm.Scalar(1), var2: pybamm.Scalar(4)} + model.variables = {"var1": var1, "var2": var2} + + disc = pybamm.Discretisation() + disc.process_model(model) + + # Solve + t_eval = np.linspace(0, 1) + solver = pybamm.AlgebraicSolver() + solution = solver.solve(model, t_eval) + + sol = np.vstack((3 * t_eval, 6 * t_eval)) + np.testing.assert_array_equal(solution.y, sol) + np.testing.assert_array_equal( + model.variables["var1"].evaluate(t=t_eval, y=solution.y).flatten(), + sol[0, :], + ) + np.testing.assert_array_equal( + model.variables["var2"].evaluate(t=t_eval, y=solution.y).flatten(), + sol[1, :], + ) + + def test_solve_with_input(self): + # Simple system: a single algebraic equation + var = pybamm.Variable("var") + model = pybamm.BaseModel() + model.algebraic = {var: var + pybamm.InputParameter("value")} + model.initial_conditions = {var: 2} + + # create discretisation + disc = pybamm.Discretisation() + disc.process_model(model) + + # Solve + solver = pybamm.AlgebraicSolver() + solution = solver.solve(model, np.linspace(0, 1, 10), inputs={"value": 7}) + np.testing.assert_array_equal(solution.y, -7) + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index 7ab105982f..ab051a9d06 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -28,6 +28,18 @@ def test_step_or_solve_empty_model(self): with self.assertRaisesRegex(pybamm.ModelError, "Cannot solve empty model"): solver.solve(model, None) + def test_t_eval_none(self): + model = pybamm.BaseModel() + v = pybamm.Variable("v") + model.rhs = {v: 1} + model.initial_conditions = {v: 1} + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.BaseSolver() + with self.assertRaisesRegex(ValueError, "t_eval cannot be None"): + solver.solve(model, None) + def test_nonmonotonic_teval(self): solver = pybamm.BaseSolver(rtol=1e-2, atol=1e-4) model = pybamm.BaseModel() @@ -56,15 +68,16 @@ def __init__(self): self.timescale = 1 t = casadi.MX.sym("t") y = casadi.MX.sym("y") - u = casadi.MX.sym("u") + p = casadi.MX.sym("p") self.casadi_algebraic = casadi.Function( - "alg", [t, y, u], [self.algebraic_eval(t, y)] + "alg", [t, y, p], [self.algebraic_eval(t, y, p)] ) + self.convert_to_format = "casadi" - def rhs_eval(self, t, y): + def rhs_eval(self, t, y, inputs): return np.array([]) - def algebraic_eval(self, t, y): + def algebraic_eval(self, t, y, inputs): return y + 2 solver = pybamm.BaseSolver(root_method="lm") @@ -87,15 +100,16 @@ def __init__(self): self.timescale = 1 t = casadi.MX.sym("t") y = casadi.MX.sym("y", vec.size) - u = casadi.MX.sym("u") + p = casadi.MX.sym("p") self.casadi_algebraic = casadi.Function( - "alg", [t, y, u], [self.algebraic_eval(t, y)] + "alg", [t, y, p], [self.algebraic_eval(t, y, p)] ) + self.convert_to_format = "casadi" - def rhs_eval(self, t, y): + def rhs_eval(self, t, y, inputs): return y[0:1] - def algebraic_eval(self, t, y): + def algebraic_eval(self, t, y, inputs): return (y[1:] - vec[1:]) ** 2 model = VectorModel() @@ -106,7 +120,7 @@ def algebraic_eval(self, t, y): np.testing.assert_array_almost_equal(init_cond, vec) # With jacobian - def jac_dense(t, y): + def jac_dense(t, y, inputs): return 2 * np.hstack([np.zeros((3, 1)), np.diag(y[1:] - vec[1:])]) model.jac_algebraic_eval = jac_dense @@ -114,7 +128,7 @@ def jac_dense(t, y): np.testing.assert_array_almost_equal(init_cond, vec) # With sparse jacobian - def jac_sparse(t, y): + def jac_sparse(t, y, inputs): return 2 * csr_matrix( np.hstack([np.zeros((3, 1)), np.diag(y[1:] - vec[1:])]) ) @@ -131,15 +145,16 @@ def __init__(self): self.timescale = 1 t = casadi.MX.sym("t") y = casadi.MX.sym("y") - u = casadi.MX.sym("u") + p = casadi.MX.sym("p") self.casadi_algebraic = casadi.Function( - "alg", [t, y, u], [self.algebraic_eval(t, y)] + "alg", [t, y, p], [self.algebraic_eval(t, y, p)] ) + self.convert_to_format = "casadi" - def rhs_eval(self, t, y): + def rhs_eval(self, t, y, inputs): return np.array([]) - def algebraic_eval(self, t, y): + def algebraic_eval(self, t, y, inputs): # algebraic equation has no root return y ** 2 + 1 @@ -164,15 +179,22 @@ def algebraic_eval(self, t, y): ): solver.calculate_consistent_state(Model()) - def test_time_too_short(self): - solver = pybamm.BaseSolver() + def test_convert_to_casadi_format(self): + # Make sure model is converted to casadi format model = pybamm.BaseModel() - v = pybamm.StateVector(slice(0, 1)) - model.rhs = {v: v} - with self.assertRaisesRegex( - pybamm.SolverError, "It looks like t_eval might be dimensionless" - ): - solver.solve(model, np.linspace(0, 0.1)) + v = pybamm.Variable("v") + model.rhs = {v: -1} + model.initial_conditions = {v: 1} + model.convert_to_format = "python" + + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.BaseSolver() + pybamm.set_logging_level("ERROR") + solver.set_up(model, {}) + self.assertEqual(model.convert_to_format, "casadi") + pybamm.set_logging_level("WARNING") if __name__ == "__main__": diff --git a/tests/unit/test_solvers/test_casadi_algebraic_solver.py b/tests/unit/test_solvers/test_casadi_algebraic_solver.py new file mode 100644 index 0000000000..9a542a3545 --- /dev/null +++ b/tests/unit/test_solvers/test_casadi_algebraic_solver.py @@ -0,0 +1,111 @@ +# +# Tests for the Casadi Algebraic Solver class +# +import casadi +import pybamm +import unittest +import numpy as np + + +class TestCasadiAlgebraicSolver(unittest.TestCase): + def test_algebraic_solver_init(self): + solver = pybamm.CasadiAlgebraicSolver(tol=1e-4) + self.assertEqual(solver.tol, 1e-4) + + solver.tol = 1e-5 + self.assertEqual(solver.tol, 1e-5) + + def test_simple_root_find(self): + # Simple system: a single algebraic equation + var = pybamm.Variable("var") + model = pybamm.BaseModel() + model.algebraic = {var: var + 2} + model.initial_conditions = {var: 2} + + # create discretisation + disc = pybamm.Discretisation() + disc.process_model(model) + + # Solve + solver = pybamm.CasadiAlgebraicSolver() + solution = solver.solve(model, np.linspace(0, 1, 10)) + np.testing.assert_array_equal(solution.y, -2) + + def test_root_find_fail(self): + class Model: + y0 = np.array([2]) + t = casadi.MX.sym("t") + y = casadi.MX.sym("y") + p = casadi.MX.sym("p") + casadi_algebraic = casadi.Function("alg", [t, y, p], [y ** 2 + 1]) + + def algebraic_eval(self, t, y, inputs): + # algebraic equation has no real root + return y ** 2 + 1 + + model = Model() + + solver = pybamm.CasadiAlgebraicSolver() + with self.assertRaisesRegex( + pybamm.SolverError, "Could not find acceptable solution: .../casadi", + ): + solver._integrate(model, np.array([0]), {}) + solver = pybamm.CasadiAlgebraicSolver(error_on_fail=False) + with self.assertRaisesRegex( + pybamm.SolverError, "Could not find acceptable solution: solver terminated", + ): + solver._integrate(model, np.array([0]), {}) + + def test_model_solver_with_time(self): + # Create model + model = pybamm.BaseModel() + var1 = pybamm.Variable("var1") + var2 = pybamm.Variable("var2") + model.algebraic = {var1: var1 - 3 * pybamm.t, var2: 2 * var1 - var2} + model.initial_conditions = {var1: pybamm.Scalar(1), var2: pybamm.Scalar(4)} + model.variables = {"var1": var1, "var2": var2} + + disc = pybamm.Discretisation() + disc.process_model(model) + + # Solve + t_eval = np.linspace(0, 1) + solver = pybamm.CasadiAlgebraicSolver() + solution = solver.solve(model, t_eval) + + sol = np.vstack((3 * t_eval, 6 * t_eval)) + np.testing.assert_array_almost_equal(solution.y, sol) + np.testing.assert_array_almost_equal( + model.variables["var1"].evaluate(t=t_eval, y=solution.y).flatten(), + sol[0, :], + ) + np.testing.assert_array_almost_equal( + model.variables["var2"].evaluate(t=t_eval, y=solution.y).flatten(), + sol[1, :], + ) + + def test_solve_with_input(self): + # Simple system: a single algebraic equation + var = pybamm.Variable("var") + model = pybamm.BaseModel() + model.algebraic = {var: var + pybamm.InputParameter("value")} + model.initial_conditions = {var: 2} + + # create discretisation + disc = pybamm.Discretisation() + disc.process_model(model) + + # Solve + solver = pybamm.CasadiAlgebraicSolver() + solution = solver.solve(model, np.linspace(0, 1, 10), inputs={"value": 7}) + np.testing.assert_array_equal(solution.y, -7) + + +if __name__ == "__main__": + print("Add -v for more debug output") + import sys + + if "-v" in sys.argv: + debug = True + pybamm.settings.debug_mode = True + unittest.main() diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 7319768462..6148a33c6a 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -309,6 +309,22 @@ def test_model_solver_with_non_identity_mass(self): np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) + def test_dae_solver_algebraic_model(self): + model = pybamm.BaseModel() + var = pybamm.Variable("var") + model.algebraic = {var: var + 1} + model.initial_conditions = {var: 0} + + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.CasadiSolver() + t_eval = np.linspace(0, 1) + with self.assertRaisesRegex( + pybamm.SolverError, "Cannot use CasadiSolver to solve algebraic model" + ): + solver.solve(model, t_eval) + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_solvers/test_dummy_solver.py b/tests/unit/test_solvers/test_dummy_solver.py new file mode 100644 index 0000000000..091da3ca27 --- /dev/null +++ b/tests/unit/test_solvers/test_dummy_solver.py @@ -0,0 +1,52 @@ +# +# Tests for the Dummy Solver class +# +import pybamm +import numpy as np +import unittest +import sys + + +class TestDummySolver(unittest.TestCase): + def test_dummy_solver(self): + model = pybamm.BaseModel() + v = pybamm.Scalar(1) + model.variables = {"v": v} + + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.DummySolver() + t_eval = np.linspace(0, 1) + sol = solver.solve(model, t_eval) + np.testing.assert_array_equal(sol.t, t_eval) + np.testing.assert_array_equal(sol.y, np.zeros((1, t_eval.size))) + np.testing.assert_array_equal(sol["v"].data, np.ones(t_eval.size)) + + def test_dummy_solver_step(self): + model = pybamm.BaseModel() + v = pybamm.Scalar(1) + model.variables = {"v": v} + + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.DummySolver() + t_eval = np.linspace(0, 1) + + sol = None + for dt in np.diff(t_eval): + sol = solver.step(sol, model, dt) + + np.testing.assert_array_equal(sol.t, t_eval) + np.testing.assert_array_equal(sol.y, np.zeros((1, t_eval.size))) + np.testing.assert_array_equal(sol["v"].data, np.ones(t_eval.size)) + + +if __name__ == "__main__": + print("Add -v for more debug output") + + if "-v" in sys.argv: + debug = True + pybamm.settings.debug_mode = True + unittest.main() diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 703673ae8e..b440d29a38 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -82,6 +82,20 @@ def test_failures(self): with self.assertRaisesRegex(pybamm.SolverError, "KLU requires the Jacobian"): solver.solve(model, t_eval) + def test_dae_solver_algebraic_model(self): + model = pybamm.BaseModel() + var = pybamm.Variable("var") + model.algebraic = {var: var + 1} + model.initial_conditions = {var: 0} + + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.IDAKLUSolver() + t_eval = np.linspace(0, 1) + solution = solver.solve(model, t_eval) + np.testing.assert_array_equal(solution.y, -1) + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_solvers/test_scikits_solvers.py b/tests/unit/test_solvers/test_scikits_solvers.py index 905507c692..369be82926 100644 --- a/tests/unit/test_solvers/test_scikits_solvers.py +++ b/tests/unit/test_solvers/test_scikits_solvers.py @@ -41,11 +41,12 @@ class Model: y0 = np.array([0.0, 1.0]) terminate_events_eval = [] timescale_eval = 1 + convert_to_format = "python" - def residuals_eval(self, t, y, ydot): + def residuals_eval(self, t, y, ydot, inputs): return np.array([0.5 * np.ones_like(y[0]) - ydot[0], 2 * y[0] - y[1]]) - def jacobian_eval(self, t, y): + def jacobian_eval(self, t, y, inputs): return np.array([[0.0, 0.0], [2.0, -1.0]]) model = Model() @@ -89,13 +90,14 @@ class Model: y0 = np.array([0.0, 0.0]) terminate_events_eval = [] timescale_eval = 1 + convert_to_format = "python" - def residuals_eval(self, t, y, ydot): + def residuals_eval(self, t, y, ydot, inputs): return np.array( [0.5 * np.ones_like(y[0]) - 4 * ydot[0], 2.0 * y[0] - y[1]] ) - def jacobian_eval(self, t, y): + def jacobian_eval(self, t, y, inputs): return np.array([[0.0, 0.0], [2.0, -1.0]]) model = Model() @@ -262,7 +264,9 @@ def nonsmooth_mult(t): rate = pybamm.Function(nonsmooth_rate, pybamm.t) mult = pybamm.Function(nonsmooth_mult, pybamm.t) - model.rhs = {var1: rate * var1} + # put in an extra heaviside with no time dependence, this should be ignored by + # the solver i.e. no extra discontinuities added + model.rhs = {var1: rate * var1 + (var1 < 0)} model.algebraic = {var2: mult * var1 - var2} model.initial_conditions = {var1: 1, var2: 2} model.events = [ @@ -630,9 +634,7 @@ def nonsmooth_rate(t): model2.rhs = {var1: (0.1 * (pybamm.t < discontinuity) + 0.1) * var1} model2.algebraic = {var2: var2} model2.initial_conditions = {var1: 1, var2: 0} - model2.events = [ - pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), - ] + model2.events = [pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5))] # third model implicitly adds a discontinuity event via another heaviside # function @@ -640,9 +642,7 @@ def nonsmooth_rate(t): model3.rhs = {var1: (-0.1 * (discontinuity < pybamm.t) + 0.2) * var1} model3.algebraic = {var2: var2} model3.initial_conditions = {var1: 1, var2: 0} - model3.events = [ - pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), - ] + model3.events = [pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5))] for model in [model1, model2, model3]: @@ -696,10 +696,23 @@ def test_ode_solver_fail_with_dae(self): with self.assertRaisesRegex(pybamm.SolverError, "Cannot use ODE solver"): solver.set_up(model) + def test_dae_solver_algebraic_model(self): + model = pybamm.BaseModel() + var = pybamm.Variable("var") + model.algebraic = {var: var + 1} + model.initial_conditions = {var: 0} + + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.ScikitsDaeSolver() + t_eval = np.linspace(0, 1) + solution = solver.solve(model, t_eval) + np.testing.assert_array_equal(solution.y, -1) + if __name__ == "__main__": print("Add -v for more debug output") - if "-v" in sys.argv: debug = True pybamm.set_logging_level("DEBUG") diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index fe064b64ca..76a9c25076 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -27,7 +27,7 @@ def test_model_solver_python(self): disc.process_model(model) # Solve solver = pybamm.ScipySolver(rtol=1e-8, atol=1e-8, method="RK45") - t_eval = np.linspace(0, 1, 100) + t_eval = np.linspace(0, 1, 80) solution = solver.solve(model, t_eval) np.testing.assert_array_equal(solution.t, t_eval) np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) @@ -69,7 +69,12 @@ def test_model_solver_with_event_python(self): var = pybamm.Variable("var", domain=domain) model.rhs = {var: -0.1 * var} model.initial_conditions = {var: 1} - model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] + # needs to work with multiple events (to avoid bug where only last event is + # used) + model.events = [ + pybamm.Event("var=0.5", pybamm.min(var - 0.5)), + pybamm.Event("var=-0.5", pybamm.min(var + 0.5)), + ] # No need to set parameters; can use base discretisation (no spatial operators) # create discretisation @@ -236,7 +241,12 @@ def test_model_solver_with_event_with_casadi(self): var = pybamm.Variable("var", domain=domain) model.rhs = {var: -0.1 * var} model.initial_conditions = {var: 1} - model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] + # needs to work with multiple events (to avoid bug where only last event is + # used) + model.events = [ + pybamm.Event("var=0.5", pybamm.min(var - 0.5)), + pybamm.Event("var=-0.5", pybamm.min(var + 0.5)), + ] # No need to set parameters; can use base discretisation (no spatial # operators) diff --git a/tests/unit/test_solvers/test_solution.py b/tests/unit/test_solvers/test_solution.py index 0fa8a33013..c1615e2f8d 100644 --- a/tests/unit/test_solvers/test_solution.py +++ b/tests/unit/test_solvers/test_solution.py @@ -125,7 +125,7 @@ def test_save(self): # to csv with self.assertRaisesRegex( - ValueError, "only 1D variables can be saved to csv" + ValueError, "only 0D variables can be saved to csv" ): solution.save_data("test.csv", to_format="csv") # only save "c" and "2c" diff --git a/tests/unit/test_spatial_methods/test_base_spatial_method.py b/tests/unit/test_spatial_methods/test_base_spatial_method.py index 708d3c73d2..475c3aa91f 100644 --- a/tests/unit/test_spatial_methods/test_base_spatial_method.py +++ b/tests/unit/test_spatial_methods/test_base_spatial_method.py @@ -50,9 +50,9 @@ def test_discretise_spatial_variable(self): ) # edges - x1_edge = pybamm.SpatialVariable("x_edge", ["negative electrode"]) - x2_edge = pybamm.SpatialVariable("x_edge", ["negative electrode", "separator"]) - r_edge = pybamm.SpatialVariable("r_edge", ["negative particle"]) + x1_edge = pybamm.SpatialVariableEdge("x", ["negative electrode"]) + x2_edge = pybamm.SpatialVariableEdge("x", ["negative electrode", "separator"]) + r_edge = pybamm.SpatialVariableEdge("r", ["negative particle"]) for var in [x1_edge, x2_edge, r_edge]: var_disc = spatial_method.spatial_variable(var) self.assertIsInstance(var_disc, pybamm.Vector) diff --git a/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py b/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py index af36bdcfc5..c80a83a9d3 100644 --- a/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py +++ b/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py @@ -811,6 +811,25 @@ def test_indefinite_integral(self): left_boundary_value_disc.evaluate(y=phi_exact), 0 ) + # -------------------------------------------------------------------- + # indefinite integral of a spatial variable + x = pybamm.SpatialVariable("x", ["negative electrode", "separator"]) + x_edge = pybamm.SpatialVariableEdge("x", ["negative electrode", "separator"]) + int_x = pybamm.IndefiniteIntegral(x, x) + int_x_edge = pybamm.IndefiniteIntegral(x_edge, x) + + x_disc = disc.process_symbol(x) + x_edge_disc = disc.process_symbol(x_edge) + int_x_disc = disc.process_symbol(int_x) + int_x_edge_disc = disc.process_symbol(int_x_edge) + + np.testing.assert_almost_equal( + int_x_disc.evaluate(), x_edge_disc.evaluate() ** 2 / 2 + ) + np.testing.assert_almost_equal( + int_x_edge_disc.evaluate(), x_disc.evaluate() ** 2 / 2, decimal=4 + ) + # -------------------------------------------------------------------- # micrsoscale case c = pybamm.Variable("c", domain=["negative particle"]) @@ -853,20 +872,52 @@ def test_indefinite_integral(self): left_boundary_value_disc.evaluate(y=c_exact), 0 ) + def test_indefinite_integral_of_broadcasted_to_cell_edges(self): + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = { + "macroscale": pybamm.FiniteVolume(), + "negative particle": pybamm.FiniteVolume(), + "positive particle": pybamm.FiniteVolume(), + "current collector": pybamm.ZeroDimensionalMethod(), + } + disc = pybamm.Discretisation(mesh, spatial_methods) + + # make a variable 'phi' and a vector 'i' which is broadcast onto edges + # the integral of this should then be put onto the nodes + phi = pybamm.Variable("phi", domain=["negative electrode", "separator"]) + i = pybamm.PrimaryBroadcastToEdges(1, phi.domain) + x = pybamm.SpatialVariable("x", phi.domain) + disc.set_variable_slices([phi]) + combined_submesh = mesh.combine_submeshes("negative electrode", "separator") + x_end = combined_submesh[0].edges[-1] + + # take indefinite integral + int_phi = pybamm.IndefiniteIntegral(i * phi, x) + # take integral again + int_int_phi = pybamm.Integral(int_phi, x) + int_int_phi_disc = disc.process_symbol(int_int_phi) + + # constant case + phi_exact = np.ones_like(combined_submesh[0].nodes) + phi_approx = int_int_phi_disc.evaluate(None, phi_exact) + np.testing.assert_array_equal(x_end ** 2 / 2, phi_approx) + + # linear case + phi_exact = combined_submesh[0].nodes[:, np.newaxis] + phi_approx = int_int_phi_disc.evaluate(None, phi_exact) + np.testing.assert_array_almost_equal(x_end ** 3 / 6, phi_approx, decimal=4) + def test_indefinite_integral_on_nodes(self): mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) - # input a phi, take grad, then integrate to recover phi approximation - # (need to test this way as check evaluated on edges using if has grad - # and no div) phi = pybamm.Variable("phi", domain=["negative electrode", "separator"]) - x = pybamm.SpatialVariable("x", ["negative electrode", "separator"]) + int_phi = pybamm.IndefiniteIntegral(phi, x) disc.set_variable_slices([phi]) - # Set boundary conditions (required for shape but don't matter) int_phi_disc = disc.process_symbol(int_phi) combined_submesh = mesh.combine_submeshes("negative electrode", "separator") @@ -1117,6 +1168,22 @@ def test_delta_function(self): np.sum(delta_fn_int_disc.evaluate(y=y)), ) + def test_heaviside(self): + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + + var = pybamm.Variable("var", domain="negative electrode") + heav = var > 1 + + disc.set_variable_slices([var]) + # process_binary_operators should work with heaviside + disc_heav = disc.process_symbol(heav * var) + nodes = mesh["negative electrode"][0].nodes + self.assertEqual(disc_heav.size, nodes.size) + np.testing.assert_array_equal(disc_heav.evaluate(y=2 * np.ones_like(nodes)), 2) + np.testing.assert_array_equal(disc_heav.evaluate(y=-2 * np.ones_like(nodes)), 0) + def test_grad_div_with_bcs_on_tab(self): # 2d macroscale mesh = get_1p1d_mesh_for_testing() diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py index 646cc33af5..b4b7f6203e 100644 --- a/tests/unit/test_util.py +++ b/tests/unit/test_util.py @@ -6,6 +6,8 @@ import pybamm import tempfile import unittest +from unittest.mock import patch +from io import StringIO class TestUtil(unittest.TestCase): @@ -97,6 +99,34 @@ def test_get_parameters_filepath(self): self.assertTrue(pybamm.get_parameters_filepath(tempfile_obj.name) == path) +class TestSearch(unittest.TestCase): + def test_url_gets_to_stdout(self): + model = pybamm.BaseModel() + model.variables = {"Electrolyte concentration": 1, "Electrode potential": 0} + + param = pybamm.ParameterValues({"a": 10, "b": 2}) + + # Test variables search (default returns key) + with patch("sys.stdout", new=StringIO()) as fake_out: + model.variables.search("Electrode") + self.assertEqual(fake_out.getvalue(), "Electrode potential\n") + + # Test bad var search (returns best matches) + with patch("sys.stdout", new=StringIO()) as fake_out: + model.variables.search("bad var") + out = ( + "No results for search using 'bad var'. " + "Best matches are ['Electrolyte concentration', " + "'Electrode potential']\n" + ) + self.assertEqual(fake_out.getvalue(), out) + + # Test param search (default returns key, value) + with patch("sys.stdout", new=StringIO()) as fake_out: + param.search("a") + self.assertEqual(fake_out.getvalue(), "a\t10\n") + + if __name__ == "__main__": print("Add -v for more debug output") import sys