From 2315989dbfbf0e90a8701e9e7b537f18388b404a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Silv=C3=A9rio?= Date: Tue, 29 Aug 2023 15:07:10 +0200 Subject: [PATCH] Give access to all EOM block parameters and allow for phase drift correction (#566) * Isolate detuning off calculation * Adding option to correct for phase drift in EOM mode * Add UTs for phase drift correction * Abstract repr support * Include phase drift correctin in the EOM tutorial --- pulser-core/pulser/channels/eom.py | 16 ++++ .../pulser/json/abstract_repr/deserializer.py | 7 +- .../schemas/sequence-schema.json | 12 +++ .../pulser/json/abstract_repr/serializer.py | 31 ++++++- pulser-core/pulser/sequence/_schedule.py | 30 +++++- pulser-core/pulser/sequence/sequence.py | 91 ++++++++++++++++--- tests/test_abstract_repr.py | 84 ++++++++++++----- tests/test_sequence.py | 54 ++++++++++- .../Output Modulation and EOM Mode.ipynb | 14 +-- 9 files changed, 286 insertions(+), 53 deletions(-) diff --git a/pulser-core/pulser/channels/eom.py b/pulser-core/pulser/channels/eom.py index 56455b6d..9409a259 100644 --- a/pulser-core/pulser/channels/eom.py +++ b/pulser-core/pulser/channels/eom.py @@ -180,6 +180,22 @@ def __post_init__(self) -> None: f" enumeration, not {self.limiting_beam}." ) + def calculate_detuning_off( + self, amp_on: float, detuning_on: float, optimal_detuning_off: float + ) -> float: + """Calculates the detuning when the amplitude is off in EOM mode. + + Args: + amp_on: The amplitude of the EOM pulses (in rad/µs). + detuning_on: The detuning of the EOM pulses (in rad/µs). + optimal_detuning_off: The optimal value of detuning (in rad/µs) + when there is no pulse being played. It will choose the closest + value among the existing options. + """ + off_options = self.detuning_off_options(amp_on, detuning_on) + closest_option = np.abs(off_options - optimal_detuning_off).argmin() + return cast(float, off_options[closest_option]) + def detuning_off_options( self, rabi_frequency: float, detuning_on: float ) -> np.ndarray: diff --git a/pulser-core/pulser/json/abstract_repr/deserializer.py b/pulser-core/pulser/json/abstract_repr/deserializer.py index 48d74360..9ce6f86b 100644 --- a/pulser-core/pulser/json/abstract_repr/deserializer.py +++ b/pulser-core/pulser/json/abstract_repr/deserializer.py @@ -263,6 +263,7 @@ def _deserialize_operation(seq: Sequence, op: dict, vars: dict) -> None: optimal_detuning_off=_deserialize_parameter( op["optimal_detuning_off"], vars ), + correct_phase_drift=op.get("correct_phase_drift", False), ) elif op["op"] == "add_eom_pulse": seq.add_eom_pulse( @@ -273,9 +274,13 @@ def _deserialize_operation(seq: Sequence, op: dict, vars: dict) -> None: op["post_phase_shift"], vars ), protocol=op["protocol"], + correct_phase_drift=op.get("correct_phase_drift", False), ) elif op["op"] == "disable_eom_mode": - seq.disable_eom_mode(channel=op["channel"]) + seq.disable_eom_mode( + channel=op["channel"], + correct_phase_drift=op.get("correct_phase_drift", False), + ) def _deserialize_channel(obj: dict[str, Any]) -> Channel: diff --git a/pulser-core/pulser/json/abstract_repr/schemas/sequence-schema.json b/pulser-core/pulser/json/abstract_repr/schemas/sequence-schema.json index ee0153c8..21d0900c 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/sequence-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/sequence-schema.json @@ -449,6 +449,10 @@ "$ref": "#/definitions/ChannelName", "description": "The name of the channel to take out of EOM mode." }, + "correct_phase_drift": { + "description": "Performs a phase shift to correct for the phase drift that occured since the last pulse (or the start of the EOM mode, if no pulse was added).", + "type": "boolean" + }, "op": { "const": "disable_eom_mode", "type": "string" @@ -467,6 +471,10 @@ "$ref": "#/definitions/ChannelName", "description": "The name of the channel to add the pulse to." }, + "correct_phase_drift": { + "description": "Performs a phase shift to correct for the phase drift that occured since the last pulse (or the start of the EOM mode, if adding the first pulse).", + "type": "boolean" + }, "duration": { "$ref": "#/definitions/ParametrizedNum", "description": "The duration of the pulse (in ns)." @@ -514,6 +522,10 @@ "$ref": "#/definitions/ChannelName", "description": "The name of the channel to put in EOM mode." }, + "correct_phase_drift": { + "description": "Performs a phase shift to correct for the phase drift incurred while turning on the EOM mode.", + "type": "boolean" + }, "detuning_on": { "$ref": "#/definitions/ParametrizedNum", "description": "The detuning of the EOM pulses (in rad/µs)." diff --git a/pulser-core/pulser/json/abstract_repr/serializer.py b/pulser-core/pulser/json/abstract_repr/serializer.py index c76e196d..ac5eb0ef 100644 --- a/pulser-core/pulser/json/abstract_repr/serializer.py +++ b/pulser-core/pulser/json/abstract_repr/serializer.py @@ -180,6 +180,15 @@ def get_all_args( } return {**default_values, **params} + def remove_kwarg_if_default( + data: dict[str, Any], call_name: str, kwarg_name: str + ) -> dict[str, Any]: + if data.get(kwarg_name, None) == get_kwarg_default( + call_name, kwarg_name + ): + data.pop(kwarg_name, None) + return data + operations = res["operations"] for call in chain(seq._calls, seq._to_build_calls): if call.name == "__init__": @@ -269,9 +278,18 @@ def get_all_args( res["slm_mask_targets"] = tuple(seq._slm_mask_targets) elif call.name == "enable_eom_mode": data = get_all_args( - ("channel", "amp_on", "detuning_on", "optimal_detuning_off"), + ( + "channel", + "amp_on", + "detuning_on", + "optimal_detuning_off", + "correct_phase_drift", + ), call, ) + data = remove_kwarg_if_default( + data, call.name, "correct_phase_drift" + ) operations.append({"op": "enable_eom_mode", **data}) elif call.name == "add_eom_pulse": data = get_all_args( @@ -281,15 +299,20 @@ def get_all_args( "phase", "post_phase_shift", "protocol", + "correct_phase_drift", ), call, ) + data = remove_kwarg_if_default( + data, call.name, "correct_phase_drift" + ) operations.append({"op": "add_eom_pulse", **data}) elif call.name == "disable_eom_mode": - data = get_all_args(("channel",), call) - operations.append( - {"op": "disable_eom_mode", "channel": data["channel"]} + data = get_all_args(("channel", "correct_phase_drift"), call) + data = remove_kwarg_if_default( + data, call.name, "correct_phase_drift" ) + operations.append({"op": "disable_eom_mode", **data}) else: raise AbstractReprError(f"Unknown call '{call.name}'.") diff --git a/pulser-core/pulser/sequence/_schedule.py b/pulser-core/pulser/sequence/_schedule.py index 816c5c91..5cfb8bc9 100644 --- a/pulser-core/pulser/sequence/_schedule.py +++ b/pulser-core/pulser/sequence/_schedule.py @@ -47,6 +47,16 @@ class _EOMSettings: tf: Optional[int] = None +@dataclass +class _PhaseDriftParams: + drift_rate: float # rad/µs + ti: int # ns + + def calc_phase_drift(self, tf: int) -> float: + """Calculate the phase drift during the elapsed time.""" + return self.drift_rate * (tf - self.ti) * 1e-3 + + @dataclass class _ChannelSchedule: channel_id: str @@ -350,8 +360,16 @@ def add_pulse( channel: str, phase_barrier_ts: list[int], protocol: str, + phase_drift_params: _PhaseDriftParams | None = None, ) -> None: - pass + def corrected_phase(tf: int) -> float: + phase_drift = ( + phase_drift_params.calc_phase_drift(tf) + if phase_drift_params + else 0 + ) + return pulse.phase - phase_drift + last = self[channel][-1] t0 = last.tf current_max_t = max(t0, *phase_barrier_ts) @@ -368,7 +386,7 @@ def add_pulse( ) last_pulse = cast(Pulse, last_pulse_slot.type) # Checks if the current pulse changes the phase - if last_pulse.phase != pulse.phase: + if last_pulse.phase != corrected_phase(current_max_t): # Subtracts the time that has already elapsed since the # last pulse from the phase_jump_time and adds the # fall_time to let the last pulse ramp down @@ -392,6 +410,14 @@ def add_pulse( ti = t0 + delay_duration tf = ti + pulse.duration self._check_duration(tf) + # dataclasses.replace() does not work on Pulse (because init=False) + if phase_drift_params is not None: + pulse = Pulse( + amplitude=pulse.amplitude, + detuning=pulse.detuning, + phase=corrected_phase(ti), + post_phase_shift=pulse.post_phase_shift, + ) self[channel].slots.append(_TimeSlot(pulse, ti, tf, last.targets)) def add_delay(self, duration: int, channel: str) -> None: diff --git a/pulser-core/pulser/sequence/sequence.py b/pulser-core/pulser/sequence/sequence.py index 72ba03ee..9d22a2be 100644 --- a/pulser-core/pulser/sequence/sequence.py +++ b/pulser-core/pulser/sequence/sequence.py @@ -55,7 +55,12 @@ from pulser.register.mappable_reg import MappableRegister from pulser.sequence._basis_ref import _QubitRef from pulser.sequence._call import _Call -from pulser.sequence._schedule import _ChannelSchedule, _Schedule, _TimeSlot +from pulser.sequence._schedule import ( + _ChannelSchedule, + _PhaseDriftParams, + _Schedule, + _TimeSlot, +) from pulser.sequence._seq_drawer import Figure, draw_sequence from pulser.sequence._seq_str import seq_to_str @@ -774,6 +779,7 @@ def enable_eom_mode( amp_on: Union[float, Parametrized], detuning_on: Union[float, Parametrized], optimal_detuning_off: Union[float, Parametrized] = 0.0, + correct_phase_drift: bool = False, ) -> None: """Puts a channel in EOM mode operation. @@ -806,6 +812,8 @@ def enable_eom_mode( optimal_detuning_off: The optimal value of detuning (in rad/µs) when there is no pulse being played. It will choose the closest value among the existing options. + correct_phase_drift: Performs a phase shift to correct for the + phase drift incurred while turning on the EOM mode. """ if self.is_in_eom_mode(channel): raise RuntimeError( @@ -822,29 +830,35 @@ def enable_eom_mode( channel_obj.validate_pulse(on_pulse) amp_on = cast(float, amp_on) detuning_on = cast(float, detuning_on) - - off_options = cast( - RydbergEOM, channel_obj.eom_config - ).detuning_off_options(amp_on, detuning_on) - + eom_config = cast(RydbergEOM, channel_obj.eom_config) if not isinstance(optimal_detuning_off, Parametrized): - closest_option = np.abs( - off_options - optimal_detuning_off - ).argmin() - detuning_off = off_options[closest_option] + detuning_off = eom_config.calculate_detuning_off( + amp_on, detuning_on, optimal_detuning_off + ) off_pulse = Pulse.ConstantPulse( channel_obj.min_duration, 0.0, detuning_off, 0.0 ) channel_obj.validate_pulse(off_pulse) if not self.is_parametrized(): + phase_drift_params = _PhaseDriftParams( + drift_rate=-detuning_off, ti=self.get_duration(channel) + ) self._schedule.enable_eom( channel, amp_on, detuning_on, detuning_off ) + if correct_phase_drift: + buffer_slot = self._last(channel) + drift = phase_drift_params.calc_phase_drift(buffer_slot.tf) + self._phase_shift( + -drift, *buffer_slot.targets, basis=channel_obj.basis + ) @seq_decorators.store @seq_decorators.block_if_measured - def disable_eom_mode(self, channel: str) -> None: + def disable_eom_mode( + self, channel: str, correct_phase_drift: bool = False + ) -> None: """Takes a channel out of EOM mode operation. For channels with a finite modulation bandwidth and an EOM, operation @@ -867,11 +881,24 @@ def disable_eom_mode(self, channel: str) -> None: Args: channel: The name of the channel to take out of EOM mode. + correct_phase_drift: Performs a phase shift to correct for the + phase drift that occured since the last pulse (or the start of + the EOM mode, if no pulse was added). """ if not self.is_in_eom_mode(channel): raise RuntimeError(f"The '{channel}' channel is not in EOM mode.") if not self.is_parametrized(): self._schedule.disable_eom(channel) + if correct_phase_drift: + ch_schedule = self._schedule[channel] + # EOM mode has just been disabled, so tf is defined + last_eom_block_tf = cast(int, ch_schedule.eom_blocks[-1].tf) + drift_params = self._get_last_eom_pulse_phase_drift(channel) + self._phase_shift( + -drift_params.calc_phase_drift(last_eom_block_tf), + *ch_schedule[-1].targets, + basis=ch_schedule.channel_obj.basis, + ) @seq_decorators.store @seq_decorators.mark_non_empty @@ -883,6 +910,7 @@ def add_eom_pulse( phase: Union[float, Parametrized], post_phase_shift: Union[float, Parametrized] = 0.0, protocol: PROTOCOLS = "min-delay", + correct_phase_drift: bool = False, ) -> None: """Adds a square pulse to a channel in EOM mode. @@ -913,6 +941,11 @@ def add_eom_pulse( immediately after the end of the pulse. protocol: Stipulates how to deal with eventual conflicts with other channels (see `Sequence.add()` for more details). + correct_phase_drift: Adjusts the phase to correct for the phase + drift that occured since the last pulse (or the start of the + EOM mode, if adding the first pulse). This effectively + changes the phase of the EOM pulse, so an extra delay might + be added to enforce the phase jump time. """ if not self.is_in_eom_mode(channel): raise RuntimeError(f"Channel '{channel}' must be in EOM mode.") @@ -936,7 +969,14 @@ def add_eom_pulse( phase, post_phase_shift=post_phase_shift, ) - self._add(eom_pulse, channel, protocol) + phase_drift_params = ( + self._get_last_eom_pulse_phase_drift(channel) + if correct_phase_drift + else None + ) + self._add( + eom_pulse, channel, protocol, phase_drift_params=phase_drift_params + ) @seq_decorators.store @seq_decorators.mark_non_empty @@ -1446,6 +1486,7 @@ def _add( pulse: Union[Pulse, Parametrized], channel: str, protocol: PROTOCOLS, + phase_drift_params: _PhaseDriftParams | None = None, ) -> None: self._validate_add_protocol(protocol) if self.is_parametrized(): @@ -1475,7 +1516,13 @@ def _add( self._basis_ref[basis][q].phase.last_time for q in last.targets ] - self._schedule.add_pulse(pulse, channel, phase_barriers, protocol) + self._schedule.add_pulse( + pulse, + channel, + phase_barriers, + protocol, + phase_drift_params=phase_drift_params, + ) true_finish = self._last(channel).tf + pulse.fall_time( channel_obj, in_eom_mode=self.is_in_eom_mode(channel) @@ -1590,6 +1637,24 @@ def _phase_shift( for qubit in target_ids: self._basis_ref[basis][qubit].increment_phase(phi) + def _get_last_eom_pulse_phase_drift( + self, channel: str + ) -> _PhaseDriftParams: + eom_settings = self._schedule[channel].eom_blocks[-1] + try: + last_pulse_tf = ( + self._schedule[channel] + .last_pulse_slot(ignore_detuned_delay=True) + .tf + ) + except RuntimeError: + # There is no previous pulse + last_pulse_tf = 0 + return _PhaseDriftParams( + drift_rate=-eom_settings.detuning_off, + ti=max(eom_settings.ti, last_pulse_tf), + ) + def _to_dict(self, _module: str = "pulser.sequence") -> dict[str, Any]: d = obj_to_dict( self, diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index 745452de..3558c705 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -732,32 +732,48 @@ def test_mappable_register(self, triangular_lattice): ] assert abstract["variables"]["var"] == dict(type="int", value=[0]) - def test_eom_mode(self, triangular_lattice): + @pytest.mark.parametrize("correct_phase_drift", (False, True)) + def test_eom_mode(self, triangular_lattice, correct_phase_drift): reg = triangular_lattice.hexagonal_register(7) seq = Sequence(reg, IroiseMVP) seq.declare_channel("ryd", "rydberg_global") det_off = seq.declare_variable("det_off", dtype=float) duration = seq.declare_variable("duration", dtype=int) seq.enable_eom_mode( - "ryd", amp_on=3.0, detuning_on=0.0, optimal_detuning_off=det_off + "ryd", + amp_on=3.0, + detuning_on=0.0, + optimal_detuning_off=det_off, + correct_phase_drift=correct_phase_drift, + ) + seq.add_eom_pulse( + "ryd", duration, 0.0, correct_phase_drift=correct_phase_drift ) - seq.add_eom_pulse("ryd", duration, 0.0) seq.delay(duration, "ryd") - seq.disable_eom_mode("ryd") + seq.disable_eom_mode("ryd", correct_phase_drift) abstract = json.loads(seq.to_abstract_repr()) validate_schema(abstract) + extra_kwargs = ( + dict(correct_phase_drift=correct_phase_drift) + if correct_phase_drift + else {} + ) + assert abstract["operations"][0] == { - "op": "enable_eom_mode", - "channel": "ryd", - "amp_on": 3.0, - "detuning_on": 0.0, - "optimal_detuning_off": { - "expression": "index", - "lhs": {"variable": "det_off"}, - "rhs": 0, + **{ + "op": "enable_eom_mode", + "channel": "ryd", + "amp_on": 3.0, + "detuning_on": 0.0, + "optimal_detuning_off": { + "expression": "index", + "lhs": {"variable": "det_off"}, + "rhs": 0, + }, }, + **extra_kwargs, } ser_duration = { @@ -766,17 +782,23 @@ def test_eom_mode(self, triangular_lattice): "rhs": 0, } assert abstract["operations"][1] == { - "op": "add_eom_pulse", - "channel": "ryd", - "duration": ser_duration, - "phase": 0.0, - "post_phase_shift": 0.0, - "protocol": "min-delay", + **{ + "op": "add_eom_pulse", + "channel": "ryd", + "duration": ser_duration, + "phase": 0.0, + "post_phase_shift": 0.0, + "protocol": "min-delay", + }, + **extra_kwargs, } assert abstract["operations"][3] == { - "op": "disable_eom_mode", - "channel": "ryd", + **{ + "op": "disable_eom_mode", + "channel": "ryd", + }, + **extra_kwargs, } @pytest.mark.parametrize("use_default", [True, False]) @@ -877,6 +899,10 @@ def _check_roundtrip(serialized_seq: dict[str, Any]): *(op[wf][qty] for qty in wf_args) ) op[wf] = reconstructed_wf._to_abstract_repr() + elif "eom" in op["op"] and not op.get("correct_phase_drift"): + # Remove correct_phase_drift when at default, since the + # roundtrip will delete it + op.pop("correct_phase_drift", None) seq = Sequence.from_abstract_repr(json.dumps(s)) defaults = { @@ -1425,7 +1451,8 @@ def test_deserialize_parametrized_pulse(self, op, pulse_cls): else: assert pulse.kwargs["detuning"] == 1 - def test_deserialize_eom_ops(self): + @pytest.mark.parametrize("correct_phase_drift", (False, True, None)) + def test_deserialize_eom_ops(self, correct_phase_drift): s = _get_serialized_seq( operations=[ { @@ -1434,6 +1461,7 @@ def test_deserialize_eom_ops(self): "amp_on": 3.0, "detuning_on": 0.0, "optimal_detuning_off": -1.0, + "correct_phase_drift": correct_phase_drift, }, { "op": "add_eom_pulse", @@ -1446,16 +1474,21 @@ def test_deserialize_eom_ops(self): "phase": 0.0, "post_phase_shift": 0.0, "protocol": "no-delay", + "correct_phase_drift": correct_phase_drift, }, { "op": "disable_eom_mode", "channel": "global", + "correct_phase_drift": correct_phase_drift, }, ], variables={"duration": {"type": "int", "value": [100]}}, device=json.loads(IroiseMVP.to_abstract_repr()), channels={"global": "rydberg_global"}, ) + if correct_phase_drift is None: + for op in s["operations"]: + del op["correct_phase_drift"] _check_roundtrip(s) seq = Sequence.from_abstract_repr(json.dumps(s)) # init + declare_channel + enable_eom_mode @@ -1470,11 +1503,15 @@ def test_deserialize_eom_ops(self): "amp_on": 3.0, "detuning_on": 0.0, "optimal_detuning_off": -1.0, + "correct_phase_drift": bool(correct_phase_drift), } disable_eom_call = seq._to_build_calls[-1] assert disable_eom_call.name == "disable_eom_mode" - assert disable_eom_call.kwargs == {"channel": "global"} + assert disable_eom_call.kwargs == { + "channel": "global", + "correct_phase_drift": bool(correct_phase_drift), + } eom_pulse_call = seq._to_build_calls[0] assert eom_pulse_call.name == "add_eom_pulse" @@ -1483,6 +1520,9 @@ def test_deserialize_eom_ops(self): assert eom_pulse_call.kwargs["phase"] == 0.0 assert eom_pulse_call.kwargs["post_phase_shift"] == 0.0 assert eom_pulse_call.kwargs["protocol"] == "no-delay" + assert eom_pulse_call.kwargs["correct_phase_drift"] == bool( + correct_phase_drift + ) @pytest.mark.parametrize( "wf_obj", diff --git a/tests/test_sequence.py b/tests/test_sequence.py index 79063b19..863a24e2 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -1402,8 +1402,11 @@ def test_multiple_index_targets(reg): assert built_seq._last("ch0").targets == {"q2", "q3"} +@pytest.mark.parametrize("correct_phase_drift", (True, False)) @pytest.mark.parametrize("custom_buffer_time", (None, 400)) -def test_eom_mode(reg, mod_device, custom_buffer_time, patch_plt_show): +def test_eom_mode( + reg, mod_device, custom_buffer_time, correct_phase_drift, patch_plt_show +): # Setting custom_buffer_time channels = mod_device.channels eom_config = dataclasses.replace( @@ -1449,22 +1452,39 @@ def test_eom_mode(reg, mod_device, custom_buffer_time, patch_plt_show): ] pulse_duration = 100 - seq.add_eom_pulse("ch0", pulse_duration, phase=0.0) + seq.add_eom_pulse( + "ch0", + pulse_duration, + phase=0.0, + correct_phase_drift=correct_phase_drift, + ) first_pulse_slot = seq._schedule["ch0"].last_pulse_slot() assert first_pulse_slot.ti == delay_slot.tf assert first_pulse_slot.tf == first_pulse_slot.ti + pulse_duration - eom_pulse = Pulse.ConstantPulse(pulse_duration, amp_on, detuning_on, 0.0) + phase = detuning_off * first_pulse_slot.ti * 1e-3 * correct_phase_drift + eom_pulse = Pulse.ConstantPulse(pulse_duration, amp_on, detuning_on, phase) assert first_pulse_slot.type == eom_pulse assert not seq._schedule["ch0"].is_detuned_delay(eom_pulse) # Check phase jump buffer - seq.add_eom_pulse("ch0", pulse_duration, phase=np.pi) + phase_ = np.pi + seq.add_eom_pulse( + "ch0", + pulse_duration, + phase=phase_, + correct_phase_drift=correct_phase_drift, + ) second_pulse_slot = seq._schedule["ch0"].last_pulse_slot() phase_buffer = ( eom_pulse.fall_time(ch0_obj, in_eom_mode=True) + seq.declared_channels["ch0"].phase_jump_time ) assert second_pulse_slot.ti == first_pulse_slot.tf + phase_buffer + # Corrects the phase acquired during the phase buffer + phase_ += detuning_off * phase_buffer * 1e-3 * correct_phase_drift + assert second_pulse_slot.type == Pulse.ConstantPulse( + pulse_duration, amp_on, detuning_on, phase_ + ) # Check phase jump buffer is not enforced with "no-delay" seq.add_eom_pulse("ch0", pulse_duration, phase=0.0, protocol="no-delay") @@ -1495,8 +1515,15 @@ def test_eom_mode(reg, mod_device, custom_buffer_time, patch_plt_show): ) assert buffer_delay.type == "delay" + assert seq.current_phase_ref("q0", basis="ground-rydberg") == 0 # Check buffer when EOM is not enabled at the start of the sequence - seq.enable_eom_mode("ch0", amp_on, detuning_on, optimal_detuning_off=-100) + seq.enable_eom_mode( + "ch0", + amp_on, + detuning_on, + optimal_detuning_off=-100, + correct_phase_drift=correct_phase_drift, + ) last_slot = seq._schedule["ch0"][-1] assert len(seq._schedule["ch0"].eom_blocks) == 2 new_eom_block = seq._schedule["ch0"].eom_blocks[1] @@ -1511,6 +1538,23 @@ def test_eom_mode(reg, mod_device, custom_buffer_time, patch_plt_show): assert last_slot.type == Pulse.ConstantPulse( duration, 0.0, new_eom_block.detuning_off, last_pulse_slot.type.phase ) + # Check the phase shift that corrects for the drift + phase_ref = ( + (new_eom_block.detuning_off * duration * 1e-3) + % (2 * np.pi) + * correct_phase_drift + ) + assert seq.current_phase_ref("q0", basis="ground-rydberg") == phase_ref + + # Add delay to test the phase drift correction in disable_eom_mode + last_delay_time = 400 + seq.delay(last_delay_time, "ch0") + + seq.disable_eom_mode("ch0", correct_phase_drift=True) + phase_ref += new_eom_block.detuning_off * last_delay_time * 1e-3 + assert seq.current_phase_ref("q0", basis="ground-rydberg") == phase_ref % ( + 2 * np.pi + ) # Test drawing in eom mode seq.draw() diff --git a/tutorials/advanced_features/Output Modulation and EOM Mode.ipynb b/tutorials/advanced_features/Output Modulation and EOM Mode.ipynb index 5451b11b..c96025b3 100644 --- a/tutorials/advanced_features/Output Modulation and EOM Mode.ipynb +++ b/tutorials/advanced_features/Output Modulation and EOM Mode.ipynb @@ -250,11 +250,11 @@ "source": [ "The modulation bandwidth of a channel can impose significant limitations on how a pulse sequence is programmed. Perhaps most importantly, it can force the user to program longer pulses than would otherwise be required, resulting in longer sequences and consequently noisier results.\n", "\n", - "To overcome these limitations, a channel can be equipped with an EOM that allows the execution of pulses with a higher modulation bandwidth. For now, EOM mode operation is reserved for `Rydberg` channels and works under very specific conditions:\n", + "To overcome these limitations, a channel can be equipped with an EOM that allows the execution of square pulses with a higher modulation bandwidth. For now, EOM mode operation is reserved for `Rydberg` channels and works under very specific conditions:\n", "\n", " 1. EOM mode must be explicitly enabled (`Sequence.enable_eom_mode()`) and disabled (`Sequence.disable_eom_mode()`).\n", " 2. A buffering time is automatically added before the EOM mode is enabled and after it is disabled, as it needs to be isolated from regular channel operation. During the starting buffer, the detuning goes to the value it will assume between EOM pulses (_i.e._ during delays).\n", - " 3. When enabling the EOM mode, one must choose the amplitude and detuning value that all square pulses will have. These values will also determine a set of options for the detuning during delays, out of which one chosen.\n", + " 3. When enabling the EOM mode, one must choose the amplitude and detuning value that all square pulses will have. These values will also determine a set of options for the detuning during delays, out of which the best one is chosen. When this detuning value is not zero, the phase of each qubit's state will drift during delays. If desired, this phase drift can be corrected through the `correct_phase_drift` option, which will adjust the phase of subsequent pulses accordingly. \n", " 4. While in EOM mode, one can only add delays or pulses of variable duration (through `Sequence.add_eom_pulse()`) – changing the phase between pulses is also allowed, but the necessary buffer time for a phase jump will still be enforced." ] }, @@ -280,12 +280,12 @@ "seq.add(Pulse.ConstantPulse(100, 1, 0, 0), \"rydberg\")\n", "seq.enable_eom_mode(\"rydberg\", amp_on=1.0, detuning_on=0.0)\n", "seq.add_eom_pulse(\"rydberg\", duration=100, phase=0.0)\n", - "seq.delay(200, \"rydberg\")\n", - "seq.add_eom_pulse(\"rydberg\", duration=60, phase=0.0)\n", + "seq.delay(300, \"rydberg\")\n", + "seq.add_eom_pulse(\"rydberg\", duration=60, phase=0.0, correct_phase_drift=True)\n", "seq.disable_eom_mode(\"rydberg\")\n", "seq.add(Pulse.ConstantPulse(100, 1, 0, 0), \"rydberg\")\n", "\n", - "seq.draw()" + "seq.draw(draw_phase_curve=True)" ] }, { @@ -302,7 +302,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As expected, inside the isolated EOM mode block in the middle we see that the pulses are much sharper, but we can only do square pulses with a fixed amplitude and there is some non-zero detuning in between them." + "As expected, inside the isolated EOM mode block in the middle we see that the pulses are much sharper, but we can only do square pulses with a fixed amplitude and there is some non-zero detuning in between them. \n", + "\n", + "We also observe how the phase of the second EOM pulse changes to correct for the phase drift during the detuned delay (because we set `correct_phase_drift=True`)." ] } ],