From 227e34dc6ac981bfe9b94c238078ee9324a880a5 Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Thu, 29 Jul 2021 18:36:05 +1000 Subject: [PATCH 1/4] feat: Add function to sample a driven control --- .../driven_controls/driven_control.py | 77 +++++++++++++++++ tests/test_driven_controls.py | 84 +++++++++++++++++++ 2 files changed, 161 insertions(+) diff --git a/qctrlopencontrols/driven_controls/driven_control.py b/qctrlopencontrols/driven_controls/driven_control.py index c14fc92a..ece106e0 100644 --- a/qctrlopencontrols/driven_controls/driven_control.py +++ b/qctrlopencontrols/driven_controls/driven_control.py @@ -20,10 +20,13 @@ from typing import ( Dict, Optional, + Tuple, + Union, ) import numpy as np +from ..exceptions import ArgumentsValueError from ..utils import ( Coordinate, FileFormat, @@ -330,6 +333,80 @@ def duration(self) -> float: return np.sum(self.durations) + def sample( + self, + sample_times: Union[np.ndarray, float], + coordinates: str = Coordinate.CYLINDRICAL.value, + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + r""" + Returns samples from the control. + + Parameters + ---------- + sample_times : np.ndarray or float + The times at which to sample, :math:`\{t_n\}`. You can pass either an array of times or + a single number :math:`dt`. In the latter case, the sample times are taken as + :math:`0, dt, 2dt, \dots`. + coordinates : str, optional + The coordinate system in which to return the sampled control. Must be 'cylindrical' or + 'cartesian'. Defaults to 'cylindrical'. + + Returns + ------- + np.ndarray + For cylindrical coordinates, the sampled Rabi rates :math:`\{\Omega(t_n)\}`. For + Cartesian coordinates, the sampled x-amplitudes, :math:`\{\Omega(t_n) \cos \phi(t_n)\}`. + np.ndarray + For cylindrical coordinates, the sampled azimuthal angles :math:\{\phi(t_n)\}`. For + Cartesian coordinates, The sampled y-amplitudes, :math:`\{\Omega(t_n) \sin \phi(t_n)\}`. + np.ndarray + The sampled detunings, :math:`\{\Delta(t_n)\}`. + + Raises + ------ + ArgumentsValueError + If the inputs are invalid. + """ + times = np.asarray(sample_times) + if times.shape == (): + times = np.arange(0, self.duration, times) + + check_arguments( + len(times.shape) == 1, + "Sample times must be a 1D array or a single number.", + {"sample_times": sample_times}, + ) + + coordinate_systems = [v.value for v in Coordinate] + check_arguments( + coordinates in coordinate_systems, + "Requested coordinate type is not supported. Please use " + "one of {}".format(coordinate_systems), + {"coordinates": coordinates}, + ) + + indices = np.digitize(times, bins=np.cumsum(self.durations), right=True) + + if coordinates == Coordinate.CARTESIAN.value: + return ( + self.amplitude_x[indices], + self.amplitude_y[indices], + self.detunings[indices], + ) + + if coordinates == Coordinate.CYLINDRICAL.value: + return ( + self.rabi_rates[indices], + self.azimuthal_angles[indices], + self.detunings[indices], + ) + + raise ArgumentsValueError( + "Requested coordinate type is not supported. Please use " + f"one of {Coordinate.CARTESIAN.value!r} and {Coordinate.CYLINDRICAL.value!r}", + {"coordinates": coordinates}, + ) + def _qctrl_expanded_export_content(self, coordinates: str) -> Dict: """ Prepare the content to be saved in Q-CTRL expanded format. diff --git a/tests/test_driven_controls.py b/tests/test_driven_controls.py index e14f7647..3cbc59f2 100644 --- a/tests/test_driven_controls.py +++ b/tests/test_driven_controls.py @@ -406,3 +406,87 @@ def test_pretty_print(): expected_string = "\n".join(_pretty_string) assert str(driven_control) == expected_string + + +def test_sample_dt_cylindrical(): + driven_control = DrivenControl( + rabi_rates=[0, 2], + azimuthal_angles=[1.5, 0.5], + detunings=[1.3, 2.3], + durations=[1, 1], + name="control", + ) + + rabi_rates, azimuthal_angles, detunings = driven_control.sample(0.3) + + assert len(rabi_rates) == 7 + assert len(azimuthal_angles) == 7 + assert len(detunings) == 7 + + assert np.allclose(rabi_rates, [0, 0, 0, 0, 2, 2, 2]) + assert np.allclose(azimuthal_angles, [1.5, 1.5, 1.5, 1.5, 0.5, 0.5, 0.5]) + assert np.allclose(detunings, [1.3, 1.3, 1.3, 1.3, 2.3, 2.3, 2.3]) + + +def test_sample_dt_cartesian(): + driven_control = DrivenControl( + rabi_rates=[3, 2], + azimuthal_angles=[0, np.pi / 2], + detunings=[1.3, 2.3], + durations=[1, 1], + name="control", + ) + + x_amplitudes, y_amplitudes, detunings = driven_control.sample(0.3, "cartesian") + + assert len(x_amplitudes) == 7 + assert len(y_amplitudes) == 7 + assert len(detunings) == 7 + + assert np.allclose(x_amplitudes, [3, 3, 3, 3, 0, 0, 0]) + assert np.allclose(y_amplitudes, [0, 0, 0, 0, 2, 2, 2]) + assert np.allclose(detunings, [1.3, 1.3, 1.3, 1.3, 2.3, 2.3, 2.3]) + + +def test_sample_cylindrical(): + driven_control = DrivenControl( + rabi_rates=[0, 2], + azimuthal_angles=[1.5, 0.5], + detunings=[1.3, 2.3], + durations=[1, 1], + name="control", + ) + + rabi_rates, azimuthal_angles, detunings = driven_control.sample( + np.array([0.5, 0.8, 1.5]) + ) + + assert len(rabi_rates) == 3 + assert len(azimuthal_angles) == 3 + assert len(detunings) == 3 + + assert np.allclose(rabi_rates, [0, 0, 2]) + assert np.allclose(azimuthal_angles, [1.5, 1.5, 0.5]) + assert np.allclose(detunings, [1.3, 1.3, 2.3]) + + +def test_sample_cartesian(): + driven_control = DrivenControl( + rabi_rates=[3, 2], + azimuthal_angles=[0, np.pi / 2], + detunings=[1.3, 2.3], + durations=[1, 1], + name="control", + ) + + x_amplitudes, y_amplitudes, detunings = driven_control.sample( + np.array([0.5, 0.8, 1.5]), "cartesian" + ) + + assert len(x_amplitudes) == 3 + assert len(y_amplitudes) == 3 + assert len(detunings) == 3 + + assert np.allclose(x_amplitudes, [3, 3, 0]) + assert np.allclose(y_amplitudes, [0, 0, 2]) + assert np.allclose(detunings, [1.3, 1.3, 2.3]) From 3b34c8a3c52d64ddefbf269b25a366605208089e Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Thu, 29 Jul 2021 18:37:04 +1000 Subject: [PATCH 2/4] remove block --- qctrlopencontrols/driven_controls/driven_control.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/qctrlopencontrols/driven_controls/driven_control.py b/qctrlopencontrols/driven_controls/driven_control.py index ece106e0..7b03c732 100644 --- a/qctrlopencontrols/driven_controls/driven_control.py +++ b/qctrlopencontrols/driven_controls/driven_control.py @@ -377,14 +377,6 @@ def sample( {"sample_times": sample_times}, ) - coordinate_systems = [v.value for v in Coordinate] - check_arguments( - coordinates in coordinate_systems, - "Requested coordinate type is not supported. Please use " - "one of {}".format(coordinate_systems), - {"coordinates": coordinates}, - ) - indices = np.digitize(times, bins=np.cumsum(self.durations), right=True) if coordinates == Coordinate.CARTESIAN.value: From f9ecf11c82a16ef15ce80b59536e30219063f5f1 Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Fri, 30 Jul 2021 09:09:47 +1000 Subject: [PATCH 3/4] feat: Add function to resample a driven control --- .../driven_controls/driven_control.py | 83 +++++++------------ tests/test_driven_controls.py | 74 ++++------------- 2 files changed, 46 insertions(+), 111 deletions(-) diff --git a/qctrlopencontrols/driven_controls/driven_control.py b/qctrlopencontrols/driven_controls/driven_control.py index 7b03c732..bc1817a1 100644 --- a/qctrlopencontrols/driven_controls/driven_control.py +++ b/qctrlopencontrols/driven_controls/driven_control.py @@ -20,8 +20,6 @@ from typing import ( Dict, Optional, - Tuple, - Union, ) import numpy as np @@ -333,70 +331,49 @@ def duration(self) -> float: return np.sum(self.durations) - def sample( - self, - sample_times: Union[np.ndarray, float], - coordinates: str = Coordinate.CYLINDRICAL.value, - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + def resample(self, dt: float, name: Optional[str] = None) -> "DrivenControl": r""" - Returns samples from the control. + Returns a new driven control obtained by resampling this control. Parameters ---------- - sample_times : np.ndarray or float - The times at which to sample, :math:`\{t_n\}`. You can pass either an array of times or - a single number :math:`dt`. In the latter case, the sample times are taken as - :math:`0, dt, 2dt, \dots`. - coordinates : str, optional - The coordinate system in which to return the sampled control. Must be 'cylindrical' or - 'cartesian'. Defaults to 'cylindrical'. + dt : float + The time step to use for resampling, :math:`\delta t`. + name : str, optional + The name for the new control. Defaults to ``None``. Returns ------- - np.ndarray - For cylindrical coordinates, the sampled Rabi rates :math:`\{\Omega(t_n)\}`. For - Cartesian coordinates, the sampled x-amplitudes, :math:`\{\Omega(t_n) \cos \phi(t_n)\}`. - np.ndarray - For cylindrical coordinates, the sampled azimuthal angles :math:\{\phi(t_n)\}`. For - Cartesian coordinates, The sampled y-amplitudes, :math:`\{\Omega(t_n) \sin \phi(t_n)\}`. - np.ndarray - The sampled detunings, :math:`\{\Delta(t_n)\}`. - - Raises - ------ - ArgumentsValueError - If the inputs are invalid. + 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. """ - times = np.asarray(sample_times) - if times.shape == (): - times = np.arange(0, self.duration, times) - check_arguments( - len(times.shape) == 1, - "Sample times must be a 1D array or a single number.", - {"sample_times": sample_times}, + dt > 0, + "Time step must be positive.", + {"dt": dt}, + ) + check_arguments( + dt <= self.duration, + "Time step must be less than or equal to the original duration.", + {"dt": dt}, + {"duration": self.duration}, ) - indices = np.digitize(times, bins=np.cumsum(self.durations), right=True) + count = int(np.ceil(self.duration / dt)) + durations = [dt] * count + times = np.arange(count) * dt - if coordinates == Coordinate.CARTESIAN.value: - return ( - self.amplitude_x[indices], - self.amplitude_y[indices], - self.detunings[indices], - ) + indices = np.digitize(times, bins=np.cumsum(self.durations)) - if coordinates == Coordinate.CYLINDRICAL.value: - return ( - self.rabi_rates[indices], - self.azimuthal_angles[indices], - self.detunings[indices], - ) - - raise ArgumentsValueError( - "Requested coordinate type is not supported. Please use " - f"one of {Coordinate.CARTESIAN.value!r} and {Coordinate.CYLINDRICAL.value!r}", - {"coordinates": coordinates}, + return DrivenControl( + durations, + self.rabi_rates[indices], + self.azimuthal_angles[indices], + self.detunings[indices], + name, ) def _qctrl_expanded_export_content(self, coordinates: str) -> Dict: diff --git a/tests/test_driven_controls.py b/tests/test_driven_controls.py index 3cbc59f2..d30796f7 100644 --- a/tests/test_driven_controls.py +++ b/tests/test_driven_controls.py @@ -408,7 +408,7 @@ def test_pretty_print(): assert str(driven_control) == expected_string -def test_sample_dt_cylindrical(): +def test_resample_exact(): driven_control = DrivenControl( rabi_rates=[0, 2], azimuthal_angles=[1.5, 0.5], @@ -417,38 +417,17 @@ def test_sample_dt_cylindrical(): name="control", ) - rabi_rates, azimuthal_angles, detunings = driven_control.sample(0.3) + new_driven_control = driven_control.resample(0.5) - assert len(rabi_rates) == 7 - assert len(azimuthal_angles) == 7 - assert len(detunings) == 7 + assert len(new_driven_control.durations) == 4 - assert np.allclose(rabi_rates, [0, 0, 0, 0, 2, 2, 2]) - assert np.allclose(azimuthal_angles, [1.5, 1.5, 1.5, 1.5, 0.5, 0.5, 0.5]) - assert np.allclose(detunings, [1.3, 1.3, 1.3, 1.3, 2.3, 2.3, 2.3]) + 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_sample_dt_cartesian(): - driven_control = DrivenControl( - rabi_rates=[3, 2], - azimuthal_angles=[0, np.pi / 2], - detunings=[1.3, 2.3], - durations=[1, 1], - name="control", - ) - - x_amplitudes, y_amplitudes, detunings = driven_control.sample(0.3, "cartesian") - - assert len(x_amplitudes) == 7 - assert len(y_amplitudes) == 7 - assert len(detunings) == 7 - - assert np.allclose(x_amplitudes, [3, 3, 3, 3, 0, 0, 0]) - assert np.allclose(y_amplitudes, [0, 0, 0, 0, 2, 2, 2]) - assert np.allclose(detunings, [1.3, 1.3, 1.3, 1.3, 2.3, 2.3, 2.3]) - - -def test_sample_cylindrical(): +def test_resample_inexact(): driven_control = DrivenControl( rabi_rates=[0, 2], azimuthal_angles=[1.5, 0.5], @@ -457,36 +436,15 @@ def test_sample_cylindrical(): name="control", ) - rabi_rates, azimuthal_angles, detunings = driven_control.sample( - np.array([0.5, 0.8, 1.5]) - ) - - assert len(rabi_rates) == 3 - assert len(azimuthal_angles) == 3 - assert len(detunings) == 3 - - assert np.allclose(rabi_rates, [0, 0, 2]) - assert np.allclose(azimuthal_angles, [1.5, 1.5, 0.5]) - assert np.allclose(detunings, [1.3, 1.3, 2.3]) + new_driven_control = driven_control.resample(0.3) + assert len(new_driven_control.durations) == 7 -def test_sample_cartesian(): - driven_control = DrivenControl( - rabi_rates=[3, 2], - azimuthal_angles=[0, np.pi / 2], - detunings=[1.3, 2.3], - durations=[1, 1], - name="control", + 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] ) - - x_amplitudes, y_amplitudes, detunings = driven_control.sample( - np.array([0.5, 0.8, 1.5]), "cartesian" + assert np.allclose( + new_driven_control.detunings, [1.3, 1.3, 1.3, 1.3, 2.3, 2.3, 2.3] ) - - assert len(x_amplitudes) == 3 - assert len(y_amplitudes) == 3 - assert len(detunings) == 3 - - assert np.allclose(x_amplitudes, [3, 3, 0]) - assert np.allclose(y_amplitudes, [0, 0, 2]) - assert np.allclose(detunings, [1.3, 1.3, 2.3]) From 60872e6cf3fc932e9bc192d1e6e1fbade5e89fe2 Mon Sep 17 00:00:00 2001 From: Harry Slatyer Date: Fri, 30 Jul 2021 09:15:59 +1000 Subject: [PATCH 4/4] lint --- .../driven_controls/driven_control.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/qctrlopencontrols/driven_controls/driven_control.py b/qctrlopencontrols/driven_controls/driven_control.py index bc1817a1..c80536bc 100644 --- a/qctrlopencontrols/driven_controls/driven_control.py +++ b/qctrlopencontrols/driven_controls/driven_control.py @@ -24,7 +24,6 @@ import numpy as np -from ..exceptions import ArgumentsValueError from ..utils import ( Coordinate, FileFormat, @@ -331,13 +330,13 @@ def duration(self) -> float: return np.sum(self.durations) - def resample(self, dt: float, name: Optional[str] = None) -> "DrivenControl": + def resample(self, time_step: float, name: Optional[str] = None) -> "DrivenControl": r""" Returns a new driven control obtained by resampling this control. Parameters ---------- - dt : float + 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``. @@ -351,20 +350,20 @@ def resample(self, dt: float, name: Optional[str] = None) -> "DrivenControl": original duration. """ check_arguments( - dt > 0, + time_step > 0, "Time step must be positive.", - {"dt": dt}, + {"time_step": time_step}, ) check_arguments( - dt <= self.duration, + time_step <= self.duration, "Time step must be less than or equal to the original duration.", - {"dt": dt}, + {"time_step": time_step}, {"duration": self.duration}, ) - count = int(np.ceil(self.duration / dt)) - durations = [dt] * count - times = np.arange(count) * dt + 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))