diff --git a/src/bloqade/analysis/address/analysis.py b/src/bloqade/analysis/address/analysis.py index 921f4b3b..6fefa08e 100644 --- a/src/bloqade/analysis/address/analysis.py +++ b/src/bloqade/analysis/address/analysis.py @@ -18,11 +18,20 @@ class AddressAnalysis(Forward[Address]): keys = ("qubit.address",) _const_prop: const.Propagate lattice = Address - next_address: int = field(init=False) + _next_address: int = field(init=False) + + # NOTE: the following are properties so we can hook into the setter in FidelityAnalysis + @property + def next_address(self) -> int: + return self._next_address + + @next_address.setter + def next_address(self, value: int): + self._next_address = value def initialize(self): super().initialize() - self.next_address: int = 0 + self.next_address = 0 self._const_prop = const.Propagate(self.dialects) self._const_prop.initialize() return self @@ -127,7 +136,9 @@ def run_lattice( case _: return Address.top() - def get_const_value(self, addr: Address, typ: Type[T]) -> T | None: + def get_const_value( + self, addr: Address, typ: Type[T] | tuple[Type[T], ...] + ) -> T | None: if not isinstance(addr, ConstResult): return None diff --git a/src/bloqade/analysis/fidelity/__init__.py b/src/bloqade/analysis/fidelity/__init__.py index 496b1b5d..c5bf623e 100644 --- a/src/bloqade/analysis/fidelity/__init__.py +++ b/src/bloqade/analysis/fidelity/__init__.py @@ -1 +1,5 @@ -from .analysis import FidelityAnalysis as FidelityAnalysis +from . import impls as impls +from .analysis import ( + FidelityRange as FidelityRange, + FidelityAnalysis as FidelityAnalysis, +) diff --git a/src/bloqade/analysis/fidelity/analysis.py b/src/bloqade/analysis/fidelity/analysis.py index 815b5725..5b3b878f 100644 --- a/src/bloqade/analysis/fidelity/analysis.py +++ b/src/bloqade/analysis/fidelity/analysis.py @@ -1,82 +1,116 @@ -from typing import Any -from dataclasses import field +from dataclasses import field, dataclass -from kirin import ir -from kirin.lattice import EmptyLattice -from kirin.analysis import Forward -from kirin.analysis.forward import ForwardFrame +from ..address import AddressReg, AddressAnalysis -from ..address import Address, AddressAnalysis +@dataclass +class FidelityRange: + """Range of fidelity for a qubit as pair of (min, max) values""" -class FidelityAnalysis(Forward): + min: float + max: float + + +@dataclass +class FidelityAnalysis(AddressAnalysis): """ This analysis pass can be used to track the global addresses of qubits and wires. ## Usage examples ``` - from bloqade import qasm2 - from bloqade.noise import native + from bloqade import squin from bloqade.analysis.fidelity import FidelityAnalysis - from bloqade.qasm2.passes.noise import NoisePass - - noise_main = qasm2.extended.add(native.dialect) - @noise_main + @squin.kernel def main(): - q = qasm2.qreg(2) - qasm2.x(q[0]) + q = squin.qalloc(1) + squin.x(q[0]) + squin.depolarize(0.1, q[0]) return q - NoisePass(main.dialects)(main) - fid_analysis = FidelityAnalysis(main.dialects) - fid_analysis.run_analysis(main, no_raise=False) + fid_analysis.run(main) - gate_fidelity = fid_analysis.gate_fidelity - atom_survival_probs = fid_analysis.atom_survival_probability + gate_fidelities = fid_analysis.gate_fidelities + qubit_survival_probs = fid_analysis.qubit_survival_fidelities ``` """ - keys = ["circuit.fidelity"] - lattice = EmptyLattice + keys = ("circuit.fidelity", "qubit.address") - gate_fidelity: float = 1.0 - """ - The fidelity of the gate set described by the analysed program. It reduces whenever a noise channel is encountered. - """ + gate_fidelities: list[FidelityRange] = field(init=False, default_factory=list) + """Gate fidelities of each qubit as (min, max) pairs to provide a range""" - atom_survival_probability: list[float] = field(init=False) - """ - The probabilities that each of the atoms in the register survive the duration of the analysed program. The order of the list follows the order they are in the register. - """ + qubit_survival_fidelities: list[FidelityRange] = field( + init=False, default_factory=list + ) + """Qubit survival fidelity given as (min, max) pairs""" - addr_frame: ForwardFrame[Address] = field(init=False) + @property + def next_address(self) -> int: + return self._next_address - def initialize(self): - super().initialize() - self._current_gate_fidelity = 1.0 - self._current_atom_survival_probability = [ - 1.0 for _ in range(len(self.atom_survival_probability)) - ] - return self + @next_address.setter + def next_address(self, value: int): + # NOTE: hook into setter to make sure we always have fidelities of the correct length + self._next_address = value + self.extend_fidelities() + + def extend_fidelities(self): + """Extend both fidelity lists so their length matches the number of qubits""" + + self.extend_fidelity(self.gate_fidelities) + self.extend_fidelity(self.qubit_survival_fidelities) + + def extend_fidelity(self, fidelities: list[FidelityRange]): + """Extend a list of fidelities so its length matches the number of qubits""" - def eval_fallback(self, frame: ForwardFrame, node: ir.Statement): - # NOTE: default is to conserve fidelity, so do nothing here - return + n = self.qubit_count + fidelities.extend([FidelityRange(1.0, 1.0) for _ in range(n - len(fidelities))]) - def run(self, method: ir.Method, *args, **kwargs) -> tuple[ForwardFrame, Any]: - self._run_address_analysis(method) - return super().run(method, *args, **kwargs) + def reset_fidelities(self): + """Reset fidelities to unity for all qubits""" - def _run_address_analysis(self, method: ir.Method): - addr_analysis = AddressAnalysis(self.dialects) - addr_frame, _ = addr_analysis.run(method=method) - self.addr_frame = addr_frame + self.gate_fidelities = [ + FidelityRange(1.0, 1.0) for _ in range(self.qubit_count) + ] + self.qubit_survival_fidelities = [ + FidelityRange(1.0, 1.0) for _ in range(self.qubit_count) + ] - # NOTE: make sure we have as many probabilities as we have addresses - self.atom_survival_probability = [1.0] * addr_analysis.qubit_count + @staticmethod + def update_fidelities( + fidelities: list[FidelityRange], fidelity: float, addresses: AddressReg + ): + """short-hand to update both (min, max) values""" + + for idx in addresses.data: + fidelities[idx].min *= fidelity + fidelities[idx].max *= fidelity + + def update_branched_fidelities( + self, + fidelities: list[FidelityRange], + current_fidelities: list[FidelityRange], + then_fidelities: list[FidelityRange], + else_fidelities: list[FidelityRange], + ): + """Update fidelity (min, max) values after evaluating differing branches such as IfElse""" + # NOTE: make sure they are all of the same length + map( + self.extend_fidelity, + (fidelities, current_fidelities, then_fidelities, else_fidelities), + ) + + # NOTE: now we update min / max accordingly + for fid, current_fid, then_fid, else_fid in zip( + fidelities, current_fidelities, then_fidelities, else_fidelities + ): + fid.min = current_fid.min * min(then_fid.min, else_fid.min) + fid.max = current_fid.max * max(then_fid.max, else_fid.max) - def method_self(self, method: ir.Method) -> EmptyLattice: - return self.lattice.bottom() + def initialize(self): + super().initialize() + self.reset_fidelities() + return self diff --git a/src/bloqade/analysis/fidelity/impls.py b/src/bloqade/analysis/fidelity/impls.py new file mode 100644 index 00000000..b6c515ff --- /dev/null +++ b/src/bloqade/analysis/fidelity/impls.py @@ -0,0 +1,86 @@ +from kirin import interp +from kirin.analysis import ForwardFrame, const +from kirin.dialects import scf + +from bloqade.analysis.address import Address, ConstResult + +from .analysis import FidelityAnalysis + + +@scf.dialect.register(key="circuit.fidelity") +class __ScfMethods(interp.MethodTable): + @interp.impl(scf.IfElse) + def if_else( + self, interp_: FidelityAnalysis, frame: ForwardFrame[Address], stmt: scf.IfElse + ): + + # NOTE: store a copy of the fidelities + current_gate_fidelities = interp_.gate_fidelities + current_survival_fidelities = interp_.qubit_survival_fidelities + + address_cond = frame.get(stmt.cond) + + # NOTE: if the condition is known at compile time, run specific branch + if isinstance(address_cond, ConstResult) and isinstance( + const_cond := address_cond.result, const.Value + ): + body = stmt.then_body if const_cond.data else stmt.else_body + with interp_.new_frame(stmt, has_parent_access=True) as body_frame: + ret = interp_.frame_call_region(body_frame, stmt, body, address_cond) + return ret + + # NOTE: runtime condition, evaluate both + with interp_.new_frame(stmt, has_parent_access=True) as then_frame: + # NOTE: reset fidelities before stepping into the then-body + interp_.reset_fidelities() + + then_results = interp_.frame_call_region( + then_frame, + stmt, + stmt.then_body, + address_cond, + ) + then_fids = interp_.gate_fidelities + then_survival = interp_.qubit_survival_fidelities + + with interp_.new_frame(stmt, has_parent_access=True) as else_frame: + # NOTE: reset again before stepping into else-body + interp_.reset_fidelities() + + else_results = interp_.frame_call_region( + else_frame, + stmt, + stmt.else_body, + address_cond, + ) + + else_fids = interp_.gate_fidelities + else_survival = interp_.qubit_survival_fidelities + + # NOTE: reset one last time + interp_.reset_fidelities() + + # NOTE: now update min / max pairs accordingly + interp_.update_branched_fidelities( + interp_.gate_fidelities, current_gate_fidelities, then_fids, else_fids + ) + interp_.update_branched_fidelities( + interp_.qubit_survival_fidelities, + current_survival_fidelities, + then_survival, + else_survival, + ) + + # TODO: pick the non-return value + if isinstance(then_results, interp.ReturnValue) and isinstance( + else_results, interp.ReturnValue + ): + return interp.ReturnValue(then_results.value.join(else_results.value)) + elif isinstance(then_results, interp.ReturnValue): + ret = else_results + elif isinstance(else_results, interp.ReturnValue): + ret = then_results + else: + ret = interp_.join_results(then_results, else_results) + + return ret diff --git a/src/bloqade/qasm2/dialects/noise/fidelity.py b/src/bloqade/qasm2/dialects/noise/fidelity.py index acd17ac9..9ed8beff 100644 --- a/src/bloqade/qasm2/dialects/noise/fidelity.py +++ b/src/bloqade/qasm2/dialects/noise/fidelity.py @@ -1,6 +1,7 @@ from kirin import interp -from kirin.lattice import EmptyLattice +from kirin.analysis import ForwardFrame +from bloqade.analysis.address import Address, AddressReg from bloqade.analysis.fidelity import FidelityAnalysis from .stmts import PauliChannel, CZPauliChannel, AtomLossChannel @@ -11,37 +12,68 @@ class FidelityMethodTable(interp.MethodTable): @interp.impl(PauliChannel) - @interp.impl(CZPauliChannel) def pauli_channel( self, - interp: FidelityAnalysis, - frame: interp.Frame[EmptyLattice], - stmt: PauliChannel | CZPauliChannel, + interp_: FidelityAnalysis, + frame: ForwardFrame[Address], + stmt: PauliChannel, + ): + (ps,) = stmt.probabilities + fidelity = 1 - sum(ps) + + addresses = frame.get(stmt.qargs) + + if not isinstance(addresses, AddressReg): + return () + + interp_.update_fidelities(interp_.gate_fidelities, fidelity, addresses) + + return () + + @interp.impl(CZPauliChannel) + def cz_pauli_channel( + self, + interp_: FidelityAnalysis, + frame: ForwardFrame[Address], + stmt: CZPauliChannel, ): - probs = stmt.probabilities - try: - ps, ps_ctrl = probs - except ValueError: - (ps,) = probs - ps_ctrl = () + ps_ctrl, ps_target = stmt.probabilities + + fidelity_ctrl = 1 - sum(ps_ctrl) + fidelity_target = 1 - sum(ps_target) + + addresses_ctrl = frame.get(stmt.ctrls) + addresses_target = frame.get(stmt.qargs) - p = sum(ps) - p_ctrl = sum(ps_ctrl) + if not isinstance(addresses_ctrl, AddressReg) or not isinstance( + addresses_target, AddressReg + ): + return () - # NOTE: fidelity is just the inverse probability of any noise to occur - fid = (1 - p) * (1 - p_ctrl) + interp_.update_fidelities( + interp_.gate_fidelities, fidelity_ctrl, addresses_ctrl + ) + interp_.update_fidelities( + interp_.gate_fidelities, fidelity_target, addresses_target + ) - interp.gate_fidelity *= fid + return () @interp.impl(AtomLossChannel) def atom_loss( self, - interp: FidelityAnalysis, - frame: interp.Frame[EmptyLattice], + interp_: FidelityAnalysis, + frame: ForwardFrame[Address], stmt: AtomLossChannel, ): - # NOTE: since AtomLossChannel acts on IList[Qubit], we know the assigned address is a tuple - addresses = interp.addr_frame.get(stmt.qargs) - # NOTE: get the corresponding index and reduce survival probability accordingly - for index in addresses.data: - interp.atom_survival_probability[index] *= 1 - stmt.prob + addresses = frame.get(stmt.qargs) + + if not isinstance(addresses, AddressReg): + return () + + fidelity = 1 - stmt.prob + interp_.update_fidelities( + interp_.qubit_survival_fidelities, fidelity, addresses + ) + + return () diff --git a/src/bloqade/squin/__init__.py b/src/bloqade/squin/__init__.py index fba2659c..9a8ffc7d 100644 --- a/src/bloqade/squin/__init__.py +++ b/src/bloqade/squin/__init__.py @@ -47,6 +47,7 @@ two_qubit_pauli_channel as two_qubit_pauli_channel, single_qubit_pauli_channel as single_qubit_pauli_channel, ) +from .analysis.fidelity import impls as impls # NOTE: it's important to keep these imports here since they import squin.kernel # we skip isort here diff --git a/src/bloqade/squin/analysis/fidelity/__init__.py b/src/bloqade/squin/analysis/fidelity/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/bloqade/squin/analysis/fidelity/impls.py b/src/bloqade/squin/analysis/fidelity/impls.py new file mode 100644 index 00000000..6cd899dd --- /dev/null +++ b/src/bloqade/squin/analysis/fidelity/impls.py @@ -0,0 +1,212 @@ +from typing import TypeVar + +from kirin import interp +from kirin.analysis import ForwardFrame +from kirin.dialects import ilist + +from bloqade.squin import noise +from bloqade.analysis.address import Address, AddressReg +from bloqade.analysis.fidelity import FidelityAnalysis +from bloqade.analysis.address.lattice import StaticContainer + +T = TypeVar("T") + + +@noise.dialect.register(key="circuit.fidelity") +class __NoiseMethods(interp.MethodTable): + + @interp.impl(noise.stmts.SingleQubitPauliChannel) + def single_qubit_pauli_channel( + self, + interp_: FidelityAnalysis, + frame: ForwardFrame[Address], + stmt: noise.stmts.SingleQubitPauliChannel, + ): + px = interp_.get_const_value(frame.get(stmt.px), float) + py = interp_.get_const_value(frame.get(stmt.py), float) + pz = interp_.get_const_value(frame.get(stmt.pz), float) + + if px is None or py is None or pz is None: + return () + + addresses = frame.get(stmt.qubits) + if not isinstance(addresses, AddressReg): + return () + + fidelity = 1 - (px + py + pz) + interp_.update_fidelities(interp_.gate_fidelities, fidelity, addresses) + + return () + + @interp.impl(noise.stmts.TwoQubitPauliChannel) + def two_qubit_pauli_channel( + self, + interp_: FidelityAnalysis, + frame: ForwardFrame[Address], + stmt: noise.stmts.TwoQubitPauliChannel, + ): + probabilities = interp_.get_const_value( + frame.get(stmt.probabilities), (list, tuple, ilist.IList) + ) + + if probabilities is None: + return () + + control_addresses = frame.get(stmt.controls) + target_addresses = frame.get(stmt.targets) + + if not isinstance(control_addresses, AddressReg) or not isinstance( + target_addresses, AddressReg + ): + return () + + # NOTE: total noise probability is the sum over all probabilities where non-identity is applied + p_control = 0.0 + p_target = 0.0 + + # NOTE: not elegant, but easy to ensure correctness + for i, (p, pauli_op) in enumerate( + zip( + probabilities, + ( + "IX", + "IY", + "IZ", + "XI", + "XX", + "XY", + "XZ", + "YI", + "YX", + "YY", + "YZ", + "ZI", + "ZX", + "ZY", + "ZZ", + ), + ) + ): + + if pauli_op[0] != "I": + p_control += p + + if pauli_op[1] != "I": + p_target += p + + fidelity_control = 1 - p_control + fidelity_target = 1 - p_target + + interp_.update_fidelities( + interp_.gate_fidelities, fidelity_control, control_addresses + ) + interp_.update_fidelities( + interp_.gate_fidelities, fidelity_target, target_addresses + ) + + return () + + @interp.impl(noise.stmts.Depolarize) + def depolarize( + self, + interp_: FidelityAnalysis, + frame: ForwardFrame[Address], + stmt: noise.stmts.Depolarize, + ): + p = interp_.get_const_value(frame.get(stmt.p), float) + + if p is None: + return () + + addresses = frame.get(stmt.qubits) + if not isinstance(addresses, AddressReg): + return () + + fidelity = 1 - p + interp_.update_fidelities(interp_.gate_fidelities, fidelity, addresses) + + return () + + @interp.impl(noise.stmts.Depolarize2) + def depolarize2( + self, + interp_: FidelityAnalysis, + frame: ForwardFrame[Address], + stmt: noise.stmts.Depolarize2, + ): + p = interp_.get_const_value(frame.get(stmt.p), float) + + if p is None: + return () + + control_addresses = frame.get(stmt.controls) + target_addresses = frame.get(stmt.targets) + + if not isinstance(control_addresses, AddressReg) or not isinstance( + target_addresses, AddressReg + ): + return () + + # NOTE: there are 15 potential noise operators, 3 of which apply identity to the first and 3 that apply identity to the second qubit + # leaving 12 / 15 noise channels for each qubit to decrease the fidelity + + fidelity = 1 - 12.0 * p / 15.0 + + interp_.update_fidelities(interp_.gate_fidelities, fidelity, control_addresses) + interp_.update_fidelities(interp_.gate_fidelities, fidelity, target_addresses) + + return () + + @interp.impl(noise.stmts.QubitLoss) + def qubit_loss( + self, + interp_: FidelityAnalysis, + frame: ForwardFrame[Address], + stmt: noise.stmts.QubitLoss, + ): + p = interp_.get_const_value(frame.get(stmt.p), float) + + if p is None: + return () + + survival = 1 - p + addresses = frame.get(stmt.qubits) + if not isinstance(addresses, AddressReg): + return () + + interp_.update_fidelities( + interp_.qubit_survival_fidelities, survival, addresses + ) + + return () + + @interp.impl(noise.stmts.CorrelatedQubitLoss) + def correlated_qubit_loss( + self, + interp_: FidelityAnalysis, + frame: ForwardFrame[Address], + stmt: noise.stmts.CorrelatedQubitLoss, + ): + p = interp_.get_const_value(frame.get(stmt.p), float) + + if p is None: + return () + + addresses = frame.get(stmt.qubits) + + if not isinstance(addresses, StaticContainer): + return () + + # NOTE: p is the probability with which an entire atom group is lost + # therefore, the fidelity of each atom decreases according to the following + fidelity = 1 - p + + for address in addresses.data: + if not isinstance(address, AddressReg): + continue + + interp_.update_fidelities( + interp_.qubit_survival_fidelities, fidelity, address + ) + + return () diff --git a/test/analysis/fidelity/test_fidelity.py b/test/analysis/fidelity/test_fidelity.py index 78ca8f26..047ffa4d 100644 --- a/test/analysis/fidelity/test_fidelity.py +++ b/test/analysis/fidelity/test_fidelity.py @@ -1,10 +1,10 @@ import math -import pytest +from kirin.dialects import ilist -from bloqade import qasm2 +from bloqade import qasm2, squin from bloqade.qasm2 import noise -from bloqade.analysis.fidelity import FidelityAnalysis +from bloqade.analysis.fidelity import FidelityRange, FidelityAnalysis from bloqade.qasm2.passes.noise import NoisePass @@ -21,9 +21,11 @@ def main(): fid_analysis = FidelityAnalysis(main.dialects) fid_analysis.run(main) - assert fid_analysis.gate_fidelity == fid_analysis._current_gate_fidelity == 1 - assert fid_analysis.atom_survival_probability[0] == 1 - p_loss - assert fid_analysis.atom_survival_probability[1] == 1 + assert fid_analysis.gate_fidelities == [FidelityRange(1.0, 1.0)] * 2 + assert fid_analysis.qubit_survival_fidelities == [ + FidelityRange(1 - p_loss, 1 - p_loss), + FidelityRange(1.0, 1.0), + ] def test_cz_noise(): @@ -51,9 +53,12 @@ def main(): fid_analysis = FidelityAnalysis(main.dialects) fid_analysis.run(main) - expected_fidelity = (1 - 3 * p_ch) ** 2 + expected_fidelity = 1 - 3 * p_ch - assert math.isclose(fid_analysis.gate_fidelity, expected_fidelity) + assert ( + fid_analysis.gate_fidelities + == [FidelityRange(expected_fidelity, expected_fidelity)] * 2 + ) def test_single_qubit_noise(): @@ -72,7 +77,10 @@ def main(): expected_fidelity = 1 - 3 * p_ch - assert math.isclose(fid_analysis.gate_fidelity, expected_fidelity) + assert fid_analysis.gate_fidelities == [ + FidelityRange(expected_fidelity, expected_fidelity), + FidelityRange(1.0, 1.0), + ] class NoiseTestModel(noise.MoveNoiseModelABC): @@ -80,7 +88,6 @@ def parallel_cz_errors(self, ctrls, qargs, rest): return {(0.01, 0.01, 0.01, 0.01): ctrls + qargs + rest} -@pytest.mark.xfail def test_if(): @qasm2.extended @@ -114,6 +121,7 @@ def main_if(): model = NoiseTestModel( global_loss_prob=p_loss, + local_loss_prob=p_loss, global_px=px, global_py=py, global_pz=pz, @@ -123,21 +131,23 @@ def main_if(): fid_analysis = FidelityAnalysis(main.dialects) fid_analysis.run(main) - model = NoiseTestModel() NoisePass(main_if.dialects, noise_model=model)(main_if) fid_if_analysis = FidelityAnalysis(main_if.dialects) fid_if_analysis.run(main_if) - assert 0 < fid_if_analysis.gate_fidelity == fid_analysis.gate_fidelity < 1 - assert ( - 0 - < fid_if_analysis.atom_survival_probability[0] - == fid_analysis.atom_survival_probability[0] - < 1 - ) + main.print() + main_if.print() + + fidelity_if = fid_if_analysis.gate_fidelities[0] + fidelity = fid_analysis.gate_fidelities[0] + + survival = fid_analysis.qubit_survival_fidelities[0] + survival_if = fid_if_analysis.qubit_survival_fidelities[0] + + assert 0 < fidelity_if.min == fidelity.min == fidelity.max < fidelity_if.max < 1 + assert 0 < survival_if.min == survival.min == survival.max < survival_if.max < 1 -@pytest.mark.xfail def test_for(): @qasm2.extended @@ -186,7 +196,6 @@ def main_for(): fid_analysis = FidelityAnalysis(main.dialects) fid_analysis.run(main) - model = NoiseTestModel() NoisePass(main_for.dialects, noise_model=model)(main_for) main_for.print() @@ -194,10 +203,182 @@ def main_for(): fid_for_analysis = FidelityAnalysis(main_for.dialects) fid_for_analysis.run(main_for) - assert 0 < fid_for_analysis.gate_fidelity == fid_analysis.gate_fidelity < 1 + fid = fid_analysis.gate_fidelities[0] + fid_for = fid_for_analysis.gate_fidelities[0] + survival = fid_analysis.qubit_survival_fidelities[0] + survival_for = fid_for_analysis.qubit_survival_fidelities[0] + + assert 0 < fid.min == fid.max == fid_for.min == fid_for.max < 1 + assert 0 < survival.min == survival.max == survival_for.min == survival_for.max < 1 + + +def test_stdlib_call(): + @squin.kernel + def main(): + q = squin.qalloc(2) + squin.h(q[0]) + squin.single_qubit_pauli_channel(0.1, 0.2, 0.3, q[0]) + squin.cx(q[0], q[1]) + squin.qubit_loss(0.1, q[1]) + + fid_analysis = FidelityAnalysis(main.dialects) + frame, _ = fid_analysis.run(main) + + print(fid_analysis.gate_fidelities) + + assert len(fid_analysis.gate_fidelities) == 2 + assert math.isclose(fid_analysis.gate_fidelities[0].max, 0.4) + assert math.isclose(fid_analysis.gate_fidelities[0].min, 0.4) + assert fid_analysis.gate_fidelities[1] == FidelityRange(1.0, 1.0) + + assert fid_analysis.qubit_survival_fidelities == [ + FidelityRange(1.0, 1.0), + FidelityRange(0.9, 0.9), + ] + + +def test_squin_if(): + + @squin.kernel + def main(): + q = squin.qalloc(2) + squin.h(q[0]) + m = squin.measure(q[0]) + + if m: + qarg = [q[0]] + squin.depolarize(0.1, qarg[0]) + squin.qubit_loss(0.25, q[1]) + else: + squin.depolarize(0.2, q[1]) + squin.qubit_loss(0.15, q[0]) + + fidelity_analysis = FidelityAnalysis(main.dialects) + frame, _ = fidelity_analysis.run(main) + + assert fidelity_analysis.gate_fidelities == [ + FidelityRange(0.9, 1.0), + FidelityRange(0.8, 1.0), + ] + assert fidelity_analysis.qubit_survival_fidelities == [ + FidelityRange(0.85, 1.0), + FidelityRange(0.75, 1.0), + ] + + +def test_squin_for(): + @squin.kernel + def main(): + q = squin.qalloc(4) + + for i in range(4): + squin.depolarize(0.01 * i, q[i]) + + fidelity_analysis = FidelityAnalysis(main.dialects) + frame, _ = fidelity_analysis.run(main) + + assert fidelity_analysis.gate_fidelities == [ + FidelityRange(1.0 - i * 0.01, 1.0 - i * 0.01) for i in range(4) + ] + + +def test_all_noise_channels(): + @squin.kernel + def main(): + q = squin.qalloc(6) + squin.single_qubit_pauli_channel(0.15, 0.2, 0.25, q[0]) + squin.depolarize(0.2, q[1]) + squin.qubit_loss(0.1, q[0]) + + squin.two_qubit_pauli_channel( + ilist.IList( + [ + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + ] + ), + q[2], + q[3], + ) + + squin.depolarize2(0.15, q[4], q[5]) + + squin.correlated_qubit_loss(0.13, ilist.IList([q[2], q[3], q[4], q[5]])) + + fidelity_analysis = FidelityAnalysis(main.dialects) + frame, _ = fidelity_analysis.run(main) + + assert fidelity_analysis.gate_fidelities == [ + FidelityRange( + 0.4, 0.4 + ), # squin.single_qubit_pauli_channel(0.15, 0.2, 0.25, q[0]) + FidelityRange(0.8, 0.8), # squin.depolarize(0.2, q[1]) + FidelityRange( + 1 - 12 * 0.01, 1 - 12 * 0.01 + ), # squin.two_qubit_pauli_channel(..., q[2]) + FidelityRange( + 1 - 12 * 0.01, 1 - 12 * 0.01 + ), # squin.two_qubit_pauli_channel(..., q[3]) + FidelityRange(0.88, 0.88), # squin.depolarize2(0.15, q[4]) + FidelityRange(0.88, 0.88), # squin.depolarize2(0.15, q[5]) + ] + assert ( - 0 - < fid_for_analysis.atom_survival_probability[0] - == fid_analysis.atom_survival_probability[0] - < 1 - ) + fidelity_analysis.qubit_survival_fidelities + == [ + FidelityRange(0.9, 0.9), # squin.qubit_loss(0.1, q[0]) + FidelityRange(1.0, 1.0), + ] + + [FidelityRange(0.87, 0.87)] * 4 + ) # squin.correlated_qubit_loss + + +def test_squin_know_if(): + @squin.kernel + def main(): + x = True + q = squin.qalloc(4) + + if x: + squin.depolarize(0.1, q[0]) + squin.qubit_loss(0.1, q[0]) + else: + squin.depolarize(0.1, q[1]) + squin.qubit_loss(0.1, q[1]) + + if not x: + squin.depolarize(0.2, q[2]) + squin.qubit_loss(0.2, q[2]) + else: + squin.depolarize(0.2, q[3]) + squin.qubit_loss(0.2, q[3]) + + fidelity_analysis = FidelityAnalysis(main.dialects) + fidelity_analysis.run(main) + + assert fidelity_analysis.gate_fidelities == [ + FidelityRange(0.9, 0.9), + FidelityRange(1.0, 1.0), + FidelityRange(1.0, 1.0), + FidelityRange(0.8, 0.8), + ] + + assert fidelity_analysis.qubit_survival_fidelities == [ + FidelityRange(0.9, 0.9), + FidelityRange(1.0, 1.0), + FidelityRange(1.0, 1.0), + FidelityRange(0.8, 0.8), + ]