Skip to content

Commit 5cd16f0

Browse files
authored
feat: Add function to resample a driven control (#201)
Alternative to #200. Even though this one is a bit less consistent with BOULDER OPAL, I think it's better. For a start, it means the user can also benefit from resampling for the export-to-file function too, which I would have thought should be very useful. Also, I don't think consistency with BOULDER OPAL is that important here, because DrivenControl is a very different beast to a plain old pulse.
1 parent fb2dd23 commit 5cd16f0

File tree

2 files changed

+87
-0
lines changed

2 files changed

+87
-0
lines changed

qctrlopencontrols/driven_controls/driven_control.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,51 @@ def duration(self) -> float:
330330

331331
return np.sum(self.durations)
332332

333+
def resample(self, time_step: float, name: Optional[str] = None) -> "DrivenControl":
334+
r"""
335+
Returns a new driven control obtained by resampling this control.
336+
337+
Parameters
338+
----------
339+
time_step : float
340+
The time step to use for resampling, :math:`\delta t`.
341+
name : str, optional
342+
The name for the new control. Defaults to ``None``.
343+
344+
Returns
345+
-------
346+
DrivenControl
347+
A new driven control, sampled at the specified rate. The durations of the new control
348+
are all equal to :math:`\delta t`. The total duration of the new control might be
349+
slightly larger than the original duration, if the time step doesn't exactly divide the
350+
original duration.
351+
"""
352+
check_arguments(
353+
time_step > 0,
354+
"Time step must be positive.",
355+
{"time_step": time_step},
356+
)
357+
check_arguments(
358+
time_step <= self.duration,
359+
"Time step must be less than or equal to the original duration.",
360+
{"time_step": time_step},
361+
{"duration": self.duration},
362+
)
363+
364+
count = int(np.ceil(self.duration / time_step))
365+
durations = [time_step] * count
366+
times = np.arange(count) * time_step
367+
368+
indices = np.digitize(times, bins=np.cumsum(self.durations))
369+
370+
return DrivenControl(
371+
durations,
372+
self.rabi_rates[indices],
373+
self.azimuthal_angles[indices],
374+
self.detunings[indices],
375+
name,
376+
)
377+
333378
def _qctrl_expanded_export_content(self, coordinates: str) -> Dict:
334379
"""
335380
Prepare the content to be saved in Q-CTRL expanded format.

tests/test_driven_controls.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,3 +406,45 @@ def test_pretty_print():
406406
expected_string = "\n".join(_pretty_string)
407407

408408
assert str(driven_control) == expected_string
409+
410+
411+
def test_resample_exact():
412+
driven_control = DrivenControl(
413+
rabi_rates=[0, 2],
414+
azimuthal_angles=[1.5, 0.5],
415+
detunings=[1.3, 2.3],
416+
durations=[1, 1],
417+
name="control",
418+
)
419+
420+
new_driven_control = driven_control.resample(0.5)
421+
422+
assert len(new_driven_control.durations) == 4
423+
424+
assert np.allclose(new_driven_control.durations, 0.5)
425+
assert np.allclose(new_driven_control.rabi_rates, [0, 0, 2, 2])
426+
assert np.allclose(new_driven_control.azimuthal_angles, [1.5, 1.5, 0.5, 0.5])
427+
assert np.allclose(new_driven_control.detunings, [1.3, 1.3, 2.3, 2.3])
428+
429+
430+
def test_resample_inexact():
431+
driven_control = DrivenControl(
432+
rabi_rates=[0, 2],
433+
azimuthal_angles=[1.5, 0.5],
434+
detunings=[1.3, 2.3],
435+
durations=[1, 1],
436+
name="control",
437+
)
438+
439+
new_driven_control = driven_control.resample(0.3)
440+
441+
assert len(new_driven_control.durations) == 7
442+
443+
assert np.allclose(new_driven_control.durations, 0.3)
444+
assert np.allclose(new_driven_control.rabi_rates, [0, 0, 0, 0, 2, 2, 2])
445+
assert np.allclose(
446+
new_driven_control.azimuthal_angles, [1.5, 1.5, 1.5, 1.5, 0.5, 0.5, 0.5]
447+
)
448+
assert np.allclose(
449+
new_driven_control.detunings, [1.3, 1.3, 1.3, 1.3, 2.3, 2.3, 2.3]
450+
)

0 commit comments

Comments
 (0)