Virtual Z frame shifts in a PulseSchedule¶
Goal¶
This notebook shows how a VirtualZ operation has two related effects:
- later physical I/Q samples are phase shifted, and
SimulationResultinterprets states in the accumulated logical frame.
The stored states remain in the simulator's physical rotating frame. Use apply_frame_shifts=False when that raw trajectory is needed.
Setup¶
import numpy as np
import plotly.graph_objects as go
from qubex import PulseChannel, PulseSchedule, VirtualZ
from qubex.pulse import Rect
from qubex.simulator import QuantumSimulator, QuantumSystem, Transmon
qubit = Transmon(
label="Q01",
dimension=2,
frequency=5.0,
)
system = QuantumSystem(objects=[qubit])
simulator = QuantumSimulator(system)
drive_channel = PulseChannel(
label="drive",
frequency=qubit.frequency,
target=qubit.label,
)
Build a schedule containing a virtual Z¶
VirtualZ(theta) stores a drive-frame shift of -theta. The second pulse therefore has the same logical X envelope as the first pulse, but its physical I/Q samples are rotated by -theta.
pulse = Rect(duration=20.0, amplitude=0.02)
vz_angle = np.pi / 2
with PulseSchedule([drive_channel]) as schedule:
schedule.add("drive", pulse)
schedule.add("drive", VirtualZ(vz_angle))
schedule.add("drive", pulse)
vz_time = pulse.duration
# Logical X/Y envelope and the accumulated virtual-Z phase.
schedule.plot(
title="Logical envelope and virtual-Z frame",
show_physical_pulse=False,
)
# Physical I/Q waveform delivered to the simulator.
schedule.plot(
title="Physical I/Q waveform after applying frame shifts",
show_physical_pulse=True,
)
Inspect the sampled metadata¶
The segment beginning at the VZ boundary uses the new frame shift. The terminal value is retained separately because a VZ may also occur after the final waveform sample.
sequence = schedule.get_sequence("drive", copy=False)
logical_samples = sequence.get_values(apply_frame_shifts=False)
physical_samples = sequence.get_values(apply_frame_shifts=True)
boundary_sample = pulse.length
sample_summary = {
"frame before VZ": sequence.frame_shifts[boundary_sample - 1],
"frame after VZ": sequence.frame_shifts[boundary_sample],
"final frame": sequence.final_frame_shift,
"logical sample after VZ": logical_samples[boundary_sample],
"physical sample after VZ": physical_samples[boundary_sample],
}
sample_summary
Simulate and compare coordinate frames¶
The raw trajectory follows the physical rotating frame. The default result helpers rotate each substate into the logical frame using the accumulated frame shift at that time.
result = simulator.simulate(
schedule,
initial_state={"Q01": "0"},
dt=0.5,
compute_propagators=False,
)
raw_bloch = result.get_bloch_vectors(
qubit.label,
apply_frame_shifts=False,
)
logical_bloch = result.get_bloch_vectors(qubit.label)
frame_shifts = result.get_frame_shifts(qubit.label)
figure = go.Figure()
for component, index, color in [("X", 0, "#0C5DA5"), ("Y", 1, "#00B945")]:
figure.add_scatter(
x=result.times,
y=raw_bloch[:, index],
name=f"raw {component}",
line={"color": color, "dash": "dot"},
)
figure.add_scatter(
x=result.times,
y=logical_bloch[:, index],
name=f"logical {component}",
line={"color": color},
)
figure.add_vline(x=vz_time, line_dash="dash", annotation_text="Virtual Z")
figure.update_layout(
title="Raw physical frame and logical frame",
xaxis_title="Time (ns)",
yaxis_title="Bloch-vector component",
template="qubex",
)
figure.show()
Visualize both trajectories on the Bloch sphere¶
The first sphere uses the raw physical rotating frame. The second sphere applies the accumulated logical frame shifts.
Raw physical frame¶
result.display_bloch_sphere(
qubit.label,
apply_frame_shifts=False,
)
Logical frame¶
result.display_bloch_sphere(qubit.label)
Checks¶
np.testing.assert_allclose(sequence.frame_shifts[:boundary_sample], 0.0)
np.testing.assert_allclose(sequence.frame_shifts[boundary_sample:], -vz_angle)
np.testing.assert_allclose(sequence.final_frame_shift, -vz_angle)
np.testing.assert_allclose(
physical_samples[:boundary_sample],
logical_samples[:boundary_sample],
)
np.testing.assert_allclose(
physical_samples[boundary_sample:],
-1j * logical_samples[boundary_sample:],
atol=1e-12,
)
expected_logical_bloch = raw_bloch.copy()
coordinate_angle = -frame_shifts
expected_logical_bloch[:, 0] = (
np.cos(coordinate_angle) * raw_bloch[:, 0]
- np.sin(coordinate_angle) * raw_bloch[:, 1]
)
expected_logical_bloch[:, 1] = (
np.sin(coordinate_angle) * raw_bloch[:, 0]
+ np.cos(coordinate_angle) * raw_bloch[:, 1]
)
np.testing.assert_allclose(logical_bloch, expected_logical_bloch, atol=1e-12)
print("All virtual-Z frame checks passed.")
Takeaways¶
- A VZ is not inserted into the Hamiltonian as a finite-duration physical pulse.
PulseScheduleapplies the stored frame shift to later physical I/Q samples.SimulationResult.statesremains raw physical-frame data.get_substates(),get_density_matrices(), andget_bloch_vectors()apply the time-dependent logical frame by default. Passapply_frame_shifts=Falsefor raw coordinates.