{ "cells": [ { "cell_type": "markdown", "id": "d62d2eea", "metadata": {}, "source": [ "# Running experiments\n", "\n", "An experiment is a sequence of steps that operate a battery through its controls, the inlet\n", "temperature and the mass flow rate. Each step either holds both controls\n", "(`Inlet`) or holds the power by adjusting one of them (`Power`). Steps end\n", "after a duration or when a termination condition is met, and they can be grouped into\n", "cycles. The result is an ordinary `pybamm.Solution`.\n", "\n", "Power is positive on discharge (heat delivered by the battery), so a charge at constant\n", "power uses a negative power.\n", "\n", "[Tutorial 6](getting_started/tutorial_6_experiments.ipynb) introduces experiments." ] }, { "cell_type": "code", "execution_count": 1, "id": "e5ebbd94", "metadata": { "execution": { "iopub.execute_input": "2026-10-23T18:21:45.018162Z", "iopub.status.busy": "2026-10-23T18:21:45.018005Z", "iopub.status.idle": "2026-10-23T18:21:46.311439Z", "shell.execute_reply": "2026-10-23T18:21:46.310296Z" } }, "outputs": [], "source": [ "import numpy as np\n", "import pybamm\n", "\n", "import pytes\n", "from pytes.experiments import Experiment, run_experiment\n", "from pytes.experiments import step as s\n", "\n", "model = pytes.SharpFrontModel()\n", "mdot = 0.02 # the default mass flow rate [kg.s-1]\n", "T_cold, T_hot = 273.15 + 10, 273.15 + 75" ] }, { "cell_type": "markdown", "id": "8ca423e2", "metadata": {}, "source": [ "The battery starts uniform at 70 °C. A cycle discharges it at 1 kW with water at 10 °C,\n", "adjusting the flow rate, until the flow would exceed twice the nominal one; rests for half an\n", "hour with no flow; and charges it at 1 kW at the nominal flow, adjusting the inlet\n", "temperature, until the inlet would exceed 75 °C or the outlet reaches 65 °C. The cycle runs\n", "twice." ] }, { "cell_type": "code", "execution_count": 2, "id": "cc1ea913", "metadata": { "execution": { "iopub.execute_input": "2026-10-23T18:21:46.314277Z", "iopub.status.busy": "2026-10-23T18:21:46.313972Z", "iopub.status.idle": "2026-10-23T18:21:49.313443Z", "shell.execute_reply": "2026-10-23T18:21:49.312473Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "cycle 1: Discharge at 1 kW 139.4 min\n", "cycle 1: Rest for 30 min 30.0 min\n", "cycle 1: Charge at 1 kW 98.3 min\n", "cycle 2: Discharge at 1 kW 99.0 min\n", "cycle 2: Rest for 30 min 30.0 min\n", "cycle 2: Charge at 1 kW 99.0 min\n" ] } ], "source": [ "discharge = s.Power(\n", " 1e3,\n", " inlet_temperature=T_cold,\n", " termination=s.mass_flow_rate_above(2 * mdot),\n", " description=\"Discharge at 1 kW\",\n", ")\n", "rest = s.Rest(duration=1800, description=\"Rest for 30 min\")\n", "charge = s.Power(\n", " -1e3,\n", " mass_flow_rate=mdot,\n", " termination=[\n", " s.inlet_temperature_above(T_hot),\n", " s.outlet_temperature_above(T_hot - 10),\n", " ],\n", " description=\"Charge at 1 kW\",\n", ")\n", "experiment = Experiment([(discharge, rest, charge)] * 2)\n", "solution = run_experiment(model, experiment)\n", "\n", "# As in PyBaMM, solution.cycles[i].steps[j] is the solution of experiment.cycles[i][j]\n", "for i, (cycle, cycle_solution) in enumerate(\n", " zip(experiment.cycles, solution.cycles, strict=True)\n", "):\n", " for step, step_solution in zip(cycle, cycle_solution.steps, strict=True):\n", " duration = (step_solution.t[-1] - step_solution.t[0]) / 60\n", " print(f\"cycle {i + 1}: {step.description:20s} {duration:5.1f} min\")" ] }, { "cell_type": "markdown", "id": "e31ddf60", "metadata": {}, "source": [ "`Power` holds the power by adjusting the control that is not given: the flow rate\n", "in the discharge, the inlet temperature in the charge." ] }, { "cell_type": "code", "execution_count": 3, "id": "fd8b7d0f", "metadata": { "execution": { "iopub.execute_input": "2026-10-23T18:21:49.315166Z", "iopub.status.busy": "2026-10-23T18:21:49.314991Z", "iopub.status.idle": "2026-10-23T18:21:50.053037Z", "shell.execute_reply": "2026-10-23T18:21:50.052166Z" } }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "81c898efeaf242ce8922233263200fa3", "version_major": 2, "version_minor": 0 }, "text/plain": [ "interactive(children=(FloatSlider(value=0.0, description='t', max=8.263858761068837, step=0.08263858761068837)…" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pybamm.dynamic_plot(\n", " solution,\n", " [\n", " [\"Inlet temperature [degC]\", \"Outlet temperature [degC]\"],\n", " \"Mass flow rate [kg.s-1]\",\n", " \"Power [kW]\",\n", " \"Discharged energy [kWh]\",\n", " ],\n", " time_unit=\"hours\",\n", ")" ] }, { "cell_type": "markdown", "id": "07ee9e58", "metadata": {}, "source": [ "The throughputs count the energy delivered while discharging and the energy put in while\n", "charging. The models have no heat losses yet, so the two differ only by the change in\n", "stored energy: the first cycle starts full and its charge stops before the battery is full\n", "again, so it delivers more than it takes back; the second starts and ends in nearly the same\n", "state, so its ratio, the round-trip efficiency, is about 1. The pumped volume is the water\n", "used, as a flow meter would total it." ] }, { "cell_type": "code", "execution_count": 4, "id": "c7c58c44", "metadata": { "execution": { "iopub.execute_input": "2026-10-23T18:21:50.056142Z", "iopub.status.busy": "2026-10-23T18:21:50.055999Z", "iopub.status.idle": "2026-10-23T18:21:50.068414Z", "shell.execute_reply": "2026-10-23T18:21:50.067381Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "cycle 1: 2.32 kWh out, 1.64 kWh in (ratio 1.42), 162 l pumped\n", "cycle 2: 1.65 kWh out, 1.65 kWh in (ratio 1.00), 152 l pumped\n" ] } ], "source": [ "for i, cycle in enumerate(solution.cycles, start=1):\n", " out = cycle[\"Discharged throughput [kWh]\"].entries\n", " put_in = cycle[\"Charged throughput [kWh]\"].entries\n", " pumped = cycle[\"Pumped volume [l]\"].entries\n", " energy_out, energy_in = out[-1] - out[0], put_in[-1] - put_in[0]\n", " print(\n", " f\"cycle {i}: {energy_out:.2f} kWh out, {energy_in:.2f} kWh in \"\n", " f\"(ratio {energy_out / energy_in:.2f}), {pumped[-1] - pumped[0]:.0f} l pumped\"\n", " )" ] }, { "cell_type": "markdown", "id": "c9d2b399", "metadata": {}, "source": [ "Step values can also be time series (a 2-column array of time since the start of the step\n", "and value) or functions of the time since the start of the step, for example a slowly\n", "rising inlet temperature." ] }, { "cell_type": "code", "execution_count": 5, "id": "30de7eff", "metadata": { "execution": { "iopub.execute_input": "2026-10-23T18:21:50.070003Z", "iopub.status.busy": "2026-10-23T18:21:50.069866Z", "iopub.status.idle": "2026-10-23T18:21:51.058784Z", "shell.execute_reply": "2026-10-23T18:21:51.057473Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "inlet temperature at 0, 30 and 60 min [degC]: [10. 20. 30.]\n" ] } ], "source": [ "ramp = s.Inlet(lambda t: T_cold + 20 * t / 3600, mdot, duration=3600)\n", "ramped = run_experiment(model, Experiment([ramp]))\n", "print(\n", " \"inlet temperature at 0, 30 and 60 min [degC]:\",\n", " ramped[\"Inlet temperature [degC]\"](np.array([0, 1800, 3600])),\n", ")" ] } ], "metadata": { "kernelspec": { "display_name": "pytes (3.14.4)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.14.4" } }, "nbformat": 4, "nbformat_minor": 5 }