Repository: pybamm-team/PyBaMM (python)

Issue:
Citing PyBaMM
Post paper release, add section to README.md (and docs?) on how to cite pybamm

Pull request:
Issue 613 citing
# Description

Add citation for pybamm to readme, and add functionality to print citations for the specific parts (models/solvers/parameters) of pybamm that were used when running a simulation

Fixes #613 

## Type of change

Please add a line in the relevant section of [CHANGELOG.md](https://github.com/pybamm-team/PyBaMM/blob/master/CHANGELOG.md) to document the change (include PR #) - note reverse order of PR #s. If necessary, also add to the list of breaking changes.

- [x] New feature (non-breaking change which adds functionality)
- [ ] Optimization (back-end change that speeds up the code)
- [ ] Bug fix (non-breaking change which fixes an issue)


# Key checklist:

- [x] No style issues: `$ flake8`
- [x] All tests pass: `$ python run-tests.py --unit`
- [x] The documentation builds: `$ cd docs` and then `$ make clean; make html`

You can run all three at once, using `$ python run-tests.py --quick`.

## Further checks:

- [x] Code is commented, particularly in hard-to-understand areas
- [x] Tests added that prove fix is effective or that feature works


Patch:
diff --git a/.gitignore b/.gitignore
index 3eab70bba..b130bb928 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@
 !requirements*
 !LICENSE.txt
 !CMakeLists.txt
+!pybamm/CITATIONS.txt
 !pybamm/input/**/*.csv
 
 # running files
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c04e94b3..d7a91a959 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,8 @@
 -   Added capacitance effects to lithium-ion models ([#842](https://github.com/pybamm-team/PyBaMM/pull/842))
 -   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))
+-   Added `Citations` object to print references when specific functionality is used ([#818](https://github.com/pybamm-team/PyBaMM/pull/818))
+-   Updated `Solution` to allow exporting to matlab and csv formats ([#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))
diff --git a/README.md b/README.md
index e224145f5..d3e5b0831 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,32 @@ For instructions on installing PyBaMM on Mac OS distributions, please see [here]
 We recommend using Windows Subsystem for Linux to install PyBaMM on a Windows OS, for
 instructions please see [here](INSTALL-WINDOWS.md)
 
+## Citing PyBaMM
+
+If you use PyBaMM in your work, please cite our paper
+
+"Sulzer, V., Marquis, S. G., Timms, R., Robinson, M., & Chapman, S. J. (2020). Python Battery Mathematical Modelling (PyBaMM). _ECSarXiv. February, 7_."
+
+You can use the bibtex
+
+```
+@article{sulzer2020python,
+  title={Python Battery Mathematical Modelling (PyBaMM)},
+  author={Sulzer, Valentin and Marquis, Scott G and Timms, Robert and Robinson, Martin and Chapman, S Jon},
+  journal={ECSarXiv. February},
+  volume={7},
+  year={2020}
+}
+```
+
+We would be grateful if you could also cite the relevant papers. These will change depending on what models and solvers you use. To find out which papers you should cite, add the line
+
+```python3
+pybamm.print_citations()
+```
+
+to the end of your script. This will print bibtex information to the terminal; passing a filename to `print_citations` will print the bibtex information to the specified file instead. A list of all citations can also be found in the [citations file](pybamm/CITATIONS.txt). In particular, PyBaMM relies heavily on [CasADi](https://web.casadi.org/publications/).
+
 ## How can I contribute to PyBaMM?
 
 If you'd like to help us develop PyBaMM by adding new methods, writing documentation, or fixing embarrassing bugs, please have a look at these [guidelines](CONTRIBUTING.md) first.
diff --git a/docs/index.rst b/docs/index.rst
index 05ad15aea..ebbecaca6 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -34,6 +34,7 @@ Contents
     source/processed_variable
     source/util
     source/simulation
+    source/citations
     source/parameters_cli
 
 Examples
diff --git a/docs/source/citations.rst b/docs/source/citations.rst
new file mode 100644
index 000000000..cd560c9c2
--- /dev/null
+++ b/docs/source/citations.rst
@@ -0,0 +1,7 @@
+Citations
+=========
+
+.. autoclass:: pybamm.Citations
+  :members:
+
+.. autofunction:: pybamm.print_citations
diff --git a/examples/scripts/DFN.py b/examples/scripts/DFN.py
index d46f466fa..9155d9eaa 100644
--- a/examples/scripts/DFN.py
+++ b/examples/scripts/DFN.py
@@ -30,7 +30,7 @@ disc.process_model(model)
 
 # solve model
 t_eval = np.linspace(0, 3600, 100)
-solver = model.default_solver
+solver = pybamm.CasadiSolver()
 solver.rtol = 1e-3
 solver.atol = 1e-6
 solution = solver.solve(model, t_eval)
diff --git a/pybamm/CITATIONS.txt b/pybamm/CITATIONS.txt
new file mode 100644
index 000000000..8b77fb447
--- /dev/null
+++ b/pybamm/CITATIONS.txt
@@ -0,0 +1,125 @@
+@Article{Andersson2019,
+  author = {Joel A E Andersson and Joris Gillis and Greg Horn
+            and James B Rawlings and Moritz Diehl},
+  title = {{CasADi} -- {A} software framework for nonlinear optimization
+           and optimal control},
+  journal = {Mathematical Programming Computation},
+  volume = {11},
+  number = {1},
+  pages = {1--36},
+  year = {2019},
+  publisher = {Springer},
+  doi = {10.1007/s12532-018-0139-4}
+}
+
+@article{Chen2020,
+author = {Chen, Chang-Hui and Brosa Planella, Ferran and O'Regan, Kieran and Gastol, Dominika and Widanage, W. Dhammika and Kendrick, Emma},
+title = {{Development of Experimental Techniques for Parameterization of Multi-scale Lithium-ion Battery Models}},
+journal = {Submitted for publication},
+year = {2020}
+}
+
+@article{doyle1993modeling,
+  title={Modeling of galvanostatic charge and discharge of the lithium/polymer/insertion cell},
+  author={Doyle, M and Fuller, TF and Newman, J},
+  journal={Journal of the Electrochemical society},
+  volume={140},
+  number={6},
+  pages={1526--1533},
+  year={1993},
+  publisher={The Electrochemical Society},
+  note = {\href{ http://dx.doi.org/10.1149/1.2221597}{doi: 10.1149/1.2221597}},
+}
+
+@techreport{hindmarsh2000pvode,
+  title={The {PVODE} and {IDA} algorithms},
+  author={Hindmarsh, AC},
+  year={2000},
+  institution={Lawrence Livermore National Lab., CA (US)},
+  note = {\href{ https://www.osti.gov/biblio/802599}{doi:10.2172/802599}},
+}
+
+@article{hindmarsh2005sundials,
+  title={SUNDIALS: Suite of nonlinear and differential/algebraic equation solvers},
+  author={Hindmarsh, Alan C and Brown, Peter N and Grant, Keith E and Lee, Steven L and Serban, Radu and Shumaker, Dan E and Woodward, Carol S},
+  journal={ACM Transactions on Mathematical Software (TOMS)},
+  volume={31},
+  number={3},
+  pages={363--396},
+  year={2005},
+  publisher={ACM New York, NY, USA}
+}
+
+@article{kim2011multi,
+  title={Multi-domain modeling of lithium-ion batteries encompassing multi-physics in varied length scales},
+  author={Kim, Gi-Heon and Smith, Kandler and Lee, Kyu-Jin and Santhanagopalan, Shriram and Pesaran, Ahmad},
+  journal={Journal of the electrochemical society},
+  volume={158},
+  number={8},
+  pages={A955--A969},
+  year={2011},
+  publisher={The Electrochemical Society}
+}
+
+@article{marquis2019asymptotic,
+title={An asymptotic derivation of a single particle model with electrolyte},
+author={Marquis, Scott G and Sulzer, Valentin and Timms, Robert and Please, Colin P and Chapman, S Jon},
+journal={Journal of The Electrochemical Society},
+volume={166},
+number={15},
+pages={A3693--A3706},
+year={2019},
+publisher={The Electrochemical Society}
+}
+
+@article{scikits-odes,
+  year  = {2018},
+  month = {feb},
+  publisher = {The Open Journal},
+  volume = {3},
+  number = {22},
+  pages = {165},
+  author = {Malengier, B and Ki{\v{s}}on, P and Tocknell, J and Abert, C and Bruckner, F and Bisotti, M-A},
+  title = {{ODES}: a high level interface to {ODE} and {DAE} solvers},
+  journal = {The Journal of Open Source Software},
+  note = {\href{ http://dx.doi.org/10.21105/joss.00165}{doi: 10.21105/joss.00165}},
+}
+
+@article{virtanen2020scipy,
+  title={SciPy 1.0: fundamental algorithms for scientific computing in Python},
+  author={Virtanen, Pauli and Gommers, Ralf and Oliphant, Travis E and Haberland, Matt and Reddy, Tyler and Cournapeau, David and Burovski, Evgeni and Peterson, Pearu and Weckesser, Warren and Bright, Jonathan and others},
+  journal={Nature Methods},
+  pages={1--12},
+  year={2020},
+  publisher={Nature Publishing Group}
+}
+
+@article{sulzer2019physical,
+  title={Faster Lead-Acid Battery Simulations from Porous-Electrode Theory: Part I. Physical Model},
+  author={Sulzer, Valentin and Chapman, S Jon and Please, Colin P and Howey, David A and Monroe, Charles W},
+  journal={Journal of The Electrochemical Society},
+  volume={166},
+  number={12},
+  pages={A2363--A2371},
+  year={2019},
+  publisher={The Electrochemical Society}
+}
+
+@article{sulzer2019asymptotic,
+  title={Faster Lead-Acid Battery Simulations from Porous-Electrode Theory: Part II. Asymptotic Analysis},
+  author={Sulzer, Valentin and Chapman, S Jon and Please, Colin P and Howey, David A and Monroe, Charles W},
+  journal={Journal of The Electrochemical Society},
+  volume={166},
+  number={12},
+  pages={A2372--A2382},
+  year={2019},
+  publisher={The Electrochemical Society}
+}
+
+@article{sulzer2020python,
+title={Python Battery Mathematical Modelling (PyBaMM)},
+author={Sulzer, Valentin and Marquis, Scott G and Timms, Robert and Robinson, Martin and Chapman, S Jon},
+journal={ECSarXiv. February},
+volume={7},
+year={2020}
+}
diff --git a/pybamm/__init__.py b/pybamm/__init__.py
index c958da664..da9022e60 100644
--- a/pybamm/__init__.py
+++ b/pybamm/__init__.py
@@ -60,6 +60,7 @@ from .util import root_dir, load_function, rmse, get_infinite_nested_dict, load
 from .util import get_parameters_filepath
 from .logger import logger, set_logging_level
 from .settings import settings
+from .citations import Citations, citations, print_citations
 
 #
 # Classes for the Expression Tree
diff --git a/pybamm/citations.py b/pybamm/citations.py
new file mode 100644
index 000000000..c2e760868
--- /dev/null
+++ b/pybamm/citations.py
@@ -0,0 +1,100 @@
+#
+# Bibliographical information for PyBaMM
+# Inspired by firedrake/PETSc citation workflow
+# https://www.firedrakeproject.org/citing.html
+#
+import pybamm
+import os
+
+
+class Citations:
+
+    """Entry point to citations management.
+    This object may be used to record Bibtex citation information and then register that
+    a particular citation is relevant for a particular simulation. For a list of all
+    possible citations, see `pybamm/CITATIONS.txt`
+
+    Examples
+    --------
+    >>> import pybamm
+    >>> pybamm.citations.register("sulzer2020python")
+    >>> pybamm.print_citations("citations.txt")
+    """
+
+    def __init__(self):
+        self.read_citations()
+        self._reset()
+
+    def _reset(self):
+        "Reset citations to default only (only for testing purposes)"
+        # Initialize empty papers to cite
+        self._papers_to_cite = set()
+        # Register the PyBaMM paper
+        self.register("sulzer2020python")
+
+    def read_citations(self):
+        "Read the citations text file"
+        self._all_citations = {}
+
+        citations_file = os.path.join(pybamm.root_dir(), "pybamm", "CITATIONS.txt")
+        citation = ""
+        start = True
+
+        for line in open(citations_file):
+            # if start is true, we need to find the key
+            if start is True:
+                # match everything between { and , in the first line to get the key
+                brace_idx = line.find("{")
+                comma_idx = line.find(",")
+                key = line[brace_idx + 1 : comma_idx]
+                # turn off start as we now have the right key
+                start = False
+            citation += line
+            # blank line means next block, add citation to dictionary and
+            # reset everything
+            if line == "\n":
+                self._all_citations[key] = citation
+                citation = ""
+                start = True
+
+        # add the final citation
+        self._all_citations[key] = citation
+
+    def register(self, key):
+        """Register a paper to be cited. The intended use is that :meth:`register`
+        should be called only when the referenced functionality is actually being used.
+
+        Parameters
+        ----------
+        key : str
+            The key for the paper to be cited
+        """
+        if key not in self._all_citations:
+            raise KeyError("'{}' is not a known citation".format(key))
+        self._papers_to_cite.add(key)
+
+    def print(self, filename=None):
+        """Print all citations that were used for running simulations.
+
+        Parameters
+        ----------
+        filename : str, optional
+            Filename to which to print citations. If None, citations are printed to the
+            terminal.
+        """
+        citations = ""
+        for key in self._papers_to_cite:
+            citations += self._all_citations[key] + "\n"
+        if filename is None:
+            print(citations)
+        else:
+            with open(filename, "w") as f:
+                f.write(citations)
+
+
+def print_citations(filename=None):
+    "See :meth:`Citations.print`"
+    pybamm.citations.print(filename)
+
+
+citations = Citations()
diff --git a/pybamm/expression_tree/operations/convert_to_casadi.py b/pybamm/expression_tree/operations/convert_to_casadi.py
index a04aec5b0..5e89ab75c 100644
--- a/pybamm/expression_tree/operations/convert_to_casadi.py
+++ b/pybamm/expression_tree/operations/convert_to_casadi.py
@@ -11,6 +11,8 @@ class CasadiConverter(object):
     def __init__(self, casadi_symbols=None):
         self._casadi_symbols = casadi_symbols or {}
 
+        pybamm.citations.register("Andersson2019")
+
     def convert(self, symbol, t=None, y=None, u=None):
         """
         This function recurses down the tree, converting the PyBaMM expression tree to
diff --git a/pybamm/models/full_battery_models/lead_acid/full.py b/pybamm/models/full_battery_models/lead_acid/full.py
index 4e0a50855..331b26b99 100644
--- a/pybamm/models/full_battery_models/lead_acid/full.py
+++ b/pybamm/models/full_battery_models/lead_acid/full.py
@@ -49,6 +49,8 @@ class Full(BaseModel):
         if build:
             self.build_model()
 
+        pybamm.citations.register("sulzer2019physical")
+
     def set_porosity_submodel(self):
         self.submodels["porosity"] = pybamm.porosity.Full(self.param)
 
diff --git a/pybamm/models/full_battery_models/lead_acid/higher_order.py b/pybamm/models/full_battery_models/lead_acid/higher_order.py
index 7154583a0..4255856d3 100644
--- a/pybamm/models/full_battery_models/lead_acid/higher_order.py
+++ b/pybamm/models/full_battery_models/lead_acid/higher_order.py
@@ -57,6 +57,8 @@ class BaseHigherOrderModel(BaseModel):
         if build:
             self.build_model()
 
+        pybamm.citations.register("sulzer2019asymptotic")
+
     def set_current_collector_submodel(self):
         cc = pybamm.current_collector
 
diff --git a/pybamm/models/full_battery_models/lead_acid/loqs.py b/pybamm/models/full_battery_models/lead_acid/loqs.py
index fc5b0d66b..18cdb4ea2 100644
--- a/pybamm/models/full_battery_models/lead_acid/loqs.py
+++ b/pybamm/models/full_battery_models/lead_acid/loqs.py
@@ -52,6 +52,8 @@ class LOQS(BaseModel):
         if self.options["dimensionality"] == 0:
             self.use_jacobian = False
 
+        pybamm.citations.register("sulzer2019asymptotic")
+
     def set_external_circuit_submodel(self):
         """
         Define how the external circuit defines the boundary conditions for the model,
diff --git a/pybamm/models/full_battery_models/lithium_ion/dfn.py b/pybamm/models/full_battery_models/lithium_ion/dfn.py
index 582629c92..74bf6ae34 100644
--- a/pybamm/models/full_battery_models/lithium_ion/dfn.py
+++ b/pybamm/models/full_battery_models/lithium_ion/dfn.py
@@ -48,6 +48,8 @@ class DFN(BaseModel):
         if build:
             self.build_model()
 
+        pybamm.citations.register("doyle1993modeling")
+
     def set_porosity_submodel(self):
 
         self.submodels["porosity"] = pybamm.porosity.Constant(self.param)
diff --git a/pybamm/models/full_battery_models/lithium_ion/spm.py b/pybamm/models/full_battery_models/lithium_ion/spm.py
index 023b64bc7..127399bb6 100644
--- a/pybamm/models/full_battery_models/lithium_ion/spm.py
+++ b/pybamm/models/full_battery_models/lithium_ion/spm.py
@@ -48,6 +48,8 @@ class SPM(BaseModel):
         if build:
             self.build_model()
 
+        pybamm.citations.register("marquis2019asymptotic")
+
     def set_porosity_submodel(self):
 
         self.submodels["porosity"] = pybamm.porosity.Constant(self.param)
diff --git a/pybamm/models/full_battery_models/lithium_ion/spme.py b/pybamm/models/full_battery_models/lithium_ion/spme.py
index a400d0dff..18c5bf14d 100644
--- a/pybamm/models/full_battery_models/lithium_ion/spme.py
+++ b/pybamm/models/full_battery_models/lithium_ion/spme.py
@@ -51,6 +51,8 @@ class SPMe(BaseModel):
         if build:
             self.build_model()
 
+        pybamm.citations.register("marquis2019asymptotic")
+
     def set_porosity_submodel(self):
 
         self.submodels["porosity"] = pybamm.porosity.Constant(self.param)
diff --git a/pybamm/parameters/parameter_sets.py b/pybamm/parameters/parameter_sets.py
index f959ca36d..3b6747c68 100644
--- a/pybamm/parameters/parameter_sets.py
+++ b/pybamm/parameters/parameter_sets.py
@@ -2,35 +2,9 @@
 # Parameter sets from papers
 #
 """
-Parameter sets from papers:
-
-Lithium-ion
------------
-Marquis2019
-    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).
-
-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.
-
-Chen2020
-    Chang-Hui Chen, Ferran Brosa Planella, Kieran O’Regan, Dominika Gastol, W. Dhammika
-    Widanage, and Emma Kendrick. "Development of Experimental Techniques for
-    Parameterization of Multi-scale Lithium-ion Battery Models." Submitted for
-    publication (2020).
-
-
-Lead-acid
----------
-Sulzer2019
-    V. Sulzer, S. J. Chapman, C. P. Please, D. A. Howey, and C. W.Monroe, “Faster
-    lead-acid battery simulations from porous-electrode theory: Part I. Physical model.”
-    `Journal of the Electrochemical Society <https://doi.org/10.1149/2.0301910jes>`_,
-    166(12), 2363 (2019).
+Parameter sets from papers. The 'citation' entry provides a reference to the appropriate
+paper in the file "pybamm/CITATIONS.txt". To see which parameter sets have been used in
+your simulation, add the line "pybamm.print_citations()" to your script.
 """
 
 #
@@ -44,6 +18,7 @@ Marquis2019 = {
     "cathode": "lico2_Marquis2019",
     "electrolyte": "lipf6_Marquis2019",
     "experiment": "1C_discharge_from_full_Marquis2019",
+    "citation": "marquis2019asymptotic",
 }
 
 NCA_Kim2011 = {
@@ -54,6 +29,7 @@ NCA_Kim2011 = {
     "cathode": "nca_Kim2011",
     "electrolyte": "lipf6_Kim2011",
     "experiment": "1C_discharge_from_full_Kim2011",
+    "citation": "kim2011multi",
 }
 
 Chen2020 = {
@@ -64,6 +40,7 @@ Chen2020 = {
     "cathode": "nmc_Chen2020",
     "electrolyte": "lipf6_Nyman2008",
     "experiment": "1C_discharge_from_full_Chen2020",
+    "citation": "Chen2020",
 }
 #
 # Lead-acid
@@ -76,4 +53,5 @@ Sulzer2019 = {
     "cathode": "lead_dioxide_Sulzer2019",
     "electrolyte": "sulfuric_acid_Sulzer2019",
     "experiment": "1C_discharge_from_full",
+    "citation": "sulzer2019physical",
 }
diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py
index ad1dbd3a0..dd596e746 100644
--- a/pybamm/parameters/parameter_values.py
+++ b/pybamm/parameters/parameter_values.py
@@ -143,6 +143,11 @@ class ParameterValues:
                 path=component_path,
             )
 
+        # register citations
+        if "citation" in chemistry:
+            citation = chemistry["citation"]
+            pybamm.citations.register(citation)
+
     def read_parameters_csv(self, filename):
         """Reads parameters from csv file into dict.
 
diff --git a/pybamm/simulation.py b/pybamm/simulation.py
index 2983a34d4..2a5387fee 100644
--- a/pybamm/simulation.py
+++ b/pybamm/simulation.py
@@ -419,7 +419,11 @@ class Simulation:
                 # Make sure we take at least 2 timesteps
                 npts = max(int(round(dt / exp_inputs["period"])) + 1, 2)
                 self.step(
-                    dt, npts=npts, external_variables=external_variables, inputs=inputs
+                    dt,
+                    solver=solver,
+                    npts=npts,
+                    external_variables=external_variables,
+                    inputs=inputs,
                 )
                 # Only allow events specified by experiment
                 if not (
diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py
index 806015e90..648edf646 100644
--- a/pybamm/solvers/base_solver.py
+++ b/pybamm/solvers/base_solver.py
@@ -424,6 +424,8 @@ class BaseSolver(object):
                 method=self.root_method,
                 tol=self.root_tol,
             )
+            pybamm.citations.register("virtanen2020scipy")
+
             # Set outputs
             y0_alg = sol.x
             success = sol.success
diff --git a/pybamm/solvers/casadi_solver.py b/pybamm/solvers/casadi_solver.py
index 34fd74043..1029a8560 100644
--- a/pybamm/solvers/casadi_solver.py
+++ b/pybamm/solvers/casadi_solver.py
@@ -72,6 +72,8 @@ class CasadiSolver(pybamm.BaseSolver):
         self.options = {}
         self.methods = {}
 
+        pybamm.citations.register("Andersson2019")
+
     def _integrate(self, model, t_eval, inputs=None):
         """
         Solve a DAE model defined by residuals with initial conditions y0.
diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py
index be4a1932c..138524b5a 100644
--- a/pybamm/solvers/idaklu_solver.py
+++ b/pybamm/solvers/idaklu_solver.py
@@ -45,6 +45,9 @@ class IDAKLUSolver(pybamm.BaseSolver):
         super().__init__("ida", rtol, atol, root_method, root_tol, max_steps)
         self.name = "IDA KLU solver"
 
+        pybamm.citations.register("hindmarsh2000pvode")
+        pybamm.citations.register("hindmarsh2005sundials")
+
     def set_atol_by_variable(self, variables_with_tols, model):
         """
         A method to set the absolute tolerances in the solver by state variable.
diff --git a/pybamm/solvers/scikits_dae_solver.py b/pybamm/solvers/scikits_dae_solver.py
index 74620cafe..65f06caf0 100644
--- a/pybamm/solvers/scikits_dae_solver.py
+++ b/pybamm/solvers/scikits_dae_solver.py
@@ -50,6 +50,10 @@ class ScikitsDaeSolver(pybamm.BaseSolver):
         super().__init__(method, rtol, atol, root_method, root_tol, max_steps)
         self.name = "Scikits DAE solver ({})".format(method)
 
+        pybamm.citations.register("scikits-odes")
+        pybamm.citations.register("hindmarsh2000pvode")
+        pybamm.citations.register("hindmarsh2005sundials")
+
     def _integrate(self, model, t_eval, inputs=None):
         """
         Solve a model defined by dydt with initial conditions y0.
diff --git a/pybamm/solvers/scikits_ode_solver.py b/pybamm/solvers/scikits_ode_solver.py
index 196c05e97..b8bf57854 100644
--- a/pybamm/solvers/scikits_ode_solver.py
+++ b/pybamm/solvers/scikits_ode_solver.py
@@ -43,6 +43,10 @@ class ScikitsOdeSolver(pybamm.BaseSolver):
         self.ode_solver = True
         self.name = "Scikits ODE solver ({})".format(method)
 
+        pybamm.citations.register("scikits-odes")
+        pybamm.citations.register("hindmarsh2000pvode")
+        pybamm.citations.register("hindmarsh2005sundials")
+
     def _integrate(self, model, t_eval, inputs=None):
         """
         Solve a model defined by dydt with initial conditions y0.
diff --git a/pybamm/solvers/scipy_solver.py b/pybamm/solvers/scipy_solver.py
index 100ecf859..980332183 100644
--- a/pybamm/solvers/scipy_solver.py
+++ b/pybamm/solvers/scipy_solver.py
@@ -24,6 +24,7 @@ class ScipySolver(pybamm.BaseSolver):
         super().__init__(method, rtol, atol)
         self.ode_solver = True
         self.name = "Scipy solver ({})".format(method)
+        pybamm.citations.register("virtanen2020scipy")
 
     def _integrate(self, model, t_eval, inputs=None):
         """


Test patch:
diff --git a/tests/unit/test_citations.py b/tests/unit/test_citations.py
new file mode 100644
index 000000000..cb7a064c7
--- /dev/null
+++ b/tests/unit/test_citations.py
@@ -0,0 +1,130 @@
+#
+# Tests the citations class.
+#
+import pybamm
+import unittest
+
+
+class TestCitations(unittest.TestCase):
+    def test_citations(self):
+        citations = pybamm.citations
+        citations._reset()
+        # Default papers should be in both _all_citations dict and in the papers to cite
+        self.assertIn("sulzer2020python", citations._all_citations.keys())
+        self.assertIn("sulzer2020python", citations._papers_to_cite)
+        # Non-default papers should only be in the _all_citations dict
+        self.assertIn("sulzer2019physical", citations._all_citations.keys())
+        self.assertNotIn("sulzer2019physical", citations._papers_to_cite)
+
+        # test key error
+        with self.assertRaises(KeyError):
+            citations.register("not a citation")
+
+    def test_print_citations(self):
+        pybamm.citations._reset()
+        pybamm.print_citations("test_citations.txt")
+        pybamm.citations._papers_to_cite = set()
+        pybamm.print_citations()
+
+    def test_Andersson_2019(self):
+        citations = pybamm.citations
+        citations._reset()
+        self.assertNotIn("Andersson2019", citations._papers_to_cite)
+        pybamm.CasadiConverter()
+        self.assertIn("Andersson2019", citations._papers_to_cite)
+
+    def test_marquis_2019(self):
+        # Test that calling relevant bits of code adds the right paper to citations
+        citations = pybamm.citations
+
+        citations._reset()
+        self.assertNotIn("marquis2019asymptotic", citations._papers_to_cite)
+        pybamm.lithium_ion.SPM(build=False)
+        self.assertIn("marquis2019asymptotic", citations._papers_to_cite)
+
+        citations._reset()
+        pybamm.lithium_ion.SPMe(build=False)
+        self.assertIn("marquis2019asymptotic", citations._papers_to_cite)
+
+    def test_doyle_1993(self):
+        citations = pybamm.citations
+        citations._reset()
+        self.assertNotIn("doyle1993modeling", citations._papers_to_cite)
+        pybamm.lithium_ion.DFN(build=False)
+        self.assertIn("doyle1993modeling", citations._papers_to_cite)
+
+    def test_sulzer_2019(self):
+        # Test that calling relevant bits of code adds the right paper to citations
+        citations = pybamm.citations
+
+        citations._reset()
+        self.assertNotIn("sulzer2019asymptotic", citations._papers_to_cite)
+        pybamm.lead_acid.LOQS(build=False)
+        self.assertIn("sulzer2019asymptotic", citations._papers_to_cite)
+
+        citations._reset()
+        pybamm.lead_acid.FOQS(build=False)
+        self.assertIn("sulzer2019asymptotic", citations._papers_to_cite)
+
+        citations._reset()
+        pybamm.lead_acid.Composite(build=False)
+        self.assertIn("sulzer2019asymptotic", citations._papers_to_cite)
+
+        citations._reset()
+        pybamm.lead_acid.Full(build=False)
+        self.assertIn("sulzer2019physical", citations._papers_to_cite)
+
+    def test_parameter_citations(self):
+        citations = pybamm.citations
+
+        citations._reset()
+        pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Chen2020)
+        self.assertIn("Chen2020", citations._papers_to_cite)
+
+        citations._reset()
+        pybamm.ParameterValues(chemistry=pybamm.parameter_sets.NCA_Kim2011)
+        self.assertIn("kim2011multi", citations._papers_to_cite)
+
+        citations._reset()
+        pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Marquis2019)
+        self.assertIn("marquis2019asymptotic", citations._papers_to_cite)
+
+        citations._reset()
+        pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Sulzer2019)
+        self.assertIn("sulzer2019physical", citations._papers_to_cite)
+
+    def test_solver_citations(self):
+        # Test that solving each solver adds the right citations
+        citations = pybamm.citations
+
+        citations._reset()
+        self.assertNotIn("virtanen2020scipy", citations._papers_to_cite)
+        pybamm.ScipySolver()
+        self.assertIn("virtanen2020scipy", citations._papers_to_cite)
+
+        if pybamm.have_scikits_odes():
+            citations._reset()
+            self.assertNotIn("scikits-odes", citations._papers_to_cite)
+            pybamm.ScikitsOdeSolver()
+            self.assertIn("scikits-odes", citations._papers_to_cite)
+
+            citations._reset()
+            self.assertNotIn("scikits-odes", citations._papers_to_cite)
+            pybamm.ScikitsDaeSolver()
+            self.assertIn("scikits-odes", citations._papers_to_cite)
+
+        if pybamm.have_idaklu():
+            citations._reset()
+            self.assertNotIn("hindmarsh2005sundials", citations._papers_to_cite)
+            pybamm.IDAKLUSolver()
+            self.assertIn("hindmarsh2005sundials", citations._papers_to_cite)
+
+
+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()