diff --git a/.gitignore b/.gitignore index 444611541e..68735399c8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,11 +5,13 @@ /local/ *.DS_Store *.mat +*.csv -# don't ignore important .txt files +# don't ignore important .txt and .csv files !requirements* !LICENSE.txt !CMakeLists.txt +!input/**/*.csv # running files *.pyc @@ -70,6 +72,12 @@ pyvenv.cfg # sundials sundials sundials4 +sundials-* +SuiteSparse-* +build_sundials + +# downloads +*.gz # third party third-party diff --git a/.travis.yml b/.travis.yml index f7a04dfbbe..b3f5ffb29a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -62,6 +62,7 @@ matrix: env: - PYTHON=3.7.4 - PYBAMM_UNIT=true + - PYBAMM_KLU=true if: type != cron - python: "3.7" addons: @@ -177,6 +178,8 @@ before_install: | brew update; # Per the `pyenv homebrew recommendations `_. brew install graphviz openssl readline; + # Other brew packages + brew install gcc cmake openblas suitesparse; # See https://docs.travis-ci.com/user/osx-ci-environment/#A-note-on-upgrading-packages. brew outdated pyenv || brew upgrade pyenv # virtualenv doesn't work without pyenv knowledge. venv in Python 3.3 @@ -200,12 +203,28 @@ before_install: | # without the packages from -dev and -doc! install: - pip install --upgrade pip + # In order to download SUNDIALS and SuiteSparse + - pip install wget - pip install . - if [[ $PYBAMM_DOCS == true ]]; then pip install -e .[docs]; fi; - if [[ $PYBAMM_STYLE == true || $PYBAMM_EXAMPLES ]]; then pip install -e .[dev]; fi; - if [[ $PYBAMM_COVER == true ]]; then pip install coverage codecov; fi; - - if [[ $PYBAMM_SCIKITS_ODES == true ]]; then source scripts/install_scikits_odes.sh; fi; - - if [[ $PYBAMM_KLU == true ]]; then source scripts/install_sundials_4.1.0.sh; fi; + - | + if [[ $PYBAMM_SCIKITS_ODES == true ]]; then + python setup.py install_odes -f; + export LD_LIBRARY_PATH=sundials/lib:$LD_LIBRARY_PATH + fi; + - | + if [[ $PYBAMM_KLU == true ]]; then + mkdir -p third-party; + cd third-party; + rm -rf pybind11; + git clone https://github.com/pybind/pybind11.git; + cd ../; + python setup.py install_klu -f; + export LD_LIBRARY_PATH=sundials/lib:$LD_LIBRARY_PATH + export LD_LIBRARY_PATH=SuiteSparse-5.6.0/lib:$LD_LIBRARY_PATH + fi; before_script: - python --version diff --git a/CHANGELOG.md b/CHANGELOG.md index 4530e9449a..0058b24240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,16 @@ ## Features - Added capacitance effects to lithium-ion models () +- Added NCA parameter set ([#824](https://github.com/pybamm-team/PyBaMM/pull/824)) +- Added functionality to `Solution` that automatically gets `t_eval` from the data when simulating drive cycles and performs checks to ensure the output has the required resolution to accurately capture the input current ([#819](https://github.com/pybamm-team/PyBaMM/pull/819)) +- Added options to export a solution to matlab or csv ([#811](https://github.com/pybamm-team/PyBaMM/pull/811)) +- Allow porosity to vary in space ([#809](https://github.com/pybamm-team/PyBaMM/pull/809)) +- Added functionality to solve DAE models with non-smooth current inputs ([#808](https://github.com/pybamm-team/PyBaMM/pull/808)) +- Added functionality to simulate experiments and testing protocols ([#807](https://github.com/pybamm-team/PyBaMM/pull/807)) - Added fuzzy string matching for parameters and variables ([#796](https://github.com/pybamm-team/PyBaMM/pull/796)) - Changed ParameterValues to raise an error when a parameter that wasn't previously defined is updated ([#796](https://github.com/pybamm-team/PyBaMM/pull/796)) - Added some basic models (BasicSPM and BasicDFN) in order to clearly demonstrate the PyBaMM model structure for battery models ([#795](https://github.com/pybamm-team/PyBaMM/pull/795)) +- Allow initial conditions in the particle to depend on x ([#786](https://github.com/pybamm-team/PyBaMM/pull/786)) - Added the harmonic mean to the Finite Volume method, which is now used when computing fluxes ([#783](https://github.com/pybamm-team/PyBaMM/pull/783)) - Refactored `Solution` to make it a dictionary that contains all of the solution variables. This automatically creates `ProcessedVariable` objects when required, so that the solution can be obtained much more easily. ([#781](https://github.com/pybamm-team/PyBaMM/pull/781)) - Added notebook to explain broadcasts ([#776](https://github.com/pybamm-team/PyBaMM/pull/776)) @@ -47,6 +54,8 @@ ## Bug fixes +- Fixed a bug where the first line of the data wasn't loaded when parameters are loaded from data ([#819](https://github.com/pybamm-team/PyBaMM/pull/819)) +- Made `graphviz` an optional dependency ([#810](https://github.com/pybamm-team/PyBaMM/pull/810)) - Fixed examples to run with basic pip installation ([#800](https://github.com/pybamm-team/PyBaMM/pull/800)) - Added events for CasADi solver when stepping ([#800](https://github.com/pybamm-team/PyBaMM/pull/800)) - Improved implementation of broadcasts ([#776](https://github.com/pybamm-team/PyBaMM/pull/776)) @@ -60,10 +69,13 @@ - 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)) +- Updated installation instructions for Mac OS ([#680](https://github.com/pybamm-team/PyBaMM/pull/680)) - Improved the way `ProcessedVariable` objects are created in higher dimensions ([#581](https://github.com/pybamm-team/PyBaMM/pull/581)) ## Breaking changes +- Model events are now represented as a list of `pybamm.Event` ([#759](https://github.com/pybamm-team/PyBaMM/issues/759) +- Removed `ParameterValues.update_model`, whose functionality is now replaced by `InputParameter` ([#801](https://github.com/pybamm-team/PyBaMM/pull/801)) - Removed `Outer` and `Kron` nodes as no longer used ([#777](https://github.com/pybamm-team/PyBaMM/pull/777)) - Moved `results` to separate repositories ([#761](https://github.com/pybamm-team/PyBaMM/pull/761)) - The parameters "Bruggeman coefficient" must now be specified separately as "Bruggeman coefficient (electrolyte)" and "Bruggeman coefficient (electrode)" diff --git a/CMakeLists.txt b/CMakeLists.txt index bfa77d4b9b..58a16ccaeb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,17 +8,18 @@ add_subdirectory(third-party/pybind11) pybind11_add_module(idaklu pybamm/solvers/c_solvers/idaklu.cpp) # Sundials -set(SUNDIALS_INCLUDE "sundials4/include") +set(SUNDIALS_INCLUDE "sundials/include") TARGET_INCLUDE_DIRECTORIES(idaklu PRIVATE ${SUNDIALS_INCLUDE}) -find_library(SUNMATSPARSE sundials_sunmatrixsparse PATHS "sundials4/lib" NO_DEFAULT_PATH) -find_library(IDA sundials_ida PATHS "sundials4/lib" NO_DEFAULT_PATH) -find_library(NVECTOR sundials_nvecserial PATHS "sundials4/lib" NO_DEFAULT_PATH) -find_library(SUNKLU sundials_sunlinsolklu PATHS "sundials4/lib" NO_DEFAULT_PATH) +find_library(SUNMATSPARSE sundials_sunmatrixsparse PATHS "sundials/lib" NO_DEFAULT_PATH) +find_library(IDA sundials_ida PATHS "sundials/lib" NO_DEFAULT_PATH) +find_library(NVECTOR sundials_nvecserial PATHS "sundials/lib" NO_DEFAULT_PATH) +find_library(SUNKLU sundials_sunlinsolklu PATHS "sundials/lib" NO_DEFAULT_PATH) TARGET_LINK_LIBRARIES(idaklu PRIVATE ${SUNMATSPARSE} ${IDA} ${NVECTOR} ${SUNKLU}) # link suitesparse set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}) +set(SuiteSparse_ROOT SuiteSparse-5.6.0) find_package(SuiteSparse OPTIONAL_COMPONENTS KLU AMD COLAMD BTF) include_directories(${SuiteSparse_INCLUDE_DIRS}) target_link_libraries(idaklu PRIVATE ${SuiteSparse_LIBRARIES}) diff --git a/INSTALL-LINUX-MAC.md b/INSTALL-LINUX-MAC.md new file mode 100644 index 0000000000..f4bdae732c --- /dev/null +++ b/INSTALL-LINUX-MAC.md @@ -0,0 +1,261 @@ +## Prerequisites + +To use and/or contribute to PyBaMM, you must have Python 3.6 or 3.7 installed (note that 3.8 is not yet supported). + +To install Python 3 on Debian-based distribution (Debian, Ubuntu, Linux mint), open a terminal and run +```bash +sudo apt update +sudo apt install python3 +``` +On Fedora or CentOS, you can use DNF or Yum. For example +```bash +sudo dnf install python3 +``` +On Mac OS distributions, you can use `homebrew`. +First [install `brew`](https://docs.python-guide.org/starting/install3/osx/): + +```bash +ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" +``` + +then follow instructions in link on adding brew to path, and run + +```bash +brew install python3 +``` + +## Install PyBaMM + +### User install +We recommend to install PyBaMM within a virtual environment, in order not +to alter any distribution python files. +To create a virtual environment `env` within your current directory type: + +```bash +python3 -m venv env +``` +You can then "activate" the environment using: + +```bash +source env/bin/activate +``` +Now all the calls to pip described below will install PyBaMM and its dependencies into +the environment `env`. When you are ready to exit the environment and go back to your +original system, just type: + +```bash +deactivate +``` + +PyBaMM can be installed via pip: +```bash +pip install pybamm +``` + +PyBaMM's dependencies (such as `numpy`, `scipy`, etc) will be installed automatically when you install PyBaMM using `pip`. + +For an introduction to virtual environments, see (https://realpython.com/python-virtual-environments-a-primer/). + +### Developer install + +If you wish to contribute to PyBaMM, you should get the latest version from the GitHub repository. +To do so, you must have Git and graphviz installed. For instance run + +```bash +sudo apt install git graphviz +``` + +on Debian-based distributions, or + +```bash +brew install git graphviz +``` + +on Mac OS. + +To install PyBaMM, the first step is to get the code by cloning this repository + +```bash +git clone https://github.com/pybamm-team/PyBaMM.git +cd PyBaMM +``` +Then, to install PyBaMM as a [developer](CONTRIBUTING.md), type + +```bash +pip install -e .[dev,docs] +``` + +To check whether PyBaMM has installed properly, you can run the tests: + +```bash +python3 run-tests.py --unit +``` + +Before you start contributing to PyBaMM, please read the [contributing guidelines](CONTRIBUTING.md). + +## Uninstall PyBaMM +PyBaMM can be uninstalled by running +```bash +pip uninstall pybamm +``` +in your virtual environment. + +## Optional dependencies +The following instructions assume that you downloaded the PyBaMM source code and that all +commands are run from the PyBaMM root directory (`PyBaMM/`). +This can be done using `git`, running + +```bash +git clone https://github.com/pybamm-team/PyBaMM.git +cd PyBaMM +``` +Alternatively, you can download the source code archive from [the PyBaMM GitHub repo](https://github.com/pybamm-team/PyBaMM.git) and extract it to the location of your choice. + +Ideally you should have the python package `wget` installed. +This allows for the automatic download of some of the dependencies that are part of the installation process. +You can install it using (within your virtual environment) +```bash +pip install wget +``` + +### [scikits.odes](https://github.com/bmcage/odes) +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). + +Before installing scikits.odes, you need to have installed: + +- Python 3 header files (`python3-dev` on Debian/Ubuntu-based distributions) +- C compiler (e.g. `gcc`) +- Fortran compiler (e.g. `gfortran`) +- BLAS/LAPACK install (OpenBLAS is recommended by the scikits.odes developers) +- CMake (for building Sundials) + +You can install these on Ubuntu or Debian using APT: + +```bash +sudo apt update +sudo apt install python3-dev gfortran gcc cmake libopenblas-dev +``` +To install scikits.odes, simply run +```bash +python setup.py install_odes +``` +This commands will first download and build the SUNDIALS library, required to install and use `scikits.odes`. +This will download approximately 16MB of data and should only take a few minutes to compile. +Alternatively, you can specify a directory containing the source code of the Sundials library +```bash +python setup.py install_odes --sundials-src= +``` +By default, sundials is installed in a `sundials` directory located at the root of the PyBaMM package. +You can provide another location by using the `--sundials-inst=` option. + +If you are installing `scikits.odes` within a virtual environment, the `activate` script will be automatically +updated to add the sundials installation directory to your `LD_LIBRARY_PATH`. +This is required in order to use `scikits.odes`. +As a consequence, after installation you should restart your virtual environment. + +If you wish to install `scikits.odes` outside of a virtual environment, your `.bashrc` will be modified instead. +After installation you should therefore run +```bash +source ~/.bashrc +``` +Please see the [scikits.odes +documentation](https://scikits-odes.readthedocs.io/en/latest/installation.html) for more +detailed installation instructions. + +Finally, you can check your install by running +```bash +python -c "import pybamm; print(pybamm.have_scikits_odes()) +``` +### Sundials with KLU sparse solver +If you wish so simulate large systems such as the 2+1D models, we recommend employing a +sparse solver. +PyBaMM currently offers a direct interface to the sparse KLU solver within Sundials. + +#### Prerequisites +The requirements are the same as for the installation of `scikits.odes` (see previous section). +Additionally, the [pybind11 GitHub repository](https://github.com/pybind/pybind11.git) should be located in `PyBaMM/third-party/`. +First create a directory `third-party` and clone the repository: +```bash +mkdir third-party +cd third-party +git clone https://github.com/pybind/pybind11.git +cd .. +``` +If you don't have `git` installed, you can download the code source manually from (https://github.com/pybind/pybind11). + +#### Install the KLU solver +The KLU solver is can be installed _via_ the following command: +```bash +python setup.py install_klu +``` +The previous command will download and install both the [SuiteSparse](http://faculty.cse.tamu.edu/davis/suitesparse.html) and [SUNDIALS](https://computing.llnl.gov/projects/sundials) libraries. +This will download approximately 70MB of data and the compilation should only take a couple of minutes. +If the source for a library is already present on your system, you can specify its location using options `--suitesparse-src` or `--sundials-src`. +Example: +```bash +python setup.py install_klu --suitesparse-src= +``` +This will not download the SuiteSparse library and compile the source code located in `path/to/suitesparse/source`. +The sundials library will be downloaded. + +Finally, you can check your install by running +```bash +python -c "import pybamm; print(pybamm.have_idaklu()) +``` + +### Install everything +It is possible to install both `scikits.odes` and the KLU solver using the command +```bash +python setup.py install_all +``` +Note that options `--sundials-src`, `--sundials-inst` and `suitesparse-src` are still usable +here. + +Finally, you can check your install by running +```bash +python -c "import pybamm; print(pybamm.have_scikits_odes()) +``` +and + +```bash +python -c "import pybamm; print(pybamm.have_idaklu()) +``` + +## Troubleshooting + +**Problem:** I've made edits to source files in PyBaMM, but these are not being used +when I run my Python script. + +**Solution:** Make sure you have installed PyBaMM using the `-e` flag, i.e. `pip install +-e .`. This sets the installed location of the source files to your current directory. + +**Problem:** When running `python run-tests.py --quick`, gives error `FileNotFoundError: +[Errno 2] No such file or directory: 'flake8': 'flake8`. + +**Solution:** make sure you have included the `[dev,docs]` flags when you pip installed +PyBaMM, i.e. `pip install -e .[dev,docs]` + +**Problem:** Errors when solving model `ValueError: Integrator name ida does not +exsist`, or `ValueError: Integrator name cvode does not exsist`. + +**Solution:** This could mean that you have not installed `scikits.odes` correctly, +check the instructions given above and make sure each command was successful. + +One possibility is that you have not set your `LD_LIBRARY_PATH` to point to the sundials +library, type `echo $LD_LIBRARY_PATH` and make sure one of the directories printed out +corresponds to where the sundials libraries are located. + +Another common reason is that you forget to install a BLAS library such as OpenBLAS +before installing sundials. Check the cmake output when you configured Sundials, it +might say: + +``` +-- A library with BLAS API not found. Please specify library location. +-- LAPACK requires BLAS +``` + +If this is the case, on a Debian or Ubuntu system you can install OpenBLAS using `sudo +apt-get install libopenblas-dev` and then re-install sundials using the instructions +above. diff --git a/INSTALL-LINUX.md b/INSTALL-LINUX.md deleted file mode 100644 index 0e6629712d..0000000000 --- a/INSTALL-LINUX.md +++ /dev/null @@ -1,268 +0,0 @@ -## Prerequisites - -You'll need the following requirements: - -- Python 3.6+ -- Git (`git` package on Ubuntu distributions) -- Python libraries: `venv` (`python3-venv` package on Ubuntu distributions) - -You can get these on a Debian based distribution using `apt-get` - -```bash -sudo apt-get install python3 git-core python3-venv -``` - -## Install PyBaMM - -The first step is to get the code by cloning this repository - -```bash -git clone https://github.com/pybamm-team/PyBaMM.git -cd PyBaMM -``` - -The safest way to install PyBaMM is to do so within a virtual environment ([introduction -to virtual environments](https://realpython.com/python-virtual-environments-a-primer/)). -To create a virtual environment `env` within your current directory type: - -```bash -python3 -m venv env -``` - -You can then "activate" the environment using: - -```bash -source env/bin/activate -``` - -Now all the calls to pip described below will install PyBaMM and its dependencies into -the environment `env`. When you are ready to exit the environment and go back to your -original system, just type: - -```bash -deactivate -``` - -PyBaMM has the following python libraries as dependencies: `numpy`, `scipy`, `pandas`, -`matplotlib`. These will be installed automatically when you install PyBaMM using `pip`, -following the instructions below. First, make sure you have activated your virtual -environment as above, and that you have the latest version of pip installed: - -```bash -pip install --upgrade pip -``` - -Then navigate to the path where you downloaded PyBaMM to (you will already be in the -correct location if you followed the instructions above), and install both PyBaMM and -its dependencies by typing: - -```bash -pip install . -``` - -Or, if you want to install PyBaMM as a [developer](CONTRIBUTING.md), use - -```bash -pip install -e .[dev,docs] -``` - -To check whether PyBaMM has installed properly, you can run the tests: - -```bash -python3 run-tests.py --unit -``` - -To uninstall PyBaMM, type - -```bash -pip uninstall pybamm -``` - -## Optional dependencies - -### [scikits.odes](https://github.com/bmcage/odes) - -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). -The Sundials DAE solver is required to solve the DFN battery model in PyBaMM. - -Before installing scikits.odes, you need to have installed: - -- Python header files (`python-dev/python3-dev` on Debian/Ubuntu-based distributions) -- C compiler -- Fortran compiler (e.g. gfortran) -- BLAS/LAPACK install (OpenBLAS is recommended by the scikits.odes developers) -- CMake (for building Sundials) -- Sundials 5.0.0 - -You can install these on Ubuntu or Debian using apt-get: - -```bash -sudo apt-get install python3-dev gfortran gcc cmake libopenblas-dev -``` - -To install Sundials 5.0.0, on the command-line type: - -```bash -INSTALL_DIR=`pwd`/sundials -wget https://computation.llnl.gov/projects/sundials/download/sundials-5.0.0.tar.gz -tar -xvf sundials-5.0.0.tar.gz -mkdir build-sundials-5.0.0 -cd build-sundials-5.0.0/ -cmake -DLAPACK_ENABLE=ON -DSUNDIALS_INDEX_TYPE=int32_t -DBUILD_ARKODE:BOOL=OFF -DEXAMPLES_ENABLE:BOOL=OFF -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR ../sundials-5.0.0/ -make install -rm -r ../sundials-5.0.0 -``` - -Then install [scikits.odes](https://github.com/bmcage/odes), letting it know the sundials install location: - -```bash -SUNDIALS_INST=$INSTALL_DIR pip install scikits.odes -``` - -After this, you will need to set your `LD_LIBRARY_PATH` to point to the sundials -library: - -```bash -export LD_LIBRARY_PATH=$INSTALL_DIR/lib:$LD_LIBRARY_PATH -``` - -You may wish to put this last line in your `.bashrc` or virtualenv `activate` script, -which will save you needing to set your `LD_LIBRARY_PATH` every time you log in. For -example, to add this line to your `.bashrc` you can type: - -```bash -echo "export LD_LIBRARY_PATH=$INSTALL_DIR/lib:\$LD_LIBRARY_PATH" >> ~/.bashrc -``` - -Please see the [scikits.odes -documentation](https://scikits-odes.readthedocs.io/en/latest/installation.html) for more -detailed installation instructions. - -### Sundials with KLU sparse solver -If you wish so simulate large systems such as the 2+1D models, we recommend employing a -sparse solver. PyBaMM currently offers a direct interface to the sparse KLU solver within Sundials. -If you are on a linux based distribution, a bash script has been provided which should -install everything for you correctly. Please note you will require the python header files, openblas, -a c compiler (e.g. gcc), cmake, and suitesparse all of which you should be able to install on ubuntu using -```bash -apt install python3-dev libopenblas-dev cmake gcc libsuitesparse-dev -``` -You will likely need to prepend `sudo` to the above command. - -To install sundials with KLU, from within the main PyBaMM directory type -```bash -./scripts/install_sundials_4.1.0.sh -``` -Note that this script has only been tested on Ubuntu 18.04.3 LTS. If this script does not work for you, you can try following the step-by-step instructions below: - -#### Download and build Sundials 4.1.0 -The KLU solver is interfaced using an updated version of Sundials so even if you have installed Sundials for use with Scikits.odes, you still need to install sundials here. If you want more information on the sundials installation please refer to the the ida_guide.pdf available at on the [sundials site](https://computing.llnl.gov/projects/sundials/sundials-software) - -First, download Sundials 4.1.0 using -```bash -wget https://computing.llnl.gov/projects/sundials/download/sundials-4.1.0.tar.gz -O sundials-4.1.0.tar.gz -tar -xvf sundials-4.1.0.tar.gz -rm sundials-4.1.0.tar.gz -``` -The cmake instructions provided with Sundials have trouble linking the required libraries related to the KLU solver, therefore we have provided a modified `CMakeLists.txt` file which fixes this. Copy this across into the sundials-4.1.0 folder, overwriting the old file, using -``` -cp scripts/replace-cmake/CMakeLists.txt sundials-4.1.0/CMakeLists.txt -``` -Now create a directory to build sundials in and set the install directory for sundials: -``` -mkdir build-sundials-4.1.0 -INSTALL_DIR=`pwd`/sundials4 -``` -Now enter the build directory, use cmake to generate the appropriate make files, and then build sundials and install sundials into the install directory using make: -``` -cd build-sundials-4.1.0 -cmake -DBLAS_ENABLE=ON\ - -DLAPACK_ENABLE=ON\ - -DSUNDIALS_INDEX_SIZE=32\ - -DBUILD_ARKODE=OFF\ - -DBUILD_CVODE=OFF\ - -DBUILD_CVODES=OFF\ - -DBUILD_IDAS=OFF\ - -DBUILD_KINSOL=OFF\ - -DEXAMPLES_ENABLE:BOOL=OFF\ - -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR\ - -DKLU_ENABLE=ON\ - ../sundials-4.1.0 -make install -``` -Now return to your PyBaMM home directory and remove the build-sundials-4.1.0 folder and the download folder: -``` -cd .. -rm -rf build-sundials-4.1.0 -rm -rf sundials-4.1.0 -``` - -#### Install pybind11 -To interface with Sundials which is written in C, we require pybind11. Clone the pybind11 repository whilst within a folder the third-party folder: -``` -mkdir third-party -cd third-party -git clone https://github.com/pybind/pybind11.git -cd .. -``` -You will also require pybind11 to be pip installed so from within your virtual enviroment (if you are using one) type: -``` -pip install pybind11 -``` - -#### Build the KLU wrapper -We now have all the tools to build a shared library to interface to the KLU solver. Within your PyBaMM home directory build the required Makefile using -``` -cmake . -``` -This will automatically find the headers for the latest version of python installed on your machine. If you are using an older version (e.g python3.6) within your virtual environment, then you instead can use `cmake -DPYBIND11_PYTHON_VERSION=3.6 .`. - -You can now simply run make to build the library (you can just run this command if you make some changes to klu.cpp) -``` -make -``` -To clean up you directory you can now remove the automatically generated cmake files: -``` -rm -rf CMakeFiles -rm CMakeCache.txt -rm cmake_install.cmake -``` - -## Troubleshooting - -**Problem:** I've made edits to source files in PyBaMM, but these are not being used -when I run my Python script. - -**Solution:** Make sure you have installed PyBaMM using the `-e` flag, i.e. `pip install --e .`. This sets the installed location of the source files to your current directory. - -**Problem:** When running `python run-tests.py --quick`, gives error `FileNotFoundError: -[Errno 2] No such file or directory: 'flake8': 'flake8`. - -**Solution:** make sure you have included the `[dev,docs]` flags when you pip installed -PyBaMM, i.e. `pip install -e .[dev,docs]` - -**Problem:** Errors when solving model `ValueError: Integrator name ida does not -exsist`, or `ValueError: Integrator name cvode does not exsist`. - -**Solution:** This could mean that you have not installed `scikits.odes` correctly, -check the instrutions given above and make sure each command was successful. - -One possibility is that you have not set your `LD_LIBRARY_PATH` to point to the sundials -library, type `echo $LD_LIBRARY_PATH` and make sure one of the directories printed out -corresponds to where the sundials libraries are located. - -Another common reason is that you forget to install a BLAS library such as OpenBLAS -before installing sundials. Check the cmake output when you configured Sundials, it -might say: - -``` --- A library with BLAS API not found. Please specify library location. --- LAPACK requires BLAS -``` - -If this is the case, on a Debian or Ubuntu system you can install OpenBLAS using `sudo -apt-get install libopenblas-dev` and then re-install sundials using the instructions -above. diff --git a/INSTALL-WINDOWS.md b/INSTALL-WINDOWS.md index 76a87100f1..608dfd63f8 100644 --- a/INSTALL-WINDOWS.md +++ b/INSTALL-WINDOWS.md @@ -18,7 +18,7 @@ To download the PyBaMM source code, you first need to install git, which you can typing ```bash -$ sudo apt install git-core +sudo apt install git-core ``` For easier integration with WSL, we recommend that you install PyBaMM in your *Windows* @@ -31,21 +31,21 @@ $ 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 +git clone https://github.com/pybamm-team/PyBaMM.git ``` This will create a new directly called `PyBaMM`, you can move to this directory in bash using the `cd` command: ```bash -$ cd PyBaMM +cd PyBaMM ``` If you are unfamiliar with the linux command line, you might find it useful to work through this [tutorial](https://tutorials.ubuntu.com/tutorial/command-line-for-beginners) provided by Ubuntu. Now head over and follow the installation instructions for PyBaMM for linux -[here](INSTALL-LINUX.md). +[here](INSTALL-LINUX-MAC.md). ## Use Visual Studio Code to run PyBaMM diff --git a/README.md b/README.md index 789f4eae29..e224145f59 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,11 @@ For further examples, see the list of repositories that use PyBaMM [here](https: ### Linux -For instructions on installing PyBaMM on Debian-based distributions, please see [here](INSTALL-LINUX.md) +For instructions on installing PyBaMM on Debian-based distributions, please see [here](INSTALL-LINUX-MAC.md) + +### Mac OS + +For instructions on installing PyBaMM on Mac OS distributions, please see [here](INSTALL-LINUX-MAC.md) ### Windows diff --git a/docs/index.rst b/docs/index.rst index d991331d6c..7b59f96c41 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,6 +30,7 @@ Contents source/meshes/index source/spatial_methods/index source/solvers/index + source/experiments/index source/processed_variable source/util source/simulation diff --git a/docs/source/experiments/experiment.rst b/docs/source/experiments/experiment.rst new file mode 100644 index 0000000000..52db069f28 --- /dev/null +++ b/docs/source/experiments/experiment.rst @@ -0,0 +1,5 @@ +Base Experiment Class +===================== + +.. autoclass:: pybamm.Experiment + :members: diff --git a/docs/source/experiments/index.rst b/docs/source/experiments/index.rst new file mode 100644 index 0000000000..05dafa4867 --- /dev/null +++ b/docs/source/experiments/index.rst @@ -0,0 +1,8 @@ +Experiments +=========== + +Classes to help set operating conditions for some standard battery modelling experiments + +.. toctree:: + + experiment \ No newline at end of file diff --git a/docs/source/models/base_models/event.rst b/docs/source/models/base_models/event.rst new file mode 100644 index 0000000000..856fb2b68f --- /dev/null +++ b/docs/source/models/base_models/event.rst @@ -0,0 +1,10 @@ +Event +===== + +.. autoclass:: pybamm.Event + :members: + +.. autoclass:: pybamm.EventType + :members: + + diff --git a/docs/source/models/base_models/index.rst b/docs/source/models/base_models/index.rst index db8dd10825..1be0692c93 100644 --- a/docs/source/models/base_models/index.rst +++ b/docs/source/models/base_models/index.rst @@ -6,3 +6,4 @@ Base Models base_model base_battery_model + event diff --git a/docs/source/models/submodels/particle/fickian/base_fickian_particle.rst b/docs/source/models/submodels/particle/fickian/base_fickian_particle.rst deleted file mode 100644 index 3330e0f4ac..0000000000 --- a/docs/source/models/submodels/particle/fickian/base_fickian_particle.rst +++ /dev/null @@ -1,7 +0,0 @@ -Base Model -========== - -.. autoclass:: pybamm.particle.fickian.BaseModel - :members: - - diff --git a/docs/source/models/submodels/particle/fickian/index.rst b/docs/source/models/submodels/particle/fickian/index.rst index 189ec2d13b..a6f8c36ceb 100644 --- a/docs/source/models/submodels/particle/fickian/index.rst +++ b/docs/source/models/submodels/particle/fickian/index.rst @@ -3,7 +3,6 @@ Fickian .. toctree:: - base_fickian_particle fickian_many_particles fickian_single_particle diff --git a/docs/source/solvers/solution.rst b/docs/source/solvers/solution.rst index 3f6dc3040d..25a9977ec9 100644 --- a/docs/source/solvers/solution.rst +++ b/docs/source/solvers/solution.rst @@ -1,5 +1,8 @@ Solution ======== +.. autoclass:: pybamm._BaseSolution + :members: + .. autoclass:: pybamm.Solution :members: diff --git a/examples/notebooks/solvers/dae-solver.ipynb b/examples/notebooks/solvers/dae-solver.ipynb index c45868bb78..7ec0606b39 100644 --- a/examples/notebooks/solvers/dae-solver.ipynb +++ b/examples/notebooks/solvers/dae-solver.ipynb @@ -76,7 +76,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA6AAAAEYCAYAAABCw5uAAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nOzdd3RVVfrG8e+bHlqAJCCQUEV6aBELojAooIhYEMVekLEN6lhH/TEMDsrYRbEgYh8VcVRQVJAiiCIE6dWAlABSAoSWhJT9+yMBE0gggSTn3uT5rJWV3H3KfZI1w+t7zzl7m3MOERERERERkdIW4HUAERERERERqRjUgIqIiIiIiEiZUAMqIiIiIiIiZUINqIiIiIiIiJQJNaAiIiIiIiJSJoK8euOoqCjXsGFDr95eRETkmObPn7/DORftdY7CqI6KiIgvK6yOetaANmzYkISEBK/eXkRE5JjMbL3XGY5FdVRERHxZYXVUt+CKiIiIiIhImVADKiIiIiIiImVCDaiIiIiIiIiUCc+eARURkZKRkZFBUlISaWlpXkfxS2FhYcTExBAcHOx1FBERKQGqi2WruHVUDaiIiJ9LSkqiatWqNGzYEDPzOo5fcc6RnJxMUlISjRo18jqOiIiUANXFsnMidVS34IqI+Lm0tDQiIyNVZE+AmREZGalPyUVEyhHVxbJzInX0uA2omY01s21mtrSQ7WZmI80s0cwWm1mHYmQ+OYvHwQutYWj1nO+Lx5XZW4uI+BIV2RNXUn87M4s1s+lmttzMlpnZPQXsU2jNNLMbzey33K8bSyTU8aiOikg5pbpYdor7ty7KFdB3gF7H2H4h0DT3axDwWrESnKjF42DiYEjZCLic7xMHq3iKiIhXMoH7nXMtgTOBu8ys5RH7FFgzzawm8E/gDKAT8E8zq1GqaVVHRUTEA8dtQJ1zM4Gdx9ilL/CeyzEHqG5mdUoqYKGmDoOM1PxjGak54yIi4pf69evH2rVrjxqfMmUKHTt2pE2bNnTs2JFp06YBOc+eAAwdOjTf6yeffPLwsQcPHuTcc88lMzOzVLM757Y4537N/XkvsAKod8RuhdXMnsAU59xO59wuYArH/vD35KmOioiUmo0bN9KtWzdatmxJq1ateOmllw5ve+edd1i3bt3hmlWSvvjiC4YNO/rf8QMHDtC7d2+aN29Oq1ateOSRRw5vc84xY8YMZsyYcTjTF198wfLlyw/v88ADDxyuvSerJJ4BrQdszPM6iaMLLgBmNsjMEswsYfv27Sf3rilJxRsXERGftmzZMrKysmjcuPFR26Kiopg4cSJLlizh3Xff5frrrwdg8uTJPPbYYxw4cIAxY8bw4osvAvkb0JCQELp3784nn3xSNr8IYGYNgfbAL0dsKqxmFqmWqo6KiPiHoKAgnnvuOZYvX86cOXMYNWoUs2fPZuDAgWzcuJEff/yR22+/vcTf9+mnn+bOO+8scNsDDzzAypUrWbBgAbNnz+abb74hNTWVm266iWXLlrF06VJuuukmUlNTj2pA//a3vzFixIgSyVimkxA550Y75+Kdc/HR0dEnd7KImOKNi4hIqfnggw/o1KkT7dq1469//Svr16+nadOm7Nixg+zsbLp06cLkyZNZt24dzZs359prr6VFixb069ePAwcOAPDhhx/St2/fAs/fvn176tatC0CrVq1ITU0lPT2dnj170rNnT1566SWSk5O57777eOSRR0hNTaVdu3Zce+21AFx66aV8+OGHZfK3MLMqwGfAvc65PSV5btVRERH/UKdOHTp0yHnMv2rVqrRo0YIDBw4wfPhw3nrrLT7++GNeey3nycU1a9bQq1cvOnbsSJcuXVi5ciUAffv25b333gPgjTfeOFzTunbtyj333EO7du1o3bo1c+fOBWD16tWEhoYSFRV1VJ5KlSrRrVs3IOeD2Q4dOpCUlER4eDivvfYaY8eO5e233+a1115jwYIFTJgwgQcffJB27dqxZs0aGjRoQHJyMn/88cdJ/21KYhmWTUBsntcxuWOlq/uQnGdV8tw+lBkYRlD3IaX+1iIivupfE5exfHOJ9jy0rFuNf/ZpVej2FStW8MknnzB79myCg4O58847+eGHH3j44Ye544476NSpEy1btqRHjx6sW7eOVatW8dZbb9G5c2duueUWXn31VR544AFmz57NgAEDjpvns88+o0OHDoSGhjJlyhRmzJjB4MGDiYyM5KWXXmLEiBG88sorLFy48PAxrVu3Zt68eSXy9zgWMwsmp/n80Dn3vwJ2KaxmbgK6HjE+o3RS5iqgjmYHhROgOioi5YgXdfFI69atY8GCBTRr1ozHH3+cW265hUaNGnHXXXfx2muvMWjQIF5//XWaNm3KL7/8wp133sm0adMYPXo0nTt3plGjRjz33HPMmTPn8DkPHDjAwoULmTlzJrfccgtLly5l9uzZh5veY9m9ezcTJ07knnvuITU1lbvuuoubb74ZgLvuuotXX32VSy65hIsvvph+/fodPq5Dhw7Mnj2bK664ohh/raOVRAM6AbjbzD4mZ/KEFOfclhI477HF9c/5PnUYLiWJrRbF+2E38ECbK9GcVyIiZWfq1KnMnz+f008/HYDU1FRq1arF0KFD+fTTT3n99dfzNYOxsbF07twZgOuuu46RI0fywAMPsGXLFo53VW/ZsmU8/PDDTJ48GYDzzz+fCy64gKFDhzJw4MBCn6cJDAwkJCSEvXv3UrVq1ZL4tY9iOdMAvgWscM49X8huBdZMM/sOeDLPxEM9gH+UStBDjqijW1wk30f/lRsOjYuIyEnbt28fV1xxBS+++CL169fnzTff5J133qFLly5cd9117Nu3j59++okrr7zy8DHp6ekA1K5dm2HDhtGtWzc+//xzataseXifQx/YnnvuuezZs4fdu3cXqY5mZmYyYMAABg8efPiRl7Fjx/LDDz8AOQ1oYbPa1qpVi82bN5/4HyPXcRtQM/uInE9lo8wsiZxZ+oIBnHOvA5OAi4BE4ABw80mnKqq4/hDXHwOm/bKBUZ8vofPaZM5ucvRlZxGRiqA4n8iWFOccN954I0899VS+8QMHDpCUlPM84b59+w43fkcWtkOvw8PDD68j9vnnn/Ovf/0LgDFjxhAfH09SUhKXXXYZ7733Hk2aNMl37KFJiI41FXx6ejphYWEn86seT2fgemCJmR3quB8F6sOxa6ZzbqeZPQEcukw7zDl3rAkAS0aeOvr218sZO3sdf9l1gJgalUr9rUVEyoIXdfGQjIwMrrjiCq699louv/zyw+M33XTT4Z+zs7OpXr16vg9q81qyZAmRkZFHNX4F1dLw8HBSUlIAyMrKomPHjgBccsklhycmGjRoEE2bNuXee+/Nd2zXrl2P+/ukpaURHh5+3P2Opyiz4A5wztVxzgU752Kcc285517PLaTkzuR3l3OuiXOujXMu4aRTnYDLO9QjqkoIb848evZEEREpPd27d2f8+PFs27YNgJ07d7J+/Xoefvhhrr32WoYNG8Ztt912eP8NGzbw888/A/Df//6Xc845B4AWLVqQmJgIwGWXXcbChQtZuHAh8fHx7N69m969ezNixIjDV0+PJTg4mIyMjMOvk5OTiYqKIjg4uMR+7yM55350zplzLs451y73a1JRa6Zzbqxz7tTcr7dLLWghbu7cKKcRnb2urN9aRKTccc5x66230qJFC/7+978Xul+1atVo1KgRn3766eHjFi1aBMDcuXP55ptvWLBgAc8++yy///774eMOTaz3448/EhERQURERL46GhgYeLiOHmo+H3/8cVJSUg5P2HcsVatWZe/evfnGVq9eTevWrYvxVyhYmU5CVJrCggO54ayGTF+1ndVb9x7/ABERKREtW7bk3//+Nz169CAuLo4LLriAdevWMW/evMNNaEhICG+/ndNTNWvWjFGjRtGiRQt27drFHXfcAUDv3r2ZMWNGge/xyiuvkJiYyLBhw2jXrh3t2rU73PAWZNCgQcTFxR2esGH69On07t27ZH/xcqZu9XD6tK3Lx3M3kJKacfwDRESkULNnz+b9999n2rRph+vWpEmTCtz3ww8/5K233qJt27a0atWKL7/8kvT0dG677TbGjh1L3bp1ee6557jlllsOP2oSFhZG+/btuf3223nrrbeAnNtxFyxYUODjKElJSQwfPpzly5fToUMH2rVrx5gxYwrNf/XVV/PMM8/Qvn171qxZQ0ZGBomJicTHx5/038ZKY/2ZooiPj3cJCSV7sXTX/oOcNWIqfeLq8syVbUv03CIivmrFihW0aNHC6xhFsm7dOi6++GKWLl161LbU1FS6devG7NmzCQwMLNH3vfzyyxkxYgSnnXZagdsL+hua2Xzn3MlX2lJSGnV0+eY9XDRyFg/3as4dXZuU6LlFRMqKP9XFE9G1a1eeffbZApvBe+65hz59+nD++eeX6Ht+/vnn/PrrrzzxxBMFbi9OHS03V0ABalQOoX98LF8s3MTWPWlexxERkWIIDw/nX//6F5s2lexE6gcPHuTSSy8ttPmUP7WsW40uTaN4e/bvpGdmeR1HRESK6dFHHz28vFlJyszM5P777y+Rc5WrBhRg4DmNycp2eoZFRMQHNWzYsMCrn4f07NmT+vXrl+h7hoSEcMMNN5ToOcuz27o0ZtvedL5cePIzHYqISMmbMWNGobfC1q5dm0suuaTE3/PKK6+kevXqJXKucteA1o+sxIWt6/DhL+vZl57pdRwRERG/0qVpFM1PqcqbM9eSne3NYzoiIlJ+lbsG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czu9f7cCT5FurSi0YZfBOdeYUfcvgdPznbcARlLDZQw4ZSIg22A4uZYxCOs/L2a2kPRn52LnXGPv8Iz1vxe4l8Kf/pcX59xB59wh7/kfgWIzm0qItoFnqO+Ar9vAzIpJh+adzrl7skwS+u/BBFGO4vtvuHIU37fBcEL/+6EcDQzlaD6cjxfEDvcgfYR2K+nTOXovPD5pwDRfo3/nCb/znp9E/84TtlL4zhPyqf+DpC+unj9geCUQ955PBd7En4uu81mGGRnPPws8745ctPy2tyyV3vOqoNXvTXcC6YvELWjbwHv/OeS+cP9T9L9o/MWgrP88659N+tqyMwcMLwXKM56vBs73o/48lmF672eHdLC8422PvD5/ftfvjU+Rvr6lNGjbwFuXvwF+MsQ0gf8e+LTdlaNOOTrR9XvTKUf9q1856nP93njlaL71+PUhHMEKu5B0T01vATd4w24m3coJkAB+733xXgTmZsx7gzffG8AFAa3/T8Ae4BXvsdIbfiaw3vuirQeWBngbfB/Y6NX6JHBCxrxf9LbNFuDvgli/93o5cMuA+QKxDUi3pO0Cukifd78U+CrwVW+8AT/zlm89sChg63+4+n8J7M/4Dqzxhs/11v2r3ufrBj/qz3MZrs74DjxPxn8Csn3+gla/N80VpDucyZwvENuA9OlkDliX8Tm5MEzfAz8fw/0GohwNwjIoRye2fuWocnRC6/emuQLlaF6P3pYGERERERERkQkV9GtARUREREREZJLQDqiIiIiIiIgUhHZARUREREREpCC0AyoiIiIiIiIFoR1QERERERERKQjtgIqEnJlVmNlVftchIiISRspRkcLSDqhI+FUACk4REZHRUY6KFJB2QEXC7xbgODN7xcx+6HcxIiIiIaMcFSkgc875XYOIjIGZzQFWOedO9rkUERGR0FGOihSWjoCKiIiIiIhIQWgHVERERERERApCO6Ai4dcMlPtdhIiISEgpR0UKSDugIiHnnGsEnjOzDeo8QUREZGSUoyKFpU6IREREREREpCB0BFREREREREQKQjugIiIiIiIiUhDaARUREREREZGC0A6oiIiIiIiIFIR2QEVERERERKQgtAMqIiIiIiIiBaEdUBERERERESmI/weLkB7PdBETzQAAAABJRU5ErkJggg==\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA6AAAAEYCAYAAABCw5uAAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvOIA7rQAAIABJREFUeJzs3Xd4VVW6x/Hvm0ZCS+gtQXqXGsGCCIMFC2BFGDsqoiCWca6MM8Ng1BlHYUQUCwI6KhbEEcGuKAgISugd6YQioYWSnqz7RwKGkJAAydk5ye/zPHlyztrrnPMjz537+p6991rmnENERERERESkuAV4HUBERERERETKBjWgIiIiIiIi4hNqQEVERERERMQn1ICKiIiIiIiIT6gBFREREREREZ9QAyoiIiIiIiI+oQZUREREREREfEINqIiIiIiIiPiEGlARERERERHxiSCvPrh69equQYMGXn28iIjICRYtWrTXOVfD6xyFpToqIiIlSWHrqGcNaIMGDYiNjfXq40VERE5gZlu9znA6VEdFRKQkKWwd1SW4IiIiIiIi4hNqQEVERERERMQn1ICKiIiIiIiIT3h2D6iIiJyZtLQ04uLiSE5O9jqKXwoNDSUyMpLg4GCvo4iISBFRbfSds62jakBFRPxMXFwclSpVokGDBpiZ13H8inOOffv2ERcXR8OGDb2OIyIiRUS10TeKoo4WeAmumU0ysz1mtjKf42ZmY81sg5ktN7OOZ5TkTCyfAi+0gZERWb+XT/HZR4uIeCU5OZlq1aqpwJ4BM6NatWrF8g25mUWZ2Q9mttrMVpnZQ3nMybdmmtkdZvZr9s8dRR4wL6qjIlJKqDb6RlHU0cLcA/oW0OsUx68Emmb/DAJePeM0p2P5FJgxDBK2Ay7r94xhKp4iUiaowJ65YvzbpQN/cs61As4HhphZq1xz8qyZZlYV+AfQBegM/MPMqhRXUEB1VERKHdVG3zjbv3OBDahz7kdg/ymm9AXedlkWABFmVuesUhXGzBhISzpxLC0pa1xERMTHnHO7nHOLsx8fBtYA9XJNy69mXgF865zb75w7AHzLqb/8PXuqoyIi4oGiWAW3HrA9x/M4Ti64AJjZIDOLNbPY+Pj4s/vUhLjTGxcRkRLpxhtvZNOmTSeNf/vtt3Tq1Ilzzz2XTp068f333wNZ958AjBw58oTn//znP4+/NjU1lW7dupGenl7M6fNmZg2ADsDPuQ7lVzMLVUtVR0VESq7t27fTo0cPWrVqRevWrXnxxRePH3vrrbfYsmXL8ZpVlKZNm0ZMzMlfHiYmJnL11VfTokULWrduzfDhw48fc84xa9YsZs2adTzTtGnTWL169fE5jz322PHaW5R8ug2Lc268cy7aORddo0aNs3uz8MjTGxcRkRJn1apVZGRk0KhRo5OOVa9enRkzZrBixQr++9//cttttwHwzTff8Ne//pXExEQmTJjAmDFjgBMb0JCQEHr27MmHH37om39IDmZWEfgYeNg5d6go31t1VESk5AoKCmL06NGsXr2aBQsWMG7cOObNm8c999zD9u3bmTt3LoMHDy7yz33uued44IEH8jz22GOPsXbtWpYsWcK8efP48ssvSUpK4s4772TVqlWsXLmSO++8k6SkpJMa0AcffJBnn322yPMWxSq4O4CoHM8js8eKV88RWfeq5Lh8yAWFYT1HFPtHi4iUFE/OWMXqnUXa49CqbmX+0bt1gfPeffddxo4dS2pqKl26dOGJJ57g0ksvZf78+VStWpVLLrmEv//97zRr1oxevXrRqVMnFi9eTOvWrXn77bcpX748kydPpm/fvnm+f4cOHY4/bt26NUlJSaSkpHDFFVcQFhbGZZddRkxMDI8//jjDhw8nKSmJ9u3b07p1ayZPnsy1117LX/7yF2655ZYi+9sUxMyCyWo+Jzvn/pfHlPxq5g6ge67xWcWTMltedTRYdVRE/J9XtbFOnTrUqZN1J2KlSpVo2bIliYmJPPPMM3Tp0oU2bdowffp0ADZu3MiQIUOIj4+nfPnyvPHGG7Ro0YK+fftyww03cPvtt/P666/z448/MnnyZLp37067du2YPXs26enpTJo0ic6dO7N+/XrKlStH9erVT8pTvnx5evToAWR9MduxY0fi4uIICwvj1Vdf5eKLLwZgzpw5LFmyhOnTpzN79myefvppPv74Yxo3bsy+ffvYvXs3tWvXLrK/ZVGcAZ0O3J69st/5QIJzblcRvO+pte0HvcdCeBQOIy6zOl81eiJrXEREitWaNWv48MMPmTdvHkuXLiUwMJDZs2fz+OOPc//99zN69GhatWrF5ZdfDsC6det44IEHWLNmDZUrV+aVV14BYN68eXTq1KnAz/v444/p2LEj5cqV49tvv+Xrr79m2LBhVKtWjRdffJFnn32WsLAwli5dyuTJkwFo06YNCxcuLL4/Qi6WtSrDRGCNc+4/+UzLr2Z+DVxuZlWyFx+6PHus+ORRRxe3e1J1VESkCGzZsoUlS5bQvHlz/va3vzFw4EBuvvlmhgwZAsCgQYN46aWXWLRoEaNGjTp+BnP8+PHExMQwZ84cRo8ezUsvvXT8PRMTE1m6dCmvvPIKAwcOBLLqaMeOBW9CcvDgQWbMmEHPnj1JSkpiyJAh3HXXXdx1110MGTKEDh060KdPH55//nmWLl1K48aNAejYsSPz5s0r0r9NgWdAzex9sr6VrW5mcWSt0hcM4Jx7DfgCuArYACQCdxVpwlNp2w/a9sOAEW8tZNnGg/RIyyA0ONBnEUREvFSYM5XFYebMmSxatIjzzjsPgKSkJGrWrMnIkSP56KOPeO2111i6dOnx+VFRUVx00UUA3HrrrYwdO5bHHnuMXbt2UdClpKtWreLxxx/nm2++AeDSSy/lsssuY+TIkdxzzz353k8TGBhISEgIhw8fplKlSkXxzy7IRcBtwAozO/aPfwKoD6eumc65/Wb2FHCsY45xzp1qAcCikV1HXabj9hdmU35zIDOc00qSIuLXvKqNxxw5coQbbriBMWPGUL9+fd544w3eeustLr74Ym699VaOHDnCTz/9xE033XT8NSkpKQDUqlWLmJgYevTowSeffELVqlWPzxkwYAAA3bp149ChQxw8eLBQdTQ9PZ0BAwYwbNiw47e8TJo0idmzZwMwZMiQfP//fs2aNdm5c+eZ/zHyUGAD6pwbUMBxBwwpskRn6L5ujbh5/AKmLorj1vPP8TqOiEip5pzjjjvu4F//+tcJ44mJicTFZS1ic+TIkeONX+7Cdux5WFjY8b3EPvnkE5588kkAJkyYQHR0NHFxcVx33XW8/fbbx7+NPfbaY4sQnapZSklJITQ09Gz+qYXmnJsLnLJzO1XNdM5NAiYVQ7QCBQQY917ciL/8bwXzN+7jwiYnX8olIiIFS0tL44YbbuCWW27h+uuvPz5+5513Hn+cmZlJRETECV/U5rRixQqqVat2UuOXVy0NCwsjISEBgIyMjONXFfXp0+f4wkSDBg2iadOmPPzwwye8tnv37gX+e5KTkwkLCytw3unw6SJExalzw6q0i4rgjTmbyMgs+tWlRETkdz179mTq1Kns2bMHgP3797N161Yef/xxbrnlFmJiYrj33nuPz9+2bRvz588H4L333qNr164AtGzZkg0bNgBw3XXXsXTpUpYuXUp0dDQHDx7k6quv5tlnnz1+9vRUgoODSUtLO/583759VK9eneDg4CL7d5dm13WoR/WK5Xjtx5NXJBYRkYI557j77rtp2bIljz76aL7zKleuTMOGDfnoo4+Ov27ZsmUA/PLLL3z55ZcsWbKEUaNGsXnz5uOvO7aw3ty5cwkPDyc8PPyEOhoYGHi8jh5rPv/2t7+RkJBwfMG+U6lUqRKHDx8+YWz9+vW0adPmNP4KBSs1DaiZMbhbI7buS+SbVbu9jiMiUqq1atWKp59+mssvv5y2bdty2WWXsWXLFhYuXHi8CQ0JCeHNN98EoHnz5owbN46WLVty4MAB7r//fgCuvvpqZs2alednvPzyy2zYsIGYmBjat29P+/btjze8eRk0aBBt27Y9vujQDz/8wNVXX120//BSLDQ4kLsuasCP6+NZs6toF+8QESkL5s2bxzvvvMP3339/vG598cUXec6dPHkyEydOpF27drRu3ZpPP/2UlJQU7r33XiZNmkTdunUZPXo0AwcOPH6rSWhoKB06dGDw4MFMnDgRyLocd8mSJXnejhIXF8czzzzD6tWr6dixI+3bt2fChAn55u/fvz/PP/88HTp0YOPGjaSlpbFhwwaio6OL4K/zOyuOvWgKIzo62sXGxhbpe2ZkOnqOnkV4+RCmPXCh7mERkVJpzZo1tGzZ0usYhbZlyxauueYaVq5cedKxpKQkevTowbx58wgMLNr796+//nqeffZZmjVrdtKxvP6GZrbIOVe0VbYYFUcdTUhM44JnZ3JF69q8cHP7In1vEZHi5G+18XR1796dUaNG5dkMPvTQQ/Tu3ZtLL720SD/zk08+YfHixTz11FMnHTubOlpqzoACBAYYd1/ciGXbD/LL5uJfu0FERM5OWFgYTz75JDt2FO3uXampqVx77bV5Np+Sv/DywfQ/rz4zlu1k58Gkgl8gIiKee+KJJ0hMTCzy901PT+dPf/pTkb9vqWpAAW7qFEm1CiGM1z0sIiIlQoMGDfI8+3nMFVdcQf369Yv0M0NCQrj99tuL9D3LioFdG+CASXM3FzhXRER8Y9asWfleClurVi369OlT5J950003ERERUeTvW+oa0NDgQO64sAEz1+5h7W7dwyIiInI6IquUp3fbOrz3yzYOHE31Oo6IiJQypa4BBbj9gnOoWC6IcT9s9DqKiIiI37m/exMSUzN486ctXkcREZFSplQ2oBHlQ7jtgnP4bPlONsYf8TqOiIiIX2leuxJXtK7FW/M2cyg5reAXiIiIFFKpbEAB7u7akHJBAbw6S2dBRURETtfQHk05lJzOO/O3eh1FRERKkVLbgFavWI4BneszbckOtu8v+lWhRET8xvIp8EIbGBmR9Xv5FK8TiR84NzKcS5rVYOLczSSmpnsdR0SkaKk2eqbUNqAAg7o1IsCM13/UWVARKaOWT4EZwyBhO+Cyfs8YpkIrhfLgH5qw/2gq7/+y3esoIiJFR7XRU6W6Aa0THsYNnSKZsjCO3w4lex1HRMT3ZsZAWq79HNOSssbPwpYtW2jTps3x56NGjWLkyJFn9Z5S8kQ3qEqXhlUZ/+NGUtIzvI4jIlI0iqE2Dh8+nHHjxh1/PnLkSEaNGnXG71ealeoGFOD+SxqT4RxvaF9QESmLEuJOb1wklwf/0JTfDqUwdZH+b0ZESoliqI0333wzU6b8fgZ1ypQp3HzzzWf8fqVZqW9A61crT992dZn88zb2HUnxOo6IiG+FR57euEguFzWpRruoCF6dtZG0jEyv44iInL1iqI0dOnRgz5497Ny5k2XLllGlShWioqLO+P1Ks1LfgAI80KMxyekZTJq32esoIiK+1XMEBIedOBYcljV+FoKCgsjM/L0ZSU7WbQ6llZnxYI8mxB1I4tOlO72OIyJy9oqpNt50001MnTqVDz/8UGc/T6FMNKBNalbiqjZ1ePunrRxMTPU6joiI77TtB73HQngUYFm/e4/NGj8LtWrVYs+ePezbt4+UlBQ+++yzoskrJVLPljVpWacyr/ywgXSdBRURf1dMtfHmm2/mgw8+YOrUqdx0001Fk7UUCvI6gK882LMJn6/YxYQ5m3nsiuZexxER8Z22/c66qOYWHBzMiBEj6Ny5M/Xq1aNFixZF+v5SspgZw/7QhPsnL+bTpTu5oZMu4RYRP1cMtbF169YcPnyYevXqUadOnSJ979KkzDSgLWpX5uq2dXhz3mYGdm1I1QohXkcSEfFrw4YNY9iwYV7HEB+5onVtWtWpzNjvf6VP+7oEB5aJi6hERE7LihUrvI5Q4pWp6vFwz6YkpmUwXiviiohIETOzSWa2x8xW5nP8z2a2NPtnpZllmFnV7GNbzGxF9rFY3yYvnIAA45HLmrF1XyKfLN7hdRwREfFTZaoBbVqrEk82WMVtC67GjYyAF9pow1kRESkqbwG98jvonHveOdfeOdce+Asw2zm3P8eUHtnHo4s55xm7tGVNhlZfTLcveqiOiojIGSkzl+ACsHwKt8b/hwDL3ng2YTvMyL58rIivARcRKU7OOczM6xh+yTlXXO/7o5k1KOT0AcD7xRKkGNmKj3gk6WUCXfaqx6qjIlKCqDb6xtnW0TJ1BpSZMQSkJ504lpYEM2O8ySMicgZCQ0PZt29fsTVSpZlzjn379hEaGupZBjMrT9aZ0o9zDDvgGzNbZGaDTvHaQWYWa2ax8fHxxR31ZDNjCMzIteWO6qiIlACqjb5RFHW0bJ0BTYg7vXERkRIoMjKSuLg4PGlASoHQ0FAiIz1dxbU3MC/X5bddnXM7zKwm8K2ZrXXO/Zj7hc658cB4gOjoaN//V5bqqIiUUKqNvnO2dbRsNaDhkVmXC+U1LiLiJ4KDg2nYsKHXMeTM9SfX5bfOuR3Zv/eY2SdAZ+CkBtRzqqMiUkKpNvqPsnUJbs8REBx2wlCqlcsaFxERKWZmFg5cAnyaY6yCmVU69hi4HMhzJV3P5VFH0wJCVUdFRKTQylYD2rYf9B4L4VGAcSC4Fo+n3cO2etd4nUxERPycmb0PzAeam1mcmd1tZoPNbHCOadcB3zjnjuYYqwXMNbNlwC/A5865r3yX/DTkqqPxgTUZ6QZxtPn1XicTERE/UbYuwYWs4pm9Ul9KQjJfPP8D7tt1jOnfweNgIiLiz5xzAwox5y2ytmvJObYJaFc8qYpBjjq6fdsBJr/yE7XmbmZYz6YeBxMREX9Qts6A5lI7PJSBXRvy6bKdrNqZ4HUcERERv9KxfhWuaF2L12dvZN+RFK/jiIiIHyjTDSjA4EsaUzk0mOe+Wud1FBEREb/z5yuak5SWwcs/bPA6ioiI+IEy34CGhwXzQPfGzF4fz/yN+7yOIyIi4lea1KxEv+goJi/Yxvb9iV7HERGREq7MN6AAd1zYgDrhoTz71VptXisiInKaHr60GWbwwrfrvY4iIiIlnBpQIDQ4kEcubcay7Qf5etVur+OIiIj4ldrhodx5UQM+WbqDNbsOeR1HRERKMDWg2a7vWI8mNSvy3NfrSM/I9DqOiIiIX3ngkiZUKhfEc1+t9TqKiIiUYGpAswUFBvDnK5qzKf4oUxfFeR1HRETEr4SXD+b+7k34YV08P2/SmgoiIpK3QjWgZtbLzNaZ2QYzG57H8fpm9oOZLTGz5WZ2VdFHLX6Xt6pFx/oRvPDdehJT072OIyIi4lfuvLABtSqX419fak0FERHJW4ENqJkFAuOAK4FWwAAza5Vr2t+AKc65DkB/4JWiDuoLZsYTV7Xkt0MpvPHjZq/jiIiI+JWwkED+dFlzlm4/yIzlu7yOIyIiJVBhzoB2BjY45zY551KBD4C+ueY4oHL243BgZ9FF9K3oBlW5+tw6vDZ7I7sTkr2OIyIi4ldu6BRJqzqV+feXa0lOy/A6joiIlDCFaUDrAdtzPI/LHstpJHCrmcUBXwAP5vVGZjbIzGLNLDY+Pv4M4vrG471akJHpeP7rdV5HERER8SuBAcbfrmnJjoNJTJyrq4lERORERbUI0QDgLedcJHAV8I6ZnfTezrnxzrlo51x0jRo1iuiji179auW5q2sDPl4cx4q4BK/jiIiI+JULG1fnsla1eOWHDew5rKuJRETkd4VpQHcAUTmeR2aP5XQ3MAXAOTcfCAWqF0VArwzp0YRqFUJ46vPVWkhBRETkND1xVUtS0jN54dv1XkcREZESpDAN6EKgqZk1NLMQshYZmp5rzjagJ4CZtSSrAS2519gWQuXQYB65rBm/bN7P16t2ex1HRETErzSsXoHbL2jAhwu3s2bXIa/jiIhICVFgA+qcSweGAl8Da8ha7XaVmcWYWZ/saX8C7jWzZcD7wJ2uFJw27H9eFM1qVeRfX64lJV0LKYiIiJyOh3o2pXJYME/raiIREclWqHtAnXNfOOeaOecaO+eeyR4b4Zybnv14tXPuIudcO+dce+fcN8UZ2leCAgP469Wt2Lovkbd/2up1HBEREb8SXj6Yh3o2Zd6GfXy/do/XcUREpAQoqkWISq1LmtXgkmY12PD9JDL+0xpGRsALbWD5FK+jiYiIlHi3nn8OjWpUYP6013AvqI6KiJR1QV4H8Af/braWyltfJ/BQatZAwnaYMSzrcdt+3gUTEREp4YIDA3ipzQYa/vQylqI6KiJS1ukMaCHUXvgc5S31xMG0JJgZ400gEREpccxskpntMbOV+RzvbmYJZrY0+2dEjmO9zGydmW0ws+G+S+0brVePUR0VERFADWjhJMSd3riIiJRFbwG9CpgzJ3uthPbOuRgAMwsExgFXAq2AAWbWqliT+prqqIiIZFMDWhjhkac3LiIiZY5z7kdg/xm8tDOwwTm3yTmXCnwA9C3ScF5THRURkWxqQAuj5wgIDjthyAWFZY2LiIgU3gVmtszMvjSz1tlj9YDtOebEZY+dxMwGmVmsmcXGx/vRdtt51dFg1VERkbJIDWhhtO0HvcdCeBQOY4erzkd1/6yFE0RE5HQsBs5xzrUDXgKmne4bOOfGO+einXPRNWrUKPKAxSZXHY3LrM6sZn9XHRURKYO0Cm5hte0HbfthwBvTV/Hf+VtotSOBNvXCvU4mIiJ+wDl3KMfjL8zsFTOrDuwAonJMjcweK12y6yjO8dc3F7J45QFm9kqmZqVQr5OJiIgP6QzoGXjksmZUqxDC3z9dSWam8zqOiIj4ATOrbWaW/bgzWTV4H7AQaGpmDc0sBOgPTPcuafEyM0b2aU1KeibPfrHW6zgiIuJjakDPQHhYMMOvbMmSbQf5YOH2gl8gIiKlnpm9D8wHmptZnJndbWaDzWxw9pQbgZVmtgwYC/R3WdKBocDXwBpginNulRf/Bl9pWL0C93ZryP+W7OCnjXu9jiMiIj5kznlzBi86OtrFxsZ68tlFwTnHgDcWsHrnIb770yW6hEhExM+Z2SLnXLTXOQrL3+tocloGl7/wI0EBxhcPXUxocKDXkURE5CwUto7qDOgZMjOeue5cktMyeeqzNV7HERER8SuhwYE8c10bNu09yiuzNnodR0REfEQN6FloXKMiQ3o0Ycayncxat8frOCIiIn7l4qY1uLZ9XV6dtYENe454HUdERHxADehZGty9EY1rVODvn64kKTXD6zgiIiJ+5W/XtKJ8SBBPfLICr24LEhER31EDepbKBQXyz+vOZfv+JF6c+avXcURERPxK9YrleOKqFvyyeT8fxcZ5HUdERIqZGtAi0KVRNfpFRzJhzibW7j5U8AtERETkuJs6RdG5QVWe+WINe4+keB1HRESKkRrQIvKXK1tSOSyYv/xvBRnaG1RERKTQAgKMf17fhsTUdJ7+bLXXcUREpBipAS0iVSqEMOKaVizZdpA35232Oo6IiIhfaVKzEvd3b8K0pTv5fu1vXscREZFioga0CPVtX5eeLWoy6pt1bNl71Os4IiIifmVojyY0r1WJv/xvBQlJaV7HERGRYqAGtAgd2xs0ODCA//t4OZm6FFdERKTQQoICeP6mtuw9ksozn+tSXBGR0kgNaBGrHR7K369pxS+b9/POgq1exxEREfErbSMjGNStEVNi45i9Pt7rOCIiUsTUgBaDmzpF0q1ZDVZ+9Qbpo1vDyAh4oQ0sn+J1NBERkRLvoZ5NaVKzIt9/+DKZ/1EdFREpTYK8DlAamRkvtlpP6NbxBB1OzRpM2A4zhmU9btvPu3AiIiIlXGhwIG902EStWa8QcEh1VESkNNEZ0GJSZf6zhFnqiYNpSTAzxptAIiIifqTh0tGUVx0VESl11IAWl4S40xsXERGR36mOioiUSmpAi0t45OmNi4iIyO9UR0VESiU1oMWl5wgIDjthKC0gNGtcRERETi2POpoeqDoqIuLv1IAWl7b9oPdYCI/CYewLqsn/pd7N6uq9vE4mIiJS8uWqo3sCajAicxB7GvbxOpmIiJwFrYJbnNr2g7b9MCDgaCrzxvzI6g+X8unQiwgNDvQ6nYiISMmWo44e2nOYj8fOZffHK5h4RzRm5nU6ERE5AzoD6iNVKoTw7xvbsu63w4z+Zp3XcUREpIiZ2SQz22NmK/M5fouZLTezFWb2k5m1y3FsS/b4UjOL9V1q/9GkZiWGX9mC79fu4f1ftnsdR0REzpAaUB/q0bwmt55fnwlzN/PTxr1exxERkaL1FnCq+yw2A5c4584FngLG5zrewznX3jkXXUz5/N4dFzSga5PqPPXZarbsPep1HBEROQNqQH3siata0qBaBR6bsoyDiakFv0BERPyCc+5HYP8pjv/knDuQ/XQBoOVcT1NAgPH8TW0JDjQe+nApaRmZXkcSEZHTpAbUx8qHBDHm5vbsOZzC4x8vxznndSQREfG9u4Evczx3wDdmtsjMBuX3IjMbZGaxZhYbHx9f7CFLojrhYfzr+rYs236Q0d+s9zqOiIicJjWgHmgXFcHjvVrw9arfePfnbV7HERERHzKzHmQ1oI/nGO7qnOsIXAkMMbNueb3WOTfeORftnIuuUaOGD9KWTFe3rcOAzlG8NnsjP64vm424iIi/KlQDama9zGydmW0ws+H5zOlnZqvNbJWZvVe0MUufu7s25JJmNXjqs9Ws2XXI6zgiIuIDZtYWmAD0dc7tOzbunNuR/XsP8AnQ2ZuE/mPENa1pWrMij05ZRvzhFK/jiIhIIRXYgJpZIDCOrG9lWwEDzKxVrjlNgb8AFznnWgMPF0PWUiUgwBjdrx3hYcEMfW8xianpXkcSEZFiZGb1gf8Btznn1ucYr2BmlY49Bi4H8lxJV34XFhLIy3/syOHkNB6dspTMTN3SIiLiDwpzBrQzsME5t8k5lwp8APTNNedeYNyxxRWyv8GVAlSvWI4xN7dn096jPDl9tddxRETkLJjZ+8B8oLmZxZnZ3WY22MwGZ08ZAVQDXsm13UotYK6ZLQN+AT53zn3l83+AH2peuxIjerdizq97GT9nk9dxRESkEIIKMacekHPDrTi5mcwaAAAgAElEQVSgS645zQDMbB4QCIzMq3hmL6wwCKB+/fpnkrfUuahJdR7o3phxP2zkwibV6Nu+nteRRETkDDjnBhRw/B7gnjzGNwHtTn6FFMYfO9dn7q97GfX1Oro0rEqH+lW8jiQiIqdQVIsQBQFNge7AAOANM4vIPUmLJ+Tt4Uub0emcKvz1k5Vsij/idRwRERG/YWY8e31balUOZeh7S7TFmYhICVeYBnQHEJXjeWT2WE5xwHTnXJpzbjOwnqyGVAohODCAsQM6EBxo3P+u7gcVERE5HeHlgxl3S0f2HE7mkQ91P6iISElWmAZ0IdDUzBqaWQjQH5iea840ss5+YmbVybokVzdjnIZ6EWG82L8DLfZ+SfJzrXAjI+CFNrB8itfRRERESrz2URGMuKYVlX79hCP/bgGqoyIiJVKB94A659LNbCjwNVn3d05yzq0ysxgg1jk3PfvY5Wa2GsgA/pxzeXkpnG7JP3BBuUkEpydnDSRshxnDsh637eddMBERET9wa4VfuLncREJSsrdlUR0VESlxCrMIEc65L4Avco2NyPHYAY9m/8iZmhlDcGbyiWNpSTAzRoVTRESkADYzhhCXa09Q1VERkRKlqBYhkqKQEHd64yIiIvI71VERkRJPDWhJEh6Z57DLZ1xERERyyK9eqo6KiJQYakBLkp4jIDjshKFEF8JnNU7aNk5ERERyy6eO/txoqEeBREQkNzWgJUnbftB7LIRHAYYLj2J6/cd5cGVTZizb6XU6ERGRku2kOhrJpCqPcNsv57B42wGv04mICGBZ6wf5XnR0tIuNjfXks/1Janomt0xYwIodCUwdfCFt6oV7HUlEpFQys0XOuWivcxSW6mjhHDiaSt9x80hKy2DG0K7UDg/1OpKISKlU2DqqM6AlXEhQAK/e2omq5UO49+1Y4g+nFPwiERERAaBKhRAm3BFNYko6g96JJTktw+tIIiJlmhpQP1C9YjneuCOaA4mpDH53ESnpKp4iIiKF1axWJcb078CKHQkM/3g5Xl39JSIiakD9Ruu64Yy6qR2Lth5gxLRVKp4iIiKn4bJWtfjTZc2YtnQnr/+4yes4IiJlVpDXAaTwrmlbl3W7D/PS9xtoUacSd13U0OtIIiIifmNIjyas3X2Yf3+1lma1KvKHFrW8jiQiUuboDKifeeTSZlzeqhZPfbaamWt+8zqOiIiI3zAznr+xHa3rVubB95awameC15FERMocNaB+JiDAGNO/PW3qhTP0vSWsiFPxFBERKaywkEAm3nEe4WHBDHxrIbsSkryOJCJSpqgB9UPlQ4KYcEc0VSuEMPC/C9lxUMVTRESksGpVDmXSXedxNCWDu95cyOHkNK8jiYiUGWpA/VTNSqG8edd5JKdl8M7rz5P5n9YwMgJeaAPLp3gdT0REpERrUbsyr9zSkV/3HOHd8aNwL6iOioj4ghpQP9asViWmXhTHsMSXCDgUBzhI2A4zhql4ioj4mJlNMrM9ZrYyn+NmZmPNbIOZLTezjjmO3WFmv2b/3OG71GVbt2Y1eOe8rdyx7z9YguqoiIgvqAH1c81XvkB5Sz1xMC0JZsZ4E0hEpOx6C+h1iuNXAk2zfwYBrwKYWVXgH0AXoDPwDzOrUqxJ5bgLt4xTHRUR8SE1oP4uIe70xkVEpFg4534E9p9iSl/gbZdlARBhZnWAK4BvnXP7nXMHgG85dSMrRUl1VETEp9SA+rvwyNMbFxERr9QDtud4Hpc9lt/4ScxskJnFmllsfHx8sQUtU1RHRUR8Sg2ov+s5AoLDThhKdCHENnnQo0AiIlJcnHPjnXPRzrnoGjVqeB2ndMijjia5EDa1fdSjQCIipZsaUH/Xth/0HgvhUYCRWTmSCREP039+FLPX69txEZESZAcQleN5ZPZYfuPiC7nqaEblSEaHDuXaOfVYt/uw1+lEREodc8558sHR0dEuNjbWk88u7Q4lp3Hz6wvYsvco793bhQ71tZaFiEhBzGyRcy76LN+jAfCZc65NHseuBoYCV5G14NBY51zn7EWIFgHHVsVdDHRyzp3qflLV0WK0fX8iN772EwBTB19IVNXyHicSESn5CltHdQa0FKocGsx/B55HjUrluOuthfz6m77BFREpbmb2PjAfaG5mcWZ2t5kNNrPB2VO+ADYBG4A3gAcAshvNp4CF2T8xBTWfUryiqpbn7YFdSErN4PZJv7D3SIrXkURESg2dAS3Ftu47yo2vzceAD++7gIbVK3gdSUSkxCqKM6C+pDpa/BZt3c8tE36mQbUKvH/v+VSpEOJ1JBGREktnQIVzqlVg8j1dSM903PLGArbvT/Q6koiIiN/odE5VJtx+Hpv2HuX2Sb+QkJTmdSQREb+nBrSUa1arEu/e3YWjqRn8ccICdiUkeR1JRETEb3RtWp3Xb+3E2t2HuOvNXziSku51JBERv6YGtAxoVbcybw/szMGjafzxjZ/ZcyjZ60giIiJ+o0eLmrw0oCPL4hIY+NZCklIzvI4kIuK31ICWEe2iInhr4Hn8diiZWyb8zD4tqCAiIlJovdrUZszN7Yndsp97344lOU1NqIjImVADWoZ0OqcqE+84j237E7llws8c/uU9eKENjIzI+r18itcRRURESqze7ery3I3tmLthL4PfXUTqkg9UR0VETlOQ1wHEty5oXI0Jd0Qz7e0xBH/xBpB9JjRhO8wYlvW4bT/P8omIiJRkN3aKJD0jk/mfvorbNhGc6qiIyOnQGdAy6OKmNXim8v8IJddluGlJMDPGm1AiIiJ+on/n+vyz8ieUc6qjIiKnSw1oGRV6dFfeBxLifBtERETED1VI2p33AdVREZFTUgNaVoVHnt64iIiI/E51VETkjKgBLat6joDgsBOGkijHns6PexRIRETEj+RRR5Mpx5GuT3gUSETEP6gBLava9oPeYyE8CjBSK9bjKbuPq36ozaqdCV6nExERKdly1dGk8nV5IuNerp1Tj10JSV6nExEpscw558kHR0dHu9jYWE8+W/K2Yc8Rbp/4M4eT05lwRzRdGlXzOpKIiM+Y2SLnXLTXOQpLdbTkWbBpH/f+N5bKYcG8fXdnGteo6HUkERGfKWwdLdQZUDPrZWbrzGyDmQ0/xbwbzMyZmd8UcPldk5oVmXr/hdSsXI7bJ/3Cd6t/8zqSiIiI3zi/UTXeH3Q+yWkZ3PTafFbE6YoiEZHcCmxAzSwQGAdcCbQCBphZqzzmVQIeAn4u6pDiO3Ujwvho8IU0r12J+95dxMeLtJqfiIhIYbWpF85Hgy8gLDiQAW8s4KeNe72OJCJSohTmDGhnYINzbpNzLhX4AOibx7yngH8DyUWYTzxQtUII7917Pl0aVuVPHy1jwpxNXkcSERHxG41qVOTj+y+kTngod05ayFcr89myRUSkDCpMA1oP2J7jeVz22HFm1hGIcs59fqo3MrNBZhZrZrHx8fGnHVZ8p2K5IN686zyubFObpz9fQ8yM1WRkenO/sIiIiL+pHR7KlPsuoFXdytw/eRFvztvsdSQRkRIh6GzfwMwCgP8AdxY01zk3HhgPWYsnnO1nS/EqFxTIy3/syFOfrWbSvM3sOJjIS603EDL76ayNtsMjs5ahb9vP66giIiIlTpUKIbx3bxce+mApT85Yzfb9Sfyt/koCvo9RHRWRMqswDegOICrH88jssWMqAW2AWWYGUBuYbmZ9nHNans/PBQYYI/u0pn7V8iz7cjyZGycCKVkHE7bDjGFZj1U8RaSMM7NewItAIDDBOfdsruMvAD2yn5YHajrnIrKPZQArso9tc8718U1qKW7lQ4J47dZOPPXZavbOf4e0xRMp51RHRaTsKkwDuhBoamYNyWo8+wN/PHbQOZcAVD/23MxmAY+p+SxdBnZtSNJP0whNTDnxQFoSzIxR4RSRMi3Hgn2XkXWrykIzm+6cW31sjnPukRzzHwQ65HiLJOdce1/lFd869mXukdWfUC5ZdVREyrYC7wF1zqUDQ4GvgTXAFOfcKjOLMTN9Q1uGhCXuyvtAglbKFZEyr7AL9h0zAHjfJ8mkxKiYnM9iRKqjIlKGFOoeUOfcF8AXucZG5DO3+9nHkhIpPDLrcqG8xkVEyra8FuzrktdEMzsHaAh8n2M41MxigXTgWefctOIKKh5SHRURKdQquCJZeo6A4LAThhJdCB9FDCQtI9OjUCIifqc/MNU5l5Fj7BznXDRZt7iMMbPGeb1Qq8n7uXzq6Hd178M5rc0oImWDGlApvLb9oPdYCI8CDBcexTeNn+DP65pz+8Rf2H801euEIiJeKWjBvpz6k+vyW+fcjuzfm4BZnHh/aM55451z0c656Bo1apxtZvG1XHU0s3IkH9b+M/csacSjU5aRnJZR4FuIiPi7s96GRcqYtv2OL5RgwLVA5uI4hv9vBX3HzeWN26NpUbuypxFFRDxwygX7jjGzFkAVYH6OsSpAonMuxcyqAxcBz/kktfhejjoaANzpHEd/2MCob9azKf4Ir98WTe3wUG8ziogUI50BlbN2fcdIptx3ASlpmVz/yk98uSKfxYpEREqp01iwrz/wgTvxesuWQKyZLQN+IOse0NVImWBmDP1DU8bf1okNe47Q5+W5LNq63+tYIiLFxry65yA6OtrFxmqnltLkt0PJ3PfOIpZuP8g9XRvy+JUtCA7Udxwi4h/MbFH2fZh+QXW09Fm3+zCD3ollx4EknriqJXdd1IDsPdZFREq8wtZRdQdSZGpVDmXKfRdwxwXnMGHuZv74xgJ+O5TsdSwRERG/0Lx2JaYP7UqPFjWJ+Ww1Q99fwpGUdK9jiYgUKd0DKkUqJCiAJ/u2oVODqgz/eDlXj53De1220WzlC1n7nIVHZq0CqA23RUREThIeFsz42zrx+o+beP7rdazZdYjJnbdRJ/Y51VERKRXUgEqx6NOuLi1rV2LKpP8QOfdlsOwVchO2w4xhWY9VPEVERE5iZgy+pDHtoyKY8c6LRHz3quqoiJQaugRXik3TWpX4S7kplLdc27OkJcHMGG9CiYiI+InzG1UjpuLHhKmOikgpogZUilXAoXy2wUuI820QERERPxR4WHVUREoXNaBSvMIj8xw+GFKLlHRtuC0iInJK+dTRw6G1ycz0ZicDEZGzoQZUilfPERAcdsJQqpVjxJHruf6Vn9gYf8SjYCIiIn4gjzqaYuX466HruOuthcQfTvEomIjImVEDKsWrbT/oPRbCowCD8ChCrnuZ3rc+zM6DSVwzdi6Tf96KV/vRioiIlGj51NHz+gxmwaZ9XPnij3y3+jevU4qIFJp59R/+2kBbdick89hHy5i7YS/dmtXg3zecS53wsIJfKCJSDAq7gXZJoToq63Yf5qEPlrB292Fu6hTJ33u3onJosNexRKSMKmwd1RlQ8Uzt8FDeHtiZp/q2ZuHm/Vz+wo/8b3GczoaKiIgUQvPalZg+tCtDezTh48Vx9HrhR+b+utfrWCIip6QGVDwVEGDcdkEDvnzoYprXqsSjU5Zx3zuLOPTLe/BCGxgZkfV7+RSvo4qIiJQ4IUEBPHZFcz6+/0JCQwK5deLP/H3aSlIWf6A6KiIlUpDXAUQAGlSvwIf3XcDEuZtY++0kgjeNB7TptoiISGF0qF+FL4ZdzPNfr2Pv/HdwSycC2QsUqY6KSAmiM6BSYgQGGIO6Nebf4Z8QhjbdFhEROR2hwYH8/ZpWPBcxjVByrY6rOioiJYQaUClxgo/szHPcadNtERGRApU7uivPcdVRESkJ1IBKyZPPptvxATVYu/uQj8OIiIj4mXzq6L7AGuw8mOTjMCIiJ1IDKiVPHptupweGMsb155qxc3nuq7UkpqZ7FE5ERKSEy6OOpgWE8q+0flz2n9lMnLuZ9IxMj8KJSFmnBlRKnjw23Q7q+xJ/fuzv9G1fj1dmbaTn6Nl8vnyXtmwRERHJLY86GnztSzz88F/p1KAqT322mqvHzmXBpn1eJxWRMsi8+g94baAtZyp2y35GfLqK1bsOcVGTajzZpzVNalbyOpaI+LnCbqBdUqiOyplwzvH1qt946rPV7DiYRJ92dfnr1S2pVTnU62gi4ucKW0d1BlT8TnSDqsx4sCsxfVuzIi6BXmPm8M8v1nAkJT1rnzPteyYiHjCzXma2zsw2mNnwPI7faWbxZrY0++eeHMfuMLNfs3/u8G1yKUvMjF5tavPdo5cw7A9N+GrVbv4wahavz95I+tIPVUNFpNhpH1DxS4EBxu0XNODqc+vw3FfrGP/jJpJi3+cf9jpBGclZk7TvmYj4iJkFAuOAy4A4YKGZTXfOrc419UPn3NBcr60K/AOIBhywKPu1B3wQXcqosJBAHr28OTd0iiRmxmpWfT2B9JCJBGnvUBEpZjoDKn6tWsVy/PvGtkwbchEP8v7vzecx2vdMRHyjM7DBObfJOZcKfAD0LeRrrwC+dc7tz246vwV6FVNOkROcU60CE+88j39r71AR8RE1oFIqtI+KoEZmfN4Hte+ZiBS/esD2HM/jssdyu8HMlpvZVDOLOs3XihSbsETtHSoivqEGVEoNy2ffswPBNYk/nJLnMRERH5oBNHDOtSXrLOd/T/cNzGyQmcWaWWx8fD5fuomciXxq6E5XjdHfrMtaZ0FEpAioAZXSI499z1KtHDFJN9LtuR947qu1JCSmeRROREq5HUBUjueR2WPHOef2OeeOfRs2AehU2NfmeI/xzrlo51x0jRo1iiS4CJBnDc0MCuPbOvfx0vcb6PbcD0yYs4nktAyPAopIaaEGVEqPPPY9C7nuZR586Al6tqzJK7M20vW573lp5q/6JldEitpCoKmZNTSzEKA/MD3nBDOrk+NpH2BN9uOvgcvNrIqZVQEuzx4T8Z08amhAn7HcOfj/mDbkIlrVqczTn6+h+/OzeHfBVlLTM71OLCJ+SvuASpmxZtchRn+znu/W/EbVCiE80L0xt55/DqFrPs5aZCEhLusSpJ4jtOKfSBl0tvuAmtlVwBggEJjknHvGzGKAWOfcdDP7F1mNZzqwH7jfObc2+7UDgSey3+oZ59ybBX2e6qj42vyN+xj1zToWbT1AVNUwHu7ZjGs71CNw5UeqoyJS6DqqBlTKnCXbDvCfb9cz59e93FbhZ/7hXicoM8fqucFhWd8Cq3iKlCln24D6muqoeME5x6x18Yz6Zh2rdh5iUEQs/5f2yomr0KuOipRJakBFCrBg0z4avXs+NTP3nHwwPAoeWen7UCLiGTWgIoWXmen4etVuOv7vYmq5PBbEUh0VKXMKW0d1D6iUWec3qpbv1i1adl5ERCR/AQHGlefWoabbm+dx1VERyU+hGlAz62Vm68xsg5kNz+P4o2a2Ontvs5lmdk7RRxUpevlt3bKLaoz6eh37jmj7FhERkfzkV0d3U40JczZxVIv+iUguBTagZhYIjAOuBFoBA8ysVa5pS4Do7L3NpgLPFXVQkWKRz7LzX9UaxLhZG7jw2e954pMVbIw/4lFAERGREiyPOpoRGMbUiLt5+vM1XPjs9/z7q7X8dig5nzcQkbImqBBzOgMbnHObAMzsA6AvsPrYBOfcDznmLwBuLcqQIsXm2AIJOVbvC+g5goFt+9Ftz2EmzNnM1EVxvPfzNi5tWZO7uzbi/EZVsRVa8U9ERCSvOhrYcwQPtu3HhVsPMGHOJl6fvZEJczbRu11d7unaiFZ1K8PyKaqjImVUgYsQmdmNQC/n3D3Zz28DujjnhuYz/2Vgt3Pu6TyODQIGAdSvX7/T1q1bzzK+SPHbeySFd+Zv5Z0FW9l/NJUh1RbxSPI4rfgnUspoESKR4rFtXyKT5m1mSux2ElMz+HOdZQw+9CKBqqMipYonixCZ2a1ANPB8Xsedc+Odc9HOuegaNWoU5UeLFJvqFcvxyGXN+Gn4H/jndedyW+LbJzafAGlJWd/kioiIyAnqVyvPyD6tmT+8J4/3asH1Byae2HyC6qhIGVKYBnQHEJXjeWT22AnM7FLgr0Af55xWbpFSJzQ4kD92qU+tU6z459W2RiIiIiVdePlg7u/emNrsy/O4Vs4VKRsK04AuBJqaWUMzCwH6A9NzTjCzDsDrZDWfeWyqKFJ65Lfi347MavQaM4d3FmzliFb9ExERydOp6uiNr/7Ep0t3kJqe6eNUIuIrBTagzrl0YCjwNbAGmOKcW2VmMWbWJ3va80BF4CMzW2pm0/N5OxH/l8eKfy4ojO0dHyM4yPj7tJV0eeY7hn+8nEVbD/x+VnT5FHihDYyMyPq9fIoH4UVERDyWTx1d3+YR9h5J4aEPlnLBv2byzOer+fW3w79PUh0VKRUKXISouGjxBPFr+aze55xj6faDTP55G58v30VSWgZNalbkicgV9Fj/NJae9Pt7aMEFkRJFixCJ+FA+dTQz0zFnw17e+3krM9fsIT3T0aF+BH+us5wLVj2pOipSghW2jqoBFSkmR1LS+WzZTqbEbmfs7tuJDMjj3tHwKHhkpe/DichJ1ICKlCx7j6TwyeIdfBi7nbcODlQdFSnhPFkFV0R+V7FcEP071+d/D1xEvYD8F1zQwkUiIiInq16xHPd2a8S3j3Q7ZR0VEf+iBlTEB0614ELP0bMZ8916Nu896uNUIiIiJZ+ZnbKO9n5pLhPmbOK3Q8l5zhGRkkUNqIgvnGLholqVQ3lx5q/0GDWLvi/PZdLczew5nKOIatEFEREp606xcJEZPP35Gs7/10xumbCAKQu3cyg5LWuSaqhIiaN7QEV8JZ8FFwB2JyQzY9lOpi3dwaqdhzCD886pygPVFtNt3VMEaNEFkWKne0BFSrhT1NGN8Uf4dOlOPl26g637EgkONB6rvYy7D44hKCPHl7qqoSLFRosQifipDXsO89nyXXy1cjcT9t+lRRdEfEQNqIj/c86xLC6BL1bsYuDC3tR28SdPUg0VKRZahEjETzWpWYmHL23GVw+fetGFtbsPaQEjERGRHMyM9lERPHFVS2q5PL7ABTIT4pg4dzPb9iX6OJ2IAAR5HUBE8mfhkZCw/aTxHZnV6DVmDvUiwujZsiZ/aFGT8xtVIzQ4MGvCKS5TEpHiYWa9gBeBQGCCc+7ZXMcfBe4B0oF4YKBzbmv2sQxgRfbUbc65Pj4LLlJK5VdD4606T322mqc+W03TmhX5Q8ua9GxRi471IwgKzD43ozoqUmx0Ca5ISbZ8CswYBmkn3gOacOlovrSLmbl2D3N/3UtSWgZhwYF0bVqdgZUXcv5KbdYtcrrO5hJcMwsE1gOXAXHAQmCAc251jjk9gJ+dc4lmdj/Q3Tl3c/axI865iqfzmaqjIgXIp4bSeyxb6l7NzLV7+H7tb/y8aT/pmY6I8sF0b1aD2yr+Qsel/1AdFTlNha2jOgMqUpIdK3S5voUNb9uP/kD/zvVJTstg/qZ9fL9mDzPX/EbUhlFYQNKJ75OWlPUeKpwixaUzsME5twnAzD4A+gLHG1Dn3A855i8AbvVpQpGyJp8aStt+NADu7tqQu7s25FByGnN/3cvMNXv4Yd0eHkt7TnVUpBjpDKhIKeKcgyerYJz8v2uHMenSJVzctDpNa1bEzDxIKFJyneUZ0BuBXs65e7Kf3wZ0cc4NzWf+y8Bu59zT2c/TgaVkXZ77rHNuWj6vGwQMAqhfv36nrVu3nklcEclHRqYjICb/Ovr+lcu5uGl1oqqW9yCdSMmmM6AiZZCZZX3Dm8c9L79l3/MCUKtyOS5uWoOLmlSjS8Nq1I0I0/0uIj5iZrcC0cAlOYbPcc7tMLNGwPdmtsI5tzH3a51z44HxkPVFrk8Ci5QhgQH519HdVOeJT7Ju1W5YvQJdm1TnwsbV6NywKtUqllMdFSkkNaAipU3PEXne81K79z+ZG9WDub/uZc6ve/luzW9MXRQHwMBKCxme/gohLiVrfsL2rPcAFU+RwtkBROV4Hpk9dgIzuxT4K3CJc8f+BwfOuR3ZvzeZ2SygA3BSAyoiPnCKOvpd7W7Mya6jUxfF8c6CrKsQ7qsSy5+Sx6mOihSCLsEVKY0K8S1sRqZj7e5DLNi0n2tnXU619D0nvc3RsDrsvWcR9auW1yW7Uuqd5SW4QWQtQtSTrMZzIfBH59yqHHM6AFPJulT31xzjVYBE51yKmVUH5gN9cy5glBfVUZFiVIg6mpqeyYodB1mwaT83z72S6hkn19HEsLocvn8JtSqH+iq5iGcKW0fVgIoIjIyAPO53yXRGo5TJVK8YQof6VehYvwod60fQNjKCsJBAXW4kpcrZNKDZr78KGEPWNiyTnHPPmFkMEOucm25m3wHnAruyX7LNOdfHzC4EXgcyydqfe4xzbmJBn6c6KlKCFFBH60WE0fGcrBrasX4VWtWtTHBggOqolCq6B1RECi+f+10yKtXl6SvbsHjbAZZsO8i3q38DICjAGFx1EcOOvqTLjUSyOee+AP6/vTuPkqus0zj+/fVSVb1UV7qTXpLurCYOBIkLEZDRYQlKxIHoDEqc4QyM8eACisuZOTqRCOooHj064zKOinpEGRHQwUQRCMvMEDBACCRhCRpDErqT0Envnd477/xRtzvV3VXdVb3UvZU8n3PqdNVdun715lY/ee/y3ntHTduQ8PziFOs9TrxjKiK5KkWODpTO48a3L2f7gRa27Wtm046DAEQK8/hI+XY+0vktQsd74gsrR+UUoQ6oiKS83qXwHTdx1YqFXHXuQgCaOnt55kAr2w+0cPWTN5zofA7p76b9dzfyVOH5nFkXoyqa5JQj7e0VEZGTTYocDV1yE+tWLGYdiwE42NrN9gMtbN/fyvuf+fiJzueQ/m46freBHUUXcWZtjFhx4dj3Uo5KjlMHVETGvVdaotmlYS5eXs3Fy6th65Gkv6q051XW/TR+WmB1WZgza2exoi7GmbUxzmp/kLLNnzoR0NrbKyIiJ4M0c3TerCLmzSrir1fMg6eT52hJz2Gu+tETACyoKOZML0NX1MZ4Q+tmiu//pHJUcpo6oDA2h9sAABJJSURBVCISt+J9mYVXitONiNVy53vews76Vp5raGNnQxsP7X4V52BL6EbKktzc2z10M6bgFBGRXDZNOepitfzssrPZ1dDGrvo2drzSyu92xi8d3xL6HMVJc/QLylHJGeqAisjkpDjdKO/iz3P24grOXlwxPLmjp5/nD7ZTe1tT0l/lWhu4/NtbOK0mymlzyzh9bpTTa8ooLwmdWEinHImIyMkkRY7mX/x53raskrctqxye3Hysj10NbdT+V6ocreeK7z0+nKPL50Z5bXWUaCThFF7lqASEOqAiMjlpnm4EEI0Ucu6S2Sn39raHq5lVXMgjLzVyl3dvUoCqaJhl1aW8J/9x3v3KVynQQA0iInKyyCBHK0pCnP/aytQ5GqoiP8/YtOMgtz9xYHh6XXkRS6tKWZP3GJftv0U5KoGg27CISPbsvDPp3l4u+9ZwAB7p6GX34XZ2H+pg9+EO9hzp5HuN/8A8Ozrm17UUVnPX237P0qpSllZGqS0vIj8v4X6l2tsrGZjqbViyTTkqcgqaIEedcxxq6+HFQ+3sPtzBS4c72NPYyQ+br6E2RY7ec8H98RytKqWmLDLyvt/KUcmAbsMiIsGTxt7eymiYyujIU4/cTclPOYr1N/Lle3cPvw7l5zG/oojFc0q4lC1cPpm9vQpbEREJqgly1MyGBzpadXr18Grj5ejNm14Yfl1UmM/C2cUsml3CO3mUd738FeWoTDt1QEUkuzIdpAGwFKcc5cXqePaf386exk72NHby8tFj7Gs6xv6mLs5p+Q4FNnZ4+6O/Wc9395/B4jklLKgopq68mLryIiKF+WP3LOsUJRERCZppzFGL1fHkp1ex59VO9hzpZN/RLvY3HeOPjR3c2P7tpDnatHE9P6g/k4WzS1g4u5har8MbKshTjkpa1AEVkeBLMVADqzYwqzjEykUVrFxUMWKVVHt7KwaPcMeTr9DdPzhi+pzSEPceX0/V8bGjCx5/8GbytLdXRERyVYoctVUbqIpGqIpGOG/pnBGrpMrR8oEj/OSxffQNHh+eZgbV0QgbB5Ln6ISj3StHTynqgIpI8GUwUMOQ8Y6avvCJSzjS0cv+5i4aWrqpb+mivqWbyl3J78lGWwNnfXEzdeVF1JYXUVNWRE0sTE2siDOO3sfiP/wLeQOT2NurwBURkWyY5hx98YbVHGrr5pXmExna0NrNnOeT56hrbeDcLz9IXXkx82YVMTcWoboswtxYhNOP3sfCxz6rHD2FqAMqIrkh01OOxjlqamZUlUWoKovw5kUJ6xxIHradkWre8Rc11Ld0sftwB//70hGO9cWPoG4J/St5Se7J1rLpc9x+9I3UxIqoKYtQEwszpzRMrKgwPsDDZE9TUtiKiMhkTGOO5ueZdwlLMTD7xPz61Dn6V0srqW/pZld9Kw8830PvQPwI6pbQl5LmaOumz/GL5rOGO6vVZWEqo2FKwwXK0RynUXBF5OSVacikMUrvkI6efg639bD0e/Mxxv4dPY6xpOf2MdND+XnMKQ1xT/+HqDo+dk9xb0kthz/wFJXRMMWhUfsIM6hvzHoK2wlpFFwRkVFmKEedc7R29XO4vYfTvr8goxyNFOZRGQ3zq54PUXW8ccz83pJaGtdtozIajo/vMIn6ptwOp6h0c1QdUBGRRJmGzDdfl3RvL7H59Fy/g1fbezjU1kNjRy9HEh5ff+H85IHrjCW98cAtDuVTGQ0zuyRERUmYr9X/HeX9r45ZZyBaS9d1O4gO7RUe/XkUtmlRB1REZBpMY44e++izHG7v4VBrD0c6e4Yz9GhnH9948YIJczQaKUjI0RC3HEiVo3X0Xr+D4lC+cnQK1AEVEcmGyQZTisDtKZ7Hb1dtPtFZ7eyl+VgvTZ193Nt6OXnjhG1hvlFeHA/ZoZ9f2rc2adj2ldZy9INPEysqHBu4k/1MQ+vmaOCqAyoi4oNpztHu4nlsuvABjnTGc7Sxo4fmY300H+vjvrY14+ZoqCCP2QkZWl4S4ot7r2RWkhztL62l+drtxIoKdaTVo/uAiohkwyQGdgBSXlsTWX0zV6yoS77ON5NfW9NdXMP6VafT3NVHixeyzcf6ePFwO7H+sacnARR0HOS8Wx6Ov22+URYpJFZUSFlRIbc2r2fO4Njrcbrv+zxPF11EaaSA0nABUe/ncAc2m9fj5HhIi4iIZ5pztGj1zbxvxfzk64yTo5+56LThDG3p6qPpWB/1LV2UpcjR/I6DnPPlhwAIFeQRKyocfnz/aOocfab4RI6WRgqIhguJFOZhu+6a/C1scixHdQRURMQvM3iN6rAUe4i7iubxmwvvp627f8Sjvbufn75yybh7iEczg9JwAQ9wHXMZe11ra6iGn7x503CHtTRSQEm4gJJQAXMPbKT20c+cGP0wnc80lSO049ARUBGRHONzjv76/PuGszMxS3/esDqjHM3PMx4NfYx5HB0zr6Wwmv94wz2Uhgu9DuuJHC0O5VOzP/dyVB1QEZFc4mPY9pXWsuOKLXT2DNDRO0BnzwCdvf3Drzc8/ZcTXo8z2pbQx6nLGxu4B5nDeyM/pCiUT0ko3/tZQFEony++nPy0YmLz4ZPPJf9MaVAHVETkFOBzjj7zN4/S2TtAZ+8AHT3xn509A3z6D+ekHIzpdcfvoKtvcMw8mDhHS8L5FIUKKAnlUxyKd1pv3nulrzmqU3BFRHJJpsPoT+bUphSnNYXecRNvXlSRer09Ke4ZN6uOvTdcyrG+E0Hb0TtAd98gtT9PfqPzuTTxltfMpqtvgGO9g3T3DXK4vYfuvsGUpxXTVp+6NhEREfA9R89ZMjv5Oi+Mc//yT65m8Lgb7rgO7QDu6huk9vbUOXrOkgq6egfp6h+kq3eA1q5uuvoGfM/RtDqgZrYa+HcgH7jVOXfLqPlh4DbgLKAJuNI5t296SxURkUnJRtjCuPeMy8szopFCopFCiCWsk+JG5xar4+vvfX3y90lxDQ+xFNfOZtFU8tLMPgusAwaBjzvn7s9i6SIikoqPnVZWbQDip+kOXWM6wjg5+o33vSH5e/mcoxN2QM0sH/gu8HagHnjKzDY6515IWGwd0OKcW2pma4GvAlfORMEiIpIFmYbt0DowrYE7betkwVTy0syWA2uBM4B5wINm9lrnXPJzrkREJNgCsPN3WteZRukcAT0b2OOc2wtgZncAa4DEQF0D3OQ9vxv4jpmZ8+sCUxER8Uc2AneyIT3zJp2X3vQ7nHO9wMtmtsf7fX/IUu0iIuK3bO389TlH0+mA1gKJx2jrgXNSLeOcGzCzNmA2jBzKycyuBa4FWLBgwSRLFhGRk8pkA9f/DudoU8nLWmDrqHVrR7+BclRERMbIsRzNy+abOed+4Jxb6ZxbWVlZmc23FhERyXnKURERyXXpdEAbgMS7udZ505IuY2YFxIeYSD4kk4iIyMlpKnmZzroiIiI5L50O6FPAMjNbbGYh4oMkbBy1zEbgau/5FcDDuv5TREROMVPJy43AWjMLm9liYBnwZJbqFhERyZoJrwH1rlG5Hrif+LDyP3bOPW9mXwC2Oec2Aj8CfuYNmtBMPHRFREROGVPJS2+5O4kPWDQAXKcRcEVE5GRkfh2oXLlypdu2bZsv7y0iIjKamT3tnFvpdx3pUo6KiEiQpJujWR2ESERERERERE5dvh0BNbMjwP5p+nVzGHXLlxyj+v2l+v2l+v2l+k9Y6JzLmaFllaMjqH5/qX5/qX5/qf4T0spR3zqg08nMtuXSaVOjqX5/qX5/qX5/qX6B3G9H1e8v1e8v1e8v1Z85nYIrIiIiIiIiWaEOqIiIiIiIiGTFydIB/YHfBUyR6veX6veX6veX6hfI/XZU/f5S/f5S/f5S/Rk6Ka4BFRERERERkeA7WY6AioiIiIiISMCpAyoiIiIiIiJZEfgOqJmtNrOXzGyPmX0myfywmf3Sm/+EmS1KmPdZb/pLZnZJNutOqGGi+j9lZi+Y2U4ze8jMFibMGzSzZ73HxuxWPlzDRPVfY2ZHEur8YMK8q83sT97j6uxWPlzDRPV/M6H2P5pZa8I8X9vfzH5sZo1m9lyK+WZm3/I+204ze1PCvCC0/UT1/71X9y4ze9zMXp8wb583/Vkz25a9qkfUN1H9F5hZW8I2siFh3rjbXTakUf8/JdT+nLe9V3jzgtD+883sEe/v4/NmdkOSZQL9HQgK5ahydCqUo8rRyVKO+t7+wc1R51xgH0A+8GdgCRACdgDLRy3zUeA/vedrgV96z5d7y4eBxd7vyQ9g/RcCxd7zjwzV773uzIH2vwb4TpJ1K4C93s9y73l50OoftfzHgB8HqP3/CngT8FyK+ZcCvwcMOBd4Iihtn2b95w3VBbxzqH7v9T5gTsDb/wLgt1Pd7vyqf9SylwEPB6z95wJv8p5HgT8m+fsT6O9AEB5p/h1Xjvpb/zUoR2eqfuVosNv/ApSjM1l/YHM06EdAzwb2OOf2Ouf6gDuANaOWWQP81Ht+N7DKzMybfodzrtc59zKwx/t92TRh/c65R5xzXd7LrUBdlmscTzrtn8olwGbnXLNzrgXYDKyeoTpTybT+9wO/yEplaXDO/R/QPM4ia4DbXNxWYJaZzSUYbT9h/c65x736IHjbfjrtn8pUvjfTJsP6A7XtAzjnDjnntnvPO4AXgdpRiwX6OxAQylF/KUd9pBz1l3LUX0HO0aB3QGuBVxJe1zO24YaXcc4NAG3A7DTXnWmZ1rCO+F6IIREz22ZmW83s3TNR4ATSrf9vvcP2d5vZ/AzXnUlp1+CdsrUYeDhhst/tP5FUny8IbZ+p0du+Ax4ws6fN7FqfakrHW8xsh5n93szO8KblVPubWTHxUPlVwuRAtb/FTwl9I/DEqFkn03dgpihHlaNToRwdOT3IlKM+UY5mrmC6fpFMjZldBawEzk+YvNA512BmS4CHzWyXc+7P/lSY0ibgF865XjP7EPG96Bf5XNNkrAXuds4NJkzLhfbPeWZ2IfHgfGvC5Ld6bV8FbDaz3d6eyCDZTnwb6TSzS4F7gGU+1zQZlwGPOecS9/IGpv3NrJR4qH/COdfuRw2SG5SjvlOO+kQ56jvlaIaCfgS0AZif8LrOm5Z0GTMrAGJAU5rrzrS0ajCzi4H1wOXOud6h6c65Bu/nXuB/iO+5yKYJ63fONSXUfCtwVrrrZkEmNaxl1KkTAWj/iaT6fEFo+7SY2Qri280a51zT0PSEtm8E/pvsn/Y3Iedcu3Ou03t+L1BoZnPIofb3jLft+9r+ZlZIPDRvd879OskiOf8dyALlKMrRKVCOjpweOMrRQFCOZsr5eHHsRA/iR2j3Ej+lY+gi5DNGLXMdIwdPuNN7fgYjB0/YS/YHT0in/jcSv9B62ajp5UDYez4H+BNZvgA7zfrnJjx/D7DVe14BvOx9jnLveUXQ6veWO434xeIWpPb33nsRqS/efxcjLxx/Mihtn2b9C4hfU3beqOklQDTh+ePA6gDWXzO0zRAPlgPev0Va253f9XvzY8SvbykJWvt7bXkb8G/jLBP474DfjzT/jitH/a1fOTqzn2G8v+OB/xsyQf3KUR/r9+YrRydTmx//mBk23qXER236M7Dem/YF4ns5ASLAXd4X8ElgScK66731XgLeGdD6HwReBZ71Hhu96ecBu7wv3S5gXUDr/wrwvFfnI8BpCet+wPt32QP8YxDr917fBNwyaj3f25/43rRDQD/xc+/XAR8GPuzNN+C73mfbBawMWNtPVP+tQEvCtr/Nm77Ea/cd3ra1PqD1X5+w7W8l4T8Ayba7oNXvLXMN8UFmEtcLSvu/lfg1NDsTtpFLc+k7EJTHRH8HUY76Xb9ydOZqV44qR2esfm+Za1COZvwY2usgIiIiIiIiMqOCfg2oiIiIiIiInCTUARUREREREZGsUAdUREREREREskIdUBEREREREckKdUBFREREREQkK9QBFclRZjbLzD7qdx0iIiK5SDkq4g91QEVy1yxAwSkiIjI5ylERH6gDKpK7bgFeY2bPmtnX/C5GREQkxyhHRXxgzjm/axCRSTCzRcBvnXOv87kUERGRnKMcFfGHjoCKiIiIiIhIVqgDKiIiIiIiIlmhDqhI7uoAon4XISIikqOUoyI+UAdUJEc555qAx8zsOQ2eICIikhnlqIg/NAiRiIiIiIiIZIWOgIqIiIiIiEhWqAMqIiIiIiIiWaEOqIiIiIiIiGSFOqAiIiIiIiKSFeqAioiIiIiISFaoAyoiIiIiIiJZoQ6oiIiIiIiIZMX/A7iBtDNiUjUfAAAAAElFTkSuQmCC\n", "text/plain": [ "
" ] @@ -163,19 +163,16 @@ "metadata": {}, "outputs": [ { - "ename": "ValueError", - "evalue": "all the input arrays must have same number of dimensions, but the array at index 0 has 1 dimension(s) and the array at index 1 has 2 dimension(s)", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 15\u001b[0m \u001b[0;31m# Solve #################################\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[0mt_eval\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlinspace\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m30\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 17\u001b[0;31m \u001b[0msolution\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mdae_solver\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msolve\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mt_eval\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 18\u001b[0m \u001b[0;31m#########################################\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 19\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/mnt/c/Users/vsulzer/Documents/Energy_Storage/PyBaMM/pybamm/solvers/casadi_solver.py\u001b[0m in \u001b[0;36msolve\u001b[0;34m(self, model, t_eval, external_variables, inputs)\u001b[0m\n\u001b[1;32m 178\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 179\u001b[0m \u001b[0;31m# Calculate more exact termination reason\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 180\u001b[0;31m \u001b[0msolution\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtermination\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_termination_reason\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msolution\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mevents\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 181\u001b[0m pybamm.logger.info(\n\u001b[1;32m 182\u001b[0m \u001b[0;34m\"Finish solving {} ({})\"\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mname\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msolution\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtermination\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/mnt/c/Users/vsulzer/Documents/Energy_Storage/PyBaMM/pybamm/solvers/base_solver.py\u001b[0m in \u001b[0;36mget_termination_reason\u001b[0;34m(self, solution, events)\u001b[0m\n\u001b[1;32m 571\u001b[0m \u001b[0mfinal_event_values\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 572\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mname\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mevent\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mevents\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mitems\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 573\u001b[0;31m \u001b[0my_event\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0madd_external\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msolution\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0my_event\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0my_pad\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0my_ext\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 574\u001b[0m final_event_values[name] = abs(\n\u001b[1;32m 575\u001b[0m \u001b[0mevent\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mevaluate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msolution\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mt_event\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my_event\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/mnt/c/Users/vsulzer/Documents/Energy_Storage/PyBaMM/pybamm/solvers/base_solver.py\u001b[0m in \u001b[0;36madd_external\u001b[0;34m(y, y_pad, y_ext)\u001b[0m\n\u001b[1;32m 587\u001b[0m \"\"\"\n\u001b[1;32m 588\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0my_pad\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0my_ext\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 589\u001b[0;31m \u001b[0my\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconcatenate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0my\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my_pad\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0my_ext\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 590\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0my\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 591\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m<__array_function__ internals>\u001b[0m in \u001b[0;36mconcatenate\u001b[0;34m(*args, **kwargs)\u001b[0m\n", - "\u001b[0;31mValueError\u001b[0m: all the input arrays must have same number of dimensions, but the array at index 0 has 1 dimension(s) and the array at index 1 has 2 dimension(s)" - ] + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" } ], "source": [ @@ -186,7 +183,8 @@ "model.rhs = {u: -v} # du/dt = -v\n", "model.algebraic = {v: 2 * u - v} # 2*v = u\n", "model.initial_conditions = {u: 1, v: 2}\n", - "model.events['v=0.2'] = v - 0.2 # adding event here\n", + "model.events.append(pybamm.Event('v=0.2', v - 0.2)) # adding event here\n", + "\n", "model.variables = {\"u\": u, \"v\": v}\n", "\n", "# Discretise using default discretisation\n", @@ -228,9 +226,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'event: v=0.2'" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "solution.termination" ] @@ -246,7 +255,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -266,7 +275,7 @@ "model.rhs = {u: -v} # du/dt = -v\n", "model.algebraic = {v: 2 * u - v} # 2*v = u\n", "model.initial_conditions = {u: 1, v: 1} # bad initial conditions, solver fixes\n", - "model.events['v=0.2'] = v - 0.2\n", + "model.events.append(pybamm.Event('v=0.2', v - 0.2))\n", "model.variables = {\"u\": u, \"v\": v}\n", "\n", "# Discretise using default discretisation\n", @@ -277,13 +286,6 @@ "dae_solver.set_up(model)\n", "print(f\"y0_fixed={model.y0}\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -302,7 +304,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.9" + "version": "3.6.7" } }, "nbformat": 4, diff --git a/examples/notebooks/solvers/ode-solver.ipynb b/examples/notebooks/solvers/ode-solver.ipynb index 834332249f..c354a5847f 100644 --- a/examples/notebooks/solvers/ode-solver.ipynb +++ b/examples/notebooks/solvers/ode-solver.ipynb @@ -75,7 +75,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA6AAAAEYCAYAAABCw5uAAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nOzdd3gV1drG4d+kJ6TRWwJEihBCIBBARY8ons+KgkIQQUVB1KMHxYoNEMUK0lFRFBVFItjAegQUUSkBktB7SaghkBDSy3x/bEACiZTsZHZ57uvyipnJnv1gZM+8a9a8yzBNExEREREREZHK5mF1ABEREREREXEPKkBFRERERESkSqgAFRERERERkSqhAlRERERERESqhApQERERERERqRJeVrxprVq1zCZNmljx1iIiIudt5cqVh0zTrG1lBp07RUTEmZR37rSkAG3SpAkJCQlWvLWIiMh5Mwxjl9UZdO4UERFnUt65U1NwRUREREREpEqoABUREREREZEqoQJUREREREREqoQlz4CKiIiIiIh7KiwsJDU1lby8PKujiB34+fkRFhaGt7f3Of28ClAREREREakyqampBAUF0aRJEwzDsDqOVIBpmqSnp5OamkpERMQ5vUZTcEVEREREpMrk5eVRs2ZNFZ8uwDAMataseV53sytcgBqGEW4YxiLDMNYbhrHOMIxHKnrMc5IcD+OiYGSo7WtyfJW8rYiIiLgxXX+I2IWKT9dxvr9Le0zBLQIeN01zlWEYQcBKwzD+Z5rmejscu2zJ8TBvCBTm2r7PTLF9DxAdV2lvKyIiIm7sQq8/kuNhwSjITIWQMOg2XNcrIuK2KlyAmqa5D9h3/N+zDMPYADQEKq8AXTDq7w//Ewpzbdv1gS4iIiKVoOSXF/Eo4/rjyLznGb3pYkzT9jyUp4dBkJ83gX5etDvyM//a9BJexcenp2nQXETcnF2fATUMowkQAywrY99gwzASDMNISEtLq9gbZaaWudksZ7uIiIjIGcqZTmuaJtvTjvHlqlRenr+evtOWEjPq53KvP0IKD/Ln1kMs3Z7Osh2HWbwljfiEFCYu2EKLteP+Lj5POF60Tl+yg1W7j1BYXFLZf1IROUVKSgpXXXUVkZGRtG7dmgkTJpTaP2PGDHbu3IlpmuUe49tvv+W1114763vt27ePm266CYDExES+//77k/vmz5/P8OHDL/BPYX9VldX4p/+w53UgwwgEfgNGm6b55T/9bGxsrJmQkHDhbzYuyjaCeJqDHnXI+U8iTWpVu/Bji4iInMYwjJWmacZamaHC504p7fTptECRhx+f1nmcKentOZiVD4Cvlwct6wXRqn4wz22OIyh/35nHCgmHoWvP2FxcYuIxqjoGZ15rlWBwUd6nAPh7e9K+cSiXRNTkqpZ1aN0g2PZMlabuiovasGEDrVq1suz99+3bx759+2jfvj1ZWVl06NCBr7/+mpCQEEaMGEHjxo2JiIjg999/5913363Qez355JNcfvnl3HLLLcyYMYOEhAQmT54M2Aa72rdvzx9//EFAQEC5xxgwYAADBgyga9euFcpSmVnL+p2Wd+60yzIshmF4A3OBT89WfNpFt+FnnjQ8/Rhb3IdvJ/zOqFta0zs2vNJjiIiIiJMq43Eer5I8/m//u6y8eA6XXFST2CbVuahWNbw8j08YSx51xvUH3v6265IyeHoYtsKxjEFzj5Awlj/WjZW7jrBsx2FW7DzMW79sZuz/NlM/xI+hdRK5be+beBar34W4thfnrWP93qN2PWZkg2BGdG9d7v769etTv359AIKCgmjVqhV79uwhMjKS0aNH07lzZ6Kiovj2228BmDhxIu+88w5eXl5ERkby+eeflyrQBgwYQHBwMAkJCezfv5833niDXr16ATB37lxefvllCgoKGD58OLm5uSxZsoRnnnmGPn360LVrV+bPn09cXMX+Xv/44488++yzFBcXU6tWLRYsWMDhw4e599572b59OwEBAUybNo3o6Gh+++03HnnE1jfWMAwWL15MUFBQlWWtcAFq2NoeTQc2mKb5VkWPd05OfPCeMiro1W04jzbuzq7ZiTw5J5m/tqfz0i1RVPPVUqciIiJiczSvkHlJe+mbmVrmc0j1SGdi35iyX1zG9cdZ70qWMWh+omitE+zH9W3qc30b24XwoWP5LNx4kAUbDtBl61Q8DfW7EKlsO3fuZPXq1XTu3Jm9e/cyYsQI7r33XiIiInjooYd4++23ee2119ixYwe+vr5kZGSUeZx9+/axZMkSNm7cyM0330yvXr3YsWMH1atXx9fXF4BRo0aVuqsIEBsby++//16hoi4tLY377ruPxYsXExERweHDhwEYMWIEMTExfP311yxcuJC77rqL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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -163,7 +163,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA6AAAAEYCAYAAABCw5uAAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nOzde3yP9f/H8ce188YOzqcNy9lmthkqHZT6dVQok6gU6azopMMXKR0VKRURlZJFKjqHQgnDNuezbI4zNjsfr98foxy2HPbZrs/heb/d3GbXZ5/reqI+1/V6X+/r9TZM00RERERERESksrlZHUBERERERERcgwpQERERERERqRIqQEVERERERKRKqAAVERERERGRKqECVERERERERKqEhxUHrV27ttm0aVMrDi0iInLOVq1adcg0zTpWZtC5U0REHEl5505LCtCmTZsSHx9vxaFFRETOmWEYf1udQedOERFxJOWdOzUFV0RERERERKqEClARERERERGpEipARUREREREpEpY8gyoiIiIiIi4psLCQlJSUsjLy7M6itiAj48PwcHBeHp6ntXPqwAVEREREZEqk5KSgr+/P02bNsUwDKvjSAWYpklaWhopKSmEhoae1Xs0BVdERERERKpMXl4etWrVUvHpBAzDoFatWud0N7vCBahhGCGGYSwyDGODYRjrDcN4tKL7PCtJcTAuHEYFlX5NiquSw4qIiIgL0/WHiE2o+HQe5/pvaYspuEXA46ZprjYMwx9YZRjGL6ZpbrDBvsuWFAfzhkBhbun3Gcml3wNExFbaYUVERMSFne/1R1IcLBgNGSkQGAzdRuh6RURcVoULUNM09wH7jv0+0zCMjUAjoPIK0AWj//3wP64wt3S7PtBFRESkEpT8+gJuZVx/HJn3PGM2t8I0S5+Hcncz8PfxpLqPB5FHfuayzS/iUXxsepoGzUXExdn0GVDDMJoCUcDyMl4bbBhGvGEY8ampqRU7UEZKmZvNcraLiIiInKac6bSmabIjNYuvVqfw0vwN9J38F1Gjfy73+iOw8CB/bjvEXzvSWL7zMIu3phIXn8yEBVtpuW7cv8XncceK1qlLd7J69xEKi0sq+08qIidITk7miiuuoG3btoSFhfH222+f9Pr06dPZtWsXpmmWu49vv/2WV1999YzH2rdvHzfeeCMA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\n", + "image/png": "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AB6w1h53Oh5fYK0tt9YmAjHAAGOMlq+dhzFmBHDEWvu107H4oCustf2A64F7K5dNiojDlDvdo7zpHuXNGnEsb/pyAZoFxJ7xdUzl90RqXeW9GG8Ay6y1bzodj6+x1uYBnwHDnI7FyyUDN1bel/EPYLAx5mVnQ/IN1tqsyn+PAG9RsdxUzqa8KfVKudNzypsXTXnTQ07mTV8uQDcD3YwxnYwxQcAtwAqHY5IGqLIpwIvAdmvtn52Ox1cYY9oYYyIqP29CReOTHc5G5d2stb+z1sZYaztS8TftU2vt7Q6H5fWMMc0qm5xgjGkGXAeoY+m5lDel3ih3uk95033Km55xOm/6bAFqrS0D7gNWUXFj+6vW2u+cjcr7GWP+DnwJdDfGHDTG/NLpmHxAMvBzKq6qpVZ+3OB0UD4gCvjMGJNGxYnvR9ZatUeXuhAJrDPGbAU2Ae9Za//pcExeR3nTM8qbHlPudJ/yptQXR/Omzz6GRURERERERHyLz86AioiIiIiIiG9RASoiIiIiIiL1QgWoiIiIiIiI1AsVoCIiIiIiIlIvVICKiIiIiIhIvVABKuLjjDERxph7nI5DRETEFyhvijhLBaiI74sAlEhFREQujvKmiINUgIr4vrlAl8qHfM93OhgREREvp7wp4iBjrXU6BhGpAWNMR2CltTbe4VBERES8nvKmiLM0AyoiIiIiIiL1QgWoiIiIiIiI1AsVoCK+rwAIdToIERERH6G8KeIgFaAiPs5amwusN8akq5mCiIjI+SlvijhLTYhERERERESkXmgGVEREREREROqFClARERERERGpFypARUREREREpF6oABUREREREZF6oQJURERERERE6oUKUBEREREREakXKkBFRERERESkXvw/EGVSOLcL1ugAAAAASUVORK5CYII=\n", "text/plain": [ "
" ] @@ -181,7 +181,7 @@ "v = pybamm.Variable(\"v\")\n", "model.rhs = {u: -v, v: u}\n", "model.initial_conditions = {u: 2, v: 1}\n", - "model.events['v=-2'] = v + 2 # New termination event\n", + "model.events.append(pybamm.Event('v=-2', v + 2)) # New termination event\n", "model.variables = {\"u\": u, \"v\": v}\n", "\n", "# Discretise using default discretisation\n", @@ -258,13 +258,6 @@ "source": [ "print(\"event time: \", solution.t_event, \"\\nevent state\", solution.y_event.flatten())" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -283,7 +276,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.9" + "version": "3.6.7" } }, "nbformat": 4, diff --git a/examples/scripts/DFN.py b/examples/scripts/DFN.py index 4811df8a19..7c4e8539e8 100644 --- a/examples/scripts/DFN.py +++ b/examples/scripts/DFN.py @@ -7,15 +7,15 @@ pybamm.set_logging_level("INFO") + # load model -model = pybamm.lithium_ion.DFN({"operating mode": "voltage"}) +model = pybamm.lithium_ion.DFN() # create geometry geometry = model.default_geometry # load parameter values and process model and geometry param = model.default_parameter_values -param.update({"Voltage function [V]": 4.1}, check_already_exists=False) param.process_model(model) param.process_geometry(geometry) diff --git a/examples/scripts/SPMe.py b/examples/scripts/SPMe.py index 422a338d23..a85283f936 100644 --- a/examples/scripts/SPMe.py +++ b/examples/scripts/SPMe.py @@ -8,7 +8,8 @@ pybamm.set_logging_level("INFO") # load model -model = pybamm.lithium_ion.DFN({"surface form": "differential"}) +model = pybamm.lithium_ion.SPMe() +model.convert_to_format = "python" # create geometry geometry = model.default_geometry diff --git a/examples/scripts/compare_lithium_ion.py b/examples/scripts/compare_lithium_ion.py index 53459a036c..f2b17c1a68 100644 --- a/examples/scripts/compare_lithium_ion.py +++ b/examples/scripts/compare_lithium_ion.py @@ -26,7 +26,7 @@ # load parameter values and process models and geometry param = models[0].default_parameter_values -param["Current function [A]"] = 1.0 +param["Current function [A]"] = 1 for model in models: param.process_model(model) diff --git a/examples/scripts/drive_cycle.py b/examples/scripts/drive_cycle.py new file mode 100644 index 0000000000..1e9c708dad --- /dev/null +++ b/examples/scripts/drive_cycle.py @@ -0,0 +1,17 @@ +# +# Simulate drive cycle loaded from csv file +# +import pybamm + +# load model and update parameters so the input current is the US06 drive cycle +model = pybamm.lithium_ion.DFN() +param = model.default_parameter_values +param["Current function [A]"] = "[current data]US06" + +# create and run simulation using the CasadiSolver in "fast" mode, remembering to +# pass in the updated parameters +sim = pybamm.Simulation( + model, parameter_values=param, solver=pybamm.CasadiSolver(mode="fast") +) +sim.solve() +sim.plot() diff --git a/examples/scripts/experimental_protocols/cccv.py b/examples/scripts/experimental_protocols/cccv.py new file mode 100644 index 0000000000..c20dc90b68 --- /dev/null +++ b/examples/scripts/experimental_protocols/cccv.py @@ -0,0 +1,64 @@ +# +# Constant-current constant-voltage charge +# +import pybamm +import matplotlib.pyplot as plt + +pybamm.set_logging_level("INFO") +experiment = pybamm.Experiment( + [ + "Discharge at C/10 for 13 hours or until 3.3 V", + "Rest for 1 hour", + "Charge at 1 A until 4.1 V", + "Hold at 4.1 V until 50 mA", + "Rest for 1 hour", + ] + * 3, + period="2 minutes", +) +model = pybamm.lithium_ion.DFN() # use {"thermal": "x-lumped"} for thermal effects +sim = pybamm.Simulation(model, experiment=experiment, solver=pybamm.CasadiSolver()) +sim.solve() + +# Plot voltages from the discharge segments only +fig, ax = plt.subplots() +for i in range(3): + # Extract sub solutions + sol = sim.solution.sub_solutions[i * 5] + # Extract variables + t = sol["Time [h]"].entries + V = sol["Terminal voltage [V]"].entries + # Plot + ax.plot(t - t[0], V, label="Discharge {}".format(i + 1)) + ax.set_xlabel("Time [h]") + ax.set_ylabel("Voltage [V]") + ax.set_xlim([0, 13]) +ax.legend() + +# Save time, voltage, current, discharge capacity and temperature to csv and matlab +# formats +sim.solution.save_data( + "output.mat", + [ + "Time [h]", + "Current [A]", + "Terminal voltage [V]", + "Discharge capacity [A.h]", + "X-averaged cell temperature [K]", + ], + to_format="matlab", +) +sim.solution.save_data( + "output.csv", + [ + "Time [h]", + "Current [A]", + "Terminal voltage [V]", + "Discharge capacity [A.h]", + "X-averaged cell temperature [K]", + ], + to_format="csv", +) + +# Show all plots +sim.plot() diff --git a/examples/scripts/experimental_protocols/cccv_lead_acid.py b/examples/scripts/experimental_protocols/cccv_lead_acid.py new file mode 100644 index 0000000000..4614fd514b --- /dev/null +++ b/examples/scripts/experimental_protocols/cccv_lead_acid.py @@ -0,0 +1,19 @@ +# +# Constant-current constant-voltage charge +# +import pybamm + +pybamm.set_logging_level("INFO") +experiment = pybamm.Experiment( + [ + "Discharge at C/2 until 11 V", + "Rest for 1 hour", + "Charge at C/2 until 14.5 V", + "Hold at 14.5 V until 200 mA", + "Rest for 1 hour", + ] +) +model = pybamm.lead_acid.Full() +sim = pybamm.Simulation(model, experiment=experiment, solver=pybamm.CasadiSolver()) +sim.solve() +sim.plot() diff --git a/examples/scripts/experimental_protocols/gitt.py b/examples/scripts/experimental_protocols/gitt.py new file mode 100644 index 0000000000..dfebc70965 --- /dev/null +++ b/examples/scripts/experimental_protocols/gitt.py @@ -0,0 +1,13 @@ +# +# GITT discharge +# +import pybamm + +pybamm.set_logging_level("INFO") +experiment = pybamm.Experiment( + ["Discharge at C/20 for 1 hour", "Rest for 1 hour"] * 20, +) +model = pybamm.lithium_ion.DFN() +sim = pybamm.Simulation(model, experiment=experiment, solver=pybamm.CasadiSolver()) +sim.solve() +sim.plot() diff --git a/examples/scripts/nca_parameters.py b/examples/scripts/nca_parameters.py new file mode 100644 index 0000000000..7e9100822e --- /dev/null +++ b/examples/scripts/nca_parameters.py @@ -0,0 +1,11 @@ +import pybamm as pb + +pb.set_logging_level("INFO") +model = pb.lithium_ion.DFN() + +chemistry = pb.parameter_sets.NCA_Kim2011 +parameter_values = pb.ParameterValues(chemistry=chemistry) + +sim = pb.Simulation(model, parameter_values=parameter_values, C_rate=1) +sim.solve() +sim.plot() diff --git a/input/parameters/lithium-ion/anodes/graphite_Kim2011/README.md b/input/parameters/lithium-ion/anodes/graphite_Kim2011/README.md new file mode 100644 index 0000000000..0105517f0b --- /dev/null +++ b/input/parameters/lithium-ion/anodes/graphite_Kim2011/README.md @@ -0,0 +1,7 @@ +# Graphite anode parameters + +Parameters for a graphite anode, from the paper + +> Kim, G. H., Smith, K., Lee, K. J., Santhanagopalan, S., & Pesaran, A. (2011). Multi-domain modeling of lithium-ion batteries encompassing multi-physics in varied length scales. Journal of The Electrochemical Society, 158(8), A955-A969. + +Note, only an effective cell volumetric heat capacity is provided in the paper. We therefore used the values for the density and specific heat capacity reported in the Marquis2019 parameter set in each region and multiplied each density by the ratio of the volumetric heat capacity provided in smith to the calculated value. This ensures that the values produce the same effective cell volumetric heat capacity. This works fine for x-lumped thermal models but not for x-full thermal models. We do the same for the planar effective thermal conductivity. diff --git a/input/parameters/lithium-ion/anodes/graphite_Kim2011/graphite_diffusivity_Kim2011.py b/input/parameters/lithium-ion/anodes/graphite_Kim2011/graphite_diffusivity_Kim2011.py new file mode 100644 index 0000000000..11373e0f50 --- /dev/null +++ b/input/parameters/lithium-ion/anodes/graphite_Kim2011/graphite_diffusivity_Kim2011.py @@ -0,0 +1,37 @@ +from pybamm import exp + + +def graphite_diffusivity_Kim2011(sto, T, T_inf, E_D_s, R_g): + """ + Graphite diffusivity [1]. + + References + ---------- + .. [1] Kim, G. H., Smith, K., Lee, K. J., Santhanagopalan, S., & Pesaran, A. + (2011). Multi-domain modeling of lithium-ion batteries encompassing + multi-physics in varied length scales. Journal of The Electrochemical + Society, 158(8), A955-A969. + + Parameters + ---------- + sto: :class: `numpy.Array` + Electrode stochiometry + T: :class: `numpy.Array` + Dimensional temperature + T_inf: double + Reference temperature + E_D_s: double + Solid diffusion activation energy + R_g: double + The ideal gas constant + + Returns + ------- + : double + Solid diffusivity + """ + + D_ref = 9 * 10 ** (-14) + arrhenius = exp(E_D_s / R_g * (1 / T_inf - 1 / T)) + + return D_ref * arrhenius diff --git a/input/parameters/lithium-ion/anodes/graphite_Kim2011/graphite_electrolyte_reaction_rate_Kim2011.py b/input/parameters/lithium-ion/anodes/graphite_Kim2011/graphite_electrolyte_reaction_rate_Kim2011.py new file mode 100644 index 0000000000..b942801b2d --- /dev/null +++ b/input/parameters/lithium-ion/anodes/graphite_Kim2011/graphite_electrolyte_reaction_rate_Kim2011.py @@ -0,0 +1,48 @@ +from pybamm import exp + + +def graphite_electrolyte_reaction_rate_Kim2011(T, T_inf, E_r, R_g): + """ + Reaction rate for Butler-Volmer reactions between graphite and LiPF6 in EC:DMC + [1]. + + References + ---------- + .. [1] Kim, G. H., Smith, K., Lee, K. J., Santhanagopalan, S., & Pesaran, A. + (2011). Multi-domain modeling of lithium-ion batteries encompassing + multi-physics in varied length scales. Journal of The Electrochemical + Society, 158(8), A955-A969. + + Parameters + ---------- + T: :class: `numpy.Array` + Dimensional temperature + T_inf: double + Reference temperature + E_r: double + Reaction activation energy + R_g: double + The ideal gas constant + + Returns + ------- + :`numpy.Array` + Reaction rate + """ + + i0_ref = 36 # reference exchange current density at 100% SOC + sto = 0.36 # stochiometry at 100% SOC + c_s_n_max = 2.87 * 10 ** 4 # max electrode concentration + c_s_n_ref = sto * c_s_n_max # reference electrode concentration + c_e_ref = 1.2 * 10 ** 3 # reference electrolyte concentration + alpha = 0.5 # charge transfer coefficient + + m_ref = ( + 2 + * i0_ref + / (c_e_ref ** alpha * (c_s_n_max - c_s_n_ref) ** alpha * c_s_n_ref ** alpha) + ) + + arrhenius = exp(E_r / R_g * (1 / T_inf - 1 / T)) + + return m_ref * arrhenius diff --git a/input/parameters/lithium-ion/anodes/graphite_Kim2011/graphite_ocp_Kim2011.py b/input/parameters/lithium-ion/anodes/graphite_Kim2011/graphite_ocp_Kim2011.py new file mode 100644 index 0000000000..0cbab00dde --- /dev/null +++ b/input/parameters/lithium-ion/anodes/graphite_Kim2011/graphite_ocp_Kim2011.py @@ -0,0 +1,29 @@ +from pybamm import exp, tanh + + +def graphite_ocp_Kim2011(sto): + """ + Graphite Open Circuit Potential (OCP) as a function of the stochiometry [1]. + + References + ---------- + .. [1] Kim, G. H., Smith, K., Lee, K. J., Santhanagopalan, S., & Pesaran, A. + (2011). Multi-domain modeling of lithium-ion batteries encompassing + multi-physics in varied length scales. Journal of The Electrochemical + Society, 158(8), A955-A969. + """ + + u_eq = ( + 0.124 + + 1.5 * exp(-70 * sto) + - 0.0351 * tanh((sto - 0.286) / 0.083) + - 0.0045 * tanh((sto - 0.9) / 0.119) + - 0.035 * tanh((sto - 0.99) / 0.05) + - 0.0147 * tanh((sto - 0.5) / 0.034) + - 0.102 * tanh((sto - 0.194) / 0.142) + - 0.022 * tanh((sto - 0.98) / 0.0164) + - 0.011 * tanh((sto - 0.124) / 0.0226) + + 0.0155 * tanh((sto - 0.105) / 0.029) + ) + + return u_eq diff --git a/input/parameters/lithium-ion/anodes/graphite_Kim2011/parameters.csv b/input/parameters/lithium-ion/anodes/graphite_Kim2011/parameters.csv new file mode 100644 index 0000000000..12af026ee2 --- /dev/null +++ b/input/parameters/lithium-ion/anodes/graphite_Kim2011/parameters.csv @@ -0,0 +1,38 @@ +Name [units],Value,Reference,Notes +# Empty rows and rows starting with ‘#’ will be ignored,,, +,,, +# Electrode properties,,, +Negative electrode conductivity [S.m-1],100,, +Maximum concentration in negative electrode [mol.m-3],2.87E4,, +Negative electrode diffusivity [m2.s-1],[function]graphite_diffusivity_Kim2011,, +Negative electrode OCP [V],[function]graphite_ocp_Kim2011, +,,, +# Microstructure,,, +Negative electrode porosity,0.4,, +Negative electrode active material volume fraction,0.51,, +Negative particle radius [m],5.083E-7,, +Negative particle distribution in x,1,, +Negative electrode surface area density [m-1],3.01E6,, +Negative electrode Bruggeman coefficient (electrolyte),2,, +Negative electrode Bruggeman coefficient (electrode),2,, +,,, +# Interfacial reactions,,, +Negative electrode cation signed stoichiometry,-1,, +Negative electrode electrons in reaction,1,, +Reference OCP vs SHE in the negative electrode [V],,, +Negative electrode charge transfer coefficient,0.5,, +Negative electrode double-layer capacity [F.m-2],0.2,Not reported in Kim2011, +,,, +# Density,,, +Negative electrode density [kg.m-3],2136.43638,1657 * 1.28934, +,,, +# Thermal parameters,,, +Negative electrode specific heat capacity [J.kg-1.K-1],700,, +Negative electrode thermal conductivity [W.m-1.K-1],1.1339,1.7 * 0.667, +Negative electrode OCP entropic change [V.K-1],0,, +,,, +# Activation energies,,, +Reference temperature [K],298.15,25C, +Negative electrode reaction rate,[function]graphite_electrolyte_reaction_rate_Kim2011,, +Negative reaction rate activation energy [J.mol-1],3E4,, +Negative solid diffusion activation energy [J.mol-1],4E3,, 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 9a5fcbcd21..260a2f62e5 100644 --- a/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/parameters.csv +++ b/input/parameters/lithium-ion/anodes/graphite_mcmb2528_Marquis2019/parameters.csv @@ -19,7 +19,6 @@ Negative electrode Bruggeman coefficient (electrode),1.5,Scott Moura FastDFN, # Interfacial reactions,,, Negative electrode cation signed stoichiometry,-1,, Negative electrode electrons in reaction,1,, -Negative electrode reference exchange-current density [A.m-2(m3.mol)1.5],2E-05,Scott Moura FastDFN,Be careful how we implement BV Reference OCP vs SHE in the negative electrode [V],,, Negative electrode charge transfer coefficient,0.5,Scott Moura FastDFN, Negative electrode double-layer capacity [F.m-2],0.2,, diff --git a/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/parameters.csv b/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/parameters.csv index 1eafb9b658..f906faad48 100644 --- a/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/parameters.csv +++ b/input/parameters/lithium-ion/cathodes/lico2_Marquis2019/parameters.csv @@ -19,7 +19,6 @@ Positive electrode Bruggeman coefficient (electrode),1.5,Scott Moura FastDFN, # Interfacial reactions,,, Positive electrode cation signed stoichiometry,-1,, Positive electrode electrons in reaction,1,, -Positive electrode reference exchange-current density [A.m-2(m3.mol)1.5],6E-07,Scott Moura FastDFN,Be careful how we implement BV Reference OCP vs SHE in the positive electrode [V],,, Positive electrode charge transfer coefficient,0.5,Scott Moura FastDFN, Positive electrode double-layer capacity [F.m-2],0.2,, diff --git a/input/parameters/lithium-ion/cathodes/nca_Kim2011/README.md b/input/parameters/lithium-ion/cathodes/nca_Kim2011/README.md new file mode 100644 index 0000000000..f816226d21 --- /dev/null +++ b/input/parameters/lithium-ion/cathodes/nca_Kim2011/README.md @@ -0,0 +1,8 @@ +# Nickel Cobalt Aluminium (NCA) cathode parameters + +Parameters for an NCA cathode, from the paper + +> Kim, G. H., Smith, K., Lee, K. J., Santhanagopalan, S., & Pesaran, A. (2011). Multi-domain modeling of lithium-ion batteries encompassing multi-physics in varied length scales. Journal of The Electrochemical Society, 158(8), A955-A969. + +Note, only an effective cell volumetric heat capacity is provided in the paper. We therefore used the values for the density and specific heat capacity reported in the Marquis2019 parameter set in each region and multiplied each density by the ratio of the volumetric heat capacity provided in smith to the calculated value. This ensures that the values produce the same effective cell volumetric heat capacity. This works fine for x-lumped thermal models but not for x-full thermal models. We do the same for the planar effective thermal conductivity. + diff --git a/input/parameters/lithium-ion/cathodes/nca_Kim2011/nca_diffusivity_Kim2011.py b/input/parameters/lithium-ion/cathodes/nca_Kim2011/nca_diffusivity_Kim2011.py new file mode 100644 index 0000000000..2cd18b310e --- /dev/null +++ b/input/parameters/lithium-ion/cathodes/nca_Kim2011/nca_diffusivity_Kim2011.py @@ -0,0 +1,37 @@ +from pybamm import exp + + +def nca_diffusivity_Kim2011(sto, T, T_inf, E_D_s, R_g): + """ + NCA diffusivity as a function of stochiometry [1]. + + References + ---------- + .. [1] Kim, G. H., Smith, K., Lee, K. J., Santhanagopalan, S., & Pesaran, A. + (2011). Multi-domain modeling of lithium-ion batteries encompassing + multi-physics in varied length scales. Journal of The Electrochemical + Society, 158(8), A955-A969. + + Parameters + ---------- + sto: :class: `numpy.Array` + Electrode stochiometry + T: :class: `numpy.Array` + Dimensional temperature + T_inf: double + Reference temperature + E_D_s: double + Solid diffusion activation energy + R_g: double + The ideal gas constant + + Returns + ------- + : double + Solid diffusivity + """ + + D_ref = 3 * 10 ** (-15) + arrhenius = exp(E_D_s / R_g * (1 / T_inf - 1 / T)) + + return D_ref * arrhenius diff --git a/input/parameters/lithium-ion/cathodes/nca_Kim2011/nca_electrolyte_reaction_rate_Kim2011.py b/input/parameters/lithium-ion/cathodes/nca_Kim2011/nca_electrolyte_reaction_rate_Kim2011.py new file mode 100644 index 0000000000..9dd468a39f --- /dev/null +++ b/input/parameters/lithium-ion/cathodes/nca_Kim2011/nca_electrolyte_reaction_rate_Kim2011.py @@ -0,0 +1,46 @@ +from pybamm import exp + + +def nca_electrolyte_reaction_rate_Kim2011(T, T_inf, E_r, R_g): + """ + Reaction rate for Butler-Volmer reactions between NCA and LiPF6 in EC:DMC + [1]. + + References + ---------- + .. [1] Kim, G. H., Smith, K., Lee, K. J., Santhanagopalan, S., & Pesaran, A. + (2011). Multi-domain modeling of lithium-ion batteries encompassing + multi-physics in varied length scales. Journal of The Electrochemical + Society, 158(8), A955-A969. + + Parameters + ---------- + T: :class: `numpy.Array` + Dimensional temperature + T_inf: double + Reference temperature + E_r: double + Reaction activation energy + R_g: double + The ideal gas constant + + Returns + ------- + : double + Reaction rate + """ + i0_ref = 4 # reference exchange current density at 100% SOC + sto = 0.41 # stochiometry at 100% SOC + c_s_max = 4.9 * 10 ** 4 # max electrode concentration + c_s_ref = sto * c_s_max # reference electrode concentration + c_e_ref = 1.2 * 10 ** 3 # reference electrolyte concentration + alpha = 0.5 # charge transfer coefficient + + m_ref = ( + 2 + * i0_ref + / (c_e_ref ** alpha * (c_s_max - c_s_ref) ** alpha * c_s_ref ** alpha) + ) + arrhenius = exp(E_r / R_g * (1 / T_inf - 1 / T)) + + return m_ref * arrhenius diff --git a/input/parameters/lithium-ion/cathodes/nca_Kim2011/nca_ocp_Kim2011_data.csv b/input/parameters/lithium-ion/cathodes/nca_Kim2011/nca_ocp_Kim2011_data.csv new file mode 100644 index 0000000000..dd00060753 --- /dev/null +++ b/input/parameters/lithium-ion/cathodes/nca_Kim2011/nca_ocp_Kim2011_data.csv @@ -0,0 +1,75 @@ +0.370214428274133, 4.210440859985937 +0.37577436378229034, 4.198214873372019 +0.3836904770360269, 4.182142025684674 +0.3918959828337426, 4.165163132847251 +0.40106922140226364, 4.149604765177982 +0.40686180736573874, 4.138286599370602 +0.4116896199858422, 4.129799619141419 +0.4194137255876832, 4.115653556008545 +0.4266554445178383, 4.10292308566477 +0.4329304171529992, 4.090189327068291 +0.4396907358872798, 4.0802916864290655 +0.44548430832656627, 4.070390757537136 +0.4532103868800297, 4.059079168235163 +0.45852157264763016, 4.050593832132332 +0.4628660121202365, 4.042105207776797 +0.4710764502970082, 4.032212499516627 +0.47638763606460877, 4.023727163413795 +0.4836313279463863, 4.013831166900922 +0.48942490038567266, 4.0039302380089925 +0.4981187117099415, 3.994039173875174 +0.5077763099017708, 3.979899687247709 +0.5164711077018508, 3.971425860029342 +0.5280592390562348, 3.9530412391609326 +0.537716837248064, 3.938901752533467 +0.5493089145056929, 3.9261860793268606 +0.5565516199116592, 3.914872845898536 +0.5671749779226715, 3.8993194106083244 +0.5768345490661232, 3.88801439781176 +0.5869745339296383, 3.872459318395196 +0.5961487589739706, 3.8583181876413786 +0.6087065960507823, 3.844188565772025 +0.6159493014567486, 3.832875332343701 +0.6246431127810175, 3.822984268209883 +0.6352684437436521, 3.8102653067505723 +0.6463781343295951, 3.798965226333064 +0.660386090848898, 3.784840536842767 +0.6763275399581893, 3.7707224238578783 +0.6917856159199836, 3.756602666746637 +0.7038630392767319, 3.7467231114972837 +0.7246372333851825, 3.7312042028604644 +0.7391305360036051, 3.71991563132742 +0.7531414519503417, 3.710042652583475 +0.7676347545687643, 3.698754081050431 +0.7797121779255124, 3.6888745258010776 +0.7917886148064496, 3.677577733636274 +0.8043484248348838, 3.6662825855978216 +0.8144923556016439, 3.65639645384306 +0.8260854193350838, 3.645098017551904 +0.8357449904785356, 3.6337930047553395 +0.8463732808686039, 3.6253257540423807 +0.8560318655362443, 3.612603504330366 +0.8652100364838214, 3.604131321238351 +0.8739028613322789, 3.5928230201890825 +0.8840477785748505, 3.5843541253497717 +0.8927425763749305, 3.5758802981314046 +0.9024031339941933, 3.5659925222502906 +0.9125490377125759, 3.5589408643264306 +0.9222105818076499, 3.5504703253607675 +0.9338056184927125, 3.5420063629005125 +0.9482959616837011, 3.526466080621117 +0.957951586923908, 3.5094921201627503 +0.965192319378252, 3.495344412903525 +0.9709740541078039, 3.4684366410261873 +0.9743448419547373, 3.450024070009794 +0.976744937603432, 3.425939263078894 +0.9805892338397507, 3.393355967034346 +0.9820196237660176, 3.36501616110439 +0.9834510001680955, 3.3380935920898844 +0.9848843495217959, 3.3140054969062804 +0.9858323527763772, 3.287081283765423 +0.987258796799399, 3.2530725301736645 +0.9896421223593032, 3.204894695680104 +0.9905703960976598, 3.1496257442302342 +0.9915055751666949, 3.104277451188519 +0.9933828386354436, 3.023501523513243 \ No newline at end of file diff --git a/input/parameters/lithium-ion/cathodes/nca_Kim2011/nca_ocp_Kim2011_function.py b/input/parameters/lithium-ion/cathodes/nca_Kim2011/nca_ocp_Kim2011_function.py new file mode 100644 index 0000000000..8366020f0f --- /dev/null +++ b/input/parameters/lithium-ion/cathodes/nca_Kim2011/nca_ocp_Kim2011_function.py @@ -0,0 +1,37 @@ +from pybamm import exp + + +def nca_ocp_Kim2011_function(sto): + """ + NCA open-circuit potential (OCP) [1]. Fit in paper seems wrong to using + nca_ocp_Kim2011_data.csv instead. + References + ---------- + .. [1] Kim, G. H., Smith, K., Lee, K. J., Santhanagopalan, S., & Pesaran, A. + (2011). Multi-domain modeling of lithium-ion batteries encompassing + multi-physics in varied length scales. Journal of The Electrochemical + Society, 158(8), A955-A969. + + Parameters + ---------- + sto: double + Stochiometry of material (li-fraction) + + """ + + u_eq = ( + 1.68 * sto ** 10 + - 2.222 * sto ** 9 + + 15.056 * sto ** 8 + - 23.488 * sto ** 7 + + 81.246 * sto ** 6 + - 344.566 * sto ** 5 + + 621.3475 * sto ** 4 + - 544.774 * sto ** 3 + + 264.427 * sto ** 2 + - 66.3691 * sto + + 11.8058 + - 0.61386 * exp(5.8201 * sto ** 136.4) + ) + + return u_eq diff --git a/input/parameters/lithium-ion/cathodes/nca_Kim2011/parameters.csv b/input/parameters/lithium-ion/cathodes/nca_Kim2011/parameters.csv new file mode 100644 index 0000000000..d152be0362 --- /dev/null +++ b/input/parameters/lithium-ion/cathodes/nca_Kim2011/parameters.csv @@ -0,0 +1,38 @@ +Name [units],Value,Reference,Notes +# Empty rows and rows starting with ‘#’ will be ignored,,, +,,, +# Electrode properties,,, +Positive electrode conductivity [S.m-1],10,, +Maximum concentration in positive electrode [mol.m-3],4.9E4,, +Positive electrode diffusivity [m2.s-1],[function]nca_diffusivity_Kim2011,, +Positive electrode OCP [V],[data]nca_ocp_Kim2011_data, +,,, +# Microstructure,,, +Positive electrode porosity,0.4,, +Positive electrode active material volume fraction,0.41,, +Positive particle radius [m],1.633E-6,, +Positive particle distribution in x,1,, +Positive electrode surface area density [m-1],0.753E6,, +Positive electrode Bruggeman coefficient (electrolyte),2,, +Positive electrode Bruggeman coefficient (electrode),2,, +,,, +# Interfacial reactions,,, +Positive electrode cation signed stoichiometry,-1,, +Positive electrode electrons in reaction,1,, +Reference OCP vs SHE in the positive electrode [V],,, +Positive electrode charge transfer coefficient,0.5,, +Positive electrode double-layer capacity [F.m-2],0.2, Not provided in Kim2011, +,,, +# Density,,, +Positive electrode density [kg.m-3],4205.82708, 3262 * 1.28934, +,,, +# Thermal parameters,,, +Positive electrode specific heat capacity [J.kg-1.K-1],700,, +Positive electrode thermal conductivity [W.m-1.K-1],1.4007, 2.1 * 0.667, +Positive electrode OCP entropic change [V.K-1],0,, +,,, +# Activation energies,,, +Reference temperature [K],298.15,25C, +Positive electrode reaction rate,[function]nca_electrolyte_reaction_rate_Kim2011,, +Positive reaction rate activation energy [J.mol-1],3E4,, +Positive solid diffusion activation energy [J.mol-1],2E4,, diff --git a/input/parameters/lithium-ion/cells/Kim2011/README.md b/input/parameters/lithium-ion/cells/Kim2011/README.md new file mode 100644 index 0000000000..4cc9794415 --- /dev/null +++ b/input/parameters/lithium-ion/cells/Kim2011/README.md @@ -0,0 +1,6 @@ +# Pouch cell parameters + +Parameters for a "Nominal Design" pouch cell, from the paper + +> Kim, G. H., Smith, K., Lee, K. J., Santhanagopalan, S., & Pesaran, A. (2011). Multi-domain modeling of lithium-ion batteries encompassing multi-physics in varied length scales. Journal of The Electrochemical Society, 158(8), A955-A969. + diff --git a/input/parameters/lithium-ion/cells/Kim2011/parameters.csv b/input/parameters/lithium-ion/cells/Kim2011/parameters.csv new file mode 100644 index 0000000000..1091bea8a3 --- /dev/null +++ b/input/parameters/lithium-ion/cells/Kim2011/parameters.csv @@ -0,0 +1,37 @@ +Name [units],Value,Reference,Notes +# Empty rows and rows starting with ‘#’ will be ignored,,, +,,, +# Macroscale geometry,,, +Negative current collector thickness [m],10E-6,, +Negative electrode thickness [m],70E-6,, +Separator thickness [m],25E-6,, +Positive electrode thickness [m],50E-6,, +Positive current collector thickness [m],10E-6,, +Electrode height [m],0.2,, +Electrode width [m],0.14,, +Negative tab width [m],0.044,, +Negative tab centre y-coordinate [m],0.013,, +Negative tab centre z-coordinate [m],0.2, At top, +Positive tab width [m],0.044,, +Positive tab centre y-coordinate [m],0.137,, +Positive tab centre z-coordinate [m],0.2,At top, +,,, +# Current collector properties ,,, +Negative current collector conductivity [S.m-1],59.6E6,, +Positive current collector conductivity [S.m-1],37.8E6,, +,,, +# Density,,, +Negative current collector density [kg.m-3],11544.75, 8954 * 1.28934, +Positive current collector density [kg.m-3],3490.24338, 2707 * 1.28934, +,,, +# Specific heat capacity,,, +Negative current collector specific heat capacity [J.kg-1.K-1],385,, +Positive current collector specific heat capacity [J.kg-1.K-1],897,, +,,, +# Thermal conductivity,,, +Negative current collector thermal conductivity [W.m-1.K-1],267.467, 401 * 0.667, +Positive current collector thermal conductivity [W.m-1.K-1],158.079, 237 * 0.667, +,,, +# Electrical,,, +Cell capacity [A.h],0.43,trial and error, +Typical current [A],0.43,0.2857,1C current diff --git a/input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/README.md b/input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/README.md new file mode 100644 index 0000000000..d7db16c804 --- /dev/null +++ b/input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/README.md @@ -0,0 +1,7 @@ +# LiPF6 electrolyte parameters + +Parameters for a LiPF6 electrolyte, from the paper + +> Kim, G. H., Smith, K., Lee, K. J., Santhanagopalan, S., & Pesaran, A. (2011). Multi-domain modeling of lithium-ion batteries encompassing multi-physics in varied length scales. Journal of The Electrochemical Society, 158(8), A955-A969. + +and references therein. diff --git a/input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/electrolyte_conductivity_Kim2011.py b/input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/electrolyte_conductivity_Kim2011.py new file mode 100644 index 0000000000..477cd4e3fc --- /dev/null +++ b/input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/electrolyte_conductivity_Kim2011.py @@ -0,0 +1,40 @@ +from pybamm import exp + + +def electrolyte_conductivity_Kim2011(c_e, T, T_inf, E_k_e, R_g): + """ + Conductivity of LiPF6 in EC as a function of ion concentration from [1]. + + References + ---------- + .. [1] Kim, G. H., Smith, K., Lee, K. J., Santhanagopalan, S., & Pesaran, A. + (2011). Multi-domain modeling of lithium-ion batteries encompassing + multi-physics in varied length scales. Journal of The Electrochemical + Society, 158(8), A955-A969. + + Parameters + ---------- + c_e: :class: `numpy.Array` + Dimensional electrolyte concentration + T: :class: `numpy.Array` + Dimensional temperature + T_inf: double + Reference temperature + E_k_e: double + Electrolyte conductivity activation energy + R_g: double + The ideal gas constant + + Returns + ------- + :`numpy.Array` + Solid diffusivity + """ + + sigma_e = ( + 3.45 * exp(-798 / T) * (c_e / 1000) ** 3 + - 48.5 * exp(-1080 / T) * (c_e / 1000) ** 2 + + 244 * exp(-1440 / T) * (c_e / 1000) + ) + + return sigma_e diff --git a/input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/electrolyte_diffusivity_Kim2011.py b/input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/electrolyte_diffusivity_Kim2011.py new file mode 100644 index 0000000000..e852c158b9 --- /dev/null +++ b/input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/electrolyte_diffusivity_Kim2011.py @@ -0,0 +1,40 @@ +from pybamm import exp + + +def electrolyte_diffusivity_Kim2011(c_e, T, T_inf, E_D_e, R_g): + """ + Diffusivity of LiPF6 in EC as a function of ion concentration from [1]. + + References + ---------- + .. [1] Kim, G. H., Smith, K., Lee, K. J., Santhanagopalan, S., & Pesaran, A. + (2011). Multi-domain modeling of lithium-ion batteries encompassing + multi-physics in varied length scales. Journal of The Electrochemical + Society, 158(8), A955-A969. + + Parameters + ---------- + c_e: :class: `numpy.Array` + Dimensional electrolyte concentration + T: :class: `numpy.Array` + Dimensional temperature + T_inf: double + Reference temperature + E_D_e: double + Electrolyte diffusion activation energy + R_g: double + The ideal gas constant + + Returns + ------- + :`numpy.Array` + Solid diffusivity + """ + + D_c_e = ( + 5.84 * 10 ** (-7) * exp(-2870 / T) * (c_e / 1000) ** 2 + - 33.9 * 10 ** (-7) * exp(-2920 / T) * (c_e / 1000) + + 129 * 10 ** (-7) * exp(-3200 / T) + ) + + return D_c_e diff --git a/input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/parameters.csv b/input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/parameters.csv new file mode 100644 index 0000000000..9ea675fe67 --- /dev/null +++ b/input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/parameters.csv @@ -0,0 +1,13 @@ +Name [units],Value,Reference,Notes +# Empty rows and rows starting with ‘#’ will be ignored,,, +,,, +# Electrolyte properties,,, +Typical electrolyte concentration [mol.m-3],1200,, +Cation transference number,0.4,Reported as a function in Kim2011 (Implement later), +Electrolyte diffusivity [m2.s-1],[function]electrolyte_diffusivity_Kim2011,, +Electrolyte conductivity [S.m-1],[function]electrolyte_conductivity_Kim2011,, +,,, +# Activation energies,,, +Reference temperature [K],298.15,25C, +Electrolyte diffusion activation energy [J.mol-1],,Not required, +Electrolyte conductivity activation energy [J.mol-1],,Not requied, diff --git a/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Kim2011/README.md b/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Kim2011/README.md new file mode 100644 index 0000000000..f83025c2ff --- /dev/null +++ b/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Kim2011/README.md @@ -0,0 +1,7 @@ +# 1C discharge from full + +Discharge lithium-ion battery from full charge at 1C, using the initial conditions from the paper + +> Kim, G. H., Smith, K., Lee, K. J., Santhanagopalan, S., & Pesaran, A. (2011). Multi-domain modeling of lithium-ion batteries encompassing multi-physics in varied length scales. Journal of The Electrochemical Society, 158(8), A955-A969. + +and references therein. diff --git a/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Kim2011/parameters.csv b/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Kim2011/parameters.csv new file mode 100644 index 0000000000..1ed5821c2f --- /dev/null +++ b/input/parameters/lithium-ion/experiments/1C_discharge_from_full_Kim2011/parameters.csv @@ -0,0 +1,19 @@ +Name [units],Value,Reference,Notes +# Empty rows and rows starting with ‘#’ will be ignored,,, +,,, +# Temperature +Reference temperature [K],298.15,25C, +Heat transfer coefficient [W.m-2.K-1],25,, +,,, +# Electrical +Number of electrodes connected in parallel to make a cell,1,, +Number of cells connected in series to make a battery,1,, +Lower voltage cut-off [V],2.7,, +Upper voltage cut-off [V],4.5,, +C-rate,1,, +,,, +# Initial conditions +Initial concentration in negative electrode [mol.m-3],18081,0.63*2.84E4, +Initial concentration in positive electrode [mol.m-3],20090,0.41*4.9E4, +Initial concentration in electrolyte [mol.m-3],1200,, +Initial temperature [K],298.15,, diff --git a/input/parameters/lithium-ion/separators/separator_Kim2011/README.md b/input/parameters/lithium-ion/separators/separator_Kim2011/README.md new file mode 100644 index 0000000000..5628b0d80a --- /dev/null +++ b/input/parameters/lithium-ion/separators/separator_Kim2011/README.md @@ -0,0 +1,9 @@ +# Separator parameters + +Parameters for the separator in the paper + +> Kim, G. H., Smith, K., Lee, K. J., Santhanagopalan, S., & Pesaran, A. (2011). Multi-domain modeling of lithium-ion batteries encompassing multi-physics in varied length scales. Journal of The Electrochemical Society, 158(8), A955-A969. + +and references therein. + +Note, only an effective cell volumetric heat capacity is provided in the paper. We therefore used the values for the density and specific heat capacity reported in the Marquis2019 parameter set in each region and multiplied each density by the ratio of the volumetric heat capacity provided in smith to the calculated value. This ensures that the values produce the same effective cell volumetric heat capacity. This works fine for x-lumped thermal models but not for x-full thermal models. We do the same for the planar effective thermal conductivity. \ No newline at end of file diff --git a/input/parameters/lithium-ion/separators/separator_Kim2011/parameters.csv b/input/parameters/lithium-ion/separators/separator_Kim2011/parameters.csv new file mode 100644 index 0000000000..9c8b2ee81d --- /dev/null +++ b/input/parameters/lithium-ion/separators/separator_Kim2011/parameters.csv @@ -0,0 +1,9 @@ +Name [units],Value,Reference,Notes +# Empty rows and rows starting with ‘#’ will be ignored,,, +,,, +Separator porosity,0.4,, +Separator Bruggeman coefficient (electrolyte),2,, +Separator Bruggeman coefficient (electrode),2,, +Separator density [kg.m-3],511.86798,397 * 1.28934, +Separator specific heat capacity [J.kg-1.K-1],700,, +Separator thermal conductivity [W.m-1.K-1],0.10672, 0.16 * 0.667, diff --git a/input/parameters/lithium-ion/separators/separator_Marquis2019/README.md b/input/parameters/lithium-ion/separators/separator_Marquis2019/README.md index 615830690c..8862a317b4 100644 --- a/input/parameters/lithium-ion/separators/separator_Marquis2019/README.md +++ b/input/parameters/lithium-ion/separators/separator_Marquis2019/README.md @@ -1,6 +1,6 @@ # Separator parameters -Parameters for a ??? separator, from the paper +Parameters for the separator in the paper > Scott G. Marquis, Valentin Sulzer, Robert Timms, Colin P. Please, and S. Jon Chapman. "An asymptotic derivation of a single particle model with electrolyte." [arXiv preprint arXiv:1905.12553](https://arxiv.org/abs/1905.12553) (2019). diff --git a/input/parameters/lithium-ion/separators/separator_Marquis2019/parameters.csv b/input/parameters/lithium-ion/separators/separator_Marquis2019/parameters.csv index d1af7d5bda..ca77c0c0c4 100644 --- a/input/parameters/lithium-ion/separators/separator_Marquis2019/parameters.csv +++ b/input/parameters/lithium-ion/separators/separator_Marquis2019/parameters.csv @@ -1,9 +1,9 @@ Name [units],Value,Reference,Notes # Empty rows and rows starting with ‘#’ will be ignored,,, ,,, -Separator porosity,1,Scott Moura FastDFN, -Separator Bruggeman coefficient (electrolyte),1.5,Scott Moura FastDFN, -Separator Bruggeman coefficient (electrode),1.5,Scott Moura FastDFN, +Separator porosity,1,, +Separator Bruggeman coefficient (electrolyte),1.5,, +Separator Bruggeman coefficient (electrode),1.5,, Separator density [kg.m-3],397,, Separator specific heat capacity [J.kg-1.K-1],700,, Separator thermal conductivity [W.m-1.K-1],0.16,, diff --git a/pybamm/__init__.py b/pybamm/__init__.py index 8140fdeedb..022ac64370 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -120,6 +120,7 @@ def version(formatted=False): OptionError, ModelError, SolverError, + SolverWarning, ShapeError, ModelWarning, UndefinedOperationError, @@ -148,6 +149,8 @@ def version(formatted=False): # from .models.base_model import BaseModel from .models import standard_variables +from .models.event import Event +from .models.event import EventType # Battery models from .models.full_battery_models.base_battery_model import BaseBatteryModel @@ -173,17 +176,6 @@ def version(formatted=False): tortuosity, ) -# -# Parameters class and methods -# -from .parameters.parameter_values import ParameterValues -from .parameters import geometric_parameters -from .parameters import electrical_parameters -from .parameters import thermal_parameters -from .parameters import standard_parameters_lithium_ion, standard_parameters_lead_acid -from .parameters.print_parameters import print_parameters, print_evaluated_parameters -from .parameters import parameter_sets - # # Geometry # @@ -202,6 +194,18 @@ def version(formatted=False): from .expression_tree.independent_variable import KNOWN_SPATIAL_VARS, KNOWN_COORD_SYS from .geometry import standard_spatial_vars +# +# Parameters class and methods +# +from .parameters.parameter_values import ParameterValues +from .parameters import geometric_parameters +from .parameters import electrical_parameters +from .parameters import thermal_parameters +from .parameters import standard_parameters_lithium_ion, standard_parameters_lead_acid +from .parameters.print_parameters import print_parameters, print_evaluated_parameters +from .parameters import parameter_sets + + # # Mesh and Discretisation classes # @@ -235,7 +239,7 @@ def version(formatted=False): # # Solver classes # -from .solvers.solution import Solution +from .solvers.solution import Solution, _BaseSolution from .solvers.base_solver import BaseSolver from .solvers.algebraic_solver import AlgebraicSolver from .solvers.casadi_solver import CasadiSolver @@ -244,6 +248,12 @@ def version(formatted=False): from .solvers.scipy_solver import ScipySolver from .solvers.idaklu_solver import IDAKLUSolver, have_idaklu +# +# Experiments +# +from .experiments.experiment import Experiment +from . import experiments + # # other # diff --git a/pybamm/discretisations/discretisation.py b/pybamm/discretisations/discretisation.py index 584606af17..790098fd58 100644 --- a/pybamm/discretisations/discretisation.py +++ b/pybamm/discretisations/discretisation.py @@ -173,11 +173,16 @@ def process_model(self, model, inplace=True, check_model=True): model_disc.algebraic, model_disc.concatenated_algebraic = alg, concat_alg # Process events - processed_events = {} + processed_events = [] pybamm.logger.info("Discretise events for {}".format(model.name)) - for event, equation in model.events.items(): - pybamm.logger.debug("Discretise event '{}'".format(event)) - processed_events[event] = self.process_symbol(equation) + for event in model.events: + pybamm.logger.debug("Discretise event '{}'".format(event.name)) + processed_event = pybamm.Event( + event.name, + self.process_symbol(event.expression), + event.event_type + ) + processed_events.append(processed_event) model_disc.events = processed_events # Create mass matrix diff --git a/pybamm/experiments/__init__.py b/pybamm/experiments/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pybamm/experiments/experiment.py b/pybamm/experiments/experiment.py new file mode 100644 index 0000000000..fb7bbef648 --- /dev/null +++ b/pybamm/experiments/experiment.py @@ -0,0 +1,251 @@ +# +# Experiment class +# + +examples = """ + + Discharge at 1C for 0.5 hours, + Discharge at C/20 for 0.5 hours, + Charge at 0.5 C for 45 minutes, + Discharge at 1 A for 90 seconds, + Charge at 200mA for 45 minutes (1 minute period), + Discharge at 1 W for 0.5 hours, + Charge at 200 mW for 45 minutes, + Rest for 10 minutes (5 minute period), + Hold at 1 V for 20 seconds, + Charge at 1 C until 4.1V, + Hold at 4.1 V until 50 mA, + Hold at 3V until C/50, + """ + + +class Experiment: + """ + Base class for experimental conditions under which to run the model. In general, a + list of operating conditions should be passed in. Each operating condition should + be of the form "Do this for this long" or "Do this until this happens". For example, + "Charge at 1 C for 1 hour", or "Charge at 1 C until 4.2 V", or "Charge at 1 C for 1 + hour or until 4.2 V". The instructions can be of the form "(Dis)charge at x A/C/W", + "Rest", or "Hold at x V". The running time should be a time in seconds, minutes or + hours, e.g. "10 seconds", "3 minutes" or "1 hour". The stopping conditions should be + a circuit state, e.g. "1 A", "C/50" or "3 V". + + Parameters + ---------- + operating_conditions : list + List of operating conditions + parameters : dict + Dictionary of parameters to use for this experiment, replacing default + parameters as appropriate + period : string, optional + Period (1/frequency) at which to record outputs. Default is 1 minute. Can be + overwritten by individual operating conditions. + + """ + + def __init__(self, operating_conditions, parameters=None, period="1 minute"): + self.period = self.convert_time_to_seconds(period.split()) + self.operating_conditions_strings = operating_conditions + self.operating_conditions, self.events = self.read_operating_conditions( + operating_conditions + ) + parameters = parameters or {} + if isinstance(parameters, dict): + self.parameters = parameters + else: + raise TypeError("experimental parameters should be a dictionary") + + def __str__(self): + return str(self.operating_conditions_strings) + + def __repr__(self): + return "pybamm.Experiment({!s})".format(self) + + def read_operating_conditions(self, operating_conditions): + """ + Convert operating conditions to the appropriate format + + Parameters + ---------- + operating_conditions : list + List of operating conditions + + Returns + ------- + operating_conditions : list + Operating conditions in the tuple format + """ + converted_operating_conditions = [] + events = [] + for cond in operating_conditions: + if isinstance(cond, str): + next_op, next_event = self.read_string(cond) + converted_operating_conditions.append(next_op) + events.append(next_event) + else: + raise TypeError( + """Operating conditions should be strings, not {}. For example: {} + """.format( + type(cond), examples + ) + ) + + return converted_operating_conditions, events + + def read_string(self, cond): + """ + Convert a string to a tuple of the right format + + Parameters + ---------- + cond : str + String of appropriate form for example "Charge at x C for y hours". x and y + must be numbers, 'C' denotes the unit of the external circuit (can be A for + current, C for C-rate, V for voltage or W for power), and 'hours' denotes + the unit of time (can be second(s), minute(s) or hour(s)) + """ + # Read period + if " period)" in cond: + cond, time_period = cond.split("(") + time, _ = time_period.split(" period)") + period = self.convert_time_to_seconds(time.split()) + else: + period = self.period + # Read instructions + if "for" in cond and "or until" in cond: + # e.g. for 3 hours or until 4.2 V + cond_list = cond.split() + idx_for = cond_list.index("for") + idx_until = cond_list.index("or") + electric = self.convert_electric(cond_list[:idx_for]) + time = self.convert_time_to_seconds(cond_list[idx_for + 1 : idx_until]) + events = self.convert_electric(cond_list[idx_until + 2 :]) + elif "for" in cond: + # e.g. for 3 hours + cond_list = cond.split() + idx = cond_list.index("for") + electric = self.convert_electric(cond_list[:idx]) + time = self.convert_time_to_seconds(cond_list[idx + 1 :]) + events = None + elif "until" in cond: + # e.g. until 4.2 V + cond_list = cond.split() + idx = cond_list.index("until") + electric = self.convert_electric(cond_list[:idx]) + time = None + events = self.convert_electric(cond_list[idx + 1 :]) + else: + raise ValueError( + """Operating conditions must contain keyword 'for' or 'until'. + For example: {}""".format( + examples + ) + ) + return electric + (time,) + (period,), events + + def convert_electric(self, electric): + "Convert electrical instructions to consistent output" + # Rest == zero current + if electric[0].lower() == "rest": + return (0, "A") + else: + if len(electric) in [3, 4]: + if len(electric) == 4: + # e.g. Charge at 4 A, Hold at 3 V + instruction, _, value, unit = electric + elif len(electric) == 3: + # e.g. Discharge at C/2, Charge at 1A + instruction, _, value_unit = electric + if value_unit[0] == "C": + # e.g. C/2 + unit = value_unit[0] + value = 1 / float(value_unit[2:]) + else: + # e.g. 1A + if "m" in value_unit: + # e.g. 1mA + unit = value_unit[-2:] + value = float(value_unit[:-2]) + else: + # e.g. 1A + unit = value_unit[-1] + value = float(value_unit[:-1]) + # Read instruction + if instruction.lower() in ["discharge", "hold"]: + sign = 1 + elif instruction.lower() == "charge": + sign = -1 + else: + raise ValueError( + """instruction must be 'discharge', 'charge', 'rest' or 'hold'. + For example: {}""".format( + examples + ) + ) + elif len(electric) == 2: + # e.g. 3 A, 4.1 V + value, unit = electric + sign = 1 + elif len(electric) == 1: + # e.g. C/2, 1A + value_unit = electric[0] + if value_unit[0] == "C": + # e.g. C/2 + unit = value_unit[0] + value = 1 / float(value_unit[2:]) + else: + if "m" in value_unit: + # e.g. 1mA + unit = value_unit[-2:] + value = float(value_unit[:-2]) + else: + # e.g. 1A + unit = value_unit[-1] + value = float(value_unit[:-1]) + sign = 1 + else: + raise ValueError( + """Instruction '{}' not recognized. Some acceptable examples are: {} + """.format( + " ".join(electric), examples + ) + ) + # Read value and units + if unit == "C": + return (sign * float(value), "C") + elif unit == "A": + return (sign * float(value), "A") + elif unit == "mA": + return (sign * float(value) / 1000, "A") + elif unit == "V": + return (float(value), "V") + elif unit == "W": + return (sign * float(value), "W") + elif unit == "mW": + return (sign * float(value) / 1000, "W") + else: + raise ValueError( + """units must be 'C', 'A', 'mA', 'V', 'W' or 'mW', not '{}'. + For example: {} + """.format( + unit, examples + ) + ) + + def convert_time_to_seconds(self, time_and_units): + "Convert a time in seconds, minutes or hours to a time in seconds" + time, units = time_and_units + if units in ["second", "seconds", "s", "sec"]: + time_in_seconds = float(time) + elif units in ["minute", "minutes", "m", "min"]: + time_in_seconds = float(time) * 60 + elif units in ["hour", "hours", "h", "hr"]: + time_in_seconds = float(time) * 3600 + else: + raise ValueError( + """time units must be 'seconds', 'minutes' or 'hours'. For example: {} + """.format( + examples + ) + ) + return time_in_seconds diff --git a/pybamm/expression_tree/binary_operators.py b/pybamm/expression_tree/binary_operators.py index 6ac3cbc729..65c459926b 100644 --- a/pybamm/expression_tree/binary_operators.py +++ b/pybamm/expression_tree/binary_operators.py @@ -644,6 +644,16 @@ def inner(left, right): class Heaviside(BinaryOperator): """A node in the expression tree representing a heaviside step function. + Adding this operation to the rhs or algebraic equations in a model can often cause a + discontinuity in the solution. For the specific cases listed below, this will be + automatically handled by the solver. In the general case, you can explicitly tell + the solver of discontinuities by adding a :class:`Event` object with + :class:`EventType` DISCONTINUITY to the model's list of events. + + In the case where the Heaviside function is of the form `pybamm.t < x`, `pybamm.t <= + x`, `x < pybamm.t`, or `x <= pybamm.t`, where `x` is any constant equation, this + DISCONTINUITY event will automatically be added by the solver. + **Extends:** :class:`BinaryOperator` """ diff --git a/pybamm/expression_tree/concatenations.py b/pybamm/expression_tree/concatenations.py index 6bac3d5d60..e1225bf03a 100644 --- a/pybamm/expression_tree/concatenations.py +++ b/pybamm/expression_tree/concatenations.py @@ -38,6 +38,8 @@ def get_children_domains(self, children): # combine domains from children domain = [] for child in children: + if not isinstance(child, pybamm.Symbol): + raise TypeError("{} is not a pybamm symbol".format(child)) child_domain = child.domain if set(domain).isdisjoint(child_domain): domain += child_domain diff --git a/pybamm/expression_tree/exceptions.py b/pybamm/expression_tree/exceptions.py index cbeca72b5e..a71172cc48 100644 --- a/pybamm/expression_tree/exceptions.py +++ b/pybamm/expression_tree/exceptions.py @@ -37,6 +37,14 @@ class SolverError(Exception): pass +class SolverWarning(UserWarning): + """ + Solver warning: the chosen solver settings may not give the desired output + """ + + pass + + class ShapeError(Exception): """ Shape error: cannot evaluate an object to find its shape diff --git a/pybamm/expression_tree/operations/simplify.py b/pybamm/expression_tree/operations/simplify.py index 6e090db82d..df033cea40 100644 --- a/pybamm/expression_tree/operations/simplify.py +++ b/pybamm/expression_tree/operations/simplify.py @@ -22,8 +22,10 @@ def simplify_if_constant(symbol, keep_domains=False): if symbol.is_constant(): result = symbol.evaluate_ignoring_errors() if result is not None: - if isinstance(result, numbers.Number) or ( - isinstance(result, np.ndarray) and result.ndim == 0 + if ( + isinstance(result, numbers.Number) + or (isinstance(result, np.ndarray) and result.ndim == 0) + or isinstance(result, np.bool_) ): return pybamm.Scalar(result) elif isinstance(result, np.ndarray) or issparse(result): diff --git a/pybamm/expression_tree/parameter.py b/pybamm/expression_tree/parameter.py index 7000bca24b..9c2049ac8c 100644 --- a/pybamm/expression_tree/parameter.py +++ b/pybamm/expression_tree/parameter.py @@ -1,6 +1,7 @@ # # Parameter classes # +import numbers import numpy as np import pybamm @@ -60,10 +61,19 @@ def __init__(self, name, *children, diff_variable=None): # assign diff variable self.diff_variable = diff_variable children_list = list(children) + + # Turn numbers into scalars + for idx, child in enumerate(children_list): + if isinstance(child, numbers.Number): + children_list[idx] = pybamm.Scalar(child) + domain = self.get_children_domains(children_list) - auxiliary_domains = self.get_children_auxiliary_domains(children) + auxiliary_domains = self.get_children_auxiliary_domains(children_list) super().__init__( - name, children=children, domain=domain, auxiliary_domains=auxiliary_domains + name, + children=children_list, + domain=domain, + auxiliary_domains=auxiliary_domains, ) def set_id(self): diff --git a/pybamm/expression_tree/scalar.py b/pybamm/expression_tree/scalar.py index 975c770879..b96d618c70 100644 --- a/pybamm/expression_tree/scalar.py +++ b/pybamm/expression_tree/scalar.py @@ -2,6 +2,7 @@ # Scalar class # import pybamm +import numpy as np class Scalar(pybamm.Symbol): @@ -40,7 +41,7 @@ def value(self): @value.setter def value(self, value): - self._value = float(value) + self._value = np.float64(value) def set_id(self): """ See :meth:`pybamm.Symbol.set_id()`. """ diff --git a/pybamm/expression_tree/symbol.py b/pybamm/expression_tree/symbol.py index cd3ec77acf..3bc390cc26 100644 --- a/pybamm/expression_tree/symbol.py +++ b/pybamm/expression_tree/symbol.py @@ -285,9 +285,13 @@ def visualise(self, filename): new_node, counter = self.relabel_tree(self, 0) - DotExporter( - new_node, nodeattrfunc=lambda node: 'label="{}"'.format(node.label) - ).to_picture(filename) + try: + DotExporter( + new_node, nodeattrfunc=lambda node: 'label="{}"'.format(node.label) + ).to_picture(filename) + except FileNotFoundError: + # raise error but only through logger so that test passes + pybamm.logger.error("Please install graphviz>=2.42.2 to use dot exporter") def relabel_tree(self, symbol, counter): """ Finds all children of a symbol and assigns them a new id so that they can be diff --git a/pybamm/models/base_model.py b/pybamm/models/base_model.py index 0f5ff41aa5..d09d430460 100644 --- a/pybamm/models/base_model.py +++ b/pybamm/models/base_model.py @@ -46,9 +46,10 @@ class BaseModel(object): variables: dict A dictionary that maps strings to expressions that represent the useful variables - events: list - A list of events that should cause the solver to terminate (e.g. concentration - goes negative) + events: list of :class:`pybamm.Event` + A list of events. Each event can either cause the solver to terminate + (e.g. concentration goes negative), or be used to inform the solver of the + existance of a discontinuity (e.g. discontinuity in the input current) concatenated_rhs : :class:`pybamm.Concatenation` After discretisation, contains the expressions representing the rhs equations concatenated into a single expression @@ -105,7 +106,7 @@ def __init__(self, name="Unnamed model"): self._initial_conditions = {} self._boundary_conditions = {} self._variables = pybamm.FuzzyDict() - self._events = {} + self._events = [] self._concatenated_rhs = None self._concatenated_algebraic = None self._concatenated_initial_conditions = None @@ -303,6 +304,11 @@ def options(self): def options(self, options): self._options = options + @property + def timescale(self): + "Default timescale for a model is 1 second" + return pybamm.Scalar(1) + def __getitem__(self, key): return self.rhs[key] @@ -337,7 +343,7 @@ def update(self, *submodels): self._boundary_conditions, submodel.boundary_conditions ) self.variables.update(submodel.variables) # keys are strings so no check - self._events.update(submodel.events) + self._events += submodel.events def check_and_combine_dict(self, dict1, dict2): # check that the key ids are distinct diff --git a/pybamm/models/event.py b/pybamm/models/event.py new file mode 100644 index 0000000000..5a9cafb159 --- /dev/null +++ b/pybamm/models/event.py @@ -0,0 +1,66 @@ +from enum import Enum + + +class EventType(Enum): + """ + Defines the type of event, see :class:`pybamm.Event` + + TERMINATION indicates an event that will terminate the solver, the expression should + return 0 when the event is triggered + + DISCONTINUITY indicates an expected discontinuity in the solution, the expression + should return the time that the discontinuity occurs. The solver will integrate up + to the discontinuity and then restart just after the discontinuity. + + """ + TERMINATION = 0 + DISCONTINUITY = 1 + + +class Event: + """ + + Defines an event for use within a pybamm model + + Attributes + ---------- + + name: str + A string giving the name of the event + event_type: :class:`pybamm.EventType` + An enum defining the type of event + expression: :class:`pybamm.Symbol` + An expression that defines when the event occurs + + + """ + + def __init__(self, name, expression, event_type=EventType.TERMINATION): + self._name = name + self._expression = expression + self._event_type = event_type + + def evaluate(self, t=None, y=None, u=None, known_evals=None): + """ + Acts as a drop-in replacement for :func:`pybamm.Symbol.evaluate` + """ + return self._expression.evaluate(t, y, u, known_evals) + + def __str__(self): + return self._name + + @property + def name(self): + return self._name + + @property + def expression(self): + return self._expression + + @expression.setter + def expression(self, value): + self._expression = value + + @property + def event_type(self): + return self._event_type diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index 434dfb7450..a12bb7507f 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -253,6 +253,11 @@ def options(self, extra_options): self._options = options + @property + def timescale(self): + "Default timescale for a battery model is the discharge timescale" + return self.param.tau_discharge + def set_standard_output_variables(self): # Standard output variables @@ -767,8 +772,20 @@ def set_voltage_variables(self): # Cut-off voltage voltage = self.variables["Terminal voltage"] - self.events["Minimum voltage"] = voltage - self.param.voltage_low_cut - self.events["Maximum voltage"] = voltage - self.param.voltage_high_cut + self.events.append( + pybamm.Event( + "Minimum voltage", + voltage - self.param.voltage_low_cut, + pybamm.EventType.TERMINATION, + ) + ) + self.events.append( + pybamm.Event( + "Maximum voltage", + voltage - self.param.voltage_high_cut, + pybamm.EventType.TERMINATION, + ) + ) # Power I_dim = self.variables["Current [A]"] @@ -808,8 +825,11 @@ def process_parameters_and_discretise(self, symbol, parameter_values, disc): variables = list(self.rhs.keys()) + list(self.algebraic.keys()) disc.set_variable_slices(variables) - # Set boundary condtions + # Set boundary condtions (also requires setting parameter values) if disc.bcs == {}: + self.boundary_conditions = parameter_values.process_boundary_conditions( + self + ) disc.bcs = disc.process_boundary_conditions(self) # Process diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py b/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py index f395877b0d..16d17eef28 100644 --- a/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py +++ b/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py @@ -100,9 +100,15 @@ def __init__(self, name="Doyle-Fuller-Newman model"): # Porosity # Primary broadcasts are used to broadcast scalar quantities across a domain # into a vector of the right shape, for multiplying with other vectors - eps_n = pybamm.PrimaryBroadcast(param.epsilon_n, "negative electrode") - eps_s = pybamm.PrimaryBroadcast(param.epsilon_s, "separator") - eps_p = pybamm.PrimaryBroadcast(param.epsilon_p, "positive electrode") + eps_n = pybamm.PrimaryBroadcast( + pybamm.Parameter("Negative electrode porosity"), "negative electrode" + ) + eps_s = pybamm.PrimaryBroadcast( + pybamm.Parameter("Separator porosity"), "separator" + ) + eps_p = pybamm.PrimaryBroadcast( + pybamm.Parameter("Positive electrode porosity"), "positive electrode" + ) eps = pybamm.Concatenation(eps_n, eps_s, eps_p) # Tortuosity @@ -177,23 +183,27 @@ def __init__(self, name="Doyle-Fuller-Newman model"): "left": (pybamm.Scalar(0), "Neumann"), "right": (-param.C_p * j_p / param.a_p / param.gamma_p, "Neumann"), } - self.initial_conditions[c_s_n] = param.c_n_init - self.initial_conditions[c_s_p] = param.c_p_init - # Events specify points at which a solution should terminate - self.events.update( - { - "Minimum negative particle surface concentration": ( - pybamm.min(c_s_surf_n) - 0.01 - ), - "Maximum negative particle surface concentration": (1 - 0.01) - - pybamm.max(c_s_surf_n), - "Minimum positive particle surface concentration": ( - pybamm.min(c_s_surf_p) - 0.01 - ), - "Maximum positive particle surface concentration": (1 - 0.01) - - pybamm.max(c_s_surf_p), - } + # c_n_init and c_p_init can in general be functions of x + # Note the broadcasting, for domains + x_n = pybamm.PrimaryBroadcast( + pybamm.standard_spatial_vars.x_n, "negative particle" + ) + self.initial_conditions[c_s_n] = param.c_n_init(x_n) + x_p = pybamm.PrimaryBroadcast( + pybamm.standard_spatial_vars.x_p, "positive particle" ) + self.initial_conditions[c_s_p] = param.c_p_init(x_p) + # Events specify points at which a solution should terminate + self.events += [ + pybamm.Event("Minimum negative particle surface concentration", + pybamm.min(c_s_surf_n) - 0.01), + pybamm.Event("Maximum negative particle surface concentration", + (1 - 0.01) - pybamm.max(c_s_surf_n)), + pybamm.Event("Minimum positive particle surface concentration", + pybamm.min(c_s_surf_p) - 0.01), + pybamm.Event("Maximum positive particle surface concentration", + (1 - 0.01) - pybamm.max(c_s_surf_p)), + ] ###################### # Current in the solid ###################### @@ -215,10 +225,12 @@ def __init__(self, name="Doyle-Fuller-Newman model"): # Initial conditions must also be provided for algebraic equations, as an # initial guess for a root-finding algorithm which calculates consistent initial # conditions + # We evaluate c_n_init at x=0 and c_p_init at x=1 (this is just an initial + # guess so actual value is not too important) self.initial_conditions[phi_s_n] = pybamm.Scalar(0) self.initial_conditions[phi_s_p] = param.U_p( - param.c_p_init, param.T_init - ) - param.U_n(param.c_n_init, param.T_init) + param.c_p_init(1), param.T_init + ) - param.U_n(param.c_n_init(0), param.T_init) ###################### # Current in the electrolyte @@ -231,7 +243,7 @@ def __init__(self, name="Doyle-Fuller-Newman model"): "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[phi_e] = -param.U_n(param.c_n_init, param.T_init) + self.initial_conditions[phi_e] = -param.U_n(param.c_n_init(0), param.T_init) ###################### # Electrolyte concentration @@ -245,7 +257,8 @@ def __init__(self, name="Doyle-Fuller-Newman model"): "right": (pybamm.Scalar(0), "Neumann"), } self.initial_conditions[c_e] = param.c_e_init - self.events["Zero electrolyte concentration cut-off"] = pybamm.min(c_e) - 0.002 + self.events.append(pybamm.Event("Zero electrolyte concentration cut-off", + pybamm.min(c_e) - 0.002)) ###################### # (Some) variables @@ -263,8 +276,10 @@ def __init__(self, name="Doyle-Fuller-Newman model"): "Positive electrode potential": phi_s_p, "Terminal voltage": voltage, } - self.events["Minimum voltage"] = voltage - param.voltage_low_cut - self.events["Maximum voltage"] = voltage - param.voltage_high_cut + self.events += [ + pybamm.Event("Minimum voltage", voltage - param.voltage_low_cut), + pybamm.Event("Maximum voltage", voltage - param.voltage_high_cut), + ] @property def default_geometry(self): diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_spm.py b/pybamm/models/full_battery_models/lithium_ion/basic_spm.py index b907f56593..15d8e2ab92 100644 --- a/pybamm/models/full_battery_models/lithium_ion/basic_spm.py +++ b/pybamm/models/full_battery_models/lithium_ion/basic_spm.py @@ -89,28 +89,26 @@ def __init__(self, name="Single Particle Model"): "left": (pybamm.Scalar(0), "Neumann"), "right": (-param.C_p * j_p / param.a_p / param.gamma_p, "Neumann"), } - self.initial_conditions[c_s_n] = param.c_n_init - self.initial_conditions[c_s_p] = param.c_p_init + # c_n_init and c_p_init are functions, but for the SPM we evaluate them at x=0 + # and x=1 since there is no x-dependence in the particles + self.initial_conditions[c_s_n] = param.c_n_init(0) + self.initial_conditions[c_s_p] = param.c_p_init(1) # Surf takes the surface value of a variable, i.e. its boundary value on the # right side. This is also accessible via `boundary_value(x, "right")`, with # "left" providing the boundary value of the left side c_s_surf_n = pybamm.surf(c_s_n) c_s_surf_p = pybamm.surf(c_s_p) # Events specify points at which a solution should terminate - self.events.update( - { - "Minimum negative particle surface concentration": ( - pybamm.min(c_s_surf_n) - 0.01 - ), - "Maximum negative particle surface concentration": (1 - 0.01) - - pybamm.max(c_s_surf_n), - "Minimum positive particle surface concentration": ( - pybamm.min(c_s_surf_p) - 0.01 - ), - "Maximum positive particle surface concentration": (1 - 0.01) - - pybamm.max(c_s_surf_p), - } - ) + self.events += [ + pybamm.Event("Minimum negative particle surface concentration", + pybamm.min(c_s_surf_n) - 0.01), + pybamm.Event("Maximum negative particle surface concentration", + (1 - 0.01) - pybamm.max(c_s_surf_n)), + pybamm.Event("Minimum positive particle surface concentration", + pybamm.min(c_s_surf_p) - 0.01), + pybamm.Event("Maximum positive particle surface concentration", + (1 - 0.01) - pybamm.max(c_s_surf_p)), + ] # Note that the SPM does not have any algebraic equations, so the `algebraic` # dictionary remains empty @@ -164,8 +162,10 @@ def __init__(self, name="Single Particle Model"): ), "Terminal voltage": V, } - self.events["Minimum voltage"] = V - param.voltage_low_cut - self.events["Maximum voltage"] = V - param.voltage_high_cut + self.events += [ + pybamm.Event("Minimum voltage", V - param.voltage_low_cut), + pybamm.Event("Maximum voltage", V - param.voltage_high_cut), + ] @property def default_geometry(self): diff --git a/pybamm/models/submodels/base_submodel.py b/pybamm/models/submodels/base_submodel.py index 99f8568773..335fdb8e9e 100644 --- a/pybamm/models/submodels/base_submodel.py +++ b/pybamm/models/submodels/base_submodel.py @@ -40,10 +40,10 @@ class BaseSubModel: variables: dict A dictionary that maps strings to expressions that represent the useful variables - events: dict - A dictionary of events that should cause the solver to terminate (e.g. - concentration goes negative). The keys are strings and the values are - symbols. + events: list + A list of events. Each event can either cause the solver to terminate + (e.g. concentration goes negative), or be used to inform the solver of the + existance of a discontinuity (e.g. discontinuity in the input current) """ def __init__( @@ -63,7 +63,7 @@ def __init__( self.boundary_conditions = {} self.initial_conditions = {} self.variables = {} - self.events = {} + self.events = [] self.domain = domain self.set_domain_for_broadcast() diff --git a/pybamm/models/submodels/electrode/ohm/full_ohm.py b/pybamm/models/submodels/electrode/ohm/full_ohm.py index 7ed04dc09b..6ba51ab766 100644 --- a/pybamm/models/submodels/electrode/ohm/full_ohm.py +++ b/pybamm/models/submodels/electrode/ohm/full_ohm.py @@ -95,8 +95,8 @@ def set_initial_conditions(self, variables): if self.domain == "Negative": phi_s_init = pybamm.Scalar(0) elif self.domain == "Positive": - phi_s_init = self.param.U_p(self.param.c_p_init, T_init) - self.param.U_n( - self.param.c_n_init, T_init - ) + phi_s_init = self.param.U_p( + self.param.c_p_init(1), T_init + ) - self.param.U_n(self.param.c_n_init(0), T_init) self.initial_conditions[phi_s] = phi_s_init diff --git a/pybamm/models/submodels/electrolyte/base_electrolyte_diffusion.py b/pybamm/models/submodels/electrolyte/base_electrolyte_diffusion.py index 9f535f0d39..0f4df2a527 100644 --- a/pybamm/models/submodels/electrolyte/base_electrolyte_diffusion.py +++ b/pybamm/models/submodels/electrolyte/base_electrolyte_diffusion.py @@ -105,4 +105,8 @@ def _get_standard_flux_variables(self, N_e): def set_events(self, variables): c_e = variables["Electrolyte concentration"] - self.events["Zero electrolyte concentration cut-off"] = pybamm.min(c_e) - 0.002 + self.events.append(pybamm.Event( + "Zero electrolyte concentration cut-off", + pybamm.min(c_e) - 0.002, + pybamm.EventType.TERMINATION + )) diff --git a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/full_stefan_maxwell_conductivity.py index e381f0daad..e15320b273 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 @@ -64,4 +64,6 @@ def set_algebraic(self, variables): def set_initial_conditions(self, variables): phi_e = variables["Electrolyte potential"] T_init = self.param.T_init - self.initial_conditions = {phi_e: -self.param.U_n(self.param.c_n_init, T_init)} + self.initial_conditions = { + phi_e: -self.param.U_n(self.param.c_n_init(0), T_init) + } 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 75916253f7..945437ac41 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 @@ -45,9 +45,9 @@ def set_initial_conditions(self, variables): delta_phi_e = variables[self.domain + " electrode surface potential difference"] if self.domain == "Negative": - delta_phi_e_init = self.param.U_n(self.param.c_n_init, self.param.T_init) + delta_phi_e_init = self.param.U_n(self.param.c_n_init(0), self.param.T_init) elif self.domain == "Positive": - delta_phi_e_init = self.param.U_p(self.param.c_p_init, self.param.T_init) + delta_phi_e_init = self.param.U_p(self.param.c_p_init(1), self.param.T_init) self.initial_conditions = {delta_phi_e: delta_phi_e_init} diff --git a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/surface_potential_form/leading_surface_form_stefan_maxwell_conductivity.py b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/surface_potential_form/leading_surface_form_stefan_maxwell_conductivity.py index a264497520..8d46bdb670 100644 --- a/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/surface_potential_form/leading_surface_form_stefan_maxwell_conductivity.py +++ b/pybamm/models/submodels/electrolyte/stefan_maxwell/conductivity/surface_potential_form/leading_surface_form_stefan_maxwell_conductivity.py @@ -49,9 +49,9 @@ def set_initial_conditions(self, variables): + " electrode surface potential difference" ] if self.domain == "Negative": - delta_phi_init = self.param.U_n(self.param.c_n_init, self.param.T_init) + delta_phi_init = self.param.U_n(self.param.c_n_init(0), self.param.T_init) elif self.domain == "Positive": - delta_phi_init = self.param.U_p(self.param.c_p_init, self.param.T_init) + delta_phi_init = self.param.U_p(self.param.c_p_init(1), self.param.T_init) self.initial_conditions = {delta_phi: delta_phi_init} diff --git a/pybamm/models/submodels/external_circuit/function_control_external_circuit.py b/pybamm/models/submodels/external_circuit/function_control_external_circuit.py index fb1dd39f6b..6e3812f99a 100644 --- a/pybamm/models/submodels/external_circuit/function_control_external_circuit.py +++ b/pybamm/models/submodels/external_circuit/function_control_external_circuit.py @@ -8,9 +8,9 @@ class FunctionControl(BaseModel): """External circuit with an arbitrary function. """ - def __init__(self, param, external_circuit_class): + def __init__(self, param, external_circuit_function): super().__init__(param) - self.external_circuit_class = external_circuit_class + self.external_circuit_function = external_circuit_function def _get_current_variable(self): return pybamm.Variable("Total current density") @@ -23,14 +23,6 @@ def get_fundamental_variables(self): # Add discharge capacity variable variables.update(super().get_fundamental_variables()) - # Add switches - # These are not implemented yet but can be used later with the Experiment class - # to simulate different external circuit conditions sequentially within a - # single model (for example Constant Current - Constant Voltage) - # for i in range(self.external_circuit_class.num_switches): - # s = pybamm.Parameter("Switch {}".format(i + 1)) - # variables["Switch {}".format(i + 1)] = s - return variables def set_initial_conditions(self, variables): @@ -44,7 +36,7 @@ def set_algebraic(self, variables): # The external circuit function should fix either the current, or the voltage, # or a combination (e.g. I*V for power control) i_cell = variables["Total current density"] - self.algebraic[i_cell] = self.external_circuit_class(variables) + self.algebraic[i_cell] = self.external_circuit_function(variables) class VoltageFunctionControl(FunctionControl): @@ -53,31 +45,25 @@ class VoltageFunctionControl(FunctionControl): """ def __init__(self, param): - super().__init__(param, ConstantVoltage()) + super().__init__(param, constant_voltage) -class ConstantVoltage: - num_switches = 0 - - def __call__(self, variables): - V = variables["Terminal voltage [V]"] - return V - pybamm.FunctionParameter("Voltage function [V]", pybamm.t) +def constant_voltage(variables): + V = variables["Terminal voltage [V]"] + return V - pybamm.FunctionParameter("Voltage function [V]", pybamm.t) class PowerFunctionControl(FunctionControl): """External circuit with power control. """ def __init__(self, param): - super().__init__(param, ConstantPower()) - + super().__init__(param, constant_power) -class ConstantPower: - num_switches = 0 - def __call__(self, variables): - I = variables["Current [A]"] - V = variables["Terminal voltage [V]"] - return I * V - pybamm.FunctionParameter("Power function [W]", pybamm.t) +def constant_power(variables): + I = variables["Current [A]"] + V = variables["Terminal voltage [V]"] + return I * V - pybamm.FunctionParameter("Power function [W]", pybamm.t) class LeadingOrderFunctionControl(FunctionControl, LeadingOrderBaseModel): @@ -97,12 +83,12 @@ class LeadingOrderVoltageFunctionControl(LeadingOrderFunctionControl): """ def __init__(self, param): - super().__init__(param, ConstantVoltage()) + super().__init__(param, constant_voltage) class LeadingOrderPowerFunctionControl(LeadingOrderFunctionControl): """External circuit with power control, at leading order. """ def __init__(self, param): - super().__init__(param, ConstantPower()) + super().__init__(param, constant_power) diff --git a/pybamm/models/submodels/particle/base_particle.py b/pybamm/models/submodels/particle/base_particle.py index bbc3cc4878..f4234d6458 100644 --- a/pybamm/models/submodels/particle/base_particle.py +++ b/pybamm/models/submodels/particle/base_particle.py @@ -70,31 +70,23 @@ def _get_standard_flux_variables(self, N_s, N_s_xav): return variables - def _flux_law(self, c, T): - raise NotImplementedError - - def _unpack(self, variables): - raise NotImplementedError - - def set_initial_conditions(self, variables): - c, _, _ = self._unpack(variables) - - if self.domain == "Negative": - c_init = self.param.c_n_init - - elif self.domain == "Positive": - c_init = self.param.c_p_init - - self.initial_conditions = {c: c_init} - def set_events(self, variables): c_s_surf = variables[self.domain + " particle surface concentration"] tol = 0.01 - self.events[ - "Minimum " + self.domain.lower() + " particle surface concentration" - ] = (pybamm.min(c_s_surf) - tol) + self.events.append( + pybamm.Event( + "Minumum " + self.domain.lower() + " particle surface concentration", + pybamm.min(c_s_surf) - tol, + pybamm.EventType.TERMINATION, + ) + ) + + self.events.append( + pybamm.Event( + "Maximum " + self.domain.lower() + " particle surface concentration", + (1 - tol) - pybamm.max(c_s_surf), + pybamm.EventType.TERMINATION, + ) + ) - self.events[ - "Maximum " + self.domain.lower() + " particle surface concentration" - ] = (1 - tol) - pybamm.max(c_s_surf) diff --git a/pybamm/models/submodels/particle/fast/base_fast_particle.py b/pybamm/models/submodels/particle/fast/base_fast_particle.py index 9c7a8870c6..a39e00b519 100644 --- a/pybamm/models/submodels/particle/fast/base_fast_particle.py +++ b/pybamm/models/submodels/particle/fast/base_fast_particle.py @@ -2,7 +2,6 @@ # Base class for particles each with uniform concentration (i.e. infinitely fast # diffusion in r) # - from ..base_particle import BaseParticle diff --git a/pybamm/models/submodels/particle/fast/fast_many_particles.py b/pybamm/models/submodels/particle/fast/fast_many_particles.py index fe23a4af8e..b1ca21a7f1 100644 --- a/pybamm/models/submodels/particle/fast/fast_many_particles.py +++ b/pybamm/models/submodels/particle/fast/fast_many_particles.py @@ -69,3 +69,16 @@ def _unpack(self, variables): j = variables[self.domain + " electrode interfacial current density"] return c_s_surf, N_s, j + + def set_initial_conditions(self, variables): + c, _, _ = self._unpack(variables) + + if self.domain == "Negative": + x_n = pybamm.standard_spatial_vars.x_n + c_init = self.param.c_n_init(x_n) + + elif self.domain == "Positive": + x_p = pybamm.standard_spatial_vars.x_p + c_init = self.param.c_p_init(x_p) + + self.initial_conditions = {c: c_init} diff --git a/pybamm/models/submodels/particle/fast/fast_single_particle.py b/pybamm/models/submodels/particle/fast/fast_single_particle.py index 925229086b..3cfc6c9d6a 100644 --- a/pybamm/models/submodels/particle/fast/fast_single_particle.py +++ b/pybamm/models/submodels/particle/fast/fast_single_particle.py @@ -77,3 +77,19 @@ def _unpack(self, variables): ] return c_s_surf_xav, N_s_xav, j_av + + def set_initial_conditions(self, variables): + """ + For single particle models, initial conditions can't depend on x so we + arbitrarily evaluate them at x=0 in the negative electrode and x=1 in the + positive electrode (they will usually be constant) + """ + c, _, _ = self._unpack(variables) + + if self.domain == "Negative": + c_init = self.param.c_n_init(0) + + elif self.domain == "Positive": + c_init = self.param.c_p_init(1) + + self.initial_conditions = {c: c_init} diff --git a/pybamm/models/submodels/particle/fickian/__init__.py b/pybamm/models/submodels/particle/fickian/__init__.py index f45a889c89..86a9ba03be 100644 --- a/pybamm/models/submodels/particle/fickian/__init__.py +++ b/pybamm/models/submodels/particle/fickian/__init__.py @@ -1,3 +1,2 @@ -from .base_fickian_particle import BaseModel from .fickian_many_particles import ManyParticles from .fickian_single_particle import SingleParticle diff --git a/pybamm/models/submodels/particle/fickian/base_fickian_particle.py b/pybamm/models/submodels/particle/fickian/base_fickian_particle.py deleted file mode 100644 index 368927c5c1..0000000000 --- a/pybamm/models/submodels/particle/fickian/base_fickian_particle.py +++ /dev/null @@ -1,50 +0,0 @@ -# -# Base class for particles with Fickian diffusion -# -import pybamm - -from ..base_particle import BaseParticle - - -class BaseModel(BaseParticle): - """Base class for molar conservation in particles which employ Fick's law. - - Parameters - ---------- - param : parameter class - The parameters to use for this submodel - domain : str - The domain of the model either 'Negative' or 'Positive' - - - **Extends:** :class:`pybamm.particle.BaseParticle` - """ - - def __init__(self, param, domain): - super().__init__(param, domain) - - def _flux_law(self, c, T): - - if self.domain == "Negative": - D = self.param.D_n(c, T) - elif self.domain == "Positive": - D = self.param.D_p(c, T) - - return -D * pybamm.grad(c) - - def _unpack(self, variables): - raise NotImplementedError - - def set_boundary_conditions(self, variables): - - c, _, j = self._unpack(variables) - - if self.domain == "Negative": - rbc = -self.param.C_n * j / self.param.a_n - - elif self.domain == "Positive": - rbc = -self.param.C_p * j / self.param.a_p / self.param.gamma_p - - self.boundary_conditions = { - c: {"left": (pybamm.Scalar(0), "Neumann"), "right": (rbc, "Neumann")} - } diff --git a/pybamm/models/submodels/particle/fickian/fickian_many_particles.py b/pybamm/models/submodels/particle/fickian/fickian_many_particles.py index 8b0d9d40cd..87677fde30 100644 --- a/pybamm/models/submodels/particle/fickian/fickian_many_particles.py +++ b/pybamm/models/submodels/particle/fickian/fickian_many_particles.py @@ -2,10 +2,10 @@ # Class for many particles with Fickian diffusion # import pybamm -from .base_fickian_particle import BaseModel +from ..base_particle import BaseParticle -class ManyParticles(BaseModel): +class ManyParticles(BaseParticle): """Base class for molar conservation in many particles which employs Fick's law. @@ -17,7 +17,7 @@ class ManyParticles(BaseModel): The domain of the model either 'Negative' or 'Positive' - **Extends:** :class:`pybamm.particle.fickian.BaseModel` + **Extends:** :class:`pybamm.particle.BaseParticle` """ def __init__(self, param, domain): @@ -43,7 +43,10 @@ def get_coupled_variables(self, variables): [self.domain.lower() + " particle"], ) - N_s = self._flux_law(c_s, T_k) + if self.domain == "Negative": + N_s = -self.param.D_n(c_s, T_k) * pybamm.grad(c_s) + elif self.domain == "Positive": + N_s = -self.param.D_p(c_s, T_k) * pybamm.grad(c_s) variables.update(self._get_standard_flux_variables(N_s, N_s)) @@ -56,23 +59,57 @@ def get_coupled_variables(self, variables): x = pybamm.standard_spatial_vars.x_p R = pybamm.FunctionParameter("Positive particle distribution in x", x) variables.update({"Positive particle distribution in x": R}) + return variables def set_rhs(self, variables): - - c, N, _ = self._unpack(variables) + c_s = variables[self.domain + " particle concentration"] + N_s = variables[self.domain + " particle flux"] if self.domain == "Negative": R = variables["Negative particle distribution in x"] - self.rhs = {c: -(1 / (R ** 2 * self.param.C_n)) * pybamm.div(N)} + self.rhs = {c_s: -(1 / (R ** 2 * self.param.C_n)) * pybamm.div(N_s)} elif self.domain == "Positive": R = variables["Positive particle distribution in x"] - self.rhs = {c: -(1 / (R ** 2 * self.param.C_p)) * pybamm.div(N)} + self.rhs = {c_s: -(1 / (R ** 2 * self.param.C_p)) * pybamm.div(N_s)} + + def set_boundary_conditions(self, variables): - def _unpack(self, variables): c_s = variables[self.domain + " particle concentration"] - N_s = variables[self.domain + " particle flux"] + c_s_surf = variables[self.domain + " particle surface concentration"] + T_k = variables[self.domain + " electrode temperature"] j = variables[self.domain + " electrode interfacial current density"] - return c_s, N_s, j + if self.domain == "Negative": + rbc = -self.param.C_n * j / self.param.a_n / self.param.D_n(c_s_surf, T_k) + + elif self.domain == "Positive": + rbc = ( + -self.param.C_p + * j + / self.param.a_p + / self.param.gamma_p + / self.param.D_p(c_s_surf, T_k) + ) + + self.boundary_conditions = { + c_s: {"left": (pybamm.Scalar(0), "Neumann"), "right": (rbc, "Neumann")} + } + + def set_initial_conditions(self, variables): + c_s = variables[self.domain + " particle concentration"] + + if self.domain == "Negative": + x_n = pybamm.PrimaryBroadcast( + pybamm.standard_spatial_vars.x_n, "negative particle" + ) + c_init = self.param.c_n_init(x_n) + + elif self.domain == "Positive": + x_p = pybamm.PrimaryBroadcast( + pybamm.standard_spatial_vars.x_p, "positive particle" + ) + c_init = self.param.c_p_init(x_p) + + self.initial_conditions = {c_s: c_init} diff --git a/pybamm/models/submodels/particle/fickian/fickian_single_particle.py b/pybamm/models/submodels/particle/fickian/fickian_single_particle.py index 9715e2e1c4..84579de66c 100644 --- a/pybamm/models/submodels/particle/fickian/fickian_single_particle.py +++ b/pybamm/models/submodels/particle/fickian/fickian_single_particle.py @@ -3,10 +3,10 @@ # import pybamm -from .base_fickian_particle import BaseModel +from ..base_particle import BaseParticle -class SingleParticle(BaseModel): +class SingleParticle(BaseParticle): """Base class for molar conservation in a single x-averaged particle which employs Fick's law. @@ -18,7 +18,7 @@ class SingleParticle(BaseModel): The domain of the model either 'Negative' or 'Positive' - **Extends:** :class:`pybamm.particle.fickian.BaseModel` + **Extends:** :class:`pybamm.particle.BaseParticle` """ def __init__(self, param, domain): @@ -41,11 +41,17 @@ def get_coupled_variables(self, variables): c_s_xav = variables[ "X-averaged " + self.domain.lower() + " particle concentration" ] + T_k_xav = pybamm.PrimaryBroadcast( variables["X-averaged " + self.domain.lower() + " electrode temperature"], [self.domain.lower() + " particle"], ) - N_s_xav = self._flux_law(c_s_xav, T_k_xav) + + if self.domain == "Negative": + N_s_xav = -self.param.D_n(c_s_xav, T_k_xav) * pybamm.grad(c_s_xav) + elif self.domain == "Positive": + N_s_xav = -self.param.D_p(c_s_xav, T_k_xav) * pybamm.grad(c_s_xav) + N_s = pybamm.SecondaryBroadcast(N_s_xav, [self._domain.lower() + " electrode"]) variables.update(self._get_standard_flux_variables(N_s, N_s_xav)) @@ -53,24 +59,73 @@ def get_coupled_variables(self, variables): return variables def set_rhs(self, variables): + c_s_xav = variables[ + "X-averaged " + self.domain.lower() + " particle concentration" + ] - c, N, _ = self._unpack(variables) + N_s_xav = variables["X-averaged " + self.domain.lower() + " particle flux"] if self.domain == "Negative": - self.rhs = {c: -(1 / self.param.C_n) * pybamm.div(N)} - + self.rhs = {c_s_xav: -(1 / self.param.C_n) * pybamm.div(N_s_xav)} elif self.domain == "Positive": - self.rhs = {c: -(1 / self.param.C_p) * pybamm.div(N)} + self.rhs = {c_s_xav: -(1 / self.param.C_p) * pybamm.div(N_s_xav)} - def _unpack(self, variables): + def set_boundary_conditions(self, variables): c_s_xav = variables[ "X-averaged " + self.domain.lower() + " particle concentration" ] - N_s_xav = variables["X-averaged " + self.domain.lower() + " particle flux"] - j_av = variables[ + + c_s_surf_xav = variables[ + "X-averaged " + self.domain.lower() + " particle surface concentration" + ] + + T_k_xav = variables[ + "X-averaged " + self.domain.lower() + " electrode temperature" + ] + + j_xav = variables[ "X-averaged " + self.domain.lower() + " electrode interfacial current density" ] - return c_s_xav, N_s_xav, j_av + if self.domain == "Negative": + rbc = ( + -self.param.C_n + * j_xav + / self.param.a_n + / self.param.D_n(c_s_surf_xav, T_k_xav) + ) + + elif self.domain == "Positive": + rbc = ( + -self.param.C_p + * j_xav + / self.param.a_p + / self.param.gamma_p + / self.param.D_p(c_s_surf_xav, T_k_xav) + ) + + self.boundary_conditions = { + c_s_xav: {"left": (pybamm.Scalar(0), "Neumann"), "right": (rbc, "Neumann")} + } + + def set_initial_conditions(self, variables): + """ + For single particle models, initial conditions can't depend on x so we + arbitrarily set the initial values of the single particles to be given + by the values at x=0 in the negative electrode and x=1 in the + positive electrode. Typically, supplied initial conditions are uniform + x. + """ + c_s_xav = variables[ + "X-averaged " + self.domain.lower() + " particle concentration" + ] + + if self.domain == "Negative": + c_init = self.param.c_n_init(0) + + elif self.domain == "Positive": + c_init = self.param.c_p_init(1) + + self.initial_conditions = {c_s_xav: c_init} diff --git a/pybamm/models/submodels/porosity/base_porosity.py b/pybamm/models/submodels/porosity/base_porosity.py index 72a5082fce..1aebacf3df 100644 --- a/pybamm/models/submodels/porosity/base_porosity.py +++ b/pybamm/models/submodels/porosity/base_porosity.py @@ -96,7 +96,25 @@ def _get_standard_porosity_change_variables(self, deps_dt, set_leading_order=Fal def set_events(self, variables): eps_n = variables["Negative electrode porosity"] eps_p = variables["Positive electrode porosity"] - self.events["Zero negative electrode porosity cut-off"] = pybamm.min(eps_n) - self.events["Max negative electrode porosity cut-off"] = pybamm.max(eps_n) - 1 - self.events["Zero positive electrode porosity cut-off"] = pybamm.min(eps_p) - self.events["Max positive electrode porosity cut-off"] = pybamm.max(eps_p) - 1 + self.events.append(pybamm.Event( + "Zero negative electrode porosity cut-off", + pybamm.min(eps_n), + pybamm.EventType.TERMINATION + )) + self.events.append(pybamm.Event( + "Max negative electrode porosity cut-off", + pybamm.max(eps_n) - 1, + pybamm.EventType.TERMINATION + )) + + self.events.append(pybamm.Event( + "Zero positive electrode porosity cut-off", + pybamm.min(eps_p), + pybamm.EventType.TERMINATION + )) + + self.events.append(pybamm.Event( + "Max positive electrode porosity cut-off", + pybamm.max(eps_p) - 1, + pybamm.EventType.TERMINATION + )) diff --git a/pybamm/models/submodels/porosity/constant_porosity.py b/pybamm/models/submodels/porosity/constant_porosity.py index c487957f2b..da07259ac5 100644 --- a/pybamm/models/submodels/porosity/constant_porosity.py +++ b/pybamm/models/submodels/porosity/constant_porosity.py @@ -20,17 +20,10 @@ class Constant(BaseModel): def get_fundamental_variables(self): - eps_n_av = self.param.epsilon_n - eps_s_av = self.param.epsilon_s - eps_p_av = self.param.epsilon_p + eps_n = self.param.epsilon_n + eps_s = self.param.epsilon_s + eps_p = self.param.epsilon_p - eps_n = pybamm.FullBroadcast( - eps_n_av, "negative electrode", "current collector" - ) - eps_s = pybamm.FullBroadcast(eps_s_av, "separator", "current collector") - eps_p = pybamm.FullBroadcast( - eps_p_av, "positive electrode", "current collector" - ) eps = pybamm.Concatenation(eps_n, eps_s, eps_p) deps_n_dt = pybamm.FullBroadcast(0, "negative electrode", "current collector") diff --git a/pybamm/parameters/electrical_parameters.py b/pybamm/parameters/electrical_parameters.py index 7e9d1ecfac..c83405ca5a 100644 --- a/pybamm/parameters/electrical_parameters.py +++ b/pybamm/parameters/electrical_parameters.py @@ -13,6 +13,7 @@ n_electrodes_parallel = pybamm.Parameter( "Number of electrodes connected in parallel to make a cell" ) +n_cells = pybamm.Parameter("Number of cells connected in series to make a battery") i_typ = pybamm.Function( np.abs, I_typ / (n_electrodes_parallel * pybamm.geometric_parameters.A_cc) ) diff --git a/pybamm/parameters/parameter_sets.py b/pybamm/parameters/parameter_sets.py index 31c09106cc..e7cd8cbaa2 100644 --- a/pybamm/parameters/parameter_sets.py +++ b/pybamm/parameters/parameter_sets.py @@ -11,6 +11,11 @@ Chapman. "An asymptotic derivation of a single particle model with electrolyte." `arXiv preprint arXiv:1905.12553 `_ (2019). +NCA_Kim2011 + Kim, G. H., Smith, K., Lee, K. J., Santhanagopalan, S., & Pesaran, A. + (2011). Multi-domain modeling of lithium-ion batteries encompassing + multi-physics in varied length scales. Journal of The Electrochemical + Society, 158(8), A955-A969. Lead-acid --------- @@ -34,6 +39,16 @@ "experiment": "1C_discharge_from_full_Marquis2019", } +NCA_Kim2011 = { + "chemistry": "lithium-ion", + "cell": "Kim2011", + "anode": "graphite_Kim2011", + "separator": "separator_Kim2011", + "cathode": "nca_Kim2011", + "electrolyte": "lipf6_Kim2011", + "experiment": "1C_discharge_from_full_Kim2011", +} + # # Lead-acid # diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py index 0a8b972e8d..aeb0fe7fa5 100644 --- a/pybamm/parameters/parameter_values.py +++ b/pybamm/parameters/parameter_values.py @@ -227,7 +227,7 @@ def update(self, values, check_conflict=False, check_already_exists=True, path=" filename = os.path.join(path, value[6:] + ".csv") function_name = value[6:] data = pd.read_csv( - filename, comment="#", skip_blank_lines=True + filename, comment="#", skip_blank_lines=True, header=None ).to_numpy() # Save name and data self._dict_items[name] = (function_name, data) @@ -350,10 +350,32 @@ def process_model(self, unprocessed_model, inplace=True): ) model.initial_conditions[variable] = self.process_symbol(equation) - # Boundary conditions are dictionaries {"left": left bc, "right": right bc} - # in general, but may be imposed on the tabs (or *not* on the tab) for a - # small number of variables, e.g. {"negative tab": neg. tab bc, - # "positive tab": pos. tab bc "no tab": no tab bc}. + model.boundary_conditions = self.process_boundary_conditions(model) + + for variable, equation in model.variables.items(): + pybamm.logger.debug( + "Processing parameters for {!r} (variables)".format(variable) + ) + model.variables[variable] = self.process_symbol(equation) + + for event in model.events: + pybamm.logger.debug( + "Processing parameters for event'{}''".format(event.name) + ) + event.expression = self.process_symbol(event.expression) + + pybamm.logger.info("Finish setting parameters for {}".format(model.name)) + + return model + + def process_boundary_conditions(self, model): + """ + Process boundary conditions for a model + Boundary conditions are dictionaries {"left": left bc, "right": right bc} + in general, but may be imposed on the tabs (or *not* on the tab) for a + small number of variables, e.g. {"negative tab": neg. tab bc, + "positive tab": pos. tab bc "no tab": no tab bc}. + """ new_boundary_conditions = {} sides = ["left", "right", "negative tab", "positive tab", "no tab"] for variable, bcs in model.boundary_conditions.items(): @@ -376,20 +398,7 @@ def process_model(self, unprocessed_model, inplace=True): else: raise KeyError(err) - model.boundary_conditions = new_boundary_conditions - - for variable, equation in model.variables.items(): - pybamm.logger.debug( - "Processing parameters for {!r} (variables)".format(variable) - ) - model.variables[variable] = self.process_symbol(equation) - for event, equation in model.events.items(): - pybamm.logger.debug("Processing parameters for event '{}''".format(event)) - model.events[event] = self.process_symbol(equation) - - pybamm.logger.info("Finish setting parameters for {}".format(model.name)) - - return model + return new_boundary_conditions def update_model(self, model, disc): raise NotImplementedError( diff --git a/pybamm/parameters/standard_parameters_lead_acid.py b/pybamm/parameters/standard_parameters_lead_acid.py index d0d7bab989..e48faefcb1 100644 --- a/pybamm/parameters/standard_parameters_lead_acid.py +++ b/pybamm/parameters/standard_parameters_lead_acid.py @@ -420,9 +420,13 @@ def U_p_dimensional(c_e, T): # hack to make consistent ic with lithium-ion -# find a way to not have to do this -c_n_init = c_e_init -c_p_init = c_e_init +def c_n_init(x): + return c_e_init + + +def c_p_init(x): + return c_e_init + # Thermal effects not implemented for lead-acid, but parameters needed for consistency T_init = pybamm.Scalar(0) diff --git a/pybamm/parameters/standard_parameters_lithium_ion.py b/pybamm/parameters/standard_parameters_lithium_ion.py index e03a4e1f48..17d143fb7b 100644 --- a/pybamm/parameters/standard_parameters_lithium_ion.py +++ b/pybamm/parameters/standard_parameters_lithium_ion.py @@ -104,12 +104,21 @@ c_e_init_dimensional = pybamm.Parameter( "Initial concentration in electrolyte [mol.m-3]" ) -c_n_init_dimensional = pybamm.Parameter( - "Initial concentration in negative electrode [mol.m-3]" -) -c_p_init_dimensional = pybamm.Parameter( - "Initial concentration in positive electrode [mol.m-3]" -) + + +def c_n_init_dimensional(x): + "Initial concentration as a function of dimensionless position x" + return pybamm.FunctionParameter( + "Initial concentration in negative electrode [mol.m-3]", x + ) + + +def c_p_init_dimensional(x): + "Initial concentration as a function of dimensionless position x" + return pybamm.FunctionParameter( + "Initial concentration in positive electrode [mol.m-3]", x + ) + # thermal Delta_T = pybamm.thermal_parameters.Delta_T @@ -279,14 +288,11 @@ def U_p_dimensional(sto, T): centre_z_tab_p = pybamm.geometric_parameters.centre_z_tab_p # Microscale geometry -epsilon_n = pybamm.Parameter("Negative electrode porosity") -epsilon_s = pybamm.Parameter("Separator porosity") -epsilon_p = pybamm.Parameter("Positive electrode porosity") -epsilon = pybamm.Concatenation( - pybamm.FullBroadcast(epsilon_n, ["negative electrode"], "current collector"), - pybamm.FullBroadcast(epsilon_s, ["separator"], "current collector"), - pybamm.FullBroadcast(epsilon_p, ["positive electrode"], "current collector"), -) +var = pybamm.standard_spatial_vars +epsilon_n = pybamm.FunctionParameter("Negative electrode porosity", var.x_n) +epsilon_s = pybamm.FunctionParameter("Separator porosity", var.x_s) +epsilon_p = pybamm.FunctionParameter("Positive electrode porosity", var.x_p) +epsilon = pybamm.Concatenation(epsilon_n, epsilon_s, epsilon_p) epsilon_s_n = pybamm.Parameter("Negative electrode active material volume fraction") epsilon_s_p = pybamm.Parameter("Positive electrode active material volume fraction") epsilon_inactive_n = 1 - epsilon_n - epsilon_s_n @@ -367,10 +373,18 @@ def chi(c_e): ) # Initial conditions -c_e_init = c_e_init_dimensional / c_e_typ -c_n_init = c_n_init_dimensional / c_n_max -c_p_init = c_p_init_dimensional / c_p_max T_init = pybamm.thermal_parameters.T_init +c_e_init = c_e_init_dimensional / c_e_typ + + +def c_n_init(x): + "Dimensionless initial concentration as a function of dimensionless position x" + return c_n_init_dimensional(x) / c_n_max + + +def c_p_init(x): + "Dimensionless initial concentration as a function of dimensionless position x" + return c_p_init_dimensional(x) / c_p_max # -------------------------------------------------------------------------------------- diff --git a/pybamm/simulation.py b/pybamm/simulation.py index d94e47e7c7..9f3f02069c 100644 --- a/pybamm/simulation.py +++ b/pybamm/simulation.py @@ -5,6 +5,8 @@ import pybamm import numpy as np import copy +import warnings +import sys def isnotebook(): @@ -20,6 +22,26 @@ def isnotebook(): return False # Probably standard Python interpreter +def constant_current_constant_voltage_constant_power(variables): + I = variables["Current [A]"] + V = variables["Terminal voltage [V]"] + s_I = pybamm.InputParameter("Current switch") + s_V = pybamm.InputParameter("Voltage switch") + s_P = pybamm.InputParameter("Power switch") + n_electrodes_parallel = pybamm.electrical_parameters.n_electrodes_parallel + n_cells = pybamm.electrical_parameters.n_cells + return ( + s_I * (I - pybamm.InputParameter("Current input [A]") / n_electrodes_parallel) + + s_V * (V - pybamm.InputParameter("Voltage input [V]") / n_cells) + + s_P + * ( + V * I + - pybamm.InputParameter("Power input [W]") + / (n_cells * n_electrodes_parallel) + ) + ) + + class Simulation: """A Simulation class for easy building and running of PyBaMM simulations. @@ -27,6 +49,8 @@ class Simulation: ---------- model : :class:`pybamm.BaseModel` The model to be simulated + experiment : : class:`pybamm.Experiment` (optional) + The experimental conditions under which to solve the model geometry: :class:`pybamm.Geometry` (optional) The geometry upon which to solve the model parameter_values: dict (optional) @@ -52,6 +76,7 @@ class Simulation: def __init__( self, model, + experiment=None, geometry=None, parameter_values=None, submesh_types=None, @@ -61,19 +86,23 @@ def __init__( quick_plot_vars=None, C_rate=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.C_rate = C_rate - if self.C_rate: - self._parameter_values.update({"C-rate": self.C_rate}) + if experiment is None: + self.operating_mode = "without experiment" + self.C_rate = C_rate + if self.C_rate: + self._parameter_values.update({"C-rate": self.C_rate}) + self.model = model + else: + self.set_up_experiment(model, experiment) + + self.geometry = geometry or self.model.default_geometry + self._submesh_types = submesh_types or self.model.default_submesh_types + self._var_pts = var_pts or self.model.default_var_pts + self._spatial_methods = spatial_methods or self.model.default_spatial_methods + self._solver = solver or self.model.default_solver + self._quick_plot_vars = quick_plot_vars self.reset(update_model=False) @@ -83,6 +112,132 @@ def __init__( warnings.filterwarnings("ignore") + def set_up_experiment(self, model, experiment): + """ + Set up a simulation to run with an experiment. This creates a dictionary of + inputs (current/voltage/power, running time, stopping condition) for each + operating condition in the experiment. The model will then be solved by + integrating the model successively with each group of inputs, one group at a + time. + """ + self.operating_mode = "with experiment" + self.model = model.new_copy( + options={ + **model.options, + "operating mode": constant_current_constant_voltage_constant_power, + } + ) + if not isinstance(experiment, pybamm.Experiment): + raise TypeError("experiment must be a pybamm `Experiment` instance") + # Save the experiment + self.experiment = experiment + # Update parameter values with experiment parameters + self._parameter_values.update(experiment.parameters) + # Create a new submodel for each set of operating conditions and update + # parameters and events accordingly + self._experiment_inputs = [] + self._experiment_times = [] + for op, events in zip(experiment.operating_conditions, experiment.events): + if op[1] in ["A", "C"]: + # Update inputs for constant current + if op[1] == "A": + I = op[0] + else: + # Scale C-rate with capacity to obtain current + capacity = self._parameter_values["Cell capacity [A.h]"] + I = op[0] * capacity + operating_inputs = { + "Current switch": 1, + "Voltage switch": 0, + "Power switch": 0, + "Current input [A]": I, + "Voltage input [V]": 0, # doesn't matter + "Power input [W]": 0, # doesn't matter + } + elif op[1] == "V": + # Update inputs for constant voltage + V = op[0] + operating_inputs = { + "Current switch": 0, + "Voltage switch": 1, + "Power switch": 0, + "Current input [A]": 0, # doesn't matter + "Voltage input [V]": V, + "Power input [W]": 0, # doesn't matter + } + elif op[1] == "W": + # Update inputs for constant power + P = op[0] + operating_inputs = { + "Current switch": 0, + "Voltage switch": 0, + "Power switch": 1, + "Current input [A]": 0, # doesn't matter + "Voltage input [V]": 0, # doesn't matter + "Power input [W]": P, + } + # Update period + operating_inputs["period"] = op[3] + # Update events + if events is None: + # make current and voltage values that won't be hit + operating_inputs.update( + {"Current cut-off [A]": -1e10, "Voltage cut-off [V]": -1e10} + ) + elif events[1] in ["A", "C"]: + # update current cut-off, make voltage a value that won't be hit + if events[1] == "A": + I = events[0] + else: + # Scale C-rate with capacity to obtain current + capacity = self._parameter_values["Cell capacity [A.h]"] + I = events[0] * capacity + operating_inputs.update( + {"Current cut-off [A]": I, "Voltage cut-off [V]": -1e10} + ) + elif events[1] == "V": + # update voltage cut-off, make current a value that won't be hit + V = events[0] + operating_inputs.update( + {"Current cut-off [A]": -1e10, "Voltage cut-off [V]": V} + ) + + self._experiment_inputs.append(operating_inputs) + # Convert time to dimensionless + dt_dimensional = op[2] + if dt_dimensional is None: + # max simulation time: 1 week + dt_dimensional = 7 * 24 * 3600 + tau = self._parameter_values.evaluate(self.model.timescale) + dt_dimensionless = dt_dimensional / tau + self._experiment_times.append(dt_dimensionless) + + # add current and voltage events to the model + # current events both negative and positive to catch specification + n_electrodes_parallel = pybamm.electrical_parameters.n_electrodes_parallel + n_cells = pybamm.electrical_parameters.n_cells + self.model.events.extend( + [ + pybamm.Event( + "Current cut-off (positive) [A] [experiment]", + self.model.variables["Current [A]"] + - abs(pybamm.InputParameter("Current cut-off [A]")) + / n_electrodes_parallel, + ), + pybamm.Event( + "Current cut-off (negative) [A] [experiment]", + self.model.variables["Current [A]"] + + abs(pybamm.InputParameter("Current cut-off [A]")) + / n_electrodes_parallel, + ), + pybamm.Event( + "Voltage cut-off [V] [experiment]", + self.model.variables["Terminal voltage [V]"] + - pybamm.InputParameter("Voltage cut-off [V]") / n_cells, + ), + ] + ) + def set_defaults(self): """ A method to set all the simulation specs to default values for the @@ -164,10 +319,11 @@ def solve( Parameters ---------- t_eval : numeric type, optional - The times at which to compute the solution. If None the model will - be solved for a full discharge (1 hour / C_rate) if the discharge - timescale is provided. Otherwise the model will be solved up to a - non-dimensional time of 1. + The times at which to compute the solution. If None and the parameter + "Current function [A]" is not read from data the model will + be solved for a full discharge (1 hour / C_rate). If None and the + parameter "Current function [A]" is read from data the model will be + solved at the times provided in the data. solver : :class:`pybamm.BaseSolver` The solver to use to solve the model. external_variables : dict @@ -181,28 +337,117 @@ def solve( If True, model checks are performed after discretisation (see :meth:`pybamm.Discretisation.process_model`). Default is True. """ + # Setup self.build(check_model=check_model) + if solver is None: + solver = self.solver - if t_eval is None: - try: - # Try to compute discharge time - tau = self._parameter_values.evaluate(self.model.param.tau_discharge) + if self.operating_mode == "without experiment": + # For drive cycles (current provided as data) we perform additional tests + # on t_eval (if provided) to ensure the returned solution captures the + # input. If the current is provided as data then the "Current function [A]" + # is the tuple (filename, data). + if isinstance(self._parameter_values["Current function [A]"], tuple): + filename = self._parameter_values["Current function [A]"][0] + tau = self._parameter_values.evaluate(self.model.param.timescale) + time_data = ( + self._parameter_values["Current function [A]"][1][:, 0] / tau + ) + # If no t_eval is provided, we use the times provided in the data. + if t_eval is None: + pybamm.logger.info( + "Setting t_eval as specified by the data '{}'".format(filename) + ) + t_eval = time_data + # If t_eval is provided we first check if it contains all of the times + # in the data to within 10-12. If it doesn't, we then check + # that the largest gap in t_eval is smaller than the smallest gap in the + # time data (to ensure the resolution of t_eval is fine enough). + # We only raise a warning here as users may genuinely only want + # the solution returned at some specified points. + elif ( + set(np.round(time_data, 12)).issubset(set(np.round(t_eval, 12))) + ) is False: + warnings.warn( + """ + t_eval does not contain all of the time points in the data + '{}'. Note: passing t_eval = None automatically sets t_eval + to be the points in the data. + """.format( + filename + ), + pybamm.SolverWarning, + ) + dt_data_min = np.min(np.diff(time_data)) + dt_eval_max = np.max(np.diff(t_eval)) + if dt_eval_max > dt_data_min + sys.float_info.epsilon: + warnings.warn( + """ + The largest timestep in t_eval ({}) is larger than + the smallest timestep in the data ({}). The returned + solution may not have the correct resolution to accurately + capture the input. Try refining t_eval. Alternatively, + passing t_eval = None automatically sets t_eval to be the + points in the data. + """.format( + dt_eval_max, dt_data_min + ), + pybamm.SolverWarning, + ) + # If not using a drive cycle and t_eval is not provided, set t_eval + # to correspond to a single discharge + elif t_eval is None: + tau = self._parameter_values.evaluate(self.model.param.timescale) C_rate = self._parameter_values["C-rate"] t_end = 3600 / tau / C_rate t_eval = np.linspace(0, t_end, 100) - except AttributeError: - t_eval = np.linspace(0, 1, 100) - if solver is None: - solver = self.solver - - self.t_eval = t_eval - self._solution = solver.solve( - self.built_model, - t_eval, - external_variables=external_variables, - inputs=inputs, - ) + self.t_eval = t_eval + self._solution = solver.solve(self.built_model, t_eval, inputs=inputs) + + elif self.operating_mode == "with experiment": + if t_eval is not None: + pybamm.logger.warning( + "Ignoring t_eval as solution times are specified by the experiment" + ) + # Step through all experimental conditions + inputs = inputs or {} + pybamm.logger.info("Start running experiment") + timer = pybamm.Timer() + for idx, (exp_inputs, dt) in enumerate( + zip(self._experiment_inputs, self._experiment_times) + ): + pybamm.logger.info(self.experiment.operating_conditions_strings[idx]) + inputs.update(exp_inputs) + # Non-dimensionalise period + tau = self._parameter_values.evaluate(self.model.timescale) + freq = exp_inputs["period"] / tau + # Make sure we take at least 2 timesteps + npts = max(int(round(dt / freq)) + 1, 2) + self.step( + dt, npts=npts, external_variables=external_variables, inputs=inputs + ) + # Only allow events specified by experiment + if not ( + self._solution.termination == "final time" + or "[experiment]" in self._solution.termination + ): + pybamm.logger.warning( + """ + Experiment is infeasible: '{}' was triggered during '{}'. Try + reducing current, shortening the time interval, or reducing + the period. + """.format( + self._solution.termination, + self.experiment.operating_conditions_strings[idx], + ) + ) + break + pybamm.logger.info( + "Finish experiment simulation, took {}".format( + timer.format(timer.time()) + ) + ) def step( self, dt, solver=None, npts=2, external_variables=None, inputs=None, save=True @@ -469,7 +714,7 @@ def specs( def save(self, filename): """Save simulation using pickle""" if self.model.convert_to_format == "python": - # We currently cannot save models in the 'python' + # We currently cannot save models in the 'python' format raise NotImplementedError( """ Cannot save simulation if model format is python. diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index a17ed8b3b9..7b11c219e9 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -7,6 +7,8 @@ import numpy as np from scipy import optimize from scipy.sparse import issparse +import sys +import itertools class BaseSolver(object): @@ -45,6 +47,7 @@ def __init__( self.root_tol = root_tol self.max_steps = max_steps + self.models_set_up = set() self.model_step_times = {} # Defaults, can be overwritten by specific solver @@ -211,21 +214,49 @@ def report(string): jac_call = None return func, func_call, jac_call + # Check for heaviside functions in rhs and algebraic and add discontinuity + # events if these exist. + # Note: only checks for the case of t < X, t <= X, X < t, or X <= t + for symbol in itertools.chain( + model.concatenated_rhs.pre_order(), model.concatenated_algebraic.pre_order() + ): + if isinstance(symbol, pybamm.Heaviside): + if symbol.right.id == pybamm.t.id: + expr = symbol.left + elif symbol.left.id == pybamm.t.id: + expr = symbol.right + + model.events.append( + pybamm.Event( + str(symbol), expr.new_copy(), pybamm.EventType.DISCONTINUITY + ) + ) + # Process rhs, algebraic and event expressions rhs, rhs_eval, jac_rhs = process(model.concatenated_rhs, "RHS") algebraic, algebraic_eval, jac_algebraic = process( model.concatenated_algebraic, "algebraic" ) - events_eval = [ - process(event, "event", use_jacobian=False)[1] - for event in model.events.values() + terminate_events_eval = [ + process(event.expression, "event", use_jacobian=False)[1] + for event in model.events + if event.event_type == pybamm.EventType.TERMINATION + ] + + # discontinuity events are evaluated before the solver is called, so don't need + # to process them + discontinuity_events_eval = [ + event + for event in model.events + if event.event_type == pybamm.EventType.DISCONTINUITY ] # Add the solver attributes model.rhs_eval = rhs_eval model.algebraic_eval = algebraic_eval model.jac_algebraic_eval = jac_algebraic - model.events_eval = events_eval + model.terminate_events_eval = terminate_events_eval + model.discontinuity_events_eval = discontinuity_events_eval # Calculate consistent initial conditions for the algebraic equations if len(model.algebraic) > 0: @@ -236,7 +267,8 @@ def report(string): residuals, residuals_eval, jacobian_eval = process(all_states, "residuals") model.residuals_eval = residuals_eval model.jacobian_eval = jacobian_eval - model.y0 = self.calculate_consistent_initial_conditions(model) + y0_guess = model.concatenated_initial_conditions.flatten() + model.y0 = self.calculate_consistent_state(model, 0, y0_guess) else: # can use DAE solver to solve ODE model model.residuals_eval = Residuals(rhs, "residuals", model) @@ -272,14 +304,14 @@ def set_inputs(self, model, ext_and_inputs): model.rhs_eval.set_inputs(ext_and_inputs) model.algebraic_eval.set_inputs(ext_and_inputs) model.residuals_eval.set_inputs(ext_and_inputs) - for evnt in model.events_eval: + for evnt in model.terminate_events_eval: evnt.set_inputs(ext_and_inputs) if model.jacobian_eval: model.jacobian_eval.set_inputs(ext_and_inputs) - def calculate_consistent_initial_conditions(self, model): + def calculate_consistent_state(self, model, time=0, y0_guess=None): """ - Calculate consistent initial conditions for the algebraic equations through + Calculate consistent state for the algebraic equations through root-finding Parameters @@ -296,8 +328,9 @@ def calculate_consistent_initial_conditions(self, model): pybamm.logger.info("Start calculating consistent initial conditions") rhs = model.rhs_eval algebraic = model.algebraic_eval - y0_guess = model.concatenated_initial_conditions.flatten() jac = model.jac_algebraic_eval + if y0_guess is None: + y0_guess = model.concatenated_initial_conditions.flatten() # Split y0_guess into differential and algebraic len_rhs = rhs(0, y0_guess).shape[0] @@ -306,7 +339,7 @@ def calculate_consistent_initial_conditions(self, model): def root_fun(y0_alg): "Evaluates algebraic using y0_diff (fixed) and y0_alg (changed by algo)" y0 = np.concatenate([y0_diff, y0_alg]) - out = algebraic(0, y0) + out = algebraic(time, y0) pybamm.logger.debug( "Evaluating algebraic equations at t=0, L2-norm is {}".format( np.linalg.norm(out) @@ -405,20 +438,109 @@ def solve(self, model, t_eval, external_variables=None, inputs=None): inputs = inputs or {} ext_and_inputs = {**external_variables, **inputs} - self.set_up(model, ext_and_inputs) - set_up_time = timer.time() - + # Set up (if not done already) + if model not in self.models_set_up: + self.set_up(model, ext_and_inputs) + set_up_time = timer.time() + self.models_set_up.add(model) + else: + set_up_time = 0 # Solve # Set inputs and external self.set_inputs(model, ext_and_inputs) - timer.reset() - pybamm.logger.info("Calling solver") - solution = self._integrate(model, t_eval, ext_and_inputs) + # Calculate discontinuities + discontinuities = [ + event.expression.evaluate(u=inputs) + for event in model.discontinuity_events_eval + ] + + # make sure they are increasing in time + discontinuities = sorted(discontinuities) + if len(discontinuities) > 0: + pybamm.logger.info( + "Discontinuity events found at t = {}".format(discontinuities) + ) + else: + pybamm.logger.info("No discontinuity events found") + + # remove any identical discontinuities + discontinuities = [ + v + for i, v in enumerate(discontinuities) + if i == len(discontinuities) - 1 + or discontinuities[i] < discontinuities[i + 1] + ] + + # insert time points around discontinuities in t_eval + # keep track of sub sections to integrate by storing start and end indices + start_indices = [0] + end_indices = [] + for dtime in discontinuities: + dindex = np.searchsorted(t_eval, dtime, side="left") + end_indices.append(dindex + 1) + start_indices.append(dindex + 1) + if t_eval[dindex] == dtime: + t_eval[dindex] += sys.float_info.epsilon + t_eval = np.insert(t_eval, dindex, dtime - sys.float_info.epsilon) + else: + t_eval = np.insert( + t_eval, + dindex, + [dtime - sys.float_info.epsilon, dtime + sys.float_info.epsilon], + ) + end_indices.append(len(t_eval)) + + # integrate separatly over each time segment and accumulate into the solution + # object, restarting the solver at each discontinuity (and recalculating a + # consistent state afterwards if a dae) + old_y0 = model.y0 + solution = None + for start_index, end_index in zip(start_indices, end_indices): + pybamm.logger.info( + "Calling solver for {} < t < {}".format( + t_eval[start_index], t_eval[end_index - 1] + ) + ) + timer.reset() + if solution is None: + solution = self._integrate( + model, t_eval[start_index:end_index], ext_and_inputs + ) + solution.solve_time = timer.time() + else: + new_solution = self._integrate( + model, t_eval[start_index:end_index], ext_and_inputs + ) + new_solution.solve_time = timer.time() + solution.append(new_solution, start_index=0) + + if solution.termination != "final time": + break + + if end_index != len(t_eval): + # setup for next integration subsection + y0_guess = solution.y[:, -1] + if model.algebraic: + model.y0 = self.calculate_consistent_state( + model, t_eval[end_index], y0_guess + ) + else: + model.y0 = y0_guess + + last_state = solution.y[:, -1] + if len(model.algebraic) > 0: + model.y0 = self.calculate_consistent_state( + model, t_eval[end_index], last_state + ) + else: + model.y0 = last_state + + # restore old y0 + model.y0 = old_y0 # Assign times solution.set_up_time = set_up_time - solution.solve_time = timer.time() # Add model and inputs to solution solution.model = model @@ -470,8 +592,13 @@ def step( If an empty model is passed (`model.rhs = {}` and `model.algebraic={}`) """ - if old_solution is not None and old_solution.termination != "final time": + + if old_solution is not None and not ( + old_solution.termination == "final time" + or "[experiment]" in old_solution.termination + ): # Return same solution as an event has already been triggered + # With hack to allow stepping past experiment current / voltage cut-off return old_solution # Make sure model isn't empty @@ -560,14 +687,16 @@ def get_termination_reason(self, solution, events): elif solution.termination == "event": # Get final event value final_event_values = {} - for name, event in events.items(): - final_event_values[name] = abs( - event.evaluate( - solution.t_event, - solution.y_event, - {k: v[-1] for k, v in solution.inputs.items()}, + + for event in events: + if event.event_type == pybamm.EventType.TERMINATION: + final_event_values[event.name] = abs( + event.expression.evaluate( + solution.t_event, + solution.y_event, + {k: v[-1] for k, v in solution.inputs.items()}, + ) ) - ) termination_event = min(final_event_values, key=final_event_values.get) # Add the event to the solution object solution.termination = "event: {}".format(termination_event) diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py index 299b8aa7dd..e29c78904f 100644 --- a/pybamm/solvers/casadi_solver.py +++ b/pybamm/solvers/casadi_solver.py @@ -94,7 +94,7 @@ def _integrate(self, model, t_eval, inputs=None): solution = self._run_integrator(integrator, y0_diff, y0_alg, inputs, t_eval) solution.termination = "final time" return solution - elif model.events == {}: + elif not model.events: pybamm.logger.info("No events found, running fast mode") integrator = self.get_integrator(model, t_eval, inputs) y0_diff, y0_alg = np.split(model.y0, [rhs_size]) @@ -104,11 +104,10 @@ def _integrate(self, model, t_eval, inputs=None): elif self.mode == "safe": # Step-and-check init_event_signs = np.sign( - np.concatenate([event(0, model.y0) for event in model.events_eval]) - ) - pybamm.logger.info( - "Start solving {} with {} in 'safe' mode".format(model.name, self.name) + np.concatenate([event(0, model.y0) + for event in model.terminate_events_eval]) ) + pybamm.logger.info("Start solving {} with {}".format(model.name, self.name)) t = t_eval[0] y0 = model.y0 # Initialize solution @@ -149,7 +148,7 @@ def _integrate(self, model, t_eval, inputs=None): np.concatenate( [ event(0, current_step_sol.y[:, -1]) - for event in model.events_eval + for event in model.terminate_events_eval ] ) ) diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index 2c5950a3f6..795f3361b8 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -199,12 +199,12 @@ def get_jac_col_ptrs(self): jac_class = SundialsJacobian() - num_of_events = len(model.events_eval) + num_of_events = len(model.terminate_events_eval) use_jac = 1 def rootfn(t, y): return_root = np.ones((num_of_events,)) - return_root[:] = [event(t, y) for event in model.events_eval] + return_root[:] = [event(t, y) for event in model.terminate_events_eval] return return_root diff --git a/pybamm/solvers/scikits_dae_solver.py b/pybamm/solvers/scikits_dae_solver.py index a461553f54..c7c534566e 100644 --- a/pybamm/solvers/scikits_dae_solver.py +++ b/pybamm/solvers/scikits_dae_solver.py @@ -66,7 +66,7 @@ def _integrate(self, model, t_eval, inputs=None): """ residuals = model.residuals_eval y0 = model.y0 - events = model.events_eval + events = model.terminate_events_eval jacobian = model.jacobian_eval mass_matrix = model.mass_matrix.entries diff --git a/pybamm/solvers/scikits_ode_solver.py b/pybamm/solvers/scikits_ode_solver.py index cda731d4f8..57ea369f80 100644 --- a/pybamm/solvers/scikits_ode_solver.py +++ b/pybamm/solvers/scikits_ode_solver.py @@ -59,7 +59,7 @@ def _integrate(self, model, t_eval, inputs=None): """ derivs = model.rhs_eval y0 = model.y0 - events = model.events_eval + events = model.terminate_events_eval jacobian = model.jacobian_eval def eqsydot(t, y, return_ydot): diff --git a/pybamm/solvers/scipy_solver.py b/pybamm/solvers/scipy_solver.py index 8ac16eafba..100ecf8592 100644 --- a/pybamm/solvers/scipy_solver.py +++ b/pybamm/solvers/scipy_solver.py @@ -54,10 +54,10 @@ def _integrate(self, model, t_eval, inputs=None): extra_options.update({"jac": model.jacobian_eval}) # make events terminal so that the solver stops when they are reached - if model.events_eval: - for event in model.events_eval: + if model.terminate_events_eval: + for event in model.terminate_events_eval: event.terminal = True - extra_options.update({"events": model.events_eval}) + extra_options.update({"events": model.terminate_events_eval}) sol = it.solve_ivp( model.rhs_eval, diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 6c73438dc0..ff10912c90 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -1,17 +1,21 @@ # # Solution class # +import copy import numbers import numpy as np import pickle import pybamm +import pandas as pd from collections import defaultdict +from scipy.io import savemat -class Solution(object): +class _BaseSolution(object): """ - Class containing the solution of, and various attributes associated with, a PyBaMM - model. + (Semi-private) class containing the solution of, and various attributes associated + with, a PyBaMM model. This class is automatically created by the `Solution` class, + and should never be called from outside the `Solution` class. Parameters ---------- @@ -27,48 +31,56 @@ class Solution(object): the event happens. termination : str String to indicate why the solution terminated + copy_this : :class:`pybamm.Solution`, optional + A solution to copy, if provided. Default is None. """ - def __init__(self, t, y, t_event=None, y_event=None, termination="final time"): - self.t = t - self.y = y - self.t_event = t_event - self.y_event = y_event - self.termination = termination + def __init__( + self, + t, + y, + t_event=None, + y_event=None, + termination="final time", + copy_this=None, + ): + self._t = t + self._y = y + self._t_event = t_event + self._y_event = y_event + self._termination = termination # initialize empty inputs and model, to be populated later - self.inputs = {} - self._model = None + if copy_this is None: + self._inputs = pybamm.FuzzyDict() + self._model = None + self.set_up_time = None + self.solve_time = None + else: + self._inputs = copy.copy(copy_this.inputs) + self._model = copy_this.model + self.set_up_time = copy_this.set_up_time + self.solve_time = copy_this.solve_time # initiaize empty variables and data - self._variables = {} - self.data = {} + self._variables = pybamm.FuzzyDict() + self.data = pybamm.FuzzyDict() # initialize empty known evals - self.known_evals = defaultdict(dict) + self._known_evals = defaultdict(dict) for time in t: - self.known_evals[time] = {} + self._known_evals[time] = {} @property def t(self): "Times at which the solution is evaluated" return self._t - @t.setter - def t(self, value): - "Updates the solution times" - self._t = value - @property def y(self): "Values of the solution" return self._y - @y.setter - def y(self, value): - "Updates the solution values" - self._y = value - @property def inputs(self): "Values of the inputs" @@ -124,41 +136,6 @@ def termination(self, value): "Updates the reason for termination" self._termination = value - def __add__(self, other): - "See :meth:`Solution.append`" - self.append(other) - return self - - def append(self, solution): - """ - Appends solution.t and solution.y onto self.t and self.y. - Note: this process removes the initial time and state of solution to avoid - duplicate times and states being stored (self.t[-1] is equal to solution.t[0], - and self.y[:, -1] is equal to solution.y[:, 0]). - - """ - # Update t, y and inputs - self.t = np.concatenate((self.t, solution.t[1:])) - self.y = np.concatenate((self.y, solution.y[:, 1:]), axis=1) - for name, inp in self.inputs.items(): - solution_inp = solution.inputs[name] - if isinstance(solution_inp, numbers.Number): - solution_inp = solution_inp * np.ones_like(solution.t) - self.inputs[name] = np.concatenate((inp, solution_inp[1:])) - # Update solution time - self.solve_time += solution.solve_time - # Update termination - self.termination = solution.termination - self.t_event = solution.t_event - self.y_event = solution.y_event - - # Update known_evals - for t, evals in solution.known_evals.items(): - self.known_evals[t].update(evals) - # Recompute existing variables - for var in self._variables.keys(): - self.update(var) - @property def total_time(self): return self.set_up_time + self.solve_time @@ -172,12 +149,12 @@ def update(self, variables): for key in variables: pybamm.logger.debug("Post-processing {}".format(key)) var = pybamm.ProcessedVariable( - self.model.variables[key], self, self.known_evals + self.model.variables[key], self, self._known_evals ) # Update known_evals in order to process any other variables faster for t in var.known_evals: - self.known_evals[t].update(var.known_evals[t]) + self._known_evals[t].update(var.known_evals[t]) # Save variable and data self._variables[key] = var @@ -215,15 +192,144 @@ def save(self, filename): with open(filename, "wb") as f: pickle.dump(self, f, pickle.HIGHEST_PROTOCOL) - def save_data(self, filename): - """Save solution data only (raw arrays) using pickle""" - if len(self.data) == 0: + def save_data(self, filename, variables=None, to_format="pickle"): + """ + Save solution data only (raw arrays) + + Parameters + ---------- + filename : str + The name of the file to save data to + variables : list, optional + List of variables to save. If None, saves all of the variables that have + been created so far + to_format : str, optional + The format to save to. Options are: + + - 'pickle' (default): creates a pickle file with the data dictionary + - 'matlab': creates a .mat file, for loading in matlab + - 'csv': creates a csv file (1D variables only) + + """ + if variables is None: + # variables not explicitly provided -> save all variables that have been + # computed + data = self.data + else: + # otherwise, save only the variables specified + data = {} + for name in variables: + data[name] = self[name].data + if len(data) == 0: raise ValueError( - """Solution does not have any data. Add variables by calling - 'solution.update', e.g. - 'solution.update(["Terminal voltage [V]", "Current [A]"])' - and then save""" + """ + Solution does not have any data. Please provide a list of variables + to save. + """ + ) + if to_format == "pickle": + with open(filename, "wb") as f: + pickle.dump(data, f, pickle.HIGHEST_PROTOCOL) + elif to_format == "matlab": + savemat(filename, data) + elif to_format == "csv": + for name, var in data.items(): + if var.ndim == 2: + raise ValueError( + "only 1D variables can be saved to csv, but '{}' is 2D".format( + name + ) + ) + df = pd.DataFrame(data) + df.to_csv(filename, index=False) + + +class Solution(_BaseSolution): + """ + Class extending the base solution, with additional functionality for concatenating + different solutions together + + **Extends**: :class:`_BaseSolution` + + """ + + def __init__( + self, t, y, t_event=None, y_event=None, termination="final time", + ): + super().__init__(t, y, t_event, y_event, termination) + + @property + def sub_solutions(self): + "List of sub solutions that have been concatenated to form the full solution" + try: + return self._sub_solutions + except AttributeError: + raise AttributeError( + "sub solutions are only created once other solutions have been appended" + ) + + def __add__(self, other): + "See :meth:`Solution.append`" + self.append(other, create_sub_solutions=True) + return self + + def append(self, solution, start_index=1, create_sub_solutions=False): + """ + Appends solution.t and solution.y onto self.t and self.y. + + Note: by default this process removes the initial time and state of solution to + avoid duplicate times and states being stored (self.t[-1] is equal to + solution.t[0], and self.y[:, -1] is equal to solution.y[:, 0]). Set the optional + argument ``start_index`` to override this behavior + """ + # Create sub-solutions if necessary + # sub-solutions are 'BaseSolution' objects, which have slightly reduced + # functionality compared to normal solutions (can't append other solutions) + if create_sub_solutions and not hasattr(self, "_sub_solutions"): + self._sub_solutions = [ + _BaseSolution( + self.t, + self.y, + self.t_event, + self.y_event, + self.termination, + copy_this=self, + ) + ] + + # (Create and) update sub-solutions + # Create a list of sub-solutions, which are simpler BaseSolution classes + + # Update t, y and inputs + self._t = np.concatenate((self._t, solution.t[start_index:])) + self._y = np.concatenate((self._y, solution.y[:, start_index:]), axis=1) + for name, inp in self.inputs.items(): + solution_inp = solution.inputs[name] + self.inputs[name] = np.concatenate((inp, solution_inp[start_index:])) + # Update solution time + self.solve_time += solution.solve_time + # Update termination + self._termination = solution.termination + self._t_event = solution._t_event + self._y_event = solution._y_event + + # Update known_evals + for t, evals in solution._known_evals.items(): + self._known_evals[t].update(evals) + # Recompute existing variables + for var in self._variables.keys(): + self.update(var) + + # Append sub_solutions + if create_sub_solutions: + self._sub_solutions.append( + _BaseSolution( + solution.t, + solution.y, + solution.t_event, + solution.y_event, + solution.termination, + copy_this=solution, + ) ) - with open(filename, "wb") as f: - pickle.dump(self.data, f, pickle.HIGHEST_PROTOCOL) diff --git a/scripts/install_scikits_odes.sh b/scripts/install_scikits_odes.sh index 997d67c4e1..ef943db8a9 100755 --- a/scripts/install_scikits_odes.sh +++ b/scripts/install_scikits_odes.sh @@ -16,7 +16,8 @@ cmake -DLAPACK_ENABLE=ON -DSUNDIALS_INDEX_TYPE=int32_t -DBUILD_ARKODE:BOOL=OFF - make install cd $CURRENT_DIR rm -rf $TMP_DIR -export LD_LIBRARY_PATH=$INSTALL_DIR/lib:$LD_LIBRARY_PATH +export LD_LIBRARY_PATH=$INSTALL_DIR/lib:$LD_LIBRARY_PATH # For Linux +export DYLD_LIBRARY_PATH=$INSTALL_DIR/lib:$DYLD_LIBRARY_PATH # For Mac export SUNDIALS_INST=$INSTALL_DIR pip install scikits.odes diff --git a/scripts/install_sundials_4.1.0.sh b/scripts/install_sundials_4.1.0.sh index 725ce5457c..6da6751e70 100755 --- a/scripts/install_sundials_4.1.0.sh +++ b/scripts/install_sundials_4.1.0.sh @@ -32,14 +32,20 @@ cmake -DBLAS_ENABLE=ON\ -DKLU_ENABLE=ON\ ../sundials-4.1.0 - -NUM_OF_CORES=$(cat /proc/cpuinfo | grep processor | wc -l) +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 cd $CURRENT_DIR rm -rf build-sundials-4.1.0 rm -rf sundials-4.1.0 -export LD_LIBRARY_PATH=$INSTALL_DIR/lib:$LD_LIBRARY_PATH +export LD_LIBRARY_PATH=$INSTALL_DIR/lib:$LD_LIBRARY_PATH # For Linux +export DYLD_LIBRARY_PATH=$INSTALL_DIR/lib:$DYLD_LIBRARY_PATH # For Mac export SUNDIALS_INST=$INSTALL_DIR # get pybind11 diff --git a/scripts/replace-cmake/sundials-3.1.1/CMakeLists.txt b/scripts/replace-cmake/sundials-3.1.1/CMakeLists.txt new file mode 100644 index 0000000000..8ea19b0db5 --- /dev/null +++ b/scripts/replace-cmake/sundials-3.1.1/CMakeLists.txt @@ -0,0 +1,1623 @@ +# --------------------------------------------------------------- +# Programmer: Radu Serban @ LLNL +# --------------------------------------------------------------- +# LLNS Copyright Start +# Copyright (c) 2014, Lawrence Livermore National Security +# This work was performed under the auspices of the U.S. Department +# of Energy by Lawrence Livermore National Laboratory in part under +# Contract W-7405-Eng-48 and in part under Contract DE-AC52-07NA27344. +# Produced at the Lawrence Livermore National Laboratory. +# All rights reserved. +# For details, see the LICENSE file. +# LLNS Copyright End +# --------------------------------------------------------------- +# Top level CMakeLists.txt for SUNDIALS (for cmake build system) +# --------------------------------------------------------------- + +# --------------------------------------------------------------- +# Initial commands +# --------------------------------------------------------------- + +# Require a fairly recent cmake version +CMAKE_MINIMUM_REQUIRED(VERSION 2.8.1) + +# Set CMake policy to allow examples to build +if(COMMAND cmake_policy) + cmake_policy(SET CMP0003 NEW) +endif(COMMAND cmake_policy) + +# Project SUNDIALS (initially only C supported) +# sets PROJECT_SOURCE_DIR and PROJECT_BINARY_DIR variables +PROJECT(sundials C) + +# Set some variables with info on the SUNDIALS project +SET(PACKAGE_BUGREPORT "woodward6@llnl.gov") +SET(PACKAGE_NAME "SUNDIALS") +SET(PACKAGE_STRING "SUNDIALS 3.1.1") +SET(PACKAGE_TARNAME "sundials") + +# set SUNDIALS version numbers +# (use "" for the version label if none is needed) +SET(PACKAGE_VERSION_MAJOR "3") +SET(PACKAGE_VERSION_MINOR "1") +SET(PACKAGE_VERSION_PATCH "1") +SET(PACKAGE_VERSION_LABEL "") + +IF(PACKAGE_VERSION_LABEL) + SET(PACKAGE_VERSION "${PACKAGE_VERSION_MAJOR}.${PACKAGE_VERSION_MINOR}.${PACKAGE_VERSION_PATCH}-${PACKAGE_VERSION_LABEL}") +ELSE() + SET(PACKAGE_VERSION "${PACKAGE_VERSION_MAJOR}.${PACKAGE_VERSION_MINOR}.${PACKAGE_VERSION_PATCH}") +ENDIF() + +# +SET_PROPERTY(GLOBAL PROPERTY USE_FOLDERS ON) + +# Prohibit in-source build +IF("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") + MESSAGE(FATAL_ERROR "In-source build prohibited.") +ENDIF("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") + +# Hide some cache variables +MARK_AS_ADVANCED(EXECUTABLE_OUTPUT_PATH LIBRARY_OUTPUT_PATH) + +# Always show the C compiler and flags +MARK_AS_ADVANCED(CLEAR + CMAKE_C_COMPILER + CMAKE_C_FLAGS) + +# Specify the VERSION and SOVERSION for shared libraries + +SET(arkodelib_VERSION "2.1.1") +SET(arkodelib_SOVERSION "2") + +SET(cvodelib_VERSION "3.1.1") +SET(cvodelib_SOVERSION "3") + +SET(cvodeslib_VERSION "3.1.1") +SET(cvodeslib_SOVERSION "3") + +SET(idalib_VERSION "3.1.1") +SET(idalib_SOVERSION "3") + +SET(idaslib_VERSION "2.1.0") +SET(idaslib_SOVERSION "2") + +SET(kinsollib_VERSION "3.1.1") +SET(kinsollib_SOVERSION "3") + +SET(cpodeslib_VERSION "0.0.0") +SET(cpodeslib_SOVERSION "0") + +SET(nveclib_VERSION "3.1.1") +SET(nveclib_SOVERSION "3") + +SET(sunmatrixlib_VERSION "1.1.1") +SET(sunmatrixlib_SOVERSION "1") + +SET(sunlinsollib_VERSION "1.1.1") +SET(sunlinsollib_SOVERSION "1") + +# Specify the location of additional CMAKE modules +SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/config) + +# --------------------------------------------------------------- +# MACRO definitions +# --------------------------------------------------------------- +INCLUDE(SundialsCMakeMacros) + +# --------------------------------------------------------------- +# Check for deprecated SUNDIALS CMake options/variables +# --------------------------------------------------------------- +INCLUDE(SundialsDeprecated) + +# --------------------------------------------------------------- +# Which modules to build? +# --------------------------------------------------------------- + +# For each SUNDIALS solver available (i.e. for which we have the +# sources), give the user the option of enabling/disabling it. + +IF(IS_DIRECTORY "${sundials_SOURCE_DIR}/src/arkode") + OPTION(BUILD_ARKODE "Build the ARKODE library" ON) +ELSE() + SET(BUILD_ARKODE OFF) +ENDIF() + +IF(IS_DIRECTORY "${sundials_SOURCE_DIR}/src/cvode") + OPTION(BUILD_CVODE "Build the CVODE library" ON) +ELSE() + SET(BUILD_CVODE OFF) +ENDIF() + +IF(IS_DIRECTORY "${sundials_SOURCE_DIR}/src/cvodes") + OPTION(BUILD_CVODES "Build the CVODES library" ON) +ELSE() + SET(BUILD_CVODES OFF) +ENDIF() + +IF(IS_DIRECTORY "${sundials_SOURCE_DIR}/src/ida") + OPTION(BUILD_IDA "Build the IDA library" ON) +ELSE() + SET(BUILD_IDA OFF) +ENDIF() + +IF(IS_DIRECTORY "${sundials_SOURCE_DIR}/src/idas") + OPTION(BUILD_IDAS "Build the IDAS library" ON) +ELSE() + SET(BUILD_IDAS OFF) +ENDIF() + +IF(IS_DIRECTORY "${sundials_SOURCE_DIR}/src/kinsol") + OPTION(BUILD_KINSOL "Build the KINSOL library" ON) +ELSE() + SET(BUILD_KINSOL OFF) +ENDIF() + +# CPODES is always OFF for now. (commented out for Release); ToDo: better way to do this? +#IF(IS_DIRECTORY "${sundials_SOURCE_DIR}/src/cpodes") +# OPTION(BUILD_CPODES "Build the CPODES library" OFF) +#ELSE() +# SET(BUILD_CPODES OFF) +#ENDIF() + +# --------------------------------------------------------------- +# xSDK specific options +# --------------------------------------------------------------- +INCLUDE(SundialsXSDK) + +# --------------------------------------------------------------- +# Build specific C flags +# --------------------------------------------------------------- + +# Hide all build type specific flags +MARK_AS_ADVANCED(FORCE + CMAKE_C_FLAGS_DEBUG + CMAKE_C_FLAGS_MINSIZEREL + CMAKE_C_FLAGS_RELEASE + CMAKE_C_FLAGS_RELWITHDEBINFO) + +# Only show flags for the current build type it is set +# NOTE: Build specific flags are appended those in CMAKE_C_FLAGS +IF(CMAKE_BUILD_TYPE) + IF(CMAKE_BUILD_TYPE MATCHES "Debug") + MESSAGE("Appending C debug flags") + MARK_AS_ADVANCED(CLEAR CMAKE_C_FLAGS_DEBUG) + ELSEIF(CMAKE_BUILD_TYPE MATCHES "MinSizeRel") + MESSAGE("Appending C min size release flags") + MARK_AS_ADVANCED(CLEAR CMAKE_C_FLAGS_MINSIZEREL) + ELSEIF(CMAKE_BUILD_TYPE MATCHES "Release") + MESSAGE("Appending C release flags") + MARK_AS_ADVANCED(CLEAR CMAKE_C_FLAGS_RELEASE) + ELSEIF(CMAKE_BUILD_TYPE MATCHES "RelWithDebInfo") + MESSAGE("Appending C release with debug info flags") + MARK_AS_ADVANCED(CLEAR CMAKE_C_FLAGS_RELWITHDEBINFO) + ENDIF() +ENDIF() + +# --------------------------------------------------------------- +# Option to specify precision (realtype) +# --------------------------------------------------------------- + +SET(DOCSTR "single, double, or extended") +SHOW_VARIABLE(SUNDIALS_PRECISION STRING "${DOCSTR}" "double") + +# prepare substitution variable PRECISION_LEVEL for sundials_config.h +STRING(TOUPPER ${SUNDIALS_PRECISION} SUNDIALS_PRECISION) +SET(PRECISION_LEVEL "#define SUNDIALS_${SUNDIALS_PRECISION}_PRECISION 1") + +# prepare substitution variable FPRECISION_LEVEL for sundials_fconfig.h +IF(SUNDIALS_PRECISION MATCHES "SINGLE") + SET(FPRECISION_LEVEL "4") +ENDIF(SUNDIALS_PRECISION MATCHES "SINGLE") +IF(SUNDIALS_PRECISION MATCHES "DOUBLE") + SET(FPRECISION_LEVEL "8") +ENDIF(SUNDIALS_PRECISION MATCHES "DOUBLE") +IF(SUNDIALS_PRECISION MATCHES "EXTENDED") + SET(FPRECISION_LEVEL "16") +ENDIF(SUNDIALS_PRECISION MATCHES "EXTENDED") + +# --------------------------------------------------------------- +# Option to specify index type +# --------------------------------------------------------------- + +SET(DOCSTR "Signed 64-bit (int64_t) or signed 32-bit (int32_t) integer") +SHOW_VARIABLE(SUNDIALS_INDEX_TYPE STRING "${DOCSTR}" "int64_t") + +# prepare substitution variable INDEX_TYPE for sundials_config.h +STRING(TOUPPER ${SUNDIALS_INDEX_TYPE} SUNDIALS_INDEX_TYPE) +SET(INDEX_TYPE "#define SUNDIALS_${SUNDIALS_INDEX_TYPE} 1") + +# prepare substitution variable FINDEX_TYPE for sundials_fconfig.h +IF(SUNDIALS_INDEX_TYPE MATCHES "INT32_T") + SET(FINDEX_TYPE "4") +ENDIF(SUNDIALS_INDEX_TYPE MATCHES "INT32_T") +IF(SUNDIALS_INDEX_TYPE MATCHES "INT64_T") + SET(FINDEX_TYPE "8") +ENDIF(SUNDIALS_INDEX_TYPE MATCHES "INT64_T") + +# --------------------------------------------------------------- +# Enable Fortran interface? +# --------------------------------------------------------------- + +# Fortran interface is disabled by default +SET(DOCSTR "Enable Fortran-C support") +SHOW_VARIABLE(FCMIX_ENABLE BOOL "${DOCSTR}" OFF) + +# Check that at least one solver with a Fortran interface is built +IF(NOT BUILD_ARKODE AND NOT BUILD_CVODE AND NOT BUILD_IDA AND NOT BUILD_KINSOL) + IF(FCMIX_ENABLE) + PRINT_WARNING("Enabled packages do not support Fortran" "Disabling FCMIX") + FORCE_VARIABLE(FCMIX_ENABLE BOOL "${DOCSTR}" OFF) + ENDIF() + HIDE_VARIABLE(FCMIX_ENABLE) +ENDIF() + +# --------------------------------------------------------------- +# Options to build static and/or shared libraries +# --------------------------------------------------------------- + +OPTION(BUILD_STATIC_LIBS "Build static libraries" ON) +OPTION(BUILD_SHARED_LIBS "Build shared libraries" ON) + +# Prepare substitution variable SUNDIALS_EXPORT for sundials_config.h +# When building shared SUNDIALS libraries under Windows, use +# #define SUNDIALS_EXPORT __declspec(dllexport) +# When linking to shared SUNDIALS libraries under Windows, use +# #define SUNDIALS_EXPORT __declspec(dllimport) +# In all other cases (other platforms or static libraries +# under Windows), the SUNDIALS_EXPORT macro is empty + +IF(BUILD_SHARED_LIBS AND WIN32) + SET(SUNDIALS_EXPORT + "#ifdef BUILD_SUNDIALS_LIBRARY +#define SUNDIALS_EXPORT __declspec(dllexport) +#else +#define SUNDIALS_EXPORT __declspec(dllimport) +#endif") +ELSE(BUILD_SHARED_LIBS AND WIN32) + SET(SUNDIALS_EXPORT "#define SUNDIALS_EXPORT") +ENDIF(BUILD_SHARED_LIBS AND WIN32) + +# Make sure we build at least one type of libraries +IF(NOT BUILD_STATIC_LIBS AND NOT BUILD_SHARED_LIBS) + PRINT_WARNING("Both static and shared library generation were disabled" + "Building static libraries was re-enabled") + FORCE_VARIABLE(BUILD_STATIC_LIBS BOOL "Build static libraries" ON) +ENDIF(NOT BUILD_STATIC_LIBS AND NOT BUILD_SHARED_LIBS) + +# --------------------------------------------------------------- +# Option to use the generic math libraries (UNIX only) +# --------------------------------------------------------------- + +IF(UNIX) + OPTION(USE_GENERIC_MATH "Use generic (std-c) math libraries" ON) + IF(USE_GENERIC_MATH) + # executables will be linked against -lm + SET(EXTRA_LINK_LIBS -lm) + # prepare substitution variable for sundials_config.h + SET(SUNDIALS_USE_GENERIC_MATH TRUE) + ENDIF(USE_GENERIC_MATH) +ENDIF(UNIX) + +## clock-monotonic, see if we need to link with rt +include(CheckSymbolExists) +set(CMAKE_REQUIRED_LIBRARIES_SAVE ${CMAKE_REQUIRED_LIBRARIES}) +set(CMAKE_REQUIRED_LIBRARIES rt) +CHECK_SYMBOL_EXISTS(_POSIX_TIMERS "unistd.h;time.h" SUNDIALS_POSIX_TIMERS) +set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES_SAVE}) +if(SUNDIALS_POSIX_TIMERS) + find_library(SUNDIALS_RT_LIBRARY NAMES rt) + mark_as_advanced(SUNDIALS_RT_LIBRARY) + if(SUNDIALS_RT_LIBRARY) + # sundials_config.h symbol + SET(SUNDIALS_HAVE_POSIX_TIMERS TRUE) + set(EXTRA_LINK_LIBS ${EXTRA_LINK_LIBS} ${SUNDIALS_RT_LIBRARY}) + endif() +endif() + + +# =============================================================== +# Options for Parallelism +# =============================================================== + +# --------------------------------------------------------------- +# Enable MPI support? +# --------------------------------------------------------------- +OPTION(MPI_ENABLE "Enable MPI support" OFF) + +# --------------------------------------------------------------- +# Enable OpenMP support? +# --------------------------------------------------------------- +OPTION(OPENMP_ENABLE "Enable OpenMP support" OFF) + +# --------------------------------------------------------------- +# Enable Pthread support? +# --------------------------------------------------------------- +OPTION(PTHREAD_ENABLE "Enable Pthreads support" OFF) + +# ------------------------------------------------------------- +# Enable CUDA support? +# ------------------------------------------------------------- +OPTION(CUDA_ENABLE "Enable CUDA support" OFF) + +# ------------------------------------------------------------- +# Enable RAJA support? +# ------------------------------------------------------------- +OPTION(RAJA_ENABLE "Enable RAJA support" OFF) + + +# =============================================================== +# Options for external packages +# =============================================================== + +# --------------------------------------------------------------- +# Enable BLAS support? +# --------------------------------------------------------------- +OPTION(BLAS_ENABLE "Enable BLAS support" OFF) + +# --------------------------------------------------------------- +# Enable LAPACK/BLAS support? +# --------------------------------------------------------------- +OPTION(LAPACK_ENABLE "Enable Lapack support" OFF) + +# LAPACK does not support extended precision +IF(LAPACK_ENABLE AND SUNDIALS_PRECISION MATCHES "EXTENDED") + PRINT_WARNING("LAPACK is not compatible with ${SUNDIALS_PRECISION} precision" + "Disabling LAPACK") + FORCE_VARIABLE(LAPACK_ENABLE BOOL "LAPACK is disabled" OFF) +ENDIF() + +# LAPACK does not support 64-bit integer index types +IF(LAPACK_ENABLE AND SUNDIALS_INDEX_TYPE MATCHES "INT64_T") + PRINT_WARNING("LAPACK is not compatible with ${SUNDIALS_INDEX_TYPE} integers" + "Disabling LAPACK") + SET(LAPACK_ENABLE OFF CACHE BOOL "LAPACK is disabled" FORCE) +ENDIF() + +# --------------------------------------------------------------- +# Enable SuperLU_MT support? +# --------------------------------------------------------------- +OPTION(SUPERLUMT_ENABLE "Enable SUPERLUMT support" OFF) + +# SuperLU_MT does not support extended precision +IF(SUPERLUMT_ENABLE AND SUNDIALS_PRECISION MATCHES "EXTENDED") + PRINT_WARNING("SuperLU_MT is not compatible with ${SUNDIALS_PRECISION} precision" + "Disabling SuperLU_MT") + FORCE_VARIABLE(SUPERLUMT_ENABLE BOOL "SuperLU_MT is disabled" OFF) +ENDIF() + +# --------------------------------------------------------------- +# Enable KLU support? +# --------------------------------------------------------------- +OPTION(KLU_ENABLE "Enable KLU support" OFF) + +# KLU does not support single or extended precision +IF(KLU_ENABLE AND + (SUNDIALS_PRECISION MATCHES "SINGLE" OR SUNDIALS_PRECISION MATCHES "EXTENDED")) + PRINT_WARNING("KLU is not compatible with ${SUNDIALS_PRECISION} precision" + "Disabling KLU") + FORCE_VARIABLE(KLU_ENABLE BOOL "KLU is disabled" OFF) +ENDIF() + +# --------------------------------------------------------------- +# Enable hypre Vector support? +# --------------------------------------------------------------- +OPTION(HYPRE_ENABLE "Enable hypre support" OFF) + +# Using hypre requres building with MPI enabled +IF(HYPRE_ENABLE AND NOT MPI_ENABLE) + PRINT_WARNING("MPI not enabled - Disabling hypre" + "Set MPI_ENABLE to ON to use parhyp") + FORCE_VARIABLE(HYPRE_ENABLE BOOL "Enable hypre support" OFF) +ENDIF() + +# --------------------------------------------------------------- +# Enable PETSc support? +# --------------------------------------------------------------- +OPTION(PETSC_ENABLE "Enable PETSc support" OFF) + +# Using PETSc requires building with MPI enabled +IF(PETSC_ENABLE AND NOT MPI_ENABLE) + PRINT_WARNING("MPI not enabled - Disabling PETSc" + "Set MPI_ENABLE to ON to use PETSc") + FORCE_VARIABLE(PETSC_ENABLE BOOL "Enable PETSc support" OFF) +ENDIF() + + +# =============================================================== +# Options for examples +# =============================================================== + +# --------------------------------------------------------------- +# Enable examples? +# --------------------------------------------------------------- + +# Enable C examples (on by default) +OPTION(EXAMPLES_ENABLE_C "Build SUNDIALS C examples" ON) + +# F77 examples (on by default) are an option only if the Fortran +# interface is enabled +SET(DOCSTR "Build SUNDIALS Fortran examples") +IF(FCMIX_ENABLE) + OPTION(EXAMPLES_ENABLE_F77 "${DOCSTR}" ON) + # Fortran examples do not support single or extended precision + IF(SUNDIALS_PRECISION MATCHES "EXTENDED" OR SUNDIALS_PRECISION MATCHES "SINGLE") + PRINT_WARNING("F77 examples are not compatible with ${SUNDIALS_PRECISION} precision" + "EXAMPLES_ENABLE_F77") + FORCE_VARIABLE(EXAMPLES_ENABLE_F77 BOOL "Fortran examples are disabled" OFF) + ENDIF() +ELSE() + # set back to OFF (in case was ON) + IF(EXAMPLES_ENABLE_F77) + PRINT_WARNING("EXAMPLES_ENABLE_F77 is ON but FCMIX is OFF" + "Disabling EXAMPLES_ENABLE_F77") + FORCE_VARIABLE(EXAMPLES_ENABLE_F77 BOOL "${DOCSTR}" OFF) + ENDIF() + HIDE_VARIABLE(EXAMPLES_ENABLE_F77) +ENDIF() + +# C++ examples (off by default) are an option only if ARKode is enabled +SET(DOCSTR "Build ARKode C++ examples") +IF(BUILD_ARKODE) + SHOW_VARIABLE(EXAMPLES_ENABLE_CXX BOOL "${DOCSTR}" OFF) +ELSE() + # set back to OFF (in case was ON) + IF(EXAMPLES_ENABLE_CXX) + PRINT_WARNING("EXAMPLES_ENABLE_CXX is ON but BUILD_ARKODE is OFF" + "Disabling EXAMPLES_ENABLE_CXX") + FORCE_VARIABLE(EXAMPLES_ENABLE_CXX BOOL "${DOCSTR}" OFF) + ENDIF() + HIDE_VARIABLE(EXAMPLES_ENABLE_CXX) +ENDIF() + +# F90 examples (off by default) are an option only if ARKode is +# built and the Fortran interface is enabled +SET(DOCSTR "Build ARKode F90 examples") +IF(FCMIX_ENABLE AND BUILD_ARKODE) + SHOW_VARIABLE(EXAMPLES_ENABLE_F90 BOOL "${DOCSTR}" OFF) + # Fortran90 examples do not support single or extended precision + # NOTE: This check can be removed after Fortran configure file is integrated into examples + IF(SUNDIALS_PRECISION MATCHES "EXTENDED" OR SUNDIALS_PRECISION MATCHES "SINGLE") + PRINT_WARNING("F90 examples are not compatible with ${SUNDIALS_PRECISION} precision" + "EXAMPLES_ENABLE_F90") + FORCE_VARIABLE(EXAMPLES_ENABLE_F90 BOOL "Fortran90 examples are disabled" OFF) + ENDIF() +ELSE() + # set back to OFF (in case was ON) + IF(EXAMPLES_ENABLE_F90) + PRINT_WARNING("EXAMPLES_ENABLE_F90 is ON but FCMIX or BUILD_ARKODE is OFF" + "Disabling EXAMPLES_ENABLE_F90") + FORCE_VARIABLE(EXAMPLES_ENABLE_F90 BOOL "${DOCSTR}" OFF) + ENDIF() + HIDE_VARIABLE(EXAMPLES_ENABLE_F90) +ENDIF() + +# CUDA examples (off by default) +SET(DOCSTR "Build SUNDIALS CUDA examples") +IF(CUDA_ENABLE) + SHOW_VARIABLE(EXAMPLES_ENABLE_CUDA BOOL "${DOCSTR}" OFF) +ELSE() + IF(EXAMPLES_ENABLE_CUDA) + PRINT_WARNING("EXAMPLES_ENABLE_CUDA is ON but CUDA_ENABLE is OFF" + "Disabling EXAMPLES_ENABLE_CUDA") + FORCE_VARIABLE(EXAMPLES_ENABLE_CUDA BOOL "${DOCSTR}" OFF) + ENDIF() +ENDIF() + +# RAJA examples (off by default) +SET(DOCSTR "Build SUNDIALS RAJA examples") +IF(RAJA_ENABLE) + SHOW_VARIABLE(EXAMPLES_ENABLE_RAJA BOOL "${DOCSTR}" OFF) +ELSE() + IF(EXAMPLES_ENABLE_RAJA) + PRINT_WARNING("EXAMPLES_ENABLE_RAJA is ON but RAJA_ENABLE is OFF" + "Disabling EXAMPLES_ENABLE_RAJA") + FORCE_VARIABLE(EXAMPLES_ENABLE_RAJA BOOL "${DOCSTR}" OFF) + ENDIF() +ENDIF() + +# If any of the above examples are enabled set EXAMPLES_ENABLED to TRUE +IF(EXAMPLES_ENABLE_C OR + EXAMPLES_ENABLE_F77 OR + EXAMPLES_ENABLE_CXX OR + EXAMPLES_ENABLE_F90 OR + EXAMPLES_ENABLE_CUDA OR + EXAMPLES_ENABLE_RAJA) + SET(EXAMPLES_ENABLED TRUE) +ELSE() + SET(EXAMPLES_ENABLED FALSE) +ENDIF() + +# --------------------------------------------------------------- +# Install examples? +# --------------------------------------------------------------- + +IF(EXAMPLES_ENABLED) + + # If examples are enabled, set different options + + # The examples will be linked with the library corresponding to the build type. + # Whenever building shared libraries, use them to link the examples. + IF(BUILD_SHARED_LIBS) + SET(LINK_LIBRARY_TYPE "shared") + ELSE(BUILD_SHARED_LIBS) + SET(LINK_LIBRARY_TYPE "static") + ENDIF(BUILD_SHARED_LIBS) + + # Enable installing examples by default + SHOW_VARIABLE(EXAMPLES_INSTALL BOOL "Install example files" ON) + + # If examples are to be exported, check where we should install them. + IF(EXAMPLES_INSTALL) + + SHOW_VARIABLE(EXAMPLES_INSTALL_PATH PATH + "Output directory for installing example files" "${CMAKE_INSTALL_PREFIX}/examples") + + IF(NOT EXAMPLES_INSTALL_PATH) + PRINT_WARNING("The example installation path is empty" + "Example installation path was reset to its default value") + SET(EXAMPLES_INSTALL_PATH "${CMAKE_INSTALL_PREFIX}/examples" CACHE STRING + "Output directory for installing example files" FORCE) + ENDIF(NOT EXAMPLES_INSTALL_PATH) + + # create test_install target and directory for running smoke tests after + # installation + ADD_CUSTOM_TARGET(test_install) + + SET(TEST_INSTALL_DIR ${PROJECT_BINARY_DIR}/Testing_Install) + + IF(NOT EXISTS ${TEST_INSTALL_DIR}) + FILE(MAKE_DIRECTORY ${TEST_INSTALL_DIR}) + ENDIF() + + + ELSE(EXAMPLES_INSTALL) + + HIDE_VARIABLE(EXAMPLES_INSTALL_PATH) + + ENDIF(EXAMPLES_INSTALL) + +ELSE(EXAMPLES_ENABLED) + + # If examples are disabled, hide all options related to + # building and installing the SUNDIALS examples + + HIDE_VARIABLE(EXAMPLES_INSTALL) + HIDE_VARIABLE(EXAMPLES_INSTALL_PATH) + +ENDIF(EXAMPLES_ENABLED) + +# --------------------------------------------------------------- +# Include development examples in regression tests? +# --------------------------------------------------------------- +OPTION(SUNDIALS_DEVTESTS "Include development tests in make test" OFF) +MARK_AS_ADVANCED(FORCE SUNDIALS_DEVTESTS) + +# =============================================================== +# Add any other necessary compiler flags & definitions +# =============================================================== + +# Under Windows, add compiler directive to inhibit warnings +# about use of unsecure functions + +IF(WIN32) + ADD_DEFINITIONS(-D_CRT_SECURE_NO_WARNINGS) +ENDIF(WIN32) + +IF(APPLE) + SET(CMAKE_SHARED_LIBRARY_CREATE_C_FLAGS "${CMAKE_SHARED_LIBRARY_CREATE_C_FLAGS} -undefined dynamic_lookup") +ENDIF(APPLE) + +# --------------------------------------------------------------- +# A Fortran compiler is needed if: +# (a) FCMIX is enabled +# (b) BLAS is enabled (for the name-mangling scheme) +# (c) LAPACK is enabled (for the name-mangling scheme) +# --------------------------------------------------------------- + +IF(FCMIX_ENABLE OR BLAS_ENABLE OR LAPACK_ENABLE) + INCLUDE(SundialsFortran) + IF(NOT F77_FOUND AND FCMIX_ENABLE) + PRINT_WARNING("Fortran compiler not functional" + "FCMIX support will not be provided") + ENDIF() +ENDIF() + +# --------------------------------------------------------------- +# A Fortran90 compiler is needed if: +# (a) F90 ARKODE examples are enabled +# --------------------------------------------------------------- + +IF(EXAMPLES_ENABLE_F90) + INCLUDE(SundialsFortran90) + IF(NOT F90_FOUND) + PRINT_WARNING("Fortran90 compiler not functional" + "F90 support will not be provided") + ENDIF() +ENDIF() + +# --------------------------------------------------------------- +# A C++ compiler is needed if: +# (a) C++ ARKODE examples are enabled +# (b) CUDA is enabled +# (c) RAJA is enabled +# --------------------------------------------------------------- + +IF(EXAMPLES_ENABLE_CXX OR CUDA_ENABLE OR RAJA_ENABLE) + INCLUDE(SundialsCXX) + IF(NOT CXX_FOUND) + PRINT_WARNING("C++ compiler not functional" + "C++ support will not be provided") + ENDIF() +ENDIF() + +# --------------------------------------------------------------- +# Check if we need an alternate way of specifying the Fortran +# name-mangling scheme if we were unable to infer it using a +# compiler. +# Ask the user to specify the case and number of appended underscores +# corresponding to the Fortran name-mangling scheme of symbol names +# that do not themselves contain underscores (recall that this is all +# we really need for the interfaces to LAPACK). +# Note: the default scheme is lower case - one underscore +# --------------------------------------------------------------- + +IF(BLAS_ENABLE OR LAPACK_ENABLE AND NOT F77SCHEME_FOUND) + # Specify the case for the Fortran name-mangling scheme + SHOW_VARIABLE(SUNDIALS_F77_FUNC_CASE STRING + "case of Fortran function names (lower/upper)" + "lower") + # Specify the number of appended underscores for the Fortran name-mangling scheme + SHOW_VARIABLE(SUNDIALS_F77_FUNC_UNDERSCORES STRING + "number of underscores appended to Fortran function names" + "one") + # Based on the given case and number of underscores, + # set the C preprocessor macro definition + IF(${SUNDIALS_F77_FUNC_CASE} MATCHES "lower") + IF(${SUNDIALS_F77_FUNC_UNDERSCORES} MATCHES "none") + SET(CMAKE_Fortran_SCHEME_NO_UNDERSCORES "mysub") + ENDIF(${SUNDIALS_F77_FUNC_UNDERSCORES} MATCHES "none") + IF(${SUNDIALS_F77_FUNC_UNDERSCORES} MATCHES "one") + SET(CMAKE_Fortran_SCHEME_NO_UNDERSCORES "mysub_") + ENDIF(${SUNDIALS_F77_FUNC_UNDERSCORES} MATCHES "one") + IF(${SUNDIALS_F77_FUNC_UNDERSCORES} MATCHES "two") + SET(CMAKE_Fortran_SCHEME_NO_UNDERSCORES "mysub__") + ENDIF(${SUNDIALS_F77_FUNC_UNDERSCORES} MATCHES "two") + ELSE(${SUNDIALS_F77_FUNC_CASE} MATCHES "lower") + IF(${SUNDIALS_F77_FUNC_UNDERSCORES} MATCHES "none") + SET(CMAKE_Fortran_SCHEME_NO_UNDERSCORES "MYSUB") + ENDIF(${SUNDIALS_F77_FUNC_UNDERSCORES} MATCHES "none") + IF(${SUNDIALS_F77_FUNC_UNDERSCORES} MATCHES "one") + SET(CMAKE_Fortran_SCHEME_NO_UNDERSCORES "MYSUB_") + ENDIF(${SUNDIALS_F77_FUNC_UNDERSCORES} MATCHES "one") + IF(${SUNDIALS_F77_FUNC_UNDERSCORES} MATCHES "two") + SET(CMAKE_Fortran_SCHEME_NO_UNDERSCORES "MYSUB__") + ENDIF(${SUNDIALS_F77_FUNC_UNDERSCORES} MATCHES "two") + ENDIF(${SUNDIALS_F77_FUNC_CASE} MATCHES "lower") + # Since the SUNDIALS codes never use symbol names containing + # underscores, set a default scheme (probably wrong) for symbols + # with underscores. + SET(CMAKE_Fortran_SCHEME_WITH_UNDERSCORES "my_sub_") + # We now "have" a scheme. + SET(F77SCHEME_FOUND TRUE) +ENDIF(BLAS_ENABLE OR LAPACK_ENABLE AND NOT F77SCHEME_FOUND) + +# --------------------------------------------------------------- +# If we have a name-mangling scheme (either automatically +# inferred or provided by the user), set the SUNDIALS +# compiler preprocessor macro definitions. +# --------------------------------------------------------------- + +SET(F77_MANGLE_MACRO1 "") +SET(F77_MANGLE_MACRO2 "") + +IF(F77SCHEME_FOUND) + # Symbols WITHOUT underscores + IF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "mysub") + SET(F77_MANGLE_MACRO1 "#define SUNDIALS_F77_FUNC(name,NAME) name") + ENDIF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "mysub") + IF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "mysub_") + SET(F77_MANGLE_MACRO1 "#define SUNDIALS_F77_FUNC(name,NAME) name ## _") + ENDIF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "mysub_") + IF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "mysub__") + SET(F77_MANGLE_MACRO1 "#define SUNDIALS_F77_FUNC(name,NAME) name ## __") + ENDIF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "mysub__") + IF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "MYSUB") + SET(F77_MANGLE_MACRO1 "#define SUNDIALS_F77_FUNC(name,NAME) NAME") + ENDIF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "MYSUB") + IF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "MYSUB_") + SET(F77_MANGLE_MACRO1 "#define SUNDIALS_F77_FUNC(name,NAME) NAME ## _") + ENDIF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "MYSUB_") + IF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "MYSUB__") + SET(F77_MANGLE_MACRO1 "#define SUNDIALS_F77_FUNC(name,NAME) NAME ## __") + ENDIF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "MYSUB__") + # Symbols with underscores + IF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "my_sub") + SET(F77_MANGLE_MACRO2 "#define SUNDIALS_F77_FUNC_(name,NAME) name") + ENDIF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "my_sub") + IF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "my_sub_") + SET(F77_MANGLE_MACRO2 "#define SUNDIALS_F77_FUNC_(name,NAME) name ## _") + ENDIF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "my_sub_") + IF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "my_sub__") + SET(F77_MANGLE_MACRO2 "#define SUNDIALS_F77_FUNC_(name,NAME) name ## __") + ENDIF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "my_sub__") + IF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "MY_SUB") + SET(F77_MANGLE_MACRO2 "#define SUNDIALS_F77_FUNC_(name,NAME) NAME") + ENDIF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "MY_SUB") + IF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "MY_SUB_") + SET(F77_MANGLE_MACRO2 "#define SUNDIALS_F77_FUNC_(name,NAME) NAME ## _") + ENDIF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "MY_SUB_") + IF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "MY_SUB__") + SET(F77_MANGLE_MACRO2 "#define SUNDIALS_F77_FUNC_(name,NAME) NAME ## __") + ENDIF(${CMAKE_Fortran_SCHEME_NO_UNDERSCORES} MATCHES "MY_SUB__") +ENDIF(F77SCHEME_FOUND) + +# --------------------------------------------------------------- +# Decide how to compile MPI codes. +# --------------------------------------------------------------- + +IF(MPI_ENABLE) + # show command to run MPI codes (defaults to mpirun) + SHOW_VARIABLE(MPI_RUN_COMMAND STRING "MPI run command" "mpirun") + + INCLUDE(SundialsMPIC) + IF(MPIC_FOUND) + IF(CXX_FOUND AND EXAMPLES_ENABLE_CXX) + INCLUDE(SundialsMPICXX) + ENDIF() + IF(F77_FOUND AND EXAMPLES_ENABLE_F77) + INCLUDE(SundialsMPIF) + ENDIF() + IF(F90_FOUND AND EXAMPLES_ENABLE_F90) + INCLUDE(SundialsMPIF90) + ENDIF() + ELSE() + PRINT_WARNING("MPI not functional" + "Parallel support will not be provided") + ENDIF() + + IF(MPIC_MPI2) + SET(F77_MPI_COMM_F2C "#define SUNDIALS_MPI_COMM_F2C 1") + ELSE() + SET(F77_MPI_COMM_F2C "#define SUNDIALS_MPI_COMM_F2C 0") + ENDIF() + +ELSE() + + HIDE_VARIABLE(MPI_INCLUDE_PATH) + HIDE_VARIABLE(MPI_LIBRARIES) + HIDE_VARIABLE(MPI_EXTRA_LIBRARIES) + HIDE_VARIABLE(MPI_MPICC) + HIDE_VARIABLE(MPI_MPICXX) + HIDE_VARIABLE(MPI_MPIF77) + HIDE_VARIABLE(MPI_MPIF90) + +ENDIF(MPI_ENABLE) + +# --------------------------------------------------------------- +# If using MPI with C++, disable C++ extensions (for known wrappers) +# --------------------------------------------------------------- + +# IF(MPICXX_FOUND) +# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DMPICH_SKIP_MPICXX -DOMPI_SKIP_MPICXX -DLAM_BUILDING") +# ENDIF(MPICXX_FOUND) + +# ------------------------------------------------------------- +# Find OpenMP +# ------------------------------------------------------------- + +IF(OPENMP_ENABLE) + FIND_PACKAGE(OpenMP) + IF(NOT OPENMP_FOUND) + message(STATUS "Disabling OpenMP support, could not determine compiler flags") + ENDIF(NOT OPENMP_FOUND) +ENDIF(OPENMP_ENABLE) + +# ------------------------------------------------------------- +# Find PThreads +# ------------------------------------------------------------- + +IF(PTHREAD_ENABLE) + FIND_PACKAGE(Threads) + IF(CMAKE_USE_PTHREADS_INIT) + message(STATUS "Using Pthreads") + SET(PTHREADS_FOUND TRUE) + # SGS + ELSE() + message(STATUS "Disabling Pthreads support, could not determine compiler flags") + endif() +ENDIF(PTHREAD_ENABLE) + +# ------------------------------------------------------------- +# Find CUDA +# ------------------------------------------------------------- + +# disable CUDA if a working C++ compiler is not found +IF(CUDA_ENABLE AND (NOT CXX_FOUND)) + PRINT_WARNING("C++ compiler required for CUDA support" "Disabling CUDA") + FORCE_VARIABLE(CUDA_ENABLE BOOL "CUDA disabled" OFF) +ENDIF() + +if(CUDA_ENABLE) + find_package(CUDA) + + if (CUDA_FOUND) + #message("CUDA found!") + set(CUDA_NVCC_FLAGS "-lineinfo") + else() + message(STATUS "Disabling CUDA support, could not find CUDA.") + endif() +endif(CUDA_ENABLE) + +# ------------------------------------------------------------- +# Find RAJA +# ------------------------------------------------------------- + +# disable RAJA if CUDA is not enabled/working +IF(RAJA_ENABLE AND (NOT CUDA_FOUND)) + PRINT_WARNING("CUDA is required for RAJA support" "Please enable CUDA and RAJA") + FORCE_VARIABLE(RAJA_ENABLE BOOL "RAJA disabled" OFF) +ENDIF() + +# Check if C++11 compiler is available +IF(RAJA_ENABLE) + include(CheckCXXCompilerFlag) + CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11) + + IF(COMPILER_SUPPORTS_CXX11) + set(CMAKE_CXX_STANDARD 11) + ELSE() + PRINT_WARNING("C++11 compliant compiler required for RAJA support" "Disabling RAJA") + FORCE_VARIABLE(RAJA_ENABLE BOOL "RAJA disabled" OFF) + ENDIF() +ENDIF() + +if(RAJA_ENABLE) + # Look for CMake configuration file in RAJA installation + find_package(RAJA CONFIGS) + if (RAJA_FOUND) + #message("RAJA found!") + include_directories(${RAJA_INCLUDE_DIR}) + set(CUDA_NVCC_FLAGS ${CUDA_NVCC_FLAGS} ${RAJA_NVCC_FLAGS}) + else() + PRINT_WARNING("RAJA configuration not found" "Please set RAJA_DIR to provide path to RAJA CMake configuration file.") + endif() +endif(RAJA_ENABLE) + +# =============================================================== +# Find (and test) external packages +# =============================================================== + +# --------------------------------------------------------------- +# Find (and test) the BLAS libraries +# --------------------------------------------------------------- + +# If BLAS is needed, first try to find the appropriate +# libraries and linker flags needed to link against them. + +IF(BLAS_ENABLE) + + # find BLAS + INCLUDE(SundialsBlas) + + # show after include so FindBlas can locate BLAS_LIBRARIES if necessary + SHOW_VARIABLE(BLAS_LIBRARIES STRING "Blas libraries" "${BLAS_LIBRARIES}") + + IF(BLAS_LIBRARIES AND NOT BLAS_FOUND) + PRINT_WARNING("BLAS not functional" + "BLAS support will not be provided") + ELSE() + #set sundials_config.h symbol via sundials_config.in + SET(SUNDIALS_BLAS TRUE) + ENDIF() + +ELSE() + + IF(NOT LAPACK_ENABLE) + HIDE_VARIABLE(SUNDIALS_F77_FUNC_CASE) + HIDE_VARIABLE(SUNDIALS_F77_FUNC_UNDERSCORES) + ENDIF() + HIDE_VARIABLE(BLAS_LIBRARIES) + +ENDIF() + +# --------------------------------------------------------------- +# Find (and test) the Lapack libraries +# --------------------------------------------------------------- + +# If LAPACK is needed, first try to find the appropriate +# libraries and linker flags needed to link against them. + +IF(LAPACK_ENABLE) + + # find LAPACK and BLAS Libraries + INCLUDE(SundialsLapack) + + # show after include so FindLapack can locate LAPCK_LIBRARIES if necessary + SHOW_VARIABLE(LAPACK_LIBRARIES STRING "Lapack and Blas libraries" "${LAPACK_LIBRARIES}") + + IF(LAPACK_LIBRARIES AND NOT LAPACK_FOUND) + PRINT_WARNING("LAPACK not functional" + "Blas/Lapack support will not be provided") + ELSE() + #set sundials_config.h symbol via sundials_config.in + SET(SUNDIALS_BLAS_LAPACK TRUE) + ENDIF() + +ELSE() + + IF(NOT BLAS_ENABLE) + HIDE_VARIABLE(SUNDIALS_F77_FUNC_CASE) + HIDE_VARIABLE(SUNDIALS_F77_FUNC_UNDERSCORES) + ENDIF() + HIDE_VARIABLE(LAPACK_LIBRARIES) + +ENDIF() + +# --------------------------------------------------------------- +# Find (and test) the SUPERLUMT libraries +# --------------------------------------------------------------- + +# >>>>>>> NOTE: Need to add check for SuperLU_MT integer type + +# If SUPERLUMT is needed, first try to find the appropriate +# libraries to link against them. + +IF(SUPERLUMT_ENABLE) + + # Show SuperLU_MT options and set default thread type (Pthreads) + SHOW_VARIABLE(SUPERLUMT_THREAD_TYPE STRING "SUPERLUMT threading type: OpenMP or Pthread" "Pthread") + SHOW_VARIABLE(SUPERLUMT_INCLUDE_DIR PATH "SUPERLUMT include directory" "${SUPERLUMT_INCLUDE_DIR}") + SHOW_VARIABLE(SUPERLUMT_LIBRARY_DIR PATH "SUPERLUMT library directory" "${SUPERLUMT_LIBRARY_DIR}") + + INCLUDE(SundialsSuperLUMT) + + IF(SUPERLUMT_FOUND) + # sundials_config.h symbols + SET(SUNDIALS_SUPERLUMT TRUE) + SET(SUNDIALS_SUPERLUMT_THREAD_TYPE ${SUPERLUMT_THREAD_TYPE}) + INCLUDE_DIRECTORIES(${SUPERLUMT_INCLUDE_DIR}) + ENDIF() + + IF(SUPERLUMT_LIBRARIES AND NOT SUPERLUMT_FOUND) + PRINT_WARNING("SUPERLUMT not functional - support will not be provided" + "Double check spelling specified libraries (search is case sensitive)") + ENDIF(SUPERLUMT_LIBRARIES AND NOT SUPERLUMT_FOUND) + +ELSE() + + HIDE_VARIABLE(SUPERLUMT_THREAD_TYPE) + HIDE_VARIABLE(SUPERLUMT_LIBRARY_DIR) + HIDE_VARIABLE(SUPERLUMT_INCLUDE_DIR) + SET (SUPERLUMT_DISABLED TRUE CACHE INTERNAL "GUI - return when first set") + +ENDIF() + +# --------------------------------------------------------------- +# Find (and test) the KLU libraries +# --------------------------------------------------------------- + +# If KLU is requested, first try to find the appropriate libraries to +# link against them. + +IF(KLU_ENABLE) + + SHOW_VARIABLE(KLU_INCLUDE_DIR PATH "KLU include directory" + "${KLU_INCLUDE_DIR}") + SHOW_VARIABLE(KLU_LIBRARY_DIR PATH + "Klu library directory" "${KLU_LIBRARY_DIR}") + + set(KLU_FOUND TRUE) + get_filename_component(PYBAMM_DIR ${PROJECT_SOURCE_DIR} DIRECTORY) + set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PYBAMM_DIR}) # use FindSuiteSparse.cmake that is in PyBaMM root + set(SuiteSparse_ROOT ${PYBAMM_DIR}/SuiteSparse-5.6.0) + find_package(SuiteSparse OPTIONAL_COMPONENTS KLU AMD COLAMD BTF) + include_directories(${SuiteSparse_INCLUDE_DIRS}) + set(KLU_LIBRARIES ${SuiteSparse_LIBRARIES}) + + IF(KLU_LIBRARIES AND NOT KLU_FOUND) + PRINT_WARNING("KLU not functional - support will not be provided" + "Double check spelling of include path and specified libraries (search is case sensitive)") + ENDIF(KLU_LIBRARIES AND NOT KLU_FOUND) + +ELSE() + + HIDE_VARIABLE(KLU_LIBRARY_DIR) + HIDE_VARIABLE(KLU_INCLUDE_DIR) + SET (KLU_DISABLED TRUE CACHE INTERNAL "GUI - return when first set") + +ENDIF(KLU_ENABLE) + +# --------------------------------------------------------------- +# Find (and test) the hypre libraries +# --------------------------------------------------------------- + +# >>>>>>> NOTE: Need to add check for hypre precision and integer type + +IF(HYPRE_ENABLE) + SHOW_VARIABLE(HYPRE_INCLUDE_DIR PATH "HYPRE include directory" + "${HYPRE_INCLUDE_DIR}") + SHOW_VARIABLE(HYPRE_LIBRARY_DIR PATH + "HYPRE library directory" "${HYPRE_LIBRARY_DIR}") + + INCLUDE(SundialsHypre) + + IF(HYPRE_FOUND) + # sundials_config.h symbol + SET(SUNDIALS_HYPRE TRUE) + INCLUDE_DIRECTORIES(${HYPRE_INCLUDE_DIR}) + ENDIF(HYPRE_FOUND) + + IF(HYPRE_LIBRARIES AND NOT HYPRE_FOUND) + PRINT_WARNING("HYPRE not functional - support will not be provided" + "Found hypre library, test code does not work") + ENDIF(HYPRE_LIBRARIES AND NOT HYPRE_FOUND) + +ELSE() + + HIDE_VARIABLE(HYPRE_INCLUDE_DIR) + HIDE_VARIABLE(HYPRE_LIBRARY_DIR) + SET (HYPRE_DISABLED TRUE CACHE INTERNAL "GUI - return when first set") + +ENDIF() + +# --------------------------------------------------------------- +# Find (and test) the PETSc libraries +# --------------------------------------------------------------- + +# >>>>>>> NOTE: Need to add check for PETSc precision and integer type + +IF(PETSC_ENABLE) + SHOW_VARIABLE(PETSC_INCLUDE_DIR PATH "PETSc include directory" + "${PETSC_INCLUDE_DIR}") + SHOW_VARIABLE(PETSC_LIBRARY_DIR PATH + "PETSc library directory" "${PETSC_LIBRARY_DIR}") + + INCLUDE(SundialsPETSc) + + IF(PETSC_FOUND) + # sundials_config.h symbol + SET(SUNDIALS_PETSC TRUE) + INCLUDE_DIRECTORIES(${PETSC_INCLUDE_DIR}) + ENDIF(PETSC_FOUND) + + IF(PETSC_LIBRARIES AND NOT PETSC_FOUND) + PRINT_WARNING("PETSC not functional - support will not be provided" + "Double check spelling specified libraries (search is case sensitive)") + ENDIF(PETSC_LIBRARIES AND NOT PETSC_FOUND) + +ELSE() + + HIDE_VARIABLE(PETSC_LIBRARY_DIR) + HIDE_VARIABLE(PETSC_INCLUDE_DIR) + SET (PETSC_DISABLED TRUE CACHE INTERNAL "GUI - return when first set") + +ENDIF() + + +# =============================================================== +# Add source and configuration files +# =============================================================== + +# --------------------------------------------------------------- +# Configure the header file sundials_config.h +# --------------------------------------------------------------- + +# All required substitution variables should be available at this point. +# Generate the header file and place it in the binary dir. +CONFIGURE_FILE( + ${PROJECT_SOURCE_DIR}/include/sundials/sundials_config.in + ${PROJECT_BINARY_DIR}/include/sundials/sundials_config.h + ) +CONFIGURE_FILE( + ${PROJECT_SOURCE_DIR}/include/sundials/sundials_fconfig.in + ${PROJECT_BINARY_DIR}/include/sundials/sundials_fconfig.h + ) + +# Add the include directory in the source tree and the one in +# the binary tree (for the header file sundials_config.h) +INCLUDE_DIRECTORIES(${PROJECT_SOURCE_DIR}/include ${PROJECT_BINARY_DIR}/include) + +# --------------------------------------------------------------- +# Add selected modules to the build system +# --------------------------------------------------------------- + +# Shared components + +ADD_SUBDIRECTORY(src/sundials) +ADD_SUBDIRECTORY(src/nvec_ser) +ADD_SUBDIRECTORY(src/sunmat_dense) +ADD_SUBDIRECTORY(src/sunmat_band) +ADD_SUBDIRECTORY(src/sunmat_sparse) +ADD_SUBDIRECTORY(src/sunlinsol_band) +ADD_SUBDIRECTORY(src/sunlinsol_dense) +IF(KLU_FOUND) + ADD_SUBDIRECTORY(src/sunlinsol_klu) +ENDIF(KLU_FOUND) +IF(SUPERLUMT_FOUND) + ADD_SUBDIRECTORY(src/sunlinsol_superlumt) +ENDIF(SUPERLUMT_FOUND) +IF(LAPACK_FOUND) + ADD_SUBDIRECTORY(src/sunlinsol_lapackband) + ADD_SUBDIRECTORY(src/sunlinsol_lapackdense) +ENDIF(LAPACK_FOUND) +ADD_SUBDIRECTORY(src/sunlinsol_spgmr) +ADD_SUBDIRECTORY(src/sunlinsol_spfgmr) +ADD_SUBDIRECTORY(src/sunlinsol_spbcgs) +ADD_SUBDIRECTORY(src/sunlinsol_sptfqmr) +ADD_SUBDIRECTORY(src/sunlinsol_pcg) +IF(MPIC_FOUND) + ADD_SUBDIRECTORY(src/nvec_par) +ENDIF(MPIC_FOUND) + +IF(HYPRE_FOUND) + ADD_SUBDIRECTORY(src/nvec_parhyp) +ENDIF(HYPRE_FOUND) + +IF(OPENMP_FOUND) + ADD_SUBDIRECTORY(src/nvec_openmp) +ENDIF(OPENMP_FOUND) + +IF(PTHREADS_FOUND) + ADD_SUBDIRECTORY(src/nvec_pthreads) +ENDIF(PTHREADS_FOUND) + +IF(PETSC_FOUND) + ADD_SUBDIRECTORY(src/nvec_petsc) +ENDIF(PETSC_FOUND) + +IF(CUDA_FOUND) + ADD_SUBDIRECTORY(src/nvec_cuda) +ENDIF(CUDA_FOUND) + +IF(RAJA_FOUND) + ADD_SUBDIRECTORY(src/nvec_raja) +ENDIF(RAJA_FOUND) + +# ARKODE library + +IF(BUILD_ARKODE) + ADD_SUBDIRECTORY(src/arkode) + IF(FCMIX_ENABLE AND F77_FOUND) + ADD_SUBDIRECTORY(src/arkode/fcmix) + ENDIF(FCMIX_ENABLE AND F77_FOUND) +ENDIF(BUILD_ARKODE) + +# CVODE library + +IF(BUILD_CVODE) + ADD_SUBDIRECTORY(src/cvode) + IF(FCMIX_ENABLE AND F77_FOUND) + ADD_SUBDIRECTORY(src/cvode/fcmix) + ENDIF(FCMIX_ENABLE AND F77_FOUND) +ENDIF(BUILD_CVODE) + +# CVODES library + +IF(BUILD_CVODES) + ADD_SUBDIRECTORY(src/cvodes) +ENDIF(BUILD_CVODES) + +# IDA library + +IF(BUILD_IDA) + ADD_SUBDIRECTORY(src/ida) + IF(FCMIX_ENABLE AND F77_FOUND) + ADD_SUBDIRECTORY(src/ida/fcmix) + ENDIF(FCMIX_ENABLE AND F77_FOUND) +ENDIF(BUILD_IDA) + +# IDAS library + +IF(BUILD_IDAS) + ADD_SUBDIRECTORY(src/idas) +ENDIF(BUILD_IDAS) + +# KINSOL library + +IF(BUILD_KINSOL) + ADD_SUBDIRECTORY(src/kinsol) + IF(FCMIX_ENABLE AND F77_FOUND) + ADD_SUBDIRECTORY(src/kinsol/fcmix) + ENDIF(FCMIX_ENABLE AND F77_FOUND) +ENDIF(BUILD_KINSOL) + +# CPODES library + +IF(BUILD_CPODES) + ADD_SUBDIRECTORY(src/cpodes) +ENDIF(BUILD_CPODES) + +# --------------------------------------------------------------- +# Include the subdirectories corresponding to various examples +# --------------------------------------------------------------- + +# If building and installing the examples is enabled, include +# the subdirectories for those examples that will be built. +# Also, if we will generate exported example Makefiles, set +# variables needed in generating them from templates. + +# For now, TestRunner is not being distributed. +# So: +# - Don't show TESTRUNNER variable +# - Don't enable testing if TestRunner if not found. +# - There will be no 'make test' target + +INCLUDE(SundialsAddTest) +HIDE_VARIABLE(TESTRUNNER) + +IF(EXAMPLES_ENABLED) + + # enable regression testing with 'make test' + IF(TESTRUNNER) + ENABLE_TESTING() + ENDIF() + + # set variables used in generating CMake and Makefiles for examples + IF(EXAMPLES_INSTALL) + + SET(SHELL "sh") + SET(prefix "${CMAKE_INSTALL_PREFIX}") + SET(exec_prefix "${CMAKE_INSTALL_PREFIX}") + SET(includedir "${prefix}/include") + SET(libdir "${exec_prefix}/lib") + SET(CPP "${CMAKE_C_COMPILER}") + SET(CPPFLAGS "${CMAKE_C_FLAGS_RELEASE}") + SET(CC "${CMAKE_C_COMPILER}") + SET(CFLAGS "${CMAKE_C_FLAGS_RELEASE}") + SET(LDFLAGS "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") + LIST2STRING(EXTRA_LINK_LIBS LIBS) + + IF(CXX_FOUND) + SET(CXX "${CMAKE_CXX_COMPILER}") + SET(CXX_LNKR "${CMAKE_CXX_COMPILER}") + SET(CXXFLAGS "${CMAKE_CXX_FLAGS_RELEASE}") + SET(CXX_LDFLAGS "${CMAKE_CXX_FLAGS_RELEASE}") + LIST2STRING(EXTRA_LINK_LIBS CXX_LIBS) + ENDIF(CXX_FOUND) + + IF(F77_FOUND) + SET(F77 "${CMAKE_Fortran_COMPILER}") + SET(F77_LNKR "${CMAKE_Fortran_COMPILER}") + SET(FFLAGS "${CMAKE_Fortran_FLAGS_RELEASE}") + SET(F77_LDFLAGS "${CMAKE_Fortran_FLAGS_RELEASE}") + LIST2STRING(EXTRA_LINK_LIBS F77_LIBS) + ENDIF(F77_FOUND) + + IF(F90_FOUND) + SET(F90 "${CMAKE_Fortran_COMPILER}") + SET(F90_LNKR "${CMAKE_Fortran_COMPILER}") + SET(F90FLAGS "${CMAKE_Fortran_FLAGS_RELEASE}") + SET(F90_LDFLAGS "${CMAKE_Fortran_FLAGS_RELEASE}") + LIST2STRING(EXTRA_LINK_LIBS F90_LIBS) + ENDIF(F90_FOUND) + + IF(SUPERLUMT_FOUND) + LIST2STRING(SUPERLUMT_LIBRARIES SUPERLUMT_LIBS) + SET(SUPERLUMT_LIBS "${SUPERLUMT_LINKER_FLAGS} ${SUPERLUMT_LIBS}") + ENDIF(SUPERLUMT_FOUND) + + IF(KLU_FOUND) + LIST2STRING(KLU_LIBRARIES KLU_LIBS) + SET(KLU_LIBS "${KLU_LINKER_FLAGS} ${KLU_LIBS}") + ENDIF(KLU_FOUND) + + IF(BLAS_FOUND) + LIST2STRING(BLAS_LIBRARIES BLAS_LIBS) + ENDIF(BLAS_FOUND) + + IF(LAPACK_FOUND) + LIST2STRING(LAPACK_LIBRARIES LAPACK_LIBS) + ENDIF(LAPACK_FOUND) + + IF(MPIC_FOUND) + IF(MPI_MPICC) + SET(MPICC "${MPI_MPICC}") + SET(MPI_INC_DIR ".") + SET(MPI_LIB_DIR ".") + SET(MPI_LIBS "") + SET(MPI_FLAGS "") + ELSE(MPI_MPICC) + SET(MPICC "${CMAKE_C_COMPILER}") + SET(MPI_INC_DIR "${MPI_INCLUDE_PATH}") + SET(MPI_LIB_DIR ".") + LIST2STRING(MPI_LIBRARIES MPI_LIBS) + ENDIF(MPI_MPICC) + SET(HYPRE_INC_DIR "${HYPRE_INCLUDE_DIR}") + SET(HYPRE_LIB_DIR "${HYPRE_LIBRARY_DIR}") + SET(HYPRE_LIBS "${HYPRE_LIBRARIES}") + ENDIF(MPIC_FOUND) + + IF(MPICXX_FOUND) + IF(MPI_MPICXX) + SET(MPICXX "${MPI_MPICXX}") + ELSE(MPI_MPICXX) + SET(MPICXX "${CMAKE_CXX_COMPILER}") + LIST2STRING(MPI_LIBRARIES MPI_LIBS) + ENDIF(MPI_MPICXX) + ENDIF(MPICXX_FOUND) + + IF(MPIF_FOUND) + IF(MPI_MPIF77) + SET(MPIF77 "${MPI_MPIF77}") + SET(MPIF77_LNKR "${MPI_MPIF77}") + ELSE(MPI_MPIF77) + SET(MPIF77 "${CMAKE_Fortran_COMPILER}") + SET(MPIF77_LNKR "${CMAKE_Fortran_COMPILER}") + SET(MPI_INC_DIR "${MPI_INCLUDE_PATH}") + SET(MPI_LIB_DIR ".") + LIST2STRING(MPI_LIBRARIES MPI_LIBS) + ENDIF(MPI_MPIF77) + ENDIF(MPIF_FOUND) + + IF(MPIF90_FOUND) + IF(MPI_MPIF90) + SET(MPIF90 "${MPI_MPIF90}") + SET(MPIF90_LNKR "${MPI_MPIF90}") + ELSE(MPI_MPIF90) + SET(MPIF90 "${CMAKE_Fortran_COMPILER}") + SET(MPIF90_LNKR "${CMAKE_Fortran_COMPILER}") + LIST2STRING(MPI_LIBRARIES MPI_LIBS) + ENDIF(MPI_MPIF90) + ENDIF(MPIF90_FOUND) + + ENDIF(EXAMPLES_INSTALL) + + # add ARKode examples + IF(BUILD_ARKODE) + # C examples + IF(EXAMPLES_ENABLE_C) + ADD_SUBDIRECTORY(examples/arkode/C_serial) + IF(OPENMP_FOUND) + ADD_SUBDIRECTORY(examples/arkode/C_openmp) + ENDIF() + IF(MPIC_FOUND) + ADD_SUBDIRECTORY(examples/arkode/C_parallel) + ENDIF() + IF(HYPRE_ENABLE AND HYPRE_FOUND) + ADD_SUBDIRECTORY(examples/arkode/C_parhyp) + ENDIF() + ENDIF() + # C++ examples + IF(EXAMPLES_ENABLE_CXX) + IF(CXX_FOUND) + ADD_SUBDIRECTORY(examples/arkode/CXX_serial) + ENDIF() + IF(MPICXX_FOUND) + ADD_SUBDIRECTORY(examples/arkode/CXX_parallel) + ENDIF() + ENDIF() + # F77 examples + IF(EXAMPLES_ENABLE_F77) + IF(F77_FOUND) + ADD_SUBDIRECTORY(examples/arkode/F77_serial) + ENDIF() + IF(MPIF_FOUND) + ADD_SUBDIRECTORY(examples/arkode/F77_parallel) + ENDIF() + ENDIF() + # F90 examples + IF(EXAMPLES_ENABLE_F90) + IF(F90_FOUND) + ADD_SUBDIRECTORY(examples/arkode/F90_serial) + ENDIF() + IF(MPIF90_FOUND) + ADD_SUBDIRECTORY(examples/arkode/F90_parallel) + ENDIF() + ENDIF() + ENDIF(BUILD_ARKODE) + + # add CVODE examples + IF(BUILD_CVODE) + # C examples + IF(EXAMPLES_ENABLE_C) + ADD_SUBDIRECTORY(examples/cvode/serial) + IF(OPENMP_FOUND) + ADD_SUBDIRECTORY(examples/cvode/C_openmp) + ENDIF() + IF(MPIC_FOUND) + ADD_SUBDIRECTORY(examples/cvode/parallel) + ENDIF() + IF(HYPRE_ENABLE AND HYPRE_FOUND) + ADD_SUBDIRECTORY(examples/cvode/parhyp) + ENDIF() + ENDIF() + # Fortran examples + IF(EXAMPLES_ENABLE_F77) + IF(F77_FOUND) + ADD_SUBDIRECTORY(examples/cvode/fcmix_serial) + ENDIF() + IF(MPIF_FOUND) + ADD_SUBDIRECTORY(examples/cvode/fcmix_parallel) + ENDIF() + ENDIF() + # cuda examples + IF(EXAMPLES_ENABLE_CUDA) + IF(CUDA_ENABLE AND CUDA_FOUND) + ADD_SUBDIRECTORY(examples/cvode/cuda) + ENDIF() + ENDIF(EXAMPLES_ENABLE_CUDA) + # raja examples + IF(EXAMPLES_ENABLE_RAJA) + IF(RAJA_ENABLE AND RAJA_FOUND) + ADD_SUBDIRECTORY(examples/cvode/raja) + ENDIF() + ENDIF(EXAMPLES_ENABLE_RAJA) + ENDIF(BUILD_CVODE) + + # add CVODES Examples + IF(BUILD_CVODES) + # C examples + IF(EXAMPLES_ENABLE_C) + ADD_SUBDIRECTORY(examples/cvodes/serial) + IF(MPIC_FOUND) + ADD_SUBDIRECTORY(examples/cvodes/parallel) + ENDIF() + IF(OPENMP_FOUND) + ADD_SUBDIRECTORY(examples/cvodes/C_openmp) + ENDIF() + ENDIF() + ENDIF(BUILD_CVODES) + + # add IDA examples + IF(BUILD_IDA) + # C examples + IF(EXAMPLES_ENABLE_C) + ADD_SUBDIRECTORY(examples/ida/serial) + IF(OPENMP_FOUND) + ADD_SUBDIRECTORY(examples/ida/C_openmp) + ENDIF() + IF(MPIC_FOUND) + ADD_SUBDIRECTORY(examples/ida/parallel) + ENDIF() + IF(PETSC_FOUND) + ADD_SUBDIRECTORY(examples/ida/petsc) + ENDIF() + ENDIF() + # Fortran examples + IF(EXAMPLES_ENABLE_F77) + IF(F77_FOUND) + ADD_SUBDIRECTORY(examples/ida/fcmix_serial) + ENDIF() + IF(OPENMP_FOUND) + ADD_SUBDIRECTORY(examples/ida/fcmix_openmp) + ENDIF() + IF(PTHREADS_FOUND) + ADD_SUBDIRECTORY(examples/ida/fcmix_pthreads) + ENDIF() + IF(MPIF_FOUND) + ADD_SUBDIRECTORY(examples/ida/fcmix_parallel) + ENDIF() + ENDIF() + ENDIF(BUILD_IDA) + + # add IDAS examples + IF(BUILD_IDAS) + # C examples + IF(EXAMPLES_ENABLE_C) + ADD_SUBDIRECTORY(examples/idas/serial) + IF(OPENMP_FOUND) + ADD_SUBDIRECTORY(examples/idas/C_openmp) + ENDIF() + IF(MPIC_FOUND) + ADD_SUBDIRECTORY(examples/idas/parallel) + ENDIF() + ENDIF() + ENDIF(BUILD_IDAS) + + # add KINSOL examples + IF(BUILD_KINSOL) + # C examples + IF(EXAMPLES_ENABLE_C) + ADD_SUBDIRECTORY(examples/kinsol/serial) + IF(OPENMP_FOUND) + # the only example here need special handling from testrunner (not yet implemented) + ADD_SUBDIRECTORY(examples/kinsol/C_openmp) + ENDIF() + IF(MPIC_FOUND) + ADD_SUBDIRECTORY(examples/kinsol/parallel) + ENDIF() + ENDIF() + # Fortran examples + IF(EXAMPLES_ENABLE_F77) + IF(F77_FOUND) + ADD_SUBDIRECTORY(examples/kinsol/fcmix_serial) + ENDIF() + IF(MPIF_FOUND) + ADD_SUBDIRECTORY(examples/kinsol/fcmix_parallel) + ENDIF() + ENDIF() + ENDIF(BUILD_KINSOL) + + # add CPODES examples + IF(BUILD_CPODES) + IF(EXAMPLES_ENABLE_C) + ADD_SUBDIRECTORY(examples/cpodes/serial) + IF(MPIC_FOUND) + ADD_SUBDIRECTORY(examples/cpodes/parallel) + ENDIF() + ENDIF() + ENDIF(BUILD_CPODES) + + # Always add the nvector serial examples + ADD_SUBDIRECTORY(examples/nvector/serial) + + # # Always add the serial sunmatrix dense/band/sparse examples + ADD_SUBDIRECTORY(examples/sunmatrix/dense) + ADD_SUBDIRECTORY(examples/sunmatrix/band) + ADD_SUBDIRECTORY(examples/sunmatrix/sparse) + + # # Always add the serial sunlinearsolver dense/band/spils examples + ADD_SUBDIRECTORY(examples/sunlinsol/band) + ADD_SUBDIRECTORY(examples/sunlinsol/dense) + IF(KLU_FOUND) + ADD_SUBDIRECTORY(examples/sunlinsol/klu) + ENDIF(KLU_FOUND) + IF(SUPERLUMT_FOUND) + ADD_SUBDIRECTORY(examples/sunlinsol/superlumt) + ENDIF(SUPERLUMT_FOUND) + IF(LAPACK_FOUND) + ADD_SUBDIRECTORY(examples/sunlinsol/lapackband) + ADD_SUBDIRECTORY(examples/sunlinsol/lapackdense) + ENDIF(LAPACK_FOUND) + ADD_SUBDIRECTORY(examples/sunlinsol/spgmr/serial) + ADD_SUBDIRECTORY(examples/sunlinsol/spfgmr/serial) + ADD_SUBDIRECTORY(examples/sunlinsol/spbcgs/serial) + ADD_SUBDIRECTORY(examples/sunlinsol/sptfqmr/serial) + ADD_SUBDIRECTORY(examples/sunlinsol/pcg/serial) + + IF(MPIC_FOUND) + ADD_SUBDIRECTORY(examples/nvector/parallel) + ADD_SUBDIRECTORY(examples/sunlinsol/spgmr/parallel) + ADD_SUBDIRECTORY(examples/sunlinsol/spfgmr/parallel) + ADD_SUBDIRECTORY(examples/sunlinsol/spbcgs/parallel) + ADD_SUBDIRECTORY(examples/sunlinsol/sptfqmr/parallel) + #ADD_SUBDIRECTORY(examples/sunlinsol/pcg/parallel) + ENDIF(MPIC_FOUND) + + IF(HYPRE_FOUND) + ADD_SUBDIRECTORY(examples/nvector/parhyp) + ENDIF() + + IF(PTHREADS_FOUND) + ADD_SUBDIRECTORY(examples/nvector/pthreads) + ENDIF() + + IF(OPENMP_FOUND) + ADD_SUBDIRECTORY(examples/nvector/C_openmp) + ENDIF() + + IF(PETSC_FOUND) + ADD_SUBDIRECTORY(examples/nvector/petsc) + ENDIF() + + IF(CUDA_FOUND) + ADD_SUBDIRECTORY(examples/nvector/cuda) + ENDIF(CUDA_FOUND) + + IF(RAJA_FOUND) + ADD_SUBDIRECTORY(examples/nvector/raja) + ENDIF(RAJA_FOUND) + +ENDIF(EXAMPLES_ENABLED) + +# --------------------------------------------------------------- +# Install configuration header files and license file +# --------------------------------------------------------------- + +# install configured header file +INSTALL( + FILES ${PROJECT_BINARY_DIR}/include/sundials/sundials_config.h + DESTINATION include/sundials + ) + +# install configured header file for Fortran 90 +INSTALL( + FILES ${PROJECT_BINARY_DIR}/include/sundials/sundials_fconfig.h + DESTINATION include/sundials + ) + +# install license file +INSTALL( + FILES ${PROJECT_SOURCE_DIR}/LICENSE + DESTINATION .) diff --git a/scripts/replace-cmake/CMakeLists.txt b/scripts/replace-cmake/sundials-4.1.0/CMakeLists.txt similarity index 99% rename from scripts/replace-cmake/CMakeLists.txt rename to scripts/replace-cmake/sundials-4.1.0/CMakeLists.txt index c85208d22a..d26c4c9582 100644 --- a/scripts/replace-cmake/CMakeLists.txt +++ b/scripts/replace-cmake/sundials-4.1.0/CMakeLists.txt @@ -995,6 +995,7 @@ IF(KLU_ENABLE) set(KLU_FOUND TRUE) get_filename_component(PYBAMM_DIR ${PROJECT_SOURCE_DIR} DIRECTORY) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PYBAMM_DIR}) # use FindSuiteSparse.cmake that is in PyBaMM root + set(SuiteSparse_ROOT ${PYBAMM_DIR}/SuiteSparse-5.6.0) find_package(SuiteSparse OPTIONAL_COMPONENTS KLU AMD COLAMD BTF) include_directories(${SuiteSparse_INCLUDE_DIRS}) set(KLU_LIBRARIES ${SuiteSparse_LIBRARIES}) diff --git a/scripts/replace-cmake/sundials-5.0.0/CMakeLists.txt b/scripts/replace-cmake/sundials-5.0.0/CMakeLists.txt new file mode 100644 index 0000000000..d26c4c9582 --- /dev/null +++ b/scripts/replace-cmake/sundials-5.0.0/CMakeLists.txt @@ -0,0 +1,1178 @@ +# --------------------------------------------------------------- +# Programmer: Radu Serban, David J. Gardner, Cody J. Balos, +# and Slaven Peles @ LLNL +# --------------------------------------------------------------- +# SUNDIALS Copyright Start +# Copyright (c) 2002-2019, Lawrence Livermore National Security +# and Southern Methodist University. +# All rights reserved. +# +# See the top-level LICENSE and NOTICE files for details. +# +# SPDX-License-Identifier: BSD-3-Clause +# SUNDIALS Copyright End +# --------------------------------------------------------------- +# Top level CMakeLists.txt for SUNDIALS (for cmake build system) +# --------------------------------------------------------------- + +# --------------------------------------------------------------- +# Initial commands +# --------------------------------------------------------------- + +# Require a fairly recent cmake version +cmake_minimum_required(VERSION 3.1.3) + +# Libraries linked via full path no longer produce linker search paths +# Allows examples to build +if(COMMAND cmake_policy) + cmake_policy(SET CMP0003 NEW) +endif(COMMAND cmake_policy) + +# MACOSX_RPATH is enabled by default +# Fixes dynamic loading on OSX +if(POLICY CMP0042) + cmake_policy(SET CMP0042 NEW) # Added in CMake 3.0 +else() + if(APPLE) + set(CMAKE_MACOSX_RPATH 1) + endif() +endif() + +# Project SUNDIALS (initially only C supported) +# sets PROJECT_SOURCE_DIR and PROJECT_BINARY_DIR variables +PROJECT(sundials C) + +# Set some variables with info on the SUNDIALS project +SET(PACKAGE_BUGREPORT "woodward6@llnl.gov") +SET(PACKAGE_NAME "SUNDIALS") +SET(PACKAGE_STRING "SUNDIALS 4.1.0") +SET(PACKAGE_TARNAME "sundials") + +# set SUNDIALS version numbers +# (use "" for the version label if none is needed) +SET(PACKAGE_VERSION_MAJOR "4") +SET(PACKAGE_VERSION_MINOR "1") +SET(PACKAGE_VERSION_PATCH "0") +SET(PACKAGE_VERSION_LABEL "") + +IF(PACKAGE_VERSION_LABEL) + SET(PACKAGE_VERSION "${PACKAGE_VERSION_MAJOR}.${PACKAGE_VERSION_MINOR}.${PACKAGE_VERSION_PATCH}-${PACKAGE_VERSION_LABEL}") +ELSE() + SET(PACKAGE_VERSION "${PACKAGE_VERSION_MAJOR}.${PACKAGE_VERSION_MINOR}.${PACKAGE_VERSION_PATCH}") +ENDIF() + +SET_PROPERTY(GLOBAL PROPERTY USE_FOLDERS ON) + +# Prohibit in-source build +IF("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") + MESSAGE(FATAL_ERROR "In-source build prohibited.") +ENDIF("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") + +# Hide some cache variables +MARK_AS_ADVANCED(EXECUTABLE_OUTPUT_PATH LIBRARY_OUTPUT_PATH) + +# Always show the C compiler and flags +MARK_AS_ADVANCED(CLEAR + CMAKE_C_COMPILER + CMAKE_C_FLAGS) + +# Specify the VERSION and SOVERSION for shared libraries + +SET(arkodelib_VERSION "3.1.0") +SET(arkodelib_SOVERSION "3") + +SET(cvodelib_VERSION "4.1.0") +SET(cvodelib_SOVERSION "4") + +SET(cvodeslib_VERSION "4.1.0") +SET(cvodeslib_SOVERSION "4") + +SET(idalib_VERSION "4.1.0") +SET(idalib_SOVERSION "4") + +SET(idaslib_VERSION "3.1.0") +SET(idaslib_SOVERSION "3") + +SET(kinsollib_VERSION "4.1.0") +SET(kinsollib_SOVERSION "4") + +SET(cpodeslib_VERSION "0.0.0") +SET(cpodeslib_SOVERSION "0") + +SET(nveclib_VERSION "4.1.0") +SET(nveclib_SOVERSION "4") + +SET(sunmatrixlib_VERSION "2.1.0") +SET(sunmatrixlib_SOVERSION "2") + +SET(sunlinsollib_VERSION "2.1.0") +SET(sunlinsollib_SOVERSION "2") + +SET(sunnonlinsollib_VERSION "1.1.0") +SET(sunnonlinsollib_SOVERSION "1") + +# Specify the location of additional CMAKE modules +SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/config) + +# Get correct build paths automatically, but expose CMAKE_INSTALL_LIBDIR +# as a regular cache variable so that a user can more easily see what +# the library dir was set to be by GNUInstallDirs. +INCLUDE(GNUInstallDirs) +MARK_AS_ADVANCED(CLEAR CMAKE_INSTALL_LIBDIR) + +# --------------------------------------------------------------- +# Which modules to build? +# --------------------------------------------------------------- + +# For each SUNDIALS solver available (i.e. for which we have the +# sources), give the user the option of enabling/disabling it. + +IF(IS_DIRECTORY "${sundials_SOURCE_DIR}/src/arkode") + OPTION(BUILD_ARKODE "Build the ARKODE library" ON) +ELSE() + SET(BUILD_ARKODE OFF) +ENDIF() + +IF(IS_DIRECTORY "${sundials_SOURCE_DIR}/src/cvode") + OPTION(BUILD_CVODE "Build the CVODE library" ON) +ELSE() + SET(BUILD_CVODE OFF) +ENDIF() + +IF(IS_DIRECTORY "${sundials_SOURCE_DIR}/src/cvodes") + OPTION(BUILD_CVODES "Build the CVODES library" ON) +ELSE() + SET(BUILD_CVODES OFF) +ENDIF() + +IF(IS_DIRECTORY "${sundials_SOURCE_DIR}/src/ida") + OPTION(BUILD_IDA "Build the IDA library" ON) +ELSE() + SET(BUILD_IDA OFF) +ENDIF() + +IF(IS_DIRECTORY "${sundials_SOURCE_DIR}/src/idas") + OPTION(BUILD_IDAS "Build the IDAS library" ON) +ELSE() + SET(BUILD_IDAS OFF) +ENDIF() + +IF(IS_DIRECTORY "${sundials_SOURCE_DIR}/src/kinsol") + OPTION(BUILD_KINSOL "Build the KINSOL library" ON) +ELSE() + SET(BUILD_KINSOL OFF) +ENDIF() + +# CPODES is always OFF for now. (commented out for Release); ToDo: better way to do this? +#IF(IS_DIRECTORY "${sundials_SOURCE_DIR}/src/cpodes") +# OPTION(BUILD_CPODES "Build the CPODES library" OFF) +#ELSE() +# SET(BUILD_CPODES OFF) +#ENDIF() + +# --------------------------------------------------------------- +# MACRO definitions +# --------------------------------------------------------------- +INCLUDE(CMakeParseArguments) # can be removed when CMake 3.5+ is required +INCLUDE(SundialsCMakeMacros) +INCLUDE(SundialsAddF2003InterfaceLibrary) +INCLUDE(SundialsAddTest) +INCLUDE(SundialsAddTestInstall) + +# --------------------------------------------------------------- +# Check for deprecated SUNDIALS CMake options/variables +# --------------------------------------------------------------- +INCLUDE(SundialsDeprecated) + +# --------------------------------------------------------------- +# xSDK specific options +# --------------------------------------------------------------- +INCLUDE(SundialsXSDK) + +# --------------------------------------------------------------- +# Build specific C flags +# --------------------------------------------------------------- + +# Hide all build type specific flags +MARK_AS_ADVANCED(FORCE + CMAKE_C_FLAGS_DEBUG + CMAKE_C_FLAGS_MINSIZEREL + CMAKE_C_FLAGS_RELEASE + CMAKE_C_FLAGS_RELWITHDEBINFO) + +# Only show flags for the current build type if it is set +# NOTE: Build specific flags are appended those in CMAKE_C_FLAGS +IF(CMAKE_BUILD_TYPE) + IF(CMAKE_BUILD_TYPE MATCHES "Debug") + MESSAGE("Appending C debug flags") + MARK_AS_ADVANCED(CLEAR CMAKE_C_FLAGS_DEBUG) + ELSEIF(CMAKE_BUILD_TYPE MATCHES "MinSizeRel") + MESSAGE("Appending C min size release flags") + MARK_AS_ADVANCED(CLEAR CMAKE_C_FLAGS_MINSIZEREL) + ELSEIF(CMAKE_BUILD_TYPE MATCHES "Release") + MESSAGE("Appending C release flags") + MARK_AS_ADVANCED(CLEAR CMAKE_C_FLAGS_RELEASE) + ELSEIF(CMAKE_BUILD_TYPE MATCHES "RelWithDebInfo") + MESSAGE("Appending C release with debug info flags") + MARK_AS_ADVANCED(CLEAR CMAKE_C_FLAGS_RELWITHDEBINFO) + ENDIF() +ENDIF() + +# --------------------------------------------------------------- +# Option to specify precision (realtype) +# --------------------------------------------------------------- + +SET(DOCSTR "single, double, or extended") +SHOW_VARIABLE(SUNDIALS_PRECISION STRING "${DOCSTR}" "double") + +# prepare substitution variable PRECISION_LEVEL for sundials_config.h +STRING(TOUPPER ${SUNDIALS_PRECISION} SUNDIALS_PRECISION) +SET(PRECISION_LEVEL "#define SUNDIALS_${SUNDIALS_PRECISION}_PRECISION 1") + +# prepare substitution variable FPRECISION_LEVEL for sundials_fconfig.h +IF(SUNDIALS_PRECISION MATCHES "SINGLE") + SET(FPRECISION_LEVEL "4") +ENDIF(SUNDIALS_PRECISION MATCHES "SINGLE") +IF(SUNDIALS_PRECISION MATCHES "DOUBLE") + SET(FPRECISION_LEVEL "8") +ENDIF(SUNDIALS_PRECISION MATCHES "DOUBLE") +IF(SUNDIALS_PRECISION MATCHES "EXTENDED") + SET(FPRECISION_LEVEL "16") +ENDIF(SUNDIALS_PRECISION MATCHES "EXTENDED") + +# --------------------------------------------------------------- +# Option to specify index type +# --------------------------------------------------------------- + +SET(DOCSTR "Signed 64-bit (64) or signed 32-bit (32) integer") +SHOW_VARIABLE(SUNDIALS_INDEX_SIZE STRING "${DOCSTR}" "64") +SET(DOCSTR "Integer type to use for indices in SUNDIALS") +SHOW_VARIABLE(SUNDIALS_INDEX_TYPE STRING "${DOCSTR}" "") +MARK_AS_ADVANCED(SUNDIALS_INDEX_TYPE) +include(SundialsIndexSize) + +# --------------------------------------------------------------- +# Enable Fortran interface? +# --------------------------------------------------------------- + +# Fortran interface is disabled by default +SET(DOCSTR "Enable Fortran 77 interfaces") +OPTION(F77_INTERFACE_ENABLE "${DOCSTR}" OFF) + +# Check that at least one solver with a Fortran 77 interface is built +IF(NOT BUILD_ARKODE AND NOT BUILD_CVODE AND NOT BUILD_IDA AND NOT BUILD_KINSOL) + IF(F77_INTERFACE_ENABLE) + PRINT_WARNING("Enabled packages do not support Fortran 77 interface" "Disabling F77 interface") + FORCE_VARIABLE(F77_INTERFACE_ENABLE BOOL "${DOCSTR}" OFF) + ENDIF() + HIDE_VARIABLE(F77_INTERFACE_ENABLE) +ENDIF() + +# Fortran 2003 interface is disabled by default +SET(DOCSTR "Enable Fortran 2003 interfaces") +OPTION(F2003_INTERFACE_ENABLE "${DOCSTR}" OFF) + +# Check that at least one solver with a Fortran 2003 interface is built +IF(NOT BUILD_CVODE) + IF(F2003_INTERFACE_ENABLE) + PRINT_WARNING("Enabled packages do not support Fortran 2003 interface" "Disabling F2003 interface") + FORCE_VARIABLE(F2003_INTERFACE_ENABLE BOOL "${DOCSTR}" OFF) + ENDIF() + HIDE_VARIABLE(F2003_INTERFACE_ENABLE) +ENDIF() + +IF(F2003_INTERFACE_ENABLE) + # F2003 interface only supports double precision + IF(NOT (SUNDIALS_PRECISION MATCHES "DOUBLE")) + PRINT_WARNING("F2003 interface is not compatible with ${SUNDIALS_PRECISION} precision" + "Disabling F2003 interface") + FORCE_VARIABLE(F2003_INTERFACE_ENABLE BOOL "${DOCSTR}" OFF) + ENDIF() + + # F2003 interface only supports 64-bit indices + IF(NOT (SUNDIALS_INDEX_SIZE MATCHES "64")) + PRINT_WARNING("F2003 interface is not compatible with ${SUNDIALS_INDEX_SIZE}-bit indicies" + "Disabling F2003 interface") + FORCE_VARIABLE(F2003_INTERFACE_ENABLE BOOL "${DOCSTR}" OFF) + ENDIF() + + # Put all F2003 modules into one build directory + SET(CMAKE_Fortran_MODULE_DIRECTORY "${CMAKE_BINARY_DIR}/fortran") + + # Allow a user to set where the Fortran modules will be installed + SET(DOCSTR "Directory where Fortran module files are installed") + SHOW_VARIABLE(Fortran_INSTALL_MODDIR DIRECTORY "${DOCSTR}" "fortran") +ENDIF() + +# --------------------------------------------------------------- +# Options to build static and/or shared libraries +# --------------------------------------------------------------- + +OPTION(BUILD_STATIC_LIBS "Build static libraries" ON) +OPTION(BUILD_SHARED_LIBS "Build shared libraries" ON) + +# Prepare substitution variable SUNDIALS_EXPORT for sundials_config.h +# When building shared SUNDIALS libraries under Windows, use +# #define SUNDIALS_EXPORT __declspec(dllexport) +# When linking to shared SUNDIALS libraries under Windows, use +# #define SUNDIALS_EXPORT __declspec(dllimport) +# In all other cases (other platforms or static libraries +# under Windows), the SUNDIALS_EXPORT macro is empty + +IF(BUILD_SHARED_LIBS AND WIN32) + SET(SUNDIALS_EXPORT + "#ifdef BUILD_SUNDIALS_LIBRARY +#define SUNDIALS_EXPORT __declspec(dllexport) +#else +#define SUNDIALS_EXPORT __declspec(dllimport) +#endif") +ELSE(BUILD_SHARED_LIBS AND WIN32) + SET(SUNDIALS_EXPORT "#define SUNDIALS_EXPORT") +ENDIF(BUILD_SHARED_LIBS AND WIN32) + +# Make sure we build at least one type of libraries +IF(NOT BUILD_STATIC_LIBS AND NOT BUILD_SHARED_LIBS) + PRINT_WARNING("Both static and shared library generation were disabled" + "Building static libraries was re-enabled") + FORCE_VARIABLE(BUILD_STATIC_LIBS BOOL "Build static libraries" ON) +ENDIF(NOT BUILD_STATIC_LIBS AND NOT BUILD_SHARED_LIBS) + +# --------------------------------------------------------------- +# Option to use the generic math libraries (UNIX only) +# --------------------------------------------------------------- + +IF(UNIX) + OPTION(USE_GENERIC_MATH "Use generic (std-c) math libraries" ON) + IF(USE_GENERIC_MATH) + # executables will be linked against -lm + SET(EXTRA_LINK_LIBS -lm) + # prepare substitution variable for sundials_config.h + SET(SUNDIALS_USE_GENERIC_MATH TRUE) + ENDIF(USE_GENERIC_MATH) +ENDIF(UNIX) + +# --------------------------------------------------------------- +# Check for POSIX timers +# --------------------------------------------------------------- +INCLUDE(SundialsPOSIXTimers) + +# =============================================================== +# Options for Parallelism +# =============================================================== + +# --------------------------------------------------------------- +# Enable MPI support? +# --------------------------------------------------------------- +OPTION(MPI_ENABLE "Enable MPI support" OFF) + +# --------------------------------------------------------------- +# Enable OpenMP support? +# --------------------------------------------------------------- +OPTION(OPENMP_ENABLE "Enable OpenMP support" OFF) + +# provide OPENMP_DEVICE_ENABLE option +OPTION(OPENMP_DEVICE_ENABLE "Enable OpenMP device offloading support" OFF) + +# Advanced option to skip OpenMP device offloading support check. +# This is needed for a specific compiler that doesn't correctly +# report its OpenMP spec date (with CMake >= 3.9). +OPTION(SKIP_OPENMP_DEVICE_CHECK "Skip the OpenMP device offloading support check" OFF) +MARK_AS_ADVANCED(FORCE SKIP_OPENMP_DEVICE_CHECK) + +# --------------------------------------------------------------- +# Enable Pthread support? +# --------------------------------------------------------------- +OPTION(PTHREAD_ENABLE "Enable Pthreads support" OFF) + +# ------------------------------------------------------------- +# Enable CUDA support? +# ------------------------------------------------------------- +OPTION(CUDA_ENABLE "Enable CUDA support" OFF) + +# ------------------------------------------------------------- +# Enable RAJA support? +# ------------------------------------------------------------- +OPTION(RAJA_ENABLE "Enable RAJA support" OFF) + + +# =============================================================== +# Options for external packages +# =============================================================== + +# --------------------------------------------------------------- +# Enable BLAS support? +# --------------------------------------------------------------- +OPTION(BLAS_ENABLE "Enable BLAS support" OFF) + +# --------------------------------------------------------------- +# Enable LAPACK/BLAS support? +# --------------------------------------------------------------- +OPTION(LAPACK_ENABLE "Enable Lapack support" OFF) + +# LAPACK does not support extended precision +IF(LAPACK_ENABLE AND SUNDIALS_PRECISION MATCHES "EXTENDED") + PRINT_WARNING("LAPACK is not compatible with ${SUNDIALS_PRECISION} precision" + "Disabling LAPACK") + FORCE_VARIABLE(LAPACK_ENABLE BOOL "LAPACK is disabled" OFF) +ENDIF() + +# LAPACK does not support 64-bit integer index types +IF(LAPACK_ENABLE AND SUNDIALS_INDEX_SIZE MATCHES "64") + PRINT_WARNING("LAPACK is not compatible with ${SUNDIALS_INDEX_SIZE} integers" + "Disabling LAPACK") + SET(LAPACK_ENABLE OFF CACHE BOOL "LAPACK is disabled" FORCE) +ENDIF() + +# --------------------------------------------------------------- +# Enable SuperLU_MT support? +# --------------------------------------------------------------- +OPTION(SUPERLUMT_ENABLE "Enable SUPERLUMT support" OFF) + +# SuperLU_MT does not support extended precision +IF(SUPERLUMT_ENABLE AND SUNDIALS_PRECISION MATCHES "EXTENDED") + PRINT_WARNING("SuperLU_MT is not compatible with ${SUNDIALS_PRECISION} precision" + "Disabling SuperLU_MT") + FORCE_VARIABLE(SUPERLUMT_ENABLE BOOL "SuperLU_MT is disabled" OFF) +ENDIF() + +# --------------------------------------------------------------- +# Enable KLU support? +# --------------------------------------------------------------- +OPTION(KLU_ENABLE "Enable KLU support" OFF) + +# KLU does not support single or extended precision +IF(KLU_ENABLE AND + (SUNDIALS_PRECISION MATCHES "SINGLE" OR SUNDIALS_PRECISION MATCHES "EXTENDED")) + PRINT_WARNING("KLU is not compatible with ${SUNDIALS_PRECISION} precision" + "Disabling KLU") + FORCE_VARIABLE(KLU_ENABLE BOOL "KLU is disabled" OFF) +ENDIF() + +# --------------------------------------------------------------- +# Enable hypre Vector support? +# --------------------------------------------------------------- +OPTION(HYPRE_ENABLE "Enable hypre support" OFF) + +# Using hypre requres building with MPI enabled +IF(HYPRE_ENABLE AND NOT MPI_ENABLE) + PRINT_WARNING("MPI not enabled - Disabling hypre" + "Set MPI_ENABLE to ON to use parhyp") + FORCE_VARIABLE(HYPRE_ENABLE BOOL "Enable hypre support" OFF) +ENDIF() + +# --------------------------------------------------------------- +# Enable PETSc support? +# --------------------------------------------------------------- +OPTION(PETSC_ENABLE "Enable PETSc support" OFF) + +# Using PETSc requires building with MPI enabled +IF(PETSC_ENABLE AND NOT MPI_ENABLE) + PRINT_WARNING("MPI not enabled - Disabling PETSc" + "Set MPI_ENABLE to ON to use PETSc") + FORCE_VARIABLE(PETSC_ENABLE BOOL "Enable PETSc support" OFF) +ENDIF() + +# --------------------------------------------------------------- +# Enable Trilinos support? +# --------------------------------------------------------------- +OPTION(Trilinos_ENABLE "Enable Trilinos support" OFF) + + +# =============================================================== +# Options for examples +# =============================================================== + +# --------------------------------------------------------------- +# Enable examples? +# --------------------------------------------------------------- + +# Enable C examples (on by default) +OPTION(EXAMPLES_ENABLE_C "Build SUNDIALS C examples" ON) + +# C++ examples (off by default, unless Trilinos is enabled) +SET(DOCSTR "Build C++ examples") +OPTION(EXAMPLES_ENABLE_CXX "${DOCSTR}" ${Trilinos_ENABLE}) + +# F77 examples (on by default) are an option only if the Fortran +# interface is enabled +SET(DOCSTR "Build SUNDIALS Fortran examples") +IF(F77_INTERFACE_ENABLE) + SHOW_VARIABLE(EXAMPLES_ENABLE_F77 BOOL "${DOCSTR}" ON) + # Fortran 77 examples do not support single or extended precision + IF(EXAMPLES_ENABLE_F77 AND (SUNDIALS_PRECISION MATCHES "EXTENDED" OR SUNDIALS_PRECISION MATCHES "SINGLE")) + PRINT_WARNING("F77 examples are not compatible with ${SUNDIALS_PRECISION} precision" + "EXAMPLES_ENABLE_F77") + FORCE_VARIABLE(EXAMPLES_ENABLE_F77 BOOL "${DOCSTR}" OFF) + ENDIF() +ELSE() + # set back to OFF (in case was ON) + IF(EXAMPLES_ENABLE_F77) + PRINT_WARNING("EXAMPLES_ENABLE_F77 is ON but F77_INTERFACE_ENABLE is OFF" + "Disabling EXAMPLES_ENABLE_F77") + FORCE_VARIABLE(EXAMPLES_ENABLE_F77 BOOL "${DOCSTR}" OFF) + ENDIF() + HIDE_VARIABLE(EXAMPLES_ENABLE_F77) +ENDIF() + +# F90 examples (on by default) are an option only if a Fortran interface is enabled. +SET(DOCSTR "Build SUNDIALS F90 examples") +IF(F77_INTERFACE_ENABLE OR F2003_INTERFACE_ENABLE) + SHOW_VARIABLE(EXAMPLES_ENABLE_F90 BOOL "${DOCSTR}" ON) + # Fortran 90 examples do not support extended precision + IF(EXAMPLES_ENABLE_F90 AND (SUNDIALS_PRECISION MATCHES "EXTENDED")) + PRINT_WARNING("F90 examples are not compatible with ${SUNDIALS_PRECISION} precision" + "Disabling EXAMPLES_ENABLE_F90") + FORCE_VARIABLE(EXAMPLES_ENABLE_F90 BOOL "${DOCSTR}" OFF) + ENDIF() +ELSE() + # set back to OFF (in case was ON) + IF(EXAMPLES_ENABLE_F90) + PRINT_WARNING("EXAMPLES_ENABLE_F90 is ON but both F77 and F2003 interfaces are OFF" + "Disabling EXAMPLES_ENABLE_F90") + FORCE_VARIABLE(EXAMPLES_ENABLE_F90 BOOL "${DOCSTR}" OFF) + ENDIF() + HIDE_VARIABLE(EXAMPLES_ENABLE_F90) +ENDIF() + +# CUDA examples (off by default) +SET(DOCSTR "Build SUNDIALS CUDA examples") +IF(CUDA_ENABLE) + OPTION(EXAMPLES_ENABLE_CUDA "${DOCSTR}" OFF) +ELSE() + IF(EXAMPLES_ENABLE_CUDA) + PRINT_WARNING("EXAMPLES_ENABLE_CUDA is ON but CUDA_ENABLE is OFF" + "Disabling EXAMPLES_ENABLE_CUDA") + FORCE_VARIABLE(EXAMPLES_ENABLE_CUDA BOOL "${DOCSTR}" OFF) + ENDIF() +ENDIF() + +# If any of the above examples are enabled set EXAMPLES_ENABLED to TRUE +IF(EXAMPLES_ENABLE_C OR + EXAMPLES_ENABLE_F77 OR + EXAMPLES_ENABLE_CXX OR + EXAMPLES_ENABLE_F90 OR + EXAMPLES_ENABLE_CUDA) + SET(EXAMPLES_ENABLED TRUE) +ELSE() + SET(EXAMPLES_ENABLED FALSE) +ENDIF() + +# --------------------------------------------------------------- +# Install examples? +# --------------------------------------------------------------- + +# Enable installing examples by default +SET(DOCSTR "Install SUNDIALS examples") +IF(EXAMPLES_ENABLED) + OPTION(EXAMPLES_INSTALL "${DOCSTR}" ON) +ELSE() + FORCE_VARIABLE(EXAMPLES_INSTALL BOOL "${DOCSTR}" OFF) + HIDE_VARIABLE(EXAMPLES_INSTALL) +ENDIF() + +# If examples are to be exported, check where we should install them. +IF(EXAMPLES_INSTALL) + + SHOW_VARIABLE(EXAMPLES_INSTALL_PATH PATH + "Output directory for installing example files" + "${CMAKE_INSTALL_PREFIX}/examples") + + IF(NOT EXAMPLES_INSTALL_PATH) + PRINT_WARNING("The example installation path is empty" + "Example installation path was reset to its default value") + SET(EXAMPLES_INSTALL_PATH "${CMAKE_INSTALL_PREFIX}/examples" CACHE STRING + "Output directory for installing example files" FORCE) + ENDIF() + +ELSE() + + HIDE_VARIABLE(EXAMPLES_INSTALL_PATH) + +ENDIF() + + +# ============================================================================== +# Advanced (hidden) options +# ============================================================================== + +# ------------------------------------------------------------------------------ +# Manually specify the Fortran name-mangling scheme +# +# The build system tries to infer the Fortran name-mangling scheme using a +# Fortran compiler and defaults to using lower case and one underscore if the +# scheme can not be determined. If a working Fortran compiler is not available +# or the user needs to override the inferred or default scheme, the following +# options specify the case and number of appended underscores corresponding to +# the Fortran name-mangling scheme of symbol names that do not themselves +# contain underscores. This is all we really need for the FCMIX and LAPACK +# interfaces. A working Fortran compiler is only necessary for building Fortran +# example programs. +# ------------------------------------------------------------------------------ + +# The case to use in the name-mangling scheme +show_variable(SUNDIALS_F77_FUNC_CASE STRING + "case of Fortran function names (lower/upper)" + "") + +# The number of underscores of appended in the name-mangling scheme +show_variable(SUNDIALS_F77_FUNC_UNDERSCORES STRING + "number of underscores appended to Fortran function names (none/one/two)" + "") + +# Hide the name-mangling varibales as advanced options +mark_as_advanced(FORCE SUNDIALS_F77_FUNC_CASE) +mark_as_advanced(FORCE SUNDIALS_F77_FUNC_UNDERSCORES) + +# If used, both case and underscores must be set +if((NOT SUNDIALS_F77_FUNC_CASE) AND SUNDIALS_F77_FUNC_UNDERSCORES) + message(FATAL_ERROR + "If SUNDIALS_F77_FUNC_UNDERSCORES is set, SUNDIALS_F77_FUNC_CASE must also be set.") +endif() + +if(SUNDIALS_F77_FUNC_CASE AND (NOT SUNDIALS_F77_FUNC_UNDERSCORES)) + message(FATAL_ERROR + "If SUNDIALS_F77_FUNC_CASE is set, SUNDIALS_F77_FUNC_UNDERSCORES must also be set.") +endif() + +# ------------------------------------------------------------------------------ +# Include development examples in regression tests? +# +# NOTE: Development examples are currently used for internal testing and may +# produce erroneous failures when run on different systems as the pass/fail +# status is determined by comparing the output against a saved output file. +# ------------------------------------------------------------------------------ +OPTION(SUNDIALS_DEVTESTS "Include development tests in make test" OFF) +MARK_AS_ADVANCED(FORCE SUNDIALS_DEVTESTS) + +# =============================================================== +# Add any platform specifc settings +# =============================================================== + +# Under Windows, add compiler directive to inhibit warnings +# about use of unsecure functions + +IF(WIN32) + ADD_DEFINITIONS(-D_CRT_SECURE_NO_WARNINGS) +ENDIF(WIN32) + +IF(APPLE) + SET(CMAKE_SHARED_LIBRARY_CREATE_C_FLAGS "${CMAKE_SHARED_LIBRARY_CREATE_C_FLAGS} -undefined dynamic_lookup") +ENDIF(APPLE) + +# =============================================================== +# Fortran and C++ settings +# =============================================================== + +# --------------------------------------------------------------- +# A Fortran compiler is needed to: +# (a) Determine the name-mangling scheme if FCMIX, BLAS, or +# LAPACK are enabled +# (b) Compile example programs if F77 or F90 examples are enabled +# --------------------------------------------------------------- + +# Do we need a Fortran name-mangling scheme? +if(F77_INTERFACE_ENABLE OR BLAS_ENABLE OR LAPACK_ENABLE) + set(NEED_FORTRAN_NAME_MANGLING TRUE) +endif() + +# Did the user provide a name-mangling scheme? +if(SUNDIALS_F77_FUNC_CASE AND SUNDIALS_F77_FUNC_UNDERSCORES) + + STRING(TOUPPER ${SUNDIALS_F77_FUNC_CASE} SUNDIALS_F77_FUNC_CASE) + STRING(TOUPPER ${SUNDIALS_F77_FUNC_UNDERSCORES} SUNDIALS_F77_FUNC_UNDERSCORES) + + # Based on the given case and number of underscores, set the C preprocessor + # macro definitions. Since SUNDIALS never uses symbols names containing + # underscores we set the name-mangling schemes to be the same. In general, + # names of symbols with and without underscore may be mangled differently + # (e.g. g77 mangles mysub to mysub_ and my_sub to my_sub__) + if(SUNDIALS_F77_FUNC_CASE MATCHES "LOWER") + if(SUNDIALS_F77_FUNC_UNDERSCORES MATCHES "NONE") + set(F77_MANGLE_MACRO1 "#define SUNDIALS_F77_FUNC(name,NAME) name") + set(F77_MANGLE_MACRO2 "#define SUNDIALS_F77_FUNC_(name,NAME) name") + elseif(SUNDIALS_F77_FUNC_UNDERSCORES MATCHES "ONE") + set(F77_MANGLE_MACRO1 "#define SUNDIALS_F77_FUNC(name,NAME) name ## _") + SET(F77_MANGLE_MACRO2 "#define SUNDIALS_F77_FUNC_(name,NAME) name ## _") + elseif(SUNDIALS_F77_FUNC_UNDERSCORES MATCHES "TWO") + set(F77_MANGLE_MACRO1 "#define SUNDIALS_F77_FUNC(name,NAME) name ## __") + set(F77_MANGLE_MACRO2 "#define SUNDIALS_F77_FUNC_(name,NAME) name ## __") + else() + message(FATAL_ERROR "Invalid SUNDIALS_F77_FUNC_UNDERSCORES option.") + endif() + elseif(SUNDIALS_F77_FUNC_CASE MATCHES "UPPER") + if(SUNDIALS_F77_FUNC_UNDERSCORES MATCHES "NONE") + set(F77_MANGLE_MACRO1 "#define SUNDIALS_F77_FUNC(name,NAME) NAME") + set(F77_MANGLE_MACRO2 "#define SUNDIALS_F77_FUNC_(name,NAME) NAME") + elseif(SUNDIALS_F77_FUNC_UNDERSCORES MATCHES "ONE") + set(F77_MANGLE_MACRO1 "#define SUNDIALS_F77_FUNC(name,NAME) NAME ## _") + set(F77_MANGLE_MACRO2 "#define SUNDIALS_F77_FUNC_(name,NAME) NAME ## _") + elseif(SUNDIALS_F77_FUNC_UNDERSCORES MATCHES "TWO") + set(F77_MANGLE_MACRO1 "#define SUNDIALS_F77_FUNC(name,NAME) NAME ## __") + set(F77_MANGLE_MACRO2 "#define SUNDIALS_F77_FUNC_(name,NAME) NAME ## __") + else() + message(FATAL_ERROR "Invalid SUNDIALS_F77_FUNC_UNDERSCORES option.") + endif() + else() + message(FATAL_ERROR "Invalid SUNDIALS_F77_FUNC_CASE option.") + endif() + + # name-mangling scheme has been manually set + set(NEED_FORTRAN_NAME_MANGLING FALSE) + +endif() + +# Do we need a Fortran compiler? +if(F2003_INTERFACE_ENABLE OR EXAMPLES_ENABLE_F77 OR EXAMPLES_ENABLE_F90 OR NEED_FORTRAN_NAME_MANGLING) + include(SundialsFortran) +endif() + +# Ensure that F90 compiler is found if F90 examples are enabled +if (EXAMPLES_ENABLE_F90 AND (NOT F90_FOUND)) + PRINT_WARNING("Compiler with F90 support not found" "Disabling F90 Examples") + SET(DOCSTR "Build F90 examples") + FORCE_VARIABLE(EXAMPLES_ENABLE_F90 "${DOCSTR}" OFF) +endif() + +# Ensure that F90 compiler found if F2003 interface is enabled +if (F2003_INTERFACE_ENABLE AND (NOT F90_FOUND)) + PRINT_WARNING("Compiler with F90 support not found" "Disabling F2003 Interface") + SET(DOCSTR "Enable Fortran 2003 interfaces") + FORCE_VARIABLE(F2003_INTERFACE_ENABLE BOOL "${DOCSTR}" OFF) +endif() + +# F2003 interface requires ISO_C_BINDING +IF(F2003_INTERFACE_ENABLE AND (NOT Fortran_COMPILER_SUPPORTS_ISOCBINDING)) + PRINT_WARNING("Fortran compiler does not provide ISO_C_BINDING support" + "Disabling F2003 interface") + SET(DOCSTR "Enable Fortran 2003 interfaces") + FORCE_VARIABLE(F2003_INTERFACE_ENABLE BOOL "${DOCSTR}" OFF) +ENDIF() + + +# --------------------------------------------------------------- +# A C++ compiler is needed if: +# (a) C++ examples are enabled +# (b) CUDA is enabled +# (c) RAJA is enabled +# (d) Trilinos is enabled +# --------------------------------------------------------------- + +if(EXAMPLES_ENABLE_CXX OR CUDA_ENABLE OR RAJA_ENABLE OR Trilinos_ENABLE) + include(SundialsCXX) +endif() + +# --------------------------------------------------------------- +# Setup CUDA. Since CUDA is its own language we do this +# separate from the TPLs. +# --------------------------------------------------------------- + +if(CUDA_ENABLE) + find_package(CUDA) + if (CUDA_FOUND) + set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -lineinfo") + else() + message(STATUS "Disabling CUDA support, could not find CUDA.") + set(CUDA_ENABLE OFF) + endif() +endif(CUDA_ENABLE) + +# --------------------------------------------------------------- +# Now that all languages are setup, we can configure them more. +# --------------------------------------------------------------- + +# C++11 is needed if: +# (a) CUDA is enabled +# C++11 should not be enabled if +# (a) RAJA is enabled (they provide a std flag) +if (CXX_FOUND AND CUDA_ENABLE AND CUDA_FOUND AND (NOT RAJA_ENABLE)) + USE_CXX_STD(11) +endif() + +# --------------------------------------------------------------- +# Decide how to compile MPI codes. We must check for MPI if +# MPI is enabled or if Trilinos is enabled because the Trilinos +# examples may need MPI without us turning on the MPI SUNDIALS +# components. +# --------------------------------------------------------------- + +if(MPI_ENABLE OR Trilinos_ENABLE) + include(SundialsMPI) +endif() + +if(MPI_ENABLE) + if(NOT MPI_C_FOUND) + print_warning("MPI not functional" "Parallel support will not be provided") + else() + set(IS_MPI_ENABLED "#ifndef SUNDIALS_MPI_ENABLED\n#define SUNDIALS_MPI_ENABLED 1\n#endif") + endif() +endif() + +# always define FMPI_COMM_F2C in sundials_fconfig.h file +if(MPIC_MPI2) + set(F77_MPI_COMM_F2C "#define SUNDIALS_MPI_COMM_F2C 1") + set(FMPI_COMM_F2C ".true.") +else() + set(F77_MPI_COMM_F2C "#define SUNDIALS_MPI_COMM_F2C 0") + set(FMPI_COMM_F2C ".false.") +endif() + +# ------------------------------------------------------------- +# Find OpenMP +# ------------------------------------------------------------- + +if(OPENMP_ENABLE OR OPENMP_DEVICE_ENABLE) + + include(SundialsOpenMP) + + # turn off OPENMP_ENABLE and OPENMP_DEVICE_ENABLE if OpenMP is not found + if(NOT OPENMP_FOUND) + print_warning("Could not determine OpenMP compiler flags" "Disabling OpenMP support") + force_variable(OPENMP_ENABLE BOOL "Enable OpenMP support" OFF) + force_variable(OPENMP_DEVICE_ENABLE BOOL "Enable OpenMP device offloading support" OFF) + endif() + + # turn off OPENMP_DEVICE_ENABLE if offloading is not supported + if(OPENMP_DEVICE_ENABLE AND (NOT OPENMP_SUPPORTS_DEVICE_OFFLOADING)) + print_warning("OpenMP found does not support device offloading" + "Disabling OpenMP device offloading support") + force_variable(OPENMP_DEVICE_ENABLE BOOL "Enable OpenMP device offloading support" OFF) + endif() + +endif() + +# ------------------------------------------------------------- +# Find PThreads +# ------------------------------------------------------------- + +IF(PTHREAD_ENABLE) + FIND_PACKAGE(Threads) + IF(CMAKE_USE_PTHREADS_INIT) + message(STATUS "Using Pthreads") + SET(PTHREADS_FOUND TRUE) + # SGS + ELSE() + message(STATUS "Disabling Pthreads support, could not determine compiler flags") + endif() +ENDIF(PTHREAD_ENABLE) + +# ------------------------------------------------------------- +# Find RAJA +# ------------------------------------------------------------- + +# disable RAJA if CUDA is not enabled/working +if(RAJA_ENABLE AND (NOT CUDA_FOUND)) + PRINT_WARNING("CUDA is required for RAJA support" "Please enable CUDA and RAJA") + FORCE_VARIABLE(RAJA_ENABLE BOOL "RAJA disabled" OFF) +endif() + +if(RAJA_ENABLE) + # Look for CMake configuration file in RAJA installation + find_package(RAJA) + if (RAJA_FOUND) + include_directories(${RAJA_INCLUDE_DIR}) + set(CUDA_NVCC_FLAGS ${CUDA_NVCC_FLAGS} ${RAJA_NVCC_FLAGS}) + else() + PRINT_WARNING("RAJA configuration not found" + "Please set RAJA_DIR to provide path to RAJA CMake configuration file.") + endif() +endif(RAJA_ENABLE) + +# =============================================================== +# Find (and test) external packages +# =============================================================== + +# --------------------------------------------------------------- +# Find (and test) the BLAS libraries +# --------------------------------------------------------------- + +# If BLAS is needed, first try to find the appropriate +# libraries and linker flags needed to link against them. + +IF(BLAS_ENABLE) + + # find BLAS + INCLUDE(SundialsBlas) + + # show after include so FindBlas can locate BLAS_LIBRARIES if necessary + SHOW_VARIABLE(BLAS_LIBRARIES STRING "Blas libraries" "${BLAS_LIBRARIES}") + + IF(BLAS_LIBRARIES AND NOT BLAS_FOUND) + PRINT_WARNING("BLAS not functional" + "BLAS support will not be provided") + ELSE() + #set sundials_config.h symbol via sundials_config.in + SET(SUNDIALS_BLAS TRUE) + ENDIF() + +ELSE() + + HIDE_VARIABLE(BLAS_LIBRARIES) + +ENDIF() + +# --------------------------------------------------------------- +# Find (and test) the Lapack libraries +# --------------------------------------------------------------- + +# If LAPACK is needed, first try to find the appropriate +# libraries and linker flags needed to link against them. + +IF(LAPACK_ENABLE) + + # find LAPACK and BLAS Libraries + INCLUDE(SundialsLapack) + + # show after include so FindLapack can locate LAPCK_LIBRARIES if necessary + SHOW_VARIABLE(LAPACK_LIBRARIES STRING "Lapack and Blas libraries" "${LAPACK_LIBRARIES}") + + IF(LAPACK_LIBRARIES AND NOT LAPACK_FOUND) + PRINT_WARNING("LAPACK not functional" + "Blas/Lapack support will not be provided") + ELSE() + #set sundials_config.h symbol via sundials_config.in + SET(SUNDIALS_BLAS_LAPACK TRUE) + ENDIF() + +ELSE() + + HIDE_VARIABLE(LAPACK_LIBRARIES) + +ENDIF() + +# --------------------------------------------------------------- +# Find (and test) the SUPERLUMT libraries +# --------------------------------------------------------------- + +# >>>>>>> NOTE: Need to add check for SuperLU_MT integer type + +# If SUPERLUMT is needed, first try to find the appropriate +# libraries to link against them. + +IF(SUPERLUMT_ENABLE) + + # Show SuperLU_MT options and set default thread type (Pthreads) + SHOW_VARIABLE(SUPERLUMT_THREAD_TYPE STRING "SUPERLUMT threading type: OpenMP or Pthread" "Pthread") + SHOW_VARIABLE(SUPERLUMT_INCLUDE_DIR PATH "SUPERLUMT include directory" "${SUPERLUMT_INCLUDE_DIR}") + SHOW_VARIABLE(SUPERLUMT_LIBRARY_DIR PATH "SUPERLUMT library directory" "${SUPERLUMT_LIBRARY_DIR}") + + INCLUDE(SundialsSuperLUMT) + + IF(SUPERLUMT_FOUND) + # sundials_config.h symbols + SET(SUNDIALS_SUPERLUMT TRUE) + SET(SUNDIALS_SUPERLUMT_THREAD_TYPE ${SUPERLUMT_THREAD_TYPE}) + INCLUDE_DIRECTORIES(${SUPERLUMT_INCLUDE_DIR}) + ENDIF() + + IF(SUPERLUMT_LIBRARIES AND NOT SUPERLUMT_FOUND) + PRINT_WARNING("SUPERLUMT not functional - support will not be provided" + "Double check spelling specified libraries (search is case sensitive)") + ENDIF(SUPERLUMT_LIBRARIES AND NOT SUPERLUMT_FOUND) + +ELSE() + + HIDE_VARIABLE(SUPERLUMT_THREAD_TYPE) + HIDE_VARIABLE(SUPERLUMT_LIBRARY_DIR) + HIDE_VARIABLE(SUPERLUMT_INCLUDE_DIR) + SET (SUPERLUMT_DISABLED TRUE CACHE INTERNAL "GUI - return when first set") + +ENDIF() + +# --------------------------------------------------------------- +# Find (and test) the KLU libraries +# --------------------------------------------------------------- + +# If KLU is requested, first try to find the appropriate libraries to +# link against them. + +IF(KLU_ENABLE) + + SHOW_VARIABLE(KLU_INCLUDE_DIR PATH "KLU include directory" + "${KLU_INCLUDE_DIR}") + SHOW_VARIABLE(KLU_LIBRARY_DIR PATH + "Klu library directory" "${KLU_LIBRARY_DIR}") + + set(KLU_FOUND TRUE) + get_filename_component(PYBAMM_DIR ${PROJECT_SOURCE_DIR} DIRECTORY) + set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PYBAMM_DIR}) # use FindSuiteSparse.cmake that is in PyBaMM root + set(SuiteSparse_ROOT ${PYBAMM_DIR}/SuiteSparse-5.6.0) + find_package(SuiteSparse OPTIONAL_COMPONENTS KLU AMD COLAMD BTF) + include_directories(${SuiteSparse_INCLUDE_DIRS}) + set(KLU_LIBRARIES ${SuiteSparse_LIBRARIES}) + + + IF(KLU_LIBRARIES AND NOT KLU_FOUND) + PRINT_WARNING("KLU not functional - support will not be provided" + "Double check spelling of include path and specified libraries (search is case sensitive)") + ENDIF(KLU_LIBRARIES AND NOT KLU_FOUND) + +ELSE() + + HIDE_VARIABLE(KLU_LIBRARY_DIR) + HIDE_VARIABLE(KLU_INCLUDE_DIR) + SET (KLU_DISABLED TRUE CACHE INTERNAL "GUI - return when first set") + +ENDIF(KLU_ENABLE) + +# --------------------------------------------------------------- +# Find (and test) the hypre libraries +# --------------------------------------------------------------- + +# >>>>>>> NOTE: Need to add check for hypre precision and integer type + +IF(HYPRE_ENABLE) + SHOW_VARIABLE(HYPRE_INCLUDE_DIR PATH "HYPRE include directory" + "${HYPRE_INCLUDE_DIR}") + SHOW_VARIABLE(HYPRE_LIBRARY_DIR PATH + "HYPRE library directory" "${HYPRE_LIBRARY_DIR}") + + INCLUDE(SundialsHypre) + + IF(HYPRE_FOUND) + # sundials_config.h symbol + SET(SUNDIALS_HYPRE TRUE) + INCLUDE_DIRECTORIES(${HYPRE_INCLUDE_DIR}) + ENDIF(HYPRE_FOUND) + + IF(HYPRE_LIBRARIES AND NOT HYPRE_FOUND) + PRINT_WARNING("HYPRE not functional - support will not be provided" + "Found hypre library, test code does not work") + ENDIF(HYPRE_LIBRARIES AND NOT HYPRE_FOUND) + +ELSE() + + HIDE_VARIABLE(HYPRE_INCLUDE_DIR) + HIDE_VARIABLE(HYPRE_LIBRARY_DIR) + SET (HYPRE_DISABLED TRUE CACHE INTERNAL "GUI - return when first set") + +ENDIF() + +# --------------------------------------------------------------- +# Find (and test) the PETSc libraries +# --------------------------------------------------------------- + +# >>>>>>> NOTE: Need to add check for PETSc precision and integer type + +IF(PETSC_ENABLE) + SHOW_VARIABLE(PETSC_INCLUDE_DIR PATH "PETSc include directory" + "${PETSC_INCLUDE_DIR}") + SHOW_VARIABLE(PETSC_LIBRARY_DIR PATH + "PETSc library directory" "${PETSC_LIBRARY_DIR}") + + INCLUDE(SundialsPETSc) + + IF(PETSC_FOUND) + # sundials_config.h symbol + SET(SUNDIALS_PETSC TRUE) + INCLUDE_DIRECTORIES(${PETSC_INCLUDE_DIR}) + ENDIF(PETSC_FOUND) + + IF(PETSC_LIBRARIES AND NOT PETSC_FOUND) + PRINT_WARNING("PETSC not functional - support will not be provided" + "Double check spelling specified libraries (search is case sensitive)") + ENDIF(PETSC_LIBRARIES AND NOT PETSC_FOUND) + +ELSE() + + HIDE_VARIABLE(PETSC_LIBRARY_DIR) + HIDE_VARIABLE(PETSC_INCLUDE_DIR) + SET (PETSC_DISABLED TRUE CACHE INTERNAL "GUI - return when first set") + +ENDIF() + +# ------------------------------------------------------------- +# Find Trilinos +# ------------------------------------------------------------- + +if(Trilinos_ENABLE) + include(SundialsTrilinos) + if(NOT Trilinos_FUNCTIONAL) + PRINT_WARNING("Trilinos not functional" "Verify the path to Trilinos and check the Trilinos installation") + endif() +endif(Trilinos_ENABLE) + + +# =============================================================== +# At this point all the configuration options are set. +# =============================================================== + +# --------------------------------------------------------------- +# Configure the header file sundials_config.h +# --------------------------------------------------------------- + +# All required substitution variables should be available at this point. +# Generate the header file and place it in the binary dir. +CONFIGURE_FILE( + ${PROJECT_SOURCE_DIR}/include/sundials/sundials_config.in + ${PROJECT_BINARY_DIR}/include/sundials/sundials_config.h + ) +CONFIGURE_FILE( + ${PROJECT_SOURCE_DIR}/include/sundials/sundials_fconfig.in + ${PROJECT_BINARY_DIR}/include/sundials/sundials_fconfig.h + ) + +# Add the include directory in the source tree and the one in +# the binary tree (for the header file sundials_config.h) +INCLUDE_DIRECTORIES(${PROJECT_SOURCE_DIR}/include ${PROJECT_BINARY_DIR}/include) + +# --------------------------------------------------------------- +# Enable testing and add source and example files to the build. +# --------------------------------------------------------------- + +# Enable testing +IF(EXAMPLES_ENABLED) + INCLUDE(SundialsTesting) +ENDIF() + +# Add selected packages and modules to the build +ADD_SUBDIRECTORY(src) + +# Add selected examples to the build +IF(EXAMPLES_ENABLED) + ADD_SUBDIRECTORY(examples) +ENDIF() + +# --------------------------------------------------------------- +# Install configuration header files and license file +# --------------------------------------------------------------- + +# install configured header file +INSTALL( + FILES ${PROJECT_BINARY_DIR}/include/sundials/sundials_config.h + DESTINATION include/sundials + ) + +# install configured header file for Fortran 90 +INSTALL( + FILES ${PROJECT_BINARY_DIR}/include/sundials/sundials_fconfig.h + DESTINATION include/sundials + ) + +# install shared Fortran 2003 modules +IF(F2003_INTERFACE_ENABLE) + # While the .mod files get generated for static and shared + # libraries, they are identical. So only install one set + # of the .mod files. + IF(BUILD_STATIC_LIBS) + INSTALL( + DIRECTORY ${CMAKE_Fortran_MODULE_DIRECTORY}_STATIC/ + DESTINATION ${Fortran_INSTALL_MODDIR} + ) + ELSE() + INSTALL( + DIRECTORY ${CMAKE_Fortran_MODULE_DIRECTORY}_SHARED/ + DESTINATION ${Fortran_INSTALL_MODDIR} + ) + ENDIF() +ENDIF() + +# install license and notice files +INSTALL( + FILES ${PROJECT_SOURCE_DIR}/LICENSE + DESTINATION include/sundials + ) +INSTALL( + FILES ${PROJECT_SOURCE_DIR}/NOTICE + DESTINATION include/sundials + ) + diff --git a/setup.py b/setup.py index b7672fcacc..db141d4f4d 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,460 @@ +import os +import sys +import subprocess +import tarfile +from shutil import copy +from platform import python_version + +try: + # wget module is required to download SUNDIALS or SuiteSparse and + # is not a core requirement. + import wget + + NO_WGET = False +except (ImportError, ModuleNotFoundError): + NO_WGET = True try: from setuptools import setup, find_packages except ImportError: from distutils.core import setup, find_packages +from distutils.cmd import Command + + +def download_extract_library(url): + # Download and extract archive at url + if NO_WGET: + # The NO_WGET is set to true if the wget could not be + # imported. + error_msg = ( + "Could not find wget module. Please install wget module (pip install wget)." + ) + raise ModuleNotFoundError(error_msg) + archive = wget.download(url) + tar = tarfile.open(archive) + tar.extractall() + + +def yes_or_no(question): + # Prompt the user with a yes or no question. + # Only accept 'y' or 'n' characters as a valid answer. + while "the answer is invalid": + reply = str(input(question + " (y/n): ")).lower().strip() + if len(reply) >= 1: + if reply[0] == "y": + return True + if reply[0] == "n": + return False + print("\n") + + +def update_LD_LIBRARY_PATH(install_dir): + # Look for current python virtual env and add export statement + # for LD_LIBRARY_PATH in activate script. If no virtual env found, + # then the current user's .bashrc file is modified instead. + + export_statement = "export LD_LIBRARY_PATH={}/lib:$LD_LIBRARY_PATH".format( + install_dir + ) + + venv_path = os.environ.get("VIRTUAL_ENV") + if venv_path: + script_path = os.path.join(venv_path, "bin/activate") + else: + script_path = os.path.join(os.environ.get("HOME"), ".bashrc") + + if os.getenv("LD_LIBRARY_PATH") and "{}/lib".format(install_dir) in os.getenv( + "LD_LIBRARY_PATH" + ): + print("{}/lib was found in LD_LIBRARY_PATH.".format(install_dir)) + print("--> Not updating venv activate or .bashrc scripts") + else: + with open(script_path, "a+") as fh: + # Just check that export statement is not already there. + if export_statement not in fh.read(): + fh.write(export_statement) + print( + "Adding {}/lib to LD_LIBRARY_PATH" + " in {}".format(install_dir, script_path) + ) + + +def install_sundials( + sundials_src, sundials_inst, download, klu=False, force_download=False +): + # Download the SUNDIALS library and compile it. + # Arguments + # ---------- + # sundials_src: str + # Absolute path to SUNDIALS source directory + # sundials_inst: str + # Absolute path to SUNDIALS installation directory + # download: bool + # Whether or not to download the SUNDIALS archive + # klu: bool, optional + # Whether or not to build the SUNDIALS with KLU enabled + + pybamm_dir = os.path.abspath(os.path.dirname(__file__)) + + try: + subprocess.run(["cmake", "--version"]) + except OSError: + raise RuntimeError("CMake must be installed to build the SUNDIALS library.") + + if download: + question = "About to download sundials, proceed?" + url = ( + "https://computing.llnl.gov/" + + "projects/sundials/download/sundials-5.0.0.tar.gz" + ) + if force_download or yes_or_no(question): + print("Downloading SUNDIALS from " + url) + download_extract_library(url) + else: + print("Exiting setup.") + sys.exit() + + fixed_cmakelists = os.path.join( + pybamm_dir, "scripts", "replace-cmake", "sundials-5.0.0", "CMakeLists.txt" + ) + copy(fixed_cmakelists, os.path.join(sundials_src, "CMakeLists.txt")) + + cmake_args = [ + "-DLAPACK_ENABLE=ON", + "-DSUNDIALS_INDEX_SIZE=32", + "-DBUILD_ARKODE:BOOL=OFF", + "-DEXAMPLES_ENABLE:BOOL=OFF", + "-DCMAKE_INSTALL_PREFIX=" + sundials_inst, + ] + + if klu: + cmake_args.append("-DKLU_ENABLE=ON") + + # SUNDIALS are built within directory 'build_sundials' in the PyBaMM root + # directory + build_directory = os.path.abspath(os.path.join(pybamm_dir, "build_sundials")) + if not os.path.exists(build_directory): + print("\n-" * 10, "Creating build dir", "-" * 40) + os.makedirs(build_directory) + + print("-" * 10, "Running CMake prepare", "-" * 40) + subprocess.run(["cmake", sundials_src] + cmake_args, cwd=build_directory) + + print("-" * 10, "Building the sundials", "-" * 40) + make_cmd = ["make", "install"] + subprocess.run(make_cmd, cwd=build_directory) + + +def build_idaklu_solver(pybamm_dir): + # Build the PyBaMM idaklu solver using cmake and pybind11. + # Arguments + # --------- + # pybamm_dir: str + # Absolute path to PyBaMM root directory + # + # The CMakeLists.txt is located in the PyBaMM root directory. + # For the build to be successful, the SUNDIALS must be installed + # with the KLU solver enabled in pybamm_dir/sundials. + + try: + subprocess.run(["cmake", "--version"]) + except OSError: + raise RuntimeError("CMake must be installed to build the KLU python module.") + + try: + assert os.path.isfile("third-party/pybind11/pybind11/tools/pybind11Tools.cmake") + except AssertionError: + print( + "Error: Could not find " + "third-party/pybind11/pybind11/tools/pybind11Tools.cmake" + ) + print("Make sure the pybind11 repository was cloned in ./third-party/") + print("See installation instructions for more information.") + + py_version = python_version() + cmake_args = ["-DPYBIND11_PYTHON_VERSION={}".format(py_version)] + + print("-" * 10, "Running CMake for idaklu solver", "-" * 40) + subprocess.run(["cmake"] + cmake_args, cwd=pybamm_dir) + + print("-" * 10, "Building idaklu module", "-" * 40) + subprocess.run(["cmake", "--build", "."], cwd=pybamm_dir) + + +class InstallKLU(Command): + """ A custom command to download and compile the SuiteSparse KLU library. + """ + + description = "Download/Compile the SuiteSparse KLU module." + user_options = [ + # The format is (long option, short option, description). + ("sundials-src=", None, "Path to sundials source directory"), + ("sundials-inst=", None, "Path to sundials install directory"), + ("suitesparse-src=", None, "Path to suitesparse source directory"), + ( + "force-download", + "f", + "Whether or not to force download of SuiteSparse and Sundials libraries", + ), + ] + # Absolute path to the PyBaMM root directory where setup.py is located. + pybamm_dir = os.path.abspath(os.path.dirname(__file__)) + # Boolean flag indicating whether or not to download/install the SUNDIALS library. + install_sundials = True + + def initialize_options(self): + """Set default values for option(s)""" + # Each user option is listed here with its default value. + self.suitesparse_src = None + self.sundials_src = None + self.sundials_inst = None + self.force_download = None + + def finalize_options(self): + """Post-process options""" + # Any unspecified option is set to the value of the 'install_all' command + # This could be the default value if 'install_klu' is invoked on its own + # or a user-specified value if invoked from 'install_all' with options. + self.set_undefined_options( + "install_all", + ("suitesparse_src", "suitesparse_src"), + ("sundials_src", "sundials_src"), + ("sundials_inst", "sundials_inst"), + ("force_download", "force_download"), + ) + + # If the SUNDIALS is already installed in sundials_inst with the KLU + # solver enabled then do not download/install the SUNDIALS library. + if os.path.isfile( + os.path.join(self.sundials_inst, "lib", "libsundials_sunlinsolklu.so") + ): + print("Found SUNDIALS installation in {}.".format(self.sundials_inst)) + print("Not installing SUNDIALS.") + self.install_sundials = False + + # If the SuiteSparse source dir was provided as a command line option + # then check that it actually contains the Makefile. + # Else, SuiteSparse must be downloaded. + if self.suitesparse_src: + self.suitesparse_src = os.path.abspath(self.suitesparse_src) + klu_makefile = os.path.join(self.suitesparse_src, "KLU", "Makefile") + assert os.path.exists(klu_makefile), "Could not find {}.".format( + klu_makefile + ) + self.download_suitesparse = False + else: + self.download_suitesparse = True + self.suitesparse_src = os.path.join(self.pybamm_dir, "SuiteSparse-5.6.0") + + # If the SUNDIALS source dir was provided as a command line option + # then check that it actually contains the CMakeLists.txt. + # Else, the SUNDIALS must be downloaded. + if self.sundials_src: + self.sundials_src = os.path.abspath(self.sundials_src) + CMakeLists = os.path.join(self.sundials_src, "CMakeLists.txt") + assert os.path.exists(CMakeLists), "Could not find {}.".format(CMakeLists) + self.download_sundials = False + else: + self.download_sundials = True + self.sundials_src = os.path.join(self.pybamm_dir, "sundials-5.0.0") + + def run(self): + """Functionality for the install_klu command. + 1. Download/build SuiteSparse + 2. Download/build SUNDIALS with KLU + 3. Build python KLU module with pybind11 + """ + try: + subprocess.run(["make", "--version"]) + except OSError: + raise RuntimeError( + "Make must be installed to compile the SuiteSparse KLU module." + ) + + if self.download_suitesparse: + question = "About to download SuiteSparse, proceed?" + url = ( + "https://github.com/DrTimothyAldenDavis/" + + "SuiteSparse/archive/v5.6.0.tar.gz" + ) + if self.force_download or yes_or_no(question): + print("Downloading SuiteSparse from " + url) + download_extract_library(url) + else: + print("Exiting setup.") + sys.exit() + + # The SuiteSparse KLU module has 4 dependencies: + # - suitesparseconfig + # - amd + # - COLAMD + # - btf + print("-" * 10, "Building SuiteSparse_config", "-" * 40) + make_cmd = ["make"] + build_dir = os.path.join(self.suitesparse_src, "SuiteSparse_config") + subprocess.run(make_cmd, cwd=build_dir) + + print("-" * 10, "Building SuiteSparse KLU module dependencies", "-" * 40) + make_cmd = ["make", "library"] + for libdir in ["AMD", "COLAMD", "BTF"]: + build_dir = os.path.join(self.suitesparse_src, libdir) + subprocess.run(make_cmd, cwd=build_dir) + + print("-" * 10, "Building SuiteSparse KLU module", "-" * 40) + build_dir = os.path.join(self.suitesparse_src, "KLU") + subprocess.run(make_cmd, cwd=build_dir) + + if self.install_sundials: + install_sundials( + self.sundials_src, + self.sundials_inst, + self.download_sundials, + klu=True, + force_download=self.force_download, + ) + build_idaklu_solver(self.pybamm_dir) + + +class InstallODES(Command): + """ A custom command to install scikits.ode with pip, as well as its main dependency the + SUNDIALS library. + """ + + description = "Installs scikits.odes using pip." + user_options = [ + # The format is (long option, short option, description). + ("sundials-src=", None, "Path to sundials source dir"), + ("sundials-inst=", None, "Path to sundials install directory"), + ( + "force-download", + "f", + "Whether or not to force download of SuiteSparse and Sundials libraries", + ), + ] + # Absolute path to the PyBaMM root directory where setup.py is located. + pybamm_dir = os.path.abspath(os.path.dirname(__file__)) + # Boolean flag indicating whether or not to download/install the SUNDIALS library. + install_sundials = True + + def initialize_options(self): + """Set default values for option(s)""" + # Each user option is listed here with its default value. + self.sundials_src = None + self.sundials_inst = None + self.force_download = None + + def finalize_options(self): + """Post-process options""" + # Any unspecified option is set to the value of the 'install_all' command + # This could be the default value if 'install_odes' is invoked on its own + # or a user-specified value if invoked from 'install_all' with options. + + # If option specified the check dir exists + self.set_undefined_options( + "install_all", + ("sundials_src", "sundials_src"), + ("sundials_inst", "sundials_inst"), + ("force_download", "force_download"), + ) + + # If the installation directory sundials_inst already exists, then it is + # assumed that the SUNDIALS library is already installed in there and + # the library is not installed. + if os.path.exists(self.sundials_inst): + print( + "Found SUNDIALS installation directory {}.".format(self.sundials_inst) + ) + print("Not installing SUNDIALS.") + self.install_sundials = False + + # If the SUNDIALS source dir was provided as a command line option + # then check that it actually contains the CMakeLists.txt. + # Else, the SUNDIALS must be downloaded. + if self.sundials_src: + self.sundials_src = os.path.abspath(self.sundials_src) + CMakeLists = os.path.join(self.sundials_src, "CMakeLists.txt") + assert os.path.exists(CMakeLists), "Could not find {}.".format(CMakeLists) + self.download_sundials = False + else: + self.download_sundials = True + self.sundials_src = os.path.join(self.pybamm_dir, "sundials-5.0.0") + + def run(self): + """Functionality for the install_odes command. + 1. Download/build SUNDIALS + 2. Update virtual env activate script or .bashrc + to add the SUNDIALS to LD_LIBRARY_PATH. + 3. Install scikits.odes using pip + """ + + if self.install_sundials: + # Download/build SUNDIALS + install_sundials( + self.sundials_src, + self.sundials_inst, + self.download_sundials, + force_download=self.force_download, + ) + + update_LD_LIBRARY_PATH(self.sundials_inst) + + # At the time scikits.odes is pip installed, the path to the sundials + # library must be contained in an env variable SUNDIALS_INST + # see https://scikits-odes.readthedocs.io/en/latest/installation.html#id1 + os.environ["SUNDIALS_INST"] = self.sundials_inst + env = os.environ.copy() + subprocess.run(["pip", "install", "scikits.odes"], env=env) + + +class InstallAll(Command): + """ Install both scikits.odes and KLU module. + This command is the combination of the two commands + 'install_odes' (InstallODES) + and + 'install_klu' (InstallKLU) + """ + + user_options = [ + ("sundials-src=", None, "Absolute path to sundials source dir"), + ("sundials-inst=", None, "Absolute path to sundials install directory"), + ("suitesparse-src=", None, "Absolute path to SuiteSparse root directory"), + ( + "force-download", + "f", + "Whether or not to force download of SuiteSparse and Sundials libraries", + ), + ] + + # Absolute path to the PyBaMM root directory where setup.py is located. + pybamm_dir = os.path.abspath(os.path.dirname(__file__)) + + def initialize_options(self): + """Set default values for option(s)""" + # Each user option is listed here with its default value. + self.sundials_src = None + self.sundials_inst = os.path.join(self.pybamm_dir, "sundials") + self.suitesparse_src = None + self.force_download = None + + def finalize_options(self): + """Post-process options""" + if self.sundials_src: + print("Using SUNDIALS source directory {}".format(self.sundials_src)) + if self.suitesparse_src: + print("Using SuiteSparse source directory {}".format(self.sundials_src)) + + def run(self): + """Install scikits.odes and KLU module""" + self.run_command("install_klu") + self.run_command("install_odes") + # Load text for description and license with open("README.md") as f: readme = f.read() -# Read version number from file def load_version(): + # Read version number from file try: import os @@ -22,10 +467,16 @@ def load_version(): setup( + cmdclass={ + "install_odes": InstallODES, + "install_klu": InstallKLU, + "install_all": InstallAll, + }, name="pybamm", - version=load_version(), + version=load_version() + ".post4", description="Python Battery Mathematical Modelling.", long_description=readme, + long_description_content_type="text/markdown", url="https://github.com/pybamm-team/PyBaMM", include_package_data=True, packages=find_packages(include=("pybamm", "pybamm.*")), @@ -42,7 +493,7 @@ def load_version(): install_requires=[ "numpy>=1.16", "scipy>=1.0", - "pandas>=0.23", + "pandas>=0.24", "anytree>=2.4.3", "autograd>=1.2", "scikit-fem>=0.2.0", diff --git a/tests/integration/test_experiments.py b/tests/integration/test_experiments.py new file mode 100644 index 0000000000..8f3a3b6880 --- /dev/null +++ b/tests/integration/test_experiments.py @@ -0,0 +1,60 @@ +# +# Test some experiments +# +import pybamm +import numpy as np +import unittest + + +class TestExperiments(unittest.TestCase): + def test_discharge_rest_charge(self): + experiment = pybamm.Experiment( + [ + "Discharge at C/2 for 1 hour", + "Rest for 1 hour", + "Charge at C/2 for 1 hour", + ], + period="0.25 hours", + ) + model = pybamm.lithium_ion.SPM() + sim = pybamm.Simulation( + model, experiment=experiment, solver=pybamm.CasadiSolver() + ) + sim.solve() + np.testing.assert_array_almost_equal( + sim._solution["Time [h]"].entries, np.linspace(0, 3, 13) + ) + cap = model.default_parameter_values["Cell capacity [A.h]"] + np.testing.assert_array_almost_equal( + sim._solution["Current [A]"].entries, + [cap / 2] * 5 + [0] * 4 + [-cap / 2] * 4, + ) + + def test_gitt(self): + experiment = pybamm.Experiment( + ["Discharge at C/20 for 1 hour", "Rest for 1 hour"] * 10, + period="6 minutes", + ) + model = pybamm.lithium_ion.SPM() + sim = pybamm.Simulation( + model, experiment=experiment, solver=pybamm.CasadiSolver() + ) + sim.solve() + np.testing.assert_array_almost_equal( + sim._solution["Time [h]"].entries, np.arange(0, 20.01, 0.1) + ) + cap = model.default_parameter_values["Cell capacity [A.h]"] + np.testing.assert_array_almost_equal( + sim._solution["Current [A]"].entries, + [cap / 20] * 11 + [0] * 10 + ([cap / 20] * 10 + [0] * 10) * 9, + ) + + +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/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs.py index b37071bf33..79d1aa3bc2 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs.py @@ -14,14 +14,15 @@ def test_compare_outputs_surface_form(self): {"surface form": cap} for cap in [False, "differential", "algebraic"] ] model_combos = [ - # ([pybamm.lithium_ion.SPM(opt) for opt in options]), - ([pybamm.lithium_ion.DFN(opt) for opt in options]) + ([pybamm.lithium_ion.SPM(opt) for opt in options]), + ([pybamm.lithium_ion.SPMe(opt) for opt in options]), + ([pybamm.lithium_ion.DFN(opt) for opt in options]), ] for models in model_combos: # load parameter values (same for all models) param = models[0].default_parameter_values - param.update({"Typical current [A]": 1}) + param.update({"Current function [A]": 1}) for model in models: param.process_model(model) @@ -40,14 +41,14 @@ def test_compare_outputs_surface_form(self): discs[model] = disc # solve model - solutions = {} + solutions = [] t_eval = np.linspace(0, 0.2, 100) for i, model in enumerate(models): - solution = model.default_solver.solve(model, t_eval) - solutions[model] = solution + solution = pybamm.CasadiSolver().solve(model, t_eval) + solutions.append(solution) # compare outputs - comparison = StandardOutputComparison(models, discs, solutions) + comparison = StandardOutputComparison(solutions) comparison.test_all(skip_first_timestep=True) 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 e3f1ad6d8f..0709b96686 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,6 +15,13 @@ def test_basic_processing(self): modeltest = tests.StandardModelTest(model) modeltest.test_all() + def test_basic_processing_python(self): + options = {"thermal": "isothermal"} + model = pybamm.lithium_ion.SPMe(options) + model.convert_to_format = "python" + modeltest = tests.StandardModelTest(model, solver=pybamm.ScipySolver()) + modeltest.test_all() + def test_basic_processing_1plus1D(self): options = {"current collector": "potential pair", "dimensionality": 1} model = pybamm.lithium_ion.SPMe(options) diff --git a/tests/integration/test_models/test_submodels/test_external_circuit/test_function_control.py b/tests/integration/test_models/test_submodels/test_external_circuit/test_function_control.py index 538846c377..1dec8e3e86 100644 --- a/tests/integration/test_models/test_submodels/test_external_circuit/test_function_control.py +++ b/tests/integration/test_models/test_submodels/test_external_circuit/test_function_control.py @@ -8,17 +8,14 @@ class TestFunctionControl(unittest.TestCase): def test_constant_current(self): - class ConstantCurrent: - num_switches = 0 - - def __call__(self, variables): - I = variables["Current [A]"] - return I + 1 + def constant_current(variables): + I = variables["Current [A]"] + return I + 1 # load models models = [ pybamm.lithium_ion.SPM(), - pybamm.lithium_ion.SPM({"operating mode": ConstantCurrent()}), + pybamm.lithium_ion.SPM({"operating mode": constant_current}), ] # load parameter values and process models and geometry @@ -56,17 +53,14 @@ def __call__(self, variables): ) def test_constant_voltage(self): - class ConstantVoltage: - num_switches = 0 - - def __call__(self, variables): - V = variables["Terminal voltage [V]"] - return V - 4.1 + def constant_voltage(variables): + V = variables["Terminal voltage [V]"] + return V - 4.1 # load models models = [ pybamm.lithium_ion.SPM({"operating mode": "voltage"}), - pybamm.lithium_ion.SPM({"operating mode": ConstantVoltage()}), + pybamm.lithium_ion.SPM({"operating mode": constant_voltage}), ] # load parameter values and process models and geometry @@ -102,18 +96,15 @@ def __call__(self, variables): np.testing.assert_array_almost_equal(abs((I1 - I0) / I0), 0, decimal=1) def test_constant_power(self): - class ConstantPower: - num_switches = 0 - - def __call__(self, variables): - I = variables["Current [A]"] - V = variables["Terminal voltage [V]"] - return I * V - 4 + def constant_power(variables): + I = variables["Current [A]"] + V = variables["Terminal voltage [V]"] + return I * V - 4 # load models models = [ pybamm.lithium_ion.SPM({"operating mode": "power"}), - pybamm.lithium_ion.SPM({"operating mode": ConstantPower()}), + pybamm.lithium_ion.SPM({"operating mode": constant_power}), ] # load parameter values and process models and geometry diff --git a/tests/unit/test_experiments/__init__.py b/tests/unit/test_experiments/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/test_experiments/test_experiment.py b/tests/unit/test_experiments/test_experiment.py new file mode 100644 index 0000000000..dbde2faa6d --- /dev/null +++ b/tests/unit/test_experiments/test_experiment.py @@ -0,0 +1,125 @@ +# +# Test the base experiment class +# +import pybamm +import unittest + + +class TestExperiment(unittest.TestCase): + def test_read_strings(self): + experiment = pybamm.Experiment( + [ + "Discharge at 1C for 0.5 hours", + "Discharge at C/20 for 0.5 hours", + "Charge at 0.5 C for 45 minutes", + "Discharge at 1 A for 0.5 hours", + "Charge at 200 mA for 45 minutes (1 minute period)", + "Discharge at 1W for 0.5 hours", + "Charge at 200 mW for 45 minutes", + "Rest for 10 minutes (5 minute period)", + "Hold at 1V for 20 seconds", + "Charge at 1 C until 4.1V", + "Hold at 4.1 V until 50mA", + "Hold at 3V until C/50", + "Discharge at C/3 for 2 hours or until 2.5 V", + ], + {"test": "test"}, + period="20 seconds", + ) + self.assertEqual( + experiment.operating_conditions, + [ + (1, "C", 1800.0, 20.0), + (0.05, "C", 1800.0, 20.0), + (-0.5, "C", 2700.0, 20.0), + (1, "A", 1800.0, 20.0), + (-0.2, "A", 2700.0, 60.0), + (1, "W", 1800.0, 20.0), + (-0.2, "W", 2700.0, 20.0), + (0, "A", 600.0, 300.0), + (1, "V", 20.0, 20.0), + (-1, "C", None, 20.0), + (4.1, "V", None, 20.0), + (3, "V", None, 20.0), + (1 / 3, "C", 7200.0, 20.0), + ], + ) + self.assertEqual( + experiment.events, + [ + None, + None, + None, + None, + None, + None, + None, + None, + None, + (4.1, "V"), + (0.05, "A"), + (0.02, "C"), + (2.5, "V"), + ], + ) + self.assertEqual(experiment.parameters, {"test": "test"}) + self.assertEqual(experiment.period, 20) + + def test_read_strings_repeat(self): + experiment = pybamm.Experiment( + ["Discharge at 10 mA for 0.5 hours"] + + ["Charge at 0.5 C for 45 minutes", "Hold at 1 V for 20 seconds"] * 2, + ) + self.assertEqual( + experiment.operating_conditions, + [ + (0.01, "A", 1800.0, 60), + (-0.5, "C", 2700.0, 60), + (1, "V", 20.0, 60), + (-0.5, "C", 2700.0, 60), + (1, "V", 20.0, 60), + ], + ) + self.assertEqual(experiment.period, 60) + + def test_str_repr(self): + conds = ["Discharge at 1 C for 20 seconds", "Charge at 0.5 W for 10 minutes"] + experiment = pybamm.Experiment(conds) + self.assertEqual(str(experiment), str(conds)) + self.assertEqual( + repr(experiment), + "pybamm.Experiment(['Discharge at 1 C for 20 seconds'" + + ", 'Charge at 0.5 W for 10 minutes'])", + ) + + def test_bad_strings(self): + with self.assertRaisesRegex( + TypeError, "Operating conditions should be strings" + ): + pybamm.Experiment([1, 2, 3]) + with self.assertRaisesRegex(ValueError, "Operating conditions must contain"): + pybamm.Experiment(["Discharge at 1 A at 2 hours"]) + with self.assertRaisesRegex(ValueError, "instruction must be"): + pybamm.Experiment(["Run at 1 A for 2 hours"]) + with self.assertRaisesRegex( + ValueError, "Instruction 'Run at at 1 A' not recognized" + ): + pybamm.Experiment(["Run at at 1 A for 2 hours"]) + with self.assertRaisesRegex(ValueError, "units must be"): + pybamm.Experiment(["Discharge at 1 B for 2 hours"]) + with self.assertRaisesRegex(ValueError, "time units must be"): + pybamm.Experiment(["Discharge at 1 A for 2 years"]) + with self.assertRaisesRegex( + TypeError, "experimental parameters should be a dictionary" + ): + pybamm.Experiment([], "not a dictionary") + + +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_experiments/test_simulation_with_experiment.py b/tests/unit/test_experiments/test_simulation_with_experiment.py new file mode 100644 index 0000000000..08d7bc50f9 --- /dev/null +++ b/tests/unit/test_experiments/test_simulation_with_experiment.py @@ -0,0 +1,106 @@ +# +# Test setting up a simulation with an experiment +# +import pybamm +import unittest + + +class TestSimulationExperiment(unittest.TestCase): + def test_set_up(self): + experiment = pybamm.Experiment( + [ + "Discharge at C/20 for 1 hour", + "Charge at 1 A until 4.1 V", + "Hold at 4.1 V until 50 mA", + "Discharge at 2 W for 1 hour", + ], + ) + model = pybamm.lithium_ion.DFN() + sim = pybamm.Simulation(model, experiment=experiment) + + self.assertEqual(sim.experiment, experiment) + self.assertEqual( + sim._experiment_inputs[0]["Current input [A]"], + 1 / 20 * model.default_parameter_values["Cell capacity [A.h]"], + ) + self.assertEqual(sim._experiment_inputs[0]["Current switch"], 1) + self.assertEqual(sim._experiment_inputs[0]["Voltage switch"], 0) + self.assertEqual(sim._experiment_inputs[0]["Power switch"], 0) + self.assertEqual(sim._experiment_inputs[0]["Current cut-off [A]"], -1e10) + self.assertEqual(sim._experiment_inputs[0]["Voltage cut-off [V]"], -1e10) + self.assertEqual(sim._experiment_inputs[1]["Current input [A]"], -1) + self.assertEqual(sim._experiment_inputs[1]["Current switch"], 1) + self.assertEqual(sim._experiment_inputs[1]["Voltage switch"], 0) + self.assertEqual(sim._experiment_inputs[1]["Power switch"], 0) + self.assertEqual(sim._experiment_inputs[1]["Current cut-off [A]"], -1e10) + self.assertEqual(sim._experiment_inputs[1]["Voltage cut-off [V]"], 4.1) + self.assertEqual(sim._experiment_inputs[2]["Current switch"], 0) + self.assertEqual(sim._experiment_inputs[2]["Voltage switch"], 1) + self.assertEqual(sim._experiment_inputs[2]["Power switch"], 0) + self.assertEqual(sim._experiment_inputs[2]["Voltage input [V]"], 4.1) + self.assertEqual(sim._experiment_inputs[2]["Current cut-off [A]"], 0.05) + self.assertEqual(sim._experiment_inputs[2]["Voltage cut-off [V]"], -1e10) + self.assertEqual(sim._experiment_inputs[3]["Current switch"], 0) + self.assertEqual(sim._experiment_inputs[3]["Voltage switch"], 0) + self.assertEqual(sim._experiment_inputs[3]["Power switch"], 1) + self.assertEqual(sim._experiment_inputs[3]["Power input [W]"], 2) + self.assertEqual(sim._experiment_inputs[3]["Current cut-off [A]"], -1e10) + self.assertEqual(sim._experiment_inputs[3]["Voltage cut-off [V]"], -1e10) + + tau = sim._parameter_values.evaluate(model.timescale) + self.assertEqual( + sim._experiment_times, + [t / tau for t in [3600, 7 * 24 * 3600, 7 * 24 * 3600, 3600]], + ) + + self.assertIn( + "Current cut-off (positive) [A] [experiment]", + [event.name for event in sim.model.events], + ) + self.assertIn( + "Current cut-off (negative) [A] [experiment]", + [event.name for event in sim.model.events], + ) + self.assertIn( + "Voltage cut-off [V] [experiment]", + [event.name for event in sim.model.events], + ) + + # fails if trying to set up with something that isn't an experiment + with self.assertRaisesRegex(TypeError, "experiment must be"): + pybamm.Simulation(model, experiment=0) + + def test_run_experiment(self): + experiment = pybamm.Experiment( + [ + "Discharge at C/20 for 1 hour", + "Charge at 1 A until 4.1 V", + "Hold at 4.1 V until C/2", + "Discharge at 2 W for 1 hour", + ], + ) + model = pybamm.lithium_ion.SPM() + sim = pybamm.Simulation(model, experiment=experiment) + sim.solve() + self.assertEqual(sim._solution.termination, "final time") + + def test_run_experiment_breaks_early(self): + experiment = pybamm.Experiment(["Discharge at 2 C for 1 hour"]) + model = pybamm.lithium_ion.SPM() + sim = pybamm.Simulation(model, experiment=experiment) + pybamm.set_logging_level("ERROR") + # giving the time, should get ignored + t_eval = [0, 1] + sim.solve(t_eval) + pybamm.set_logging_level("WARNING") + self.assertIn("event", sim._solution.termination) + + +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_concatenations.py b/tests/unit/test_expression_tree/test_concatenations.py index 2a5a161148..eb18381ddf 100644 --- a/tests/unit/test_expression_tree/test_concatenations.py +++ b/tests/unit/test_expression_tree/test_concatenations.py @@ -25,6 +25,10 @@ def test_base_concatenation(self): with self.assertRaises(TypeError): conc2.evaluate() + # trying to concatenate non-pybamm symbols + with self.assertRaises(TypeError): + pybamm.Concatenation(1, 2) + def test_concatenation_domains(self): a = pybamm.Symbol("a", domain=["negative electrode"]) b = pybamm.Symbol("b", domain=["separator", "positive electrode"]) diff --git a/tests/unit/test_models/test_base_model.py b/tests/unit/test_models/test_base_model.py index 7feb273503..b9c5e6c703 100644 --- a/tests/unit/test_models/test_base_model.py +++ b/tests/unit/test_models/test_base_model.py @@ -401,6 +401,10 @@ def test_default_parameters(self): ) os.chdir(cwd) + def test_timescale(self): + model = pybamm.BaseModel() + self.assertEqual(model.timescale.evaluate(), 1) + if __name__ == "__main__": print("Add -v for more debug output") 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 59697e2b3a..0c7757d17f 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 @@ -167,15 +167,12 @@ def test_well_posed_power(self): model.check_well_posedness() def test_well_posed_function(self): - class ExternalCircuitFunction: - num_switches = 0 + def external_circuit_function(variables): + I = variables["Current [A]"] + V = variables["Terminal voltage [V]"] + return V + I - pybamm.FunctionParameter("Function", pybamm.t) - def __call__(self, variables): - I = variables["Current [A]"] - V = variables["Terminal voltage [V]"] - return V + I - pybamm.FunctionParameter("Function", pybamm.t) - - options = {"operating mode": ExternalCircuitFunction()} + options = {"operating mode": external_circuit_function} model = pybamm.lead_acid.LOQS(options) model.check_well_posedness() diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_base_lithium_ion_model.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_base_lithium_ion_model.py index 3ac309316f..9e5bda3d5b 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_base_lithium_ion_model.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_base_lithium_ion_model.py @@ -7,8 +7,6 @@ class TestBaseLithiumIonModel(unittest.TestCase): def test_incompatible_options(self): - with self.assertRaisesRegex(pybamm.OptionError, "surface form not implemented"): - pybamm.lithium_ion.BaseModel({"surface form": "differential"}) with self.assertRaisesRegex(pybamm.OptionError, "convection not implemented"): pybamm.lithium_ion.BaseModel({"convection": True}) 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 d8bc4fd2ea..9d75316b73 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 @@ -200,15 +200,12 @@ def test_well_posed_power(self): model.check_well_posedness() def test_well_posed_function(self): - class ExternalCircuitFunction: - num_switches = 0 + def external_circuit_function(variables): + I = variables["Current [A]"] + V = variables["Terminal voltage [V]"] + return V + I - pybamm.FunctionParameter("Function", pybamm.t) - def __call__(self, variables): - I = variables["Current [A]"] - V = variables["Terminal voltage [V]"] - return V + I - pybamm.FunctionParameter("Function", pybamm.t) - - options = {"operating mode": ExternalCircuitFunction()} + options = {"operating mode": external_circuit_function} model = pybamm.lithium_ion.SPM(options) model.check_well_posedness() diff --git a/tests/unit/test_models/test_submodels/test_external_circuit/test_function_control.py b/tests/unit/test_models/test_submodels/test_external_circuit/test_function_control.py index ec961b4316..4a06002baa 100644 --- a/tests/unit/test_models/test_submodels/test_external_circuit/test_function_control.py +++ b/tests/unit/test_models/test_submodels/test_external_circuit/test_function_control.py @@ -6,22 +6,17 @@ import unittest -class ExternalCircuitFunction: - num_switches = 0 - - def __call__(self, variables): - I = variables["Current [A]"] - V = variables["Terminal voltage [V]"] - return ( - V + I - pybamm.FunctionParameter("Current plus voltage function", pybamm.t) - ) +def external_circuit_function(variables): + I = variables["Current [A]"] + V = variables["Terminal voltage [V]"] + return V + I - pybamm.FunctionParameter("Current plus voltage function", pybamm.t) class TestFunctionControl(unittest.TestCase): def test_public_functions(self): param = pybamm.standard_parameters_lithium_ion submodel = pybamm.external_circuit.FunctionControl( - param, ExternalCircuitFunction() + param, external_circuit_function ) variables = {"Terminal voltage [V]": pybamm.Scalar(0)} std_tests = tests.StandardSubModelTests(submodel, variables) diff --git a/tests/unit/test_models/test_submodels/test_particle/test_base_particle.py b/tests/unit/test_models/test_submodels/test_particle/test_base_particle.py index 8cc958d1d2..084bc3b621 100644 --- a/tests/unit/test_models/test_submodels/test_particle/test_base_particle.py +++ b/tests/unit/test_models/test_submodels/test_particle/test_base_particle.py @@ -9,15 +9,17 @@ class TestBaseParticle(unittest.TestCase): def test_public_functions(self): + variables = { + "Negative particle surface concentration": 0, + "Positive particle surface concentration": 0, + } submodel = pybamm.particle.BaseParticle(None, "Negative") - std_tests = tests.StandardSubModelTests(submodel) - with self.assertRaises(NotImplementedError): - std_tests.test_all() + std_tests = tests.StandardSubModelTests(submodel, variables) + std_tests.test_all() submodel = pybamm.particle.BaseParticle(None, "Positive") - std_tests = tests.StandardSubModelTests(submodel) - with self.assertRaises(NotImplementedError): - std_tests.test_all() + std_tests = tests.StandardSubModelTests(submodel, variables) + std_tests.test_all() if __name__ == "__main__": diff --git a/tests/unit/test_models/test_submodels/test_particle/test_fickian/test_base_fickian_particle.py b/tests/unit/test_models/test_submodels/test_particle/test_fickian/test_base_fickian_particle.py deleted file mode 100644 index be03973287..0000000000 --- a/tests/unit/test_models/test_submodels/test_particle/test_fickian/test_base_fickian_particle.py +++ /dev/null @@ -1,30 +0,0 @@ -# -# Test base fickian submodel -# - -import pybamm -import tests -import unittest - - -class TestBaseModel(unittest.TestCase): - def test_public_functions(self): - submodel = pybamm.particle.fickian.BaseModel(None, "Negative") - std_tests = tests.StandardSubModelTests(submodel) - with self.assertRaises(NotImplementedError): - std_tests.test_all() - - submodel = pybamm.particle.fickian.BaseModel(None, "Positive") - std_tests = tests.StandardSubModelTests(submodel) - with self.assertRaises(NotImplementedError): - std_tests.test_all() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_parameters/test_dimensionless_parameter_values_lithium_ion.py b/tests/unit/test_parameters/test_dimensionless_parameter_values_lithium_ion.py index 73ad08911d..7c7dd3000c 100644 --- a/tests/unit/test_parameters/test_dimensionless_parameter_values_lithium_ion.py +++ b/tests/unit/test_parameters/test_dimensionless_parameter_values_lithium_ion.py @@ -63,7 +63,7 @@ def test_lithium_ion(self): "particle dynamics" # neg diffusion coefficient np.testing.assert_almost_equal( - values.evaluate(param.D_n_dimensional(param.c_n_init, param.T_ref)), + values.evaluate(param.D_n_dimensional(param.c_n_init(0), param.T_ref)), 3.9 * 10 ** (-14), 2, ) @@ -78,7 +78,7 @@ def test_lithium_ion(self): # pos diffusion coefficient np.testing.assert_almost_equal( - values.evaluate(param.D_p_dimensional(param.c_p_init, param.T_ref)), + values.evaluate(param.D_p_dimensional(param.c_p_init(1), param.T_ref)), 1 * 10 ** (-13), 2, ) diff --git a/tests/unit/test_parameters/test_parameter_sets/__init__.py b/tests/unit/test_parameters/test_parameter_sets/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/test_parameters/test_parameter_sets/test_NCA_Kim2011.py b/tests/unit/test_parameters/test_parameter_sets/test_NCA_Kim2011.py new file mode 100644 index 0000000000..3225b3b45f --- /dev/null +++ b/tests/unit/test_parameters/test_parameter_sets/test_NCA_Kim2011.py @@ -0,0 +1,50 @@ +# +# Tests for NCA parameter set loads +# +import pybamm +import unittest + + +class TestKim(unittest.TestCase): + def test_load_params(self): + anode = pybamm.ParameterValues({}).read_parameters_csv( + "input/parameters/lithium-ion/anodes/graphite_Kim2011/parameters.csv" + ) + self.assertEqual(anode["Reference temperature [K]"], "298.15") + + cathode = pybamm.ParameterValues({}).read_parameters_csv( + "input/parameters/lithium-ion/cathodes/nca_Kim2011/parameters.csv" + ) + self.assertEqual(cathode["Reference temperature [K]"], "298.15") + + electrolyte = pybamm.ParameterValues({}).read_parameters_csv( + "input/parameters/lithium-ion/electrolytes/lipf6_Kim2011/parameters.csv" + ) + self.assertEqual(electrolyte["Reference temperature [K]"], "298.15") + + cell = pybamm.ParameterValues({}).read_parameters_csv( + "input/parameters/lithium-ion/cells/Kim2011/parameters.csv" + ) + self.assertAlmostEqual( + cell["Negative current collector thickness [m]"], 10 ** (-5) + ) + + def test_standard_lithium_parameters(self): + + chemistry = pybamm.parameter_sets.NCA_Kim2011 + parameter_values = pybamm.ParameterValues(chemistry=chemistry) + + model = pybamm.lithium_ion.DFN() + sim = pybamm.Simulation(model, parameter_values=parameter_values) + sim.set_parameters() + sim.build() + + +if __name__ == "__main__": + print("Add -v for more debug output") + import sys + + if "-v" in sys.argv: + debug = True + pybamm.settings.debug_mode = True + unittest.main() diff --git a/tests/unit/test_simulation.py b/tests/unit/test_simulation.py index 5e3a090a40..c2aa884c29 100644 --- a/tests/unit/test_simulation.py +++ b/tests/unit/test_simulation.py @@ -1,5 +1,7 @@ import pybamm import numpy as np +import pandas as pd +import os import unittest @@ -347,6 +349,40 @@ def test_plot(self): sim.solve(t_eval=t_eval) sim.plot(testing=True) + def test_drive_cycle_data(self): + model = pybamm.lithium_ion.SPM() + param = model.default_parameter_values + param["Current function [A]"] = "[current data]US06" + + drive_cycle = pd.read_csv( + os.path.join(pybamm.root_dir(), "input", "drive_cycles", "US06.csv"), + comment="#", + skip_blank_lines=True, + header=None, + ) + time_data = drive_cycle.values[:, 0] + tau = param.evaluate(pybamm.standard_parameters_lithium_ion.tau_discharge) + + sim = pybamm.Simulation(model, parameter_values=param) + + # check solution is returned at the times in the data (only almost equal due + # to multiplication by tau) + sim.solve() + np.testing.assert_array_almost_equal(sim.solution.t * tau, time_data) + + # check warning raised if the largest gap in t_eval is bigger than the + # smallest gap in the data + sim.reset() + with self.assertWarns(pybamm.SolverWarning): + sim.solve(t_eval=np.linspace(0, 1, 100)) + + # check warning raised if t_eval doesnt conatin time_data , but has a finer + # resolution (can still solve, but good for users to know they dont have + # the solution returned at the data points) + sim.reset() + with self.assertWarns(pybamm.SolverWarning): + sim.solve(t_eval=np.linspace(0, time_data[-1] / tau, 800)) + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index 5883781346..3921f20315 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -58,7 +58,8 @@ def algebraic_eval(self, t, y): return y + 2 solver = pybamm.BaseSolver() - init_cond = solver.calculate_consistent_initial_conditions(ScalarModel()) + model = ScalarModel() + init_cond = solver.calculate_consistent_state(model) np.testing.assert_array_equal(init_cond, -2) # More complicated system @@ -75,7 +76,7 @@ def algebraic_eval(self, t, y): return (y[1:] - vec[1:]) ** 2 model = VectorModel() - init_cond = solver.calculate_consistent_initial_conditions(model) + init_cond = solver.calculate_consistent_state(model) np.testing.assert_array_almost_equal(init_cond, vec) # With jacobian @@ -83,7 +84,7 @@ def jac_dense(t, y): return 2 * np.hstack([np.zeros((3, 1)), np.diag(y[1:] - vec[1:])]) model.jac_algebraic_eval = jac_dense - init_cond = solver.calculate_consistent_initial_conditions(model) + init_cond = solver.calculate_consistent_state(model) np.testing.assert_array_almost_equal(init_cond, vec) # With sparse jacobian @@ -93,7 +94,7 @@ def jac_sparse(t, y): ) model.jac_algebraic_eval = jac_sparse - init_cond = solver.calculate_consistent_initial_conditions(model) + init_cond = solver.calculate_consistent_state(model) np.testing.assert_array_almost_equal(init_cond, vec) def test_fail_consistent_initial_conditions(self): @@ -114,13 +115,13 @@ def algebraic_eval(self, t, y): pybamm.SolverError, "Could not find consistent initial conditions: The iteration is not making", ): - solver.calculate_consistent_initial_conditions(Model()) + solver.calculate_consistent_state(Model()) solver = pybamm.BaseSolver() with self.assertRaisesRegex( pybamm.SolverError, "Could not find consistent initial conditions: solver terminated", ): - solver.calculate_consistent_initial_conditions(Model()) + solver.calculate_consistent_state(Model()) if __name__ == "__main__": diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index ffb5d6aa1f..a109f5bc6e 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -34,7 +34,7 @@ def test_model_solver(self): ) # Safe mode (enforce events that won't be triggered) - model.events = {"an event": var + 1} + model.events = [pybamm.Event("an event", var + 1)] disc.process_model(model) solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) t_eval = np.linspace(0, 1, 100) @@ -74,7 +74,7 @@ def test_model_solver_failure(self): model.rhs = {var: -pybamm.sqrt(var)} model.initial_conditions = {var: 1} # add events so that safe mode is used (won't be triggered) - model.events = {"10": var - 10} + model.events = [pybamm.Event("10", var - 10)] # No need to set parameters; can use base discretisation (no spatial operators) # create discretisation @@ -103,10 +103,10 @@ def test_model_solver_events(self): 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), - } + model.events = [ + pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), + ] disc = get_discretisation_for_testing() disc.process_model(model) @@ -205,10 +205,10 @@ def test_model_step_events(self): 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), - } + model.events = [ + pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), + ] disc = pybamm.Discretisation() disc.process_model(model) @@ -237,7 +237,7 @@ def test_model_solver_with_inputs(self): var = pybamm.Variable("var", domain=domain) model.rhs = {var: -pybamm.InputParameter("rate") * var} model.initial_conditions = {var: 1} - model.events = {"var=0.5": pybamm.min(var - 0.5)} + model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] # No need to set parameters; can use base discretisation (no spatial # operators) diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index f6f25c0453..9683b5cbc6 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -20,7 +20,10 @@ def test_ida_roberts_klu(self): model.rhs = {u: 0.1 * v} model.algebraic = {v: 1 - v} model.initial_conditions = {u: 0, v: 1} - model.events = {"1": u - 0.2, "2": v} + model.events = [ + pybamm.Event("1", u - 0.2), + pybamm.Event("2", v), + ] disc = pybamm.Discretisation() disc.process_model(model) diff --git a/tests/unit/test_solvers/test_scikits_solvers.py b/tests/unit/test_solvers/test_scikits_solvers.py index 99e4053850..3a7067e8f8 100644 --- a/tests/unit/test_solvers/test_scikits_solvers.py +++ b/tests/unit/test_solvers/test_scikits_solvers.py @@ -6,6 +6,7 @@ import unittest import warnings from tests import get_mesh_for_testing, get_discretisation_for_testing +import sys @unittest.skipIf(not pybamm.have_scikits_odes(), "scikits.odes not installed") @@ -38,7 +39,7 @@ def test_model_dae_integrate_failure_bad_ics(self): class Model: mass_matrix = pybamm.Matrix(np.array([[1.0, 0.0], [0.0, 0.0]])) y0 = np.array([0.0, 1.0]) - events_eval = [] + terminate_events_eval = [] def residuals_eval(self, t, y, ydot): return np.array([0.5 * np.ones_like(y[0]) - ydot[0], 2 * y[0] - y[1]]) @@ -84,7 +85,7 @@ def test_dae_integrate_with_non_unity_mass(self): class Model: mass_matrix = pybamm.Matrix(np.array([[4.0, 0.0], [0.0, 0.0]])) y0 = np.array([0.0, 0.0]) - events_eval = [] + terminate_events_eval = [] def residuals_eval(self, t, y, ydot): return np.array( @@ -125,10 +126,10 @@ def test_model_solver_ode_events_python(self): 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), - } + model.events = [ + pybamm.Event("2 * var = 2.5", pybamm.min(2 * var - 2.5)), + pybamm.Event("var = 1.5", pybamm.min(var - 1.5)), + ] disc = get_discretisation_for_testing() disc.process_model(model) @@ -226,10 +227,10 @@ def test_model_solver_dae_events_python(self): 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), - } + model.events = [ + pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), + ] disc = get_discretisation_for_testing() disc.process_model(model) @@ -242,6 +243,82 @@ def test_model_solver_dae_events_python(self): np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) np.testing.assert_allclose(solution.y[-1], 2 * np.exp(0.1 * solution.t)) + def test_model_solver_dae_nonsmooth_python(self): + model = pybamm.BaseModel() + model.convert_to_format = "python" + whole_cell = ["negative electrode", "separator", "positive electrode"] + var1 = pybamm.Variable("var1", domain=whole_cell) + var2 = pybamm.Variable("var2", domain=whole_cell) + discontinuity = 0.6 + + def nonsmooth_rate(t): + return 0.1 * int(t < discontinuity) + 0.1 + + def nonsmooth_mult(t): + return int(t < discontinuity) + 1.0 + rate = pybamm.Function(nonsmooth_rate, pybamm.t) + mult = pybamm.Function(nonsmooth_mult, pybamm.t) + model.rhs = {var1: rate * var1} + model.algebraic = {var2: mult * var1 - var2} + model.initial_conditions = {var1: 1, var2: 2} + model.events = [ + pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), + pybamm.Event("nonsmooth rate", + pybamm.Scalar(discontinuity), + pybamm.EventType.DISCONTINUITY + ), + pybamm.Event("nonsmooth mult", + pybamm.Scalar(discontinuity), + pybamm.EventType.DISCONTINUITY + ) + ] + disc = get_discretisation_for_testing() + disc.process_model(model) + + # Solve + solver = pybamm.ScikitsDaeSolver(rtol=1e-8, atol=1e-8) + + # create two time series, one without a time point on the discontinuity, + # and one with + t_eval1 = np.linspace(0, 5, 10) + t_eval2 = np.insert(t_eval1, + np.searchsorted(t_eval1, discontinuity), + discontinuity) + solution1 = solver.solve(model, t_eval1) + solution2 = solver.solve(model, t_eval2) + + # check time vectors + for solution in [solution1, solution2]: + # time vectors are ordered + self.assertTrue(np.all(solution.t[:-1] <= solution.t[1:])) + + # time value before and after discontinuity is an epsilon away + dindex = np.searchsorted(solution.t, discontinuity) + value_before = solution.t[dindex - 1] + value_after = solution.t[dindex] + self.assertEqual(value_before + sys.float_info.epsilon, discontinuity) + self.assertEqual(value_after - sys.float_info.epsilon, discontinuity) + + # both solution time vectors should have same number of points + self.assertEqual(len(solution1.t), len(solution2.t)) + + # check solution + for solution in [solution1, solution2]: + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[-1], 2.5) + var1_soln = np.exp(0.2 * solution.t) + y0 = np.exp(0.2 * discontinuity) + var1_soln[solution.t > discontinuity] = \ + y0 * np.exp( + 0.1 * (solution.t[solution.t > discontinuity] - discontinuity) + ) + var2_soln = 2 * var1_soln + var2_soln[solution.t > discontinuity] = \ + var1_soln[solution.t > discontinuity] + np.testing.assert_allclose(solution.y[0], var1_soln, rtol=1e-06) + np.testing.assert_allclose(solution.y[-1], var2_soln, rtol=1e-06) + def test_model_solver_dae_with_jacobian_python(self): model = pybamm.BaseModel() model.convert_to_format = "python" @@ -369,10 +446,10 @@ def test_model_solver_ode_events_casadi(self): 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), - } + model.events = [ + pybamm.Event("2 * var = 2.5", pybamm.min(2 * var - 2.5)), + pybamm.Event("var = 1.5", pybamm.min(var - 1.5)), + ] disc = get_discretisation_for_testing() disc.process_model(model) @@ -396,10 +473,10 @@ def test_model_solver_dae_events_casadi(self): 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), - } + model.events = [ + pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), + ] disc = get_discretisation_for_testing() disc.process_model(model) @@ -423,10 +500,10 @@ def test_model_solver_dae_inputs_events(self): model.rhs = {var1: pybamm.InputParameter("rate 1") * var1} model.algebraic = {var2: pybamm.InputParameter("rate 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), - } + model.events = [ + pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), + ] disc = get_discretisation_for_testing() disc.process_model(model) @@ -487,10 +564,10 @@ def test_model_step_events(self): 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), - } + model.events = [ + pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), + ] disc = pybamm.Discretisation() disc.process_model(model) @@ -523,9 +600,9 @@ def test_ode_solver_fail_with_dae(self): if __name__ == "__main__": print("Add -v for more debug output") - import sys if "-v" in sys.argv: debug = True + pybamm.set_logging_level("DEBUG") pybamm.settings.debug_mode = True unittest.main() diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index 31fb7489a9..0068835b15 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -6,6 +6,8 @@ import numpy as np from tests import get_mesh_for_testing import warnings +from tests import get_discretisation_for_testing +import sys class TestScipySolver(unittest.TestCase): @@ -68,7 +70,7 @@ def test_model_solver_with_event_python(self): var = pybamm.Variable("var", domain=domain) model.rhs = {var: -0.1 * var} model.initial_conditions = {var: 1} - model.events = {"var=0.5": pybamm.min(var - 0.5)} + model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] # No need to set parameters; can use base discretisation (no spatial operators) # create discretisation @@ -136,6 +138,91 @@ def jacobian(t, y): np.ones((N, T.size)) * (T[np.newaxis, :] - np.exp(T[np.newaxis, :])), ) + def test_model_solver_ode_nonsmooth(self): + whole_cell = ["negative electrode", "separator", "positive electrode"] + var1 = pybamm.Variable("var1", domain=whole_cell) + discontinuity = 0.6 + + # Create three different models with the same solution, each expressing the + # discontinuity in a different way + + # first model explicitly adds a discontinuity event + def nonsmooth_rate(t): + return 0.1 * (t < discontinuity) + 0.1 + + rate = pybamm.Function(nonsmooth_rate, pybamm.t) + model1 = pybamm.BaseModel() + model1.rhs = {var1: rate * var1} + model1.initial_conditions = {var1: 1} + model1.events = [ + pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + pybamm.Event("nonsmooth rate", + pybamm.Scalar(discontinuity), + pybamm.EventType.DISCONTINUITY + ), + ] + + # second model implicitly adds a discontinuity event via a heaviside function + model2 = pybamm.BaseModel() + model2.rhs = {var1: (0.1 * (pybamm.t < discontinuity) + 0.1) * var1} + model2.initial_conditions = {var1: 1} + model2.events = [ + pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + ] + + # third model implicitly adds a discontinuity event via another heaviside + # function + model3 = pybamm.BaseModel() + model3.rhs = {var1: (-0.1 * (discontinuity < pybamm.t) + 0.2) * var1} + model3.initial_conditions = {var1: 1} + model3.events = [ + pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), + ] + + for model in [model1, model2, model3]: + + disc = get_discretisation_for_testing() + disc.process_model(model) + + # Solve + solver = pybamm.ScipySolver(rtol=1e-8, atol=1e-8) + + # create two time series, one without a time point on the discontinuity, + # and one with + t_eval1 = np.linspace(0, 5, 10) + t_eval2 = np.insert(t_eval1, + np.searchsorted(t_eval1, discontinuity), + discontinuity) + solution1 = solver.solve(model, t_eval1) + solution2 = solver.solve(model, t_eval2) + + # check time vectors + for solution in [solution1, solution2]: + # time vectors are ordered + self.assertTrue(np.all(solution.t[:-1] <= solution.t[1:])) + + # time value before and after discontinuity is an epsilon away + dindex = np.searchsorted(solution.t, discontinuity) + value_before = solution.t[dindex - 1] + value_after = solution.t[dindex] + self.assertEqual(value_before + sys.float_info.epsilon, discontinuity) + self.assertEqual(value_after - sys.float_info.epsilon, discontinuity) + + # both solution time vectors should have same number of points + self.assertEqual(len(solution1.t), len(solution2.t)) + + # check solution + for solution in [solution1, solution2]: + np.testing.assert_array_less(solution.y[0], 1.5) + np.testing.assert_array_less(solution.y[-1], 2.5) + var1_soln = np.exp(0.2 * solution.t) + y0 = np.exp(0.2 * discontinuity) + var1_soln[solution.t > discontinuity] = \ + y0 * np.exp( + 0.1 * (solution.t[solution.t > discontinuity] - discontinuity) + ) + np.testing.assert_allclose(solution.y[0], var1_soln, rtol=1e-06) + def test_model_step_python(self): # Create model model = pybamm.BaseModel() @@ -180,7 +267,7 @@ def test_model_solver_with_inputs(self): var = pybamm.Variable("var", domain=domain) model.rhs = {var: -pybamm.InputParameter("rate") * var} model.initial_conditions = {var: 1} - model.events = {"var=0.5": pybamm.min(var - 0.5)} + model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] # No need to set parameters; can use base discretisation (no spatial # operators) @@ -233,7 +320,7 @@ def test_model_solver_with_event_with_casadi(self): var = pybamm.Variable("var", domain=domain) model.rhs = {var: -0.1 * var} model.initial_conditions = {var: 1} - model.events = {"var=0.5": pybamm.min(var - 0.5)} + model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] # No need to set parameters; can use base discretisation (no spatial # operators) @@ -258,7 +345,7 @@ def test_model_solver_with_inputs_with_casadi(self): var = pybamm.Variable("var", domain=domain) model.rhs = {var: -pybamm.InputParameter("rate") * var} model.initial_conditions = {var: 1} - model.events = {"var=0.5": pybamm.min(var - 0.5)} + model.events = [pybamm.Event("var=0.5", pybamm.min(var - 0.5))] # No need to set parameters; can use base discretisation (no spatial # operators) @@ -278,7 +365,6 @@ def test_model_solver_with_inputs_with_casadi(self): if __name__ == "__main__": print("Add -v for more debug output") - import sys if "-v" in sys.argv: debug = True diff --git a/tests/unit/test_solvers/test_solution.py b/tests/unit/test_solvers/test_solution.py index 7aa4db34fe..2793a65905 100644 --- a/tests/unit/test_solvers/test_solution.py +++ b/tests/unit/test_solvers/test_solution.py @@ -4,6 +4,9 @@ import pybamm import unittest import numpy as np +import pandas as pd +from scipy.io import loadmat +from tests import get_discretisation_for_testing class TestSolution(unittest.TestCase): @@ -19,25 +22,47 @@ def test_init(self): self.assertEqual(sol.inputs, {}) self.assertEqual(sol.model, None) + with self.assertRaisesRegex(AttributeError, "sub solutions"): + print(sol.sub_solutions) + def test_append(self): # Set up first solution t1 = np.linspace(0, 1) y1 = np.tile(t1, (20, 1)) sol1 = pybamm.Solution(t1, y1) sol1.solve_time = 1.5 - sol1.inputs = {} + sol1.model = pybamm.BaseModel() + sol1.inputs = {"a": 1} # Set up second solution t2 = np.linspace(1, 2) y2 = np.tile(t2, (20, 1)) sol2 = pybamm.Solution(t2, y2) sol2.solve_time = 1 - sol1.append(sol2) + sol2.inputs = {"a": 2} + sol1.append(sol2, create_sub_solutions=True) # Test self.assertEqual(sol1.solve_time, 2.5) np.testing.assert_array_equal(sol1.t, np.concatenate([t1, t2[1:]])) np.testing.assert_array_equal(sol1.y, np.concatenate([y1, y2[:, 1:]], axis=1)) + np.testing.assert_array_equal( + sol1.inputs["a"], + np.concatenate([1 * np.ones_like(t1), 2 * np.ones_like(t2[1:])]), + ) + + # Test sub-solutions + self.assertEqual(len(sol1.sub_solutions), 2) + np.testing.assert_array_equal(sol1.sub_solutions[0].t, t1) + np.testing.assert_array_equal(sol1.sub_solutions[1].t, t2) + self.assertEqual(sol1.sub_solutions[0].model, sol1.model) + np.testing.assert_array_equal( + sol1.sub_solutions[0].inputs["a"], 1 * np.ones_like(t1) + ) + self.assertEqual(sol1.sub_solutions[1].model, sol2.model) + np.testing.assert_array_equal( + sol1.sub_solutions[1].inputs["a"], 2 * np.ones_like(t2) + ) def test_total_time(self): sol = pybamm.Solution([], None) @@ -71,12 +96,14 @@ def test_getitem(self): def test_save(self): model = pybamm.BaseModel() + # create both 1D and 2D variables c = pybamm.Variable("c") - model.rhs = {c: -c} - model.initial_conditions = {c: 1} - model.variables["c"] = c + d = pybamm.Variable("d", domain="negative electrode") + model.rhs = {c: -c, d: 1} + model.initial_conditions = {c: 1, d: 2} + model.variables = {"c": c, "d": d, "2c": 2 * c} - disc = pybamm.Discretisation() + disc = get_discretisation_for_testing() disc.process_model(model) solution = pybamm.ScipySolver().solve(model, np.linspace(0, 1)) @@ -84,16 +111,36 @@ def test_save(self): with self.assertRaises(ValueError): solution.save_data("test.pickle") # set variables first then save - solution.update(["c"]) + solution.update(["c", "d"]) solution.save_data("test.pickle") data_load = pybamm.load("test.pickle") np.testing.assert_array_equal(solution.data["c"], data_load["c"]) - - # test save + np.testing.assert_array_equal(solution.data["d"], data_load["d"]) + + # to matlab + solution.save_data("test.mat", to_format="matlab") + data_load = loadmat("test.mat") + np.testing.assert_array_equal(solution.data["c"], data_load["c"].flatten()) + np.testing.assert_array_equal(solution.data["d"], data_load["d"]) + + # to csv + with self.assertRaisesRegex( + ValueError, "only 1D variables can be saved to csv" + ): + solution.save_data("test.csv", to_format="csv") + # only save "c" and "2c" + solution.save_data("test.csv", ["c", "2c"], to_format="csv") + # read csv + df = pd.read_csv("test.csv") + np.testing.assert_array_almost_equal(df["c"], solution.data["c"]) + np.testing.assert_array_almost_equal(df["2c"], solution.data["2c"]) + + # test save whole solution solution.save("test.pickle") solution_load = pybamm.load("test.pickle") self.assertEqual(solution.model.name, solution_load.model.name) np.testing.assert_array_equal(solution["c"].entries, solution_load["c"].entries) + np.testing.assert_array_equal(solution["d"].entries, solution_load["d"].entries) def test_solution_evals_with_inputs(self): model = pybamm.lithium_ion.SPM()