Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions qctrlopencontrols/driven_controls/driven_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,51 @@ def duration(self) -> float:

return np.sum(self.durations)

def resample(self, time_step: float, name: Optional[str] = None) -> "DrivenControl":
r"""
Returns a new driven control obtained by resampling this control.

Parameters
----------
time_step : float
The time step to use for resampling, :math:`\delta t`.
name : str, optional
The name for the new control. Defaults to ``None``.

Returns
-------
DrivenControl
A new driven control, sampled at the specified rate. The durations of the new control
are all equal to :math:`\delta t`. The total duration of the new control might be
slightly larger than the original duration, if the time step doesn't exactly divide the
original duration.
"""
check_arguments(
time_step > 0,
"Time step must be positive.",
{"time_step": time_step},
)
check_arguments(
time_step <= self.duration,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the time_step is larger than the minimum segment duration time, but smaller than duration? I guess we don't expect it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was thinking that's just a case we need to accept. Even for a sensible time_step, you can still get a wonky sampling if it doesn't exactly divide a segment duration, which I think is unavoidable.

"Time step must be less than or equal to the original duration.",
{"time_step": time_step},
{"duration": self.duration},
)

count = int(np.ceil(self.duration / time_step))
durations = [time_step] * count
times = np.arange(count) * time_step

indices = np.digitize(times, bins=np.cumsum(self.durations))

return DrivenControl(
durations,
self.rabi_rates[indices],
self.azimuthal_angles[indices],
self.detunings[indices],
name,
)

def _qctrl_expanded_export_content(self, coordinates: str) -> Dict:
"""
Prepare the content to be saved in Q-CTRL expanded format.
Expand Down
42 changes: 42 additions & 0 deletions tests/test_driven_controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,3 +406,45 @@ def test_pretty_print():
expected_string = "\n".join(_pretty_string)

assert str(driven_control) == expected_string


def test_resample_exact():
driven_control = DrivenControl(
rabi_rates=[0, 2],
azimuthal_angles=[1.5, 0.5],
detunings=[1.3, 2.3],
durations=[1, 1],
name="control",
)

new_driven_control = driven_control.resample(0.5)

assert len(new_driven_control.durations) == 4

assert np.allclose(new_driven_control.durations, 0.5)
assert np.allclose(new_driven_control.rabi_rates, [0, 0, 2, 2])
assert np.allclose(new_driven_control.azimuthal_angles, [1.5, 1.5, 0.5, 0.5])
assert np.allclose(new_driven_control.detunings, [1.3, 1.3, 2.3, 2.3])


def test_resample_inexact():
driven_control = DrivenControl(
rabi_rates=[0, 2],
azimuthal_angles=[1.5, 0.5],
detunings=[1.3, 2.3],
durations=[1, 1],
name="control",
)

new_driven_control = driven_control.resample(0.3)

assert len(new_driven_control.durations) == 7

assert np.allclose(new_driven_control.durations, 0.3)
assert np.allclose(new_driven_control.rabi_rates, [0, 0, 0, 0, 2, 2, 2])
assert np.allclose(
new_driven_control.azimuthal_angles, [1.5, 1.5, 1.5, 1.5, 0.5, 0.5, 0.5]
)
assert np.allclose(
new_driven_control.detunings, [1.3, 1.3, 1.3, 1.3, 2.3, 2.3, 2.3]
)