diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1429c554..1dfc4f41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.11"] + python-version: ["3.8", "3.12"] steps: - name: Check out Pulser uses: actions/checkout@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b9f5ba27..e09976b2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -66,7 +66,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3,12"] steps: - name: Check out Pulser uses: actions/checkout@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 21f0184d..a293f022 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: # Python 3.8 and 3.9 does not run on macos-latest (14) # Uses macos-13 for 3.8 and 3.9 and macos-latest for >=3.10 os: [ubuntu-latest, macos-13, macos-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] exclude: - os: macos-latest python-version: "3.8" @@ -28,6 +28,8 @@ jobs: python-version: "3.10" - os: macos-13 python-version: "3.11" + - os: macos-13 + python-version: "3.12" steps: - name: Check out Pulser uses: actions/checkout@v4 diff --git a/VERSION.txt b/VERSION.txt index 66333910..249afd51 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.18.0 +0.18.1 diff --git a/pulser-core/pulser/channels/eom.py b/pulser-core/pulser/channels/eom.py index 9409a259..a75598d1 100644 --- a/pulser-core/pulser/channels/eom.py +++ b/pulser-core/pulser/channels/eom.py @@ -174,7 +174,9 @@ def __post_init__(self) -> None: "There must be at least one beam in 'controlled_beams'." ) for beam in chain((self.limiting_beam,), self.controlled_beams): - if not (isinstance(beam, RydbergBeam) and beam in RydbergBeam): + if not ( + isinstance(beam, RydbergBeam) and beam in tuple(RydbergBeam) + ): raise TypeError( "Every beam must be one of options of the `RydbergBeam`" f" enumeration, not {self.limiting_beam}." diff --git a/pulser-core/pulser/pulse.py b/pulser-core/pulser/pulse.py index 6da05130..d02fa97b 100644 --- a/pulser-core/pulser/pulse.py +++ b/pulser-core/pulser/pulse.py @@ -35,6 +35,8 @@ __all__ = ["Pulse"] +PHASE_PRECISION = 1e-6 + @dataclass(init=False, repr=False, frozen=True) class Pulse: @@ -263,6 +265,26 @@ def __repr__(self) -> str: + f"post_phase_shift={self.post_phase_shift:.3g})" ) + def __eq__(self, other: Any) -> bool: + if type(other) is not type(self): + return False + + def check_phase_eq(phase1: float, phase2: float) -> np.bool_: + # Comparing with an offset ensures we don't fail just because + # we are very close to the wraping point + return np.isclose(phase1, phase2, atol=1e-6) or np.isclose( + (phase1 + 1) % (2 * np.pi), + (phase2 + 1) % (2 * np.pi), + atol=PHASE_PRECISION, + ) + + return bool( + self.amplitude == other.amplitude + and self.detuning == other.detuning + and check_phase_eq(self.phase, other.phase) + and check_phase_eq(self.post_phase_shift, other.post_phase_shift) + ) + # Replicate __init__'s signature in __new__ functools.update_wrapper(Pulse.__new__, Pulse.__init__) diff --git a/pulser-core/pulser/sequence/sequence.py b/pulser-core/pulser/sequence/sequence.py index 1aaae7e8..ae4f8891 100644 --- a/pulser-core/pulser/sequence/sequence.py +++ b/pulser-core/pulser/sequence/sequence.py @@ -1124,7 +1124,10 @@ def enable_eom_mode( if not self.is_parametrized(): phase_drift_params = _PhaseDriftParams( - drift_rate=-detuning_off, ti=self.get_duration(channel) + drift_rate=-detuning_off, + # enable_eom() calls wait for fall, so the block only + # starts after fall time + ti=self.get_duration(channel, include_fall_time=True), ) self._schedule.enable_eom( channel, amp_on, detuning_on, detuning_off @@ -2028,16 +2031,21 @@ def _add( 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) - ) + new_pulse_slot = self._last(channel) for qubit in last.targets: - self._basis_ref[basis][qubit].update_last_used(true_finish) - - if pulse.post_phase_shift: - self._phase_shift( - pulse.post_phase_shift, *last.targets, basis=basis + self._basis_ref[basis][qubit].update_last_used(new_pulse_slot.tf) + + total_phase_shift = pulse.post_phase_shift + if phase_drift_params: + # The phase correction done to the EOM pulse's phase must + # also be done to the phase shift, as the phase reference is + # effectively changed by -drift + total_phase_shift = ( + total_phase_shift + - phase_drift_params.calc_phase_drift(new_pulse_slot.ti) ) + if total_phase_shift: + self._phase_shift(total_phase_shift, *last.targets, basis=basis) if ( self._in_ising and self._slm_mask_dmm diff --git a/tests/test_pulse.py b/tests/test_pulse.py index 292d181e..d9c1ee5f 100644 --- a/tests/test_pulse.py +++ b/tests/test_pulse.py @@ -119,3 +119,13 @@ def test_full_duration(eom_channel): assert pls.get_full_duration( eom_channel, in_eom_mode=True ) == pls.duration + pls.fall_time(eom_channel, in_eom_mode=True) + + +def test_eq(): + assert (pls_ := Pulse.ConstantPulse(100, 1, -1, 0)) == Pulse( + ConstantWaveform(100, 1), + ConstantWaveform(100, -1), + 1e-6, + post_phase_shift=-1e-6, + ) + assert pls_ != repr(pls_) diff --git a/tests/test_sequence.py b/tests/test_sequence.py index 5402655d..6d7ecd23 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -2044,10 +2044,16 @@ def test_multiple_index_targets(reg): assert built_seq._last("ch0").targets == {"q2", "q3"} +@pytest.mark.parametrize("check_wait_for_fall", (True, False)) @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, correct_phase_drift, patch_plt_show + reg, + mod_device, + custom_buffer_time, + correct_phase_drift, + check_wait_for_fall, + patch_plt_show, ): # Setting custom_buffer_time channels = mod_device.channels @@ -2103,8 +2109,14 @@ def test_eom_mode( 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 - phase = detuning_off * first_pulse_slot.ti * 1e-3 * correct_phase_drift - eom_pulse = Pulse.ConstantPulse(pulse_duration, amp_on, detuning_on, phase) + phase_ref = ( + detuning_off * first_pulse_slot.ti * 1e-3 * correct_phase_drift + ) % (2 * np.pi) + # The phase correction becomes the new phase reference point + assert seq.current_phase_ref("q0", basis="ground-rydberg") == phase_ref + eom_pulse = Pulse.ConstantPulse( + pulse_duration, amp_on, detuning_on, phase_ref + ) assert first_pulse_slot.type == eom_pulse assert not seq._schedule["ch0"].is_detuned_delay(eom_pulse) @@ -2123,9 +2135,9 @@ def test_eom_mode( ) 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 + phase_ref += detuning_off * phase_buffer * 1e-3 * correct_phase_drift assert second_pulse_slot.type == Pulse.ConstantPulse( - pulse_duration, amp_on, detuning_on, phase_ + pulse_duration, amp_on, detuning_on, phase_ + phase_ref ) # Check phase jump buffer is not enforced with "no-delay" @@ -2157,8 +2169,15 @@ def test_eom_mode( ) assert buffer_delay.type == "delay" - assert seq.current_phase_ref("q0", basis="ground-rydberg") == 0 + assert seq.current_phase_ref("q0", basis="ground-rydberg") == phase_ref # Check buffer when EOM is not enabled at the start of the sequence + interval_time = 0 + if check_wait_for_fall: + cte_pulse = Pulse.ConstantPulse(100, 1, 0, 0) + seq.add(cte_pulse, "ch0") + interval_time = cte_pulse.duration + cte_pulse.fall_time( + seq.declared_channels["ch0"] + ) seq.enable_eom_mode( "ch0", amp_on, @@ -2170,7 +2189,7 @@ def test_eom_mode( assert len(seq._schedule["ch0"].eom_blocks) == 2 new_eom_block = seq._schedule["ch0"].eom_blocks[1] assert new_eom_block.detuning_off != 0 - assert last_slot.ti == buffer_delay.tf # Nothing else was added + assert last_slot.ti == buffer_delay.tf + interval_time duration = last_slot.tf - last_slot.ti assert ( duration == custom_buffer_time @@ -2181,12 +2200,15 @@ def test_eom_mode( duration, 0.0, new_eom_block.detuning_off, last_pulse_slot.type.phase ) # Check the phase shift that corrects for the drift - phase_ref = ( + 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 + assert np.isclose( + seq.current_phase_ref("q0", basis="ground-rydberg"), + phase_ref % (2 * np.pi), + ) # Add delay to test the phase drift correction in disable_eom_mode last_delay_time = 400 @@ -2194,8 +2216,9 @@ def test_eom_mode( 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 + assert np.isclose( + seq.current_phase_ref("q0", basis="ground-rydberg"), + phase_ref % (2 * np.pi), ) # Test drawing in eom mode