Repository: pybamm-team/PyBaMM (python)

Issue:
[Bug]: [24.5rc0] Simulations with `pybamm.Experiment` terminate after 24 h
### PyBaMM Version

24.5rc0

### Python Version

3.9.13

### Describe the bug

Simulations using `pybamm.Experiment` with a voltage cut-off only appear to terminate with a time cut-off after 24 h.

I'm afraid I haven't had time to get into logs to figure out why this is happening!

### Steps to Reproduce

The following simulation should proceed for approximately 100 h = 360000 s, until the specified voltage cut-off. However, it terminates after 24 h.

```python
import pybamm

parameter_values = pybamm.ParameterValues("Chen2020")
model = pybamm.lithium_ion.DFN()
experiment = pybamm.Experiment(
    [
        "Discharge at 0.01C until 2.5 V",
    ]
)

sim = pybamm.Simulation(
    model,
    parameter_values=parameter_values,
    experiment=experiment,
)

sol = sim.solve()
sim.plot()

print(f'Last time step at {sim.solution["Time [s]"].entries[-1] / 3600} h')
```

### Relevant log output

_No response_

Pull request:
Issue 4224 duration bug
# Description

Fixes #4224

- ~~longer default duration (1000 hours)~~ 
- default duration is 2 / C-rate hours for C-rate steps
- warning if step finishes due to hitting the default duration

Currently working on adding a warning if maximum duration is reached

## Type of change

Please add a line in the relevant section of [CHANGELOG.md](https://github.com/pybamm-team/PyBaMM/blob/develop/CHANGELOG.md) to document the change (include PR #) - note reverse order of PR #s. If necessary, also add to the list of breaking changes.

- [ ] New feature (non-breaking change which adds functionality)
- [ ] Optimization (back-end change that speeds up the code)
- [ ] Bug fix (non-breaking change which fixes an issue)

# Key checklist:

- [ ] No style issues: `$ pre-commit run` (or `$ nox -s pre-commit`) (see [CONTRIBUTING.md](https://github.com/pybamm-team/PyBaMM/blob/develop/CONTRIBUTING.md#installing-and-using-pre-commit) for how to set this up to run automatically when committing locally, in just two lines of code)
- [ ] All tests pass: `$ python run-tests.py --all` (or `$ nox -s tests`)
- [ ] The documentation builds: `$ python run-tests.py --doctest` (or `$ nox -s doctests`)

You can run integration tests, unit tests, and doctests together at once, using `$ python run-tests.py --quick` (or `$ nox -s quick`).

## Further checks:

- [ ] Code is commented, particularly in hard-to-understand areas
- [ ] Tests added that prove fix is effective or that feature works


Patch:
diff --git a/docs/source/examples/notebooks/getting_started/tutorial-5-run-experiments.ipynb b/docs/source/examples/notebooks/getting_started/tutorial-5-run-experiments.ipynb
index eb82f5971..85be34e42 100644
--- a/docs/source/examples/notebooks/getting_started/tutorial-5-run-experiments.ipynb
+++ b/docs/source/examples/notebooks/getting_started/tutorial-5-run-experiments.ipynb
@@ -25,18 +25,8 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "\n",
-      "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.3.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m24.0\u001b[0m\n",
-      "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n",
       "Note: you may need to restart the kernel to use updated packages.\n"
      ]
-    },
-    {
-     "name": "stderr",
-     "output_type": "stream",
-     "text": [
-      "An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu.\n"
-     ]
     }
    ],
    "source": [
@@ -163,18 +153,19 @@
      "name": "stderr",
      "output_type": "stream",
      "text": [
-      "At t = 522.66 and h = 1.1556e-13, the corrector convergence failed repeatedly or with |h| = hmin.\n"
+      "At t = 339.952 and h = 1.4337e-18, the corrector convergence failed repeatedly or with |h| = hmin.\n",
+      "At t = 522.687 and h = 4.04917e-14, the corrector convergence failed repeatedly or with |h| = hmin.\n"
      ]
     },
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "5ab1f22de6af4878b6ca43d27ffc01c5",
+       "model_id": "93feca98298f4111909ae487e2a1e273",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "interactive(children=(FloatSlider(value=0.0, description='t', max=40.132949019384355, step=0.40132949019384356…"
+       "interactive(children=(FloatSlider(value=0.0, description='t', max=40.13268704803602, step=0.4013268704803602),…"
       ]
      },
      "metadata": {},
@@ -183,7 +174,7 @@
     {
      "data": {
       "text/plain": [
-       "<pybamm.plotting.quick_plot.QuickPlot at 0x7f1a11f76690>"
+       "<pybamm.plotting.quick_plot.QuickPlot at 0x16520e690>"
       ]
      },
      "execution_count": 5,
@@ -211,12 +202,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "7cdac234d74241a2814918053454f6a6",
+       "model_id": "4d6e43032f4e4aa6be5843c4916b4b50",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "interactive(children=(FloatSlider(value=0.0, description='t', max=13.076977041121545, step=0.13076977041121546…"
+       "interactive(children=(FloatSlider(value=0.0, description='t', max=13.076887099589111, step=0.1307688709958911)…"
       ]
      },
      "metadata": {},
@@ -225,7 +216,7 @@
     {
      "data": {
       "text/plain": [
-       "<pybamm.plotting.quick_plot.QuickPlot at 0x7f1a105ecb50>"
+       "<pybamm.plotting.quick_plot.QuickPlot at 0x1696cf290>"
       ]
      },
      "execution_count": 6,
@@ -255,7 +246,7 @@
     {
      "data": {
       "text/plain": [
-       "_Step(C-rate, 1.0, duration=1 hour, period=1 minute, temperature=25oC, tags=['tag1'], description=Discharge at 1C for 1 hour)"
+       "Step(1.0, duration=1 hour, period=1 minute, temperature=25oC, tags=['tag1'], description=Discharge at 1C for 1 hour, direction=Discharge)"
       ]
      },
      "execution_count": 7,
@@ -293,7 +284,7 @@
     {
      "data": {
       "text/plain": [
-       "_Step(current, 1, duration=1 hour, termination=2.5 V)"
+       "Step(1, duration=1 hour, termination=2.5 V, direction=Discharge)"
       ]
      },
      "execution_count": 8,
@@ -321,7 +312,7 @@
     {
      "data": {
       "text/plain": [
-       "_Step(current, 1.0, duration=1 hour, termination=2.5V, description=Discharge at 1A for 1 hour or until 2.5V)"
+       "Step(1.0, duration=1 hour, termination=2.5V, description=Discharge at 1A for 1 hour or until 2.5V, direction=Discharge)"
       ]
      },
      "execution_count": 9,
@@ -348,10 +339,78 @@
    "execution_count": 10,
    "metadata": {},
    "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "2024-07-10 14:41:02.625 - [WARNING] callbacks.on_experiment_infeasible_time(240): \n",
+      "\n",
+      "\tExperiment is infeasible: default duration (1.0 seconds) was reached during 'Step([[ 0.00000000e+00  0.00000000e+00]\n",
+      " [ 1.69491525e-02  5.31467428e-02]\n",
+      " [ 3.38983051e-02  1.05691312e-01]\n",
+      " [ 5.08474576e-02  1.57038356e-01]\n",
+      " [ 6.77966102e-02  2.06606093e-01]\n",
+      " [ 8.47457627e-02  2.53832900e-01]\n",
+      " [ 1.01694915e-01  2.98183679e-01]\n",
+      " [ 1.18644068e-01  3.39155918e-01]\n",
+      " [ 1.35593220e-01  3.76285385e-01]\n",
+      " [ 1.52542373e-01  4.09151388e-01]\n",
+      " [ 1.69491525e-01  4.37381542e-01]\n",
+      " [ 1.86440678e-01  4.60655989e-01]\n",
+      " [ 2.03389831e-01  4.78711019e-01]\n",
+      " [ 2.20338983e-01  4.91342062e-01]\n",
+      " [ 2.37288136e-01  4.98406004e-01]\n",
+      " [ 2.54237288e-01  4.99822806e-01]\n",
+      " [ 2.71186441e-01  4.95576416e-01]\n",
+      " [ 2.88135593e-01  4.85714947e-01]\n",
+      " [ 3.05084746e-01  4.70350133e-01]\n",
+      " [ 3.22033898e-01  4.49656065e-01]\n",
+      " [ 3.38983051e-01  4.23867214e-01]\n",
+      " [ 3.55932203e-01  3.93275778e-01]\n",
+      " [ 3.72881356e-01  3.58228370e-01]\n",
+      " [ 3.89830508e-01  3.19122092e-01]\n",
+      " [ 4.06779661e-01  2.76400033e-01]\n",
+      " [ 4.23728814e-01  2.30546251e-01]\n",
+      " [ 4.40677966e-01  1.82080288e-01]\n",
+      " [ 4.57627119e-01  1.31551282e-01]\n",
+      " [ 4.74576271e-01  7.95317480e-02]\n",
+      " [ 4.91525424e-01  2.66110874e-02]\n",
+      " [ 5.08474576e-01 -2.66110874e-02]\n",
+      " [ 5.25423729e-01 -7.95317480e-02]\n",
+      " [ 5.42372881e-01 -1.31551282e-01]\n",
+      " [ 5.59322034e-01 -1.82080288e-01]\n",
+      " [ 5.76271186e-01 -2.30546251e-01]\n",
+      " [ 5.93220339e-01 -2.76400033e-01]\n",
+      " [ 6.10169492e-01 -3.19122092e-01]\n",
+      " [ 6.27118644e-01 -3.58228370e-01]\n",
+      " [ 6.44067797e-01 -3.93275778e-01]\n",
+      " [ 6.61016949e-01 -4.23867214e-01]\n",
+      " [ 6.77966102e-01 -4.49656065e-01]\n",
+      " [ 6.94915254e-01 -4.70350133e-01]\n",
+      " [ 7.11864407e-01 -4.85714947e-01]\n",
+      " [ 7.28813559e-01 -4.95576416e-01]\n",
+      " [ 7.45762712e-01 -4.99822806e-01]\n",
+      " [ 7.62711864e-01 -4.98406004e-01]\n",
+      " [ 7.79661017e-01 -4.91342062e-01]\n",
+      " [ 7.96610169e-01 -4.78711019e-01]\n",
+      " [ 8.13559322e-01 -4.60655989e-01]\n",
+      " [ 8.30508475e-01 -4.37381542e-01]\n",
+      " [ 8.47457627e-01 -4.09151388e-01]\n",
+      " [ 8.64406780e-01 -3.76285385e-01]\n",
+      " [ 8.81355932e-01 -3.39155918e-01]\n",
+      " [ 8.98305085e-01 -2.98183679e-01]\n",
+      " [ 9.15254237e-01 -2.53832900e-01]\n",
+      " [ 9.32203390e-01 -2.06606093e-01]\n",
+      " [ 9.49152542e-01 -1.57038356e-01]\n",
+      " [ 9.66101695e-01 -1.05691312e-01]\n",
+      " [ 9.83050847e-01 -5.31467428e-02]\n",
+      " [ 1.00000000e+00 -1.22464680e-16]], duration=1.0, period=0.016949152542372836, direction=Rest)'. The returned solution only contains up to step 1 of cycle 1. Please specify a duration in the step instructions.\n"
+     ]
+    },
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "730d5e19b17e447ebde5679de68c46ef",
+       "model_id": "6364b4579fc447e2a607f2f8414172ba",
        "version_major": 2,
        "version_minor": 0
       },
@@ -365,7 +424,7 @@
     {
      "data": {
       "text/plain": [
-       "<pybamm.plotting.quick_plot.QuickPlot at 0x7f1a1a4c2ed0>"
+       "<pybamm.plotting.quick_plot.QuickPlot at 0x16a1d8d90>"
       ]
      },
      "execution_count": 10,
@@ -419,13 +478,14 @@
      "output_type": "stream",
      "text": [
       "[1] 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",
-      "[2] 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",
-      "[3] 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",
-      "[4] 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",
-      "[5] Peyman Mohtat, Suhak Lee, Jason B Siegel, and Anna G Stefanopoulou. Towards better estimability of electrode-specific state of health: decoding the cell expansion. Journal of Power Sources, 427:101–111, 2019.\n",
-      "[6] 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",
-      "[7] Pauli Virtanen, Ralf Gommers, Travis E. Oliphant, Matt Haberland, Tyler Reddy, David Cournapeau, Evgeni Burovski, Pearu Peterson, Warren Weckesser, Jonathan Bright, and others. SciPy 1.0: fundamental algorithms for scientific computing in Python. Nature Methods, 17(3):261–272, 2020. doi:10.1038/s41592-019-0686-2.\n",
-      "[8] Andrew Weng, Jason B Siegel, and Anna Stefanopoulou. Differential voltage analysis for battery manufacturing process control. arXiv preprint arXiv:2303.07088, 2023.\n",
+      "[2] 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",
+      "[3] 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",
+      "[4] 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",
+      "[5] 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",
+      "[6] Peyman Mohtat, Suhak Lee, Jason B Siegel, and Anna G Stefanopoulou. Towards better estimability of electrode-specific state of health: decoding the cell expansion. Journal of Power Sources, 427:101–111, 2019.\n",
+      "[7] 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",
+      "[8] Pauli Virtanen, Ralf Gommers, Travis E. Oliphant, Matt Haberland, Tyler Reddy, David Cournapeau, Evgeni Burovski, Pearu Peterson, Warren Weckesser, Jonathan Bright, and others. SciPy 1.0: fundamental algorithms for scientific computing in Python. Nature Methods, 17(3):261–272, 2020. doi:10.1038/s41592-019-0686-2.\n",
+      "[9] Andrew Weng, Jason B Siegel, and Anna Stefanopoulou. Differential voltage analysis for battery manufacturing process control. arXiv preprint arXiv:2303.07088, 2023.\n",
       "\n"
      ]
     }
diff --git a/pybamm/callbacks.py b/pybamm/callbacks.py
index 57f2426d0..4e8c67c8b 100644
--- a/pybamm/callbacks.py
+++ b/pybamm/callbacks.py
@@ -36,37 +36,37 @@ class Callback:
         """
         Called at the start of an experiment simulation.
         """
-        pass
+        pass  # pragma: no cover
 
     def on_cycle_start(self, logs):
         """
         Called at the start of each cycle in an experiment simulation.
         """
-        pass
+        pass  # pragma: no cover
 
     def on_step_start(self, logs):
         """
         Called at the start of each step in an experiment simulation.
         """
-        pass
+        pass  # pragma: no cover
 
     def on_step_end(self, logs):
         """
         Called at the end of each step in an experiment simulation.
         """
-        pass
+        pass  # pragma: no cover
 
     def on_cycle_end(self, logs):
         """
         Called at the end of each cycle in an experiment simulation.
         """
-        pass
+        pass  # pragma: no cover
 
     def on_experiment_end(self, logs):
         """
         Called at the end of an experiment simulation.
         """
-        pass
+        pass  # pragma: no cover
 
     def on_experiment_error(self, logs):
         """
@@ -75,13 +75,19 @@ class Callback:
         For example, this could be used to send an error alert with a bug report when
         running batch simulations in the cloud.
         """
-        pass
+        pass  # pragma: no cover
 
-    def on_experiment_infeasible(self, logs):
+    def on_experiment_infeasible_time(self, logs):
         """
-        Called when an experiment simulation is infeasible.
+        Called when an experiment simulation is infeasible due to reaching maximum time.
         """
-        pass
+        pass  # pragma: no cover
+
+    def on_experiment_infeasible_event(self, logs):
+        """
+        Called when an experiment simulation is infeasible due to an event.
+        """
+        pass  # pragma: no cover
 
 
 ########################################################################################
@@ -226,7 +232,19 @@ class LoggingCallback(Callback):
         error = logs["error"]
         pybamm.logger.error(f"Simulation error: {error}")
 
-    def on_experiment_infeasible(self, logs):
+    def on_experiment_infeasible_time(self, logs):
+        duration = logs["step duration"]
+        cycle_num = logs["cycle number"][0]
+        step_num = logs["step number"][0]
+        operating_conditions = logs["step operating conditions"]
+        self.logger.warning(
+            f"\n\n\tExperiment is infeasible: default duration ({duration} seconds) "
+            f"was reached during '{operating_conditions}'. The returned solution only "
+            f"contains up to step {step_num} of cycle {cycle_num}. "
+            "Please specify a duration in the step instructions."
+        )
+
+    def on_experiment_infeasible_event(self, logs):
         termination = logs["termination"]
         cycle_num = logs["cycle number"][0]
         step_num = logs["step number"][0]
diff --git a/pybamm/experiment/step/base_step.py b/pybamm/experiment/step/base_step.py
index 16224a6af..6b77bed2c 100644
--- a/pybamm/experiment/step/base_step.py
+++ b/pybamm/experiment/step/base_step.py
@@ -71,6 +71,8 @@ class BaseStep:
         description=None,
         direction=None,
     ):
+        self.input_duration = duration
+        self.input_value = value
         # Check if drive cycle
         is_drive_cycle = isinstance(value, np.ndarray)
         is_python_function = callable(value)
@@ -100,8 +102,11 @@ class BaseStep:
                     f"Input function must return a real number output at t = {t0}"
                 )
 
+        # Record whether the step uses the default duration
+        # This will be used by the experiment to check whether the step is feasible
+        self.uses_default_duration = duration is None
         # Set duration
-        if duration is None:
+        if self.uses_default_duration:
             duration = self.default_duration(value)
         self.duration = _convert_time_to_seconds(duration)
 
@@ -195,8 +200,8 @@ class BaseStep:
             A copy of the step.
         """
         return self.__class__(
-            self.value,
-            duration=self.duration,
+            self.input_value,
+            duration=self.input_duration,
             termination=self.termination,
             period=self.period,
             temperature=self.temperature,
@@ -259,7 +264,7 @@ class BaseStep:
             t = value[:, 0]
             return t[-1]
         else:
-            return 24 * 3600  # 24 hours in seconds
+            return 24 * 3600  # one day in seconds
 
     def process_model(self, model, parameter_values):
         new_model = model.new_copy()
@@ -411,10 +416,16 @@ _type_to_units = {
 
 def _convert_time_to_seconds(time_and_units):
     """Convert a time in seconds, minutes or hours to a time in seconds"""
-    # If the time is a number, assume it is in seconds
-    if isinstance(time_and_units, numbers.Number) or time_and_units is None:
+    if time_and_units is None:
         return time_and_units
 
+    # If the time is a number, assume it is in seconds
+    if isinstance(time_and_units, numbers.Number):
+        if time_and_units <= 0:
+            raise ValueError("time must be positive")
+        else:
+            return time_and_units
+
     # Split number and units
     units = time_and_units.lstrip("0123456789.- ")
     time = time_and_units[: -len(units)]
diff --git a/pybamm/experiment/step/steps.py b/pybamm/experiment/step/steps.py
index e3322104b..e66178dc8 100644
--- a/pybamm/experiment/step/steps.py
+++ b/pybamm/experiment/step/steps.py
@@ -156,6 +156,11 @@ class CRate(BaseStepExplicit):
     def current_value(self, variables):
         return self.value * pybamm.Parameter("Nominal cell capacity [A.h]")
 
+    def default_duration(self, value):
+        # "value" is C-rate, so duration is "1 / value" hours in seconds
+        # with a 2x safety factor
+        return 1 / abs(value) * 3600 * 2
+
 
 def c_rate(value, **kwargs):
     """
diff --git a/pybamm/simulation.py b/pybamm/simulation.py
index 8ec85d67d..a55310870 100644
--- a/pybamm/simulation.py
+++ b/pybamm/simulation.py
@@ -679,6 +679,7 @@ class Simulation:
 
                     logs["step number"] = (step_num, cycle_length)
                     logs["step operating conditions"] = step_str
+                    logs["step duration"] = step.duration
                     callbacks.on_step_start(logs)
 
                     inputs = {
@@ -767,23 +768,33 @@ class Simulation:
                     callbacks.on_step_end(logs)
 
                     logs["termination"] = step_solution.termination
-                    # Only allow events specified by experiment
-                    if not (
+
+                    # Check for some cases that would make the experiment end early
+                    if step_termination == "final time" and step.uses_default_duration:
+                        # reached the default duration of a step (typically we should
+                        # reach an event before the default duration)
+                        callbacks.on_experiment_infeasible_time(logs)
+                        feasible = False
+                        break
+
+                    elif not (
                         isinstance(step_solution, pybamm.EmptySolution)
                         or step_termination == "final time"
                         or "[experiment]" in step_termination
                     ):
-                        callbacks.on_experiment_infeasible(logs)
+                        # Step has reached an event that is not specified in the
+                        # experiment
+                        callbacks.on_experiment_infeasible_event(logs)
                         feasible = False
                         break
 
-                    if time_stop is not None:
-                        max_time = cycle_solution.t[-1]
-                        if max_time >= time_stop:
-                            break
+                    elif time_stop is not None and logs["experiment time"] >= time_stop:
+                        # reached the time limit of the experiment
+                        break
 
-                    # Increment index for next iteration
-                    idx += 1
+                    else:
+                        # Increment index for next iteration, then continue
+                        idx += 1
 
                 if save_this_cycle or feasible is False:
                     self._solution = self._solution + cycle_solution


Test patch:
diff --git a/tests/unit/test_callbacks.py b/tests/unit/test_callbacks.py
index b36fef9ec..649c7d9ec 100644
--- a/tests/unit/test_callbacks.py
+++ b/tests/unit/test_callbacks.py
@@ -81,6 +81,7 @@ class TestCallbacks(TestCase):
             "cycle number": (5, 12),
             "step number": (1, 4),
             "elapsed time": 0.45,
+            "step duration": 1,
             "step operating conditions": "Charge",
             "termination": "event",
         }
@@ -96,10 +97,14 @@ class TestCallbacks(TestCase):
         with open("test_callback.log") as f:
             self.assertIn("Cycle 5/12, step 1/4", f.read())
 
-        callback.on_experiment_infeasible(logs)
+        callback.on_experiment_infeasible_event(logs)
         with open("test_callback.log") as f:
             self.assertIn("Experiment is infeasible: 'event'", f.read())
 
+        callback.on_experiment_infeasible_time(logs)
+        with open("test_callback.log") as f:
+            self.assertIn("Experiment is infeasible: default duration", f.read())
+
         callback.on_experiment_end(logs)
         with open("test_callback.log") as f:
             self.assertIn("took 0.45", f.read())
diff --git a/tests/unit/test_experiments/test_experiment_steps.py b/tests/unit/test_experiments/test_experiment_steps.py
index 9d1abcc13..4bb686986 100644
--- a/tests/unit/test_experiments/test_experiment_steps.py
+++ b/tests/unit/test_experiments/test_experiment_steps.py
@@ -43,15 +43,20 @@ class TestExperimentSteps(unittest.TestCase):
         with self.assertRaisesRegex(ValueError, "temperature units"):
             step = pybamm.step.current(1, temperature="298T")
 
+        with self.assertRaisesRegex(ValueError, "time must be positive"):
+            pybamm.step.current(1, duration=0)
+
     def test_specific_steps(self):
         current = pybamm.step.current(1)
         self.assertIsInstance(current, pybamm.step.Current)
         self.assertEqual(current.value, 1)
         self.assertEqual(str(current), repr(current))
+        self.assertEqual(current.duration, 24 * 3600)
 
         c_rate = pybamm.step.c_rate(1)
         self.assertIsInstance(c_rate, pybamm.step.CRate)
         self.assertEqual(c_rate.value, 1)
+        self.assertEqual(c_rate.duration, 3600 * 2)
 
         voltage = pybamm.step.voltage(1)
         self.assertIsInstance(voltage, pybamm.step.Voltage)
@@ -145,19 +150,19 @@ class TestExperimentSteps(unittest.TestCase):
             {
                 "type": "CRate",
                 "value": -1,
-                "duration": 86400,
+                "duration": 7200,
                 "termination": [pybamm.step.VoltageTermination(4.1)],
             },
             {
                 "value": 4.1,
                 "type": "Voltage",
-                "duration": 86400,
+                "duration": 3600 * 24,
                 "termination": [pybamm.step.CurrentTermination(0.05)],
             },
             {
                 "value": 3,
                 "type": "Voltage",
-                "duration": 86400,
+                "duration": 3600 * 24,
                 "termination": [pybamm.step.CrateTermination(0.02)],
             },
             {
diff --git a/tests/unit/test_experiments/test_simulation_with_experiment.py b/tests/unit/test_experiments/test_simulation_with_experiment.py
index bfc5ad6de..4b3fa3366 100644
--- a/tests/unit/test_experiments/test_simulation_with_experiment.py
+++ b/tests/unit/test_experiments/test_simulation_with_experiment.py
@@ -10,6 +10,12 @@ import unittest
 from datetime import datetime
 
 
+class ShortDurationCRate(pybamm.step.CRate):
+    def default_duration(self, value):
+        # Set a short default duration for testing early stopping due to infeasible time
+        return 1
+
+
 class TestSimulationExperiment(TestCase):
     def test_set_up(self):
         experiment = pybamm.Experiment(
@@ -272,6 +278,19 @@ class TestSimulationExperiment(TestCase):
         # Different callback - this is for coverage on the `Callback` class
         sol = sim.solve(callbacks=pybamm.callbacks.Callback())
 
+    def test_run_experiment_infeasible_time(self):
+        experiment = pybamm.Experiment(
+            [ShortDurationCRate(1, termination="2.5V"), "Rest for 1 hour"]
+        )
+        model = pybamm.lithium_ion.SPM()
+        parameter_values = pybamm.ParameterValues("Chen2020")
+        sim = pybamm.Simulation(
+            model, parameter_values=parameter_values, experiment=experiment
+        )
+        sol = sim.solve()
+        self.assertEqual(len(sol.cycles), 1)
+        self.assertEqual(len(sol.cycles[0].steps), 1)
+
     def test_run_experiment_termination_capacity(self):
         # with percent
         experiment = pybamm.Experiment(