Skip to content

Commit

Permalink
Fix global phase for PauliI rotation and DumpRegister (#1461)
Browse files Browse the repository at this point in the history
This change introduces a new, stdlib-only intrinsic for applying global
phase during simulation to make phase behavior more consisten and fixes
#1450. This intrinsic only applies the global phase during simulation;
the new `GlobalPhase` intrinsic is a no-op for QIR generation, circuit
generation, and resource estimation. The change also includes updates to
the stdlib and associated tests to account for the corrected global
phase behavior.

This also fixes a bug in `DumpRegister` related to iteration through the
state vector that ensures the "base" amplitude used for separating the
state is consistently chosen, which avoids corner cases that could
introduce a phase in the displayed output that wasn't present in the
underlying state.
  • Loading branch information
swernli authored May 6, 2024
1 parent cd2cabb commit fc4ebf4
Show file tree
Hide file tree
Showing 18 changed files with 550 additions and 108 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ wasm-bindgen-futures = "0.4"
rand = "0.8"
serde_json = "1.0"
pyo3 = "0.20"
quantum-sparse-sim = { git = "https://github.com/qir-alliance/qir-runner", rev = "e7de80bf06dcaf69367576ec31f901c0496a9832", default-features = false }
quantum-sparse-sim = { git = "https://github.com/qir-alliance/qir-runner", rev = "5ceea4ff0d53cf7bb26d722dae91efe5586bd7ec", default-features = false }
async-trait = "0.1"
tokio = { version = "1.35", features = ["macros", "rt"] }

Expand Down
6 changes: 6 additions & 0 deletions compiler/qsc_codegen/src/qir_base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,12 @@ impl Backend for BaseProfSim {
name: &str,
arg: Value,
) -> Option<std::result::Result<Value, String>> {
// Global phase is a special case that is non-physical, so there is no need to generate
// a call here, just do a shortcut return.
if name == "GlobalPhase" {
return Some(Ok(Value::unit()));
}

match self.write_decl(name, &arg) {
Ok(()) => {}
Err(e) => return Some(Err(e)),
Expand Down
63 changes: 63 additions & 0 deletions compiler/qsc_codegen/src/qir_base/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1592,3 +1592,66 @@ fn custom_intrinsic_fail_on_non_unit_return() {
"#]],
);
}

#[test]
fn pauli_i_rotation_for_global_phase_is_noop() {
check(
indoc! {"
namespace Test {
@EntryPoint()
operation Test() : Result {
use q = Qubit();
R(PauliI, 1.0, q);
return MResetZ(q);
}
}
"},
None,
&expect![[r#"
%Result = type opaque
%Qubit = type opaque
define void @ENTRYPOINT__main() #0 {
call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) #1
call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null)
ret void
}
declare void @__quantum__qis__ccx__body(%Qubit*, %Qubit*, %Qubit*)
declare void @__quantum__qis__cx__body(%Qubit*, %Qubit*)
declare void @__quantum__qis__cy__body(%Qubit*, %Qubit*)
declare void @__quantum__qis__cz__body(%Qubit*, %Qubit*)
declare void @__quantum__qis__rx__body(double, %Qubit*)
declare void @__quantum__qis__rxx__body(double, %Qubit*, %Qubit*)
declare void @__quantum__qis__ry__body(double, %Qubit*)
declare void @__quantum__qis__ryy__body(double, %Qubit*, %Qubit*)
declare void @__quantum__qis__rz__body(double, %Qubit*)
declare void @__quantum__qis__rzz__body(double, %Qubit*, %Qubit*)
declare void @__quantum__qis__h__body(%Qubit*)
declare void @__quantum__qis__s__body(%Qubit*)
declare void @__quantum__qis__s__adj(%Qubit*)
declare void @__quantum__qis__t__body(%Qubit*)
declare void @__quantum__qis__t__adj(%Qubit*)
declare void @__quantum__qis__x__body(%Qubit*)
declare void @__quantum__qis__y__body(%Qubit*)
declare void @__quantum__qis__z__body(%Qubit*)
declare void @__quantum__qis__swap__body(%Qubit*, %Qubit*)
declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) #1
declare void @__quantum__rt__result_record_output(%Result*, i8*)
declare void @__quantum__rt__array_record_output(i64, i8*)
declare void @__quantum__rt__tuple_record_output(i64, i8*)
attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="base_profile" "required_num_qubits"="1" "required_num_results"="1" }
attributes #1 = { "irreversible" }
; module flags
!llvm.module.flags = !{!0, !1, !2, !3}
!0 = !{i32 1, !"qir_major_version", i32 1}
!1 = !{i32 7, !"qir_minor_version", i32 0}
!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
!3 = !{i32 1, !"dynamic_result_management", i1 false}
"#]],
);
}
22 changes: 21 additions & 1 deletion compiler/qsc_eval/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,28 @@ impl Backend for SparseSim {
self.sim.qubit_is_zero(q)
}

fn custom_intrinsic(&mut self, name: &str, _arg: Value) -> Option<Result<Value, String>> {
fn custom_intrinsic(&mut self, name: &str, arg: Value) -> Option<Result<Value, String>> {
match name {
"GlobalPhase" => {
// Apply a global phase to the simulation by doing an Rz to a fresh qubit.
// The controls list may be empty, in which case the phase is applied unconditionally.
let [ctls_val, theta] = &*arg.unwrap_tuple() else {
panic!("tuple arity for GlobalPhase intrinsic should be 2");
};
let ctls = ctls_val
.clone()
.unwrap_array()
.iter()
.map(|q| q.clone().unwrap_qubit().0)
.collect::<Vec<_>>();
let q = self.sim.allocate();
// The new qubit is by-definition in the |0⟩ state, so by reversing the sign of the
// angle we can apply the phase to the entire state without increasing its size in memory.
self.sim
.mcrz(&ctls, -2.0 * theta.clone().unwrap_double(), q);
self.sim.release(q);
Some(Ok(Value::unit()))
}
"BeginEstimateCaching" => Some(Ok(Value::Bool(true))),
"EndEstimateCaching"
| "AccountForEstimatesInternal"
Expand Down
2 changes: 1 addition & 1 deletion compiler/qsc_eval/src/intrinsic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ pub(crate) fn call(
return Err(Error::QubitUniqueness(arg_span));
}
let (state, qubit_count) = sim.capture_quantum_state();
let state = utils::split_state(&qubits, state, qubit_count)
let state = utils::split_state(&qubits, &state, qubit_count)
.map_err(|()| Error::QubitsNotSeparable(arg_span))?;
match out.state(state, qubits.len()) {
Ok(()) => Ok(Value::unit()),
Expand Down
39 changes: 39 additions & 0 deletions compiler/qsc_eval/src/intrinsic/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,45 @@ fn dump_register_qubits_not_unique_fails() {
);
}

#[test]
fn dump_register_target_in_minus_with_other_in_zero() {
check_intrinsic_output(
"",
indoc! {"{
use qs = Qubit[2];
X(qs[0]);
H(qs[0]);
Microsoft.Quantum.Diagnostics.DumpRegister([qs[0]]);
ResetAll(qs);
}"},
&expect![[r#"
STATE:
|0⟩: 0.7071+0.0000𝑖
|1⟩: −0.7071+0.0000𝑖
"#]],
);
}

#[test]
fn dump_register_target_in_minus_with_other_in_one() {
check_intrinsic_output(
"",
indoc! {"{
use qs = Qubit[2];
X(qs[1]);
X(qs[0]);
H(qs[0]);
Microsoft.Quantum.Diagnostics.DumpRegister([qs[0]]);
ResetAll(qs);
}"},
&expect![[r#"
STATE:
|0⟩: 0.7071+0.0000𝑖
|1⟩: −0.7071+0.0000𝑖
"#]],
);
}

#[test]
fn message() {
check_intrinsic_output(
Expand Down
14 changes: 8 additions & 6 deletions compiler/qsc_eval/src/intrinsic/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ use rustc_hash::FxHashMap;
/// This function will return an error if the state is not separable using the provided qubit identifiers.
pub fn split_state(
qubits: &[usize],
state: Vec<(BigUint, Complex64)>,
state: &[(BigUint, Complex64)],
qubit_count: usize,
) -> Result<Vec<(BigUint, Complex64)>, ()> {
let state = state.into_iter().collect::<FxHashMap<_, _>>();
let mut dump_state = FxHashMap::default();
let mut other_state = FxHashMap::default();

Expand All @@ -25,7 +24,7 @@ pub fn split_state(
// Try to split out the state for the given qubits from the whole state, detecting any entanglement
// and returning an error if the qubits are not separable.
let dump_norm = collect_split_state(
&state,
state,
&dump_mask,
&other_mask,
&mut dump_state,
Expand Down Expand Up @@ -70,13 +69,16 @@ fn compute_mask(qubit_count: usize, qubits: &[usize]) -> (BigUint, BigUint) {
/// On success, the `dump_state` and `other_state` maps will be populated with the separated states, and the
/// function returns the accumulated norm of the dump state.
fn collect_split_state(
state: &FxHashMap<BigUint, Complex64>,
state: &[(BigUint, Complex64)],
dump_mask: &BigUint,
other_mask: &BigUint,
dump_state: &mut FxHashMap<BigUint, Complex64>,
other_state: &mut FxHashMap<BigUint, Complex64>,
) -> Result<f64, ()> {
// To ensure consistent ordering, we iterate over the vector directly (returned from the simulator in a deterministic order),
// and not the map used for arbitrary lookup.
let mut state_iter = state.iter();
let state_map = state.iter().cloned().collect::<FxHashMap<_, _>>();
let (base_label, base_val) = state_iter.next().expect("state should never be empty");
let dump_base_label = base_label & dump_mask;
let other_base_label = base_label & other_mask;
Expand All @@ -93,10 +95,10 @@ fn collect_split_state(

// If either the state identified by the dump mask or the state identified by the other mask
// is None, that means it has zero amplitude and we can conclude the state is not separable.
let Some(dump_val) = state.get(&(&dump_label | &other_base_label)) else {
let Some(dump_val) = state_map.get(&(&dump_label | &other_base_label)) else {
return Err(());
};
let Some(other_val) = state.get(&(&dump_base_label | &other_label)) else {
let Some(other_val) = state_map.get(&(&dump_base_label | &other_label)) else {
return Err(());
};

Expand Down
3 changes: 2 additions & 1 deletion compiler/qsc_partial_eval/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -791,7 +791,8 @@ impl<'a> PartialEvaluator<'a> {
"DumpRegister"
| "AccountForEstimatesInternal"
| "BeginRepeatEstimatesInternal"
| "EndRepeatEstimatesInternal" => Ok(Value::unit()),
| "EndRepeatEstimatesInternal"
| "GlobalPhase" => Ok(Value::unit()),
// The following intrinsic functions and operations should never make it past conditional compilation and
// the capabilities check pass.
"CheckZero" | "DrawRandomInt" | "DrawRandomDouble" | "Length" => {
Expand Down
2 changes: 1 addition & 1 deletion compiler/qsc_partial_eval/src/management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ impl Backend for QuantumIntrinsicsChecker {
) -> Option<std::result::Result<Value, String>> {
match name {
"BeginEstimateCaching" => Some(Ok(Value::Bool(true))),
"EndEstimateCaching" => Some(Ok(Value::unit())),
"EndEstimateCaching" | "GlobalPhase" => Some(Ok(Value::unit())),
_ => None,
}
}
Expand Down
23 changes: 23 additions & 0 deletions compiler/qsc_partial_eval/tests/intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1031,3 +1031,26 @@ fn call_to_length_in_inner_function_succeeds() {
Return"#]],
);
}

#[test]
fn call_to_pauli_i_rotation_for_global_phase_is_noop() {
let program = get_rir_program(indoc! {
r#"
namespace Test {
@EntryPoint()
operation Main() : Unit {
use q = Qubit();
R(PauliI, 1.0, q);
}
}
"#,
});
assert_block_instructions(
&program,
BlockId(0),
&expect![[r#"
Block:
Call id(1), args( Integer(0), Pointer, )
Return"#]],
);
}
Loading

0 comments on commit fc4ebf4

Please sign in to comment.