diff --git a/.gitignore b/.gitignore index 037defc395..d59641db3c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.tmp *.png /local/ +*.DS_Store # don't ignore important .txt files !requirements* diff --git a/.requirements-docs.txt b/.requirements-docs.txt index 1aa9e60102..c7bcd6446f 100644 --- a/.requirements-docs.txt +++ b/.requirements-docs.txt @@ -5,5 +5,6 @@ pandas>=0.23 anytree>=2.4.3 autograd>=1.2 scikit-fem>=0.2.0 +casadi>=3.5.0 guzzle-sphinx-theme sphinx>=1.5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 07421aef02..066d81d63e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,23 +2,32 @@ ## Features +- Added Simulation class ([#693](https://github.com/pybamm-team/PyBaMM/pull/693)) +- Added interface to CasADi solver ([#687](https://github.com/pybamm-team/PyBaMM/pull/687), [#691](https://github.com/pybamm-team/PyBaMM/pull/691), [#714](https://github.com/pybamm-team/PyBaMM/pull/714)). This makes the SUNDIALS DAE solvers (Scikits and KLU) truly optional (though IDA KLU is recommended for solving the DFN). +- Added option to use CasADi's Algorithmic Differentiation framework to calculate Jacobians ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) +- Added method to evaluate parameters more easily ([#669](https://github.com/pybamm-team/PyBaMM/pull/669)) +- Added `Jacobian` class to reuse known Jacobians of expressions ([#665](https://github.com/pybamm-team/PyBaMM/pull/670)) +- Added `Interpolant` class to interpolate experimental data (e.g. OCP curves) ([#661](https://github.com/pybamm-team/PyBaMM/pull/661)) - Added interface (via pybind11) to sundials with the IDA KLU sparse linear solver ([#657](https://github.com/pybamm-team/PyBaMM/pull/657)) -- Add method to evaluate parameters more easily ([#669](https://github.com/pybamm-team/PyBaMM/pull/669)) -- Add `Jacobian` class to reuse known Jacobians of expressions ([#665](https://github.com/pybamm-team/PyBaMM/pull/670)) -- Add `Interpolant` class to interpolate experimental data (e.g. OCP curves) ([#661](https://github.com/pybamm-team/PyBaMM/pull/661)) -- Allow parameters to be set by material or by specifying a particular paper ([#647](https://github.com/pybamm-team/PyBaMM/pull/647)) +- Allowed parameters to be set by material or by specifying a particular paper ([#647](https://github.com/pybamm-team/PyBaMM/pull/647)) - Set relative and absolute tolerances independently in solvers ([#645](https://github.com/pybamm-team/PyBaMM/pull/645)) -- Add some non-uniform meshes in 1D and 2D ([#617](https://github.com/pybamm-team/PyBaMM/pull/617)) +- Added basic method to allow (a part of) the State Vector to be updated with results obtained from another solution or package ([#624](https://github.com/pybamm-team/PyBaMM/pull/624)) +- Added some non-uniform meshes in 1D and 2D ([#617](https://github.com/pybamm-team/PyBaMM/pull/617)) ## Optimizations +- Use CasADi's automatic differentation algorithms by default when solving a model ([#714](https://github.com/pybamm-team/PyBaMM/pull/714)) - Avoid re-checking size when making a copy of an `Index` object ([#656](https://github.com/pybamm-team/PyBaMM/pull/656)) - Avoid recalculating `_evaluation_array` when making a copy of a `StateVector` object ([#653](https://github.com/pybamm-team/PyBaMM/pull/653)) ## Bug fixes -- Add warning if `ProcessedVariable` is called outisde its interpolation range ([#681](https://github.com/pybamm-team/PyBaMM/pull/681)) -- Improve the way `ProcessedVariable` objects are created in higher dimensions ([#581](https://github.com/pybamm-team/PyBaMM/pull/581)) +- Corrected a sign error in Dirichlet boundary conditions in the Finite Element Method ([#706](https://github.com/pybamm-team/PyBaMM/pull/706)) +- Passed the correct dimensional temperature to open circuit potential ([#702](https://github.com/pybamm-team/PyBaMM/pull/702)) +- Added missing temperature dependence in electrolyte and interface submodels ([#698](https://github.com/pybamm-team/PyBaMM/pull/698)) +- Fixed differentiation of functions that have more than one argument ([#687](https://github.com/pybamm-team/PyBaMM/pull/687)) +- Added warning if `ProcessedVariable` is called outside its interpolation range ([#681](https://github.com/pybamm-team/PyBaMM/pull/681)) +- Improved the way `ProcessedVariable` objects are created in higher dimensions ([#581](https://github.com/pybamm-team/PyBaMM/pull/581)) # [v0.1.0](https://github.com/pybamm-team/PyBaMM/tree/v0.1.0) - 2019-10-08 diff --git a/INSTALL-LINUX-MAC.md b/INSTALL-LINUX-MAC.md index c391c2ffce..6fedef0eda 100644 --- a/INSTALL-LINUX-MAC.md +++ b/INSTALL-LINUX-MAC.md @@ -97,11 +97,8 @@ pip uninstall pybamm ## Optional dependencies -Two DAE solvers (`scikits.odes` and `KLU`) can be optionally installed in PyBaMM. At least one of these is required to solve DAE models, such as the DFN, but you can install both if you like. - -### [scikits.odes](https://github.com/bmcage/odes) - -A python wrapper for the SUNDIALS ODE and DAE integrators. [Installation instructions](INSTALL-SCIKITS.md). +CasADi's DAE solvers are included by default in PyBaMM, but two additional DAE solvers (`scikits.odes` and `KLU`) can be optionally installed as well. +In particular, the [KLU sparse solver](INSTALL-KLU.md) is recommended for solving the DFN. ### Sundials with KLU sparse solver @@ -109,6 +106,10 @@ If you wish so simulate large systems such as the 2+1D models, we recommend empl sparse solver. PyBaMM currently offers a direct interface to the sparse KLU solver within Sundials. [Installation instructions](INSTALL-KLU.md). +### [scikits.odes](https://github.com/bmcage/odes) + +A python wrapper for the SUNDIALS ODE and DAE integrators. [Installation instructions](INSTALL-SCIKITS.md). + ## Troubleshooting **Problem:** I've made edits to source files in PyBaMM, but these are not being used diff --git a/INSTALL-SCIKITS.md b/INSTALL-SCIKITS.md index b0b800e219..df194e464f 100644 --- a/INSTALL-SCIKITS.md +++ b/INSTALL-SCIKITS.md @@ -7,7 +7,6 @@ This file provides installation instructions for either Ubuntu-based distributio --- - Users can install [scikits.odes](https://github.com/bmcage/odes) in order to use the wrapped SUNDIALS ODE and DAE [solvers](https://pybamm.readthedocs.io/en/latest/source/solvers/scikits_solvers.html). @@ -20,7 +19,7 @@ Before installing scikits.odes, you need to have installed: - Fortran compiler (e.g. gfortran, comes with gcc in brew) - BLAS/LAPACK install (OpenBLAS is recommended by the scikits.odes developers) - CMake (for building Sundials) -- Sundials 3.1.1 (see instructions below) +- Sundials 4.1.0 (see instructions below) You can install these on Ubuntu or Debian using apt-get: @@ -48,18 +47,17 @@ If this works, skip to [the final section](#setting-library-path). Otherwise, tr ## Option 2: install manually - -To install SUNDIALS 3.1.1 manually, on the command-line type: +To install SUNDIALS 4.1.0 manually, on the command-line type: ```bash INSTALL_DIR=`pwd`/sundials -wget https://computation.llnl.gov/projects/sundials/download/sundials-3.1.1.tar.gz -tar -xvf sundials-3.1.1.tar.gz -mkdir build-sundials-3.1.1 -cd build-sundials-3.1.1/ -cmake -DLAPACK_ENABLE=ON -DSUNDIALS_INDEX_TYPE=int32_t -DBUILD_ARKODE:BOOL=OFF -DEXAMPLES_ENABLE:BOOL=OFF -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR ../sundials-3.1.1/ +wget https://computation.llnl.gov/projects/sundials/download/sundials-4.1.0.tar.gz +tar -xvf sundials-4.1.0.tar.gz +mkdir build-sundials-4.1.0 +cd build-sundials-4.1.0/ +cmake -DLAPACK_ENABLE=ON -DSUNDIALS_INDEX_TYPE=int32_t -DBUILD_ARKODE:BOOL=OFF -DEXAMPLES_ENABLE:BOOL=OFF -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR ../sundials-4.1.0/ make install -rm -r ../sundials-3.1.1 +rm -r ../sundials-4.1.0 ``` Then install [scikits.odes](https://github.com/bmcage/odes), letting it know the sundials install location: diff --git a/INSTALL-WINDOWS.md b/INSTALL-WINDOWS.md index 47c425bd8c..608dfd63f8 100644 --- a/INSTALL-WINDOWS.md +++ b/INSTALL-WINDOWS.md @@ -21,7 +21,14 @@ typing sudo apt install git-core ``` -Now use git to clone the PyBaMM repository: +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 diff --git a/LICENSE.txt b/LICENSE.txt index 5002a05e3c..e63d315d14 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2017-2018, University of Oxford (University of Oxford means the Chancellor, Masters and Scholars of the University of Oxford, having an administrative office at Wellington Square, Oxford OX1 2JD, UK). +Copyright (c) 2017-2019, University of Oxford (University of Oxford means the Chancellor, Masters and Scholars of the University of Oxford, having an administrative office at Wellington Square, Oxford OX1 2JD, UK). All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index 2bb648c9a6..6b34958b56 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,17 @@ Python Battery Mathematical Modelling solves continuum models for batteries, usi ## How do I use PyBaMM? -PyBaMM comes with a number of [detailed examples](examples/notebooks/README.md), hosted here on +The easiest way to use PyBaMM is to run a 1C constant-current discharge with a model of your choice with all the default settings: +```python3 +import pybamm +model = pybamm.lithium_ion.DFN() # Doyle-Fuller-Newman model +sim = pybamm.Simulation(model) +sim.solve() +sim.plot() +``` +However, much greater customisation is available. It is possible to change the physics, parameter values, geometry, submesh type, number of submesh points, methods for spatial discretisation and solver for integration (see DFN [script](examples/scripts/DFN.py) or [notebook](examples/notebooks/models/dfn.ipynb)). + +Further details can be found in a number of [detailed examples](examples/notebooks/README.md), hosted here on github. In addition, there is a [full API documentation](http://pybamm.readthedocs.io/), hosted on [Read The Docs](readthedocs.io). A set of slides giving an overview of PyBaMM can be found diff --git a/docs/index.rst b/docs/index.rst index 49b6913f16..d991331d6c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,6 +32,7 @@ Contents source/solvers/index source/processed_variable source/util + source/simulation Examples ======== diff --git a/docs/source/expression_tree/index.rst b/docs/source/expression_tree/index.rst index c5055334dd..3eedd7b622 100644 --- a/docs/source/expression_tree/index.rst +++ b/docs/source/expression_tree/index.rst @@ -17,6 +17,4 @@ Expression Tree broadcasts functions interpolant - evaluate - simplify - jacobian + operations/index diff --git a/docs/source/expression_tree/operations/convert_to_casadi.rst b/docs/source/expression_tree/operations/convert_to_casadi.rst new file mode 100644 index 0000000000..76a6ed614e --- /dev/null +++ b/docs/source/expression_tree/operations/convert_to_casadi.rst @@ -0,0 +1,5 @@ +Convert to CasADi +================= + +.. autoclass:: pybamm.CasadiConverter + :members: diff --git a/docs/source/expression_tree/evaluate.rst b/docs/source/expression_tree/operations/evaluate.rst similarity index 100% rename from docs/source/expression_tree/evaluate.rst rename to docs/source/expression_tree/operations/evaluate.rst diff --git a/docs/source/expression_tree/operations/index.rst b/docs/source/expression_tree/operations/index.rst new file mode 100644 index 0000000000..31004a2204 --- /dev/null +++ b/docs/source/expression_tree/operations/index.rst @@ -0,0 +1,11 @@ +Operations on expression trees +============================== + +Classes and functions that operate on the expression tree + +.. toctree:: + + simplify + evaluate + jacobian + convert_to_casadi diff --git a/docs/source/expression_tree/jacobian.rst b/docs/source/expression_tree/operations/jacobian.rst similarity index 100% rename from docs/source/expression_tree/jacobian.rst rename to docs/source/expression_tree/operations/jacobian.rst diff --git a/docs/source/expression_tree/simplify.rst b/docs/source/expression_tree/operations/simplify.rst similarity index 100% rename from docs/source/expression_tree/simplify.rst rename to docs/source/expression_tree/operations/simplify.rst diff --git a/docs/source/models/submodels/current_collector/index.rst b/docs/source/models/submodels/current_collector/index.rst index caf1d70df5..3ae2821bfd 100644 --- a/docs/source/models/submodels/current_collector/index.rst +++ b/docs/source/models/submodels/current_collector/index.rst @@ -11,5 +11,4 @@ Current Collector potential_pair quite_conductive_potential_pair single_particle_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 new file mode 100644 index 0000000000..b87ebbf42e --- /dev/null +++ b/docs/source/models/submodels/current_collector/set_potential_single_particle.rst @@ -0,0 +1,11 @@ +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/thermal/x_lumped/index.rst b/docs/source/models/submodels/thermal/x_lumped/index.rst index 3df689d0ea..aaad69755f 100644 --- a/docs/source/models/submodels/thermal/x_lumped/index.rst +++ b/docs/source/models/submodels/thermal/x_lumped/index.rst @@ -9,3 +9,4 @@ 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 new file mode 100644 index 0000000000..ea31df59d9 --- /dev/null +++ b/docs/source/models/submodels/thermal/x_lumped/x_lumped_1D_set_temperature.rst @@ -0,0 +1,5 @@ +Set Temperature 1D current collector +==================================== + +.. autoclass:: pybamm.thermal.x_lumped.SetTemperature1D + :members: diff --git a/docs/source/simulation.rst b/docs/source/simulation.rst new file mode 100644 index 0000000000..a50a67eb21 --- /dev/null +++ b/docs/source/simulation.rst @@ -0,0 +1,5 @@ +Simulation +========== + +.. autoclass:: pybamm.Simulation + :members: \ No newline at end of file diff --git a/docs/source/solvers/casadi_solver.rst b/docs/source/solvers/casadi_solver.rst new file mode 100644 index 0000000000..f21e3c74cf --- /dev/null +++ b/docs/source/solvers/casadi_solver.rst @@ -0,0 +1,5 @@ +Casadi Solver +============= + +.. autoclass:: pybamm.CasadiSolver + :members: diff --git a/docs/source/solvers/index.rst b/docs/source/solvers/index.rst index 6cc6c0021d..d85865dec7 100644 --- a/docs/source/solvers/index.rst +++ b/docs/source/solvers/index.rst @@ -7,4 +7,5 @@ Solvers base_solvers scipy_solver scikits_solvers + casadi_solver solution diff --git a/examples/notebooks/change-input-current.ipynb b/examples/notebooks/change-input-current.ipynb index 7a1f0f7e9b..b4c6305de5 100644 --- a/examples/notebooks/change-input-current.ipynb +++ b/examples/notebooks/change-input-current.ipynb @@ -22,7 +22,7 @@ "\n", "In this notebook we will use the SPM as the example model, and change the input current from the default option. If you are not familiar with running a model in PyBaMM, please see [this](./models/SPM.ipynb) notebook for more details.\n", "\n", - "In PyBaMM, the current function is set using the parameter \"Current function\". By default this is set to be a constant current provided by the class [`pybamm.GetConstantCurrent`](https://pybamm.readthedocs.io/en/latest/source/parameters/standard_current_functions/get_constant_current.html). This class takes a single optional argument \"current\" which is the size of the current in Amperes. If no argument is passed, the value of the current is set by the parameter \"Typical current [A]\" (see [this](parameter-values.ipynb) notebook for more information about parameters).\n", + "In PyBaMM, the current function is set using the parameter \"Current function\". By default this is set to be a constant current provided by the class [`pybamm.ConstantCurrent`](https://pybamm.readthedocs.io/en/latest/source/parameters/standard_current_functions/get_constant_current.html). This class takes a single optional argument \"current\" which is the size of the current in Amperes. If no argument is passed, the value of the current is set by the parameter \"Typical current [A]\" (see [this](parameter-values.ipynb) notebook for more information about parameters).\n", "\n", "In general it is recommended to change the size of a constant current input by changing the parameter \"Typical current [A]\", since this value is used to non-dimensionlise the model. Below we load the SPM with the default parameters, and then change the the typical current 16A. We then explicilty set the current function to be a constant current." ] @@ -49,14 +49,14 @@ "\n", "# change the typical current and set a constant discharge using the typical current value\n", "param[\"Typical current [A]\"] = 16\n", - "param[\"Current function\"] = pybamm.GetConstantCurrent()\n" + "param[\"Current function\"] = pybamm.ConstantCurrent()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "However, you may wish to change the value of the constant current without altering the typical current value used for non-dimensionlidation. For instance, the current function could be change to draw a current of 8A by updating the current function and passing the optional argument \"current\" to the class `GetConstantCurrent`" + "However, you may wish to change the value of the constant current without altering the typical current value used for non-dimensionlidation. For instance, the current function could be change to draw a current of 8A by updating the current function and passing the optional argument \"current\" to the class `ConstantCurrent`" ] }, { @@ -66,7 +66,7 @@ "outputs": [], "source": [ "# update the value of the current profile *without* changing the typical current used for non-dimensionlisation\n", - "param[\"Current function\"] = pybamm.GetConstantCurrent(current=pybamm.Scalar(8))" + "param[\"Current function\"] = pybamm.ConstantCurrent(current=pybamm.Scalar(8))" ] }, { @@ -84,7 +84,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "fc997d14787542f69f2cba123ce0b994", + "model_id": "b900a39a72704c88927058510b7913b2", "version_major": 2, "version_minor": 0 }, @@ -127,11 +127,7 @@ "source": [ "## Loading in current data \n", "\n", - "Data can be loaded in from a csv file using the class [`pybamm.GetCurrentData`](https://pybamm.readthedocs.io/en/latest/source/parameters/standard_current_functions/get_current_data). Data should be given as a function of time (in seconds) and may be dimensional (in Amperes) or non-diemsnional (e.g. C-rate). Optionally, voltage data (in Volts) may be loaded in for convinient plotting and comparison. \n", - "\n", - "The input csv files should be stored in PyBaMM/input/drive_cycles and have the headings: \"time [s]\"; either \"current [A]\" for dimensional data, or \"current []\" for dimensionless data; and, optionally, \"voltage [V]\". \n", - "\n", - "To load in dimensional data we simply provide the filename and set the units to \"[A]\", e.g." + "Data can be loaded in from a csv file by specifying the path to that file and using the prefix \"[current data]\"." ] }, { @@ -140,54 +136,30 @@ "metadata": {}, "outputs": [], "source": [ - "param[\"Current function\"] = pybamm.GetCurrentData(\"US06.csv\", units=\"[A]\")" + "param[\"Current function\"] = \"[current data]US06\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "For non dimensional data, you also need to provide a current scale. For instance, if the data were C-rate then you would set the current scale to the 1C discharge current. If the 1C discharge current were 24 A and we had some C-rate data in the file car_current.csv, this would be loaded in as" + "As an example, we show how to solve the SPM using the US06 drive cycle" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, - "outputs": [], - "source": [ - "param[\"Current function\"] = pybamm.GetCurrentData(\"car_current.csv\", units=\"[]\", current_scale=24)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As an example, we how to solve the SPM using the US06 drive cycle" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/user/Documents/PyBaMM/pybamm/parameters/standard_current_functions/get_current_data.py:88: ModelWarning: Requested time (2387.404004088835) is outside of the data range [0, 600]\n", - " pybamm.ModelWarning,\n" - ] - }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "3f4c99601ab8417ba2186987cb532600", + "model_id": "26e7b28104b34991b3ebac266dab4934", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=0.03900922998674932, step=0.001), Output()),…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=0.026526276390989537, step=0.001), Output())…" ] }, "metadata": {}, @@ -202,7 +174,7 @@ "\n", "# load parameter values and process model and geometry\n", "param = model.default_parameter_values\n", - "param[\"Current function\"] = pybamm.GetCurrentData(\"US06.csv\", units=\"[A]\")\n", + "param[\"Current function\"] = \"[current data]US06\"\n", "param.process_model(model)\n", "param.process_geometry(geometry)\n", "\n", @@ -240,14 +212,14 @@ "source": [ "## Adding your own current function \n", "\n", - "A user defined current function can be passed to any model using the class [`pybamm.GetUserCurrent`](https://pybamm.readthedocs.io/en/latest/source/parameters/standard_current_functions/get_user_current). The class takes in a method, which returns the current as a function of time, followed by any keyword arguments required by the method. \n", + "A user defined current function can be passed to any model using the class [`pybamm.UserCurrent`](https://pybamm.readthedocs.io/en/latest/source/parameters/standard_current_functions/get_user_current). The class takes in a method, which returns the current as a function of time, followed by any keyword arguments required by the method. \n", "\n", "For example, you may want to simulate a sinusoidal current with amplitude A and freqency omega. In order to do so you must first define the method" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -262,12 +234,12 @@ "source": [ "Note that time *must* me the first arguemnt of the function. The parameters may either be provided as floats, or may be one of the standard parameters provided in PyBaMM (see [here](https://pybamm.readthedocs.io/en/latest/source/parameters)). \n", "\n", - "The the model may be loaded and the \"Current function\" parameter updated to be a `GetUserCurrent` class which calls `my_fun`" + "The the model may be loaded and the \"Current function\" parameter updated to be a `UserCurrent` class which calls `my_fun`" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -282,7 +254,7 @@ "# set user defined current function\n", "A = pybamm.electrical_parameters.I_typ\n", "omega = 0.1\n", - "param[\"Current function\"] = pybamm.GetUserCurrent(my_fun, A=A, omega=omega)\n", + "param[\"Current function\"] = pybamm.UserCurrent(my_fun, A=A, omega=omega)\n", "\n", "# process model and geometry\n", "param.process_model(model)\n", @@ -293,31 +265,23 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Note that the parameters in `my_fun` were passed as keyword arguments to the `GetUserCurrent` class. The model may then be solved in the usual way" + "Note that the parameters in `my_fun` were passed as keyword arguments to the `UserCurrent` class. The model may then be solved in the usual way" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/user/Documents/PyBaMM/venv/lib/python3.6/site-packages/ipykernel_launcher.py:12: DeprecationWarning: object of type cannot be safely interpreted as an integer.\n", - " if sys.path[0] == '':\n" - ] - }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "913efe4e3e9e4b37bad2ded9c71b975e", + "model_id": "629672e99152464dad01e57ff152347a", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=0.0019504614993374662, step=9.75230749668733…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=0.0013263138195494769, step=6.63156909774738…" ] }, "metadata": {}, @@ -372,7 +336,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.8" + "version": "3.7.4" } }, "nbformat": 4, diff --git a/examples/notebooks/models/SPM.ipynb b/examples/notebooks/models/SPM.ipynb index 54bf98ee01..47a3dd06c0 100644 --- a/examples/notebooks/models/SPM.ipynb +++ b/examples/notebooks/models/SPM.ipynb @@ -638,17 +638,17 @@ "\t- Local current collector potential difference\n", "\t- Local current collector potential difference [V]\n", "\t- Ohmic heating\n", - "\t- Ohmic heating [A.V.m-3]\n", + "\t- Ohmic heating [W.m-3]\n", "\t- Irreversible electrochemical heating\n", - "\t- Irreversible electrochemical heating [A.V.m-3]\n", + "\t- Irreversible electrochemical heating [W.m-3]\n", "\t- Reversible heating\n", - "\t- Reversible heating [A.V.m-3]\n", + "\t- Reversible heating [W.m-3]\n", "\t- Total heating\n", - "\t- Total heating [A.V.m-3]\n", + "\t- Total heating [W.m-3]\n", "\t- X-averaged total heating\n", - "\t- X-averaged total heating [A.V.m-3]\n", + "\t- X-averaged total heating [W.m-3]\n", "\t- Volume-averaged total heating\n", - "\t- Volume-averaged total heating [A.V.m-3]\n", + "\t- Volume-averaged total heating [W.m-3]\n", "\t- Positive current collector potential [V]\n", "\t- Local potential difference\n", "\t- Local potential difference [V]\n", diff --git a/examples/notebooks/models/compare-lithium-ion.ipynb b/examples/notebooks/models/compare-lithium-ion.ipynb index 718009401b..076b1eb468 100644 --- a/examples/notebooks/models/compare-lithium-ion.ipynb +++ b/examples/notebooks/models/compare-lithium-ion.ipynb @@ -463,7 +463,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.8" + "version": "3.7.3" } }, "nbformat": 4, diff --git a/examples/notebooks/parameter-values.ipynb b/examples/notebooks/parameter-values.ipynb index 1a2e2b798a..0ece138286 100644 --- a/examples/notebooks/parameter-values.ipynb +++ b/examples/notebooks/parameter-values.ipynb @@ -482,7 +482,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.8" + "version": "3.7.3" } }, "nbformat": 4, diff --git a/examples/scripts/compare-dae-solver.py b/examples/scripts/compare-dae-solver.py index 05476790cf..a092a78652 100644 --- a/examples/scripts/compare-dae-solver.py +++ b/examples/scripts/compare-dae-solver.py @@ -16,9 +16,7 @@ # set mesh var = pybamm.standard_spatial_vars - -var_pts = {var.x_n: 60, var.x_s: 100, var.x_p: 60, var.r_n: 50, var.r_p: 50} -# var_pts = model.default_var_pts +var_pts = {var.x_n: 50, var.x_s: 50, var.x_p: 50, var.r_n: 20, var.r_p: 20} mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) # discretise model @@ -26,13 +24,14 @@ disc.process_model(model) # solve model -t_eval = np.linspace(0, 0.2, 100) +t_eval = np.linspace(0, 0.17, 100) -klu_sol = pybamm.IDAKLU(atol=1e-8, rtol=1e-8).solve(model, t_eval) +casadi_sol = pybamm.CasadiSolver(atol=1e-8, rtol=1e-8).solve(model, t_eval) +klu_sol = pybamm.IDAKLUSolver(atol=1e-8, rtol=1e-8).solve(model, t_eval) scikits_sol = pybamm.ScikitsDaeSolver(atol=1e-8, rtol=1e-8).solve(model, t_eval) # plot -models = [model, model] -solutions = [scikits_sol, klu_sol] +models = [model, model, model] +solutions = [casadi_sol, klu_sol, casadi_sol] plot = pybamm.QuickPlot(models, mesh, solutions) plot.dynamic_plot() diff --git a/examples/scripts/compare_lead_acid.py b/examples/scripts/compare_lead_acid.py index dc02b534dc..028a2a1629 100644 --- a/examples/scripts/compare_lead_acid.py +++ b/examples/scripts/compare_lead_acid.py @@ -41,16 +41,18 @@ # solve model solutions = [None] * len(models) -t_eval = np.linspace(0, 3, 1000) +t_eval = np.linspace(0, 1, 1000) for i, model in enumerate(models): solution = model.default_solver.solve(model, t_eval) solutions[i] = solution # plot output_variables = [ - "Electrolyte pressure", - "Electrolyte concentration", - "Volume-averaged velocity [m.s-1]", + "Interfacial current density [A.m-2]", + "Electrolyte concentration [mol.m-3]", + "Current [A]", + "Porosity", + "Electrolyte potential [V]", "Terminal voltage [V]", ] plot = pybamm.QuickPlot(models, mesh, solutions, output_variables) diff --git a/examples/scripts/compare_lead_acid_3D.py b/examples/scripts/compare_lead_acid_3D.py index b7b8cfb66a..584d027351 100644 --- a/examples/scripts/compare_lead_acid_3D.py +++ b/examples/scripts/compare_lead_acid_3D.py @@ -21,8 +21,7 @@ # {"current collector": "potential pair", "dimensionality": 2}, name="2+1D LOQS" # ), pybamm.lead_acid.Full( - {"current collector": "potential pair", "dimensionality": 1}, - name="1+1D Full", + {"current collector": "potential pair", "dimensionality": 1}, name="1+1D Full" ), # pybamm.lead_acid.Full( # {"dimensionality": 1}, name="1+1D uniform Full" diff --git a/examples/scripts/compare_lithium_ion.py b/examples/scripts/compare_lithium_ion.py index 1aca4c7816..d92fb46f84 100644 --- a/examples/scripts/compare_lithium_ion.py +++ b/examples/scripts/compare_lithium_ion.py @@ -45,7 +45,7 @@ # solve model solutions = [None] * len(models) -t_eval = np.linspace(0, 0.17, 100) +t_eval = np.linspace(0, 0.3, 100) for i, model in enumerate(models): solutions[i] = model.default_solver.solve(model, t_eval) diff --git a/examples/scripts/compare_lithium_ion_3D.py b/examples/scripts/compare_lithium_ion_3D.py index e62fc7697b..f271b63cf1 100644 --- a/examples/scripts/compare_lithium_ion_3D.py +++ b/examples/scripts/compare_lithium_ion_3D.py @@ -18,12 +18,10 @@ # load models models = [ pybamm.lithium_ion.SPM( - {"current collector": "potential pair", "dimensionality": 2}, - name="2+1D 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", + {"current collector": "potential pair", "dimensionality": 2}, name="2+1D SPMe" ), ] diff --git a/examples/scripts/run_simulation.py b/examples/scripts/run_simulation.py new file mode 100644 index 0000000000..09e2f3ed42 --- /dev/null +++ b/examples/scripts/run_simulation.py @@ -0,0 +1,7 @@ +import pybamm + +model = pybamm.lithium_ion.SPM() + +sim = pybamm.Simulation(model) +sim.solve() +sim.plot() diff --git a/examples/scripts/thermal_lithium_ion.py b/examples/scripts/thermal_lithium_ion.py index 13f3826eb2..79747040d8 100644 --- a/examples/scripts/thermal_lithium_ion.py +++ b/examples/scripts/thermal_lithium_ion.py @@ -18,7 +18,7 @@ # load parameter values and process models and geometry param = models[0].default_parameter_values -param.update({"Heat transfer coefficient [W.m-2.K-1]": 0.1}) +param.update({"Heat transfer coefficient [W.m-2.K-1]": 1}) for model in models: param.process_model(model) @@ -40,7 +40,8 @@ solutions = [None] * len(models) t_eval = np.linspace(0, 0.17, 100) for i, model in enumerate(models): - solution = model.default_solver.solve(model, t_eval) + solver = pybamm.ScipySolver(atol=1e-8, rtol=1e-8) + solution = solver.solve(model, t_eval) solutions[i] = solution # plot diff --git a/input/drive_cycles/US06.csv b/input/drive_cycles/US06.csv index 287fd07020..e534a5db0f 100644 --- a/input/drive_cycles/US06.csv +++ b/input/drive_cycles/US06.csv @@ -1,603 +1,603 @@ # Based on the US06 drive cycle, -current [A],time [s] -0.012859,0 -0.012859,1 -0.012859,2 -0.012859,3 -0.012859,4 -0.012859,5 -0.013668,6 -0.023524,7 -0.03126,8 -0.050602,9 -0.58049,10 -2.6467,11 -3.8424,12 -4.1121,13 --0.23677,14 -3.1954,15 -4.134,16 -2.8259,17 -2.5489,18 -2.6931,19 -2.3144,20 -3.5036,21 -2.7033,22 -1.057,23 --0.44999,24 --0.4451,25 --1.8319,26 --0.6912,27 --0.41631,28 -0.39479,29 -1.4094,30 --0.067283,31 -0.40082,32 --1.6765,33 --3.0094,34 --2.9658,35 --2.8139,36 --2.1029,37 --1.7886,38 --1.0515,39 --0.33154,40 --0.00027238,41 -0.012859,42 -0.012859,43 -0.012859,44 -0.012859,45 -0.012859,46 -0.012859,47 -0.012859,48 -0.026246,49 -1.4175,50 -2.3404,51 -1.9203,52 -2.8051,53 -4.2382,54 -4.2743,55 -2.8574,56 -4.2758,57 -4.9662,58 -4.8662,59 -3.8788,60 -2.521,61 -3.5651,62 -3.3848,63 -3.6869,64 -3.1176,65 -2.8487,66 -2.1836,67 -0.18726,68 -0.91501,69 -0.0053402,70 --0.37207,71 --0.37225,72 -0.33741,73 -1.0426,74 -0.33741,75 --0.37219,76 --0.96565,77 -0.13474,78 -0.46979,79 -0.46799,80 -0.80472,81 -0.80717,82 -1.4932,83 -2.2074,84 -3.3239,85 -4.1708,86 -4.5251,87 -4.3083,88 -4.2568,89 -4.8156,90 -5.2028,91 -4.5034,92 -4.6358,93 -3.3841,94 -1.5935,95 -1.3703,96 -0.22134,97 --2.5008,98 --1.8485,99 --1.6727,100 --1.0737,101 --1.065,102 --1.4701,103 --1.7157,104 --1.5508,105 --1.6473,106 --1.3644,107 --1.5766,108 --1.4219,109 --1.4997,110 --1.242,111 --1.4213,112 --1.9789,113 --2.4495,114 --1.771,115 --0.67,116 --0.65407,117 --0.79789,118 --3.6676,119 --3.2481,120 --2.1471,121 --1.5972,122 --1.3075,123 --0.80837,124 --0.29408,125 --0.039143,126 --0.012101,127 --0.00027238,128 -0.012859,129 -0.012859,130 -0.012859,131 -0.012859,132 -0.012859,133 -0.012859,134 -0.012859,135 -0.13367,136 -1.3164,137 -2.9506,138 -4.3345,139 -5.7205,140 -5.4552,141 -5.8237,142 -6.0151,143 -3.7866,144 -2.7655,145 -2.3944,146 -2.948,147 -1.8924,148 -2.5999,149 -1.8149,150 -1.3287,151 -1.3425,152 -2.057,153 -0.67879,154 -0.67879,155 -2.0956,156 -3.7768,157 -1.8459,158 -3.0051,159 -1.3509,160 -3.1006,161 -1.4034,162 -1.4141,163 -1.4248,164 --0.50076,165 --1.2951,166 -0.40453,167 -0.21185,168 --0.11201,169 -1.1354,170 --0.62707,171 -1.4858,172 -0.011324,173 -0.55864,174 -0.18866,175 -1.1018,176 -1.1079,177 -0.37419,178 -0.18726,179 -0.5484,180 -0.54637,181 --0.86914,182 --0.24991,183 -1.3988,184 --2.4102,185 -5.0098,186 -2.5027,187 -3.1156,188 -2.6317,189 -0.96709,190 -3.2753,191 -1.7871,192 -2.998,193 -1.6541,194 -1.8721,195 -2.0978,196 -0.89478,197 -1.3038,198 --0.078976,199 -1.4977,200 -2.1259,201 -0.089321,202 -1.1053,203 -1.3141,204 -3.1926,205 -0.52137,206 -1.3521,207 -1.3591,208 -1.3661,209 -1.7966,210 -1.8134,211 -2.4758,212 -0.995,213 -1.8602,214 --2.6906,215 --0.067612,216 -1.5503,217 -1.3521,218 -0.10446,219 -2.1829,220 -0.73539,221 -0.31463,222 -1.5617,223 --0.49752,224 -2.1685,225 -0.7278,226 -1.1428,227 -0.51918,228 -1.3486,229 --0.35495,230 -1.124,231 -1.9596,232 -1.1428,233 -1.5655,234 -1.9998,235 -1.5964,236 --0.93064,237 -1.3556,238 --0.92625,239 -2.1543,240 --0.21363,241 -2.1543,242 -0.51263,243 --0.07259,244 -1.1146,245 -0.70538,246 -1.5276,247 -1.124,248 -0.7128,249 --0.49887,250 -0.89478,251 -1.7142,252 -0.70048,253 --0.21884,254 -1.2937,255 -0.68831,256 -0.88929,257 -1.093,258 -0.28185,259 -1.698,260 -1.5089,261 --0.078194,262 -0.88929,263 -1.093,264 --0.081297,265 -2.7118,266 -0.90306,267 --0.078194,268 -1.9111,269 -0.084907,270 -0.28185,271 -0.47868,272 -0.67631,273 -1.4829,274 -0.88383,275 -0.88383,276 -0.6811,277 -1.694,278 -0.48492,279 -0.88656,280 -1.2937,281 -1.5051,282 -0.69559,283 -3.1671,284 -2.1829,285 -1.9998,286 -1.5964,287 -2.6805,288 -1.4233,289 -2.9549,290 -1.4636,291 -1.6914,292 -2.8152,293 -1.0654,294 -2.1852,295 -1.757,296 -1.7695,297 -6.4018,298 -2.1159,299 -7.9136,300 --2.9379,301 -0.26072,302 -1.1932,303 -0.72206,304 -2.3667,305 -1.9156,306 -0.97617,307 --0.62823,308 -0.24381,309 -0.46769,310 -1.156,311 -0.69207,312 -1.3812,313 -1.1527,314 -1.6173,315 -3.2663,316 -2.3718,317 -3.5953,318 -4.151,319 -3.4975,320 -5.3167,321 -4.1756,322 -3.7387,323 --0.94696,324 --2.8522,325 -0.33794,326 -3.3004,327 -4.3626,328 -2.4058,329 -4.2276,330 -2.4839,331 -2.505,332 -1.4794,333 -2.5263,334 -1.4955,335 --0.40618,336 -1.4633,337 -0.42687,338 -0.41621,339 --0.94842,340 --0.95058,341 --2.8125,342 --1.9414,343 --2.0775,344 --4.2071,345 --1.091,346 -1.6593,347 --0.19359,348 -0.5667,349 -1.6394,350 -0.78484,351 -3.174,352 -1.6874,353 -3.0325,354 -1.0654,355 -1.9606,356 --0.023889,357 -0.83956,358 -0.83676,359 --0.63943,360 --0.34073,361 -0.35759,362 -0.13671,363 -1.6315,364 -1.8602,365 -0.14178,366 -1.6434,367 -2.5283,368 -2.1238,369 -1.9253,370 -1.4972,371 -1.9517,372 -1.9695,373 -0.41043,374 -2.8783,375 -1.1003,376 -1.1003,377 -2.4667,378 -2.0372,379 -1.5935,380 -1.8329,381 --0.15832,382 -1.8201,383 -0.44887,384 --0.16389,385 -1.5661,386 -1.5739,387 --0.0085663,388 -0.42601,389 -2.4615,390 -0.66037,391 -0.65518,392 -2.0145,393 -1.8031,394 -1.1263,395 -1.1263,396 -2.9716,397 -0.68939,398 -1.3775,399 -0.68671,400 -1.6054,401 -1.8458,402 -1.3922,403 -0.0053429,404 -2.074,405 -2.3266,406 -1.6454,407 -2.8354,408 -0.49679,409 -1.1966,410 -0.72483,411 -0.95438,412 -0.9513,413 -2.8354,414 -1.682,415 -2.1679,416 --0.95265,417 --0.14664,418 --0.15257,419 -0.00062616,420 -1.363,421 -2.0601,422 --0.31705,423 -2.9716,424 --0.47505,425 -1.3558,426 --1.1047,427 -0.41485,428 --0.17703,429 -1.2852,430 -0.84237,431 --0.029129,432 -0.16447,433 -1.4748,434 -1.2612,435 -1.0436,436 -0.16268,437 -0.59278,438 --0.34126,439 -1.6553,440 -0.1452,441 -1.4342,442 --0.046997,443 -1.4233,444 --0.19793,445 --4.0792,446 -0.88656,447 --0.50003,448 -0.26455,449 -1.4573,450 -0.26455,451 -1.4573,452 -0.86493,453 -1.67,454 -0.87569,455 -0.87569,456 -0.87569,457 -1.686,458 -0.076253,459 -1.686,460 -1.7021,461 -0.081997,462 -1.7021,463 -0.081997,464 -1.7021,465 -0.89753,466 -1.7183,467 -0.087843,468 -0.081997,469 --0.9152,470 --1.5834,471 --0.36787,472 --2.0567,473 --0.87672,474 --1.4815,475 --2.6241,476 --2.178,477 --0.036534,478 --0.90616,479 --2.4373,480 --2.9694,481 --3.5147,482 --1.8457,483 --0.98341,484 --2.4483,485 --3.8294,486 --1.9441,487 --0.048456,488 --1.0641,489 --1.0035,490 --0.66563,491 --0.32669,492 --0.027036,493 -0.012859,494 -0.012859,495 -0.012859,496 -0.012859,497 -0.012859,498 -0.012859,499 -0.012859,500 -0.013668,501 -0.34446,502 -1.4125,503 -2.4117,504 -3.237,505 -3.4134,506 -2.4722,507 -0.71203,508 --0.68697,509 --0.9946,510 --1.1241,511 --2.2309,512 --1.7637,513 --0.84058,514 -0.20754,515 -1.1729,516 -1.9691,517 -2.7371,518 -3.2787,519 -2.6509,520 -1.5214,521 --0.0058875,522 --1.3985,523 --1.9241,524 --1.808,525 --1.6276,526 --0.95015,527 --0.20711,528 --0.2798,529 --0.026526,530 -0.059693,531 -0.71684,532 -1.6183,533 -2.4505,534 -3.2363,535 -3.4634,536 -1.8132,537 -0.52817,538 --0.37846,539 --1.398,540 --1.9658,541 --1.8804,542 --1.5482,543 --0.64195,544 --0.15352,545 --0.0033494,546 -1.1079,547 -1.9138,548 -2.325,549 -3.0374,550 -3.3647,551 -3.7312,552 -0.086116,553 --2.2017,554 --2.3495,555 --2.0339,556 --1.4035,557 --0.91224,558 --0.37758,559 --0.056562,560 -0.012859,561 -0.012859,562 -0.012859,563 -0.012859,564 -0.012859,565 -0.012859,566 -0.012859,567 -0.014561,568 -0.70309,569 -2.0434,570 -3.5113,571 -3.4062,572 -3.9559,573 -7.1727,574 -3.4216,575 -3.9417,576 -6.9749,577 -8.1,578 -3.415,579 --0.024227,580 --0.25555,581 --0.3689,582 --1.9022,583 --2.9537,584 --2.95,585 --2.7289,586 --3.271,587 --3.5216,588 --2.3332,589 --2.2084,590 --2.0047,591 --1.3769,592 --0.34366,593 --0.035978,594 -0.012859,595 -0.012859,596 -0.012859,597 -0.012859,598 -0.012859,599 -0.012859,600 +# time [s],current [A] +0,0.012859 +1,0.012859 +2,0.012859 +3,0.012859 +4,0.012859 +5,0.012859 +6,0.013668 +7,0.023524 +8,0.03126 +9,0.050602 +10,0.58049 +11,2.6467 +12,3.8424 +13,4.1121 +14,-0.23677 +15,3.1954 +16,4.134 +17,2.8259 +18,2.5489 +19,2.6931 +20,2.3144 +21,3.5036 +22,2.7033 +23,1.057 +24,-0.44999 +25,-0.4451 +26,-1.8319 +27,-0.6912 +28,-0.41631 +29,0.39479 +30,1.4094 +31,-0.067283 +32,0.40082 +33,-1.6765 +34,-3.0094 +35,-2.9658 +36,-2.8139 +37,-2.1029 +38,-1.7886 +39,-1.0515 +40,-0.33154 +41,-0.00027238 +42,0.012859 +43,0.012859 +44,0.012859 +45,0.012859 +46,0.012859 +47,0.012859 +48,0.012859 +49,0.026246 +50,1.4175 +51,2.3404 +52,1.9203 +53,2.8051 +54,4.2382 +55,4.2743 +56,2.8574 +57,4.2758 +58,4.9662 +59,4.8662 +60,3.8788 +61,2.521 +62,3.5651 +63,3.3848 +64,3.6869 +65,3.1176 +66,2.8487 +67,2.1836 +68,0.18726 +69,0.91501 +70,0.0053402 +71,-0.37207 +72,-0.37225 +73,0.33741 +74,1.0426 +75,0.33741 +76,-0.37219 +77,-0.96565 +78,0.13474 +79,0.46979 +80,0.46799 +81,0.80472 +82,0.80717 +83,1.4932 +84,2.2074 +85,3.3239 +86,4.1708 +87,4.5251 +88,4.3083 +89,4.2568 +90,4.8156 +91,5.2028 +92,4.5034 +93,4.6358 +94,3.3841 +95,1.5935 +96,1.3703 +97,0.22134 +98,-2.5008 +99,-1.8485 +100,-1.6727 +101,-1.0737 +102,-1.065 +103,-1.4701 +104,-1.7157 +105,-1.5508 +106,-1.6473 +107,-1.3644 +108,-1.5766 +109,-1.4219 +110,-1.4997 +111,-1.242 +112,-1.4213 +113,-1.9789 +114,-2.4495 +115,-1.771 +116,-0.67 +117,-0.65407 +118,-0.79789 +119,-3.6676 +120,-3.2481 +121,-2.1471 +122,-1.5972 +123,-1.3075 +124,-0.80837 +125,-0.29408 +126,-0.039143 +127,-0.012101 +128,-0.00027238 +129,0.012859 +130,0.012859 +131,0.012859 +132,0.012859 +133,0.012859 +134,0.012859 +135,0.012859 +136,0.13367 +137,1.3164 +138,2.9506 +139,4.3345 +140,5.7205 +141,5.4552 +142,5.8237 +143,6.0151 +144,3.7866 +145,2.7655 +146,2.3944 +147,2.948 +148,1.8924 +149,2.5999 +150,1.8149 +151,1.3287 +152,1.3425 +153,2.057 +154,0.67879 +155,0.67879 +156,2.0956 +157,3.7768 +158,1.8459 +159,3.0051 +160,1.3509 +161,3.1006 +162,1.4034 +163,1.4141 +164,1.4248 +165,-0.50076 +166,-1.2951 +167,0.40453 +168,0.21185 +169,-0.11201 +170,1.1354 +171,-0.62707 +172,1.4858 +173,0.011324 +174,0.55864 +175,0.18866 +176,1.1018 +177,1.1079 +178,0.37419 +179,0.18726 +180,0.5484 +181,0.54637 +182,-0.86914 +183,-0.24991 +184,1.3988 +185,-2.4102 +186,5.0098 +187,2.5027 +188,3.1156 +189,2.6317 +190,0.96709 +191,3.2753 +192,1.7871 +193,2.998 +194,1.6541 +195,1.8721 +196,2.0978 +197,0.89478 +198,1.3038 +199,-0.078976 +200,1.4977 +201,2.1259 +202,0.089321 +203,1.1053 +204,1.3141 +205,3.1926 +206,0.52137 +207,1.3521 +208,1.3591 +209,1.3661 +210,1.7966 +211,1.8134 +212,2.4758 +213,0.995 +214,1.8602 +215,-2.6906 +216,-0.067612 +217,1.5503 +218,1.3521 +219,0.10446 +220,2.1829 +221,0.73539 +222,0.31463 +223,1.5617 +224,-0.49752 +225,2.1685 +226,0.7278 +227,1.1428 +228,0.51918 +229,1.3486 +230,-0.35495 +231,1.124 +232,1.9596 +233,1.1428 +234,1.5655 +235,1.9998 +236,1.5964 +237,-0.93064 +238,1.3556 +239,-0.92625 +240,2.1543 +241,-0.21363 +242,2.1543 +243,0.51263 +244,-0.07259 +245,1.1146 +246,0.70538 +247,1.5276 +248,1.124 +249,0.7128 +250,-0.49887 +251,0.89478 +252,1.7142 +253,0.70048 +254,-0.21884 +255,1.2937 +256,0.68831 +257,0.88929 +258,1.093 +259,0.28185 +260,1.698 +261,1.5089 +262,-0.078194 +263,0.88929 +264,1.093 +265,-0.081297 +266,2.7118 +267,0.90306 +268,-0.078194 +269,1.9111 +270,0.084907 +271,0.28185 +272,0.47868 +273,0.67631 +274,1.4829 +275,0.88383 +276,0.88383 +277,0.6811 +278,1.694 +279,0.48492 +280,0.88656 +281,1.2937 +282,1.5051 +283,0.69559 +284,3.1671 +285,2.1829 +286,1.9998 +287,1.5964 +288,2.6805 +289,1.4233 +290,2.9549 +291,1.4636 +292,1.6914 +293,2.8152 +294,1.0654 +295,2.1852 +296,1.757 +297,1.7695 +298,6.4018 +299,2.1159 +300,7.9136 +301,-2.9379 +302,0.26072 +303,1.1932 +304,0.72206 +305,2.3667 +306,1.9156 +307,0.97617 +308,-0.62823 +309,0.24381 +310,0.46769 +311,1.156 +312,0.69207 +313,1.3812 +314,1.1527 +315,1.6173 +316,3.2663 +317,2.3718 +318,3.5953 +319,4.151 +320,3.4975 +321,5.3167 +322,4.1756 +323,3.7387 +324,-0.94696 +325,-2.8522 +326,0.33794 +327,3.3004 +328,4.3626 +329,2.4058 +330,4.2276 +331,2.4839 +332,2.505 +333,1.4794 +334,2.5263 +335,1.4955 +336,-0.40618 +337,1.4633 +338,0.42687 +339,0.41621 +340,-0.94842 +341,-0.95058 +342,-2.8125 +343,-1.9414 +344,-2.0775 +345,-4.2071 +346,-1.091 +347,1.6593 +348,-0.19359 +349,0.5667 +350,1.6394 +351,0.78484 +352,3.174 +353,1.6874 +354,3.0325 +355,1.0654 +356,1.9606 +357,-0.023889 +358,0.83956 +359,0.83676 +360,-0.63943 +361,-0.34073 +362,0.35759 +363,0.13671 +364,1.6315 +365,1.8602 +366,0.14178 +367,1.6434 +368,2.5283 +369,2.1238 +370,1.9253 +371,1.4972 +372,1.9517 +373,1.9695 +374,0.41043 +375,2.8783 +376,1.1003 +377,1.1003 +378,2.4667 +379,2.0372 +380,1.5935 +381,1.8329 +382,-0.15832 +383,1.8201 +384,0.44887 +385,-0.16389 +386,1.5661 +387,1.5739 +388,-0.0085663 +389,0.42601 +390,2.4615 +391,0.66037 +392,0.65518 +393,2.0145 +394,1.8031 +395,1.1263 +396,1.1263 +397,2.9716 +398,0.68939 +399,1.3775 +400,0.68671 +401,1.6054 +402,1.8458 +403,1.3922 +404,0.0053429 +405,2.074 +406,2.3266 +407,1.6454 +408,2.8354 +409,0.49679 +410,1.1966 +411,0.72483 +412,0.95438 +413,0.9513 +414,2.8354 +415,1.682 +416,2.1679 +417,-0.95265 +418,-0.14664 +419,-0.15257 +420,0.00062616 +421,1.363 +422,2.0601 +423,-0.31705 +424,2.9716 +425,-0.47505 +426,1.3558 +427,-1.1047 +428,0.41485 +429,-0.17703 +430,1.2852 +431,0.84237 +432,-0.029129 +433,0.16447 +434,1.4748 +435,1.2612 +436,1.0436 +437,0.16268 +438,0.59278 +439,-0.34126 +440,1.6553 +441,0.1452 +442,1.4342 +443,-0.046997 +444,1.4233 +445,-0.19793 +446,-4.0792 +447,0.88656 +448,-0.50003 +449,0.26455 +450,1.4573 +451,0.26455 +452,1.4573 +453,0.86493 +454,1.67 +455,0.87569 +456,0.87569 +457,0.87569 +458,1.686 +459,0.076253 +460,1.686 +461,1.7021 +462,0.081997 +463,1.7021 +464,0.081997 +465,1.7021 +466,0.89753 +467,1.7183 +468,0.087843 +469,0.081997 +470,-0.9152 +471,-1.5834 +472,-0.36787 +473,-2.0567 +474,-0.87672 +475,-1.4815 +476,-2.6241 +477,-2.178 +478,-0.036534 +479,-0.90616 +480,-2.4373 +481,-2.9694 +482,-3.5147 +483,-1.8457 +484,-0.98341 +485,-2.4483 +486,-3.8294 +487,-1.9441 +488,-0.048456 +489,-1.0641 +490,-1.0035 +491,-0.66563 +492,-0.32669 +493,-0.027036 +494,0.012859 +495,0.012859 +496,0.012859 +497,0.012859 +498,0.012859 +499,0.012859 +500,0.012859 +501,0.013668 +502,0.34446 +503,1.4125 +504,2.4117 +505,3.237 +506,3.4134 +507,2.4722 +508,0.71203 +509,-0.68697 +510,-0.9946 +511,-1.1241 +512,-2.2309 +513,-1.7637 +514,-0.84058 +515,0.20754 +516,1.1729 +517,1.9691 +518,2.7371 +519,3.2787 +520,2.6509 +521,1.5214 +522,-0.0058875 +523,-1.3985 +524,-1.9241 +525,-1.808 +526,-1.6276 +527,-0.95015 +528,-0.20711 +529,-0.2798 +530,-0.026526 +531,0.059693 +532,0.71684 +533,1.6183 +534,2.4505 +535,3.2363 +536,3.4634 +537,1.8132 +538,0.52817 +539,-0.37846 +540,-1.398 +541,-1.9658 +542,-1.8804 +543,-1.5482 +544,-0.64195 +545,-0.15352 +546,-0.0033494 +547,1.1079 +548,1.9138 +549,2.325 +550,3.0374 +551,3.3647 +552,3.7312 +553,0.086116 +554,-2.2017 +555,-2.3495 +556,-2.0339 +557,-1.4035 +558,-0.91224 +559,-0.37758 +560,-0.056562 +561,0.012859 +562,0.012859 +563,0.012859 +564,0.012859 +565,0.012859 +566,0.012859 +567,0.012859 +568,0.014561 +569,0.70309 +570,2.0434 +571,3.5113 +572,3.4062 +573,3.9559 +574,7.1727 +575,3.4216 +576,3.9417 +577,6.9749 +578,8.1 +579,3.415 +580,-0.024227 +581,-0.25555 +582,-0.3689 +583,-1.9022 +584,-2.9537 +585,-2.95 +586,-2.7289 +587,-3.271 +588,-3.5216 +589,-2.3332 +590,-2.2084 +591,-2.0047 +592,-1.3769 +593,-0.34366 +594,-0.035978 +595,0.012859 +596,0.012859 +597,0.012859 +598,0.012859 +599,0.012859 +600,0.012859 diff --git a/input/drive_cycles/car_current.csv b/input/drive_cycles/car_current.csv index aaae1b7e5a..3d23ca51bc 100644 --- a/input/drive_cycles/car_current.csv +++ b/input/drive_cycles/car_current.csv @@ -1,16 +1,16 @@ # This is adapted from the file getCarCurrent.m which is part of the LIONSIMBA toolbox., -current [],time [s] -1,0 -1,50 --0.5,50.001 --0.5,60 -0.5,60.001 -0.5,210 -1,210.001 -1,410 -2,410.001 -2,415 -1.25,415.001 -1.25,615 --0.5,615.001 --0.5,3600 +# time [s], current [A] +0, 1 +50, 1 +50.001, -0.5 +60, -0.5 +60.001, 0.5 +210, 0.5 +210.001, 1 +410, 1 +410.001, 2 +415, 2 +415.001, 1.25 +615, 1.25 +615.001, -0.5 +3600, -0.5 diff --git a/input/parameters/lead-acid/experiments/1C_discharge_from_full/parameters.csv b/input/parameters/lead-acid/experiments/1C_discharge_from_full/parameters.csv index a69a021238..15dc8b80fa 100644 --- a/input/parameters/lead-acid/experiments/1C_discharge_from_full/parameters.csv +++ b/input/parameters/lead-acid/experiments/1C_discharge_from_full/parameters.csv @@ -11,7 +11,7 @@ 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 C-rate,1,, -Current function,[inbuilt class]GetConstantCurrent,, +Current function,[inbuilt class]ConstantCurrent,, ,,, # Initial conditions Initial State of Charge,1,-, diff --git a/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/parameters.csv b/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/parameters.csv index 614e516fb1..28a1733cb4 100644 --- a/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/parameters.csv +++ b/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/parameters.csv @@ -4,7 +4,6 @@ Name [units],Value,Reference,Notes # Electrode properties,,, Negative electrode conductivity [S.m-1],100,Scott Moura FastDFN,graphite Maximum concentration in negative electrode [mol.m-3],24983.2619938437,Scott Moura FastDFN, -Negative electrode diffusion coefficient [m2.s-1],3.9E-14,Scott Moura FastDFN, Negative electrode diffusivity [m2.s-1],[function]graphite_mcmb2528_diffusivity_Dualfoil1998,, Negative electrode OCP [V],[function]graphite_mcmb2528_ocp_Dualfoil1998, ,,, diff --git a/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/lico2_data_example.csv b/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/lico2_data_example.csv index f2f1809c79..5f2f5fef15 100644 --- a/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/lico2_data_example.csv +++ b/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/lico2_data_example.csv @@ -1,50 +1,50 @@ -0.000000000000000000e+00 4.714135898019971016e+00 -2.040816326530612082e-02 4.708899441575220557e+00 -4.081632653061224164e-02 4.702448345762175741e+00 -6.122448979591836593e-02 4.694558534379876136e+00 -8.163265306122448328e-02 4.684994372928071193e+00 -1.020408163265306006e-01 4.673523893805322516e+00 -1.224489795918367319e-01 4.659941254449398329e+00 -1.428571428571428492e-01 4.644096031712390271e+00 -1.632653061224489666e-01 4.625926611260677390e+00 -1.836734693877550839e-01 4.605491824833229053e+00 -2.040816326530612013e-01 4.582992038370575116e+00 -2.244897959183673186e-01 4.558769704421606228e+00 -2.448979591836734637e-01 4.533281647154224103e+00 -2.653061224489795533e-01 4.507041620859735254e+00 -2.857142857142856984e-01 4.480540404981123714e+00 -3.061224489795917880e-01 4.454158468368703439e+00 -3.265306122448979331e-01 4.428089899175588151e+00 -3.469387755102040782e-01 4.402295604083254155e+00 -3.673469387755101678e-01 4.376502631465185367e+00 -3.877551020408163129e-01 4.350272100879827519e+00 -4.081632653061224025e-01 4.323179536958428493e+00 -4.285714285714285476e-01 4.295195829713853719e+00 -4.489795918367346372e-01 4.267407675466301065e+00 -4.693877551020407823e-01 4.243081968022011985e+00 -4.897959183673469274e-01 4.220583168834260768e+00 -5.102040816326530726e-01 4.177032236370062712e+00 -5.306122448979591066e-01 4.134943568540559333e+00 -5.510204081632652517e-01 4.075402582839823928e+00 -5.714285714285713969e-01 4.055407164381796825e+00 -5.918367346938775420e-01 4.036052896449991323e+00 -6.122448979591835760e-01 4.012970397550268409e+00 -6.326530612244897211e-01 3.990385577539371287e+00 -6.530612244897958663e-01 3.970744780585252709e+00 -6.734693877551020114e-01 3.954753574690877738e+00 -6.938775510204081565e-01 3.942237451863396025e+00 -7.142857142857141906e-01 3.932683425747200534e+00 -7.346938775510203357e-01 3.925509771581312979e+00 -7.551020408163264808e-01 3.920182838859009422e+00 -7.755102040816326259e-01 3.916256861206461881e+00 -7.959183673469386600e-01 3.913378070528176877e+00 -8.163265306122448051e-01 3.911274218446639583e+00 -8.367346938775509502e-01 3.909739285381772067e+00 -8.571428571428570953e-01 3.908613829807601192e+00 -8.775510204081632404e-01 3.907726324580658162e+00 -8.979591836734692745e-01 3.906474088522892796e+00 -9.183673469387754196e-01 3.900204875423951556e+00 -9.387755102040815647e-01 3.848912814816038974e+00 -9.591836734693877098e-01 3.445226042113884724e+00 -9.795918367346938549e-01 1.687177743081021308e+00 -1.000000000000000000e+00 6.378908986260003328e-03 +0.000000000000000000e+00, 4.714135898019971016e+00 +2.040816326530612082e-02, 4.708899441575220557e+00 +4.081632653061224164e-02, 4.702448345762175741e+00 +6.122448979591836593e-02, 4.694558534379876136e+00 +8.163265306122448328e-02, 4.684994372928071193e+00 +1.020408163265306006e-01, 4.673523893805322516e+00 +1.224489795918367319e-01, 4.659941254449398329e+00 +1.428571428571428492e-01, 4.644096031712390271e+00 +1.632653061224489666e-01, 4.625926611260677390e+00 +1.836734693877550839e-01, 4.605491824833229053e+00 +2.040816326530612013e-01, 4.582992038370575116e+00 +2.244897959183673186e-01, 4.558769704421606228e+00 +2.448979591836734637e-01, 4.533281647154224103e+00 +2.653061224489795533e-01, 4.507041620859735254e+00 +2.857142857142856984e-01, 4.480540404981123714e+00 +3.061224489795917880e-01, 4.454158468368703439e+00 +3.265306122448979331e-01, 4.428089899175588151e+00 +3.469387755102040782e-01, 4.402295604083254155e+00 +3.673469387755101678e-01, 4.376502631465185367e+00 +3.877551020408163129e-01, 4.350272100879827519e+00 +4.081632653061224025e-01, 4.323179536958428493e+00 +4.285714285714285476e-01, 4.295195829713853719e+00 +4.489795918367346372e-01, 4.267407675466301065e+00 +4.693877551020407823e-01, 4.243081968022011985e+00 +4.897959183673469274e-01, 4.220583168834260768e+00 +5.102040816326530726e-01, 4.177032236370062712e+00 +5.306122448979591066e-01, 4.134943568540559333e+00 +5.510204081632652517e-01, 4.075402582839823928e+00 +5.714285714285713969e-01, 4.055407164381796825e+00 +5.918367346938775420e-01, 4.036052896449991323e+00 +6.122448979591835760e-01, 4.012970397550268409e+00 +6.326530612244897211e-01, 3.990385577539371287e+00 +6.530612244897958663e-01, 3.970744780585252709e+00 +6.734693877551020114e-01, 3.954753574690877738e+00 +6.938775510204081565e-01, 3.942237451863396025e+00 +7.142857142857141906e-01, 3.932683425747200534e+00 +7.346938775510203357e-01, 3.925509771581312979e+00 +7.551020408163264808e-01, 3.920182838859009422e+00 +7.755102040816326259e-01, 3.916256861206461881e+00 +7.959183673469386600e-01, 3.913378070528176877e+00 +8.163265306122448051e-01, 3.911274218446639583e+00 +8.367346938775509502e-01, 3.909739285381772067e+00 +8.571428571428570953e-01, 3.908613829807601192e+00 +8.775510204081632404e-01, 3.907726324580658162e+00 +8.979591836734692745e-01, 3.906474088522892796e+00 +9.183673469387754196e-01, 3.900204875423951556e+00 +9.387755102040815647e-01, 3.848912814816038974e+00 +9.591836734693877098e-01, 3.445226042113884724e+00 +9.795918367346938549e-01, 1.687177743081021308e+00 +1.000000000000000000e+00, 6.378908986260003328e-03 diff --git a/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/parameters.csv b/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/parameters.csv index f79e4fc401..99f2b3840b 100644 --- a/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/parameters.csv +++ b/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/parameters.csv @@ -4,7 +4,6 @@ Name [units],Value,Reference,Notes # Electrode properties,,, Positive electrode conductivity [S.m-1],10,Scott Moura FastDFN,lithium cobalt oxide Maximum concentration in positive electrode [mol.m-3],51217.9257309275,Scott Moura FastDFN, -Positive electrode diffusion coefficient [m2.s-1],1E-13,Scott Moura FastDFN, Positive electrode diffusivity [m2.s-1],[function]lico2_diffusivity_Dualfoil1998,, Positive electrode OCP [V],[function]lico2_ocp_Dualfoil1998, ,,, diff --git a/input/parameters/lithium-ion/electrolytes/lipf6_Marquis2019/parameters.csv b/input/parameters/lithium-ion/electrolytes/lipf6_Marquis2019/parameters.csv index d99621a3ac..c77aca897b 100644 --- a/input/parameters/lithium-ion/electrolytes/lipf6_Marquis2019/parameters.csv +++ b/input/parameters/lithium-ion/electrolytes/lipf6_Marquis2019/parameters.csv @@ -4,9 +4,8 @@ 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, -Typical lithium ion diffusivity [m2.s-1],5.34E-10,Scott Moura FastDFN, Electrolyte diffusivity [m2.s-1],[function]electrolyte_diffusivity_Capiglia1999,, -Electrolyte conductivity [S.m-1],[function]electrolyte_conductivity_Capiglia1999,, +Electrolyte conductivity [S.m-1],[function]electrolyte_conductivity_Capiglia1999,, ,,, # Activation energies,,, Reference temperature [K],298.15,25C, diff --git a/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Marquis2019/parameters.csv b/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Marquis2019/parameters.csv index dab0b3b48a..7c390e804e 100644 --- a/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Marquis2019/parameters.csv +++ b/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Marquis2019/parameters.csv @@ -11,7 +11,7 @@ Number of cells connected in series to make a battery,1,, Lower voltage cut-off [V],3.105,, Upper voltage cut-off [V],4.7,, C-rate,1,, -Current function,[inbuilt class]GetConstantCurrent,, +Current function,[inbuilt class]ConstantCurrent,, ,,, # Initial conditions Initial concentration in negative electrode [mol.m-3],19986.609595075,Scott Moura FastDFN, diff --git a/pybamm/__init__.py b/pybamm/__init__.py index ce9fc755b7..07bf57749f 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -140,19 +140,22 @@ def version(formatted=False): UndefinedOperationError, GeometryError, ) -from .expression_tree.simplify import ( + +# Operations +from .expression_tree.operations.simplify import ( Simplification, simplify_if_constant, simplify_addition_subtraction, simplify_multiplication_division, ) -from .expression_tree.jacobian import Jacobian -from .expression_tree.evaluate import ( +from .expression_tree.operations.evaluate import ( find_symbols, id_to_python_variable, to_python, EvaluatorPython, ) +from .expression_tree.operations.jacobian import Jacobian +from .expression_tree.operations.convert_to_casadi import CasadiConverter # # Model classes @@ -248,23 +251,19 @@ def version(formatted=False): from .solvers.base_solver import BaseSolver from .solvers.ode_solver import OdeSolver from .solvers.dae_solver import DaeSolver -from .solvers.scipy_solver import ScipySolver -from .solvers.scikits_dae_solver import ScikitsDaeSolver -from .solvers.scikits_ode_solver import ScikitsOdeSolver -from .solvers.scikits_ode_solver import have_scikits_odes from .solvers.algebraic_solver import AlgebraicSolver -from .solvers.idaklu_solver import IDAKLU, have_idaklu - +from .solvers.casadi_solver import CasadiSolver +from .solvers.scikits_dae_solver import ScikitsDaeSolver +from .solvers.scikits_ode_solver import ScikitsOdeSolver, have_scikits_odes +from .solvers.scipy_solver import ScipySolver +from .solvers.idaklu_solver import IDAKLUSolver, have_idaklu # # Current profiles # -from .parameters.standard_current_functions.base_current import GetCurrent -from .parameters.standard_current_functions.get_constant_current import ( - GetConstantCurrent, -) -from .parameters.standard_current_functions.get_user_current import GetUserCurrent -from .parameters.standard_current_functions.get_current_data import GetCurrentData +from .parameters.standard_current_functions.base_current import BaseCurrent +from .parameters.standard_current_functions.constant_current import ConstantCurrent +from .parameters.standard_current_functions.user_current import UserCurrent # # other @@ -272,6 +271,8 @@ def version(formatted=False): from .processed_variable import post_process_variables, ProcessedVariable from .quick_plot import QuickPlot, ax_min, ax_max +from .simulation import Simulation + # # 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 a78206b3f0..0c1ab0c6ab 100644 --- a/pybamm/discretisations/discretisation.py +++ b/pybamm/discretisations/discretisation.py @@ -122,13 +122,13 @@ def process_model(self, model, inplace=True): # since they point to the same object model_disc = model else: - # create a blank model so that original model is unchanged - model_disc = pybamm.BaseModel() + # create a model of the same class as the original model + model_disc = model.__class__(model.options) model_disc.name = model.name model_disc.options = model.options model_disc.use_jacobian = model.use_jacobian model_disc.use_simplify = model.use_simplify - model_disc.use_to_python = model.use_to_python + model_disc.convert_to_format = model.convert_to_format model_disc.bcs = self.bcs diff --git a/pybamm/expression_tree/binary_operators.py b/pybamm/expression_tree/binary_operators.py index 70e9b1ea6d..0645d5b97b 100644 --- a/pybamm/expression_tree/binary_operators.py +++ b/pybamm/expression_tree/binary_operators.py @@ -277,9 +277,15 @@ def _binary_simplify(self, left, right): return left # Check matrices after checking scalars if is_matrix_zero(left): - return right + if isinstance(right, pybamm.Scalar): + return pybamm.Array(right.value * np.ones(left.shape_for_testing)) + else: + return right if is_matrix_zero(right): - return left + if isinstance(left, pybamm.Scalar): + return pybamm.Array(left.value * np.ones(right.shape_for_testing)) + else: + return left return pybamm.simplify_addition_subtraction(self.__class__, left, right) @@ -325,9 +331,15 @@ def _binary_simplify(self, left, right): return left # Check matrices after checking scalars if is_matrix_zero(left): - return -right + if isinstance(right, pybamm.Scalar): + return pybamm.Array(-right.value * np.ones(left.shape_for_testing)) + else: + return -right if is_matrix_zero(right): - return left + if isinstance(left, pybamm.Scalar): + return pybamm.Array(left.value * np.ones(right.shape_for_testing)) + else: + return left return pybamm.simplify_addition_subtraction(self.__class__, left, right) diff --git a/pybamm/expression_tree/functions.py b/pybamm/expression_tree/functions.py index 41c2492edd..bd46013eeb 100644 --- a/pybamm/expression_tree/functions.py +++ b/pybamm/expression_tree/functions.py @@ -21,10 +21,19 @@ class Function(pybamm.Symbol): derivative : str, optional Which derivative to use when differentiating ("autograd" or "derivative"). Default is "autograd". + differentiated_function : method, optional + The function which was differentiated to obtain this one. Default is None. **Extends:** :class:`pybamm.Symbol` """ - def __init__(self, function, *children, name=None, derivative="autograd"): + def __init__( + self, + function, + *children, + name=None, + derivative="autograd", + differentiated_function=None + ): if name is not None: self.name = name @@ -39,6 +48,7 @@ def __init__(self, function, *children, name=None, derivative="autograd"): self.function = function self.derivative = derivative + self.differentiated_function = differentiated_function # hack to work out whether function takes any params # (signature doesn't work for numpy) @@ -85,7 +95,9 @@ def diff(self, variable): # if variable appears in the function,use autograd to differentiate # function, and apply chain rule if variable.id in [symbol.id for symbol in child.pre_order()]: - partial_derivatives[i] = self._diff(children) * child.diff(variable) + partial_derivatives[i] = self._function_diff( + children, i + ) * child.diff(variable) # remove None entries partial_derivatives = list(filter(None, partial_derivatives)) @@ -96,15 +108,34 @@ def diff(self, variable): return derivative - def _diff(self, children): - """ See :meth:`pybamm.Symbol._diff()`. """ + def _function_diff(self, children, idx): + """ + Derivative with respect to child number 'idx'. + See :meth:`pybamm.Symbol._diff()`. + """ + # Store differentiated function, needed in case we want to convert to CasADi if self.derivative == "autograd": - return Function(autograd.elementwise_grad(self.function), *children) - elif self.derivative == "derivative": - # keep using "derivative" as derivative - return pybamm.Function( - self.function.derivative(), *children, derivative="derivative" + return Function( + autograd.elementwise_grad(self.function, idx), + *children, + differentiated_function=self.function ) + elif self.derivative == "derivative": + if len(children) > 1: + raise ValueError( + """ + differentiation using '.derivative()' not implemented for functions + with more than one child + """ + ) + else: + # keep using "derivative" as derivative + return pybamm.Function( + self.function.derivative(), + *children, + derivative="derivative", + differentiated_function=self.function + ) def _function_jac(self, children_jacs): """ Calculate the jacobian of a function. """ @@ -118,7 +149,7 @@ def _function_jac(self, children_jacs): children = self.orphans for i, child in enumerate(children): if not child.evaluates_to_number(): - jac_fun = self._diff(children) * children_jacs[i] + jac_fun = self._function_diff(children, i) * children_jacs[i] jac_fun.domain = [] if jacobian is None: jacobian = jac_fun @@ -175,7 +206,11 @@ def _function_new_copy(self, children): A new copy of the function """ return pybamm.Function( - self.function, *children, name=self.name, derivative=self.derivative + self.function, + *children, + name=self.name, + derivative=self.derivative, + differentiated_function=self.differentiated_function ) def _function_simplify(self, simplified_children): @@ -195,7 +230,7 @@ def _function_simplify(self, simplified_children): if self.takes_no_params is True: # If self.function() takes no parameters then we can always simplify it return pybamm.Scalar(self.function()) - elif isinstance(self.function, pybamm.GetConstantCurrent): + elif isinstance(self.function, pybamm.ConstantCurrent): # If self.function() is a constant current then simplify to scalar return pybamm.Scalar(self.function.parameters_eval["Current [A]"]) else: @@ -203,7 +238,8 @@ def _function_simplify(self, simplified_children): self.function, *simplified_children, name=self.name, - derivative=self.derivative + derivative=self.derivative, + differentiated_function=self.differentiated_function ) @@ -239,8 +275,8 @@ class Cos(SpecificFunction): def __init__(self, child): super().__init__(np.cos, child) - def _diff(self, children): - """ See :meth:`pybamm.Symbol._diff()`. """ + def _function_diff(self, children, idx): + """ See :meth:`pybamm.Symbol._function_diff()`. """ return -Sin(children[0]) @@ -255,8 +291,8 @@ class Cosh(SpecificFunction): def __init__(self, child): super().__init__(np.cosh, child) - def _diff(self, children): - """ See :meth:`pybamm.Function._diff()`. """ + def _function_diff(self, children, idx): + """ See :meth:`pybamm.Function._function_diff()`. """ return Sinh(children[0]) @@ -271,8 +307,8 @@ class Exponential(SpecificFunction): def __init__(self, child): super().__init__(np.exp, child) - def _diff(self, children): - """ See :meth:`pybamm.Function._diff()`. """ + def _function_diff(self, children, idx): + """ See :meth:`pybamm.Function._function_diff()`. """ return Exponential(children[0]) @@ -287,8 +323,8 @@ class Log(SpecificFunction): def __init__(self, child): super().__init__(np.log, child) - def _diff(self, children): - """ See :meth:`pybamm.Function._diff()`. """ + def _function_diff(self, children, idx): + """ See :meth:`pybamm.Function._function_diff()`. """ return 1 / children[0] @@ -313,8 +349,8 @@ class Sin(SpecificFunction): def __init__(self, child): super().__init__(np.sin, child) - def _diff(self, children): - """ See :meth:`pybamm.Function._diff()`. """ + def _function_diff(self, children, idx): + """ See :meth:`pybamm.Function._function_diff()`. """ return Cos(children[0]) @@ -329,8 +365,8 @@ class Sinh(SpecificFunction): def __init__(self, child): super().__init__(np.sinh, child) - def _diff(self, children): - """ See :meth:`pybamm.Function._diff()`. """ + def _function_diff(self, children, idx): + """ See :meth:`pybamm.Function._function_diff()`. """ return Cosh(children[0]) diff --git a/pybamm/expression_tree/interpolant.py b/pybamm/expression_tree/interpolant.py index 5cb8a9c6ca..d3e3758602 100644 --- a/pybamm/expression_tree/interpolant.py +++ b/pybamm/expression_tree/interpolant.py @@ -60,5 +60,22 @@ def __init__( interpolating_function, child, name=name, derivative="derivative" ) # Store information as attributes + self.data = data + self.x = data[:, 0] + self.y = data[:, 1] self.interpolator = interpolator self.extrapolate = extrapolate + + def _function_new_copy(self, children): + """ See :meth:`Function._function_new_copy()` """ + return pybamm.Interpolant( + self.data, + *children, + name=self.name, + interpolator=self.interpolator, + extrapolate=self.extrapolate + ) + + def _function_simplify(self, simplified_children): + """ See :meth:`Function._function_new_simplify()` """ + return self._function_new_copy(simplified_children) diff --git a/pybamm/expression_tree/operations/__init__.py b/pybamm/expression_tree/operations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pybamm/expression_tree/operations/convert_to_casadi.py b/pybamm/expression_tree/operations/convert_to_casadi.py new file mode 100644 index 0000000000..5deac9ce59 --- /dev/null +++ b/pybamm/expression_tree/operations/convert_to_casadi.py @@ -0,0 +1,134 @@ +# +# Convert a PyBaMM expression tree to a CasADi expression tree +# +import pybamm +import casadi +import numpy as np +from scipy.interpolate import PchipInterpolator, CubicSpline + + +class CasadiConverter(object): + def __init__(self, casadi_symbols=None): + self._casadi_symbols = casadi_symbols or {} + + def convert(self, symbol, t=None, y=None): + """ + This function recurses down the tree, applying any simplifications defined in + classes derived from pybamm.Symbol. E.g. any expression multiplied by a + pybamm.Scalar(0) will be simplified to a pybamm.Scalar(0). + If a symbol has already been simplified, the stored value is returned. + + Parameters + ---------- + symbol : :class:`pybamm.Symbol` + The symbol to convert + + Returns + ------- + CasADi symbol + The convert symbol + """ + + try: + return self._casadi_symbols[symbol.id] + except KeyError: + casadi_symbol = self._convert(symbol, t, y) + self._casadi_symbols[symbol.id] = casadi_symbol + + return casadi_symbol + + def _convert(self, symbol, t, y): + """ See :meth:`CasadiConverter.convert()`. """ + if isinstance(symbol, (pybamm.Scalar, pybamm.Array, pybamm.Time)): + return casadi.MX(symbol.evaluate(t, y)) + + 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.BinaryOperator): + left, right = symbol.children + # process children + converted_left = self.convert(left, t, y) + converted_right = self.convert(right, t, y) + if isinstance(symbol, pybamm.Outer): + return casadi.kron(converted_left, converted_right) + else: + # _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) + 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) for child in symbol.children + ] + # Special functions + if symbol.function == np.min: + return casadi.mmin(*converted_children) + elif symbol.function == np.max: + return casadi.mmax(*converted_children) + elif symbol.function == np.abs: + return casadi.fabs(*converted_children) + elif isinstance(symbol.function, (PchipInterpolator, CubicSpline)): + return casadi.interpolant("LUT", "bspline", [symbol.x], symbol.y)( + *converted_children + ) + elif not isinstance( + symbol.function, pybamm.BaseCurrent + ) and symbol.function.__name__.startswith("elementwise_grad_of_"): + differentiating_child_idx = int(symbol.function.__name__[-1]) + # Create dummy symbolic variables in order to differentiate using CasADi + dummy_vars = [ + casadi.MX.sym("y_" + str(i)) for i in range(len(converted_children)) + ] + func_diff = casadi.gradient( + symbol.differentiated_function(*dummy_vars), + dummy_vars[differentiating_child_idx], + ) + # Create function and evaluate it using the children + casadi_func_diff = casadi.Function("func_diff", dummy_vars, [func_diff]) + return casadi_func_diff(*converted_children) + # Other functions + else: + return symbol._function_evaluate(converted_children) + elif isinstance(symbol, pybamm.Concatenation): + converted_children = [ + self.convert(child, t, y) for child in symbol.children + ] + if isinstance(symbol, (pybamm.NumpyConcatenation, pybamm.SparseStack)): + return casadi.vertcat(*converted_children) + # DomainConcatenation specifies a particular ordering for the concatenation, + # which we must follow + elif isinstance(symbol, pybamm.DomainConcatenation): + slice_starts = [] + all_child_vectors = [] + for i in range(symbol.secondary_dimensions_npts): + child_vectors = [] + for child_var, slices in zip( + converted_children, symbol._children_slices + ): + for child_dom, child_slice in slices.items(): + slice_starts.append(symbol._slices[child_dom][i].start) + child_vectors.append( + child_var[child_slice[i].start : child_slice[i].stop] + ) + all_child_vectors.extend( + [v for _, v in sorted(zip(slice_starts, child_vectors))] + ) + return casadi.vertcat(*all_child_vectors) + + else: + raise TypeError( + """ + Cannot convert symbol of type '{}' to CasADi. Symbols must all be + 'linear algebra' at this stage. + """.format( + type(symbol) + ) + ) diff --git a/pybamm/expression_tree/evaluate.py b/pybamm/expression_tree/operations/evaluate.py similarity index 100% rename from pybamm/expression_tree/evaluate.py rename to pybamm/expression_tree/operations/evaluate.py diff --git a/pybamm/expression_tree/jacobian.py b/pybamm/expression_tree/operations/jacobian.py similarity index 100% rename from pybamm/expression_tree/jacobian.py rename to pybamm/expression_tree/operations/jacobian.py diff --git a/pybamm/expression_tree/simplify.py b/pybamm/expression_tree/operations/simplify.py similarity index 100% rename from pybamm/expression_tree/simplify.py rename to pybamm/expression_tree/operations/simplify.py diff --git a/pybamm/expression_tree/state_vector.py b/pybamm/expression_tree/state_vector.py index 1aa170e9d8..688f07173a 100644 --- a/pybamm/expression_tree/state_vector.py +++ b/pybamm/expression_tree/state_vector.py @@ -113,7 +113,7 @@ def _base_evaluate(self, t=None, y=None): ) else: out = (y[: len(self._evaluation_array)])[self._evaluation_array] - if out.ndim == 1: + if isinstance(out, np.ndarray) and out.ndim == 1: out = out[:, np.newaxis] return out diff --git a/pybamm/expression_tree/symbol.py b/pybamm/expression_tree/symbol.py index a189e539c9..8a94f05447 100644 --- a/pybamm/expression_tree/symbol.py +++ b/pybamm/expression_tree/symbol.py @@ -599,6 +599,13 @@ 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, 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) + def new_copy(self): """ Make a new copy of a symbol, to avoid Tree corruption errors while bypassing diff --git a/pybamm/models/base_model.py b/pybamm/models/base_model.py index 505988e7c7..08b33033ed 100644 --- a/pybamm/models/base_model.py +++ b/pybamm/models/base_model.py @@ -68,11 +68,18 @@ class BaseModel(object): Whether to simplify the expression tress representing the rhs and algebraic equations, Jacobain (if using) and events, before solving the model (default is True) - use_to_python : bool - Whether to convert the expression tress representing the rhs and - algebraic equations, Jacobain (if using) and events into pure python code - that will calculate the result of calling `evaluate(t, y)` on the given - expression tree (default is True) + convert_to_format : str + Whether to convert the expression trees representing the rhs and + algebraic equations, Jacobain (if using) and events into a different format: + + - None: keep PyBaMM expression tree structure. + - "python": convert into pure python code that will calculate the result of \ + calling `evaluate(t, y)` on the given expression treeself. + - "casadi": convert into CasADi expression tree, which then uses CasADi's \ + algorithm to calculate the Jacobian. + + Default is "python". + """ def __init__(self, name="Unnamed model"): @@ -96,7 +103,7 @@ def __init__(self, name="Unnamed model"): # Default behaviour is to use the jacobian and simplify self.use_jacobian = True self.use_simplify = True - self.use_to_python = True + self.convert_to_format = "casadi" def _set_dictionary(self, dict, name): """ @@ -472,3 +479,15 @@ def check_variables(self): var ) ) + + @property + def default_solver(self): + "Return default solver based on whether model is ODE model or DAE model" + if len(self.algebraic) == 0: + return pybamm.ScipySolver() + elif pybamm.have_idaklu() and self.use_jacobian is True: + # KLU solver requires jacobian to be provided + return pybamm.IDAKLUSolver() + else: + return pybamm.CasadiSolver(mode="safe") + diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index 28083f5317..a6b0a7779a 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -37,15 +37,20 @@ 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 "single particle - potential pair". + "potential pair", "potential pair quite conductive", "single particle + potential pair" or "set external potential". The submodel + "single particle potential pair" can only be used with lithium-ion + single particle models. The submodel "set external potential" can only + be used with the SPM. * "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" or "lumped". Must be "isothermal" for - lead-acid models. + "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. * "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 @@ -129,13 +134,6 @@ def default_spatial_methods(self): base_spatial_methods["current collector"] = pybamm.ScikitFiniteElement return base_spatial_methods - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - return pybamm.ScipySolver() - @property def options(self): return self._options @@ -184,6 +182,7 @@ def options(self, extra_options): "potential pair", "potential pair quite conductive", "single particle potential pair", + "set external potential", ]: raise pybamm.OptionError( "current collector model '{}' not recognised".format( @@ -202,6 +201,7 @@ def options(self, extra_options): "x-lumped", "xyz-lumped", "lumped", + "set external temperature", ]: raise pybamm.OptionError( "Unknown thermal model '{}'".format(options["thermal"]) @@ -230,6 +230,25 @@ def options(self, extra_options): raise pybamm.OptionError( "thermal effects not implemented for lead-acid models" ) + if options[ + "current collector" + ] == "single particle potential pair" and not isinstance( + self, (pybamm.lithium_ion.SPM, pybamm.lithium_ion.SPMe) + ): + raise pybamm.OptionError( + "option {} only compatible with SPM or SPMe".format( + options["current collector"] + ) + ) + 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"] + ) + ) + self._options = options def set_standard_output_variables(self): @@ -555,6 +574,14 @@ 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): @@ -568,7 +595,20 @@ def set_current_collector_submodel(self): submodel = pybamm.current_collector.PotentialPair2plus1D(self.param) elif self.options["current collector"] == "single particle potential pair": submodel = pybamm.current_collector.SingleParticlePotentialPair(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 b0a50dd623..a25c7db88c 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 @@ -34,8 +34,22 @@ def default_geometry(self): @property def default_var_pts(self): + # Choose points that give uniform grid for the standard parameter values var = pybamm.standard_spatial_vars - return {var.x_n: 30, var.x_s: 30, var.x_p: 30, var.y: 10, var.z: 10} + return {var.x_n: 25, var.x_s: 41, var.x_p: 34, var.y: 10, var.z: 10} + + @property + def default_solver(self): + """ + Return default solver based on whether model is ODE model or DAE model. + There are bugs with KLU on the lead-acid models. + """ + if len(self.algebraic) == 0: + return pybamm.ScipySolver() + elif pybamm.have_scikits_odes(): + return pybamm.ScikitsDaeSolver() + else: + return pybamm.CasadiSolver(mode="safe") def set_standard_output_variables(self): super().set_standard_output_variables() diff --git a/pybamm/models/full_battery_models/lead_acid/full.py b/pybamm/models/full_battery_models/lead_acid/full.py index 70f70a29d4..056a3efc31 100644 --- a/pybamm/models/full_battery_models/lead_acid/full.py +++ b/pybamm/models/full_battery_models/lead_acid/full.py @@ -122,20 +122,3 @@ def set_side_reaction_submodels(self): self.submodels[ "negative oxygen interface" ] = pybamm.interface.lead_acid_oxygen.NoReaction(self.param, "Negative") - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - # Different solver depending on whether we solve ODEs or DAEs - if ( - self.options["surface form"] == "differential" - and self.options["current collector"] == "uniform" - ): - return pybamm.ScipySolver() - else: - if pybamm.have_scikit_odes(): - return pybamm.ScikitsDaeSolver() - elif pybamm.have_idaklu(): # pragma: no cover - return pybamm.IDAKLU() 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 600789cbb0..a03afdc51b 100644 --- a/pybamm/models/full_battery_models/lead_acid/higher_order.py +++ b/pybamm/models/full_battery_models/lead_acid/higher_order.py @@ -166,23 +166,6 @@ def set_full_porosity_submodel(self): """ self.submodels["full porosity"] = pybamm.porosity.Full(self.param) - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - # Different solver depending on whether we solve ODEs or DAEs - if ( - self.options["current collector"] != "uniform" - or self.options["surface form"] == "algebraic" - ): - if pybamm.have_scikit_odes(): - return pybamm.ScikitsDaeSolver() - elif pybamm.have_idaklu(): # pragma: no cover - return pybamm.IDAKLU() - else: - return pybamm.ScipySolver() - class FOQS(BaseHigherOrderModel): """First-order quasi-static model for lead-acid, from [1]_. diff --git a/pybamm/models/full_battery_models/lead_acid/loqs.py b/pybamm/models/full_battery_models/lead_acid/loqs.py index 8814d3268c..cf6bea3ed1 100644 --- a/pybamm/models/full_battery_models/lead_acid/loqs.py +++ b/pybamm/models/full_battery_models/lead_acid/loqs.py @@ -172,17 +172,3 @@ def set_side_reaction_submodels(self): self.reaction_submodels["Positive"].append( self.submodels["leading-order positive oxygen interface"] ) - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - - if ( - self.options["current collector"] != "uniform" - or self.options["surface form"] == "algebraic" - ): - return pybamm.ScikitsDaeSolver() - else: - return pybamm.ScipySolver() diff --git a/pybamm/models/full_battery_models/lithium_ion/dfn.py b/pybamm/models/full_battery_models/lithium_ion/dfn.py index 67a46814a9..6268229624 100644 --- a/pybamm/models/full_battery_models/lithium_ion/dfn.py +++ b/pybamm/models/full_battery_models/lithium_ion/dfn.py @@ -109,14 +109,3 @@ def default_geometry(self): return pybamm.Geometry("1+1D macro", "(1+1)+1D micro") elif dimensionality == 2: return pybamm.Geometry("2+1D macro", "(2+1)+1D micro") - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - - if pybamm.have_scikit_odes(): - return pybamm.ScikitsDaeSolver() - elif pybamm.have_idaklu(): # pragma: no cover - return pybamm.IDAKLU() diff --git a/pybamm/models/full_battery_models/lithium_ion/spm.py b/pybamm/models/full_battery_models/lithium_ion/spm.py index c8a9549127..d11f38abe1 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spm.py +++ b/pybamm/models/full_battery_models/lithium_ion/spm.py @@ -87,8 +87,14 @@ def set_negative_electrode_submodel(self): def set_positive_electrode_submodel(self): + if self.options["current collector"] == "set external potential": + # Potentials are set by external model + set_positive_potential = False + else: + # Potential determined by 1D model + set_positive_potential = True self.submodels["positive electrode"] = pybamm.electrode.ohm.LeadingOrder( - self.param, "Positive" + self.param, "Positive", set_positive_potential=set_positive_potential ) def set_electrolyte_submodel(self): @@ -111,15 +117,3 @@ def default_geometry(self): return pybamm.Geometry("1+1D macro", "(1+0)+1D micro") elif dimensionality == 2: return pybamm.Geometry("2+1D macro", "(2+0)+1D micro") - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - # Different solver depending on whether we solve ODEs or DAEs - dimensionality = self.options["dimensionality"] - if dimensionality == 0: - return pybamm.ScipySolver() - else: - return pybamm.ScikitsDaeSolver() diff --git a/pybamm/models/full_battery_models/lithium_ion/spme.py b/pybamm/models/full_battery_models/lithium_ion/spme.py index e5e6167592..974b60759d 100644 --- a/pybamm/models/full_battery_models/lithium_ion/spme.py +++ b/pybamm/models/full_battery_models/lithium_ion/spme.py @@ -115,15 +115,3 @@ def default_geometry(self): return pybamm.Geometry("1+1D macro", "(1+0)+1D micro") elif dimensionality == 2: return pybamm.Geometry("2+1D macro", "(2+0)+1D micro") - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - # Different solver depending on whether we solve ODEs or DAEs - dimensionality = self.options["dimensionality"] - if dimensionality == 0: - return pybamm.ScipySolver() - else: - return pybamm.ScikitsDaeSolver() diff --git a/pybamm/models/submodels/current_collector/__init__.py b/pybamm/models/submodels/current_collector/__init__.py index 0c7e6a6de4..2b575b750d 100644 --- a/pybamm/models/submodels/current_collector/__init__.py +++ b/pybamm/models/submodels/current_collector/__init__.py @@ -18,3 +18,8 @@ QuiteConductivePotentialPair1plus1D, QuiteConductivePotentialPair2plus1D, ) +from .set_potential_single_particle import ( + BaseSetPotentialSingleParticle, + SetPotentialSingleParticle1plus1D, + SetPotentialSingleParticle2plus1D, +) diff --git a/pybamm/models/submodels/current_collector/base_current_collector.py b/pybamm/models/submodels/current_collector/base_current_collector.py index d96ec11404..fb1af13345 100644 --- a/pybamm/models/submodels/current_collector/base_current_collector.py +++ b/pybamm/models/submodels/current_collector/base_current_collector.py @@ -74,12 +74,15 @@ def _get_standard_potential_variables(self, phi_s_cn, phi_s_cp): pot_scale = self.param.potential_scale U_ref = self.param.U_p_ref - self.param.U_n_ref - # add more to this + # Local potential difference + V_cc = phi_s_cp - phi_s_cn + variables = { "Positive current collector potential": phi_s_cp, "Positive current collector potential [V]": U_ref + phi_s_cp * pot_scale, - "Local potential difference": phi_s_cp - phi_s_cn, - "Local potential difference [V]": U_ref + (phi_s_cp - phi_s_cn) * pot_scale, + "Local current collector potential difference": V_cc, + "Local current collector potential difference [V]": U_ref + + V_cc * pot_scale, } variables.update(self._get_standard_negative_potential_variables(phi_s_cn)) diff --git a/pybamm/models/submodels/current_collector/composite_potential_pair.py b/pybamm/models/submodels/current_collector/composite_potential_pair.py index ddab082fc5..9f8e131dec 100644 --- a/pybamm/models/submodels/current_collector/composite_potential_pair.py +++ b/pybamm/models/submodels/current_collector/composite_potential_pair.py @@ -1,5 +1,5 @@ # -# Class for two-dimensional current collectors - composite models +# Class for one- and two-dimensional composite potential pair current collector models # import pybamm from .potential_pair import ( diff --git a/pybamm/models/submodels/current_collector/potential_pair.py b/pybamm/models/submodels/current_collector/potential_pair.py index b28a8469b8..79b1382a78 100644 --- a/pybamm/models/submodels/current_collector/potential_pair.py +++ b/pybamm/models/submodels/current_collector/potential_pair.py @@ -1,5 +1,5 @@ # -# Class for two-dimensional current collectors +# Class for one- and two-dimensional potential pair current collector models # import pybamm from .base_current_collector import BaseModel @@ -73,7 +73,7 @@ def set_initial_conditions(self, variables): class PotentialPair1plus1D(BasePotentialPair): - "Base class for a 1+1D potential pair model" + "Base class for a 1+1D potential pair model." def __init__(self, param): super().__init__(param) @@ -154,7 +154,7 @@ def set_boundary_conditions(self, variables): # giving the zero Dirichlet condition on phi_s_cn. Elsewhere, the boundary # is insulated, giving no flux conditions on phi_s_cn. This is automatically # applied everywhere, apart from the region corresponding to the projection - # of the positive tab, so we need to explititly apply a zero-flux boundary + # of the positive tab, so we need to explitly apply a zero-flux boundary # condition on the region "positive tab" for phi_s_cn. # A current is drawn from the positive tab, giving the non-zero Neumann # boundary condition on phi_s_cp at "positive tab". Elsewhere, the boundary is diff --git a/pybamm/models/submodels/current_collector/quite_conductive_potential_pair.py b/pybamm/models/submodels/current_collector/quite_conductive_potential_pair.py index 1309fcddf5..e71b1307f4 100644 --- a/pybamm/models/submodels/current_collector/quite_conductive_potential_pair.py +++ b/pybamm/models/submodels/current_collector/quite_conductive_potential_pair.py @@ -1,5 +1,6 @@ # -# Class for two-dimensional current collectors +# Class for one- and two-dimensional potential pair "quite conductive" +# current collector models # import pybamm from .potential_pair import ( diff --git a/pybamm/models/submodels/current_collector/set_potential_single_particle.py b/pybamm/models/submodels/current_collector/set_potential_single_particle.py new file mode 100644 index 0000000000..146094ec6a --- /dev/null +++ b/pybamm/models/submodels/current_collector/set_potential_single_particle.py @@ -0,0 +1,121 @@ +# +# 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 + phi_s_cp = pybamm.standard_variables.phi_s_cp + + variables = self._get_standard_potential_variables(phi_s_cn, phi_s_cp) + + # 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"] + phi_s_cp = variables["Positive 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), phi_s_cp: 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 current collector potential difference"] + # 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): + + param = self.param + applied_current = param.current_with_time + cc_area = self._get_effective_current_collector_area() + phi_s_cn = variables["Negative current collector potential"] + phi_s_cp = variables["Positive current collector potential"] + i_boundary_cc = variables["Current collector current density"] + + self.initial_conditions = { + phi_s_cn: pybamm.Scalar(0), + phi_s_cp: param.U_p(param.c_p_init, param.T_ref) + - param.U_n(param.c_n_init, param.T_ref), + 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 7047ed0239..146c8c916d 100644 --- a/pybamm/models/submodels/electrode/base_electrode.py +++ b/pybamm/models/submodels/electrode/base_electrode.py @@ -13,12 +13,15 @@ class BaseElectrode(pybamm.BaseSubModel): The parameters to use for this submodel domain : str Either 'Negative' or 'Positive' - + set_positive_potential : bool, optional + If True the battery model sets the positve potential based on the current. + If False, the potential is specified by the user. Default is True. **Extends:** :class:`pybamm.BaseSubModel` """ - def __init__(self, param, domain, reactions=None): + def __init__(self, param, domain, reactions=None, set_positive_potential=True): super().__init__(param, domain, reactions) + self.set_positive_potential = set_positive_potential def _get_standard_potential_variables(self, phi_s): """ @@ -121,33 +124,21 @@ def _get_standard_whole_cell_variables(self, variables): The variables in the whole model with the whole-cell current variables added. """ - pot_scale = self.param.potential_scale - U_ref = self.param.U_p_ref - self.param.U_n_ref i_s_n = variables["Negative electrode current density"] i_s_s = pybamm.FullBroadcast(0, ["separator"], "current collector") i_s_p = variables["Positive electrode current density"] - phi_s_p = variables["Positive electrode potential"] - - phi_s_cn = variables["Negative current collector potential"] - phi_s_cp = pybamm.boundary_value(phi_s_p, "right") - v_boundary_cc = phi_s_cp - phi_s_cn i_s = pybamm.Concatenation(i_s_n, i_s_s, i_s_p) - variables = { - "Electrode current density": i_s, - "Positive current collector potential": phi_s_cp, - "Local current collector potential difference": v_boundary_cc, - "Local current collector potential difference [V]": U_ref - + v_boundary_cc * pot_scale, - } + if self.set_positive_potential: + phi_s_p = variables["Positive electrode potential"] + phi_s_cp = pybamm.boundary_value(phi_s_p, "right") + variables = { + "Electrode current density": i_s, + "Positive current collector potential": phi_s_cp, + } + else: + variables = {"Electrode current density": i_s} return variables - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - return pybamm.ScikitsDaeSolver() diff --git a/pybamm/models/submodels/electrode/ohm/base_ohm.py b/pybamm/models/submodels/electrode/ohm/base_ohm.py index 532fb563a9..b2bb673e5a 100644 --- a/pybamm/models/submodels/electrode/ohm/base_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/base_ohm.py @@ -20,8 +20,8 @@ class BaseModel(BaseElectrode): **Extends:** :class:`pybamm.electrode.BaseElectrode` """ - def __init__(self, param, domain, reactions=None): - super().__init__(param, domain, reactions) + def __init__(self, param, domain, reactions=None, set_positive_potential=True): + super().__init__(param, domain, reactions, set_positive_potential) def set_boundary_conditions(self, variables): @@ -43,10 +43,3 @@ def set_boundary_conditions(self, variables): ) self.boundary_conditions[phi_s] = {"left": lbc, "right": rbc} - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - return pybamm.ScikitsDaeSolver() diff --git a/pybamm/models/submodels/electrode/ohm/composite_ohm.py b/pybamm/models/submodels/electrode/ohm/composite_ohm.py index fa6950beb3..696179856c 100644 --- a/pybamm/models/submodels/electrode/ohm/composite_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/composite_ohm.py @@ -18,7 +18,6 @@ class Composite(BaseModel): domain : str Either 'Negative electrode' or 'Positive electrode' - **Extends:** :class:`pybamm.BaseOhm` """ @@ -96,4 +95,3 @@ def set_boundary_conditions(self, variables): rbc = (-i_boundary_cc_0 / sigma_eff_0, "Neumann") self.boundary_conditions[phi_s] = {"left": lbc, "right": rbc} - diff --git a/pybamm/models/submodels/electrode/ohm/full_ohm.py b/pybamm/models/submodels/electrode/ohm/full_ohm.py index 61a10911f4..fac16021bc 100644 --- a/pybamm/models/submodels/electrode/ohm/full_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/full_ohm.py @@ -102,10 +102,3 @@ def set_initial_conditions(self, variables): ) self.initial_conditions[phi_s] = phi_s_init - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - return pybamm.ScikitsDaeSolver() diff --git a/pybamm/models/submodels/electrode/ohm/leading_ohm.py b/pybamm/models/submodels/electrode/ohm/leading_ohm.py index 269df31371..2cc50a6e54 100644 --- a/pybamm/models/submodels/electrode/ohm/leading_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/leading_ohm.py @@ -16,13 +16,15 @@ class LeadingOrder(BaseModel): The parameters to use for this submodel domain : str Either 'Negative' or 'Positive' - + set_positive_potential : bool, optional + If True the battery model sets the positve potential based on the current. + If False, the potential is specified by the user. Default is True. **Extends:** :class:`pybamm.electrode.ohm.BaseModel` """ - def __init__(self, param, domain): - super().__init__(param, domain) + def __init__(self, param, domain, set_positive_potential=True): + super().__init__(param, domain, set_positive_potential=set_positive_potential) def get_coupled_variables(self, variables): """ @@ -69,10 +71,3 @@ def set_boundary_conditions(self, variables): rbc = (pybamm.Scalar(0), "Neumann") self.boundary_conditions[phi_s] = {"left": lbc, "right": rbc} - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - return pybamm.ScikitsOdeSolver() diff --git a/pybamm/models/submodels/electrode/ohm/surface_form_ohm.py b/pybamm/models/submodels/electrode/ohm/surface_form_ohm.py index 6dad293054..f78453e495 100644 --- a/pybamm/models/submodels/electrode/ohm/surface_form_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/surface_form_ohm.py @@ -66,10 +66,3 @@ def get_coupled_variables(self, variables): variables.update(self._get_standard_whole_cell_variables(variables)) return variables - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - return pybamm.ScikitsDaeSolver() diff --git a/pybamm/models/submodels/electrolyte/base_electrolyte_conductivity.py b/pybamm/models/submodels/electrolyte/base_electrolyte_conductivity.py index 7d685dc7fe..b282465c7b 100644 --- a/pybamm/models/submodels/electrolyte/base_electrolyte_conductivity.py +++ b/pybamm/models/submodels/electrolyte/base_electrolyte_conductivity.py @@ -250,7 +250,7 @@ def _get_domain_current_variables(self, i_e, domain=None): variables = { domain + " electrolyte current density": i_e, - domain + " electrolyte current density [V]": i_e * i_typ, + domain + " electrolyte current density [A.m-2]": i_e * i_typ, } return variables @@ -258,7 +258,7 @@ def _get_domain_current_variables(self, i_e, domain=None): def _get_whole_cell_variables(self, variables): """ A private function to obtain the potential and current concatenated - across the whole cell. Note required 'variables' to contain the potential + across the whole cell. Note: requires 'variables' to contain the potential and current in the subdomains: 'negative electrode', 'separator', and 'positive electrode'. 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 0b482ba956..b6c5bf756c 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 @@ -48,11 +48,10 @@ def get_coupled_variables(self, variables): eps_s_av = variables["Leading-order x-averaged separator porosity"] eps_p_av = variables["Leading-order x-averaged positive electrode porosity"] - # Note: here we want the average of the temperature over the negative - # electrode, separator and positive electrode (not including the current - # collectors) - T = variables["Cell temperature"] - T_av = pybamm.x_average(T) + T_av = variables["X-averaged cell temperature"] + T_av_n = pybamm.PrimaryBroadcast(T_av, "negative electrode") + T_av_s = pybamm.PrimaryBroadcast(T_av, "separator") + T_av_p = pybamm.PrimaryBroadcast(T_av, "positive electrode") c_e_n, c_e_s, c_e_p = c_e.orphans @@ -90,6 +89,7 @@ def get_coupled_variables(self, variables): + phi_s_n_av - ( chi_av + * (1 + param.Theta * T_av) * pybamm.x_average( self._higher_order_macinnes_function( c_e_n / pybamm.PrimaryBroadcast(c_e_av, "negative electrode") @@ -106,6 +106,7 @@ def get_coupled_variables(self, variables): pybamm.PrimaryBroadcast(phi_e_const, "negative electrode") + ( chi_av_n + * (1 + param.Theta * T_av_n) * self._higher_order_macinnes_function( c_e_n / pybamm.PrimaryBroadcast(c_e_av, "negative electrode") ) @@ -124,6 +125,7 @@ def get_coupled_variables(self, variables): pybamm.PrimaryBroadcast(phi_e_const, "separator") + ( chi_av_s + * (1 + param.Theta * T_av_s) * self._higher_order_macinnes_function( c_e_s / pybamm.PrimaryBroadcast(c_e_av, "separator") ) @@ -137,6 +139,7 @@ def get_coupled_variables(self, variables): pybamm.PrimaryBroadcast(phi_e_const, "positive electrode") + ( chi_av_p + * (1 + param.Theta * T_av_p) * self._higher_order_macinnes_function( c_e_p / pybamm.PrimaryBroadcast(c_e_av, "positive electrode") ) @@ -155,15 +158,19 @@ def get_coupled_variables(self, variables): phi_e_av = pybamm.x_average(phi_e) # concentration overpotential - eta_c_av = chi_av * ( - pybamm.x_average( - self._higher_order_macinnes_function( - c_e_p / pybamm.PrimaryBroadcast(c_e_av, "positive electrode") + eta_c_av = ( + chi_av + * (1 + param.Theta * T_av) + * ( + pybamm.x_average( + self._higher_order_macinnes_function( + c_e_p / pybamm.PrimaryBroadcast(c_e_av, "positive electrode") + ) ) - ) - - pybamm.x_average( - self._higher_order_macinnes_function( - c_e_n / pybamm.PrimaryBroadcast(c_e_av, "negative electrode") + - pybamm.x_average( + self._higher_order_macinnes_function( + c_e_n / pybamm.PrimaryBroadcast(c_e_av, "negative electrode") + ) ) ) ) diff --git a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py index 536c989094..89272892c8 100644 --- a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py +++ b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py @@ -39,7 +39,8 @@ def get_coupled_variables(self, variables): phi_e = variables["Electrolyte potential"] i_e = (param.kappa_e(c_e, T) * (eps ** param.b) * param.gamma_e / param.C_e) * ( - param.chi(c_e) * pybamm.grad(c_e) / c_e - pybamm.grad(phi_e) + param.chi(c_e) * (1 + param.Theta * T) * pybamm.grad(c_e) / c_e + - pybamm.grad(phi_e) ) variables.update(self._get_standard_current_variables(i_e)) @@ -64,10 +65,3 @@ def set_initial_conditions(self, variables): phi_e = variables["Electrolyte potential"] T_ref = self.param.T_ref self.initial_conditions = {phi_e: -self.param.U_n(self.param.c_n_init, T_ref)} - - @property - def default_solver(self): - """ - Create and return the default solver for this model - """ - return pybamm.ScikitsDaeSolver() 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 5059986287..996f9fdfe3 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 @@ -75,11 +75,15 @@ def set_boundary_conditions(self, variables): delta_phi = variables[self.domain + " electrode surface potential difference"] if self.domain == "Negative": + T = variables["Negative electrode temperature"] c_e_flux = pybamm.BoundaryGradient(c_e, "right") flux_left = -i_boundary_cc * pybamm.BoundaryValue(1 / sigma_eff, "left") flux_right = ( (i_boundary_cc / pybamm.BoundaryValue(conductivity, "right")) - - pybamm.BoundaryValue(param.chi(c_e) / c_e, "right") * c_e_flux + - pybamm.BoundaryValue( + (1 + param.Theta * T) * param.chi(c_e) / c_e, "right" + ) + * c_e_flux - i_boundary_cc * pybamm.BoundaryValue(1 / sigma_eff, "right") ) @@ -89,10 +93,14 @@ def set_boundary_conditions(self, variables): rbc_c_e = (c_e_flux, "Neumann") elif self.domain == "Positive": + T = variables["Positive electrode temperature"] c_e_flux = pybamm.BoundaryGradient(c_e, "left") flux_left = ( (i_boundary_cc / pybamm.BoundaryValue(conductivity, "left")) - - pybamm.BoundaryValue(param.chi(c_e) / c_e, "left") * c_e_flux + - pybamm.BoundaryValue( + (1 + param.Theta * T) * param.chi(c_e) / c_e, "left" + ) + * c_e_flux - i_boundary_cc * pybamm.BoundaryValue(1 / sigma_eff, "left") ) flux_right = -i_boundary_cc * pybamm.BoundaryValue(1 / sigma_eff, "right") @@ -152,9 +160,10 @@ def _get_neg_pos_coupled_variables(self, variables): i_boundary_cc = variables["Current collector current density"] c_e = variables[self.domain + " electrolyte concentration"] delta_phi = variables[self.domain + " electrode surface potential difference"] + T = variables[self.domain + " electrode temperature"] i_e = conductivity * ( - (param.chi(c_e) / c_e) * pybamm.grad(c_e) + ((1 + param.Theta * T) * param.chi(c_e) / c_e) * pybamm.grad(c_e) + pybamm.grad(delta_phi) + pybamm.PrimaryBroadcast(i_boundary_cc, self.domain_for_broadcast) / sigma_eff @@ -190,7 +199,7 @@ def _get_sep_coupled_variables(self, variables): phi_e_s = pybamm.PrimaryBroadcast( pybamm.boundary_value(phi_e_n, "right"), "separator" ) + pybamm.IndefiniteIntegral( - chi_e_s / c_e_s * pybamm.grad(c_e_s) + (1 + param.Theta * T) * chi_e_s / c_e_s * pybamm.grad(c_e_s) - param.C_e * pybamm.PrimaryBroadcast(i_boundary_cc, self.domain_for_broadcast) / kappa_s_eff, diff --git a/pybamm/models/submodels/interface/inverse_kinetics/base_inverse_kinetics.py b/pybamm/models/submodels/interface/inverse_kinetics/base_inverse_kinetics.py index 6c7acfe61c..244f3d2d65 100644 --- a/pybamm/models/submodels/interface/inverse_kinetics/base_inverse_kinetics.py +++ b/pybamm/models/submodels/interface/inverse_kinetics/base_inverse_kinetics.py @@ -41,8 +41,13 @@ def get_coupled_variables(self, variables): 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) + eta_r = self._get_overpotential(j, j0, ne, T) delta_phi = eta_r + ocp variables.update(self._get_standard_interfacial_current_variables(j)) @@ -73,5 +78,5 @@ def get_coupled_variables(self, variables): return variables - def _get_overpotential(self, j, j0, ne): + 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 37afd228ab..7edd3b5753 100644 --- a/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py +++ b/pybamm/models/submodels/interface/inverse_kinetics/inverse_butler_volmer.py @@ -28,5 +28,7 @@ class InverseButlerVolmer(BaseInverseKinetics, ButlerVolmer): def __init__(self, param, domain): super().__init__(param, domain) - def _get_overpotential(self, j, j0, ne): - return (2 / ne) * pybamm.Function(np.arcsinh, j / (2 * j0)) + def _get_overpotential(self, j, j0, ne, T): + return (2 * (1 + self.param.Theta * T) / ne) * pybamm.Function( + np.arcsinh, j / (2 * j0) + ) diff --git a/pybamm/models/submodels/interface/kinetics/base_kinetics.py b/pybamm/models/submodels/interface/kinetics/base_kinetics.py index a4cfe9f33e..b65181e792 100644 --- a/pybamm/models/submodels/interface/kinetics/base_kinetics.py +++ b/pybamm/models/submodels/interface/kinetics/base_kinetics.py @@ -40,8 +40,13 @@ def get_coupled_variables(self, variables): eta_r = delta_phi - ocp # Get number of electrons in reaction ne = self._get_number_of_electrons_in_reaction() - - j = self._get_kinetics(j0, ne, eta_r) + # Get kinetics. 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"] + j = self._get_kinetics(j0, ne, eta_r, T) + # Get average interfacial current density j_tot_av = self._get_average_total_interfacial_current_density(variables) # j = j_tot_av + (j - pybamm.x_average(j)) # enforce true average @@ -73,7 +78,7 @@ def get_coupled_variables(self, variables): def _get_exchange_current_density(self, variables): raise NotImplementedError - def _get_kinetics(self, j0, ne, eta_r): + def _get_kinetics(self, j0, ne, eta_r, T): raise NotImplementedError def _get_open_circuit_potential(self, variables): @@ -84,10 +89,10 @@ def _get_dj_dc(self, variables): Default to calculate derivative of interfacial current density with respect to concentration. Can be overwritten by specific kinetic functions. """ - c_e, delta_phi, j0, ne, ocp = self._get_interface_variables_for_first_order( + c_e, delta_phi, j0, ne, ocp, T = self._get_interface_variables_for_first_order( variables ) - j = self._get_kinetics(j0, ne, delta_phi - ocp) + j = self._get_kinetics(j0, ne, delta_phi - ocp, T) return j.diff(c_e) def _get_dj_ddeltaphi(self, variables): @@ -95,10 +100,10 @@ def _get_dj_ddeltaphi(self, variables): Default to calculate derivative of interfacial current density with respect to surface potential difference. Can be overwritten by specific kinetic functions. """ - _, delta_phi, j0, ne, ocp = self._get_interface_variables_for_first_order( + _, delta_phi, j0, ne, ocp, T = self._get_interface_variables_for_first_order( variables ) - j = self._get_kinetics(j0, ne, delta_phi - ocp) + j = self._get_kinetics(j0, ne, delta_phi - ocp, T) return j.diff(delta_phi) def _get_interface_variables_for_first_order(self, variables): @@ -118,7 +123,11 @@ def _get_interface_variables_for_first_order(self, variables): j0 = self._get_exchange_current_density(hacked_variables) ne = self._get_number_of_electrons_in_reaction() ocp = self._get_open_circuit_potential(hacked_variables)[0] - return c_e_0, delta_phi, j0, ne, ocp + if j0.domain in ["current collector", ["current collector"]]: + T = variables["X-averaged cell temperature"] + else: + T = variables[self.domain + " electrode temperature"] + return c_e_0, delta_phi, j0, ne, ocp, T def _get_j_diffusion_limited_first_order(self, variables): """ diff --git a/pybamm/models/submodels/interface/kinetics/butler_volmer.py b/pybamm/models/submodels/interface/kinetics/butler_volmer.py index 437298bb84..0ee18a8f6b 100644 --- a/pybamm/models/submodels/interface/kinetics/butler_volmer.py +++ b/pybamm/models/submodels/interface/kinetics/butler_volmer.py @@ -12,7 +12,7 @@ class ButlerVolmer(BaseModel): Base submodel which implements the forward Butler-Volmer equation: .. math:: - j = j_0(c) * \\sinh(\\eta_r(c)) + j = 2 * j_0(c) * \\sinh( (ne / (2 * (1 + \\Theta T)) * \\eta_r(c)) Parameters ---------- @@ -28,26 +28,29 @@ class ButlerVolmer(BaseModel): def __init__(self, param, domain): super().__init__(param, domain) - def _get_kinetics(self, j0, ne, eta_r): - return 2 * j0 * pybamm.sinh((ne / 2) * eta_r) + 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`" - c_e, delta_phi, j0, ne, ocp = self._get_interface_variables_for_first_order( + c_e, delta_phi, j0, ne, ocp, T = self._get_interface_variables_for_first_order( variables ) eta_r = delta_phi - ocp - return (2 * j0.diff(c_e) * pybamm.sinh((ne / 2) * eta_r)) - ( - 2 * j0 * (ne / 2) * ocp.diff(c_e) * pybamm.cosh((ne / 2) * eta_r) + prefactor = ne / (2 * (1 + self.param.Theta * T)) + return (2 * j0.diff(c_e) * pybamm.sinh(prefactor * eta_r)) - ( + 2 * j0 * prefactor * ocp.diff(c_e) * pybamm.cosh(prefactor * eta_r) ) def _get_dj_ddeltaphi(self, variables): "See :meth:`pybamm.interface.kinetics.BaseModel._get_dj_ddeltaphi`" - _, delta_phi, j0, ne, ocp = self._get_interface_variables_for_first_order( + _, delta_phi, j0, ne, ocp, T = self._get_interface_variables_for_first_order( variables ) eta_r = delta_phi - ocp - return 2 * j0 * (ne / 2) * pybamm.cosh((ne / 2) * eta_r) + prefactor = ne / (2 * (1 + self.param.Theta * T)) + return 2 * j0 * prefactor * pybamm.cosh(prefactor * eta_r) class FirstOrderButlerVolmer(ButlerVolmer, BaseFirstOrderKinetics): diff --git a/pybamm/models/submodels/interface/kinetics/no_reaction.py b/pybamm/models/submodels/interface/kinetics/no_reaction.py index e135c5f25c..d16c32a933 100644 --- a/pybamm/models/submodels/interface/kinetics/no_reaction.py +++ b/pybamm/models/submodels/interface/kinetics/no_reaction.py @@ -24,5 +24,5 @@ class NoReaction(BaseModel): def __init__(self, param, domain): super().__init__(param, domain) - def _get_kinetics(self, j0, ne, eta_r): + 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 fbd0c5f770..26ab18e807 100644 --- a/pybamm/models/submodels/interface/kinetics/tafel.py +++ b/pybamm/models/submodels/interface/kinetics/tafel.py @@ -11,7 +11,7 @@ class ForwardTafel(BaseModel): Base submodel which implements the forward Tafel equation: .. math:: - j = j_0(c) * \\exp(\\eta_r(c)) + j = j_0(c) * \\exp((ne / (2 * (1 + \\Theta T)) * \\eta_r(c)) Parameters ---------- @@ -27,24 +27,33 @@ class ForwardTafel(BaseModel): def __init__(self, param, domain): super().__init__(param, domain) - def _get_kinetics(self, j0, ne, eta_r): - return j0 * pybamm.exp((ne / 2) * eta_r) + def _get_kinetics(self, j0, ne, eta_r, T): + return j0 * pybamm.exp((ne / (2 * (1 + self.param.Theta * T))) * eta_r) def _get_dj_dc(self, variables): "See :meth:`pybamm.interface.kinetics.BaseKinetics._get_dj_dc`" - c_e, delta_phi, j0, ne, ocp = self._get_interface_variables_for_first_order( + c_e, delta_phi, j0, ne, ocp, T = self._get_interface_variables_for_first_order( variables ) eta_r = delta_phi - ocp - return 2 * j0.diff(c_e) * pybamm.exp((ne / 2) * eta_r) + return ( + 2 + * j0.diff(c_e) + * pybamm.exp((ne / (2 * (1 + self.param.Theta * T))) * eta_r) + ) def _get_dj_ddeltaphi(self, variables): "See :meth:`pybamm.interface.kinetics.BaseKinetics._get_dj_ddeltaphi`" - _, delta_phi, j0, ne, ocp = self._get_interface_variables_for_first_order( + _, delta_phi, j0, ne, ocp, T = self._get_interface_variables_for_first_order( variables ) eta_r = delta_phi - ocp - return 2 * j0 * (ne / 2) * pybamm.exp((ne / 2) * eta_r) + return ( + 2 + * j0 + * (ne / (2 * (1 + self.param.Theta * T))) + * pybamm.exp((ne / 2) * eta_r) + ) class FirstOrderForwardTafel(ForwardTafel, BaseFirstOrderKinetics): @@ -73,5 +82,5 @@ class BackwardTafel(BaseModel): def __init__(self, param, domain): super().__init__(param, domain) - def _get_kinetics(self, j0, ne, eta_r): - return -j0 * pybamm.exp(-(ne / 2) * eta_r) + def _get_kinetics(self, j0, ne, eta_r, T): + return -j0 * pybamm.exp(-(ne / (2 * (1 + self.param.Theta * T))) * eta_r) diff --git a/pybamm/models/submodels/particle/base_particle.py b/pybamm/models/submodels/particle/base_particle.py index ab3e5c7f0f..c8bf288fc1 100644 --- a/pybamm/models/submodels/particle/base_particle.py +++ b/pybamm/models/submodels/particle/base_particle.py @@ -53,11 +53,11 @@ def _get_standard_concentration_variables(self, c_s, c_s_xav): + self.domain.lower() + " particle surface concentration [mol.m-3]": c_scale * c_s_surf_av, self.domain + " electrode active volume fraction": active_volume, + self.domain + " electrode volume-averaged concentration": c_s_r_av_vol, self.domain - + " electrode volume-averaged concentration": c_s_r_av_vol, - self.domain + " electrode " + + " electrode " + "volume-averaged concentration [mol.m-3]": c_s_r_av_vol * c_scale, - self.domain + " electrode average extent of lithiation": c_s_r_av + self.domain + " electrode average extent of lithiation": c_s_r_av, } return variables diff --git a/pybamm/models/submodels/thermal/base_thermal.py b/pybamm/models/submodels/thermal/base_thermal.py index e2b801db14..2f7186b187 100644 --- a/pybamm/models/submodels/thermal/base_thermal.py +++ b/pybamm/models/submodels/thermal/base_thermal.py @@ -92,16 +92,31 @@ def _get_standard_coupled_variables(self, variables): phi_s_n = variables["Negative electrode potential"] phi_s_p = variables["Positive electrode potential"] + # Ohmic heating in solid Q_ohm_s_cn, Q_ohm_s_cp = self._current_collector_heating(variables) Q_ohm_s_n = -pybamm.inner(i_s_n, pybamm.grad(phi_s_n)) Q_ohm_s_s = pybamm.FullBroadcast(0, ["separator"], "current collector") Q_ohm_s_p = -pybamm.inner(i_s_p, pybamm.grad(phi_s_p)) Q_ohm_s = pybamm.Concatenation(Q_ohm_s_n, Q_ohm_s_s, Q_ohm_s_p) - Q_ohm_e = -pybamm.inner(i_e, pybamm.grad(phi_e)) + # Ohmic heating in electrolyte + # TODO: change full stefan-maxwell conductivity so that i_e is always + # a Concatenation + if isinstance(i_e, pybamm.Concatenation): + # compute by domain if possible + i_e_n, i_e_s, i_e_p = i_e.orphans + phi_e_n, phi_e_s, phi_e_p = phi_e.orphans + Q_ohm_e_n = -pybamm.inner(i_e_n, pybamm.grad(phi_e_n)) + Q_ohm_e_s = -pybamm.inner(i_e_s, pybamm.grad(phi_e_s)) + Q_ohm_e_p = -pybamm.inner(i_e_p, pybamm.grad(phi_e_p)) + Q_ohm_e = pybamm.Concatenation(Q_ohm_e_n, Q_ohm_e_s, Q_ohm_e_p) + else: + Q_ohm_e = -pybamm.inner(i_e, pybamm.grad(phi_e)) + # Total Ohmic heating Q_ohm = Q_ohm_s + Q_ohm_e + # Irreversible electrochemical heating Q_rxn_n = j_n * eta_r_n Q_rxn_p = j_p * eta_r_p Q_rxn = pybamm.Concatenation( @@ -112,6 +127,7 @@ def _get_standard_coupled_variables(self, variables): ] ) + # Reversible electrochemical heating Q_rev_n = j_n * (param.Theta ** (-1) + T_n) * dUdT_n Q_rev_p = j_p * (param.Theta ** (-1) + T_p) * dUdT_p Q_rev = pybamm.Concatenation( @@ -122,6 +138,7 @@ def _get_standard_coupled_variables(self, variables): ] ) + # Total heating Q = Q_ohm + Q_rxn + Q_rev # Compute the X-average over the current collectors by default. @@ -133,73 +150,43 @@ def _get_standard_coupled_variables(self, variables): Q_rev_av = self._x_average(Q_rev, 0, 0) Q_av = self._x_average(Q, Q_ohm_s_cn, Q_ohm_s_cp) + # Compute volume-averaged heat source terms Q_ohm_vol_av = self._yz_average(Q_ohm_av) Q_rxn_vol_av = self._yz_average(Q_rxn_av) Q_rev_vol_av = self._yz_average(Q_rev_av) Q_vol_av = self._yz_average(Q_av) + # Dimensional scaling for heat source terms + Q_scale = param.i_typ * param.potential_scale / param.L_x + variables.update( { "Ohmic heating": Q_ohm, - "Ohmic heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_ohm - / param.L_x, + "Ohmic heating [W.m-3]": Q_ohm * Q_scale, "X-averaged Ohmic heating": Q_ohm_av, - "X-averaged Ohmic heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_ohm_av - / param.L_x, + "X-averaged Ohmic heating [W.m-3]": Q_ohm_av * Q_scale, "Volume-averaged Ohmic heating": Q_ohm_vol_av, - "Volume-averaged Ohmic heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_ohm_vol_av - / param.L_x, + "Volume-averaged Ohmic heating [W.m-3]": Q_ohm_vol_av * Q_scale, "Irreversible electrochemical heating": Q_rxn, - "Irreversible electrochemical heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_rxn - / param.L_x, - "X-averaged electrochemical heating": Q_rxn_av, - "X-averaged electrochemical heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_rxn_av - / param.L_x, - "Volume-averaged electrochemical heating": Q_rxn_vol_av, - "Volume-averaged electrochemical heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_rxn_vol_av - / param.L_x, + "Irreversible electrochemical heating [W.m-3]": Q_rxn * Q_scale, + "X-averaged irreversible electrochemical heating": Q_rxn_av, + "X-averaged irreversible electrochemical heating [W.m-3]": Q_rxn_av + * Q_scale, + "Volume-averaged irreversible electrochemical heating": Q_rxn_vol_av, + "Volume-averaged irreversible electrochemical heating [W.m-3]": + Q_rxn_vol_av * Q_scale, "Reversible heating": Q_rev, - "Reversible heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_rev - / param.L_x, + "Reversible heating [W.m-3]": Q_rev * Q_scale, "X-averaged reversible heating": Q_rev_av, - "X-averaged reversible heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_rev_av - / param.L_x, + "X-averaged reversible heating [W.m-3]": Q_rev_av * Q_scale, "Volume-averaged reversible heating": Q_rev_vol_av, - "Volume-averaged reversible heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_rev_vol_av - / param.L_x, + "Volume-averaged reversible heating [W.m-3]": Q_rev_vol_av * Q_scale, "Total heating": Q, - "Total heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q - / param.L_x, + "Total heating [W.m-3]": Q * Q_scale, "X-averaged total heating": Q_av, - "X-averaged total heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_av - / param.L_x, + "X-averaged total heating [W.m-3]": Q_av * Q_scale, "Volume-averaged total heating": Q_vol_av, - "Volume-averaged total heating [A.V.m-3]": param.i_typ - * param.potential_scale - * Q_vol_av - / param.L_x, + "Volume-averaged total heating [W.m-3]": Q_vol_av * Q_scale, } ) return variables @@ -252,3 +239,16 @@ def _x_average(self, var, var_cn, var_cp): + self.param.l_cp * var_cp ) / self.param.l return out + + def _effective_properties(self): + """ + Computes the effective effective product of density and specific heat, and + effective thermal conductivity, respectively. These are computed differently + depending upon whether current collectors are included or not. Defualt + behaviour is to assume the presence of current collectors. Due to the choice + of non-dimensionalisation, the dimensionless effective properties are equal + to 1 in the case where current collectors are accounted for. + """ + rho_eff = pybamm.Scalar(1) + lambda_eff = pybamm.Scalar(1) + return rho_eff, lambda_eff diff --git a/pybamm/models/submodels/thermal/isothermal/isothermal.py b/pybamm/models/submodels/thermal/isothermal/isothermal.py index 20dc2721ba..3c12d58d5e 100644 --- a/pybamm/models/submodels/thermal/isothermal/isothermal.py +++ b/pybamm/models/submodels/thermal/isothermal/isothermal.py @@ -39,17 +39,17 @@ def get_coupled_variables(self, variables): variables.update( { "Ohmic heating": pybamm.Scalar(0), - "Ohmic heating [A.V.m-3]": pybamm.Scalar(0), + "Ohmic heating [W.m-3]": pybamm.Scalar(0), "Irreversible electrochemical heating": pybamm.Scalar(0), - "Irreversible electrochemical heating [A.V.m-3]": pybamm.Scalar(0), + "Irreversible electrochemical heating [W.m-3]": pybamm.Scalar(0), "Reversible heating": pybamm.Scalar(0), - "Reversible heating [A.V.m-3]": pybamm.Scalar(0), + "Reversible heating [W.m-3]": pybamm.Scalar(0), "Total heating": pybamm.Scalar(0), - "Total heating [A.V.m-3]": pybamm.Scalar(0), + "Total heating [W.m-3]": pybamm.Scalar(0), "X-averaged total heating": pybamm.Scalar(0), - "X-averaged total heating [A.V.m-3]": pybamm.Scalar(0), + "X-averaged total heating [W.m-3]": pybamm.Scalar(0), "Volume-averaged total heating": pybamm.Scalar(0), - "Volume-averaged total heating [A.V.m-3]": pybamm.Scalar(0), + "Volume-averaged total heating [W.m-3]": pybamm.Scalar(0), } ) return variables @@ -73,6 +73,6 @@ def _yz_average(self, var): def _x_average(self, var, var_cn, var_cp): """ Temperature is uniform and heat source terms are zero, so the average - returns the input variable. + returns zeros broadcasted onto the current collector domain. """ - return var + return pybamm.PrimaryBroadcast(0, "current collector") 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 9d81bf76ca..a919e6003b 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 @@ -50,8 +50,10 @@ def _current_collector_heating(self, variables): return Q_s_cn, Q_s_cp def _yz_average(self, var): - """Computes the y-z average by integration over y and z - In this case this is just equal to the input variable""" + """ + Computes the y-z average by integration over y and z + In this case this is just equal to the input variable + """ return var def _x_average(self, var, var_cn, var_cp): diff --git a/pybamm/models/submodels/thermal/x_lumped/__init__.py b/pybamm/models/submodels/thermal/x_lumped/__init__.py index b07e6f3d1f..de180a7f5d 100644 --- a/pybamm/models/submodels/thermal/x_lumped/__init__.py +++ b/pybamm/models/submodels/thermal/x_lumped/__init__.py @@ -3,3 +3,4 @@ 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 3114a2ceb2..fa93592e60 100644 --- a/pybamm/models/submodels/thermal/x_lumped/base_x_lumped.py +++ b/pybamm/models/submodels/thermal/x_lumped/base_x_lumped.py @@ -51,3 +51,11 @@ def _flux_law(self, T): def set_initial_conditions(self, variables): T = variables["X-averaged cell temperature"] self.initial_conditions = {T: self.param.T_init} + + def _surface_cooling_coefficient(self): + """Returns the surface cooling coefficient in for x-lumped models.""" + # Account for surface area to volume ratio in cooling coefficient + # Note: assumes pouch cell geometry + A = self.param.l_y * self.param.l_z + V = self.param.l * self.param.l_y * self.param.l_z + return -2 * self.param.h * A / V / (self.param.delta ** 2) 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 d705c0d684..1d2025a16a 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 @@ -14,12 +14,10 @@ def set_rhs(self, variables): T_av = variables["X-averaged cell temperature"] Q_av = variables["X-averaged total heating"] + cooling_coeff = self._surface_cooling_coefficient() + self.rhs = { - T_av: ( - self.param.B * Q_av - - (2 * self.param.h / (self.param.delta ** 2) / self.param.l) * T_av - ) - / self.param.C_th + T_av: (self.param.B * Q_av + cooling_coeff * T_av) / 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 71599b3203..fa96d98e70 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,12 +15,11 @@ def __init__(self, param): def set_rhs(self, variables): T_av = variables["X-averaged cell temperature"] Q_av = variables["X-averaged total heating"] + + cooling_coeff = self._surface_cooling_coefficient() + self.rhs = { - T_av: ( - pybamm.laplacian(T_av) - + self.param.B * Q_av - - (2 * self.param.h / (self.param.delta ** 2) / self.param.l) * T_av - ) + T_av: (pybamm.laplacian(T_av) + self.param.B * Q_av + cooling_coeff * T_av) / self.param.C_th } 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 new file mode 100644 index 0000000000..226a7e59a2 --- /dev/null +++ b/pybamm/models/submodels/thermal/x_lumped/x_lumped_1D_set_temperature.py @@ -0,0 +1,47 @@ +# +# 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 a8117bd5b5..b9d301cc14 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 @@ -16,6 +16,8 @@ def set_rhs(self, variables): T_av = variables["X-averaged cell temperature"] Q_av = variables["X-averaged total heating"] + cooling_coeff = self._surface_cooling_coefficient() + # Add boundary source term which accounts for surface cooling around # the edge of the domain in the weak formulation. # TODO: update to allow different cooling conditions at the tabs @@ -23,8 +25,7 @@ def set_rhs(self, variables): T_av: ( pybamm.laplacian(T_av) + self.param.B * pybamm.source(Q_av, T_av) - - (2 * self.param.h / (self.param.delta ** 2) / self.param.l) - * pybamm.source(T_av, T_av) + + cooling_coeff * pybamm.source(T_av, T_av) - (self.param.h / self.param.delta) * pybamm.source(T_av, T_av, boundary=True) ) 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 1e35fd4b88..db66a8f36f 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 @@ -28,12 +28,13 @@ def set_rhs(self, variables): T_av = variables["X-averaged cell temperature"] Q_av = variables["X-averaged total heating"] + # Get effective properties + rho_eff, _ = self._effective_properties() + cooling_coeff = self._surface_cooling_coefficient() + self.rhs = { - T_av: ( - self.param.B * Q_av - - (2 * self.param.h / (self.param.delta ** 2) / self.param.l) * T_av - ) - / self.param.C_th + T_av: (self.param.B * Q_av + cooling_coeff * T_av) + / (self.param.C_th * rho_eff) } def _current_collector_heating(self, variables): @@ -42,6 +43,18 @@ def _current_collector_heating(self, variables): Q_s_cp = pybamm.Scalar(0) return Q_s_cn, Q_s_cp + def _surface_cooling_coefficient(self): + """ + Returns the surface cooling coefficient in the absence of current + collectors. + """ + # Account for surface area to volume ratio in cooling coefficient + # Note: assumes pouch cell geometry and volume doesn't include current + # collectors + A = self.param.l_y * self.param.l_z + V = self.param.l_x * self.param.l_y * self.param.l_z + return -2 * self.param.h * A / V / (self.param.delta ** 2) + def _yz_average(self, var): """In 1D volume-averaged quantities are unchanged""" return var @@ -52,3 +65,26 @@ def _x_average(self, var, var_cn, var_cp): collectors. This overwrites the default behaviour of 'base_thermal'. """ return pybamm.x_average(var) + + def _effective_properties(self): + """ + Computes the effective effective product of density and specific heat, and + effective thermal conductivity, respectively. This overwrites the default + behaviour of 'base_thermal'. + """ + l_n = self.param.l_n + l_s = self.param.l_s + l_p = self.param.l_p + rho_n = self.param.rho_n + rho_s = self.param.rho_s + rho_p = self.param.rho_p + lambda_n = self.param.lambda_n + lambda_s = self.param.lambda_s + lambda_p = self.param.lambda_p + + rho_eff = (rho_n * l_n + rho_s * l_s + rho_p * l_p) / (l_n + l_n + l_p) + lambda_eff = (lambda_n * l_n + lambda_s * l_s + lambda_p * l_p) / ( + l_n + l_n + l_p + ) + + return rho_eff, lambda_eff diff --git a/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_1D_current_collector.py b/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_1D_current_collector.py index ffca162efa..a4919709f4 100644 --- a/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_1D_current_collector.py +++ b/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_1D_current_collector.py @@ -32,6 +32,7 @@ def _current_collector_heating(self, variables): def _surface_cooling_coefficient(self): """Returns the surface cooling coefficient in 1+1D""" + # Note: assumes pouch cell geometry return ( -2 * self.param.h / (self.param.delta ** 2) / self.param.l - self.param.l_z * self.param.h / self.param.delta diff --git a/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_2D_current_collector.py b/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_2D_current_collector.py index fcfbbd5329..60fe551a74 100644 --- a/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_2D_current_collector.py +++ b/pybamm/models/submodels/thermal/xyz_lumped/xyz_lumped_2D_current_collector.py @@ -33,6 +33,7 @@ def _current_collector_heating(self, variables): def _surface_cooling_coefficient(self): """Returns the surface cooling coefficient in 2+1D""" + # Note: assumes pouch cell geometry return ( -2 * self.param.h / (self.param.delta ** 2) / self.param.l - 2 * (self.param.l_y + self.param.l_z) * self.param.h / self.param.delta diff --git a/pybamm/parameters/electrical_parameters.py b/pybamm/parameters/electrical_parameters.py index 36c4f6b215..8db66dbe29 100644 --- a/pybamm/parameters/electrical_parameters.py +++ b/pybamm/parameters/electrical_parameters.py @@ -5,13 +5,6 @@ import numpy as np -def abs_non_zero(x): - if x == 0: # pragma: no cover - return 1 - else: - return abs(x) - - # -------------------------------------------------------------------------------------- # Dimensional Parameters I_typ = pybamm.Parameter("Typical current [A]") @@ -21,7 +14,7 @@ def abs_non_zero(x): "Number of electrodes connected in parallel to make a cell" ) i_typ = pybamm.Function( - abs_non_zero, (I_typ / (n_electrodes_parallel * pybamm.geometric_parameters.A_cc)) + np.abs, I_typ / (n_electrodes_parallel * pybamm.geometric_parameters.A_cc) ) voltage_low_cut_dimensional = pybamm.Parameter("Lower voltage cut-off [V]") voltage_high_cut_dimensional = pybamm.Parameter("Upper voltage cut-off [V]") diff --git a/pybamm/parameters/geometric_parameters.py b/pybamm/parameters/geometric_parameters.py index 1d7ea4780b..3b70db4865 100644 --- a/pybamm/parameters/geometric_parameters.py +++ b/pybamm/parameters/geometric_parameters.py @@ -53,11 +53,13 @@ l_s = L_s / L_x l_p = L_p / L_x l_cp = L_cp / L_x +l_x = L_x / L_x l_y = L_y / L_z l_z = L_z / L_z +a_cc = l_y * l_z l = L / L_x -delta = L_x / L_z +delta = L_x / L_z # Aspect ratio # Tab geometry l_tab_n = L_tab_n / L_z diff --git a/pybamm/parameters/parameter_sets.py b/pybamm/parameters/parameter_sets.py index 943591434d..31c09106cc 100644 --- a/pybamm/parameters/parameter_sets.py +++ b/pybamm/parameters/parameter_sets.py @@ -46,4 +46,3 @@ "electrolyte": "sulfuric_acid_Sulzer2019", "experiment": "1C_discharge_from_full", } - diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py index 5aff085d3e..fbc388bb95 100644 --- a/pybamm/parameters/parameter_values.py +++ b/pybamm/parameters/parameter_values.py @@ -4,7 +4,6 @@ import pybamm import pandas as pd import os -import numpy as np class ParameterValues(dict): @@ -126,6 +125,13 @@ def read_parameters_csv(self, filename): df.dropna(how="all", inplace=True) return {k: v for (k, v) in zip(df["Name [units]"], df["Value"])} + def __setitem__(self, key, value): + "Call the update functionality when doing a setitem" + self.update({key: value}) + + def setitemsuper(self, key, value): + super().__setitem__(key, value) + def update(self, values, check_conflict=False, path=""): # check parameter values values = self.check_and_update_parameter_values(values) @@ -155,16 +161,29 @@ def update(self, values, check_conflict=False, path=""): # Extra set of brackets at the end makes an instance of the # class self[name] = getattr(pybamm, value[15:])() - # Data is flagged with the string "[data]" - elif value.startswith("[data]"): - data = np.loadtxt(os.path.join(path, value[6:] + ".csv")) + # 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( + pybamm.root_dir(), "input", "drive_cycles" + ) + filename = os.path.join(data_path, value[14:] + ".csv") + function_name = value[14:] + else: + filename = os.path.join(path, value[6:] + ".csv") + function_name = value[6:] + data = pd.read_csv( + filename, comment="#", skip_blank_lines=True + ).to_numpy() # Save name and data - self[name] = (value[6:], data) + self.setitemsuper(name, (function_name, data)) # Anything else should be a converted to a float else: - self[name] = float(value) + self.setitemsuper(name, float(value)) else: - self[name] = value + self.setitemsuper(name, value) # reset processed symbols self._processed_symbols = {} @@ -174,14 +193,14 @@ def check_and_update_parameter_values(self, values): raise ValueError( """ "C-rate" cannot be zero. A possible alternative is to set - "Current function" to `pybamm.GetConstantCurrent(current=0)` instead. + "Current function" to `pybamm.ConstantCurrent(current=0)` instead. """ ) 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" to `pybamm.GetConstantCurrent(current=0)` instead. + "Current function" to `pybamm.ConstantCurrent(current=0)` instead. """ ) # If the capacity of the cell has been provided, make sure "C-rate" and current @@ -211,13 +230,13 @@ def check_and_update_parameter_values(self, values): values["C-rate"] = float(values["Typical current [A]"]) / capacity return values - def process_model(self, model, processing="process"): + def process_model(self, unprocessed_model, processing="process", inplace=True): """Assign parameter values to a model. Currently inplace, could be changed to return a new model. Parameters ---------- - model : :class:`pybamm.BaseModel` + unprocessed_model : :class:`pybamm.BaseModel` Model to assign parameter values for processing : str, optional Flag to indicate how to process model (default 'process') @@ -226,6 +245,9 @@ def process_model(self, model, processing="process"): and replace any Parameter with a Value) * 'update': Calls :meth:`update_scalars()` for use on already-processed \ model (update the value of any Scalars in the expression tree.) + inplace: bool, optional + If True, replace the parameters in the model in place. Otherwise, return a + new model with parameter values set. Default is True. Raises ------ @@ -233,9 +255,25 @@ def process_model(self, model, processing="process"): If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) """ - pybamm.logger.info("Start setting parameters for {}".format(model.name)) - - if len(model.rhs) == 0 and len(model.algebraic) == 0: + pybamm.logger.info( + "Start setting parameters for {}".format(unprocessed_model.name) + ) + + # set up inplace vs not inplace + if inplace: + # any changes to model_disc attributes will change model attributes + # since they point to the same object + model = unprocessed_model + else: + # create a blank model of the same class + model = unprocessed_model.__class__(unprocessed_model.options) + model.name = unprocessed_model.name + model.options = unprocessed_model.options + model.use_jacobian = unprocessed_model.use_jacobian + model.use_simplify = unprocessed_model.use_simplify + model.convert_to_format = unprocessed_model.convert_to_format + + if len(unprocessed_model.rhs) == 0 and len(unprocessed_model.algebraic) == 0: raise pybamm.ModelError("Cannot process parameters for empty model") if processing == "process": @@ -243,13 +281,13 @@ def process_model(self, model, processing="process"): elif processing == "update": processing_function = self.update_scalars - for variable, equation in model.rhs.items(): + for variable, equation in unprocessed_model.rhs.items(): pybamm.logger.debug( "{} parameters for {!r} (rhs)".format(processing.capitalize(), variable) ) model.rhs[variable] = processing_function(equation) - for variable, equation in model.algebraic.items(): + for variable, equation in unprocessed_model.algebraic.items(): pybamm.logger.debug( "{} parameters for {!r} (algebraic)".format( processing.capitalize(), variable @@ -257,7 +295,7 @@ def process_model(self, model, processing="process"): ) model.algebraic[variable] = processing_function(equation) - for variable, equation in model.initial_conditions.items(): + for variable, equation in unprocessed_model.initial_conditions.items(): pybamm.logger.debug( "{} parameters for {!r} (initial conditions)".format( processing.capitalize(), variable @@ -270,7 +308,7 @@ def process_model(self, model, processing="process"): # small number of variables, e.g. {"negative tab": neg. tab bc, # "positive tab": pos. tab bc "no tab": no tab bc}. new_boundary_conditions = {} - for variable, bcs in model.boundary_conditions.items(): + for variable, bcs in unprocessed_model.boundary_conditions.items(): processed_variable = processing_function(variable) new_boundary_conditions[processed_variable] = {} for side in ["left", "right", "negative tab", "positive tab", "no tab"]: @@ -288,14 +326,14 @@ def process_model(self, model, processing="process"): model.boundary_conditions = new_boundary_conditions - for variable, equation in model.variables.items(): + for variable, equation in unprocessed_model.variables.items(): pybamm.logger.debug( "{} parameters for {!r} (variables)".format( processing.capitalize(), variable ) ) model.variables[variable] = processing_function(equation) - for event, equation in model.events.items(): + for event, equation in unprocessed_model.events.items(): pybamm.logger.debug( "{} parameters for event '{}''".format(processing.capitalize(), event) ) @@ -303,6 +341,8 @@ def process_model(self, model, processing="process"): pybamm.logger.info("Finish setting parameters for {}".format(model.name)) + return model + def update_model(self, model, disc): """Process a discretised model. Currently inplace, could be changed to return a new model. @@ -388,16 +428,12 @@ def _process_symbol(self, symbol): # if current setter, process any parameters that are symbols and # store the evaluated symbol in the parameters_eval dict - if isinstance(function_name, pybamm.GetCurrent): + if isinstance(function_name, pybamm.BaseCurrent): for param, sym in function_name.parameters.items(): if isinstance(sym, pybamm.Symbol): new_sym = self.process_symbol(sym) function_name.parameters[param] = new_sym function_name.parameters_eval[param] = new_sym.evaluate() - # If loading data, need to update interpolant with - # evaluated parameters - if isinstance(function_name, pybamm.GetCurrentData): - function_name.interpolate() # Create Function or Interpolant objec if isinstance(function_name, tuple): @@ -479,7 +515,7 @@ def update_scalars(self, symbol): # KeyError -> name not in parameter dict, don't update continue elif isinstance(x, pybamm.Function): - if isinstance(x.function, pybamm.GetCurrent): + if isinstance(x.function, pybamm.BaseCurrent): # Need to update parameters dict to be that of the new current # function and make new parameters_eval dict to be processed x.function.parameters = self["Current function"].parameters @@ -497,9 +533,6 @@ def update_scalars(self, symbol): # KeyError -> name not in parameter dict, evaluate # unnamed Scalar x.function.parameters_eval[param] = new_sym.evaluate() - if isinstance(x.function, pybamm.GetCurrentData): - # update interpolant - x.function.interpolate() return symbol diff --git a/pybamm/parameters/standard_current_functions/base_current.py b/pybamm/parameters/standard_current_functions/base_current.py index 3eb6cfdf2c..2f239d00b7 100644 --- a/pybamm/parameters/standard_current_functions/base_current.py +++ b/pybamm/parameters/standard_current_functions/base_current.py @@ -3,7 +3,7 @@ # -class GetCurrent(object): +class BaseCurrent(object): """ The base class for setting the input current for a simulation. The parameters dictionary holds the symbols of any paramters required to evaluate the current. diff --git a/pybamm/parameters/standard_current_functions/get_constant_current.py b/pybamm/parameters/standard_current_functions/constant_current.py similarity index 86% rename from pybamm/parameters/standard_current_functions/get_constant_current.py rename to pybamm/parameters/standard_current_functions/constant_current.py index 099cec05bf..ccfdefbd73 100644 --- a/pybamm/parameters/standard_current_functions/get_constant_current.py +++ b/pybamm/parameters/standard_current_functions/constant_current.py @@ -4,7 +4,7 @@ import pybamm -class GetConstantCurrent(pybamm.GetCurrent): +class ConstantCurrent(pybamm.BaseCurrent): """ Sets a constant input current for a simulation. @@ -13,7 +13,7 @@ class GetConstantCurrent(pybamm.GetCurrent): current : :class:`pybamm.Symbol` or float The size of the current in Amperes. - **Extends:"": :class:`pybamm.GetCurrent` + **Extends:"": :class:`pybamm.BaseCurrent` """ def __init__(self, current=pybamm.electrical_parameters.I_typ): diff --git a/pybamm/parameters/standard_current_functions/get_current_data.py b/pybamm/parameters/standard_current_functions/get_current_data.py deleted file mode 100644 index d683120ea3..0000000000 --- a/pybamm/parameters/standard_current_functions/get_current_data.py +++ /dev/null @@ -1,91 +0,0 @@ -# -# Load current profile from a csv file -# -import pybamm -import os -import pandas as pd -import numpy as np -import warnings -import scipy.interpolate as interp - - -class GetCurrentData(pybamm.GetCurrent): - """ - A class which loads a current profile from a csv file and creates an - interpolating function which can be called during solve. - - Parameters - ---------- - filename : str - The name of the file to load. - units : str, optional - The units of the current data which is to be loaded. Can be "[]" for - dimenionless data (default), or "[A]" for current in Amperes. - current_scale : :class:`pybamm.Symbol` or float, optional - The scale the current in Amperes if loading non-dimensional data. Default - is to use the typical current I_typ - - **Extends:"": :class:`pybamm.GetCurrent` - """ - - def __init__( - self, filename, units="[]", current_scale=pybamm.electrical_parameters.I_typ - ): - self.parameters = {"Current [A]": current_scale} - self.parameters_eval = {"Current [A]": current_scale} - - # Load data from csv - if filename: - pybamm_path = pybamm.root_dir() - data = pd.read_csv( - os.path.join(pybamm_path, "input", "drive_cycles", filename), - comment="#", - skip_blank_lines=True, - ).to_dict("list") - - self.time = np.array(data["time [s]"]) - self.units = units - self.current = np.array(data["current " + units]) - # If voltage data is present, load it into the class - try: - self.voltage = np.array(data["voltage [V]"]) - except KeyError: - self.voltage = None - else: - raise pybamm.ModelError("No input file provided for current") - - def __str__(self): - return "Current from data" - - def interpolate(self): - " Creates the interpolant from the loaded data " - # If data is dimenionless, multiply by a typical current (e.g. data - # could be C-rate and current_scale the 1C discharge current). Otherwise, - # just import the current data. - if self.units == "[]": - current = self.parameters_eval["Current [A]"] * self.current - elif self.units == "[A]": - current = self.current - else: - raise pybamm.ModelError( - "Current data must have units [A] or be dimensionless" - ) - # Interpolate using Piecewise Cubic Hermite Interpolating Polynomial - # (does not overshoot non-smooth data) - self.current_interp = interp.PchipInterpolator(self.time, current) - - def __call__(self, t): - """ - Calls the interpolating function created using the data from user-supplied - data file at time t (seconds). - """ - - if np.min(t) < self.time[0] or np.max(t) > self.time[-1]: - warnings.warn( - "Requested time ({}) is outside of the data range [{}, {}]".format( - t, self.time[0], self.time[-1] - ), - pybamm.ModelWarning, - ) - - return self.current_interp(t) diff --git a/pybamm/parameters/standard_current_functions/get_user_current.py b/pybamm/parameters/standard_current_functions/get_user_current.py index 0b1cc0b40f..c4c01b13bc 100644 --- a/pybamm/parameters/standard_current_functions/get_user_current.py +++ b/pybamm/parameters/standard_current_functions/get_user_current.py @@ -4,7 +4,7 @@ import pybamm -class GetUserCurrent(pybamm.GetCurrent): +class UserCurrent(pybamm.BaseCurrent): """ Sets a user-defined function as the input current for a simulation. @@ -16,7 +16,7 @@ class GetUserCurrent(pybamm.GetCurrent): any keyword arguments, i.e. function(t, **kwargs). **kwargs : Any keyword arguments required by function. - **Extends:"": :class:`pybamm.GetCurrent` + **Extends:"": :class:`pybamm.BaseCurrent` """ def __init__(self, function, **kwargs): diff --git a/pybamm/parameters/standard_current_functions/user_current.py b/pybamm/parameters/standard_current_functions/user_current.py new file mode 100644 index 0000000000..c4c01b13bc --- /dev/null +++ b/pybamm/parameters/standard_current_functions/user_current.py @@ -0,0 +1,31 @@ +# +# Allow a user-defined current function +# +import pybamm + + +class UserCurrent(pybamm.BaseCurrent): + """ + Sets a user-defined function as the input current for a simulation. + + Parameters + ---------- + function : method + The method which returns the current (in Amperes) as a function of time + (in seconds). The first argument of function must be time, followed by + any keyword arguments, i.e. function(t, **kwargs). + **kwargs : Any keyword arguments required by function. + + **Extends:"": :class:`pybamm.BaseCurrent` + """ + + def __init__(self, function, **kwargs): + self.parameters = kwargs + self.parameters_eval = kwargs + self.function = function + + def __str__(self): + return "User defined current" + + def __call__(self, t): + return self.function(t, **self.parameters_eval) diff --git a/pybamm/parameters/standard_parameters_lead_acid.py b/pybamm/parameters/standard_parameters_lead_acid.py index 90840bd316..496d5fb85e 100644 --- a/pybamm/parameters/standard_parameters_lead_acid.py +++ b/pybamm/parameters/standard_parameters_lead_acid.py @@ -416,6 +416,9 @@ def U_p_dimensional(c_e, T): c_n_init = c_e_init c_p_init = c_e_init +# Thermal effects not implemented for lead-acid, but parameter needed for consistency +Theta = pybamm.Scalar(0) # ratio of typical temperature change to ambient temperature + # -------------------------------------------------------------------------------------- "5. Dimensionless Functions" diff --git a/pybamm/parameters/standard_parameters_lithium_ion.py b/pybamm/parameters/standard_parameters_lithium_ion.py index 69403e3bd2..73a9f4b793 100644 --- a/pybamm/parameters/standard_parameters_lithium_ion.py +++ b/pybamm/parameters/standard_parameters_lithium_ion.py @@ -38,7 +38,6 @@ L = pybamm.geometric_parameters.L A_cc = pybamm.geometric_parameters.A_cc - # Tab geometry L_tab_n = pybamm.geometric_parameters.L_tab_n Centre_y_tab_n = pybamm.geometric_parameters.Centre_y_tab_n @@ -254,8 +253,10 @@ def U_p_dimensional(sto, T): l_s = pybamm.geometric_parameters.l_s l_p = pybamm.geometric_parameters.l_p l_cp = pybamm.geometric_parameters.l_cp +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 @@ -405,13 +406,15 @@ def m_p(T): def U_n(c_s_n, T): "Dimensionless open-circuit potential in the negative electrode" sto = c_s_n - return (U_n_dimensional(sto, T) - U_n_ref) / potential_scale + T_dim = Delta_T * T + T_ref + return (U_n_dimensional(sto, T_dim) - U_n_ref) / potential_scale def U_p(c_s_p, T): "Dimensionless open-circuit potential in the positive electrode" sto = c_s_p - return (U_p_dimensional(sto, T) - U_p_ref) / potential_scale + T_dim = Delta_T * T + T_ref + return (U_p_dimensional(sto, T_dim) - U_p_ref) / potential_scale def dUdT_n(c_s_n): diff --git a/pybamm/parameters/thermal_parameters.py b/pybamm/parameters/thermal_parameters.py index 9c08124240..4041c4f5b1 100644 --- a/pybamm/parameters/thermal_parameters.py +++ b/pybamm/parameters/thermal_parameters.py @@ -38,12 +38,7 @@ "Positive current collector thermal conductivity [W.m-1.K-1]" ) -# Thermal parameters -h_dim = pybamm.Parameter("Heat transfer coefficient [W.m-2.K-1]") -Phi_dim = pybamm.Scalar(1) # typical scale for voltage drop across cell (order 1V) -Delta_T = ( - pybamm.electrical_parameters.i_typ * Phi_dim / h_dim -) # computed from balance of typical cross-cell Ohmic heating with surface heat loss +# Effective thermal properties rho_eff_dim = ( rho_cn_dim * c_p_cn_dim * pybamm.geometric_parameters.L_cn + rho_n_dim * c_p_n_dim * pybamm.geometric_parameters.L_n @@ -59,6 +54,15 @@ + lambda_cp_dim * pybamm.geometric_parameters.L_cp ) / pybamm.geometric_parameters.L +# Cooling coefficient +h_dim = pybamm.Parameter("Heat transfer coefficient [W.m-2.K-1]") + +# Typical temperature rise +Phi_dim = pybamm.Scalar(1) # typical scale for voltage drop across cell (order 1V) +Delta_T = ( + pybamm.electrical_parameters.i_typ * Phi_dim / h_dim +) # computed from balance of typical cross-cell Ohmic heating with surface heat loss + # Activation energies E_r_n = pybamm.Parameter("Negative reaction rate activation energy [J.mol-1]") E_r_p = pybamm.Parameter("Positive reaction rate activation energy [J.mol-1]") diff --git a/pybamm/quick_plot.py b/pybamm/quick_plot.py index b2470f0ac3..472dc1237f 100644 --- a/pybamm/quick_plot.py +++ b/pybamm/quick_plot.py @@ -8,7 +8,7 @@ def ax_min(data): "Calculate appropriate minimum axis value for plotting" - data_min = np.min(data) + data_min = np.nanmin(data) if data_min <= 0: return 1.04 * data_min else: @@ -17,7 +17,7 @@ def ax_min(data): def ax_max(data): "Calculate appropriate maximum axis value for plotting" - data_max = np.max(data) + data_max = np.nanmax(data) if data_max <= 0: return 0.96 * data_max else: @@ -256,14 +256,26 @@ def reset_axis(self): # Get min and max y values y_min = np.min( [ - ax_min(var(self.ts[i], **{spatial_var_name: spatial_var_value})) + 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})) + 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 ] diff --git a/pybamm/simulation.py b/pybamm/simulation.py new file mode 100644 index 0000000000..774a5750b8 --- /dev/null +++ b/pybamm/simulation.py @@ -0,0 +1,279 @@ +import pybamm +import numpy as np +import copy + + +class Simulation: + """A Simulation class for easy building and running of PyBaMM simulations. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model to be simulated + """ + + def __init__( + self, + model, + geometry=None, + parameter_values=None, + submesh_types=None, + var_pts=None, + spatial_methods=None, + solver=None, + quick_plot_vars=None, + ): + self.model = model + + self.geometry = geometry or model.default_geometry + self._parameter_values = parameter_values or model.default_parameter_values + self._submesh_types = submesh_types or model.default_submesh_types + self._var_pts = var_pts or model.default_var_pts + self._spatial_methods = spatial_methods or model.default_spatial_methods + self._solver = solver or self._model.default_solver + self._quick_plot_vars = quick_plot_vars + + self.reset() + + def set_defaults(self): + """ + A method to set all the simulation specs to default values for the + supplied model. + """ + self.geometry = self._model.default_geometry + self._parameter_values = self._model.default_parameter_values + self._submesh_types = self._model.default_submesh_types + self._var_pts = self._model.default_var_pts + self._spatial_methods = self._model.default_spatial_methods + self._solver = self._model.default_solver + self._quick_plot_vars = None + + def reset(self): + """ + A method to reset a simulation back to its unprocessed state. + """ + self.model = self._model_class(self._model_options) + self.geometry = copy.deepcopy(self._unprocessed_geometry) + self._model_with_set_params = None + self._built_model = None + self._mesh = None + self._disc = None + self._solution = None + + def set_parameters(self): + """ + A method to set the parameters in the model and the associated geometry. If + the model has already been built or solved then this will first reset to the + unprocessed state and then set the parameter values. + """ + + if self.model_with_set_params: + return None + + self._model_with_set_params = self._parameter_values.process_model( + self._model, inplace=True + ) + self._parameter_values.process_geometry(self._geometry) + + def build(self): + """ + A method to build the model into a system of matrices and vectors suitable for + performing numerical computations. If the model has already been built or + solved then this function will have no effect. If you want to rebuild, + first use "reset()". This method will automatically set the parameters + if they have not already been set. + """ + + if self.built_model: + return None + + self.set_parameters() + self._mesh = pybamm.Mesh(self._geometry, self._submesh_types, self._var_pts) + self._disc = pybamm.Discretisation(self._mesh, self._spatial_methods) + self._built_model = self._disc.process_model(self._model, inplace=False) + + def solve(self, t_eval=None, solver=None): + """ + A method to solve the model. This method will automatically build + and set the model parameters if not already done so. + + Parameters + ---------- + t_eval : numeric type (optional) + The times at which to compute the solution + solver : :class:`pybamm.BaseSolver` + The solver to use to solve the model. + """ + self.build() + + if t_eval is None: + t_eval = np.linspace(0, 1, 100) + + if solver is None: + solver = self.solver + + self._solution = solver.solve(self.built_model, t_eval) + + def plot(self, quick_plot_vars=None): + """ + A method to quickly plot the outputs of the simulation. + + Parameters + ---------- + quick_plot_vars: list + A list of the variables to plot. + """ + + if self._solution is None: + raise ValueError( + "Model has not been solved, please solve the model before plotting." + ) + + if quick_plot_vars is None: + quick_plot_vars = self.quick_plot_vars + + plot = pybamm.QuickPlot( + self.built_model, + self._mesh, + self._solution, + output_variables=quick_plot_vars, + ) + plot.dynamic_plot() + + @property + def model(self): + return self._model + + @model.setter + def model(self, model): + self._model = model + self._model_class = model.__class__ + self._model_options = model.options + + @property + def model_with_set_params(self): + return self._model_with_set_params + + @property + def built_model(self): + return self._built_model + + @property + def model_options(self): + return self._model_options + + @property + def geometry(self): + return self._geometry + + @geometry.setter + def geometry(self, geometry): + self._geometry = geometry + self._unprocessed_geometry = copy.deepcopy(geometry) + + @property + def unprocessed_geometry(self): + return self._unprocessed_geometry + + @property + def parameter_values(self): + return self._parameter_values + + @property + def submesh_types(self): + return self._submesh_types + + @property + def var_pts(self): + return self._var_pts + + @property + def spatial_methods(self): + return self._spatial_methods + + @property + def solver(self): + return self._solver + + @solver.setter + def solver(self, solver): + self._solver = solver + + @property + def quick_plot_vars(self): + return self._quick_plot_vars + + @quick_plot_vars.setter + def quick_plot_vars(self, quick_plot_vars): + self._quick_plot_vars = quick_plot_vars + + @property + def solution(self): + return self._solution + + def specs( + self, + model_options=None, + geometry=None, + parameter_values=None, + submesh_types=None, + var_pts=None, + spatial_methods=None, + solver=None, + quick_plot_vars=None, + ): + """ + A method to set the various specs of the simulation. This method + automatically resets the model after the new specs have been set. + + Parameters + ---------- + model_options: dict (optional) + A dictionary of options to tweak the model you are using + 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 + submesh_types: dict (optional) + A dictionary of the types of submesh to use on each subdomain + var_pts: dict (optional) + A dictionary of the number of points used by each spatial + variable + spatial_methods: dict (optional) + A dictionary of the types of spatial method to use on each + domain (e.g. pybamm.FiniteVolume) + solver: :class:`pybamm.BaseSolver` + The solver to use to solve the model. + quick_plot_vars: list + A list of variables to plot automatically + """ + + if model_options: + self._model_options = model_options + + if geometry: + self.geometry = geometry + + if parameter_values: + self._parameter_values = parameter_values + if submesh_types: + self._submesh_types = submesh_types + if var_pts: + self._var_pts = var_pts + if spatial_methods: + self._spatial_methods = spatial_methods + if solver: + self._solver = solver + if quick_plot_vars: + self._quick_plot_vars = quick_plot_vars + + if ( + model_options + or geometry + or parameter_values + or submesh_types + or var_pts + or spatial_methods + ): + self.reset() diff --git a/pybamm/solvers/algebraic_solver.py b/pybamm/solvers/algebraic_solver.py index fe051cc344..ff69455ef9 100644 --- a/pybamm/solvers/algebraic_solver.py +++ b/pybamm/solvers/algebraic_solver.py @@ -24,6 +24,7 @@ class AlgebraicSolver(object): def __init__(self, method="lm", tol=1e-6): self.method = method self.tol = tol + self.name = "Algebraic solver ({})".format(method) @property def method(self): @@ -51,7 +52,7 @@ def solve(self, model): equations. """ - pybamm.logger.info("Start solving {}".format(model.name)) + pybamm.logger.info("Start solving {} with {}".format(model.name, self.name)) # Set up timer = pybamm.Timer() @@ -87,7 +88,6 @@ def jacobian(y): # Assign times solution.solve_time = timer.time() - solve_start_time - solution.total_time = timer.time() - start_time solution.set_up_time = set_up_time pybamm.logger.info("Finish solving {}".format(model.name)) @@ -204,14 +204,14 @@ def set_up(self, model): pybamm.logger.info("Simplifying jacobian") jac = simp.simplify(jac) - if model.use_to_python: + if model.convert_to_format == "python": pybamm.logger.info("Converting jacobian to python") jac = pybamm.EvaluatorPython(jac) else: jac = None - if model.use_to_python: + if model.convert_to_format == "python": pybamm.logger.info("Converting algebraic to python") concatenated_algebraic = pybamm.EvaluatorPython(concatenated_algebraic) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index ad1ad2b4a8..37f2fd819d 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -20,6 +20,7 @@ def __init__(self, method=None, rtol=1e-6, atol=1e-6): self._method = method self._rtol = rtol self._atol = atol + self.name = "Base solver" @property def method(self): @@ -64,7 +65,7 @@ def solve(self, model, t_eval): If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) """ - pybamm.logger.info("Start solving {}".format(model.name)) + 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: @@ -73,7 +74,10 @@ def solve(self, model, t_eval): # Set up timer = pybamm.Timer() start_time = timer.time() - self.set_up(model) + if model.convert_to_format == "casadi" or isinstance(self, pybamm.CasadiSolver): + self.set_up_casadi(model) + else: + self.set_up(model) set_up_time = timer.time() - start_time # Solve @@ -81,7 +85,6 @@ def solve(self, model, t_eval): # Assign times solution.solve_time = solve_time - solution.total_time = timer.time() - start_time solution.set_up_time = set_up_time pybamm.logger.info("Finish solving {} ({})".format(model.name, termination)) @@ -94,7 +97,7 @@ def solve(self, model, t_eval): ) return solution - def step(self, model, dt, npts=2): + def step(self, model, dt, npts=2, log=True): """ Step the solution of the model forward by a given time increment. The first time this method is called it executes the necessary setup by @@ -126,31 +129,40 @@ def step(self, model, dt, npts=2): # Run set up on first step if not hasattr(self, "y0"): - start_time = timer.time() - self.set_up(model) + pybamm.logger.info( + "Start stepping {} with {}".format(model.name, self.name) + ) + + if model.convert_to_format == "casadi" or isinstance( + self, pybamm.CasadiSolver + ): + self.set_up_casadi(model) + else: + pybamm.logger.debug( + "Start stepping {} with {}".format(model.name, self.name) + ) + self.set_up(model) self.t = 0.0 - set_up_time = timer.time() - start_time + set_up_time = timer.time() else: set_up_time = None # Step - pybamm.logger.info("Start stepping {}".format(model.name)) t_eval = np.linspace(self.t, self.t + dt, npts) solution, solve_time, termination = self.compute_solution(model, t_eval) # Assign times solution.solve_time = solve_time if set_up_time: - solution.total_time = timer.time() - start_time solution.set_up_time = set_up_time # Set self.t and self.y0 to their values at the final step self.t = solution.t[-1] self.y0 = solution.y[:, -1] - pybamm.logger.info("Finish stepping {} ({})".format(model.name, termination)) + pybamm.logger.debug("Finish stepping {} ({})".format(model.name, termination)) if set_up_time: - pybamm.logger.info( + pybamm.logger.debug( "Set-up time: {}, Step time: {}, Total time: {}".format( timer.format(solution.set_up_time), timer.format(solution.solve_time), @@ -158,7 +170,7 @@ def step(self, model, dt, npts=2): ) ) else: - pybamm.logger.info( + pybamm.logger.debug( "Step time: {}".format(timer.format(solution.solve_time)) ) return solution @@ -187,11 +199,17 @@ def set_up(self, model): The model whose solution to calculate. Must have attributes rhs and initial_conditions - Raises - ------ - :class:`pybamm.SolverError` - If the model contains any algebraic equations (in which case a DAE solver - should be used instead) + """ + raise NotImplementedError + + def set_up_casadi(self, model): + """Convert model to casadi format and use their inbuilt functionalities. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. Must have attributes rhs and + initial_conditions """ raise NotImplementedError diff --git a/pybamm/solvers/c_solvers/idaklu.cpp b/pybamm/solvers/c_solvers/idaklu.cpp index 0e4735d8b6..55704e2161 100644 --- a/pybamm/solvers/c_solvers/idaklu.cpp +++ b/pybamm/solvers/c_solvers/idaklu.cpp @@ -214,11 +214,12 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, residual_type res, jacobian_type jac, jac_get_type gjd, jac_get_type gjrv, jac_get_type gjcp, int nnz, event_type event, int number_of_events, int use_jacobian, np_array rhs_alg_id, - double abs_tol, double rel_tol) + np_array atol_np, double rel_tol) { auto t = t_np.unchecked<1>(); auto y0 = y0_np.unchecked<1>(); auto yp0 = yp0_np.unchecked<1>(); + auto atol = atol_np.unchecked<1>(); int number_of_states; number_of_states = y0_np.request().size; @@ -240,11 +241,13 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, // set initial value yval = N_VGetArrayPointer(yy); ypval = N_VGetArrayPointer(yp); + atval = N_VGetArrayPointer(avtol); int i; for (i = 0; i < number_of_states; i++) { yval[i] = y0[i]; ypval[i] = yp0[i]; + atval[i] = atol[i]; } // allocate memory for solver @@ -256,13 +259,6 @@ Solution solve(np_array t_np, np_array y0_np, np_array yp0_np, // set tolerances rtol = RCONST(rel_tol); - atval = N_VGetArrayPointer(avtol); - - for (i = 0; i < number_of_states; i++) - { - atval[i] = - RCONST(abs_tol); // nb: this can be set differently for each state - } IDASVtolerances(ida_mem, rtol, avtol); @@ -369,7 +365,7 @@ PYBIND11_MODULE(idaklu, m) py::arg("yp0"), py::arg("res"), py::arg("jac"), py::arg("get_jac_data"), py::arg("get_jac_row_vals"), py::arg("get_jac_col_ptr"), py::arg("nnz"), py::arg("events"), py::arg("number_of_events"), py::arg("use_jacobian"), - py::arg("rhs_alg_id"), py::arg("rtol"), py::arg("atol"), + py::arg("rhs_alg_id"), py::arg("atol"), py::arg("rtol"), py::return_value_policy::take_ownership); py::class_(m, "solution") diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py new file mode 100644 index 0000000000..c14f49802b --- /dev/null +++ b/pybamm/solvers/casadi_solver.py @@ -0,0 +1,247 @@ +# +# CasADi Solver class +# +import casadi +import pybamm +import numpy as np + + +class CasadiSolver(pybamm.DaeSolver): + """Solve a discretised model, using CasADi. + + **Extends**: :class:`pybamm.DaeSolver` + + Parameters + ---------- + method : str, optional + The method to use for solving the system ('cvodes', for ODEs, or 'idas', for + DAEs). Default is 'idas'. + mode : str + How to solve the model (default is "safe"): + + - "fast": perform direct integration, without accounting for events. \ + Recommended when simulating a drive cycle or other simulation where \ + no events should be triggered. + - "safe": perform step-and-check integration, checking whether events have \ + been triggered. Recommended for simulations of a full charge or discharge. + rtol : float, optional + The relative tolerance for the solver (default is 1e-6). + atol : float, optional + The absolute tolerance for the solver (default is 1e-6). + root_method : str, optional + The method to use for finding consistend initial conditions. Default is 'lm'. + root_tol : float, optional + The tolerance for root-finding. Default is 1e-6. + max_step_decrease_counts : float, optional + The maximum number of times step size can be decreased before an error is + raised. Default is 5. + extra_options : keyword arguments, optional + Any extra keyword-arguments; these are passed directly to the CasADi integrator. + Please consult `CasADi documentation `_ for + details. + + """ + + def __init__( + self, + method="idas", + mode="safe", + rtol=1e-6, + atol=1e-6, + root_method="lm", + root_tol=1e-6, + max_step_decrease_count=5, + **extra_options, + ): + super().__init__(method, rtol, atol, root_method, root_tol) + if mode in ["safe", "fast"]: + self.mode = mode + else: + raise ValueError( + """ + invalid mode '{}'. Must be either 'safe', for solving with events, + or 'fast', for solving quickly without events""".format( + mode + ) + ) + self.max_step_decrease_count = max_step_decrease_count + self.extra_options = extra_options + self.name = "CasADi solver ({}) with '{}' mode".format(method, mode) + + def solve(self, model, t_eval): + """ + Execute the solver setup and calculate the solution of the model at + specified times. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. Must have attributes rhs and + initial_conditions + t_eval : numeric type + The times at which to compute the solution + + Raises + ------ + :class:`pybamm.ValueError` + If an invalid mode is passed. + :class:`pybamm.ModelError` + If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) + + """ + if self.mode == "fast": + # Solve model normally by calling the solve method from parent class + return super().solve(model, t_eval) + elif model.events == {}: + pybamm.logger.info("No events found, running fast mode") + # Solve model normally by calling the solve method from parent class + return super().solve(model, t_eval) + elif self.mode == "safe": + # Step-and-check + timer = pybamm.Timer() + self.set_up_casadi(model) + set_up_time = timer.time() + init_event_signs = np.sign( + np.concatenate([event(0, self.y0) for event in self.event_funs]) + ) + solution = None + pybamm.logger.info( + "Start solving {} with {} in 'safe' mode".format(model.name, self.name) + ) + self.t = 0.0 + for dt in np.diff(t_eval): + # Step + solved = False + count = 0 + while not solved: + # Try to solve with the current step, if it fails then halve the + # step size and try again. This will make solution.t slightly + # different to t_eval, but shouldn't matter too much as it should + # only happen near events. + try: + current_step_sol = self.step(model, dt) + solved = True + except pybamm.SolverError: + dt /= 2 + count += 1 + if count >= self.max_step_decrease_count: + if solution is None: + t = 0 + else: + t = solution.t[-1] + raise pybamm.SolverError( + """ + Maximum number of decreased steps occurred at t={}. Try + solving the model up to this time only + """.format( + t + ) + ) + # Check most recent y + new_event_signs = np.sign( + np.concatenate( + [ + event(0, current_step_sol.y[:, -1]) + for event in self.event_funs + ] + ) + ) + # Exit loop if the sign of an event changes + if (new_event_signs != init_event_signs).any(): + solution.termination = "event" + solution.t_event = solution.t[-1] + solution.y_event = solution.y[:, -1] + break + else: + if not solution: + # create solution object on first step + solution = current_step_sol + solution.set_up_time = set_up_time + else: + # append solution from the current step to solution + solution.append(current_step_sol) + + # Calculate more exact termination reason + solution.termination = self.get_termination_reason(solution, self.events) + pybamm.logger.info( + "Finish solving {} ({})".format(model.name, solution.termination) + ) + 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 compute_solution(self, model, t_eval): + """Calculate the solution of the model at specified times. In this class, we + overwrite the behaviour of :class:`pybamm.DaeSolver`, since CasADi requires + slightly different syntax. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. Must have attributes rhs and + initial_conditions + t_eval : numeric type + The times at which to compute the solution + + """ + timer = pybamm.Timer() + + solve_start_time = timer.time() + pybamm.logger.debug("Calling DAE solver") + solution = self.integrate_casadi( + self.casadi_problem, self.y0, t_eval, mass_matrix=model.mass_matrix.entries + ) + solve_time = timer.time() - solve_start_time + + # Events not implemented, termination is always 'final time' + termination = "final time" + + return solution, solve_time, termination + + def integrate_casadi(self, problem, y0, t_eval, mass_matrix=None): + """ + Solve a DAE model defined by residuals with initial conditions y0. + + Parameters + ---------- + residuals : method + A function that takes in t, y and ydot and returns the residuals of the + equations + y0 : numeric type + The initial conditions + t_eval : numeric type + The times at which to compute the solution + mass_matrix : array_like, optional + The (sparse) mass matrix for the chosen spatial method. This is only passed + to check that the mass matrix is diagonal with 1s for the odes and 0s for + the algebraic equations, as CasADi does not allow to pass mass matrices. + """ + options = { + "grid": t_eval, + "reltol": self.rtol, + "abstol": self.atol, + "output_t0": True, + "max_num_steps": self.max_steps, + } + options.update(self.extra_options) + if self.method == "idas": + options["calc_ic"] = True + + # set up and solve + integrator = casadi.integrator("F", self.method, problem, options) + try: + # Try solving + len_rhs = problem["x"].size()[0] + y0_diff, y0_alg = np.split(y0, [len_rhs]) + sol = integrator(x0=y0_diff, z0=y0_alg) + y_values = np.concatenate([sol["xf"].full(), sol["zf"].full()]) + return pybamm.Solution(t_eval, y_values, None, None, "final time") + except RuntimeError as e: + # If it doesn't work raise error + raise pybamm.SolverError(e.args[0]) + diff --git a/pybamm/solvers/dae_solver.py b/pybamm/solvers/dae_solver.py index ba14d26d2c..6032f8247a 100644 --- a/pybamm/solvers/dae_solver.py +++ b/pybamm/solvers/dae_solver.py @@ -1,6 +1,7 @@ # # Base solver class # +import casadi import pybamm import numpy as np from scipy import optimize @@ -38,6 +39,7 @@ def __init__( self.root_method = root_method self.root_tol = root_tol self.max_steps = max_steps + self.name = "Base DAE solver" @property def root_method(self): @@ -102,12 +104,6 @@ def set_up(self, model): model : :class:`pybamm.BaseModel` The model whose solution to calculate. Must have attributes rhs and initial_conditions - - Raises - ------ - :class:`pybamm.SolverError` - If the model contains any algebraic equations (in which case a DAE solver - should be used instead) """ # create simplified rhs, algebraic and event expressions concatenated_rhs = model.concatenated_rhs @@ -144,19 +140,22 @@ def set_up(self, model): jac_algebraic = simp.simplify(jac_algebraic) jac = simp.simplify(jac) - if model.use_to_python: + if model.convert_to_format == "python": pybamm.logger.info("Converting jacobian to python") jac_algebraic = pybamm.EvaluatorPython(jac_algebraic) jac = pybamm.EvaluatorPython(jac) - def jac_alg_fn(t, y): - return jac_algebraic.evaluate(t, y) + def jacobian(t, y): + return jac.evaluate(t, y, known_evals={})[0] + + def jacobian_alg(t, y): + return jac_algebraic.evaluate(t, y, known_evals={})[0] else: - jac = None - jac_alg_fn = None + jacobian = None + jacobian_alg = None - if model.use_to_python: + if model.convert_to_format == "python": pybamm.logger.info("Converting RHS to python") concatenated_rhs = pybamm.EvaluatorPython(concatenated_rhs) pybamm.logger.info("Converting algebraic to python") @@ -175,7 +174,10 @@ def algebraic(t, y): if len(model.algebraic) > 0: y0 = self.calculate_consistent_initial_conditions( - rhs, algebraic, model.concatenated_initial_conditions[:, 0], jac_alg_fn + rhs, + algebraic, + model.concatenated_initial_conditions[:, 0], + jacobian_alg, ) else: # can use DAE solver to solve ODE model @@ -206,14 +208,117 @@ def eval_event(t, y): event_funs = [event_fun(event) for event in events.values()] - # Create function to evaluate jacobian - if jac is not None: + # Add the solver attributes + self.y0 = y0 + self.rhs = rhs + self.algebraic = algebraic + self.residuals = residuals + self.events = events + self.event_funs = event_funs + self.jacobian = jacobian + + def set_up_casadi(self, model): + """Convert model to casadi format and use their inbuilt functionalities. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. Must have attributes rhs and + initial_conditions + """ + # Convert model attributes to casadi + t_casadi = casadi.MX.sym("t") + y0 = model.concatenated_initial_conditions + y_diff = casadi.MX.sym("y_diff", len(model.concatenated_rhs.evaluate(0, y0))) + y_alg = casadi.MX.sym( + "y_alg", len(model.concatenated_algebraic.evaluate(0, y0)) + ) + y_casadi = casadi.vertcat(y_diff, y_alg) + pybamm.logger.info("Converting RHS to CasADi") + concatenated_rhs = model.concatenated_rhs.to_casadi(t_casadi, y_casadi) + pybamm.logger.info("Converting algebraic to CasADi") + concatenated_algebraic = model.concatenated_algebraic.to_casadi( + t_casadi, y_casadi + ) + all_states = casadi.vertcat(concatenated_rhs, concatenated_algebraic) + pybamm.logger.info("Converting events to CasADi") + casadi_events = { + name: event.to_casadi(t_casadi, y_casadi) + for name, event in model.events.items() + } + + # Create functions to evaluate rhs and algebraic + concatenated_rhs_fn = casadi.Function( + "rhs", [t_casadi, y_casadi], [concatenated_rhs] + ) + concatenated_algebraic_fn = casadi.Function( + "algebraic", [t_casadi, y_casadi], [concatenated_algebraic] + ) + all_states_fn = casadi.Function("all", [t_casadi, y_casadi], [all_states]) + + if model.use_jacobian: + + pybamm.logger.info("Calculating jacobian") + casadi_jac = casadi.jacobian(all_states, y_casadi) + casadi_jac_fn = casadi.Function( + "jacobian", [t_casadi, y_casadi], [casadi_jac] + ) + casadi_jac_alg = casadi.jacobian(concatenated_algebraic, y_casadi) + casadi_jac_alg_fn = casadi.Function( + "jacobian", [t_casadi, y_casadi], [casadi_jac_alg] + ) def jacobian(t, y): - return jac.evaluate(t, y, known_evals={})[0] + return casadi_jac_fn(t, y) + + def jacobian_alg(t, y): + return casadi_jac_alg_fn(t, y) else: jacobian = None + jacobian_alg = None + + # Calculate consistent initial conditions for the algebraic equations + def rhs(t, y): + return concatenated_rhs_fn(t, y).full()[:, 0] + + if len(model.algebraic) > 0: + + def algebraic(t, y): + return concatenated_algebraic_fn(t, y).full()[:, 0] + + y0 = self.calculate_consistent_initial_conditions( + rhs, + algebraic, + model.concatenated_initial_conditions[:, 0], + jacobian_alg, + ) + else: + # can use DAE solver to solve ODE model (just return empty algebraic) + def algebraic(t, y): + return np.empty(0) + + y0 = model.concatenated_initial_conditions[:, 0] + + # Create functions to evaluate residuals + + def residuals(t, y, ydot): + pybamm.logger.debug( + "Evaluating residuals for {} at t={}".format(model.name, t) + ) + states_eval = all_states_fn(t, y).full()[:, 0] + return states_eval - model.mass_matrix.entries @ ydot + + # Create event-dependent function to evaluate events + def event_fun(event): + casadi_event_fn = casadi.Function("event", [t_casadi, y_casadi], [event]) + + def eval_event(t, y): + return casadi_event_fn(t, y) + + return eval_event + + event_funs = [event_fun(event) for event in casadi_events.values()] # Add the solver attributes # Note: these are the (possibly) converted to python version rhs, algebraic @@ -222,10 +327,19 @@ def jacobian(t, y): self.rhs = rhs self.algebraic = algebraic self.residuals = residuals - self.events = events + self.events = model.events self.event_funs = event_funs self.jacobian = jacobian + # Create CasADi problem for the CasADi solver + self.casadi_problem = { + "t": t_casadi, + "x": y_diff, + "z": y_alg, + "ode": concatenated_rhs, + "alg": concatenated_algebraic, + } + def calculate_consistent_initial_conditions( self, rhs, algebraic, y0_guess, jac=None ): diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index b055c95db5..e5b2ecdca1 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -14,10 +14,10 @@ def have_idaklu(): - return idaklu_spec is None + return idaklu_spec is not None -class IDAKLU(pybamm.DaeSolver): +class IDAKLUSolver(pybamm.DaeSolver): """Solve a discretised model, using sundials with the KLU sparse linear solver. Parameters @@ -43,6 +43,86 @@ def __init__( raise ImportError("KLU is not installed") super().__init__("ida", rtol, atol, root_method, root_tol, max_steps) + self.name = "IDA KLU solver" + + def set_atol_by_variable(self, variables_with_tols, model): + """ + A method to set the absolute tolerances in the solver by state variable. + This method modifies self._atol. + + Parameters + ---------- + variables_with_tols : dict + A dictionary with keys that are strings indicating the variable you + wish to set the tolerance of and values that are the tolerances. + + model : :class:`pybamm.BaseModel` + The model that is going to be solved. + """ + + size = model.concatenated_initial_conditions.size + self._check_atol_type(size) + for var, tol in variables_with_tols.items(): + variable = model.variables[var] + if isinstance(variable, pybamm.StateVector): + self.set_state_vec_tol(variable, tol) + elif isinstance(variable, pybamm.Concatenation): + for child in variable.children: + if isinstance(child, pybamm.StateVector): + self.set_state_vec_tol(child, tol) + else: + raise pybamm.SolverError( + """Can only set tolerances for state variables + or concatenations of state variables""" + ) + else: + raise pybamm.SolverError( + """Can only set tolerances for state variables or + concatenations of state variables""" + ) + + def set_state_vec_tol(self, state_vec, tol): + """ + A method to set the tolerances in the atol vector of a specific + state variable. This method modifies self._atol + + Parameters + ---------- + state_vec : :class:`pybamm.StateVector` + The state vector to apply to the tolerance to + tol: float + The tolerance value + """ + slices = state_vec.y_slices[0] + self._atol[slices] = tol + + def _check_atol_type(self, size): + """ + This method checks that the atol vector is of the right shape and + type. + + Parameters + ---------- + size: int + The length of the atol vector + """ + + if isinstance(self._atol, float): + self._atol = self._atol * np.ones(size) + elif isinstance(self._atol, list): + self._atol = np.array(self._atol) + elif isinstance(self._atol, np.ndarray): + pass + else: + raise pybamm.SolverError( + "Absolute tolerances must be a numpy array, float, or list" + ) + + if self._atol.size != size: + raise pybamm.SolverError( + """Absolute tolerances must be either a scalar or a numpy arrray + of the same shape at y0""" + ) def integrate(self, residuals, y0, t_eval, events, mass_matrix, jacobian): """ @@ -75,6 +155,7 @@ def integrate(self, residuals, y0, t_eval, events, mass_matrix, jacobian): pybamm.SolverError("KLU requires events to be provided") rtol = self._rtol + self._check_atol_type(y0.size) atol = self._atol if jacobian: @@ -148,8 +229,8 @@ def rootfn(t, y): num_of_events, use_jac, ids, - rtol, atol, + rtol, ) t = sol.t diff --git a/pybamm/solvers/ode_solver.py b/pybamm/solvers/ode_solver.py index 973c10401c..168bc0479a 100644 --- a/pybamm/solvers/ode_solver.py +++ b/pybamm/solvers/ode_solver.py @@ -1,6 +1,7 @@ # # Base solver class # +import casadi import pybamm import numpy as np @@ -18,6 +19,7 @@ class OdeSolver(pybamm.BaseSolver): def __init__(self, method=None, rtol=1e-6, atol=1e-6): super().__init__(method, rtol, atol) + self.name = "Base ODE solver" def compute_solution(self, model, t_eval): """Calculate the solution of the model at specified times. @@ -102,13 +104,13 @@ def set_up(self, model): pybamm.logger.info("Simplifying jacobian") jac_rhs = simp.simplify(jac_rhs) - if model.use_to_python: + if model.convert_to_format == "python": pybamm.logger.info("Converting jacobian to python") jac_rhs = pybamm.EvaluatorPython(jac_rhs) else: jac_rhs = None - if model.use_to_python: + if model.convert_to_format == "python": pybamm.logger.info("Converting RHS to python") concatenated_rhs = pybamm.EvaluatorPython(concatenated_rhs) pybamm.logger.info("Converting events to python") @@ -150,6 +152,82 @@ def jacobian(t, y): self.event_funs = event_funs self.jacobian = jacobian + def set_up_casadi(self, model): + """Convert model to casadi format and use their inbuilt functionalities. + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model whose solution to calculate. Must have attributes rhs and + initial_conditions + + Raises + ------ + :class:`pybamm.SolverError` + If the model contains any algebraic equations (in which case a DAE solver + should be used instead) + + """ + # Check for algebraic equations + if len(model.algebraic) > 0: + raise pybamm.SolverError( + """Cannot use ODE solver to solve model with DAEs""" + ) + + y0 = model.concatenated_initial_conditions[:, 0] + + t_casadi = casadi.MX.sym("t") + y_casadi = casadi.MX.sym("y", len(y0)) + pybamm.logger.info("Converting RHS to CasADi") + concatenated_rhs = model.concatenated_rhs.to_casadi(t_casadi, y_casadi) + pybamm.logger.info("Converting events to CasADi") + casadi_events = { + name: event.to_casadi(t_casadi, y_casadi) + for name, event in model.events.items() + } + + # Create function to evaluate rhs + concatenated_rhs_fn = casadi.Function( + "rhs", [t_casadi, y_casadi], [concatenated_rhs] + ) + + def dydt(t, y): + pybamm.logger.debug("Evaluating RHS for {} at t={}".format(model.name, t)) + dy = concatenated_rhs_fn(t, y).full() + return dy[:, 0] + + # Create event-dependent function to evaluate events + def event_fun(event): + casadi_event_fn = casadi.Function("event", [t_casadi, y_casadi], [event]) + + def eval_event(t, y): + return casadi_event_fn(t, y) + + return eval_event + + event_funs = [event_fun(event) for event in casadi_events.values()] + + # Create function to evaluate jacobian + if model.use_jacobian: + pybamm.logger.info("Calculating jacobian") + casadi_jac = casadi.jacobian(concatenated_rhs, y_casadi) + casadi_jac_fn = casadi.Function( + "jacobian", [t_casadi, y_casadi], [casadi_jac] + ) + + def jacobian(t, y): + return casadi_jac_fn(t, y) + + else: + jacobian = None + + # Add the solver attributes + self.y0 = y0 + self.dydt = dydt + self.events = model.events + self.event_funs = event_funs + self.jacobian = jacobian + def integrate( self, derivs, y0, t_eval, events=None, mass_matrix=None, jacobian=None ): diff --git a/pybamm/solvers/scikits_dae_solver.py b/pybamm/solvers/scikits_dae_solver.py index b45425e815..1e572f4af6 100644 --- a/pybamm/solvers/scikits_dae_solver.py +++ b/pybamm/solvers/scikits_dae_solver.py @@ -48,6 +48,7 @@ def __init__( raise ImportError("scikits.odes is not installed") super().__init__(method, rtol, atol, root_method, root_tol, max_steps) + self.name = "Scikits DAE solver ({})".format(method) def integrate( self, residuals, y0, t_eval, events=None, mass_matrix=None, jacobian=None diff --git a/pybamm/solvers/scikits_ode_solver.py b/pybamm/solvers/scikits_ode_solver.py index 1a409ae92f..1f8b78b5e8 100644 --- a/pybamm/solvers/scikits_ode_solver.py +++ b/pybamm/solvers/scikits_ode_solver.py @@ -40,6 +40,7 @@ def __init__(self, method="cvode", rtol=1e-6, atol=1e-6, linsolver="dense"): super().__init__(method, rtol, atol) self.linsolver = linsolver + self.name = "Scikits ODE solver ({})".format(method) def integrate( self, derivs, y0, t_eval, events=None, mass_matrix=None, jacobian=None diff --git a/pybamm/solvers/scipy_solver.py b/pybamm/solvers/scipy_solver.py index fd7afbf498..fd2af8d763 100644 --- a/pybamm/solvers/scipy_solver.py +++ b/pybamm/solvers/scipy_solver.py @@ -22,6 +22,7 @@ class ScipySolver(pybamm.OdeSolver): def __init__(self, method="BDF", rtol=1e-6, atol=1e-6): super().__init__(method, rtol, atol) + self.name = "Scipy solver ({})".format(method) def integrate( self, derivs, y0, t_eval, events=None, mass_matrix=None, jacobian=None @@ -83,7 +84,7 @@ def integrate( termination = "event" t_event = [] for time in sol.t_events: - if time: + if time.size > 0: t_event = np.append(t_event, np.max(time)) t_event = np.array([np.max(t_event)]) y_event = sol.sol(t_event) diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index d185e5e450..a25d53d1a5 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -93,3 +93,8 @@ def append(self, solution): """ self.t = np.concatenate((self.t, solution.t[1:])) self.y = np.concatenate((self.y, solution.y[:, 1:]), axis=1) + self.solve_time += solution.solve_time + + @property + def total_time(self): + return self.set_up_time + self.solve_time diff --git a/pybamm/spatial_methods/scikit_finite_element.py b/pybamm/spatial_methods/scikit_finite_element.py index ddf501049e..464a1d68a7 100644 --- a/pybamm/spatial_methods/scikit_finite_element.py +++ b/pybamm/spatial_methods/scikit_finite_element.py @@ -51,11 +51,11 @@ def spatial_variable(self, symbol): symbol_mesh = self.mesh if symbol.name == "y": vector = pybamm.Vector( - symbol_mesh["current collector"][0].edges["y"], domain=symbol.domain + symbol_mesh["current collector"][0].coordinates[0, :][:, np.newaxis] ) elif symbol.name == "z": vector = pybamm.Vector( - symbol_mesh["current collector"][0].edges["z"], domain=symbol.domain + symbol_mesh["current collector"][0].coordinates[1, :][:, np.newaxis] ) else: raise pybamm.GeometryError( @@ -121,7 +121,7 @@ def unit_bc_load_form(v, dv, w): # set Dirichlet value at facets corresponding to tab neg_bc_load = np.zeros(mesh.npts) neg_bc_load[mesh.negative_tab_dofs] = 1 - boundary_load = boundary_load - neg_bc_value * pybamm.Vector(neg_bc_load) + boundary_load = boundary_load + neg_bc_value * pybamm.Vector(neg_bc_load) else: raise ValueError( "boundary condition must be Dirichlet or Neumann, not '{}'".format( @@ -138,7 +138,7 @@ def unit_bc_load_form(v, dv, w): # set Dirichlet value at facets corresponding to tab pos_bc_load = np.zeros(mesh.npts) pos_bc_load[mesh.positive_tab_dofs] = 1 - boundary_load = boundary_load - pos_bc_value * pybamm.Vector(pos_bc_load) + boundary_load = boundary_load + pos_bc_value * pybamm.Vector(pos_bc_load) else: raise ValueError( "boundary condition must be Dirichlet or Neumann, not '{}'".format( diff --git a/results/2019_08_sulzer_thesis/self_discharge.py b/results/2019_08_sulzer_thesis/self_discharge.py index 3b284d7b00..a040101832 100644 --- a/results/2019_08_sulzer_thesis/self_discharge.py +++ b/results/2019_08_sulzer_thesis/self_discharge.py @@ -32,7 +32,7 @@ def self_discharge_states(compute): ), ] extra_parameter_values = { - "Current function": pybamm.GetConstantCurrent(current=0) + "Current function": pybamm.ConstantCurrent(current=0) } t_eval = np.linspace(0, 1000, 100) all_variables, t_eval = model_comparison( diff --git a/results/2plus1D/dfn_2plus1D.py b/results/2plus1D/dfn_2plus1D.py index 9beff8d5a4..32daf9ed58 100644 --- a/results/2plus1D/dfn_2plus1D.py +++ b/results/2plus1D/dfn_2plus1D.py @@ -34,7 +34,7 @@ var.y: 5, var.z: 5, } -# depnding on number of points in y-z plane may need to increase recursion depth... +# depending on number of points in y-z plane may need to increase recursion depth... sys.setrecursionlimit(10000) mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) @@ -46,7 +46,7 @@ tau = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge) t_end = 3600 / tau.evaluate(0) t_eval = np.linspace(0, t_end, 120) -solution = pybamm.IDAKLU().solve(model, t_eval) +solution = pybamm.IDAKLUSolver().solve(model, t_eval) # TO DO: 2+1D automated plotting phi_s_cn = pybamm.ProcessedVariable( diff --git a/results/2plus1D/set_temperature_spm_1plus1D.py b/results/2plus1D/set_temperature_spm_1plus1D.py new file mode 100644 index 0000000000..f6101a8824 --- /dev/null +++ b/results/2plus1D/set_temperature_spm_1plus1D.py @@ -0,0 +1,192 @@ +# +# Example of 1+1D SPM where the temperature can be set by the user +# + +import pybamm +import numpy as np +import matplotlib.pyplot as plt +import sys + +# set logging level +pybamm.set_logging_level("INFO") + +# load (1+1D) SPM model +options = { + "current collector": "potential pair", + "dimensionality": 1, + "thermal": "set external temperature", +} +model = pybamm.lithium_ion.SPM(options) + +# create geometry +geometry = model.default_geometry + +# load parameter values and process model and geometry +param = model.default_parameter_values +param.process_model(model) +param.process_geometry(geometry) + +# set mesh +nbat = 5 +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.z: nbat} +# depending on number of points in y-z plane may need to increase recursion depth... +sys.setrecursionlimit(10000) +mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) + +# discretise model +disc = pybamm.Discretisation(mesh, model.default_spatial_methods) +disc.process_model(model) + + +# define a method which updates statevector +def update_statevector(variables, statevector): + "takes in a dict of variable name and vector of updated state" + for name, new_vector in variables.items(): + var_slice = model.variables[name].y_slices + statevector[var_slice] = new_vector + return statevector + + +# define a method to non-dimensionalise a temperature +def non_dim_temperature(temperature): + "takes in a temperature and returns the non-dimensional version" + Delta_T = param.process_symbol(model.submodels["thermal"].param.Delta_T).evaluate() + T_ref = param.process_symbol(model.submodels["thermal"].param.T_ref).evaluate() + return (temperature - T_ref) / Delta_T + + +# step model in time and process variables for later plotting +solver = model.default_solver +dt = 0.1 # timestep to take +npts = 20 # number of points to store the solution at during this step +solution1 = solver.step(model, dt, npts=npts) +# create dict of variables to post process +output_variables = [ + "Negative current collector potential [V]", + "Positive current collector potential [V]", + "Current [A]", + "X-averaged total heating [W.m-3]", + "X-averaged positive particle surface concentration [mol.m-3]", + "X-averaged cell temperature [K]", +] +output_variables_dict = {} +for var in output_variables: + output_variables_dict[var] = model.variables[var] +processed_vars_step1 = pybamm.post_process_variables( + output_variables_dict, solution1.t, solution1.y, mesh +) + +# get the current state and temperature +current_state = solution1.y[:, -1] +temp_ave = current_state[model.variables["X-averaged cell temperature"].y_slices] + +# update the temperature +T_ref = param.process_symbol(model.submodels["thermal"].param.T_ref).evaluate() +t_external = np.linspace(T_ref, T_ref + 6.0, nbat) +non_dim_t_external = non_dim_temperature(t_external) +variables = {"X-averaged cell temperature": non_dim_t_external} +new_state = update_statevector(variables, current_state) + +# step in time again and process variables for later plotting +# use new state as initial condition. Note: need to to recompute consistent initial +# values for the algebraic part of the model. Since the (dummy) equation for the +# temperature is an ODE, the imposed change in temperature is unaffected by this +# process +solver.y0 = solver.calculate_consistent_initial_conditions( + solver.rhs, solver.algebraic, new_state +) +solution2 = solver.step(model, dt, npts=npts) +processed_vars_step2 = pybamm.post_process_variables( + output_variables_dict, solution2.t, solution2.y, mesh +) + +# plots +t_sec = param.evaluate(pybamm.standard_parameters_lithium_ion.tau_discharge) +t_hour = t_sec / (3600) +z = np.linspace(0, 1, nbat) + +# local voltage +plt.figure() +for bat_id in range(nbat): + plt.plot( + solution1.t * t_hour, + processed_vars_step1["Positive current collector potential [V]"]( + solution1.t, z=z + )[bat_id, :] + - processed_vars_step1["Negative current collector potential [V]"]( + solution1.t, z=z + )[bat_id, :], + solution2.t * t_hour, + processed_vars_step2["Positive current collector potential [V]"]( + solution2.t, z=z + )[bat_id, :] + - processed_vars_step1["Negative current collector potential [V]"]( + solution2.t, z=z + )[bat_id, :], + ) +plt.xlabel("t [hrs]") +plt.ylabel("Local voltage [V]") + +# applied current +plt.figure() +plt.plot( + solution1.t, + processed_vars_step1["Current [A]"](solution1.t), + solution2.t, + processed_vars_step2["Current [A]"](solution2.t), +) +plt.xlabel("t") +plt.ylabel("Current [A]") + +# local heating +plt.figure() +z = np.linspace(0, 1, nbat) +for bat_id in range(nbat): + plt.plot( + solution1.t * t_hour, + processed_vars_step1["X-averaged total heating [W.m-3]"](solution1.t, z=z)[ + bat_id, : + ], + solution2.t * t_hour, + processed_vars_step2["X-averaged total heating [W.m-3]"](solution2.t, z=z)[ + bat_id, : + ], + ) +plt.xlabel("t [hrs]") +plt.ylabel("X-averaged total heating [W.m-3]") +plt.yscale("log") + +# local concentration +plt.figure() +for bat_id in range(nbat): + plt.plot( + solution1.t * t_hour, + processed_vars_step1[ + "X-averaged positive particle surface concentration [mol.m-3]" + ](solution1.t, z=z)[bat_id, :], + solution2.t * t_hour, + processed_vars_step2[ + "X-averaged positive particle surface concentration [mol.m-3]" + ](solution2.t, z=z)[bat_id, :], + ) +plt.xlabel("t [hrs]") +plt.ylabel("X-averaged positive particle surface concentration [mol.m-3]") + +# local temperature +plt.figure() +for bat_id in range(nbat): + plt.plot( + solution1.t * t_hour, + processed_vars_step1["X-averaged cell temperature [K]"](solution1.t, z=z)[ + bat_id, : + ], + solution2.t * t_hour, + processed_vars_step2["X-averaged cell temperature [K]"](solution2.t, z=z)[ + bat_id, : + ], + ) +plt.xlabel("t [hrs]") +plt.ylabel("X-averaged cell temperature [K]") + +plt.show() diff --git a/results/2plus1D/spm_1plus1D.py b/results/2plus1D/spm_1plus1D.py new file mode 100644 index 0000000000..993ab68ae9 --- /dev/null +++ b/results/2plus1D/spm_1plus1D.py @@ -0,0 +1,63 @@ +import pybamm +import numpy as np +import sys + +# set logging level +pybamm.set_logging_level("INFO") + +# load (1+1D) SPMe model +options = { + "current collector": "potential pair", + "dimensionality": 1, + "thermal": "lumped", +} +model = pybamm.lithium_ion.SPM(options) + +# create geometry +geometry = model.default_geometry + +# load parameter values and process model and geometry +param = model.default_parameter_values +C_rate = 1 +current_1C = 24 * param.process_symbol(pybamm.geometric_parameters.A_cc).evaluate() +param.update( + { + "Typical current [A]": C_rate * current_1C, + "Initial temperature [K]": 298.15, + "Negative current collector conductivity [S.m-1]": 1e7, + "Positive current collector conductivity [S.m-1]": 1e7, + "Heat transfer coefficient [W.m-2.K-1]": 1, + } +) +param.process_model(model) +param.process_geometry(geometry) + +# set mesh +var = pybamm.standard_spatial_vars +var_pts = {var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 10, var.r_p: 10, var.z: 15} +# depending on number of points in y-z plane may need to increase recursion depth... +sys.setrecursionlimit(10000) +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 -- simulate one hour discharge +tau = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge) +t_end = 3600 / tau.evaluate(0) +t_eval = np.linspace(0, t_end, 120) +solution = model.default_solver.solve(model, t_eval) + +# plot +output_variables = [ + "X-averaged negative particle surface concentration [mol.m-3]", + "X-averaged positive particle surface concentration [mol.m-3]", + # "X-averaged cell temperature [K]", + "Local potenital difference [V]", + "Current collector current density [A.m-2]", + "Terminal voltage [V]", + "Volume-averaged cell temperature [K]", +] +plot = pybamm.QuickPlot(model, mesh, solution, output_variables) +plot.dynamic_plot() diff --git a/results/2plus1D/spm_2plus1D.py b/results/2plus1D/spm_2plus1D.py index 1b9a08411c..5f95cd31d0 100644 --- a/results/2plus1D/spm_2plus1D.py +++ b/results/2plus1D/spm_2plus1D.py @@ -34,7 +34,7 @@ var.y: 5, var.z: 5, } -# depnding on number of points in y-z plane may need to increase recursion depth... +# depending on number of points in y-z plane may need to increase recursion depth... sys.setrecursionlimit(10000) mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) @@ -46,7 +46,7 @@ tau = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge) t_end = 3600 / tau.evaluate(0) t_eval = np.linspace(0, t_end, 120) -solution = pybamm.IDAKLU().solve(model, t_eval) +solution = pybamm.IDAKLUSolver().solve(model, t_eval) # TO DO: 2+1D automated plotting phi_s_cn = pybamm.ProcessedVariable( diff --git a/results/2plus1D/spme_2plus1D.py b/results/2plus1D/spme_2plus1D.py index d356f1a62f..c7256b993a 100644 --- a/results/2plus1D/spme_2plus1D.py +++ b/results/2plus1D/spme_2plus1D.py @@ -34,7 +34,7 @@ var.y: 5, var.z: 5, } -# depnding on number of points in y-z plane may need to increase recursion depth... +# depending on number of points in y-z plane may need to increase recursion depth... sys.setrecursionlimit(10000) mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) @@ -46,7 +46,7 @@ tau = param.process_symbol(pybamm.standard_parameters_lithium_ion.tau_discharge) t_end = 3600 / tau.evaluate(0) t_eval = np.linspace(0, t_end, 120) -solution = pybamm.IDAKLU().solve(model, t_eval) +solution = pybamm.IDAKLUSolver().solve(model, t_eval) # TO DO: 2+1D automated plotting phi_s_cn = pybamm.ProcessedVariable( diff --git a/results/2plus1D/user_mesh_spm_1plus1D.py b/results/2plus1D/user_mesh_spm_1plus1D.py index a639500260..9d479d6a66 100644 --- a/results/2plus1D/user_mesh_spm_1plus1D.py +++ b/results/2plus1D/user_mesh_spm_1plus1D.py @@ -5,7 +5,7 @@ # set logging level pybamm.set_logging_level("INFO") -# load (1+1D) SPMe model +# load (1+1D) SPM model options = { "current collector": "potential pair", "dimensionality": 1, @@ -35,7 +35,7 @@ param.process_geometry(geometry) # set mesh using user-supplied edges in z -z_edges = np.array([0, 0.03, 0.1, 0.3, 0.47, 0.5, 0.73, 0.8, 0.911, 1]) +z_edges = np.array([0, 0.025, 0.05, 0.1, 0.3, 0.5, 0.7, 0.9, 0.95, 0.975, 1]) submesh_types = model.default_submesh_types submesh_types["current collector"] = pybamm.MeshGenerator( pybamm.UserSupplied1DSubMesh, submesh_params={"edges": z_edges} @@ -64,7 +64,7 @@ "X-averaged negative particle surface concentration [mol.m-3]", "X-averaged positive particle surface concentration [mol.m-3]", # "X-averaged cell temperature [K]", - "Local potential difference [V]", + "Local current collector potential difference [V]", "Current collector current density [A.m-2]", "Terminal voltage [V]", "Volume-averaged cell temperature [K]", diff --git a/results/drive_cycles/US06_simulation.py b/results/drive_cycles/US06_simulation.py index 3f5ae2bb64..64f47fd5e5 100644 --- a/results/drive_cycles/US06_simulation.py +++ b/results/drive_cycles/US06_simulation.py @@ -13,25 +13,25 @@ # load parameter values and process model and geometry param = model.default_parameter_values -param["Current function"] = pybamm.GetCurrentData("US06.csv", units="[A]") +param["Current function"] = "[current data]US06" param.process_model(model) param.process_geometry(geometry) # set mesh -mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) +var = pybamm.standard_spatial_vars +var_pts = {var.x_n: 10, var.x_s: 10, var.x_p: 10, var.r_n: 5, var.r_p: 5} +mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) # discretise model disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) # simulate US06 drive cycle -tau = param.process_symbol( - pybamm.standard_parameters_lithium_ion.tau_discharge -).evaluate(0) +tau = param.evaluate(pybamm.standard_parameters_lithium_ion.tau_discharge) t_eval = np.linspace(0, 600 / tau, 600) # need to increase max solver steps if solving DAEs along with an erratic drive cycle -solver = model.default_solver +solver = pybamm.CasadiSolver() if isinstance(solver, pybamm.DaeSolver): solver.max_steps = 10000 diff --git a/results/drive_cycles/car_current_simulation.py b/results/drive_cycles/car_current_simulation.py index 81570e1bef..bcedf38408 100644 --- a/results/drive_cycles/car_current_simulation.py +++ b/results/drive_cycles/car_current_simulation.py @@ -40,7 +40,7 @@ def car_current(t): # load parameter values and process model and geometry param = model.default_parameter_values -param["Current function"] = pybamm.GetUserCurrent(car_current) +param["Current function"] = pybamm.UserCurrent(car_current) param.process_model(model) param.process_geometry(geometry) diff --git a/results/drive_cycles/discharge_rest.py b/results/drive_cycles/discharge_rest.py index 07f9007957..a576252285 100644 --- a/results/drive_cycles/discharge_rest.py +++ b/results/drive_cycles/discharge_rest.py @@ -41,7 +41,7 @@ # solve again with zero current, using last step of solution1 as initial conditions # update the current to be zero -param["Current function"] = pybamm.GetConstantCurrent(current=pybamm.Scalar(0)) +param["Current function"] = pybamm.ConstantCurrent(current=pybamm.Scalar(0)) param.update_model(model, disc) # Note: need to update model.concatenated_initial_conditions *after* update_model, # as update_model updates model.concatenated_initial_conditions, by concatenting diff --git a/results/drive_cycles/user_sin_current_simulation.py b/results/drive_cycles/user_sin_current_simulation.py index a2401c481e..ce579dd265 100644 --- a/results/drive_cycles/user_sin_current_simulation.py +++ b/results/drive_cycles/user_sin_current_simulation.py @@ -23,9 +23,9 @@ def my_fun(t, A, omega): # load parameter values and process models param = models[0].default_parameter_values for i, frequency in enumerate(frequencies): - # pass my_fun to GetUserCurrent class, giving the additonal parameters as + # pass my_fun to UserCurrent class, giving the additonal parameters as # keyword arguments - current = pybamm.GetUserCurrent(my_fun, A=A, omega=frequency) + current = pybamm.UserCurrent(my_fun, A=A, omega=frequency) param.update({"Current function": current}) param.process_model(models[i]) diff --git a/scripts/install_scikits_odes.sh b/scripts/install_scikits_odes.sh index 01bf56b4cb..fb3c6a3cd6 100755 --- a/scripts/install_scikits_odes.sh +++ b/scripts/install_scikits_odes.sh @@ -1,7 +1,7 @@ #!/bin/bash -SUNDIALS_URL=https://github.com/LLNL/sundials/archive/v3.1.1.tar.gz -SUNDIALS_NAME=sundials-3.1.1.tar.gz +SUNDIALS_URL=https://github.com/LLNL/sundials/archive/v4.1.0.tar.gz +SUNDIALS_NAME=sundials-4.1.0.tar.gz CURRENT_DIR=`pwd` TMP_DIR=$CURRENT_DIR/tmp mkdir $TMP_DIR @@ -10,18 +10,10 @@ INSTALL_DIR=$CURRENT_DIR/sundials cd $TMP_DIR wget $SUNDIALS_URL -O $SUNDIALS_NAME tar -xvf $SUNDIALS_NAME -mkdir build-sundials-3.1.1 -cd build-sundials-3.1.1/ -cmake -DLAPACK_ENABLE=ON -DSUNDIALS_INDEX_TYPE=int32_t -DBUILD_ARKODE:BOOL=OFF -DEXAMPLES_ENABLE:BOOL=OFF -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR ../sundials-3.1.1/ -if [ "$(uname)" == "Darwin" ]; then - # Mac OS X platform - NUM_OF_CORES=$(sysctl -n hw.cpu) -elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then - # GNU/Linux platform - NUM_OF_CORES=$(cat /proc/cpuinfo | grep processor | wc -l) -fi -make clean -make -j$NUM_OF_CORES install +mkdir build-sundials-4.1.0 +cd build-sundials-4.1.0/ +cmake -DLAPACK_ENABLE=ON -DSUNDIALS_INDEX_TYPE=int32_t -DBUILD_ARKODE:BOOL=OFF -DEXAMPLES_ENABLE:BOOL=OFF -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR ../sundials-4.1.0/ +make install cd $CURRENT_DIR rm -rf $TMP_DIR export LD_LIBRARY_PATH=$INSTALL_DIR/lib:$LD_LIBRARY_PATH # For Linux diff --git a/setup.py b/setup.py index c8cb541f5d..c3d29d450a 100644 --- a/setup.py +++ b/setup.py @@ -17,10 +17,11 @@ description="Python Battery Mathematical Modelling.", long_description=readme, url="https://github.com/pybamm-team/PyBaMM", - # include_package_data=True, + include_package_data=True, packages=find_packages(include=("pybamm", "pybamm.*")), package_data={ "pybamm": [ + "./version", "../input/parameters/lithium-ion/*.csv", "../input/parameters/lithium-ion/*.py", "../input/parameters/lead-acid/*.csv", @@ -35,6 +36,8 @@ "anytree>=2.4.3", "autograd>=1.2", "scikit-fem>=0.2.0", + "casadi>=3.5.0", + "jupyter", # For example notebooks # Note: Matplotlib is loaded for debug plots, but to ensure pybamm runs # on systems without an attached display, it should never be imported # outside of plot() methods. @@ -46,7 +49,6 @@ "dev": [ "flake8>=3", # For code style checking "black", # For code style auto-formatting - "jupyter", # For documentation and testing ], }, ) diff --git a/tests/integration/test_models/standard_model_tests.py b/tests/integration/test_models/standard_model_tests.py index 2b5afa1244..ba827e9cd3 100644 --- a/tests/integration/test_models/standard_model_tests.py +++ b/tests/integration/test_models/standard_model_tests.py @@ -172,6 +172,7 @@ def evaluate_model(self, simplify=False, use_known_evals=False, to_python=False) def set_up_model(self, simplify=False, to_python=False): self.model.use_simplify = simplify - self.model.use_to_python = to_python + if to_python is True: + self.model.convert_to_format = "python" self.model.default_solver.set_up(self.model) return None diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_asymptotics_convergence.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_asymptotics_convergence.py index 4928475f8b..7efc5b9729 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_asymptotics_convergence.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_asymptotics_convergence.py @@ -8,7 +8,6 @@ class TestAsymptoticConvergence(unittest.TestCase): - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_leading_order_convergence(self): """ Check that the leading-order model solution converges linearly in C_e to the diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_compare_outputs.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_compare_outputs.py index 699a920425..aa31ad8386 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_compare_outputs.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_compare_outputs.py @@ -7,7 +7,6 @@ from tests import StandardOutputComparison -@unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") class TestCompareOutputs(unittest.TestCase): def test_compare_averages_asymptotics(self): """ 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 fae4ffd57f..05c2d9bb16 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 @@ -55,38 +55,13 @@ def test_basic_processing_differential(self): modeltest = tests.StandardModelTest(model, parameter_values=param) modeltest.test_all() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_algebraic(self): options = {"surface form": "algebraic"} model = pybamm.lead_acid.Composite(options) param = model.default_parameter_values param.update({"Typical current [A]": 1}) modeltest = tests.StandardModelTest(model, parameter_values=param) - modeltest.test_all() - - def test_optimisations(self): - options = {"surface form": "differential"} - model = pybamm.lead_acid.Composite(options) - optimtest = tests.OptimisationsTest(model) - - original = optimtest.evaluate_model() - simplified = optimtest.evaluate_model(simplify=True) - using_known_evals = optimtest.evaluate_model(use_known_evals=True) - simp_and_known = optimtest.evaluate_model(simplify=True, use_known_evals=True) - simp_and_python = optimtest.evaluate_model(simplify=True, to_python=True) - np.testing.assert_array_almost_equal(original, simplified) - np.testing.assert_array_almost_equal(original, using_known_evals) - np.testing.assert_array_almost_equal(original, simp_and_known) - np.testing.assert_array_almost_equal(original, simp_and_python) - - def test_set_up(self): - options = {"surface form": "differential"} - model = pybamm.lead_acid.Composite(options) - optimtest = tests.OptimisationsTest(model) - optimtest.set_up_model(simplify=False, to_python=True) - optimtest.set_up_model(simplify=True, to_python=True) - optimtest.set_up_model(simplify=False, to_python=False) - optimtest.set_up_model(simplify=True, to_python=False) + modeltest.test_all() # solver=pybamm.CasadiSolver()) class TestLeadAcidCompositeExtended(unittest.TestCase): diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_foqs.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_foqs.py index 473f855b58..be8c4cbca0 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_foqs.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_foqs.py @@ -64,7 +64,6 @@ def test_basic_processing_differential(self): modeltest = tests.StandardModelTest(model, parameter_values=param) modeltest.test_all() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_algebraic(self): options = { "surface form": "algebraic", @@ -77,38 +76,6 @@ def test_basic_processing_algebraic(self): modeltest = tests.StandardModelTest(model, parameter_values=param) modeltest.test_all() - def test_optimisations(self): - options = { - "surface form": "differential", - "thermal": "isothermal", - "convection": False, - } - model = pybamm.lead_acid.FOQS(options) - optimtest = tests.OptimisationsTest(model) - - original = optimtest.evaluate_model() - simplified = optimtest.evaluate_model(simplify=True) - using_known_evals = optimtest.evaluate_model(use_known_evals=True) - simp_and_known = optimtest.evaluate_model(simplify=True, use_known_evals=True) - simp_and_python = optimtest.evaluate_model(simplify=True, to_python=True) - np.testing.assert_array_almost_equal(original, simplified) - np.testing.assert_array_almost_equal(original, using_known_evals) - np.testing.assert_array_almost_equal(original, simp_and_known) - np.testing.assert_array_almost_equal(original, simp_and_python) - - def test_set_up(self): - options = { - "surface form": "differential", - "thermal": "isothermal", - "convection": False, - } - model = pybamm.lead_acid.FOQS(options) - optimtest = tests.OptimisationsTest(model) - optimtest.set_up_model(simplify=False, to_python=True) - optimtest.set_up_model(simplify=True, to_python=True) - optimtest.set_up_model(simplify=False, to_python=False) - optimtest.set_up_model(simplify=True, to_python=False) - 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_full.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py index 7c829cea5a..0fe2b5f688 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 @@ -9,14 +9,12 @@ class TestLeadAcidFull(unittest.TestCase): - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing(self): options = {"thermal": "isothermal"} model = pybamm.lead_acid.Full(options) modeltest = tests.StandardModelTest(model) modeltest.test_all(t_eval=np.linspace(0, 0.6)) - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_with_convection(self): options = {"thermal": "isothermal", "convection": True} model = pybamm.lead_acid.Full(options) @@ -40,7 +38,6 @@ def test_optimisations(self): np.testing.assert_array_almost_equal(original, simp_and_known) np.testing.assert_array_almost_equal(original, simp_and_python) - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_set_up(self): options = {"thermal": "isothermal"} model = pybamm.lead_acid.Full(options) @@ -58,7 +55,6 @@ def test_basic_processing_differential(self): modeltest = tests.StandardModelTest(model) modeltest.test_all() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_algebraic(self): options = {"surface form": "algebraic"} model = pybamm.lead_acid.Full(options) @@ -78,7 +74,6 @@ def test_optimisations(self): np.testing.assert_array_almost_equal(original, using_known_evals) np.testing.assert_array_almost_equal(original, simp_and_known, decimal=5) - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_set_up(self): options = {"surface form": "differential"} model = pybamm.lead_acid.Full(options) @@ -91,6 +86,7 @@ def test_set_up(self): if __name__ == "__main__": print("Add -v for more debug output") + # pybamm.set_logging_level("DEBUG") import sys if "-v" in sys.argv: 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 ce1f3e8e21..cc04f85dbb 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 @@ -47,7 +47,7 @@ def test_zero_current(self): model = pybamm.lead_acid.LOQS() parameter_values = model.default_parameter_values parameter_values.update( - {"Current function": pybamm.GetConstantCurrent(current=0)} + {"Current function": pybamm.ConstantCurrent(current=0)} ) modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all() diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_composite_side_reactions.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_composite_side_reactions.py index 2d25498234..ab27afcc30 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_composite_side_reactions.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_composite_side_reactions.py @@ -15,7 +15,6 @@ def test_basic_processing_differential(self): modeltest = tests.StandardModelTest(model) modeltest.test_all(skip_output_tests=True) - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_algebraic(self): options = {"side reactions": ["oxygen"], "surface form": "algebraic"} model = pybamm.lead_acid.Composite(options) @@ -36,9 +35,7 @@ def test_basic_processing_zero_current(self): options = {"side reactions": ["oxygen"], "surface form": "differential"} model = pybamm.lead_acid.Composite(options) parameter_values = model.default_parameter_values - parameter_values.update( - {"Current function": pybamm.GetConstantCurrent(current=0)} - ) + parameter_values.update({"Current function": pybamm.ConstantCurrent(current=0)}) modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all(skip_output_tests=True) diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_full_side_reactions.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_full_side_reactions.py index 409998f96f..7ea41e2e34 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_full_side_reactions.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_full_side_reactions.py @@ -9,7 +9,6 @@ class TestLeadAcidFullSideReactions(unittest.TestCase): - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing(self): options = {"side reactions": ["oxygen"]} model = pybamm.lead_acid.Full(options) @@ -22,7 +21,6 @@ def test_basic_processing_differential(self): modeltest = tests.StandardModelTest(model) modeltest.test_all(skip_output_tests=True) - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_algebraic(self): options = {"side reactions": ["oxygen"], "surface form": "algebraic"} model = pybamm.lead_acid.Full(options) @@ -43,9 +41,7 @@ def test_basic_processing_zero_current(self): options = {"side reactions": ["oxygen"], "surface form": "differential"} model = pybamm.lead_acid.Full(options) parameter_values = model.default_parameter_values - parameter_values.update( - {"Current function": pybamm.GetConstantCurrent(current=0)} - ) + parameter_values.update({"Current function": pybamm.ConstantCurrent(current=0)}) modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all(skip_output_tests=True) diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_loqs_side_reactions.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_loqs_side_reactions.py index b62535bb69..50685e37b9 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_loqs_side_reactions.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_loqs_side_reactions.py @@ -23,7 +23,6 @@ def test_discharge_differential_varying_surface_area(self): modeltest = tests.StandardModelTest(model) modeltest.test_all() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_discharge_algebraic(self): options = {"surface form": "algebraic", "side reactions": ["oxygen"]} model = pybamm.lead_acid.LOQS(options) @@ -44,9 +43,7 @@ def test_zero_current(self): options = {"surface form": "differential", "side reactions": ["oxygen"]} model = pybamm.lead_acid.LOQS(options) parameter_values = model.default_parameter_values - parameter_values.update( - {"Current function": pybamm.GetConstantCurrent(current=0)} - ) + parameter_values.update({"Current function": pybamm.ConstantCurrent(current=0)}) modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all(skip_output_tests=True) diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py index d16639af24..b8ac651251 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py @@ -8,7 +8,6 @@ import unittest -@unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") class TestDFN(unittest.TestCase): def test_basic_processing(self): options = {"thermal": "isothermal"} @@ -18,7 +17,6 @@ def test_basic_processing(self): modeltest = tests.StandardModelTest(model, var_pts=var_pts) modeltest.test_all() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_1plus1D(self): options = {"current collector": "potential pair", "dimensionality": 1} model = pybamm.lithium_ion.DFN(options) @@ -35,7 +33,6 @@ def test_basic_processing_1plus1D(self): modeltest = tests.StandardModelTest(model, var_pts=var_pts) modeltest.test_all(skip_output_tests=True) - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_2plus1D(self): options = {"current collector": "potential pair", "dimensionality": 2} model = pybamm.lithium_ion.DFN(options) diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py index 1d8810a29a..3f77fa6e85 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py @@ -14,7 +14,6 @@ def test_basic_processing(self): modeltest = tests.StandardModelTest(model) modeltest.test_all() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_1plus1D(self): options = {"current collector": "potential pair", "dimensionality": 1} model = pybamm.lithium_ion.SPM(options) @@ -31,7 +30,6 @@ def test_basic_processing_1plus1D(self): modeltest = tests.StandardModelTest(model, var_pts=var_pts) modeltest.test_all(skip_output_tests=True) - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_2plus1D(self): options = {"current collector": "potential pair", "dimensionality": 2} model = pybamm.lithium_ion.SPM(options) @@ -83,9 +81,7 @@ def test_zero_current(self): options = {"thermal": "isothermal"} model = pybamm.lithium_ion.SPM(options) parameter_values = model.default_parameter_values - parameter_values.update( - {"Current function": pybamm.GetConstantCurrent(current=0)} - ) + parameter_values.update({"Current function": pybamm.ConstantCurrent(current=0)}) modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all() diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py index c411fb69f6..0dc59f881c 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py @@ -15,7 +15,6 @@ def test_basic_processing(self): modeltest = tests.StandardModelTest(model) modeltest.test_all() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_1plus1D(self): options = {"current collector": "potential pair", "dimensionality": 1} model = pybamm.lithium_ion.SPMe(options) @@ -32,7 +31,6 @@ def test_basic_processing_1plus1D(self): modeltest = tests.StandardModelTest(model, var_pts=var_pts) modeltest.test_all(skip_output_tests=True) - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_basic_processing_2plus1D(self): options = {"current collector": "potential pair", "dimensionality": 2} model = pybamm.lithium_ion.SPMe(options) diff --git a/tests/integration/test_quick_plot.py b/tests/integration/test_quick_plot.py index 6924486c54..0ebc03d08b 100644 --- a/tests/integration/test_quick_plot.py +++ b/tests/integration/test_quick_plot.py @@ -8,7 +8,6 @@ class TestQuickPlot(unittest.TestCase): Tests that QuickPlot is created correctly """ - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_plot_lithium_ion(self): spm = pybamm.lithium_ion.SPM() spme = pybamm.lithium_ion.SPMe() @@ -43,7 +42,7 @@ def test_plot_lithium_ion(self): quick_plot.update(0.01) # Update parameters, solve, plot again - param.update({"Current function": pybamm.GetConstantCurrent(current=0)}) + param.update({"Current function": pybamm.ConstantCurrent(current=0)}) param.update_model(spm, disc_spm) solution_spm = spm.default_solver.solve(spm, t_eval) quick_plot = pybamm.QuickPlot(spm, mesh, solution_spm) @@ -73,7 +72,6 @@ def test_plot_lithium_ion(self): quick_plot.update(0.01) - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_plot_lead_acid(self): loqs = pybamm.lead_acid.LOQS() geometry = loqs.default_geometry diff --git a/tests/integration/test_solvers/test_idaklu.py b/tests/integration/test_solvers/test_idaklu.py index 7885d54433..4ba46238f2 100644 --- a/tests/integration/test_solvers/test_idaklu.py +++ b/tests/integration/test_solvers/test_idaklu.py @@ -4,7 +4,7 @@ import unittest -@unittest.skipIf(pybamm.have_idaklu(), "idaklu solver is not installed") +@unittest.skipIf(not pybamm.have_idaklu(), "idaklu solver is not installed") class TestIDAKLUSolver(unittest.TestCase): def test_on_spme(self): model = pybamm.lithium_ion.SPMe() @@ -16,9 +16,26 @@ def test_on_spme(self): disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) t_eval = np.linspace(0, 0.2, 100) - solution = pybamm.IDAKLU().solve(model, t_eval) + solution = pybamm.IDAKLUSolver().solve(model, t_eval) np.testing.assert_array_less(1, solution.t.size) + def test_set_tol_by_variable(self): + model = pybamm.lithium_ion.SPMe() + geometry = model.default_geometry + param = model.default_parameter_values + param.process_model(model) + param.process_geometry(geometry) + mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) + disc = pybamm.Discretisation(mesh, model.default_spatial_methods) + disc.process_model(model) + t_eval = np.linspace(0, 0.2, 100) + solver = pybamm.IDAKLUSolver() + + variable_tols = {"Electrolyte concentration": 1e-3} + solver.set_atol_by_variable(variable_tols, model) + + solver.solve(model, t_eval) + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_expression_tree/test_functions.py b/tests/unit/test_expression_tree/test_functions.py index bb10dd2429..28d1586b36 100644 --- a/tests/unit/test_expression_tree/test_functions.py +++ b/tests/unit/test_expression_tree/test_functions.py @@ -21,6 +21,10 @@ def test_multi_var_function(arg1, arg2): return arg1 + arg2 +def test_multi_var_function_cube(arg1, arg2): + return arg1 + arg2 ** 3 + + class TestFunction(unittest.TestCase): def test_constant_functions(self): d = pybamm.Scalar(6) @@ -52,8 +56,9 @@ def test_function_of_one_variable(self): logvar.evaluate(y=y, known_evals={})[0], np.log1p(y) ) - def test_with_autograd(self): + def test_diff(self): a = pybamm.StateVector(slice(0, 1)) + b = pybamm.StateVector(slice(1, 2)) y = np.array([5]) func = pybamm.Function(test_function, a) self.assertEqual(func.diff(a).evaluate(y=y), 2) @@ -68,6 +73,21 @@ def test_with_autograd(self): # multiple variables func = pybamm.Function(test_multi_var_function, 4 * a, 3 * a) self.assertEqual(func.diff(a).evaluate(y=y), 7) + func = pybamm.Function(test_multi_var_function, 4 * a, 3 * b) + self.assertEqual(func.diff(a).evaluate(y=np.array([5, 6])), 4) + self.assertEqual(func.diff(b).evaluate(y=np.array([5, 6])), 3) + func = pybamm.Function(test_multi_var_function_cube, 4 * a, 3 * b) + self.assertEqual(func.diff(a).evaluate(y=np.array([5, 6])), 4) + self.assertEqual( + func.diff(b).evaluate(y=np.array([5, 6])), 3 * 3 * (3 * 6) ** 2 + ) + + # exceptions + func = pybamm.Function( + test_multi_var_function_cube, 4 * a, 3 * b, derivative="derivative" + ) + with self.assertRaises(ValueError): + func.diff(a) def test_function_of_multiple_variables(self): a = pybamm.Variable("a") diff --git a/tests/unit/test_expression_tree/test_operations/__init__.py b/tests/unit/test_expression_tree/test_operations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..7f8287591e --- /dev/null +++ b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py @@ -0,0 +1,186 @@ +# +# Test for the Simplify class +# +import casadi +import numpy as np +import autograd.numpy as anp +import pybamm +import unittest +from tests import get_mesh_for_testing, get_1p1d_discretisation_for_testing + + +class TestCasadiConverter(unittest.TestCase): + def assert_casadi_equal(self, a, b, evalf=False): + if evalf is True: + self.assertTrue((casadi.evalf(a) - casadi.evalf(b)).is_zero()) + else: + self.assertTrue((a - b).is_zero()) + + def test_convert_scalar_symbols(self): + a = pybamm.Scalar(0) + b = pybamm.Scalar(1) + c = pybamm.Scalar(-1) + d = pybamm.Scalar(2) + + self.assertEqual(a.to_casadi(), casadi.MX(0)) + self.assertEqual(d.to_casadi(), casadi.MX(2)) + + # negate + self.assertEqual((-b).to_casadi(), casadi.MX(-1)) + # absolute value + self.assertEqual(abs(c).to_casadi(), casadi.MX(1)) + + # function + def sin(x): + return np.sin(x) + + f = pybamm.Function(sin, b) + self.assertEqual(f.to_casadi(), casadi.MX(np.sin(1))) + + def myfunction(x, y): + return x + y + + f = pybamm.Function(myfunction, b, d) + self.assertEqual(f.to_casadi(), casadi.MX(3)) + + # addition + self.assertEqual((a + b).to_casadi(), casadi.MX(1)) + # subtraction + self.assertEqual((c - d).to_casadi(), casadi.MX(-3)) + # multiplication + self.assertEqual((c * d).to_casadi(), casadi.MX(-2)) + # power + self.assertEqual((c ** d).to_casadi(), casadi.MX(1)) + # division + self.assertEqual((b / d).to_casadi(), casadi.MX(1 / 2)) + + def test_convert_array_symbols(self): + # Arrays + a = np.array([1, 2, 3, 4, 5]) + pybamm_a = pybamm.Array(a) + self.assert_casadi_equal(pybamm_a.to_casadi(), casadi.MX(a)) + + casadi_t = casadi.MX.sym("t") + casadi_y = casadi.MX.sym("y", 10) + + pybamm_t = pybamm.Time() + pybamm_y = pybamm.StateVector(slice(0, 10)) + + # Time + self.assertEqual(pybamm_t.to_casadi(casadi_t, casadi_y), casadi_t) + + # State Vector + self.assert_casadi_equal(pybamm_y.to_casadi(casadi_t, casadi_y), casadi_y) + + # outer product + outer = pybamm.Outer(pybamm_a, pybamm_a) + self.assert_casadi_equal( + outer.to_casadi(), casadi.MX(outer.evaluate()), evalf=True + ) + + 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) + self.assert_casadi_equal(pybamm.min(a).to_casadi(), casadi.MX(1), evalf=True) + b = pybamm.Array(np.array([-2])) + c = pybamm.Array(np.array([3])) + self.assert_casadi_equal( + pybamm.Function(np.abs, b).to_casadi(), casadi.MX(2), evalf=True + ) + self.assert_casadi_equal( + pybamm.Function(np.abs, c).to_casadi(), casadi.MX(3), evalf=True + ) + + def test_interpolation(self): + x = np.linspace(0, 1)[:, np.newaxis] + y = pybamm.StateVector(slice(0, 2)) + casadi_y = casadi.MX.sym("y", 2) + # linear + linear = np.hstack([x, 2 * x]) + y_test = np.array([0.4, 0.6]) + for interpolator in ["pchip", "cubic spline"]: + interp = pybamm.Interpolant(linear, y, interpolator=interpolator) + interp_casadi = interp.to_casadi(y=casadi_y) + f = casadi.Function("f", [casadi_y], [interp_casadi]) + np.testing.assert_array_almost_equal(interp.evaluate(y=y_test), f(y_test)) + # square + square = np.hstack([x, x ** 2]) + y = pybamm.StateVector(slice(0, 1)) + for interpolator in ["pchip", "cubic spline"]: + interp = pybamm.Interpolant(square, y, interpolator=interpolator) + interp_casadi = interp.to_casadi(y=casadi_y) + f = casadi.Function("f", [casadi_y], [interp_casadi]) + np.testing.assert_array_almost_equal(interp.evaluate(y=y_test), f(y_test)) + + def test_concatenations(self): + y = np.linspace(0, 1, 10)[:, np.newaxis] + a = pybamm.Vector(y) + b = pybamm.Scalar(16) + c = pybamm.Scalar(3) + conc = pybamm.NumpyConcatenation(a, b, c) + self.assert_casadi_equal( + conc.to_casadi(), casadi.MX(conc.evaluate()), evalf=True + ) + + # Domain concatenation + mesh = get_mesh_for_testing() + a_dom = ["negative electrode"] + b_dom = ["positive electrode"] + a = 2 * pybamm.Vector(np.ones_like(mesh[a_dom[0]][0].nodes), domain=a_dom) + b = pybamm.Vector(np.ones_like(mesh[b_dom[0]][0].nodes), domain=b_dom) + conc = pybamm.DomainConcatenation([b, a], mesh) + self.assert_casadi_equal( + conc.to_casadi(), casadi.MX(conc.evaluate()), evalf=True + ) + + # 2d + disc = get_1p1d_discretisation_for_testing() + a = pybamm.Variable("a", domain=a_dom) + b = pybamm.Variable("b", domain=b_dom) + conc = pybamm.Concatenation(a, b) + disc.set_variable_slices([conc]) + expr = disc.process_symbol(conc) + y = casadi.SX.sym("y", expr.size) + x = expr.to_casadi(None, y) + f = casadi.Function("f", [x], [x]) + y_eval = np.linspace(0, 1, expr.size) + self.assert_casadi_equal(f(y_eval), casadi.SX(expr.evaluate(y=y_eval))) + + def test_convert_differentiated_function(self): + a = pybamm.Scalar(0) + b = pybamm.Scalar(1) + + # function + def sin(x): + return anp.sin(x) + + f = pybamm.Function(sin, b).diff(b) + self.assert_casadi_equal(f.to_casadi(), casadi.MX(np.cos(1)), evalf=True) + + def myfunction(x, y): + return x + y ** 3 + + f = pybamm.Function(myfunction, a, b).diff(a) + self.assert_casadi_equal(f.to_casadi(), casadi.MX(1), evalf=True) + f = pybamm.Function(myfunction, a, b).diff(b) + self.assert_casadi_equal(f.to_casadi(), casadi.MX(3), evalf=True) + + def test_errors(self): + y = pybamm.StateVector(slice(0, 10)) + with self.assertRaisesRegex( + ValueError, "Must provide a 'y' for converting state vectors" + ): + y.to_casadi() + var = pybamm.Variable("var") + with self.assertRaisesRegex(TypeError, "Cannot convert symbol of type"): + var.to_casadi() + + +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_copy.py b/tests/unit/test_expression_tree/test_operations/test_copy.py similarity index 100% rename from tests/unit/test_expression_tree/test_copy.py rename to tests/unit/test_expression_tree/test_operations/test_copy.py diff --git a/tests/unit/test_expression_tree/test_evaluate.py b/tests/unit/test_expression_tree/test_operations/test_evaluate.py similarity index 100% rename from tests/unit/test_expression_tree/test_evaluate.py rename to tests/unit/test_expression_tree/test_operations/test_evaluate.py diff --git a/tests/unit/test_expression_tree/test_jac.py b/tests/unit/test_expression_tree/test_operations/test_jac.py similarity index 100% rename from tests/unit/test_expression_tree/test_jac.py rename to tests/unit/test_expression_tree/test_operations/test_jac.py diff --git a/tests/unit/test_expression_tree/test_jac_2D.py b/tests/unit/test_expression_tree/test_operations/test_jac_2D.py similarity index 100% rename from tests/unit/test_expression_tree/test_jac_2D.py rename to tests/unit/test_expression_tree/test_operations/test_jac_2D.py diff --git a/tests/unit/test_expression_tree/test_simplify.py b/tests/unit/test_expression_tree/test_operations/test_simplify.py similarity index 97% rename from tests/unit/test_expression_tree/test_simplify.py rename to tests/unit/test_expression_tree/test_operations/test_simplify.py index 1d1f61c805..f84909706a 100644 --- a/tests/unit/test_expression_tree/test_simplify.py +++ b/tests/unit/test_expression_tree/test_operations/test_simplify.py @@ -102,6 +102,17 @@ def myfunction(x, y): self.assertIsInstance((b - a).simplify(), pybamm.Scalar) self.assertEqual((b - a).simplify().evaluate(), 1) + # addition and subtraction with matrix zero + v = pybamm.Vector(np.zeros((10, 1))) + self.assertIsInstance((b + v).simplify(), pybamm.Array) + np.testing.assert_array_equal((b + v).simplify().evaluate(), np.ones((10, 1))) + self.assertIsInstance((v + b).simplify(), pybamm.Array) + np.testing.assert_array_equal((v + b).simplify().evaluate(), np.ones((10, 1))) + self.assertIsInstance((b - v).simplify(), pybamm.Array) + np.testing.assert_array_equal((b - v).simplify().evaluate(), np.ones((10, 1))) + self.assertIsInstance((v - b).simplify(), pybamm.Array) + np.testing.assert_array_equal((v - b).simplify().evaluate(), -np.ones((10, 1))) + # multiplication self.assertIsInstance((a * b).simplify(), pybamm.Scalar) self.assertEqual((a * b).simplify().evaluate(), 0) diff --git a/tests/unit/test_models/test_base_model.py b/tests/unit/test_models/test_base_model.py index 1e6e54579c..6421e41bce 100644 --- a/tests/unit/test_models/test_base_model.py +++ b/tests/unit/test_models/test_base_model.py @@ -374,6 +374,17 @@ def test_default_solver(self): ) self.assertIsInstance(solver, pybamm.BaseModel) + # check that adding algebraic variables gives DAE solver + a = pybamm.Variable("a") + model.algebraic = {a: a - 1} + self.assertIsInstance( + model.default_solver, (pybamm.IDAKLUSolver, pybamm.CasadiSolver) + ) + + # Check that turning off jacobian gives casadi solver + model.use_jacobian = False + self.assertIsInstance(model.default_solver, pybamm.CasadiSolver) + def test_default_parameters(self): # check parameters are read in ok model = pybamm.BaseBatteryModel() 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 04ec8ba70a..be33fb9688 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 @@ -122,6 +122,12 @@ 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 single"): + pybamm.BaseBatteryModel( + {"current collector": "single particle potential pair"} + ) + with self.assertRaisesRegex(pybamm.OptionError, "option set external"): + pybamm.BaseBatteryModel({"current collector": "set external potential"}) def test_build_twice(self): model = pybamm.lithium_ion.SPM() # need to pick a model to set vars and build diff --git a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_composite.py b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_composite.py index 2a9e16edba..183fe3cc46 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_composite.py +++ b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_composite.py @@ -22,12 +22,13 @@ def test_well_posed_differential(self): class TestLeadAcidCompositeMultiDimensional(unittest.TestCase): - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_well_posed(self): model = pybamm.lead_acid.Composite( {"dimensionality": 1, "current collector": "potential pair"} ) - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) + self.assertIsInstance( + model.default_solver, (pybamm.ScikitsDaeSolver, pybamm.CasadiSolver) + ) model.check_well_posedness() model = pybamm.lead_acid.Composite( @@ -58,12 +59,13 @@ def test_well_posed_differential(self): model = pybamm.lead_acid.Composite(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_well_posed_algebraic(self): options = {"surface form": "algebraic", "side reactions": ["oxygen"]} model = pybamm.lead_acid.Composite(options) model.check_well_posedness() - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) + self.assertIsInstance( + model.default_solver, (pybamm.ScikitsDaeSolver, pybamm.CasadiSolver) + ) class TestLeadAcidCompositeExtended(unittest.TestCase): diff --git a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_full.py b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_full.py index 44737342cf..a30b2c76f7 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_full.py +++ b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_full.py @@ -15,11 +15,6 @@ def test_well_posed_with_convection(self): model = pybamm.lead_acid.Full(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") - def test_default_solver(self): - model = pybamm.lead_acid.Full() - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) - class TestLeadAcidFullSurfaceForm(unittest.TestCase): def test_well_posed_differential(self): @@ -37,15 +32,6 @@ def test_well_posed_algebraic(self): model = pybamm.lead_acid.Full(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") - def test_default_solver(self): - options = {"surface form": "differential"} - model = pybamm.lead_acid.Full(options) - self.assertIsInstance(model.default_solver, pybamm.ScipySolver) - options = {"surface form": "algebraic"} - model = pybamm.lead_acid.Full(options) - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) - class TestLeadAcidFullSideReactions(unittest.TestCase): def test_well_posed(self): 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 681b362f1b..89e76f1801 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 @@ -146,22 +146,6 @@ def test_well_posed_1plus1D(self): model = pybamm.lead_acid.LOQS(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") - def test_default_solver(self): - options = {"surface form": "differential"} - model = pybamm.lead_acid.LOQS(options) - self.assertIsInstance(model.default_solver, pybamm.ScipySolver) - options = { - "surface form": "differential", - "current collector": "potential pair", - "dimensionality": 1, - } - model = pybamm.lead_acid.LOQS(options) - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) - options = {"surface form": "algebraic"} - model = pybamm.lead_acid.LOQS(options) - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) - def test_default_geometry(self): options = {"surface form": "differential"} 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 eb22429f2b..4fcabbccb0 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 @@ -48,7 +48,6 @@ def test_x_full_thermal_model_no_current_collector(self): with self.assertRaises(NotImplementedError): model = pybamm.lithium_ion.DFN(options) - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_full_Nplus1D_not_implemented(self): # 1plus1D options = { @@ -91,7 +90,6 @@ def test_x_lumped_thermal_model_0D_current_collector(self): model = pybamm.lithium_ion.DFN(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_xyz_lumped_thermal_1D_current_collector(self): options = { "current collector": "potential pair", @@ -109,7 +107,6 @@ def test_xyz_lumped_thermal_1D_current_collector(self): model = pybamm.lithium_ion.DFN(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_xyz_lumped_thermal_2D_current_collector(self): options = { "current collector": "potential pair", @@ -127,7 +124,6 @@ def test_xyz_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.DFN(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_lumped_thermal_1D_current_collector(self): options = { "current collector": "potential pair", @@ -137,7 +133,6 @@ def test_x_lumped_thermal_1D_current_collector(self): model = pybamm.lithium_ion.DFN(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_lumped_thermal_2D_current_collector(self): options = { "current collector": "potential pair", @@ -147,11 +142,22 @@ def test_x_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.DFN(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") - def test_default_solver(self): - options = {"thermal": "isothermal"} + 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) - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) + 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"} 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 42336ac4c5..3115655336 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,6 +39,13 @@ def test_well_posed_2plus1D(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() + options = { + "current collector": "single particle potential pair", + "dimensionality": 1, + } + model = pybamm.lithium_ion.SPM(options) + model.check_well_posedness() + options = { "current collector": "single particle potential pair", "dimensionality": 2, @@ -46,6 +53,18 @@ 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) @@ -56,7 +75,6 @@ def test_x_full_thermal_model_no_current_collector(self): with self.assertRaises(NotImplementedError): model = pybamm.lithium_ion.SPM(options) - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_full_Nplus1D_not_implemented(self): # 1plus1D options = { @@ -99,7 +117,6 @@ def test_x_lumped_thermal_model_0D_current_collector(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_xyz_lumped_thermal_1D_current_collector(self): options = { "current collector": "potential pair", @@ -117,7 +134,6 @@ def test_xyz_lumped_thermal_1D_current_collector(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_xyz_lumped_thermal_2D_current_collector(self): options = { "current collector": "potential pair", @@ -135,7 +151,6 @@ def test_xyz_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_lumped_thermal_1D_current_collector(self): options = { "current collector": "potential pair", @@ -145,7 +160,6 @@ def test_x_lumped_thermal_1D_current_collector(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_lumped_thermal_2D_current_collector(self): options = { "current collector": "potential pair", @@ -155,15 +169,22 @@ def test_x_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") - def test_default_solver(self): - options = {"thermal": "isothermal"} + 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) - self.assertIsInstance(model.default_solver, pybamm.ScipySolver) + model.check_well_posedness() - options = {"current collector": "potential pair", "dimensionality": 2} - model = pybamm.lithium_ion.SPM(options) - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) + 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"} 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 8f3b101cb7..8b73fdbffb 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 @@ -34,6 +34,13 @@ def test_well_posed_2plus1D(self): model = pybamm.lithium_ion.SPMe(options) model.check_well_posedness() + options = { + "current collector": "single particle potential pair", + "dimensionality": 1, + } + model = pybamm.lithium_ion.SPMe(options) + model.check_well_posedness() + options = { "current collector": "single particle potential pair", "dimensionality": 2, @@ -55,7 +62,6 @@ def test_x_full_thermal_model_no_current_collector(self): with self.assertRaises(NotImplementedError): model = pybamm.lithium_ion.SPMe(options) - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_full_Nplus1D_not_implemented(self): # 1plus1D options = { @@ -98,7 +104,6 @@ def test_x_lumped_thermal_model_0D_current_collector(self): model = pybamm.lithium_ion.SPMe(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_xyz_lumped_thermal_1D_current_collector(self): options = { "current collector": "potential pair", @@ -116,7 +121,6 @@ def test_xyz_lumped_thermal_1D_current_collector(self): model = pybamm.lithium_ion.SPMe(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_xyz_lumped_thermal_2D_current_collector(self): options = { "current collector": "potential pair", @@ -126,7 +130,6 @@ def test_xyz_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.SPMe(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_lumped_thermal_1D_current_collector(self): options = { "current collector": "potential pair", @@ -144,7 +147,6 @@ def test_x_lumped_thermal_1D_current_collector(self): model = pybamm.lithium_ion.SPMe(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") def test_x_lumped_thermal_2D_current_collector(self): options = { "current collector": "potential pair", @@ -154,14 +156,22 @@ def test_x_lumped_thermal_2D_current_collector(self): model = pybamm.lithium_ion.SPMe(options) model.check_well_posedness() - @unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") - def test_default_solver(self): - options = {"thermal": "isothermal"} - model = pybamm.lithium_ion.SPMe(options) - self.assertIsInstance(model.default_solver, pybamm.ScipySolver) - options = {"current collector": "potential pair", "dimensionality": 2} + 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) - self.assertIsInstance(model.default_solver, pybamm.ScikitsDaeSolver) + 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"} diff --git a/tests/unit/test_models/test_submodels/test_current_collector/test_potential_pair.py b/tests/unit/test_models/test_submodels/test_current_collector/test_potential_pair.py index b4112ad090..f1f8d0dffc 100644 --- a/tests/unit/test_models/test_submodels/test_current_collector/test_potential_pair.py +++ b/tests/unit/test_models/test_submodels/test_current_collector/test_potential_pair.py @@ -10,14 +10,13 @@ class TestBaseModel(unittest.TestCase): def test_public_functions(self): param = pybamm.standard_parameters_lithium_ion - submodel = pybamm.current_collector.PotentialPair1plus1D(param) variables = { "Positive current collector potential": pybamm.PrimaryBroadcast( 0, "current collector" ) } + submodel = pybamm.current_collector.PotentialPair1plus1D(param) std_tests = tests.StandardSubModelTests(submodel, variables) - std_tests.test_all() submodel = pybamm.current_collector.PotentialPair2plus1D(param) std_tests = tests.StandardSubModelTests(submodel, variables) 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 new file mode 100644 index 0000000000..f58ed75f1b --- /dev/null +++ b/tests/unit/test_models/test_submodels/test_current_collector/test_set_potential_spm_1plus1d.py @@ -0,0 +1,56 @@ +# +# Test base current collector submodel +# + +import pybamm +import tests +import unittest +import pybamm.models.submodels.current_collector as cc + + +class TestSetPotetetialSPM1plus1DModel(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 + } + 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 + } + 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_conductivity/test_composite_stefan_maxwell_conductivity.py b/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_conductivity/test_composite_stefan_maxwell_conductivity.py index 6eff2e9d01..c3e55d659f 100644 --- a/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_conductivity/test_composite_stefan_maxwell_conductivity.py +++ b/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_conductivity/test_composite_stefan_maxwell_conductivity.py @@ -22,7 +22,7 @@ def test_public_functions(self): "Leading-order x-averaged negative electrode porosity": a, "Leading-order x-averaged separator porosity": a, "Leading-order x-averaged positive electrode porosity": a, - "Cell temperature": a, + "X-averaged cell temperature": a, } submodel = pybamm.electrolyte.stefan_maxwell.conductivity.Composite(param) std_tests = tests.StandardSubModelTests(submodel, variables) diff --git a/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_conductivity/test_surface_form/test_full_surface_form_stefan_maxwell_conductivity.py b/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_conductivity/test_surface_form/test_full_surface_form_stefan_maxwell_conductivity.py index 464cb91915..bc4bc47d73 100644 --- a/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_conductivity/test_surface_form/test_full_surface_form_stefan_maxwell_conductivity.py +++ b/tests/unit/test_models/test_submodels/test_electrolyte/test_stefan_maxwell/test_conductivity/test_surface_form/test_full_surface_form_stefan_maxwell_conductivity.py @@ -27,6 +27,8 @@ def test_public_functions(self): "Negative electrode interfacial current density": a_n, "Electrolyte potential": pybamm.Concatenation(a_n, a_s, a_p), "Negative electrode temperature": a_n, + "Separator temperature": a_s, + "Positive electrode temperature": a_p, } icd = " interfacial current density" reactions = { 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 bad1837147..da98c47ac0 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 @@ -20,6 +20,7 @@ def test_public_functions(self): "Negative electrolyte potential": a_n, "Negative electrode open circuit potential": a_n, "Negative electrolyte concentration": a_n, + "Negative electrode temperature": a_n, } submodel = pybamm.interface.lead_acid.ButlerVolmer(param, "Negative") std_tests = tests.StandardSubModelTests(submodel, variables) @@ -32,6 +33,7 @@ def test_public_functions(self): "Positive electrolyte potential": a_p, "Positive electrode open circuit potential": a_p, "Positive electrolyte concentration": a_p, + "Positive electrode temperature": a_p, "Negative electrode interfacial current density": a_n, "Negative electrode exchange current density": a_n, } 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 new file mode 100644 index 0000000000..d3ece87c76 --- /dev/null +++ b/tests/unit/test_models/test_submodels/test_thermal/test_x_lumped/test_x_lumped_1D_set_temperature.py @@ -0,0 +1,40 @@ +# +# 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_current_functions.py b/tests/unit/test_parameters/test_current_functions.py index 0d6a2a248d..0581511bb9 100644 --- a/tests/unit/test_parameters/test_current_functions.py +++ b/tests/unit/test_parameters/test_current_functions.py @@ -9,11 +9,11 @@ class TestCurrentFunctions(unittest.TestCase): def test_base_current(self): - function = pybamm.GetCurrent() + function = pybamm.BaseCurrent() self.assertEqual(function(10), 1) def test_constant_current(self): - function = pybamm.GetConstantCurrent(current=4) + function = pybamm.ConstantCurrent(current=4) assert isinstance(function(0), numbers.Number) assert isinstance(function(np.zeros(3)), numbers.Number) assert isinstance(function(np.zeros([3, 3])), numbers.Number) @@ -24,30 +24,20 @@ def test_constant_current(self): { "Typical current [A]": 2, "Typical timescale [s]": 1, - "Current function": pybamm.GetConstantCurrent(), + "Current function": pybamm.ConstantCurrent(), } ) processed_current = parameter_values.process_symbol(current) self.assertIsInstance(processed_current.simplify(), pybamm.Scalar) def test_get_current_data(self): - # test units - function_list = [ - pybamm.GetCurrentData("US06.csv", units="[A]"), - pybamm.GetCurrentData("car_current.csv", units="[]", current_scale=10), - ] - for function in function_list: - function.interpolate() - # test process parameters dimensional_current = pybamm.electrical_parameters.dimensional_current_with_time parameter_values = pybamm.ParameterValues( { "Typical current [A]": 2, "Typical timescale [s]": 1, - "Current function": pybamm.GetCurrentData( - "car_current.csv", units="[]" - ), + "Current function": "[current data]car_current", } ) dimensional_current_eval = parameter_values.process_symbol(dimensional_current) @@ -55,9 +45,7 @@ def test_get_current_data(self): def current(t): return dimensional_current_eval.evaluate(t=t) - function_list.append(current) - - standard_tests = StandardCurrentFunctionTests(function_list, always_array=True) + standard_tests = StandardCurrentFunctionTests([current], always_array=True) standard_tests.test_all() def test_user_current(self): @@ -69,9 +57,9 @@ def my_fun(t, A, omega): A = pybamm.electrical_parameters.I_typ omega = 3 - # pass my_fun to GetUserCurrent class, giving the additonal parameters as + # pass my_fun to UserCurrent class, giving the additonal parameters as # keyword arguments - current = pybamm.GetUserCurrent(my_fun, A=A, omega=omega) + current = pybamm.UserCurrent(my_fun, A=A, omega=omega) # set and process parameters parameter_values = pybamm.ParameterValues( diff --git a/tests/unit/test_parameters/test_electrical_parameters.py b/tests/unit/test_parameters/test_electrical_parameters.py index 8f8f7cad79..4075d3e733 100644 --- a/tests/unit/test_parameters/test_electrical_parameters.py +++ b/tests/unit/test_parameters/test_electrical_parameters.py @@ -24,7 +24,7 @@ def test_current_functions(self): "Number of electrodes connected in parallel to make a cell": 8, "Typical current [A]": 2, "Typical timescale [s]": 60, - "Current function": pybamm.GetConstantCurrent(), + "Current function": pybamm.ConstantCurrent(), } ) dimensional_current_eval = parameter_values.process_symbol(dimensional_current) 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 47b1fd16b6..6a311ab7cc 100644 --- a/tests/unit/test_parameters/test_standard_parameters_lead_acid.py +++ b/tests/unit/test_parameters/test_standard_parameters_lead_acid.py @@ -89,7 +89,7 @@ def test_current_functions(self): "Typical electrolyte concentration [mol.m-3]": 1, "Number of electrodes connected in parallel to make a cell": 8, "Typical current [A]": 2, - "Current function": pybamm.GetConstantCurrent(), + "Current function": pybamm.ConstantCurrent(), } ) dimensional_current_density_eval = parameter_values.process_symbol( diff --git a/tests/unit/test_parameters/test_update_parameters.py b/tests/unit/test_parameters/test_update_parameters.py index ccc13a3c7e..8f6794fcc3 100644 --- a/tests/unit/test_parameters/test_update_parameters.py +++ b/tests/unit/test_parameters/test_update_parameters.py @@ -75,7 +75,7 @@ def test_update_model(self): chemistry=pybamm.parameter_sets.Marquis2019 ) parameter_values_update.update( - {"Current function": pybamm.GetConstantCurrent(current=pybamm.Scalar(0))} + {"Current function": pybamm.ConstantCurrent(current=pybamm.Scalar(0))} ) modeltest3.test_update_parameters(parameter_values_update) modeltest3.test_solving(t_eval=t_eval) @@ -94,6 +94,17 @@ def test_update_model(self): # results should be different self.assertNotEqual(np.linalg.norm(Y1 - Y3), 0) + def test_inplace(self): + model = pybamm.lithium_ion.SPM() + param = model.default_parameter_values + new_model = param.process_model(model, inplace=False) + + for val in list(model.rhs.values()): + self.assertTrue(val.has_symbol_of_classes(pybamm.Parameter)) + + for val in list(new_model.rhs.values()): + self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) + def test_update_geometry(self): # test on simple lead-acid model model1 = pybamm.lead_acid.LOQS() diff --git a/tests/unit/test_processed_variable.py b/tests/unit/test_processed_variable.py index a891fdbc6b..87c29a84a0 100644 --- a/tests/unit/test_processed_variable.py +++ b/tests/unit/test_processed_variable.py @@ -140,13 +140,11 @@ def test_processed_variable_3D_x_z(self): def test_processed_variable_3D_scikit(self): var = pybamm.Variable("var", domain=["current collector"]) - y = pybamm.SpatialVariable("y", domain=["current collector"]) - z = pybamm.SpatialVariable("z", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() disc.set_variable_slices([var]) - y_sol = disc.process_symbol(y).entries[:, 0] - z_sol = disc.process_symbol(z).entries[:, 0] + y = disc.mesh["current collector"][0].edges["y"] + z = disc.mesh["current collector"][0].edges["z"] var_sol = disc.process_symbol(var) t_sol = np.linspace(0, 1) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] * np.linspace(0, 5) @@ -154,25 +152,23 @@ def test_processed_variable_3D_scikit(self): processed_var = pybamm.ProcessedVariable(var_sol, t_sol, u_sol, mesh=disc.mesh) np.testing.assert_array_equal( processed_var.entries, - np.reshape(u_sol, [len(y_sol), len(z_sol), len(t_sol)]), + np.reshape(u_sol, [len(y), len(z), len(t_sol)]), ) def test_processed_variable_2Dspace_scikit(self): var = pybamm.Variable("var", domain=["current collector"]) - y = pybamm.SpatialVariable("y", domain=["current collector"]) - z = pybamm.SpatialVariable("z", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() disc.set_variable_slices([var]) - y_sol = disc.process_symbol(y).entries[:, 0] - z_sol = disc.process_symbol(z).entries[:, 0] + y = disc.mesh["current collector"][0].edges["y"] + z = disc.mesh["current collector"][0].edges["z"] var_sol = disc.process_symbol(var) t_sol = np.array([0]) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] processed_var = pybamm.ProcessedVariable(var_sol, t_sol, u_sol, mesh=disc.mesh) np.testing.assert_array_equal( - processed_var.entries, np.reshape(u_sol, [len(y_sol), len(z_sol)]) + processed_var.entries, np.reshape(u_sol, [len(y), len(z)]) ) def test_processed_var_1D_interpolation(self): @@ -367,13 +363,11 @@ def test_processed_var_3D_r_first_dimension(self): def test_processed_var_3D_scikit_interpolation(self): var = pybamm.Variable("var", domain=["current collector"]) - y = pybamm.SpatialVariable("y", domain=["current collector"]) - z = pybamm.SpatialVariable("z", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() disc.set_variable_slices([var]) - y_sol = disc.process_symbol(y).entries[:, 0] - z_sol = disc.process_symbol(z).entries[:, 0] + y_sol = disc.mesh["current collector"][0].edges["y"] + z_sol = disc.mesh["current collector"][0].edges["z"] var_sol = disc.process_symbol(var) t_sol = np.linspace(0, 1) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] * np.linspace(0, 5) @@ -406,13 +400,11 @@ def test_processed_var_3D_scikit_interpolation(self): def test_processed_var_2Dspace_scikit_interpolation(self): var = pybamm.Variable("var", domain=["current collector"]) - y = pybamm.SpatialVariable("y", domain=["current collector"]) - z = pybamm.SpatialVariable("z", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() disc.set_variable_slices([var]) - y_sol = disc.process_symbol(y).entries[:, 0] - z_sol = disc.process_symbol(z).entries[:, 0] + y_sol = disc.mesh["current collector"][0].edges["y"] + z_sol = disc.mesh["current collector"][0].edges["z"] var_sol = disc.process_symbol(var) t_sol = np.array([0]) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] diff --git a/tests/unit/test_simulation.py b/tests/unit/test_simulation.py new file mode 100644 index 0000000000..8dd3aa1b36 --- /dev/null +++ b/tests/unit/test_simulation.py @@ -0,0 +1,140 @@ +import pybamm +import unittest + + +class TestSimulation(unittest.TestCase): + def test_basic_ops(self): + + model = pybamm.lithium_ion.SPM() + sim = pybamm.Simulation(model) + + self.assertEqual(model.__class__, sim._model_class) + self.assertEqual(model.options, sim._model_options) + + # check that the model is unprocessed + self.assertEqual(sim._mesh, None) + self.assertEqual(sim._disc, None) + for val in list(sim.model.rhs.values()): + self.assertTrue(val.has_symbol_of_classes(pybamm.Parameter)) + self.assertFalse(val.has_symbol_of_classes(pybamm.Matrix)) + + sim.set_parameters() + self.assertEqual(sim._mesh, None) + self.assertEqual(sim._disc, None) + for val in list(sim.model_with_set_params.rhs.values()): + self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) + self.assertFalse(val.has_symbol_of_classes(pybamm.Matrix)) + + sim.build() + self.assertFalse(sim._mesh is None) + self.assertFalse(sim._disc is None) + for val in list(sim.built_model.rhs.values()): + self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) + self.assertTrue(val.has_symbol_of_classes(pybamm.Matrix)) + + sim.reset() + sim.set_parameters() + self.assertEqual(sim._mesh, None) + self.assertEqual(sim._disc, None) + self.assertEqual(sim.built_model, None) + + for val in list(sim.model_with_set_params.rhs.values()): + self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) + self.assertFalse(val.has_symbol_of_classes(pybamm.Matrix)) + + sim.build() + sim.reset() + self.assertEqual(sim._mesh, None) + self.assertEqual(sim._disc, None) + self.assertEqual(sim.model_with_set_params, None) + self.assertEqual(sim.built_model, None) + for val in list(sim.model.rhs.values()): + self.assertTrue(val.has_symbol_of_classes(pybamm.Parameter)) + self.assertFalse(val.has_symbol_of_classes(pybamm.Matrix)) + + def test_solve(self): + + sim = pybamm.Simulation(pybamm.lithium_ion.SPM()) + sim.solve() + self.assertFalse(sim._solution is None) + for val in list(sim.built_model.rhs.values()): + self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) + self.assertTrue(val.has_symbol_of_classes(pybamm.Matrix)) + + sim.reset() + self.assertEqual(sim.model_with_set_params, None) + self.assertEqual(sim.built_model, None) + for val in list(sim.model.rhs.values()): + self.assertTrue(val.has_symbol_of_classes(pybamm.Parameter)) + self.assertFalse(val.has_symbol_of_classes(pybamm.Matrix)) + + self.assertEqual(sim._solution, None) + + def test_reuse_commands(self): + + sim = pybamm.Simulation(pybamm.lithium_ion.SPM()) + + sim.set_parameters() + sim.set_parameters() + + sim.build() + sim.build() + + sim.solve() + sim.solve() + + sim.build() + sim.solve() + sim.set_parameters() + + def test_specs(self): + # test can rebuild after setting specs + sim = pybamm.Simulation(pybamm.lithium_ion.SPM()) + sim.build() + + model_options = {"thermal": "lumped"} + sim.specs(model_options=model_options) + sim.build() + self.assertEqual(sim.model.options["thermal"], "lumped") + + params = sim.parameter_values + # normally is 0.0001 + params.update({"Negative electrode thickness [m]": 0.0002}) + sim.specs(parameter_values=params) + + self.assertEqual( + sim.parameter_values["Negative electrode thickness [m]"], 0.0002 + ) + sim.build() + + geometry = sim.unprocessed_geometry + custom_geometry = {} + x_n = pybamm.standard_spatial_vars.x_n + custom_geometry["negative electrode"] = { + "primary": { + x_n: {"min": pybamm.Scalar(0), "max": pybamm.geometric_parameters.l_n} + } + } + geometry.update(custom_geometry) + sim.specs(geometry=geometry) + sim.build() + + var_pts = sim.var_pts + var_pts[pybamm.standard_spatial_vars.x_n] = 5 + sim.specs(var_pts=var_pts) + sim.build() + + spatial_methods = sim.spatial_methods + # nothing to change this to at the moment but just reload in + sim.specs(spatial_methods=spatial_methods) + sim.build() + + +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/unit/test_solvers/test_algebraic_solver.py b/tests/unit/test_solvers/test_algebraic_solver.py index 629eb29522..772426bb6d 100644 --- a/tests/unit/test_solvers/test_algebraic_solver.py +++ b/tests/unit/test_solvers/test_algebraic_solver.py @@ -107,11 +107,6 @@ def test_model_solver(self): model.variables["var2"].evaluate(t=None, y=solution.y), sol[100:] ) - # Test time - self.assertGreater( - solution.total_time, solution.solve_time + solution.set_up_time - ) - # Test without jacobian model.use_jacobian = False solution_no_jac = solver.solve(model) diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py new file mode 100644 index 0000000000..ca2769bda2 --- /dev/null +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -0,0 +1,193 @@ +# +# Tests for the Casadi Solver class +# +import casadi +import pybamm +import unittest +import numpy as np +from tests import get_mesh_for_testing, get_discretisation_for_testing +import warnings + + +class TestCasadiSolver(unittest.TestCase): + def test_integrate(self): + # Constant + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") + + y = casadi.MX.sym("y") + constant_growth = casadi.MX(0.5) + problem = {"x": y, "ode": constant_growth} + + y0 = np.array([0]) + t_eval = np.linspace(0, 1, 100) + solution = solver.integrate_casadi(problem, y0, t_eval) + np.testing.assert_array_equal(solution.t, t_eval) + np.testing.assert_allclose(0.5 * solution.t, solution.y[0]) + + # Exponential decay + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="cvodes") + + exponential_decay = -0.1 * y + problem = {"x": y, "ode": exponential_decay} + + y0 = np.array([1]) + t_eval = np.linspace(0, 1, 100) + solution = solver.integrate_casadi(problem, y0, t_eval) + np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t)) + self.assertEqual(solution.termination, "final time") + + def test_integrate_failure(self): + # Turn off warnings to ignore sqrt error + warnings.simplefilter("ignore") + + y = casadi.MX.sym("y") + sqrt_decay = -np.sqrt(y) + + y0 = np.array([1]) + t_eval = np.linspace(0, 3, 100) + solver = pybamm.CasadiSolver() + problem = {"x": y, "ode": sqrt_decay} + # Expect solver to fail when y goes negative + with self.assertRaises(pybamm.SolverError): + solver.integrate_casadi(problem, y0, t_eval) + + # Set up as a model and solve + # Create model + model = pybamm.BaseModel() + domain = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=domain) + model.rhs = {var: -pybamm.Function(np.sqrt, var)} + model.initial_conditions = {var: 1} + # add events so that safe mode is used (won't be triggered) + model.events = {"10": var - 10} + # No need to set parameters; can use base discretisation (no spatial operators) + + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + # Solve with failure at t=2 + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") + t_eval = np.linspace(0, 20, 100) + with self.assertRaises(pybamm.SolverError): + solver.solve(model, t_eval) + # Solve with failure at t=0 + model.initial_conditions = {var: 0} + disc.process_model(model) + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") + t_eval = np.linspace(0, 20, 100) + with self.assertRaises(pybamm.SolverError): + solver.solve(model, t_eval) + + # Turn warnings back on + warnings.simplefilter("default") + + def test_bad_mode(self): + with self.assertRaisesRegex(ValueError, "invalid mode"): + pybamm.CasadiSolver(mode="bad mode") + + def test_model_solver(self): + # Create model + model = pybamm.BaseModel() + domain = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=domain) + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + # No need to set parameters; can use base discretisation (no spatial operators) + + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + # Solve + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") + t_eval = np.linspace(0, 1, 100) + 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)) + + # Safe mode (enforce events that won't be triggered) + model.events = {"an event": var + 1} + disc.process_model(model) + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") + t_eval = np.linspace(0, 1, 100) + 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)) + + def test_model_solver_events(self): + # Create model + model = pybamm.BaseModel() + whole_cell = ["negative electrode", "separator", "positive electrode"] + var1 = pybamm.Variable("var1", domain=whole_cell) + var2 = pybamm.Variable("var2", domain=whole_cell) + model.rhs = {var1: 0.1 * var1} + model.algebraic = {var2: 2 * var1 - var2} + model.initial_conditions = {var1: 1, var2: 2} + model.events = { + "var1 = 1.5": pybamm.min(var1 - 1.5), + "var2 = 2.5": pybamm.min(var2 - 2.5), + } + disc = get_discretisation_for_testing() + disc.process_model(model) + + # Solve + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[-1], 2.5) + np.testing.assert_array_almost_equal( + solution.y[0], np.exp(0.1 * solution.t), decimal=5 + ) + np.testing.assert_array_almost_equal( + solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 + ) + + def test_model_step(self): + # Create model + model = pybamm.BaseModel() + domain = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=domain) + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + # No need to set parameters; can use base discretisation (no spatial operators) + + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8, method="idas") + + # Step once + dt = 0.1 + step_sol = solver.step(model, dt) + np.testing.assert_array_equal(step_sol.t, [0, dt]) + np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t)) + + # Step again (return 5 points) + step_sol_2 = solver.step(model, dt, npts=5) + np.testing.assert_array_equal(step_sol_2.t, np.linspace(dt, 2 * dt, 5)) + np.testing.assert_allclose(step_sol_2.y[0], np.exp(0.1 * step_sol_2.t)) + + # append solutions + step_sol.append(step_sol_2) + + # Check steps give same solution as solve + t_eval = step_sol.t + solution = solver.solve(model, t_eval) + np.testing.assert_allclose(solution.y[0], step_sol.y[0]) + + +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_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index a644ec7e3b..ac01bf66ca 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -7,7 +7,7 @@ import unittest -@unittest.skipIf(pybamm.have_idaklu(), "idaklu solver is not installed") +@unittest.skipIf(not pybamm.have_idaklu(), "idaklu solver is not installed") class TestIDAKLUSolver(unittest.TestCase): def test_ida_roberts_klu(self): # this test implements a python version of the ida Roberts @@ -53,7 +53,7 @@ def rhs(t, y): def alg(t, y): return np.array([1 - y[1]]) - solver = pybamm.IDAKLU() + solver = pybamm.IDAKLUSolver() solver.residuals = res solver.rhs = rhs solver.algebraic = alg @@ -76,6 +76,20 @@ def alg(t, y): true_solution = 0.1 * solution.t np.testing.assert_array_almost_equal(solution.y[0, :], true_solution) + def test_set_atol(self): + model = pybamm.lithium_ion.SPMe() + geometry = model.default_geometry + param = model.default_parameter_values + param.process_model(model) + param.process_geometry(geometry) + mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) + disc = pybamm.Discretisation(mesh, model.default_spatial_methods) + disc.process_model(model) + solver = pybamm.IDAKLUSolver() + + variable_tols = {"Electrolyte concentration": 1e-3} + solver.set_atol_by_variable(variable_tols, model) + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_solvers/test_ode_solver.py b/tests/unit/test_solvers/test_ode_solver.py index 009dbcca23..5469e4f726 100644 --- a/tests/unit/test_solvers/test_ode_solver.py +++ b/tests/unit/test_solvers/test_ode_solver.py @@ -24,6 +24,10 @@ def test_wrong_solver(self): pybamm.SolverError, "Cannot use ODE solver to solve model with DAEs" ): solver.solve(model, None) + with self.assertRaisesRegex( + pybamm.SolverError, "Cannot use ODE solver to solve model with DAEs" + ): + solver.set_up_casadi(model) if __name__ == "__main__": diff --git a/tests/unit/test_solvers/test_scikits_solvers.py b/tests/unit/test_solvers/test_scikits_solvers.py index 436043cf20..af36063ff2 100644 --- a/tests/unit/test_solvers/test_scikits_solvers.py +++ b/tests/unit/test_solvers/test_scikits_solvers.py @@ -9,7 +9,7 @@ from tests import get_mesh_for_testing, get_discretisation_for_testing -@unittest.skipIf(~pybamm.have_scikits_odes(), "scikits.odes not installed") +@unittest.skipIf(not pybamm.have_scikits_odes(), "scikits.odes not installed") class TestScikitsSolvers(unittest.TestCase): def test_ode_integrate(self): # Constant @@ -411,11 +411,6 @@ def test_model_solver_ode(self): np.testing.assert_array_equal(solution.t, t_eval) np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) - # Test time - self.assertGreater( - solution.total_time, solution.solve_time + solution.set_up_time - ) - def test_model_solver_ode_events(self): # Create model model = pybamm.BaseModel() @@ -508,11 +503,6 @@ def test_model_solver_dae(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)) - # Test time - self.assertGreater( - solution.total_time, solution.solve_time + solution.set_up_time - ) - def test_model_solver_dae_bad_ics(self): # Create model model = pybamm.BaseModel() @@ -676,6 +666,57 @@ def test_model_step_dae(self): np.testing.assert_allclose(solution.y[0], step_sol.y[0, :]) np.testing.assert_allclose(solution.y[-1], step_sol.y[-1, :]) + def test_model_solver_ode_events_casadi(self): + # Create model + model = pybamm.BaseModel() + model.convert_to_format = "casadi" + whole_cell = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=whole_cell) + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + model.events = { + "2 * var = 2.5": pybamm.min(2 * var - 2.5), + "var = 1.5": pybamm.min(var - 1.5), + } + disc = get_discretisation_for_testing() + disc.process_model(model) + + # Solve + solver = pybamm.ScikitsOdeSolver(rtol=1e-9, atol=1e-9) + t_eval = np.linspace(0, 10, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[0], 1.25) + + def test_model_solver_dae_events_casadi(self): + # Create model + model = pybamm.BaseModel() + for use_jacobian in [True, False]: + model.use_jacobian = use_jacobian + model.convert_to_format = "casadi" + whole_cell = ["negative electrode", "separator", "positive electrode"] + var1 = pybamm.Variable("var1", domain=whole_cell) + var2 = pybamm.Variable("var2", domain=whole_cell) + model.rhs = {var1: 0.1 * var1} + model.algebraic = {var2: 2 * var1 - var2} + model.initial_conditions = {var1: 1, var2: 2} + model.events = { + "var1 = 1.5": pybamm.min(var1 - 1.5), + "var2 = 2.5": pybamm.min(var2 - 2.5), + } + disc = get_discretisation_for_testing() + disc.process_model(model) + + # Solve + solver = pybamm.ScikitsDaeSolver(rtol=1e-8, atol=1e-8) + t_eval = np.linspace(0, 5, 100) + solution = solver.solve(model, t_eval) + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[-1], 2.5) + 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)) + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index 1e5aaeebb9..c083dbabc7 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -154,7 +154,7 @@ def test_model_solver(self): np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) # Test time - self.assertGreater( + self.assertEqual( solution.total_time, solution.solve_time + solution.set_up_time ) @@ -268,6 +268,33 @@ def test_model_step(self): solution = solver.solve(model, t_eval) np.testing.assert_allclose(solution.y[0], step_sol.y[0]) + def test_model_solver_with_event_with_casadi(self): + # Create model + model = pybamm.BaseModel() + for use_jacobian in [True, False]: + model.use_jacobian = use_jacobian + model.convert_to_format = "casadi" + domain = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=domain) + model.rhs = {var: -0.1 * var} + model.initial_conditions = {var: 1} + model.events = {"var=0.5": pybamm.min(var - 0.5)} + # No need to set parameters; can use base discretisation (no spatial + # operators) + + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + # Solve + solver = pybamm.ScipySolver(rtol=1e-8, atol=1e-8, method="RK45") + t_eval = np.linspace(0, 10, 100) + solution = solver.solve(model, t_eval) + self.assertLess(len(solution.t), len(t_eval)) + np.testing.assert_array_equal(solution.t, t_eval[: len(solution.t)]) + np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t)) + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_spatial_methods/test_scikit_finite_element.py b/tests/unit/test_spatial_methods/test_scikit_finite_element.py index 5a108e275d..21bd4b8799 100644 --- a/tests/unit/test_spatial_methods/test_scikit_finite_element.py +++ b/tests/unit/test_spatial_methods/test_scikit_finite_element.py @@ -461,6 +461,68 @@ def test_pure_neumann_poisson(self): u_exact = z ** 2 / 2 - 1 / 6 np.testing.assert_array_almost_equal(solution.y[:-1], u_exact, decimal=1) + def test_dirichlet_bcs(self): + # manufactured solution u = a*z^2 + b*z + c + model = pybamm.BaseModel() + a = 3 + b = 4 + c = 5 + u = pybamm.Variable("variable", domain="current collector") + model.algebraic = {u: -pybamm.laplacian(u) + pybamm.source(2 * a, u)} + # set boundary conditions ("negative tab" = bottom of unit square, + # "positive tab" = top of unit square, elsewhere normal derivative is zero) + model.boundary_conditions = { + u: { + "negative tab": (pybamm.Scalar(c), "Dirichlet"), + "positive tab": (pybamm.Scalar(a + b + c), "Dirichlet"), + } + } + # bad initial guess (on purpose) + model.initial_conditions = {u: pybamm.Scalar(1)} + model.variables = {"u": u} + # create discretisation + mesh = get_unit_2p1D_mesh_for_testing(ypts=8, zpts=32) + spatial_methods = { + "macroscale": pybamm.FiniteVolume, + "current collector": pybamm.ScikitFiniteElement, + } + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + + # solve model + solver = pybamm.AlgebraicSolver() + solution = solver.solve(model) + + # indepedent of y, so just check values for one y + z = mesh["current collector"][0].edges["z"][:, np.newaxis] + u_exact = a * z ** 2 + b * z + c + np.testing.assert_array_almost_equal(solution.y[0 : len(z)], u_exact) + + def test_disc_spatial_var(self): + mesh = get_unit_2p1D_mesh_for_testing(ypts=4, zpts=5) + spatial_methods = { + "macroscale": pybamm.FiniteVolume, + "current collector": pybamm.ScikitFiniteElement, + } + disc = pybamm.Discretisation(mesh, spatial_methods) + + # discretise y and z + y = pybamm.SpatialVariable("y", ["current collector"]) + z = pybamm.SpatialVariable("z", ["current collector"]) + y_disc = disc.process_symbol(y) + z_disc = disc.process_symbol(z) + + # create expected meshgrid + y_vec = np.linspace(0, 1, 4) + z_vec = np.linspace(0, 1, 5) + Y, Z = np.meshgrid(y_vec, z_vec) + y_actual = np.transpose(Y).flatten()[:, np.newaxis] + z_actual = np.transpose(Z).flatten()[:, np.newaxis] + + # spatial vars should discretise to the flattend meshgrid + np.testing.assert_array_equal(y_disc.evaluate(), y_actual) + np.testing.assert_array_equal(z_disc.evaluate(), z_actual) + if __name__ == "__main__": print("Add -v for more debug output")