Repository: pybamm-team/PyBaMM

Issue:
[Bug]: C-rate drive cycle calls wrong default duration
### PyBaMM Version

25.6

### Python Version

3.12

### Describe the bug

Using a `pybamm.step.CRate` with a drive cycle and no explicit `duration` argument doesn't call the `default_duration()` method in the `BaseStep` class which is supposed to handle drive cycles, but rather its own function which is correct for single C-rate values only.

Should change `CRate.default_duration()` to:

```python
def default_duration(self, value):
    if isinstance(value, np.ndarray):
        t = value[:, 0]
        return t[-1]
    else:
        # "value" is C-rate, so duration is "1 / value" hours in seconds
        # with a 2x safety factor
        return 1 / abs(value) * 3600 * 2

```

### Steps to Reproduce

```python
t = np.linspace(0, 100, 101)
I = np.sin(t)
drive_cycle = np.column_stack([t, I])
exp = pybamm.Experiment([pybamm.step.CRate(drive_cycle)])
```

### Relevant log output

```shell
AttributeError: 'numpy.ndarray' object has no attribute 'lstrip'
```
[Bug]: Period for drive cycle experiment step gets overwritten by data
### PyBaMM Version

25.6

### Python Version

3.12

### Describe the bug

Defining an experiment step with a drive cycle array seems to ignore any manually defined t_eval period. It just uses the period of the data (or in the case of non-uniform timesteps, it looks like it does a fixed period of total_duration/N_points)

I tried specifying the `period` in both the step and the experiment, it doesn't make a difference.

### Steps to Reproduce

```python

import pybamm
import numpy as np

t = np.linspace(0, 100, 101)
I = np.sin(t)
drive_cycle = np.column_stack([t, I])
exp = pybamm.Experiment([pybamm.step.CRate(drive_cycle, duration=100, period=0.05)], period=0.05)

model = pybamm.lithium_ion.SPMe()
params = model.default_parameter_values

sim = pybamm.Simulation(model, exp, parameter_values=params, solver=pybamm.IDAKLUSolver())
sol = sim.solve(initial_soc=0.5)


sol["Time [s]"].entries # this has the period of the linspace
```

### Relevant log output

```shell

```


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49275b2ee6..b453e8c009 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
 
 ## Bug fixes
 
+- Fixed a bug that ignored the default duration of drive cycles for `CRate` steps and a bug that overwrote custom `period` arguments for drive cycles. ([#5090](https://github.com/pybamm-team/PyBaMM/pull/5090))
 - Converts sensitivities to numpy objects, fixing bug in `DiscreteTimeSum` sensitivity calculation ([#5037](https://github.com/pybamm-team/PyBaMM/pull/5037))
 - Raises error if `pybamm.Interpolant` given 1D x values that are not strictly increasing ([#5061](https://github.com/pybamm-team/PyBaMM/pull/5061))
 
diff --git a/src/pybamm/experiment/step/base_step.py b/src/pybamm/experiment/step/base_step.py
index 73704e1580..07c7bb1231 100644
--- a/src/pybamm/experiment/step/base_step.py
+++ b/src/pybamm/experiment/step/base_step.py
@@ -147,7 +147,12 @@ def __init__(
                 pybamm.t - pybamm.InputParameter("start time"),
                 name="Drive Cycle",
             )
-            self.period = np.diff(t).min()
+            if period is None:
+                # Infer the period from the drive cycle
+                self.period = np.diff(t).min()
+            else:
+                self.period = _convert_time_to_seconds(period)
+
         elif is_python_function:
             t = pybamm.t - pybamm.InputParameter("start time")
             self.value = value(t)
@@ -272,6 +277,12 @@ def __eq__(self, other):
     def __hash__(self):
         return hash(self.basic_repr())
 
+    def _default_timespan(self, value):
+        """
+        Default timespan for the step is one day (24 hours).
+        """
+        return 24 * 3600
+
     def default_duration(self, value):
         """
         Default duration for the step is one day (24 hours) or the duration of the
@@ -281,7 +292,7 @@ def default_duration(self, value):
             t = value[:, 0]
             return t[-1]
         else:
-            return 24 * 3600  # one day in seconds
+            return self._default_timespan(value)
 
     @staticmethod
     def default_period():
diff --git a/src/pybamm/experiment/step/steps.py b/src/pybamm/experiment/step/steps.py
index dc26f98c18..880f687817 100644
--- a/src/pybamm/experiment/step/steps.py
+++ b/src/pybamm/experiment/step/steps.py
@@ -157,7 +157,7 @@ def __init__(self, value, **kwargs):
     def current_value(self, variables):
         return self.value * pybamm.Parameter("Nominal cell capacity [A.h]")
 
-    def default_duration(self, value):
+    def _default_timespan(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


Test patch:
diff --git a/tests/unit/test_experiments/test_experiment_steps.py b/tests/unit/test_experiments/test_experiment_steps.py
index e2bdcf43c8..8d31594d89 100644
--- a/tests/unit/test_experiments/test_experiment_steps.py
+++ b/tests/unit/test_experiments/test_experiment_steps.py
@@ -228,6 +228,20 @@ def test_drive_cycle_duration(self):
         assert drive_cycle_step.period == 1
         assert drive_cycle_step.temperature == 273.15 - 5
 
+        # Check that the default c_rate duration is the length of the drive cycle
+        drive_cycle_step_c_rate = pybamm.step.c_rate(drive_cycle)
+        assert drive_cycle_step_c_rate.duration == 9
+
+    def test_drive_cycle_period(self):
+        # Import drive cycle from file
+        drive_cycle = np.array([np.arange(10), np.arange(10)]).T
+
+        drive_cycle_step = pybamm.step.current(drive_cycle, period=0.01)
+        assert drive_cycle_step.period == 0.01
+
+        drive_cycle_step_no_period = pybamm.step.current(drive_cycle)
+        assert drive_cycle_step_no_period.period == 1
+
     def test_bad_strings(self):
         with pytest.raises(TypeError, match="Input to step.string"):
             pybamm.step.string(1)
diff --git a/tests/unit/test_experiments/test_simulation_with_experiment.py b/tests/unit/test_experiments/test_simulation_with_experiment.py
index 6a388cde34..253a06a981 100644
--- a/tests/unit/test_experiments/test_simulation_with_experiment.py
+++ b/tests/unit/test_experiments/test_simulation_with_experiment.py
@@ -9,7 +9,7 @@
 
 
 class ShortDurationCRate(pybamm.step.CRate):
-    def default_duration(self, value):
+    def _default_timespan(self, value):
         # Set a short default duration for testing early stopping due to infeasible time
         return 1