Skip to content

Commit c4827e0

Browse files
authored
Add function to generate modulated Gaussian control (#110)
1 parent 1d7d60c commit c4827e0

File tree

2 files changed

+242
-0
lines changed

2 files changed

+242
-0
lines changed

qctrlopencontrols/driven_controls/predefined.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
WAMF1,
3939
)
4040
from ..exceptions import ArgumentsValueError
41+
from ..utils import check_arguments
4142
from .driven_control import DrivenControl
4243

4344

@@ -812,3 +813,128 @@ def _new_walsh_amplitude_modulated_filter_1_control(
812813
durations=durations,
813814
**kwargs,
814815
)
816+
817+
818+
def new_modulated_gaussian_control(
819+
maximum_rabi_rate: float,
820+
minimum_segment_duration: float,
821+
duration: float,
822+
modulation_frequency: float,
823+
) -> DrivenControl:
824+
"""
825+
Generate a Gaussian driven control sequence modulated by a sinusoidal signal at a specific
826+
frequency.
827+
828+
The net effect of this control sequence is an identity gate.
829+
830+
Parameters
831+
----------
832+
maximum_rabi_rate: float
833+
Maximum Rabi rate of the system.
834+
835+
minimum_segment_duration : float
836+
Minimum length of each segment in the control sequence.
837+
838+
duration : float
839+
Total duration of the control sequence.
840+
841+
modulation_frequency: float
842+
Frequency of the modulation sinusoidal signal.
843+
844+
Returns
845+
-------
846+
DrivenControl
847+
A control sequence as an instance of DrivenControl.
848+
"""
849+
850+
check_arguments(
851+
maximum_rabi_rate > 0.0,
852+
"Maximum Rabi rate must be greater than zero.",
853+
{"maximum_rabi_rate": maximum_rabi_rate},
854+
)
855+
856+
check_arguments(
857+
minimum_segment_duration > 0.0,
858+
"Minimum segment duration must be greater than zero.",
859+
{"minimum_segment_duration": minimum_segment_duration},
860+
)
861+
862+
check_arguments(
863+
duration > minimum_segment_duration,
864+
"Total duration must be greater than minimum segment duration.",
865+
{"duration": duration, "minimum_segment_duration": minimum_segment_duration,},
866+
)
867+
868+
# default spread of the gaussian shaped pulse as a fraction of its duration
869+
_pulse_width = 0.1
870+
871+
# default mean of the gaussian shaped pulse as a fraction of its duration
872+
_pulse_mean = 0.5
873+
874+
min_required_upper_bound = np.sqrt(2 * np.pi) / (_pulse_width * duration)
875+
check_arguments(
876+
maximum_rabi_rate >= min_required_upper_bound,
877+
"Maximum Rabi rate must be large enough to permit a 2Pi rotation.",
878+
{"maximum_rabi_rate": maximum_rabi_rate},
879+
extras={
880+
"minimum required value for upper_bound "
881+
"(sqrt(2pi)/(0.1*maximum_duration))": min_required_upper_bound
882+
},
883+
)
884+
885+
# work out exact segment duration
886+
segment_count = int(np.ceil(duration / minimum_segment_duration))
887+
segment_duration = duration / segment_count
888+
segment_start_times = np.arange(segment_count) * segment_duration
889+
segment_midpoints = segment_start_times + segment_duration / 2
890+
891+
# prepare a base gaussian shaped pulse
892+
gaussian_mean = _pulse_mean * duration
893+
gaussian_width = _pulse_width * duration
894+
base_gaussian_segments = np.exp(
895+
-0.5 * ((segment_midpoints - gaussian_mean) / gaussian_width) ** 2
896+
)
897+
898+
if modulation_frequency != 0:
899+
# prepare the modulation signals. We use sinusoids that are zero at the center of the pulse,
900+
# which ensures the pulses are antisymmetric about the center of the pulse and thus effect
901+
# a net zero rotation.
902+
modulation_signals = np.sin(
903+
2.0 * np.pi * modulation_frequency * (segment_midpoints - duration / 2)
904+
)
905+
# modulate the base gaussian
906+
modulated_gaussian_segments = base_gaussian_segments * modulation_signals
907+
908+
# maximum segment value
909+
pulse_segments_maximum = np.max(modulated_gaussian_segments)
910+
# normalize to maximum Rabi rate
911+
modulated_gaussian_segments = (
912+
maximum_rabi_rate * modulated_gaussian_segments / pulse_segments_maximum
913+
)
914+
else:
915+
# for the zero-frequency pulse, we need to produce the largest possible full rotation (i.e.
916+
# multiple of 2pi) while respecting the maximum Rabi rate. Note that if the maximum Rabi
917+
# rate does not permit even a single rotation (which could happen to a small degree due to
918+
# discretization issues) then we allow values to exceed the maximum Rabi rate.
919+
normalized_gaussian_segments = base_gaussian_segments / np.max(
920+
base_gaussian_segments
921+
)
922+
maximum_rotation_angle = (
923+
segment_duration * np.sum(normalized_gaussian_segments) * maximum_rabi_rate
924+
)
925+
maximum_full_rotation_angle = max(
926+
maximum_rotation_angle - maximum_rotation_angle % (2 * np.pi), 2 * np.pi
927+
)
928+
modulated_gaussian_segments = (
929+
normalized_gaussian_segments
930+
* maximum_rabi_rate
931+
* (maximum_full_rotation_angle / maximum_rotation_angle)
932+
)
933+
934+
azimuthal_angles = [0 if v >= 0 else np.pi for v in modulated_gaussian_segments]
935+
936+
return DrivenControl(
937+
rabi_rates=np.abs(modulated_gaussian_segments),
938+
azimuthal_angles=azimuthal_angles,
939+
durations=np.array([segment_duration] * segment_count),
940+
)

tests/test_predefined_driven_controls.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@
2828
CORPSE_IN_SK1,
2929
PRIMITIVE,
3030
SCROFULOUS,
31+
SIGMA_X,
3132
SK1,
3233
WAMF1,
3334
)
35+
from qctrlopencontrols.driven_controls.predefined import new_modulated_gaussian_control
3436
from qctrlopencontrols.exceptions import ArgumentsValueError
3537

3638

@@ -592,3 +594,117 @@ def test_walsh_control():
592594
)
593595

594596
assert np.allclose(pi_on_4_segments, _pi_on_4_segments)
597+
598+
599+
def test_modulated_gaussian_control():
600+
"""
601+
Tests modulated Gaussian control at different modulate frequencies.
602+
"""
603+
maximum_rabi_rate = 20 * 2 * np.pi
604+
minimum_segment_duration = 0.01
605+
maximum_duration = 0.2
606+
607+
# set modulation frequency to 0
608+
pulses_zero = new_modulated_gaussian_control(
609+
maximum_rabi_rate=maximum_rabi_rate,
610+
minimum_segment_duration=minimum_segment_duration,
611+
duration=maximum_duration,
612+
modulation_frequency=0,
613+
)
614+
pulse_zero_segments = [
615+
{"duration": d, "value": np.real(v)}
616+
for d, v in zip(
617+
pulses_zero.durations,
618+
pulses_zero.rabi_rates * np.exp(1j * pulses_zero.azimuthal_angles),
619+
)
620+
]
621+
622+
# set modulation frequency to 50/3
623+
pulses_non_zero = new_modulated_gaussian_control(
624+
maximum_rabi_rate=maximum_rabi_rate,
625+
minimum_segment_duration=minimum_segment_duration,
626+
duration=maximum_duration,
627+
modulation_frequency=50 / 3,
628+
)
629+
pulse_non_zero_segments = [
630+
{"duration": d, "value": np.real(v)}
631+
for d, v in zip(
632+
pulses_non_zero.durations,
633+
pulses_non_zero.rabi_rates * np.exp(1j * pulses_non_zero.azimuthal_angles),
634+
)
635+
]
636+
637+
# pulses should have 20 segments; 0.2/0.01 = 20
638+
assert len(pulse_zero_segments) == 20
639+
assert len(pulse_non_zero_segments) == 20
640+
641+
# determine the segment mid-points
642+
segment_mid_points = 0.2 / 20 * (0.5 + np.arange(20))
643+
base_gaussian_mean = maximum_duration * 0.5
644+
base_gaussian_width = maximum_duration * 0.1
645+
base_gaussian = np.exp(
646+
-0.5 * ((segment_mid_points - base_gaussian_mean) / base_gaussian_width) ** 2.0
647+
) / (np.sqrt(2 * np.pi) * base_gaussian_width)
648+
649+
# for modulation at frequency = 0
650+
segment_values = np.array([p["value"] for p in pulse_zero_segments])
651+
segment_durations = np.array([p["duration"] for p in pulse_zero_segments])
652+
# The base Gaussian creates a rotation of 1rad and has maximum value
653+
# 1/(sqrt(2pi)*0.2*0.1)=~19.9. Therefore, with a maximum Rabi rate of 20*2pi, we can achieve
654+
# only a single 2pi rotation, which corresponds to scaling up the Gaussian by 2pi.
655+
expected_gaussian = 2 * np.pi * base_gaussian
656+
657+
assert np.allclose(segment_values, expected_gaussian)
658+
assert np.allclose(segment_durations, 0.2 / 20)
659+
660+
# for modulation at frequency = 50/3
661+
segment_values = np.array([segment["value"] for segment in pulse_non_zero_segments])
662+
segment_durations = np.array(
663+
[segment["duration"] for segment in pulse_non_zero_segments]
664+
)
665+
expected_base_segments = base_gaussian * np.sin(
666+
2 * np.pi * (50 / 3) * (segment_mid_points - 0.2 / 2)
667+
)
668+
expected_base_segments /= np.max(expected_base_segments)
669+
expected_base_segments *= maximum_rabi_rate
670+
671+
assert np.allclose(segment_values, expected_base_segments)
672+
assert np.allclose(segment_durations, 0.2 / 20)
673+
674+
675+
def test_modulated_gaussian_control_give_identity_gate():
676+
"""
677+
Tests that the modulated Gaussian sequences produce identity gates.
678+
679+
Apply the modulated sequences to drive a noiseless qubit rotating along X. The net
680+
effect should be an identity gate.
681+
"""
682+
683+
maximum_rabi_rate = 50 * 2 * np.pi
684+
minimum_segment_duration = 0.02
685+
maximum_duration = 0.2
686+
687+
pulses = [
688+
new_modulated_gaussian_control(
689+
maximum_rabi_rate=maximum_rabi_rate,
690+
minimum_segment_duration=minimum_segment_duration,
691+
duration=maximum_duration,
692+
modulation_frequency=f,
693+
)
694+
for f in [0, 20]
695+
]
696+
697+
unitaries = [
698+
np.linalg.multi_dot(
699+
[
700+
np.cos(d * v) * np.eye(2) + 1j * np.sin(d * v) * SIGMA_X
701+
for d, v in zip(
702+
p.durations, p.rabi_rates * np.exp(1j * p.azimuthal_angles),
703+
)
704+
]
705+
)
706+
for p in pulses
707+
]
708+
709+
for _u in unitaries:
710+
assert np.allclose(_u, np.eye(2))

0 commit comments

Comments
 (0)