CQCL / pytket-quantinuum

pytket-quantinuum, extensions for pytket quantum SDK
Apache License 2.0
29 stars 14 forks source link

Noiseless emulator with QIR submission produces incorrect results #263

Closed cqc-alec closed 10 months ago

cqc-alec commented 1 year ago
from pytket.circuit import Circuit
from pytket.extensions.quantinuum import QuantinuumBackend, QuantinuumAPI, Language

api_handler = QuantinuumAPI(api_url="https://hqapi.quantinuum.com/")
b = QuantinuumBackend("H1-1E", api_handler=api_handler)

c = Circuit(2).H(0).CX(0, 1).measure_all()

c1 = b.get_compiled_circuit(c)
h = b.process_circuit(c1, n_shots=1000, language=Language.QIR, noisy_simulation=False)
r = b.get_result(h)
print(r.get_counts())

Output:

Counter({(1, 0): 501, (0, 0): 499})

All shots should produce (0, 0) or (1, 1); instead they produce (0, 0) or (1, 0).

cqc-alec commented 1 year ago

I checked the QIR and I believe it represents the circuit:

    PhasedX(3.5, 0.5, 0)
    PhasedX(0.5, 0.5, 1)
    ZZPhase(0.5, 0, 1)
    Rz(3.0, 0)
    PhasedX(0.5, 0.0, 1)

which ought to yield the (0,0)/(1,1) results. (It isn't unitarily equivalent to the original circuit because an Rz gate before a measure has been removed, which shouldn't affect the result.)

As confirmation, the unitary of the above circuit is:

[[ 0.5+0.5j  0. -0.j   0.5+0.5j  0. -0.j ]
 [ 0. +0.j  -0.5+0.5j  0. +0.j  -0.5+0.5j]
 [-0. +0.j   0.5-0.5j  0. -0.j  -0.5+0.5j]
 [ 0.5+0.5j  0. +0.j  -0.5-0.5j -0. -0.j ]]

The first column is equally weighted between (0,0) and (1,1).

cqc-alec commented 1 year ago

The QIR:

; ModuleID = 'c0'
source_filename = "c0"

%Qubit = type opaque
%Result = type opaque

@0 = internal constant [2 x i8] c"c\00"

define void @main() #0 {
entry:
  %0 = call i1* @create_creg(i64 2)
  call void @__quantum__qis__phasedx__body(double 0x4025FDBBE9BBA775, double 0x3FF921FB54442D18, %Qubit* null)
  call void @__quantum__qis__phasedx__body(double 0x3FF921FB54442D18, double 0x3FF921FB54442D18, %Qubit* inttoptr (i64 1 to %Qubit*))
  call void @__quantum__qis__rzz__body(double 0x3FF921FB54442D18, %Qubit* null, %Qubit* inttoptr (i64 1 to %Qubit*))
  call void @__quantum__qis__rz__body(double 0x4022D97C7F3321D2, %Qubit* null)
  call void @__quantum__qis__phasedx__body(double 0x3FF921FB54442D18, double 0.000000e+00, %Qubit* inttoptr (i64 1 to %Qubit*))
  call void @__quantum__qis__mz__body(%Qubit* null, %Result* null)
  %1 = call i1 @__quantum__qis__read_result__body(%Result* null)
  call void @set_creg_bit(i1* %0, i64 0, i1 %1)
  call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 1 to %Result*))
  %2 = call i1 @__quantum__qis__read_result__body(%Result* inttoptr (i64 1 to %Result*))
  call void @set_creg_bit(i1* %0, i64 1, i1 %2)
  call void @__quantum__rt__tuple_start_record_output()
  %3 = call i64 @get_int_from_creg(i1* %0)
  call void @__quantum__rt__int_record_output(i64 %3, i8* getelementptr inbounds ([2 x i8], [2 x i8]* @0, i32 0, i32 0))
  call void @__quantum__rt__tuple_end_record_output()
  ret void
}

declare i1 @get_creg_bit(i1*, i64)

declare void @set_creg_bit(i1*, i64, i1)

declare void @set_creg_to_int(i1*, i64)

declare i1 @__quantum__qis__read_result__body(%Result*)

declare i1* @create_creg(i64)

declare i64 @get_int_from_creg(i1*)

declare void @__quantum__rt__int_record_output(i64, i8*)

declare void @__quantum__rt__tuple_start_record_output()

declare void @__quantum__rt__tuple_end_record_output()

declare void @__quantum__qis__phasedx__body(double, double, %Qubit*)

declare void @__quantum__qis__rzz__body(double, %Qubit*, %Qubit*)

declare void @__quantum__qis__rz__body(double, %Qubit*)

declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) #1

attributes #0 = { "entry_point" "num_required_qubits"="2" "num_required_results"="2" "output_labeling_schema" "qir_profiles"="custom" }
attributes #1 = { "irreversible" }

!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}
cqc-alec commented 1 year ago

The method I used to decode the floats:

import struct

def decode_double(encoding: str) -> float:
    try:
        return float(encoding)
    except ValueError:
        n = int(encoding, 16)
        return struct.unpack("d", struct.pack("Q", n))[0]
cqc-alec commented 1 year ago

This issue is now fixed, but we should add a test.

I will raise a separate issue for the fact that the simulation appears to be noiseless (i.e. all results are (0,0) or (1,1)), even when noisy_simulation=True is specified.