diff --git a/examples/lir/bqc/bqc_5_5_client.lhr b/examples/lir/bqc/bqc_5_5_client.lhr new file mode 100644 index 00000000..0cbaf558 --- /dev/null +++ b/examples/lir/bqc/bqc_5_5_client.lhr @@ -0,0 +1,41 @@ +delta1_discrete = assign_cval() : 1 +delta2_discrete = assign_cval() : 1 + +run_subroutine(vec) : + return M0 -> p1 + NETQASM_START + set C0 0 + set C1 1 + set C2 2 + set C10 10 + set C11 20 + array C10 @0 + array C1 @1 + store C0 @1[C0] + array C11 @2 + store C0 @2[C0] + store C1 @2[C1] + set R5 0 + set R6 0 + set R7 1 + set R8 0 + create_epr C1 C0 C1 C2 C0 + wait_all @0[C0:C11] + set Q0 0 + rot_z Q0 {theta_discrete} 4 + rot_y Q0 8 4 + rot_x Q0 16 4 + meas Q0 M0 + qfree Q0 + ret_reg M0 + NETQASM_END + +p1 = mult_const(p1, 16) +delta1 = add_cval_c(delta1_discrete, p1) +send_cmsg(delta1) +m1 = recv_cmsg() + +delta2 = bcond_mult_const(delta2_discrete, -1, m1) +send_cmsg(delta2) + +return_result(p1) diff --git a/examples/lir/bqc/bqc_5_5_nv_raw_lhr.py b/examples/lir/bqc/bqc_5_5_nv_raw_lhr.py new file mode 100644 index 00000000..49c52367 --- /dev/null +++ b/examples/lir/bqc/bqc_5_5_nv_raw_lhr.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import math +import os +from typing import List + +from netqasm.sdk.epr_socket import EPRSocket + +from squidasm.qoala.lang import lhr as lp +from squidasm.qoala.runtime.config import ( + LinkConfig, + NVQDeviceConfig, + StackConfig, + StackNetworkConfig, +) +from squidasm.qoala.runtime.program import ProgramContext, ProgramInstance, SdkProgram +from squidasm.qoala.runtime.run import run +from squidasm.qoala.sim.common import LogManager +from squidasm.qoala.sim.netstack import EprSocket + +PI = math.pi +PI_OVER_2 = math.pi / 2 + + +def computation_round( + cfg: StackNetworkConfig, + num_times: int = 1, + alpha: float = 0.0, + beta: float = 0.0, + theta1: float = 0.0, +) -> None: + # TODO: use alpha, beta to calculate delta1_discrete etc. + + client_lhr_file = os.path.join(os.path.dirname(__file__), "bqc_5_5_client.lhr") + with open(client_lhr_file) as file: + client_lhr_text = file.read() + client_program = lp.LhrParser(client_lhr_text).parse() + client_program.meta = lp.ProgramMeta( + name="client_program", + parameters=["alpha", "beta", "theta1", "r1"], + csockets=["server"], + epr_sockets=["server"], + max_qubits=2, + ) + + server_lhr_file = os.path.join(os.path.dirname(__file__), "bqc_5_5_server.lhr") + with open(server_lhr_file) as file: + server_lhr_text = file.read() + server_program = lp.LhrParser(server_lhr_text).parse() + server_program.meta = lp.ProgramMeta( + name="server_program", + parameters={}, + csockets=["client"], + epr_sockets=["client"], + max_qubits=2, + ) + + client_instance = ProgramInstance(client_program, {"theta_discrete": 0}, 1, 0) + server_instance = ProgramInstance(server_program, {}, 1, 0) + + _, server_results = run( + cfg, {"client": client_instance, "server": server_instance}, num_times=num_times + ) + + m2s = [result["m2"] for result in server_results] + num_zeros = len([m for m in m2s if m == 0]) + frac0 = round(num_zeros / num_times, 2) + frac1 = 1 - frac0 + print(f"dist (0, 1) = ({frac0}, {frac1})") + + +if __name__ == "__main__": + num_times = 1 + + LogManager.set_log_level("DEBUG") + LogManager.log_to_file("dump.log") + + sender_stack = StackConfig( + name="client", + qdevice_typ="nv", + qdevice_cfg=NVQDeviceConfig.perfect_config(), + ) + receiver_stack = StackConfig( + name="server", + qdevice_typ="nv", + qdevice_cfg=NVQDeviceConfig.perfect_config(), + ) + link = LinkConfig( + stack1="client", + stack2="server", + typ="perfect", + ) + + cfg = StackNetworkConfig(stacks=[sender_stack, receiver_stack], links=[link]) + + computation_round(cfg, num_times, alpha=PI_OVER_2, beta=PI_OVER_2) diff --git a/examples/lir/bqc/bqc_5_5_server.lhr b/examples/lir/bqc/bqc_5_5_server.lhr new file mode 100644 index 00000000..676cb4a3 --- /dev/null +++ b/examples/lir/bqc/bqc_5_5_server.lhr @@ -0,0 +1,99 @@ +run_subroutine(vec<>) : + NETQASM_START + set C0 0 + set C1 1 + set C10 10 + array C10 @0 + array C1 @1 + store C0 @1[C0] + set R5 0 + set R6 0 + set R7 1 + set R8 0 + recv_epr C0 C0 C1 C0 + set R0 0 +LABEL7: + set R5 1 + beq R0 R5 LABEL1 + set R1 0 + set R2 0 + set R3 0 + set R4 0 +LABEL6: + set R5 10 + beq R4 R5 LABEL5 + add R1 R1 R0 + set R5 1 + add R4 R4 R5 + jmp LABEL6 +LABEL5: + set R5 1 + add R2 R0 R5 + set R4 0 +LABEL4: + set R5 10 + beq R4 R5 LABEL3 + add R3 R3 R2 + set R5 1 + add R4 R4 R5 + jmp LABEL4 +LABEL3: + wait_all @0[R1:R3] + set R5 0 + beq R0 R5 LABEL2 + set R5 0 + sub R1 R5 R0 + rot_y C0 8 4 + crot_y C0 R1 24 4 + rot_x C0 24 4 + crot_x C0 R1 8 4 + qfree C0 +LABEL2: + set R5 1 + add R0 R0 R5 + jmp LABEL7 +LABEL1: + qalloc C1 + init C1 + rot_y C1 8 4 + rot_x C1 16 4 + rot_y C1 8 4 + crot_x C0 C1 8 4 + rot_z C0 24 4 + rot_x C1 24 4 + rot_y C1 24 4 + ret_arr @0 + ret_arr @1 + NETQASM_END + +delta1 = recv_cmsg() + +run_subroutine(vec) : + return M0 -> m1 + NETQASM_START + set Q0 0 + rot_z Q0 {delta1} 4 + rot_y Q0 8 4 + rot_x Q0 16 4 + meas Q0 M0 + qfree Q0 + ret_reg M0 + NETQASM_END + +send_cmsg(m1) +delta2 = recv_cmsg() + +run_subroutine(vec) : + return M0 -> m2 + NETQASM_START + set Q1 1 + rot_z Q1 {delta2} 4 + rot_y Q1 8 4 + rot_x Q1 16 4 + meas Q1 M0 + qfree Q1 + ret_reg M0 + NETQASM_END + +return_result(m1) +return_result(m2) diff --git a/examples/lir/bqc/example_bqc.py b/examples/lir/bqc/example_bqc.py new file mode 100644 index 00000000..00b39be7 --- /dev/null +++ b/examples/lir/bqc/example_bqc.py @@ -0,0 +1,299 @@ +from __future__ import annotations + +import math +from typing import List + +from netqasm.lang.operand import Template + +from squidasm.qoala.lang import lhr as lp +from squidasm.run.stack.config import ( + GenericQDeviceConfig, + LinkConfig, + StackConfig, + StackNetworkConfig, +) +from squidasm.run.stack.run import run +from squidasm.sim.stack.program import ProgramContext, ProgramMeta + + +class ClientProgram(lp.LhrProgram): + PEER = "server" + + def __init__( + self, + alpha: float, + beta: float, + trap: bool, + dummy: int, + theta1: float, + theta2: float, + r1: int, + r2: int, + ): + self._alpha = alpha + self._beta = beta + self._trap = trap + self._dummy = dummy + self._theta1 = theta1 + self._theta2 = theta2 + self._r1 = r1 + self._r2 = r2 + + super().__init__([], {}) + + @property + def meta(self) -> ProgramMeta: + return ProgramMeta( + name="client_program", + parameters={ + "alpha": self._alpha, + "beta": self._beta, + "trap": self._trap, + "dummy": self._dummy, + "theta1": self._theta1, + "theta2": self._theta2, + "r1": self._r1, + "r2": self._r2, + }, + csockets=[self.PEER], + epr_sockets=[self.PEER], + max_qubits=2, + ) + + def compile(self, context: ProgramContext) -> None: + conn = context.connection + epr_socket = context.epr_sockets[self.PEER] + + # Create EPR pair + epr1 = epr_socket.create_keep()[0] + + # RSP + if self._trap and self._dummy == 2: + # remotely-prepare a dummy state + p2 = epr1.measure(store_array=False) + else: + epr1.rot_Z(angle=self._theta2) + epr1.H() + p2 = epr1.measure(store_array=False) + + # Create EPR pair + epr2 = epr_socket.create_keep()[0] + + # RSP + if self._trap and self._dummy == 1: + # remotely-prepare a dummy state + p1 = epr2.measure(store_array=False) + else: + epr2.rot_Z(angle=self._theta1) + epr2.H() + p1 = epr2.measure(store_array=False) + + subrt = conn.compile() + subroutines = {"subrt": subrt} + + instrs: List[lp.ClassicalLhrOp] = [] + instrs.append(lp.RunSubroutineOp("subrt")) + instrs.append(lp.AssignCValueOp("p1", p1)) + instrs.append(lp.AssignCValueOp("p2", p2)) + + if self._trap and self._dummy == 2: + delta1 = -self._theta1 + self._r1 * math.pi + else: + delta1 = self._alpha - self._theta1 + self._r1 * math.pi + delta1_discrete = delta1 / (math.pi / 16) + + instrs.append(lp.AssignCValueOp("delta1", delta1_discrete)) + instrs.append(lp.MultiplyConstantCValueOp("p1", "p1", 16)) + instrs.append(lp.AddCValueOp("delta1", "delta1", "p1")) + instrs.append(lp.SendCMsgOp("delta1")) + + instrs.append(lp.ReceiveCMsgOp("m1")) + + if self._trap and self._dummy == 1: + delta2 = -self._theta2 + self._r2 * math.pi + delta2_discrete = delta2 / (math.pi / 16) + instrs.append(lp.AssignCValueOp("delta2", delta2_discrete)) + else: + beta = math.pow(-1, self._r1) * self._beta + beta_discrete = beta / (math.pi / 16) + instrs.append(lp.AssignCValueOp("beta", beta_discrete)) + instrs.append( + lp.BitConditionalMultiplyConstantCValueOp( + result="beta", value0="beta", value1=16, cond="m1" + ) + ) + delta2 = self._theta2 + self._r2 * math.pi + delta2_discrete = delta2 / (math.pi / 16) + instrs.append(lp.AssignCValueOp("delta2", delta2_discrete)) + instrs.append(lp.AddCValueOp("delta2", "delta2", "beta")) + + instrs.append(lp.MultiplyConstantCValueOp("p2", "p2", 16)) + instrs.append(lp.AddCValueOp("delta2", "delta2", "p2")) + instrs.append(lp.SendCMsgOp("delta2")) + + instrs.append(lp.ReturnResultOp("p1")) + instrs.append(lp.ReturnResultOp("p2")) + + self.instructions = instrs + self.subroutines = subroutines + + +class ServerProgram(lp.LhrProgram): + PEER = "client" + + def __init__(self) -> None: + super().__init__([], {}) + + @property + def meta(self) -> ProgramMeta: + return ProgramMeta( + name="server_program", + parameters={}, + csockets=[self.PEER], + epr_sockets=[self.PEER], + max_qubits=2, + ) + + def compile(self, context: ProgramContext) -> None: + conn = context.connection + epr_socket = context.epr_sockets[self.PEER] + + # Create EPR Pair + epr1 = epr_socket.recv_keep()[0] + epr2 = epr_socket.recv_keep()[0] + epr2.cphase(epr1) + + subrt1 = conn.compile() + subroutines = {"subrt1": subrt1} + + instrs: List[lp.ClassicalLhrOp] = [] + instrs.append(lp.RunSubroutineOp("subrt1")) + + instrs.append(lp.ReceiveCMsgOp("delta1")) + + epr2.rot_Z(n=Template("delta1"), d=4) + epr2.H() + m1 = epr2.measure(store_array=False) + + subrt2 = conn.compile() + subroutines["subrt2"] = subrt2 + + instrs.append(lp.RunSubroutineOp("subrt2")) + instrs.append(lp.AssignCValueOp("m1", m1)) + instrs.append(lp.SendCMsgOp("m1")) + + instrs.append(lp.ReceiveCMsgOp("delta2")) + + epr1.rot_Z(n=Template("delta2"), d=4) + epr1.H() + m2 = epr1.measure(store_array=False) + subrt3 = conn.compile() + subroutines["subrt3"] = subrt3 + + instrs.append(lp.RunSubroutineOp("subrt3")) + instrs.append(lp.AssignCValueOp("m2", m2)) + instrs.append(lp.ReturnResultOp("m1")) + instrs.append(lp.ReturnResultOp("m2")) + + self.instructions = instrs + self.subroutines = subroutines + + +PI = math.pi +PI_OVER_2 = math.pi / 2 + + +def computation_round( + cfg: StackNetworkConfig, + num_times: int = 1, + alpha: float = 0.0, + beta: float = 0.0, + theta1: float = 0.0, + theta2: float = 0.0, +) -> None: + client_program = ClientProgram( + alpha=alpha, + beta=beta, + trap=False, + dummy=-1, + theta1=theta1, + theta2=theta2, + r1=0, + r2=0, + ) + server_program = ServerProgram() + + _, server_results = run( + cfg, {"client": client_program, "server": server_program}, num_times=num_times + ) + + m2s = [result["m2"] for result in server_results] + num_zeros = len([m for m in m2s if m == 0]) + frac0 = round(num_zeros / num_times, 2) + frac1 = 1 - frac0 + print(f"dist (0, 1) = ({frac0}, {frac1})") + + +def trap_round( + cfg: StackNetworkConfig, + num_times: int = 1, + alpha: float = 0.0, + beta: float = 0.0, + theta1: float = 0.0, + theta2: float = 0.0, + dummy: int = 1, +) -> None: + client_program = ClientProgram( + alpha=alpha, + beta=beta, + trap=True, + dummy=dummy, + theta1=theta1, + theta2=theta2, + r1=0, + r2=0, + ) + server_program = ServerProgram() + + client_results, server_results = run( + cfg, {"client": client_program, "server": server_program}, num_times=num_times + ) + + p1s = [result["p1"] for result in client_results] + p2s = [result["p2"] for result in client_results] + m1s = [result["m1"] for result in server_results] + m2s = [result["m2"] for result in server_results] + + assert dummy in [1, 2] + if dummy == 1: + num_fails = len([(p, m) for (p, m) in zip(p1s, m2s) if p != m]) + else: + num_fails = len([(p, m) for (p, m) in zip(p2s, m1s) if p != m]) + + frac_fail = round(num_fails / num_times, 2) + print(f"fail rate: {frac_fail}") + + +if __name__ == "__main__": + num_times = 1 + + sender_stack = StackConfig( + name="client", + qdevice_typ="generic", + qdevice_cfg=GenericQDeviceConfig.perfect_config(), + ) + receiver_stack = StackConfig( + name="server", + qdevice_typ="generic", + qdevice_cfg=GenericQDeviceConfig.perfect_config(), + ) + link = LinkConfig( + stack1="client", + stack2="server", + typ="perfect", + ) + + cfg = StackNetworkConfig(stacks=[sender_stack, receiver_stack], links=[link]) + + # computation_round(cfg, num_times, alpha=PI_OVER_2, beta=PI_OVER_2) + trap_round(cfg, num_times, dummy=1) diff --git a/examples/lir/teleport/example_teleport.py b/examples/lir/teleport/example_teleport.py new file mode 100644 index 00000000..5d077a86 --- /dev/null +++ b/examples/lir/teleport/example_teleport.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import math +from typing import List + +from netqasm.lang.operand import Template +from netqasm.sdk.qubit import Qubit +from netqasm.sdk.toolbox import set_qubit_state + +from squidasm.qoala.lang import lhr as lp +from squidasm.run.stack.config import ( + GenericQDeviceConfig, + LinkConfig, + StackConfig, + StackNetworkConfig, +) +from squidasm.run.stack.run import run +from squidasm.sim.stack.common import LogManager +from squidasm.sim.stack.program import ProgramContext, ProgramMeta + + +class SenderProgram(lp.SdkProgram): + PEER = "receiver" + + def __init__( + self, + theta: float, + phi: float, + ): + self._theta = theta + self._phi = phi + + super().__init__([], {}) + + @property + def meta(self) -> ProgramMeta: + return ProgramMeta( + name="sender_program", + parameters={ + "theta": self._theta, + "phi": self._phi, + }, + csockets=[self.PEER], + epr_sockets=[self.PEER], + max_qubits=2, + ) + + def compile(self, context: ProgramContext) -> lp.LhrProgram: + conn = context.connection + epr_socket = context.epr_sockets[self.PEER] + + q = Qubit(conn) + set_qubit_state(q, self._phi, self._theta) + + e = epr_socket.create_keep()[0] + q.cnot(e) + q.H() + m1 = q.measure() + m2 = e.measure() + + subrt = conn.compile() + subroutines = {"subrt": subrt} + + instrs: List[lp.ClassicalLhrOp] = [] + instrs.append(lp.RunSubroutineOp("subrt")) + instrs.append(lp.AssignCValueOp("m1", m1)) + instrs.append(lp.AssignCValueOp("m2", m2)) + instrs.append(lp.SendCMsgOp("m1")) + instrs.append(lp.SendCMsgOp("m2")) + instrs.append(lp.ReturnResultOp("m1")) + instrs.append(lp.ReturnResultOp("m2")) + + self.instructions = instrs + self.subroutines = subroutines + return lp.LhrProgram(instrs, subroutines, self.meta) + + +class ReceiverProgram(lp.SdkProgram): + PEER = "sender" + + def __init__(self) -> None: + super().__init__([], {}) + + @property + def meta(self) -> ProgramMeta: + return ProgramMeta( + name="receiver_program", + parameters={}, + csockets=[self.PEER], + epr_sockets=[self.PEER], + max_qubits=1, + ) + + def compile(self, context: ProgramContext) -> lp.LhrProgram: + conn = context.connection + epr_socket = context.epr_sockets[self.PEER] + + e = epr_socket.recv_keep()[0] + subrt1 = conn.compile() + + subroutines = {"subrt1": subrt1} + + instrs: List[lp.ClassicalLhrOp] = [] + instrs.append(lp.RunSubroutineOp("subrt1")) + instrs.append(lp.ReceiveCMsgOp("m1")) + instrs.append(lp.ReceiveCMsgOp("m2")) + + m1 = conn.builder.new_register(init_value=Template("m1"), return_reg=False) + m2 = conn.builder.new_register(init_value=Template("m2"), return_reg=False) + + with m2.if_eq(1): + e.X() + with m1.if_eq(1): + e.Z() + + m = e.measure(store_array=False) + + subrt2 = conn.compile() + subroutines["subrt2"] = subrt2 + + instrs.append(lp.RunSubroutineOp("subrt2")) + instrs.append(lp.AssignCValueOp("m", m)) + instrs.append(lp.ReturnResultOp("m")) + + self.instructions = instrs + self.subroutines = subroutines + return lp.LhrProgram(instrs, subroutines, self.meta) + + +if __name__ == "__main__": + LogManager.set_log_level("INFO") + + sender_stack = StackConfig( + name="sender", + qdevice_typ="generic", + qdevice_cfg=GenericQDeviceConfig.perfect_config(), + ) + receiver_stack = StackConfig( + name="receiver", + qdevice_typ="generic", + qdevice_cfg=GenericQDeviceConfig.perfect_config(), + ) + link = LinkConfig( + stack1="sender", + stack2="receiver", + typ="perfect", + ) + + cfg = StackNetworkConfig(stacks=[sender_stack, receiver_stack], links=[link]) + + sender_program = SenderProgram(theta=math.pi, phi=0) + receiver_program = ReceiverProgram() + + results = run(cfg, {"sender": sender_program, "receiver": receiver_program}) + print(results) diff --git a/setup.cfg b/setup.cfg index 48f56d8d..1493e3a1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,7 +22,7 @@ install_requires = netsquid >=1.0.6, <2.0 netsquid-magic >=12.1, <13.0.0 netsquid-nv >=8.0, <9.0 - netqasm ==0.11.2 + netqasm >=0.11 [options.extras_require] dev = diff --git a/squidasm/qoala/__init__.py b/squidasm/qoala/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/squidasm/qoala/arch/qoala-runtime-data.drawio b/squidasm/qoala/arch/qoala-runtime-data.drawio new file mode 100644 index 00000000..2af679d8 --- /dev/null +++ b/squidasm/qoala/arch/qoala-runtime-data.drawio @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/squidasm/qoala/arch/qoala2.drawio b/squidasm/qoala/arch/qoala2.drawio new file mode 100644 index 00000000..9ebee61a --- /dev/null +++ b/squidasm/qoala/arch/qoala2.drawio @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/squidasm/qoala/lang/lhr.py b/squidasm/qoala/lang/lhr.py new file mode 100644 index 00000000..01304690 --- /dev/null +++ b/squidasm/qoala/lang/lhr.py @@ -0,0 +1,516 @@ +import abc +from dataclasses import dataclass +from enum import Enum, auto +from typing import Any, Dict, List, Optional, Union + +from netqasm.lang.instr import NetQASMInstruction +from netqasm.lang.instr.flavour import NVFlavour +from netqasm.lang.operand import Template +from netqasm.lang.parsing.text import parse_text_subroutine +from netqasm.lang.subroutine import Subroutine + +LhrValue = Union[int, Template] + + +@dataclass +class ProgramMeta: + name: str + parameters: Dict[str, Any] + csockets: List[str] + epr_sockets: List[str] + max_qubits: int + + +class LhrInstructionType(Enum): + CC = 0 + CL = auto() + QC = auto() + QL = auto() + + +@dataclass +class LhrInstructionSignature: + typ: LhrInstructionType + duration: int = 0 + + +class StaticLhrProgramInfo: + pass + + +class DynamicLhrProgramInfo: + pass + + +class LhrAttribute: + def __init__(self, value: LhrValue) -> None: + self._value = value + + @property + def value(self) -> LhrValue: + return self._value + + +class LhrSharedMemLoc: + def __init__(self, loc: str) -> None: + self._loc = loc + + @property + def loc(self) -> str: + return self._loc + + def __str__(self) -> str: + return str(self.loc) + + +class LhrVector: + def __init__(self, values: List[str]) -> None: + self._values = values + + @property + def values(self) -> List[str]: + return self._values + + def __str__(self) -> str: + return f"vec<{','.join(v for v in self.values)}>" + + +class LhrSubroutine: + def __init__( + self, subrt: Subroutine, return_map: Dict[str, LhrSharedMemLoc] + ) -> None: + self._subrt = subrt + self._return_map = return_map + + @property + def subroutine(self) -> Subroutine: + return self._subrt + + @property + def return_map(self) -> Dict[str, LhrSharedMemLoc]: + return self._return_map + + def __str__(self) -> str: + s = "\n" + for key, value in self.return_map.items(): + s += f"return {str(value)} -> {key}\n" + s += "NETQASM_START\n" + s += self.subroutine.print_instructions() + s += "\nNETQASM_END" + return s + + +class ClassicalLhrOp: + def __init__( + self, + arguments: Optional[List[str]] = None, + results: Optional[List[str]] = None, + attributes: Optional[List[LhrValue]] = None, + ) -> None: + self._arguments: List[str] + self._results: List[str] + self._attributes: List[LhrValue] + + if arguments is None: + self._arguments = [] + else: + self._arguments = arguments + + if results is None: + self._results = [] + else: + self._results = results + + if attributes is None: + self._attributes = [] + else: + self._attributes = attributes + + def __str__(self) -> str: + results = ", ".join(str(r) for r in self.results) + args = ", ".join(str(a) for a in self.arguments) + attrs = ", ".join(str(a) for a in self.attributes) + s = "" + if len(results) > 0: + s += f"{results} = " + + s += f"{self.op_name}({args})" + + if len(attrs) > 0: + s += f" : {attrs}" + return s + + @property + def op_name(self) -> str: + return self.__class__.OP_NAME + + @property + def arguments(self) -> List[str]: + return self._arguments + + @property + def results(self) -> List[str]: + return self._results + + @property + def attributes(self) -> List[LhrValue]: + return self._attributes + + +class SendCMsgOp(ClassicalLhrOp): + OP_NAME = "send_cmsg" + TYP = LhrInstructionType.CC + + def __init__(self, value: str) -> None: + super().__init__(arguments=[value]) + + @classmethod + def from_generic_args(cls, result: str, args: List[str], attr: LhrValue): + assert result is None + assert len(args) == 1 + assert attr is None + return cls(args[0]) + + +class ReceiveCMsgOp(ClassicalLhrOp): + OP_NAME = "recv_cmsg" + TYP = LhrInstructionType.CC + + def __init__(self, result: str) -> None: + super().__init__(results=[result]) + + @classmethod + def from_generic_args(cls, result: str, args: List[str], attr: LhrValue): + assert result is not None + assert len(args) == 0 + assert attr is None + return cls(result) + + +class AddCValueOp(ClassicalLhrOp): + OP_NAME = "add_cval_c" + TYP = LhrInstructionType.CL + + def __init__(self, result: str, value0: str, value1: str) -> None: + super().__init__(arguments=[value0, value1], results=[result]) + + @classmethod + def from_generic_args(cls, result: str, args: List[str], attr: LhrValue): + assert result is not None + assert len(args) == 2 + assert attr is None + return cls(result, args[0], args[1]) + + +class MultiplyConstantCValueOp(ClassicalLhrOp): + OP_NAME = "mult_const" + TYP = LhrInstructionType.CL + + def __init__(self, result: str, value0: str, value1: LhrAttribute) -> None: + super().__init__(arguments=[value0, value1], results=[result]) + + @classmethod + def from_generic_args(cls, result: str, args: List[str], attr: LhrValue): + assert result is not None + assert len(args) == 2 + assert attr is None + return cls(result, args[0], args[1]) + + +class BitConditionalMultiplyConstantCValueOp(ClassicalLhrOp): + OP_NAME = "bcond_mult_const" + TYP = LhrInstructionType.CL + + def __init__( + self, result: str, value0: str, value1: LhrAttribute, cond: str + ) -> None: + super().__init__(arguments=[value0, value1, cond], results=[result]) + + @classmethod + def from_generic_args(cls, result: str, args: List[str], attr: LhrValue): + assert result is not None + assert len(args) == 3 + assert attr is None + return cls(result, args[0], args[1], args[2]) + + +class AssignCValueOp(ClassicalLhrOp): + OP_NAME = "assign_cval" + TYP = LhrInstructionType.CL + + def __init__(self, result: str, value: LhrValue) -> None: + super().__init__(results=[result], attributes=[value]) + + @classmethod + def from_generic_args(cls, result: str, args: List[str], attr: LhrValue): + assert result is not None + assert len(args) == 0 + assert attr is not None + return cls(result, attr) + + +class RunSubroutineOp(ClassicalLhrOp): + OP_NAME = "run_subroutine" + TYP = LhrInstructionType.CL + + def __init__(self, values: LhrVector, subrt: LhrSubroutine) -> None: + super().__init__(arguments=[values], attributes=[subrt]) + + @classmethod + def from_generic_args(cls, result: str, args: List[str], attr: LhrValue): + assert result is None + assert len(args) == 1 + assert isinstance(args[0], LhrVector) + assert attr is not None + return cls(args[0], attr) + + @property + def subroutine(self) -> LhrSubroutine: + return self.attributes[0] + + def __str__(self) -> str: + return super().__str__() + + +class ReturnResultOp(ClassicalLhrOp): + OP_NAME = "return_result" + TYP = LhrInstructionType.CL + + def __init__(self, value: str) -> None: + super().__init__(arguments=[value]) + + @classmethod + def from_generic_args(cls, result: str, args: List[str], attr: LhrValue): + assert result is None + assert len(args) == 1 + assert attr is None + return cls(args[0]) + + +LHR_OP_NAMES = { + cls.OP_NAME: cls + for cls in [ + SendCMsgOp, + ReceiveCMsgOp, + AddCValueOp, + MultiplyConstantCValueOp, + BitConditionalMultiplyConstantCValueOp, + AssignCValueOp, + RunSubroutineOp, + ReturnResultOp, + ] +} + + +def netqasm_instr_to_type(instr: NetQASMInstruction) -> LhrInstructionType: + if instr.mnemonic in ["create_epr", "recv_epr"]: + return LhrInstructionType.QC + else: + return LhrInstructionType.QL + + +class LhrProgram: + def __init__( + self, + instructions: List[ClassicalLhrOp], + subroutines: Dict[str, Subroutine], + meta: Optional[ProgramMeta] = None, + ) -> None: + self._instructions: List[ClassicalLhrOp] = instructions + self._subroutines: Dict[str, Subroutine] = subroutines + self._meta: Optional[ProgramMeta] = meta + + @property + def meta(self) -> ProgramMeta: + if self._meta is None: + raise NotImplementedError + return self._meta + + @meta.setter + def meta(self, new_meta: ProgramMeta) -> None: + self._meta = new_meta + + @property + def instructions(self) -> List[ClassicalLhrOp]: + return self._instructions + + @instructions.setter + def instructions(self, new_instrs) -> None: + self._instructions = new_instrs + + def get_instr_signatures(self) -> List[LhrInstructionSignature]: + sigs: List[LhrInstructionSignature] = [] + for instr in self.instructions: + if isinstance(instr, RunSubroutineOp): + subrt = instr.subroutine + for nq_instr in subrt.subroutine.instructions: + typ = netqasm_instr_to_type(nq_instr) + sigs.append(typ) + else: + sigs.append(instr.TYP) + return sigs + + @property + def subroutines(self) -> Dict[str, Subroutine]: + return self._subroutines + + @subroutines.setter + def subroutines(self, new_subroutines) -> None: + self._subroutines = new_subroutines + + def __str__(self) -> str: + # instrs = [ + # f"{str(i)}\n{self.subroutines[i.arguments[0]]}" # inline subroutine contents + # if isinstance(i, RunSubroutineOp) + # else str(i) + # for i in self.instructions + # ] + + # return "\n".join(" " + i for i in instrs) + return "\n".join(" " + str(i) for i in self.instructions) + + +class EndOfTextException(Exception): + pass + + +class LhrParser: + def __init__(self, text: str) -> None: + self._text = text + lines = [line.strip() for line in text.split("\n")] + self._lines = [line for line in lines if len(line) > 0] + self._lineno: int = 0 + + def _next_line(self) -> None: + self._lineno += 1 + if self._lineno >= len(self._lines): + raise EndOfTextException + + def _parse_lhr(self) -> ClassicalLhrOp: + line = self._lines[self._lineno] + + assign_parts = [x.strip() for x in line.split("=")] + assert len(assign_parts) <= 2 + if len(assign_parts) == 1: + value = assign_parts[0] + result = None + elif len(assign_parts) == 2: + value = assign_parts[1] + result = assign_parts[0] + value_parts = [x.strip() for x in value.split(":")] + assert len(value_parts) <= 2 + if len(value_parts) == 2: + value = value_parts[0] + attr = value_parts[1] + try: + attr = int(attr) + except ValueError: + pass + else: + value = value_parts[0] + attr = None + + op_parts = [x.strip() for x in value.split("(")] + assert len(op_parts) == 2 + op = op_parts[0] + arguments = op_parts[1].rstrip(")") + if len(arguments) == 0: + args = [] + else: + args = [x.strip() for x in arguments.split(",")] + + def parse_arg(arg): + if arg.startswith("vec<"): + vec_values_str = arg[4:-1] + if len(vec_values_str) == 0: + vec_values = [] + else: + vec_values = [x.strip() for x in vec_values_str.split(";")] + return LhrVector(vec_values) + return arg + + args = [parse_arg(arg) for arg in args] + + # print(f"result = {result}, op = {op}, args = {args}, attr = {attr}") + + lhr_op = LHR_OP_NAMES[op].from_generic_args(result, args, attr) + return lhr_op + + def _read_line(self) -> str: + self._next_line() + return self._lines[self._lineno] + + def _parse_subroutine(self) -> LhrSubroutine: + return_dict: Dict[str, LhrSharedMemLoc] = {} + while (line := self._read_line()) != "NETQASM_START": + ret_text = "return " + assert line.startswith(ret_text) + map_text = line[len(ret_text) :] + map_parts = [x.strip() for x in map_text.split("->")] + assert len(map_parts) == 2 + shared_loc = map_parts[0] + variable = map_parts[1] + return_dict[variable] = LhrSharedMemLoc(shared_loc) + subrt_lines = [] + while True: + line = self._read_line() + if line == "NETQASM_END": + break + subrt_lines.append(line) + subrt_text = "\n".join(subrt_lines) + try: + subrt = parse_text_subroutine(subrt_text) + except KeyError: + subrt = parse_text_subroutine(subrt_text, flavour=NVFlavour()) + return LhrSubroutine(subrt, return_dict) + + def parse(self) -> LhrProgram: + instructions = [] + subroutines = {} + + try: + while True: + instr = self._parse_lhr() + if isinstance(instr, RunSubroutineOp): + subrt = self._parse_subroutine() + instr = RunSubroutineOp(instr.arguments[0], subrt) + instructions.append(instr) + self._next_line() + except EndOfTextException: + pass + + return LhrProgram(instructions, subroutines) + + +if __name__ == "__main__": + ops = [] + ops.append(SendCMsgOp("my_value")) + ops.append(ReceiveCMsgOp("received_value")) + ops.append(AssignCValueOp("new_value", 3)) + ops.append(AddCValueOp("my_value", "new_value", "new_value")) + + subrt_text = """ + set Q0 0 + rot_z Q0 {my_value} 4 + meas Q0 M0 + ret_reg M0 + """ + subrt = parse_text_subroutine(subrt_text) + lhr_subrt = LhrSubroutine(subrt, {"m": LhrSharedMemLoc("M0")}) + ops.append(RunSubroutineOp(LhrVector(["my_value"]), lhr_subrt)) + ops.append(ReturnResultOp("m")) + + program = LhrProgram( + instructions=ops, + subroutines={"subrt1": subrt}, + ) + print("original program:") + print(program) + + text = str(program) + parsed_program = LhrParser(text).parse() + + print("\nto text and parsed back:") + print(parsed_program) + + print(parsed_program.get_instr_signatures()) diff --git a/squidasm/qoala/lang/target.py b/squidasm/qoala/lang/target.py new file mode 100644 index 00000000..0f5db4cb --- /dev/null +++ b/squidasm/qoala/lang/target.py @@ -0,0 +1,4 @@ +class OfflineHardwareInfo: + """Hardware made available to offline compiler.""" + + pass diff --git a/squidasm/qoala/runtime/config.py b/squidasm/qoala/runtime/config.py new file mode 100644 index 00000000..05fae9e1 --- /dev/null +++ b/squidasm/qoala/runtime/config.py @@ -0,0 +1,198 @@ +from __future__ import annotations + +from typing import Any, List + +import yaml +from pydantic import BaseModel + + +def _from_file(path: str, typ: Any) -> Any: + with open(path, "r") as f: + raw_config = yaml.load(f, Loader=yaml.Loader) + return typ(**raw_config) + + +class GenericQDeviceConfig(BaseModel): + # total number of qubits + num_qubits: int = 2 + # number of communication qubits + num_comm_qubits: int = 2 + + # coherence times (same for each qubit) + T1: int = 10_000_000_000 + T2: int = 1_000_000_000 + + # gate execution times + init_time: int = 10_000 + single_qubit_gate_time: int = 1_000 + two_qubit_gate_time: int = 100_000 + measure_time: int = 10_000 + + # noise model + single_qubit_gate_depolar_prob: float = 0.0 + two_qubit_gate_depolar_prob: float = 0.01 + + @classmethod + def from_file(cls, path: str) -> GenericQDeviceConfig: + return _from_file(path, GenericQDeviceConfig) # type: ignore + + @classmethod + def perfect_config(cls) -> GenericQDeviceConfig: + cfg = GenericQDeviceConfig() + cfg.single_qubit_gate_depolar_prob = 0.0 + cfg.two_qubit_gate_depolar_prob = 0.0 + return cfg + + +class NVQDeviceConfig(BaseModel): + # number of qubits per NV + num_qubits: int = 2 + + # initialization error of the electron spin + electron_init_depolar_prob: float = 0.05 + + # error of the single-qubit gate + electron_single_qubit_depolar_prob: float = 0.0 + + # measurement errors (prob_error_X is the probability that outcome X is flipped to 1 - X) + prob_error_0: float = 0.05 + prob_error_1: float = 0.005 + + # initialization error of the carbon nuclear spin + carbon_init_depolar_prob: float = 0.05 + + # error of the Z-rotation gate on the carbon nuclear spin + carbon_z_rot_depolar_prob: float = 0.001 + + # error of the native NV two-qubit gate + ec_gate_depolar_prob: float = 0.008 + + # coherence times + electron_T1: int = 1_000_000_000 + electron_T2: int = 300_000_000 + carbon_T1: int = 150_000_000_000 + carbon_T2: int = 1_500_000_000 + + # gate execution times + carbon_init: int = 310_000 + carbon_rot_x: int = 500_000 + carbon_rot_y: int = 500_000 + carbon_rot_z: int = 500_000 + electron_init: int = 2_000 + electron_rot_x: int = 5_000 + electron_rot_y: int = 5_000 + electron_rot_z: int = 5_000 + ec_controlled_dir_x: int = 500_000 + ec_controlled_dir_y: int = 500_000 + measure: int = 3_700 + + @classmethod + def from_file(cls, path: str) -> NVQDeviceConfig: + return _from_file(path, NVQDeviceConfig) # type: ignore + + @classmethod + def perfect_config(cls) -> NVQDeviceConfig: + # get default config + cfg = NVQDeviceConfig() + + # set all error params to 0 + cfg.electron_init_depolar_prob = 0 + cfg.electron_single_qubit_depolar_prob = 0 + cfg.prob_error_0 = 0 + cfg.prob_error_1 = 0 + cfg.carbon_init_depolar_prob = 0 + cfg.carbon_z_rot_depolar_prob = 0 + cfg.ec_gate_depolar_prob = 0 + return cfg + + +class StackConfig(BaseModel): + name: str + qdevice_typ: str + qdevice_cfg: Any + host_qnos_latency: float = 0.0 + instr_latency: float = 0.0 + + @classmethod + def from_file(cls, path: str) -> StackConfig: + return _from_file(path, StackConfig) # type: ignore + + @classmethod + def perfect_generic_config(cls, name: str) -> StackConfig: + return StackConfig( + name=name, + qdevice_typ="generic", + qdevice_cfg=GenericQDeviceConfig.perfect_config(), + host_qnos_latency=0.0, + instr_latency=0.0, + ) + + +class DepolariseLinkConfig(BaseModel): + fidelity: float + prob_success: float + t_cycle: float + + @classmethod + def from_file(cls, path: str) -> DepolariseLinkConfig: + return _from_file(path, DepolariseLinkConfig) # type: ignore + + +class NVLinkConfig(BaseModel): + length_A: float + length_B: float + full_cycle: float + cycle_time: float + alpha: float + + @classmethod + def from_file(cls, path: str) -> NVLinkConfig: + return _from_file(path, NVLinkConfig) # type: ignore + + +class HeraldedLinkConfig(BaseModel): + length: float + p_loss_init: float = 0 + p_loss_length: float = 0.25 + speed_of_light: float = 200_000 + dark_count_probability: float = 0 + detector_efficiency: float = 1.0 + visibility: float = 1.0 + num_resolving: bool = False + + @classmethod + def from_file(cls, path: str) -> HeraldedLinkConfig: + return _from_file(path, HeraldedLinkConfig) # type: ignore + + +class LinkConfig(BaseModel): + stack1: str + stack2: str + typ: str + cfg: Any + host_host_latency: float = 0.0 + qnos_qnos_latency: float = 0.0 + + @classmethod + def from_file(cls, path: str) -> LinkConfig: + return _from_file(path, LinkConfig) # type: ignore + + @classmethod + def perfect_config(cls, stack1: str, stack2: str) -> LinkConfig: + return LinkConfig( + stack1=stack1, + stack2=stack2, + typ="perfect", + cfg=None, + host_host_latency=0.0, + qnos_qnos_latency=0.0, + ) + + +class StackNetworkConfig(BaseModel): + stacks: List[StackConfig] + links: List[LinkConfig] + + @classmethod + def from_file(cls, path: str) -> StackNetworkConfig: + return _from_file(path, StackNetworkConfig) # type: ignore diff --git a/squidasm/qoala/runtime/context.py b/squidasm/qoala/runtime/context.py new file mode 100644 index 00000000..e75132cb --- /dev/null +++ b/squidasm/qoala/runtime/context.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from netqasm.sdk.network import NetworkInfo + +from squidasm.qoala.runtime.environment import GlobalEnvironment + + +class NetSquidNetworkInfo(NetworkInfo): + _global_env: GlobalEnvironment + + @classmethod + def _get_node_id(cls, node_name: str) -> int: + nodes = cls._global_env.get_nodes() + for id, info in nodes.items(): + if info.name == node_name: + return id + raise ValueError(f"Node with name {node_name} not found") + + @classmethod + def _get_node_name(cls, node_id: int) -> str: + return cls._global_env.get_nodes()[node_id].name + + @classmethod + def get_node_id_for_app(cls, app_name: str) -> int: + return cls._get_node_id(node_name=app_name) + + @classmethod + def get_node_name_for_app(cls, app_name: str) -> str: + raise NotImplementedError diff --git a/squidasm/qoala/runtime/environment.py b/squidasm/qoala/runtime/environment.py new file mode 100644 index 00000000..5da1baf1 --- /dev/null +++ b/squidasm/qoala/runtime/environment.py @@ -0,0 +1,145 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, List, Tuple, Union + +from squidasm.qoala.runtime.config import ( + GenericQDeviceConfig, + LinkConfig, + NVQDeviceConfig, +) +from squidasm.qoala.runtime.program import ProgramInstance + + +@dataclass +class GlobalNodeInfo: + """Node information available at runtime.""" + + name: str + + # total number of qubits + num_qubits: int + # number of communication qubits + num_comm_qubits: int + + # coherence times for communication qubits + comm_T1: int + comm_T2: int + + # coherence times for memory (non-communication) qubits + mem_T1: int + mem_T2: int + + @classmethod + def from_config( + cls, name: str, config: Union[GenericQDeviceConfig, NVQDeviceConfig] + ) -> GlobalNodeInfo: + if isinstance(config, GenericQDeviceConfig): + return GlobalNodeInfo( + name=name, + num_qubits=config.num_qubits, + num_comm_qubits=config.num_comm_qubits, + comm_T1=config.T1, + comm_T2=config.T2, + mem_T1=config.T1, + mem_T2=config.T2, + ) + else: + assert isinstance(config, NVQDeviceConfig) + return GlobalNodeInfo( + name=name, + num_qubits=config.num_qubits, + num_comm_qubits=1, + comm_T1=config.electron_T1, + comm_T2=config.electron_T2, + mem_T1=config.carbon_T1, + mem_T2=config.carbon_T2, + ) + + +@dataclass +class GlobalLinkInfo: + node_name1: str + node_name2: str + + fidelity: float + + @classmethod + def from_config( + cls, node_name1: str, node_name2: str, config: LinkConfig + ) -> GlobalLinkInfo: + if config.typ == "perfect": + return GlobalLinkInfo( + node_name1=node_name1, node_name2=node_name2, fidelity=1.0 + ) + elif config.typ == "depolarise": + return GlobalLinkInfo( + node_name1=node_name1, + node_name2=node_name2, + fidelity=config.cfg.fidelity, # type: ignore + ) + else: + raise NotImplementedError + + +class GlobalEnvironment: + def __init__(self) -> None: + # node ID -> node info + self._nodes: Dict[int, GlobalNodeInfo] = {} + + # (node A ID, node B ID) -> link info + # for a pair (a, b) there exists no separate (b, a) info (it is the same) + self._links: Dict[Tuple[int, int], GlobalLinkInfo] = {} + + def get_nodes(self) -> Dict[int, GlobalNodeInfo]: + return self._nodes + + def get_node_id(self, name: str) -> int: + for id, node in self._nodes.items(): + if node.name == name: + return id + + def set_nodes(self, nodes: Dict[int, GlobalNodeInfo]) -> None: + self._nodes = nodes + + def add_node(self, id: int, node: GlobalNodeInfo) -> None: + self._nodes[id] = node + + def get_links(self) -> Dict[int, GlobalLinkInfo]: + return self._links + + def set_links(self, links: Dict[int, GlobalLinkInfo]) -> None: + self._links = links + + def add_link(self, id: int, link: GlobalLinkInfo) -> None: + self._links[id] = link + + +class LocalEnvironment: + def __init__(self, global_env: GlobalEnvironment, node_id: int) -> None: + self._global_env: GlobalEnvironment = global_env + + # node ID of self + self._node_id: int = node_id + + self._programs: List[ProgramInstance] = [] + self._csockets: List[str] = [] + self._epr_sockets: List[str] = [] + + def get_global_env(self) -> GlobalEnvironment: + return self._global_env + + def get_node_id(self) -> int: + return self._node_id + + def register_program(self, program: ProgramInstance) -> None: + self._programs.append(program) + + def open_epr_socket(self) -> None: + pass + + +class ProgramEnvironment: + """Environment interface given to a program""" + + pass diff --git a/squidasm/qoala/runtime/program.py b/squidasm/qoala/runtime/program.py new file mode 100644 index 00000000..22a520ef --- /dev/null +++ b/squidasm/qoala/runtime/program.py @@ -0,0 +1,23 @@ +import abc +from dataclasses import dataclass +from typing import Any, Dict, Union + +from squidasm.qoala.lang.lhr import LhrProgram + + +class ProgramContext(abc.ABC): + pass + + +class SdkProgram(abc.ABC): + @abc.abstractmethod + def compile(self, context: ProgramContext) -> LhrProgram: + raise NotImplementedError + + +@dataclass +class ProgramInstance: + program: Union[LhrProgram, SdkProgram] + inputs: Dict[str, Any] + num_iterations: int + deadline: float diff --git a/squidasm/qoala/runtime/run.py b/squidasm/qoala/runtime/run.py new file mode 100644 index 00000000..62c59093 --- /dev/null +++ b/squidasm/qoala/runtime/run.py @@ -0,0 +1,192 @@ +from __future__ import annotations + +import itertools +from typing import Any, Dict, List + +import netsquid as ns +from netsquid_magic.link_layer import ( + MagicLinkLayerProtocol, + MagicLinkLayerProtocolWithSignaling, + SingleClickTranslationUnit, +) +from netsquid_magic.magic_distributor import ( + DepolariseWithFailureMagicDistributor, + DoubleClickMagicDistributor, + PerfectStateMagicDistributor, +) +from netsquid_nv.magic_distributor import NVSingleClickMagicDistributor +from netsquid_physlayer.heralded_connection import MiddleHeraldedConnection + +from squidasm.qoala.runtime.config import ( + DepolariseLinkConfig, + GenericQDeviceConfig, + HeraldedLinkConfig, + NVLinkConfig, + NVQDeviceConfig, + StackNetworkConfig, +) +from squidasm.qoala.runtime.context import NetSquidNetworkInfo +from squidasm.qoala.runtime.environment import GlobalEnvironment, GlobalNodeInfo +from squidasm.qoala.runtime.program import ProgramInstance +from squidasm.qoala.sim.build import build_generic_qdevice, build_nv_qdevice +from squidasm.qoala.sim.globals import GlobalSimData +from squidasm.qoala.sim.stack import NodeStack, StackNetwork + + +def fidelity_to_prob_max_mixed(fid: float) -> float: + return (1 - fid) * 4.0 / 3.0 + + +def _setup_network(config: StackNetworkConfig, rte: GlobalEnvironment) -> StackNetwork: + assert len(config.stacks) <= 2 + assert len(config.links) <= 1 + + stacks: Dict[str, NodeStack] = {} + link_prots: List[MagicLinkLayerProtocol] = [] + + for node_id, cfg in enumerate(config.stacks): + # TODO !!! + # get HW info from config + node_info = GlobalNodeInfo(cfg.name, 2, 1, 0, 0, 0, 0) + rte.add_node(node_id, node_info) + + if cfg.qdevice_typ == "nv": + qdevice_cfg = cfg.qdevice_cfg + if not isinstance(qdevice_cfg, NVQDeviceConfig): + qdevice_cfg = NVQDeviceConfig(**cfg.qdevice_cfg) + qdevice = build_nv_qdevice(f"qdevice_{cfg.name}", cfg=qdevice_cfg) + stack = NodeStack( + cfg.name, + global_env=rte, + qdevice_type="nv", + qdevice=qdevice, + node_id=node_id, + ) + elif cfg.qdevice_typ == "generic": + qdevice_cfg = cfg.qdevice_cfg + if not isinstance(qdevice_cfg, GenericQDeviceConfig): + qdevice_cfg = GenericQDeviceConfig(**cfg.qdevice_cfg) + qdevice = build_generic_qdevice(f"qdevice_{cfg.name}", cfg=qdevice_cfg) + stack = NodeStack( + cfg.name, + global_env=rte, + qdevice_type="generic", + qdevice=qdevice, + node_id=node_id, + ) + + stacks[cfg.name] = stack + + for (_, s1), (_, s2) in itertools.combinations(stacks.items(), 2): + s1.connect_to(s2) + + for link in config.links: + stack1 = stacks[link.stack1] + stack2 = stacks[link.stack2] + if link.typ == "perfect": + link_dist = PerfectStateMagicDistributor( + nodes=[stack1.node, stack2.node], state_delay=1000.0 + ) + elif link.typ == "depolarise": + link_cfg = link.cfg + if not isinstance(link_cfg, DepolariseLinkConfig): + link_cfg = DepolariseLinkConfig(**link.cfg) + prob_max_mixed = fidelity_to_prob_max_mixed(link_cfg.fidelity) + link_dist = DepolariseWithFailureMagicDistributor( + nodes=[stack1.node, stack2.node], + prob_max_mixed=prob_max_mixed, + prob_success=link_cfg.prob_success, + t_cycle=link_cfg.t_cycle, + ) + elif link.typ == "nv": + link_cfg = link.cfg + if not isinstance(link_cfg, NVLinkConfig): + link_cfg = NVLinkConfig(**link.cfg) + link_dist = NVSingleClickMagicDistributor( + nodes=[stack1.node, stack2.node], + length_A=link_cfg.length_A, + length_B=link_cfg.length_B, + full_cycle=link_cfg.full_cycle, + cycle_time=link_cfg.cycle_time, + alpha=link_cfg.alpha, + ) + elif link.typ == "heralded": + link_cfg = link.cfg + if not isinstance(link_cfg, HeraldedLinkConfig): + link_cfg = HeraldedLinkConfig(**link.cfg) + connection = MiddleHeraldedConnection( + name="heralded_conn", **link_cfg.dict() + ) + link_dist = DoubleClickMagicDistributor( + [stack1.node, stack2.node], connection + ) + else: + raise ValueError + + link_prot = MagicLinkLayerProtocolWithSignaling( + nodes=[stack1.node, stack2.node], + magic_distributor=link_dist, + translation_unit=SingleClickTranslationUnit(), + ) + stack1.assign_ll_protocol(link_prot) + stack2.assign_ll_protocol(link_prot) + + link_prots.append(link_prot) + + return StackNetwork(stacks, link_prots) + + +def _run(network: StackNetwork) -> List[Dict[str, Any]]: + """Run the protocols of a network and programs running in that network. + + NOTE: For now, only two nodes and a single link are supported. + + :param network: `StackNetwork` representing the nodes and links + :return: final results of the programs + """ + assert len(network.stacks) <= 2 + assert len(network.links) <= 1 + + # Start the link protocols. + for link in network.links: + link.start() + + # Start the node protocols. + for _, stack in network.stacks.items(): + stack.start() + + # Start the NetSquid simulation. + ns.sim_run() + + return [stack.host.get_results() for _, stack in network.stacks.items()] + + +def run( + config: StackNetworkConfig, programs: Dict[str, ProgramInstance], num_times: int = 1 +) -> List[Dict[str, Any]]: + """Run programs on a network specified by a network configuration. + + :param config: configuration of the network + :param programs: dictionary of node names to programs + :param num_times: numbers of times to run the programs, defaults to 1 + :return: program results + """ + # Create global runtime environment. + rte = GlobalEnvironment() + + # Build the network. Info about created nodes will be added to the runtime environment. + network = _setup_network(config, rte) + + # TODO: rewrite + NetSquidNetworkInfo._global_env = rte + + GlobalSimData.set_network(network) + + for name, program in programs.items(): + network.stacks[name]._local_env.register_program(program) + + for name in programs.keys(): + network.stacks[name].install_environment() + + results = _run(network) + return results diff --git a/squidasm/qoala/sim/build.py b/squidasm/qoala/sim/build.py new file mode 100644 index 00000000..63313bd3 --- /dev/null +++ b/squidasm/qoala/sim/build.py @@ -0,0 +1,232 @@ +import numpy as np +from netsquid.components.instructions import ( + INSTR_CNOT, + INSTR_CXDIR, + INSTR_CYDIR, + INSTR_CZ, + INSTR_H, + INSTR_INIT, + INSTR_MEASURE, + INSTR_ROT_X, + INSTR_ROT_Y, + INSTR_ROT_Z, + INSTR_X, + INSTR_Y, + INSTR_Z, +) +from netsquid.components.models.qerrormodels import DepolarNoiseModel, T1T2NoiseModel +from netsquid.components.qprocessor import PhysicalInstruction, QuantumProcessor +from netsquid.qubits.operators import Operator + +from squidasm.run.stack.config import GenericQDeviceConfig, NVQDeviceConfig + + +def build_generic_qdevice(name: str, cfg: GenericQDeviceConfig) -> QuantumProcessor: + phys_instructions = [] + + single_qubit_gate_noise = DepolarNoiseModel( + depolar_rate=cfg.single_qubit_gate_depolar_prob, time_independent=True + ) + + two_qubit_gate_noise = DepolarNoiseModel( + depolar_rate=cfg.two_qubit_gate_depolar_prob, time_independent=True + ) + + phys_instructions.append( + PhysicalInstruction( + INSTR_INIT, + parallel=False, + duration=cfg.init_time, + ) + ) + + for instr in [ + INSTR_ROT_X, + INSTR_ROT_Y, + INSTR_ROT_Z, + INSTR_X, + INSTR_Y, + INSTR_Z, + INSTR_H, + ]: + phys_instructions.append( + PhysicalInstruction( + instr, + parallel=False, + quantum_noise_model=single_qubit_gate_noise, + apply_q_noise_after=True, + duration=cfg.single_qubit_gate_time, + ) + ) + + for instr in [INSTR_CNOT, INSTR_CZ]: + phys_instructions.append( + PhysicalInstruction( + instr, + parallel=False, + quantum_noise_model=two_qubit_gate_noise, + apply_q_noise_after=True, + duration=cfg.two_qubit_gate_time, + ) + ) + + phys_instr_measure = PhysicalInstruction( + INSTR_MEASURE, + parallel=False, + duration=cfg.measure_time, + ) + phys_instructions.append(phys_instr_measure) + + electron_qubit_noise = T1T2NoiseModel(T1=cfg.T1, T2=cfg.T2) + mem_noise_models = [electron_qubit_noise] * cfg.num_qubits + qmem = QuantumProcessor( + name=name, + num_positions=cfg.num_qubits, + mem_noise_models=mem_noise_models, + phys_instructions=phys_instructions, + ) + return qmem + + +def build_nv_qdevice(name: str, cfg: NVQDeviceConfig) -> QuantumProcessor: + + # noise models for single- and multi-qubit operations + electron_init_noise = DepolarNoiseModel( + depolar_rate=cfg.electron_init_depolar_prob, time_independent=True + ) + + electron_single_qubit_noise = DepolarNoiseModel( + depolar_rate=cfg.electron_single_qubit_depolar_prob, time_independent=True + ) + + carbon_init_noise = DepolarNoiseModel( + depolar_rate=cfg.carbon_init_depolar_prob, time_independent=True + ) + + carbon_z_rot_noise = DepolarNoiseModel( + depolar_rate=cfg.carbon_z_rot_depolar_prob, time_independent=True + ) + + ec_noise = DepolarNoiseModel( + depolar_rate=cfg.ec_gate_depolar_prob, time_independent=True + ) + + electron_qubit_noise = T1T2NoiseModel(T1=cfg.electron_T1, T2=cfg.electron_T2) + + carbon_qubit_noise = T1T2NoiseModel(T1=cfg.carbon_T1, T2=cfg.carbon_T2) + + # defining gates and their gate times + + phys_instructions = [] + + electron_position = 0 + carbon_positions = [pos + 1 for pos in range(cfg.num_qubits - 1)] + + phys_instructions.append( + PhysicalInstruction( + INSTR_INIT, + parallel=False, + topology=carbon_positions, + quantum_noise_model=carbon_init_noise, + apply_q_noise_after=True, + duration=cfg.carbon_init, + ) + ) + + for (instr, dur) in zip( + [INSTR_ROT_X, INSTR_ROT_Y, INSTR_ROT_Z], + [cfg.carbon_rot_x, cfg.carbon_rot_y, cfg.carbon_rot_z], + ): + phys_instructions.append( + PhysicalInstruction( + instr, + parallel=False, + topology=carbon_positions, + quantum_noise_model=carbon_z_rot_noise, + apply_q_noise_after=True, + duration=dur, + ) + ) + + phys_instructions.append( + PhysicalInstruction( + INSTR_INIT, + parallel=False, + topology=[electron_position], + quantum_noise_model=electron_init_noise, + apply_q_noise_after=True, + duration=cfg.electron_init, + ) + ) + + for (instr, dur) in zip( + [INSTR_ROT_X, INSTR_ROT_Y, INSTR_ROT_Z], + [cfg.electron_rot_x, cfg.electron_rot_y, cfg.electron_rot_z], + ): + phys_instructions.append( + PhysicalInstruction( + instr, + parallel=False, + topology=[electron_position], + quantum_noise_model=electron_single_qubit_noise, + apply_q_noise_after=True, + duration=dur, + ) + ) + + electron_carbon_topologies = [ + (electron_position, carbon_pos) for carbon_pos in carbon_positions + ] + phys_instructions.append( + PhysicalInstruction( + INSTR_CXDIR, + parallel=False, + topology=electron_carbon_topologies, + quantum_noise_model=ec_noise, + apply_q_noise_after=True, + duration=cfg.ec_controlled_dir_x, + ) + ) + + phys_instructions.append( + PhysicalInstruction( + INSTR_CYDIR, + parallel=False, + topology=electron_carbon_topologies, + quantum_noise_model=ec_noise, + apply_q_noise_after=True, + duration=cfg.ec_controlled_dir_y, + ) + ) + + M0 = Operator( + "M0", np.diag([np.sqrt(1 - cfg.prob_error_0), np.sqrt(cfg.prob_error_1)]) + ) + M1 = Operator( + "M1", np.diag([np.sqrt(cfg.prob_error_0), np.sqrt(1 - cfg.prob_error_1)]) + ) + + # hack to set imperfect measurements + INSTR_MEASURE._meas_operators = [M0, M1] + + phys_instr_measure = PhysicalInstruction( + INSTR_MEASURE, + parallel=False, + topology=[electron_position], + quantum_noise_model=None, + duration=cfg.measure, + ) + + phys_instructions.append(phys_instr_measure) + + # add qubits + mem_noise_models = [electron_qubit_noise] + [carbon_qubit_noise] * len( + carbon_positions + ) + qmem = QuantumProcessor( + name=name, + num_positions=cfg.num_qubits, + mem_noise_models=mem_noise_models, + phys_instructions=phys_instructions, + ) + return qmem diff --git a/squidasm/qoala/sim/common.py b/squidasm/qoala/sim/common.py new file mode 100644 index 00000000..8fb2aab2 --- /dev/null +++ b/squidasm/qoala/sim/common.py @@ -0,0 +1,393 @@ +import logging +from dataclasses import dataclass +from typing import Dict, Generator, List, Optional, Set, Tuple, Union + +import netsquid as ns +from netqasm.lang import operand +from netqasm.lang.encoding import RegisterName +from netqasm.sdk.shared_memory import Arrays, RegisterGroup, setup_registers +from netsquid.components.component import Component, Port +from netsquid.protocols import Protocol + +from pydynaa import EventExpression + + +class SimTimeFilter(logging.Filter): + def filter(self, record): + record.simtime = ns.sim_time() + return True + + +class LogManager: + STACK_LOGGER = "Stack" + _LOGGER_HAS_BEEN_SETUP = False + + @classmethod + def _setup_stack_logger(cls) -> None: + logger = logging.getLogger(cls.STACK_LOGGER) + formatter = logging.Formatter( + "%(levelname)s:%(simtime)s ns:%(name)s:%(message)s" + ) + syslog = logging.StreamHandler() + syslog.setFormatter(formatter) + syslog.addFilter(SimTimeFilter()) + logger.addHandler(syslog) + logger.propagate = False + cls._LOGGER_HAS_BEEN_SETUP = True + + @classmethod + def get_stack_logger(cls, sub_logger: Optional[str] = None) -> logging.Logger: + if not cls._LOGGER_HAS_BEEN_SETUP: + cls._setup_stack_logger() + logger = logging.getLogger(cls.STACK_LOGGER) + if sub_logger is None: + return logger + else: + return logger.getChild(sub_logger) + + @classmethod + def set_log_level(cls, level: Union[int, str]) -> None: + logger = cls.get_stack_logger() + logger.setLevel(level) + + @classmethod + def get_log_level(cls) -> int: + return cls.get_stack_logger().level + + @classmethod + def log_to_file(cls, path: str) -> None: + fileHandler = logging.FileHandler(path, mode="w") + formatter = logging.Formatter( + "%(levelname)s:%(simtime)s ns:%(name)s:%(message)s" + ) + fileHandler.setFormatter(formatter) + fileHandler.addFilter(SimTimeFilter()) + cls.get_stack_logger().addHandler(fileHandler) + + +class PortListener(Protocol): + def __init__(self, port: Port, signal_label: str) -> None: + self._buffer: List[bytes] = [] + self._port: Port = port + self._signal_label = signal_label + self.add_signal(signal_label) + + @property + def buffer(self) -> List[bytes]: + return self._buffer + + def run(self) -> Generator[EventExpression, None, None]: + while True: + # Wait for an event saying that there is new input. + yield self.await_port_input(self._port) + + counter = 0 + # Read all inputs and count them. + while True: + input = self._port.rx_input() + if input is None: + break + self._buffer += input.items + counter += 1 + # If there are n inputs, there have been n events, but we yielded only + # on one of them so far. "Flush" these n-1 additional events: + while counter > 1: + yield self.await_port_input(self._port) + counter -= 1 + + # Only after having yielded on all current events, we can schedule a + # notification event, so that its reactor can handle all inputs at once. + self.send_signal(self._signal_label) + + +class RegisterMeta: + @classmethod + def prefixes(cls) -> List[str]: + return ["R", "C", "Q", "M"] + + @classmethod + def parse(cls, name: str) -> Tuple[RegisterName, int]: + assert len(name) >= 2 + assert name[0] in cls.prefixes() + group = RegisterName[name[0]] + index = int(name[1:]) + assert index < 16 + return group, index + + +class ComponentProtocol(Protocol): + def __init__(self, name: str, comp: Component) -> None: + super().__init__(name) + self._listeners: Dict[str, PortListener] = {} + self._logger: logging.Logger = LogManager.get_stack_logger( + f"{self.__class__.__name__}({comp.name})" + ) + + def add_listener(self, name, listener: PortListener) -> None: + self._listeners[name] = listener + + def _receive_msg( + self, listener_name: str, wake_up_signal: str + ) -> Generator[EventExpression, None, str]: + listener = self._listeners[listener_name] + if len(listener.buffer) == 0: + yield self.await_signal(sender=listener, signal_label=wake_up_signal) + return listener.buffer.pop(0) + + def start(self) -> None: + super().start() + for listener in self._listeners.values(): + listener.start() + + def stop(self) -> None: + for listener in self._listeners.values(): + listener.stop() + super().stop() + + +class AppMemory: + def __init__(self, app_id: int, max_qubits: int) -> None: + self._app_id: int = app_id + self._registers: Dict[RegisterName, RegisterGroup] = setup_registers() + self._arrays: Arrays = Arrays() + self._virt_qubits: Dict[int, Optional[int]] = { + i: None for i in range(max_qubits) + } + self._prog_counter: int = 0 + + @property + def prog_counter(self) -> int: + return self._prog_counter + + def increment_prog_counter(self) -> None: + self._prog_counter += 1 + + def set_prog_counter(self, value: int) -> None: + self._prog_counter = value + + def map_virt_id(self, virt_id: int, phys_id: int) -> None: + self._virt_qubits[virt_id] = phys_id + + def unmap_virt_id(self, virt_id: int) -> None: + self._virt_qubits[virt_id] = None + + def unmap_all(self) -> None: + for virt_id in self._virt_qubits: + self._virt_qubits[virt_id] = None + + @property + def qubit_mapping(self) -> Dict[int, Optional[int]]: + return self._virt_qubits + + def phys_id_for(self, virt_id: int) -> int: + return self._virt_qubits[virt_id] + + def virt_id_for(self, phys_id: int) -> Optional[int]: + for virt, phys in self._virt_qubits.items(): + if phys == phys_id: + return virt + return None + + def set_reg_value(self, register: Union[str, operand.Register], value: int) -> None: + if isinstance(register, str): + name, index = RegisterMeta.parse(register) + else: + name, index = register.name, register.index + self._registers[name][index] = value + + def get_reg_value(self, register: Union[str, operand.Register]) -> int: + if isinstance(register, str): + name, index = RegisterMeta.parse(register) + else: + name, index = register.name, register.index + return self._registers[name][index] + + # for compatibility with netqasm Futures + def get_register(self, register: Union[str, operand.Register]) -> Optional[int]: + return self.get_reg_value(register) + + # for compatibility with netqasm Futures + def get_array_part( + self, address: int, index: Union[int, slice] + ) -> Union[None, int, List[Optional[int]]]: + if isinstance(index, int): + return self.get_array_value(address, index) + elif isinstance(index, slice): + return self.get_array_values(address, index.start, index.stop) + + def init_new_array(self, address: int, length: int) -> None: + self._arrays.init_new_array(address, length) + + def get_array(self, address: int) -> List[Optional[int]]: + return self._arrays._get_array(address) + + def get_array_entry(self, array_entry: operand.ArrayEntry) -> Optional[int]: + address, index = self.expand_array_part(array_part=array_entry) + result = self._arrays[address, index] + assert (result is None) or isinstance(result, int) + return result + + def get_array_value(self, addr: int, offset: int) -> Optional[int]: + address, index = self.expand_array_part( + array_part=operand.ArrayEntry(operand.Address(addr), offset) + ) + result = self._arrays[address, index] + assert (result is None) or isinstance(result, int) + return result + + def get_array_values( + self, addr: int, start_offset: int, end_offset + ) -> List[Optional[int]]: + values = self.get_array_slice( + operand.ArraySlice(operand.Address(addr), start_offset, end_offset) + ) + assert values is not None + return values + + def set_array_entry( + self, array_entry: operand.ArrayEntry, value: Optional[int] + ) -> None: + address, index = self.expand_array_part(array_part=array_entry) + self._arrays[address, index] = value + + def set_array_value(self, addr: int, offset: int, value: Optional[int]) -> None: + address, index = self.expand_array_part( + array_part=operand.ArrayEntry(operand.Address(addr), offset) + ) + self._arrays[address, index] = value + + def get_array_slice( + self, array_slice: operand.ArraySlice + ) -> Optional[List[Optional[int]]]: + address, index = self.expand_array_part(array_part=array_slice) + result = self._arrays[address, index] + assert (result is None) or isinstance(result, list) + return result + + def expand_array_part( + self, array_part: Union[operand.ArrayEntry, operand.ArraySlice] + ) -> Tuple[int, Union[int, slice]]: + address: int = array_part.address.address + index: Union[int, slice] + if isinstance(array_part, operand.ArrayEntry): + if isinstance(array_part.index, int): + index = array_part.index + else: + index_from_reg = self.get_reg_value(register=array_part.index) + if index_from_reg is None: + raise RuntimeError( + f"Trying to use register {array_part.index} " + "to index an array but its value is None" + ) + index = index_from_reg + elif isinstance(array_part, operand.ArraySlice): + startstop: List[int] = [] + for raw_s in [array_part.start, array_part.stop]: + if isinstance(raw_s, int): + startstop.append(raw_s) + elif isinstance(raw_s, operand.Register): + s = self.get_reg_value(register=raw_s) + if s is None: + raise RuntimeError( + f"Trying to use register {raw_s} to " + "index an array but its value is None" + ) + startstop.append(s) + else: + raise RuntimeError( + f"Something went wrong: raw_s should be int " + f"or Register but is {type(raw_s)}" + ) + index = slice(*startstop) + else: + raise RuntimeError( + f"Something went wrong: array_part is a {type(array_part)}" + ) + return address, index + + +@dataclass +class NetstackCreateRequest: + app_id: int + remote_node_id: int + epr_socket_id: int + qubit_array_addr: int + arg_array_addr: int + result_array_addr: int + + +@dataclass +class NetstackReceiveRequest: + app_id: int + remote_node_id: int + epr_socket_id: int + qubit_array_addr: int + result_array_addr: int + + +@dataclass +class NetstackBreakpointCreateRequest: + app_id: int + + +@dataclass +class NetstackBreakpointReceiveRequest: + app_id: int + + +class AllocError(Exception): + pass + + +class PhysicalQuantumMemory: + def __init__(self, qubit_count: int) -> None: + self._qubit_count = qubit_count + self._allocated_ids: Set[int] = set() + self._comm_qubit_ids: Set[int] = {i for i in range(qubit_count)} + + @property + def qubit_count(self) -> int: + return self._qubit_count + + @property + def comm_qubit_count(self) -> int: + return len(self._comm_qubit_ids) + + def allocate(self) -> int: + """Allocate a qubit (communcation or memory).""" + for i in range(self._qubit_count): + if i not in self._allocated_ids: + self._allocated_ids.add(i) + return i + raise AllocError("No more qubits available") + + def allocate_comm(self) -> int: + """Allocate a communication qubit.""" + for i in range(self._qubit_count): + if i not in self._allocated_ids and i in self._comm_qubit_ids: + self._allocated_ids.add(i) + return i + raise AllocError("No more comm qubits available") + + def allocate_mem(self) -> int: + """Allocate a memory qubit.""" + for i in range(self._qubit_count): + if i not in self._allocated_ids and i not in self._comm_qubit_ids: + self._allocated_ids.add(i) + return i + raise AllocError("No more mem qubits available") + + def free(self, id: int) -> None: + self._allocated_ids.remove(id) + + def is_allocated(self, id: int) -> bool: + return id in self._allocated_ids + + def clear(self) -> None: + self._allocated_ids = {} + + +class NVPhysicalQuantumMemory(PhysicalQuantumMemory): + def __init__(self, qubit_count: int) -> None: + super().__init__(qubit_count) + self._comm_qubit_ids: Set[int] = {0} diff --git a/squidasm/qoala/sim/connection.py b/squidasm/qoala/sim/connection.py new file mode 100644 index 00000000..72bdd9d9 --- /dev/null +++ b/squidasm/qoala/sim/connection.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Callable, Generator, Optional, Type + +from netqasm.backend.messages import SubroutineMessage +from netqasm.lang.ir import ProtoSubroutine +from netqasm.lang.subroutine import Subroutine +from netqasm.sdk.build_types import GenericHardwareConfig, HardwareConfig +from netqasm.sdk.builder import Builder +from netqasm.sdk.connection import T_Message +from netqasm.sdk.shared_memory import SharedMemory + +from pydynaa import EventExpression + +if TYPE_CHECKING: + from netqasm.sdk.transpile import SubroutineTranspiler + from squidasm.qoala.sim.host import Host + +from squidasm.qoala.sim.common import LogManager + + +class QnosConnection: + def __init__( + self, + host: Host, + app_id: int, + app_name: str, + max_qubits: int = 5, + hardware_config: Optional[HardwareConfig] = None, + compiler: Optional[Type[SubroutineTranspiler]] = None, + **kwargs, + ) -> None: + self._app_name = app_name + self._app_id = app_id + self._node_name = app_name + self._max_qubits = max_qubits + + self._host = host + + self._shared_memory = None + self._logger: logging.Logger = LogManager.get_stack_logger( + f"{self.__class__.__name__}({self._app_name})" + ) + + if hardware_config is None: + hardware_config = GenericHardwareConfig(max_qubits) + + self._builder = Builder( + connection=self, + app_id=self._app_id, + hardware_config=hardware_config, + compiler=compiler, + ) + + @property + def shared_memory(self) -> SharedMemory: + return self._shared_memory + + def _commit_message( + self, msg: T_Message, block: bool = True, callback: Optional[Callable] = None + ) -> None: + assert isinstance(msg, SubroutineMessage) + self._logger.debug(f"Committing message {msg}") + self._host.send_qnos_msg(bytes(msg)) + + def commit_protosubroutine( + self, + protosubroutine: ProtoSubroutine, + block: bool = True, + callback: Optional[Callable] = None, + ) -> Generator[EventExpression, None, None]: + self._logger.info(f"Flushing protosubroutine:\n{protosubroutine}") + + subroutine = self._builder.subrt_compile_subroutine(protosubroutine) + self._logger.info(f"Flushing compiled subroutine:\n{subroutine}") + + yield from self.commit_subroutine(subroutine, block, callback) + self._builder._reset() + + def commit_subroutine( + self, + subroutine: Subroutine, + block: bool = True, + callback: Optional[Callable] = None, + ) -> Generator[EventExpression, None, None]: + self._logger.info(f"Commiting compiled subroutine:\n{subroutine}") + + self._commit_message( + msg=SubroutineMessage(subroutine=subroutine), + block=block, + callback=callback, + ) + + result = yield from self._host.receive_qnos_msg() + self._shared_memory = result + + def flush( + self, block: bool = True, callback: Optional[Callable] = None + ) -> Generator[EventExpression, None, None]: + subroutine = self._builder.subrt_pop_pending_subroutine() + if subroutine is None: + return + + yield from self.commit_protosubroutine( + protosubroutine=subroutine, + block=block, + callback=callback, + ) + + def _commit_serialized_message( + self, raw_msg: bytes, block: bool = True, callback: Optional[Callable] = None + ) -> None: + pass diff --git a/squidasm/qoala/sim/csocket.py b/squidasm/qoala/sim/csocket.py new file mode 100644 index 00000000..6ebd2592 --- /dev/null +++ b/squidasm/qoala/sim/csocket.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Generator + +from netqasm.sdk.classical_communication.message import StructuredMessage + +from pydynaa import EventExpression + +if TYPE_CHECKING: + from squidasm.qoala.sim.host import Host + + +class ClassicalSocket: + def __init__(self, host: Host, remote_name: str): + self._host = host + self._remote_name = remote_name + + def send(self, msg: str) -> None: + """Sends a message to the remote node.""" + self._host.send_peer_msg(msg) + + def recv(self) -> Generator[EventExpression, None, str]: + return (yield from self._host.receive_peer_msg()) + + def send_int(self, value: int) -> None: + self.send(str(value)) + + def recv_int(self) -> Generator[EventExpression, None, int]: + value = yield from self.recv() + return int(value) + + def send_float(self, value: float) -> None: + self.send(str(value)) + + def recv_float(self) -> Generator[EventExpression, None, float]: + value = yield from self.recv() + return float(value) + + def send_structured(self, msg: StructuredMessage) -> None: + self.send(msg) + + def recv_structured(self) -> Generator[EventExpression, None, StructuredMessage]: + value = yield from self.recv() + return value diff --git a/squidasm/qoala/sim/egp.py b/squidasm/qoala/sim/egp.py new file mode 100644 index 00000000..b0549d02 --- /dev/null +++ b/squidasm/qoala/sim/egp.py @@ -0,0 +1,110 @@ +from abc import ABCMeta, abstractmethod + +from netsquid import BellIndex +from netsquid.protocols import ServiceProtocol +from netsquid_magic.link_layer import TranslationUnit +from qlink_interface import ( + ReqCreateAndKeep, + ReqMeasureDirectly, + ReqReceive, + ReqRemoteStatePrep, + ReqStopReceive, + ResCreateAndKeep, + ResError, + ResMeasureDirectly, + ResRemoteStatePrep, +) + +# (Mostly) copied from the nlblueprint repo. +# This is done to not have nlblueprint as a dependency. + + +class EGPService(ServiceProtocol, metaclass=ABCMeta): + def __init__(self, node, name=None): + super().__init__(node=node, name=name) + self.register_request(ReqCreateAndKeep, self.create_and_keep) + self.register_request(ReqMeasureDirectly, self.measure_directly) + self.register_request(ReqReceive, self.receive) + self.register_request(ReqStopReceive, self.stop_receive) + self.register_response(ResCreateAndKeep) + self.register_response(ResMeasureDirectly) + self.register_response(ResRemoteStatePrep) + self.register_response(ResError) + self._current_create_id = 0 + + @abstractmethod + def create_and_keep(self, req): + assert isinstance(req, ReqCreateAndKeep) + + @abstractmethod + def measure_directly(self, req): + assert isinstance(req, ReqMeasureDirectly) + + @abstractmethod + def remote_state_preparation(self, req): + assert isinstance(req, ReqRemoteStatePrep) + + @abstractmethod + def receive(self, req): + assert isinstance(req, ReqReceive) + + @abstractmethod + def stop_receive(self, req): + assert isinstance(req, ReqStopReceive) + + def _get_create_id(self): + create_id = self._current_create_id + self._current_create_id += 1 + return create_id + + +class EgpProtocol(EGPService): + def __init__(self, node, magic_link_layer_protocol, name=None): + super().__init__(node=node, name=name) + self._ll_prot = magic_link_layer_protocol + + def run(self): + while True: + yield self.await_signal( + sender=self._ll_prot, + signal_label="react_to_{}".format(self.node.ID), + ) + result = self._ll_prot.get_signal_result( + label="react_to_{}".format(self.node.ID), receiver=self + ) + if result.node_id == self.node.ID: + try: + BellIndex(result.msg.bell_state) + except AttributeError: + pass + except ValueError: + raise TypeError( + f"{result.msg.bell_state}, which was obtained from magic link layer protocol," + f"is not a :class:`netsquid.qubits.ketstates.BellIndex`." + ) + self.send_response(response=result.msg) + + def create_and_keep(self, req): + super().create_and_keep(req) + return self._ll_prot.put_from(self.node.ID, req) + + def measure_directly(self, req): + super().measure_directly(req) + return self._ll_prot.put_from(self.node.ID, req) + + def remote_state_preparation(self, req): + super().remote_state_preparation(req) + return self._ll_prot.put_from(self.node.ID, req) + + def receive(self, req): + super().receive(req) + self._ll_prot.put_from(self.node.ID, req) + + def stop_receive(self, req): + super().stop_receive(req) + self._ll_prot.put_from(self.node.ID, req) + + +class EgpTranslationUnit(TranslationUnit): + def request_to_parameters(self, request, **fixed_parameters): + return {} diff --git a/squidasm/qoala/sim/globals.py b/squidasm/qoala/sim/globals.py new file mode 100644 index 00000000..f3f0e27b --- /dev/null +++ b/squidasm/qoala/sim/globals.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Dict, List, Optional + +import numpy as np +from netsquid.qubits import qubitapi +from netsquid.qubits.qubit import Qubit + +if TYPE_CHECKING: + from squidasm.qoala.sim.stack import StackNetwork + +T_QubitData = Dict[str, Dict[int, Qubit]] +T_StateData = Dict[str, Dict[int, np.ndarray]] + + +class GlobalSimData: + _NETWORK: Optional[StackNetwork] = None + _BREAKPOINT_STATES: List[np.ndarray] = [] + + @classmethod + def set_network(cls, network: StackNetwork) -> None: + cls._NETWORK = network + + @classmethod + def get_network(cls) -> Optional[StackNetwork]: + return cls._NETWORK + + @classmethod + def get_quantum_state(cls, save: bool = False) -> T_QubitData: + network = cls.get_network() + assert network is not None + + qubits: T_QubitData = {} + states: T_StateData = {} + for name, qdevice in network.qdevices.items(): + qubits[name] = {} + states[name] = {} + for i in range(qdevice.num_positions): + if qdevice.mem_positions[i].in_use: + [q] = qdevice.peek(i, skip_noise=True) + qubits[name][i] = q + if save: + states[name][i] = qubitapi.reduced_dm(q) + if save: + cls._BREAKPOINT_STATES.append(states) + return qubits + + @classmethod + def get_last_breakpoint_state(cls) -> np.ndarray: + assert len(cls._BREAKPOINT_STATES) > 0 + return cls._BREAKPOINT_STATES[-1] diff --git a/squidasm/qoala/sim/handler.py b/squidasm/qoala/sim/handler.py new file mode 100644 index 00000000..7f04f0a3 --- /dev/null +++ b/squidasm/qoala/sim/handler.py @@ -0,0 +1,288 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional + +from netqasm.backend.messages import ( + InitNewAppMessage, + Message, + OpenEPRSocketMessage, + StopAppMessage, + SubroutineMessage, + deserialize_host_msg, +) +from netqasm.lang.instr import flavour +from netqasm.lang.parsing import deserialize as deser_subroutine +from netqasm.lang.subroutine import Subroutine +from netsquid.components.component import Component, Port +from netsquid.nodes import Node + +from pydynaa import EventExpression +from squidasm.qoala.sim.common import ( + AppMemory, + ComponentProtocol, + PhysicalQuantumMemory, + PortListener, +) +from squidasm.qoala.sim.netstack import Netstack, NetstackComponent +from squidasm.qoala.sim.signals import SIGNAL_HOST_HAND_MSG, SIGNAL_PROC_HAND_MSG + +if TYPE_CHECKING: + from squidasm.qoala.sim.processor import ProcessorComponent + from squidasm.qoala.sim.qnos import Qnos, QnosComponent + + +class HandlerComponent(Component): + """NetSquid component representing a QNodeOS handler. + + Subcomponent of a QnosComponent. + + The "QnodeOS handler" represents the combination of the following components + within QNodeOS: + - interface with the Host + - scheduler + + Has communications ports with + - the processor component of this QNodeOS + - the Host component on this node + + This is a static container for handler-related components and ports. + Behavior of a QNodeOS handler is modeled in the `Handler` class, + which is a subclass of `Protocol`. + """ + + def __init__(self, node: Node) -> None: + super().__init__(f"{node.name}_handler") + self._node = node + self.add_ports(["proc_out", "proc_in"]) + self.add_ports(["host_out", "host_in"]) + + @property + def processor_in_port(self) -> Port: + return self.ports["proc_in"] + + @property + def processor_out_port(self) -> Port: + return self.ports["proc_out"] + + @property + def host_in_port(self) -> Port: + return self.ports["host_in"] + + @property + def host_out_port(self) -> Port: + return self.ports["host_out"] + + @property + def node(self) -> Node: + return self._node + + @property + def processor_comp(self) -> ProcessorComponent: + return self.supercomponent.processor + + @property + def netstack_comp(self) -> NetstackComponent: + return self.supercomponent.netstack + + @property + def qnos_comp(self) -> QnosComponent: + return self.supercomponent + + +class RunningApp: + def __init__(self, app_id: int) -> None: + self._id = app_id + self._pending_subroutines: List[Subroutine] = [] + + def add_subroutine(self, subroutine: Subroutine) -> None: + self._pending_subroutines.append(subroutine) + + def next_subroutine(self) -> Optional[Subroutine]: + if len(self._pending_subroutines) > 0: + return self._pending_subroutines.pop() + return None + + @property + def id(self) -> int: + return self._id + + +class Handler(ComponentProtocol): + """NetSquid protocol representing a QNodeOS handler.""" + + def __init__( + self, comp: HandlerComponent, qnos: Qnos, qdevice_type: Optional[str] = "nv" + ) -> None: + """Processor handler constructor. Typically created indirectly through + constructing a `Qnos` instance. + + :param comp: NetSquid component representing the handler + :param qnos: `Qnos` protocol that owns this protocol + """ + super().__init__(name=f"{comp.name}_protocol", comp=comp) + self._comp = comp + self._qnos = qnos + + self.add_listener( + "host", + PortListener(self._comp.ports["host_in"], SIGNAL_HOST_HAND_MSG), + ) + self.add_listener( + "processor", + PortListener(self._comp.ports["proc_in"], SIGNAL_PROC_HAND_MSG), + ) + + # Number of applications that were handled so far. Used as a unique ID for + # the next application. + self._app_counter = 0 + + # Currently active (running or waiting) applications. + self._applications: Dict[int, RunningApp] = {} + + # Whether the quantum memory for applications should be reset when the + # application finishes. + self._should_clear_memory: bool = True + + # Set the expected flavour such that Host messages are deserialized correctly. + if qdevice_type == "nv": + self._flavour: Optional[flavour.Flavour] = flavour.NVFlavour() + elif qdevice_type == "generic": + self._flavour: Optional[flavour.Flavour] = flavour.VanillaFlavour() + else: + raise ValueError + + @property + def app_memories(self) -> Dict[int, AppMemory]: + return self._qnos.app_memories + + @property + def physical_memory(self) -> PhysicalQuantumMemory: + return self._qnos.physical_memory + + @property + def should_clear_memory(self) -> bool: + return self._should_clear_memory + + @should_clear_memory.setter + def should_clear_memory(self, value: bool) -> None: + self._should_clear_memory = value + + @property + def flavour(self) -> Optional[flavour.Flavour]: + return self._flavour + + @flavour.setter + def flavour(self, flavour: Optional[flavour.Flavour]) -> None: + self._flavour = flavour + + def _send_host_msg(self, msg: Any) -> None: + self._comp.host_out_port.tx_output(msg) + + def _receive_host_msg(self) -> Generator[EventExpression, None, str]: + return (yield from self._receive_msg("host", SIGNAL_HOST_HAND_MSG)) + + def _send_processor_msg(self, msg: str) -> None: + self._comp.processor_out_port.tx_output(msg) + + def _receive_processor_msg(self) -> Generator[EventExpression, None, str]: + return (yield from self._receive_msg("processor", SIGNAL_PROC_HAND_MSG)) + + @property + def qnos(self) -> Qnos: + return self._qnos + + @property + def netstack(self) -> Netstack: + return self.qnos.netstack + + def _next_app(self) -> Optional[RunningApp]: + for app in self._applications.values(): + return app + return None + + def init_new_app(self, app_id: int) -> int: + self._app_counter += 1 + self.app_memories[app_id] = AppMemory(app_id, self.physical_memory.qubit_count) + self._applications[app_id] = RunningApp(app_id) + self._logger.debug(f"registered app with ID {app_id}") + return app_id + + def open_epr_socket(self, app_id: int, socket_id: int, remote_id: int) -> None: + self._logger.debug(f"Opening EPR socket ({socket_id}, {remote_id})") + self.netstack.open_epr_socket(app_id, socket_id, remote_id) + + def add_subroutine(self, app_id: int, subroutine: Subroutine) -> None: + self._applications[app_id].add_subroutine(subroutine) + + def _deserialize_subroutine(self, msg: SubroutineMessage) -> Subroutine: + # return deser_subroutine(msg.subroutine, flavour=flavour.NVFlavour()) + return deser_subroutine(msg.subroutine, flavour=self._flavour) + + def clear_application(self, app_id: int) -> None: + for virt_id, phys_id in self.app_memories[app_id].qubit_mapping.items(): + self.app_memories[app_id].unmap_virt_id(virt_id) + if phys_id is not None: + self.physical_memory.free(phys_id) + self.app_memories.pop(app_id) + + def stop_application(self, app_id: int) -> None: + self._logger.debug(f"stopping application with ID {app_id}") + if self.should_clear_memory: + self._logger.debug(f"clearing qubits for application with ID {app_id}") + self.clear_application(app_id) + self._applications.pop(app_id) + else: + self._logger.info(f"NOT clearing qubits for application with ID {app_id}") + + def assign_processor( + self, app_id: int, subroutine: Subroutine + ) -> Generator[EventExpression, None, AppMemory]: + """Tell the processor to execute a subroutine and wait for it to finish. + + :param app_id: ID of the application this subroutine is for + :param subroutine: the subroutine to execute + """ + self._send_processor_msg(subroutine) + result = yield from self._receive_processor_msg() + assert result == "subroutine done" + self._logger.debug(f"result: {result}") + app_mem = self.app_memories[app_id] + return app_mem + + def msg_from_host(self, msg: Message) -> None: + """Handle a deserialized message from the Host.""" + if isinstance(msg, InitNewAppMessage): + app_id = self.init_new_app(msg.max_qubits) + self._send_host_msg(app_id) + elif isinstance(msg, OpenEPRSocketMessage): + self.open_epr_socket(msg.app_id, msg.epr_socket_id, msg.remote_node_id) + elif isinstance(msg, SubroutineMessage): + subroutine = self._deserialize_subroutine(msg) + self.add_subroutine(subroutine.app_id, subroutine) + elif isinstance(msg, StopAppMessage): + self.stop_application(msg.app_id) + + def run(self) -> Generator[EventExpression, None, None]: + """Run this protocol. Automatically called by NetSquid during simulation.""" + + # Loop forever acting on messages from the Host. + while True: + # Wait for a new message from the Host. + raw_host_msg = yield from self._receive_host_msg() + self._logger.debug(f"received new msg from host: {raw_host_msg}") + msg = deserialize_host_msg(raw_host_msg) + + # Handle the message. This updates the handler's state and may e.g. + # add a pending subroutine for an application. + self.msg_from_host(msg) + + # Get the next application that needs work. + app = self._next_app() + if app is not None: + # Flush all pending subroutines for this app. + while True: + subrt = app.next_subroutine() + if subrt is None: + break + app_mem = yield from self.assign_processor(app.id, subrt) + self._send_host_msg(app_mem) diff --git a/squidasm/qoala/sim/host.py b/squidasm/qoala/sim/host.py new file mode 100644 index 00000000..63e01d67 --- /dev/null +++ b/squidasm/qoala/sim/host.py @@ -0,0 +1,321 @@ +from __future__ import annotations + +import logging +from typing import Any, Dict, Generator, List, Optional, Type + +from netqasm.backend.messages import StopAppMessage +from netqasm.lang.operand import Register +from netqasm.lang.parsing.text import NetQASMSyntaxError, parse_register +from netqasm.sdk.transpile import NVSubroutineTranspiler, SubroutineTranspiler +from netsquid.components.component import Component, Port +from netsquid.nodes import Node + +from pydynaa import EventExpression +from squidasm.qoala.lang import lhr +from squidasm.qoala.runtime.environment import LocalEnvironment +from squidasm.qoala.runtime.program import ProgramContext, ProgramInstance, SdkProgram +from squidasm.qoala.sim.common import ComponentProtocol, LogManager, PortListener +from squidasm.qoala.sim.connection import QnosConnection +from squidasm.qoala.sim.csocket import ClassicalSocket +from squidasm.qoala.sim.signals import SIGNAL_HAND_HOST_MSG, SIGNAL_HOST_HOST_MSG + + +class HostProgramContext(ProgramContext): + def __init__( + self, + conn: QnosConnection, + csockets: Dict[str, ClassicalSocket], + app_id: int, + ): + self._conn = conn + self._csockets = csockets + self._app_id = app_id + + @property + def conn(self) -> QnosConnection: + return self._conn + + @property + def csockets(self) -> Dict[str, ClassicalSocket]: + return self._csockets + + @property + def app_id(self) -> int: + return self._app_id + + +class LhrProcess: + def __init__( + self, host: Host, program: ProgramInstance, context: HostProgramContext + ) -> None: + self._host = host + self._name = f"{host._comp.name}_Lhr" + self._logger: logging.Logger = LogManager.get_stack_logger( + f"{self.__class__.__name__}({self._name})" + ) + self._program = program + self._program_results: List[Dict[str, Any]] = [] + + self._context = context + + self._memory: Dict[str, Any] = {} + + def run(self, num_times: int = 1) -> Generator[EventExpression, None, None]: + for _ in range(num_times): + result = yield from self.execute_program() + self._program_results.append(result) + self._host.send_qnos_msg(bytes(StopAppMessage(self._context.app_id))) + return self._program_results + + @property + def context(self) -> HostProgramContext: + return self._context + + @property + def memory(self) -> Dict[str, Any]: + return self._memory + + @property + def program(self) -> ProgramInstance: + return self._program + + def execute_program(self) -> Generator[EventExpression, None, Dict[str, Any]]: + context = self._context + memory = self._memory + program = self._program.program + + csockets = list(self._context.csockets.values()) + csck = csockets[0] if len(csockets) > 0 else None + conn = context.conn + + for name, value in self._program.inputs.items(): + memory[name] = value + + results: Dict[str, Any] = {} + + for instr in program.instructions: + self._logger.info(f"Interpreting LHR instruction {instr}") + if isinstance(instr, lhr.SendCMsgOp): + value = memory[instr.arguments[0]] + self._logger.info(f"sending msg {value}") + csck.send(value) + elif isinstance(instr, lhr.ReceiveCMsgOp): + msg = yield from csck.recv() + msg = int(msg) + memory[instr.results[0]] = msg + self._logger.info(f"received msg {msg}") + elif isinstance(instr, lhr.AddCValueOp): + arg0 = int(memory[instr.arguments[0]]) + arg1 = int(memory[instr.arguments[1]]) + memory[instr.results[0]] = arg0 + arg1 + elif isinstance(instr, lhr.MultiplyConstantCValueOp): + arg0 = memory[instr.arguments[0]] + arg1 = int(instr.arguments[1]) + memory[instr.results[0]] = arg0 * arg1 + elif isinstance(instr, lhr.BitConditionalMultiplyConstantCValueOp): + arg0 = memory[instr.arguments[0]] + arg1 = int(instr.arguments[1]) + cond = memory[instr.arguments[2]] + if cond == 1: + memory[instr.results[0]] = arg0 * arg1 + else: + memory[instr.results[0]] = arg0 + elif isinstance(instr, lhr.AssignCValueOp): + value = instr.attributes[0] + # if isinstance(value, str) and value.startswith("RegFuture__"): + # reg_str = value[len("RegFuture__") :] + memory[instr.results[0]] = instr.attributes[0] + elif isinstance(instr, lhr.RunSubroutineOp): + arg_vec: lhr.LhrVector = instr.arguments[0] + args = arg_vec.values + lhr_subrt: lhr.LhrSubroutine = instr.attributes[0] + subrt = lhr_subrt.subroutine + self._logger.info(f"executing subroutine {subrt}") + + arg_values = {arg: memory[arg] for arg in args} + + self._logger.warning( + f"instantiating subroutine with values {arg_values}" + ) + subrt.instantiate(context.app_id, arg_values) + + yield from conn.commit_subroutine(subrt) + + for key, mem_loc in lhr_subrt.return_map.items(): + try: + reg: Register = parse_register(mem_loc.loc) + value = conn.shared_memory.get_register(reg) + self._logger.debug( + f"writing shared memory value {value} from location " + f"{mem_loc} to variable {key}" + ) + memory[key] = value + except NetQASMSyntaxError: + pass + elif isinstance(instr, lhr.ReturnResultOp): + value = instr.arguments[0] + results[value] = int(memory[value]) + + return results + + +class HostComponent(Component): + """NetSquid compmonent representing a Host. + + Subcomponent of a ProcessingNode. + + This is a static container for Host-related components and ports. Behavior + of a Host is modeled in the `Host` class, which is a subclass of `Protocol`. + """ + + def __init__(self, node: Node) -> None: + super().__init__(f"{node.name}_host") + self.add_ports(["qnos_in", "qnos_out"]) + self.add_ports(["peer_in", "peer_out"]) + + @property + def qnos_in_port(self) -> Port: + return self.ports["qnos_in"] + + @property + def qnos_out_port(self) -> Port: + return self.ports["qnos_out"] + + @property + def peer_in_port(self) -> Port: + return self.ports["peer_in"] + + @property + def peer_out_port(self) -> Port: + return self.ports["peer_out"] + + +class Host(ComponentProtocol): + """NetSquid protocol representing a Host.""" + + def __init__( + self, + comp: HostComponent, + local_env: LocalEnvironment, + qdevice_type: Optional[str] = "nv", + ) -> None: + """Qnos protocol constructor. + + :param comp: NetSquid component representing the Host + :param qdevice_type: hardware type of the QDevice of this node + """ + super().__init__(name=f"{comp.name}_protocol", comp=comp) + self._comp = comp + + self._local_env = local_env + + self.add_listener( + "qnos", + PortListener(self._comp.ports["qnos_in"], SIGNAL_HAND_HOST_MSG), + ) + self.add_listener( + "peer", + PortListener(self._comp.ports["peer_in"], SIGNAL_HOST_HOST_MSG), + ) + + if qdevice_type == "nv": + self._compiler: Optional[ + Type[SubroutineTranspiler] + ] = NVSubroutineTranspiler + elif qdevice_type == "generic": + self._compiler: Optional[Type[SubroutineTranspiler]] = None + else: + raise ValueError + + # Programs that need to be executed. + self._programs: Dict[int, ProgramInstance] = {} + self._program_counter: int = 0 + + self._connections: Dict[int, QnosConnection] = {} + + self._csockets: Dict[int, Dict[str, ClassicalSocket]] = {} + + # Number of times the current program still needs to be run. + self._num_pending: int = 0 + + # Results of program runs so far. + self._program_results: List[Dict[str, Any]] = [] + + @property + def compiler(self) -> Optional[Type[SubroutineTranspiler]]: + return self._compiler + + @compiler.setter + def compiler(self, typ: Optional[Type[SubroutineTranspiler]]) -> None: + self._compiler = typ + + @property + def local_env(self) -> LocalEnvironment: + return self._local_env + + def send_qnos_msg(self, msg: bytes) -> None: + self._comp.qnos_out_port.tx_output(msg) + + def receive_qnos_msg(self) -> Generator[EventExpression, None, str]: + return (yield from self._receive_msg("qnos", SIGNAL_HAND_HOST_MSG)) + + def send_peer_msg(self, msg: str) -> None: + self._comp.peer_out_port.tx_output(msg) + + def receive_peer_msg(self) -> Generator[EventExpression, None, str]: + return (yield from self._receive_msg("peer", SIGNAL_HOST_HOST_MSG)) + + def run_lhr_program( + self, program: ProgramInstance, context: HostProgramContext + ) -> Generator[EventExpression, None, None]: + self._logger.warning(f"Creating LHR process for program:\n{program}") + process = LhrProcess(self, program, context) + result = yield from process.run() + return result + + def run(self) -> Generator[EventExpression, None, None]: + """Run this protocol. Automatically called by NetSquid during simulation.""" + + # Run a single program as many times as requested. + programs = list(self._programs.items()) + if len(programs) == 0: + return + + app_id, prog_instance = programs[0] + + context = HostProgramContext( + conn=self._connections[app_id], + csockets=self._csockets[app_id], + app_id=app_id, + ) + + if isinstance(prog_instance.program, SdkProgram): + prog_instance.program = prog_instance.program.compile(context) + assert isinstance(prog_instance.program, lhr.LhrProgram) + + yield from self.run_lhr_program(prog_instance, context) + + def init_new_program(self, program: ProgramInstance) -> int: + app_id = self._program_counter + self._program_counter += 1 + self._programs[app_id] = program + + conn = QnosConnection( + host=self, + app_id=app_id, + app_name=program.program.meta.name, + max_qubits=program.program.meta.max_qubits, + compiler=self._compiler, + ) + self._connections[app_id] = conn + + self._csockets[app_id] = {} + + return app_id + + def open_csocket(self, app_id: int, remote_name: str) -> None: + assert app_id in self._csockets + self._csockets[app_id][remote_name] = ClassicalSocket(self, remote_name) + + def get_results(self) -> List[Dict[str, Any]]: + return self._program_results diff --git a/squidasm/qoala/sim/netstack.py b/squidasm/qoala/sim/netstack.py new file mode 100644 index 00000000..0a7bfc5b --- /dev/null +++ b/squidasm/qoala/sim/netstack.py @@ -0,0 +1,779 @@ +from __future__ import annotations + +import math +from dataclasses import dataclass +from typing import TYPE_CHECKING, Dict, Generator, List, Optional + +import netsquid as ns +from netqasm.sdk.build_epr import ( + SER_CREATE_IDX_NUMBER, + SER_CREATE_IDX_TYPE, + SER_RESPONSE_KEEP_IDX_BELL_STATE, + SER_RESPONSE_KEEP_IDX_GOODNESS, + SER_RESPONSE_KEEP_LEN, + SER_RESPONSE_MEASURE_IDX_MEASUREMENT_BASIS, + SER_RESPONSE_MEASURE_IDX_MEASUREMENT_OUTCOME, + SER_RESPONSE_MEASURE_LEN, +) +from netsquid.components import QuantumProcessor +from netsquid.components.component import Component, Port +from netsquid.components.instructions import INSTR_ROT_X, INSTR_ROT_Z +from netsquid.components.qprogram import QuantumProgram +from netsquid.nodes import Node +from netsquid.qubits.ketstates import BellIndex +from netsquid_magic.link_layer import MagicLinkLayerProtocolWithSignaling +from qlink_interface import ( + ReqCreateAndKeep, + ReqCreateBase, + ReqMeasureDirectly, + ReqReceive, + ResCreateAndKeep, + ResMeasureDirectly, +) +from qlink_interface.interface import ReqRemoteStatePrep + +from pydynaa import EventExpression +from squidasm.qoala.sim.common import ( + AllocError, + AppMemory, + ComponentProtocol, + NetstackBreakpointCreateRequest, + NetstackBreakpointReceiveRequest, + NetstackCreateRequest, + NetstackReceiveRequest, + PhysicalQuantumMemory, + PortListener, +) +from squidasm.qoala.sim.egp import EgpProtocol +from squidasm.qoala.sim.signals import ( + SIGNAL_MEMORY_FREED, + SIGNAL_PEER_NSTK_MSG, + SIGNAL_PROC_NSTK_MSG, +) + +if TYPE_CHECKING: + from squidasm.qoala.sim.qnos import Qnos + +PI = math.pi +PI_OVER_2 = math.pi / 2 + + +class NetstackComponent(Component): + """NetSquid component representing the network stack in QNodeOS. + + Subcomponent of a QnosComponent. + + Has communications ports with + - the processor component of this QNodeOS + - the netstack compmonent of the remote node + NOTE: at this moment only a single other node is supported in the network + + This is a static container for network-stack-related components and ports. + Behavior of a QNodeOS network stack is modeled in the `NetStack` class, + which is a subclass of `Protocol`. + """ + + def __init__(self, node: Node) -> None: + super().__init__(f"{node.name}_netstack") + self._node = node + self.add_ports(["proc_out", "proc_in"]) + self.add_ports(["peer_out", "peer_in"]) + + @property + def processor_in_port(self) -> Port: + return self.ports["proc_in"] + + @property + def processor_out_port(self) -> Port: + return self.ports["proc_out"] + + @property + def peer_in_port(self) -> Port: + return self.ports["peer_in"] + + @property + def peer_out_port(self) -> Port: + return self.ports["peer_out"] + + @property + def node(self) -> Node: + return self._node + + +@dataclass +class EprSocket: + """EPR Socket. Allows for EPR pair generation with a single remote node. + + Multiple EPR Sockets may be created for a single pair of nodes. These + sockets have a different ID, and may e.g be used for EPR generation requests + with different parameters.""" + + socket_id: int + remote_id: int + + +class Netstack(ComponentProtocol): + """NetSquid protocol representing the QNodeOS network stack.""" + + def __init__(self, comp: NetstackComponent, qnos: Qnos) -> None: + """Network stack protocol constructor. Typically created indirectly through + constructing a `Qnos` instance. + + :param comp: NetSquid component representing the network stack + :param qnos: `Qnos` protocol that owns this protocol + """ + super().__init__(name=f"{comp.name}_protocol", comp=comp) + self._comp = comp + self._qnos = qnos + + self.add_listener( + "processor", + PortListener(self._comp.processor_in_port, SIGNAL_PROC_NSTK_MSG), + ) + self.add_listener( + "peer", + PortListener(self._comp.peer_in_port, SIGNAL_PEER_NSTK_MSG), + ) + + self._egp: Optional[EgpProtocol] = None + self._epr_sockets: Dict[int, List[EprSocket]] = {} # app ID -> [socket] + + def assign_ll_protocol(self, prot: MagicLinkLayerProtocolWithSignaling) -> None: + """Set the magic link layer protocol that this network stack uses to produce + entangled pairs with the remote node. + + :param prot: link layer protocol instance + """ + self._egp = EgpProtocol(self._comp.node, prot) + + def open_epr_socket(self, app_id: int, socket_id: int, remote_node_id: int) -> None: + """Create a new EPR socket with the specified remote node. + + :param app_id: ID of the application that creates this EPR socket + :param socket_id: ID of the socket + :param remote_node_id: ID of the remote node + """ + if app_id not in self._epr_sockets: + self._epr_sockets[app_id] = [] + self._epr_sockets[app_id].append(EprSocket(socket_id, remote_node_id)) + + def _send_processor_msg(self, msg: str) -> None: + """Send a message to the processor.""" + self._comp.processor_out_port.tx_output(msg) + + def _receive_processor_msg(self) -> Generator[EventExpression, None, str]: + """Receive a message from the processor. Block until there is at least one + message.""" + return (yield from self._receive_msg("processor", SIGNAL_PROC_NSTK_MSG)) + + def _send_peer_msg(self, msg: str) -> None: + """Send a message to the network stack of the other node. + + NOTE: for now we assume there is only one other node, which is 'the' peer.""" + self._comp.peer_out_port.tx_output(msg) + + def _receive_peer_msg(self) -> Generator[EventExpression, None, str]: + """Receive a message from the network stack of the other node. Block until + there is at least one message. + + NOTE: for now we assume there is only one other node, which is 'the' peer.""" + return (yield from self._receive_msg("peer", SIGNAL_PEER_NSTK_MSG)) + + def start(self) -> None: + """Start this protocol. The NetSquid simulator will call and yield on the + `run` method. Also start the underlying EGP protocol.""" + super().start() + if self._egp: + self._egp.start() + + def stop(self) -> None: + """Stop this protocol. The NetSquid simulator will stop calling `run`. + Also stop the underlying EGP protocol.""" + if self._egp: + self._egp.stop() + super().stop() + + def _read_request_args_array(self, app_id: int, array_addr: int) -> List[int]: + app_mem = self.app_memories[app_id] + app_mem.get_array(array_addr) + return app_mem.get_array(array_addr) + + def _construct_request(self, remote_id: int, args: List[int]) -> ReqCreateBase: + """Construct a link layer request from application request info. + + :param remote_id: ID of remote node + :param args: NetQASM array elements from the arguments array specified by the + application + :return: link layer request object + """ + typ = args[SER_CREATE_IDX_TYPE] + assert typ is not None + num_pairs = args[SER_CREATE_IDX_NUMBER] + assert num_pairs is not None + + # TODO + MINIMUM_FIDELITY = 0.99 + + if typ == 0: + request = ReqCreateAndKeep( + remote_node_id=remote_id, + number=num_pairs, + minimum_fidelity=MINIMUM_FIDELITY, + ) + elif typ == 1: + request = ReqMeasureDirectly( + remote_node_id=remote_id, + number=num_pairs, + minimum_fidelity=MINIMUM_FIDELITY, + ) + elif typ == 2: + request = ReqRemoteStatePrep( + remote_node_id=remote_id, + number=num_pairs, + minimum_fidelity=MINIMUM_FIDELITY, + ) + else: + raise ValueError(f"Unsupported create type {typ}") + return request + + @property + def app_memories(self) -> Dict[int, AppMemory]: + return self._qnos.app_memories + + @property + def physical_memory(self) -> PhysicalQuantumMemory: + return self._qnos.physical_memory + + @property + def qdevice(self) -> QuantumProcessor: + return self._comp.node.qdevice + + def find_epr_socket( + self, app_id: int, sck_id: int, rem_id: int + ) -> Optional[EprSocket]: + """Get a specific EPR socket or None if it does not exist. + + :param app_id: app ID + :param sck_id: EPR socket ID + :param rem_id: remote node ID + :return: the corresponding EPR socket or None if it does not exist + """ + if app_id not in self._epr_sockets: + return None + for sck in self._epr_sockets[app_id]: + if sck.socket_id == sck_id and sck.remote_id == rem_id: + return sck + return None + + def handle_create_ck_request( + self, req: NetstackCreateRequest, request: ReqCreateAndKeep + ) -> Generator[EventExpression, None, None]: + """Handle a Create and Keep request as the initiator/creator, until all + pairs have been created. + + This method uses the EGP protocol to create and measure EPR pairs with + the remote node. It will fully complete the request before returning. If + the pair created by the EGP protocol is another Bell state than Phi+, + local gates are applied to do a correction, such that the final + delivered pair is always Phi+. + + The method can however yield (i.e. give control back to the simulator + scheduler) in the following cases: - no communication qubit is + available; this method will resume when a + SIGNAL_MEMORY_FREED is given (currently only the processor can do + this) + - when waiting for the EGP protocol to produce the next pair; this + method resumes when the pair is delivered + - a Bell correction gate is applied + + This method does not return anything. This method has the side effect + that NetQASM array value are written to. + + :param req: application request info (app ID and NetQASM array IDs) + :param request: link layer request object + """ + num_pairs = request.number + + app_mem = self.app_memories[req.app_id] + qubit_ids = app_mem.get_array(req.qubit_array_addr) + + self._logger.info(f"putting CK request to EGP for {num_pairs} pairs") + self._logger.info(f"qubit IDs specified by application: {qubit_ids}") + self._logger.info(f"splitting request into {num_pairs} 1-pair requests") + request.number = 1 + + start_time = ns.sim_time() + + for pair_index in range(num_pairs): + self._logger.info(f"trying to allocate comm qubit for pair {pair_index}") + while True: + try: + phys_id = self.physical_memory.allocate_comm() + break + except AllocError: + self._logger.info("no comm qubit available, waiting...") + + # Wait for a signal indicating the communication qubit might be free + # again. + yield self.await_signal( + sender=self._qnos.processor, signal_label=SIGNAL_MEMORY_FREED + ) + self._logger.info( + "a 'free' happened, trying again to allocate comm qubit..." + ) + + # Put the request to the EGP. + self._logger.info(f"putting CK request for pair {pair_index}") + self._egp.put(request) + + # Wait for a signal from the EGP. + self._logger.info(f"waiting for result for pair {pair_index}") + yield self.await_signal( + sender=self._egp, signal_label=ResCreateAndKeep.__name__ + ) + # Get the EGP's result. + result: ResCreateAndKeep = self._egp.get_signal_result( + ResCreateAndKeep.__name__, receiver=self + ) + self._logger.info(f"got result for pair {pair_index}: {result}") + + # Bell state corrections. Resulting state is always Phi+ (i.e. B00). + if result.bell_state == BellIndex.B00: + pass + elif result.bell_state == BellIndex.B01: + prog = QuantumProgram() + prog.apply(INSTR_ROT_X, qubit_indices=[0], angle=PI) + yield self.qdevice.execute_program(prog) + elif result.bell_state == BellIndex.B10: + prog = QuantumProgram() + prog.apply(INSTR_ROT_Z, qubit_indices=[0], angle=PI) + yield self.qdevice.execute_program(prog) + elif result.bell_state == BellIndex.B11: + prog = QuantumProgram() + prog.apply(INSTR_ROT_X, qubit_indices=[0], angle=PI) + prog.apply(INSTR_ROT_Z, qubit_indices=[0], angle=PI) + yield self.qdevice.execute_program(prog) + + virt_id = app_mem.get_array_value(req.qubit_array_addr, pair_index) + app_mem.map_virt_id(virt_id, phys_id) + self._logger.info( + f"mapping virtual qubit {virt_id} to physical qubit {phys_id}" + ) + + gen_duration_ns_float = ns.sim_time() - start_time + gen_duration_us_int = int(gen_duration_ns_float / 1000) + self._logger.info(f"gen duration (us): {gen_duration_us_int}") + + # Length of response array slice for a single pair. + slice_len = SER_RESPONSE_KEEP_LEN + + # Populate results array. + for i in range(slice_len): + # Write -1 to unused array elements. + value = -1 + + # Write corresponding result value to the other array elements. + if i == SER_RESPONSE_KEEP_IDX_GOODNESS: + value = gen_duration_us_int + if i == SER_RESPONSE_KEEP_IDX_BELL_STATE: + value = result.bell_state + + # Calculate array element location. + arr_index = slice_len * pair_index + i + + app_mem.set_array_value(req.result_array_addr, arr_index, value) + self._logger.debug( + f"wrote to @{req.result_array_addr}[{slice_len * pair_index}:" + f"{slice_len * pair_index + slice_len}] for app ID {req.app_id}" + ) + self._send_processor_msg("wrote to array") + + def handle_create_md_request( + self, req: NetstackCreateRequest, request: ReqMeasureDirectly + ) -> Generator[EventExpression, None, None]: + """Handle a Create and Measure request as the initiator/creator, until all + pairs have been created and measured. + + This method uses the EGP protocol to create EPR pairs with the remote node. + It will fully complete the request before returning. + + No Bell state corrections are done. This means that application code should + use the result information to check, for each pair, the generated Bell state + and possibly post-process the measurement outcomes. + + The method can yield (i.e. give control back to the simulator scheduler) in + the following cases: + - no communication qubit is available; this method will resume when a + SIGNAL_MEMORY_FREED is given (currently only the processor can do this) + - when waiting for the EGP protocol to produce the next pair; this method + resumes when the pair is delivered + + This method does not return anything. + This method has the side effect that NetQASM array value are written to. + + :param req: application request info (app ID and NetQASM array IDs) + :param request: link layer request object + """ + + # Put the reqeust to the EGP. + self._egp.put(request) + + results: List[ResMeasureDirectly] = [] + + # Wait for all pairs to be created. For each pair, the EGP sends a separate + # signal that is awaited here. Only after the last pair, we write the results + # to the array. This is done since the whole request (i.e. all pairs) is + # expected to finish in a short time anyway. However, writing results for a + # pair as soon as they are done may be implemented in the future. + for _ in range(request.number): + phys_id = self.physical_memory.allocate_comm() + + yield self.await_signal( + sender=self._egp, signal_label=ResMeasureDirectly.__name__ + ) + result: ResMeasureDirectly = self._egp.get_signal_result( + ResMeasureDirectly.__name__, receiver=self + ) + self._logger.debug(f"bell index: {result.bell_state}") + results.append(result) + self.physical_memory.free(phys_id) + + app_mem = self.app_memories[req.app_id] + + # Length of response array slice for a single pair. + slice_len = SER_RESPONSE_MEASURE_LEN + + # Populate results array. + for pair_index in range(request.number): + result = results[pair_index] + + for i in range(slice_len): + # Write -1 to unused array elements. + value = -1 + + # Write corresponding result value to the other array elements. + if i == SER_RESPONSE_MEASURE_IDX_MEASUREMENT_OUTCOME: + value = result.measurement_outcome + elif i == SER_RESPONSE_MEASURE_IDX_MEASUREMENT_BASIS: + value = result.measurement_basis.value + elif i == SER_RESPONSE_KEEP_IDX_BELL_STATE: + value = result.bell_state.value + + # Calculate array element location. + arr_index = slice_len * pair_index + i + + app_mem.set_array_value(req.result_array_addr, arr_index, value) + + self._send_processor_msg("wrote to array") + + def handle_create_request( + self, req: NetstackCreateRequest + ) -> Generator[EventExpression, None, None]: + """Issue a request to create entanglement with a remote node. + + :param req: request info + """ + + # EPR socket should exist. + assert ( + self.find_epr_socket(req.app_id, req.epr_socket_id, req.remote_node_id) + is not None + ) + + # Read request parameters from the corresponding NetQASM array. + args = self._read_request_args_array(req.app_id, req.arg_array_addr) + + # Create the link layer request object. + request = self._construct_request(req.remote_node_id, args) + + # Send it to the receiver node and wait for an acknowledgement. + self._send_peer_msg(request) + peer_msg = yield from self._receive_peer_msg() + self._logger.debug(f"received peer msg: {peer_msg}") + + # Handle the request. + if isinstance(request, ReqCreateAndKeep): + yield from self.handle_create_ck_request(req, request) + elif isinstance(request, ReqMeasureDirectly): + yield from self.handle_create_md_request(req, request) + + def handle_receive_ck_request( + self, req: NetstackReceiveRequest, request: ReqCreateAndKeep + ) -> Generator[EventExpression, None, None]: + """Handle a Create and Keep request as the receiver, until all pairs have + been created. + + This method uses the EGP protocol to create EPR pairs with the remote + node. It will fully complete the request before returning. + + If the pair created by the EGP protocol is another Bell state than Phi+, + it is assumed that the *other* node applies local gates such that the + final delivered pair is always Phi+. + + The method can yield (i.e. give control back to the simulator scheduler) + in the following cases: - no communication qubit is available; this + method will resume when a + SIGNAL_MEMORY_FREED is given (currently only the processor can do + this) + - when waiting for the EGP protocol to produce the next pair; this + method resumes when the pair is delivered + + This method does not return anything. This method has the side effect + that NetQASM array value are written to. + + :param req: application request info (app ID and NetQASM array IDs) + :param request: link layer request object + """ + assert isinstance(request, ReqCreateAndKeep) + + num_pairs = request.number + + self._logger.info(f"putting CK request to EGP for {num_pairs} pairs") + self._logger.info(f"splitting request into {num_pairs} 1-pair requests") + + start_time = ns.sim_time() + + for pair_index in range(num_pairs): + self._logger.info(f"trying to allocate comm qubit for pair {pair_index}") + while True: + try: + phys_id = self.physical_memory.allocate_comm() + break + except AllocError: + self._logger.info("no comm qubit available, waiting...") + + # Wait for a signal indicating the communication qubit might be free + # again. + yield self.await_signal( + sender=self._qnos.processor, signal_label=SIGNAL_MEMORY_FREED + ) + self._logger.info( + "a 'free' happened, trying again to allocate comm qubit..." + ) + + # Put the request to the EGP. + self._logger.info(f"putting CK request for pair {pair_index}") + self._egp.put(ReqReceive(remote_node_id=req.remote_node_id)) + self._logger.info(f"waiting for result for pair {pair_index}") + + # Wait for a signal from the EGP. + yield self.await_signal( + sender=self._egp, signal_label=ResCreateAndKeep.__name__ + ) + # Get the EGP's result. + result: ResCreateAndKeep = self._egp.get_signal_result( + ResCreateAndKeep.__name__, receiver=self + ) + self._logger.info(f"got result for pair {pair_index}: {result}") + + app_mem = self.app_memories[req.app_id] + virt_id = app_mem.get_array_value(req.qubit_array_addr, pair_index) + app_mem.map_virt_id(virt_id, phys_id) + self._logger.info( + f"mapping virtual qubit {virt_id} to physical qubit {phys_id}" + ) + + gen_duration_ns_float = ns.sim_time() - start_time + gen_duration_us_int = int(gen_duration_ns_float / 1000) + self._logger.info(f"gen duration (us): {gen_duration_us_int}") + + # Length of response array slice for a single pair. + slice_len = SER_RESPONSE_KEEP_LEN + + for i in range(slice_len): + # Write -1 to unused array elements. + value = -1 + + # Write corresponding result value to the other array elements. + if i == SER_RESPONSE_KEEP_IDX_GOODNESS: + value = gen_duration_us_int + if i == SER_RESPONSE_KEEP_IDX_BELL_STATE: + value = result.bell_state.value + + # Calculate array element location. + arr_index = slice_len * pair_index + i + + app_mem.set_array_value(req.result_array_addr, arr_index, value) + self._logger.debug( + f"wrote to @{req.result_array_addr}[{slice_len * pair_index}:" + f"{slice_len * pair_index + slice_len}] for app ID {req.app_id}" + ) + self._send_processor_msg("wrote to array") + + def handle_receive_md_request( + self, req: NetstackReceiveRequest, request: ReqMeasureDirectly + ) -> Generator[EventExpression, None, None]: + """Handle a Create and Measure request as the receiver, until all + pairs have been created and measured. + + This method uses the EGP protocol to create EPR pairs with the remote node. + It will fully complete the request before returning. + + No Bell state corrections are done. This means that application code should + use the result information to check, for each pair, the generated Bell state + and possibly post-process the measurement outcomes. + + The method can yield (i.e. give control back to the simulator scheduler) + in the following cases: - no communication qubit is available; this + method will resume when a + SIGNAL_MEMORY_FREED is given (currently only the processor can do + this) + - when waiting for the EGP protocol to produce the next pair; this + method resumes when the pair is delivered + + This method does not return anything. This method has the side effect + that NetQASM array value are written to. + + :param req: application request info (app ID and NetQASM array IDs) + :param request: link layer request object + """ + assert isinstance(request, ReqMeasureDirectly) + + self._egp.put(ReqReceive(remote_node_id=req.remote_node_id)) + + results: List[ResMeasureDirectly] = [] + + for _ in range(request.number): + phys_id = self.physical_memory.allocate_comm() + + yield self.await_signal( + sender=self._egp, signal_label=ResMeasureDirectly.__name__ + ) + result: ResMeasureDirectly = self._egp.get_signal_result( + ResMeasureDirectly.__name__, receiver=self + ) + results.append(result) + + self.physical_memory.free(phys_id) + + app_mem = self.app_memories[req.app_id] + + # Length of response array slice for a single pair. + slice_len = SER_RESPONSE_MEASURE_LEN + + # Populate results array. + for pair_index in range(request.number): + result = results[pair_index] + + for i in range(slice_len): + # Write -1 to unused array elements. + value = -1 + + # Write corresponding result value to the other array elements. + if i == SER_RESPONSE_MEASURE_IDX_MEASUREMENT_OUTCOME: + value = result.measurement_outcome + elif i == SER_RESPONSE_MEASURE_IDX_MEASUREMENT_BASIS: + value = result.measurement_basis.value + elif i == SER_RESPONSE_KEEP_IDX_BELL_STATE: + value = result.bell_state.value + + # Calculate array element location. + arr_index = slice_len * pair_index + i + + app_mem.set_array_value(req.result_array_addr, arr_index, value) + + self._send_processor_msg("wrote to array") + + def handle_receive_request( + self, req: NetstackReceiveRequest + ) -> Generator[EventExpression, None, None]: + """Issue a request to receive entanglement from a remote node. + + :param req: request info + """ + + # EPR socket should exist. + assert ( + self.find_epr_socket(req.app_id, req.epr_socket_id, req.remote_node_id) + is not None + ) + + # Wait for the network stack in the remote node to get the corresponding + # 'create' request from its local application and send it to us. + # NOTE: we do not check if the request from the other node matches our own + # request. Also, we simply block until synchronizing with the other node, + # and then fully handle the request. There is no support for queueing + # and/or interleaving multiple different requests. + create_request = yield from self._receive_peer_msg() + self._logger.debug(f"received {create_request} from peer") + + # Acknowledge to the remote node that we received the request and we will + # start handling it. + self._logger.debug("sending 'ready' to peer") + self._send_peer_msg("ready") + + # Handle the request, based on the type that we now know because of the + # other node. + if isinstance(create_request, ReqCreateAndKeep): + yield from self.handle_receive_ck_request(req, create_request) + elif isinstance(create_request, ReqMeasureDirectly): + yield from self.handle_receive_md_request(req, create_request) + + def handle_breakpoint_create_request( + self, + ) -> Generator[EventExpression, None, None]: + # Synchronize with the remote node. + self._send_peer_msg("breakpoint start") + response = yield from self._receive_peer_msg() + assert response == "breakpoint start" + + # Remote node is now ready. Notify the processor. + self._send_processor_msg("breakpoint ready") + + # Wait for the processor to finish handling the breakpoint. + processor_msg = yield from self._receive_processor_msg() + assert processor_msg == "breakpoint end" + + # Tell the remote node that the breakpoint has finished. + self._send_peer_msg("breakpoint end") + + # Wait for the remote node to have finsihed as well. + response = yield from self._receive_peer_msg() + assert response == "breakpoint end" + + # Notify the processor that we are done. + self._send_processor_msg("breakpoint finished") + + def handle_breakpoint_receive_request( + self, + ) -> Generator[EventExpression, None, None]: + # Synchronize with the remote node. + msg = yield from self._receive_peer_msg() + assert msg == "breakpoint start" + self._send_peer_msg("breakpoint start") + + # Notify the processor we are ready to handle the breakpoint. + self._send_processor_msg("breakpoint ready") + + # Wait for the processor to finish handling the breakpoint. + processor_msg = yield from self._receive_processor_msg() + assert processor_msg == "breakpoint end" + + # Wait for the remote node to finish and tell it we are finished as well. + peer_msg = yield from self._receive_peer_msg() + assert peer_msg == "breakpoint end" + self._send_peer_msg("breakpoint end") + + # Notify the processor that we are done. + self._send_processor_msg("breakpoint finished") + + def run(self) -> Generator[EventExpression, None, None]: + # Loop forever acting on messages from the processor. + while True: + # Wait for a new message. + msg = yield from self._receive_processor_msg() + self._logger.debug(f"received new msg from processor: {msg}") + + # Handle it. + if isinstance(msg, NetstackCreateRequest): + yield from self.handle_create_request(msg) + self._logger.debug("create request done") + elif isinstance(msg, NetstackReceiveRequest): + yield from self.handle_receive_request(msg) + self._logger.debug("receive request done") + elif isinstance(msg, NetstackBreakpointCreateRequest): + yield from self.handle_breakpoint_create_request() + self._logger.debug("breakpoint create request done") + elif isinstance(msg, NetstackBreakpointReceiveRequest): + yield from self.handle_breakpoint_receive_request() + self._logger.debug("breakpoint receive request done") diff --git a/squidasm/qoala/sim/processor.py b/squidasm/qoala/sim/processor.py new file mode 100644 index 00000000..9a5ff7c5 --- /dev/null +++ b/squidasm/qoala/sim/processor.py @@ -0,0 +1,870 @@ +from __future__ import annotations + +import math +from typing import TYPE_CHECKING, Dict, Generator, Optional, Union + +import netsquid as ns +from netqasm.lang.instr import NetQASMInstruction, core, nv, vanilla +from netqasm.lang.operand import Register +from netqasm.lang.subroutine import Subroutine +from netsquid.components import QuantumProcessor +from netsquid.components.component import Component, Port +from netsquid.components.instructions import ( + INSTR_CNOT, + INSTR_CXDIR, + INSTR_CYDIR, + INSTR_CZ, + INSTR_H, + INSTR_INIT, + INSTR_MEASURE, + INSTR_ROT_X, + INSTR_ROT_Y, + INSTR_ROT_Z, + INSTR_X, + INSTR_Y, + INSTR_Z, +) +from netsquid.components.instructions import Instruction as NsInstr +from netsquid.components.qprogram import QuantumProgram +from netsquid.nodes import Node +from netsquid.qubits import qubitapi + +from pydynaa import EventExpression +from squidasm.qoala.sim.common import ( + AllocError, + AppMemory, + ComponentProtocol, + NetstackBreakpointCreateRequest, + NetstackBreakpointReceiveRequest, + NetstackCreateRequest, + NetstackReceiveRequest, + PhysicalQuantumMemory, + PortListener, +) +from squidasm.qoala.sim.globals import GlobalSimData +from squidasm.qoala.sim.signals import ( + SIGNAL_HAND_PROC_MSG, + SIGNAL_MEMORY_FREED, + SIGNAL_NSTK_PROC_MSG, +) + +if TYPE_CHECKING: + from squidasm.qoala.sim.qnos import Qnos + +PI = math.pi +PI_OVER_2 = math.pi / 2 + + +class ProcessorComponent(Component): + """NetSquid component representing a QNodeOS processor. + + Subcomponent of a QnosComponent. + + Has communications ports with + - the netstack component of this QNodeOS + - the handler compmonent of this QNodeOS + + This is a static container for processor-related components and ports. + Behavior of a QNodeOS processor is modeled in the `Processor` class, + which is a subclass of `Protocol`. + """ + + def __init__(self, node: Node) -> None: + super().__init__(f"{node.name}_processor") + self._node = node + self.add_ports(["nstk_out", "nstk_in"]) + self.add_ports(["hand_out", "hand_in"]) + + @property + def netstack_in_port(self) -> Port: + return self.ports["nstk_in"] + + @property + def netstack_out_port(self) -> Port: + return self.ports["nstk_out"] + + @property + def handler_in_port(self) -> Port: + return self.ports["hand_in"] + + @property + def handler_out_port(self) -> Port: + return self.ports["hand_out"] + + @property + def qdevice(self) -> QuantumProcessor: + return self.supercomponent.qdevice + + @property + def node(self) -> Node: + return self._node + + +class Processor(ComponentProtocol): + """NetSquid protocol representing a QNodeOS processor.""" + + def __init__(self, comp: ProcessorComponent, qnos: Qnos) -> None: + """Processor protocol constructor. Typically created indirectly through + constructing a `Qnos` instance. + + :param comp: NetSquid component representing the processor + :param qnos: `Qnos` protocol that owns this protocol + """ + super().__init__(name=f"{comp.name}_protocol", comp=comp) + self._comp = comp + self._qnos = qnos + + self.add_listener( + "handler", + PortListener(self._comp.ports["hand_in"], SIGNAL_HAND_PROC_MSG), + ) + self.add_listener( + "netstack", + PortListener(self._comp.ports["nstk_in"], SIGNAL_NSTK_PROC_MSG), + ) + + self.add_signal(SIGNAL_MEMORY_FREED) + + @property + def app_memories(self) -> Dict[int, AppMemory]: + """Get a dictionary of app IDs to application memories.""" + return self._qnos.app_memories + + @property + def physical_memory(self) -> PhysicalQuantumMemory: + """Get the physical quantum memory object.""" + return self._qnos.physical_memory + + @property + def qdevice(self) -> QuantumProcessor: + """Get the NetSquid `QuantumProcessor` object of this node.""" + return self._comp.qdevice + + def _send_handler_msg(self, msg: str) -> None: + self._comp.handler_out_port.tx_output(msg) + + def _receive_handler_msg(self) -> Generator[EventExpression, None, str]: + return (yield from self._receive_msg("handler", SIGNAL_HAND_PROC_MSG)) + + def _send_netstack_msg(self, msg: str) -> None: + self._comp.netstack_out_port.tx_output(msg) + + def _receive_netstack_msg(self) -> Generator[EventExpression, None, str]: + return (yield from self._receive_msg("netstack", SIGNAL_NSTK_PROC_MSG)) + + def _flush_netstack_msgs(self) -> None: + self._listeners["netstack"].buffer.clear() + + def run(self) -> Generator[EventExpression, None, None]: + """Run this protocol. Automatically called by NetSquid during simulation.""" + while True: + subroutine = yield from self._receive_handler_msg() + # assert isinstance(subroutine, Subroutine) + self._logger.debug(f"received new subroutine from handler: {subroutine}") + + yield from self.execute_subroutine(subroutine) + + self._send_handler_msg("subroutine done") + + def execute_subroutine( + self, subroutine: Subroutine + ) -> Generator[EventExpression, None, None]: + """Execute a NetQASM subroutine on this processor.""" + app_id = subroutine.app_id + assert app_id in self.app_memories + app_mem = self.app_memories[app_id] + app_mem.set_prog_counter(0) + while app_mem.prog_counter < len(subroutine.instructions): + instr = subroutine.instructions[app_mem.prog_counter] + self._logger.debug( + f"{ns.sim_time()} interpreting instruction {instr} at line {app_mem.prog_counter}" + ) + + if ( + isinstance(instr, core.JmpInstruction) + or isinstance(instr, core.BranchUnaryInstruction) + or isinstance(instr, core.BranchBinaryInstruction) + ): + self._interpret_branch_instr(app_id, instr) + else: + generator = self._interpret_instruction(app_id, instr) + if generator: + yield from generator + app_mem.increment_prog_counter() + + def _interpret_instruction( + self, app_id: int, instr: NetQASMInstruction + ) -> Optional[Generator[EventExpression, None, None]]: + if isinstance(instr, core.SetInstruction): + return self._interpret_set(app_id, instr) + elif isinstance(instr, core.QAllocInstruction): + return self._interpret_qalloc(app_id, instr) + elif isinstance(instr, core.QFreeInstruction): + return self._interpret_qfree(app_id, instr) + elif isinstance(instr, core.StoreInstruction): + return self._interpret_store(app_id, instr) + elif isinstance(instr, core.LoadInstruction): + return self._interpret_load(app_id, instr) + elif isinstance(instr, core.LeaInstruction): + return self._interpret_lea(app_id, instr) + elif isinstance(instr, core.UndefInstruction): + return self._interpret_undef(app_id, instr) + elif isinstance(instr, core.ArrayInstruction): + return self._interpret_array(app_id, instr) + elif isinstance(instr, core.InitInstruction): + return self._interpret_init(app_id, instr) + elif isinstance(instr, core.MeasInstruction): + return self._interpret_meas(app_id, instr) + elif isinstance(instr, core.CreateEPRInstruction): + return self._interpret_create_epr(app_id, instr) + elif isinstance(instr, core.RecvEPRInstruction): + return self._interpret_recv_epr(app_id, instr) + elif isinstance(instr, core.WaitAllInstruction): + return self._interpret_wait_all(app_id, instr) + elif isinstance(instr, core.RetRegInstruction): + pass + elif isinstance(instr, core.RetArrInstruction): + pass + elif isinstance(instr, core.SingleQubitInstruction): + return self._interpret_single_qubit_instr(app_id, instr) + elif isinstance(instr, core.TwoQubitInstruction): + return self._interpret_two_qubit_instr(app_id, instr) + elif isinstance(instr, core.RotationInstruction): + return self._interpret_single_rotation_instr(app_id, instr) + elif isinstance(instr, core.ControlledRotationInstruction): + return self._interpret_controlled_rotation_instr(app_id, instr) + elif isinstance(instr, core.ClassicalOpInstruction) or isinstance( + instr, core.ClassicalOpModInstruction + ): + return self._interpret_binary_classical_instr(app_id, instr) + elif isinstance(instr, core.BreakpointInstruction): + return self._interpret_breakpoint(app_id, instr) + else: + raise RuntimeError(f"Invalid instruction {instr}") + + def _interpret_breakpoint( + self, app_id: int, instr: core.BreakpointInstruction + ) -> None: + if instr.action.value == 0: + self._logger.info("BREAKPOINT: no action taken") + elif instr.action.value == 1: + self._logger.info("BREAKPOINT: dumping local state:") + for i in range(self.qdevice.num_positions): + if self.qdevice.mem_positions[i].in_use: + q = self.qdevice.peek(i, skip_noise=True) + qstate = qubitapi.reduced_dm(q) + self._logger.info(f"physical qubit {i}:\n{qstate}") + + GlobalSimData.get_quantum_state(save=True) # TODO: rewrite this + elif instr.action.value == 2: + self._logger.info("BREAKPOINT: dumping global state:") + if instr.role.value == 0: + self._send_netstack_msg(NetstackBreakpointCreateRequest(app_id)) + ready = yield from self._receive_netstack_msg() + assert ready == "breakpoint ready" + + state = GlobalSimData.get_quantum_state(save=True) + self._logger.info(state) + + self._send_netstack_msg("breakpoint end") + finished = yield from self._receive_netstack_msg() + assert finished == "breakpoint finished" + elif instr.role.value == 1: + self._send_netstack_msg(NetstackBreakpointReceiveRequest(app_id)) + ready = yield from self._receive_netstack_msg() + assert ready == "breakpoint ready" + self._send_netstack_msg("breakpoint end") + finished = yield from self._receive_netstack_msg() + assert finished == "breakpoint finished" + else: + raise ValueError + else: + raise ValueError + + def _interpret_set(self, app_id: int, instr: core.SetInstruction) -> None: + self._logger.debug(f"Set register {instr.reg} to {instr.imm}") + self.app_memories[app_id].set_reg_value(instr.reg, instr.imm.value) + + def _interpret_qalloc(self, app_id: int, instr: core.QAllocInstruction) -> None: + app_mem = self.app_memories[app_id] + + virt_id = app_mem.get_reg_value(instr.reg) + if virt_id is None: + raise RuntimeError(f"qubit address in register {instr.reg} is not defined") + self._logger.debug(f"Allocating qubit with virtual ID {virt_id}") + + phys_id = self.physical_memory.allocate() + app_mem.map_virt_id(virt_id, phys_id) + + def _interpret_qfree(self, app_id: int, instr: core.QFreeInstruction) -> None: + app_mem = self.app_memories[app_id] + + virt_id = app_mem.get_reg_value(instr.reg) + assert virt_id is not None + self._logger.debug(f"Freeing virtual qubit {virt_id}") + phys_id = app_mem.phys_id_for(virt_id) + assert phys_id is not None + app_mem.unmap_virt_id(virt_id) + self.physical_memory.free(phys_id) + self.send_signal(SIGNAL_MEMORY_FREED) + self.qdevice.mem_positions[phys_id].in_use = False + + def _interpret_store(self, app_id: int, instr: core.StoreInstruction) -> None: + app_mem = self.app_memories[app_id] + + value = app_mem.get_reg_value(instr.reg) + if value is None: + raise RuntimeError(f"value in register {instr.reg} is not defined") + self._logger.debug( + f"Storing value {value} from register {instr.reg} " + f"to array entry {instr.entry}" + ) + + app_mem.set_array_entry(instr.entry, value) + + def _interpret_load(self, app_id: int, instr: core.LoadInstruction) -> None: + app_mem = self.app_memories[app_id] + + value = app_mem.get_array_entry(instr.entry) + if value is None: + raise RuntimeError(f"array value at {instr.entry} is not defined") + self._logger.debug( + f"Storing value {value} from array entry {instr.entry} " + f"to register {instr.reg}" + ) + + app_mem.set_reg_value(instr.reg, value) + + def _interpret_lea(self, app_id: int, instr: core.LeaInstruction) -> None: + app_mem = self.app_memories[app_id] + self._logger.debug( + f"Storing address of {instr.address} to register {instr.reg}" + ) + app_mem.set_reg_value(instr.reg, instr.address.address) + + def _interpret_undef(self, app_id: int, instr: core.UndefInstruction) -> None: + app_mem = self.app_memories[app_id] + self._logger.debug(f"Unset array entry {instr.entry}") + app_mem.set_array_entry(instr.entry, None) + + def _interpret_array(self, app_id: int, instr: core.ArrayInstruction) -> None: + app_mem = self.app_memories[app_id] + + length = app_mem.get_reg_value(instr.size) + assert length is not None + self._logger.debug( + f"Initializing an array of length {length} at address {instr.address}" + ) + + app_mem.init_new_array(instr.address.address, length) + + def _interpret_branch_instr( + self, + app_id: int, + instr: Union[ + core.BranchUnaryInstruction, + core.BranchBinaryInstruction, + core.JmpInstruction, + ], + ) -> None: + app_mem = self.app_memories[app_id] + a, b = None, None + registers = [] + if isinstance(instr, core.BranchUnaryInstruction): + a = app_mem.get_reg_value(instr.reg) + registers = [instr.reg] + elif isinstance(instr, core.BranchBinaryInstruction): + a = app_mem.get_reg_value(instr.reg0) + b = app_mem.get_reg_value(instr.reg1) + registers = [instr.reg0, instr.reg1] + + if isinstance(instr, core.JmpInstruction): + condition = True + elif isinstance(instr, core.BranchUnaryInstruction): + condition = instr.check_condition(a) + elif isinstance(instr, core.BranchBinaryInstruction): + condition = instr.check_condition(a, b) + + if condition: + jump_address = instr.line + self._logger.debug( + f"Branching to line {jump_address}, since {instr}(a={a}, b={b}) " + f"is True, with values from registers {registers}" + ) + app_mem.set_prog_counter(jump_address.value) + else: + self._logger.debug( + f"Don't branch, since {instr}(a={a}, b={b}) " + f"is False, with values from registers {registers}" + ) + app_mem.increment_prog_counter() + + def _interpret_binary_classical_instr( + self, + app_id: int, + instr: Union[ + core.ClassicalOpInstruction, + core.ClassicalOpModInstruction, + ], + ) -> None: + app_mem = self.app_memories[app_id] + mod = None + if isinstance(instr, core.ClassicalOpModInstruction): + mod = app_mem.get_reg_value(instr.regmod) + if mod is not None and mod < 1: + raise RuntimeError(f"Modulus needs to be greater or equal to 1, not {mod}") + a = app_mem.get_reg_value(instr.regin0) + b = app_mem.get_reg_value(instr.regin1) + assert a is not None + assert b is not None + value = self._compute_binary_classical_instr(instr, a, b, mod=mod) + mod_str = "" if mod is None else f"(mod {mod})" + self._logger.debug( + f"Performing {instr} of a={a} and b={b} {mod_str} " + f"and storing the value {value} at register {instr.regout}" + ) + app_mem.set_reg_value(instr.regout, value) + + def _compute_binary_classical_instr( + self, instr: NetQASMInstruction, a: int, b: int, mod: Optional[int] = 1 + ) -> int: + if isinstance(instr, core.AddInstruction): + return a + b + elif isinstance(instr, core.AddmInstruction): + assert mod is not None + return (a + b) % mod + elif isinstance(instr, core.SubInstruction): + return a - b + elif isinstance(instr, core.SubmInstruction): + assert mod is not None + return (a - b) % mod + else: + raise ValueError(f"{instr} cannot be used as binary classical function") + + def _interpret_init( + self, app_id: int, instr: core.InitInstruction + ) -> Generator[EventExpression, None, None]: + raise NotImplementedError + + def _do_single_rotation( + self, + app_id: int, + instr: core.RotationInstruction, + ns_instr: NsInstr, + ) -> Generator[EventExpression, None, None]: + app_mem = self.app_memories[app_id] + virt_id = app_mem.get_reg_value(instr.reg) + phys_id = app_mem.phys_id_for(virt_id) + angle = self._get_rotation_angle_from_operands( + n=instr.angle_num.value, + d=instr.angle_denom.value, + ) + self._logger.debug( + f"Performing {instr} with angle {angle} on virtual qubit " + f"{virt_id} (physical ID: {phys_id})" + ) + prog = QuantumProgram() + prog.apply(ns_instr, qubit_indices=[phys_id], angle=angle) + yield self.qdevice.execute_program(prog) + + def _interpret_single_rotation_instr( + self, app_id: int, instr: nv.RotXInstruction + ) -> Generator[EventExpression, None, None]: + raise NotImplementedError + + def _do_controlled_rotation( + self, + app_id: int, + instr: core.ControlledRotationInstruction, + ns_instr: NsInstr, + ) -> Generator[EventExpression, None, None]: + app_mem = self.app_memories[app_id] + virt_id0 = app_mem.get_reg_value(instr.reg0) + phys_id0 = app_mem.phys_id_for(virt_id0) + virt_id1 = app_mem.get_reg_value(instr.reg1) + phys_id1 = app_mem.phys_id_for(virt_id1) + angle = self._get_rotation_angle_from_operands( + n=instr.angle_num.value, + d=instr.angle_denom.value, + ) + self._logger.debug( + f"Performing {instr} with angle {angle} on virtual qubits " + f"{virt_id0} and {virt_id1} (physical IDs: {phys_id0} and {phys_id1})" + ) + prog = QuantumProgram() + prog.apply(ns_instr, qubit_indices=[phys_id0, phys_id1], angle=angle) + yield self.qdevice.execute_program(prog) + + def _interpret_controlled_rotation_instr( + self, app_id: int, instr: core.ControlledRotationInstruction + ) -> Generator[EventExpression, None, None]: + raise NotImplementedError + + def _get_rotation_angle_from_operands(self, n: int, d: int) -> float: + return float(n * PI / (2**d)) + + def _interpret_meas( + self, app_id: int, instr: core.MeasInstruction + ) -> Generator[EventExpression, None, None]: + raise NotImplementedError + + def _interpret_create_epr( + self, app_id: int, instr: core.CreateEPRInstruction + ) -> None: + app_mem = self.app_memories[app_id] + remote_node_id = app_mem.get_reg_value(instr.remote_node_id) + epr_socket_id = app_mem.get_reg_value(instr.epr_socket_id) + qubit_array_addr = app_mem.get_reg_value(instr.qubit_addr_array) + arg_array_addr = app_mem.get_reg_value(instr.arg_array) + result_array_addr = app_mem.get_reg_value(instr.ent_results_array) + assert remote_node_id is not None + assert epr_socket_id is not None + # qubit_array_addr can be None + assert arg_array_addr is not None + assert result_array_addr is not None + self._logger.debug( + f"Creating EPR pair with remote node id {remote_node_id} " + f"and EPR socket ID {epr_socket_id}, " + f"using qubit addresses stored in array with address {qubit_array_addr}, " + f"using arguments stored in array with address {arg_array_addr}, " + f"placing the entanglement information in array at " + f"address {result_array_addr}" + ) + + msg = NetstackCreateRequest( + app_id, + remote_node_id, + epr_socket_id, + qubit_array_addr, + arg_array_addr, + result_array_addr, + ) + self._send_netstack_msg(msg) + # result = yield from self._receive_netstack_msg() + # self._logger.debug(f"result from netstack: {result}") + + def _interpret_recv_epr(self, app_id: int, instr: core.RecvEPRInstruction) -> None: + app_mem = self.app_memories[app_id] + remote_node_id = app_mem.get_reg_value(instr.remote_node_id) + epr_socket_id = app_mem.get_reg_value(instr.epr_socket_id) + qubit_array_addr = app_mem.get_reg_value(instr.qubit_addr_array) + result_array_addr = app_mem.get_reg_value(instr.ent_results_array) + assert remote_node_id is not None + assert epr_socket_id is not None + # qubit_array_addr can be None + assert result_array_addr is not None + self._logger.debug( + f"Receiving EPR pair with remote node id {remote_node_id} " + f"and EPR socket ID {epr_socket_id}, " + f"using qubit addresses stored in array with address {qubit_array_addr}, " + f"placing the entanglement information in array at " + f"address {result_array_addr}" + ) + + msg = NetstackReceiveRequest( + app_id, + remote_node_id, + epr_socket_id, + qubit_array_addr, + result_array_addr, + ) + self._send_netstack_msg(msg) + # result = yield from self._receive_netstack_msg() + # self._logger.debug(f"result from netstack: {result}") + + def _interpret_wait_all( + self, app_id: int, instr: core.WaitAllInstruction + ) -> Generator[EventExpression, None, None]: + app_mem = self.app_memories[app_id] + self._logger.debug( + f"Waiting for all entries in array slice {instr.slice} to become defined" + ) + assert isinstance(instr.slice.start, Register) + assert isinstance(instr.slice.stop, Register) + start: int = app_mem.get_reg_value(instr.slice.start) + end: int = app_mem.get_reg_value(instr.slice.stop) + addr: int = instr.slice.address.address + + self._logger.debug( + f"checking if @{addr}[{start}:{end}] has values for app ID {app_id}" + ) + + while True: + values = self.app_memories[app_id].get_array_values(addr, start, end) + if any(v is None for v in values): + self._logger.debug( + f"waiting for netstack to write to @{addr}[{start}:{end}] " + f"for app ID {app_id}" + ) + yield from self._receive_netstack_msg() + self._logger.debug("netstack wrote something") + else: + break + self._flush_netstack_msgs() + self._logger.debug("all entries were written") + + self._logger.info(f"\nFinished waiting for array slice {instr.slice}") + + def _interpret_ret_reg(self, app_id: int, instr: core.RetRegInstruction) -> None: + pass + + def _interpret_ret_arr(self, app_id: int, instr: core.RetArrInstruction) -> None: + pass + + def _interpret_single_qubit_instr( + self, app_id: int, instr: core.SingleQubitInstruction + ) -> Generator[EventExpression, None, None]: + raise NotImplementedError + + def _interpret_two_qubit_instr( + self, app_id: int, instr: core.SingleQubitInstruction + ) -> Generator[EventExpression, None, None]: + raise NotImplementedError + + +class GenericProcessor(Processor): + """A `Processor` for nodes with a generic quantum hardware.""" + + def _interpret_init( + self, app_id: int, instr: core.InitInstruction + ) -> Generator[EventExpression, None, None]: + app_mem = self.app_memories[app_id] + virt_id = app_mem.get_reg_value(instr.reg) + phys_id = app_mem.phys_id_for(virt_id) + self._logger.debug( + f"Performing {instr} on virtual qubit " + f"{virt_id} (physical ID: {phys_id})" + ) + prog = QuantumProgram() + prog.apply(INSTR_INIT, qubit_indices=[phys_id]) + yield self.qdevice.execute_program(prog) + + def _interpret_meas( + self, app_id: int, instr: core.MeasInstruction + ) -> Generator[EventExpression, None, None]: + app_mem = self.app_memories[app_id] + virt_id = app_mem.get_reg_value(instr.qreg) + phys_id = app_mem.phys_id_for(virt_id) + + self._logger.debug( + f"Measuring qubit {virt_id} (physical ID: {phys_id}), " + f"placing the outcome in register {instr.creg}" + ) + + prog = QuantumProgram() + prog.apply(INSTR_MEASURE, qubit_indices=[phys_id]) + yield self.qdevice.execute_program(prog) + outcome: int = prog.output["last"][0] + app_mem.set_reg_value(instr.creg, outcome) + + def _interpret_single_qubit_instr( + self, app_id: int, instr: core.SingleQubitInstruction + ) -> Generator[EventExpression, None, None]: + app_mem = self.app_memories[app_id] + virt_id = app_mem.get_reg_value(instr.qreg) + phys_id = app_mem.phys_id_for(virt_id) + if isinstance(instr, vanilla.GateXInstruction): + prog = QuantumProgram() + prog.apply(INSTR_X, qubit_indices=[phys_id]) + yield self.qdevice.execute_program(prog) + elif isinstance(instr, vanilla.GateYInstruction): + prog = QuantumProgram() + prog.apply(INSTR_Y, qubit_indices=[phys_id]) + yield self.qdevice.execute_program(prog) + elif isinstance(instr, vanilla.GateZInstruction): + prog = QuantumProgram() + prog.apply(INSTR_Z, qubit_indices=[phys_id]) + yield self.qdevice.execute_program(prog) + elif isinstance(instr, vanilla.GateHInstruction): + prog = QuantumProgram() + prog.apply(INSTR_H, qubit_indices=[phys_id]) + yield self.qdevice.execute_program(prog) + else: + raise RuntimeError(f"Unsupported instruction {instr}") + + def _interpret_single_rotation_instr( + self, app_id: int, instr: nv.RotXInstruction + ) -> Generator[EventExpression, None, None]: + if isinstance(instr, vanilla.RotXInstruction): + yield from self._do_single_rotation(app_id, instr, INSTR_ROT_X) + elif isinstance(instr, vanilla.RotYInstruction): + yield from self._do_single_rotation(app_id, instr, INSTR_ROT_Y) + elif isinstance(instr, vanilla.RotZInstruction): + yield from self._do_single_rotation(app_id, instr, INSTR_ROT_Z) + else: + raise RuntimeError(f"Unsupported instruction {instr}") + + def _interpret_controlled_rotation_instr( + self, app_id: int, instr: core.ControlledRotationInstruction + ) -> Generator[EventExpression, None, None]: + raise RuntimeError(f"Unsupported instruction {instr}") + + def _interpret_two_qubit_instr( + self, app_id: int, instr: core.SingleQubitInstruction + ) -> Generator[EventExpression, None, None]: + app_mem = self.app_memories[app_id] + virt_id0 = app_mem.get_reg_value(instr.reg0) + phys_id0 = app_mem.phys_id_for(virt_id0) + virt_id1 = app_mem.get_reg_value(instr.reg1) + phys_id1 = app_mem.phys_id_for(virt_id1) + if isinstance(instr, vanilla.CnotInstruction): + prog = QuantumProgram() + prog.apply(INSTR_CNOT, qubit_indices=[phys_id0, phys_id1]) + yield self.qdevice.execute_program(prog) + elif isinstance(instr, vanilla.CphaseInstruction): + prog = QuantumProgram() + prog.apply(INSTR_CZ, qubit_indices=[phys_id0, phys_id1]) + yield self.qdevice.execute_program(prog) + else: + raise RuntimeError(f"Unsupported instruction {instr}") + + +class NVProcessor(Processor): + """A `Processor` for nodes with a NV hardware.""" + + def _interpret_qalloc(self, app_id: int, instr: core.QAllocInstruction) -> None: + app_mem = self.app_memories[app_id] + + virt_id = app_mem.get_reg_value(instr.reg) + if virt_id is None: + raise RuntimeError(f"qubit address in register {instr.reg} is not defined") + self._logger.debug(f"Allocating qubit with virtual ID {virt_id}") + + # Virtual ID > 0 corresponds to memory qubits + if virt_id > 0: + phys_id = self.physical_memory.allocate_mem() + else: + phys_id = self.physical_memory.allocate_comm() + app_mem.map_virt_id(virt_id, phys_id) + + def _interpret_init( + self, app_id: int, instr: core.InitInstruction + ) -> Generator[EventExpression, None, None]: + app_mem = self.app_memories[app_id] + virt_id = app_mem.get_reg_value(instr.reg) + phys_id = app_mem.phys_id_for(virt_id) + self._logger.debug( + f"Performing {instr} on virtual qubit " + f"{virt_id} (physical ID: {phys_id})" + ) + prog = QuantumProgram() + prog.apply(INSTR_INIT, qubit_indices=[phys_id]) + yield self.qdevice.execute_program(prog) + + def _measure_electron(self) -> Generator[EventExpression, None, int]: + prog = QuantumProgram() + prog.apply(INSTR_MEASURE, qubit_indices=[0]) + yield self.qdevice.execute_program(prog) + outcome: int = prog.output["last"][0] + return outcome + + def _move_carbon_to_electron_for_measure( + self, carbon_id: int + ) -> Generator[EventExpression, None, None]: + prog = QuantumProgram() + prog.apply(INSTR_INIT, qubit_indices=[0]) + prog.apply(INSTR_ROT_Y, qubit_indices=[0], angle=PI_OVER_2) + prog.apply(INSTR_CYDIR, qubit_indices=[0, carbon_id], angle=-PI_OVER_2) + prog.apply(INSTR_ROT_X, qubit_indices=[0], angle=-PI_OVER_2) + prog.apply(INSTR_CXDIR, qubit_indices=[0, carbon_id], angle=PI_OVER_2) + prog.apply(INSTR_ROT_Y, qubit_indices=[0], angle=-PI_OVER_2) + yield self.qdevice.execute_program(prog) + + def _move_electron_to_carbon( + self, carbon_id: int + ) -> Generator[EventExpression, None, None]: + prog = QuantumProgram() + prog.apply(INSTR_INIT, qubit_indices=[carbon_id]) + prog.apply(INSTR_ROT_Y, qubit_indices=[0], angle=PI_OVER_2) + prog.apply(INSTR_CYDIR, qubit_indices=[0, carbon_id], angle=-PI_OVER_2) + prog.apply(INSTR_ROT_X, qubit_indices=[0], angle=-PI_OVER_2) + prog.apply(INSTR_CXDIR, qubit_indices=[0, carbon_id], angle=PI_OVER_2) + yield self.qdevice.execute_program(prog) + + def _interpret_meas( + self, app_id: int, instr: core.MeasInstruction + ) -> Generator[EventExpression, None, None]: + app_mem = self.app_memories[app_id] + virt_id = app_mem.get_reg_value(instr.qreg) + phys_id = app_mem.phys_id_for(virt_id) + + # Only the electron (phys ID 0) can be measured. + # Measuring any other physical qubit (i.e one of the carbons) requires + # freeing up the electron and moving the target qubit to the electron first. + + if phys_id == 0: + # Measuring the electron. This can be done immediately. + outcome = yield from self._measure_electron() + app_mem.set_reg_value(instr.creg, outcome) + else: + # We want to measure a carbon. + # Move it to the electron first. + if self.physical_memory.is_allocated(0): + # Electron is already allocated. Try to move it to a free carbon. + try: + new_qubit = self.physical_memory.allocate() + except AllocError: + self._logger.error( + f"Allocation error. Reason:\n" + f"Measuring virtual qubit {virt_id}.\n" + f"-> Measuring physical qubit {phys_id}.\n" + f"-> Measuring physical qubit with ID > 0 requires " + f"physical qubit 0 to be free." + f"-> Physical qubit 0 is in use.\n" + f"-> Trying to find free physical qubit for qubit 0.\n" + f"-> No physical qubits available." + ) + yield from self._move_electron_to_carbon(new_qubit) + elec_app_id, elec_virt_id = self._qnos.get_virt_qubit_for_phys_id(0) + self._logger.warning( + f"moving virtual qubit {elec_virt_id} from app " + f"{app_id} from physical ID 0 to {new_qubit}" + ) + # Update qubit ID mapping. + self.app_memories[elec_app_id].unmap_virt_id(elec_virt_id) + self.app_memories[elec_app_id].map_virt_id(elec_virt_id, new_qubit) + app_mem.unmap_virt_id(virt_id) + app_mem.map_virt_id(virt_id, 0) + yield from self._move_carbon_to_electron_for_measure(phys_id) + self.physical_memory.free(phys_id) + self.send_signal(SIGNAL_MEMORY_FREED) + self.qdevice.mem_positions[phys_id].in_use = False + outcome = yield from self._measure_electron() + app_mem.set_reg_value(instr.creg, outcome) + else: + self.physical_memory.allocate_comm() + app_mem.unmap_virt_id(virt_id) + app_mem.map_virt_id(virt_id, 0) + yield from self._move_carbon_to_electron_for_measure(phys_id) + self.physical_memory.free(phys_id) + self.send_signal(SIGNAL_MEMORY_FREED) + self.qdevice.mem_positions[phys_id].in_use = False + outcome = yield from self._measure_electron() + app_mem.set_reg_value(instr.creg, outcome) + + self._logger.debug( + f"Measuring qubit {virt_id} (physical ID: {phys_id}), " + f"placing the outcome in register {instr.creg}" + ) + + def _interpret_single_rotation_instr( + self, app_id: int, instr: nv.RotXInstruction + ) -> Generator[EventExpression, None, None]: + if isinstance(instr, nv.RotXInstruction): + yield from self._do_single_rotation(app_id, instr, INSTR_ROT_X) + elif isinstance(instr, nv.RotYInstruction): + yield from self._do_single_rotation(app_id, instr, INSTR_ROT_Y) + elif isinstance(instr, nv.RotZInstruction): + yield from self._do_single_rotation(app_id, instr, INSTR_ROT_Z) + else: + raise RuntimeError(f"Unsupported instruction {instr}") + + def _interpret_controlled_rotation_instr( + self, app_id: int, instr: core.ControlledRotationInstruction + ) -> Generator[EventExpression, None, None]: + if isinstance(instr, nv.ControlledRotXInstruction): + yield from self._do_controlled_rotation(app_id, instr, INSTR_CXDIR) + elif isinstance(instr, nv.ControlledRotYInstruction): + yield from self._do_controlled_rotation(app_id, instr, INSTR_CYDIR) + else: + raise RuntimeError(f"Unsupported instruction {instr}") diff --git a/squidasm/qoala/sim/qnos.py b/squidasm/qoala/sim/qnos.py new file mode 100644 index 00000000..700acf6b --- /dev/null +++ b/squidasm/qoala/sim/qnos.py @@ -0,0 +1,200 @@ +from __future__ import annotations + +from typing import Dict, Optional, Tuple + +from netsquid.components import QuantumProcessor +from netsquid.components.component import Component, Port +from netsquid.nodes import Node +from netsquid.protocols import Protocol +from netsquid_magic.link_layer import MagicLinkLayerProtocolWithSignaling + +from squidasm.qoala.sim.common import ( + AppMemory, + NVPhysicalQuantumMemory, + PhysicalQuantumMemory, +) +from squidasm.qoala.sim.handler import Handler, HandlerComponent +from squidasm.qoala.sim.netstack import Netstack, NetstackComponent +from squidasm.qoala.sim.processor import ( + GenericProcessor, + NVProcessor, + Processor, + ProcessorComponent, +) + +# TODO: make this a parameter +NUM_QUBITS = 5 + + +class QnosComponent(Component): + """NetSquid component representing a QNodeOS instance. + + Subcomponent of a ProcessingNode. + + This is a static container for QNodeOS-related components and ports. + Behavior of a QNodeOS instance is modeled in the `Qnos` class, + which is a subclass of `Protocol`. + """ + + def __init__(self, node: Node) -> None: + super().__init__(name=f"{node.name}_qnos") + self._node = node + + # Ports for communicating with Host + self.add_ports(["host_out", "host_in"]) + + # Ports for communicating with other nodes + self.add_ports(["peer_out", "peer_in"]) + + comp_handler = HandlerComponent(node) + self.add_subcomponent(comp_handler, "handler") + + comp_processor = ProcessorComponent(node) + self.add_subcomponent(comp_processor, "processor") + + comp_netstack = NetstackComponent(node) + self.add_subcomponent(comp_netstack, "netstack") + + self.netstack_comp.ports["peer_out"].forward_output(self.peer_out_port) + self.peer_in_port.forward_input(self.netstack_comp.ports["peer_in"]) + + self.handler_comp.ports["host_out"].forward_output(self.host_out_port) + self.host_in_port.forward_input(self.handler_comp.ports["host_in"]) + + self.handler_comp.processor_out_port.connect( + self.processor_comp.handler_in_port + ) + self.handler_comp.processor_in_port.connect( + self.processor_comp.handler_out_port + ) + + self.processor_comp.netstack_out_port.connect( + self.netstack_comp.processor_in_port + ) + self.processor_comp.netstack_in_port.connect( + self.netstack_comp.processor_out_port + ) + + @property + def handler_comp(self) -> HandlerComponent: + return self.subcomponents["handler"] + + @property + def processor_comp(self) -> ProcessorComponent: + return self.subcomponents["processor"] + + @property + def netstack_comp(self) -> NetstackComponent: + return self.subcomponents["netstack"] + + @property + def qdevice(self) -> QuantumProcessor: + return self.node.qmemory + + @property + def host_in_port(self) -> Port: + return self.ports["host_in"] + + @property + def host_out_port(self) -> Port: + return self.ports["host_out"] + + @property + def peer_in_port(self) -> Port: + return self.ports["peer_in"] + + @property + def peer_out_port(self) -> Port: + return self.ports["peer_out"] + + @property + def node(self) -> Node: + return self._node + + +class Qnos(Protocol): + """NetSquid protocol representing a QNodeOS instance.""" + + def __init__(self, comp: QnosComponent, qdevice_type: Optional[str] = "nv") -> None: + """Qnos protocol constructor. + + :param comp: NetSquid component representing the QNodeOS instance + :param qdevice_type: hardware type of the QDevice of this node + """ + super().__init__(name=f"{comp.name}_protocol") + self._comp = comp + + # Create internal protocols. + self.handler = Handler(comp.handler_comp, self, qdevice_type) + self.netstack = Netstack(comp.netstack_comp, self) + if qdevice_type == "generic": + self.processor = GenericProcessor(comp.processor_comp, self) + self._physical_memory = PhysicalQuantumMemory(comp.qdevice.num_positions) + elif qdevice_type == "nv": + self.processor = NVProcessor(comp.processor_comp, self) + self._physical_memory = NVPhysicalQuantumMemory(comp.qdevice.num_positions) + else: + raise ValueError + + # Classical memories that are shared (virtually) with the Host. + # Each application has its own `AppMemory`, identified by the application ID. + self._app_memories: Dict[int, AppMemory] = {} # app ID -> app memory + + # TODO: move this to a separate memory manager object + def get_virt_qubit_for_phys_id(self, phys_id: int) -> Tuple[int, int]: + # returns (app_id, virt_id) + for app_id, app_mem in self._app_memories.items(): + virt_id = app_mem.virt_id_for(phys_id) + if virt_id is not None: + return app_id, virt_id + raise RuntimeError(f"no virtual ID found for physical ID {phys_id}") + + def assign_ll_protocol(self, prot: MagicLinkLayerProtocolWithSignaling) -> None: + self.netstack.assign_ll_protocol(prot) + + @property + def handler(self) -> Handler: + return self._handler + + @handler.setter + def handler(self, handler: Handler) -> None: + self._handler = handler + + @property + def processor(self) -> Processor: + return self._processor + + @processor.setter + def processor(self, processor: Processor) -> None: + self._processor = processor + + @property + def netstack(self) -> Netstack: + return self._netstack + + @netstack.setter + def netstack(self, netstack: Netstack) -> None: + self._netstack = netstack + + @property + def app_memories(self) -> Dict[int, AppMemory]: + return self._app_memories + + @property + def physical_memory(self) -> PhysicalQuantumMemory: + return self._physical_memory + + def start(self) -> None: + assert self._handler is not None + assert self._processor is not None + assert self._netstack is not None + super().start() + self._handler.start() + self._processor.start() + self._netstack.start() + + def stop(self) -> None: + self._netstack.stop() + self._processor.stop() + self._handler.stop() + super().stop() diff --git a/squidasm/qoala/sim/signals.py b/squidasm/qoala/sim/signals.py new file mode 100644 index 00000000..d10bd6bf --- /dev/null +++ b/squidasm/qoala/sim/signals.py @@ -0,0 +1,10 @@ +SIGNAL_HOST_HOST_MSG = "EvHostHandMsg" +SIGNAL_HOST_HAND_MSG = "EvHostHandMsg" +SIGNAL_HAND_HOST_MSG = "EvHandHostMsg" +SIGNAL_HAND_PROC_MSG = "EvHandProcMsg" +SIGNAL_PROC_HAND_MSG = "EvProcHandMsg" +SIGNAL_PROC_NSTK_MSG = "EvProcNstkMsg" +SIGNAL_NSTK_PROC_MSG = "EvNstkProcMsg" +SIGNAL_PEER_NSTK_MSG = "EvPeerNstkMsg" + +SIGNAL_MEMORY_FREED = "EvMemoryFreed" diff --git a/squidasm/qoala/sim/stack.py b/squidasm/qoala/sim/stack.py new file mode 100644 index 00000000..adfcddd4 --- /dev/null +++ b/squidasm/qoala/sim/stack.py @@ -0,0 +1,268 @@ +from __future__ import annotations + +from typing import Dict, List, Optional + +from netsquid.components import QuantumProcessor +from netsquid.components.component import Port +from netsquid.nodes import Node +from netsquid.nodes.network import Network +from netsquid.protocols import Protocol +from netsquid_magic.link_layer import ( + MagicLinkLayerProtocol, + MagicLinkLayerProtocolWithSignaling, +) + +from squidasm.qoala.runtime.environment import GlobalEnvironment, LocalEnvironment +from squidasm.qoala.sim.host import Host, HostComponent +from squidasm.qoala.sim.qnos import Qnos, QnosComponent + + +class ProcessingNode(Node): + """NetSquid component representing a quantum network node containing a software + stack consisting of Host, QNodeOS and QDevice. + + This component has two subcomponents: a QnosComponent and a HostComponent. + + Has communications ports between + - the Host component on this node and the Host component on the peer node + - the QNodeOS component on this node and the QNodeOS component on the peer node + + For now, it is assumed there is only a single other nodes in the network, + which is "the" peer. + + This is a static container for components and ports. + Behavior of the node is modeled in the `NodeStack` class, which is a subclass + of `Protocol`. + """ + + def __init__( + self, + name: str, + qdevice: QuantumProcessor, + node_id: Optional[int] = None, + ) -> None: + """ProcessingNode constructor. Typically created indirectly through + constructing a `NodeStack`.""" + super().__init__(name, ID=node_id) + self.qmemory = qdevice + + qnos_comp = QnosComponent(self) + self.add_subcomponent(qnos_comp, "qnos") + + host_comp = HostComponent(self) + self.add_subcomponent(host_comp, "host") + + self.host_comp.ports["qnos_out"].connect(self.qnos_comp.ports["host_in"]) + self.host_comp.ports["qnos_in"].connect(self.qnos_comp.ports["host_out"]) + + # Ports for communicating with other nodes + self.add_ports(["qnos_peer_out", "qnos_peer_in"]) + self.add_ports(["host_peer_out", "host_peer_in"]) + + self.qnos_comp.peer_out_port.forward_output(self.qnos_peer_out_port) + self.qnos_peer_in_port.forward_input(self.qnos_comp.peer_in_port) + self.host_comp.peer_out_port.forward_output(self.host_peer_out_port) + self.host_peer_in_port.forward_input(self.host_comp.peer_in_port) + + @property + def qnos_comp(self) -> QnosComponent: + return self.subcomponents["qnos"] + + @property + def host_comp(self) -> HostComponent: + return self.subcomponents["host"] + + @property + def qdevice(self) -> QuantumProcessor: + return self.qmemory + + @property + def host_peer_in_port(self) -> Port: + return self.ports["host_peer_in"] + + @property + def host_peer_out_port(self) -> Port: + return self.ports["host_peer_out"] + + @property + def qnos_peer_in_port(self) -> Port: + return self.ports["qnos_peer_in"] + + @property + def qnos_peer_out_port(self) -> Port: + return self.ports["qnos_peer_out"] + + +class NodeStack(Protocol): + """NetSquid protocol representing a node with a software stack. + + The software stack consists of a Host, QNodeOS and a QDevice. + The Host and QNodeOS are each represented by separate subprotocols. + The QDevice is handled/modeled as part of the QNodeOS protocol. + """ + + def __init__( + self, + name: str, + global_env: Optional[GlobalEnvironment] = None, + node: Optional[ProcessingNode] = None, + qdevice_type: Optional[str] = "generic", + qdevice: Optional[QuantumProcessor] = None, + node_id: Optional[int] = None, + use_default_components: bool = True, + ) -> None: + """NodeStack constructor. + + :param name: name of this node + :param node: an existing ProcessingNode object containing the static + components or None. If None, a ProcessingNode is automatically + created. + :param qdevice_type: hardware type of the QDevice, defaults to "generic" + :param qdevice: NetSquid `QuantumProcessor` representing the QDevice, + defaults to None. If None, a QuantumProcessor is created + automatically. + :param node_id: ID to use for the internal NetSquid node object + :param use_default_components: whether to automatically create NetSquid + components for the Host and QNodeOS, defaults to True. If False, + this allows for manually creating and adding these components. + """ + super().__init__(name=f"{name}") + if node: + self._node = node + else: + assert qdevice is not None + self._node = ProcessingNode(name, qdevice, node_id) + + self._global_env = global_env + self._local_env = LocalEnvironment(global_env, global_env.get_node_id(name)) + + self._host: Optional[Host] = None + self._qnos: Optional[Qnos] = None + + # Create internal components. + # If `use_default_components` is False, these components must be manually + # created and added to this NodeStack. + if use_default_components: + self._host = Host(self.host_comp, self._local_env, qdevice_type) + self._qnos = Qnos(self.qnos_comp, qdevice_type) + + def install_environment(self) -> None: + for instance in self._local_env._programs: + app_id = self._host.init_new_program(instance) + self._qnos.handler.init_new_app(app_id) + + # Open the EPR sockets required by the program. + for i, remote_name in enumerate(instance.program.meta.epr_sockets): + remote_id = None + + # TODO: rewrite + nodes = self._global_env.get_nodes() + for id, info in nodes.items(): + if info.name == remote_name: + remote_id = id + + assert remote_id is not None + self._qnos.handler.open_epr_socket(app_id, i, remote_id) + + for i, remote_name in enumerate(instance.program.meta.csockets): + remote_id = None + + # TODO: rewrite + nodes = self._global_env.get_nodes() + for id, info in nodes.items(): + if info.name == remote_name: + remote_id = id + + assert remote_id is not None + self._host.open_csocket(app_id, remote_name) + + def assign_ll_protocol(self, prot: MagicLinkLayerProtocolWithSignaling) -> None: + """Set the link layer protocol to use for entanglement generation. + + The same link layer protocol object is used by both nodes sharing a link in + the network.""" + self.qnos.assign_ll_protocol(prot) + + @property + def node(self) -> ProcessingNode: + return self._node + + @property + def host_comp(self) -> HostComponent: + return self.node.host_comp + + @property + def qnos_comp(self) -> QnosComponent: + return self.node.qnos_comp + + @property + def qdevice(self) -> QuantumProcessor: + return self.node.qdevice + + @property + def host(self) -> Host: + return self._host + + @host.setter + def host(self, host: Host) -> None: + self._host = host + + @property + def qnos(self) -> Qnos: + return self._qnos + + @qnos.setter + def qnos(self, qnos: Qnos) -> None: + self._qnos = qnos + + def connect_to(self, other: NodeStack) -> None: + """Create connections between ports of this NodeStack and those of + another NodeStack.""" + self.node.host_peer_out_port.connect(other.node.host_peer_in_port) + self.node.host_peer_in_port.connect(other.node.host_peer_out_port) + self.node.qnos_peer_out_port.connect(other.node.qnos_peer_in_port) + self.node.qnos_peer_in_port.connect(other.node.qnos_peer_out_port) + + def start(self) -> None: + assert self._host is not None + assert self._qnos is not None + super().start() + self._host.start() + self._qnos.start() + + def stop(self) -> None: + assert self._host is not None + assert self._qnos is not None + self._qnos.stop() + self._host.stop() + super().stop() + + +class StackNetwork(Network): + """A network of `NodeStack`s connected by links, which are + `MagicLinkLayerProtocol`s.""" + + def __init__( + self, stacks: Dict[str, NodeStack], links: List[MagicLinkLayerProtocol] + ) -> None: + """StackNetwork constructor. + + :param stacks: dictionary of node name to `NodeStack` object representing + that node + :param links: list of link layer protocol objects. Each object internally + contains the IDs of the two nodes that this link connects + """ + self._stacks = stacks + self._links = links + + @property + def stacks(self) -> Dict[str, NodeStack]: + return self._stacks + + @property + def links(self) -> List[MagicLinkLayerProtocol]: + return self._links + + @property + def qdevices(self) -> Dict[str, QuantumProcessor]: + return {name: stack.qdevice for name, stack in self._stacks.items()} diff --git a/squidasm/sim/stack/host.py b/squidasm/sim/stack/host.py index 4e22c305..d71b5325 100644 --- a/squidasm/sim/stack/host.py +++ b/squidasm/sim/stack/host.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging from typing import Any, Dict, Generator, List, Optional, Type from netqasm.backend.messages import ( @@ -7,13 +8,15 @@ OpenEPRSocketMessage, StopAppMessage, ) +from netqasm.lang.operand import Register +from netqasm.lang.parsing.text import NetQASMSyntaxError, parse_register from netqasm.sdk.epr_socket import EPRSocket from netqasm.sdk.transpile import NVSubroutineTranspiler, SubroutineTranspiler from netsquid.components.component import Component, Port from netsquid.nodes import Node from pydynaa import EventExpression -from squidasm.sim.stack.common import ComponentProtocol, PortListener +from squidasm.sim.stack.common import ComponentProtocol, LogManager, PortListener from squidasm.sim.stack.connection import QnosConnection from squidasm.sim.stack.context import NetSquidContext from squidasm.sim.stack.csocket import ClassicalSocket @@ -111,6 +114,65 @@ def send_peer_msg(self, msg: str) -> None: def receive_peer_msg(self) -> Generator[EventExpression, None, str]: return (yield from self._receive_msg("peer", SIGNAL_HOST_HOST_MSG)) + def run_sdk_program( + self, program: Program + ) -> Generator[EventExpression, None, None]: + prog_meta = program.meta + + # Register the new program (called 'application' by QNodeOS) with QNodeOS. + self.send_qnos_msg(bytes(InitNewAppMessage(max_qubits=prog_meta.max_qubits))) + app_id = yield from self.receive_qnos_msg() + self._logger.debug(f"got app id from qnos: {app_id}") + + # Set up the Connection object to be used by the program SDK code. + conn = QnosConnection( + self, + app_id, + prog_meta.name, + max_qubits=prog_meta.max_qubits, + compiler=self._compiler, + ) + + # Create EPR sockets that can be used by the program SDK code. + epr_sockets: Dict[int, EPRSocket] = {} + for i, remote_name in enumerate(prog_meta.epr_sockets): + remote_id = None + nodes = NetSquidContext.get_nodes() + for id, name in nodes.items(): + if name == remote_name: + remote_id = id + assert remote_id is not None + self.send_qnos_msg(bytes(OpenEPRSocketMessage(app_id, i, remote_id))) + epr_sockets[remote_name] = EPRSocket(remote_name, i) + epr_sockets[remote_name].conn = conn + + # Create classical sockets that can be used by the program SDK code. + classical_sockets: Dict[int, ClassicalSocket] = {} + for i, remote_name in enumerate(prog_meta.csockets): + remote_id = None + nodes = NetSquidContext.get_nodes() + for id, name in nodes.items(): + if name == remote_name: + remote_id = id + assert remote_id is not None + classical_sockets[remote_name] = ClassicalSocket( + self, prog_meta.name, remote_name + ) + + context = ProgramContext( + netqasm_connection=conn, + csockets=classical_sockets, + epr_sockets=epr_sockets, + app_id=app_id, + ) + + # Run the program by evaluating its run() method. + result = yield from program.run(context) + self._program_results.append(result) + + # Tell QNodeOS the program has finished. + self.send_qnos_msg(bytes(StopAppMessage(app_id))) + def run(self) -> Generator[EventExpression, None, None]: """Run this protocol. Automatically called by NetSquid during simulation.""" @@ -120,63 +182,8 @@ def run(self) -> Generator[EventExpression, None, None]: self._num_pending -= 1 assert self._program is not None - prog_meta = self._program.meta - - # Register the new program (called 'application' by QNodeOS) with QNodeOS. - self.send_qnos_msg( - bytes(InitNewAppMessage(max_qubits=prog_meta.max_qubits)) - ) - app_id = yield from self.receive_qnos_msg() - self._logger.debug(f"got app id from qnos: {app_id}") - - # Set up the Connection object to be used by the program SDK code. - conn = QnosConnection( - self, - app_id, - prog_meta.name, - max_qubits=prog_meta.max_qubits, - compiler=self._compiler, - ) - - # Create EPR sockets that can be used by the program SDK code. - epr_sockets: Dict[int, EPRSocket] = {} - for i, remote_name in enumerate(prog_meta.epr_sockets): - remote_id = None - nodes = NetSquidContext.get_nodes() - for id, name in nodes.items(): - if name == remote_name: - remote_id = id - assert remote_id is not None - self.send_qnos_msg(bytes(OpenEPRSocketMessage(app_id, i, remote_id))) - epr_sockets[remote_name] = EPRSocket(remote_name, i) - epr_sockets[remote_name].conn = conn - - # Create classical sockets that can be used by the program SDK code. - classical_sockets: Dict[int, ClassicalSocket] = {} - for i, remote_name in enumerate(prog_meta.csockets): - remote_id = None - nodes = NetSquidContext.get_nodes() - for id, name in nodes.items(): - if name == remote_name: - remote_id = id - assert remote_id is not None - classical_sockets[remote_name] = ClassicalSocket( - self, prog_meta.name, remote_name - ) - - context = ProgramContext( - netqasm_connection=conn, - csockets=classical_sockets, - epr_sockets=epr_sockets, - app_id=app_id, - ) - - # Run the program by evaluating its run() method. - result = yield from self._program.run(context) - self._program_results.append(result) - # Tell QNodeOS the program has finished. - self.send_qnos_msg(bytes(StopAppMessage(app_id))) + yield from self.run_sdk_program(self._program) def enqueue_program(self, program: Program, num_times: int = 1): """Queue a program to be run the given number of times. diff --git a/tests/qoala/test_lhr.py b/tests/qoala/test_lhr.py new file mode 100644 index 00000000..025ad349 --- /dev/null +++ b/tests/qoala/test_lhr.py @@ -0,0 +1,207 @@ +from squidasm.qoala.lang import lhr as lp +from squidasm.qoala.runtime.config import ( + GenericQDeviceConfig, + LinkConfig, + StackConfig, + StackNetworkConfig, +) +from squidasm.qoala.runtime.program import ProgramInstance +from squidasm.qoala.runtime.run import run +from squidasm.sim.stack.common import LogManager +from squidasm.sim.stack.program import ProgramMeta + + +def test_parse(): + program_text = """ +my_value = assign_cval() : 1 +send_cmsg(my_value) +received_value = recv_cmsg() +new_value = assign_cval() : 3 +my_value = add_cval_c(new_value, new_value) +run_subroutine(vec) : + return M0 -> m + NETQASM_START + set Q0 0 + rot_z Q0 {my_value} 4 + meas Q0 M0 + ret_reg M0 + NETQASM_END +return_result(m) + """ + parsed_program = lp.LhrParser(program_text).parse() + + print(parsed_program) + + +def test_run(): + program_text = """ +new_value = assign_cval() : 8 +my_value = add_cval_c(new_value, new_value) +run_subroutine(vec) : + return M0 -> m + NETQASM_START + set Q0 0 + qalloc Q0 + init Q0 + rot_x Q0 {my_value} 4 + meas Q0 M0 + ret_reg M0 + NETQASM_END +return_result(m) + """ + parsed_program = lp.LhrParser(program_text).parse() + parsed_program.meta = ProgramMeta( + name="client", parameters={}, csockets=[], epr_sockets=[], max_qubits=1 + ) + + sender_stack = StackConfig( + name="client", + qdevice_typ="generic", + qdevice_cfg=GenericQDeviceConfig.perfect_config(), + ) + + prog_instance = ProgramInstance(parsed_program, {}, 1, 0.0) + + cfg = StackNetworkConfig(stacks=[sender_stack], links=[]) + result = run(cfg, programs={"client": prog_instance}) + print(result) + + +def test_run_two_nodes_classical(): + program_text_client = """ +new_value = assign_cval() : 8 +send_cmsg(new_value) + """ + program_client = lp.LhrParser(program_text_client).parse() + program_client.meta = ProgramMeta( + name="client", parameters={}, csockets=["server"], epr_sockets=[], max_qubits=1 + ) + client_stack = StackConfig( + name="client", + qdevice_typ="generic", + qdevice_cfg=GenericQDeviceConfig.perfect_config(), + ) + + program_text_server = """ +new_value = assign_cval() : 8 +value = recv_cmsg() +return_result(value) + """ + program_server = lp.LhrParser(program_text_server).parse() + program_server.meta = ProgramMeta( + name="client", parameters={}, csockets=["client"], epr_sockets=[], max_qubits=1 + ) + server_stack = StackConfig( + name="server", + qdevice_typ="generic", + qdevice_cfg=GenericQDeviceConfig.perfect_config(), + ) + + prog_server_instance = ProgramInstance(program_server, {}, 1, 0.0) + prog_client_instance = ProgramInstance(program_client, {}, 1, 0.0) + + cfg = StackNetworkConfig(stacks=[client_stack, server_stack], links=[]) + result = run( + cfg, programs={"client": prog_client_instance, "server": prog_server_instance} + ) + print(result) + + +def test_run_two_nodes_epr(): + program_text_client = """ +remote_id = assign_cval() : 1 +epr_sck_id = assign_cval() : 0 +run_subroutine(vec) : + return M0 -> m + NETQASM_START + set R0 0 + set R1 1 + set R2 2 + set R3 20 + set R10 10 + set C0 {remote_id} + set C1 {epr_sck_id} + array R10 @0 + array R1 @1 + array R3 @2 + store R0 @1[R0] + store R0 @2[R0] + store R1 @2[R1] + create_epr C0 C1 R1 R2 R0 + wait_all @0[R0:R10] + set Q0 0 + meas Q0 M0 + qfree Q0 + ret_reg M0 + NETQASM_END +return_result(m) + """ + program_client = lp.LhrParser(program_text_client).parse() + program_client.meta = ProgramMeta( + name="client", + parameters={}, + csockets=["server"], + epr_sockets=["server"], + max_qubits=1, + ) + client_stack = StackConfig( + name="client", + qdevice_typ="generic", + qdevice_cfg=GenericQDeviceConfig.perfect_config(), + ) + + program_text_server = """ +run_subroutine(vec<>) : + return M0 -> m + NETQASM_START + set R0 0 + set R1 1 + set R2 2 + set R10 10 + array R10 @0 + array R1 @1 + store R0 @1[R0] + recv_epr R0 R0 R1 R0 + wait_all @0[R0:R10] + set Q0 0 + meas Q0 M0 + qfree Q0 + ret_reg M0 + NETQASM_END +return_result(m) + """ + program_server = lp.LhrParser(program_text_server).parse() + program_server.meta = ProgramMeta( + name="client", + parameters={}, + csockets=["client"], + epr_sockets=["client"], + max_qubits=1, + ) + server_stack = StackConfig( + name="server", + qdevice_typ="generic", + qdevice_cfg=GenericQDeviceConfig.perfect_config(), + ) + link = LinkConfig( + stack1="client", + stack2="server", + typ="perfect", + ) + + prog_server_instance = ProgramInstance(program_server, {}, 1, 0.0) + prog_client_instance = ProgramInstance(program_client, {}, 1, 0.0) + + cfg = StackNetworkConfig(stacks=[client_stack, server_stack], links=[link]) + result = run( + cfg, programs={"client": prog_client_instance, "server": prog_server_instance} + ) + print(result) + + +if __name__ == "__main__": + LogManager.set_log_level("DEBUG") + test_parse() + test_run() + test_run_two_nodes_classical() + test_run_two_nodes_epr()