diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 784ccebc4f..efc8b23141 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 + uses: github/codeql-action/upload-sarif@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 with: sarif_file: results.sarif diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 43928bbc56..2135fa3c71 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.6.3" + rev: "v0.6.5" hooks: - id: ruff args: [--fix, --show-fixes] diff --git a/CHANGELOG.md b/CHANGELOG.md index 7764a3b24a..80cda39db7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,20 @@ # [Unreleased](https://github.com/pybamm-team/PyBaMM/) +## Features +- Added sensitivity calculation support for `pybamm.Simulation` and `pybamm.Experiment` ([#4415](https://github.com/pybamm-team/PyBaMM/pull/4415)) + ## Optimizations - Removed the `start_step_offset` setting and disabled minimum `dt` warnings for drive cycles with the (`IDAKLUSolver`). ([#4416](https://github.com/pybamm-team/PyBaMM/pull/4416)) +## Features + +- Added phase-dependent particle options to LAM #4369 + ## Breaking changes +- The parameters "... electrode OCP entropic change [V.K-1]" and "... electrode volume change" are now expected to be functions of stoichiometry only instead of functions of both stoichiometry and maximum concentration ([#4427](https://github.com/pybamm-team/PyBaMM/pull/4427)) - Renamed `set_events` function to `add_events_from` to better reflect its purpose. ([#4421](https://github.com/pybamm-team/PyBaMM/pull/4421)) + # [v24.9.0](https://github.com/pybamm-team/PyBaMM/tree/v24.9.0) - 2024-09-03 ## Features diff --git a/docs/source/examples/notebooks/models/loss_of_active_materials.ipynb b/docs/source/examples/notebooks/models/loss_of_active_materials.ipynb index ad428e6791..1ce1cca826 100644 --- a/docs/source/examples/notebooks/models/loss_of_active_materials.ipynb +++ b/docs/source/examples/notebooks/models/loss_of_active_materials.ipynb @@ -30,7 +30,7 @@ "output_type": "stream", "text": [ "At t = 57.3387, , mxstep steps taken before reaching tout.\n", - "At t = 57.3387 and h = 7.05477e-15, the corrector convergence failed repeatedly or with |h| = hmin.\n", + "At t = 57.3387, , mxstep steps taken before reaching tout.\n", "At t = 57.3387, , mxstep steps taken before reaching tout.\n", "At t = 57.3387, , mxstep steps taken before reaching tout.\n" ] @@ -83,12 +83,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "6b19474c3912495eb75217e009760637", + "model_id": "ccfc7ae873d1492197fa7b554339a3d7", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=2.329196798170269, step=0.02329196798170269)…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=2.3291967981693755, step=0.02329196798169375…" ] }, "metadata": {}, @@ -97,7 +97,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 2, @@ -137,11 +137,11 @@ "output_type": "stream", "text": [ "At t = 57.3387, , mxstep steps taken before reaching tout.\n", - "At t = 57.3387 and h = 7.05477e-15, the corrector convergence failed repeatedly or with |h| = hmin.\n", + "At t = 57.3387, , mxstep steps taken before reaching tout.\n", "At t = 57.3387, , mxstep steps taken before reaching tout.\n", "At t = 57.3387, , mxstep steps taken before reaching tout.\n", "At t = 57.3307, , mxstep steps taken before reaching tout.\n", - "At t = 57.3307, , mxstep steps taken before reaching tout.\n", + "At t = 57.3307 and h = 3.45325e-14, the corrector convergence failed repeatedly or with |h| = hmin.\n", "At t = 57.3307, , mxstep steps taken before reaching tout.\n", "At t = 57.3307, , mxstep steps taken before reaching tout.\n", "At t = 57.2504, , mxstep steps taken before reaching tout.\n", @@ -153,12 +153,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "789a681c8c574bb8b3d3016a844dd9a2", + "model_id": "b34472112ae344da92ccc8af5178c64b", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=2.329196798170269, step=0.02329196798170269)…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=2.3291967981693755, step=0.02329196798169375…" ] }, "metadata": {}, @@ -167,7 +167,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 3, @@ -225,12 +225,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ad36439975754b29bbbef1bd94379408", + "model_id": "60db1d0de494460493cc8edd5b61d4e7", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=1.8531298311682403, step=0.01853129831168240…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=1.85353350947348, step=0.0185353350947348), …" ] }, "metadata": {}, @@ -239,7 +239,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 4, @@ -248,6 +248,16 @@ } ], "source": [ + "import pybamm\n", + "\n", + "experiment = pybamm.Experiment(\n", + " [\n", + " \"Discharge at 1C until 3 V\",\n", + " \"Rest for 600 seconds\",\n", + " \"Charge at 1C until 4.2 V\",\n", + " \"Hold at 4.199 V for 600 seconds\",\n", + " ]\n", + ")\n", "model = pybamm.lithium_ion.DFN(\n", " options={\n", " \"SEI\": \"solvent-diffusion limited\",\n", @@ -255,7 +265,7 @@ " }\n", ")\n", "param = pybamm.ParameterValues(\"Chen2020\")\n", - "param.update({\"Negative electrode reaction-driven LAM factor [m3.mol-1]\": 1e-3})\n", + "param.update({\"Negative electrode reaction-driven LAM factor [m3.mol-1]\": 1e-4})\n", "sim = pybamm.Simulation(\n", " model,\n", " experiment=experiment,\n", @@ -300,12 +310,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "91ea043e10d342049929095e48e98c5e", + "model_id": "1dfb1de5ccde449c9eefcda1b1f44468", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=1.8506629989989005, step=0.01850662998998900…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=1.8506629988943608, step=0.01850662998894360…" ] }, "metadata": {}, @@ -314,7 +324,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 5, @@ -358,6 +368,297 @@ ")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## LAM with composite electrode\n", + "The LAM submodel is also compatible with multiple phases within an electrode for both stress- and reaction-driven loss of active material. Currently, there is no single parameter set that combines both LAM degradation and composite materials. The following examples use the Chen2020 composite parameter set with LAM parameters taken from the Ai2020 parameter set. " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# Volume change functions from Ai2020 parameters\n", + "\n", + "\n", + "def graphite_volume_change_Ai2020(sto):\n", + " p1 = 145.907\n", + " p2 = -681.229\n", + " p3 = 1334.442\n", + " p4 = -1415.710\n", + " p5 = 873.906\n", + " p6 = -312.528\n", + " p7 = 60.641\n", + " p8 = -5.706\n", + " p9 = 0.386\n", + " p10 = -4.966e-05\n", + " t_change = (\n", + " p1 * sto**9\n", + " + p2 * sto**8\n", + " + p3 * sto**7\n", + " + p4 * sto**6\n", + " + p5 * sto**5\n", + " + p6 * sto**4\n", + " + p7 * sto**3\n", + " + p8 * sto**2\n", + " + p9 * sto\n", + " + p10\n", + " )\n", + " return t_change\n", + "\n", + "\n", + "def lico2_volume_change_Ai2020(sto):\n", + " omega = pybamm.Parameter(\"Positive electrode partial molar volume [m3.mol-1]\")\n", + " c_s_max = pybamm.Parameter(\"Maximum concentration in positive electrode [mol.m-3]\")\n", + " t_change = omega * c_s_max * sto\n", + " return t_change" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Stress-driven composite anode\n", + "The secondary phase LAM parameters have been adjusted from the Ai2020 by about 10% to show less degradation in that phase. The model is set up in the same way the single-phase simulation is but with additional parameters." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "options = {\n", + " \"particle phases\": (\"2\", \"1\"),\n", + " \"open-circuit potential\": ((\"single\", \"current sigmoid\"), \"single\"),\n", + " \"loss of active material\": \"stress-driven\",\n", + "}\n", + "\n", + "model = pybamm.lithium_ion.SPM(options)\n", + "parameter_values = pybamm.ParameterValues(\"Chen2020_composite\")\n", + "second = 0.1\n", + "parameter_values.update(\n", + " {\n", + " \"Primary: Negative electrode LAM constant proportional term [s-1]\": 1e-4 / 3600,\n", + " \"Secondary: Negative electrode LAM constant proportional term [s-1]\": 1e-4\n", + " / 3600\n", + " * second,\n", + " \"Positive electrode LAM constant proportional term [s-1]\": 1e-4 / 3600,\n", + " \"Primary: Negative electrode partial molar volume [m3.mol-1]\": 3.1e-06,\n", + " \"Primary: Negative electrode Young's modulus [Pa]\": 15000000000.0,\n", + " \"Primary: Negative electrode Poisson's ratio\": 0.3,\n", + " \"Primary: Negative electrode critical stress [Pa]\": 60000000.0,\n", + " \"Secondary: Negative electrode critical stress [Pa]\": 60000000.0,\n", + " \"Primary: Negative electrode LAM constant exponential term\": 2.0,\n", + " \"Secondary: Negative electrode LAM constant exponential term\": 2.0,\n", + " \"Secondary: Negative electrode partial molar volume [m3.mol-1]\": 3.1e-06\n", + " * second,\n", + " \"Secondary: Negative electrode Young's modulus [Pa]\": 15000000000.0 * second,\n", + " \"Secondary: Negative electrode Poisson's ratio\": 0.3 * second,\n", + " \"Negative electrode reference concentration for free of deformation [mol.m-3]\": 0.0,\n", + " \"Primary: Negative electrode volume change\": graphite_volume_change_Ai2020,\n", + " \"Secondary: Negative electrode volume change\": graphite_volume_change_Ai2020,\n", + " \"Positive electrode partial molar volume [m3.mol-1]\": -7.28e-07,\n", + " \"Positive electrode Young's modulus [Pa]\": 375000000000.0,\n", + " \"Positive electrode Poisson's ratio\": 0.2,\n", + " \"Positive electrode critical stress [Pa]\": 375000000.0,\n", + " \"Positive electrode LAM constant exponential term\": 2.0,\n", + " \"Positive electrode reference concentration for free of deformation [mol.m-3]\": 0.0,\n", + " \"Positive electrode volume change\": lico2_volume_change_Ai2020,\n", + " },\n", + " check_already_exists=False,\n", + ")\n", + "\n", + "# sim = pybamm.Simulation(model, parameter_values=parameter_values)\n", + "# sim.solve([0, 4500])\n", + "experiment = pybamm.Experiment(\n", + " [\n", + " \"Discharge at 1C until 3 V\",\n", + " \"Rest for 600 seconds\",\n", + " \"Charge at 1C until 4.2 V\",\n", + " \"Hold at 4.199 V for 600 seconds\",\n", + " ]\n", + ")\n", + "sim = pybamm.Simulation(\n", + " model,\n", + " experiment=experiment,\n", + " parameter_values=parameter_values,\n", + " discretisation_kwargs={\"remove_independent_variables_from_rhs\": True},\n", + ")\n", + "solution = sim.solve(calc_esoh=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The two phase LAM model can be compared between the cathode and two anode phases." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "074bcadceb3e4fbd8cc786e798bb6508", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(FloatSlider(value=0.0, description='t', max=2.1702864080208446, step=0.02170286408020844…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pybamm.dynamic_plot(\n", + " sim,\n", + " [\n", + " \"Voltage [V]\",\n", + " \"Current [A]\",\n", + " [\n", + " \"Average negative primary particle concentration\",\n", + " \"Average negative secondary particle concentration\",\n", + " \"Average positive particle concentration\",\n", + " ],\n", + " \"X-averaged negative electrode primary active material volume fraction\",\n", + " \"X-averaged positive electrode active material volume fraction\",\n", + " \"X-averaged negative electrode secondary active material volume fraction\",\n", + " \"Sum of x-averaged positive electrode volumetric interfacial current densities [A.m-3]\",\n", + " \"Sum of x-averaged negative electrode volumetric interfacial current densities [A.m-3]\",\n", + " \"X-averaged positive particle surface tangential stress [Pa]\",\n", + " \"X-averaged negative primary particle surface tangential stress [Pa]\",\n", + " \"X-averaged negative secondary particle surface tangential stress [Pa]\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Reaction-driven composite anode\n", + "The same process is repeated for the reaction-driven LAM degradation." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "98a2b1762a3c43bcaa9ceff5a146d704", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(FloatSlider(value=0.0, description='t', max=2.081773444877257, step=0.02081773444877257)…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "options = {\n", + " \"particle phases\": (\"2\", \"1\"),\n", + " \"open-circuit potential\": ((\"single\", \"current sigmoid\"), \"single\"),\n", + " \"SEI\": \"solvent-diffusion limited\",\n", + " \"loss of active material\": \"reaction-driven\",\n", + "}\n", + "\n", + "model = pybamm.lithium_ion.SPM(options)\n", + "parameter_values = pybamm.ParameterValues(\"Chen2020_composite\")\n", + "second = 0.9\n", + "\n", + "parameter_values.update(\n", + " {\n", + " \"Primary: Negative electrode partial molar volume [m3.mol-1]\": 3.1e-06,\n", + " \"Primary: Negative electrode Young's modulus [Pa]\": 15000000000.0,\n", + " \"Primary: Negative electrode Poisson's ratio\": 0.3,\n", + " \"Negative electrode critical stress [Pa]\": 60000000.0,\n", + " \"Negative electrode LAM constant exponential term\": 2.0,\n", + " \"Secondary: Negative electrode partial molar volume [m3.mol-1]\": 3.1e-06\n", + " * second,\n", + " \"Secondary: Negative electrode Young's modulus [Pa]\": 15000000000.0 * second,\n", + " \"Secondary: Negative electrode Poisson's ratio\": 0.3 * second,\n", + " \"Negative electrode reference concentration for free of deformation [mol.m-3]\": 0.0,\n", + " \"Primary: Negative electrode volume change\": graphite_volume_change_Ai2020,\n", + " \"Secondary: Negative electrode volume change\": graphite_volume_change_Ai2020,\n", + " \"Positive electrode partial molar volume [m3.mol-1]\": -7.28e-07,\n", + " \"Positive electrode Young's modulus [Pa]\": 375000000000.0,\n", + " \"Positive electrode Poisson's ratio\": 0.2,\n", + " \"Positive electrode critical stress [Pa]\": 375000000.0,\n", + " \"Positive electrode LAM constant exponential term\": 2.0,\n", + " \"Positive electrode reference concentration for free of deformation [mol.m-3]\": 0.0,\n", + " \"Positive electrode volume change\": lico2_volume_change_Ai2020,\n", + " \"Primary: Negative electrode reaction-driven LAM factor [m3.mol-1]\": 1e-9,\n", + " \"Secondary: Negative electrode reaction-driven LAM factor [m3.mol-1]\": 10,\n", + " },\n", + " check_already_exists=False,\n", + ")\n", + "\n", + "# Changing secondary SEI solvent diffusivity to show different degradation between phases\n", + "parameter_values.update(\n", + " {\n", + " \"Secondary: Outer SEI solvent diffusivity [m2.s-1]\": 2.5000000000000002e-24,\n", + " }\n", + ")\n", + "\n", + "# sim = pybamm.Simulation(model, parameter_values=parameter_values)\n", + "# sim.solve([0, 4100])\n", + "sim = pybamm.Simulation(\n", + " model,\n", + " experiment=experiment,\n", + " parameter_values=parameter_values,\n", + " solver=pybamm.CasadiSolver(\"fast with events\"),\n", + ")\n", + "solution = sim.solve(calc_esoh=False)\n", + "\n", + "sim.plot(\n", + " [\n", + " \"Voltage [V]\",\n", + " \"Current [A]\",\n", + " \"Sum of x-averaged negative electrode volumetric interfacial current densities [A.m-3]\",\n", + " \"X-averaged negative electrode primary active material volume fraction\",\n", + " \"X-averaged negative electrode secondary active material volume fraction\",\n", + " \"Negative total primary SEI thickness [m]\",\n", + " \"Negative total secondary SEI thickness [m]\",\n", + " ]\n", + ")" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -369,22 +670,26 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[1] Weilong Ai, Ludwig Kraft, Johannes Sturm, Andreas Jossen, and Billy Wu. Electrochemical thermal-mechanical modelling of stress inhomogeneity in lithium-ion pouch cells. Journal of The Electrochemical Society, 167(1):013512, 2019. doi:10.1149/2.0122001JES.\n", - "[2] Joel A. E. Andersson, Joris Gillis, Greg Horn, James B. Rawlings, and Moritz Diehl. CasADi – A software framework for nonlinear optimization and optimal control. Mathematical Programming Computation, 11(1):1–36, 2019. doi:10.1007/s12532-018-0139-4.\n", - "[3] 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. Journal of The Electrochemical Society, 167(8):080534, 2020. doi:10.1149/1945-7111/ab9050.\n", - "[4] Rutooj Deshpande, Mark Verbrugge, Yang-Tse Cheng, John Wang, and Ping Liu. Battery cycle life prediction with coupled chemical degradation and fatigue mechanics. Journal of the Electrochemical Society, 159(10):A1730, 2012. doi:10.1149/2.049210jes.\n", - "[5] Marc Doyle, Thomas F. Fuller, and John Newman. Modeling of galvanostatic charge and discharge of the lithium/polymer/insertion cell. Journal of the Electrochemical society, 140(6):1526–1533, 1993. doi:10.1149/1.2221597.\n", - "[6] Charles R. Harris, K. Jarrod Millman, Stéfan J. van der Walt, Ralf Gommers, Pauli Virtanen, David Cournapeau, Eric Wieser, Julian Taylor, Sebastian Berg, Nathaniel J. Smith, and others. Array programming with NumPy. Nature, 585(7825):357–362, 2020. doi:10.1038/s41586-020-2649-2.\n", - "[7] Scott G. Marquis. Long-term degradation of lithium-ion batteries. PhD thesis, University of Oxford, 2020.\n", - "[8] Jorn M. Reniers, Grietus Mulder, and David A. Howey. Review and performance comparison of mechanical-chemical degradation models for lithium-ion batteries. Journal of The Electrochemical Society, 166(14):A3189, 2019. doi:10.1149/2.0281914jes.\n", - "[9] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.\n", + "[1] Weilong Ai, Niall Kirkaldy, Yang Jiang, Gregory Offer, Huizhi Wang, and Billy Wu. A composite electrode model for lithium-ion batteries with silicon/graphite negative electrodes. Journal of Power Sources, 527:231142, 2022. URL: https://www.sciencedirect.com/science/article/pii/S0378775322001604, doi:https://doi.org/10.1016/j.jpowsour.2022.231142.\n", + "[2] Weilong Ai, Ludwig Kraft, Johannes Sturm, Andreas Jossen, and Billy Wu. Electrochemical thermal-mechanical modelling of stress inhomogeneity in lithium-ion pouch cells. Journal of The Electrochemical Society, 167(1):013512, 2019. doi:10.1149/2.0122001JES.\n", + "[3] Joel A. E. Andersson, Joris Gillis, Greg Horn, James B. Rawlings, and Moritz Diehl. CasADi – A software framework for nonlinear optimization and optimal control. Mathematical Programming Computation, 11(1):1–36, 2019. doi:10.1007/s12532-018-0139-4.\n", + "[4] Ferran Brosa Planella and W. Dhammika Widanage. Systematic derivation of a Single Particle Model with Electrolyte and Side Reactions (SPMe+SR) for degradation of lithium-ion batteries. Submitted for publication, ():, 2022. doi:.\n", + "[5] Von DAG Bruggeman. Berechnung verschiedener physikalischer konstanten von heterogenen substanzen. i. dielektrizitätskonstanten und leitfähigkeiten der mischkörper aus isotropen substanzen. Annalen der physik, 416(7):636–664, 1935.\n", + "[6] 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. Journal of The Electrochemical Society, 167(8):080534, 2020. doi:10.1149/1945-7111/ab9050.\n", + "[7] Rutooj Deshpande, Mark Verbrugge, Yang-Tse Cheng, John Wang, and Ping Liu. Battery cycle life prediction with coupled chemical degradation and fatigue mechanics. Journal of the Electrochemical Society, 159(10):A1730, 2012. doi:10.1149/2.049210jes.\n", + "[8] Marc Doyle, Thomas F. Fuller, and John Newman. Modeling of galvanostatic charge and discharge of the lithium/polymer/insertion cell. Journal of the Electrochemical society, 140(6):1526–1533, 1993. doi:10.1149/1.2221597.\n", + "[9] Charles R. Harris, K. Jarrod Millman, Stéfan J. van der Walt, Ralf Gommers, Pauli Virtanen, David Cournapeau, Eric Wieser, Julian Taylor, Sebastian Berg, Nathaniel J. Smith, and others. Array programming with NumPy. Nature, 585(7825):357–362, 2020. doi:10.1038/s41586-020-2649-2.\n", + "[10] Scott G. Marquis. Long-term degradation of lithium-ion batteries. PhD thesis, University of Oxford, 2020.\n", + "[11] Scott G. Marquis, Valentin Sulzer, Robert Timms, Colin P. Please, and S. Jon Chapman. An asymptotic derivation of a single particle model with electrolyte. Journal of The Electrochemical Society, 166(15):A3693–A3706, 2019. doi:10.1149/2.0341915jes.\n", + "[12] Jorn M. Reniers, Grietus Mulder, and David A. Howey. Review and performance comparison of mechanical-chemical degradation models for lithium-ion batteries. Journal of The Electrochemical Society, 166(14):A3189, 2019. doi:10.1149/2.0281914jes.\n", + "[13] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.\n", "\n" ] } @@ -417,7 +722,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.11.9" }, "toc": { "base_numbering": 1, diff --git a/docs/source/examples/notebooks/parameterization/parameterization.ipynb b/docs/source/examples/notebooks/parameterization/parameterization.ipynb index dabb5e5f76..a6ff34c772 100644 --- a/docs/source/examples/notebooks/parameterization/parameterization.ipynb +++ b/docs/source/examples/notebooks/parameterization/parameterization.ipynb @@ -586,7 +586,7 @@ "outputs": [ { "data": { - "text/plain": "{'Ideal gas constant [J.K-1.mol-1]': 8.314462618,\n 'Faraday constant [C.mol-1]': 96485.33212,\n 'Negative electrode thickness [m]': 0.0001,\n 'Separator thickness [m]': 2.5e-05,\n 'Positive electrode thickness [m]': 0.0001,\n 'Electrode height [m]': 0.137,\n 'Electrode width [m]': 0.207,\n 'Nominal cell capacity [A.h]': 0.680616,\n 'Current function [A]': 0.680616,\n 'Maximum concentration in negative electrode [mol.m-3]': 24983.2619938437,\n 'Negative particle diffusivity [m2.s-1]': ,\n 'Negative electrode OCP [V]': ,\n 'Negative electrode porosity': 0.3,\n 'Negative electrode active material volume fraction': 0.6,\n 'Negative particle radius [m]': 1e-05,\n 'Negative electrode Bruggeman coefficient (electrolyte)': 1.5,\n 'Negative electrode Bruggeman coefficient (electrode)': 1.5,\n 'Negative electrode exchange-current density [A.m-2]': ,\n 'Negative electrode OCP entropic change [V.K-1]': ,\n 'Maximum concentration in positive electrode [mol.m-3]': 51217.9257309275,\n 'Positive particle diffusivity [m2.s-1]': ,\n 'Positive electrode OCP [V]': ,\n 'Positive electrode porosity': 0.3,\n 'Positive electrode active material volume fraction': 0.5,\n 'Positive particle radius [m]': 1e-05,\n 'Positive electrode Bruggeman coefficient (electrolyte)': 1.5,\n 'Positive electrode Bruggeman coefficient (electrode)': 1.5,\n 'Positive electrode exchange-current density [A.m-2]': ,\n 'Positive electrode OCP entropic change [V.K-1]': ,\n 'Separator porosity': 1.0,\n 'Separator Bruggeman coefficient (electrolyte)': 1.5,\n 'Initial concentration in electrolyte [mol.m-3]': 1000.0,\n 'Reference temperature [K]': 298.15,\n 'Ambient temperature [K]': 298.15,\n 'Number of electrodes connected in parallel to make a cell': 1.0,\n 'Number of cells connected in series to make a battery': 1.0,\n 'Lower voltage cut-off [V]': 3.105,\n 'Upper voltage cut-off [V]': 4.1,\n 'Initial concentration in negative electrode [mol.m-3]': 19986.609595075,\n 'Initial concentration in positive electrode [mol.m-3]': 30730.7554385565}" + "text/plain": "{'Ideal gas constant [J.K-1.mol-1]': 8.314462618,\n 'Faraday constant [C.mol-1]': 96485.33212,\n 'Negative electrode thickness [m]': 0.0001,\n 'Separator thickness [m]': 2.5e-05,\n 'Positive electrode thickness [m]': 0.0001,\n 'Electrode height [m]': 0.137,\n 'Electrode width [m]': 0.207,\n 'Nominal cell capacity [A.h]': 0.680616,\n 'Current function [A]': 0.680616,\n 'Maximum concentration in negative electrode [mol.m-3]': 24983.2619938437,\n 'Negative particle diffusivity [m2.s-1]': ,\n 'Negative electrode OCP [V]': ,\n 'Negative electrode porosity': 0.3,\n 'Negative electrode active material volume fraction': 0.6,\n 'Negative particle radius [m]': 1e-05,\n 'Negative electrode Bruggeman coefficient (electrolyte)': 1.5,\n 'Negative electrode Bruggeman coefficient (electrode)': 1.5,\n 'Negative electrode exchange-current density [A.m-2]': ,\n 'Negative electrode OCP entropic change [V.K-1]': ,\n 'Maximum concentration in positive electrode [mol.m-3]': 51217.9257309275,\n 'Positive particle diffusivity [m2.s-1]': ,\n 'Positive electrode OCP [V]': ,\n 'Positive electrode porosity': 0.3,\n 'Positive electrode active material volume fraction': 0.5,\n 'Positive particle radius [m]': 1e-05,\n 'Positive electrode Bruggeman coefficient (electrolyte)': 1.5,\n 'Positive electrode Bruggeman coefficient (electrode)': 1.5,\n 'Positive electrode exchange-current density [A.m-2]': ,\n 'Positive electrode OCP entropic change [V.K-1]': ,\n 'Separator porosity': 1.0,\n 'Separator Bruggeman coefficient (electrolyte)': 1.5,\n 'Initial concentration in electrolyte [mol.m-3]': 1000.0,\n 'Reference temperature [K]': 298.15,\n 'Ambient temperature [K]': 298.15,\n 'Number of electrodes connected in parallel to make a cell': 1.0,\n 'Number of cells connected in series to make a battery': 1.0,\n 'Lower voltage cut-off [V]': 3.105,\n 'Upper voltage cut-off [V]': 4.1,\n 'Initial concentration in negative electrode [mol.m-3]': 19986.609595075,\n 'Initial concentration in positive electrode [mol.m-3]': 30730.7554385565}" }, "execution_count": 61, "metadata": {}, diff --git a/src/pybamm/input/parameters/lithium_ion/Ai2020.py b/src/pybamm/input/parameters/lithium_ion/Ai2020.py index bffe4ccbd7..4bf51f3440 100644 --- a/src/pybamm/input/parameters/lithium_ion/Ai2020.py +++ b/src/pybamm/input/parameters/lithium_ion/Ai2020.py @@ -5,7 +5,7 @@ def graphite_diffusivity_Dualfoil1998(sto, T): """ - Graphite diffusivity as a function of stochiometry [1, 2, 3]. + Graphite diffusivity as a function of stoichiometry [1, 2, 3]. References ---------- @@ -20,7 +20,7 @@ def graphite_diffusivity_Dualfoil1998(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature, [K] @@ -72,10 +72,10 @@ def graphite_electrolyte_exchange_current_density_Dualfoil1998( return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5 -def graphite_entropy_Enertech_Ai2020_function(sto, c_s_max): +def graphite_entropy_Enertech_Ai2020_function(sto): """ Lithium Cobalt Oxide (LiCO2) entropic change in open-circuit potential (OCP) at - a temperature of 298.15K as a function of the stochiometry. The fit is taken + a temperature of 298.15K as a function of the stoichiometry. The fit is taken from Ref [1], which is only accurate for 0.43 < sto < 0.9936. @@ -89,7 +89,7 @@ def graphite_entropy_Enertech_Ai2020_function(sto, c_s_max): Parameters ---------- sto: double - Stochiometry of material (li-fraction) + stoichiometry of material (li-fraction) Returns ------- @@ -126,9 +126,9 @@ def graphite_entropy_Enertech_Ai2020_function(sto, c_s_max): return du_dT -def graphite_volume_change_Ai2020(sto, c_s_max): +def graphite_volume_change_Ai2020(sto): """ - Graphite particle volume change as a function of stochiometry [1, 2]. + Graphite particle volume change as a function of stoichiometry [1, 2]. References ---------- @@ -143,7 +143,7 @@ def graphite_volume_change_Ai2020(sto, c_s_max): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry, dimensionless + Electrode stoichiometry, dimensionless should be R-averaged particle concentration c_s_max : :class:`pybamm.Symbol` Maximum particle concentration [mol.m-3] @@ -214,7 +214,7 @@ def graphite_cracking_rate_Ai2020(T_dim): def lico2_diffusivity_Dualfoil1998(sto, T): """ - LiCo2 diffusivity as a function of stochiometry, in this case the + LiCo2 diffusivity as a function of stoichiometry, in this case the diffusivity is taken to be a constant. The value is taken from Dualfoil [1]. References @@ -224,7 +224,7 @@ def lico2_diffusivity_Dualfoil1998(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature, [K] @@ -273,10 +273,10 @@ def lico2_electrolyte_exchange_current_density_Dualfoil1998(c_e, c_s_surf, c_s_m return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5 -def lico2_entropic_change_Ai2020_function(sto, c_s_max): +def lico2_entropic_change_Ai2020_function(sto): """ Lithium Cobalt Oxide (LiCO2) entropic change in open-circuit potential (OCP) at - a temperature of 298.15K as a function of the stochiometry. The fit is taken + a temperature of 298.15K as a function of the stoichiometry. The fit is taken from Ref [1], which is only accurate for 0.43 < sto < 0.9936. @@ -290,7 +290,7 @@ def lico2_entropic_change_Ai2020_function(sto, c_s_max): Parameters ---------- sto: double - Stochiometry of material (li-fraction) + stoichiometry of material (li-fraction) Returns ------- @@ -323,9 +323,9 @@ def lico2_entropic_change_Ai2020_function(sto, c_s_max): return du_dT -def lico2_volume_change_Ai2020(sto, c_s_max): +def lico2_volume_change_Ai2020(sto): """ - lico2 particle volume change as a function of stochiometry [1, 2]. + lico2 particle volume change as a function of stoichiometry [1, 2]. References ---------- @@ -340,10 +340,8 @@ def lico2_volume_change_Ai2020(sto, c_s_max): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry, dimensionless + Electrode stoichiometry, dimensionless should be R-averaged particle concentration - c_s_max : :class:`pybamm.Symbol` - Maximum particle concentration [mol.m-3] Returns ------- @@ -351,6 +349,7 @@ def lico2_volume_change_Ai2020(sto, c_s_max): volume change, dimensionless, normalised by particle volume """ omega = pybamm.Parameter("Positive electrode partial molar volume [m3.mol-1]") + c_s_max = pybamm.Parameter("Maximum concentration in positive electrode [mol.m-3]") t_change = omega * c_s_max * sto return t_change diff --git a/src/pybamm/input/parameters/lithium_ion/Chen2020.py b/src/pybamm/input/parameters/lithium_ion/Chen2020.py index 17f33bacd8..eccac74615 100644 --- a/src/pybamm/input/parameters/lithium_ion/Chen2020.py +++ b/src/pybamm/input/parameters/lithium_ion/Chen2020.py @@ -4,7 +4,7 @@ def graphite_LGM50_ocp_Chen2020(sto): """ - LG M50 Graphite open-circuit potential as a function of stochiometry, fit taken + LG M50 Graphite open-circuit potential as a function of stoichiometry, fit taken from [1]. References @@ -17,7 +17,7 @@ def graphite_LGM50_ocp_Chen2020(sto): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry Returns ------- @@ -75,7 +75,7 @@ def graphite_LGM50_electrolyte_exchange_current_density_Chen2020( def nmc_LGM50_ocp_Chen2020(sto): """ - LG M50 NMC open-circuit potential as a function of stochiometry, fit taken + LG M50 NMC open-circuit potential as a function of stoichiometry, fit taken from [1]. References @@ -88,7 +88,7 @@ def nmc_LGM50_ocp_Chen2020(sto): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry Returns ------- diff --git a/src/pybamm/input/parameters/lithium_ion/Chen2020_composite.py b/src/pybamm/input/parameters/lithium_ion/Chen2020_composite.py index 58b6211072..69b622a7c5 100644 --- a/src/pybamm/input/parameters/lithium_ion/Chen2020_composite.py +++ b/src/pybamm/input/parameters/lithium_ion/Chen2020_composite.py @@ -43,7 +43,7 @@ def graphite_LGM50_electrolyte_exchange_current_density_Chen2020( def silicon_ocp_lithiation_Mark2016(sto): """ silicon Open-circuit Potential (OCP) as a a function of the - stochiometry. The fit is taken from the Enertech cell [1], which is only accurate + stoichiometry. The fit is taken from the Enertech cell [1], which is only accurate for 0 < sto < 1. References @@ -55,7 +55,7 @@ def silicon_ocp_lithiation_Mark2016(sto): Parameters ---------- sto: double - Stochiometry of material (li-fraction) + stoichiometry of material (li-fraction) Returns ------- @@ -87,7 +87,7 @@ def silicon_ocp_lithiation_Mark2016(sto): def silicon_ocp_delithiation_Mark2016(sto): """ silicon Open-circuit Potential (OCP) as a a function of the - stochiometry. The fit is taken from the Enertech cell [1], which is only accurate + stoichiometry. The fit is taken from the Enertech cell [1], which is only accurate for 0 < sto < 1. References @@ -99,7 +99,7 @@ def silicon_ocp_delithiation_Mark2016(sto): Parameters ---------- sto: double - Stochiometry of material (li-fraction) + stoichiometry of material (li-fraction) Returns ------- @@ -170,7 +170,7 @@ def silicon_LGM50_electrolyte_exchange_current_density_Chen2020( def nmc_LGM50_ocp_Chen2020(sto): """ - LG M50 NMC open-circuit potential as a function of stochiometry, fit taken + LG M50 NMC open-circuit potential as a function of stoichiometry, fit taken from [1]. References @@ -183,7 +183,7 @@ def nmc_LGM50_ocp_Chen2020(sto): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry Returns ------- diff --git a/src/pybamm/input/parameters/lithium_ion/Ecker2015.py b/src/pybamm/input/parameters/lithium_ion/Ecker2015.py index ec6cd1924a..30ca2ef827 100644 --- a/src/pybamm/input/parameters/lithium_ion/Ecker2015.py +++ b/src/pybamm/input/parameters/lithium_ion/Ecker2015.py @@ -4,7 +4,7 @@ def graphite_diffusivity_Ecker2015(sto, T): """ - Graphite diffusivity as a function of stochiometry [1, 2, 3]. + Graphite diffusivity as a function of stoichiometry [1, 2, 3]. References ---------- @@ -21,7 +21,7 @@ def graphite_diffusivity_Ecker2015(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature @@ -42,7 +42,7 @@ def graphite_diffusivity_Ecker2015(sto, T): def graphite_ocp_Ecker2015(sto): """ - Graphite OCP as a function of stochiometry [1, 2, 3]. + Graphite OCP as a function of stoichiometry [1, 2, 3]. References ---------- @@ -59,7 +59,7 @@ def graphite_ocp_Ecker2015(sto): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry Returns ------- @@ -152,7 +152,7 @@ def graphite_electrolyte_exchange_current_density_Ecker2015(c_e, c_s_surf, c_s_m def nco_diffusivity_Ecker2015(sto, T): """ - NCO diffusivity as a function of stochiometry [1, 2, 3]. + NCO diffusivity as a function of stoichiometry [1, 2, 3]. References ---------- @@ -169,7 +169,7 @@ def nco_diffusivity_Ecker2015(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature @@ -190,7 +190,7 @@ def nco_diffusivity_Ecker2015(sto, T): def nco_ocp_Ecker2015(sto): """ - NCO OCP as a function of stochiometry [1, 2, 3]. + NCO OCP as a function of stoichiometry [1, 2, 3]. References ---------- @@ -207,7 +207,7 @@ def nco_ocp_Ecker2015(sto): Parameters ---------- sto : :class:`pybamm.Symbol` - Stochiometry of material (li-fraction) + stoichiometry of material (li-fraction) """ diff --git a/src/pybamm/input/parameters/lithium_ion/Ecker2015_graphite_halfcell.py b/src/pybamm/input/parameters/lithium_ion/Ecker2015_graphite_halfcell.py index 365bb6386c..267f55e774 100644 --- a/src/pybamm/input/parameters/lithium_ion/Ecker2015_graphite_halfcell.py +++ b/src/pybamm/input/parameters/lithium_ion/Ecker2015_graphite_halfcell.py @@ -34,7 +34,7 @@ def li_metal_electrolyte_exchange_current_density_Xu2019(c_e, c_Li, T): def graphite_diffusivity_Ecker2015(sto, T): """ - Graphite diffusivity as a function of stochiometry [1, 2, 3]. + Graphite diffusivity as a function of stoichiometry [1, 2, 3]. References ---------- @@ -51,7 +51,7 @@ def graphite_diffusivity_Ecker2015(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature @@ -72,7 +72,7 @@ def graphite_diffusivity_Ecker2015(sto, T): def graphite_ocp_Ecker2015(sto): """ - Graphite OCP as a function of stochiometry [1, 2, 3]. + Graphite OCP as a function of stoichiometry [1, 2, 3]. References ---------- @@ -89,7 +89,7 @@ def graphite_ocp_Ecker2015(sto): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry Returns ------- diff --git a/src/pybamm/input/parameters/lithium_ion/Marquis2019.py b/src/pybamm/input/parameters/lithium_ion/Marquis2019.py index d3b78d0b9a..13b8f57966 100644 --- a/src/pybamm/input/parameters/lithium_ion/Marquis2019.py +++ b/src/pybamm/input/parameters/lithium_ion/Marquis2019.py @@ -4,7 +4,7 @@ def graphite_mcmb2528_diffusivity_Dualfoil1998(sto, T): """ - Graphite MCMB 2528 diffusivity as a function of stochiometry, in this case the + Graphite MCMB 2528 diffusivity as a function of stoichiometry, in this case the diffusivity is taken to be a constant. The value is taken from Dualfoil [1]. References @@ -14,7 +14,7 @@ def graphite_mcmb2528_diffusivity_Dualfoil1998(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature @@ -34,7 +34,7 @@ def graphite_mcmb2528_diffusivity_Dualfoil1998(sto, T): def graphite_mcmb2528_ocp_Dualfoil1998(sto): """ Graphite MCMB 2528 Open-circuit Potential (OCP) as a function of the - stochiometry. The fit is taken from Dualfoil [1]. Dualfoil states that the data + stoichiometry. The fit is taken from Dualfoil [1]. Dualfoil states that the data was measured by Chris Bogatu at Telcordia and PolyStor materials, 2000. However, we could not find any other records of this measurment. @@ -93,10 +93,10 @@ def graphite_electrolyte_exchange_current_density_Dualfoil1998( return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5 -def graphite_entropic_change_Moura2016(sto, c_s_max): +def graphite_entropic_change_Moura2016(sto): """ Graphite entropic change in open-circuit potential (OCP) at a temperature of - 298.15K as a function of the stochiometry taken from Scott Moura's FastDFN code + 298.15K as a function of the stoichiometry taken from Scott Moura's FastDFN code [1]. References @@ -106,9 +106,12 @@ def graphite_entropic_change_Moura2016(sto, c_s_max): Parameters ---------- sto : :class:`pybamm.Symbol` - Stochiometry of material (li-fraction) + stoichiometry of material (li-fraction) """ + # Original parametrization was expressed in terms of c_s_max, but we want to + # express it in terms of stoichiometry only + c_s_max = 24983.2619938437 du_dT = ( -1.5 * (120.0 / c_s_max) * np.exp(-120 * sto) + (0.0351 / (0.083 * c_s_max)) * ((np.cosh((sto - 0.286) / 0.083)) ** (-2)) @@ -126,7 +129,7 @@ def graphite_entropic_change_Moura2016(sto, c_s_max): def lico2_diffusivity_Dualfoil1998(sto, T): """ - LiCo2 diffusivity as a function of stochiometry, in this case the + LiCo2 diffusivity as a function of stoichiometry, in this case the diffusivity is taken to be a constant. The value is taken from Dualfoil [1]. References @@ -136,7 +139,7 @@ def lico2_diffusivity_Dualfoil1998(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature @@ -155,7 +158,7 @@ def lico2_diffusivity_Dualfoil1998(sto, T): def lico2_ocp_Dualfoil1998(sto): """ Lithium Cobalt Oxide (LiCO2) Open-circuit Potential (OCP) as a a function of the - stochiometry. The fit is taken from Dualfoil [1]. Dualfoil states that the data + stoichiometry. The fit is taken from Dualfoil [1]. Dualfoil states that the data was measured by Oscar Garcia 2001 using Quallion electrodes for 0.5 < sto < 0.99 and by Marc Doyle for sto<0.4 (for unstated electrodes). We could not find any other records of the Garcia measurements. Doyles fits can be found in his @@ -170,7 +173,7 @@ def lico2_ocp_Dualfoil1998(sto): Parameters ---------- sto : :class:`pybamm.Symbol` - Stochiometry of material (li-fraction) + stoichiometry of material (li-fraction) """ @@ -222,10 +225,10 @@ def lico2_electrolyte_exchange_current_density_Dualfoil1998(c_e, c_s_surf, c_s_m return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5 -def lico2_entropic_change_Moura2016(sto, c_s_max): +def lico2_entropic_change_Moura2016(sto): """ Lithium Cobalt Oxide (LiCO2) entropic change in open-circuit potential (OCP) at - a temperature of 298.15K as a function of the stochiometry. The fit is taken + a temperature of 298.15K as a function of the stoichiometry. The fit is taken from Scott Moura's FastDFN code [1]. References @@ -235,13 +238,15 @@ def lico2_entropic_change_Moura2016(sto, c_s_max): Parameters ---------- sto : :class:`pybamm.Symbol` - Stochiometry of material (li-fraction) + stoichiometry of material (li-fraction) """ # Since the equation for LiCo2 from this ref. has the stretch factor, # should this too? If not, the "bumps" in the OCV don't line up. stretch = 1.062 sto = stretch * sto - + # Original parametrization was expressed in terms of c_s_max, but we want to + # express it in terms of stoichiometry only + c_s_max = 51217.9257309275 du_dT = ( 0.07645 * (-54.4806 / c_s_max) * ((1.0 / np.cosh(30.834 - 54.4806 * sto)) ** 2) + 2.1581 * (-50.294 / c_s_max) * ((np.cosh(52.294 - 50.294 * sto)) ** (-2)) diff --git a/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py b/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py index b54fe51491..044cebe3c5 100644 --- a/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py +++ b/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py @@ -4,7 +4,7 @@ def graphite_diffusivity_PeymanMPM(sto, T): """ - Graphite diffusivity as a function of stochiometry, in this case the + Graphite diffusivity as a function of stoichiometry, in this case the diffusivity is taken to be a constant. The value is taken from Peyman MPM. References @@ -14,7 +14,7 @@ def graphite_diffusivity_PeymanMPM(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature @@ -34,7 +34,7 @@ def graphite_diffusivity_PeymanMPM(sto, T): def graphite_ocp_PeymanMPM(sto): """ Graphite Open-circuit Potential (OCP) as a function of the - stochiometry. The fit is taken from Peyman MPM [1]. + stoichiometry. The fit is taken from Peyman MPM [1]. References ---------- @@ -89,10 +89,10 @@ def graphite_electrolyte_exchange_current_density_PeymanMPM(c_e, c_s_surf, c_s_m return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5 -def graphite_entropic_change_PeymanMPM(sto, c_s_max): +def graphite_entropic_change_PeymanMPM(sto): """ Graphite entropic change in open-circuit potential (OCP) at a temperature of - 298.15K as a function of the stochiometry taken from [1] + 298.15K as a function of the stoichiometry taken from [1] References ---------- @@ -102,7 +102,7 @@ def graphite_entropic_change_PeymanMPM(sto, c_s_max): Parameters ---------- sto : :class:`pybamm.Symbol` - Stochiometry of material (li-fraction) + stoichiometry of material (li-fraction) """ @@ -121,7 +121,7 @@ def graphite_entropic_change_PeymanMPM(sto, c_s_max): def NMC_diffusivity_PeymanMPM(sto, T): """ - NMC diffusivity as a function of stochiometry, in this case the + NMC diffusivity as a function of stoichiometry, in this case the diffusivity is taken to be a constant. The value is taken from Peyman MPM. References @@ -131,7 +131,7 @@ def NMC_diffusivity_PeymanMPM(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature @@ -151,7 +151,7 @@ def NMC_diffusivity_PeymanMPM(sto, T): def NMC_ocp_PeymanMPM(sto): """ Nickel Managanese Cobalt Oxide (NMC) Open-circuit Potential (OCP) as a - function of the stochiometry. The fit is taken from Peyman MPM. + function of the stoichiometry. The fit is taken from Peyman MPM. References ---------- @@ -160,7 +160,7 @@ def NMC_ocp_PeymanMPM(sto): Parameters ---------- sto : :class:`pybamm.Symbol` - Stochiometry of material (li-fraction) + stoichiometry of material (li-fraction) """ @@ -209,7 +209,7 @@ def NMC_electrolyte_exchange_current_density_PeymanMPM(c_e, c_s_surf, c_s_max, T return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5 -def NMC_entropic_change_PeymanMPM(sto, c_s_max): +def NMC_entropic_change_PeymanMPM(sto): """ Nickel Manganese Cobalt (NMC) entropic change in open-circuit potential (OCP) at a temperature of 298.15K as a function of the OCP. The fit is taken from [1]. @@ -224,7 +224,7 @@ def NMC_entropic_change_PeymanMPM(sto, c_s_max): Parameters ---------- sto : :class:`pybamm.Symbol` - Stochiometry of material (li-fraction) + stoichiometry of material (li-fraction) """ diff --git a/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py b/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py index e6a695a589..da1191fa8c 100644 --- a/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py +++ b/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py @@ -16,7 +16,7 @@ def graphite_diffusivity_Kim2011(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature @@ -35,7 +35,7 @@ def graphite_diffusivity_Kim2011(sto, T): def graphite_ocp_Kim2011(sto): """ - Graphite Open-circuit Potential (OCP) as a function of the stochiometry [1]. + Graphite Open-circuit Potential (OCP) as a function of the stoichiometry [1]. References ---------- @@ -92,7 +92,7 @@ def graphite_electrolyte_exchange_current_density_Kim2011(c_e, c_s_surf, c_s_max """ i0_ref = 36 # reference exchange current density at 100% SOC - sto = 0.36 # stochiometry at 100% SOC + sto = 0.36 # stoichiometry at 100% SOC c_s_n_ref = sto * c_s_max # reference electrode concentration c_e_ref = pybamm.Parameter("Initial concentration in electrolyte [mol.m-3]") alpha = 0.5 # charge transfer coefficient @@ -111,7 +111,7 @@ def graphite_electrolyte_exchange_current_density_Kim2011(c_e, c_s_surf, c_s_max def nca_diffusivity_Kim2011(sto, T): """ - NCA diffusivity as a function of stochiometry [1]. + NCA diffusivity as a function of stoichiometry [1]. References ---------- @@ -123,7 +123,7 @@ def nca_diffusivity_Kim2011(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature @@ -168,7 +168,7 @@ def nca_electrolyte_exchange_current_density_Kim2011(c_e, c_s_surf, c_s_max, T): Exchange-current density [A.m-2] """ i0_ref = 4 # reference exchange current density at 100% SOC - sto = 0.41 # stochiometry at 100% SOC + sto = 0.41 # stoichiometry at 100% SOC c_s_ref = sto * c_s_max # reference electrode concentration c_e_ref = pybamm.Parameter("Initial concentration in electrolyte [mol.m-3]") alpha = 0.5 # charge transfer coefficient @@ -252,7 +252,7 @@ def electrolyte_conductivity_Kim2011(c_e, T): def nca_ocp_Kim2011(sto): """ - Graphite Open Circuit Potential (OCP) as a function of the stochiometry [1]. + Graphite Open Circuit Potential (OCP) as a function of the stoichiometry [1]. References ---------- diff --git a/src/pybamm/input/parameters/lithium_ion/OKane2022.py b/src/pybamm/input/parameters/lithium_ion/OKane2022.py index b1e852dbdf..4ccb72bf62 100644 --- a/src/pybamm/input/parameters/lithium_ion/OKane2022.py +++ b/src/pybamm/input/parameters/lithium_ion/OKane2022.py @@ -96,7 +96,7 @@ def SEI_limited_dead_lithium_OKane2022(L_sei): def graphite_LGM50_diffusivity_Chen2020(sto, T): """ - LG M50 Graphite diffusivity as a function of stochiometry, in this case the + LG M50 Graphite diffusivity as a function of stoichiometry, in this case the diffusivity is taken to be a constant. The value is taken from [1]. References @@ -109,7 +109,7 @@ def graphite_LGM50_diffusivity_Chen2020(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature @@ -165,9 +165,9 @@ def graphite_LGM50_electrolyte_exchange_current_density_Chen2020( return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5 -def graphite_volume_change_Ai2020(sto, c_s_max): +def graphite_volume_change_Ai2020(sto): """ - Graphite particle volume change as a function of stochiometry [1, 2]. + Graphite particle volume change as a function of stoichiometry [1, 2]. References ---------- @@ -182,7 +182,7 @@ def graphite_volume_change_Ai2020(sto, c_s_max): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry, dimensionless + Electrode stoichiometry, dimensionless should be R-averaged particle concentration Returns ------- @@ -260,7 +260,7 @@ def nmc_LGM50_diffusivity_Chen2020(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature @@ -279,7 +279,7 @@ def nmc_LGM50_diffusivity_Chen2020(sto, T): def nmc_LGM50_ocp_Chen2020(sto): """ - LG M50 NMC open-circuit potential as a function of stochiometry, fit taken + LG M50 NMC open-circuit potential as a function of stoichiometry, fit taken from [1]. References @@ -292,7 +292,7 @@ def nmc_LGM50_ocp_Chen2020(sto): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry Returns ------- @@ -344,9 +344,9 @@ def nmc_LGM50_electrolyte_exchange_current_density_Chen2020(c_e, c_s_surf, c_s_m return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5 -def volume_change_Ai2020(sto, c_s_max): +def volume_change_Ai2020(sto): """ - Particle volume change as a function of stochiometry [1, 2]. + Particle volume change as a function of stoichiometry [1, 2]. References ---------- @@ -361,7 +361,7 @@ def volume_change_Ai2020(sto, c_s_max): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry, dimensionless + Electrode stoichiometry, dimensionless should be R-averaged particle concentration Returns ------- @@ -369,6 +369,7 @@ def volume_change_Ai2020(sto, c_s_max): volume change, dimensionless, normalised by particle volume """ omega = pybamm.Parameter("Positive electrode partial molar volume [m3.mol-1]") + c_s_max = pybamm.Parameter("Maximum concentration in positive electrode [mol.m-3]") t_change = omega * c_s_max * sto return t_change diff --git a/src/pybamm/input/parameters/lithium_ion/OKane2022_graphite_SiOx_halfcell.py b/src/pybamm/input/parameters/lithium_ion/OKane2022_graphite_SiOx_halfcell.py index 35533ba80e..c343dd23f4 100644 --- a/src/pybamm/input/parameters/lithium_ion/OKane2022_graphite_SiOx_halfcell.py +++ b/src/pybamm/input/parameters/lithium_ion/OKane2022_graphite_SiOx_halfcell.py @@ -126,7 +126,7 @@ def SEI_limited_dead_lithium_OKane2022(L_sei): def graphite_LGM50_diffusivity_Chen2020(sto, T): """ - LG M50 Graphite diffusivity as a function of stochiometry, in this case the + LG M50 Graphite diffusivity as a function of stoichiometry, in this case the diffusivity is taken to be a constant. The value is taken from [1]. References @@ -139,7 +139,7 @@ def graphite_LGM50_diffusivity_Chen2020(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature @@ -195,9 +195,9 @@ def graphite_LGM50_electrolyte_exchange_current_density_Chen2020( return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5 -def graphite_volume_change_Ai2020(sto, c_s_max): +def graphite_volume_change_Ai2020(sto): """ - Graphite particle volume change as a function of stochiometry [1, 2]. + Graphite particle volume change as a function of stoichiometry [1, 2]. References ---------- @@ -212,7 +212,7 @@ def graphite_volume_change_Ai2020(sto, c_s_max): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry, dimensionless + Electrode stoichiometry, dimensionless should be R-averaged particle concentration Returns ------- diff --git a/src/pybamm/input/parameters/lithium_ion/ORegan2022.py b/src/pybamm/input/parameters/lithium_ion/ORegan2022.py index b8d8f8671b..d7e240a7b6 100644 --- a/src/pybamm/input/parameters/lithium_ion/ORegan2022.py +++ b/src/pybamm/input/parameters/lithium_ion/ORegan2022.py @@ -233,7 +233,7 @@ def copper_thermal_conductivity_CRC(T): def graphite_LGM50_diffusivity_ORegan2022(sto, T): """ - LG M50 Graphite diffusivity as a function of stochiometry, in this case the + LG M50 Graphite diffusivity as a function of stoichiometry, in this case the diffusivity is taken to be a constant. The value is taken from [1]. References @@ -245,7 +245,7 @@ def graphite_LGM50_diffusivity_ORegan2022(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature @@ -292,7 +292,7 @@ def graphite_LGM50_diffusivity_ORegan2022(sto, T): def graphite_LGM50_ocp_Chen2020(sto): """ - LG M50 Graphite open-circuit potential as a function of stochiometry, fit taken + LG M50 Graphite open-circuit potential as a function of stoichiometry, fit taken from [1]. References @@ -305,7 +305,7 @@ def graphite_LGM50_ocp_Chen2020(sto): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry Returns ------- @@ -439,10 +439,10 @@ def graphite_LGM50_thermal_conductivity_ORegan2022(T): return lambda_wet -def graphite_LGM50_entropic_change_ORegan2022(sto, c_s_max): +def graphite_LGM50_entropic_change_ORegan2022(sto): """ LG M50 Graphite entropic change in open-circuit potential (OCP) at a temperature of - 298.15K as a function of the stochiometry. The fit is taken from [1]. + 298.15K as a function of the stoichiometry. The fit is taken from [1]. References ---------- @@ -453,7 +453,7 @@ def graphite_LGM50_entropic_change_ORegan2022(sto, c_s_max): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry Returns ------- @@ -525,7 +525,7 @@ def nmc_LGM50_diffusivity_ORegan2022(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature @@ -579,7 +579,7 @@ def nmc_LGM50_ocp_Chen2020(sto): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry Returns ------- @@ -712,10 +712,10 @@ def nmc_LGM50_thermal_conductivity_ORegan2022(T): return lambda_wet -def nmc_LGM50_entropic_change_ORegan2022(sto, c_s_max): +def nmc_LGM50_entropic_change_ORegan2022(sto): """ LG M50 NMC 811 entropic change in open-circuit potential (OCP) at a temperature of - 298.15K as a function of the stochiometry. The fit is taken from [1]. + 298.15K as a function of the stoichiometry. The fit is taken from [1]. References ---------- @@ -726,7 +726,7 @@ def nmc_LGM50_entropic_change_ORegan2022(sto, c_s_max): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry Returns ------- diff --git a/src/pybamm/input/parameters/lithium_ion/Prada2013.py b/src/pybamm/input/parameters/lithium_ion/Prada2013.py index 0ba56516ab..f27ba23bdd 100644 --- a/src/pybamm/input/parameters/lithium_ion/Prada2013.py +++ b/src/pybamm/input/parameters/lithium_ion/Prada2013.py @@ -4,7 +4,7 @@ def graphite_LGM50_ocp_Chen2020(sto): """ - LG M50 Graphite open-circuit potential as a function of stochiometry, fit taken + LG M50 Graphite open-circuit potential as a function of stoichiometry, fit taken from [1]. Prada2013 doesn't give an OCP for graphite, so we use this instead. References @@ -17,7 +17,7 @@ def graphite_LGM50_ocp_Chen2020(sto): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry Returns ------- @@ -86,7 +86,7 @@ def LFP_ocp_Afshar2017(sto): Parameters ---------- sto : :class:`pybamm.Symbol` - Stochiometry of material (li-fraction) + stoichiometry of material (li-fraction) """ diff --git a/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py b/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py index 1fb55154b3..82c0df76bf 100644 --- a/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py +++ b/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py @@ -4,7 +4,7 @@ def graphite_mcmb2528_diffusivity_Dualfoil1998(sto, T): """ - Graphite MCMB 2528 diffusivity as a function of stochiometry, in this case the + Graphite MCMB 2528 diffusivity as a function of stoichiometry, in this case the diffusivity is taken to be a constant. The value is taken from Dualfoil [1]. References @@ -14,7 +14,7 @@ def graphite_mcmb2528_diffusivity_Dualfoil1998(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature @@ -34,7 +34,7 @@ def graphite_mcmb2528_diffusivity_Dualfoil1998(sto, T): def graphite_ocp_Ramadass2004(sto): """ Graphite Open-circuit Potential (OCP) as a function of the - stochiometry (theta?). The fit is taken from Ramadass 2004. + stoichiometry (theta?). The fit is taken from Ramadass 2004. References ---------- @@ -92,10 +92,10 @@ def graphite_electrolyte_exchange_current_density_Ramadass2004( return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5 -def graphite_entropic_change_Moura2016(sto, c_s_max): +def graphite_entropic_change_Moura2016(sto): """ Graphite entropic change in open-circuit potential (OCP) at a temperature of - 298.15K as a function of the stochiometry taken from Scott Moura's FastDFN code + 298.15K as a function of the stoichiometry taken from Scott Moura's FastDFN code [1]. References @@ -105,9 +105,12 @@ def graphite_entropic_change_Moura2016(sto, c_s_max): Parameters ---------- sto : :class:`pybamm.Symbol` - Stochiometry of material (li-fraction) + stoichiometry of material (li-fraction) """ + # Original parametrization was expressed in terms of c_s_max, but we want to + # express it in terms of stoichiometry only + c_s_max = 24983.2619938437 du_dT = ( -1.5 * (120.0 / c_s_max) * np.exp(-120 * sto) + (0.0351 / (0.083 * c_s_max)) * ((np.cosh((sto - 0.286) / 0.083)) ** (-2)) @@ -125,7 +128,7 @@ def graphite_entropic_change_Moura2016(sto, c_s_max): def lico2_diffusivity_Ramadass2004(sto, T): """ - LiCo2 diffusivity as a function of stochiometry, in this case the + LiCo2 diffusivity as a function of stoichiometry, in this case the diffusivity is taken to be a constant. The value is taken from Ramadass 2004. References @@ -137,7 +140,7 @@ def lico2_diffusivity_Ramadass2004(sto, T): Parameters ---------- sto: :class:`pybamm.Symbol` - Electrode stochiometry + Electrode stoichiometry T: :class:`pybamm.Symbol` Dimensional temperature @@ -156,7 +159,7 @@ def lico2_diffusivity_Ramadass2004(sto, T): def lico2_ocp_Ramadass2004(sto): """ Lithium Cobalt Oxide (LiCO2) Open-circuit Potential (OCP) as a a function of the - stochiometry. The fit is taken from Ramadass 2004. Stretch is considered the + stoichiometry. The fit is taken from Ramadass 2004. Stretch is considered the overhang area negative electrode / area positive electrode, in Ramadass 2002. References @@ -168,7 +171,7 @@ def lico2_ocp_Ramadass2004(sto): Parameters ---------- sto : :class:`pybamm.Symbol` - Stochiometry of material (li-fraction) + stoichiometry of material (li-fraction) """ @@ -228,10 +231,10 @@ def lico2_electrolyte_exchange_current_density_Ramadass2004(c_e, c_s_surf, c_s_m return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5 -def lico2_entropic_change_Moura2016(sto, c_s_max): +def lico2_entropic_change_Moura2016(sto): """ Lithium Cobalt Oxide (LiCO2) entropic change in open-circuit potential (OCP) at - a temperature of 298.15K as a function of the stochiometry. The fit is taken + a temperature of 298.15K as a function of the stoichiometry. The fit is taken from Scott Moura's FastDFN code [1]. References @@ -241,13 +244,15 @@ def lico2_entropic_change_Moura2016(sto, c_s_max): Parameters ---------- sto : :class:`pybamm.Symbol` - Stochiometry of material (li-fraction) + stoichiometry of material (li-fraction) """ # Since the equation for LiCo2 from this ref. has the stretch factor, # should this too? If not, the "bumps" in the OCV don't line up. stretch = 1.062 sto = stretch * sto - + # Original parametrization was expressed in terms of c_s_max, but we want to + # express it in terms of stoichiometry only + c_s_max = 51217.9257309275 du_dT = ( 0.07645 * (-54.4806 / c_s_max) * ((1.0 / np.cosh(30.834 - 54.4806 * sto)) ** 2) + 2.1581 * (-50.294 / c_s_max) * ((np.cosh(52.294 - 50.294 * sto)) ** (-2)) diff --git a/src/pybamm/input/parameters/lithium_ion/Xu2019.py b/src/pybamm/input/parameters/lithium_ion/Xu2019.py index fe5a9376af..d1c5edea98 100644 --- a/src/pybamm/input/parameters/lithium_ion/Xu2019.py +++ b/src/pybamm/input/parameters/lithium_ion/Xu2019.py @@ -36,7 +36,7 @@ def li_metal_electrolyte_exchange_current_density_Xu2019(c_e, c_Li, T): def nmc_ocp_Xu2019(sto): """ Nickel Managanese Cobalt Oxide (NMC) Open-circuit Potential (OCP) as a - function of the stochiometry, from [1]. + function of the stoichiometry, from [1]. References ---------- @@ -48,7 +48,7 @@ def nmc_ocp_Xu2019(sto): Parameters ---------- sto : :class:`pybamm.Symbol` - Stochiometry of material (li-fraction) + stoichiometry of material (li-fraction) """ diff --git a/src/pybamm/models/full_battery_models/base_battery_model.py b/src/pybamm/models/full_battery_models/base_battery_model.py index e0ade7b429..5340d685e3 100644 --- a/src/pybamm/models/full_battery_models/base_battery_model.py +++ b/src/pybamm/models/full_battery_models/base_battery_model.py @@ -618,14 +618,11 @@ def __init__(self, extra_options): options["surface form"] != "false" and options["particle size"] == "single" and options["particle"] == "Fickian diffusion" - and options["particle mechanics"] == "none" - and options["loss of active material"] == "none" ): raise pybamm.OptionError( "If there are multiple particle phases: 'surface form' cannot be " "'false', 'particle size' must be 'single', 'particle' must be " - "'Fickian diffusion'. Also the following must " - "be 'none': 'particle mechanics', 'loss of active material'" + "'Fickian diffusion'." ) if options["surface temperature"] == "lumped": @@ -752,7 +749,7 @@ def print_options(self): Print the possible options with the ones currently selected """ for key, value in self.items(): - print(f"{key!r}: {value!r} (possible: {self.possible_options[key]!r})") + print(rf"{key!r}: {value!r} (possible: {self.possible_options[key]!r})") def print_detailed_options(self): """ diff --git a/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py b/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py index dfe2512f6e..b1367f8300 100644 --- a/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py +++ b/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py @@ -365,29 +365,31 @@ def set_crack_submodel(self): for domain in self.options.whole_cell_domains: if domain != "separator": domain = domain.split()[0].lower() - crack = getattr(self.options, domain)["particle mechanics"] - if crack == "none": - self.submodels[f"{domain} particle mechanics"] = ( - pybamm.particle_mechanics.NoMechanics( - self.param, domain, options=self.options, phase="primary" + phases = self.options.phases[domain] + for phase in phases: + crack = getattr(self.options, domain)["particle mechanics"] + if crack == "none": + self.submodels[f"{domain} {phase}particle mechanics"] = ( + pybamm.particle_mechanics.NoMechanics( + self.param, domain, options=self.options, phase=phase + ) ) - ) - elif crack == "swelling only": - self.submodels[f"{domain} particle mechanics"] = ( - pybamm.particle_mechanics.SwellingOnly( - self.param, domain, options=self.options, phase="primary" + elif crack == "swelling only": + self.submodels[f"{domain} {phase}particle mechanics"] = ( + pybamm.particle_mechanics.SwellingOnly( + self.param, domain, options=self.options, phase=phase + ) ) - ) - elif crack == "swelling and cracking": - self.submodels[f"{domain} particle mechanics"] = ( - pybamm.particle_mechanics.CrackPropagation( - self.param, - domain, - self.x_average, - options=self.options, - phase="primary", + elif crack == "swelling and cracking": + self.submodels[f"{domain} {phase}particle mechanics"] = ( + pybamm.particle_mechanics.CrackPropagation( + self.param, + domain, + self.x_average, + options=self.options, + phase=phase, + ) ) - ) def set_active_material_submodel(self): for domain in ["negative", "positive"]: @@ -401,7 +403,7 @@ def set_active_material_submodel(self): ) else: submod = pybamm.active_material.LossActiveMaterial( - self.param, domain, self.options, self.x_average + self.param, domain, self.options, self.x_average, phase ) self.submodels[f"{domain} {phase} active material"] = submod diff --git a/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py index a5710dc986..c5b6a9b911 100644 --- a/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py +++ b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py @@ -657,6 +657,8 @@ def get_initial_stoichiometries(self, initial_value, tol=1e-6, inputs=None): The tolerance for the solver used to compute the initial stoichiometries. A lower value results in higher precision but may increase computation time. Default is 1e-6. + inputs : dict, optional + A dictionary of input parameters passed to the model. Returns ------- @@ -727,6 +729,11 @@ def get_min_max_stoichiometries(self, inputs=None): Calculate min/max stoichiometries given voltage limits, open-circuit potentials, etc defined by parameter_values + Parameters + ---------- + inputs : dict, optional + A dictionary of input parameters passed to the model. + Returns ------- x_0, x_100, y_100, y_0 @@ -751,7 +758,7 @@ def get_min_max_stoichiometries(self, inputs=None): sol = self.solve(all_inputs) return [sol["x_0"], sol["x_100"], sol["y_100"], sol["y_0"]] - def get_initial_ocps(self, initial_value, tol=1e-6): + def get_initial_ocps(self, initial_value, tol=1e-6, inputs=None): """ Calculate initial open-circuit potentials to start off the simulation at a particular state of charge, given voltage limits, open-circuit potentials, etc @@ -760,9 +767,14 @@ def get_initial_ocps(self, initial_value, tol=1e-6): Parameters ---------- initial_value : float - Target SOC, must be between 0 and 1. + Target initial value. + If integer, interpreted as SOC, must be between 0 and 1. + If string e.g. "4 V", interpreted as voltage, + must be between V_min and V_max. tol: float, optional Tolerance for the solver used in calculating initial stoichiometries. + inputs : dict, optional + A dictionary of input parameters passed to the model. Returns ------- @@ -771,7 +783,7 @@ def get_initial_ocps(self, initial_value, tol=1e-6): """ parameter_values = self.parameter_values param = self.param - x, y = self.get_initial_stoichiometries(initial_value, tol) + x, y = self.get_initial_stoichiometries(initial_value, tol, inputs=inputs) if self.options["open-circuit potential"] == "MSMR": msmr_pot_model = _get_msmr_potential_model( self.parameter_values, self.param @@ -783,8 +795,8 @@ def get_initial_ocps(self, initial_value, tol=1e-6): Up = sol["Up"].data[0] else: T_ref = parameter_values["Reference temperature [K]"] - Un = parameter_values.evaluate(param.n.prim.U(x, T_ref)) - Up = parameter_values.evaluate(param.p.prim.U(y, T_ref)) + Un = parameter_values.evaluate(param.n.prim.U(x, T_ref), inputs=inputs) + Up = parameter_values.evaluate(param.p.prim.U(y, T_ref), inputs=inputs) return Un, Up def get_min_max_ocps(self): @@ -871,6 +883,8 @@ def get_initial_stoichiometries( The tolerance for the solver used to compute the initial stoichiometries. A lower value results in higher precision but may increase computation time. Default is 1e-6. + inputs : dict, optional + A dictionary of input parameters passed to the model. Returns ------- @@ -918,6 +932,8 @@ def get_initial_ocps( param=None, known_value="cyclable lithium capacity", options=None, + tol=1e-6, + inputs=None, ): """ Calculate initial open-circuit potentials to start off the simulation at a @@ -942,6 +958,10 @@ def get_initial_ocps( options : dict-like, optional A dictionary of options to be passed to the model, see :class:`pybamm.BatteryModelOptions`. + tol: float, optional + Tolerance for the solver used in calculating initial open-circuit potentials. + inputs : dict, optional + A dictionary of input parameters passed to the model. Returns ------- @@ -949,7 +969,7 @@ def get_initial_ocps( The initial electrode OCPs that give the desired initial state of charge """ esoh_solver = ElectrodeSOHSolver(parameter_values, param, known_value, options) - return esoh_solver.get_initial_ocps(initial_value) + return esoh_solver.get_initial_ocps(initial_value, tol, inputs=inputs) def get_min_max_ocps( diff --git a/src/pybamm/models/submodels/active_material/constant_active_material.py b/src/pybamm/models/submodels/active_material/constant_active_material.py index 3237775f1c..e978168e9a 100644 --- a/src/pybamm/models/submodels/active_material/constant_active_material.py +++ b/src/pybamm/models/submodels/active_material/constant_active_material.py @@ -23,6 +23,7 @@ class Constant(BaseModel): def get_fundamental_variables(self): domain = self.domain + phase = self.phase_name eps_solid = self.phase_param.epsilon_s deps_solid_dt = pybamm.FullBroadcast( 0, f"{domain} electrode", "current collector" @@ -35,7 +36,7 @@ def get_fundamental_variables(self): variables.update( { - "Loss of lithium due to loss of active material " + f"Loss of lithium due to loss of {phase}active material " f"in {domain} electrode [mol]": pybamm.Scalar(0) } ) diff --git a/src/pybamm/models/submodels/active_material/loss_active_material.py b/src/pybamm/models/submodels/active_material/loss_active_material.py index 6f027d89e6..ffba064d03 100644 --- a/src/pybamm/models/submodels/active_material/loss_active_material.py +++ b/src/pybamm/models/submodels/active_material/loss_active_material.py @@ -23,34 +23,36 @@ class LossActiveMaterial(BaseModel): """ - def __init__(self, param, domain, options, x_average): - super().__init__(param, domain, options=options) + def __init__(self, param, domain, options, x_average, phase): + super().__init__(param, domain, options=options, phase=phase) pybamm.citations.register("Reniers2019") self.x_average = x_average def get_fundamental_variables(self): domain, Domain = self.domain_Domain + phase = self.phase_name if self.x_average is True: eps_solid_xav = pybamm.Variable( - f"X-averaged {domain} electrode active material volume fraction", + f"X-averaged {domain} electrode {phase}active material volume fraction", domain="current collector", ) eps_solid = pybamm.PrimaryBroadcast(eps_solid_xav, f"{domain} electrode") else: eps_solid = pybamm.Variable( - f"{Domain} electrode active material volume fraction", + f"{Domain} electrode {phase}active material volume fraction", domain=f"{domain} electrode", auxiliary_domains={"secondary": "current collector"}, ) variables = self._get_standard_active_material_variables(eps_solid) lli_due_to_lam = pybamm.Variable( - "Loss of lithium due to loss of active material " + f"Loss of lithium due to loss of {phase}active material " f"in {domain} electrode [mol]" ) + variables.update( { - "Loss of lithium due to loss of active material " + f"Loss of lithium due to loss of {phase}active material " f"in {domain} electrode [mol]": lli_due_to_lam } ) @@ -58,6 +60,7 @@ def get_fundamental_variables(self): def get_coupled_variables(self, variables): domain, Domain = self.domain_Domain + phase_name = self.phase_name deps_solid_dt = 0 lam_option = getattr(getattr(self.options, domain), self.phase)[ @@ -68,22 +71,22 @@ def get_coupled_variables(self, variables): # This is loss of active material model by mechanical effects if self.x_average is True: stress_t_surf = variables[ - f"X-averaged {domain} particle surface tangential stress [Pa]" + f"X-averaged {domain} {phase_name}particle surface tangential stress [Pa]" ] stress_r_surf = variables[ - f"X-averaged {domain} particle surface radial stress [Pa]" + f"X-averaged {domain} {phase_name}particle surface radial stress [Pa]" ] else: stress_t_surf = variables[ - f"{Domain} particle surface tangential stress [Pa]" + f"{Domain} {phase_name}particle surface tangential stress [Pa]" ] stress_r_surf = variables[ - f"{Domain} particle surface radial stress [Pa]" + f"{Domain} {phase_name}particle surface radial stress [Pa]" ] - beta_LAM = self.domain_param.beta_LAM - stress_critical = self.domain_param.stress_critical - m_LAM = self.domain_param.m_LAM + beta_LAM = self.phase_param.beta_LAM + stress_critical = self.phase_param.stress_critical + m_LAM = self.phase_param.m_LAM stress_h_surf = (stress_r_surf + 2 * stress_t_surf) / 3 # compressive stress make no contribution @@ -97,15 +100,15 @@ def get_coupled_variables(self, variables): deps_solid_dt += j_stress_LAM if "reaction" in lam_option: - beta_LAM_sei = self.domain_param.beta_LAM_sei + beta_LAM_sei = self.phase_param.beta_LAM_sei if self.x_average is True: a_j_sei = variables[ - f"X-averaged {domain} electrode SEI " + f"X-averaged {domain} electrode {phase_name}SEI " "volumetric interfacial current density [A.m-3]" ] else: a_j_sei = variables[ - f"{Domain} electrode SEI volumetric " + f"{Domain} electrode {phase_name}SEI volumetric " "interfacial current density [A.m-3]" ] @@ -131,19 +134,22 @@ def get_coupled_variables(self, variables): def set_rhs(self, variables): domain, Domain = self.domain_Domain + phase_name = self.phase_name if self.x_average is True: eps_solid = variables[ - f"X-averaged {domain} electrode active material volume fraction" + f"X-averaged {domain} electrode {phase_name}active material volume fraction" ] deps_solid_dt = variables[ - f"X-averaged {domain} electrode active material " + f"X-averaged {domain} electrode {phase_name}active material " "volume fraction change [s-1]" ] else: - eps_solid = variables[f"{Domain} electrode active material volume fraction"] + eps_solid = variables[ + f"{Domain} electrode {phase_name}active material volume fraction" + ] deps_solid_dt = variables[ - f"{Domain} electrode active material volume fraction change [s-1]" + f"{Domain} electrode {phase_name}active material volume fraction change [s-1]" ] # Loss of lithium due to loss of active material @@ -151,11 +157,13 @@ def set_rhs(self, variables): # simulations using adaptive inter-cycle extrapolation algorithm." # Journal of The Electrochemical Society 168.12 (2021): 120531. lli_due_to_lam = variables[ - "Loss of lithium due to loss of active material " + f"Loss of lithium due to loss of {phase_name}active material " f"in {domain} electrode [mol]" ] # Multiply by mol.m-3 * m3 to get mol - c_s_av = variables[f"Average {domain} particle concentration [mol.m-3]"] + c_s_av = variables[ + f"Average {domain} {phase_name}particle concentration [mol.m-3]" + ] V = self.domain_param.L * self.param.A_cc self.rhs = { @@ -166,20 +174,23 @@ def set_rhs(self, variables): def set_initial_conditions(self, variables): domain, Domain = self.domain_Domain + phase_name = self.phase_name - eps_solid_init = self.domain_param.prim.epsilon_s + eps_solid_init = self.phase_param.epsilon_s if self.x_average is True: eps_solid_xav = variables[ - f"X-averaged {domain} electrode active material volume fraction" + f"X-averaged {domain} electrode {phase_name}active material volume fraction" ] self.initial_conditions = {eps_solid_xav: pybamm.x_average(eps_solid_init)} else: - eps_solid = variables[f"{Domain} electrode active material volume fraction"] + eps_solid = variables[ + f"{Domain} electrode {phase_name}active material volume fraction" + ] self.initial_conditions = {eps_solid: eps_solid_init} lli_due_to_lam = variables[ - "Loss of lithium due to loss of active material " + f"Loss of lithium due to loss of {phase_name}active material " f"in {domain} electrode [mol]" ] self.initial_conditions[lli_due_to_lam] = pybamm.Scalar(0) diff --git a/src/pybamm/models/submodels/active_material/total_active_material.py b/src/pybamm/models/submodels/active_material/total_active_material.py index 5e1d7e2f92..f86486ff53 100644 --- a/src/pybamm/models/submodels/active_material/total_active_material.py +++ b/src/pybamm/models/submodels/active_material/total_active_material.py @@ -34,6 +34,7 @@ def get_coupled_variables(self, variables): f"{Domain} electrode {{}}active material volume fraction change [s-1]", f"X-averaged {domain} electrode {{}}active material " "volume fraction change [s-1]", + f"Loss of lithium due to loss of {{}}active material in {domain} electrode [mol]", ]: sumvar = sum( variables[variable_template.format(phase + " ")] for phase in phases diff --git a/src/pybamm/models/submodels/particle/base_particle.py b/src/pybamm/models/submodels/particle/base_particle.py index dab48b5f79..fe37d2ff2e 100644 --- a/src/pybamm/models/submodels/particle/base_particle.py +++ b/src/pybamm/models/submodels/particle/base_particle.py @@ -57,9 +57,9 @@ def _get_effective_diffusivity(self, c, T, current): if stress_option == "true": # Ai2019 eq [12] sto = c / phase_param.c_max - Omega = pybamm.r_average(domain_param.Omega(sto, T)) - E = pybamm.r_average(domain_param.E(sto, T)) - nu = domain_param.nu + Omega = pybamm.r_average(phase_param.Omega(sto, T)) + E = pybamm.r_average(phase_param.E(sto, T)) + nu = phase_param.nu theta_M = Omega / (param.R * T) * (2 * Omega * E) / (9 * (1 - nu)) stress_factor = 1 + theta_M * (c - domain_param.c_0) else: diff --git a/src/pybamm/models/submodels/particle_mechanics/base_mechanics.py b/src/pybamm/models/submodels/particle_mechanics/base_mechanics.py index 4e25becbab..1301722da0 100644 --- a/src/pybamm/models/submodels/particle_mechanics/base_mechanics.py +++ b/src/pybamm/models/submodels/particle_mechanics/base_mechanics.py @@ -38,30 +38,37 @@ def _get_standard_variables(self, l_cr): def _get_mechanical_results(self, variables): domain_param = self.domain_param domain, Domain = self.domain_Domain + phase_name = self.phase_name + phase_param = self.phase_param - c_s_rav = variables[f"R-averaged {domain} particle concentration [mol.m-3]"] - sto_rav = variables[f"R-averaged {domain} particle concentration"] - c_s_surf = variables[f"{Domain} particle surface concentration [mol.m-3]"] + c_s_rav = variables[ + f"R-averaged {domain} {phase_name}particle concentration [mol.m-3]" + ] + sto_rav = variables[f"R-averaged {domain} {phase_name}particle concentration"] + c_s_surf = variables[ + f"{Domain} {phase_name}particle surface concentration [mol.m-3]" + ] T_xav = variables["X-averaged cell temperature [K]"] - phase_name = self.phase_name T = pybamm.PrimaryBroadcast( variables[f"{Domain} electrode temperature [K]"], [f"{domain} {phase_name}particle"], ) - eps_s = variables[f"{Domain} electrode active material volume fraction"] + eps_s = variables[ + f"{Domain} electrode {phase_name}active material volume fraction" + ] # use a tangential approximation for omega - sto = variables[f"{Domain} particle concentration"] - Omega = pybamm.r_average(domain_param.Omega(sto, T)) - R0 = domain_param.prim.R + sto = variables[f"{Domain} {phase_name}particle concentration"] + Omega = pybamm.r_average(phase_param.Omega(sto, T)) + R0 = phase_param.R c_0 = domain_param.c_0 - E0 = pybamm.r_average(domain_param.E(sto, T)) - nu = domain_param.nu + E0 = pybamm.r_average(phase_param.E(sto, T)) + nu = phase_param.nu L0 = domain_param.L - sto_init = pybamm.r_average(domain_param.prim.c_init / domain_param.prim.c_max) + sto_init = pybamm.r_average(phase_param.c_init / phase_param.c_max) v_change = pybamm.x_average( - eps_s * domain_param.prim.t_change(sto_rav) - ) - pybamm.x_average(eps_s * domain_param.prim.t_change(sto_init)) + eps_s * phase_param.t_change(sto_rav) + ) - pybamm.x_average(eps_s * phase_param.t_change(sto_init)) electrode_thickness_change = self.param.n_electrodes_parallel * v_change * L0 # Ai2019 eq [10] @@ -81,18 +88,27 @@ def _get_mechanical_results(self, variables): variables.update( { - f"{Domain} particle surface radial stress [Pa]": stress_r_surf, - f"{Domain} particle surface tangential stress [Pa]": stress_t_surf, - f"{Domain} particle surface displacement [m]": disp_surf, - f"X-averaged {domain} particle surface " + f"{Domain} {phase_name}particle surface radial stress [Pa]": stress_r_surf, + f"{Domain} {phase_name}particle surface tangential stress [Pa]": stress_t_surf, + f"{Domain} {phase_name}particle surface displacement [m]": disp_surf, + f"X-averaged {domain} {phase_name}particle surface " "radial stress [Pa]": stress_r_surf_av, - f"X-averaged {domain} particle surface " + f"X-averaged {domain} {phase_name}particle surface " "tangential stress [Pa]": stress_t_surf_av, - f"X-averaged {domain} particle surface displacement [m]": disp_surf_av, - f"{Domain} electrode thickness change [m]": electrode_thickness_change, + f"X-averaged {domain} {phase_name}particle surface displacement [m]": disp_surf_av, + f"{Domain} electrode {phase_name}thickness change [m]": electrode_thickness_change, } ) + if ( + f"{Domain} primary thickness change [m]" in variables + and f"{Domain} secondary thickness change [m]" in variables + ): + variables[f"{Domain} thickness change [m]"] = ( + variables[f"{Domain} primary thickness change [m]"] + + variables[f"{Domain} secondary thickness change [m]"] + ) + if ( "Negative electrode thickness change [m]" in variables and "Positive electrode thickness change [m]" in variables diff --git a/src/pybamm/parameters/bpx.py b/src/pybamm/parameters/bpx.py index 7485e805b9..df380ad627 100644 --- a/src/pybamm/parameters/bpx.py +++ b/src/pybamm/parameters/bpx.py @@ -228,12 +228,6 @@ def _bpx_to_param_dict(bpx: BPX) -> dict: def _arrhenius(Ea, T): return exp(Ea / constants.R * (1 / T_ref - 1 / T)) - def _entropic_change(sto, c_s_max, dUdT, constant=False): - if constant: - return dUdT - else: - return dUdT(sto) - # reaction rates in pybamm exchange current is defined j0 = k * sqrt(ce * cs * # (cs-cs_max)) in BPX exchange current is defined j0 = F * k_norm * sqrt((ce/ce0) * # (cs/cs_max) * (1-cs/cs_max)) @@ -284,25 +278,10 @@ def _conductivity(c_e, T, Ea, sigma_ref, constant=False): ) # entropic change - dUdT = pybamm_dict[ - phase_domain_pre_name + "entropic change coefficient [V.K-1]" - ] - if callable(dUdT): + dUdT = pybamm_dict[phase_domain_pre_name + "OCP entropic change [V.K-1]"] + if isinstance(dUdT, tuple): pybamm_dict[phase_domain_pre_name + "OCP entropic change [V.K-1]"] = ( - partial(_entropic_change, dUdT=dUdT) - ) - elif isinstance(dUdT, tuple): - pybamm_dict[phase_domain_pre_name + "OCP entropic change [V.K-1]"] = ( - partial( - _entropic_change, - dUdT=partial( - _interpolant_func, name=dUdT[0], x=dUdT[1][0], y=dUdT[1][1] - ), - ) - ) - else: - pybamm_dict[phase_domain_pre_name + "OCP entropic change [V.K-1]"] = ( - partial(_entropic_change, dUdT=dUdT, constant=True) + partial(_interpolant_func, name=dUdT[0], x=dUdT[1][0], y=dUdT[1][1]) ) # reaction rate @@ -440,6 +419,10 @@ def _get_pybamm_name(pybamm_name, domain): pybamm_name = domain.short_pre_name + pybamm_name_lower elif pybamm_name.startswith("OCP"): pybamm_name = domain.pre_name + pybamm_name + elif pybamm_name.startswith("Entropic change"): + pybamm_name = domain.pre_name + pybamm_name.replace( + "Entropic change coefficient", "OCP entropic change" + ) elif pybamm_name.startswith("Cation transference number"): pybamm_name = pybamm_name elif domain.pre_name != "": diff --git a/src/pybamm/parameters/lithium_ion_parameters.py b/src/pybamm/parameters/lithium_ion_parameters.py index f5a76c6d48..3902242d78 100644 --- a/src/pybamm/parameters/lithium_ion_parameters.py +++ b/src/pybamm/parameters/lithium_ion_parameters.py @@ -269,7 +269,6 @@ def _set_parameters(self): self.tau_s = self.geo.tau_s # Mechanical parameters - self.nu = pybamm.Parameter(f"{Domain} electrode Poisson's ratio") self.c_0 = pybamm.Parameter( f"{Domain} electrode reference concentration for free of deformation " "[mol.m-3]" @@ -283,20 +282,6 @@ def _set_parameters(self): self.b_cr = pybamm.Parameter(f"{Domain} electrode Paris' law constant b") self.m_cr = pybamm.Parameter(f"{Domain} electrode Paris' law constant m") - # Loss of active material parameters - self.m_LAM = pybamm.Parameter( - f"{Domain} electrode LAM constant exponential term" - ) - self.beta_LAM = pybamm.Parameter( - f"{Domain} electrode LAM constant proportional term [s-1]" - ) - self.stress_critical = pybamm.Parameter( - f"{Domain} electrode critical stress [Pa]" - ) - self.beta_LAM_sei = pybamm.Parameter( - f"{Domain} electrode reaction-driven LAM factor [m3.mol-1]" - ) - # Utilisation parameters self.u_init = pybamm.Parameter( f"Initial {domain} electrode interface utilisation" @@ -313,22 +298,6 @@ def C_dl(self, T): f"{Domain} electrode double-layer capacity [F.m-2]", inputs ) - def Omega(self, sto, T): - """Dimensional partial molar volume of Li in solid solution [m3.mol-1]""" - Domain = self.domain.capitalize() - inputs = {f"{Domain} particle stoichiometry": sto, "Temperature [K]": T} - return pybamm.FunctionParameter( - f"{Domain} electrode partial molar volume [m3.mol-1]", inputs - ) - - def E(self, sto, T): - """Dimensional Young's modulus""" - Domain = self.domain.capitalize() - inputs = {f"{Domain} particle stoichiometry": sto, "Temperature [K]": T} - return pybamm.FunctionParameter( - f"{Domain} electrode Young's modulus [Pa]", inputs - ) - def sigma(self, T): """Dimensional electrical conductivity in electrode""" inputs = {"Temperature [K]": T} @@ -538,6 +507,23 @@ def _set_parameters(self): if self.options["particle shape"] == "spherical": self.a_typ = 3 * pybamm.xyz_average(self.epsilon_s) / self.R_typ + # Mechanical property + self.nu = pybamm.Parameter(f"{pref}{Domain} electrode Poisson's ratio") + + # Loss of active material parameters + self.m_LAM = pybamm.Parameter( + f"{pref}{Domain} electrode LAM constant exponential term" + ) + self.beta_LAM = pybamm.Parameter( + f"{pref}{Domain} electrode LAM constant proportional term [s-1]" + ) + self.stress_critical = pybamm.Parameter( + f"{pref}{Domain} electrode critical stress [Pa]" + ) + self.beta_LAM_sei = pybamm.Parameter( + f"{pref}{Domain} electrode reaction-driven LAM factor [m3.mol-1]" + ) + def D(self, c_s, T, lithiation=None): """ Dimensional diffusivity in particle. In the parameter sets this is defined as @@ -669,11 +655,9 @@ def dUdT(self, sto): "MSMR" formulation, stoichiometry is explicitly defined as a function of U and T, and dUdT is only used to calculate the reversible heat generation term. """ - domain, Domain = self.domain_Domain + Domain = self.domain.capitalize() inputs = { f"{Domain} particle stoichiometry": sto, - f"{self.phase_prefactor}Maximum {domain} particle " - "surface concentration [mol.m-3]": self.c_max, } return pybamm.FunctionParameter( f"{self.phase_prefactor}{Domain} electrode OCP entropic change [V.K-1]", @@ -794,12 +778,33 @@ def t_change(self, sto): """ Volume change for the electrode; sto should be R-averaged """ - domain, Domain = self.domain_Domain + Domain = self.domain.capitalize() return pybamm.FunctionParameter( - f"{Domain} electrode volume change", + f"{self.phase_prefactor}{Domain} electrode volume change", { - "Particle stoichiometry": sto, - f"{self.phase_prefactor}Maximum {domain} particle " - "surface concentration [mol.m-3]": self.c_max, + f"{Domain} particle stoichiometry": sto, }, ) + + def Omega(self, sto, T): + """Dimensional partial molar volume of Li in solid solution [m3.mol-1]""" + domain, Domain = self.domain_Domain + inputs = { + f"{self.phase_prefactor} particle stoichiometry": sto, + "Temperature [K]": T, + } + return pybamm.FunctionParameter( + f"{self.phase_prefactor}{Domain} electrode partial molar volume [m3.mol-1]", + inputs, + ) + + def E(self, sto, T): + """Dimensional Young's modulus""" + domain, Domain = self.domain_Domain + inputs = { + f"{self.phase_prefactor} particle stoichiometry": sto, + "Temperature [K]": T, + } + return pybamm.FunctionParameter( + f"{self.phase_prefactor}{Domain} electrode Young's modulus [Pa]", inputs + ) diff --git a/src/pybamm/simulation.py b/src/pybamm/simulation.py index 0aa85d1c20..da0ac08316 100644 --- a/src/pybamm/simulation.py +++ b/src/pybamm/simulation.py @@ -174,7 +174,7 @@ def _set_random_seed(self): % (2**32) ) - def set_up_and_parameterise_experiment(self): + def set_up_and_parameterise_experiment(self, solve_kwargs=None): """ Create and parameterise the models for each step in the experiment. @@ -182,6 +182,46 @@ def set_up_and_parameterise_experiment(self): reduces simulation time since the model formulation is efficient. """ parameter_values = self._parameter_values.copy() + + # some parameters are used to control the experiment, and should not be + # input parameters + restrict_list = {"Initial temperature [K]", "Ambient temperature [K]"} + for step in self.experiment.steps: + if issubclass(step.__class__, pybamm.experiment.step.BaseStepImplicit): + restrict_list.update(step.get_parameter_values([]).keys()) + elif issubclass(step.__class__, pybamm.experiment.step.BaseStepExplicit): + restrict_list.update(["Current function [A]"]) + for key in restrict_list: + if key in parameter_values.keys() and isinstance( + parameter_values[key], pybamm.InputParameter + ): + raise pybamm.ModelError( + f"Cannot use '{key}' as an input parameter in this experiment. " + f"This experiment is controlled via the following parameters: {restrict_list}. " + f"None of these parameters are able to be input parameters." + ) + + if ( + solve_kwargs is not None + and "calculate_sensitivities" in solve_kwargs + and solve_kwargs["calculate_sensitivities"] + ): + for step in self.experiment.steps: + if any( + [ + isinstance( + term, + pybamm.experiment.step.step_termination.BaseTermination, + ) + for term in step.termination + ] + ): + pybamm.logger.warning( + f"Step '{step}' has a termination condition based on an event. Sensitivity calculation will be inaccurate " + "if the time of each step event changes rapidly with respect to the parameters. " + ) + break + # Set the initial temperature to be the temperature of the first step # We can set this globally for all steps since any subsequent steps will either # start at the temperature at the end of the previous step (if non-isothermal @@ -303,7 +343,7 @@ def build(self, initial_soc=None, inputs=None): # rebuilt model so clear solver setup self._solver._model_set_up = {} - def build_for_experiment(self, initial_soc=None, inputs=None): + def build_for_experiment(self, initial_soc=None, inputs=None, solve_kwargs=None): """ Similar to :meth:`Simulation.build`, but for the case of simulating an experiment, where there may be several models and solvers to build. @@ -314,7 +354,7 @@ def build_for_experiment(self, initial_soc=None, inputs=None): if self.steps_to_built_models: return else: - self.set_up_and_parameterise_experiment() + self.set_up_and_parameterise_experiment(solve_kwargs) # Can process geometry with default parameter values (only electrical # parameters change between parameter values) @@ -497,7 +537,9 @@ def solve( elif self.operating_mode == "with experiment": callbacks.on_experiment_start(logs) - self.build_for_experiment(initial_soc=initial_soc, inputs=inputs) + self.build_for_experiment( + initial_soc=initial_soc, inputs=inputs, solve_kwargs=kwargs + ) if t_eval is not None: pybamm.logger.warning( "Ignoring t_eval as solution times are specified by the experiment" diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index 9c0d94f1a9..1df9aef35f 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -670,6 +670,33 @@ def calculate_consistent_state(self, model, time=0, inputs=None): y0 = root_sol.all_ys[0] return y0 + def _solve_process_calculate_sensitivities_arg( + inputs, model, calculate_sensitivities + ): + # get a list-only version of calculate_sensitivities + if isinstance(calculate_sensitivities, bool): + if calculate_sensitivities: + calculate_sensitivities_list = [p for p in inputs.keys()] + else: + calculate_sensitivities_list = [] + else: + calculate_sensitivities_list = calculate_sensitivities + + calculate_sensitivities_list.sort() + if not hasattr(model, "calculate_sensitivities"): + model.calculate_sensitivities = [] + + # Check that calculate_sensitivites have not been updated + sensitivities_have_changed = ( + calculate_sensitivities_list != model.calculate_sensitivities + ) + + # save sensitivity parameters so we can identify them later on + # (FYI: this is used in the Solution class) + model.calculate_sensitivities = calculate_sensitivities_list + + return calculate_sensitivities_list, sensitivities_have_changed + def solve( self, model, @@ -700,7 +727,11 @@ def solve( calculate_sensitivities : list of str or bool, optional Whether the solver calculates sensitivities of all input parameters. Defaults to False. If only a subset of sensitivities are required, can also pass a - list of input parameter names + list of input parameter names. **Limitations**: sensitivities are not calculated up to numerical tolerances + so are not guarenteed to be within the tolerances set by the solver, please raise an issue if you + require this functionality. Also, when using this feature with `pybamm.Experiment`, the sensitivities + do not take into account the movement of step-transitions wrt input parameters, so do not use this feature + if the timings of your experimental protocol change rapidly with respect to your input parameters. t_interp : None, list or ndarray, optional The times (in seconds) at which to interpolate the solution. Defaults to None. Only valid for solvers that support intra-solve interpolation (`IDAKLUSolver`). @@ -722,15 +753,6 @@ def solve( """ pybamm.logger.info(f"Start solving {model.name} with {self.name}") - # get a list-only version of calculate_sensitivities - if isinstance(calculate_sensitivities, bool): - if calculate_sensitivities: - calculate_sensitivities_list = [p for p in inputs.keys()] - else: - calculate_sensitivities_list = [] - else: - calculate_sensitivities_list = calculate_sensitivities - # Make sure model isn't empty self._check_empty_model(model) @@ -772,6 +794,12 @@ def solve( self._set_up_model_inputs(model, inputs) for inputs in inputs_list ] + calculate_sensitivities_list, sensitivities_have_changed = ( + BaseSolver._solve_process_calculate_sensitivities_arg( + model_inputs_list[0], model, calculate_sensitivities + ) + ) + # (Re-)calculate consistent initialization # Assuming initial conditions do not depend on input parameters # when len(inputs_list) > 1, only `model_inputs_list[0]` @@ -792,13 +820,8 @@ def solve( "for initial conditions." ) - # Check that calculate_sensitivites have not been updated - calculate_sensitivities_list.sort() - if hasattr(model, "calculate_sensitivities"): - model.calculate_sensitivities.sort() - else: - model.calculate_sensitivities = [] - if calculate_sensitivities_list != model.calculate_sensitivities: + # if any setup configuration has changed, we need to re-set up + if sensitivities_have_changed: self._model_set_up.pop(model, None) # CasadiSolver caches its integrators using model, so delete this too if isinstance(self, pybamm.CasadiSolver): @@ -1066,6 +1089,58 @@ def _check_events_with_initialization(t_eval, model, inputs_dict): f"Events {event_names} are non-positive at initial conditions" ) + def _set_sens_initial_conditions_from( + self, solution: pybamm.Solution, model: pybamm.BaseModel + ) -> tuple: + """ + A restricted version of BaseModel.set_initial_conditions_from that only extracts the + sensitivities from a solution object, and only for a model that has been descretised. + This is used when setting the initial conditions for a sensitivity model. + + Parameters + ---------- + solution : :class:`pybamm.Solution` + The solution to use to initialize the model + + model: :class:`pybamm.BaseModel` + The model whose sensitivities to set + + Returns + ------- + + initial_conditions : tuple of ndarray + The initial conditions for the sensitivities, each element of the tuple + corresponds to an input parameter + """ + + ninputs = len(model.calculate_sensitivities) + initial_conditions = tuple([] for _ in range(ninputs)) + solution = solution.last_state + for var in model.initial_conditions: + final_state = solution[var.name] + final_state = final_state.sensitivities + final_state_eval = tuple( + final_state[key] for key in model.calculate_sensitivities + ) + + scale, reference = var.scale.value, var.reference.value + for i in range(ninputs): + scaled_final_state_eval = (final_state_eval[i] - reference) / scale + initial_conditions[i].append(scaled_final_state_eval) + + # Also update the concatenated initial conditions if the model is already + # discretised + # Unpack slices for sorting + y_slices = {var: slce for var, slce in model.y_slices.items()} + slices = [y_slices[symbol][0] for symbol in model.initial_conditions.keys()] + + # sort equations according to slices + concatenated_initial_conditions = [ + casadi.vertcat(*[eq for _, eq in sorted(zip(slices, init))]) + for init in initial_conditions + ] + return concatenated_initial_conditions + def process_t_interp(self, t_interp): # set a variable for this no_interp = (not self.supports_interp) and ( @@ -1092,6 +1167,7 @@ def step( npts=None, inputs=None, save=True, + calculate_sensitivities=False, t_interp=None, ): """ @@ -1117,6 +1193,14 @@ def step( Any input parameters to pass to the model when solving save : bool, optional Save solution with all previous timesteps. Defaults to True. + calculate_sensitivities : list of str or bool, optional + Whether the solver calculates sensitivities of all input parameters. Defaults to False. + If only a subset of sensitivities are required, can also pass a + list of input parameter names. **Limitations**: sensitivities are not calculated up to numerical tolerances + so are not guarenteed to be within the tolerances set by the solver, please raise an issue if you + require this functionality. Also, when using this feature with `pybamm.Experiment`, the sensitivities + do not take into account the movement of step-transitions wrt input parameters, so do not use this feature + if the timings of your experimental protocol change rapidly with respect to your input parameters. t_interp : None, list or ndarray, optional The times (in seconds) at which to interpolate the solution. Defaults to None. Only valid for solvers that support intra-solve interpolation (`IDAKLUSolver`). @@ -1188,8 +1272,15 @@ def step( # Set up inputs model_inputs = self._set_up_model_inputs(model, inputs) + # process calculate_sensitivities argument + calculate_sensitivities_list, sensitivities_have_changed = ( + BaseSolver._solve_process_calculate_sensitivities_arg( + model_inputs, model, calculate_sensitivities + ) + ) + first_step_this_model = model not in self._model_set_up - if first_step_this_model: + if first_step_this_model or sensitivities_have_changed: if len(self._model_set_up) > 0: existing_model = next(iter(self._model_set_up)) raise RuntimeError( @@ -1208,18 +1299,45 @@ def step( ): pybamm.logger.verbose(f"Start stepping {model.name} with {self.name}") + using_sensitivities = len(model.calculate_sensitivities) > 0 + if isinstance(old_solution, pybamm.EmptySolution): if not first_step_this_model: # reset y0 to original initial conditions self.set_up(model, model_inputs, ics_only=True) elif old_solution.all_models[-1] == model: - # initialize with old solution - model.y0 = old_solution.all_ys[-1][:, -1] + last_state = old_solution.last_state + model.y0 = last_state.all_ys[0] + if using_sensitivities and isinstance(last_state._all_sensitivities, dict): + full_sens = last_state._all_sensitivities["all"][0] + model.y0S = tuple(full_sens[:, i] for i in range(full_sens.shape[1])) + else: _, concatenated_initial_conditions = model.set_initial_conditions_from( old_solution, return_type="ics" ) model.y0 = concatenated_initial_conditions.evaluate(0, inputs=model_inputs) + if using_sensitivities: + model.y0S = self._set_sens_initial_conditions_from(old_solution, model) + + # hopefully we'll get rid of explicit sensitivities soon so we can remove this + explicit_sensitivities = model.len_rhs_sens > 0 or model.len_alg_sens > 0 + if ( + explicit_sensitivities + and using_sensitivities + and not isinstance(old_solution, pybamm.EmptySolution) + and not old_solution.all_models[-1] == model + ): + y0_list = [] + if model.len_rhs > 0: + y0_list.append(model.y0[: model.len_rhs]) + for s in model.y0S: + y0_list.append(s[: model.len_rhs]) + if model.len_alg > 0: + y0_list.append(model.y0[model.len_rhs :]) + for s in model.y0S: + y0_list.append(s[model.len_rhs :]) + model.y0 = casadi.vertcat(*y0_list) set_up_time = timer.time() diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl index 7ed4dcfad8..fd8eb38257 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl +++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl @@ -445,7 +445,7 @@ Solution IDAKLUSolverOpenMP::solve( } if (sensitivity) { - CheckErrors(IDAGetSens(ida_mem, &t_val, yyS)); + CheckErrors(IDAGetSensDky(ida_mem, t_val, 0, yyS)); } // Store Consistent initialization @@ -478,7 +478,7 @@ Solution IDAKLUSolverOpenMP::solve( bool hit_adaptive = save_adaptive_steps && retval == IDA_SUCCESS; if (sensitivity) { - CheckErrors(IDAGetSens(ida_mem, &t_val, yyS)); + CheckErrors(IDAGetSensDky(ida_mem, t_val, 0, yyS)); } if (hit_tinterp) { @@ -499,7 +499,7 @@ Solution IDAKLUSolverOpenMP::solve( // Reset the states and sensitivities at t = t_val CheckErrors(IDAGetDky(ida_mem, t_val, 0, yy)); if (sensitivity) { - CheckErrors(IDAGetSens(ida_mem, &t_val, yyS)); + CheckErrors(IDAGetSensDky(ida_mem, t_val, 0, yyS)); } } diff --git a/src/pybamm/solvers/casadi_algebraic_solver.py b/src/pybamm/solvers/casadi_algebraic_solver.py index cf44912952..2dd6f2d341 100644 --- a/src/pybamm/solvers/casadi_algebraic_solver.py +++ b/src/pybamm/solvers/casadi_algebraic_solver.py @@ -170,7 +170,7 @@ def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): model, inputs_dict, termination="final time", - sensitivities=explicit_sensitivities, + all_sensitivities=explicit_sensitivities, ) sol.integration_time = integration_time return sol diff --git a/src/pybamm/solvers/casadi_solver.py b/src/pybamm/solvers/casadi_solver.py index b4ac9d1561..89e20631dd 100644 --- a/src/pybamm/solvers/casadi_solver.py +++ b/src/pybamm/solvers/casadi_solver.py @@ -193,7 +193,7 @@ def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): y0, model, inputs_dict, - sensitivities=False, + all_sensitivities=False, ) solution.solve_time = 0 solution.integration_time = 0 @@ -478,7 +478,7 @@ def integer_bisect(): np.array([t_event]), y_event[:, np.newaxis], "event", - sensitivities=bool(model.calculate_sensitivities), + all_sensitivities=False, ) solution.integration_time = ( coarse_solution.integration_time + dense_step_sol.integration_time @@ -696,7 +696,7 @@ def _run_integrator( y_sol, model, inputs_dict, - sensitivities=extract_sensitivities_in_solution, + all_sensitivities=extract_sensitivities_in_solution, check_solution=False, ) sol.integration_time = integration_time @@ -736,7 +736,7 @@ def _run_integrator( y_sol, model, inputs_dict, - sensitivities=extract_sensitivities_in_solution, + all_sensitivities=extract_sensitivities_in_solution, check_solution=False, ) sol.integration_time = integration_time diff --git a/src/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py index 41e0c8855f..08f86b3264 100644 --- a/src/pybamm/solvers/idaklu_solver.py +++ b/src/pybamm/solvers/idaklu_solver.py @@ -818,7 +818,7 @@ def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): np.array([sol.t[-1]]), np.transpose(y_event)[:, np.newaxis], termination, - sensitivities=yS_out, + all_sensitivities=yS_out, ) newsol.integration_time = integration_time if not self.output_variables: diff --git a/src/pybamm/solvers/processed_variable.py b/src/pybamm/solvers/processed_variable.py index 8c1190c2f4..2464466348 100644 --- a/src/pybamm/solvers/processed_variable.py +++ b/src/pybamm/solvers/processed_variable.py @@ -66,7 +66,7 @@ def __init__( # Sensitivity starts off uninitialized, only set when called self._sensitivities = None - self.solution_sensitivities = solution.sensitivities + self.all_solution_sensitivities = solution._all_sensitivities # Store time self.t_pts = solution.t @@ -404,7 +404,7 @@ def sensitivities(self): return {} # Otherwise initialise and return sensitivities if self._sensitivities is None: - if self.solution_sensitivities != {}: + if self.all_solution_sensitivities: self.initialise_sensitivity_explicit_forward() else: raise ValueError( @@ -417,48 +417,54 @@ def sensitivities(self): def initialise_sensitivity_explicit_forward(self): "Set up the sensitivity dictionary" - inputs_stacked = self.all_inputs_casadi[0] - - # Set up symbolic variables - t_casadi = casadi.MX.sym("t") - y_casadi = casadi.MX.sym("y", self.all_ys[0].shape[0]) - p_casadi = { - name: casadi.MX.sym(name, value.shape[0]) - for name, value in self.all_inputs[0].items() - } - - p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()]) - # Convert variable to casadi format for differentiating - var_casadi = self.base_variables[0].to_casadi( - t_casadi, y_casadi, inputs=p_casadi - ) - dvar_dy = casadi.jacobian(var_casadi, y_casadi) - dvar_dp = casadi.jacobian(var_casadi, p_casadi_stacked) - - # Convert to functions and evaluate index-by-index - dvar_dy_func = casadi.Function( - "dvar_dy", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dy] - ) - dvar_dp_func = casadi.Function( - "dvar_dp", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dp] - ) - for index, (ts, ys) in enumerate(zip(self.all_ts, self.all_ys)): + all_S_var = [] + for ts, ys, inputs_stacked, inputs, base_variable, dy_dp in zip( + self.all_ts, + self.all_ys, + self.all_inputs_casadi, + self.all_inputs, + self.base_variables, + self.all_solution_sensitivities["all"], + ): + # Set up symbolic variables + t_casadi = casadi.MX.sym("t") + y_casadi = casadi.MX.sym("y", ys.shape[0]) + p_casadi = { + name: casadi.MX.sym(name, value.shape[0]) + for name, value in inputs.items() + } + + p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()]) + + # Convert variable to casadi format for differentiating + var_casadi = base_variable.to_casadi(t_casadi, y_casadi, inputs=p_casadi) + dvar_dy = casadi.jacobian(var_casadi, y_casadi) + dvar_dp = casadi.jacobian(var_casadi, p_casadi_stacked) + + # Convert to functions and evaluate index-by-index + dvar_dy_func = casadi.Function( + "dvar_dy", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dy] + ) + dvar_dp_func = casadi.Function( + "dvar_dp", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dp] + ) for idx, t in enumerate(ts): u = ys[:, idx] next_dvar_dy_eval = dvar_dy_func(t, u, inputs_stacked) next_dvar_dp_eval = dvar_dp_func(t, u, inputs_stacked) - if index == 0 and idx == 0: + if idx == 0: dvar_dy_eval = next_dvar_dy_eval dvar_dp_eval = next_dvar_dp_eval else: dvar_dy_eval = casadi.diagcat(dvar_dy_eval, next_dvar_dy_eval) dvar_dp_eval = casadi.vertcat(dvar_dp_eval, next_dvar_dp_eval) - # Compute sensitivity - dy_dp = self.solution_sensitivities["all"] - S_var = dvar_dy_eval @ dy_dp + dvar_dp_eval + # Compute sensitivity + S_var = dvar_dy_eval @ dy_dp + dvar_dp_eval + all_S_var.append(S_var) + S_var = casadi.vertcat(*all_S_var) sensitivities = {"all": S_var} # Add the individual sensitivity diff --git a/src/pybamm/solvers/scipy_solver.py b/src/pybamm/solvers/scipy_solver.py index 226b096887..daa8f706de 100644 --- a/src/pybamm/solvers/scipy_solver.py +++ b/src/pybamm/solvers/scipy_solver.py @@ -150,7 +150,7 @@ def event_fn(t, y): t_event, y_event, termination, - sensitivities=bool(model.calculate_sensitivities), + all_sensitivities=bool(model.calculate_sensitivities), ) sol.integration_time = integration_time return sol diff --git a/src/pybamm/solvers/solution.py b/src/pybamm/solvers/solution.py index c3c8451634..74d9ce7baf 100644 --- a/src/pybamm/solvers/solution.py +++ b/src/pybamm/solvers/solution.py @@ -2,6 +2,7 @@ # Solution class # import casadi +import copy import json import numbers import numpy as np @@ -57,11 +58,10 @@ class Solution: the event happens. termination : str String to indicate why the solution terminated - - sensitivities: bool or dict + all_sensitivities: bool or dict of lists True if sensitivities included as the solution of the explicit forwards equations. False if no sensitivities included/wanted. Dict if sensitivities are - provided as a dict of {parameter: sensitivities} pairs. + provided as a dict of {parameter: [sensitivities]} pairs. """ @@ -74,7 +74,7 @@ def __init__( t_event=None, y_event=None, termination="final time", - sensitivities=False, + all_sensitivities=False, check_solution=True, ): if not isinstance(all_ts, list): @@ -98,7 +98,18 @@ def __init__( else: self.all_inputs = all_inputs - self.sensitivities = sensitivities + if isinstance(all_sensitivities, bool): + self._all_sensitivities = all_sensitivities + elif isinstance(all_sensitivities, dict): + self._all_sensitivities = {} + for key, value in all_sensitivities.items(): + if isinstance(value, list): + self._all_sensitivities[key] = value + else: + self._all_sensitivities[key] = [value] + + else: + raise TypeError("sensitivities arg needs to be a bool or dict") # Check no ys are too large if check_solution: @@ -134,47 +145,31 @@ def __init__( # Solution now uses CasADi pybamm.citations.register("Andersson2019") - def extract_explicit_sensitivities(self): - # if we got here, we haven't set y yet - self.set_y() + def has_sensitivities(self) -> bool: + if isinstance(self._all_sensitivities, bool): + return self._all_sensitivities + elif isinstance(self._all_sensitivities, dict): + return len(self._all_sensitivities) > 0 - # extract sensitivities from full y solution - self._y, self._sensitivities = self._extract_explicit_sensitivities( - self.all_models[0], self.y, self.t, self.all_inputs[0] - ) + def extract_explicit_sensitivities(self): + self._all_sensitivities = {} - # make sure we remove all sensitivities from all_ys + # extract sensitivities from each sub-solution for index, (model, ys, ts, inputs) in enumerate( zip(self.all_models, self.all_ys, self.all_ts, self.all_inputs) ): - self._all_ys[index], _ = self._extract_explicit_sensitivities( + self._all_ys[index], sens_segment = self._extract_explicit_sensitivities( model, ys, ts, inputs ) + for key, value in sens_segment.items(): + if key in self._all_sensitivities: + self._all_sensitivities[key] = self._all_sensitivities[key] + [ + value + ] + else: + self._all_sensitivities[key] = [value] - def _extract_explicit_sensitivities(self, model, y, t_eval, inputs): - """ - given a model and a solution y, extracts the sensitivities - - Parameters - -------- - model : :class:`pybamm.BaseModel` - A model that has been already setup by this base solver - y: ndarray - The solution of the full explicit sensitivity equations - t_eval: ndarray - The evaluation times - inputs: dict - parameter inputs - - Returns - ------- - y: ndarray - The solution of the ode/dae in model - sensitivities: dict of (string: ndarray) - A dictionary of parameter names, and the corresponding solution of - the sensitivity equations - """ - + def _extract_sensitivity_matrix(self, model, y): n_states = model.len_rhs_and_alg n_rhs = model.len_rhs n_alg = model.len_alg @@ -185,7 +180,6 @@ def _extract_explicit_sensitivities(self, model, y, t_eval, inputs): n_p = model.len_alg_sens // model.len_alg len_rhs_and_sens = model.len_rhs + model.len_rhs_sens - n_t = len(t_eval) # y gets the part of the solution vector that correspond to the # actual ODE/DAE solution @@ -211,6 +205,8 @@ def _extract_explicit_sensitivities(self, model, y, t_eval, inputs): y_full = y.full() else: y_full = y + + n_t = y.shape[1] ode_sens = y_full[n_rhs:len_rhs_and_sens, :].reshape(n_p, n_rhs, n_t) alg_sens = y_full[len_rhs_and_sens + n_alg :, :].reshape(n_p, n_alg, n_t) # 2. Concatenate into a single 3D matrix with shape (n_p, n_states, n_t) @@ -221,6 +217,44 @@ def _extract_explicit_sensitivities(self, model, y, t_eval, inputs): n_t * n_states, n_p ) + # convert back to casadi (todo: this is not very efficient, should refactor + # to avoid this) + full_sens_matrix = casadi.DM(full_sens_matrix) + + y_dae = np.vstack( + [ + y[: model.len_rhs, :], + y[len_rhs_and_sens : len_rhs_and_sens + model.len_alg, :], + ] + ) + return y_dae, full_sens_matrix + + def _extract_explicit_sensitivities(self, model, y, t_eval, inputs): + """ + given a model and a solution y, extracts the sensitivities + + Parameters + -------- + model : :class:`pybamm.BaseModel` + A model that has been already setup by this base solver + y: ndarray + The solution of the full explicit sensitivity equations + t_eval: ndarray + The evaluation times + inputs: dict + parameter inputs + + Returns + ------- + y: ndarray + The solution of the ode/dae in model + sensitivities: dict of (string: ndarray) + A dictionary of parameter names, and the corresponding solution of + the sensitivity equations + """ + + y_dae, full_sens_matrix = self._extract_sensitivity_matrix(model, y) + # Save the full sensitivity matrix sensitivity = {"all": full_sens_matrix} @@ -234,12 +268,6 @@ def _extract_explicit_sensitivities(self, model, y, t_eval, inputs): sensitivity[name] = full_sens_matrix[:, start:end] start = end - y_dae = np.vstack( - [ - y[: model.len_rhs, :], - y[len_rhs_and_sens : len_rhs_and_sens + model.len_alg, :], - ] - ) return y_dae, sensitivity @property @@ -262,31 +290,56 @@ def y(self): try: return self._y except AttributeError: - self.set_y() - # if y is evaluated before sensitivities then need to extract them - if isinstance(self._sensitivities, bool) and self._sensitivities: + if isinstance(self._all_sensitivities, bool) and self._all_sensitivities: self.extract_explicit_sensitivities() + self.set_y() + return self._y @property def sensitivities(self): """Values of the sensitivities. Returns a dict of param_name: np_array""" - if isinstance(self._sensitivities, bool): - if self._sensitivities: - self.extract_explicit_sensitivities() - else: - self._sensitivities = {} + try: + return self._sensitivities + except AttributeError: + self.set_sensitivities() return self._sensitivities @sensitivities.setter def sensitivities(self, value): - """Updates the sensitivity""" + """Updates the sensitivity if False or True. Raises an error if sensitivities are a dict""" # sensitivities must be a dict or bool - if not isinstance(value, (bool, dict)): - raise TypeError("sensitivities arg needs to be a bool or dict") - self._sensitivities = value + if not isinstance(value, bool): + raise TypeError("sensitivities arg needs to be a bool") + + if isinstance(self._all_sensitivities, dict): + raise NotImplementedError( + "Setting sensitivities is not supported if sensitivities are " + "already provided as a dict of {parameter: sensitivities} pairs." + ) + + self._all_sensitivities = value + + def set_sensitivities(self): + if not self.has_sensitivities(): + self._sensitivities = {} + return + + # extract sensitivities if they are not already extracted + if isinstance(self._all_sensitivities, bool) and self._all_sensitivities: + self.extract_explicit_sensitivities() + + is_casadi = isinstance( + next(iter(self._all_sensitivities.values()))[0], (casadi.DM, casadi.MX) + ) + self._sensitivities = {} + for key, sens in self._all_sensitivities.items(): + if is_casadi: + self._sensitivities[key] = casadi.vertcat(*sens) + else: + self._sensitivities[key] = np.vstack(sens) def set_y(self): try: @@ -374,6 +427,13 @@ def first_state(self): than the full solution when only the first state is needed (e.g. to initialize a model with the solution) """ + if isinstance(self._all_sensitivities, bool): + sensitivities = self._all_sensitivities + elif isinstance(self._all_sensitivities, dict): + sensitivities = {} + n_states = self.all_models[0].len_rhs_and_alg + for key in self._all_sensitivities: + sensitivities[key] = self._all_sensitivities[key][0][-n_states:, :] new_sol = Solution( self.all_ts[0][:1], self.all_ys[0][:, :1], @@ -382,6 +442,7 @@ def first_state(self): None, None, "final time", + all_sensitivities=sensitivities, ) new_sol._all_inputs_casadi = self.all_inputs_casadi[:1] new_sol._sub_solutions = self.sub_solutions[:1] @@ -399,6 +460,13 @@ def last_state(self): than the full solution when only the final state is needed (e.g. to initialize a model with the solution) """ + if isinstance(self._all_sensitivities, bool): + sensitivities = self._all_sensitivities + elif isinstance(self._all_sensitivities, dict): + sensitivities = {} + n_states = self.all_models[-1].len_rhs_and_alg + for key in self._all_sensitivities: + sensitivities[key] = self._all_sensitivities[key][-1][-n_states:, :] new_sol = Solution( self.all_ts[-1][-1:], self.all_ys[-1][:, -1:], @@ -407,10 +475,10 @@ def last_state(self): self.t_event, self.y_event, self.termination, + all_sensitivities=sensitivities, ) new_sol._all_inputs_casadi = self.all_inputs_casadi[-1:] new_sol._sub_solutions = self.sub_solutions[-1:] - new_sol.solve_time = 0 new_sol.integration_time = 0 new_sol.set_up_time = 0 @@ -457,7 +525,7 @@ def set_summary_variables(self, all_summary_variables): def update(self, variables): """Add ProcessedVariables to the dictionary of variables in the solution""" # make sure that sensitivities are extracted if required - if isinstance(self._sensitivities, bool) and self._sensitivities: + if isinstance(self._all_sensitivities, bool) and self._all_sensitivities: self.extract_explicit_sensitivities() # Convert single entry to list @@ -758,6 +826,30 @@ def __add__(self, other): all_ts = self.all_ts + other.all_ts all_ys = self.all_ys + other.all_ys + # sensitivities can be: + # - bool if not using sensitivities or using explicit sensitivities which still + # need to be extracted + # - dict if sensitivities are provided as a dict of {parameter: sensitivities} + # both self and other should have the same type of sensitivities + # OR both can be either False or {} (i.e. no sensitivities) + if isinstance(self._all_sensitivities, bool) and isinstance( + other._all_sensitivities, bool + ): + all_sensitivities = self._all_sensitivities or other._all_sensitivities + elif isinstance(self._all_sensitivities, dict) and isinstance( + other._all_sensitivities, dict + ): + all_sensitivities = self._all_sensitivities + # we can assume that the keys are the same for both solutions + for key in other._all_sensitivities: + all_sensitivities[key] = ( + all_sensitivities[key] + other._all_sensitivities[key] + ) + elif not self._all_sensitivities and not other._all_sensitivities: + all_sensitivities = {} + else: + raise ValueError("Sensitivities must be of the same type") + new_sol = Solution( all_ts, all_ys, @@ -766,15 +858,19 @@ def __add__(self, other): other.t_event, other.y_event, other.termination, - bool(self.sensitivities), + all_sensitivities=all_sensitivities, ) new_sol.closest_event_idx = other.closest_event_idx new_sol._all_inputs_casadi = self.all_inputs_casadi + other.all_inputs_casadi - # Set solution time - new_sol.solve_time = self.solve_time + other.solve_time - new_sol.integration_time = self.integration_time + other.integration_time + # Add timers (if available) + for attr in ["solve_time", "integration_time", "set_up_time"]: + if ( + getattr(self, attr, None) is not None + and getattr(other, attr, None) is not None + ): + setattr(new_sol, attr, getattr(self, attr) + getattr(other, attr)) # Set sub_solutions new_sol._sub_solutions = self.sub_solutions + other.sub_solutions @@ -787,12 +883,14 @@ def __radd__(self, other): def copy(self): new_sol = self.__class__( self.all_ts, - self.all_ys, + # need to copy y in case it is modified by extract explicit sensitivities + [copy.copy(y) for y in self.all_ys], self.all_models, self.all_inputs, self.t_event, self.y_event, self.termination, + self._all_sensitivities, ) new_sol._all_inputs_casadi = self.all_inputs_casadi new_sol._sub_solutions = self.sub_solutions @@ -902,6 +1000,7 @@ def make_cycle_solution( sum_sols.t_event, sum_sols.y_event, sum_sols.termination, + sum_sols._all_sensitivities, ) cycle_solution._all_inputs_casadi = sum_sols.all_inputs_casadi cycle_solution._sub_solutions = sum_sols.sub_solutions diff --git a/tests/integration/test_models/standard_output_tests.py b/tests/integration/test_models/standard_output_tests.py index 83b88c0ff0..ca5e27607d 100644 --- a/tests/integration/test_models/standard_output_tests.py +++ b/tests/integration/test_models/standard_output_tests.py @@ -449,7 +449,7 @@ def test_conservation(self): # this seems to be linked to using constant concentration but not sure why decimal = 12 elif self.model.options["particle phases"] != "1": - decimal = 13 + decimal = 9 elif "current-driven" in self.model.options["loss of active material"]: # current driven LAM model doesn't perfectly conserve lithium, not sure why decimal = 9 diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py index 60e8dfb819..7c176249fd 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py @@ -375,3 +375,161 @@ def temp_drive_cycle(y, z, t): model = self.model() modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all(skip_output_tests=True) + + def test_composite_stress_driven_LAM(self): + options = { + "particle phases": ("2", "1"), + "open-circuit potential": (("single", "current sigmoid"), "single"), + "loss of active material": "stress-driven", + } + + # taken from Ai2020 + def graphite_volume_change_Ai2020(sto): + p1 = 145.907 + p2 = -681.229 + p3 = 1334.442 + p4 = -1415.710 + p5 = 873.906 + p6 = -312.528 + p7 = 60.641 + p8 = -5.706 + p9 = 0.386 + p10 = -4.966e-05 + t_change = ( + p1 * sto**9 + + p2 * sto**8 + + p3 * sto**7 + + p4 * sto**6 + + p5 * sto**5 + + p6 * sto**4 + + p7 * sto**3 + + p8 * sto**2 + + p9 * sto + + p10 + ) + return t_change + + # taken from Ai2020 + def lico2_volume_change_Ai2020(sto): + omega = pybamm.Parameter( + "Positive electrode partial molar volume [m3.mol-1]" + ) + c_s_max = pybamm.Parameter( + "Maximum concentration in positive electrode [mol.m-3]" + ) + t_change = omega * c_s_max * sto + return t_change + + # use Chen2020 composite and add Ai2020 stress-driven parameters + parameter_values = pybamm.ParameterValues("Chen2020_composite") + parameter_values.update( + { + "Primary: Negative electrode LAM constant proportional term [s-1]": 1e-4 + / 3600, + "Secondary: Negative electrode LAM constant proportional term [s-1]": 1e-4 + / 3600, + "Positive electrode LAM constant proportional term [s-1]": 1e-4 / 3600, + "Primary: Negative electrode partial molar volume [m3.mol-1]": 3.1e-06, + "Primary: Negative electrode Young's modulus [Pa]": 15000000000.0, + "Primary: Negative electrode Poisson's ratio": 0.3, + "Primary: Negative electrode critical stress [Pa]": 60000000.0, + "Secondary: Negative electrode critical stress [Pa]": 60000000.0, + "Primary: Negative electrode LAM constant exponential term": 2.0, + "Secondary: Negative electrode LAM constant exponential term": 2.0, + "Secondary: Negative electrode partial molar volume [m3.mol-1]": 3.1e-06, + "Secondary: Negative electrode Young's modulus [Pa]": 15000000000.0, + "Secondary: Negative electrode Poisson's ratio": 0.3, + "Negative electrode reference concentration for free of deformation [mol.m-3]": 0.0, + "Primary: Negative electrode volume change": graphite_volume_change_Ai2020, + "Secondary: Negative electrode volume change": graphite_volume_change_Ai2020, + "Positive electrode partial molar volume [m3.mol-1]": -7.28e-07, + "Positive electrode Young's modulus [Pa]": 375000000000.0, + "Positive electrode Poisson's ratio": 0.2, + "Positive electrode critical stress [Pa]": 375000000.0, + "Positive electrode LAM constant exponential term": 2.0, + "Positive electrode reference concentration for free of deformation [mol.m-3]": 0.0, + "Positive electrode volume change": lico2_volume_change_Ai2020, + }, + check_already_exists=False, + ) + + self.run_basic_processing_test(options, parameter_values=parameter_values) + + def test_composite_reaction_driven_LAM(self): + options = { + "particle phases": ("2", "1"), + "open-circuit potential": (("single", "current sigmoid"), "single"), + "loss of active material": "reaction-driven", + } + + # taken from Ai2020 + def graphite_volume_change_Ai2020(sto): + p1 = 145.907 + p2 = -681.229 + p3 = 1334.442 + p4 = -1415.710 + p5 = 873.906 + p6 = -312.528 + p7 = 60.641 + p8 = -5.706 + p9 = 0.386 + p10 = -4.966e-05 + t_change = ( + p1 * sto**9 + + p2 * sto**8 + + p3 * sto**7 + + p4 * sto**6 + + p5 * sto**5 + + p6 * sto**4 + + p7 * sto**3 + + p8 * sto**2 + + p9 * sto + + p10 + ) + return t_change + + # taken from Ai2020 + def lico2_volume_change_Ai2020(sto): + omega = pybamm.Parameter( + "Positive electrode partial molar volume [m3.mol-1]" + ) + c_s_max = pybamm.Parameter( + "Maximum concentration in positive electrode [mol.m-3]" + ) + t_change = omega * c_s_max * sto + return t_change + + # use Chen2020 composite and add Ai2020 stress-driven parameters + parameter_values = pybamm.ParameterValues("Chen2020_composite") + parameter_values.update( + { + "Primary: Negative electrode LAM constant proportional term [s-1]": 1e-4 + / 3600, + "Secondary: Negative electrode LAM constant proportional term [s-1]": 1e-4 + / 3600, + "Positive electrode LAM constant proportional term [s-1]": 1e-4 / 3600, + "Primary: Negative electrode partial molar volume [m3.mol-1]": 3.1e-06, + "Primary: Negative electrode Young's modulus [Pa]": 15000000000.0, + "Primary: Negative electrode Poisson's ratio": 0.3, + "Primary: Negative electrode critical stress [Pa]": 60000000.0, + "Secondary: Negative electrode critical stress [Pa]": 60000000.0, + "Primary: Negative electrode LAM constant exponential term": 2.0, + "Secondary: Negative electrode LAM constant exponential term": 2.0, + "Secondary: Negative electrode partial molar volume [m3.mol-1]": 3.1e-06, + "Secondary: Negative electrode Young's modulus [Pa]": 15000000000.0, + "Secondary: Negative electrode Poisson's ratio": 0.3, + "Negative electrode reference concentration for free of deformation [mol.m-3]": 0.0, + "Primary: Negative electrode volume change": graphite_volume_change_Ai2020, + "Secondary: Negative electrode volume change": graphite_volume_change_Ai2020, + "Positive electrode partial molar volume [m3.mol-1]": -7.28e-07, + "Positive electrode Young's modulus [Pa]": 375000000000.0, + "Positive electrode Poisson's ratio": 0.2, + "Positive electrode critical stress [Pa]": 375000000.0, + "Positive electrode LAM constant exponential term": 2.0, + "Positive electrode reference concentration for free of deformation [mol.m-3]": 0.0, + "Positive electrode volume change": lico2_volume_change_Ai2020, + }, + check_already_exists=False, + ) + + self.run_basic_processing_test(options, parameter_values=parameter_values) diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py index 7d457fd7dc..121bc3a018 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py @@ -26,3 +26,9 @@ def test_composite_graphite_silicon(self): def test_composite_graphite_silicon_sei(self): pass # skip this test + + def test_composite_reaction_driven_LAM(self): + pass # skip this test + + def test_composite_stress_driven_LAM(self): + pass # skip this test diff --git a/tests/unit/test_citations.py b/tests/unit/test_citations.py index 7133cf234a..c87912490f 100644 --- a/tests/unit/test_citations.py +++ b/tests/unit/test_citations.py @@ -296,7 +296,7 @@ def test_reniers_2019(self): citations._reset() assert "Reniers2019" not in citations._papers_to_cite - pybamm.active_material.LossActiveMaterial(None, "negative", None, True) + pybamm.active_material.LossActiveMaterial(None, "negative", None, True, None) assert "Reniers2019" in citations._papers_to_cite assert "Reniers2019" in citations._citation_tags.keys() diff --git a/tests/unit/test_experiments/test_simulation_with_experiment.py b/tests/unit/test_experiments/test_simulation_with_experiment.py index 3507d6e5c1..4f981ba04c 100644 --- a/tests/unit/test_experiments/test_simulation_with_experiment.py +++ b/tests/unit/test_experiments/test_simulation_with_experiment.py @@ -201,6 +201,83 @@ def test_run_experiment_cccv_solvers(self): ) self.assertEqual(solutions[1].termination, "final time") + @unittest.skipIf(not pybamm.has_idaklu(), "idaklu solver is not installed") + def test_solve_with_sensitivities_and_experiment(self): + experiment_2step = 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 30 min", + "Discharge at 2 W for 30 min", # repeat to cover this case (changes initialisation) + ), + ] + * 2, + ) + + solutions = [] + for solver in [ + pybamm.CasadiSolver(), + pybamm.IDAKLUSolver(), + pybamm.ScipySolver(), + ]: + for calculate_sensitivities in [False, True]: + model = pybamm.lithium_ion.SPM() + param = model.default_parameter_values + input_param_name = "Negative electrode active material volume fraction" + input_param_value = param[input_param_name] + param.update({input_param_name: "[input]"}) + sim = pybamm.Simulation( + model, + experiment=experiment_2step, + solver=solver, + parameter_values=param, + ) + solution = sim.solve( + inputs={input_param_name: input_param_value}, + calculate_sensitivities=calculate_sensitivities, + ) + solutions.append(solution) + + # check solutions are the same, leave out the last solution point as it is slightly different + # for each solve due to numerical errors + # TODO: scipy solver does not work for this experiment, with or without sensitivities, + # so we skip this test for now + for i in range(1, len(solutions) - 2): + np.testing.assert_allclose( + solutions[0]["Voltage [V]"].data[:-1], + solutions[i]["Voltage [V]"](solutions[0].t[:-1]), + rtol=5e-2, + equal_nan=True, + ) + + # check sensitivities are roughly the same. Sundials isn't doing error control on the sensitivities + # by default, and the solution can be quite coarse for quickly changing sensitivities + sens_casadi = ( + solutions[1]["Voltage [V]"] + .sensitivities[input_param_name][:-2] + .full() + .flatten() + ) + sens_idaklu = np.interp( + solutions[1].t[:-2], + solutions[3].t, + solutions[3]["Voltage [V]"] + .sensitivities[input_param_name] + .full() + .flatten(), + ) + rtol = 1e-1 + atol = 1e-2 + error = np.sqrt( + np.sum( + ((sens_casadi - sens_idaklu) / (rtol * np.abs(sens_casadi) + atol)) ** 2 + ) + / len(sens_casadi) + ) + self.assertLess(error, 1.0) + def test_run_experiment_drive_cycle(self): drive_cycle = np.array([np.arange(10), np.arange(10)]).T experiment = pybamm.Experiment( diff --git a/tests/unit/test_expression_tree/test_symbolic_diff.py b/tests/unit/test_expression_tree/test_symbolic_diff.py index fb08740305..e03953e667 100644 --- a/tests/unit/test_expression_tree/test_symbolic_diff.py +++ b/tests/unit/test_expression_tree/test_symbolic_diff.py @@ -2,13 +2,13 @@ # Tests for the symbolic differentiation methods # +import pytest import numpy as np import pybamm -import unittest from numpy import testing -class TestSymbolicDifferentiation(unittest.TestCase): +class TestSymbolicDifferentiation: def test_advanced(self): a = pybamm.StateVector(slice(0, 1)) b = pybamm.StateVector(slice(1, 2)) @@ -16,14 +16,14 @@ def test_advanced(self): # func = (a * 2 + 5 * (-b)) / (a * b) - self.assertEqual(func.diff(a).evaluate(y=y), 1 / 5) - self.assertEqual(func.diff(b).evaluate(y=y), -2 / 9) + assert func.diff(a).evaluate(y=y) == 1 / 5 + assert func.diff(b).evaluate(y=y) == -2 / 9 # func = a * b**a testing.assert_array_almost_equal( func.diff(a).evaluate(y=y)[0], 3**5 * (5 * np.log(3) + 1) ) - self.assertEqual(func.diff(b).evaluate(y=y), 5**2 * 3**4) + assert func.diff(b).evaluate(y=y) == 5**2 * 3**4 def test_advanced_functions(self): a = pybamm.StateVector(slice(0, 1)) @@ -32,84 +32,68 @@ def test_advanced_functions(self): # func = a * pybamm.exp(b) - self.assertAlmostEqual(func.diff(a).evaluate(y=y)[0], np.exp(3)) + assert func.diff(a).evaluate(y=y)[0] == pytest.approx(np.exp(3)) func = pybamm.exp(a + 2 * b + a * b) + a * pybamm.exp(b) - self.assertEqual( - func.diff(a).evaluate(y=y), (4 * np.exp(3 * 5 + 5 + 2 * 3) + np.exp(3)) - ) - self.assertEqual( - func.diff(b).evaluate(y=y), np.exp(3) * (7 * np.exp(3 * 5 + 5 + 3) + 5) - ) + assert func.diff(a).evaluate(y=y) == (4 * np.exp(3 * 5 + 5 + 2 * 3) + np.exp(3)) + assert func.diff(b).evaluate(y=y) == np.exp(3) * (7 * np.exp(3 * 5 + 5 + 3) + 5) # func = pybamm.sin(pybamm.cos(a * 4) / 2) * pybamm.cos(4 * pybamm.exp(b / 3)) - self.assertEqual( - func.diff(a).evaluate(y=y), - -2 * np.sin(20) * np.cos(np.cos(20) / 2) * np.cos(4 * np.exp(1)), - ) - self.assertEqual( - func.diff(b).evaluate(y=y), - -4 / 3 * np.exp(1) * np.sin(4 * np.exp(1)) * np.sin(np.cos(20) / 2), - ) + assert func.diff(a).evaluate(y=y) == -2 * np.sin(20) * np.cos( + np.cos(20) / 2 + ) * np.cos(4 * np.exp(1)) + assert func.diff(b).evaluate(y=y) == -4 / 3 * np.exp(1) * np.sin( + 4 * np.exp(1) + ) * np.sin(np.cos(20) / 2) # func = pybamm.sin(a * b) - self.assertEqual(func.diff(a).evaluate(y=y), 3 * np.cos(15)) + assert func.diff(a).evaluate(y=y) == 3 * np.cos(15) def test_diff_zero(self): a = pybamm.StateVector(slice(0, 1)) b = pybamm.StateVector(slice(1, 2)) func = (a * 2 + 5 * (-a)) / (a * a) - self.assertEqual(func.diff(b), pybamm.Scalar(0)) - self.assertNotEqual(func.diff(a), pybamm.Scalar(0)) + assert func.diff(b) == pybamm.Scalar(0) + assert func.diff(a) != pybamm.Scalar(0) def test_diff_state_vector_dot(self): a = pybamm.StateVectorDot(slice(0, 1)) b = pybamm.StateVector(slice(1, 2)) - self.assertEqual(a.diff(a), pybamm.Scalar(1)) - self.assertEqual(a.diff(b), pybamm.Scalar(0)) + assert a.diff(a) == pybamm.Scalar(1) + assert a.diff(b) == pybamm.Scalar(0) def test_diff_heaviside(self): a = pybamm.Scalar(1) b = pybamm.StateVector(slice(0, 1)) func = (a < b) * (2 * b) - self.assertEqual(func.diff(b).evaluate(y=np.array([2])), 2) - self.assertEqual(func.diff(b).evaluate(y=np.array([-2])), 0) + assert func.diff(b).evaluate(y=np.array([2])) == 2 + assert func.diff(b).evaluate(y=np.array([-2])) == 0 def test_diff_modulo(self): a = pybamm.Scalar(3) b = pybamm.StateVector(slice(0, 1)) func = (a % b) * (b**2) - self.assertEqual(func.diff(b).evaluate(y=np.array([2])), 0) - self.assertEqual(func.diff(b).evaluate(y=np.array([5])), 30) - self.assertEqual(func.diff(b).evaluate(y=np.array([-2])), 12) + assert func.diff(b).evaluate(y=np.array([2])) == 0 + assert func.diff(b).evaluate(y=np.array([5])) == 30 + assert func.diff(b).evaluate(y=np.array([-2])) == 12 def test_diff_maximum_minimum(self): a = pybamm.Scalar(1) b = pybamm.StateVector(slice(0, 1)) func = pybamm.minimum(a, b**3) - self.assertEqual(func.diff(b).evaluate(y=np.array([10])), 0) - self.assertEqual(func.diff(b).evaluate(y=np.array([2])), 0) - self.assertEqual(func.diff(b).evaluate(y=np.array([-2])), 3 * (-2) ** 2) + assert func.diff(b).evaluate(y=np.array([10])) == 0 + assert func.diff(b).evaluate(y=np.array([2])) == 0 + assert func.diff(b).evaluate(y=np.array([-2])) == 3 * (-2) ** 2 func = pybamm.maximum(a, b**3) - self.assertEqual(func.diff(b).evaluate(y=np.array([10])), 3 * 10**2) - self.assertEqual(func.diff(b).evaluate(y=np.array([2])), 3 * 2**2) - self.assertEqual(func.diff(b).evaluate(y=np.array([-2])), 0) + assert func.diff(b).evaluate(y=np.array([10])) == 3 * 10**2 + assert func.diff(b).evaluate(y=np.array([2])) == 3 * 2**2 + assert func.diff(b).evaluate(y=np.array([-2])) == 0 def test_exceptions(self): a = pybamm.Symbol("a") b = pybamm.Symbol("b") - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): a._diff(b) - - -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_unary_operators.py b/tests/unit/test_expression_tree/test_unary_operators.py index 39cd05cf1d..812cbd8f6b 100644 --- a/tests/unit/test_expression_tree/test_unary_operators.py +++ b/tests/unit/test_expression_tree/test_unary_operators.py @@ -1,9 +1,7 @@ # # Tests for the Unary Operator classes # -import unittest - -import unittest.mock as mock +import pytest import numpy as np from scipy.sparse import diags @@ -15,49 +13,48 @@ import pybamm -class TestUnaryOperators(unittest.TestCase): +class TestUnaryOperators: def test_unary_operator(self): a = pybamm.Symbol("a", domain=["test"]) un = pybamm.UnaryOperator("unary test", a) - self.assertEqual(un.children[0].name, a.name) - self.assertEqual(un.domain, a.domain) + assert un.children[0].name == a.name + assert un.domain == a.domain # with number a = pybamm.InputParameter("a") absval = pybamm.AbsoluteValue(-a) - self.assertEqual(absval.evaluate(inputs={"a": 10}), 10) + assert absval.evaluate(inputs={"a": 10}) == 10 - def test_negation(self): + def test_negation(self, mocker): a = pybamm.Symbol("a") nega = pybamm.Negate(a) - self.assertEqual(nega.name, "-") - self.assertEqual(nega.children[0].name, a.name) + assert nega.name == "-" + assert nega.children[0].name == a.name b = pybamm.Scalar(4) negb = pybamm.Negate(b) - self.assertEqual(negb.evaluate(), -4) + assert negb.evaluate() == -4 # Test broadcast gets switched broad_a = pybamm.PrimaryBroadcast(a, "test") neg_broad = -broad_a - self.assertEqual(neg_broad, pybamm.PrimaryBroadcast(nega, "test")) + assert neg_broad == pybamm.PrimaryBroadcast(nega, "test") broad_a = pybamm.FullBroadcast(a, "test", "test2") neg_broad = -broad_a - self.assertEqual(neg_broad, pybamm.FullBroadcast(nega, "test", "test2")) + assert neg_broad == pybamm.FullBroadcast(nega, "test", "test2") # Test recursion broad_a = pybamm.PrimaryBroadcast(pybamm.PrimaryBroadcast(a, "test"), "test2") neg_broad = -broad_a - self.assertEqual( - neg_broad, - pybamm.PrimaryBroadcast(pybamm.PrimaryBroadcast(nega, "test"), "test2"), + assert neg_broad == pybamm.PrimaryBroadcast( + pybamm.PrimaryBroadcast(nega, "test"), "test2" ) # Test from_json input_json = { "name": "-", - "id": mock.ANY, + "id": mocker.ANY, "domains": { "primary": [], "secondary": [], @@ -66,39 +63,38 @@ def test_negation(self): }, "children": [a], } - self.assertEqual(pybamm.Negate._from_json(input_json), nega) + assert pybamm.Negate._from_json(input_json) == nega - def test_absolute(self): + def test_absolute(self, mocker): a = pybamm.Symbol("a") absa = pybamm.AbsoluteValue(a) - self.assertEqual(absa.name, "abs") - self.assertEqual(absa.children[0].name, a.name) + assert absa.name == "abs" + assert absa.children[0].name == a.name b = pybamm.Scalar(-4) absb = pybamm.AbsoluteValue(b) - self.assertEqual(absb.evaluate(), 4) + assert absb.evaluate() == 4 # Test broadcast gets switched broad_a = pybamm.PrimaryBroadcast(a, "test") abs_broad = abs(broad_a) - self.assertEqual(abs_broad, pybamm.PrimaryBroadcast(absa, "test")) + assert abs_broad == pybamm.PrimaryBroadcast(absa, "test") broad_a = pybamm.FullBroadcast(a, "test", "test2") abs_broad = abs(broad_a) - self.assertEqual(abs_broad, pybamm.FullBroadcast(absa, "test", "test2")) + assert abs_broad == pybamm.FullBroadcast(absa, "test", "test2") # Test recursion broad_a = pybamm.PrimaryBroadcast(pybamm.PrimaryBroadcast(a, "test"), "test2") abs_broad = abs(broad_a) - self.assertEqual( - abs_broad, - pybamm.PrimaryBroadcast(pybamm.PrimaryBroadcast(absa, "test"), "test2"), + assert abs_broad == pybamm.PrimaryBroadcast( + pybamm.PrimaryBroadcast(absa, "test"), "test2" ) # Test from_json input_json = { "name": "abs", - "id": mock.ANY, + "id": mocker.ANY, "domains": { "primary": [], "secondary": [], @@ -107,24 +103,23 @@ def test_absolute(self): }, "children": [a], } - self.assertEqual(pybamm.AbsoluteValue._from_json(input_json), absa) + assert pybamm.AbsoluteValue._from_json(input_json) == absa def test_smooth_absolute_value(self): a = pybamm.StateVector(slice(0, 1)) expr = pybamm.smooth_absolute_value(a, 10) - self.assertAlmostEqual(expr.evaluate(y=np.array([1]))[0, 0], 1) - self.assertEqual(expr.evaluate(y=np.array([0])), 0) - self.assertAlmostEqual(expr.evaluate(y=np.array([-1]))[0, 0], 1) - self.assertEqual( - str(expr), - "y[0:1] * (exp(10.0 * y[0:1]) - exp(-10.0 * y[0:1])) " - "/ (exp(10.0 * y[0:1]) + exp(-10.0 * y[0:1]))", + assert expr.evaluate(y=np.array([1]))[0, 0] == pytest.approx(1) + assert expr.evaluate(y=np.array([0])) == 0 + assert expr.evaluate(y=np.array([-1]))[0, 0] == pytest.approx(1) + assert ( + str(expr) == "y[0:1] * (exp(10.0 * y[0:1]) - exp(-10.0 * y[0:1])) " + "/ (exp(10.0 * y[0:1]) + exp(-10.0 * y[0:1]))" ) def test_sign(self): b = pybamm.Scalar(-4) signb = pybamm.sign(b) - self.assertEqual(signb.evaluate(), -1) + assert signb.evaluate() == -1 A = diags(np.linspace(-1, 1, 5)) b = pybamm.Matrix(A) @@ -134,40 +129,37 @@ def test_sign(self): ) broad = pybamm.PrimaryBroadcast(-4, "test domain") - self.assertEqual(pybamm.sign(broad), pybamm.PrimaryBroadcast(-1, "test domain")) + assert pybamm.sign(broad) == pybamm.PrimaryBroadcast(-1, "test domain") conc = pybamm.Concatenation(broad, pybamm.PrimaryBroadcast(2, "another domain")) - self.assertEqual( - pybamm.sign(conc), - pybamm.Concatenation( - pybamm.PrimaryBroadcast(-1, "test domain"), - pybamm.PrimaryBroadcast(1, "another domain"), - ), + assert pybamm.sign(conc) == pybamm.Concatenation( + pybamm.PrimaryBroadcast(-1, "test domain"), + pybamm.PrimaryBroadcast(1, "another domain"), ) # Test from_json - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): # signs are always scalar/array types in a discretised model pybamm.Sign._from_json({}) - def test_floor(self): + def test_floor(self, mocker): a = pybamm.Symbol("a") floora = pybamm.Floor(a) - self.assertEqual(floora.name, "floor") - self.assertEqual(floora.children[0].name, a.name) + assert floora.name == "floor" + assert floora.children[0].name == a.name b = pybamm.Scalar(3.5) floorb = pybamm.Floor(b) - self.assertEqual(floorb.evaluate(), 3) + assert floorb.evaluate() == 3 c = pybamm.Scalar(-3.2) floorc = pybamm.Floor(c) - self.assertEqual(floorc.evaluate(), -4) + assert floorc.evaluate() == -4 # Test from_json input_json = { "name": "floor", - "id": mock.ANY, + "id": mocker.ANY, "domains": { "primary": [], "secondary": [], @@ -176,26 +168,26 @@ def test_floor(self): }, "children": [a], } - self.assertEqual(pybamm.Floor._from_json(input_json), floora) + assert pybamm.Floor._from_json(input_json) == floora - def test_ceiling(self): + def test_ceiling(self, mocker): a = pybamm.Symbol("a") ceila = pybamm.Ceiling(a) - self.assertEqual(ceila.name, "ceil") - self.assertEqual(ceila.children[0].name, a.name) + assert ceila.name == "ceil" + assert ceila.children[0].name == a.name b = pybamm.Scalar(3.5) ceilb = pybamm.Ceiling(b) - self.assertEqual(ceilb.evaluate(), 4) + assert ceilb.evaluate() == 4 c = pybamm.Scalar(-3.2) ceilc = pybamm.Ceiling(c) - self.assertEqual(ceilc.evaluate(), -3) + assert ceilc.evaluate() == -3 # Test from_json input_json = { "name": "ceil", - "id": mock.ANY, + "id": mocker.ANY, "domains": { "primary": [], "secondary": [], @@ -204,81 +196,77 @@ def test_ceiling(self): }, "children": [a], } - self.assertEqual(pybamm.Ceiling._from_json(input_json), ceila) + assert pybamm.Ceiling._from_json(input_json) == ceila def test_gradient(self): # gradient of scalar symbol should fail a = pybamm.Symbol("a") - with self.assertRaisesRegex( - pybamm.DomainError, "Cannot take gradient of 'a' since its domain is empty" + with pytest.raises( + pybamm.DomainError, + match="Cannot take gradient of 'a' since its domain is empty", ): pybamm.Gradient(a) # gradient of variable evaluating on edges should fail a = pybamm.PrimaryBroadcastToEdges(pybamm.Scalar(1), "test") - with self.assertRaisesRegex(TypeError, "evaluates on edges"): + with pytest.raises(TypeError, match="evaluates on edges"): pybamm.Gradient(a) # gradient of broadcast should return broadcasted zero a = pybamm.PrimaryBroadcast(pybamm.Variable("a"), "test domain") grad = pybamm.grad(a) - self.assertEqual(grad, pybamm.PrimaryBroadcastToEdges(0, "test domain")) + assert grad == pybamm.PrimaryBroadcastToEdges(0, "test domain") # gradient of a secondary broadcast moves the secondary out of the gradient a = pybamm.Symbol("a", domain="test domain") a_broad = pybamm.SecondaryBroadcast(a, "another domain") grad = pybamm.grad(a_broad) - self.assertEqual( - grad, pybamm.SecondaryBroadcast(pybamm.grad(a), "another domain") - ) + assert grad == pybamm.SecondaryBroadcast(pybamm.grad(a), "another domain") # otherwise gradient should work a = pybamm.Symbol("a", domain="test domain") grad = pybamm.Gradient(a) - self.assertEqual(grad.children[0].name, a.name) - self.assertEqual(grad.domain, a.domain) + assert grad.children[0].name == a.name + assert grad.domain == a.domain def test_div(self): # divergence of scalar symbol should fail a = pybamm.Symbol("a") - with self.assertRaisesRegex( + with pytest.raises( pybamm.DomainError, - "Cannot take divergence of 'a' since its domain is empty", + match="Cannot take divergence of 'a' since its domain is empty", ): pybamm.Divergence(a) # divergence of variable evaluating on edges should fail a = pybamm.PrimaryBroadcast(pybamm.Scalar(1), "test") - with self.assertRaisesRegex(TypeError, "evaluate on edges"): + with pytest.raises(TypeError, match="evaluate on edges"): pybamm.Divergence(a) # divergence of broadcast should return broadcasted zero a = pybamm.PrimaryBroadcastToEdges(pybamm.Variable("a"), "test domain") div = pybamm.div(a) - self.assertEqual(div, pybamm.PrimaryBroadcast(0, "test domain")) + assert div == pybamm.PrimaryBroadcast(0, "test domain") a = pybamm.PrimaryBroadcastToEdges( pybamm.Variable("a", "some domain"), "test domain" ) div = pybamm.div(a) - self.assertEqual( - div, - pybamm.PrimaryBroadcast( - pybamm.PrimaryBroadcast(0, "some domain"), "test domain" - ), + assert div == pybamm.PrimaryBroadcast( + pybamm.PrimaryBroadcast(0, "some domain"), "test domain" ) # otherwise divergence should work a = pybamm.Symbol("a", domain="test domain") div = pybamm.Divergence(pybamm.Gradient(a)) - self.assertEqual(div.domain, a.domain) + assert div.domain == a.domain # check div commutes with negation a = pybamm.Symbol("a", domain="test domain") div = pybamm.div(-pybamm.Gradient(a)) - self.assertEqual(div, (-pybamm.Divergence(pybamm.Gradient(a)))) + assert div == (-pybamm.Divergence(pybamm.Gradient(a))) div = pybamm.div(-a * pybamm.Gradient(a)) - self.assertEqual(div, (-pybamm.Divergence(a * pybamm.Gradient(a)))) + assert div == (-pybamm.Divergence(a * pybamm.Gradient(a))) # div = pybamm.div(a * -pybamm.Gradient(a)) # self.assertEqual(div, (-pybamm.Divergence(a * pybamm.Gradient(a)))) @@ -288,9 +276,9 @@ def test_integral(self): a = pybamm.Symbol("a", domain=["negative electrode"]) x = pybamm.SpatialVariable("x", ["negative electrode"]) inta = pybamm.Integral(a, x) - self.assertEqual(inta.name, "integral dx ['negative electrode']") - self.assertEqual(inta.children[0].name, a.name) - self.assertEqual(inta.integration_variable[0], x) + assert inta.name == "integral dx ['negative electrode']" + assert inta.children[0].name == a.name + assert inta.integration_variable[0] == x assert_domain_equal(inta.domains, {}) # space integral with secondary domain a_sec = pybamm.Symbol( @@ -393,18 +381,18 @@ def test_integral(self): y = pybamm.SpatialVariable("y", ["current collector"]) z = pybamm.SpatialVariable("z", ["current collector"]) inta = pybamm.Integral(b, [y, z]) - self.assertEqual(inta.name, "integral dy dz ['current collector']") - self.assertEqual(inta.children[0].name, b.name) - self.assertEqual(inta.integration_variable[0], y) - self.assertEqual(inta.integration_variable[1], z) - self.assertEqual(inta.domain, []) + assert inta.name == "integral dy dz ['current collector']" + assert inta.children[0].name == b.name + assert inta.integration_variable[0] == y + assert inta.integration_variable[1] == z + assert inta.domain == [] # Indefinite inta = pybamm.IndefiniteIntegral(a, x) - self.assertEqual(inta.name, "a integrated w.r.t x on ['negative electrode']") - self.assertEqual(inta.children[0].name, a.name) - self.assertEqual(inta.integration_variable[0], x) - self.assertEqual(inta.domain, ["negative electrode"]) + assert inta.name == "a integrated w.r.t x on ['negative electrode']" + assert inta.children[0].name == a.name + assert inta.integration_variable[0] == x + assert inta.domain == ["negative electrode"] inta_sec = pybamm.IndefiniteIntegral(a_sec, x) assert_domain_equal( inta_sec.domains, @@ -412,22 +400,20 @@ def test_integral(self): ) # backward indefinite integral inta = pybamm.BackwardIndefiniteIntegral(a, x) - self.assertEqual( - inta.name, "a integrated backward w.r.t x on ['negative electrode']" - ) + assert inta.name == "a integrated backward w.r.t x on ['negative electrode']" # expected errors a = pybamm.Symbol("a", domain=["negative electrode"]) x = pybamm.SpatialVariable("x", ["separator"]) y = pybamm.Variable("y") z = pybamm.SpatialVariable("z", ["negative electrode"]) - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): pybamm.Integral(a, x) - with self.assertRaisesRegex(TypeError, "integration_variable must be"): + with pytest.raises(TypeError, match="integration_variable must be"): pybamm.Integral(a, y) - with self.assertRaisesRegex( + with pytest.raises( NotImplementedError, - "Indefinite integral only implemented w.r.t. one variable", + match="Indefinite integral only implemented w.r.t. one variable", ): pybamm.IndefiniteIntegral(a, [x, y]) @@ -436,166 +422,166 @@ def test_index(self): y_test = np.array([1, 2, 3, 4, 5]) # with integer ind = pybamm.Index(vec, 3) - self.assertIsInstance(ind, pybamm.Index) - self.assertEqual(ind.slice, slice(3, 4)) - self.assertEqual(ind.evaluate(y=y_test), 4) + assert isinstance(ind, pybamm.Index) + assert ind.slice == slice(3, 4) + assert ind.evaluate(y=y_test) == 4 # with -1 ind = pybamm.Index(vec, -1) - self.assertIsInstance(ind, pybamm.Index) - self.assertEqual(ind.slice, slice(-1, None)) - self.assertEqual(ind.evaluate(y=y_test), 5) - self.assertEqual(ind.name, "Index[-1]") + assert isinstance(ind, pybamm.Index) + assert ind.slice == slice(-1, None) + assert ind.evaluate(y=y_test) == 5 + assert ind.name == "Index[-1]" # with slice ind = pybamm.Index(vec, slice(1, 3)) - self.assertIsInstance(ind, pybamm.Index) - self.assertEqual(ind.slice, slice(1, 3)) + assert isinstance(ind, pybamm.Index) + assert ind.slice == slice(1, 3) np.testing.assert_array_equal(ind.evaluate(y=y_test), np.array([[2], [3]])) # with only stop slice ind = pybamm.Index(vec, slice(3)) - self.assertIsInstance(ind, pybamm.Index) - self.assertEqual(ind.slice, slice(3)) + assert isinstance(ind, pybamm.Index) + assert ind.slice == slice(3) np.testing.assert_array_equal(ind.evaluate(y=y_test), np.array([[1], [2], [3]])) # errors - with self.assertRaisesRegex(TypeError, "index must be integer or slice"): + with pytest.raises(TypeError, match="index must be integer or slice"): pybamm.Index(vec, 0.0) debug_mode = pybamm.settings.debug_mode pybamm.settings.debug_mode = True - with self.assertRaisesRegex(ValueError, "slice size exceeds child size"): + with pytest.raises(ValueError, match="slice size exceeds child size"): pybamm.Index(vec, 5) pybamm.settings.debug_mode = debug_mode def test_evaluate_at(self): a = pybamm.Symbol("a", domain=["negative electrode"]) f = pybamm.EvaluateAt(a, 1) - self.assertEqual(f.position, 1) + assert f.position == 1 def test_upwind_downwind(self): # upwind of scalar symbol should fail a = pybamm.Symbol("a") - with self.assertRaisesRegex( - pybamm.DomainError, "Cannot upwind 'a' since its domain is empty" + with pytest.raises( + pybamm.DomainError, match="Cannot upwind 'a' since its domain is empty" ): pybamm.Upwind(a) # upwind of variable evaluating on edges should fail a = pybamm.PrimaryBroadcastToEdges(pybamm.Scalar(1), "test") - with self.assertRaisesRegex(TypeError, "evaluate on nodes"): + with pytest.raises(TypeError, match="evaluate on nodes"): pybamm.Upwind(a) # otherwise upwind should work a = pybamm.Symbol("a", domain="test domain") upwind = pybamm.upwind(a) - self.assertIsInstance(upwind, pybamm.Upwind) - self.assertEqual(upwind.children[0].name, a.name) - self.assertEqual(upwind.domain, a.domain) + assert isinstance(upwind, pybamm.Upwind) + assert upwind.children[0].name == a.name + assert upwind.domain == a.domain # also test downwind a = pybamm.Symbol("a", domain="test domain") downwind = pybamm.downwind(a) - self.assertIsInstance(downwind, pybamm.Downwind) - self.assertEqual(downwind.children[0].name, a.name) - self.assertEqual(downwind.domain, a.domain) + assert isinstance(downwind, pybamm.Downwind) + assert downwind.children[0].name == a.name + assert downwind.domain == a.domain def test_diff(self): a = pybamm.StateVector(slice(0, 1)) y = np.array([5]) # negation - self.assertEqual((-a).diff(a).evaluate(y=y), -1) - self.assertEqual((-a).diff(-a).evaluate(), 1) + assert (-a).diff(a).evaluate(y=y) == -1 + assert (-a).diff(-a).evaluate() == 1 # absolute value - self.assertEqual((a**3).diff(a).evaluate(y=y), 3 * 5**2) - self.assertEqual((abs(a**3)).diff(a).evaluate(y=y), 3 * 5**2) - self.assertEqual((a**3).diff(a).evaluate(y=-y), 3 * 5**2) - self.assertEqual((abs(a**3)).diff(a).evaluate(y=-y), -3 * 5**2) + assert (a**3).diff(a).evaluate(y=y) == 3 * 5**2 + assert (abs(a**3)).diff(a).evaluate(y=y) == 3 * 5**2 + assert (a**3).diff(a).evaluate(y=-y) == 3 * 5**2 + assert (abs(a**3)).diff(a).evaluate(y=-y) == -3 * 5**2 # sign - self.assertEqual((pybamm.sign(a)).diff(a).evaluate(y=y), 0) + assert (pybamm.sign(a)).diff(a).evaluate(y=y) == 0 # floor - self.assertEqual((pybamm.Floor(a)).diff(a).evaluate(y=y), 0) + assert (pybamm.Floor(a)).diff(a).evaluate(y=y) == 0 # ceil - self.assertEqual((pybamm.Ceiling(a)).diff(a).evaluate(y=y), 0) + assert (pybamm.Ceiling(a)).diff(a).evaluate(y=y) == 0 # spatial operator (not implemented) spatial_a = pybamm.SpatialOperator("name", a) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): spatial_a.diff(a) def test_printing(self): a = pybamm.Symbol("a", domain="test") - self.assertEqual(str(-a), "-a") + assert str(-a) == "-a" grad = pybamm.Gradient(a) - self.assertEqual(grad.name, "grad") - self.assertEqual(str(grad), "grad(a)") + assert grad.name == "grad" + assert str(grad) == "grad(a)" def test_eq(self): a = pybamm.Scalar(4) un1 = pybamm.UnaryOperator("test", a) un2 = pybamm.UnaryOperator("test", a) un3 = pybamm.UnaryOperator("new test", a) - self.assertEqual(un1, un2) - self.assertNotEqual(un1, un3) + assert un1 == un2 + assert un1 != un3 a = pybamm.Scalar(4) un4 = pybamm.UnaryOperator("test", a) - self.assertEqual(un1, un4) + assert un1 == un4 d = pybamm.Scalar(42) un5 = pybamm.UnaryOperator("test", d) - self.assertNotEqual(un1, un5) + assert un1 != un5 def test_delta_function(self): a = pybamm.Symbol("a") delta_a = pybamm.DeltaFunction(a, "right", "some domain") - self.assertEqual(delta_a.side, "right") - self.assertEqual(delta_a.child, a) - self.assertEqual(delta_a.domain, ["some domain"]) - self.assertFalse(delta_a.evaluates_on_edges("primary")) + assert delta_a.side == "right" + assert delta_a.child == a + assert delta_a.domain == ["some domain"] + assert not delta_a.evaluates_on_edges("primary") a = pybamm.Symbol("a", domain="some domain") delta_a = pybamm.DeltaFunction(a, "left", "another domain") - self.assertEqual(delta_a.side, "left") + assert delta_a.side == "left" assert_domain_equal( delta_a.domains, {"primary": ["another domain"], "secondary": ["some domain"]}, ) - with self.assertRaisesRegex( - pybamm.DomainError, "Delta function domain cannot be None" + with pytest.raises( + pybamm.DomainError, match="Delta function domain cannot be None" ): delta_a = pybamm.DeltaFunction(a, "right", None) def test_boundary_operators(self): a = pybamm.Symbol("a", domain="some domain") boundary_a = pybamm.BoundaryOperator("boundary", a, "right") - self.assertEqual(boundary_a.side, "right") - self.assertEqual(boundary_a.child, a) + assert boundary_a.side == "right" + assert boundary_a.child == a def test_evaluates_on_edges(self): a = pybamm.StateVector(slice(0, 10), domain="test") - self.assertFalse(pybamm.Index(a, slice(1)).evaluates_on_edges("primary")) - self.assertFalse(pybamm.Laplacian(a).evaluates_on_edges("primary")) - self.assertFalse(pybamm.GradientSquared(a).evaluates_on_edges("primary")) - self.assertFalse(pybamm.BoundaryIntegral(a).evaluates_on_edges("primary")) - self.assertTrue(pybamm.Upwind(a).evaluates_on_edges("primary")) - self.assertTrue(pybamm.Downwind(a).evaluates_on_edges("primary")) + assert not pybamm.Index(a, slice(1)).evaluates_on_edges("primary") + assert not pybamm.Laplacian(a).evaluates_on_edges("primary") + assert not pybamm.GradientSquared(a).evaluates_on_edges("primary") + assert not pybamm.BoundaryIntegral(a).evaluates_on_edges("primary") + assert pybamm.Upwind(a).evaluates_on_edges("primary") + assert pybamm.Downwind(a).evaluates_on_edges("primary") def test_boundary_value(self): a = pybamm.Scalar(1) boundary_a = pybamm.boundary_value(a, "right") - self.assertEqual(boundary_a, a) + assert boundary_a == a boundary_broad_a = pybamm.boundary_value( pybamm.PrimaryBroadcast(a, ["negative electrode"]), "left" ) - self.assertEqual(boundary_broad_a.evaluate(), np.array([1])) + assert boundary_broad_a.evaluate() == np.array([1]) a = pybamm.Symbol("a", domain=["separator"]) boundary_a = pybamm.boundary_value(a, "right") - self.assertIsInstance(boundary_a, pybamm.BoundaryValue) - self.assertEqual(boundary_a.side, "right") + assert isinstance(boundary_a, pybamm.BoundaryValue) + assert boundary_a.side == "right" assert_domain_equal(boundary_a.domains, {}) # test with secondary domain a_sec = pybamm.Symbol( @@ -627,7 +613,7 @@ def test_boundary_value(self): }, ) boundary_a_quat = pybamm.boundary_value(a_quat, "right") - self.assertEqual(boundary_a_quat.domain, ["current collector"]) + assert boundary_a_quat.domain == ["current collector"] assert_domain_equal( boundary_a_quat.domains, { @@ -639,26 +625,26 @@ def test_boundary_value(self): # error if boundary value on tabs and domain is not "current collector" var = pybamm.Variable("var", domain=["negative electrode"]) - with self.assertRaisesRegex(pybamm.ModelError, "Can only take boundary"): + with pytest.raises(pybamm.ModelError, match="Can only take boundary"): pybamm.boundary_value(var, "negative tab") pybamm.boundary_value(var, "positive tab") # boundary value of symbol that evaluates on edges raises error symbol_on_edges = pybamm.PrimaryBroadcastToEdges(1, "domain") - with self.assertRaisesRegex( + with pytest.raises( ValueError, - "Can't take the boundary value of a symbol that evaluates on edges", + match="Can't take the boundary value of a symbol that evaluates on edges", ): pybamm.boundary_value(symbol_on_edges, "right") def test_boundary_gradient(self): var = pybamm.Variable("var", domain=["negative electrode"]) grad = pybamm.boundary_gradient(var, "right") - self.assertIsInstance(grad, pybamm.BoundaryGradient) + assert isinstance(grad, pybamm.BoundaryGradient) zero = pybamm.PrimaryBroadcast(0, ["negative electrode"]) grad = pybamm.boundary_gradient(zero, "right") - self.assertEqual(grad, 0) + assert grad == 0 def test_unary_simplifications(self): a = pybamm.Scalar(0) @@ -666,25 +652,25 @@ def test_unary_simplifications(self): d = pybamm.Scalar(-1) # negate - self.assertIsInstance((-a), pybamm.Scalar) - self.assertEqual((-a).evaluate(), 0) - self.assertIsInstance((-b), pybamm.Scalar) - self.assertEqual((-b).evaluate(), -1) + assert isinstance((-a), pybamm.Scalar) + assert (-a).evaluate() == 0 + assert isinstance((-b), pybamm.Scalar) + assert (-b).evaluate() == -1 # absolute value - self.assertIsInstance((abs(a)), pybamm.Scalar) - self.assertEqual((abs(a)).evaluate(), 0) - self.assertIsInstance((abs(d)), pybamm.Scalar) - self.assertEqual((abs(d)).evaluate(), 1) + assert isinstance((abs(a)), pybamm.Scalar) + assert (abs(a)).evaluate() == 0 + assert isinstance((abs(d)), pybamm.Scalar) + assert (abs(d)).evaluate() == 1 def test_not_constant(self): a = pybamm.NotConstant(pybamm.Scalar(1)) - self.assertEqual(a.name, "not_constant") - self.assertEqual(a.domain, []) - self.assertEqual(a.evaluate(), 1) - self.assertEqual(a.jac(pybamm.StateVector(slice(0, 1))).evaluate(), 0) - self.assertFalse(a.is_constant()) - self.assertFalse((2 * a).is_constant()) + assert a.name == "not_constant" + assert a.domain == [] + assert a.evaluate() == 1 + assert a.jac(pybamm.StateVector(slice(0, 1))).evaluate() == 0 + assert not a.is_constant() + assert not (2 * a).is_constant() def test_to_equation(self): a = pybamm.Symbol("a", domain="negative particle") @@ -695,62 +681,57 @@ def test_to_equation(self): # Test print_name pybamm.Floor.print_name = "test" - self.assertEqual(pybamm.Floor(-2.5).to_equation(), sympy.Symbol("test")) + assert pybamm.Floor(-2.5).to_equation() == sympy.Symbol("test") # Test Negate value = 4 - self.assertEqual(pybamm.Negate(value).to_equation(), -value) + assert pybamm.Negate(value).to_equation() == -value # Test AbsoluteValue - self.assertEqual(pybamm.AbsoluteValue(-value).to_equation(), value) + assert pybamm.AbsoluteValue(-value).to_equation() == value # Test Gradient - self.assertEqual(pybamm.Gradient(a).to_equation(), sympy_Gradient("a")) + assert pybamm.Gradient(a).to_equation() == sympy_Gradient("a") # Test Divergence - self.assertEqual( - pybamm.Divergence(pybamm.Gradient(a)).to_equation(), - sympy_Divergence(sympy_Gradient("a")), + assert pybamm.Divergence(pybamm.Gradient(a)).to_equation() == sympy_Divergence( + sympy_Gradient("a") ) # Test BoundaryValue - self.assertEqual( - pybamm.BoundaryValue(one, "right").to_equation(), sympy.Symbol("1") + assert pybamm.BoundaryValue(one, "right").to_equation() == sympy.Symbol("1") + assert pybamm.BoundaryValue(a, "right").to_equation() == sympy.Symbol( + "a^{surf}" ) - self.assertEqual( - pybamm.BoundaryValue(a, "right").to_equation(), sympy.Symbol("a^{surf}") + assert pybamm.BoundaryValue(b, "positive tab").to_equation() == sympy.Symbol( + str(b) ) - self.assertEqual( - pybamm.BoundaryValue(b, "positive tab").to_equation(), sympy.Symbol(str(b)) - ) - self.assertEqual( - pybamm.BoundaryValue(c, "left").to_equation(), - sympy.Symbol(r"c^{\mathtt{\text{left}}}"), + assert pybamm.BoundaryValue(c, "left").to_equation() == sympy.Symbol( + r"c^{\mathtt{\text{left}}}" ) # Test Integral xn = pybamm.SpatialVariable("xn", ["negative electrode"]) - self.assertEqual( - pybamm.Integral(d, xn).to_equation(), - sympy.Integral("d", sympy.Symbol("xn")), + assert pybamm.Integral(d, xn).to_equation() == sympy.Integral( + "d", sympy.Symbol("xn") ) def test_explicit_time_integral(self): expr = pybamm.ExplicitTimeIntegral(pybamm.Parameter("param"), pybamm.Scalar(1)) - self.assertEqual(expr.child, pybamm.Parameter("param")) - self.assertEqual(expr.initial_condition, pybamm.Scalar(1)) - self.assertEqual(expr.name, "explicit time integral") - self.assertEqual(expr.create_copy(), expr) - self.assertFalse(expr.is_constant()) + assert expr.child == pybamm.Parameter("param") + assert expr.initial_condition == pybamm.Scalar(1) + assert expr.name == "explicit time integral" + assert expr.create_copy() == expr + assert not expr.is_constant() - def test_to_from_json(self): + def test_to_from_json(self, mocker): # UnaryOperator a = pybamm.Symbol("a", domain=["test"]) un = pybamm.UnaryOperator("unary test", a) un_json = { "name": "unary test", - "id": mock.ANY, + "id": mocker.ANY, "domains": { "primary": ["test"], "secondary": [], @@ -759,10 +740,10 @@ def test_to_from_json(self): }, } - self.assertEqual(un.to_json(), un_json) + assert un.to_json() == un_json un_json["children"] = [a] - self.assertEqual(pybamm.UnaryOperator._from_json(un_json), un) + assert pybamm.UnaryOperator._from_json(un_json) == un # Index vec = pybamm.StateVector(slice(0, 5)) @@ -770,41 +751,31 @@ def test_to_from_json(self): ind_json = { "name": "Index[3]", - "id": mock.ANY, + "id": mocker.ANY, "index": {"start": 3, "stop": 4, "step": None}, "check_size": False, } - self.assertEqual(ind.to_json(), ind_json) + assert ind.to_json() == ind_json ind_json["children"] = [vec] - self.assertEqual(pybamm.Index._from_json(ind_json), ind) + assert pybamm.Index._from_json(ind_json) == ind # SpatialOperator spatial_vec = pybamm.SpatialOperator("name", vec) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): spatial_vec.to_json() - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.SpatialOperator._from_json({}) # ExplicitTimeIntegral expr = pybamm.ExplicitTimeIntegral(pybamm.Parameter("param"), pybamm.Scalar(1)) - expr_json = {"name": "explicit time integral", "id": mock.ANY} + expr_json = {"name": "explicit time integral", "id": mocker.ANY} - self.assertEqual(expr.to_json(), expr_json) + assert expr.to_json() == expr_json expr_json["children"] = [pybamm.Parameter("param")] expr_json["initial_condition"] = [pybamm.Scalar(1)] - self.assertEqual(pybamm.ExplicitTimeIntegral._from_json(expr_json), expr) - - -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() + assert pybamm.ExplicitTimeIntegral._from_json(expr_json) == expr diff --git a/tests/unit/test_expression_tree/test_variable.py b/tests/unit/test_expression_tree/test_variable.py index fb17968ca8..25c7955cc0 100644 --- a/tests/unit/test_expression_tree/test_variable.py +++ b/tests/unit/test_expression_tree/test_variable.py @@ -2,7 +2,7 @@ # Tests for the Variable class # -import unittest +import pytest import numpy as np @@ -10,97 +10,85 @@ import sympy -class TestVariable(unittest.TestCase): +class TestVariable: def test_variable_init(self): a = pybamm.Variable("a") - self.assertEqual(a.name, "a") - self.assertEqual(a.domain, []) + assert a.name == "a" + assert a.domain == [] a = pybamm.Variable("a", domain=["test"]) - self.assertEqual(a.domain[0], "test") - self.assertRaises(TypeError, pybamm.Variable("a", domain="test")) - self.assertEqual(a.scale, 1) - self.assertEqual(a.reference, 0) + assert a.domain[0] == "test" + assert a.scale == 1 + assert a.reference == 0 a = pybamm.Variable("a", scale=2, reference=-1) - self.assertEqual(a.scale, 2) - self.assertEqual(a.reference, -1) + assert a.scale == 2 + assert a.reference == -1 def test_variable_diff(self): a = pybamm.Variable("a") b = pybamm.Variable("b") - self.assertIsInstance(a.diff(a), pybamm.Scalar) - self.assertEqual(a.diff(a).evaluate(), 1) - self.assertIsInstance(a.diff(b), pybamm.Scalar) - self.assertEqual(a.diff(b).evaluate(), 0) + assert isinstance(a.diff(a), pybamm.Scalar) + assert a.diff(a).evaluate() == 1 + assert isinstance(a.diff(b), pybamm.Scalar) + assert a.diff(b).evaluate() == 0 def test_variable_eq(self): a1 = pybamm.Variable("a", domain=["negative electrode"]) a2 = pybamm.Variable("a", domain=["negative electrode"]) - self.assertEqual(a1, a2) + assert a1 == a2 a3 = pybamm.Variable("b", domain=["negative electrode"]) a4 = pybamm.Variable("a", domain=["positive electrode"]) - self.assertNotEqual(a1, a3) - self.assertNotEqual(a1, a4) + assert a1 != a3 + assert a1 != a4 def test_variable_bounds(self): var = pybamm.Variable("var") - self.assertEqual(var.bounds, (-np.inf, np.inf)) + assert var.bounds == (-np.inf, np.inf) var = pybamm.Variable("var", bounds=(0, 1)) - self.assertEqual(var.bounds, (0, 1)) + assert var.bounds == (0, 1) - with self.assertRaisesRegex(ValueError, "Invalid bounds"): + with pytest.raises(ValueError, match="Invalid bounds"): pybamm.Variable("var", bounds=(1, 0)) - with self.assertRaisesRegex(ValueError, "Invalid bounds"): + with pytest.raises(ValueError, match="Invalid bounds"): pybamm.Variable("var", bounds=(1, 1)) def test_to_equation(self): # Test print_name func = pybamm.Variable("test_string") func.print_name = "test" - self.assertEqual(func.to_equation(), sympy.Symbol("test")) + assert func.to_equation() == sympy.Symbol("test") # Test name - self.assertEqual(pybamm.Variable("name").to_equation(), sympy.Symbol("name")) + assert pybamm.Variable("name").to_equation() == sympy.Symbol("name") def test_to_json_error(self): func = pybamm.Variable("test_string") - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): func.to_json() -class TestVariableDot(unittest.TestCase): +class TestVariableDot: def test_variable_init(self): a = pybamm.VariableDot("a'") - self.assertEqual(a.name, "a'") - self.assertEqual(a.domain, []) + assert a.name == "a'" + assert a.domain == [] a = pybamm.VariableDot("a", domain=["test"]) - self.assertEqual(a.domain[0], "test") - self.assertRaises(TypeError, pybamm.Variable("a", domain="test")) + assert a.domain[0] == "test" def test_variable_id(self): a1 = pybamm.VariableDot("a", domain=["negative electrode"]) a2 = pybamm.VariableDot("a", domain=["negative electrode"]) - self.assertEqual(a1, a2) + assert a1 == a2 a3 = pybamm.VariableDot("b", domain=["negative electrode"]) a4 = pybamm.VariableDot("a", domain=["positive electrode"]) - self.assertNotEqual(a1, a3) - self.assertNotEqual(a1, a4) + assert a1 != a3 + assert a1 != a4 def test_variable_diff(self): a = pybamm.VariableDot("a") b = pybamm.Variable("b") - self.assertIsInstance(a.diff(a), pybamm.Scalar) - self.assertEqual(a.diff(a).evaluate(), 1) - self.assertIsInstance(a.diff(b), pybamm.Scalar) - self.assertEqual(a.diff(b).evaluate(), 0) - - -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() + assert isinstance(a.diff(a), pybamm.Scalar) + assert a.diff(a).evaluate() == 1 + assert isinstance(a.diff(b), pybamm.Scalar) + assert a.diff(b).evaluate() == 0 diff --git a/tests/unit/test_geometry/test_battery_geometry.py b/tests/unit/test_geometry/test_battery_geometry.py index 38e1ce1908..c73ef89af1 100644 --- a/tests/unit/test_geometry/test_battery_geometry.py +++ b/tests/unit/test_geometry/test_battery_geometry.py @@ -1,25 +1,27 @@ # # Tests for the base model class # +import pytest import pybamm -import unittest -class TestBatteryGeometry(unittest.TestCase): - def test_geometry_keys(self): - for cc_dimension in [0, 1, 2]: - geometry = pybamm.battery_geometry( - options={ - "particle size": "distribution", - "dimensionality": cc_dimension, - }, +class TestBatteryGeometry: + @pytest.fixture(params=[0, 1, 2]) + def geometry(self, request): + geometry = pybamm.battery_geometry( + options={ + "particle size": "distribution", + "dimensionality": request.param, + }, + ) + return geometry + + def test_geometry_keys(self, geometry): + for domain_geoms in geometry.values(): + assert all( + isinstance(spatial_var, str) for spatial_var in domain_geoms.keys() ) - for domain_geoms in geometry.values(): - all( - self.assertIsInstance(spatial_var, str) - for spatial_var in domain_geoms.keys() - ) geometry.print_parameter_info() def test_geometry(self): @@ -31,60 +33,60 @@ def test_geometry(self): "dimensionality": cc_dimension, }, ) - self.assertIsInstance(geometry, pybamm.Geometry) - self.assertIn("negative electrode", geometry) - self.assertIn("negative particle", geometry) - self.assertIn("negative particle size", geometry) - self.assertEqual(geometry["negative electrode"]["x_n"]["min"], 0) - self.assertEqual(geometry["negative electrode"]["x_n"]["max"], geo.n.L) + assert isinstance(geometry, pybamm.Geometry) + assert "negative electrode" in geometry + assert "negative particle" in geometry + assert "negative particle size" in geometry + assert geometry["negative electrode"]["x_n"]["min"] == 0 + assert geometry["negative electrode"]["x_n"]["max"] == geo.n.L if cc_dimension == 1: - self.assertIn("tabs", geometry["current collector"]) + assert "tabs" in geometry["current collector"] geometry = pybamm.battery_geometry(include_particles=False) - self.assertNotIn("negative particle", geometry) + assert "negative particle" not in geometry geometry = pybamm.battery_geometry() - self.assertNotIn("negative particle size", geometry) + assert "negative particle size" not in geometry geometry = pybamm.battery_geometry(form_factor="cylindrical") - self.assertEqual(geometry["current collector"]["r_macro"]["position"], 1) + assert geometry["current collector"]["r_macro"]["position"] == 1 geometry = pybamm.battery_geometry( form_factor="cylindrical", options={"dimensionality": 1} ) - self.assertEqual(geometry["current collector"]["r_macro"]["min"], geo.r_inner) - self.assertEqual(geometry["current collector"]["r_macro"]["max"], 1) + assert geometry["current collector"]["r_macro"]["min"] == geo.r_inner + assert geometry["current collector"]["r_macro"]["max"] == 1 options = {"particle phases": "2"} geometry = pybamm.battery_geometry(options=options) geo = pybamm.GeometricParameters(options=options) - self.assertEqual(geometry["negative primary particle"]["r_n_prim"]["min"], 0) - self.assertEqual( - geometry["negative primary particle"]["r_n_prim"]["max"], geo.n.prim.R_typ + assert geometry["negative primary particle"]["r_n_prim"]["min"] == 0 + assert ( + geometry["negative primary particle"]["r_n_prim"]["max"] == geo.n.prim.R_typ ) - self.assertEqual(geometry["negative secondary particle"]["r_n_sec"]["min"], 0) - self.assertEqual( - geometry["negative secondary particle"]["r_n_sec"]["max"], geo.n.sec.R_typ + assert geometry["negative secondary particle"]["r_n_sec"]["min"] == 0 + assert ( + geometry["negative secondary particle"]["r_n_sec"]["max"] == geo.n.sec.R_typ ) - self.assertEqual(geometry["positive primary particle"]["r_p_prim"]["min"], 0) - self.assertEqual( - geometry["positive primary particle"]["r_p_prim"]["max"], geo.p.prim.R_typ + assert geometry["positive primary particle"]["r_p_prim"]["min"] == 0 + assert ( + geometry["positive primary particle"]["r_p_prim"]["max"] == geo.p.prim.R_typ ) - self.assertEqual(geometry["positive secondary particle"]["r_p_sec"]["min"], 0) - self.assertEqual( - geometry["positive secondary particle"]["r_p_sec"]["max"], geo.p.sec.R_typ + assert geometry["positive secondary particle"]["r_p_sec"]["min"] == 0 + assert ( + geometry["positive secondary particle"]["r_p_sec"]["max"] == geo.p.sec.R_typ ) def test_geometry_error(self): - with self.assertRaisesRegex(pybamm.GeometryError, "Invalid current"): + with pytest.raises(pybamm.GeometryError, match="Invalid current"): pybamm.battery_geometry( form_factor="cylindrical", options={"dimensionality": 2} ) - with self.assertRaisesRegex(pybamm.GeometryError, "Invalid form"): + with pytest.raises(pybamm.GeometryError, match="Invalid form"): pybamm.battery_geometry(form_factor="triangle") -class TestReadParameters(unittest.TestCase): +class TestReadParameters: # This is the most complicated geometry and should test the parameters are # all returned for the deepest dict def test_read_parameters(self): @@ -103,37 +105,22 @@ def test_read_parameters(self): geometry = pybamm.battery_geometry(options={"dimensionality": 2}) - self.assertEqual( - set([x.name for x in geometry.parameters]), - set( - [ - x.name - for x in [ - L_n, - L_s, - L_p, - L_y, - L_z, - tab_n_y, - tab_n_z, - L_tab_n, - tab_p_y, - tab_p_z, - L_tab_p, - ] + assert set([x.name for x in geometry.parameters]) == set( + [ + x.name + for x in [ + L_n, + L_s, + L_p, + L_y, + L_z, + tab_n_y, + tab_n_z, + L_tab_n, + tab_p_y, + tab_p_z, + L_tab_p, ] - ), - ) - self.assertTrue( - all(isinstance(x, pybamm.Parameter) for x in geometry.parameters) + ] ) - - -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() + assert all(isinstance(x, pybamm.Parameter) for x in geometry.parameters) diff --git a/tests/unit/test_meshes/test_meshes.py b/tests/unit/test_meshes/test_meshes.py index 2f3bffddfb..eecc1a8911 100644 --- a/tests/unit/test_meshes/test_meshes.py +++ b/tests/unit/test_meshes/test_meshes.py @@ -2,9 +2,9 @@ # Test for the Finite Volume Mesh class # +import pytest import pybamm import numpy as np -import unittest def get_param(): @@ -19,7 +19,19 @@ def get_param(): ) -class TestMesh(unittest.TestCase): +class TestMesh: + @pytest.fixture(scope="class") + def submesh_types(self): + submesh_types = { + "negative electrode": pybamm.Uniform1DSubMesh, + "separator": pybamm.Uniform1DSubMesh, + "positive electrode": pybamm.Uniform1DSubMesh, + "negative particle": pybamm.Uniform1DSubMesh, + "positive particle": pybamm.Uniform1DSubMesh, + "current collector": pybamm.SubMesh0D, + } + return submesh_types + def test_mesh_creation_no_parameters(self): r = pybamm.SpatialVariable( "r", domain=["negative particle"], coord_sys="spherical polar" @@ -36,17 +48,17 @@ def test_mesh_creation_no_parameters(self): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # check geometry - self.assertEqual(mesh.geometry, geometry) + assert mesh.geometry == geometry # check boundary locations - self.assertEqual(mesh["negative particle"].edges[0], 0) - self.assertEqual(mesh["negative particle"].edges[-1], 1) + assert mesh["negative particle"].edges[0] == 0 + assert mesh["negative particle"].edges[-1] == 1 # check number of edges and nodes - self.assertEqual(len(mesh["negative particle"].nodes), var_pts[r]) - self.assertEqual( - len(mesh["negative particle"].edges), - len(mesh["negative particle"].nodes) + 1, + assert len(mesh["negative particle"].nodes) == var_pts[r] + assert ( + len(mesh["negative particle"].edges) + == len(mesh["negative particle"].nodes) + 1 ) # errors if old format @@ -55,12 +67,12 @@ def test_mesh_creation_no_parameters(self): "primary": {r: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}} } } - with self.assertRaisesRegex( - pybamm.GeometryError, "Geometry should no longer be given keys" + with pytest.raises( + pybamm.GeometryError, match="Geometry should no longer be given keys" ): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) - def test_mesh_creation(self): + def test_mesh_creation(self, submesh_types): param = get_param() geometry = pybamm.battery_geometry() @@ -68,52 +80,32 @@ def test_mesh_creation(self): var_pts = {"x_n": 10, "x_s": 10, "x_p": 12, "r_n": 5, "r_p": 6} - submesh_types = { - "negative electrode": pybamm.Uniform1DSubMesh, - "separator": pybamm.Uniform1DSubMesh, - "positive electrode": pybamm.Uniform1DSubMesh, - "negative particle": pybamm.Uniform1DSubMesh, - "positive particle": pybamm.Uniform1DSubMesh, - "current collector": pybamm.SubMesh0D, - } - # create mesh mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # check geometry - self.assertEqual(mesh.geometry, geometry) + assert mesh.geometry == geometry # check boundary locations - self.assertEqual(mesh["negative electrode"].edges[0], 0) - self.assertAlmostEqual(mesh["positive electrode"].edges[-1], 0.6) + assert mesh["negative electrode"].edges[0] == 0 + assert mesh["positive electrode"].edges[-1] == pytest.approx(0.6) # check internal boundary locations - self.assertEqual( - mesh["negative electrode"].edges[-1], mesh["separator"].edges[0] - ) - self.assertEqual( - mesh["positive electrode"].edges[0], mesh["separator"].edges[-1] - ) + assert mesh["negative electrode"].edges[-1] == mesh["separator"].edges[0] + assert mesh["positive electrode"].edges[0] == mesh["separator"].edges[-1] for domain in mesh.base_domains: if domain != "current collector": - self.assertEqual(len(mesh[domain].edges), len(mesh[domain].nodes) + 1) + assert len(mesh[domain].edges) == len(mesh[domain].nodes) + 1 - def test_init_failure(self): + def test_init_failure(self, submesh_types): geometry = pybamm.battery_geometry() - submesh_types = { - "negative electrode": pybamm.Uniform1DSubMesh, - "separator": pybamm.Uniform1DSubMesh, - "positive electrode": pybamm.Uniform1DSubMesh, - "negative particle": pybamm.Uniform1DSubMesh, - "positive particle": pybamm.Uniform1DSubMesh, - "current collector": pybamm.SubMesh0D, - } - with self.assertRaisesRegex(KeyError, "Points not given"): + + with pytest.raises(KeyError, match="Points not given"): pybamm.Mesh(geometry, submesh_types, {}) var_pts = {"x_n": 10, "x_s": 10, "x_p": 12} geometry = pybamm.battery_geometry(options={"dimensionality": 1}) - with self.assertRaisesRegex(KeyError, "Points not given"): + with pytest.raises(KeyError, match="Points not given"): pybamm.Mesh(geometry, submesh_types, var_pts) # Not processing geometry parameters @@ -121,26 +113,17 @@ def test_init_failure(self): var_pts = {"x_n": 10, "x_s": 10, "x_p": 12, "r_n": 5, "r_p": 6} - submesh_types = { - "negative electrode": pybamm.Uniform1DSubMesh, - "separator": pybamm.Uniform1DSubMesh, - "positive electrode": pybamm.Uniform1DSubMesh, - "negative particle": pybamm.Uniform1DSubMesh, - "positive particle": pybamm.Uniform1DSubMesh, - "current collector": pybamm.SubMesh0D, - } - - with self.assertRaisesRegex(pybamm.DiscretisationError, "Parameter values"): + with pytest.raises(pybamm.DiscretisationError, match="Parameter values"): pybamm.Mesh(geometry, submesh_types, var_pts) # Geometry has an unrecognized variable type geometry["negative electrode"] = { "x_n": {"min": 0, "max": pybamm.Variable("var")} } - with self.assertRaisesRegex(NotImplementedError, "for symbol var"): + with pytest.raises(NotImplementedError, match="for symbol var"): pybamm.Mesh(geometry, submesh_types, var_pts) - def test_mesh_sizes(self): + def test_mesh_sizes(self, submesh_types): param = get_param() geometry = pybamm.battery_geometry() @@ -148,27 +131,19 @@ def test_mesh_sizes(self): # provide mesh properties var_pts = {"x_n": 10, "x_s": 10, "x_p": 12, "r_n": 5, "r_p": 6} - submesh_types = { - "negative electrode": pybamm.Uniform1DSubMesh, - "separator": pybamm.Uniform1DSubMesh, - "positive electrode": pybamm.Uniform1DSubMesh, - "negative particle": pybamm.Uniform1DSubMesh, - "positive particle": pybamm.Uniform1DSubMesh, - "current collector": pybamm.SubMesh0D, - } # create mesh mesh = pybamm.Mesh(geometry, submesh_types, var_pts) - self.assertEqual(mesh["negative electrode"].npts, var_pts["x_n"]) - self.assertEqual(mesh["separator"].npts, var_pts["x_s"]) - self.assertEqual(mesh["positive electrode"].npts, var_pts["x_p"]) + assert mesh["negative electrode"].npts == var_pts["x_n"] + assert mesh["separator"].npts == var_pts["x_s"] + assert mesh["positive electrode"].npts == var_pts["x_p"] - self.assertEqual(len(mesh["negative electrode"].edges) - 1, var_pts["x_n"]) - self.assertEqual(len(mesh["separator"].edges) - 1, var_pts["x_s"]) - self.assertEqual(len(mesh["positive electrode"].edges) - 1, var_pts["x_p"]) + assert len(mesh["negative electrode"].edges) - 1 == var_pts["x_n"] + assert len(mesh["separator"].edges) - 1 == var_pts["x_s"] + assert len(mesh["positive electrode"].edges) - 1 == var_pts["x_p"] - def test_mesh_sizes_using_standard_spatial_vars(self): + def test_mesh_sizes_using_standard_spatial_vars(self, submesh_types): param = get_param() geometry = pybamm.battery_geometry() @@ -177,27 +152,19 @@ def test_mesh_sizes_using_standard_spatial_vars(self): # provide mesh properties var = pybamm.standard_spatial_vars var_pts = {var.x_n: 10, var.x_s: 10, var.x_p: 12, var.r_n: 5, var.r_p: 6} - submesh_types = { - "negative electrode": pybamm.Uniform1DSubMesh, - "separator": pybamm.Uniform1DSubMesh, - "positive electrode": pybamm.Uniform1DSubMesh, - "negative particle": pybamm.Uniform1DSubMesh, - "positive particle": pybamm.Uniform1DSubMesh, - "current collector": pybamm.SubMesh0D, - } # create mesh mesh = pybamm.Mesh(geometry, submesh_types, var_pts) - self.assertEqual(mesh["negative electrode"].npts, var_pts[var.x_n]) - self.assertEqual(mesh["separator"].npts, var_pts[var.x_s]) - self.assertEqual(mesh["positive electrode"].npts, var_pts[var.x_p]) + assert mesh["negative electrode"].npts == var_pts[var.x_n] + assert mesh["separator"].npts == var_pts[var.x_s] + assert mesh["positive electrode"].npts == var_pts[var.x_p] - self.assertEqual(len(mesh["negative electrode"].edges) - 1, var_pts[var.x_n]) - self.assertEqual(len(mesh["separator"].edges) - 1, var_pts[var.x_s]) - self.assertEqual(len(mesh["positive electrode"].edges) - 1, var_pts[var.x_p]) + assert len(mesh["negative electrode"].edges) - 1 == var_pts[var.x_n] + assert len(mesh["separator"].edges) - 1 == var_pts[var.x_s] + assert len(mesh["positive electrode"].edges) - 1 == var_pts[var.x_p] - def test_combine_submeshes(self): + def test_combine_submeshes(self, submesh_types): param = get_param() geometry = pybamm.battery_geometry() @@ -205,22 +172,14 @@ def test_combine_submeshes(self): # provide mesh properties var_pts = {"x_n": 10, "x_s": 10, "x_p": 12, "r_n": 5, "r_p": 6} - submesh_types = { - "negative electrode": pybamm.Uniform1DSubMesh, - "separator": pybamm.Uniform1DSubMesh, - "positive electrode": pybamm.Uniform1DSubMesh, - "negative particle": pybamm.Uniform1DSubMesh, - "positive particle": pybamm.Uniform1DSubMesh, - "current collector": pybamm.SubMesh0D, - } # create mesh mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # create submesh submesh = mesh[("negative electrode", "separator")] - self.assertEqual(submesh.edges[0], 0) - self.assertEqual(submesh.edges[-1], mesh["separator"].edges[-1]) + assert submesh.edges[0] == 0 + assert submesh.edges[-1] == mesh["separator"].edges[-1] np.testing.assert_almost_equal( submesh.nodes - np.concatenate( @@ -228,8 +187,8 @@ def test_combine_submeshes(self): ), 0, ) - self.assertEqual(submesh.internal_boundaries, [0.1]) - with self.assertRaises(pybamm.DomainError): + assert submesh.internal_boundaries == [0.1] + with pytest.raises(pybamm.DomainError): mesh.combine_submeshes("negative electrode", "positive electrode") # test errors @@ -241,15 +200,15 @@ def test_combine_submeshes(self): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) - with self.assertRaisesRegex(pybamm.DomainError, "trying"): + with pytest.raises(pybamm.DomainError, match="trying"): mesh.combine_submeshes("negative electrode", "negative particle") - with self.assertRaisesRegex( - ValueError, "Submesh domains being combined cannot be empty" + with pytest.raises( + ValueError, match="Submesh domains being combined cannot be empty" ): mesh.combine_submeshes() - def test_ghost_cells(self): + def test_ghost_cells(self, submesh_types): param = get_param() geometry = pybamm.battery_geometry() @@ -257,14 +216,6 @@ def test_ghost_cells(self): # provide mesh properties var_pts = {"x_n": 10, "x_s": 10, "x_p": 12, "r_n": 5, "r_p": 6} - submesh_types = { - "negative electrode": pybamm.Uniform1DSubMesh, - "separator": pybamm.Uniform1DSubMesh, - "positive electrode": pybamm.Uniform1DSubMesh, - "negative particle": pybamm.Uniform1DSubMesh, - "positive particle": pybamm.Uniform1DSubMesh, - "current collector": pybamm.SubMesh0D, - } # create mesh mesh = pybamm.Mesh(geometry, submesh_types, var_pts) @@ -282,7 +233,7 @@ def test_ghost_cells(self): mesh["positive electrode"].edges[-1], ) - def test_mesh_coord_sys(self): + def test_mesh_coord_sys(self, submesh_types): param = get_param() geometry = pybamm.battery_geometry() @@ -290,21 +241,12 @@ def test_mesh_coord_sys(self): var_pts = {"x_n": 10, "x_s": 10, "x_p": 12, "r_n": 5, "r_p": 6} - submesh_types = { - "negative electrode": pybamm.Uniform1DSubMesh, - "separator": pybamm.Uniform1DSubMesh, - "positive electrode": pybamm.Uniform1DSubMesh, - "negative particle": pybamm.Uniform1DSubMesh, - "positive particle": pybamm.Uniform1DSubMesh, - "current collector": pybamm.SubMesh0D, - } - # create mesh mesh = pybamm.Mesh(geometry, submesh_types, var_pts) for submesh in mesh.values(): if not isinstance(submesh, pybamm.SubMesh0D): - self.assertTrue(submesh.coord_sys in pybamm.KNOWN_COORD_SYS) + assert submesh.coord_sys in pybamm.KNOWN_COORD_SYS def test_unimplemented_meshes(self): var_pts = {"x_n": 10, "y": 10} @@ -315,10 +257,10 @@ def test_unimplemented_meshes(self): } } submesh_types = {"negative electrode": pybamm.Uniform1DSubMesh} - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): pybamm.Mesh(geometry, submesh_types, var_pts) - def test_1plus1D_tabs_left_right(self): + def test_1plus1_d_tabs_left_right(self): param = pybamm.ParameterValues( values={ "Electrode width [m]": 0.4, @@ -349,12 +291,12 @@ def test_1plus1D_tabs_left_right(self): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # negative tab should be "left" - self.assertEqual(mesh["current collector"].tabs["negative tab"], "left") + assert mesh["current collector"].tabs["negative tab"] == "left" # positive tab should be "right" - self.assertEqual(mesh["current collector"].tabs["positive tab"], "right") + assert mesh["current collector"].tabs["positive tab"] == "right" - def test_1plus1D_tabs_right_left(self): + def test_1plus1_d_tabs_right_left(self): param = pybamm.ParameterValues( values={ "Electrode width [m]": 0.4, @@ -385,10 +327,10 @@ def test_1plus1D_tabs_right_left(self): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # negative tab should be "right" - self.assertEqual(mesh["current collector"].tabs["negative tab"], "right") + assert mesh["current collector"].tabs["negative tab"] == "right" # positive tab should be "left" - self.assertEqual(mesh["current collector"].tabs["positive tab"], "left") + assert mesh["current collector"].tabs["positive tab"] == "left" def test_to_json(self): r = pybamm.SpatialVariable( @@ -412,20 +354,10 @@ def test_to_json(self): "base_domains": ["negative particle"], } - self.assertEqual(mesh_json, expected_json) + assert mesh_json == expected_json -class TestMeshGenerator(unittest.TestCase): +class TestMeshGenerator: def test_init_name(self): mesh_generator = pybamm.MeshGenerator(pybamm.SubMesh0D) - self.assertEqual(mesh_generator.__repr__(), "Generator for SubMesh0D") - - -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() + assert mesh_generator.__repr__() == "Generator for SubMesh0D" diff --git a/tests/unit/test_meshes/test_one_dimensional_submesh.py b/tests/unit/test_meshes/test_one_dimensional_submesh.py index 82429e475c..94895b6c4b 100644 --- a/tests/unit/test_meshes/test_one_dimensional_submesh.py +++ b/tests/unit/test_meshes/test_one_dimensional_submesh.py @@ -1,20 +1,36 @@ +import pytest import pybamm -import unittest import numpy as np -class TestSubMesh1D(unittest.TestCase): +@pytest.fixture() +def r(): + r = pybamm.SpatialVariable( + "r", domain=["negative particle"], coord_sys="spherical polar" + ) + return r + + +@pytest.fixture() +def geometry(r): + geometry = { + "negative particle": {r: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}} + } + return geometry + + +class TestSubMesh1D: def test_tabs(self): edges = np.linspace(0, 1, 10) tabs = {"negative": {"z_centre": 0}, "positive": {"z_centre": 1}} mesh = pybamm.SubMesh1D(edges, None, tabs=tabs) - self.assertEqual(mesh.tabs["negative tab"], "left") - self.assertEqual(mesh.tabs["positive tab"], "right") + assert mesh.tabs["negative tab"] == "left" + assert mesh.tabs["positive tab"] == "right" def test_exceptions(self): edges = np.linspace(0, 1, 10) tabs = {"negative": {"z_centre": 0.2}, "positive": {"z_centre": 1}} - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): pybamm.SubMesh1D(edges, None, tabs=tabs) def test_to_json(self): @@ -41,28 +57,20 @@ def test_to_json(self): "tabs": {"negative tab": "left", "positive tab": "right"}, } - self.assertEqual(mesh_json, expected_json) + assert mesh_json == expected_json # check tabs work new_mesh = pybamm.Uniform1DSubMesh._from_json(mesh_json) - self.assertEqual(mesh.tabs, new_mesh.tabs) + assert mesh.tabs == new_mesh.tabs -class TestUniform1DSubMesh(unittest.TestCase): +class TestUniform1DSubMesh: def test_exceptions(self): lims = {"a": 1, "b": 2} - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): pybamm.Uniform1DSubMesh(lims, None) - def test_symmetric_mesh_creation_no_parameters(self): - r = pybamm.SpatialVariable( - "r", domain=["negative particle"], coord_sys="spherical polar" - ) - - geometry = { - "negative particle": {r: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}} - } - + def test_symmetric_mesh_creation_no_parameters(self, r, geometry): submesh_types = {"negative particle": pybamm.Uniform1DSubMesh} var_pts = {r: 20} @@ -70,27 +78,19 @@ def test_symmetric_mesh_creation_no_parameters(self): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # check boundary locations - self.assertEqual(mesh["negative particle"].edges[0], 0) - self.assertEqual(mesh["negative particle"].edges[-1], 1) + assert mesh["negative particle"].edges[0] == 0 + assert mesh["negative particle"].edges[-1] == 1 # check number of edges and nodes - self.assertEqual(len(mesh["negative particle"].nodes), var_pts[r]) - self.assertEqual( - len(mesh["negative particle"].edges), - len(mesh["negative particle"].nodes) + 1, - ) - - -class TestExponential1DSubMesh(unittest.TestCase): - def test_symmetric_mesh_creation_no_parameters_even(self): - r = pybamm.SpatialVariable( - "r", domain=["negative particle"], coord_sys="spherical polar" + assert len(mesh["negative particle"].nodes) == var_pts[r] + assert ( + len(mesh["negative particle"].edges) + == len(mesh["negative particle"].nodes) + 1 ) - geometry = { - "negative particle": {r: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}} - } +class TestExponential1DSubMesh: + def test_symmetric_mesh_creation_no_parameters_even(self, r, geometry): submesh_params = {"side": "symmetric"} submesh_types = { "negative particle": pybamm.MeshGenerator( @@ -103,25 +103,17 @@ def test_symmetric_mesh_creation_no_parameters_even(self): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # check boundary locations - self.assertEqual(mesh["negative particle"].edges[0], 0) - self.assertEqual(mesh["negative particle"].edges[-1], 1) + assert mesh["negative particle"].edges[0] == 0 + assert mesh["negative particle"].edges[-1] == 1 # check number of edges and nodes - self.assertEqual(len(mesh["negative particle"].nodes), var_pts[r]) - self.assertEqual( - len(mesh["negative particle"].edges), - len(mesh["negative particle"].nodes) + 1, + assert len(mesh["negative particle"].nodes) == var_pts[r] + assert ( + len(mesh["negative particle"].edges) + == len(mesh["negative particle"].nodes) + 1 ) - def test_symmetric_mesh_creation_no_parameters_odd(self): - r = pybamm.SpatialVariable( - "r", domain=["negative particle"], coord_sys="spherical polar" - ) - - geometry = { - "negative particle": {r: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}} - } - + def test_symmetric_mesh_creation_no_parameters_odd(self, r, geometry): submesh_params = {"side": "symmetric", "stretch": 1.5} submesh_types = { "negative particle": pybamm.MeshGenerator( @@ -134,25 +126,17 @@ def test_symmetric_mesh_creation_no_parameters_odd(self): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # check boundary locations - self.assertEqual(mesh["negative particle"].edges[0], 0) - self.assertEqual(mesh["negative particle"].edges[-1], 1) + assert mesh["negative particle"].edges[0] == 0 + assert mesh["negative particle"].edges[-1] == 1 # check number of edges and nodes - self.assertEqual(len(mesh["negative particle"].nodes), var_pts[r]) - self.assertEqual( - len(mesh["negative particle"].edges), - len(mesh["negative particle"].nodes) + 1, - ) - - def test_left_mesh_creation_no_parameters(self): - r = pybamm.SpatialVariable( - "r", domain=["negative particle"], coord_sys="spherical polar" + assert len(mesh["negative particle"].nodes) == var_pts[r] + assert ( + len(mesh["negative particle"].edges) + == len(mesh["negative particle"].nodes) + 1 ) - geometry = { - "negative particle": {r: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}} - } - + def test_left_mesh_creation_no_parameters(self, r, geometry): submesh_params = {"side": "left"} submesh_types = { "negative particle": pybamm.MeshGenerator( @@ -165,25 +149,17 @@ def test_left_mesh_creation_no_parameters(self): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # check boundary locations - self.assertEqual(mesh["negative particle"].edges[0], 0) - self.assertEqual(mesh["negative particle"].edges[-1], 1) + assert mesh["negative particle"].edges[0] == 0 + assert mesh["negative particle"].edges[-1] == 1 # check number of edges and nodes - self.assertEqual(len(mesh["negative particle"].nodes), var_pts[r]) - self.assertEqual( - len(mesh["negative particle"].edges), - len(mesh["negative particle"].nodes) + 1, + assert len(mesh["negative particle"].nodes) == var_pts[r] + assert ( + len(mesh["negative particle"].edges) + == len(mesh["negative particle"].nodes) + 1 ) - def test_right_mesh_creation_no_parameters(self): - r = pybamm.SpatialVariable( - "r", domain=["negative particle"], coord_sys="spherical polar" - ) - - geometry = { - "negative particle": {r: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}} - } - + def test_right_mesh_creation_no_parameters(self, r, geometry): submesh_params = {"side": "right", "stretch": 2} submesh_types = { "negative particle": pybamm.MeshGenerator( @@ -196,27 +172,19 @@ def test_right_mesh_creation_no_parameters(self): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # check boundary locations - self.assertEqual(mesh["negative particle"].edges[0], 0) - self.assertEqual(mesh["negative particle"].edges[-1], 1) + assert mesh["negative particle"].edges[0] == 0 + assert mesh["negative particle"].edges[-1] == 1 # check number of edges and nodes - self.assertEqual(len(mesh["negative particle"].nodes), var_pts[r]) - self.assertEqual( - len(mesh["negative particle"].edges), - len(mesh["negative particle"].nodes) + 1, - ) - - -class TestChebyshev1DSubMesh(unittest.TestCase): - def test_mesh_creation_no_parameters(self): - r = pybamm.SpatialVariable( - "r", domain=["negative particle"], coord_sys="spherical polar" + assert len(mesh["negative particle"].nodes) == var_pts[r] + assert ( + len(mesh["negative particle"].edges) + == len(mesh["negative particle"].nodes) + 1 ) - geometry = { - "negative particle": {r: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}} - } +class TestChebyshev1DSubMesh: + def test_mesh_creation_no_parameters(self, r, geometry): submesh_types = {"negative particle": pybamm.Chebyshev1DSubMesh} var_pts = {r: 20} @@ -224,18 +192,18 @@ def test_mesh_creation_no_parameters(self): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # check boundary locations - self.assertEqual(mesh["negative particle"].edges[0], 0) - self.assertEqual(mesh["negative particle"].edges[-1], 1) + assert mesh["negative particle"].edges[0] == 0 + assert mesh["negative particle"].edges[-1] == 1 # check number of edges and nodes - self.assertEqual(len(mesh["negative particle"].nodes), var_pts[r]) - self.assertEqual( - len(mesh["negative particle"].edges), - len(mesh["negative particle"].nodes) + 1, + assert len(mesh["negative particle"].nodes) == var_pts[r] + assert ( + len(mesh["negative particle"].edges) + == len(mesh["negative particle"].nodes) + 1 ) -class TestUser1DSubMesh(unittest.TestCase): +class TestUser1DSubMesh: def test_exceptions(self): edges = np.array([0, 0.3, 1]) submesh_params = {"edges": edges} @@ -244,35 +212,27 @@ def test_exceptions(self): # error if npts+1 != len(edges) lims = {"x_n": {"min": 0, "max": 1}} npts = {"x_n": 10} - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): mesh(lims, npts) # error if lims[0] not equal to edges[0] lims = {"x_n": {"min": 0.1, "max": 1}} npts = {"x_n": len(edges) - 1} - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): mesh(lims, npts) # error if lims[-1] not equal to edges[-1] lims = {"x_n": {"min": 0, "max": 10}} npts = {"x_n": len(edges) - 1} - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): mesh(lims, npts) # no user points mesh = pybamm.MeshGenerator(pybamm.UserSupplied1DSubMesh) - with self.assertRaisesRegex(pybamm.GeometryError, "User mesh requires"): + with pytest.raises(pybamm.GeometryError, match="User mesh requires"): mesh(None, None) - def test_mesh_creation_no_parameters(self): - r = pybamm.SpatialVariable( - "r", domain=["negative particle"], coord_sys="spherical polar" - ) - - geometry = { - "negative particle": {r: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}} - } - + def test_mesh_creation_no_parameters(self, r, geometry): edges = np.array([0, 0.3, 1]) submesh_params = {"edges": edges} submesh_types = { @@ -286,18 +246,18 @@ def test_mesh_creation_no_parameters(self): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # check boundary locations - self.assertEqual(mesh["negative particle"].edges[0], 0) - self.assertEqual(mesh["negative particle"].edges[-1], 1) + assert mesh["negative particle"].edges[0] == 0 + assert mesh["negative particle"].edges[-1] == 1 # check number of edges and nodes - self.assertEqual(len(mesh["negative particle"].nodes), var_pts[r]) - self.assertEqual( - len(mesh["negative particle"].edges), - len(mesh["negative particle"].nodes) + 1, + assert len(mesh["negative particle"].nodes) == var_pts[r] + assert ( + len(mesh["negative particle"].edges) + == len(mesh["negative particle"].nodes) + 1 ) -class TestSpectralVolume1DSubMesh(unittest.TestCase): +class TestSpectralVolume1DSubMesh: def test_exceptions(self): edges = np.array([0, 0.3, 1]) submesh_params = {"edges": edges} @@ -306,30 +266,22 @@ def test_exceptions(self): # error if npts+1 != len(edges) lims = {"x_n": {"min": 0, "max": 1}} npts = {"x_n": 10} - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): mesh(lims, npts) # error if lims[0] not equal to edges[0] lims = {"x_n": {"min": 0.1, "max": 1}} npts = {"x_n": len(edges) - 1} - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): mesh(lims, npts) # error if lims[-1] not equal to edges[-1] lims = {"x_n": {"min": 0, "max": 10}} npts = {"x_n": len(edges) - 1} - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): mesh(lims, npts) - def test_mesh_creation_no_parameters(self): - r = pybamm.SpatialVariable( - "r", domain=["negative particle"], coord_sys="spherical polar" - ) - - geometry = { - "negative particle": {r: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}} - } - + def test_mesh_creation_no_parameters(self, r, geometry): edges = np.array([0, 0.3, 1]) order = 3 submesh_params = {"edges": edges, "order": order} @@ -344,15 +296,15 @@ def test_mesh_creation_no_parameters(self): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # check boundary locations - self.assertEqual(mesh["negative particle"].edges[0], 0) - self.assertEqual(mesh["negative particle"].edges[-1], 1) + assert mesh["negative particle"].edges[0] == 0 + assert mesh["negative particle"].edges[-1] == 1 # check number of edges and nodes - self.assertEqual(len(mesh["negative particle"].sv_nodes), var_pts[r]) - self.assertEqual(len(mesh["negative particle"].nodes), order * var_pts[r]) - self.assertEqual( - len(mesh["negative particle"].edges), - len(mesh["negative particle"].nodes) + 1, + assert len(mesh["negative particle"].sv_nodes) == var_pts[r] + assert len(mesh["negative particle"].nodes) == order * var_pts[r] + assert ( + len(mesh["negative particle"].edges) + == len(mesh["negative particle"].nodes) + 1 ) # check Chebyshev subdivision locations @@ -360,7 +312,7 @@ def test_mesh_creation_no_parameters(self): mesh["negative particle"].edges.tolist(), [0, 0.075, 0.225, 0.3, 0.475, 0.825, 1], ): - self.assertAlmostEqual(a, b) + assert a == pytest.approx(b) # test uniform submesh creation submesh_params = {"order": order} @@ -377,14 +329,4 @@ def test_mesh_creation_no_parameters(self): mesh["negative particle"].edges.tolist(), [0.0, 0.125, 0.375, 0.5, 0.625, 0.875, 1.0], ): - self.assertAlmostEqual(a, b) - - -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() + assert a == pytest.approx(b) diff --git a/tests/unit/test_meshes/test_scikit_fem_submesh.py b/tests/unit/test_meshes/test_scikit_fem_submesh.py index 07e7dd016a..30c45510e4 100644 --- a/tests/unit/test_meshes/test_scikit_fem_submesh.py +++ b/tests/unit/test_meshes/test_scikit_fem_submesh.py @@ -2,12 +2,13 @@ # Test for the scikit-fem Finite Element Mesh class # +import pytest import pybamm -import unittest import numpy as np -def get_param(): +@pytest.fixture() +def param(): return pybamm.ParameterValues( { "Electrode width [m]": 0.4, @@ -25,9 +26,8 @@ def get_param(): ) -class TestScikitFiniteElement2DSubMesh(unittest.TestCase): - def test_mesh_creation(self): - param = get_param() +class TestScikitFiniteElement2DSubMesh: + def test_mesh_creation(self, param): geometry = pybamm.battery_geometry( include_particles=False, options={"dimensionality": 2} ) @@ -46,23 +46,19 @@ def test_mesh_creation(self): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # check boundary locations - self.assertEqual(mesh["negative electrode"].edges[0], 0) - self.assertEqual(mesh["positive electrode"].edges[-1], 1) + assert mesh["negative electrode"].edges[0] == 0 + assert mesh["positive electrode"].edges[-1] == 1 # check internal boundary locations - self.assertEqual( - mesh["negative electrode"].edges[-1], mesh["separator"].edges[0] - ) - self.assertEqual( - mesh["positive electrode"].edges[0], mesh["separator"].edges[-1] - ) + assert mesh["negative electrode"].edges[-1] == mesh["separator"].edges[0] + assert mesh["positive electrode"].edges[0] == mesh["separator"].edges[-1] for domain in mesh.base_domains: if domain == "current collector": # NOTE: only for degree 1 npts = var_pts["y"] * var_pts["z"] - self.assertEqual(mesh[domain].npts, npts) + assert mesh[domain].npts == npts else: - self.assertEqual(len(mesh[domain].edges), len(mesh[domain].nodes) + 1) + assert len(mesh[domain].edges) == len(mesh[domain].nodes) + 1 def test_init_failure(self): submesh_types = { @@ -74,26 +70,26 @@ def test_init_failure(self): geometry = pybamm.battery_geometry( include_particles=False, options={"dimensionality": 2} ) - with self.assertRaises(KeyError): + with pytest.raises(KeyError): pybamm.Mesh(geometry, submesh_types, {}) var_pts = {"x_n": 10, "x_s": 10, "x_p": 10, "y": 10, "z": 10} # there are parameters in the variables that need to be processed - with self.assertRaisesRegex( + with pytest.raises( pybamm.DiscretisationError, - "Parameter values have not yet been set for geometry", + match="Parameter values have not yet been set for geometry", ): pybamm.Mesh(geometry, submesh_types, var_pts) lims = {"x_n": {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}} - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): pybamm.ScikitUniform2DSubMesh(lims, None) lims = { "x_n": {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}, "x_p": {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}, } - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): pybamm.ScikitUniform2DSubMesh(lims, None) lims = { @@ -106,7 +102,7 @@ def test_init_failure(self): "y": {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}, z: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}, } - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): pybamm.ScikitUniform2DSubMesh(lims, npts) def test_tab_error(self): @@ -142,7 +138,7 @@ def test_tab_error(self): include_particles=False, options={"dimensionality": 2} ) param.process_geometry(geometry) - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): pybamm.Mesh(geometry, submesh_types, var_pts) def test_tab_left_right(self): @@ -180,8 +176,7 @@ def test_tab_left_right(self): param.process_geometry(geometry) pybamm.Mesh(geometry, submesh_types, var_pts) - def test_to_json(self): - param = get_param() + def test_to_json(self, param): geometry = pybamm.battery_geometry( include_particles=False, options={"dimensionality": 2} ) @@ -216,7 +211,7 @@ def test_to_json(self): ], } - self.assertEqual(mesh_json, expected_json) + assert mesh_json == expected_json # test Uniform2DSubMesh serialisation @@ -276,7 +271,7 @@ def test_to_json(self): }, } - self.assertEqual(submesh, expected_submesh) + assert submesh == expected_submesh new_submesh = pybamm.ScikitUniform2DSubMesh._from_json(submesh) @@ -284,10 +279,8 @@ def test_to_json(self): np.testing.assert_array_equal(x, y) -class TestScikitFiniteElementChebyshev2DSubMesh(unittest.TestCase): - def test_mesh_creation(self): - param = get_param() - +class TestScikitFiniteElementChebyshev2DSubMesh: + def test_mesh_creation(self, param): geometry = pybamm.battery_geometry( include_particles=False, options={"dimensionality": 2} ) @@ -306,28 +299,24 @@ def test_mesh_creation(self): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # check boundary locations - self.assertEqual(mesh["negative electrode"].edges[0], 0) - self.assertEqual(mesh["positive electrode"].edges[-1], 1) + assert mesh["negative electrode"].edges[0] == 0 + assert mesh["positive electrode"].edges[-1] == 1 # check internal boundary locations - self.assertEqual( - mesh["negative electrode"].edges[-1], mesh["separator"].edges[0] - ) - self.assertEqual( - mesh["positive electrode"].edges[0], mesh["separator"].edges[-1] - ) + assert mesh["negative electrode"].edges[-1] == mesh["separator"].edges[0] + assert mesh["positive electrode"].edges[0] == mesh["separator"].edges[-1] for domain in mesh.base_domains: if domain == "current collector": # NOTE: only for degree 1 npts = var_pts["y"] * var_pts["z"] - self.assertEqual(mesh[domain].npts, npts) + assert mesh[domain].npts == npts else: - self.assertEqual(len(mesh[domain].edges), len(mesh[domain].nodes) + 1) + assert len(mesh[domain].edges) == len(mesh[domain].nodes) + 1 def test_init_failure(self): # only one lim lims = {"x_n": {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}} - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): pybamm.ScikitChebyshev2DSubMesh(lims, None) # different coord_sys @@ -335,7 +324,7 @@ def test_init_failure(self): "r_n": {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}, "z": {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}, } - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): pybamm.ScikitChebyshev2DSubMesh(lims, None) # not y and z @@ -343,14 +332,12 @@ def test_init_failure(self): "x_n": {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}, "z": {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}, } - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): pybamm.ScikitChebyshev2DSubMesh(lims, None) -class TestScikitExponential2DSubMesh(unittest.TestCase): - def test_mesh_creation(self): - param = get_param() - +class TestScikitExponential2DSubMesh: + def test_mesh_creation(self, param): geometry = pybamm.battery_geometry( include_particles=False, options={"dimensionality": 2} ) @@ -371,28 +358,24 @@ def test_mesh_creation(self): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # check boundary locations - self.assertEqual(mesh["negative electrode"].edges[0], 0) - self.assertEqual(mesh["positive electrode"].edges[-1], 1) + assert mesh["negative electrode"].edges[0] == 0 + assert mesh["positive electrode"].edges[-1] == 1 # check internal boundary locations - self.assertEqual( - mesh["negative electrode"].edges[-1], mesh["separator"].edges[0] - ) - self.assertEqual( - mesh["positive electrode"].edges[0], mesh["separator"].edges[-1] - ) + assert mesh["negative electrode"].edges[-1] == mesh["separator"].edges[0] + assert mesh["positive electrode"].edges[0] == mesh["separator"].edges[-1] for domain in mesh.base_domains: if domain == "current collector": # NOTE: only for degree 1 npts = var_pts["y"] * var_pts["z"] - self.assertEqual(mesh[domain].npts, npts) + assert mesh[domain].npts == npts else: - self.assertEqual(len(mesh[domain].edges), len(mesh[domain].nodes) + 1) + assert len(mesh[domain].edges) == len(mesh[domain].nodes) + 1 def test_init_failure(self): # only one lim lims = {"x_n": {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}} - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): pybamm.ScikitExponential2DSubMesh(lims, None) # different coord_sys @@ -400,7 +383,7 @@ def test_init_failure(self): "r_n": {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}, "z": {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}, } - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): pybamm.ScikitExponential2DSubMesh(lims, None) # not y and z @@ -408,18 +391,16 @@ def test_init_failure(self): "x_n": {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}, "z": {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}, } - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): pybamm.ScikitExponential2DSubMesh(lims, None) # side not top - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): pybamm.ScikitExponential2DSubMesh(None, None, side="bottom") -class TestScikitUser2DSubMesh(unittest.TestCase): - def test_mesh_creation(self): - param = get_param() - +class TestScikitUser2DSubMesh: + def test_mesh_creation(self, param): geometry = pybamm.battery_geometry( include_particles=False, options={"dimensionality": 2} ) @@ -444,23 +425,19 @@ def test_mesh_creation(self): mesh = pybamm.Mesh(geometry, submesh_types, var_pts) # check boundary locations - self.assertEqual(mesh["negative electrode"].edges[0], 0) - self.assertEqual(mesh["positive electrode"].edges[-1], 1) + assert mesh["negative electrode"].edges[0] == 0 + assert mesh["positive electrode"].edges[-1] == 1 # check internal boundary locations - self.assertEqual( - mesh["negative electrode"].edges[-1], mesh["separator"].edges[0] - ) - self.assertEqual( - mesh["positive electrode"].edges[0], mesh["separator"].edges[-1] - ) + assert mesh["negative electrode"].edges[-1] == mesh["separator"].edges[0] + assert mesh["positive electrode"].edges[0] == mesh["separator"].edges[-1] for domain in mesh.base_domains: if domain == "current collector": # NOTE: only for degree 1 npts = var_pts["y"] * var_pts["z"] - self.assertEqual(mesh[domain].npts, npts) + assert mesh[domain].npts == npts else: - self.assertEqual(len(mesh[domain].edges), len(mesh[domain].nodes) + 1) + assert len(mesh[domain].edges) == len(mesh[domain].nodes) + 1 def test_exceptions(self): lims = {"y": {"min": 0, "max": 1}} @@ -469,48 +446,38 @@ def test_exceptions(self): submesh_params = {"y_edges": y_edges, "z_edges": z_edges} mesh = pybamm.MeshGenerator(pybamm.UserSupplied2DSubMesh, submesh_params) # test not enough lims - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): mesh(lims, None) lims = {"y": {"min": 0, "max": 1}, "z": {"min": 0, "max": 1}} # error if len(edges) != npts npts = {"y": 10, "z": 3} - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): mesh(lims, npts) # error if lims[0] not equal to edges[0] lims = {"y": {"min": 0.1, "max": 1}, "z": {"min": 0, "max": 1}} npts = {"y": 3, "z": 3} - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): mesh(lims, npts) # error if lims[-1] not equal to edges[-1] lims = {"y": {"min": 0, "max": 1}, "z": {"min": 0, "max": 1.3}} npts = {"y": 3, "z": 3} - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): mesh(lims, npts) # error if different coordinate system lims = {"y": {"min": 0, "max": 1}, "r_n": {"min": 0, "max": 1}} npts = {"y": 3, "r_n": 3} - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): mesh(lims, npts) mesh = pybamm.MeshGenerator(pybamm.UserSupplied2DSubMesh) - with self.assertRaisesRegex(pybamm.GeometryError, "User mesh requires"): + with pytest.raises(pybamm.GeometryError, match="User mesh requires"): mesh(None, None) submesh_params = {"y_edges": np.array([0, 0.3, 1])} mesh = pybamm.MeshGenerator(pybamm.UserSupplied2DSubMesh, submesh_params) - with self.assertRaisesRegex(pybamm.GeometryError, "User mesh requires"): + with pytest.raises(pybamm.GeometryError, match="User mesh requires"): mesh(None, None) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_base_model.py b/tests/unit/test_models/test_base_model.py index d50cd66d2d..bfc7ad7473 100644 --- a/tests/unit/test_models/test_base_model.py +++ b/tests/unit/test_models/test_base_model.py @@ -2,10 +2,10 @@ # Tests for the base model class # +import pytest import os import platform import subprocess # nosec -import unittest from io import StringIO import sys @@ -16,7 +16,7 @@ import pybamm -class TestBaseModel(unittest.TestCase): +class TestBaseModel: def test_rhs_set_get(self): model = pybamm.BaseModel() rhs = { @@ -24,7 +24,7 @@ def test_rhs_set_get(self): pybamm.Symbol("d"): pybamm.Symbol("beta"), } model.rhs = rhs - self.assertEqual(rhs, model.rhs) + assert rhs == model.rhs # test domains rhs = { pybamm.Symbol("c", domain=["negative electrode"]): pybamm.Symbol( @@ -35,9 +35,9 @@ def test_rhs_set_get(self): ), } model.rhs = rhs - self.assertEqual(rhs, model.rhs) + assert rhs == model.rhs # non-matching domains should fail - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): model.rhs = { pybamm.Symbol("c", domain=["positive electrode"]): pybamm.Symbol( "alpha", domain=["negative electrode"] @@ -48,7 +48,7 @@ def test_algebraic_set_get(self): model = pybamm.BaseModel() algebraic = {pybamm.Symbol("b"): pybamm.Symbol("c") - pybamm.Symbol("a")} model.algebraic = algebraic - self.assertEqual(algebraic, model.algebraic) + assert algebraic == model.algebraic def test_initial_conditions_set_get(self): model = pybamm.BaseModel() @@ -57,22 +57,22 @@ def test_initial_conditions_set_get(self): pybamm.Symbol("d0"): pybamm.Symbol("delta"), } model.initial_conditions = initial_conditions - self.assertEqual(initial_conditions, model.initial_conditions) + assert initial_conditions == model.initial_conditions # Test number input c0 = pybamm.Symbol("c0") model.initial_conditions[c0] = 34 - self.assertIsInstance(model.initial_conditions[c0], pybamm.Scalar) - self.assertEqual(model.initial_conditions[c0].value, 34) + assert isinstance(model.initial_conditions[c0], pybamm.Scalar) + assert model.initial_conditions[c0].value == 34 # Variable in initial conditions should fail - with self.assertRaisesRegex( - TypeError, "Initial conditions cannot contain 'Variable' objects" + with pytest.raises( + TypeError, match="Initial conditions cannot contain 'Variable' objects" ): model.initial_conditions = {c0: pybamm.Variable("v")} # non-matching domains should fail - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): model.initial_conditions = { pybamm.Symbol("c", domain=["positive electrode"]): pybamm.Symbol( "alpha", domain=["negative electrode"] @@ -85,7 +85,7 @@ def test_boundary_conditions_set_get(self): "c": {"left": ("epsilon", "Dirichlet"), "right": ("eta", "Dirichlet")} } model.boundary_conditions = boundary_conditions - self.assertEqual(boundary_conditions, model.boundary_conditions) + assert boundary_conditions == model.boundary_conditions # Test number input c0 = pybamm.Symbol("c0") @@ -93,29 +93,29 @@ def test_boundary_conditions_set_get(self): "left": (-2, "Dirichlet"), "right": (4, "Dirichlet"), } - self.assertIsInstance(model.boundary_conditions[c0]["left"][0], pybamm.Scalar) - self.assertIsInstance(model.boundary_conditions[c0]["right"][0], pybamm.Scalar) - self.assertEqual(model.boundary_conditions[c0]["left"][0].value, -2) - self.assertEqual(model.boundary_conditions[c0]["right"][0].value, 4) - self.assertEqual(model.boundary_conditions[c0]["left"][1], "Dirichlet") - self.assertEqual(model.boundary_conditions[c0]["right"][1], "Dirichlet") + assert isinstance(model.boundary_conditions[c0]["left"][0], pybamm.Scalar) + assert isinstance(model.boundary_conditions[c0]["right"][0], pybamm.Scalar) + assert model.boundary_conditions[c0]["left"][0].value == -2 + assert model.boundary_conditions[c0]["right"][0].value == 4 + assert model.boundary_conditions[c0]["left"][1] == "Dirichlet" + assert model.boundary_conditions[c0]["right"][1] == "Dirichlet" # Check bad bc type bad_bcs = {c0: {"left": (-2, "bad type"), "right": (4, "bad type")}} - with self.assertRaisesRegex(pybamm.ModelError, "boundary condition"): + with pytest.raises(pybamm.ModelError, match="boundary condition"): model.boundary_conditions = bad_bcs def test_variables_set_get(self): model = pybamm.BaseModel() variables = {"c": "alpha", "d": "beta"} model.variables = variables - self.assertEqual(variables, model.variables) - self.assertEqual(model.variable_names(), list(variables.keys())) + assert variables == model.variables + assert model.variable_names() == list(variables.keys()) def test_jac_set_get(self): model = pybamm.BaseModel() model.jacobian = "test" - self.assertEqual(model.jacobian, "test") + assert model.jacobian == "test" def test_read_parameters(self): # Read parameters from different parts of the model @@ -143,18 +143,15 @@ def test_read_parameters(self): } # Test variables_and_events - self.assertIn("v+f+i", model.variables_and_events) - self.assertIn("Event: u=e", model.variables_and_events) + assert "v+f+i" in model.variables_and_events + assert "Event: u=e" in model.variables_and_events - self.assertEqual( - set([x.name for x in model.parameters]), - set([x.name for x in [a, b, c, d, e, f, g, h, i]]), + assert set([x.name for x in model.parameters]) == set( + [x.name for x in [a, b, c, d, e, f, g, h, i]] ) - self.assertTrue( - all( - isinstance(x, (pybamm.Parameter, pybamm.InputParameter)) - for x in model.parameters - ) + assert all( + isinstance(x, (pybamm.Parameter, pybamm.InputParameter)) + for x in model.parameters ) model.variables = { @@ -162,7 +159,8 @@ def test_read_parameters(self): } model.print_parameter_info() - def test_get_parameter_info(self): + @pytest.mark.parametrize("symbols", ["c", "d", "e", "f", "h", "i"]) + def test_get_parameter_info(self, symbols): model = pybamm.BaseModel() a = pybamm.InputParameter("a") b = pybamm.InputParameter("b", "test") @@ -187,17 +185,28 @@ def test_get_parameter_info(self): } parameter_info = model.get_parameter_info() - self.assertEqual(parameter_info["a"][1], "InputParameter") - self.assertEqual(parameter_info["b"][1], "InputParameter in ['test']") - self.assertIn("c", parameter_info) - self.assertIn("d", parameter_info) - self.assertIn("e", parameter_info) - self.assertIn("f", parameter_info) - self.assertEqual(parameter_info["g"][1], "Parameter") - self.assertIn("h", parameter_info) - self.assertIn("i", parameter_info) - - def test_get_parameter_info_submodel(self): + assert parameter_info["a"][1] == "InputParameter" + assert parameter_info["b"][1] == "InputParameter in ['test']" + assert symbols in parameter_info + assert parameter_info["g"][1] == "Parameter" + + @pytest.mark.parametrize( + "sub, key, parameter_value", + [ + ("sub1", "a", "InputParameter"), + ("sub1", "w", "InputParameter"), + ("sub1", "e", "InputParameter"), + ("sub1", "g", "Parameter"), + ("sub1", "x", "Parameter"), + ("sub1", "f", "InputParameter in ['test']"), + ("sub2", "b", "InputParameter in ['test']"), + ("sub2", "h", "Parameter"), + ("sub1", "c", "FunctionParameter with inputs(s) ''"), + ("sub2", "d", "FunctionParameter with inputs(s) ''"), + ("sub2", "i", "FunctionParameter with inputs(s) ''"), + ], + ) + def test_get_parameter_info_submodel(self, sub, key, parameter_value): submodel = pybamm.lithium_ion.SPM().submodels["electrolyte diffusion"] class SubModel1(pybamm.BaseSubModel): @@ -270,34 +279,15 @@ def set_initial_conditions(self, variables): expected_error_message = "Cannot use get_parameter_info" - with self.assertRaisesRegex(NotImplementedError, expected_error_message): + with pytest.raises(NotImplementedError, match=expected_error_message): submodel.get_parameter_info(by_submodel=True) - with self.assertRaisesRegex(NotImplementedError, expected_error_message): + with pytest.raises(NotImplementedError, match=expected_error_message): submodel.get_parameter_info(by_submodel=False) - self.assertIn("a", parameter_info["sub1"]) - self.assertIn("b", parameter_info["sub2"]) - self.assertEqual(parameter_info["sub1"]["a"][1], "InputParameter") - self.assertEqual(parameter_info["sub1"]["w"][1], "InputParameter") - self.assertEqual(parameter_info["sub1"]["e"][1], "InputParameter") - self.assertEqual(parameter_info["sub1"]["g"][1], "Parameter") - self.assertEqual(parameter_info["sub1"]["x"][1], "Parameter") - self.assertEqual(parameter_info["sub1"]["f"][1], "InputParameter in ['test']") - self.assertEqual(parameter_info["sub2"]["b"][1], "InputParameter in ['test']") - self.assertEqual(parameter_info["sub2"]["h"][1], "Parameter") - self.assertEqual( - parameter_info["sub1"]["c"][1], - "FunctionParameter with inputs(s) ''", - ) - self.assertEqual( - parameter_info["sub2"]["d"][1], - "FunctionParameter with inputs(s) ''", - ) - self.assertEqual( - parameter_info["sub2"]["i"][1], - "FunctionParameter with inputs(s) ''", - ) + assert "a" in parameter_info["sub1"] + assert "b" in parameter_info["sub2"] + assert parameter_info[sub][key][1] == parameter_value def test_print_parameter_info(self): model = pybamm.BaseModel() @@ -339,14 +329,31 @@ def test_print_parameter_info(self): sys.stdout = sys.__stdout__ result = captured_output.getvalue().strip() - self.assertIn("a", result) - self.assertIn("b", result) - self.assertIn("InputParameter", result) - self.assertIn("InputParameter in ['test']", result) - self.assertIn("Parameter", result) - self.assertIn("FunctionParameter with inputs(s) ''", result) - - def test_print_parameter_info_submodel(self): + assert "a" in result + assert "b" in result + assert "InputParameter" in result + assert "InputParameter in ['test']" in result + assert "Parameter" in result + assert "FunctionParameter with inputs(s) ''" in result + + @pytest.mark.parametrize( + "values", + [ + "'sub1' submodel parameters:", + "'sub2' submodel parameters:", + "Parameter", + "InputParameter", + "FunctionParameter with inputs(s) ''", + "InputParameter in ['test']", + "g", + "a", + "c", + "h", + "b", + "d", + ], + ) + def test_print_parameter_info_submodel(self, values): model = pybamm.BaseModel() a = pybamm.InputParameter("a") b = pybamm.InputParameter("b", "test") @@ -385,18 +392,7 @@ def test_print_parameter_info_submodel(self): sys.stdout = sys.__stdout__ result = captured_output.getvalue().strip() - self.assertIn("'sub1' submodel parameters:", result) - self.assertIn("'sub2' submodel parameters:", result) - self.assertIn("Parameter", result) - self.assertIn("InputParameter", result) - self.assertIn("FunctionParameter with inputs(s) ''", result) - self.assertIn("InputParameter in ['test']", result) - self.assertIn("g", result) - self.assertIn("a", result) - self.assertIn("c", result) - self.assertIn("h", result) - self.assertIn("b", result) - self.assertIn("d", result) + assert values in result def test_read_input_parameters(self): # Read input parameters from different parts of the model @@ -416,13 +412,10 @@ def test_read_input_parameters(self): model.events = [pybamm.Event("u=e", u - e)] model.variables = {"v+f": v + f} - self.assertEqual( - set([x.name for x in model.input_parameters]), - set([x.name for x in [a, b, c, d, e, f]]), - ) - self.assertTrue( - all(isinstance(x, pybamm.InputParameter) for x in model.input_parameters) + assert set([x.name for x in model.input_parameters]) == set( + [x.name for x in [a, b, c, d, e, f]] ) + assert all(isinstance(x, pybamm.InputParameter) for x in model.input_parameters) def test_update(self): # model @@ -452,19 +445,19 @@ def test_update(self): model.update(submodel) # check - self.assertEqual(model.rhs[d], submodel.rhs[d]) - self.assertEqual(model.initial_conditions[d], submodel.initial_conditions[d]) - self.assertEqual(model.boundary_conditions[d], submodel.boundary_conditions[d]) - self.assertEqual(model.variables["d"], submodel.variables["d"]) - self.assertEqual(model.rhs[c], rhs[c]) - self.assertEqual(model.initial_conditions[c], initial_conditions[c]) - self.assertEqual(model.boundary_conditions[c], boundary_conditions[c]) - self.assertEqual(model.variables["c"], variables["c"]) + assert model.rhs[d] == submodel.rhs[d] + assert model.initial_conditions[d] == submodel.initial_conditions[d] + assert model.boundary_conditions[d] == submodel.boundary_conditions[d] + assert model.variables["d"] == submodel.variables["d"] + assert model.rhs[c] == rhs[c] + assert model.initial_conditions[c] == initial_conditions[c] + assert model.boundary_conditions[c] == boundary_conditions[c] + assert model.variables["c"] == variables["c"] # update with conflicting submodel submodel2 = pybamm.BaseModel() submodel2.rhs = {d: pybamm.div(pybamm.grad(d)) - 1} - with self.assertRaises(pybamm.ModelError): + with pytest.raises(pybamm.ModelError): model.update(submodel2) # update with multiple submodels @@ -482,12 +475,12 @@ def test_update(self): model = pybamm.BaseModel() model.update(submodel1, submodel2) - self.assertEqual(model.rhs[d], submodel1.rhs[d]) - self.assertEqual(model.initial_conditions[d], submodel1.initial_conditions[d]) - self.assertEqual(model.boundary_conditions[d], submodel1.boundary_conditions[d]) - self.assertEqual(model.rhs[e], submodel2.rhs[e]) - self.assertEqual(model.initial_conditions[e], submodel2.initial_conditions[e]) - self.assertEqual(model.boundary_conditions[e], submodel2.boundary_conditions[e]) + assert model.rhs[d] == submodel1.rhs[d] + assert model.initial_conditions[d] == submodel1.initial_conditions[d] + assert model.boundary_conditions[d] == submodel1.boundary_conditions[d] + assert model.rhs[e] == submodel2.rhs[e] + assert model.initial_conditions[e] == submodel2.initial_conditions[e] + assert model.boundary_conditions[e] == submodel2.boundary_conditions[e] def test_new_copy(self): model = pybamm.BaseModel(name="a model") @@ -504,9 +497,9 @@ def test_new_copy(self): model.convert_to_format = "python" new_model = model.new_copy() - self.assertEqual(new_model.name, model.name) - self.assertEqual(new_model.use_jacobian, model.use_jacobian) - self.assertEqual(new_model.convert_to_format, model.convert_to_format) + assert new_model.name == model.name + assert new_model.use_jacobian == model.use_jacobian + assert new_model.convert_to_format == model.convert_to_format def test_check_no_repeated_keys(self): model = pybamm.BaseModel() @@ -515,7 +508,7 @@ def test_check_no_repeated_keys(self): model.rhs = {var: -1} var = pybamm.Variable("var") model.algebraic = {var: var} - with self.assertRaisesRegex(pybamm.ModelError, "Multiple equations specified"): + with pytest.raises(pybamm.ModelError, match="Multiple equations specified"): model.check_no_repeated_keys() def test_check_well_posedness_variables(self): @@ -540,26 +533,26 @@ def test_check_well_posedness_variables(self): # Underdetermined model - not enough differential equations model.rhs = {c: 5 * pybamm.div(pybamm.grad(d)) - 1} model.algebraic = {e: e - c - d} - with self.assertRaisesRegex(pybamm.ModelError, "underdetermined"): + with pytest.raises(pybamm.ModelError, match="underdetermined"): model.check_well_posedness() # Underdetermined model - not enough algebraic equations model.algebraic = {} - with self.assertRaisesRegex(pybamm.ModelError, "underdetermined"): + with pytest.raises(pybamm.ModelError, match="underdetermined"): model.check_well_posedness() # Overdetermined model - repeated keys model.algebraic = {c: c - d, d: e + d} - with self.assertRaisesRegex(pybamm.ModelError, "overdetermined"): + with pytest.raises(pybamm.ModelError, match="overdetermined"): model.check_well_posedness() # Overdetermined model - extra keys in algebraic model.rhs = {c: 5 * pybamm.div(pybamm.grad(d)) - 1, d: -d} model.algebraic = {e: c - d} - with self.assertRaisesRegex(pybamm.ModelError, "overdetermined"): + with pytest.raises(pybamm.ModelError, match="overdetermined"): model.check_well_posedness() model.rhs = {c: 1, d: -1} model.algebraic = {e: c - d} - with self.assertRaisesRegex(pybamm.ModelError, "overdetermined"): + with pytest.raises(pybamm.ModelError, match="overdetermined"): model.check_well_posedness() # After discretisation, don't check for overdetermined from extra algebraic keys @@ -568,7 +561,7 @@ def test_check_well_posedness_variables(self): # passes with post_discretisation=True model.check_well_posedness(post_discretisation=True) # fails with post_discretisation=False (default) - with self.assertRaisesRegex(pybamm.ModelError, "extra algebraic keys"): + with pytest.raises(pybamm.ModelError, match="extra algebraic keys"): model.check_well_posedness() # after discretisation, algebraic equation without a StateVector fails @@ -577,9 +570,9 @@ def test_check_well_posedness_variables(self): c: 1, d: pybamm.StateVector(slice(0, 15)) - pybamm.StateVector(slice(15, 30)), } - with self.assertRaisesRegex( + with pytest.raises( pybamm.ModelError, - "each algebraic equation must contain at least one StateVector", + match="each algebraic equation must contain at least one StateVector", ): model.check_well_posedness(post_discretisation=True) @@ -587,8 +580,8 @@ def test_check_well_posedness_variables(self): model = pybamm.BaseModel() model.rhs = {c: d.diff(pybamm.t), d: -1} model.initial_conditions = {c: 1, d: 1} - with self.assertRaisesRegex( - pybamm.ModelError, "time derivative of variable found" + with pytest.raises( + pybamm.ModelError, match="time derivative of variable found" ): model.check_well_posedness() @@ -596,8 +589,8 @@ def test_check_well_posedness_variables(self): model = pybamm.BaseModel() model.algebraic = {c: 2 * d - c, d: c * d.diff(pybamm.t) - d} model.initial_conditions = {c: 1, d: 1} - with self.assertRaisesRegex( - pybamm.ModelError, "time derivative of variable found" + with pytest.raises( + pybamm.ModelError, match="time derivative of variable found" ): model.check_well_posedness() @@ -605,8 +598,8 @@ def test_check_well_posedness_variables(self): model = pybamm.BaseModel() model.rhs = {c: d.diff(pybamm.t), d: -1} model.initial_conditions = {c: 1, d: 1} - with self.assertRaisesRegex( - pybamm.ModelError, "time derivative of variable found" + with pytest.raises( + pybamm.ModelError, match="time derivative of variable found" ): model.check_well_posedness() @@ -616,8 +609,8 @@ def test_check_well_posedness_variables(self): d: 5 * pybamm.StateVector(slice(0, 15)) - 1, c: 5 * pybamm.StateVectorDot(slice(0, 15)) - 1, } - with self.assertRaisesRegex( - pybamm.ModelError, "time derivative of state vector found" + with pytest.raises( + pybamm.ModelError, match="time derivative of state vector found" ): model.check_well_posedness(post_discretisation=True) @@ -625,8 +618,8 @@ def test_check_well_posedness_variables(self): model = pybamm.BaseModel() model.rhs = {c: 5 * pybamm.StateVectorDot(slice(0, 15)) - 1} model.initial_conditions = {c: 1} - with self.assertRaisesRegex( - pybamm.ModelError, "time derivative of state vector found" + with pytest.raises( + pybamm.ModelError, match="time derivative of state vector found" ): model.check_well_posedness(post_discretisation=True) @@ -651,7 +644,7 @@ def test_check_well_posedness_initial_boundary_conditions(self): # Model with bad initial conditions (expect assertion error) d = pybamm.Variable("d", domain=whole_cell) model.initial_conditions = {d: 3} - with self.assertRaisesRegex(pybamm.ModelError, "initial condition"): + with pytest.raises(pybamm.ModelError, match="initial condition"): model.check_well_posedness() # Algebraic well-posed model @@ -686,14 +679,14 @@ def test_check_well_posedness_output_variables(self): model.rhs = {c: -c} model.initial_conditions = {c: 1} model.variables = {"d": d} - with self.assertRaisesRegex(pybamm.ModelError, "No key set for variable"): + with pytest.raises(pybamm.ModelError, match="No key set for variable"): model.check_well_posedness() # check error is raised even if some modified form of d is in model.rhs two_d = 2 * d model.rhs[two_d] = -d model.initial_conditions[two_d] = 1 - with self.assertRaisesRegex(pybamm.ModelError, "No key set for variable"): + with pytest.raises(pybamm.ModelError, match="No key set for variable"): model.check_well_posedness() # add d to rhs, fine @@ -730,13 +723,13 @@ def test_export_casadi(self): var_fn = casadi.Function("var", [t, x, z, p], [var]) # Test that function values are as expected - self.assertEqual(x0_fn([0, 5]), 5) - self.assertEqual(z0_fn([0, 0]), 1) - self.assertEqual(rhs_fn(0, 3, 2, [7, 2]), -21) - self.assertEqual(alg_fn(0, 3, 2, [7, 2]), 1) + assert x0_fn([0, 5]) == 5 + assert z0_fn([0, 0]) == 1 + assert rhs_fn(0, 3, 2, [7, 2]) == -21 + assert alg_fn(0, 3, 2, [7, 2]) == 1 np.testing.assert_array_equal(np.array(jac_rhs_fn(5, 6, 7, [8, 9])), [[-8, 0]]) np.testing.assert_array_equal(np.array(jac_alg_fn(5, 6, 7, [8, 9])), [[1, -1]]) - self.assertEqual(var_fn(6, 3, 2, [7, 2]), -1) + assert var_fn(6, 3, 2, [7, 2]) == -1 # Now change the order of input parameters out = model.export_casadi_objects(["a+b"], input_parameter_order=["q", "p"]) @@ -756,18 +749,16 @@ def test_export_casadi(self): var_fn = casadi.Function("var", [t, x, z, p], [var]) # Test that function values are as expected - self.assertEqual(x0_fn([5, 0]), 5) - self.assertEqual(z0_fn([0, 0]), 1) - self.assertEqual(rhs_fn(0, 3, 2, [2, 7]), -21) - self.assertEqual(alg_fn(0, 3, 2, [2, 7]), 1) + assert x0_fn([5, 0]) == 5 + assert z0_fn([0, 0]) == 1 + assert rhs_fn(0, 3, 2, [2, 7]) == -21 + assert alg_fn(0, 3, 2, [2, 7]) == 1 np.testing.assert_array_equal(np.array(jac_rhs_fn(5, 6, 7, [9, 8])), [[-8, 0]]) np.testing.assert_array_equal(np.array(jac_alg_fn(5, 6, 7, [9, 8])), [[1, -1]]) - self.assertEqual(var_fn(6, 3, 2, [2, 7]), -1) + assert var_fn(6, 3, 2, [2, 7]) == -1 # Test fails if order not specified - with self.assertRaisesRegex( - ValueError, "input_parameter_order must be specified" - ): + with pytest.raises(ValueError, match="input_parameter_order must be specified"): model.export_casadi_objects(["a+b"]) # Fine if order is not specified if there is only one input parameter @@ -787,16 +778,16 @@ def test_export_casadi(self): # Test that function values are as expected # a + b - p = 3 + 2 - 7 = -2 - self.assertEqual(var_fn(6, 3, 2, [7]), -2) + assert var_fn(6, 3, 2, [7]) == -2 # Test fails if not discretised model = pybamm.lithium_ion.SPMe() - with self.assertRaisesRegex( - pybamm.DiscretisationError, "Cannot automatically discretise model" + with pytest.raises( + pybamm.DiscretisationError, match="Cannot automatically discretise model" ): model.export_casadi_objects(["Electrolyte concentration [mol.m-3]"]) - @unittest.skipIf(platform.system() == "Windows", "Skipped for Windows") + @pytest.mark.skipif(platform.system() == "Windows", reason="Skipped for Windows") def test_generate_casadi(self): model = pybamm.BaseModel() t = pybamm.t @@ -825,13 +816,13 @@ def test_generate_casadi(self): var_fn = casadi.external("variables", "./test.so") # Test that function values are as expected - self.assertEqual(x0_fn([2, 5]), 5) - self.assertEqual(z0_fn([0, 0]), 1) - self.assertEqual(rhs_fn(0, 3, 2, [7, 2]), -21) - self.assertEqual(alg_fn(0, 3, 2, [7, 2]), 1) + assert x0_fn([2, 5]) == 5 + assert z0_fn([0, 0]) == 1 + assert rhs_fn(0, 3, 2, [7, 2]) == -21 + assert alg_fn(0, 3, 2, [7, 2]) == 1 np.testing.assert_array_equal(np.array(jac_rhs_fn(5, 6, 7, [8, 9])), [[-8, 0]]) np.testing.assert_array_equal(np.array(jac_alg_fn(5, 6, 7, [8, 9])), [[1, -1]]) - self.assertEqual(var_fn(6, 3, 2, [7, 2]), -1) + assert var_fn(6, 3, 2, [7, 2]) == -1 # Remove generated files. os.remove("test.c") @@ -863,10 +854,10 @@ def test_set_initial_conditions(self): } # Test original initial conditions - self.assertEqual(model.initial_conditions[var_scalar].value, 1) - self.assertEqual(model.initial_conditions[var_1D].value, 1) - self.assertEqual(model.initial_conditions[var_2D].value, 1) - self.assertEqual(model.initial_conditions[var_concat].value, 1) + assert model.initial_conditions[var_scalar].value == 1 + assert model.initial_conditions[var_1D].value == 1 + assert model.initial_conditions[var_2D].value == 1 + assert model.initial_conditions[var_concat].value == 1 # Discretise geometry = { @@ -911,22 +902,22 @@ def test_set_initial_conditions(self): # Test new initial conditions (both in place and not) for mdl in [model, new_model]: var_scalar = mdl.variables["var_scalar"] - self.assertIsInstance(mdl.initial_conditions[var_scalar], pybamm.Vector) - self.assertEqual(mdl.initial_conditions[var_scalar].entries, 3) + assert isinstance(mdl.initial_conditions[var_scalar], pybamm.Vector) + assert mdl.initial_conditions[var_scalar].entries == 3 var_1D = mdl.variables["var_1D"] - self.assertIsInstance(mdl.initial_conditions[var_1D], pybamm.Vector) - self.assertEqual(mdl.initial_conditions[var_1D].shape, (10, 1)) + assert isinstance(mdl.initial_conditions[var_1D], pybamm.Vector) + assert mdl.initial_conditions[var_1D].shape == (10, 1) np.testing.assert_array_equal(mdl.initial_conditions[var_1D].entries, 3) var_2D = mdl.variables["var_2D"] - self.assertIsInstance(mdl.initial_conditions[var_2D], pybamm.Vector) - self.assertEqual(mdl.initial_conditions[var_2D].shape, (50, 1)) + assert isinstance(mdl.initial_conditions[var_2D], pybamm.Vector) + assert mdl.initial_conditions[var_2D].shape == (50, 1) np.testing.assert_array_equal(mdl.initial_conditions[var_2D].entries, 3) var_concat = mdl.variables["var_concat"] - self.assertIsInstance(mdl.initial_conditions[var_concat], pybamm.Vector) - self.assertEqual(mdl.initial_conditions[var_concat].shape, (20, 1)) + assert isinstance(mdl.initial_conditions[var_concat], pybamm.Vector) + assert mdl.initial_conditions[var_concat].shape == (20, 1) np.testing.assert_array_equal(mdl.initial_conditions[var_concat].entries, 3) # Test updating a discretised model (out-of-place) @@ -934,30 +925,26 @@ def test_set_initial_conditions(self): # Test new initial conditions var_scalar = next(iter(new_model_disc.initial_conditions.keys())) - self.assertIsInstance( - new_model_disc.initial_conditions[var_scalar], pybamm.Vector - ) - self.assertEqual(new_model_disc.initial_conditions[var_scalar].entries, 3) + assert isinstance(new_model_disc.initial_conditions[var_scalar], pybamm.Vector) + assert new_model_disc.initial_conditions[var_scalar].entries == 3 var_1D = list(new_model_disc.initial_conditions.keys())[1] - self.assertIsInstance(new_model_disc.initial_conditions[var_1D], pybamm.Vector) - self.assertEqual(new_model_disc.initial_conditions[var_1D].shape, (10, 1)) + assert isinstance(new_model_disc.initial_conditions[var_1D], pybamm.Vector) + assert new_model_disc.initial_conditions[var_1D].shape == (10, 1) np.testing.assert_array_equal( new_model_disc.initial_conditions[var_1D].entries, 3 ) var_2D = list(new_model_disc.initial_conditions.keys())[2] - self.assertIsInstance(new_model_disc.initial_conditions[var_2D], pybamm.Vector) - self.assertEqual(new_model_disc.initial_conditions[var_2D].shape, (50, 1)) + assert isinstance(new_model_disc.initial_conditions[var_2D], pybamm.Vector) + assert new_model_disc.initial_conditions[var_2D].shape == (50, 1) np.testing.assert_array_equal( new_model_disc.initial_conditions[var_2D].entries, 3 ) var_concat = list(new_model_disc.initial_conditions.keys())[3] - self.assertIsInstance( - new_model_disc.initial_conditions[var_concat], pybamm.Vector - ) - self.assertEqual(new_model_disc.initial_conditions[var_concat].shape, (20, 1)) + assert isinstance(new_model_disc.initial_conditions[var_concat], pybamm.Vector) + assert new_model_disc.initial_conditions[var_concat].shape == (20, 1) np.testing.assert_array_equal( new_model_disc.initial_conditions[var_concat].entries, 3 ) @@ -1008,22 +995,22 @@ def test_set_initial_conditions(self): # Test new initial conditions (both in place and not) var_scalar = new_model.variables["var_scalar"] - self.assertIsInstance(new_model.initial_conditions[var_scalar], pybamm.Vector) - self.assertEqual(new_model.initial_conditions[var_scalar].entries, 3) + assert isinstance(new_model.initial_conditions[var_scalar], pybamm.Vector) + assert new_model.initial_conditions[var_scalar].entries == 3 var_1D = new_model.variables["var_1D"] - self.assertIsInstance(new_model.initial_conditions[var_1D], pybamm.Vector) - self.assertEqual(new_model.initial_conditions[var_1D].shape, (10, 1)) + assert isinstance(new_model.initial_conditions[var_1D], pybamm.Vector) + assert new_model.initial_conditions[var_1D].shape == (10, 1) np.testing.assert_array_equal(new_model.initial_conditions[var_1D].entries, 3) var_2D = new_model.variables["var_2D"] - self.assertIsInstance(new_model.initial_conditions[var_2D], pybamm.Vector) - self.assertEqual(new_model.initial_conditions[var_2D].shape, (50, 1)) + assert isinstance(new_model.initial_conditions[var_2D], pybamm.Vector) + assert new_model.initial_conditions[var_2D].shape == (50, 1) np.testing.assert_array_equal(new_model.initial_conditions[var_2D].entries, 3) var_concat = new_model.variables["var_concat"] - self.assertIsInstance(new_model.initial_conditions[var_concat], pybamm.Vector) - self.assertEqual(new_model.initial_conditions[var_concat].shape, (20, 1)) + assert isinstance(new_model.initial_conditions[var_concat], pybamm.Vector) + assert new_model.initial_conditions[var_concat].shape == (20, 1) np.testing.assert_array_equal( new_model.initial_conditions[var_concat].entries, 3 ) @@ -1040,22 +1027,22 @@ def test_set_initial_conditions(self): # Test new initial conditions (both in place and not) var_scalar = new_model.variables["var_scalar"] - self.assertIsInstance(new_model.initial_conditions[var_scalar], pybamm.Vector) - self.assertEqual(new_model.initial_conditions[var_scalar].entries, 5) + assert isinstance(new_model.initial_conditions[var_scalar], pybamm.Vector) + assert new_model.initial_conditions[var_scalar].entries == 5 var_1D = new_model.variables["var_1D"] - self.assertIsInstance(new_model.initial_conditions[var_1D], pybamm.Vector) - self.assertEqual(new_model.initial_conditions[var_1D].shape, (10, 1)) + assert isinstance(new_model.initial_conditions[var_1D], pybamm.Vector) + assert new_model.initial_conditions[var_1D].shape == (10, 1) np.testing.assert_array_equal(new_model.initial_conditions[var_1D].entries, 5) var_2D = new_model.variables["var_2D"] - self.assertIsInstance(new_model.initial_conditions[var_2D], pybamm.Vector) - self.assertEqual(new_model.initial_conditions[var_2D].shape, (50, 1)) + assert isinstance(new_model.initial_conditions[var_2D], pybamm.Vector) + assert new_model.initial_conditions[var_2D].shape == (50, 1) np.testing.assert_array_equal(new_model.initial_conditions[var_2D].entries, 5) var_concat = new_model.variables["var_concat"] - self.assertIsInstance(new_model.initial_conditions[var_concat], pybamm.Vector) - self.assertEqual(new_model.initial_conditions[var_concat].shape, (20, 1)) + assert isinstance(new_model.initial_conditions[var_concat], pybamm.Vector) + assert new_model.initial_conditions[var_concat].shape == (20, 1) np.testing.assert_array_equal( new_model.initial_conditions[var_concat].entries, 5 ) @@ -1066,30 +1053,26 @@ def test_set_initial_conditions(self): # Test new initial conditions var_scalar = next(iter(new_model_disc.initial_conditions.keys())) - self.assertIsInstance( - new_model_disc.initial_conditions[var_scalar], pybamm.Vector - ) - self.assertEqual(new_model_disc.initial_conditions[var_scalar].entries, 5) + assert isinstance(new_model_disc.initial_conditions[var_scalar], pybamm.Vector) + assert new_model_disc.initial_conditions[var_scalar].entries == 5 var_1D = list(new_model_disc.initial_conditions.keys())[1] - self.assertIsInstance(new_model_disc.initial_conditions[var_1D], pybamm.Vector) - self.assertEqual(new_model_disc.initial_conditions[var_1D].shape, (10, 1)) + assert isinstance(new_model_disc.initial_conditions[var_1D], pybamm.Vector) + assert new_model_disc.initial_conditions[var_1D].shape == (10, 1) np.testing.assert_array_equal( new_model_disc.initial_conditions[var_1D].entries, 5 ) var_2D = list(new_model_disc.initial_conditions.keys())[2] - self.assertIsInstance(new_model_disc.initial_conditions[var_2D], pybamm.Vector) - self.assertEqual(new_model_disc.initial_conditions[var_2D].shape, (50, 1)) + assert isinstance(new_model_disc.initial_conditions[var_2D], pybamm.Vector) + assert new_model_disc.initial_conditions[var_2D].shape == (50, 1) np.testing.assert_array_equal( new_model_disc.initial_conditions[var_2D].entries, 5 ) var_concat = list(new_model_disc.initial_conditions.keys())[3] - self.assertIsInstance( - new_model_disc.initial_conditions[var_concat], pybamm.Vector - ) - self.assertEqual(new_model_disc.initial_conditions[var_concat].shape, (20, 1)) + assert isinstance(new_model_disc.initial_conditions[var_concat], pybamm.Vector) + assert new_model_disc.initial_conditions[var_concat].shape == (20, 1) np.testing.assert_array_equal( new_model_disc.initial_conditions[var_concat].entries, 5 ) @@ -1103,7 +1086,7 @@ def test_set_initial_condition_errors(self): var = pybamm.Scalar(1) model.rhs = {var: -var} model.initial_conditions = {var: 1} - with self.assertRaisesRegex(NotImplementedError, "Variable must have type"): + with pytest.raises(NotImplementedError, match="Variable must have type"): model.set_initial_conditions_from({}) var = pybamm.Variable( @@ -1116,9 +1099,7 @@ def test_set_initial_condition_errors(self): ) model.rhs = {var: -var} model.initial_conditions = {var: 1} - with self.assertRaisesRegex( - NotImplementedError, "Variable must be 0D, 1D, or 2D" - ): + with pytest.raises(NotImplementedError, match="Variable must be 0D, 1D, or 2D"): model.set_initial_conditions_from({"var": np.ones((5, 6, 7, 8))}) var_concat_neg = pybamm.Variable("var concat neg", domain="negative electrode") @@ -1126,8 +1107,8 @@ def test_set_initial_condition_errors(self): var_concat = pybamm.concatenation(var_concat_neg, var_concat_sep) model.algebraic = {var_concat: -var_concat} model.initial_conditions = {var_concat: 1} - with self.assertRaisesRegex( - NotImplementedError, "Variable in concatenation must be 1D" + with pytest.raises( + NotImplementedError, match="Variable in concatenation must be 1D" ): model.set_initial_conditions_from({"var concat neg": np.ones((5, 6, 7))}) @@ -1136,20 +1117,20 @@ def test_set_initial_condition_errors(self): var = pybamm.Variable("var") model.rhs = {var: -var} model.initial_conditions = {var: pybamm.Scalar(1)} - with self.assertRaisesRegex(pybamm.ModelError, "must appear in the solution"): + with pytest.raises(pybamm.ModelError, match="must appear in the solution"): model.set_initial_conditions_from({"wrong var": 2}) var = pybamm.concatenation( pybamm.Variable("var", "test"), pybamm.Variable("var2", "test2") ) model.rhs = {var: -var} model.initial_conditions = {var: pybamm.Scalar(1)} - with self.assertRaisesRegex(pybamm.ModelError, "must appear in the solution"): + with pytest.raises(pybamm.ModelError, match="must appear in the solution"): model.set_initial_conditions_from({"wrong var": 2}) def test_set_variables_error(self): var = pybamm.Variable("var") model = pybamm.BaseModel() - with self.assertRaisesRegex(ValueError, "not var"): + with pytest.raises(ValueError, match="not var"): model.variables = {"not var": var} def test_build_submodels(self): @@ -1202,23 +1183,23 @@ def get_coupled_variables(self, variables): "submodel 1": Submodel1(None, "negative"), "submodel 2": Submodel2(None, "negative"), } - self.assertFalse(model._built) + assert not model._built model.build_model() - self.assertTrue(model._built) + assert model._built u = model.variables["u"] v = model.variables["v"] - self.assertEqual(model.rhs[u].value, 2) - self.assertEqual(model.algebraic[v], -1.0 + v) + assert model.rhs[u].value == 2 + assert model.algebraic[v] == -1.0 + v def test_timescale_lengthscale_get_set_not_implemented(self): model = pybamm.BaseModel() - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): model.timescale - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): model.length_scales - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): model.timescale = 1 - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): model.length_scales = 1 def test_save_load_model(self): @@ -1284,7 +1265,7 @@ def test_save_load_model(self): testing.assert_array_equal(solution.all_ys, new_solution.all_ys) # raises warning if variables are saved without mesh - with self.assertWarns(pybamm.ModelWarning): + with pytest.warns(pybamm.ModelWarning): model_disc.save_model( filename="test_base_model", variables=model_disc.variables ) @@ -1297,13 +1278,3 @@ def test_save_load_model(self): new_model = pybamm.load_model("test_base_model.json") os.remove("test_base_model.json") - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py b/tests/unit/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py index 39bfaf1145..4f54db7035 100644 --- a/tests/unit/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py +++ b/tests/unit/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py @@ -3,10 +3,10 @@ # import pybamm -import unittest +import pytest -class TestThevenin(unittest.TestCase): +class TestThevenin: def test_standard_model(self): model = pybamm.equivalent_circuit.Thevenin() model.check_well_posedness() @@ -16,22 +16,18 @@ def test_default_properties(self): x = model.variables["x ECMD"] # test var_pts - self.assertEqual(model.default_var_pts, {x: 20}) + assert model.default_var_pts == {x: 20} # test geometry - self.assertEqual( - model.default_geometry, {"ECMD particle": {x: {"min": 0, "max": 1}}} - ) + assert model.default_geometry == {"ECMD particle": {x: {"min": 0, "max": 1}}} # test spatial methods - self.assertIsInstance( + assert isinstance( model.default_spatial_methods["ECMD particle"], pybamm.FiniteVolume ) # test submesh types - self.assertEqual( - model.default_submesh_types, {"ECMD particle": pybamm.Uniform1DSubMesh} - ) + assert model.default_submesh_types == {"ECMD particle": pybamm.Uniform1DSubMesh} def test_changing_number_of_rcs(self): options = {"number of rc elements": 0} @@ -50,7 +46,7 @@ def test_changing_number_of_rcs(self): model = pybamm.equivalent_circuit.Thevenin(options=options) model.check_well_posedness() - with self.assertRaisesRegex(pybamm.OptionError, "natural numbers"): + with pytest.raises(pybamm.OptionError, match="natural numbers"): options = {"number of rc elements": -1} model = pybamm.equivalent_circuit.Thevenin(options=options) model.check_well_posedness() @@ -114,35 +110,25 @@ def external_circuit_function(variables): def test_raise_option_error(self): options = {"not an option": "something"} - with self.assertRaisesRegex( - pybamm.OptionError, "Option 'not an option' not recognised" + with pytest.raises( + pybamm.OptionError, match="Option 'not an option' not recognised" ): pybamm.equivalent_circuit.Thevenin(options=options) def test_not_a_valid_option(self): options = {"operating mode": "not a valid option"} - with self.assertRaisesRegex( - pybamm.OptionError, "Option 'operating mode' must be one of" + with pytest.raises( + pybamm.OptionError, match="Option 'operating mode' must be one of" ): pybamm.equivalent_circuit.Thevenin(options=options) def test_get_default_parameters(self): model = pybamm.equivalent_circuit.Thevenin() values = model.default_parameter_values - self.assertIn("Initial SoC", list(values.keys())) + assert "Initial SoC" in list(values.keys()) values.process_model(model) def test_get_default_quick_plot_variables(self): model = pybamm.equivalent_circuit.Thevenin() variables = model.default_quick_plot_variables - self.assertIn("Current [A]", variables) - - -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() + assert "Current [A]" in variables 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 d68686936c..e1e3dce5f5 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 @@ -3,10 +3,9 @@ # import pybamm -import unittest -class TestLeadAcidLOQS(unittest.TestCase): +class TestLeadAcidLOQS: def test_well_posed(self): options = {"thermal": "isothermal"} model = pybamm.lead_acid.LOQS(options) @@ -20,17 +19,15 @@ def test_well_posed(self): def test_default_geometry(self): options = {"thermal": "isothermal"} model = pybamm.lead_acid.LOQS(options) - self.assertNotIn("negative particle", model.default_geometry) - self.assertIsInstance(model.default_spatial_methods, dict) - self.assertIsInstance( + assert "negative particle" not in model.default_geometry + assert isinstance(model.default_spatial_methods, dict) + assert isinstance( model.default_spatial_methods["current collector"], pybamm.ZeroDimensionalSpatialMethod, ) - self.assertTrue( - issubclass( - model.default_submesh_types["current collector"], - pybamm.SubMesh0D, - ) + assert issubclass( + model.default_submesh_types["current collector"], + pybamm.SubMesh0D, ) def test_well_posed_with_convection(self): @@ -42,7 +39,7 @@ def test_well_posed_with_convection(self): model = pybamm.lead_acid.LOQS(options) model.check_well_posedness() - def test_well_posed_1plus1D(self): + def test_well_posed_1plus1_d(self): options = { "surface form": "differential", "current collector": "potential pair", @@ -50,17 +47,15 @@ def test_well_posed_1plus1D(self): } model = pybamm.lead_acid.LOQS(options) model.check_well_posedness() - self.assertIsInstance( + assert isinstance( model.default_spatial_methods["current collector"], pybamm.FiniteVolume ) - self.assertTrue( - issubclass( - model.default_submesh_types["current collector"], - pybamm.Uniform1DSubMesh, - ) + assert issubclass( + model.default_submesh_types["current collector"], + pybamm.Uniform1DSubMesh, ) - def test_well_posed_2plus1D(self): + def test_well_posed_2plus1_d(self): options = { "surface form": "differential", "current collector": "potential pair", @@ -68,19 +63,17 @@ def test_well_posed_2plus1D(self): } model = pybamm.lead_acid.LOQS(options) model.check_well_posedness() - self.assertIsInstance( + assert isinstance( model.default_spatial_methods["current collector"], pybamm.ScikitFiniteElement, ) - self.assertTrue( - issubclass( - model.default_submesh_types["current collector"], - pybamm.ScikitUniform2DSubMesh, - ) + assert issubclass( + model.default_submesh_types["current collector"], + pybamm.ScikitUniform2DSubMesh, ) -class TestLeadAcidLOQSWithSideReactions(unittest.TestCase): +class TestLeadAcidLOQSWithSideReactions: def test_well_posed_differential(self): options = {"surface form": "differential", "hydrolysis": "true"} model = pybamm.lead_acid.LOQS(options) @@ -92,7 +85,7 @@ def test_well_posed_algebraic(self): model.check_well_posedness() -class TestLeadAcidLOQSSurfaceForm(unittest.TestCase): +class TestLeadAcidLOQSSurfaceForm: def test_well_posed_differential(self): options = {"surface form": "differential"} model = pybamm.lead_acid.LOQS(options) @@ -103,7 +96,7 @@ def test_well_posed_algebraic(self): model = pybamm.lead_acid.LOQS(options) model.check_well_posedness() - def test_well_posed_1plus1D(self): + def test_well_posed_1plus1_d(self): options = { "surface form": "differential", "current collector": "potential pair", @@ -115,13 +108,13 @@ def test_well_posed_1plus1D(self): def test_default_geometry(self): options = {"surface form": "differential"} model = pybamm.lead_acid.LOQS(options) - self.assertIn("current collector", model.default_geometry) + assert "current collector" in model.default_geometry options.update({"current collector": "potential pair", "dimensionality": 1}) model = pybamm.lead_acid.LOQS(options) - self.assertIn("current collector", model.default_geometry) + assert "current collector" in model.default_geometry -class TestLeadAcidLOQSExternalCircuits(unittest.TestCase): +class TestLeadAcidLOQSExternalCircuits: def test_well_posed_voltage(self): options = {"operating mode": "voltage"} model = pybamm.lead_acid.LOQS(options) @@ -152,13 +145,3 @@ def test_well_posed_discharge_energy(self): options = {"calculate discharge energy": "true"} model = pybamm.lead_acid.LOQS(options) model.check_well_posedness() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py index 9c093c0c65..7b690257dc 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py @@ -588,3 +588,20 @@ def test_well_posed_composite_different_degradation(self): "lithium plating": (("none", "irreversible"), "none"), } self.check_well_posedness(options) + + def test_well_posed_composite_LAM(self): + # phases with LAM degradation + options = { + "particle phases": ("2", "1"), + "open-circuit potential": (("single", "current sigmoid"), "single"), + "SEI": "solvent-diffusion limited", + "loss of active material": "reaction-driven", + } + self.check_well_posedness(options) + + options = { + "particle phases": ("2", "1"), + "open-circuit potential": (("single", "current sigmoid"), "single"), + "loss of active material": "stress-driven", + } + self.check_well_posedness(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_electrode_soh.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_electrode_soh.py index 9f044b0566..80ff155369 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_electrode_soh.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_electrode_soh.py @@ -2,11 +2,23 @@ # Tests for the lithium-ion electrode-specific SOH model # +import pytest import pybamm -import unittest -class TestElectrodeSOH(unittest.TestCase): +# Fixture for TestElectrodeSOHMSMR, TestCalculateTheoreticalEnergy and TestGetInitialOCPMSMR class. +@pytest.fixture() +def options(): + options = { + "open-circuit potential": "MSMR", + "particle": "MSMR", + "number of MSMR reactions": ("6", "4"), + "intercalation kinetics": "MSMR", + } + return options + + +class TestElectrodeSOH: def test_known_solution(self): param = pybamm.LithiumIonParameters() parameter_values = pybamm.ParameterValues("Mohtat2020") @@ -24,16 +36,16 @@ def test_known_solution(self): # Solve the model and check outputs sol = esoh_solver.solve(inputs) - self.assertAlmostEqual(sol["Up(y_100) - Un(x_100)"], Vmax, places=5) - self.assertAlmostEqual(sol["Up(y_0) - Un(x_0)"], Vmin, places=5) - self.assertAlmostEqual(sol["Q_Li"], Q_Li, places=5) + assert sol["Up(y_100) - Un(x_100)"] == pytest.approx(Vmax, abs=1e-05) + assert sol["Up(y_0) - Un(x_0)"] == pytest.approx(Vmin, abs=1e-05) + assert sol["Q_Li"] == pytest.approx(Q_Li, abs=1e-05) # Solve with split esoh and check outputs ics = esoh_solver._set_up_solve(inputs) sol_split = esoh_solver._solve_split(inputs, ics) for key in sol: if key != "Maximum theoretical energy [W.h]": - self.assertAlmostEqual(sol[key], sol_split[key].data[0], places=5) + assert sol[key] == pytest.approx(sol_split[key].data[0], abs=1e-05) else: # theoretical_energy is not present in sol_split inputs = { @@ -41,7 +53,7 @@ def test_known_solution(self): for k in ["x_0", "y_0", "x_100", "y_100", "Q_p"] } energy = esoh_solver.theoretical_energy_integral(inputs) - self.assertAlmostEqual(sol[key], energy, places=5) + assert sol[key] == pytest.approx(energy, abs=1e-05) def test_known_solution_cell_capacity(self): param = pybamm.LithiumIonParameters() @@ -62,9 +74,9 @@ def test_known_solution_cell_capacity(self): # Solve the model and check outputs sol = esoh_solver.solve(inputs) - self.assertAlmostEqual(sol["Up(y_100) - Un(x_100)"], Vmax, places=5) - self.assertAlmostEqual(sol["Up(y_0) - Un(x_0)"], Vmin, places=5) - self.assertAlmostEqual(sol["Q"], Q, places=5) + assert sol["Up(y_100) - Un(x_100)"] == pytest.approx(Vmax, abs=1e-05) + assert sol["Up(y_0) - Un(x_0)"] == pytest.approx(Vmin, abs=1e-05) + assert sol["Q"] == pytest.approx(Q, abs=1e-05) def test_error(self): param = pybamm.LithiumIonParameters() @@ -79,7 +91,7 @@ def test_error(self): inputs = {"Q_Li": Q_Li, "Q_n": Q_n, "Q_p": Q_p} # Solve the model and check outputs - with self.assertRaisesRegex(ValueError, "outside the range"): + with pytest.raises(ValueError, match="outside the range"): esoh_solver.solve(inputs) Q_Li = parameter_values.evaluate(param.Q_Li_particles_init) @@ -93,8 +105,8 @@ def test_error(self): esoh_solver = pybamm.lithium_ion.ElectrodeSOHSolver(parameter_values, param) inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li} # Solver fails to find a solution but voltage limits are not violated - with self.assertRaisesRegex( - pybamm.SolverError, "Could not find acceptable solution" + with pytest.raises( + pybamm.SolverError, match="Could not find acceptable solution" ): esoh_solver.solve(inputs) # Solver fails to find a solution due to upper voltage limit @@ -108,7 +120,7 @@ def test_error(self): ) esoh_solver = pybamm.lithium_ion.ElectrodeSOHSolver(parameter_values, param) inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li} - with self.assertRaisesRegex(ValueError, "upper bound of the voltage"): + with pytest.raises(ValueError, match="upper bound of the voltage"): esoh_solver.solve(inputs) # Solver fails to find a solution due to lower voltage limit parameter_values.update( @@ -121,7 +133,7 @@ def test_error(self): ) esoh_solver = pybamm.lithium_ion.ElectrodeSOHSolver(parameter_values, param) inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li} - with self.assertRaisesRegex(ValueError, "lower bound of the voltage"): + with pytest.raises(ValueError, match="lower bound of the voltage"): esoh_solver.solve(inputs) # errors for cell capacity based solver @@ -136,24 +148,18 @@ def test_error(self): esoh_solver = pybamm.lithium_ion.ElectrodeSOHSolver( parameter_values, param, known_value="cell capacity" ) - with self.assertRaisesRegex(ValueError, "solve_for must be "): + with pytest.raises(ValueError, match="solve_for must be "): esoh_solver._get_electrode_soh_sims_split() inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q": 2 * Q_p} - with self.assertRaisesRegex( - ValueError, "larger than the maximum possible capacity" + with pytest.raises( + ValueError, match="larger than the maximum possible capacity" ): esoh_solver.solve(inputs) -class TestElectrodeSOHMSMR(unittest.TestCase): - def test_known_solution(self): - options = { - "open-circuit potential": "MSMR", - "particle": "MSMR", - "number of MSMR reactions": ("6", "4"), - "intercalation kinetics": "MSMR", - } +class TestElectrodeSOHMSMR: + def test_known_solution(self, options): param = pybamm.LithiumIonParameters(options=options) parameter_values = pybamm.ParameterValues("MSMR_Example") @@ -172,27 +178,21 @@ def test_known_solution(self): # Solve the model and check outputs sol = esoh_solver.solve(inputs) - self.assertAlmostEqual(sol["Up(y_100) - Un(x_100)"], Vmax, places=5) - self.assertAlmostEqual(sol["Up(y_0) - Un(x_0)"], Vmin, places=5) - self.assertAlmostEqual(sol["Q_Li"], Q_Li, places=5) + assert sol["Up(y_100) - Un(x_100)"] == pytest.approx(Vmax, abs=1e-05) + assert sol["Up(y_0) - Un(x_0)"] == pytest.approx(Vmin, abs=1e-05) + assert sol["Q_Li"] == pytest.approx(Q_Li, abs=1e-05) # Solve with split esoh and check outputs ics = esoh_solver._set_up_solve(inputs) sol_split = esoh_solver._solve_split(inputs, ics) for key in sol: if key != "Maximum theoretical energy [W.h]": - self.assertAlmostEqual(sol[key], sol_split[key].data[0], places=5) + assert sol[key] == pytest.approx(sol_split[key].data[0], abs=1e-05) # Check feasibility checks can be performed successfully esoh_solver._check_esoh_feasible(inputs) - def test_known_solution_cell_capacity(self): - options = { - "open-circuit potential": "MSMR", - "particle": "MSMR", - "number of MSMR reactions": ("6", "4"), - "intercalation kinetics": "MSMR", - } + def test_known_solution_cell_capacity(self, options): param = pybamm.LithiumIonParameters(options) parameter_values = pybamm.ParameterValues("MSMR_Example") @@ -211,28 +211,22 @@ def test_known_solution_cell_capacity(self): # Solve the model and check outputs sol = esoh_solver.solve(inputs) - self.assertAlmostEqual(sol["Up(y_100) - Un(x_100)"], Vmax, places=5) - self.assertAlmostEqual(sol["Up(y_0) - Un(x_0)"], Vmin, places=5) - self.assertAlmostEqual(sol["Q"], Q, places=5) + assert sol["Up(y_100) - Un(x_100)"] == pytest.approx(Vmax, abs=1e-05) + assert sol["Up(y_0) - Un(x_0)"] == pytest.approx(Vmin, abs=1e-05) + assert sol["Q"] == pytest.approx(Q, abs=1e-05) - def test_error(self): - options = { - "open-circuit potential": "MSMR", - "particle": "MSMR", - "number of MSMR reactions": ("6", "4"), - "intercalation kinetics": "MSMR", - } + def test_error(self, options): param = pybamm.LithiumIonParameters(options) parameter_values = pybamm.ParameterValues("MSMR_Example") esoh_solver = pybamm.lithium_ion.ElectrodeSOHSolver( parameter_values, param, known_value="cell capacity", options=options ) - with self.assertRaisesRegex(ValueError, "solve_for must be "): + with pytest.raises(ValueError, match="solve_for must be "): esoh_solver._get_electrode_soh_sims_split() -class TestElectrodeSOHHalfCell(unittest.TestCase): +class TestElectrodeSOHHalfCell: def test_known_solution(self): model = pybamm.lithium_ion.ElectrodeSOHHalfCell() param = pybamm.LithiumIonParameters({"working electrode": "positive"}) @@ -243,12 +237,12 @@ def test_known_solution(self): V_max = 4.2 # Solve the model and check outputs sol = sim.solve([0], inputs={"Q_w": Q_w}) - self.assertAlmostEqual(sol["Uw(x_100)"].data[0], V_max, places=5) - self.assertAlmostEqual(sol["Uw(x_0)"].data[0], V_min, places=5) + assert sol["Uw(x_100)"].data[0] == pytest.approx(V_max, abs=1e-05) + assert sol["Uw(x_0)"].data[0] == pytest.approx(V_min, abs=1e-05) -class TestCalculateTheoreticalEnergy(unittest.TestCase): - def test_efficiency(self): +class TestCalculateTheoreticalEnergy: + def test_efficiency(self, options): model = pybamm.lithium_ion.DFN(options={"calculate discharge energy": "true"}) parameter_values = pybamm.ParameterValues("Chen2020") sim = pybamm.Simulation(model, parameter_values=parameter_values) @@ -261,12 +255,12 @@ def test_efficiency(self): ) # Real energy should be less than discharge energy, # and both should be greater than 0 - self.assertLess(discharge_energy, theoretical_energy) - self.assertLess(0, discharge_energy) - self.assertLess(0, theoretical_energy) + assert discharge_energy < theoretical_energy + assert 0 < discharge_energy + assert 0 < theoretical_energy -class TestGetInitialSOC(unittest.TestCase): +class TestGetInitialSOC: def test_initial_soc(self): param = pybamm.LithiumIonParameters() parameter_values = pybamm.ParameterValues("Mohtat2020") @@ -276,26 +270,26 @@ def test_initial_soc(self): 1, parameter_values, param ) V = parameter_values.evaluate(param.p.prim.U(y100, T) - param.n.prim.U(x100, T)) - self.assertAlmostEqual(V, 4.2) + assert V == pytest.approx(4.2) x0, y0 = pybamm.lithium_ion.get_initial_stoichiometries( 0, parameter_values, param ) V = parameter_values.evaluate(param.p.prim.U(y0, T) - param.n.prim.U(x0, T)) - self.assertAlmostEqual(V, 2.8) + assert V == pytest.approx(2.8) x, y = pybamm.lithium_ion.get_initial_stoichiometries( 0.4, parameter_values, param ) - self.assertEqual(x, x0 + 0.4 * (x100 - x0)) - self.assertEqual(y, y0 - 0.4 * (y0 - y100)) + assert x == x0 + 0.4 * (x100 - x0) + assert y == y0 - 0.4 * (y0 - y100) x, y = pybamm.lithium_ion.get_initial_stoichiometries( "4 V", parameter_values, param ) T = parameter_values.evaluate(param.T_ref) V = parameter_values.evaluate(param.p.prim.U(y, T) - param.n.prim.U(x, T)) - self.assertAlmostEqual(V, 4) + assert V == pytest.approx(4) def test_min_max_stoich(self): param = pybamm.LithiumIonParameters() @@ -306,9 +300,9 @@ def test_min_max_stoich(self): parameter_values, param ) V = parameter_values.evaluate(param.p.prim.U(y100, T) - param.n.prim.U(x100, T)) - self.assertAlmostEqual(V, 4.2) + assert V == pytest.approx(4.2) V = parameter_values.evaluate(param.p.prim.U(y0, T) - param.n.prim.U(x0, T)) - self.assertAlmostEqual(V, 2.8) + assert V == pytest.approx(2.8) x0, x100, y100, y0 = pybamm.lithium_ion.get_min_max_stoichiometries( parameter_values, @@ -316,9 +310,9 @@ def test_min_max_stoich(self): known_value="cell capacity", ) V = parameter_values.evaluate(param.p.prim.U(y100, T) - param.n.prim.U(x100, T)) - self.assertAlmostEqual(V, 4.2) + assert V == pytest.approx(4.2) V = parameter_values.evaluate(param.p.prim.U(y0, T) - param.n.prim.U(x0, T)) - self.assertAlmostEqual(V, 2.8) + assert V == pytest.approx(2.8) def test_initial_soc_cell_capacity(self): param = pybamm.LithiumIonParameters() @@ -329,7 +323,7 @@ def test_initial_soc_cell_capacity(self): 1, parameter_values, param, known_value="cell capacity" ) V = parameter_values.evaluate(param.p.prim.U(y100, T) - param.n.prim.U(x100, T)) - self.assertAlmostEqual(V, 4.2) + assert V == pytest.approx(4.2) def test_error(self): parameter_values = pybamm.ParameterValues("Chen2020") @@ -337,43 +331,41 @@ def test_error(self): {"working electrode": "positive"} ).default_parameter_values - with self.assertRaisesRegex( - ValueError, "Initial SOC should be between 0 and 1" - ): + with pytest.raises(ValueError, match="Initial SOC should be between 0 and 1"): pybamm.lithium_ion.get_initial_stoichiometries(2, parameter_values) - with self.assertRaisesRegex(ValueError, "outside the voltage limits"): + with pytest.raises(ValueError, match="outside the voltage limits"): pybamm.lithium_ion.get_initial_stoichiometries("1 V", parameter_values) - with self.assertRaisesRegex(ValueError, "must be a float"): + with pytest.raises(ValueError, match="must be a float"): pybamm.lithium_ion.get_initial_stoichiometries("5 A", parameter_values) - with self.assertRaisesRegex(ValueError, "outside the voltage limits"): + with pytest.raises(ValueError, match="outside the voltage limits"): pybamm.lithium_ion.get_initial_stoichiometry_half_cell( "1 V", parameter_values_half_cell ) - with self.assertRaisesRegex(ValueError, "must be a float"): + with pytest.raises(ValueError, match="must be a float"): pybamm.lithium_ion.get_initial_stoichiometry_half_cell( "5 A", parameter_values_half_cell ) - with self.assertRaisesRegex( - ValueError, "Initial SOC should be between 0 and 1" - ): + with pytest.raises(ValueError, match="Initial SOC should be between 0 and 1"): pybamm.lithium_ion.get_initial_stoichiometry_half_cell( 2, parameter_values_half_cell ) - with self.assertRaisesRegex( - ValueError, "Known value must be cell capacity or cyclable lithium capacity" + with pytest.raises( + ValueError, + match="Known value must be cell capacity or cyclable lithium capacity", ): pybamm.lithium_ion.ElectrodeSOHSolver( parameter_values, known_value="something else" ) - with self.assertRaisesRegex( - ValueError, "Known value must be cell capacity or cyclable lithium capacity" + with pytest.raises( + ValueError, + match="Known value must be cell capacity or cyclable lithium capacity", ): param_MSMR = pybamm.lithium_ion.MSMR( {"number of MSMR reactions": "3"} @@ -382,24 +374,25 @@ def test_error(self): param=param_MSMR, known_value="something else" ) - with self.assertRaisesRegex( - ValueError, "Known value must be cell capacity or cyclable lithium capacity" + with pytest.raises( + ValueError, + match="Known value must be cell capacity or cyclable lithium capacity", ): pybamm.models.full_battery_models.lithium_ion.electrode_soh._ElectrodeSOH( known_value="something else" ) -class TestGetInitialOCP(unittest.TestCase): +class TestGetInitialOCP: def test_get_initial_ocp(self): param = pybamm.LithiumIonParameters() parameter_values = pybamm.ParameterValues("Mohtat2020") Un, Up = pybamm.lithium_ion.get_initial_ocps(1, parameter_values, param) - self.assertAlmostEqual(Up - Un, 4.2) + assert Up - Un == pytest.approx(4.2) Un, Up = pybamm.lithium_ion.get_initial_ocps(0, parameter_values, param) - self.assertAlmostEqual(Up - Un, 2.8) + assert Up - Un == pytest.approx(2.8) Un, Up = pybamm.lithium_ion.get_initial_ocps("4 V", parameter_values, param) - self.assertAlmostEqual(Up - Un, 4) + assert Up - Un == pytest.approx(4) def test_min_max_ocp(self): param = pybamm.LithiumIonParameters() @@ -408,61 +401,39 @@ def test_min_max_ocp(self): Un_0, Un_100, Up_100, Up_0 = pybamm.lithium_ion.get_min_max_ocps( parameter_values, param ) - self.assertAlmostEqual(Up_100 - Un_100, 4.2) - self.assertAlmostEqual(Up_0 - Un_0, 2.8) + assert Up_100 - Un_100 == pytest.approx(4.2) + assert Up_0 - Un_0 == pytest.approx(2.8) -class TestGetInitialOCPMSMR(unittest.TestCase): - def test_get_initial_ocp(self): - options = { - "open-circuit potential": "MSMR", - "particle": "MSMR", - "number of MSMR reactions": ("6", "4"), - "intercalation kinetics": "MSMR", - } +class TestGetInitialOCPMSMR: + def test_get_initial_ocp(self, options): param = pybamm.LithiumIonParameters(options) parameter_values = pybamm.ParameterValues("MSMR_Example") Un, Up = pybamm.lithium_ion.get_initial_ocps( 1, parameter_values, param, options=options ) - self.assertAlmostEqual(Up - Un, 4.2, places=5) + assert Up - Un == pytest.approx(4.2, abs=1e-05) Un, Up = pybamm.lithium_ion.get_initial_ocps( 0, parameter_values, param, options=options ) - self.assertAlmostEqual(Up - Un, 2.8, places=5) + assert Up - Un == pytest.approx(2.8, abs=1e-05) Un, Up = pybamm.lithium_ion.get_initial_ocps( "4 V", parameter_values, param, options=options ) - self.assertAlmostEqual(Up - Un, 4) + assert Up - Un == pytest.approx(4) - def test_min_max_ocp(self): - options = { - "open-circuit potential": "MSMR", - "particle": "MSMR", - "number of MSMR reactions": ("6", "4"), - "intercalation kinetics": "MSMR", - } + def test_min_max_ocp(self, options): param = pybamm.LithiumIonParameters(options) parameter_values = pybamm.ParameterValues("MSMR_Example") Un_0, Un_100, Up_100, Up_0 = pybamm.lithium_ion.get_min_max_ocps( parameter_values, param, options=options ) - self.assertAlmostEqual(Up_100 - Un_100, 4.2) - self.assertAlmostEqual(Up_0 - Un_0, 2.8) + assert Up_100 - Un_100 == pytest.approx(4.2) + assert Up_0 - Un_0 == pytest.approx(2.8) Un_0, Un_100, Up_100, Up_0 = pybamm.lithium_ion.get_min_max_ocps( parameter_values, param, known_value="cell capacity", options=options ) - self.assertAlmostEqual(Up_100 - Un_100, 4.2) - self.assertAlmostEqual(Up_0 - Un_0, 2.8) - - -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() + assert Up_100 - Un_100 == pytest.approx(4.2) + assert Up_0 - Un_0 == pytest.approx(2.8) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index e5147f01e2..ad02212840 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -2,11 +2,11 @@ # Tests for the lithium-ion MPM model # +import pytest import pybamm -import unittest -class TestMPM(unittest.TestCase): +class TestMPM: def test_well_posed(self): options = {"thermal": "isothermal"} model = pybamm.lithium_ion.MPM(options) @@ -20,9 +20,9 @@ def test_well_posed(self): def test_default_parameter_values(self): # check default parameters are added correctly model = pybamm.lithium_ion.MPM() - self.assertEqual( - model.default_parameter_values["Negative minimum particle radius [m]"], - 0.0, + assert ( + model.default_parameter_values["Negative minimum particle radius [m]"] + == 0.0 ) def test_lumped_thermal_model_1D(self): @@ -32,7 +32,7 @@ def test_lumped_thermal_model_1D(self): def test_x_full_thermal_not_implemented(self): options = {"thermal": "x-full"} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) def test_thermal_1plus1D(self): @@ -51,7 +51,7 @@ def test_particle_uniform(self): def test_particle_quadratic(self): options = {"particle": "quadratic profile"} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) def test_differential_surface_form(self): @@ -66,31 +66,31 @@ def test_current_sigmoid(self): def test_necessary_options(self): options = {"particle size": "single"} - with self.assertRaises(pybamm.OptionError): + with pytest.raises(pybamm.OptionError): pybamm.lithium_ion.MPM(options) options = {"surface form": "false"} - with self.assertRaises(pybamm.OptionError): + with pytest.raises(pybamm.OptionError): pybamm.lithium_ion.MPM(options) def test_nonspherical_particle_not_implemented(self): options = {"particle shape": "user"} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) def test_loss_active_material_stress_negative_not_implemented(self): options = {"loss of active material": ("stress-driven", "none")} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) def test_loss_active_material_stress_positive_not_implemented(self): options = {"loss of active material": ("none", "stress-driven")} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) def test_loss_active_material_stress_both_not_implemented(self): options = {"loss of active material": "stress-driven"} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) def test_reversible_plating_with_porosity_not_implemented(self): @@ -98,12 +98,12 @@ def test_reversible_plating_with_porosity_not_implemented(self): "lithium plating": "reversible", "lithium plating porosity change": "true", } - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) def test_stress_induced_diffusion_not_implemented(self): options = {"stress-induced diffusion": "true"} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) def test_msmr(self): @@ -124,7 +124,7 @@ def test_wycisk_ocp(self): model.check_well_posedness() -class TestMPMExternalCircuits(unittest.TestCase): +class TestMPMExternalCircuits: def test_well_posed_voltage(self): options = {"operating mode": "voltage"} model = pybamm.lithium_ion.MPM(options) @@ -146,25 +146,25 @@ def external_circuit_function(variables): model.check_well_posedness() -class TestMPMWithSEI(unittest.TestCase): +class TestMPMWithSEI: def test_reaction_limited_not_implemented(self): options = {"SEI": "reaction limited"} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) def test_solvent_diffusion_limited_not_implemented(self): options = {"SEI": "solvent-diffusion limited"} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) def test_electron_migration_limited_not_implemented(self): options = {"SEI": "electron-migration limited"} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) def test_interstitial_diffusion_limited_not_implemented(self): options = {"SEI": "interstitial-diffusion limited"} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) def test_ec_reaction_limited_not_implemented(self): @@ -172,49 +172,39 @@ def test_ec_reaction_limited_not_implemented(self): "SEI": "ec reaction limited", "SEI porosity change": "true", } - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) -class TestMPMWithMechanics(unittest.TestCase): +class TestMPMWithMechanics: def test_well_posed_negative_cracking_not_implemented(self): options = {"particle mechanics": ("swelling and cracking", "none")} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) def test_well_posed_positive_cracking_not_implemented(self): options = {"particle mechanics": ("none", "swelling and cracking")} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) def test_well_posed_both_cracking_not_implemented(self): options = {"particle mechanics": "swelling and cracking"} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) def test_well_posed_both_swelling_only_not_implemented(self): options = {"particle mechanics": "swelling only"} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) -class TestMPMWithPlating(unittest.TestCase): +class TestMPMWithPlating: def test_well_posed_reversible_plating_not_implemented(self): options = {"lithium plating": "reversible"} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) def test_well_posed_irreversible_plating_not_implemented(self): options = {"lithium plating": "irreversible"} - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.lithium_ion.MPM(options) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py index c979474e13..ea641ee7cc 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py @@ -39,3 +39,7 @@ def test_well_posed_composite_diffusion_hysteresis(self): @pytest.mark.skip(reason="Test currently not implemented") def test_well_posed_composite_different_degradation(self): pass # skip this test + + @pytest.mark.skip(reason="Test currently not implemented") + def test_well_posed_composite_LAM(self): + pass # skip this test diff --git a/tests/unit/test_parameters/test_bpx.py b/tests/unit/test_parameters/test_bpx.py index ab4c25f97a..aeb249cbc0 100644 --- a/tests/unit/test_parameters/test_bpx.py +++ b/tests/unit/test_parameters/test_bpx.py @@ -1,5 +1,4 @@ import tempfile -import unittest import json import pybamm import copy @@ -7,7 +6,8 @@ import pytest -class TestBPX(unittest.TestCase): +class TestBPX: + @pytest.fixture(autouse=True) def setUp(self): self.base = { "Header": { @@ -197,13 +197,13 @@ def check_constant_output(func): stos = [0, 1] T = 298.15 p_vals = [func(sto, T) for sto in stos] - self.assertEqual(p_vals[0], p_vals[1]) + assert p_vals[0] == p_vals[1] for electrode in ["Negative", "Positive"]: D = param[f"{electrode} particle diffusivity [m2.s-1]"] dUdT = param[f"{electrode} electrode OCP entropic change [V.K-1]"] check_constant_output(D) - check_constant_output(dUdT) + assert dUdT == 1 kappa = param["Electrolyte conductivity [S.m-1]"] De = param["Electrolyte diffusivity [m2.s-1]"] @@ -250,23 +250,21 @@ def test_table_data(self): # correct child c = pybamm.Variable("c") kappa = param["Electrolyte conductivity [S.m-1]"](c, 298.15) - self.assertIsInstance(kappa, pybamm.Interpolant) - self.assertEqual(kappa.children[0], c) + assert isinstance(kappa, pybamm.Interpolant) + assert kappa.children[0] == c # Check other parameters give interpolants D = param["Electrolyte diffusivity [m2.s-1]"](c, 298.15) - self.assertIsInstance(D, pybamm.Interpolant) + assert isinstance(D, pybamm.Interpolant) for electrode in ["Negative", "Positive"]: D = param[f"{electrode} particle diffusivity [m2.s-1]"](c, 298.15) - self.assertIsInstance(D, pybamm.Interpolant) + assert isinstance(D, pybamm.Interpolant) OCP = param[f"{electrode} electrode OCP [V]"](c) - self.assertIsInstance(OCP, pybamm.Interpolant) - dUdT = param[f"{electrode} electrode OCP entropic change [V.K-1]"]( - c, 10000 - ) - self.assertIsInstance(dUdT, pybamm.Interpolant) + assert isinstance(OCP, pybamm.Interpolant) + dUdT = param[f"{electrode} electrode OCP entropic change [V.K-1]"](c) + assert isinstance(dUdT, pybamm.Interpolant) def test_bpx_soc_error(self): - with self.assertRaisesRegex(ValueError, "Target SOC"): + with pytest.raises(ValueError, match="Target SOC"): pybamm.ParameterValues.create_from_bpx("blah.json", target_soc=10) def test_bpx_arrhenius(self): @@ -305,7 +303,7 @@ def arrhenius_assertion(pv, param_key, Ea_key): calc_ratio = pybamm.exp(Ea / pybamm.constants.R * (1 / T_ref - 1 / T)).value - self.assertAlmostEqual(eval_ratio, calc_ratio) + assert eval_ratio == pytest.approx(calc_ratio) param_keys = [ "Electrolyte conductivity [S.m-1]", @@ -454,7 +452,7 @@ def test_bpx_blended_error(self): json.dump(bpx_obj, tmp) tmp.flush() - with self.assertRaisesRegex(NotImplementedError, "PyBaMM does not support"): + with pytest.raises(NotImplementedError, match="PyBaMM does not support"): pybamm.ParameterValues.create_from_bpx(tmp.name) def test_bpx_user_defined(self): @@ -478,21 +476,11 @@ def test_bpx_user_defined(self): param = pybamm.ParameterValues.create_from_bpx(tmp.name) - self.assertEqual(param["User-defined scalar parameter"], 1.0) + assert param["User-defined scalar parameter"] == 1.0 var = pybamm.Variable("var") - self.assertIsInstance( + assert isinstance( param["User-defined parameter data"](var), pybamm.Interpolant ) - self.assertIsInstance( + assert isinstance( param["User-defined parameter data function"](var), pybamm.Power ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_parameters/test_current_functions.py b/tests/unit/test_parameters/test_current_functions.py index b00cba0b89..6e4914092b 100644 --- a/tests/unit/test_parameters/test_current_functions.py +++ b/tests/unit/test_parameters/test_current_functions.py @@ -4,21 +4,20 @@ import pybamm import numbers -import unittest import numpy as np import pandas as pd import pytest from tests import no_internet_connection -class TestCurrentFunctions(unittest.TestCase): +class TestCurrentFunctions: def test_constant_current(self): # test simplify param = pybamm.electrical_parameters current = param.current_with_time parameter_values = pybamm.ParameterValues({"Current function [A]": 2}) processed_current = parameter_values.process_symbol(current) - self.assertIsInstance(processed_current, pybamm.Scalar) + assert isinstance(processed_current, pybamm.Scalar) @pytest.mark.skipif( no_internet_connection(), @@ -99,13 +98,3 @@ def test_output_type(self): def test_all(self): self.test_output_type() - - -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_ecm_parameters.py b/tests/unit/test_parameters/test_ecm_parameters.py index 543b4f4e5b..39ee80afee 100644 --- a/tests/unit/test_parameters/test_ecm_parameters.py +++ b/tests/unit/test_parameters/test_ecm_parameters.py @@ -3,7 +3,6 @@ # import pybamm -import unittest values = { @@ -33,7 +32,7 @@ parameter_values = pybamm.ParameterValues(values) -class TestEcmParameters(unittest.TestCase): +class TestEcmParameters: def test_init_parameters(self): param = pybamm.EcmParameters() @@ -54,13 +53,13 @@ def test_init_parameters(self): for symbol, key in simpled_mapped_parameters: value = parameter_values.evaluate(symbol) expected_value = values[key] - self.assertEqual(value, expected_value) + assert value == expected_value value = parameter_values.evaluate(param.initial_T_cell) - self.assertEqual(value, values["Initial temperature [K]"] - 273.15) + assert value == values["Initial temperature [K]"] - 273.15 value = parameter_values.evaluate(param.initial_T_jig) - self.assertEqual(value, values["Initial temperature [K]"] - 273.15) + assert value == values["Initial temperature [K]"] - 273.15 compatibility_parameters = [ (param.n_electrodes_parallel, 1), @@ -70,7 +69,7 @@ def test_init_parameters(self): for symbol, expected_value in compatibility_parameters: value = parameter_values.evaluate(symbol) - self.assertEqual(value, expected_value) + assert value == expected_value def test_function_parameters(self): param = pybamm.EcmParameters() @@ -89,17 +88,7 @@ def test_function_parameters(self): for symbol, key in mapped_functions: value = parameter_values.evaluate(symbol) expected_value = values[key] - self.assertEqual(value, expected_value) + assert value == expected_value value = parameter_values.evaluate(param.T_amb(sym)) - self.assertEqual(value, values["Ambient temperature [K]"] - 273.15) - - -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() + assert value == values["Ambient temperature [K]"] - 273.15 diff --git a/tests/unit/test_parameters/test_lead_acid_parameters.py b/tests/unit/test_parameters/test_lead_acid_parameters.py index 3fc62fde93..1d02d91ff0 100644 --- a/tests/unit/test_parameters/test_lead_acid_parameters.py +++ b/tests/unit/test_parameters/test_lead_acid_parameters.py @@ -1,19 +1,19 @@ # # Test for the standard lead acid parameters # +import pytest import os import pybamm from tests import get_discretisation_for_testing from tempfile import TemporaryDirectory -import unittest -class TestStandardParametersLeadAcid(unittest.TestCase): +class TestStandardParametersLeadAcid: def test_scipy_constants(self): constants = pybamm.constants - self.assertAlmostEqual(constants.R.evaluate(), 8.314, places=3) - self.assertAlmostEqual(constants.F.evaluate(), 96485, places=0) + assert constants.R.evaluate() == pytest.approx(8.314, abs=0.001) + assert constants.F.evaluate() == pytest.approx(96485, abs=1) def test_print_parameters(self): with TemporaryDirectory() as dir_name: @@ -30,17 +30,19 @@ def test_parameters_defaults_lead_acid(self): # Volume change positive in negative electrode and negative in positive # electrode - self.assertLess(param_eval["n.DeltaVsurf"], 0) - self.assertGreater(param_eval["p.DeltaVsurf"], 0) + assert param_eval["n.DeltaVsurf"] < 0 + assert param_eval["p.DeltaVsurf"] > 0 def test_concatenated_parameters(self): # create param = pybamm.LeadAcidParameters() eps_param = param.epsilon_init - self.assertIsInstance(eps_param, pybamm.Concatenation) - self.assertEqual( - eps_param.domain, ["negative electrode", "separator", "positive electrode"] - ) + assert isinstance(eps_param, pybamm.Concatenation) + assert eps_param.domain == [ + "negative electrode", + "separator", + "positive electrode", + ] # process parameters and discretise parameter_values = pybamm.ParameterValues("Sulzer2019") @@ -49,7 +51,7 @@ def test_concatenated_parameters(self): # test output submeshes = disc.mesh[("negative electrode", "separator", "positive electrode")] - self.assertEqual(processed_eps.shape, (submeshes.npts, 1)) + assert processed_eps.shape == (submeshes.npts, 1) def test_current_functions(self): # create current functions @@ -70,7 +72,7 @@ def test_current_functions(self): } ) current_density_eval = parameter_values.process_symbol(current_density) - self.assertAlmostEqual(current_density_eval.evaluate(t=3), 2 / (8 * 0.1 * 0.1)) + assert current_density_eval.evaluate(t=3) == pytest.approx(2 / (8 * 0.1 * 0.1)) def test_thermal_parameters(self): values = pybamm.lead_acid.BaseModel().default_parameter_values @@ -78,18 +80,18 @@ def test_thermal_parameters(self): T = 300 # dummy temperature as the values are constant # Density - self.assertEqual(values.evaluate(param.n.rho_c_p_cc(T)), 11300 * 130) - self.assertEqual(values.evaluate(param.n.rho_c_p(T)), 11300 * 130) - self.assertEqual(values.evaluate(param.s.rho_c_p(T)), 1680 * 700) - self.assertEqual(values.evaluate(param.p.rho_c_p(T)), 9375 * 256) - self.assertEqual(values.evaluate(param.p.rho_c_p_cc(T)), 9375 * 256) + assert values.evaluate(param.n.rho_c_p_cc(T)) == 11300 * 130 + assert values.evaluate(param.n.rho_c_p(T)) == 11300 * 130 + assert values.evaluate(param.s.rho_c_p(T)) == 1680 * 700 + assert values.evaluate(param.p.rho_c_p(T)) == 9375 * 256 + assert values.evaluate(param.p.rho_c_p_cc(T)) == 9375 * 256 # Thermal conductivity - self.assertEqual(values.evaluate(param.n.lambda_cc(T)), 35) - self.assertEqual(values.evaluate(param.n.lambda_(T)), 35) - self.assertEqual(values.evaluate(param.s.lambda_(T)), 0.04) - self.assertEqual(values.evaluate(param.p.lambda_(T)), 35) - self.assertEqual(values.evaluate(param.p.lambda_cc(T)), 35) + assert values.evaluate(param.n.lambda_cc(T)) == 35 + assert values.evaluate(param.n.lambda_(T)) == 35 + assert values.evaluate(param.s.lambda_(T)) == 0.04 + assert values.evaluate(param.p.lambda_(T)) == 35 + assert values.evaluate(param.p.lambda_cc(T)) == 35 def test_functions_lead_acid(self): # Load parameters to be tested @@ -112,17 +114,7 @@ def test_functions_lead_acid(self): param_eval = parameter_values.print_parameters(parameters) # Known monotonicity for functions - self.assertGreater(param_eval["chi_1"], param_eval["chi_0.5"]) - self.assertLess(param_eval["U_n_1"], param_eval["U_n_0.5"]) - self.assertGreater(param_eval["U_p_1"], param_eval["U_p_0.5"]) - self.assertGreater(param_eval["j0_Ox_1"], param_eval["j0_Ox_0.5"]) - - -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() + assert param_eval["chi_1"] > param_eval["chi_0.5"] + assert param_eval["U_n_1"] < param_eval["U_n_0.5"] + assert param_eval["U_p_1"] > param_eval["U_p_0.5"] + assert param_eval["j0_Ox_1"] > param_eval["j0_Ox_0.5"] diff --git a/tests/unit/test_parameters/test_lithium_ion_parameters.py b/tests/unit/test_parameters/test_lithium_ion_parameters.py index 66c4ea398e..7ac573d785 100644 --- a/tests/unit/test_parameters/test_lithium_ion_parameters.py +++ b/tests/unit/test_parameters/test_lithium_ion_parameters.py @@ -5,11 +5,10 @@ import pybamm from tempfile import TemporaryDirectory -import unittest import numpy as np -class TestLithiumIonParameterValues(unittest.TestCase): +class TestLithiumIonParameterValues: def test_print_parameters(self): with TemporaryDirectory() as dir_name: parameters = pybamm.LithiumIonParameters() @@ -138,13 +137,3 @@ def test_parameter_functions(self): c_e_test = 1000 values.evaluate(param.D_e(c_e_test, T_test)) values.evaluate(param.kappa_e(c_e_test, T_test)) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_parameters/test_parameter_sets/test_Ai2020.py b/tests/unit/test_parameters/test_parameter_sets/test_Ai2020.py index f7302330bf..05fe8f68fa 100644 --- a/tests/unit/test_parameters/test_parameter_sets/test_Ai2020.py +++ b/tests/unit/test_parameters/test_parameter_sets/test_Ai2020.py @@ -22,10 +22,10 @@ def test_functions(self): 0.6098, ), "Positive electrode OCP entropic change [V.K-1]": ( - [sto, c_p_max], + [sto], -2.1373e-4, ), - "Positive electrode volume change": ([sto, c_p_max], -1.8179e-2), + "Positive electrode volume change": ([sto], -1.8179e-2), # Negative electrode "Negative electrode cracking rate": ([T], 3.9e-20), "Negative particle diffusivity [m2.s-1]": ([sto, T], 3.9e-14), @@ -34,10 +34,10 @@ def test_functions(self): 0.4172, ), "Negative electrode OCP entropic change [V.K-1]": ( - [sto, c_n_max], + [sto], -1.1033e-4, ), - "Negative electrode volume change": ([sto, c_n_max], 5.1921e-2), + "Negative electrode volume change": ([sto], 5.1921e-2), } for name, value in fun_test.items(): diff --git a/tests/unit/test_parameters/test_parameter_sets/test_LCO_Ramadass2004.py b/tests/unit/test_parameters/test_parameter_sets/test_LCO_Ramadass2004.py index e6c4b04fdf..287c4e97d8 100644 --- a/tests/unit/test_parameters/test_parameter_sets/test_LCO_Ramadass2004.py +++ b/tests/unit/test_parameters/test_parameter_sets/test_LCO_Ramadass2004.py @@ -21,7 +21,7 @@ def test_functions(self): 1.4517, ), "Positive electrode OCP entropic change [V.K-1]": ( - [sto, c_p_max], + [sto], -3.4664e-5, ), "Positive electrode OCP [V]": ([sto], 4.1249), @@ -32,7 +32,7 @@ def test_functions(self): 2.2007, ), "Negative electrode OCP entropic change [V.K-1]": ( - [sto, c_n_max], + [sto], -1.5079e-5, ), "Negative electrode OCP [V]": ([sto], 0.1215), diff --git a/tests/unit/test_parameters/test_parameter_sets/test_LGM50_ORegan2022.py b/tests/unit/test_parameters/test_parameter_sets/test_LGM50_ORegan2022.py index 05a38b6245..8a7e401d66 100644 --- a/tests/unit/test_parameters/test_parameter_sets/test_LGM50_ORegan2022.py +++ b/tests/unit/test_parameters/test_parameter_sets/test_LGM50_ORegan2022.py @@ -15,7 +15,7 @@ def test_functions(self): fun_test = { # Positive electrode "Positive electrode OCP entropic change [V.K-1]": ( - [0.5, c_p_max], + [0.5], -9.7940e-07, ), "Positive electrode specific heat capacity [J.kg-1.K-1]": ( @@ -32,7 +32,7 @@ def test_functions(self): "Positive electrode thermal conductivity [W.m-1.K-1]": ([T], 0.8047), # Negative electrode "Negative electrode OCP entropic change [V.K-1]": ( - [0.5, c_n_max], + [0.5], -2.6460e-07, ), "Negative electrode specific heat capacity [J.kg-1.K-1]": ( diff --git a/tests/unit/test_parameters/test_parameter_sets/test_OKane2022.py b/tests/unit/test_parameters/test_parameter_sets/test_OKane2022.py index e34f837b38..014b467715 100644 --- a/tests/unit/test_parameters/test_parameter_sets/test_OKane2022.py +++ b/tests/unit/test_parameters/test_parameter_sets/test_OKane2022.py @@ -27,7 +27,7 @@ def test_functions(self): 0.33947, ), "Negative electrode cracking rate": ([T], 3.9e-20), - "Negative electrode volume change": ([sto, 33133], 0.0897), + "Negative electrode volume change": ([sto], 0.0897), # Positive electrode "Positive particle diffusivity [m2.s-1]": ([sto, T], 4e-15), "Positive electrode exchange-current density [A.m-2]": ( @@ -36,7 +36,7 @@ def test_functions(self): ), "Positive electrode OCP [V]": ([sto], 3.5682), "Positive electrode cracking rate": ([T], 3.9e-20), - "Positive electrode volume change": ([sto, 63104], 0.70992), + "Positive electrode volume change": ([sto], 0.70992), } for name, value in fun_test.items(): diff --git a/tests/unit/test_parameters/test_parameter_sets/test_OKane2022_negative_halfcell.py b/tests/unit/test_parameters/test_parameter_sets/test_OKane2022_negative_halfcell.py index bf39457dc4..beebeb35e3 100644 --- a/tests/unit/test_parameters/test_parameter_sets/test_OKane2022_negative_halfcell.py +++ b/tests/unit/test_parameters/test_parameter_sets/test_OKane2022_negative_halfcell.py @@ -26,7 +26,7 @@ def test_functions(self): 0.33947, ), "Positive electrode cracking rate": ([T], 3.9e-20), - "Positive electrode volume change": ([sto, 33133], 0.0897), + "Positive electrode volume change": ([sto], 0.0897), } for name, value in fun_test.items(): diff --git a/tests/unit/test_parameters/test_parameter_values.py b/tests/unit/test_parameters/test_parameter_values.py index eaeb4a5a42..4086abea6d 100644 --- a/tests/unit/test_parameters/test_parameter_values.py +++ b/tests/unit/test_parameters/test_parameter_values.py @@ -3,8 +3,8 @@ # +import pytest import os -import unittest import numpy as np import pandas as pd @@ -19,88 +19,82 @@ import casadi -class TestParameterValues(unittest.TestCase): +class TestParameterValues: def test_init(self): # from dict param = pybamm.ParameterValues({"a": 1}) - self.assertEqual(param["a"], 1) - self.assertIn("a", param.keys()) - self.assertIn(1, param.values()) - self.assertIn(("a", 1), param.items()) + assert param["a"] == 1 + assert "a" in param.keys() + assert 1 in param.values() + assert ("a", 1) in param.items() # from dict with strings param = pybamm.ParameterValues({"a": "1"}) - self.assertEqual(param["a"], 1) + assert param["a"] == 1 # from dict "chemistry" key gets removed param = pybamm.ParameterValues({"a": 1, "chemistry": "lithium-ion"}) - self.assertNotIn("chemistry", param.keys()) + assert "chemistry" not in param.keys() # chemistry kwarg removed - with self.assertRaisesRegex( - ValueError, "'chemistry' keyword argument has been deprecated" + with pytest.raises( + ValueError, match="'chemistry' keyword argument has been deprecated" ): pybamm.ParameterValues(None, chemistry="lithium-ion") # junk param values rejected - with self.assertRaisesRegex(ValueError, "'Junk' is not a valid parameter set."): + with pytest.raises(ValueError, match="'Junk' is not a valid parameter set."): pybamm.ParameterValues("Junk") def test_repr(self): param = pybamm.ParameterValues({"a": 1}) - self.assertEqual( - repr(param), - "{'Boltzmann constant [J.K-1]': 1.380649e-23,\n" + assert ( + repr(param) == "{'Boltzmann constant [J.K-1]': 1.380649e-23,\n" " 'Electron charge [C]': 1.602176634e-19,\n" " 'Faraday constant [C.mol-1]': 96485.33212,\n" " 'Ideal gas constant [J.K-1.mol-1]': 8.314462618,\n" - " 'a': 1}", - ) - self.assertEqual( - param._ipython_key_completions_(), - [ - "Ideal gas constant [J.K-1.mol-1]", - "Faraday constant [C.mol-1]", - "Boltzmann constant [J.K-1]", - "Electron charge [C]", - "a", - ], + " 'a': 1}" ) + assert param._ipython_key_completions_() == [ + "Ideal gas constant [J.K-1.mol-1]", + "Faraday constant [C.mol-1]", + "Boltzmann constant [J.K-1]", + "Electron charge [C]", + "a", + ] def test_eq(self): - self.assertEqual( - pybamm.ParameterValues({"a": 1}), pybamm.ParameterValues({"a": 1}) - ) + assert pybamm.ParameterValues({"a": 1}) == pybamm.ParameterValues({"a": 1}) def test_update(self): # equate values param = pybamm.ParameterValues({"a": 1}) - self.assertEqual(param["a"], 1) + assert param["a"] == 1 # no conflict param.update({"a": 2}) - self.assertEqual(param["a"], 2) + assert param["a"] == 2 param.update({"a": 2}, check_conflict=True) - self.assertEqual(param["a"], 2) + assert param["a"] == 2 # with conflict param.update({"a": 3}) # via __setitem__ param["a"] = 2 - self.assertEqual(param["a"], 2) - with self.assertRaisesRegex( - ValueError, "parameter 'a' already defined with value '2'" + assert param["a"] == 2 + with pytest.raises( + ValueError, match="parameter 'a' already defined with value '2'" ): param.update({"a": 4}, check_conflict=True) # with parameter not existing yet - with self.assertRaisesRegex(KeyError, "Cannot update parameter"): + with pytest.raises(KeyError, match="Cannot update parameter"): param.update({"b": 1}) # update with a ParameterValues object new_param = pybamm.ParameterValues(param) - self.assertEqual(new_param["a"], 2) + assert new_param["a"] == 2 # test deleting a parameter del param["a"] - self.assertNotIn("a", param.keys()) + assert "a" not in param.keys() def test_set_initial_stoichiometries(self): param = pybamm.ParameterValues("Chen2020") @@ -113,12 +107,12 @@ def test_set_initial_stoichiometries(self): x = param["Initial concentration in negative electrode [mol.m-3]"] x_0 = param_0["Initial concentration in negative electrode [mol.m-3]"] x_100 = param_100["Initial concentration in negative electrode [mol.m-3]"] - self.assertAlmostEqual(x, x_0 + 0.4 * (x_100 - x_0)) + assert x == pytest.approx(x_0 + 0.4 * (x_100 - x_0)) y = param["Initial concentration in positive electrode [mol.m-3]"] y_0 = param_0["Initial concentration in positive electrode [mol.m-3]"] y_100 = param_100["Initial concentration in positive electrode [mol.m-3]"] - self.assertAlmostEqual(y, y_0 - 0.4 * (y_0 - y_100)) + assert y == pytest.approx(y_0 - 0.4 * (y_0 - y_100)) def test_set_initial_stoichiometry_half_cell(self): param = pybamm.lithium_ion.DFN( @@ -137,7 +131,7 @@ def test_set_initial_stoichiometry_half_cell(self): y = param["Initial concentration in positive electrode [mol.m-3]"] y_0 = param_0["Initial concentration in positive electrode [mol.m-3]"] y_100 = param_100["Initial concentration in positive electrode [mol.m-3]"] - self.assertAlmostEqual(y, y_0 - 0.4 * (y_0 - y_100)) + assert y == pytest.approx(y_0 - 0.4 * (y_0 - y_100)) # inplace for 100% coverage param_t = pybamm.lithium_ion.DFN( @@ -161,11 +155,11 @@ def test_set_initial_stoichiometry_half_cell(self): 1, inplace=True, options={"working electrode": "positive"} ) y_100 = param_100["Initial concentration in positive electrode [mol.m-3]"] - self.assertAlmostEqual(y, y_0 - 0.4 * (y_0 - y_100)) + assert y == pytest.approx(y_0 - 0.4 * (y_0 - y_100)) # test error param = pybamm.ParameterValues("Chen2020") - with self.assertRaisesRegex(OptionError, "working electrode"): + with pytest.raises(OptionError, match="working electrode"): param.set_initial_stoichiometry_half_cell( 0.1, options={"working electrode": "negative"} ) @@ -183,20 +177,20 @@ def test_set_initial_ocps(self): Un_0 = param_0["Initial voltage in negative electrode [V]"] Up_0 = param_0["Initial voltage in positive electrode [V]"] - self.assertAlmostEqual(Up_0 - Un_0, 2.8) + assert Up_0 - Un_0 == pytest.approx(2.8) Un_100 = param_100["Initial voltage in negative electrode [V]"] Up_100 = param_100["Initial voltage in positive electrode [V]"] - self.assertAlmostEqual(Up_100 - Un_100, 4.2) + assert Up_100 - Un_100 == pytest.approx(4.2) def test_check_parameter_values(self): - with self.assertRaisesRegex(ValueError, "propotional term"): + with pytest.raises(ValueError, match="propotional term"): pybamm.ParameterValues( {"Negative electrode LAM constant propotional term": 1} ) # The + character in "1 + dlnf/dlnc" is appended with a backslash (\+), # since + has other meanings in regex - with self.assertRaisesRegex(ValueError, "Thermodynamic factor"): + with pytest.raises(ValueError, match="Thermodynamic factor"): pybamm.ParameterValues({"1 + dlnf/dlnc": 1}) def test_process_symbol(self): @@ -204,86 +198,86 @@ def test_process_symbol(self): # process parameter a = pybamm.Parameter("a") processed_a = parameter_values.process_symbol(a) - self.assertIsInstance(processed_a, pybamm.Scalar) - self.assertEqual(processed_a.value, 4) + assert isinstance(processed_a, pybamm.Scalar) + assert processed_a.value == 4 # process binary operation var = pybamm.Variable("var") add = a + var processed_add = parameter_values.process_symbol(add) - self.assertIsInstance(processed_add, pybamm.Addition) - self.assertIsInstance(processed_add.children[0], pybamm.Scalar) - self.assertIsInstance(processed_add.children[1], pybamm.Variable) - self.assertEqual(processed_add.children[0].value, 4) + assert isinstance(processed_add, pybamm.Addition) + assert isinstance(processed_add.children[0], pybamm.Scalar) + assert isinstance(processed_add.children[1], pybamm.Variable) + assert processed_add.children[0].value == 4 b = pybamm.Parameter("b") add = a + b processed_add = parameter_values.process_symbol(add) - self.assertIsInstance(processed_add, pybamm.Scalar) - self.assertEqual(processed_add.value, 6) + assert isinstance(processed_add, pybamm.Scalar) + assert processed_add.value == 6 scal = pybamm.Scalar(34) mul = a * scal processed_mul = parameter_values.process_symbol(mul) - self.assertIsInstance(processed_mul, pybamm.Scalar) - self.assertEqual(processed_mul.value, 136) + assert isinstance(processed_mul, pybamm.Scalar) + assert processed_mul.value == 136 # process integral aa = pybamm.PrimaryBroadcast(pybamm.Parameter("a"), "negative electrode") x = pybamm.SpatialVariable("x", domain=["negative electrode"]) integ = pybamm.Integral(aa, x) processed_integ = parameter_values.process_symbol(integ) - self.assertIsInstance(processed_integ, pybamm.Integral) - self.assertIsInstance(processed_integ.children[0], pybamm.PrimaryBroadcast) - self.assertEqual(processed_integ.children[0].child.value, 4) - self.assertEqual(processed_integ.integration_variable[0], x) + assert isinstance(processed_integ, pybamm.Integral) + assert isinstance(processed_integ.children[0], pybamm.PrimaryBroadcast) + assert processed_integ.children[0].child.value == 4 + assert processed_integ.integration_variable[0] == x # process unary operation v = pybamm.Variable("v", domain="test") grad = pybamm.Gradient(v) processed_grad = parameter_values.process_symbol(grad) - self.assertIsInstance(processed_grad, pybamm.Gradient) - self.assertIsInstance(processed_grad.children[0], pybamm.Variable) + assert isinstance(processed_grad, pybamm.Gradient) + assert isinstance(processed_grad.children[0], pybamm.Variable) # process delta function aa = pybamm.Parameter("a") delta_aa = pybamm.DeltaFunction(aa, "left", "some domain") processed_delta_aa = parameter_values.process_symbol(delta_aa) - self.assertIsInstance(processed_delta_aa, pybamm.DeltaFunction) - self.assertEqual(processed_delta_aa.side, "left") + assert isinstance(processed_delta_aa, pybamm.DeltaFunction) + assert processed_delta_aa.side == "left" processed_a = processed_delta_aa.children[0] - self.assertIsInstance(processed_a, pybamm.Scalar) - self.assertEqual(processed_a.value, 4) + assert isinstance(processed_a, pybamm.Scalar) + assert processed_a.value == 4 # process boundary operator (test for BoundaryValue) aa = pybamm.Parameter("a") x = pybamm.SpatialVariable("x", domain=["negative electrode"]) boundary_op = pybamm.BoundaryValue(aa * x, "left") processed_boundary_op = parameter_values.process_symbol(boundary_op) - self.assertIsInstance(processed_boundary_op, pybamm.BoundaryOperator) + assert isinstance(processed_boundary_op, pybamm.BoundaryOperator) processed_a = processed_boundary_op.children[0].children[0] processed_x = processed_boundary_op.children[0].children[1] - self.assertIsInstance(processed_a, pybamm.Scalar) - self.assertEqual(processed_a.value, 4) - self.assertEqual(processed_x, x) + assert isinstance(processed_a, pybamm.Scalar) + assert processed_a.value == 4 + assert processed_x == x # process EvaluateAt evaluate_at = pybamm.EvaluateAt(x, aa) processed_evaluate_at = parameter_values.process_symbol(evaluate_at) - self.assertIsInstance(processed_evaluate_at, pybamm.EvaluateAt) - self.assertEqual(processed_evaluate_at.children[0], x) - self.assertEqual(processed_evaluate_at.position, 4) - with self.assertRaisesRegex(ValueError, "'position' in 'EvaluateAt'"): + assert isinstance(processed_evaluate_at, pybamm.EvaluateAt) + assert processed_evaluate_at.children[0] == x + assert processed_evaluate_at.position == 4 + with pytest.raises(ValueError, match="'position' in 'EvaluateAt'"): parameter_values.process_symbol(pybamm.EvaluateAt(x, x)) # process broadcast whole_cell = ["negative electrode", "separator", "positive electrode"] broad = pybamm.PrimaryBroadcast(a, whole_cell) processed_broad = parameter_values.process_symbol(broad) - self.assertIsInstance(processed_broad, pybamm.Broadcast) - self.assertEqual(processed_broad.domain, whole_cell) - self.assertIsInstance(processed_broad.children[0], pybamm.Scalar) - self.assertEqual(processed_broad.children[0].evaluate(), 4) + assert isinstance(processed_broad, pybamm.Broadcast) + assert processed_broad.domain == whole_cell + assert isinstance(processed_broad.children[0], pybamm.Scalar) + assert processed_broad.children[0].evaluate() == 4 # process concatenation conc = pybamm.concatenation( @@ -291,8 +285,8 @@ def test_process_symbol(self): pybamm.Vector(2 * np.ones(15), domain="test 2"), ) processed_conc = parameter_values.process_symbol(conc) - self.assertIsInstance(processed_conc.children[0], pybamm.Vector) - self.assertIsInstance(processed_conc.children[1], pybamm.Vector) + assert isinstance(processed_conc.children[0], pybamm.Vector) + assert isinstance(processed_conc.children[1], pybamm.Vector) np.testing.assert_array_equal(processed_conc.children[0].entries, 1) np.testing.assert_array_equal(processed_conc.children[1].entries, 2) @@ -304,52 +298,52 @@ def test_process_symbol(self): processed_dom_con = parameter_values.process_symbol(dom_con) a_proc = processed_dom_con.children[0].children[0] b_proc = processed_dom_con.children[1].children[0] - self.assertIsInstance(a_proc, pybamm.Scalar) - self.assertIsInstance(b_proc, pybamm.Scalar) - self.assertEqual(a_proc.value, 4) - self.assertEqual(b_proc.value, 2) + assert isinstance(a_proc, pybamm.Scalar) + assert isinstance(b_proc, pybamm.Scalar) + assert a_proc.value == 4 + assert b_proc.value == 2 # process variable c = pybamm.Variable("c") processed_c = parameter_values.process_symbol(c) - self.assertIsInstance(processed_c, pybamm.Variable) - self.assertEqual(processed_c.name, "c") + assert isinstance(processed_c, pybamm.Variable) + assert processed_c.name == "c" # process scalar d = pybamm.Scalar(14) processed_d = parameter_values.process_symbol(d) - self.assertIsInstance(processed_d, pybamm.Scalar) - self.assertEqual(processed_d.value, 14) + assert isinstance(processed_d, pybamm.Scalar) + assert processed_d.value == 14 # process array types e = pybamm.Vector(np.ones(4)) processed_e = parameter_values.process_symbol(e) - self.assertIsInstance(processed_e, pybamm.Vector) + assert isinstance(processed_e, pybamm.Vector) np.testing.assert_array_equal(processed_e.evaluate(), np.ones((4, 1))) f = pybamm.Matrix(np.ones((5, 6))) processed_f = parameter_values.process_symbol(f) - self.assertIsInstance(processed_f, pybamm.Matrix) + assert isinstance(processed_f, pybamm.Matrix) np.testing.assert_array_equal(processed_f.evaluate(), np.ones((5, 6))) # process statevector g = pybamm.StateVector(slice(0, 10)) processed_g = parameter_values.process_symbol(g) - self.assertIsInstance(processed_g, pybamm.StateVector) + assert isinstance(processed_g, pybamm.StateVector) np.testing.assert_array_equal( processed_g.evaluate(y=np.ones(10)), np.ones((10, 1)) ) # not found - with self.assertRaises(KeyError): + with pytest.raises(KeyError): x = pybamm.Parameter("x") parameter_values.process_symbol(x) parameter_values = pybamm.ParameterValues({"x": np.nan}) - with self.assertRaisesRegex(ValueError, "Parameter 'x' not found"): + with pytest.raises(ValueError, match="Parameter 'x' not found"): x = pybamm.Parameter("x") parameter_values.process_symbol(x) - with self.assertRaisesRegex(ValueError, "possibly a function"): + with pytest.raises(ValueError, match="possibly a function"): x = pybamm.FunctionParameter("x", {}) parameter_values.process_symbol(x) @@ -361,11 +355,11 @@ def test_process_parameter_in_parameter(self): # process 2a parameter a = pybamm.Parameter("2a") processed_a = parameter_values.process_symbol(a) - self.assertEqual(processed_a.evaluate(), 4) + assert processed_a.evaluate() == 4 # case where parameter can't be processed b = pybamm.Parameter("b") - with self.assertRaisesRegex(TypeError, "Cannot process parameter"): + with pytest.raises(TypeError, match="Cannot process parameter"): parameter_values.process_symbol(b) def test_process_input_parameter(self): @@ -375,19 +369,19 @@ def test_process_input_parameter(self): # process input parameter a = pybamm.Parameter("a") processed_a = parameter_values.process_symbol(a) - self.assertIsInstance(processed_a, pybamm.InputParameter) - self.assertEqual(processed_a.evaluate(inputs={"a": 5}), 5) + assert isinstance(processed_a, pybamm.InputParameter) + assert processed_a.evaluate(inputs={"a": 5}) == 5 # process binary operation b = pybamm.Parameter("b") add = a + b processed_add = parameter_values.process_symbol(add) - self.assertEqual(processed_add, 3 + pybamm.InputParameter("a")) + assert processed_add == 3 + pybamm.InputParameter("a") # process complex input parameter c = pybamm.Parameter("c times 2") processed_c = parameter_values.process_symbol(c) - self.assertEqual(processed_c.evaluate(inputs={"c": 5}), 10) + assert processed_c.evaluate(inputs={"c": 5}) == 10 def test_process_function_parameter(self): def test_function(var): @@ -408,7 +402,7 @@ def test_function(var): # process function func = pybamm.FunctionParameter("func", {"a": a}) processed_func = parameter_values.process_symbol(func) - self.assertEqual(processed_func.evaluate(inputs={"a": 3}), 369) + assert processed_func.evaluate(inputs={"a": 3}) == 369 # process constant function # this should work even if the parameter in the function is not provided @@ -416,35 +410,35 @@ def test_function(var): "const", {"a": pybamm.Parameter("not provided")} ) processed_const = parameter_values.process_symbol(const) - self.assertIsInstance(processed_const, pybamm.Scalar) - self.assertEqual(processed_const.evaluate(), 254) + assert isinstance(processed_const, pybamm.Scalar) + assert processed_const.evaluate() == 254 # process case where parameter provided is a pybamm symbol # (e.g. a multiplication) mult = pybamm.FunctionParameter("mult", {"a": a}) processed_mult = parameter_values.process_symbol(mult) - self.assertEqual(processed_mult.evaluate(inputs={"a": 14, "b": 63}), 63 * 5) + assert processed_mult.evaluate(inputs={"a": 14, "b": 63}) == 63 * 5 # process differentiated function parameter diff_func = func.diff(a) processed_diff_func = parameter_values.process_symbol(diff_func) - self.assertEqual(processed_diff_func.evaluate(inputs={"a": 3}), 123) + assert processed_diff_func.evaluate(inputs={"a": 3}) == 123 # make sure diff works, despite simplifications, when the child is constant a_const = pybamm.Scalar(3) func_const = pybamm.FunctionParameter("func", {"a": a_const}) diff_func_const = func_const.diff(a_const) processed_diff_func_const = parameter_values.process_symbol(diff_func_const) - self.assertEqual(processed_diff_func_const.evaluate(), 123) + assert processed_diff_func_const.evaluate() == 123 # function parameter that returns a python float func = pybamm.FunctionParameter("float_func", {"a": a}) processed_func = parameter_values.process_symbol(func) - self.assertEqual(processed_func.evaluate(), 42) + assert processed_func.evaluate() == 42 # weird type raises error func = pybamm.FunctionParameter("bad type", {"a": a}) - with self.assertRaisesRegex(TypeError, "Parameter provided for"): + with pytest.raises(TypeError, match="Parameter provided for"): parameter_values.process_symbol(func) # function itself as input (different to the variable being an input) @@ -452,7 +446,7 @@ def test_function(var): a = pybamm.Scalar(3) func = pybamm.FunctionParameter("func", {"a": a}) processed_func = parameter_values.process_symbol(func) - self.assertEqual(processed_func.evaluate(inputs={"func": 13}), 13) + assert processed_func.evaluate(inputs={"func": 13}) == 13 # make sure function keeps the domain of the original function @@ -473,11 +467,11 @@ def my_func(x): ) func3 = parameter_values.process_symbol(func) - self.assertEqual(func1.domains, func2.domains) - self.assertEqual(func1.domains, func3.domains) + assert func1.domains == func2.domains + assert func1.domains == func3.domains # [function] is deprecated - with self.assertRaisesRegex(ValueError, "[function]"): + with pytest.raises(ValueError, match="[function]"): pybamm.ParameterValues({"func": "[function]something"}) def test_process_inline_function_parameters(self): @@ -490,12 +484,12 @@ def D(c): func = pybamm.FunctionParameter("Diffusivity", {"a": a}) processed_func = parameter_values.process_symbol(func) - self.assertEqual(processed_func.evaluate(), 9) + assert processed_func.evaluate() == 9 # process differentiated function parameter diff_func = func.diff(a) processed_diff_func = parameter_values.process_symbol(diff_func) - self.assertEqual(processed_diff_func.evaluate(), 6) + assert processed_diff_func.evaluate() == 6 def test_multi_var_function_with_parameters(self): def D(a, b): @@ -508,8 +502,8 @@ def D(a, b): processed_func = parameter_values.process_symbol(func) # Function of scalars gets automatically simplified - self.assertIsInstance(processed_func, pybamm.Scalar) - self.assertEqual(processed_func.evaluate(), 3) + assert isinstance(processed_func, pybamm.Scalar) + assert processed_func.evaluate() == 3 def test_multi_var_function_parameter(self): def D(a, b): @@ -522,7 +516,7 @@ def D(a, b): func = pybamm.FunctionParameter("Diffusivity", {"a": a, "b": b}) processed_func = parameter_values.process_symbol(func) - self.assertEqual(processed_func.evaluate(), 3) + assert processed_func.evaluate() == 3 def test_process_interpolant(self): x = np.linspace(0, 10)[:, np.newaxis] @@ -533,18 +527,18 @@ def test_process_interpolant(self): func = pybamm.FunctionParameter("Times two", {"a": a}) processed_func = parameter_values.process_symbol(func) - self.assertIsInstance(processed_func, pybamm.Interpolant) - self.assertEqual(processed_func.evaluate(inputs={"a": 3.01}), 6.02) + assert isinstance(processed_func, pybamm.Interpolant) + assert processed_func.evaluate(inputs={"a": 3.01}) == 6.02 # interpolant defined up front interp = pybamm.Interpolant(data[:, 0], data[:, 1], a, interpolator="cubic") processed_interp = parameter_values.process_symbol(interp) - self.assertEqual(processed_interp.evaluate(inputs={"a": 3.01}), 6.02) + assert processed_interp.evaluate(inputs={"a": 3.01}) == 6.02 # process differentiated function parameter diff_interp = interp.diff(a) processed_diff_interp = parameter_values.process_symbol(diff_interp) - self.assertEqual(processed_diff_interp.evaluate(inputs={"a": 3.01}), 2) + assert processed_diff_interp.evaluate(inputs={"a": 3.01}) == 2 def test_process_interpolant_2d(self): x_ = [np.linspace(0, 10), np.linspace(0, 20)] @@ -566,9 +560,9 @@ def test_process_interpolant_2d(self): func = pybamm.FunctionParameter("Times two", {"a": a, "b": b}) processed_func = parameter_values.process_symbol(func) - self.assertIsInstance(processed_func, pybamm.Interpolant) - self.assertAlmostEqual( - processed_func.evaluate(inputs={"a": 3.01, "b": 4.4}), 14.82 + assert isinstance(processed_func, pybamm.Interpolant) + assert processed_func.evaluate(inputs={"a": 3.01, "b": 4.4}) == pytest.approx( + 14.82 ) # process differentiated function parameter @@ -579,9 +573,7 @@ def test_process_interpolant_2d(self): # interpolant defined up front interp2 = pybamm.Interpolant(data[0], data[1], children=(a, b)) processed_interp2 = parameter_values.process_symbol(interp2) - self.assertEqual( - processed_interp2.evaluate(inputs={"a": 3.01, "b": 4.4}), 14.82 - ) + assert processed_interp2.evaluate(inputs={"a": 3.01, "b": 4.4}) == 14.82 y3 = (3 * x).sum(axis=1) @@ -598,7 +590,7 @@ def test_process_interpolant_2d(self): func = pybamm.FunctionParameter("Times three", {"a": a, "b": b}) processed_func = parameter_values.process_symbol(func) - self.assertIsInstance(processed_func, pybamm.Interpolant) + assert isinstance(processed_func, pybamm.Interpolant) # self.assertEqual(processed_func.evaluate().flatten()[0], 22.23) np.testing.assert_almost_equal( processed_func.evaluate(inputs={"a": 3.01, "b": 4.4}).flatten()[0], @@ -765,7 +757,7 @@ def test_process_integral_broadcast(self): param = pybamm.ParameterValues({"func": 2}) func_proc = param.process_symbol(func) - self.assertEqual(func_proc, pybamm.Scalar(2, name="func")) + assert func_proc == pybamm.Scalar(2, name="func") # test with auxiliary domains @@ -780,9 +772,8 @@ def test_process_integral_broadcast(self): param = pybamm.ParameterValues({"func": 2}) func_proc = param.process_symbol(func) - self.assertEqual( - func_proc, - pybamm.PrimaryBroadcast(pybamm.Scalar(2, name="func"), "current collector"), + assert func_proc == pybamm.PrimaryBroadcast( + pybamm.Scalar(2, name="func"), "current collector" ) # secondary and tertiary @@ -799,11 +790,8 @@ def test_process_integral_broadcast(self): param = pybamm.ParameterValues({"func": 2}) func_proc = param.process_symbol(func) - self.assertEqual( - func_proc, - pybamm.FullBroadcast( - pybamm.Scalar(2, name="func"), "negative particle", "current collector" - ), + assert func_proc == pybamm.FullBroadcast( + pybamm.Scalar(2, name="func"), "negative particle", "current collector" ) # secondary, tertiary and quaternary @@ -821,16 +809,13 @@ def test_process_integral_broadcast(self): param = pybamm.ParameterValues({"func": 2}) func_proc = param.process_symbol(func) - self.assertEqual( - func_proc, - pybamm.FullBroadcast( - pybamm.Scalar(2, name="func"), - "negative particle", - { - "secondary": "negative particle size", - "tertiary": "current collector", - }, - ), + assert func_proc == pybamm.FullBroadcast( + pybamm.Scalar(2, name="func"), + "negative particle", + { + "secondary": "negative particle size", + "tertiary": "current collector", + }, ) # special case for integral of concatenations of broadcasts @@ -854,7 +839,7 @@ def test_process_integral_broadcast(self): ) func_proc = param.process_symbol(func) - self.assertEqual(func_proc, pybamm.Scalar(3)) + assert func_proc == pybamm.Scalar(3) # with auxiliary domains var_n = pybamm.Variable( @@ -889,9 +874,8 @@ def test_process_integral_broadcast(self): ) func_proc = param.process_symbol(func) - self.assertEqual( - func_proc, - pybamm.PrimaryBroadcast(pybamm.Scalar(3), "current collector"), + assert func_proc == pybamm.PrimaryBroadcast( + pybamm.Scalar(3), "current collector" ) def test_process_size_average(self): @@ -913,16 +897,16 @@ def dist(R): ) var_av_proc = param.process_symbol(var_av) - self.assertIsInstance(var_av_proc, pybamm.SizeAverage) + assert isinstance(var_av_proc, pybamm.SizeAverage) R = pybamm.SpatialVariable("R", "negative particle size") - self.assertEqual(var_av_proc.f_a_dist, R**2) + assert var_av_proc.f_a_dist == R**2 def test_process_not_constant(self): param = pybamm.ParameterValues({"a": 4}) a = pybamm.NotConstant(pybamm.Parameter("a")) - self.assertIsInstance(param.process_symbol(a), pybamm.NotConstant) - self.assertEqual(param.process_symbol(a).evaluate(), 4) + assert isinstance(param.process_symbol(a), pybamm.NotConstant) + assert param.process_symbol(a).evaluate() == 4 def test_process_complex_expression(self): var1 = pybamm.Variable("var1") @@ -933,12 +917,12 @@ def test_process_complex_expression(self): param = pybamm.ParameterValues({"par1": 2, "par2": 4}) exp_param = param.process_symbol(expression) - self.assertEqual(exp_param, 3.0 * (2.0**var2) / ((-4.0 + var1) + var2)) + assert exp_param == 3.0 * (2.0**var2) / ((-4.0 + var1) + var2) def test_process_geometry(self): var = pybamm.Variable("var") geometry = {"negative electrode": {"x": {"min": 0, "max": var}}} - with self.assertRaisesRegex(ValueError, "Geometry parameters must be Scalars"): + with pytest.raises(ValueError, match="Geometry parameters must be Scalars"): pybamm.ParameterValues({}).process_geometry(geometry) def test_process_model(self): @@ -965,39 +949,37 @@ def test_process_model(self): parameter_values = pybamm.ParameterValues({"a": 1, "b": 2, "c": 3, "d": 42}) parameter_values.process_model(model) # rhs - self.assertIsInstance(model.rhs[var1], pybamm.Gradient) + assert isinstance(model.rhs[var1], pybamm.Gradient) # algebraic - self.assertIsInstance(model.algebraic[var2], pybamm.Multiplication) - self.assertIsInstance(model.algebraic[var2].children[0], pybamm.Scalar) - self.assertIsInstance(model.algebraic[var2].children[1], pybamm.Variable) - self.assertEqual(model.algebraic[var2].children[0].value, 3) + assert isinstance(model.algebraic[var2], pybamm.Multiplication) + assert isinstance(model.algebraic[var2].children[0], pybamm.Scalar) + assert isinstance(model.algebraic[var2].children[1], pybamm.Variable) + assert model.algebraic[var2].children[0].value == 3 # initial conditions - self.assertIsInstance(model.initial_conditions[var1], pybamm.Scalar) - self.assertEqual(model.initial_conditions[var1].value, 2) + assert isinstance(model.initial_conditions[var1], pybamm.Scalar) + assert model.initial_conditions[var1].value == 2 # boundary conditions bc_key = next(iter(model.boundary_conditions.keys())) - self.assertIsInstance(bc_key, pybamm.Variable) + assert isinstance(bc_key, pybamm.Variable) bc_value = next(iter(model.boundary_conditions.values())) - self.assertIsInstance(bc_value["left"][0], pybamm.Scalar) - self.assertEqual(bc_value["left"][0].value, 3) - self.assertIsInstance(bc_value["right"][0], pybamm.Scalar) - self.assertEqual(bc_value["right"][0].value, 42) + assert isinstance(bc_value["left"][0], pybamm.Scalar) + assert bc_value["left"][0].value == 3 + assert isinstance(bc_value["right"][0], pybamm.Scalar) + assert bc_value["right"][0].value == 42 # variables - self.assertEqual(model.variables["var1"], var1) - self.assertIsInstance(model.variables["grad_var1"], pybamm.Gradient) - self.assertIsInstance(model.variables["grad_var1"].children[0], pybamm.Variable) - self.assertEqual( - model.variables["d_var1"], (pybamm.Scalar(42, name="d") * var1) - ) - self.assertIsInstance(model.variables["d_var1"].children[0], pybamm.Scalar) - self.assertIsInstance(model.variables["d_var1"].children[1], pybamm.Variable) + assert model.variables["var1"] == var1 + assert isinstance(model.variables["grad_var1"], pybamm.Gradient) + assert isinstance(model.variables["grad_var1"].children[0], pybamm.Variable) + assert model.variables["d_var1"] == (pybamm.Scalar(42, name="d") * var1) + assert isinstance(model.variables["d_var1"].children[0], pybamm.Scalar) + assert isinstance(model.variables["d_var1"].children[1], pybamm.Variable) # bad boundary conditions model = pybamm.BaseModel() model.algebraic = {var1: var1} x = pybamm.Parameter("x") model.boundary_conditions = {var1: {"left": (x, "Dirichlet")}} - with self.assertRaises(KeyError): + with pytest.raises(KeyError): parameter_values.process_model(model) def test_inplace(self): @@ -1006,16 +988,16 @@ def test_inplace(self): new_model = param.process_model(model, inplace=False) V = model.variables["Voltage [V]"] - self.assertTrue(V.has_symbol_of_classes(pybamm.Parameter)) + assert V.has_symbol_of_classes(pybamm.Parameter) V = new_model.variables["Voltage [V]"] - self.assertFalse(V.has_symbol_of_classes(pybamm.Parameter)) + assert not V.has_symbol_of_classes(pybamm.Parameter) def test_process_empty_model(self): model = pybamm.BaseModel() parameter_values = pybamm.ParameterValues({"a": 1, "b": 2, "c": 3, "d": 42}) - with self.assertRaisesRegex( - pybamm.ModelError, "Cannot process parameters for empty model" + with pytest.raises( + pybamm.ModelError, match="Cannot process parameters for empty model" ): parameter_values.process_model(model) @@ -1024,15 +1006,15 @@ def test_evaluate(self): a = pybamm.Parameter("a") b = pybamm.Parameter("b") c = pybamm.Parameter("c") - self.assertEqual(parameter_values.evaluate(a), 1) - self.assertEqual(parameter_values.evaluate(a + (b * c)), 7) + assert parameter_values.evaluate(a) == 1 + assert parameter_values.evaluate(a + (b * c)) == 7 d = pybamm.Parameter("a") + pybamm.Parameter("b") * pybamm.Array([4, 5]) np.testing.assert_array_equal( parameter_values.evaluate(d), np.array([9, 11])[:, np.newaxis] ) y = pybamm.StateVector(slice(0, 1)) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): parameter_values.evaluate(y) def test_exchange_current_density_plating(self): @@ -1042,18 +1024,8 @@ def test_exchange_current_density_plating(self): param = pybamm.Parameter( "Exchange-current density for lithium metal electrode [A.m-2]" ) - with self.assertRaisesRegex( + with pytest.raises( KeyError, - "referring to the reaction at the surface of a lithium metal electrode", + match="referring to the reaction at the surface of a lithium metal electrode", ): parameter_values.evaluate(param) - - -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_process_parameter_data.py b/tests/unit/test_parameters/test_process_parameter_data.py index 3230f374f2..9352894c5c 100644 --- a/tests/unit/test_parameters/test_process_parameter_data.py +++ b/tests/unit/test_parameters/test_process_parameter_data.py @@ -7,62 +7,52 @@ import numpy as np import pybamm -import unittest +import pytest -class TestProcessParameterData(unittest.TestCase): +class TestProcessParameterData: def test_process_1D_data(self): name = "lico2_ocv_example" path = os.path.abspath(os.path.dirname(__file__)) processed = pybamm.parameters.process_1D_data(name, path) - self.assertEqual(processed[0], name) - self.assertIsInstance(processed[1], tuple) - self.assertIsInstance(processed[1][0][0], np.ndarray) - self.assertIsInstance(processed[1][1], np.ndarray) + assert processed[0] == name + assert isinstance(processed[1], tuple) + assert isinstance(processed[1][0][0], np.ndarray) + assert isinstance(processed[1][1], np.ndarray) def test_process_2D_data(self): name = "lico2_diffusivity_Dualfoil1998_2D" path = os.path.abspath(os.path.dirname(__file__)) processed = pybamm.parameters.process_2D_data(name, path) - self.assertEqual(processed[0], name) - self.assertIsInstance(processed[1], tuple) - self.assertIsInstance(processed[1][0][0], np.ndarray) - self.assertIsInstance(processed[1][0][1], np.ndarray) - self.assertIsInstance(processed[1][1], np.ndarray) + assert processed[0] == name + assert isinstance(processed[1], tuple) + assert isinstance(processed[1][0][0], np.ndarray) + assert isinstance(processed[1][0][1], np.ndarray) + assert isinstance(processed[1][1], np.ndarray) def test_process_2D_data_csv(self): name = "data_for_testing_2D" path = os.path.abspath(os.path.dirname(__file__)) processed = pybamm.parameters.process_2D_data_csv(name, path) - self.assertEqual(processed[0], name) - self.assertIsInstance(processed[1], tuple) - self.assertIsInstance(processed[1][0][0], np.ndarray) - self.assertIsInstance(processed[1][0][1], np.ndarray) - self.assertIsInstance(processed[1][1], np.ndarray) + assert processed[0] == name + assert isinstance(processed[1], tuple) + assert isinstance(processed[1][0][0], np.ndarray) + assert isinstance(processed[1][0][1], np.ndarray) + assert isinstance(processed[1][1], np.ndarray) def test_process_3D_data_csv(self): name = "data_for_testing_3D" path = os.path.abspath(os.path.dirname(__file__)) processed = pybamm.parameters.process_3D_data_csv(name, path) - self.assertEqual(processed[0], name) - self.assertIsInstance(processed[1], tuple) - self.assertIsInstance(processed[1][0][0], np.ndarray) - self.assertIsInstance(processed[1][0][1], np.ndarray) - self.assertIsInstance(processed[1][0][2], np.ndarray) - self.assertIsInstance(processed[1][1], np.ndarray) + assert processed[0] == name + assert isinstance(processed[1], tuple) + assert isinstance(processed[1][0][0], np.ndarray) + assert isinstance(processed[1][0][1], np.ndarray) + assert isinstance(processed[1][0][2], np.ndarray) + assert isinstance(processed[1][1], np.ndarray) def test_error(self): - with self.assertRaisesRegex(FileNotFoundError, "Could not find file"): + with pytest.raises(FileNotFoundError, match="Could not find file"): pybamm.parameters.process_1D_data("not_a_real_file", "not_a_real_path") - - -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_plotting/test_quick_plot.py b/tests/unit/test_plotting/test_quick_plot.py index eb6c0607e3..d5d994117d 100644 --- a/tests/unit/test_plotting/test_quick_plot.py +++ b/tests/unit/test_plotting/test_quick_plot.py @@ -1,12 +1,12 @@ import os import pybamm -import unittest +import pytest import numpy as np from tempfile import TemporaryDirectory -class TestQuickPlot(unittest.TestCase): +class TestQuickPlot: def test_simple_ode_model(self): model = pybamm.lithium_ion.BaseModel(name="Simple ODE Model") @@ -77,11 +77,11 @@ def test_simple_ode_model(self): # update the axis new_axis = [0, 0.5, 0, 1] quick_plot.axis_limits.update({("a",): new_axis}) - self.assertEqual(quick_plot.axis_limits[("a",)], new_axis) + assert quick_plot.axis_limits[("a",)] == new_axis # and now reset them quick_plot.reset_axis() - self.assertNotEqual(quick_plot.axis_limits[("a",)], new_axis) + assert quick_plot.axis_limits[("a",)] != new_axis # check dynamic plot loads quick_plot.dynamic_plot(show_plot=False) @@ -90,7 +90,7 @@ def test_simple_ode_model(self): # Test with different output variables quick_plot = pybamm.QuickPlot(solution, ["b broadcasted"]) - self.assertEqual(len(quick_plot.axis_limits), 1) + assert len(quick_plot.axis_limits) == 1 quick_plot.plot(0) quick_plot = pybamm.QuickPlot( @@ -103,18 +103,18 @@ def test_simple_ode_model(self): "c broadcasted positive electrode", ], ) - self.assertEqual(len(quick_plot.axis_limits), 5) + assert len(quick_plot.axis_limits) == 5 quick_plot.plot(0) # update the axis new_axis = [0, 0.5, 0, 1] var_key = ("c broadcasted",) quick_plot.axis_limits.update({var_key: new_axis}) - self.assertEqual(quick_plot.axis_limits[var_key], new_axis) + assert quick_plot.axis_limits[var_key] == new_axis # and now reset them quick_plot.reset_axis() - self.assertNotEqual(quick_plot.axis_limits[var_key], new_axis) + assert quick_plot.axis_limits[var_key] != new_axis # check dynamic plot loads quick_plot.dynamic_plot(show_plot=False) @@ -135,19 +135,19 @@ def test_simple_ode_model(self): labels=["sol 1", "sol 2"], n_rows=2, ) - self.assertEqual(quick_plot.colors, ["r", "g", "b"]) - self.assertEqual(quick_plot.linestyles, ["-", "--"]) - self.assertEqual(quick_plot.figsize, (1, 2)) - self.assertEqual(quick_plot.labels, ["sol 1", "sol 2"]) - self.assertEqual(quick_plot.n_rows, 2) - self.assertEqual(quick_plot.n_cols, 1) + assert quick_plot.colors == ["r", "g", "b"] + assert quick_plot.linestyles == ["-", "--"] + assert quick_plot.figsize == (1, 2) + assert quick_plot.labels == ["sol 1", "sol 2"] + assert quick_plot.n_rows == 2 + assert quick_plot.n_cols == 1 # Test different time units quick_plot = pybamm.QuickPlot(solution, ["a"]) - self.assertEqual(quick_plot.time_scaling_factor, 1) + assert quick_plot.time_scaling_factor == 1 quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="seconds") quick_plot.plot(0) - self.assertEqual(quick_plot.time_scaling_factor, 1) + assert quick_plot.time_scaling_factor == 1 np.testing.assert_array_almost_equal( quick_plot.plots[("a",)][0][0].get_xdata(), t_eval ) @@ -156,7 +156,7 @@ def test_simple_ode_model(self): ) quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="minutes") quick_plot.plot(0) - self.assertEqual(quick_plot.time_scaling_factor, 60) + assert quick_plot.time_scaling_factor == 60 np.testing.assert_array_almost_equal( quick_plot.plots[("a",)][0][0].get_xdata(), t_eval / 60 ) @@ -165,30 +165,30 @@ def test_simple_ode_model(self): ) quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="hours") quick_plot.plot(0) - self.assertEqual(quick_plot.time_scaling_factor, 3600) + assert quick_plot.time_scaling_factor == 3600 np.testing.assert_array_almost_equal( quick_plot.plots[("a",)][0][0].get_xdata(), t_eval / 3600 ) np.testing.assert_array_almost_equal( quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_eval ) - with self.assertRaisesRegex(ValueError, "time unit"): + with pytest.raises(ValueError, match="time unit"): pybamm.QuickPlot(solution, ["a"], time_unit="bad unit") # long solution defaults to hours instead of seconds solution_long = solver.solve(model, np.linspace(0, 1e5)) quick_plot = pybamm.QuickPlot(solution_long, ["a"]) - self.assertEqual(quick_plot.time_scaling_factor, 3600) + assert quick_plot.time_scaling_factor == 3600 # Test different spatial units quick_plot = pybamm.QuickPlot(solution, ["a"]) - self.assertEqual(quick_plot.spatial_unit, r"$\mu$m") + assert quick_plot.spatial_unit == r"$\mu$m" quick_plot = pybamm.QuickPlot(solution, ["a"], spatial_unit="m") - self.assertEqual(quick_plot.spatial_unit, "m") + assert quick_plot.spatial_unit == "m" quick_plot = pybamm.QuickPlot(solution, ["a"], spatial_unit="mm") - self.assertEqual(quick_plot.spatial_unit, "mm") + assert quick_plot.spatial_unit == "mm" quick_plot = pybamm.QuickPlot(solution, ["a"], spatial_unit="um") - self.assertEqual(quick_plot.spatial_unit, r"$\mu$m") - with self.assertRaisesRegex(ValueError, "spatial unit"): + assert quick_plot.spatial_unit == r"$\mu$m" + with pytest.raises(ValueError, match="spatial unit"): pybamm.QuickPlot(solution, ["a"], spatial_unit="bad unit") # Test 2D variables @@ -197,24 +197,25 @@ def test_simple_ode_model(self): quick_plot.dynamic_plot(show_plot=False) quick_plot.slider_update(0.01) - with self.assertRaisesRegex(NotImplementedError, "Cannot plot 2D variables"): + with pytest.raises(NotImplementedError, match="Cannot plot 2D variables"): pybamm.QuickPlot([solution, solution], ["2D variable"]) # Test different variable limits quick_plot = pybamm.QuickPlot( solution, ["a", ["c broadcasted", "c broadcasted"]], variable_limits="tight" ) - self.assertEqual(quick_plot.axis_limits[("a",)][2:], [None, None]) - self.assertEqual( - quick_plot.axis_limits[("c broadcasted", "c broadcasted")][2:], [None, None] - ) + assert quick_plot.axis_limits[("a",)][2:] == [None, None] + assert quick_plot.axis_limits[("c broadcasted", "c broadcasted")][2:] == [ + None, + None, + ] quick_plot.plot(0) quick_plot.slider_update(1) quick_plot = pybamm.QuickPlot( solution, ["2D variable"], variable_limits="tight" ) - self.assertEqual(quick_plot.variable_limits[("2D variable",)], (None, None)) + assert quick_plot.variable_limits[("2D variable",)] == (None, None) quick_plot.plot(0) quick_plot.slider_update(1) @@ -223,41 +224,37 @@ def test_simple_ode_model(self): ["a", ["c broadcasted", "c broadcasted"]], variable_limits={"a": [1, 2], ("c broadcasted", "c broadcasted"): [3, 4]}, ) - self.assertEqual(quick_plot.axis_limits[("a",)][2:], [1, 2]) - self.assertEqual( - quick_plot.axis_limits[("c broadcasted", "c broadcasted")][2:], [3, 4] - ) + assert quick_plot.axis_limits[("a",)][2:] == [1, 2] + assert quick_plot.axis_limits[("c broadcasted", "c broadcasted")][2:] == [3, 4] quick_plot.plot(0) quick_plot.slider_update(1) quick_plot = pybamm.QuickPlot( solution, ["a", "b broadcasted"], variable_limits={"a": "tight"} ) - self.assertEqual(quick_plot.axis_limits[("a",)][2:], [None, None]) - self.assertNotEqual( - quick_plot.axis_limits[("b broadcasted",)][2:], [None, None] - ) + assert quick_plot.axis_limits[("a",)][2:] == [None, None] + assert quick_plot.axis_limits[("b broadcasted",)][2:] != [None, None] quick_plot.plot(0) quick_plot.slider_update(1) - with self.assertRaisesRegex( - TypeError, "variable_limits must be 'fixed', 'tight', or a dict" + with pytest.raises( + TypeError, match="variable_limits must be 'fixed', 'tight', or a dict" ): pybamm.QuickPlot( solution, ["a", "b broadcasted"], variable_limits="bad variable limits" ) # Test errors - with self.assertRaisesRegex(ValueError, "Mismatching variable domains"): + with pytest.raises(ValueError, match="Mismatching variable domains"): pybamm.QuickPlot(solution, [["a", "b broadcasted"]]) - with self.assertRaisesRegex(ValueError, "labels"): + with pytest.raises(ValueError, match="labels"): pybamm.QuickPlot( [solution, solution], ["a"], labels=["sol 1", "sol 2", "sol 3"] ) # No variable can be NaN - with self.assertRaisesRegex( - ValueError, "All-NaN variable 'NaN variable' provided" + with pytest.raises( + ValueError, match="All-NaN variable 'NaN variable' provided" ): pybamm.QuickPlot(solution, ["NaN variable"]) @@ -269,7 +266,7 @@ def test_plot_with_different_models(self): model.rhs = {a: pybamm.Scalar(0)} model.initial_conditions = {a: pybamm.Scalar(0)} solution = pybamm.CasadiSolver("fast").solve(model, [0, 1]) - with self.assertRaisesRegex(ValueError, "No default output variables"): + with pytest.raises(ValueError, match="No default output variables"): pybamm.QuickPlot(solution) def test_spm_simulation(self): @@ -462,17 +459,17 @@ def test_plot_2plus1D_spm(self): ][1] np.testing.assert_array_almost_equal(qp_data.T, phi_n[:, :, -1]) - with self.assertRaisesRegex(NotImplementedError, "Shape not recognized for"): + with pytest.raises(NotImplementedError, match="Shape not recognized for"): pybamm.QuickPlot(solution, ["Negative particle concentration [mol.m-3]"]) pybamm.close_plots() def test_invalid_input_type_failure(self): - with self.assertRaisesRegex(TypeError, "Solutions must be"): + with pytest.raises(TypeError, match="Solutions must be"): pybamm.QuickPlot(1) def test_empty_list_failure(self): - with self.assertRaisesRegex(TypeError, "QuickPlot requires at least 1"): + with pytest.raises(TypeError, match="QuickPlot requires at least 1"): pybamm.QuickPlot([]) def test_model_with_inputs(self): @@ -509,20 +506,10 @@ def test_model_with_inputs(self): pybamm.close_plots() -class TestQuickPlotAxes(unittest.TestCase): +class TestQuickPlotAxes: def test_quick_plot_axes(self): axes = pybamm.QuickPlotAxes() axes.add(("test 1", "test 2"), 1) - self.assertEqual(axes[0], 1) - self.assertEqual(axes.by_variable("test 1"), 1) - self.assertEqual(axes.by_variable("test 2"), 1) - - -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() + assert axes[0] == 1 + assert axes.by_variable("test 1") == 1 + assert axes.by_variable("test 2") == 1 diff --git a/tests/unit/test_serialisation/test_serialisation.py b/tests/unit/test_serialisation/test_serialisation.py index a1286cad26..adf53b7b46 100644 --- a/tests/unit/test_serialisation/test_serialisation.py +++ b/tests/unit/test_serialisation/test_serialisation.py @@ -4,8 +4,7 @@ import json import os -import unittest -import unittest.mock as mock +import pytest from datetime import datetime import numpy as np import pybamm @@ -14,14 +13,14 @@ from pybamm.expression_tree.operations.serialise import Serialise -def scalar_var_dict(): +def scalar_var_dict(mocker): """variable, json pair for a pybamm.Scalar instance""" a = pybamm.Scalar(5) a_dict = { - "py/id": mock.ANY, + "py/id": mocker.ANY, "py/object": "pybamm.expression_tree.scalar.Scalar", "name": "5.0", - "id": mock.ANY, + "id": mocker.ANY, "value": 5.0, "children": [], } @@ -29,7 +28,7 @@ def scalar_var_dict(): return a, a_dict -def mesh_var_dict(): +def mesh_var_dict(mocker): """mesh, json pair for a pybamm.Mesh instance""" r = pybamm.SpatialVariable( @@ -48,13 +47,13 @@ def mesh_var_dict(): mesh_json = { "py/object": "pybamm.meshes.meshes.Mesh", - "py/id": mock.ANY, + "py/id": mocker.ANY, "submesh_pts": {"negative particle": {"r": 20}}, "base_domains": ["negative particle"], "sub_meshes": { "negative particle": { "py/object": "pybamm.meshes.one_dimensional_submeshes.Uniform1DSubMesh", - "py/id": mock.ANY, + "py/id": mocker.ANY, "edges": [ 0.0, 0.05, @@ -86,7 +85,7 @@ def mesh_var_dict(): return mesh, mesh_json -class TestSerialiseModels(unittest.TestCase): +class TestSerialiseModels: def test_user_defined_model_recreaction(self): # Start with a base model model = pybamm.BaseModel() @@ -146,26 +145,26 @@ def test_user_defined_model_recreaction(self): os.remove("heat_equation.json") -class TestSerialise(unittest.TestCase): +class TestSerialise: # test the symbol encoder - def test_symbol_encoder_symbol(self): + def test_symbol_encoder_symbol(self, mocker): """test basic symbol encoder with & without children""" # without children - a, a_dict = scalar_var_dict() + a, a_dict = scalar_var_dict(mocker) a_ser_json = Serialise._SymbolEncoder().default(a) - self.assertEqual(a_ser_json, a_dict) + assert a_ser_json == a_dict # with children add = pybamm.Addition(2, 4) add_json = { - "py/id": mock.ANY, + "py/id": mocker.ANY, "py/object": "pybamm.expression_tree.binary_operators.Addition", "name": "+", - "id": mock.ANY, + "id": mocker.ANY, "domains": { "primary": [], "secondary": [], @@ -174,18 +173,18 @@ def test_symbol_encoder_symbol(self): }, "children": [ { - "py/id": mock.ANY, + "py/id": mocker.ANY, "py/object": "pybamm.expression_tree.scalar.Scalar", "name": "2.0", - "id": mock.ANY, + "id": mocker.ANY, "value": 2.0, "children": [], }, { - "py/id": mock.ANY, + "py/id": mocker.ANY, "py/object": "pybamm.expression_tree.scalar.Scalar", "name": "4.0", - "id": mock.ANY, + "id": mocker.ANY, "value": 4.0, "children": [], }, @@ -194,32 +193,32 @@ def test_symbol_encoder_symbol(self): add_ser_json = Serialise._SymbolEncoder().default(add) - self.assertEqual(add_ser_json, add_json) + assert add_ser_json == add_json - def test_symbol_encoder_explicitTimeIntegral(self): + def test_symbol_encoder_explicit_time_integral(self, mocker): """test symbol encoder with initial conditions""" expr = pybamm.ExplicitTimeIntegral(pybamm.Scalar(5), pybamm.Scalar(1)) expr_json = { "py/object": "pybamm.expression_tree.unary_operators.ExplicitTimeIntegral", - "py/id": mock.ANY, + "py/id": mocker.ANY, "name": "explicit time integral", - "id": mock.ANY, + "id": mocker.ANY, "children": [ { "py/object": "pybamm.expression_tree.scalar.Scalar", - "py/id": mock.ANY, + "py/id": mocker.ANY, "name": "5.0", - "id": mock.ANY, + "id": mocker.ANY, "value": 5.0, "children": [], } ], "initial_condition": { "py/object": "pybamm.expression_tree.scalar.Scalar", - "py/id": mock.ANY, + "py/id": mocker.ANY, "name": "1.0", - "id": mock.ANY, + "id": mocker.ANY, "value": 1.0, "children": [], }, @@ -227,9 +226,9 @@ def test_symbol_encoder_explicitTimeIntegral(self): expr_ser_json = Serialise._SymbolEncoder().default(expr) - self.assertEqual(expr_json, expr_ser_json) + assert expr_json == expr_ser_json - def test_symbol_encoder_event(self): + def test_symbol_encoder_event(self, mocker): """test symbol encoder with event""" expression = pybamm.Scalar(1) @@ -237,32 +236,32 @@ def test_symbol_encoder_event(self): event_json = { "py/object": "pybamm.models.event.Event", - "py/id": mock.ANY, + "py/id": mocker.ANY, "name": "my event", "event_type": ["EventType.TERMINATION", 0], "expression": { "py/object": "pybamm.expression_tree.scalar.Scalar", - "py/id": mock.ANY, + "py/id": mocker.ANY, "name": "1.0", - "id": mock.ANY, + "id": mocker.ANY, "value": 1.0, "children": [], }, } event_ser_json = Serialise._SymbolEncoder().default(event) - self.assertEqual(event_ser_json, event_json) + assert event_ser_json == event_json # test the mesh encoder - def test_mesh_encoder(self): - mesh, mesh_json = mesh_var_dict() + def test_mesh_encoder(self, mocker): + mesh, mesh_json = mesh_var_dict(mocker) # serialise mesh mesh_ser_json = Serialise._MeshEncoder().default(mesh) - self.assertEqual(mesh_ser_json, mesh_json) + assert mesh_ser_json == mesh_json - def test_deconstruct_pybamm_dicts(self): + def test_deconstruct_pybamm_dicts(self, mocker): """tests serialisation of dictionaries with pybamm classes as keys""" x = pybamm.SpatialVariable("x", "negative electrode") @@ -273,9 +272,9 @@ def test_deconstruct_pybamm_dicts(self): "rod": { "symbol_x": { "py/object": "pybamm.expression_tree.independent_variable.SpatialVariable", - "py/id": mock.ANY, + "py/id": mocker.ANY, "name": "x", - "id": mock.ANY, + "id": mocker.ANY, "domains": { "primary": ["negative electrode"], "secondary": [], @@ -288,40 +287,40 @@ def test_deconstruct_pybamm_dicts(self): } } - self.assertEqual(Serialise()._deconstruct_pybamm_dicts(test_dict), ser_dict) + assert Serialise()._deconstruct_pybamm_dicts(test_dict) == ser_dict - def test_get_pybamm_class(self): + def test_get_pybamm_class(self, mocker): # symbol - _, scalar_dict = scalar_var_dict() + _, scalar_dict = scalar_var_dict(mocker) scalar_class = Serialise()._get_pybamm_class(scalar_dict) - self.assertIsInstance(scalar_class, pybamm.Scalar) + assert isinstance(scalar_class, pybamm.Scalar) # mesh - _, mesh_dict = mesh_var_dict() + _, mesh_dict = mesh_var_dict(mocker) mesh_class = Serialise()._get_pybamm_class(mesh_dict) - self.assertIsInstance(mesh_class, pybamm.Mesh) + assert isinstance(mesh_class, pybamm.Mesh) - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): unrecognised_symbol = { - "py/id": mock.ANY, + "py/id": mocker.ANY, "py/object": "pybamm.expression_tree.scalar.Scale", "name": "5.0", - "id": mock.ANY, + "id": mocker.ANY, "value": 5.0, "children": [], } Serialise()._get_pybamm_class(unrecognised_symbol) - def test_reconstruct_symbol(self): - scalar, scalar_dict = scalar_var_dict() + def test_reconstruct_symbol(self, mocker): + scalar, scalar_dict = scalar_var_dict(mocker) new_scalar = Serialise()._reconstruct_symbol(scalar_dict) - self.assertEqual(new_scalar, scalar) + assert new_scalar == scalar def test_reconstruct_expression_tree(self): y = pybamm.StateVector(slice(0, 1)) @@ -395,10 +394,10 @@ def test_reconstruct_expression_tree(self): new_equation = Serialise()._reconstruct_expression_tree(equation_json) - self.assertEqual(new_equation, equation) + assert new_equation == equation - def test_reconstruct_mesh(self): - mesh, mesh_dict = mesh_var_dict() + def test_reconstruct_mesh(self, mocker): + mesh, mesh_dict = mesh_var_dict(mocker) new_mesh = Serialise()._reconstruct_mesh(mesh_dict) @@ -410,12 +409,12 @@ def test_reconstruct_mesh(self): ) # reconstructed meshes are only used for plotting, geometry not reconstructed. - with self.assertRaisesRegex( - AttributeError, "'Mesh' object has no attribute '_geometry'" + with pytest.raises( + AttributeError, match="'Mesh' object has no attribute '_geometry'" ): - self.assertEqual(new_mesh.geometry, mesh.geometry) + assert new_mesh.geometry == mesh.geometry - def test_reconstruct_pybamm_dict(self): + def test_reconstruct_pybamm_dict(self, mocker): x = pybamm.SpatialVariable("x", "negative electrode") test_dict = {"rod": {x: {"min": 0.0, "max": 2.0}}} @@ -424,9 +423,9 @@ def test_reconstruct_pybamm_dict(self): "rod": { "symbol_x": { "py/object": "pybamm.expression_tree.independent_variable.SpatialVariable", - "py/id": mock.ANY, + "py/id": mocker.ANY, "name": "x", - "id": mock.ANY, + "id": mocker.ANY, "domains": { "primary": ["negative electrode"], "secondary": [], @@ -441,13 +440,13 @@ def test_reconstruct_pybamm_dict(self): new_dict = Serialise()._reconstruct_pybamm_dict(ser_dict) - self.assertEqual(new_dict, test_dict) + assert new_dict == test_dict # test recreation if not passed a dict test_list = ["left", "right"] new_list = Serialise()._reconstruct_pybamm_dict(test_list) - self.assertEqual(test_list, new_list) + assert test_list == new_list def test_convert_options(self): options_dict = { @@ -462,7 +461,7 @@ def test_convert_options(self): "open-circuit potential": (("single", "current sigmoid"), "single"), } - self.assertEqual(Serialise()._convert_options(options_dict), options_result) + assert Serialise()._convert_options(options_dict) == options_result def test_save_load_model(self): model = pybamm.lithium_ion.SPM(name="test_spm") @@ -473,9 +472,9 @@ def test_save_load_model(self): mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) # test error if not discretised - with self.assertRaisesRegex( + with pytest.raises( NotImplementedError, - "PyBaMM can only serialise a discretised, ready-to-solve model", + match="PyBaMM can only serialise a discretised, ready-to-solve model", ): Serialise().save_model(model, filename="test_model") @@ -484,12 +483,12 @@ def test_save_load_model(self): # default save Serialise().save_model(model, filename="test_model") - self.assertTrue(os.path.exists("test_model.json")) + assert os.path.exists("test_model.json") # default save where filename isn't provided Serialise().save_model(model) filename = "test_spm_" + datetime.now().strftime("%Y_%m_%d-%p%I_%M") + ".json" - self.assertTrue(os.path.exists(filename)) + assert os.path.exists(filename) os.remove(filename) # default load @@ -500,9 +499,9 @@ def test_save_load_model(self): new_solution = new_solver.solve(new_model, [0, 3600]) # check an error is raised when plotting the solution - with self.assertRaisesRegex( + with pytest.raises( AttributeError, - "No variables to plot", + match="No variables to plot", ): new_solution.plot() @@ -519,7 +518,7 @@ def test_save_load_model(self): with open("test_model.json", "w") as f: json.dump(model_data, f) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): Serialise().load_model("test_model.json") os.remove("test_model.json") @@ -534,9 +533,9 @@ def test_save_experiment_model_error(self): sim = pybamm.Simulation(model, experiment=experiment) sim.solve() - with self.assertRaisesRegex( + with pytest.raises( NotImplementedError, - "Serialising models coupled to experiments is not yet supported.", + match="Serialising models coupled to experiments is not yet supported.", ): sim.save_model("spm_experiment", mesh=False, variables=False) @@ -591,13 +590,3 @@ def test_serialised_model_plotting(self): # check dynamic plot loads new_solution.plot(show_plot=False) - - -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 744ea2457c..becd70cbe4 100644 --- a/tests/unit/test_simulation.py +++ b/tests/unit/test_simulation.py @@ -3,8 +3,6 @@ import pandas as pd import os -import sys -import unittest import uuid import pytest from tempfile import TemporaryDirectory @@ -12,7 +10,7 @@ from tests import no_internet_connection -class TestSimulation(unittest.TestCase): +class TestSimulation: def test_simple_model(self): model = pybamm.BaseModel() v = pybamm.Variable("v") @@ -27,49 +25,49 @@ def test_basic_ops(self): sim = pybamm.Simulation(model) # check that the model is unprocessed - self.assertEqual(sim._mesh, None) - self.assertEqual(sim._disc, None) + assert sim._mesh is None + assert sim._disc is None V = sim.model.variables["Voltage [V]"] - self.assertTrue(V.has_symbol_of_classes(pybamm.Parameter)) - self.assertFalse(V.has_symbol_of_classes(pybamm.Matrix)) + assert V.has_symbol_of_classes(pybamm.Parameter) + assert not V.has_symbol_of_classes(pybamm.Matrix) sim.set_parameters() - self.assertEqual(sim._mesh, None) - self.assertEqual(sim._disc, None) + assert sim._mesh is None + assert sim._disc is None V = sim.model_with_set_params.variables["Voltage [V]"] - self.assertFalse(V.has_symbol_of_classes(pybamm.Parameter)) - self.assertFalse(V.has_symbol_of_classes(pybamm.Matrix)) + assert not V.has_symbol_of_classes(pybamm.Parameter) + assert not V.has_symbol_of_classes(pybamm.Matrix) # Make sure model is unchanged - self.assertNotEqual(sim.model, model) + assert sim.model != model V = model.variables["Voltage [V]"] - self.assertTrue(V.has_symbol_of_classes(pybamm.Parameter)) - self.assertFalse(V.has_symbol_of_classes(pybamm.Matrix)) + assert V.has_symbol_of_classes(pybamm.Parameter) + assert not V.has_symbol_of_classes(pybamm.Matrix) - self.assertEqual(sim.submesh_types, model.default_submesh_types) - self.assertEqual(sim.var_pts, model.default_var_pts) - self.assertIsNone(sim.mesh) + assert sim.submesh_types == model.default_submesh_types + assert sim.var_pts == model.default_var_pts + assert sim.mesh is None for key in sim.spatial_methods.keys(): - self.assertEqual( - sim.spatial_methods[key].__class__, - model.default_spatial_methods[key].__class__, + assert ( + sim.spatial_methods[key].__class__ + == model.default_spatial_methods[key].__class__ ) sim.build() - self.assertFalse(sim._mesh is None) - self.assertFalse(sim._disc is None) + assert sim._mesh is not None + assert sim._disc is not None V = sim.built_model.variables["Voltage [V]"] - self.assertFalse(V.has_symbol_of_classes(pybamm.Parameter)) - self.assertTrue(V.has_symbol_of_classes(pybamm.Matrix)) + assert not V.has_symbol_of_classes(pybamm.Parameter) + assert V.has_symbol_of_classes(pybamm.Matrix) def test_solve(self): sim = pybamm.Simulation(pybamm.lithium_ion.SPM()) sim.solve([0, 600]) - self.assertFalse(sim._solution is None) + assert sim._solution is not None for val in list(sim.built_model.rhs.values()): - self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) + assert not val.has_symbol_of_classes(pybamm.Parameter) # skip test for scalar variables (e.g. discharge capacity) if val.size > 1: - self.assertTrue(val.has_symbol_of_classes(pybamm.Matrix)) + assert val.has_symbol_of_classes(pybamm.Matrix) # test solve without check sim = pybamm.Simulation( @@ -77,15 +75,15 @@ def test_solve(self): ) sol = sim.solve(t_eval=[0, 600]) for val in list(sim.built_model.rhs.values()): - self.assertFalse(val.has_symbol_of_classes(pybamm.Parameter)) + assert not val.has_symbol_of_classes(pybamm.Parameter) # skip test for scalar variables (e.g. discharge capacity) if val.size > 1: - self.assertTrue(val.has_symbol_of_classes(pybamm.Matrix)) + assert val.has_symbol_of_classes(pybamm.Matrix) # Test options that are only available when simulating an experiment - with self.assertRaisesRegex(ValueError, "save_at_cycles"): + with pytest.raises(ValueError, match="save_at_cycles"): sim.solve(save_at_cycles=2) - with self.assertRaisesRegex(ValueError, "starting_solution"): + with pytest.raises(ValueError, match="starting_solution"): sim.solve(starting_solution=sol) def test_solve_remove_independent_variables_from_rhs(self): @@ -157,8 +155,8 @@ def test_set_crate(self): model = pybamm.lithium_ion.SPM() current_1C = model.default_parameter_values["Current function [A]"] sim = pybamm.Simulation(model, C_rate=2) - self.assertEqual(sim.parameter_values["Current function [A]"], 2 * current_1C) - self.assertEqual(sim.C_rate, 2) + assert sim.parameter_values["Current function [A]"] == 2 * current_1C + assert sim.C_rate == 2 def test_step(self): dt = 0.001 @@ -166,24 +164,24 @@ def test_step(self): sim = pybamm.Simulation(model) sim.step(dt) # 1 step stores first two points - self.assertEqual(sim.solution.y.full()[0, :].size, 2) + assert sim.solution.y.full()[0, :].size == 2 np.testing.assert_array_almost_equal(sim.solution.t, np.array([0, dt])) saved_sol = sim.solution sim.step(dt) # automatically append the next step - self.assertEqual(sim.solution.y.full()[0, :].size, 4) + assert sim.solution.y.full()[0, :].size == 4 np.testing.assert_array_almost_equal( sim.solution.t, np.array([0, dt, dt + 1e-9, 2 * dt]) ) sim.step(dt, save=False) # now only store the two end step points - self.assertEqual(sim.solution.y.full()[0, :].size, 2) + assert sim.solution.y.full()[0, :].size == 2 np.testing.assert_array_almost_equal( sim.solution.t, np.array([2 * dt + 1e-9, 3 * dt]) ) # Start from saved solution sim.step(dt, starting_solution=saved_sol) - self.assertEqual(sim.solution.y.full()[0, :].size, 4) + assert sim.solution.y.full()[0, :].size == 4 np.testing.assert_array_almost_equal( sim.solution.t, np.array([0, dt, dt + 1e-9, 2 * dt]) ) @@ -197,15 +195,15 @@ def test_solve_with_initial_soc(self): param = model.default_parameter_values sim = pybamm.Simulation(model, parameter_values=param) sim.solve(t_eval=[0, 600], initial_soc=1) - self.assertEqual(sim._built_initial_soc, 1) + assert sim._built_initial_soc == 1 sim.solve(t_eval=[0, 600], initial_soc=0.5) - self.assertEqual(sim._built_initial_soc, 0.5) + assert sim._built_initial_soc == 0.5 exp = pybamm.Experiment( [pybamm.step.string("Discharge at 1C until 3.6V", period="1 minute")] ) sim = pybamm.Simulation(model, parameter_values=param, experiment=exp) sim.solve(initial_soc=0.8) - self.assertEqual(sim._built_initial_soc, 0.8) + assert sim._built_initial_soc == 0.8 # test with drive cycle data_loader = pybamm.DataLoader() @@ -220,12 +218,12 @@ def test_solve_with_initial_soc(self): param["Current function [A]"] = current_interpolant sim = pybamm.Simulation(model, parameter_values=param) sim.solve(initial_soc=0.8) - self.assertEqual(sim._built_initial_soc, 0.8) + assert sim._built_initial_soc == 0.8 # Test that build works with initial_soc sim = pybamm.Simulation(model, parameter_values=param) sim.build(initial_soc=0.5) - self.assertEqual(sim._built_initial_soc, 0.5) + assert sim._built_initial_soc == 0.5 # Test that initial soc works with a relevant input parameter model = pybamm.lithium_ion.DFN() @@ -236,7 +234,7 @@ def test_solve_with_initial_soc(self): ) sim = pybamm.Simulation(model, parameter_values=param) sim.solve(t_eval=[0, 1], initial_soc=0.8, inputs={"eps_p": og_eps_p}) - self.assertEqual(sim._built_initial_soc, 0.8) + assert sim._built_initial_soc == 0.8 # test having an input parameter in the ocv function model = pybamm.lithium_ion.SPM() @@ -264,14 +262,14 @@ def ocv_with_parameter(sto): model = pybamm.lithium_ion.DFN(options) sim = pybamm.Simulation(model) sim.solve([0, 1], initial_soc=0.9) - self.assertEqual(sim._built_initial_soc, 0.9) + assert sim._built_initial_soc == 0.9 # Test whether initial_soc works with half cell (build) options = {"working electrode": "positive"} model = pybamm.lithium_ion.DFN(options) sim = pybamm.Simulation(model) sim.build(initial_soc=0.9) - self.assertEqual(sim._built_initial_soc, 0.9) + assert sim._built_initial_soc == 0.9 # Test whether initial_soc works with half cell when it is a voltage model = pybamm.lithium_ion.SPM({"working electrode": "positive"}) @@ -284,14 +282,14 @@ def ocv_with_parameter(sto): sim = pybamm.Simulation(model, parameter_values=parameter_values) sol = sim.solve([0, 1], initial_soc=f"{ucv} V") voltage = sol["Terminal voltage [V]"].entries - self.assertAlmostEqual(voltage[0], ucv, places=5) + assert voltage[0] == pytest.approx(ucv, abs=1e-05) # test with MSMR model = pybamm.lithium_ion.MSMR({"number of MSMR reactions": ("6", "4")}) param = pybamm.ParameterValues("MSMR_Example") sim = pybamm.Simulation(model, parameter_values=param) sim.build(initial_soc=0.5) - self.assertEqual(sim._built_initial_soc, 0.5) + assert sim._built_initial_soc == 0.5 def test_solve_with_initial_soc_with_input_param_in_ocv(self): # test having an input parameter in the ocv function @@ -314,7 +312,18 @@ def ocv_with_parameter(sto): model, parameter_values=parameter_values, experiment=experiment ) sim.solve([0, 3600], inputs={"a": 1}, initial_soc=0.8) - self.assertEqual(sim._built_initial_soc, 0.8) + assert sim._built_initial_soc == 0.8 + + def test_restricted_input_params(self): + model = pybamm.lithium_ion.SPM() + parameter_values = model.default_parameter_values + parameter_values.update({"Initial temperature [K]": "[input]"}) + experiment = pybamm.Experiment(["Discharge at 1C until 2.5 V"]) + sim = pybamm.Simulation( + model, parameter_values=parameter_values, experiment=experiment + ) + with pytest.raises(pybamm.ModelError, match="Initial temperature"): + sim.solve([0, 3600]) def test_esoh_with_input_param(self): # Test that initial soc works with a relevant input parameter @@ -326,7 +335,7 @@ def test_esoh_with_input_param(self): ) sim = pybamm.Simulation(model, parameter_values=param) sim.solve(t_eval=[0, 1], initial_soc=0.8, inputs={"eps_p": original_eps_p}) - self.assertEqual(sim._built_initial_soc, 0.8) + assert sim._built_initial_soc == 0.8 def test_solve_with_inputs(self): model = pybamm.lithium_ion.SPM() @@ -338,6 +347,38 @@ def test_solve_with_inputs(self): sim.solution.all_inputs[0]["Current function [A]"], 1 ) + def test_solve_with_sensitivities(self): + model = pybamm.lithium_ion.SPM() + param = model.default_parameter_values + param.update({"Current function [A]": "[input]"}) + sim = pybamm.Simulation(model, parameter_values=param) + h = 1e-6 + sol1 = sim.solve( + t_eval=[0, 600], + inputs={"Current function [A]": 1}, + calculate_sensitivities=True, + ) + + # check that the sensitivities are stored + assert "Current function [A]" in sol1.sensitivities + + sol2 = sim.solve(t_eval=[0, 600], inputs={"Current function [A]": 1 + h}) + + # check that the sensitivities are not stored + assert "Current function [A]" not in sol2.sensitivities + + # check that the sensitivities are roughly correct + np.testing.assert_array_almost_equal( + sol1["Terminal voltage [V]"].entries + + h + * sol1["Terminal voltage [V]"] + .sensitivities["Current function [A]"] + .full() + .flatten(), + sol2["Terminal voltage [V]"].entries, + decimal=5, + ) + def test_step_with_inputs(self): dt = 0.001 model = pybamm.lithium_ion.SPM() @@ -347,17 +388,17 @@ def test_step_with_inputs(self): sim.step( dt, inputs={"Current function [A]": 1} ) # 1 step stores first two points - self.assertEqual(sim.solution.t.size, 2) - self.assertEqual(sim.solution.y.full()[0, :].size, 2) - self.assertEqual(sim.solution.t[0], 0) - self.assertEqual(sim.solution.t[1], dt) + assert sim.solution.t.size == 2 + assert sim.solution.y.full()[0, :].size == 2 + assert sim.solution.t[0] == 0 + assert sim.solution.t[1] == dt np.testing.assert_array_equal( sim.solution.all_inputs[0]["Current function [A]"], 1 ) sim.step( dt, inputs={"Current function [A]": 2} ) # automatically append the next step - self.assertEqual(sim.solution.y.full()[0, :].size, 4) + assert sim.solution.y.full()[0, :].size == 4 np.testing.assert_array_almost_equal( sim.solution.t, np.array([0, dt, dt + 1e-9, 2 * dt]) ) @@ -399,13 +440,13 @@ def oscillating(t): def f(t, x=x): return x + t - with self.assertRaises(ValueError): + with pytest.raises(ValueError): operating_mode(f) def g(t, y): return t - with self.assertRaises(TypeError): + with pytest.raises(TypeError): operating_mode(g) def test_save_load(self): @@ -418,13 +459,13 @@ def test_save_load(self): sim.save(test_name) sim_load = pybamm.load_sim(test_name) - self.assertEqual(sim.model.name, sim_load.model.name) + assert sim.model.name == sim_load.model.name # save after solving sim.solve([0, 600]) sim.save(test_name) sim_load = pybamm.load_sim(test_name) - self.assertEqual(sim.model.name, sim_load.model.name) + assert sim.model.name == sim_load.model.name # with python formats model.convert_to_format = None @@ -434,8 +475,9 @@ def test_save_load(self): model.convert_to_format = "python" sim = pybamm.Simulation(model) sim.solve([0, 600]) - with self.assertRaisesRegex( - NotImplementedError, "Cannot save simulation if model format is python" + with pytest.raises( + NotImplementedError, + match="Cannot save simulation if model format is python", ): sim.save(test_name) @@ -454,11 +496,11 @@ def test_load_param(self): os.remove(filename) raise excep - self.assertEqual( - "graphite_LGM50_electrolyte_exchange_current_density_Chen2020", - pkl_obj.parameter_values[ + assert ( + "graphite_LGM50_electrolyte_exchange_current_density_Chen2020" + == pkl_obj.parameter_values[ "Negative electrode exchange-current density [A.m-2]" - ].__name__, + ].__name__ ) os.remove(filename) @@ -474,7 +516,7 @@ def test_save_load_dae(self): sim.solve([0, 600]) sim.save(test_name) sim_load = pybamm.load_sim(test_name) - self.assertEqual(sim.model.name, sim_load.model.name) + assert sim.model.name == sim_load.model.name # with python format model.convert_to_format = None @@ -492,7 +534,7 @@ def test_save_load_dae(self): sim.solve([0, 600]) sim.save(test_name) sim_load = pybamm.load_sim(test_name) - self.assertEqual(sim.model.name, sim_load.model.name) + assert sim.model.name == sim_load.model.name def test_save_load_model(self): model = pybamm.lead_acid.LOQS({"surface form": "algebraic"}) @@ -500,7 +542,7 @@ def test_save_load_model(self): sim = pybamm.Simulation(model) # test exception if not discretised - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): sim.save_model("sim_save") # save after solving @@ -510,7 +552,7 @@ def test_save_load_model(self): # load model saved_model = pybamm.load_model("sim_save.json") - self.assertEqual(model.options, saved_model.options) + assert model.options == saved_model.options os.remove("sim_save.json") @@ -518,7 +560,7 @@ def test_plot(self): sim = pybamm.Simulation(pybamm.lithium_ion.SPM()) # test exception if not solved - with self.assertRaises(ValueError): + with pytest.raises(ValueError): sim.plot() # now solve and plot @@ -529,8 +571,8 @@ def test_plot(self): def test_create_gif(self): with TemporaryDirectory() as dir_name: sim = pybamm.Simulation(pybamm.lithium_ion.SPM()) - with self.assertRaisesRegex( - ValueError, "The simulation has not been solved yet." + with pytest.raises( + ValueError, match="The simulation has not been solved yet." ): sim.create_gif() sim.solve(t_eval=[0, 10]) @@ -577,13 +619,13 @@ def test_drive_cycle_interpolant(self): # check warning raised if the largest gap in t_eval is bigger than the # smallest gap in the data - with self.assertWarns(pybamm.SolverWarning): + with pytest.warns(pybamm.SolverWarning): sim.solve(t_eval=np.linspace(0, 10, 3)) # check warning raised if t_eval doesnt contain 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) - with self.assertWarns(pybamm.SolverWarning): + with pytest.warns(pybamm.SolverWarning): sim.solve(t_eval=np.linspace(0, time_data[-1], 800)) def test_discontinuous_current(self): @@ -604,20 +646,20 @@ def car_current(t): ) sim.solve([0, 3600]) current = sim.solution["Current [A]"] - self.assertEqual(current(0), 1) - self.assertEqual(current(1500), -0.5) - self.assertEqual(current(3000), 0.5) + assert current(0) == 1 + assert current(1500) == -0.5 + assert current(3000) == 0.5 def test_t_eval(self): model = pybamm.lithium_ion.SPM() sim = pybamm.Simulation(model) # test no t_eval - with self.assertRaisesRegex(pybamm.SolverError, "'t_eval' must be provided"): + with pytest.raises(pybamm.SolverError, match="'t_eval' must be provided"): sim.solve() # test t_eval list of length != 2 - with self.assertRaisesRegex(pybamm.SolverError, "'t_eval' can be provided"): + with pytest.raises(pybamm.SolverError, match="'t_eval' can be provided"): sim.solve(t_eval=[0, 1, 2]) # tets list gets turned into np.linspace(t0, tf, 100) @@ -633,11 +675,3 @@ def test_battery_model_with_input_height(self): inputs = {"Electrode height [m]": 0.2} sim = pybamm.Simulation(model=model, parameter_values=parameter_values) sim.solve(t_eval=t_eval, inputs=inputs) - - -if __name__ == "__main__": - print("Add -v for more debug output") - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/unit/test_solvers/test_algebraic_solver.py b/tests/unit/test_solvers/test_algebraic_solver.py index 6e8b3a3d80..89ca7d750c 100644 --- a/tests/unit/test_solvers/test_algebraic_solver.py +++ b/tests/unit/test_solvers/test_algebraic_solver.py @@ -3,24 +3,24 @@ # import pybamm -import unittest +import pytest import numpy as np from tests import get_discretisation_for_testing -class TestAlgebraicSolver(unittest.TestCase): +class TestAlgebraicSolver: def test_algebraic_solver_init(self): solver = pybamm.AlgebraicSolver( method="hybr", tol=1e-4, extra_options={"maxfev": 100} ) - self.assertEqual(solver.method, "hybr") - self.assertEqual(solver.extra_options, {"maxfev": 100}) - self.assertEqual(solver.tol, 1e-4) + assert solver.method == "hybr" + assert solver.extra_options == {"maxfev": 100} + assert solver.tol == 1e-4 solver.method = "krylov" - self.assertEqual(solver.method, "krylov") + assert solver.method == "krylov" solver.tol = 1e-5 - self.assertEqual(solver.tol, 1e-5) + assert solver.tol == 1e-5 def test_wrong_solver(self): # Create model @@ -31,9 +31,9 @@ def test_wrong_solver(self): # test errors solver = pybamm.AlgebraicSolver() - with self.assertRaisesRegex( + with pytest.raises( pybamm.SolverError, - "Cannot use algebraic solver to solve model with time derivatives", + match="Cannot use algebraic solver to solve model with time derivatives", ): solver.solve(model) @@ -61,7 +61,7 @@ def algebraic_eval(self, t, y, inputs): # Relax options and see worse results solver = pybamm.AlgebraicSolver(extra_options={"ftol": 1}) solution = solver._integrate(model, np.array([0])) - self.assertNotEqual(solution.y, -2) + assert solution.y != -2 def test_root_find_fail(self): class Model(pybamm.BaseModel): @@ -81,15 +81,16 @@ def algebraic_eval(self, t, y, inputs): model = Model() solver = pybamm.AlgebraicSolver(method="hybr") - with self.assertRaisesRegex( + with pytest.raises( pybamm.SolverError, - "Could not find acceptable solution: The iteration is not making", + match="Could not find acceptable solution: The iteration is not making", ): solver._integrate(model, np.array([0])) solver = pybamm.AlgebraicSolver() - with self.assertRaisesRegex( - pybamm.SolverError, "Could not find acceptable solution: solver terminated" + with pytest.raises( + pybamm.SolverError, + match="Could not find acceptable solution: solver terminated", ): solver._integrate(model, np.array([0])) @@ -303,13 +304,3 @@ def test_solve_with_input(self): solver = pybamm.AlgebraicSolver() solution = solver.solve(model, np.linspace(0, 1, 10), inputs={"value": 7}) np.testing.assert_array_equal(solution.y, -7) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index 9a1e87acec..6753513e72 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -2,39 +2,38 @@ # Tests for the Base Solver class # +import pytest import casadi import pybamm import numpy as np from scipy.sparse import csr_matrix -import unittest - -class TestBaseSolver(unittest.TestCase): +class TestBaseSolver: def test_base_solver_init(self): solver = pybamm.BaseSolver(rtol=1e-2, atol=1e-4) - self.assertEqual(solver.rtol, 1e-2) - self.assertEqual(solver.atol, 1e-4) + assert solver.rtol == 1e-2 + assert solver.atol == 1e-4 solver.rtol = 1e-5 - self.assertEqual(solver.rtol, 1e-5) + assert solver.rtol == 1e-5 solver.rtol = 1e-7 - self.assertEqual(solver.rtol, 1e-7) + assert solver.rtol == 1e-7 def test_root_method_init(self): solver = pybamm.BaseSolver(root_method="casadi") - self.assertIsInstance(solver.root_method, pybamm.CasadiAlgebraicSolver) + assert isinstance(solver.root_method, pybamm.CasadiAlgebraicSolver) solver = pybamm.BaseSolver(root_method="lm") - self.assertIsInstance(solver.root_method, pybamm.AlgebraicSolver) - self.assertEqual(solver.root_method.method, "lm") + assert isinstance(solver.root_method, pybamm.AlgebraicSolver) + assert solver.root_method.method == "lm" root_solver = pybamm.AlgebraicSolver() solver = pybamm.BaseSolver(root_method=root_solver) - self.assertEqual(solver.root_method, root_solver) + assert solver.root_method == root_solver - with self.assertRaisesRegex( - pybamm.SolverError, "Root method must be an algebraic solver" + with pytest.raises( + pybamm.SolverError, match="Root method must be an algebraic solver" ): pybamm.BaseSolver(root_method=pybamm.ScipySolver()) @@ -42,9 +41,9 @@ def test_step_or_solve_empty_model(self): model = pybamm.BaseModel() solver = pybamm.BaseSolver() error = "Cannot simulate an empty model" - with self.assertRaisesRegex(pybamm.ModelError, error): + with pytest.raises(pybamm.ModelError, match=error): solver.step(None, model, None) - with self.assertRaisesRegex(pybamm.ModelError, error): + with pytest.raises(pybamm.ModelError, match=error): solver.solve(model, None) def test_t_eval_none(self): @@ -56,7 +55,7 @@ def test_t_eval_none(self): disc.process_model(model) solver = pybamm.BaseSolver() - with self.assertRaisesRegex(ValueError, "t_eval cannot be None"): + with pytest.raises(ValueError, match="t_eval cannot be None"): solver.solve(model, None) def test_nonmonotonic_teval(self): @@ -64,29 +63,29 @@ def test_nonmonotonic_teval(self): model = pybamm.BaseModel() a = pybamm.Scalar(0) model.rhs = {a: a} - with self.assertRaisesRegex( - pybamm.SolverError, "t_eval must increase monotonically" + with pytest.raises( + pybamm.SolverError, match="t_eval must increase monotonically" ): solver.solve(model, np.array([1, 2, 3, 2])) # Check stepping with step size too small dt = -1e-9 - with self.assertRaisesRegex(pybamm.SolverError, "Step time must be >0"): + with pytest.raises(pybamm.SolverError, match="Step time must be >0"): solver.step(None, model, dt) # Checking if array t_eval lies within range dt = 2 t_eval = np.array([0, 1]) - with self.assertRaisesRegex( + with pytest.raises( pybamm.SolverError, - "Elements inside array t_eval must lie in the closed interval 0 to dt", + match="Elements inside array t_eval must lie in the closed interval 0 to dt", ): solver.step(None, model, dt, t_eval=t_eval) t_eval = np.array([1, dt]) - with self.assertRaisesRegex( + with pytest.raises( pybamm.SolverError, - "Elements inside array t_eval must lie in the closed interval 0 to dt", + match="Elements inside array t_eval must lie in the closed interval 0 to dt", ): solver.step(None, model, dt, t_eval=t_eval) @@ -96,8 +95,8 @@ def test_solution_time_length_fail(self): model.variables = {"v": v} solver = pybamm.DummySolver() t_eval = np.array([0]) - with self.assertRaisesRegex( - pybamm.SolverError, "Solution time vector has length 1" + with pytest.raises( + pybamm.SolverError, match="Solution time vector has length 1" ): solver.solve(model, t_eval) @@ -107,9 +106,7 @@ def test_block_symbolic_inputs(self): a = pybamm.Variable("a") p = pybamm.InputParameter("p") model.rhs = {a: a * p} - with self.assertRaisesRegex( - pybamm.SolverError, "No value provided for input 'p'" - ): + with pytest.raises(pybamm.SolverError, match="No value provided for input 'p'"): solver.solve(model, np.array([1, 2, 3])) def test_ode_solver_fail_with_dae(self): @@ -118,7 +115,7 @@ def test_ode_solver_fail_with_dae(self): model.algebraic = {a: a} model.concatenated_initial_conditions = pybamm.Scalar(0) solver = pybamm.ScipySolver() - with self.assertRaisesRegex(pybamm.SolverError, "Cannot use ODE solver"): + with pytest.raises(pybamm.SolverError, match="Cannot use ODE solver"): solver.set_up(model) def test_find_consistent_initialization(self): @@ -231,20 +228,22 @@ def algebraic_eval(self, t, y, inputs): solver = pybamm.BaseSolver(root_method="hybr") - with self.assertRaisesRegex( + with pytest.raises( pybamm.SolverError, - "Could not find acceptable solution: The iteration is not making", + match="Could not find acceptable solution: The iteration is not making", ): solver.calculate_consistent_state(Model()) solver = pybamm.BaseSolver(root_method="lm") - with self.assertRaisesRegex( - pybamm.SolverError, "Could not find acceptable solution: solver terminated" + with pytest.raises( + pybamm.SolverError, + match="Could not find acceptable solution: solver terminated", ): solver.calculate_consistent_state(Model()) # with casadi solver = pybamm.BaseSolver(root_method="casadi") - with self.assertRaisesRegex( - pybamm.SolverError, "Could not find acceptable solution: Error in Function" + with pytest.raises( + pybamm.SolverError, + match="Could not find acceptable solution: Error in Function", ): solver.calculate_consistent_state(Model()) @@ -256,9 +255,9 @@ def test_discretise_model(self): model.initial_conditions = {v: 1} solver = pybamm.BaseSolver() - self.assertFalse(model.is_discretised) + assert not model.is_discretised solver.set_up(model, {}) - self.assertTrue(model.is_discretised) + assert model.is_discretised # 1D model cannot be automatically discretised model = pybamm.BaseModel() @@ -266,8 +265,8 @@ def test_discretise_model(self): model.rhs = {v: -1} model.initial_conditions = {v: 1} - with self.assertRaisesRegex( - pybamm.DiscretisationError, "Cannot automatically discretise model" + with pytest.raises( + pybamm.DiscretisationError, match="Cannot automatically discretise model" ): solver.set_up(model, {}) @@ -285,7 +284,7 @@ def test_convert_to_casadi_format(self): solver = pybamm.BaseSolver(root_method="casadi") pybamm.set_logging_level("ERROR") solver.set_up(model, {}) - self.assertEqual(model.convert_to_format, "casadi") + assert model.convert_to_format == "casadi" pybamm.set_logging_level("WARNING") def test_inputs_step(self): @@ -301,7 +300,7 @@ def test_inputs_step(self): sol = solver.step( old_solution=None, model=model, dt=1.0, inputs={input_key: interp} ) - self.assertFalse(input_key in sol.all_inputs[0]) + assert input_key not in sol.all_inputs[0] def test_extrapolation_warnings(self): # Make sure the extrapolation warnings work @@ -326,10 +325,10 @@ def test_extrapolation_warnings(self): solver = pybamm.ScipySolver() solver.set_up(model) - with self.assertWarns(pybamm.SolverWarning): + with pytest.warns(pybamm.SolverWarning): solver.step(old_solution=None, model=model, dt=1.0) - with self.assertWarns(pybamm.SolverWarning): + with pytest.warns(pybamm.SolverWarning): solver.solve(model, t_eval=[0, 1]) def test_multiple_models_error(self): @@ -344,7 +343,7 @@ def test_multiple_models_error(self): solver = pybamm.ScipySolver() solver.solve(model, t_eval=[0, 1]) - with self.assertRaisesRegex(RuntimeError, "already been initialised"): + with pytest.raises(RuntimeError, match="already been initialised"): solver.solve(model2, t_eval=[0, 1]) def test_multiprocess_context(self): @@ -353,12 +352,16 @@ def test_multiprocess_context(self): assert solver.get_platform_context("Linux") == "fork" assert solver.get_platform_context("Darwin") == "fork" - @unittest.skipIf(not pybamm.has_idaklu(), "idaklu solver is not installed") + @pytest.mark.skipif( + not pybamm.has_idaklu(), reason="idaklu solver is not installed" + ) def test_sensitivities(self): def exact_diff_a(y, a, b): return np.array([[y[0] ** 2 + 2 * a], [y[0]]]) - @unittest.skipIf(not pybamm.has_jax(), "jax or jaxlib is not installed") + @pytest.mark.skipif( + not pybamm.has_jax(), reason="jax or jaxlib is not installed" + ) def exact_diff_b(y, a, b): return np.array([[y[0]], [0]]) @@ -397,13 +400,3 @@ def exact_diff_b(y, a, b): np.testing.assert_allclose( sens_b, exact_diff_b(y, inputs["a"], inputs["b"]) ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_solvers/test_casadi_algebraic_solver.py b/tests/unit/test_solvers/test_casadi_algebraic_solver.py index b85f4292b9..b2dc92d25b 100644 --- a/tests/unit/test_solvers/test_casadi_algebraic_solver.py +++ b/tests/unit/test_solvers/test_casadi_algebraic_solver.py @@ -1,18 +1,18 @@ import casadi import pybamm -import unittest +import pytest import numpy as np from scipy.optimize import least_squares import tests -class TestCasadiAlgebraicSolver(unittest.TestCase): +class TestCasadiAlgebraicSolver: def test_algebraic_solver_init(self): solver = pybamm.CasadiAlgebraicSolver(tol=1e-4) - self.assertEqual(solver.tol, 1e-4) + assert solver.tol == 1e-4 solver.tol = 1e-5 - self.assertEqual(solver.tol, 1e-5) + assert solver.tol == 1e-5 def test_simple_root_find(self): # Simple system: a single algebraic equation @@ -65,13 +65,15 @@ def algebraic_eval(self, t, y, inputs): model = Model() solver = pybamm.CasadiAlgebraicSolver() - with self.assertRaisesRegex( - pybamm.SolverError, "Could not find acceptable solution: Error in Function" + with pytest.raises( + pybamm.SolverError, + match="Could not find acceptable solution: Error in Function", ): solver._integrate(model, np.array([0]), {}) solver = pybamm.CasadiAlgebraicSolver(extra_options={"error_on_fail": False}) - with self.assertRaisesRegex( - pybamm.SolverError, "Could not find acceptable solution: solver terminated" + with pytest.raises( + pybamm.SolverError, + match="Could not find acceptable solution: solver terminated", ): solver._integrate(model, np.array([0]), {}) @@ -91,9 +93,9 @@ def algebraic_eval(self, t, y, inputs): return y**0.5 model = NaNModel() - with self.assertRaisesRegex( + with pytest.raises( pybamm.SolverError, - "Could not find acceptable solution: solver returned NaNs", + match="Could not find acceptable solution: solver returned NaNs", ): solver._integrate(model, np.array([0]), {}) @@ -170,7 +172,7 @@ def test_solve_with_input(self): np.testing.assert_array_equal(solution.y, -7) -class TestCasadiAlgebraicSolverSensitivity(unittest.TestCase): +class TestCasadiAlgebraicSolverSensitivity: def test_solve_with_symbolic_input(self): # Simple system: a single algebraic equation var = pybamm.Variable("var") @@ -344,13 +346,3 @@ def objective(x): # without Jacobian lsq_sol = least_squares(objective, [2, 2], method="lm") np.testing.assert_array_almost_equal(lsq_sol.x, [3, 3], decimal=3) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 76f5a6c9dc..e6f631392a 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -1,13 +1,13 @@ +import pytest import pybamm -import unittest import numpy as np from tests import get_mesh_for_testing, get_discretisation_for_testing from scipy.sparse import eye -class TestCasadiSolver(unittest.TestCase): +class TestCasadiSolver: def test_bad_mode(self): - with self.assertRaisesRegex(ValueError, "invalid mode"): + with pytest.raises(ValueError, match="invalid mode"): pybamm.CasadiSolver(mode="bad mode") def test_model_solver(self): @@ -102,7 +102,7 @@ def test_without_grid(self): # Safe mode, without grid (enforce events that won't be triggered) solver = pybamm.CasadiSolver(mode="safe without grid", rtol=1e-8, atol=1e-8) - with self.assertRaisesRegex(pybamm.SolverError, "Maximum number of decreased"): + with pytest.raises(pybamm.SolverError, match="Maximum number of decreased"): solver.solve(model, [0, 10]) def test_model_solver_python(self): @@ -143,9 +143,9 @@ def test_model_solver_failure(self): # Solution fails early but manages to take some steps so we return it anyway # Check that the final solution does indeed stop before t=20 t_eval = np.linspace(0, 20, 100) - with self.assertWarns(pybamm.SolverWarning): + with pytest.warns(pybamm.SolverWarning): solution = solver.solve(model_disc, t_eval) - self.assertLess(solution.t[-1], 20) + assert solution.t[-1] < 20 # Solve with failure at t=0 solver = pybamm.CasadiSolver( dt_max=1e-3, return_solution_if_failed_early=True, max_step_decrease_count=2 @@ -155,7 +155,7 @@ def test_model_solver_failure(self): t_eval = np.linspace(0, 20, 100) # This one should fail immediately and throw a `SolverError` # since no progress can be made from the first timestep - with self.assertRaisesRegex(pybamm.SolverError, "Maximum number of decreased"): + with pytest.raises(pybamm.SolverError, match="Maximum number of decreased"): solver.solve(model, t_eval) def test_model_solver_events(self): @@ -390,7 +390,7 @@ def test_model_solver_with_inputs(self): solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) t_eval = np.linspace(0, 10, 100) solution = solver.solve(model, t_eval, inputs={"rate": 0.1}) - self.assertLess(len(solution.t), len(t_eval)) + assert len(solution.t) < len(t_eval) np.testing.assert_allclose( solution.y.full()[0], np.exp(-0.1 * solution.t), rtol=1e-04 ) @@ -399,12 +399,12 @@ def test_model_solver_with_inputs(self): solver = pybamm.CasadiSolver(mode="safe without grid", rtol=1e-8, atol=1e-8) t_eval = np.linspace(0, 10, 100) solution = solver.solve(model, t_eval, inputs={"rate": 0.1}) - self.assertLess(len(solution.t), len(t_eval)) + assert len(solution.t) < len(t_eval) np.testing.assert_allclose( solution.y.full()[0], np.exp(-0.1 * solution.t), rtol=1e-04 ) solution = solver.solve(model, t_eval, inputs={"rate": 1.1}) - self.assertLess(len(solution.t), len(t_eval)) + assert len(solution.t) < len(t_eval) np.testing.assert_allclose( solution.y.full()[0], np.exp(-1.1 * solution.t), rtol=1e-04 ) @@ -482,8 +482,8 @@ def test_dae_solver_algebraic_model(self): solver = pybamm.CasadiSolver() t_eval = np.linspace(0, 1) - with self.assertRaisesRegex( - pybamm.SolverError, "Cannot use CasadiSolver to solve algebraic model" + with pytest.raises( + pybamm.SolverError, match="Cannot use CasadiSolver to solve algebraic model" ): solver.solve(model, t_eval) @@ -507,7 +507,7 @@ def func(var): solver = pybamm.CasadiSolver() t_eval = [0, 5] - with self.assertRaisesRegex(pybamm.SolverError, "interpolation bounds"): + with pytest.raises(pybamm.SolverError, match="interpolation bounds"): solver.solve(model, t_eval) def test_casadi_safe_no_termination(self): @@ -532,7 +532,7 @@ def test_casadi_safe_no_termination(self): solver = pybamm.CasadiSolver(mode="safe") solver.set_up(model) - with self.assertRaisesRegex(pybamm.SolverError, "interpolation bounds"): + with pytest.raises(pybamm.SolverError, match="interpolation bounds"): solver.solve(model, t_eval=[0, 1]) def test_modulo_non_smooth_events(self): @@ -581,7 +581,7 @@ def test_modulo_non_smooth_events(self): ) -class TestCasadiSolverODEsWithForwardSensitivityEquations(unittest.TestCase): +class TestCasadiSolverODEsWithForwardSensitivityEquations: def test_solve_sensitivity_scalar_var_scalar_input(self): # Create model model = pybamm.BaseModel() @@ -931,7 +931,7 @@ def test_solve_sensitivity_subset(self): solution.sensitivities["q"], (0.1 * solution.t)[:, np.newaxis], ) - self.assertTrue("r" not in solution.sensitivities) + assert "r" not in solution.sensitivities np.testing.assert_allclose( solution.sensitivities["all"], np.hstack( @@ -949,8 +949,8 @@ def test_solve_sensitivity_subset(self): calculate_sensitivities=["r"], ) np.testing.assert_allclose(solution.y[0], -1 + 0.2 * solution.t) - self.assertTrue("p" not in solution.sensitivities) - self.assertTrue("q" not in solution.sensitivities) + assert "p" not in solution.sensitivities + assert "q" not in solution.sensitivities np.testing.assert_allclose(solution.sensitivities["r"], 1) np.testing.assert_allclose( solution.sensitivities["all"], @@ -962,7 +962,7 @@ def test_solve_sensitivity_subset(self): ) -class TestCasadiSolverDAEsWithForwardSensitivityEquations(unittest.TestCase): +class TestCasadiSolverDAEsWithForwardSensitivityEquations: def test_solve_sensitivity_scalar_var_scalar_input(self): # Create model model = pybamm.BaseModel() @@ -1022,7 +1022,7 @@ def test_solve_sensitivity_algebraic(self): model, t_eval, inputs={"p": 0.1}, calculate_sensitivities=True ) np.testing.assert_array_equal(solution.t, t_eval) - np.testing.assert_allclose(solution.y[0], 0.1 * solution.t) + np.testing.assert_allclose(np.array(solution.y)[0], 0.1 * solution.t) np.testing.assert_allclose( solution.sensitivities["p"], solution.t.reshape(-1, 1), atol=1e-7 ) @@ -1066,8 +1066,8 @@ def test_solve_sensitivity_subset(self): solution.sensitivities["q"][::2], (0.1 * solution.t)[:, np.newaxis], ) - self.assertTrue("r" not in solution.sensitivities) - self.assertTrue("s" not in solution.sensitivities) + assert "r" not in solution.sensitivities + assert "s" not in solution.sensitivities np.testing.assert_allclose( solution.sensitivities["all"], np.hstack( @@ -1096,18 +1096,8 @@ def test_solver_interpolation_warning(self): # Check for warning with t_interp t_eval = np.linspace(0, 1, 10) t_interp = t_eval - with self.assertWarns( + with pytest.warns( pybamm.SolverWarning, - msg=f"Explicit interpolation times not implemented for {solver.name}", + match=f"Explicit interpolation times not implemented for {solver.name}", ): solver.solve(model, t_eval, t_interp=t_interp) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 3f9dcf0508..32e289b3e0 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -4,7 +4,6 @@ from contextlib import redirect_stdout import io -import unittest import pytest import numpy as np @@ -13,8 +12,8 @@ @pytest.mark.cibw -@unittest.skipIf(not pybamm.has_idaklu(), "idaklu solver is not installed") -class TestIDAKLUSolver(unittest.TestCase): +@pytest.mark.skipif(not pybamm.has_idaklu(), reason="idaklu solver is not installed") +class TestIDAKLUSolver: def test_ida_roberts_klu(self): # this test implements a python version of the ida Roberts # example provided in sundials @@ -105,7 +104,7 @@ def test_model_events(self): ) # Check invalid atol type raises an error - with self.assertRaises(pybamm.SolverError): + with pytest.raises(pybamm.SolverError): solver._check_atol_type({"key": "value"}, []) # enforce events that won't be triggered @@ -136,7 +135,7 @@ def test_model_events(self): options={"jax_evaluator": "iree"} if form == "iree" else {}, ) solution = solver.solve(model_disc, t_eval, t_interp=t_interp) - self.assertLess(len(solution.t), len(t_interp)) + assert len(solution.t) < len(t_interp) np.testing.assert_array_almost_equal( solution.y[0], np.exp(0.1 * solution.t), @@ -344,7 +343,7 @@ def test_ida_roberts_klu_sensitivities(self): ) # should be no sensitivities calculated - with self.assertRaises(KeyError): + with pytest.raises(KeyError): print(sol.sensitivities["a"]) # now solve with sensitivities (this should cause set_up to be run again) @@ -566,7 +565,7 @@ def test_failures(self): solver = pybamm.IDAKLUSolver() t_eval = [0, 3] - with self.assertRaisesRegex(pybamm.SolverError, "KLU requires the Jacobian"): + with pytest.raises(pybamm.SolverError, match="KLU requires the Jacobian"): solver.solve(model, t_eval) model = pybamm.BaseModel() @@ -581,8 +580,8 @@ def test_failures(self): # will give solver error t_eval = [0, -3] - with self.assertRaisesRegex( - pybamm.SolverError, "t_eval must increase monotonically" + with pytest.raises( + pybamm.SolverError, match="t_eval must increase monotonically" ): solver.solve(model, t_eval) @@ -598,7 +597,7 @@ def test_failures(self): solver = pybamm.IDAKLUSolver() t_eval = [0, 3] - with self.assertRaisesRegex(pybamm.SolverError, "FAILURE IDA"): + with pytest.raises(pybamm.SolverError, match="FAILURE IDA"): solver.solve(model, t_eval) def test_dae_solver_algebraic_model(self): @@ -677,14 +676,14 @@ def test_setup_options(self): with redirect_stdout(f): solver.solve(model, t_eval, t_interp=t_interp) s = f.getvalue() - self.assertIn("Solver Stats", s) + assert "Solver Stats" in s solver = pybamm.IDAKLUSolver(options={"print_stats": False}) f = io.StringIO() with redirect_stdout(f): solver.solve(model, t_eval, t_interp=t_interp) s = f.getvalue() - self.assertEqual(len(s), 0) + assert len(s) == 0 # test everything else for jacobian in ["none", "dense", "sparse", "matrix-free", "garbage"]: @@ -731,7 +730,7 @@ def test_setup_options(self): soln = solver.solve(model, t_eval, t_interp=t_interp) np.testing.assert_array_almost_equal(soln.y, soln_base.y, 4) else: - with self.assertRaises(ValueError): + with pytest.raises(ValueError): soln = solver.solve(model, t_eval, t_interp=t_interp) def test_solver_options(self): @@ -796,7 +795,7 @@ def test_solver_options(self): options = {option: options_fail[option]} solver = pybamm.IDAKLUSolver(options=options) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): solver.solve(model, t_eval) def test_with_output_variables(self): @@ -899,7 +898,7 @@ def construct_model(): # Check that the missing variables are not available in the solution for varname in inaccessible_vars: - with self.assertRaises(KeyError): + with pytest.raises(KeyError): sol[varname].data # Mock a 1D current collector and initialise (none in the model) @@ -1005,13 +1004,13 @@ def test_with_output_variables_and_sensitivities(self): def test_bad_jax_evaluator(self): model = pybamm.lithium_ion.DFN() model.convert_to_format = "jax" - with self.assertRaises(pybamm.SolverError): + with pytest.raises(pybamm.SolverError): pybamm.IDAKLUSolver(options={"jax_evaluator": "bad_evaluator"}) def test_bad_jax_evaluator_output_variables(self): model = pybamm.lithium_ion.DFN() model.convert_to_format = "jax" - with self.assertRaises(pybamm.SolverError): + with pytest.raises(pybamm.SolverError): pybamm.IDAKLUSolver( options={"jax_evaluator": "bad_evaluator"}, output_variables=["Terminal voltage [V]"], @@ -1027,7 +1026,7 @@ def test_with_output_variables_and_event_termination(self): solver=pybamm.IDAKLUSolver(output_variables=["Terminal voltage [V]"]), ) sol = sim.solve(np.linspace(0, 3600, 2)) - self.assertEqual(sol.termination, "event: Minimum voltage [V]") + assert sol.termination == "event: Minimum voltage [V]" # create an event that doesn't require the state vector eps_p = model.variables["Positive electrode porosity"] @@ -1045,7 +1044,7 @@ def test_with_output_variables_and_event_termination(self): solver=pybamm.IDAKLUSolver(output_variables=["Terminal voltage [V]"]), ) sol3 = sim3.solve(np.linspace(0, 3600, 2)) - self.assertEqual(sol3.termination, "event: Minimum voltage [V]") + assert sol3.termination == "event: Minimum voltage [V]" def test_simulation_period(self): model = pybamm.lithium_ion.DFN() @@ -1107,29 +1106,18 @@ def test_python_idaklu_deprecation_errors(self): ) if form == "python": - with self.assertRaisesRegex( + with pytest.raises( pybamm.SolverError, - "Unsupported option for convert_to_format=python", + match="Unsupported option for convert_to_format=python", ): - with self.assertWarnsRegex( + with pytest.raises( DeprecationWarning, - "The python-idaklu solver has been deprecated.", + match="The python-idaklu solver has been deprecated.", ): _ = solver.solve(model, t_eval) elif form == "jax": - with self.assertRaisesRegex( + with pytest.raises( pybamm.SolverError, - "Unsupported evaluation engine for convert_to_format=jax", + match="Unsupported evaluation engine for convert_to_format=jax", ): _ = solver.solve(model, t_eval) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - - unittest.main() diff --git a/tests/unit/test_solvers/test_jax_bdf_solver.py b/tests/unit/test_solvers/test_jax_bdf_solver.py index e0064ae463..98eaed8e6a 100644 --- a/tests/unit/test_solvers/test_jax_bdf_solver.py +++ b/tests/unit/test_solvers/test_jax_bdf_solver.py @@ -1,16 +1,15 @@ +import pytest import pybamm -import unittest from tests import get_mesh_for_testing -import sys import numpy as np if pybamm.has_jax(): import jax -@unittest.skipIf(not pybamm.has_jax(), "jax or jaxlib is not installed") -class TestJaxBDFSolver(unittest.TestCase): +@pytest.mark.skipif(not pybamm.has_jax(), reason="jax or jaxlib is not installed") +class TestJaxBDFSolver: def test_solver_(self): # Trailing _ manipulates the random seed # Create model model = pybamm.BaseModel() @@ -113,7 +112,7 @@ def solve_bdf(rate): grad_solve_bdf = jax.jit(jax.grad(solve_bdf)) grad_bdf = grad_solve_bdf(rate) - self.assertAlmostEqual(grad_bdf, grad_num, places=3) + assert grad_bdf == pytest.approx(grad_num, abs=0.001) def test_mass_matrix_with_sensitivities(self): # Solve @@ -146,7 +145,7 @@ def solve_bdf(rate): grad_solve_bdf = jax.jit(jax.grad(solve_bdf)) grad_bdf = grad_solve_bdf(rate) - self.assertAlmostEqual(grad_bdf, grad_num, places=3) + assert grad_bdf == pytest.approx(grad_num, abs=0.001) def test_solver_with_inputs(self): # Create model @@ -176,12 +175,3 @@ def fun(y, t, inputs): ) np.testing.assert_allclose(y[:, 0].reshape(-1), np.exp(-0.1 * t_eval)) - - -if __name__ == "__main__": - print("Add -v for more debug output") - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_solvers/test_jax_solver.py b/tests/unit/test_solvers/test_jax_solver.py index b1c293c2f2..f7e5b8d3b6 100644 --- a/tests/unit/test_solvers/test_jax_solver.py +++ b/tests/unit/test_solvers/test_jax_solver.py @@ -1,16 +1,15 @@ +import pytest import pybamm -import unittest from tests import get_mesh_for_testing -import sys import numpy as np if pybamm.has_jax(): import jax -@unittest.skipIf(not pybamm.has_jax(), "jax or jaxlib is not installed") -class TestJaxSolver(unittest.TestCase): +@pytest.mark.skipif(not pybamm.has_jax(), reason="jax or jaxlib is not installed") +class TestJaxSolver: def test_model_solver(self): # Create model model = pybamm.BaseModel() @@ -38,10 +37,8 @@ def test_model_solver(self): ) # Test time - self.assertEqual( - solution.total_time, solution.solve_time + solution.set_up_time - ) - self.assertEqual(solution.termination, "final time") + assert solution.total_time == solution.solve_time + solution.set_up_time + assert solution.termination == "final time" second_solution = solver.solve(model, t_eval) @@ -76,10 +73,8 @@ def test_semi_explicit_model(self): np.testing.assert_allclose(solution.y[-1], 2 * soln, rtol=1e-7, atol=1e-7) # Test time - self.assertEqual( - solution.total_time, solution.solve_time + solution.set_up_time - ) - self.assertEqual(solution.termination, "final time") + assert solution.total_time == solution.solve_time + solution.set_up_time + assert solution.termination == "final time" second_solution = solver.solve(model, t_eval) np.testing.assert_array_equal(second_solution.y, solution.y) @@ -124,7 +119,7 @@ def solve_model(rate, solve=solve): grad_solve = jax.jit(jax.grad(solve_model)) grad = grad_solve(rate) - self.assertAlmostEqual(grad, grad_num, places=1) + assert grad == pytest.approx(grad_num, abs=0.1) def test_solver_only_works_with_jax(self): model = pybamm.BaseModel() @@ -144,7 +139,7 @@ def test_solver_only_works_with_jax(self): model.convert_to_format = convert_to_format solver = pybamm.JaxSolver() - with self.assertRaisesRegex(RuntimeError, "must be converted to JAX"): + with pytest.raises(RuntimeError, match="must be converted to JAX"): solver.solve(model, t_eval) def test_solver_doesnt_support_events(self): @@ -171,7 +166,7 @@ def test_solver_doesnt_support_events(self): # Solve solver = pybamm.JaxSolver() t_eval = np.linspace(0, 10, 100) - with self.assertRaisesRegex(RuntimeError, "Terminate events not supported"): + with pytest.raises(RuntimeError, match="Terminate events not supported"): solver.solve(model, t_eval) def test_model_solver_with_inputs(self): @@ -223,14 +218,14 @@ def test_get_solve(self): disc.process_model(model) # test that another method string gives error - with self.assertRaises(ValueError): + with pytest.raises(ValueError): solver = pybamm.JaxSolver(method="not_real") # Solve solver = pybamm.JaxSolver(rtol=1e-8, atol=1e-8) t_eval = np.linspace(0, 5, 80) - with self.assertRaisesRegex(RuntimeError, "Model is not set up for solving"): + with pytest.raises(RuntimeError, match="Model is not set up for solving"): solver.get_solve(model, t_eval) solver.solve(model, t_eval, inputs={"rate": 0.1}) @@ -242,12 +237,3 @@ def test_get_solve(self): y = solver({"rate": 0.2}) np.testing.assert_allclose(y[0], np.exp(-0.2 * t_eval), rtol=1e-6, atol=1e-6) - - -if __name__ == "__main__": - print("Add -v for more debug output") - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index b6ae669878..6cd456347d 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -7,7 +7,7 @@ import tests import numpy as np -import unittest +import pytest def to_casadi(var_pybamm, y, inputs=None): @@ -61,7 +61,7 @@ def process_and_check_2D_variable( return y_sol, first_sol, second_sol, t_sol -class TestProcessedVariable(unittest.TestCase): +class TestProcessedVariable: def test_processed_variable_0D(self): # without space t = pybamm.t @@ -112,7 +112,7 @@ def test_processed_variable_0D_no_sensitivity(self): ) # test no inputs (i.e. no sensitivity) - self.assertDictEqual(processed_var.sensitivities, {}) + assert processed_var.sensitivities == {} # with parameter t = pybamm.t @@ -132,7 +132,7 @@ def test_processed_variable_0D_no_sensitivity(self): ) # test no sensitivity raises error - with self.assertRaisesRegex(ValueError, "Cannot compute sensitivities"): + with pytest.raises(ValueError, match="Cannot compute sensitivities"): print(processed_var.sensitivities) def test_processed_variable_1D(self): @@ -562,10 +562,10 @@ def test_processed_var_1D_interpolation(self): processed_eqn(t_sol, x_sol), t_sol * y_sol + x_sol[:, np.newaxis] ) # 1 vector, 1 scalar - self.assertEqual(processed_eqn(0.5, x_sol[10:30]).shape, (20,)) - self.assertEqual(processed_eqn(t_sol[4:9], x_sol[-1]).shape, (5,)) + assert processed_eqn(0.5, x_sol[10:30]).shape == (20,) + assert processed_eqn(t_sol[4:9], x_sol[-1]).shape == (5,) # 2 scalars - self.assertEqual(processed_eqn(0.5, x_sol[-1]).shape, ()) + assert processed_eqn(0.5, x_sol[-1]).shape == () # test x x_disc = disc.process_symbol(x) @@ -686,7 +686,7 @@ def test_processed_var_wrong_spatial_variable_names(self): "domain B": {b: {"min": 0, "max": 1}}, } ) - with self.assertRaisesRegex(NotImplementedError, "Spatial variable name"): + with pytest.raises(NotImplementedError, match="Spatial variable name"): pybamm.ProcessedVariable( [var_sol], [var_casadi], @@ -892,7 +892,7 @@ def test_processed_var_2D_secondary_broadcast(self): processed_var(t_sol, x_sol, r_sol).shape, (10, 35, 50) ) - def test_processed_var_2D_scikit_interpolation(self): + def test_processed_var_2_d_scikit_interpolation(self): var = pybamm.Variable("var", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() @@ -1061,7 +1061,7 @@ def test_3D_raises_error(self): u_sol = np.ones(var_sol.shape[0] * 3)[:, np.newaxis] var_casadi = to_casadi(var_sol, u_sol) - with self.assertRaisesRegex(NotImplementedError, "Shape not recognized"): + with pytest.raises(NotImplementedError, match="Shape not recognized"): pybamm.ProcessedVariable( [var_sol], [var_casadi], @@ -1088,51 +1088,23 @@ def test_process_spatial_variable_names(self): ) # Test empty list returns None - self.assertIsNone(processed_var._process_spatial_variable_names([])) + assert processed_var._process_spatial_variable_names([]) is None # Test tabs is ignored - self.assertEqual( - processed_var._process_spatial_variable_names(["tabs", "var"]), - "var", - ) + assert processed_var._process_spatial_variable_names(["tabs", "var"]) == "var" # Test strings stay strings - self.assertEqual( - processed_var._process_spatial_variable_names(["y"]), - "y", - ) + assert processed_var._process_spatial_variable_names(["y"]) == "y" # Test spatial variables are converted to strings x = pybamm.SpatialVariable("x", domain=["domain"]) - self.assertEqual( - processed_var._process_spatial_variable_names([x]), - "x", - ) + assert processed_var._process_spatial_variable_names([x]) == "x" # Test renaming for PyBaMM convention - self.assertEqual( - processed_var._process_spatial_variable_names(["x_a", "x_b"]), - "x", - ) - self.assertEqual( - processed_var._process_spatial_variable_names(["r_a", "r_b"]), - "r", - ) - self.assertEqual( - processed_var._process_spatial_variable_names(["R_a", "R_b"]), - "R", - ) + assert processed_var._process_spatial_variable_names(["x_a", "x_b"]) == "x" + assert processed_var._process_spatial_variable_names(["r_a", "r_b"]) == "r" + assert processed_var._process_spatial_variable_names(["R_a", "R_b"]) == "R" # Test error raised if spatial variable name not recognised - with self.assertRaisesRegex(NotImplementedError, "Spatial variable name"): + with pytest.raises(NotImplementedError, match="Spatial variable name"): processed_var._process_spatial_variable_names(["var1", "var2"]) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_solvers/test_processed_variable_computed.py b/tests/unit/test_solvers/test_processed_variable_computed.py index 407d422e4c..59a062b199 100644 --- a/tests/unit/test_solvers/test_processed_variable_computed.py +++ b/tests/unit/test_solvers/test_processed_variable_computed.py @@ -6,12 +6,12 @@ # values itself since it does not have access to the full state vector # +import pytest import casadi import pybamm import tests import numpy as np -import unittest def to_casadi(var_pybamm, y, inputs=None): @@ -68,7 +68,7 @@ def process_and_check_2D_variable( return y_sol, first_sol, second_sol, t_sol -class TestProcessedVariableComputed(unittest.TestCase): +class TestProcessedVariableComputed: def test_processed_variable_0D(self): # without space y = pybamm.StateVector(slice(0, 1)) @@ -115,7 +115,7 @@ def test_processed_variable_0D_no_sensitivity(self): ) # test no inputs (i.e. no sensitivity) - self.assertDictEqual(processed_var.sensitivities, {}) + assert processed_var.sensitivities == {} # with parameter t = pybamm.t @@ -136,7 +136,7 @@ def test_processed_variable_0D_no_sensitivity(self): ) # test no sensitivity raises error - self.assertIsNone(processed_var.sensitivities) + assert processed_var.sensitivities is None def test_processed_variable_1D(self): var = pybamm.Variable("var", domain=["negative electrode", "separator"]) @@ -386,7 +386,7 @@ def test_processed_variable_2D_space_only(self): np.testing.assert_array_equal(processed_var.unroll(), y_sol.reshape(10, 40, 1)) # Check unroll function (3D) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): processed_var.dimensions = 3 processed_var.unroll() @@ -428,7 +428,7 @@ def test_3D_raises_error(self): u_sol = np.ones(var_sol.shape[0] * 3)[:, np.newaxis] var_casadi = to_casadi(var_sol, u_sol) - with self.assertRaisesRegex(NotImplementedError, "Shape not recognized"): + with pytest.raises(NotImplementedError, match="Shape not recognized"): pybamm.ProcessedVariableComputed( [var_sol], [var_casadi], @@ -436,13 +436,3 @@ def test_3D_raises_error(self): pybamm.Solution(t_sol, u_sol, pybamm.BaseModel(), {}), warn=False, ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_solvers/test_solution.py b/tests/unit/test_solvers/test_solution.py index 995898e8dd..5a584fabbf 100644 --- a/tests/unit/test_solvers/test_solution.py +++ b/tests/unit/test_solvers/test_solution.py @@ -52,6 +52,20 @@ def test_errors(self): pybamm.Solution(ts, bad_ys, model, {}) self.assertIn("exceeds the maximum", captured.records[0].getMessage()) + with self.assertRaisesRegex( + TypeError, "sensitivities arg needs to be a bool or dict" + ): + pybamm.Solution(ts, bad_ys, model, {}, all_sensitivities="bad") + + sol = pybamm.Solution(ts, bad_ys, model, {}, all_sensitivities={}) + with self.assertRaisesRegex(TypeError, "sensitivities arg needs to be a bool"): + sol.sensitivities = "bad" + with self.assertRaisesRegex( + NotImplementedError, + "Setting sensitivities is not supported if sensitivities are already provided as a dict", + ): + sol.sensitivities = True + def test_add_solutions(self): # Set up first solution t1 = np.linspace(0, 1) @@ -89,7 +103,7 @@ def test_add_solutions(self): # Add solution already contained in existing solution t3 = np.array([2]) - y3 = np.ones((20, 1)) + y3 = np.ones((1, 1)) sol3 = pybamm.Solution(t3, y3, pybamm.BaseModel(), {"a": 3}) self.assertEqual((sol_sum + sol3).all_ts, sol_sum.copy().all_ts) @@ -111,6 +125,23 @@ def test_add_solutions(self): ): 2 + sol3 + sol1 = pybamm.Solution( + t1, + y1, + pybamm.BaseModel(), + {}, + all_sensitivities={"test": [np.ones((1, 3))]}, + ) + sol2 = pybamm.Solution(t2, y2, pybamm.BaseModel(), {}, all_sensitivities=True) + with self.assertRaisesRegex( + ValueError, "Sensitivities must be of the same type" + ): + sol3 = sol1 + sol2 + sol1 = pybamm.Solution(t1, y3, pybamm.BaseModel(), {}, all_sensitivities=False) + sol2 = pybamm.Solution(t3, y3, pybamm.BaseModel(), {}, all_sensitivities={}) + sol3 = sol1 + sol2 + self.assertFalse(sol3._all_sensitivities) + def test_add_solutions_different_models(self): # Set up first solution t1 = np.linspace(0, 1) @@ -146,7 +177,8 @@ def test_copy(self): sol_copy = sol1.copy() self.assertEqual(sol_copy.all_ts, sol1.all_ts) - self.assertEqual(sol_copy.all_ys, sol1.all_ys) + for ys_copy, ys1 in zip(sol_copy.all_ys, sol1.all_ys): + np.testing.assert_array_equal(ys_copy, ys1) self.assertEqual(sol_copy.all_inputs, sol1.all_inputs) self.assertEqual(sol_copy.all_inputs_casadi, sol1.all_inputs_casadi) self.assertEqual(sol_copy.set_up_time, sol1.set_up_time) diff --git a/tests/unit/test_spatial_methods/test_base_spatial_method.py b/tests/unit/test_spatial_methods/test_base_spatial_method.py index 647616c924..190fbd8f94 100644 --- a/tests/unit/test_spatial_methods/test_base_spatial_method.py +++ b/tests/unit/test_spatial_methods/test_base_spatial_method.py @@ -2,9 +2,9 @@ # Test for the base Spatial Method class # +import pytest import numpy as np import pybamm -import unittest from tests import ( get_mesh_for_testing, get_1p1d_mesh_for_testing, @@ -12,31 +12,31 @@ ) -class TestSpatialMethod(unittest.TestCase): +class TestSpatialMethod: def test_basics(self): mesh = get_mesh_for_testing() spatial_method = pybamm.SpatialMethod() spatial_method.build(mesh) - self.assertEqual(spatial_method.mesh, mesh) - with self.assertRaises(NotImplementedError): + assert spatial_method.mesh == mesh + with pytest.raises(NotImplementedError): spatial_method.gradient(None, None, None) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): spatial_method.divergence(None, None, None) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): spatial_method.laplacian(None, None, None) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): spatial_method.gradient_squared(None, None, None) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): spatial_method.integral(None, None, None) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): spatial_method.indefinite_integral(None, None, None) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): spatial_method.boundary_integral(None, None, None) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): spatial_method.delta_function(None, None) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): spatial_method.internal_neumann_condition(None, None, None, None) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): spatial_method.evaluate_at(None, None, None) def test_get_auxiliary_domain_repeats(self): @@ -47,20 +47,18 @@ def test_get_auxiliary_domain_repeats(self): # No auxiliary domains repeats = spatial_method._get_auxiliary_domain_repeats({}) - self.assertEqual(repeats, 1) + assert repeats == 1 # Just secondary domain repeats = spatial_method._get_auxiliary_domain_repeats( {"secondary": ["negative electrode"]} ) - self.assertEqual(repeats, mesh["negative electrode"].npts) + assert repeats == mesh["negative electrode"].npts repeats = spatial_method._get_auxiliary_domain_repeats( {"secondary": ["negative electrode", "separator"]} ) - self.assertEqual( - repeats, mesh["negative electrode"].npts + mesh["separator"].npts - ) + assert repeats == mesh["negative electrode"].npts + mesh["separator"].npts # With tertiary domain repeats = spatial_method._get_auxiliary_domain_repeats( @@ -69,17 +67,17 @@ def test_get_auxiliary_domain_repeats(self): "tertiary": ["current collector"], } ) - self.assertEqual( - repeats, - (mesh["negative electrode"].npts + mesh["separator"].npts) - * mesh["current collector"].npts, + assert ( + repeats + == (mesh["negative electrode"].npts + mesh["separator"].npts) + * mesh["current collector"].npts ) # Just tertiary domain repeats = spatial_method._get_auxiliary_domain_repeats( {"tertiary": ["current collector"]}, ) - self.assertEqual(repeats, mesh["current collector"].npts) + assert repeats == mesh["current collector"].npts # With quaternary domain repeats = spatial_method._get_auxiliary_domain_repeats( @@ -89,11 +87,11 @@ def test_get_auxiliary_domain_repeats(self): "quaternary": ["current collector"], } ) - self.assertEqual( - repeats, - mesh["negative particle size"].npts + assert ( + repeats + == mesh["negative particle size"].npts * (mesh["negative electrode"].npts + mesh["separator"].npts) - * mesh["current collector"].npts, + * mesh["current collector"].npts ) def test_discretise_spatial_variable(self): @@ -108,7 +106,7 @@ def test_discretise_spatial_variable(self): r = pybamm.SpatialVariable("r", ["negative particle"]) for var in [x1, x2, r]: var_disc = spatial_method.spatial_variable(var) - self.assertIsInstance(var_disc, pybamm.Vector) + assert isinstance(var_disc, pybamm.Vector) np.testing.assert_array_equal( var_disc.evaluate()[:, 0], mesh[var.domain].nodes ) @@ -119,7 +117,7 @@ def test_discretise_spatial_variable(self): r_edge = pybamm.SpatialVariableEdge("r", ["negative particle"]) for var in [x1_edge, x2_edge, r_edge]: var_disc = spatial_method.spatial_variable(var) - self.assertIsInstance(var_disc, pybamm.Vector) + assert isinstance(var_disc, pybamm.Vector) np.testing.assert_array_equal( var_disc.evaluate()[:, 0], mesh[var.domain].edges ) @@ -130,12 +128,12 @@ def test_boundary_value_checks(self): mesh = get_mesh_for_testing() spatial_method = pybamm.SpatialMethod() spatial_method.build(mesh) - with self.assertRaisesRegex(TypeError, "Cannot process BoundaryGradient"): + with pytest.raises(TypeError, match="Cannot process BoundaryGradient"): spatial_method.boundary_value_or_flux(symbol, child) # test also symbol "right" symbol = pybamm.BoundaryGradient(child, "right") - with self.assertRaisesRegex(TypeError, "Cannot process BoundaryGradient"): + with pytest.raises(TypeError, match="Cannot process BoundaryGradient"): spatial_method.boundary_value_or_flux(symbol, child) mesh = get_1p1d_mesh_for_testing() @@ -147,15 +145,5 @@ def test_boundary_value_checks(self): auxiliary_domains={"secondary": "current collector"}, ) symbol = pybamm.BoundaryGradient(child, "left") - with self.assertRaisesRegex(NotImplementedError, "Cannot process 2D symbol"): + with pytest.raises(NotImplementedError, match="Cannot process 2D symbol"): spatial_method.boundary_value_or_flux(symbol, child) - - -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_spatial_methods/test_finite_volume/test_extrapolation.py b/tests/unit/test_spatial_methods/test_finite_volume/test_extrapolation.py index c51e2d9a13..8479097031 100644 --- a/tests/unit/test_spatial_methods/test_finite_volume/test_extrapolation.py +++ b/tests/unit/test_spatial_methods/test_finite_volume/test_extrapolation.py @@ -9,7 +9,6 @@ get_1p1d_mesh_for_testing, ) import numpy as np -import unittest def errors(pts, function, method_options, bcs=None): @@ -57,7 +56,7 @@ def get_errors(function, method_options, pts, bcs=None): return l_errors, r_errors -class TestExtrapolation(unittest.TestCase): +class TestExtrapolation: def test_convergence_without_bcs(self): # all tests are performed on x in [0, 1] linear = {"extrapolation": {"order": "linear"}} @@ -262,8 +261,8 @@ def test_linear_extrapolate_left_right(self): # check constant extrapolates to constant constant_y = np.ones_like(macro_submesh.nodes[:, np.newaxis]) - self.assertEqual(extrap_left_disc.evaluate(None, constant_y), 2) - self.assertEqual(extrap_right_disc.evaluate(None, constant_y), 3) + assert extrap_left_disc.evaluate(None, constant_y) == 2 + assert extrap_right_disc.evaluate(None, constant_y) == 3 # check linear variable extrapolates correctly linear_y = macro_submesh.nodes @@ -297,7 +296,7 @@ def test_linear_extrapolate_left_right(self): # check constant extrapolates to constant constant_y = np.ones_like(micro_submesh.nodes[:, np.newaxis]) - self.assertEqual(surf_eqn_disc.evaluate(None, constant_y), 1.0) + assert surf_eqn_disc.evaluate(None, constant_y) == 1.0 # check linear variable extrapolates correctly linear_y = micro_submesh.nodes @@ -359,7 +358,7 @@ def test_quadratic_extrapolate_left_right(self): np.testing.assert_array_almost_equal( extrap_flux_left_disc.evaluate(None, constant_y), 0 ) - self.assertEqual(extrap_flux_right_disc.evaluate(None, constant_y), 0) + assert extrap_flux_right_disc.evaluate(None, constant_y) == 0 # check linear variable extrapolates correctly np.testing.assert_array_almost_equal( @@ -448,7 +447,7 @@ def test_extrapolate_2d_models(self): extrap_right = pybamm.BoundaryValue(var, "right") disc.set_variable_slices([var]) extrap_right_disc = disc.process_symbol(extrap_right) - self.assertEqual(extrap_right_disc.domain, ["negative electrode"]) + assert extrap_right_disc.domain == ["negative electrode"] # evaluate y_macro = mesh["negative electrode"].nodes y_micro = mesh["negative particle"].nodes @@ -462,7 +461,7 @@ def test_extrapolate_2d_models(self): extrap_right = pybamm.BoundaryValue(var, "right") disc.set_variable_slices([var]) extrap_right_disc = disc.process_symbol(extrap_right) - self.assertEqual(extrap_right_disc.domain, []) + assert extrap_right_disc.domain == [] # 2d macroscale mesh = get_1p1d_mesh_for_testing() @@ -471,7 +470,7 @@ def test_extrapolate_2d_models(self): extrap_right = pybamm.BoundaryValue(var, "right") disc.set_variable_slices([var]) extrap_right_disc = disc.process_symbol(extrap_right) - self.assertEqual(extrap_right_disc.domain, []) + assert extrap_right_disc.domain == [] # test extrapolate to "negative tab" gives same as "left" and # "positive tab" gives same "right" (see get_mesh_for_testing) @@ -497,13 +496,3 @@ def test_extrapolate_2d_models(self): extrap_pos_disc.evaluate(None, constant_y), extrap_right_disc.evaluate(None, constant_y), ) - - -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_spatial_methods/test_finite_volume/test_finite_volume.py b/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py index de31b770ff..204e831855 100644 --- a/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py +++ b/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py @@ -10,10 +10,10 @@ ) import numpy as np from scipy.sparse import kron, eye -import unittest +import pytest -class TestFiniteVolume(unittest.TestCase): +class TestFiniteVolume: def test_node_to_edge_to_node(self): # Create discretisation mesh = get_mesh_for_testing() @@ -46,14 +46,14 @@ def test_node_to_edge_to_node(self): ) # bad shift key - with self.assertRaisesRegex(ValueError, "shift key"): + with pytest.raises(ValueError, match="shift key"): fin_vol.shift(c, "bad shift key", "arithmetic") - with self.assertRaisesRegex(ValueError, "shift key"): + with pytest.raises(ValueError, match="shift key"): fin_vol.shift(c, "bad shift key", "harmonic") # bad method - with self.assertRaisesRegex(ValueError, "method"): + with pytest.raises(ValueError, match="method"): fin_vol.shift(c, "shift key", "bad method") def test_concatenation(self): @@ -71,7 +71,7 @@ def test_concatenation(self): edges = [ pybamm.Vector(np.ones(mesh[dom].npts + 2), domain=dom) for dom in whole_cell ] - with self.assertRaisesRegex(pybamm.ShapeError, "child must have size n_nodes"): + with pytest.raises(pybamm.ShapeError, match="child must have size n_nodes"): fin_vol.concatenation(edges) def test_discretise_diffusivity_times_spatial_operator(self): @@ -154,14 +154,14 @@ def test_discretise_spatial_variable(self): # macroscale x1 = pybamm.SpatialVariable("x", ["negative electrode"]) x1_disc = disc.process_symbol(x1) - self.assertIsInstance(x1_disc, pybamm.Vector) + assert isinstance(x1_disc, pybamm.Vector) np.testing.assert_array_equal( x1_disc.evaluate(), disc.mesh["negative electrode"].nodes[:, np.newaxis] ) # macroscale with concatenation x2 = pybamm.SpatialVariable("x", ["negative electrode", "separator"]) x2_disc = disc.process_symbol(x2) - self.assertIsInstance(x2_disc, pybamm.Vector) + assert isinstance(x2_disc, pybamm.Vector) np.testing.assert_array_equal( x2_disc.evaluate(), disc.mesh[("negative electrode", "separator")].nodes[:, np.newaxis], @@ -169,7 +169,7 @@ def test_discretise_spatial_variable(self): # microscale r = 3 * pybamm.SpatialVariable("r", ["negative particle"]) r_disc = disc.process_symbol(r) - self.assertIsInstance(r_disc, pybamm.Vector) + assert isinstance(r_disc, pybamm.Vector) np.testing.assert_array_equal( r_disc.evaluate(), 3 * disc.mesh["negative particle"].nodes[:, np.newaxis] ) @@ -326,8 +326,8 @@ def test_boundary_value_domain(self): c_s_p_surf = pybamm.surf(c_s_p) c_s_n_surf_disc = disc.process_symbol(c_s_n_surf) c_s_p_surf_disc = disc.process_symbol(c_s_p_surf) - self.assertEqual(c_s_n_surf_disc.domain, ["negative electrode"]) - self.assertEqual(c_s_p_surf_disc.domain, ["positive electrode"]) + assert c_s_n_surf_disc.domain == ["negative electrode"] + assert c_s_p_surf_disc.domain == ["positive electrode"] def test_delta_function(self): mesh = get_mesh_for_testing() @@ -344,17 +344,17 @@ def test_delta_function(self): # Basic shape and type tests y = np.ones_like(mesh["negative electrode"].nodes[:, np.newaxis]) # Left - self.assertEqual(delta_fn_left_disc.domains, delta_fn_left.domains) - self.assertIsInstance(delta_fn_left_disc, pybamm.Multiplication) - self.assertIsInstance(delta_fn_left_disc.left, pybamm.Matrix) + assert delta_fn_left_disc.domains == delta_fn_left.domains + assert isinstance(delta_fn_left_disc, pybamm.Multiplication) + assert isinstance(delta_fn_left_disc.left, pybamm.Matrix) np.testing.assert_array_equal(delta_fn_left_disc.left.evaluate()[:, 1:], 0) - self.assertEqual(delta_fn_left_disc.shape, y.shape) + assert delta_fn_left_disc.shape == y.shape # Right - self.assertEqual(delta_fn_right_disc.domains, delta_fn_right.domains) - self.assertIsInstance(delta_fn_right_disc, pybamm.Multiplication) - self.assertIsInstance(delta_fn_right_disc.left, pybamm.Matrix) + assert delta_fn_right_disc.domains == delta_fn_right.domains + assert isinstance(delta_fn_right_disc, pybamm.Multiplication) + assert isinstance(delta_fn_right_disc.left, pybamm.Matrix) np.testing.assert_array_equal(delta_fn_right_disc.left.evaluate()[:, :-1], 0) - self.assertEqual(delta_fn_right_disc.shape, y.shape) + assert delta_fn_right_disc.shape == y.shape # Value tests # Delta function should integrate to the same thing as variable @@ -378,7 +378,7 @@ def test_heaviside(self): # process_binary_operators should work with heaviside disc_heav = disc.process_symbol(heav * var) nodes = mesh["negative electrode"].nodes - self.assertEqual(disc_heav.size, nodes.size) + assert disc_heav.size == nodes.size np.testing.assert_array_equal(disc_heav.evaluate(y=2 * np.ones_like(nodes)), 2) np.testing.assert_array_equal(disc_heav.evaluate(y=-2 * np.ones_like(nodes)), 0) @@ -404,8 +404,8 @@ def test_upwind_downwind(self): nodes = mesh["negative electrode"].nodes n = mesh["negative electrode"].npts - self.assertEqual(disc_upwind.size, nodes.size + 1) - self.assertEqual(disc_downwind.size, nodes.size + 1) + assert disc_upwind.size == nodes.size + 1 + assert disc_downwind.size == nodes.size + 1 y_test = 2 * np.ones_like(nodes) np.testing.assert_array_equal( @@ -420,7 +420,7 @@ def test_upwind_downwind(self): # Remove boundary conditions and check error is raised disc.bcs = {} disc._discretised_symbols = {} - with self.assertRaisesRegex(pybamm.ModelError, "Boundary conditions"): + with pytest.raises(pybamm.ModelError, match="Boundary conditions"): disc.process_symbol(upwind) # Set wrong boundary conditions and check error is raised @@ -430,9 +430,9 @@ def test_upwind_downwind(self): "right": (pybamm.Scalar(3), "Neumann"), } } - with self.assertRaisesRegex(pybamm.ModelError, "Dirichlet boundary conditions"): + with pytest.raises(pybamm.ModelError, match="Dirichlet boundary conditions"): disc.process_symbol(upwind) - with self.assertRaisesRegex(pybamm.ModelError, "Dirichlet boundary conditions"): + with pytest.raises(pybamm.ModelError, match="Dirichlet boundary conditions"): disc.process_symbol(downwind) def test_grad_div_with_bcs_on_tab(self): @@ -525,10 +525,10 @@ def test_neg_pos_bcs(self): # check after disc that negative tab goes to left and positive tab goes # to right disc.process_symbol(grad_eqn) - self.assertEqual(disc.bcs[var]["left"][0], pybamm.Scalar(1)) - self.assertEqual(disc.bcs[var]["left"][1], "Dirichlet") - self.assertEqual(disc.bcs[var]["right"][0], pybamm.Scalar(0)) - self.assertEqual(disc.bcs[var]["right"][1], "Neumann") + assert disc.bcs[var]["left"][0] == pybamm.Scalar(1) + assert disc.bcs[var]["left"][1] == "Dirichlet" + assert disc.bcs[var]["right"][0] == pybamm.Scalar(0) + assert disc.bcs[var]["right"][1] == "Neumann" def test_full_broadcast_domains(self): model = pybamm.BaseModel() @@ -568,12 +568,12 @@ def test_evaluate_at(self): evaluate_at = pybamm.EvaluateAt(var, position) evaluate_at_disc = disc.process_symbol(evaluate_at) - self.assertIsInstance(evaluate_at_disc, pybamm.MatrixMultiplication) - self.assertIsInstance(evaluate_at_disc.left, pybamm.Matrix) - self.assertIsInstance(evaluate_at_disc.right, pybamm.StateVector) + assert isinstance(evaluate_at_disc, pybamm.MatrixMultiplication) + assert isinstance(evaluate_at_disc.left, pybamm.Matrix) + assert isinstance(evaluate_at_disc.right, pybamm.StateVector) y = np.arange(n)[:, np.newaxis] - self.assertEqual(evaluate_at_disc.evaluate(y=y), y[idx]) + assert evaluate_at_disc.evaluate(y=y) == y[idx] def test_inner(self): # standard @@ -598,9 +598,9 @@ def test_inner(self): disc.bcs = boundary_conditions inner_disc = disc.process_symbol(inner) - self.assertIsInstance(inner_disc, pybamm.Inner) - self.assertIsInstance(inner_disc.left, pybamm.MatrixMultiplication) - self.assertIsInstance(inner_disc.right, pybamm.MatrixMultiplication) + assert isinstance(inner_disc, pybamm.Inner) + assert isinstance(inner_disc.left, pybamm.MatrixMultiplication) + assert isinstance(inner_disc.right, pybamm.MatrixMultiplication) n = mesh["negative particle"].npts y = np.ones(n)[:, np.newaxis] @@ -613,19 +613,9 @@ def test_inner(self): inner_disc = disc.process_symbol(inner) - self.assertIsInstance(inner_disc, pybamm.Inner) - self.assertIsInstance(inner_disc.left, pybamm.MatrixMultiplication) - self.assertIsInstance(inner_disc.right, pybamm.MatrixMultiplication) + assert isinstance(inner_disc, pybamm.Inner) + assert isinstance(inner_disc.left, pybamm.MatrixMultiplication) + assert isinstance(inner_disc.right, pybamm.MatrixMultiplication) m = mesh["negative electrode"].npts np.testing.assert_array_equal(inner_disc.evaluate(y=y), np.zeros((n * m, 1))) - - -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_spatial_methods/test_finite_volume/test_ghost_nodes_and_neumann.py b/tests/unit/test_spatial_methods/test_finite_volume/test_ghost_nodes_and_neumann.py index ba82f2fb09..0044d20c0a 100644 --- a/tests/unit/test_spatial_methods/test_finite_volume/test_ghost_nodes_and_neumann.py +++ b/tests/unit/test_spatial_methods/test_finite_volume/test_ghost_nodes_and_neumann.py @@ -5,10 +5,10 @@ import pybamm from tests import get_mesh_for_testing, get_p2d_mesh_for_testing import numpy as np -import unittest +import pytest -class TestGhostNodes(unittest.TestCase): +class TestGhostNodes: def test_add_ghost_nodes(self): # Set up @@ -36,25 +36,25 @@ def test_add_ghost_nodes(self): np.testing.assert_array_equal( sym_ghost.evaluate(y=y_test)[1:-1], discretised_symbol.evaluate(y=y_test) ) - self.assertEqual( - (sym_ghost.evaluate(y=y_test)[0] + sym_ghost.evaluate(y=y_test)[1]) / 2, 0 - ) - self.assertEqual( - (sym_ghost.evaluate(y=y_test)[-2] + sym_ghost.evaluate(y=y_test)[-1]) / 2, 3 - ) + assert ( + sym_ghost.evaluate(y=y_test)[0] + sym_ghost.evaluate(y=y_test)[1] + ) / 2 == 0 + assert ( + sym_ghost.evaluate(y=y_test)[-2] + sym_ghost.evaluate(y=y_test)[-1] + ) / 2 == 3 # test errors bcs = {"left": (pybamm.Scalar(0), "x"), "right": (pybamm.Scalar(3), "Neumann")} - with self.assertRaisesRegex(ValueError, "boundary condition must be"): + with pytest.raises(ValueError, match="boundary condition must be"): sp_meth.add_ghost_nodes(var, discretised_symbol, bcs) - with self.assertRaisesRegex(ValueError, "boundary condition must be"): + with pytest.raises(ValueError, match="boundary condition must be"): sp_meth.add_neumann_values(var, discretised_symbol, bcs, var.domain) bcs = {"left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(3), "x")} - with self.assertRaisesRegex(ValueError, "boundary condition must be"): + with pytest.raises(ValueError, match="boundary condition must be"): sp_meth.add_ghost_nodes(var, discretised_symbol, bcs) - with self.assertRaisesRegex(ValueError, "No boundary conditions"): + with pytest.raises(ValueError, match="No boundary conditions"): sp_meth.add_ghost_nodes(var, discretised_symbol, {}) - with self.assertRaisesRegex(ValueError, "boundary condition must be"): + with pytest.raises(ValueError, match="boundary condition must be"): sp_meth.add_neumann_values(var, discretised_symbol, bcs, var.domain) def test_add_ghost_nodes_concatenation(self): @@ -92,22 +92,14 @@ def test_add_ghost_nodes_concatenation(self): symbol_plus_ghost_both.evaluate(None, y_test)[1:-1], discretised_symbol.evaluate(None, y_test), ) - self.assertEqual( - ( - symbol_plus_ghost_both.evaluate(None, y_test)[0] - + symbol_plus_ghost_both.evaluate(None, y_test)[1] - ) - / 2, - 0, - ) - self.assertEqual( - ( - symbol_plus_ghost_both.evaluate(None, y_test)[-2] - + symbol_plus_ghost_both.evaluate(None, y_test)[-1] - ) - / 2, - 3, - ) + assert ( + symbol_plus_ghost_both.evaluate(None, y_test)[0] + + symbol_plus_ghost_both.evaluate(None, y_test)[1] + ) / 2 == 0 + assert ( + symbol_plus_ghost_both.evaluate(None, y_test)[-2] + + symbol_plus_ghost_both.evaluate(None, y_test)[-1] + ) / 2 == 3 def test_p2d_add_ghost_nodes(self): # create discretisation @@ -187,13 +179,3 @@ def test_p2d_add_ghost_nodes(self): np.testing.assert_array_equal( (c_s_p_ghost_eval[:, -2] + c_s_p_ghost_eval[:, -1]) / 2, 3 ) - - -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_spatial_methods/test_finite_volume/test_grad_div_shapes.py b/tests/unit/test_spatial_methods/test_finite_volume/test_grad_div_shapes.py index a1dd402f56..9e7f993e2a 100644 --- a/tests/unit/test_spatial_methods/test_finite_volume/test_grad_div_shapes.py +++ b/tests/unit/test_spatial_methods/test_finite_volume/test_grad_div_shapes.py @@ -10,10 +10,9 @@ get_cylindrical_mesh_for_testing, ) import numpy as np -import unittest -class TestFiniteVolumeGradDiv(unittest.TestCase): +class TestFiniteVolumeGradDiv: def test_grad_div_shapes_Dirichlet_bcs(self): """ Test grad and div with Dirichlet boundary conditions in Cartesian coordinates @@ -637,13 +636,3 @@ def test_grad_1plus1d(self): np.testing.assert_array_almost_equal( grad_eqn_disc.evaluate(None, linear_y), expected ) - - -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_spatial_methods/test_finite_volume/test_integration.py b/tests/unit/test_spatial_methods/test_finite_volume/test_integration.py index e9730a8eb7..bf6f44059a 100644 --- a/tests/unit/test_spatial_methods/test_finite_volume/test_integration.py +++ b/tests/unit/test_spatial_methods/test_finite_volume/test_integration.py @@ -2,6 +2,7 @@ # Tests for integration using Finite Volume method # +import pytest import pybamm from tests import ( get_mesh_for_testing, @@ -9,10 +10,9 @@ get_cylindrical_mesh_for_testing, ) import numpy as np -import unittest -class TestFiniteVolumeIntegration(unittest.TestCase): +class TestFiniteVolumeIntegration: def test_definite_integral(self): # create discretisation mesh = get_mesh_for_testing(xpts=200, rpts=200) @@ -37,7 +37,7 @@ def test_definite_integral(self): submesh = mesh[("negative electrode", "separator")] constant_y = np.ones_like(submesh.nodes[:, np.newaxis]) - self.assertEqual(integral_eqn_disc.evaluate(None, constant_y), ln + ls) + assert integral_eqn_disc.evaluate(None, constant_y) == ln + ls linear_y = submesh.nodes np.testing.assert_array_almost_equal( integral_eqn_disc.evaluate(None, linear_y), (ln + ls) ** 2 / 2 @@ -56,10 +56,10 @@ def test_definite_integral(self): submesh = mesh[("separator", "positive electrode")] constant_y = np.ones_like(submesh.nodes[:, np.newaxis]) - self.assertEqual(integral_eqn_disc.evaluate(None, constant_y), ls + lp) + assert integral_eqn_disc.evaluate(None, constant_y) == ls + lp linear_y = submesh.nodes - self.assertAlmostEqual( - integral_eqn_disc.evaluate(None, linear_y)[0][0], (1 - (ln) ** 2) / 2 + assert integral_eqn_disc.evaluate(None, linear_y)[0][0] == pytest.approx( + (1 - (ln) ** 2) / 2 ) cos_y = np.cos(submesh.nodes[:, np.newaxis]) np.testing.assert_array_almost_equal( @@ -122,9 +122,9 @@ def test_definite_integral(self): # test failure for secondary dimension column form finite_volume = pybamm.FiniteVolume() finite_volume.build(mesh) - with self.assertRaisesRegex( + with pytest.raises( NotImplementedError, - "Integral in secondary vector only implemented in 'row' form", + match="Integral in secondary vector only implemented in 'row' form", ): finite_volume.definite_integral_matrix(var, "column", "secondary") @@ -293,14 +293,14 @@ def test_definite_integral_vector(self): # row (default) vec = pybamm.DefiniteIntegralVector(var) vec_disc = disc.process_symbol(vec) - self.assertEqual(vec_disc.shape[0], 1) - self.assertEqual(vec_disc.shape[1], mesh["negative electrode"].npts) + assert vec_disc.shape[0] == 1 + assert vec_disc.shape[1] == mesh["negative electrode"].npts # column vec = pybamm.DefiniteIntegralVector(var, vector_type="column") vec_disc = disc.process_symbol(vec) - self.assertEqual(vec_disc.shape[0], mesh["negative electrode"].npts) - self.assertEqual(vec_disc.shape[1], 1) + assert vec_disc.shape[0] == mesh["negative electrode"].npts + assert vec_disc.shape[1] == 1 def test_indefinite_integral(self): # create discretisation @@ -340,7 +340,7 @@ def test_indefinite_integral(self): phi_approx = int_grad_phi_disc.evaluate(None, phi_exact) phi_approx += 1 # add constant of integration np.testing.assert_array_almost_equal(phi_exact, phi_approx) - self.assertEqual(left_boundary_value_disc.evaluate(y=phi_exact), 0) + assert left_boundary_value_disc.evaluate(y=phi_exact) == 0 # linear case phi_exact = submesh.nodes[:, np.newaxis] phi_approx = int_grad_phi_disc.evaluate(None, phi_exact) @@ -380,7 +380,7 @@ def test_indefinite_integral(self): phi_approx = int_grad_phi_disc.evaluate(None, phi_exact) phi_approx += 1 # add constant of integration np.testing.assert_array_almost_equal(phi_exact, phi_approx) - self.assertEqual(left_boundary_value_disc.evaluate(y=phi_exact), 0) + assert left_boundary_value_disc.evaluate(y=phi_exact) == 0 # linear case phi_exact = submesh.nodes[:, np.newaxis] - submesh.edges[0] @@ -441,7 +441,7 @@ def test_indefinite_integral(self): c_approx = c_integral_disc.evaluate(None, c_exact) c_approx += 1 # add constant of integration np.testing.assert_array_almost_equal(c_exact, c_approx) - self.assertEqual(left_boundary_value_disc.evaluate(y=c_exact), 0) + assert left_boundary_value_disc.evaluate(y=c_exact) == 0 # linear case c_exact = submesh.nodes[:, np.newaxis] @@ -489,7 +489,7 @@ def test_backward_indefinite_integral(self): phi_approx = int_grad_phi_disc.evaluate(None, phi_exact) phi_approx += 1 # add constant of integration np.testing.assert_array_almost_equal(phi_exact, phi_approx) - self.assertEqual(right_boundary_value_disc.evaluate(y=phi_exact), 0) + assert right_boundary_value_disc.evaluate(y=phi_exact) == 0 # linear case phi_exact = submesh.nodes - submesh.edges[-1] @@ -583,9 +583,9 @@ def test_indefinite_integral_on_nodes(self): int_c = pybamm.IndefiniteIntegral(c, r) disc.set_variable_slices([c]) - with self.assertRaisesRegex( + with pytest.raises( NotImplementedError, - "Indefinite integral on a spherical polar domain is not implemented", + match="Indefinite integral on a spherical polar domain is not implemented", ): disc.process_symbol(int_c) @@ -655,13 +655,3 @@ def test_forward_plus_backward_integral(self): full_int_phi_disc.evaluate(y=phi_exact).flatten(), int_plus_back_int_phi_disc.evaluate(y=phi_exact).flatten(), ) - - -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_spatial_methods/test_scikit_finite_element.py b/tests/unit/test_spatial_methods/test_scikit_finite_element.py index 18c941517b..42b282e08a 100644 --- a/tests/unit/test_spatial_methods/test_scikit_finite_element.py +++ b/tests/unit/test_spatial_methods/test_scikit_finite_element.py @@ -2,21 +2,21 @@ # Test for the operator class # +import pytest import pybamm from tests import get_2p1d_mesh_for_testing, get_unit_2p1D_mesh_for_testing import numpy as np -import unittest -class TestScikitFiniteElement(unittest.TestCase): +class TestScikitFiniteElement: def test_not_implemented(self): mesh = get_2p1d_mesh_for_testing(include_particles=False) spatial_method = pybamm.ScikitFiniteElement() spatial_method.build(mesh) - self.assertEqual(spatial_method.mesh, mesh) - with self.assertRaises(NotImplementedError): + assert spatial_method.mesh == mesh + with pytest.raises(NotImplementedError): spatial_method.divergence(None, None, None) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): spatial_method.indefinite_integral(None, None, None) def test_discretise_equations(self): @@ -100,7 +100,7 @@ def test_discretise_equations(self): "positive tab": (pybamm.Scalar(1), "Other BC"), } } - with self.assertRaises(ValueError): + with pytest.raises(ValueError): eqn_disc = disc.process_symbol(eqn) disc.bcs = { var: { @@ -108,19 +108,19 @@ def test_discretise_equations(self): "positive tab": (pybamm.Scalar(1), "Neumann"), } } - with self.assertRaises(ValueError): + with pytest.raises(ValueError): eqn_disc = disc.process_symbol(eqn) # raise ModelError if no BCs provided new_var = pybamm.Variable("new_var", domain="current collector") disc.set_variable_slices([new_var]) eqn = pybamm.laplacian(new_var) - with self.assertRaises(pybamm.ModelError): + with pytest.raises(pybamm.ModelError): eqn_disc = disc.process_symbol(eqn) # check GeometryError if using scikit-fem not in y or z x = pybamm.SpatialVariable("x", ["current collector"]) - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): disc.process_symbol(x) def test_gradient(self): @@ -389,14 +389,14 @@ def test_definite_integral_vector(self): # row (default) vec = pybamm.DefiniteIntegralVector(var) vec_disc = disc.process_symbol(vec) - self.assertEqual(vec_disc.shape[0], 1) - self.assertEqual(vec_disc.shape[1], mesh["current collector"].npts) + assert vec_disc.shape[0] == 1 + assert vec_disc.shape[1] == mesh["current collector"].npts # column vec = pybamm.DefiniteIntegralVector(var, vector_type="column") vec_disc = disc.process_symbol(vec) - self.assertEqual(vec_disc.shape[0], mesh["current collector"].npts) - self.assertEqual(vec_disc.shape[1], 1) + assert vec_disc.shape[0] == mesh["current collector"].npts + assert vec_disc.shape[1] == 1 def test_neg_pos(self): mesh = get_2p1d_mesh_for_testing(include_particles=False) @@ -423,7 +423,7 @@ def test_neg_pos(self): # test BoundaryGradient not implemented extrap_neg = pybamm.BoundaryGradient(var, "negative tab") - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): disc.process_symbol(extrap_neg) def test_boundary_integral(self): @@ -562,13 +562,3 @@ def test_disc_spatial_var(self): # spatial vars should discretise to the flattend meshgrid np.testing.assert_array_equal(y_disc.evaluate(), y_actual) np.testing.assert_array_equal(z_disc.evaluate(), z_actual) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_spatial_methods/test_spectral_volume.py b/tests/unit/test_spatial_methods/test_spectral_volume.py index f6a631e84c..1fd97c2ebd 100644 --- a/tests/unit/test_spatial_methods/test_spectral_volume.py +++ b/tests/unit/test_spatial_methods/test_spectral_volume.py @@ -2,9 +2,9 @@ # Test for the operator class # +import pytest import pybamm import numpy as np -import unittest def get_mesh_for_testing( @@ -87,10 +87,10 @@ def get_1p1d_mesh_for_testing( ) -class TestSpectralVolume(unittest.TestCase): +class TestSpectralVolume: def test_exceptions(self): sp_meth = pybamm.SpectralVolume() - with self.assertRaises(ValueError): + with pytest.raises(ValueError): sp_meth.chebyshev_differentiation_matrices(3, 3) mesh = get_mesh_for_testing() @@ -104,14 +104,14 @@ def test_exceptions(self): sp_meth.build(mesh) bcs = {"left": (pybamm.Scalar(0), "x"), "right": (pybamm.Scalar(3), "Neumann")} - with self.assertRaisesRegex(ValueError, "boundary condition must be"): + with pytest.raises(ValueError, match="boundary condition must be"): sp_meth.replace_dirichlet_values(var, discretised_symbol, bcs) - with self.assertRaisesRegex(ValueError, "boundary condition must be"): + with pytest.raises(ValueError, match="boundary condition must be"): sp_meth.replace_neumann_values(var, discretised_symbol, bcs) bcs = {"left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(3), "x")} - with self.assertRaisesRegex(ValueError, "boundary condition must be"): + with pytest.raises(ValueError, match="boundary condition must be"): sp_meth.replace_dirichlet_values(var, discretised_symbol, bcs) - with self.assertRaisesRegex(ValueError, "boundary condition must be"): + with pytest.raises(ValueError, match="boundary condition must be"): sp_meth.replace_neumann_values(var, discretised_symbol, bcs) def test_grad_div_shapes_Dirichlet_bcs(self): @@ -628,13 +628,3 @@ def test_grad_1plus1d(self): np.testing.assert_array_almost_equal( grad_eqn_disc.evaluate(None, linear_y), expected ) - - -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()